We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Async From First Principles step 12 of 25
Hand-compile an `async fn` into a state machine
You have been told that an async fn compiles to a state machine. This is
where you write one by hand and check that it behaves identically.
Given, and unchangeable:
pub async fn step(n: u32) -> u32 { /* pends once, then n * 2 */ }
pub async fn pipeline(n: u32) -> u32 {
let a = step(n).await;
let b = step(a + 1).await;
a + b
}
Write a Pipeline struct with an explicit enum State and an
impl Future<Output = u32> that does the same thing, then:
pub fn compare(inputs: Vec<u32>) -> Vec<(u32, u32)>
drives both versions for every input and returns the pairs. Every pair must be equal.
The transformation
An async fn body is cut at its suspension points. Everything between two
.awaits becomes one arm of a state machine, and every local that is still
alive across an .await has to be stored in the future itself, because the
stack frame is gone the moment poll returns.
pipeline has two suspension points, so it needs four states:
- Start — nothing has run yet.
-
AwaitingA —
step(n)is in flight. Nothing else is live. -
AwaitingB —
step(a + 1)is in flight, andais live, because the final line needs it. So this variant carriesa. - Done — completed; polling again is a contract violation.
Look at AwaitingB carrying a. That is the whole reason a future has a
size, why a big async fn is expensive to move, and why an async fn that
holds a MutexGuard across an .await keeps that lock held — the guard is
literally a field of the state machine. One picture, several mysteries
retired.
The compiler does not literally build this enum; rustc lowers async through
MIR coroutines. But it is conceptually equivalent, and the size rule carries
over exactly: a future is as large as its largest state.
Two things the tests will catch
poll must loop. When the child in AwaitingA returns Ready, you move
to AwaitingB — and you must then poll the new child in the same call,
not return Pending and wait to be polled again. A single match with no
surrounding loop produces a future that needs extra polls to finish. Since
step wakes itself, the driver will re-poll and the answer eventually comes
out right, but you will have written a future that does strictly more work
than the compiler’s. Loop.
Do not poll a child after it returned Ready. Each state owns exactly one
child and leaves it behind when it transitions.
The error you are meant to meet
The starter matches on self.state directly:
match this.state {
State::AwaitingA(mut fut) => ...
}
E0507: cannot move out of this.state as enum variant AwaitingA which is
behind a mutable reference. You have a &mut Pipeline, not a Pipeline.
Moving the boxed future out of the enum would leave this.state holding
garbage, and if poll panicked in the middle you would have a half-destroyed
value. The borrow checker will not have it.
Matching on &mut this.state gets you references, but then you cannot rebuild
a different variant out of the pieces you are borrowing. The idiomatic
answer is the one every real state machine uses:
match std::mem::replace(&mut this.state, State::Done) {
mem::replace swaps a placeholder in and hands you the old value by
value, atomically as far as the type system is concerned. There is never a
moment when this.state is invalid. You now own the variant, can take the
boxed child out of it, and must remember to write the new state back — including
on the Pending path, where you put the same variant back unchanged.
That “leave a valid placeholder behind” move is worth internalising on its
own. It is how you take ownership out of a struct you only borrow, and
mem::take is the same trick when the placeholder can be Default::default().
Keep the children boxed
Store them as Pin<Box<dyn Future<Output = u32>>> behind a type alias — that
keeps Pipeline Unpin, so &mut *self works, and it keeps
clippy::type_complexity quiet. Storing an async fn future inline here would
demand pin projection, and that is item 16.16.
Do not try to make the two versions poll the same number of times. rustc’s
state machine is free to differ. Assert on outputs and ordering, which is what
compare does.
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.