Skip to content

← Async From First Principles step 12 of 25

Hard Framework

Hand-compile an async fn into a state machine

Repair a hand-written future that performs the same two steps as the supplied async function. Keep PendOnce, step, pipeline, the boxed child type and compare unchanged. Fix how Pipeline::poll takes and restores its state.

pub async fn pipeline(n: u32) -> u32 {
    let a = step(n).await;
    let b = step(a + 1).await;
    a + b
}

Each step(n) returns Pending once, requests another poll through the waker, then returns 2 * n. Your Pipeline must drive those same children in order and preserve any values needed after they finish.

pub fn compare(inputs: Vec<u32>) -> Vec<(u32, u32)>

For each input, compare returns (async_result, manual_result) in input order. Both results must equal 6 * n + 2. For example, [0, 3] produces [(2, 2), (20, 20)]. Empty input returns an empty vector.

Inputs are between 0 and 715_827_882, inclusive, so the intermediate arithmetic and final result fit in u32. Use the child futures rather than replacing the state machine with the result formula.

What each state remembers

State Work remaining
Start Create the first child from the stored input n.
AwaitingA Poll the first child.
AwaitingB Poll the second child and keep a for the final addition.
Done The result has been returned.

A future must retain what it needs when a poll returns Pending. Here that includes the child future and, in AwaitingB, the value a. Locals that are not retained would otherwise be dropped when the call returns.

This enum is a useful model of an async state machine, not a promise about the compiler's exact representation. Future size depends on retained values, state bookkeeping, layout and optimization. It is not necessarily equal to the size of the largest group of source-level locals.

Take ownership while leaving a valid state

The starter matches directly on this.state and tries to move a boxed child out of it. this is a mutable reference, so this produces E0507: the move would leave a field uninitialized behind that reference.

Use a placeholder to take the old state by value:

match std::mem::replace(&mut this.state, State::Done) {
    // Handle the owned previous state here.
}

This is a pattern to complete inside poll, not a complete match on its own. mem::replace installs Done and returns the old state without dropping it. You can then move its fields into the next state. This is an ownership operation, not an atomic synchronization operation.

On a child's Pending result, restore its state, including the same child and any saved a, before returning Pending. On a child's Ready result, transition forward. Once the second child completes, leave Done in place and return the sum. Keep the supplied panic for a later poll of Done.

Borrowing the state instead of taking it can work in other designs. mem::replace fits this starter because its arms already move owned children between enum variants.

Keep progressing until a child needs to wait

Keep the loop around the match. After Start, poll the first child in the same call. When it finishes, create and poll the second child in that call. Return Pending only when the active child does, forwarding the context so it can request another poll.

Returning Pending just because you changed states can stall a real executor unless another wake is arranged. An earlier wake may already have been consumed; it is not a guarantee of a future poll. Polling counts are not the public contract, but making progress and arranging wakeups are essential.

The supplied driver repeatedly polls with a no-op waker and a 100,000-poll limit. It can complete these children, but can hide a missing-wakeup bug. It is a teaching helper, not a general executor for I/O or timers. The JSON harness checks returned pairs, not wakeup correctness or internal ordering.

Why the children are boxed and pinned

Pin<Box<dyn Future<Output = u32>>> keeps each child at a stable address while allowing its owning pointer to move between variants. With the supplied fields, Pipeline is Unpin, so &mut *self is valid. Keeping a potentially non-Unpin child inline would need a different pin-projection design.

Do not poll a child again after it completes. The general Future contract does not require repeated polling after Ready to work; this exercise's manual future deliberately panics in its completed state.