More precise tracking for generators (including async functions) is planned (e.g. Tracking issue for more precise coroutine captures · Issue #69663 · rust-lang/rust · GitHub and more).
However, thanks to a conversation on Stack Overflow, I realized that this feature is at odds with another potential feature, so we need to decide which one we prefer.
An issue that has been brought up several times is that the following code doesn't compile, even though it is completely sound:
use std::rc::Rc;
async fn foo() {}
async fn bar() {
let rc = Rc::new("hello");
foo().await;
&rc;
}
fn require_send<T: Send>(_: T) {}
fn main() {
require_send(bar());
}
The problem is that Rc
, a non Send
type, is held across an .await
point. However, Rc
specifically is not problematic; it is true that it cannot be moved between threads, but only if there is no clones left in the original threads. Async blocks will never leave a clone in the original thread, so they can safely hold a Rc
across .await
points.
The suggested fix is to have some another auto trait, let's call it SendNoEscape
, that is implemented for Rc
, and not implemented for, say, MutexGuard
.
All of this is already known, and probably already discussed (whether we really want an additional auto trait, is it worth it etc.). However, an important enlightening that I had due to the abovementioned Stack Overflow discussion, is that the following statement is true:
While values created in async block can safely use SendNoEscape
, values captured by async block (including async fn parameters) cannot, as they can leave copies in the caller.
This raises the question: How does the compiler differentiate between the two?
Syntactically, it's very easy. But semantically, a value created in the async block can originate from a value captured by the async block, and the compiler can't tell. So, this leads us to the following understanding:
If any value captured by the async block is !Send
, the async block must be !SendNoEscape
, even if this value is dropped before the first .await
point.
Today, this is always true. The only way to make a value considered dropped early for generator computation is by using blocks, and captured values cannot be enclosed in blocks.
However, with the precise capturing effort, this is no longer true. So if precise capturing is implemented (and stabilized), supporting SendNoEscape
will become a breaking change, which means it cannot happen.
This means we must decide, and decide now, before we stabilize precise capturing: is there any chance we will ever want to support SendNoEscape
? If yes, precise capturing cannot be implemented.
There is also a middle ground: implement precise capturing, except for captured values. We can even not store them in the generator, but make it "as if" they are held wrt. auto traits.