Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SemVer: Add section on RPIT capturing #14849

Merged
merged 2 commits into from
Dec 8, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/doc/src/reference/semver.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ considered incompatible.
* [Minor: generalizing a type to use generics (with identical types)](#generic-generalize-identical)
* [Major: generalizing a type to use generics (with possibly different types)](#generic-generalize-different)
* [Minor: changing a generic type to a more generic type](#generic-more-generic)
* [Major: capturing more generic parameters in RPIT](#generic-rpit-capture)
* Functions
* [Major: adding/removing function parameters](#fn-change-arity)
* [Possibly-breaking: introducing a new function type parameter](#fn-generic-new)
Expand Down Expand Up @@ -1616,6 +1617,47 @@ fn main() {
}
```

### Major: capturing more generic parameters in RPIT {#generic-rpit-capture}

It is a breaking change to capture additional generic parameters in an [RPIT] (return-position impl trait).

```rust,ignore
// MAJOR CHANGE

///////////////////////////////////////////////////////////
// Before
pub fn f<'a, 'b>(x: &'a str, y: &'b str) -> impl Iterator<Item = char> + use<'a> {
x.chars()
}

///////////////////////////////////////////////////////////
// After
pub fn f<'a, 'b>(x: &'a str, y: &'b str) -> impl Iterator<Item = char> + use<'a, 'b> {
x.chars().chain(y.chars())
}

///////////////////////////////////////////////////////////
// Example usage that will break.
fn main() {
let a = String::new();
let b = String::new();
let iter = updated_crate::f(&a, &b);
drop(b); // Error: cannot move out of `b` because it is borrowed
}
```
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A thought: would it be worth adding an example, or even just mentioning in a sentence or two that use<> syntax can also be used to capture type parameters, not just lifetimes? That wasn't immediately obvious to me when I first heard about this new language feature.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I added a note here. I don't think type and const generics have any impact here because you cannot change them. I generally would like to avoid using this space for teaching how the language works, and that's why there are links to the reference and edition guide which do elaborate on how it works.


Adding generic parameters to an RPIT places additional constraints on how the resulting type may be used.

Note that there are implicit captures when the `use<>` syntax is not specified. In Rust 2021 and earlier editions, the lifetime parameters are only captured if they appear syntactically within a bound in the RPIT type signature. Starting in Rust 2024, all lifetime parameters are unconditionally captured. This means that starting in Rust 2024, the default is maximally compatible, requiring you to be explicit when you want to capture less, which is a SemVer commitment.

See the [edition guide][rpit-capture-guide] and the [reference][rpit-reference] for more information on RPIT capturing.

It is a minor change to capture fewer generic parameters in an RPIT.
Copy link
Member

@obi1kenobi obi1kenobi Nov 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this might not be true for functions in non-sealed traits. If the trait's function definition changes the RPIT captures, I expect implementations of the trait have to be updated to match.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm… sounds very likely. Could you give an example of it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah it seems use syntax in RPIT in traits isn't supported yet, so I can't give you a playground example.

But here's proof from a related thread: rust-lang/rust#132795

It shows this example:

trait TypeParam<T> {
    fn test() -> impl Sized;
}
// Indirectly capturing a lifetime param through a type param substitution.
impl<'a> TypeParam<&'a ()> for i32 {
    fn test() -> impl Sized + use<> {}
    //~^ WARN impl trait in impl method captures fewer lifetimes than in trait
}

and states that the impl is a valid refinement of the trait.

But if it's a refinement, that means not all implementations of the trait have to adhere to it — it's a stricter interface than what the trait requires.

Therefore, applying this to our question at hand about removing generics from use<>: If the trait were edited to go from (implicitly) fn test() -> impl Sized + use<T> to fn test() -> impl Sized + use<>, that would be a breaking change. The impls that had applied the refinement would continue to work, and impls that didn't refine in this way would be broken.

Copy link
Member

@obi1kenobi obi1kenobi Dec 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So to summarize, the statement is accurate as written so long as use<> in RPITIT isn't supported. But it sounds like that limitation is of a temporary "work in progress" nature, so it's probably a good idea to make the semver reference a bit more future-proof.

I think the accurate statement here is "major change in RPIT in traits unless the trait is sealed, minor change otherwise."

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not quite following here. Since RPITIT is not stabilized, I would prefer to not refer to it or document its behavior. When it is stabilized, we can update the documentation to say that it is a major change to change which generics are captured in RPITIT unless the trait is sealed. (Depending on if the design of RPITIT changes.) Does that make sense?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, sorry. My statement was not intended for inclusion in this document. It was merely intended as a summary of my understanding of Rust's present and plausible future rules here.

If you're sure we can remember to follow up on this section when use<> in RPITIT is stabilized, then I'm fine with either wording here. Otherwise, it may be wise to consider slightly more defensive wording here that explicitly excludes RPIT in traits.

I just wanted to raise this for your consideration — it's your call to make and I'm happy with whatever you think is best.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure how to make sure to remember updating the doc, but at least now we have three people aware of it. I think it's good to move forward.

Thanks folks for the discussion!
(I have more understanding for generics capturing after this 😆)


[RPIT]: ../../reference/types/impl-trait.md#abstract-return-types
[rpit-capture-guide]: ../../edition-guide/rust-2024/rpit-lifetime-capture.html
[rpit-reference]: ../../reference/types/impl-trait.md#capturing

### Major: adding/removing function parameters {#fn-change-arity}

Changing the arity of a function is a breaking change.
Expand Down