rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
97.97k stars 12.69k forks source link

Make lifetime elision rules for closures consistent with lifetime elision rules for functions #86921

Open bstrie opened 3 years ago

bstrie commented 3 years ago

The following code does not compile:

fn function(x: &i32) -> &i32 { x } // ok
let closure = |x: &i32| -> &i32 { x }; // fails

Output:

error: lifetime may not live long enough
 --> src/main.rs:3:35
  |
3 | let closure = |x: &i32| -> &i32 { x };
  |                   -        -      ^ returning this value requires that `'1` must outlive `'2`
  |                   |        |
  |                   |        let's call the lifetime of this reference `'2`
  |                   let's call the lifetime of this reference `'1`

For function, the lifetime of the return type is properly inferred to be equal to the only input lifetime, as per the lifetime elision rules:

If there is exactly one lifetime used in the parameters (elided or not), that lifetime is assigned to all elided output lifetimes.

However, for closure this is not done, and the return type is assigned a distinct lifetime from the input. AFAIK it is only an accident of history that closures do not have the same lifetime elision rules as functions. This would be a breaking change to fix, but should be able to be done over an edition.


Here's a simplified example of code that compiles today that might break if the function elision rules were applied to closures:

fn foo(s: &str) -> &str {
    let bar = |_| { s };
    bar(&String::new())
}

See https://github.com/rust-lang/rust/issues/56537 for prior discussion.

jhpratt commented 3 years ago

Why would this be a breaking change? Wouldn't it only permit compilation of more code?

bstrie commented 3 years ago

Hm, I was worried that it might break code like the following:

let closure = |x: &str| -> &str { "hello" };

...but actually maybe that might compile just fine? I thought I remembered somebody coming up with a reason that it couldn't be done backwards-compatibly, but I can't recall it now.

ecstatic-morse commented 3 years ago

See #56537 for why this would be a breaking change.

It would be great to see this inconsistency fixed for new code. Some proc macros want to wrap the body of a function in a closure to inspect the returned value, and this is a blocker. This is the root cause of https://gitlab.com/karroffel/contracts/-/issues/11 for example, and it makes that crate basically unusable whenever mutable references are involved.

matklad commented 3 years ago

Hit this rather obscure issue in a rather simple code. Which got me thinking:

Making closures to use lifetime elision doesn't make sense to me, it feels exactly backwards. We have lifetime elision and not lifetime inference for functions because we want function's signature to specify function's interface completely and create an abstraction boundary. We explitelly do not want function body to affect the types, which allows us to declare functions "at the top level" and do various separate-compilation-like things (like having semver guarantees, for example).

Closures are deliberately different from functions in that they explicitly allow inferring types from bodies. The price we pay for this is that we need "context-aware inference" to figure out closure's type, so we don't allow closures on the top-level.

That is, fns and closures are different mechanisms with offer different trade-offs to the user.

For this reason, we don't want closure's lifetimes to be elided (which is an approximation), we want them to be inferred.

So it seems to be that the original example in the issue is just a bug in lifetime inference? That is, the compiler should understand precisely what's different between f and g in the following example:

fn main<'x>(x: &'x str) {
  let f: |y: &str| -> x;
  let g: |y: &str| -> y;
}

Both casse should just work without adding any kind of extra syntax or annotations and infer for<'y>: &'y str -> &'x str and for<'y>: &'y str -> &'y str signatures for closures.

bstrie commented 2 years ago

Yes, that argument does seem reasonable, and as a bonus it means that this wouldn't be a breaking change. On the other hand, while I presume that making closures participate in lifetime elision would be an extremely small amount of work, this sounds like it might be much more work (does the compiler have existing mechanisms for lifetime inference anywhere?), and it also raises questions about whether this would impact compilation times negatively (closures are already a big offender, though for monomorphization reasons).

bstrie commented 2 years ago

An instance of this discrepancy being encountered in the wild: https://www.reddit.com/r/rust/comments/rjtphp/why_doesnt_the_compiler_infer_the_proper_lifetimes/