Perform lifetime elision (more) syntactically, before type-checking. by eddyb · Pull Request #39305 · rust-lang/rust (original) (raw)

The initial goal of this patch was to remove the (contextual) &RegionScope argument passed around rustc_typeck::astconv and allow converting arbitrary (syntactic) hir::Ty to (semantic) Ty.
I've tried to closely match the existing behavior while moving the logic to the earlier resolve_lifetime pass, and the crater report suggests none of the changes broke real code, but I will try to list everything:

There are few cases in lifetime elision that could trip users up due to "hidden knowledge":

type StaticStr = &'static str; // hides 'static trait WithLifetime<'a> { type Output; // can hide 'a }

// This worked because the type of the first argument contains // 'static, although StaticStr doesn't even have parameters. fn foo(x: StaticStr) -> &str { x }

// This worked because the compiler resolved the argument type // to <T as WithLifetime<'a>>::Output which has the hidden 'a. fn bar<'a, T: WithLifetime<'a>>(_: T::Output) -> &str { "baz" }

In the two examples above, elision wasn't using lifetimes that were in the source, not even needed by paths in the source, but rather happened to be part of the semantic representation of the types.
To me, this suggests they should have never worked through elision (and they don't with this PR).

Next we have an actual rule with a strange result, that is, the return type here elides to &'x str:

impl<'a, 'b> Trait for Foo<'a, 'b> { fn method<'x, 'y>(self: &'x Foo<'a, 'b>, _: Bar<'y>) -> &str { &self.name } }

All 3 of 'a, 'b and 'y are being ignored, because the &self elision rule only cares that the first argument is "self by reference". Due implementation considerations (elision running before typeck), I've limited it in this PR to a reference to a primitive/struct/enum/union, but not other types, but I am doing another crater run to assess the impact of limiting it to literally &self and self: &Self (they're identical in HIR).

It's probably ideal to keep an "implicit Self for self" type around and only apply the rule to &self itself, but that would result in more bikeshed, and #21400 suggests some people expect otherwise.
Another decent option is treating self: X, ... -> Y like X -> Y (one unique lifetime in X used for Y).

The remaining changes have to do with "object lifetime defaults" (see RFCs 599 and 1156):

trait Trait {} struct Ref2<'a, 'b, T: 'a+'b>(&'a T, &'b T);

// These apply specifically within a (fn) body, // which allows type and lifetime inference: fn main() { // Used to be &'a mut (Trait+'a) - where 'a is one // inference variable - &'a mut (Trait+'b) in this PR. let _: &mut Trait;

// Used to be an ambiguity error, but in this PR it's
// Ref2<'a, 'b, Trait+'c> (3 inference variables).
let _: Ref2<Trait>;

}

What's happening here is that inference variables are created on the fly by typeck whenever a lifetime has no resolution attached to it - while it would be possible to alter the implementation to reuse inference variables based on decisions made early by resolve_lifetime, not doing that is more flexible and works better - it can compile all testcases from #38624 by not ending up with &'static mut (Trait+'static).

The ambiguity specifically cannot be an early error, because this is only the "default" (typeck can still pick something better based on the definition of Trait and whether it has any lifetime bounds), and having an error at all doesn't help anyone, as we can perfectly infer an appropriate lifetime inside the fn body.

TODO: write tests for the user-visible changes.

cc @nikomatsakis @arielb1