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

Stabilize anonymous_lifetime_in_impl_trait #107378

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

c410-f3r
Copy link
Contributor

@c410-f3r c410-f3r commented Jan 27, 2023

Stabilization proposal

This PR proposes the stabilization of #![feature(anonymous_lifetime_in_impl_trait)].

Version: 1.69 (beta => 2023-03-09, stable => 2023-04-20).

What is stabilized

For non-asynchronous functions, allows the usage of anonymous lifetimes in APITs.

fn _example(_: impl Iterator<Item = &()>) {}

Motivation

In addition to ergonomics, the lack of parity between asynchronous and non-asynchronous functions can be confusing as the former is already allowed on stable toolchains.

// OK!
async fn _example(_: impl Iterator<Item = &()>) {}

History

cc @cjgillot

@rustbot
Copy link
Collaborator

rustbot commented Jan 27, 2023

r? @estebank

(rustbot has picked a reviewer for you, use r? to override)

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Jan 27, 2023
@apiraino
Copy link
Contributor

apiraino commented Feb 9, 2023

I think this was reviewed and can be switched to waiting on author. Feel free to request a review with @rustbot ready, thanks!

edit: I retract the review switch (I had missed the comments in the review)

@rustbot ready

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Feb 9, 2023
@c410-f3r
Copy link
Contributor Author

c410-f3r commented Feb 9, 2023

Well, I am actually waiting for a FCP that should be started by the lang or compile team but didn't receive any feedback yet 🤷

@cjgillot cjgillot added T-lang Relevant to the language team, which will review and decide on the PR/issue. I-lang-nominated The issue / PR has been nominated for discussion during a lang team meeting. and removed T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Feb 9, 2023
@cjgillot
Copy link
Contributor

cjgillot commented Feb 9, 2023

Submitting to lang team consideration for stabilization.

@joshtriplett joshtriplett added the relnotes Marks issues that should be documented in the release notes of the next release. label Feb 14, 2023
@joshtriplett
Copy link
Member

We discussed this in today's @rust-lang/lang meeting, and we think this is ready for an FCP to merge:

@rfcbot merge

We'd also like to make sure that future work on type-alias impl Trait (TAIT) doesn't automatically assume anonymous lifetimes will work there, and thinks carefully about how or if that should work.

@rfcbot
Copy link

rfcbot commented Feb 14, 2023

Team member @joshtriplett has proposed to merge this. The next step is review by the rest of the tagged team members:

Concerns:

Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

cc @rust-lang/lang-advisors: FCP proposed for lang, please feel free to register concerns.
See this document for info about what commands tagged team members can give me.

@rfcbot rfcbot added proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. labels Feb 14, 2023
@joshtriplett joshtriplett removed the I-lang-nominated The issue / PR has been nominated for discussion during a lang team meeting. label Feb 14, 2023
@c410-f3r
Copy link
Contributor Author

c410-f3r commented Feb 15, 2023

We discussed this in today's @rust-lang/lang meeting, and we think this is ready for an FCP to merge:

@rfcbot merge

We'd also like to make sure that future work on type-alias impl Trait (TAIT) doesn't automatically assume anonymous lifetimes will work there, and thinks carefully about how or if that should work.

Just to confirm, the following snippet that is currently allowed will be denied due to possible future compatibility issues.

#![feature(anonymous_lifetime_in_impl_trait, type_alias_impl_trait)]

type Foo<'a> = impl IntoIterator<Item = &'a i32>;

// TAIT won't work without this function
pub fn type_resolution<'a>(slice: &'a [i32]) -> Foo<'a> {
    slice
}

pub fn bar<'a>(slice: &'a [i32]) {
    foo([type_resolution(slice)]);
}

// Implies foo<'a>(_: impl IntoIterator<Item = Foo<'a>) {}
pub fn foo(_: impl IntoIterator<Item = Foo>) {}

If that is really the case, then I will create a PR to address such concern.

@bors
Copy link
Contributor

bors commented Feb 17, 2023

☔ The latest upstream changes (presumably #108145) made this pull request unmergeable. Please resolve the merge conflicts.

Copy link
Contributor

@estebank estebank left a comment

Choose a reason for hiding this comment

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

r=me on the changes after t-lang approval and rebase

@QuineDot
Copy link

Lifetime elision seems to ignore the anonymous lifetime parameter introduced.

Example 1.

async fn does_not_compile(mut iter: impl Iterator<Item = &()>) -> &() {
    iter.next().unwrap()
}

async fn compiles<'a>(mut iter: impl Iterator<Item = &'a ()>) -> &'a () {
    iter.next().unwrap()
}

async fn wrong_error(iter: &mut impl Iterator<Item = &()>) -> &() {
    iter.next().unwrap()
}
Error output
error[[E0106]](https://doc.rust-lang.org/stable/error_codes/E0106.html): missing lifetime specifier
 --> src/lib.rs:1:67
  |
1 | async fn does_not_compile(mut iter: impl Iterator<Item = &()>) -> &() {
  |                                                                   ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
  |
1 | async fn does_not_compile(mut iter: impl Iterator<Item = &()>) -> &'static () {
  |                                                                    +++++++

error: lifetime may not live long enough
 --> src/lib.rs:2:5
  |
1 | async fn does_not_compile(mut iter: impl Iterator<Item = &()>) -> &() {
  |                                                                   --- return type `impl Future<Output = &'static ()>` contains a lifetime `'1`
2 |     iter.next().unwrap()
  |     ^^^^^^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'static`

error: lifetime may not live long enough
  --> src/lib.rs:10:5
   |
9  | async fn wrong_error(iter: &mut impl Iterator<Item = &()>) -> &() {
   |                            -                                  --- return type `impl Future<Output = &()>` contains a lifetime `'1`
   |                            |
   |                            let's call the lifetime of this reference `'2`
10 |     iter.next().unwrap()
   |     ^^^^^^^^^^^^^^^^^^^^ function was supposed to return data with lifetime `'2` but it is returning data with lifetime `'1`

For more information about this error, try `rustc --explain E0106`.
error: could not compile `playground` due to 3 previous errors

And similarly for non-async (on nightly):

#![feature(anonymous_lifetime_in_impl_trait)]

trait Trait<'a> {
    fn foo(&self) -> &'a str { "" }
}

// Comment this to see the other two errors
pub fn f(t: impl Trait<'_>) -> &str {
    t.foo()
}

pub fn g(t: &impl Trait<'_>) -> &str {
    t.foo()
}

pub fn parse_reg(it: &mut impl Iterator<Item=&Token>) -> Result<&str, String> {
    let c = it.next().unwrap();
    match c {
        Token::Text(text) => Ok(text.as_str()),
        _ => Err(format!("Expected register got {:?}", c))
    }
}
Error output
error[[E0106]](https://doc.rust-lang.org/nightly/error_codes/E0106.html): missing lifetime specifier
 --> src/lib.rs:8:32
  |
8 | pub fn f(t: impl Trait<'_>) -> &str {
  |                                ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
  |
8 | pub fn f(t: impl Trait<'_>) -> &'static str {
  |                                 +++++++

For more information about this error, try `rustc --explain E0106`.
error: lifetime may not live long enough
  --> src/lib.rs:15:5
   |
14 | pub fn g(t: &impl Trait<'_>) -> &str {
   |          -  - let's call the lifetime of this reference `'2`
   |          |
   |          has type `t`
15 |     t.foo()
   |     ^^^^^^^ function was supposed to return data with lifetime `'2` but it is returning data with lifetime `'1`

error: lifetime may not live long enough
  --> src/lib.rs:27:30
   |
24 | pub fn parse_reg(it: &mut impl Iterator<Item=&Token>) -> Result<&str, String> {
   |                  --  - let's call the lifetime of this reference `'2`
   |                  |
   |                  has type `it`
...
27 |         Token::Text(text) => Ok(text.as_str()),
   |                              ^^^^^^^^^^^^^^^^^ function was supposed to return data with lifetime `'2` but it is returning data with lifetime `'1`

error: could not compile `playground` (lib) due to 2 previous errors

The feature should IMO treat the introduced anonymous lifetime like one introduced by a reference.

  • Is the only input lifetime, and thus is the output lifetime, for fn does_not_compile and fn f
  • Causes ambiguity errors due to more than one input lifetime for fn wrong_error, fn g, and fn parse_reg

@tmandry
Copy link
Member

tmandry commented Mar 22, 2023

This was discussed in the lang team meeting yesterday (notes). We felt that the PR description needed more detail before we can decide on stabilizing this.

As in the Zulip discussion, it seems there are multiple ways of interpreting this elision.

fn _example(_: impl Iterator<Item = &()>) {}
  1. Adding a lifetime parameter to the function: fn _example<'a>(_: impl Iterator<Item = &'a ()>) {}
  2. Higher-ranked bound: fn _example(_: for<'a> impl Iterator<Item = &'a ()>) {}

There needs to be an argument why (1) is the correct behavior to stabilize, if we go with that.

@rfcbot concern why-not-higher-rank

We'd also like the PR to lay out how anonymous lifetimes will behave in the following cases (and other interesting cases if we didn't think of them):

  • impl Fn(&u32)
  • impl PartialEq<&u32>
  • impl Iterator<Item = &u32>
  • impl Foo<'_> (does '_ behave the same as elision in every case)

as well as whether/how we might extend this behavior to where clauses.

@rfcbot concern elaborate-cases-and-future-directions

@c410-f3r
Copy link
Contributor Author

Well, it is a surprising statement because my understanding is that all rules governing anonymous lifetimes in non-async functions should equal their async counterparts that is currently stable. Any difference between both will probably induce unnecessary confusion for users.

Anyway, I will try to elaborate a report taking into consideration the above assumption as well as the team's concerns in the following days/weeks.

@tmandry
Copy link
Member

tmandry commented Oct 18, 2023

@rfcbot reviewed

Based on my comment above,

@rfcbot resolve why-not-higher-rank

I also think the remaining use cases we listed were elaborated in the code samples in #107378 (comment), so

@rfcbot resolve elaborate-cases-and-future-directions

@c410-f3r
Copy link
Contributor Author

Thank you @tmandry for your time, review, commitment and help that allowed this feature to progress.

@nikomatsakis
Copy link
Contributor

We discussed this in the @rust-lang/lang meeting. I had a few concerns/observations and conclusion was that I should summarize them here.

On associated type position (without GATs)

For a case like impl Foo<Bar = &u32>, I agree with @tmandry's conclusion that the only sensible binding site is the function. If you desugared that to impl for<'a> Foo<Bar = &'a u32>, it would literally be impossible to implement that trait, since the two options both fail...

// OPTION A: true for all `'a`, but not legal, since `'a` cannot appear unconstrained
impl<'a> Foo for SomeType {
    type Item = &'a u32; // ERROR: 'a is not constrained by the impl header
}

// OPTION B: not true "for all" `'a`
impl<'a> Foo for &'a SomeType {
    type Item = &'a u32; // OK
}

On the other hand, if you have GATs (as @joshtriplett pointed out), then this is not true. Something like impl LendingIterator<Item<'_> = &u32> desugaring to impl for<'a> LendingIterator<Item<'a> = &'a u32> could well make sense. My experience with GATs is that you almost almost always want them to be higher-ranked over their inputs.

Inputs vs outputs

In general our elision rules are oriented around "inputs" (contravariant, roughly) and "outputs" (covariant, roughly). Roughly speaking, elision in return position is an "output" and elision in input position is an "input". Usually for inputs we create a fresh parameter and for outputs we match against something from the input.

The value of an associated type is (to my mind) clearly an output, so I kind of expect it to match against something in the input, but in the case of impl Iterator<Item = &u32>, the "input" is going to be some lifetime that appears in the impl type, and so effectively matching against that is like matching against some lifetime from the input parameter type, which is like matching against a fresh lifetime parameter on the fn. This is kind of why @tmandry's proposal here makes good sense to me.

But it is less clear once GATs come into play, because now there are more choices of where the "input" might come from.

On GATs

I think one of the main usability hits from GATs is that you always wind up using for<'a> syntax and it sucks. You want to be able to write impl LendingIterator<Item<'_> = &u32>

As trait inputs

The other examples were lifetimes elided in "trait input" position. Here I was wishing to see more examples. I am not convinced that the Fn traits are so special, though it may be the case.

I have for a long time thought that '_ in where clauses, at least, would be a nice way to write HRTB. The Serde Deserialize is actually a great example, in that I almost always write T: for<'de> Deserialize<'de> and would prefer to write

fn foo<T>()
where
    T: Deserialize<'_>

or something like it (indeed the DeserializeOwned trait alias exists for this reason, which I admit I only recently learned).

The root of our problems... references vs declarations

I think the root of our problems is the fact that we pun "declaration of a new generic type" from "reference to other types" syntactically, relying on the position to disambiguate. e.g. <T> in a struct declaration is a declaration: struct Foo<T>, but in a reference type a type,it's a reference impl Foo for Foo<T>. This is a regular source of confusion for people. Sometimes I wish we had used a keyword for declarations, so that struct Foo<type T> and impl Foo for Foo<type T> would both be declaring new generic parameters (one on the struct, and one on the impl).

Anyway, the reason this is relevant is that these HRTB bounds are places where both references and declarations make sense.

So what to do about it?

I'm not exactly sure, but I'm a bit reluctant to go forward here until we have talked through the GAT implications. One thing I think would be helpful is elaborating the examples where e.g. Visitor and Deserialize are used in the way @tmandry described. I guess I can see that one normally wants a Visitor that can visit some specific tcx that you have, so you are unlikely to want impl for<'tcx> Visitor<'tcx>, but I'm not sure the same holds for Deserialize.

All that said, this issue is sufficiently "murky" that I am wondering if what we need for GATs is some new syntax. We've tossed around a lot of ideas in this space before. Back in Ye Olde Days (and proposed again in RFC 2115, and then retracted), in-band lifetimes were targeting this general space. But I don't think they solve the problem and indeed just raise the same questions:

fn foo(
    x: impl LendingIterator<Item<'a> = &'a u32>, 
    y: impl LendingIterator<Item<'a> = &'a u32>,  // are these the *same* `'a`, or not?
)

Maybe we want a way to say "forall" explicitly, like '*, so that impl Deserialize<'*> means 'forall'. But what then does one write for lending iterator? impl LendingIterator<Item<'*> = &'* u32>?

Another option is to use for, as in impl LendingIterator<Item<for 'a> = &'a u32>, or T: Deserialize<for '_>. That's pretty wacky.

I don't really know! I'm going to raise a concern about GAT-consistency though, as I think this deep dive has highlighted GATs as a key consideration.

I think it would be really helpful to create a hackmd and start collecting concrete code examples and the desired desugaring for each.

@rfcbot concern gat-consistency

@traviscross
Copy link
Contributor

Given the thoughtful analysis and concern raised by @nikomatsakis above, let's remove the nomination until the further analysis and write-up that @nikomatsakis requested is done. Until that happens, there's probably little value in further discussion during the meeting.

For anyone who picks this up and writes such an analysis, please renominate this for T-lang.

@rustbot labels -I-lang-nominated

@rustbot rustbot removed the I-lang-nominated The issue / PR has been nominated for discussion during a lang team meeting. label Oct 25, 2023
@nikomatsakis
Copy link
Contributor

To get things started, I've created a hackmd where we can add some of the usage patterns we would like to work (it's ok, to my mind, if they're somewhat contradictory).

@tmandry
Copy link
Member

tmandry commented Oct 27, 2023

@nikomatsakis Following the same line of reasoning from my comment above, GATs are clearly different in that their parameters can't be determined ahead of time by the caller. Take the example

fn fun(x: impl LendingIterator<Item = &u32>) {  // or, if you prefer: Item<'_> = &'_ u32
    for _ in x {}
}

By construction, our callee fun has a trait bound in the environment whose GAT allows us to construct a type out of any generic arguments. More concretely, these arguments usually come from the invocation of a method on the trait (next in this case). The invocation happens in the callee, so it stands to reason that this parameter must be higher-ranked.

It seems like our options for GATs are:

  1. We could disallow elided lifetime arguments of GATs in impl Trait associated type bounds
  2. We could desugar the elided lifetime arguments to HRTB

I personally don't see a need to allow elided lifetimes in arguments to GATs right away, so I would propose going with option 1 for now.

I think the positions we've discussed all interact differently with impl Trait in argument position. I would agree, though, that if we decide to stabilize with "heuristic" behavior that differs across positions (increasing the complexity of the language mechanics), we'd better be sure that the heuristics are almost always right (reducing the complexity of writing actual Rust code).

To get things started, I've created a hackmd where we can add some of the usage patterns we would like to work (it's ok, to my mind, if they're somewhat contradictory).

The reasoning about trait parameters in my post above rests on a lack of common examples where trait parameters are not used in the Self type (and a result of appearing in Self, would be determined at the callsite of a function which takes impl Trait<...> as an argument). If there are examples (other than the Fn traits, which are special), they would be a good starting point for filling out the use cases in that doc.

@nikomatsakis
Copy link
Contributor

@tmandry

The reasoning about trait parameters in my post above rests on a lack of common examples where trait parameters are not used in the Self type

I'm thinking here -- I think I actually have two concerns:

  • Looking ahead to GATs -- I agree we don't have to support '_ in GATs, but I want to talk about what it might mean in that context.
  • Where-clauses vs impl Trait bounds: I realize I am thinking more of cases like where T: Deserialize<'_> than impl Deserialize<'_>. We don't necessarily have to have '_ behave the same there, but I could see it being awfully confusing if it doesn't.

That said, looking purely at types, our rules are that () indicates a binding site for '_. This is why fn(&u8) -> &u8 and SomeFnType<&u8, &u8> behave differently. This backs up the "Fn traits are special". In the past, my assumption was that we would eventually make () -> sugar applicable to any trait, so I thought of it more like "() is special". One could certainly say that the "binding site" for a where-clause is the where-clause itself, which would mean that where T: Deserialize<'_> is equivalent to where for<'de> T: Deserialize<'de> but fn foo(x: impl Deserialize<'_>) is equivalent to fn foo<'de>(x: impl Deserialize<'de>). I still find myself a bit surprised there, though, I think I expect impl to act as a binding site if where-clauses do.

If there are examples (other than the Fn traits, which are special), they would be a good starting point for filling out the use cases in that doc.

Agreed! Though I'd like to see the patterns where people do want the '_ to expand to "some anonymous lifetime that appears in the self type". For example, you mentioned Visitor, but I think in that case 'tcx is almost always in scope and nameable. Serde is an interesting case.

I added your fn foo(x: impl Iterator<Item = &u32>) case, which I agree really ought to work. I don't know how to square that at present with my other intuitions.

I think one thing that might be useful -- I've tried to do this in the past but I never got it to quite work out -- is to describe the "underlying logic" for elision in a way that naturally expands to other locations.

I would be curious to get a sense for what "your average experienced Rust user" expects things to mean. Maybe we can design an experiment. :)

@nikomatsakis
Copy link
Contributor

nikomatsakis commented Oct 27, 2023

I did some grepping around the compiler and found that:

  • the impl Iterator<Item = &u32> pattern is very common
  • impl for is mostly used with the Fn traits, but not exclusively. For example, the DecorateLint uses it.
  • As an argument in favor of binding at the fn site, the IntoDiagnostic trait seems to match that pattern -- I take it back, all of these cases use the 'a more than once
  • in the compiler at least, impl Trait<'X is relatively rare, the other examples were all 'tcx related. I consider this case (a "well known" lifetime) to be an interesting one, and one where I personally would prefer to see that lifetime named explicitly. I expect '_ to be used for "throwaway" lifetimes that just exist "temporarily" / for the duration of the fn (I realize 'tcx is often similar, from the POV of a particular function).

Also, in addition to accidentally stabilizing '_ in impl Trait in async fn, we appear to have (accidentally?) stabilized it in return types:

@nikomatsakis
Copy link
Contributor

nikomatsakis commented Oct 27, 2023

I've been thinking about tmandry's point -- in other words, why do people put lifetime parameters on traits anyway. @tmandry is correct that the role of such a parameter is to capture a lifetime that (may) appear in the self type (or some other input type) and which we wish to name in the interface. I think part of the rub is that often this lifetime may or may not appear, and it's useful to distinguish that (this is the Serde Deserialize case).

Looking at e.g. the DecorateLint trait:

pub trait DecorateLint<'a, G: EmissionGuarantee> {
    /// Decorate and emit a lint.
    fn decorate_lint<'b>(
        self,
        diag: &'b mut DiagnosticBuilder<'a, G>,
    ) -> &'b mut DiagnosticBuilder<'a, G>;

    fn msg(&self) -> DiagnosticMessage;
}

Presumably, the reason that this trait has the 'a parameter bound on the trait and not the function is because, for some of the implementing types, it also appears in the Self parameter, and they wish to take references to self and store them in this builder. That said, I couldn't actually find any examples like this in the compiler, all of which look like:

impl<'a> DecorateLint<'a, ()> for BuiltinMissingDebugImpl<'_>

(And this is presumably why we see impl for<'a> DecorateLint<'a, ()> so often.)

(I'm assuming clippy or external code bases may be different? If not, this trait just seems more complex than it has to be.)

@nikomatsakis
Copy link
Contributor

OK, I'm feeling quite effectively nerdswiped by this issue. In theory I'm on vacation today but I am still thinking about it. :)

Here are some more discombulated thoughts that I don't have time to knit into a coherent story.

There are really a few issues at play here.

One of them is elided lifetimes the value of an associated type, which may well reference lifetimes that appear in the trait input types. This is relatively clear cut for a trait like Iterator, where there are no lifetime arguments and the associated type is not a GAT -- there is only one expansion that is useful (anonymous lifetime bound on the fn).

It's less clear when you have GATs or lifetime parameters on the trait. In cases like impl Foo<'_, Item = &u32> or impl Foo<Item<'_> = &u32>, there I can imagine various choices that would be appropriate in some cases, and so I think we have to think about other forms of consistency.

The other case is when you have a lifetime parameter on a trait. That is relatively unusual and (I think) it is sort of ill understood when it makes sense. The basic reason to have a lifetime parameter in a trait is if you want it to appear in a function signature (e.g., the 'a in fn some_method(&'a self) or fn get(&self) -> &'a u32). In that case, the reason you put the lifetime parameter on the trait (and not the function) is either because you need it to be the same as some elements of your input types or because you need consistency between the methods and an associated type. On its own, though, this latter case is typically better handled by GATs.

Reason 1a: The lifetime appears in your interface and is tied to one of your input types. This is the 'tcx lifetime on Visitor, for example, and also the case for IntoDiagnostic. On the one hand, in this case, you definitely do NOT want impl for<'a> Foo<'a> semantics. On the other hand, an anonymous lifetime on the function isn't that useful either -- for example, looking at all the uses of IntoDiagnostic, they have the pattern fn foo<'a>(&'a self, x: impl IntoDiagnostic<'a, ...>). This makes sense: the lifetime appears in the interface, and you have to link it to the input you are going to be providing (in this case, some kind of buffer argument).

Reason 1b: Same as above, but the lifetime is only sometimes necessary. This corresponds to Deserialize in serde. In this case, impl for<'a> Deserialize<'a> might be ok, but it's not clear; you're probably also happy with some anonymous lifetime bound on the function. The idea is that you are going to be deserializing into the function -- and clearly you aren't going to be storing the resulting type, because you don't have to know its name, so it's ok if you just know that it outlives the function body. On where-clauses though it can be really useful to distinguish the case of "this implementation works with any lifetime of data".

Reason 2: The lifetime appears in your function inputs and also in the value of an associated type. This is what happens with the Fn<A> traits, where the A argument may carry lifetimes that should appear in the return type (Output). Hence why T: Fn(&u32) -> &u32 expands to T: for<'a> Fn<(&'a u32,), Output = &'a u32>.

All of this is definitely making me want to do a bit of a "study" of common patterns across where-clauses, associated types, etc.

I'm also finding myself grasping a bit towards some design tenets or principles, though I don't know the ordering yet...

  • useful -- the expansion should capture what people want most of the time
  • memorable -- the rules we use can't be too complicated or people will be overwhelmed
  • backwards compatible -- of course we have to get consistency with existing rules for types etc; not sure how to think about the accidental stabilization, but I do think they carry some weight as breakage in the wild is no fun
  • impl and where-clauses should be consistent -- where T: $BOUNDS and impl $BOUNDS should mean the same thing

...as I was thinking before, it may be that the best answer to get past '_ as the only option, so that we can capture some of the other cases another way. Hmm.

@nikomatsakis
Copy link
Contributor

nikomatsakis commented Oct 27, 2023

In the interest of incremental progress:

I wonder about stabilizing '_ in the value of non-generic associated types for impl, with the rule that it behaves the same as it would in a lifetime parameter of a type like Vec.

i.e.,

fn foo(x: impl Iterator<Item = &u32>) // equivalent to `'a` in `fn foo<'a>`
fn foo(&self) -> impl Iterator<Item = &u32> // equivalent to `'a` in `fn foo<'a>(&'a self)`
fn foo<'b>(x: impl Iterator<'b, Item = &u32>) // equivalent to `'a` in `fn foo<'a>` (NOT `'b`)

// NOT STABILIZED
fn foo(x: impl LendingIterator<Item<'_> = &u32>)
fn foo(x: impl FooBar<_, Item = &u32> 

Also, ideally, async fn and fn would behave the same.

This definitely commits us to a certain sort of principle I'd like to articulate but I really have to stop leaving comments. :) But it seems like a principle I feel pretty comfortable with and reasonably sure we are going to want.

@tmandry
Copy link
Member

tmandry commented Oct 30, 2023

I would be happy to start with the subset that @nikomatsakis proposed. @c410-f3r are you interested in making those changes?

@c410-f3r
Copy link
Contributor Author

Personally, I need more time to digest all the information, but at first glance it seems that the current behaviour matches the desired subset.

However, if it is necessary to perform more than a feature removal and/or test management, then I probably won't have the time to empirically hack the compiler to achieve the goals in the near future.

@rust-log-analyzer

This comment has been minimized.

@tmandry
Copy link
Member

tmandry commented Oct 31, 2023

This proposal is to only allow elided lifetimes in equality bounds of non-generic associated types. I think the changes from the current behavior would be:

  1. Elided lifetimes are not allowed in the generics of the trait itself.
  2. Elided lifetimes are not allowed in equality bounds of GATs. (Though I'm not sure if they are currently)

@bors
Copy link
Contributor

bors commented Dec 11, 2023

☔ The latest upstream changes (presumably #118823) made this pull request unmergeable. Please resolve the merge conflicts.

@Dylan-DPC Dylan-DPC added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-team Status: Awaiting decision from the relevant subteam (see the T-<team> label). labels Feb 6, 2024
@Dylan-DPC
Copy link
Member

@c410-f3r any updates on this? thanks

@c410-f3r
Copy link
Contributor Author

I will try to get back to this PR next week

@c410-f3r
Copy link
Contributor Author

c410-f3r commented Mar 31, 2024

In regards to not stabilizing elided lifetimes in trait generics, the following is already allowed today.

trait FooBar<'a> {
    type Item;
}

async fn foo(_: impl FooBar<'_, Item = &u32>) {}

So leaving this case out for non-async functions does not help with the lack of parity.

If the elaboration of elided rules is not trivial and needs more investigation, then the accidental stabilization of elided lifetimes for asynchronous functions should probably be re-considered and possibly partially-reverted.

@c410-f3r c410-f3r force-pushed the stabilize-anon-lt branch 2 times, most recently from 70abdc8 to 5717003 Compare April 21, 2024 21:48
@traviscross
Copy link
Contributor

@rustbot labels +I-lang-nominated

We unnominated this back in October 2023 as more analysis seemed to be needed. Since then, @nikomatsakis and @tmandry have posted substantive analysis that it seems we should discuss.

@rustbot rustbot added the I-lang-nominated The issue / PR has been nominated for discussion during a lang team meeting. label May 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. I-lang-nominated The issue / PR has been nominated for discussion during a lang team meeting. proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. relnotes Marks issues that should be documented in the release notes of the next release. S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-lang Relevant to the language team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet