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

Unclear lifetime error in closure producing a future #74497

Open
95th opened this issue Jul 19, 2020 · 17 comments
Open

Unclear lifetime error in closure producing a future #74497

95th opened this issue Jul 19, 2020 · 17 comments
Assignees
Labels
A-async-await Area: Async & Await A-diagnostics Area: Messages for errors, warnings, and lints A-lifetimes Area: Lifetimes / regions AsyncAwait-Polish Async-await issues that are part of the "polish" area AsyncAwait-Triaged Async-await issues that have been triaged during a working group meeting. C-bug Category: This is a bug. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Comments

@95th
Copy link
Contributor

95th commented Jul 19, 2020

Code:

use std::future::Future;

pub async fn bar() {
    foo(|x| baz(x)).await;
}

pub async fn baz(x: &u8) -> bool {
    if *x == 1 {
        false
    } else {
        true
    }
}

pub async fn foo<F, T>(f: F) -> bool
where
    F: Fn(&u8) -> T,
    T: Future<Output = bool>,
{
    f(&32).await
}

produces following error:

error: lifetime may not live long enough
 --> src/lib.rs:4:13
  |
4 |     foo(|x| baz(x)).await;
  |          -- ^^^^^^ returning this value requires that `'1` must outlive `'2`
  |          ||
  |          |return type of closure is impl std::future::Future
  |          has type `&'1 u8`

Is this error correct? If yes, why?

following also doesn't work:

pub async fn bar() {
    foo(baz).await;
}

error (still not clear):

error[E0271]: type mismatch resolving `for<'r> <for<'_> fn(&u8) -> impl std::future::Future {baz} as std::ops::FnOnce<(&'r u8,)>>::Output == _`
  --> src/lib.rs:4:5
   |
4  |     foo(baz).await;
   |     ^^^ expected bound lifetime parameter, found concrete lifetime
...
15 | pub async fn foo<F, T>(f: F) -> bool
   |              --- required by a bound in this
16 | where
17 |     F: Fn(&u8) -> T,
   |                   - required by this bound in `foo`

meta:

$ rustc --version --verbose
rustc 1.45.0 (5c1f21c3b 2020-07-13)
binary: rustc
commit-hash: 5c1f21c3b82297671ad3ae1e8c942d2ca92e84f2
commit-date: 2020-07-13
host: x86_64-unknown-linux-gnu
release: 1.45.0
LLVM version: 10.0
@SNCPlay42
Copy link
Contributor

This note

return type of closure is impl std::future::Future

Is supposed to define the lifetime '2 referred to by the error message by showing it as a parameter of the return type, but '2 doesn't appear in the type anywhere, possibly because of the opaque impl Future type.

cc #74072

@jonas-schievink jonas-schievink added A-diagnostics Area: Messages for errors, warnings, and lints A-lifetimes Area: Lifetimes / regions C-bug Category: This is a bug. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Jul 19, 2020
@csmoe csmoe added the A-async-await Area: Async & Await label Jul 19, 2020
@nikomatsakis
Copy link
Contributor

This code is invalid. The closure is given a &u8, which is only valid during the closure execution. But baz(x) captures x and embeds it into a future that is returned -- this future, when awaited, will (or could) attempt to use x, even though the closure itself has returned, and hence x is no longer valid.

The error message here is obviously not great. We have specialized errors for these kinds of cases in closures normally, I'm not sure why it didn't trigger in this case.

@tmandry tmandry added the AsyncAwait-Triaged Async-await issues that have been triaged during a working group meeting. label Jul 21, 2020
@nikomatsakis
Copy link
Contributor

There isn't a great workaround that uses stable syntax to make this compile, interestingly. We discussed it some on Zulip.

The error message for a comparable scenario without async isn't that great either, actually, I am somewhat surprised (playground):

error: lifetime may not live long enough
 --> src/lib.rs:4:13
  |
4 |     foo(|x| baz(x))
  |          -- ^^^^^^ returning this value requires that `'1` must outlive `'2`
  |          ||
  |          |return type of closure is &'2 u8
  |          has type `&'1 u8`

@nikomatsakis
Copy link
Contributor

Returning a reference to a local variable is somewhat better (playground):

error[E0515]: cannot return value referencing local variable `p`
 --> src/lib.rs:4:26
  |
4 |     foo(|x| { let p = 3; baz(&p) })
  |                          ^^^^--^
  |                          |   |
  |                          |   `p` is borrowed here
  |                          returns a value referencing data owned by the current function

Dylan-DPC-zz pushed a commit to Dylan-DPC-zz/rust that referenced this issue Jul 24, 2020
…r=estebank

Refactor `region_name`: add `RegionNameHighlight`

This PR does not change any diagnostics itself, rather it enables further code changes, but I would like to get approval for the refactoring first before making use of it.

In `rustc_mir::borrow_check::diagnostics::region_name`, there is code that allows for, when giving a synthesized name like `'1` to an anonymous lifetime, pointing at e.g. the exact '`&`' that introduces the lifetime.

This PR decouples that code from the specific case of arguments, adding a new enum `RegionNameHighlight`, enabling future changes to use it in other places.

This allows:

* We could change the other `AnonRegionFrom*` variants to use `RegionNameHighlight` to precisely point at where lifetimes are introduced in other locations when they have type annotations, e.g. a closure return `|...| -> &i32`.
  * Because of how async functions are lowered this affects async functions as well, see rust-lang#74072
* for rust-lang#74597, we could add a second, optional `RegionNameHighlight` to the `AnonRegionFromArgument` variant that highlights a lifetime in the return type of a function when, due to elision, this is the same as the argument lifetime.
* in rust-lang#74497 (comment) I noticed that a diagnostic was trying to introduce a lifetime `'2` in the opaque type `impl std::future::Future`. The code for the case of arguments has [code to handle cases like this](https://github.com/rust-lang/rust/blob/bbebe7351fcd29af1eb9a35e315369b15887ea09/src/librustc_mir/borrow_check/diagnostics/region_name.rs#L365) but not the others. This refactoring would allow the same code path to handle this.
  * It might be appropriate to add another variant of `RegionNameHighlight` to say something like `lifetime '1 appears in the opaque type impl std::future::Future`.

These are quite a few changes so I thought I would make sure the refactoring is OK before I start making changes that rely on it. :)
Manishearth added a commit to Manishearth/rust that referenced this issue Jul 24, 2020
…r=estebank

Refactor `region_name`: add `RegionNameHighlight`

This PR does not change any diagnostics itself, rather it enables further code changes, but I would like to get approval for the refactoring first before making use of it.

In `rustc_mir::borrow_check::diagnostics::region_name`, there is code that allows for, when giving a synthesized name like `'1` to an anonymous lifetime, pointing at e.g. the exact '`&`' that introduces the lifetime.

This PR decouples that code from the specific case of arguments, adding a new enum `RegionNameHighlight`, enabling future changes to use it in other places.

This allows:

* We could change the other `AnonRegionFrom*` variants to use `RegionNameHighlight` to precisely point at where lifetimes are introduced in other locations when they have type annotations, e.g. a closure return `|...| -> &i32`.
  * Because of how async functions are lowered this affects async functions as well, see rust-lang#74072
* for rust-lang#74597, we could add a second, optional `RegionNameHighlight` to the `AnonRegionFromArgument` variant that highlights a lifetime in the return type of a function when, due to elision, this is the same as the argument lifetime.
* in rust-lang#74497 (comment) I noticed that a diagnostic was trying to introduce a lifetime `'2` in the opaque type `impl std::future::Future`. The code for the case of arguments has [code to handle cases like this](https://github.com/rust-lang/rust/blob/bbebe7351fcd29af1eb9a35e315369b15887ea09/src/librustc_mir/borrow_check/diagnostics/region_name.rs#L365) but not the others. This refactoring would allow the same code path to handle this.
  * It might be appropriate to add another variant of `RegionNameHighlight` to say something like `lifetime '1 appears in the opaque type impl std::future::Future`.

These are quite a few changes so I thought I would make sure the refactoring is OK before I start making changes that rely on it. :)
Dylan-DPC-zz pushed a commit to Dylan-DPC-zz/rust that referenced this issue Nov 9, 2020
…mulacrum

Improve lifetime name annotations for closures & async functions

* Don't refer to async functions as "generators" in error output
* Where possible, emit annotations pointing exactly at the `&` in the return type of closures (when they have explicit return types) and async functions, like we do for arguments.
Addresses rust-lang#74072, but I wouldn't call that *closed* until annotations are identical for async and non-async functions.
* Emit a better annotation when the lifetime doesn't appear in the full name type, which currently happens for opaque types like `impl Future`. Addresses rust-lang#74497, but further improves could probably be made (why *doesn't* it appear in the type as `impl Future + '1`?)
This is included in the same PR because the changes to `give_name_if_anonymous_region_appears_in_output` would introduce ICE otherwise (it would return `None` in cases where it didn't previously, which then gets `unwrap`ped)
@WinLinux1028
Copy link

WinLinux1028 commented May 27, 2021

This code is invalid. The closure is given a &u8, which is only valid during the closure execution. But baz(x) captures x and embeds it into a future that is returned -- this future, when awaited, will (or could) attempt to use x, even though the closure itself has returned, and hence x is no longer valid.

The error message here is obviously not great. We have specialized errors for these kinds of cases in closures normally, I'm not sure why it didn't trigger in this case.

If the Future which returned by f(&32) awaited out of &32's lifetime, I think so too.
But if not, this code is valid.

This code will work:

pub async fn bar() {
    println!("{:?}",foo(|x| baz(x)).await);
}

pub async fn baz(x: &u8) -> bool {
    if *x == 1 {
        false
    } else {
        true
    }
}

pub async fn foo<F, T>(f: F) -> bool
where
    F: Fn(&'static u8) -> T,
    T: Future<Output = bool>,
{
    f(force_static(&32)).await
}

fn force_static<T>(a: &T) -> &'static T {
    unsafe { &*(a as *const T) }
}

@eholk
Copy link
Contributor

eholk commented Sep 8, 2021

One of the things I find surprising here is when the error messages talk about '1 and '2. It's not really clear where these lifetimes would come from, although presumably they are some implied or inferred lifetime that's not written explicitly. Would it be possible to have rustc give some more information about where these come from, such as what values are borrowed with each lifetime?

@eholk
Copy link
Contributor

eholk commented Sep 8, 2021

@WinLinux1028 - That's interesting that the force_static makes it work. I wonder if we could do this automatically, since I think &32 has a static lifetime anyway, or at least can be promoted to one?

Also, is the force_static actually needed, since f already has a 'static bound on its parameter?

@eholk
Copy link
Contributor

eholk commented Sep 8, 2021

Yeah, it looks like @WinLinux1028's latest example works without force_static: playground link.

@estebank
Copy link
Contributor

Triage: the current output is the same as for sync fns:

error: lifetime may not live long enough
 --> src/lib.rs:4:13
  |
4 |     foo(|x| baz(x)).await;
  |          -- ^^^^^^ returning this value requires that `'1` must outlive `'2`
  |          ||
  |          |return type of closure `impl Future` contains a lifetime `'2`
  |          has type `&'1 u8`

@eholk
Copy link
Contributor

eholk commented Oct 18, 2021

@rustbot label +AsyncAwait-polish

@rustbot rustbot added the AsyncAwait-Polish Async-await issues that are part of the "polish" area label Oct 18, 2021
@estebank
Copy link
Contributor

estebank commented Dec 9, 2021

This should point at the bounds in foo and potentially suggest

pub async fn foo<'a, F, T>(f: F) -> bool
where
    F: Fn(&'a u8) -> T,
    T: Future<Output = bool> + 'a,

@estebank estebank self-assigned this Dec 9, 2021
@estebank estebank removed their assignment Jul 7, 2022
@estebank
Copy link
Contributor

Current output of the original test case:

error[E0308]: mismatched types
  --> src/lib.rs:4:5
   |
4  |     foo(baz).await;
   |     ^^^^^^^^ one type is more general than the other
   |
   = note: expected trait `for<'r> <for<'r> fn(&'r u8) -> impl for<'r> Future<Output = bool> {baz} as FnOnce<(&'r u8,)>>`
              found trait `for<'r> <for<'r> fn(&'r u8) -> impl for<'r> Future<Output = bool> {baz} as FnOnce<(&'r u8,)>>`
note: the lifetime requirement is introduced here
  --> src/lib.rs:17:19
   |
17 |     F: Fn(&u8) -> T,
   |                   ^

@vincenzopalazzo
Copy link
Member

I will try to resurrect this with a deep dive into it

@rustbot claim

@vincenzopalazzo vincenzopalazzo moved this from On deck to Claimed in wg-async work Mar 2, 2023
@vincenzopalazzo vincenzopalazzo moved this from Claimed to In progress (current sprint) in wg-async work Mar 2, 2023
@vincenzopalazzo
Copy link
Member

vincenzopalazzo commented May 4, 2023

Okay, after spending some time on it, I've come up with a summary.

This is the complete example that includes the error, and also explains why this code will not work.

use std::future::Future;

pub async fn bar() {
    foo(|x| baz(x)).await;
}

pub async fn baz(x: &u8) -> bool {
    if *x == 1 {
        false
    } else {
        true
    }
}

/// From Nico
///
/// This code is invalid. The closure is given a &u8, which is only valid
/// during the closure execution. But baz(x) captures x and embeds it
/// into a future that is returned -- this future, when awaited, will
/// (or could) attempt to use x, even though the closure itself has
/// returned, and hence x is no longer valid.
///
/// The error message here is obviously not great. We have specialized
/// errors for these kinds of cases in closures normally, I'm not sure
/// why it didn't trigger in this case.
pub async fn foo<F, T>(f: F) -> bool
where
    F: Fn(&u8) -> T,
    T: Future<Output = bool>,
{
    f(&32).await
}

fn main() {}

But it seems that, upon further reflection, the point made by @WinLinux1028 and analyzed by @eholk is correct. I wasn't able to find a counterexample where this code doesn't work. However, I do need to say that the solution proposed by @estebank is more attractive to me because it exactly describes what the callback does (since I don't like using 'static lifetimes).

Here is the revised code:

use std::future::Future;

pub async fn bar() {
    foo(|x| baz(x)).await;
}

pub async fn baz(x: &u8) -> bool {
    if *x == 1 {
        false
    } else {
        true
    }
}

pub async fn foo<'a, F, T>(f: F) -> bool
where
    F: Fn(&'a u8) -> T,
    T: Future<Output = bool>,
{
    f(&32).await
}

fn main() {}

Furthermore, both synchronous and asynchronous code produce the same error. Therefore, I believe that this issue should be addressed for both types of code, rather than just the asynchronous function.

As a result, I intend to propose this matter for discussion at the upcoming async triage meeting, in order to receive feedback on the solution suggested by @eholk for inclusion in the compiler.

I did not check in deep, but this looks like another instance #102201

@rustbot label -AsyncAwait-Triaged +I-async-nominated

@rustbot rustbot added I-async-nominated Nominated for discussion during an async working group meeting. and removed AsyncAwait-Triaged Async-await issues that have been triaged during a working group meeting. labels May 4, 2023
@vincenzopalazzo vincenzopalazzo moved this from In progress (current sprint) to In Progress (long term) in wg-async work Sep 23, 2023
@vincenzopalazzo vincenzopalazzo moved this from In Progress (long term) to In progress (current sprint) in wg-async work Sep 23, 2023
@eholk
Copy link
Contributor

eholk commented Sep 25, 2023

We revisited this in @rust-lang/wg-async triage today. We think the behavior of the compiler is correct, but there's room to improve the error message. We'll spend some more time iterating on what an improved error message would look like.

@eholk eholk added AsyncAwait-Triaged Async-await issues that have been triaged during a working group meeting. and removed I-async-nominated Nominated for discussion during an async working group meeting. labels Sep 25, 2023
@vincenzopalazzo vincenzopalazzo removed their assignment Sep 25, 2023
@traviscross
Copy link
Contributor

Here's the minimal reproduction of this issue:

fn foo<F: Fn(&()) -> T, T>(f: F) {
    f(&());
}

// error: lifetime may not live long enough
fn test() {
    foo(|x: &()| x);
    //      -  - ^ returning this value requires that `'1` must outlive `'2`
    //      |  |
    //      |  return type of closure is &'2 ()
    //      let's call the lifetime of this reference `'1`
}

Playground link

(Thanks to @compiler-errors.)

@compiler-errors compiler-errors self-assigned this Sep 25, 2023
@timvermeulen
Copy link
Contributor

This should point at the bounds in foo and potentially suggest

pub async fn foo<'a, F, T>(f: F) -> bool
where
    F: Fn(&'a u8) -> T,
    T: Future<Output = bool> + 'a,

This restricts foo to only call f with references that are valid for any arbitrary lifetime 'a, would that ever actually be useful? I expect this pattern to mostly arise when the function needs to call the closure with a reference to a local variable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-async-await Area: Async & Await A-diagnostics Area: Messages for errors, warnings, and lints A-lifetimes Area: Lifetimes / regions AsyncAwait-Polish Async-await issues that are part of the "polish" area AsyncAwait-Triaged Async-await issues that have been triaged during a working group meeting. C-bug Category: This is a bug. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
Status: In progress (current sprint)
Development

No branches or pull requests