Module rustc_mir_transform::coroutine::by_move_body

source ·
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:

#![feature(async_closure)]

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 .awaited).

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.

Structs§