Open tmandry opened 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:
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.
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.)
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/
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:
sendable_future
is Send
, you need to prove the internals are also Send
, i.e. you need to prove that s
is Send
.Send
, all of its member fields must be Send
. Thus for Foo
, you must prove that T::Assoc
is Send
.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 ())
.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?
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;
};
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.