rust-lang / rust

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

Tracking issue for incorrect lifetime bound errors in async #110338

Open tmandry opened 1 year ago

tmandry commented 1 year ago
### Reported issues
- [ ] #71723
- [ ] #79648
- [ ] #82921
- [ ] #90696
- [ ] #64552
- [ ] #87425
- [ ] #92415
- [ ] #92096
- [ ] #102211
- [ ] #60658
- [ ] #64650
- [ ] #71671
- [ ] #110339
- [ ] #104382
- [ ] #111105
- [ ] https://github.com/rust-lang/rust/issues/114046
- [ ] https://github.com/rust-lang/rust/issues/124757
- [ ] https://github.com/rust-lang/rust/issues/126551
- [ ] https://github.com/rust-lang/rust/issues/126550
- [ ] https://github.com/rust-lang/rust/issues/89976
- [ ] https://github.com/rust-lang/rust/issues/126044
- [ ] https://github.com/rust-lang/rust/issues/126350
- [ ] https://github.com/rust-lang/rust/issues/114177
- [ ] https://github.com/rust-lang/rust/issues/130113

Cause

We erase lifetime relations of types inside generators, making it impossible in some cases to prove bounds or normalize associated types.

See https://github.com/rust-lang/rust/issues/64552#issuecomment-533267241 for a more detailed explanation.

Implementation history

Notes

Please keep discussion in this thread at a "meta-level" and open new issues for code that does not compile.

These might not all have the same cause, though it seems plausible that they do. My primary motivation is to try to group all the related issues together so they're easier to keep track of. If we can split them into sub-groups, all the better.

Many of the issues in this list are cribbed from #92449, thanks to @compiler-errors for coming up with that list.

compiler-errors commented 1 year ago

For the record, the approach laid out in https://github.com/rust-lang/rust/issues/64552#issuecomment-533335959 is not sufficient.

It's essentially what I attempted in #92449, which did not fix all of the issues listed, especially after I actually made it (kinda) sound in "attempt 2" in the PR by using placeholder types rather than region infer vars. Some additional discussion lives in https://rust-lang.zulipchat.com/#narrow/stream/131828-t-compiler/topic/Generator.20interior.20auto.20traits.

It's also not clear that the approach in my PR is even sound (https://github.com/rust-lang/rust/pull/92449#discussion_r795823087). Any attempt to fix this issue should probably run their approach by T-types before wasting much time on it, unless you want to be like me and waste a ton of time on it :sweat_smile:

compiler-errors commented 1 year ago

These might not all have the same cause, though it seems plausible that they do.

Yes and no.

They all have the same "root cause", which is that we erase lifetime information from generator witnesses, which causes us to no longer be able to prove certain lifetime requirements that are needed to prove that generators implement certain auto traits.

This is a very general problem, and does not have a single solution to fix it, IMO. That's because the way that these lifetime requirements actually come about are incredibly diverse, though. Revisiting exactly how each of these issues manifests will be necessary for any solution to this issue.

tmandry commented 1 year ago

Thanks for your comments @compiler-errors, very helpful!

That's because the way that these lifetime requirements actually come about are incredibly diverse, though. Revisiting exactly how each of these issues manifests will be necessary for any solution to this issue.

Do you think sorting issues into camps like "failed normalization", "requirement from a trait impl" would help? (I guess a single error might satisfy multiple of these.)

safinaskar commented 1 year ago

Attempt to simulate async lambdas using lambdas, which return futures, leads to lifetime errors, as explained in this Niko's post: https://smallcultfollowing.com/babysteps/blog/2023/03/29/thoughts-on-async-closures/

emersonford commented 1 year ago

It wasn't immediately clear to me how async + generators + auto traits + HRTBs tie together to result in this bug, so wanted to give a shot at documenting it for folks not as familiar with rustc's type internals as this seems to somewhat common in async programming that requires Send futures (and please correct me if I'm wrong!).

Async fns are effectively desugared into generators, so in something like

fn foo() -> impl Future<Output = i64> { // effectively the same signature as `async fn foo() -> i64 { ... }`
    async {
        ready(10).await
    }
}

the impl Future<Output = i64> opaque return type will be resolved as some concrete generator type (similar to the SumGenerator struct in the blog post). You can see this in this playground using the "Show HIR" functionality.

So when you want the future to be send (e.g. using -> impl Future<Output = i64> + Send as the return type or having T: Future<Output = i64> + Send as the parameter type as found in tokio::spawn), you need to prove that the opaque generator type is also Send. Since Send is an auto trait, the type checker will automatically try to prove that the opaque generator type is Send by inspecting the internals of the generator. The bug occurs at this step, specifically when some internal value of a generator requires a trait implementation with some lifetime bound. For example, using @danielhenrymantilla's example here:

trait Trait { type Assoc; }

struct Foo<T : Trait>(T::Assoc);

impl Trait for fn(&'static ()) {
    type Assoc = ();
}

fn main() {
    let sendable_future: &dyn Send = &async {
        let s = Foo::<fn(&'static ())>(());
        async{}.await;
    };
}

taking it step by step:

  1. To prove that the opaque generator type for sendable_future is Send, you need to prove the internals are also Send, i.e. you need to prove that s is Send.
  2. To prove a given struct is Send, all of its member fields must be Send. Thus for Foo, you must prove that T::Assoc is Send.
  3. In order to do this though, you must first prove the T: Trait bound of struct Foo<T: Trait>(...);. Thus the first step of proving Send for s = Foo::<fn(&'static ())>(()) is proving that Trait is implemented for fn(&'static ()).
  4. However, lifetimes in generator interiors are currently being erased(?), so after region replacement, the type checker attempts to prove the bound for<'a> fn(&'static ()): Trait. This obviously doesn't hold, so you end up with the error message
    = note: `fn(&'0 ())` must implement `Trait`, for any lifetime `'0`...
    = note: ...but `Trait` is actually implemented for the type `fn(&'static ())`

You run into the same issue with something like

struct Foo<T>(T);

unsafe impl Send for Foo<&'static str> {}

fn main() {
    let sendable_future: &dyn Send = &async {
        let s = Foo::<&'static str>("");
        async{}.await;
    };
}

which gives you the implementation of 'Send' is not general enough error message as the type checker attempts to prove for<'a> Foo<&'static str>: Send.

So one non-trivial fix here is to fix step 4 and not erase lifetimes inside of generator interiors.


What's not immediately clear to me though is why this is so specific to auto traits + generators. Even if you don't need to prove Send for the generator, don't you still need to prove that Trait is implemented for fn(&'static ()) inside of the generator interior? I.e., why does

use std::future::Future;

trait Trait { type Assoc; }

struct Foo<T : Trait>(T::Assoc);

impl Trait for fn(&'static ()) {
    type Assoc = ();
}

fn main() {
    let f: &dyn Future<Output = ()> = &async {
        let s = Foo::<fn(&'static ())>(());
        async{}.await;
    };
}

compile just fine?

emersonford commented 1 year ago

In terms of workarounds:

For something like

trait Trait { type Assoc; }

struct Foo<T : Trait>(T::Assoc);

impl Trait for fn(&'static ()) {
    type Assoc = ();
}

fn main() {
    let sendable_future: &dyn Send = &async {
        let s = Foo::<fn(&'static ())>(());
        async{}.await;
    };
}

you can insert a drop like so:

trait Trait { type Assoc; }

struct Foo<T : Trait>(T::Assoc);

impl Trait for fn(&'static ()) {
    type Assoc = ();
}

fn main() {
    let sendable_future: &dyn Send = &async {
        let s = Foo::<fn(&'static ())>(());
        std::mem::drop(s);
        async{}.await;
    };
}

and enable -Zdrop-tracking, which will omit checking Send for s as part of the generator Send check (since drop tracking will correctly identify that the generator does not hold s across yield points).


For something like

let _: &dyn Send = &async {
    let _it = [()].iter().map(|it| None::<()>).flatten();
    async {}.await;
};

you should collect the iterator, e.g.

let _: &dyn Send = &async {
    let collect = [()].iter().map(|it| None::<()>).flatten().collect::<Vec<_>>();
    async {}.await;
};