Expand description
This pass constructs a second coroutine body sufficient for return from
FnOnce
/AsyncFnOnce
implementations for coroutine-closures (e.g. async closures).
Consider an async closure like:
let x = vec![1, 2, 3];
let closure = async move || {
println!("{x:#?}");
};
This desugars to something like:
let x = vec![1, 2, 3];
let closure = move || {
async {
println!("{x:#?}");
}
};
Important to note here is that while the outer closure moves x: Vec<i32>
into its upvars, the inner async
coroutine simply captures a ref of x
.
This is the “magic” of async closures – the futures that they return are
allowed to borrow from their parent closure’s upvars.
However, what happens when we call closure
with AsyncFnOnce
(or FnOnce
,
since all async closures implement that too)? Well, recall the signature:
use std::future::Future;
pub trait AsyncFnOnce<Args>
{
type CallOnceFuture: Future<Output = Self::Output>;
type Output;
fn async_call_once(
self,
args: Args
) -> Self::CallOnceFuture;
}
This signature consumes the async closure (self
) and returns a CallOnceFuture
.
How do we deal with the fact that the coroutine is supposed to take a reference
to the captured x
from the parent closure, when that parent closure has been
destroyed?
This is the second piece of magic of async closures. We can simply create a
second async
coroutine body where that x
that was previously captured
by reference is now captured by value. This means that we consume the outer
closure and return a new coroutine that will hold onto all of these captures,
and drop them when it is finished (i.e. after it has been .await
ed).
We do this with the analysis below, which detects the captures that come from
borrowing from the outer closure, and we simply peel off a deref
projection
from them. This second body is stored alongside the first body, and optimized
with it in lockstep. When we need to resolve a body for FnOnce
or AsyncFnOnce
,
we use this “by-move” body instead.
§How does this work?
This pass essentially remaps the body of the (child) closure of the coroutine-closure to take the set of upvars of the parent closure by value. This at least requires changing a by-ref upvar to be by-value in the case that the outer coroutine-closure captures something by value; however, it may also require renumbering field indices in case precise captures (edition 2021 closure capture rules) caused the inner coroutine to split one field capture into two.