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

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

Open
bstrie opened this issue Jul 7, 2021 · 6 comments
Labels
A-closures Area: Closures (`|…| { … }`) A-inference Area: Type inference A-lifetimes Area: Lifetimes / regions A-maybe-future-edition Something we may consider for a future edition. T-lang Relevant to the language team, which will review and decide on the PR/issue.

Comments

@bstrie
Copy link
Contributor

bstrie commented Jul 7, 2021

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 #56537 for prior discussion.

@bstrie bstrie added T-lang Relevant to the language team, which will review and decide on the PR/issue. A-inference Area: Type inference A-maybe-future-edition Something we may consider for a future edition. labels Jul 7, 2021
@jonas-schievink jonas-schievink added A-closures Area: Closures (`|…| { … }`) A-lifetimes Area: Lifetimes / regions labels Jul 7, 2021
@jhpratt
Copy link
Member

jhpratt commented Jul 18, 2021

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

@bstrie
Copy link
Contributor Author

bstrie commented Jul 19, 2021

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
Copy link
Contributor

ecstatic-morse commented Jul 20, 2021

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
Copy link
Member

matklad commented Oct 13, 2021

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
Copy link
Contributor Author

bstrie commented Nov 6, 2021

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
Copy link
Contributor Author

bstrie commented Dec 20, 2021

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/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-closures Area: Closures (`|…| { … }`) A-inference Area: Type inference A-lifetimes Area: Lifetimes / regions A-maybe-future-edition Something we may consider for a future edition. T-lang Relevant to the language team, which will review and decide on the PR/issue.
Projects
Status: Idea
Development

No branches or pull requests

5 participants