Skip to content

← Async From First Principles step 5 of 25

Easy Primitives

Countdown futures and the shape of `Pending`

“Pending means try again later” has to become automatic before combinators or executors make any sense. So: write the future, drive it, count the polls.

pub struct Countdown { /* left, polls */ }
pub fn poll_trace(script: Vec<u32>) -> Vec<u32>

For each n in script, build a Countdown that returns Pending exactly n times and then resolves to the total number of polls it received. Drive it with the supplied block_on and collect the results. The trace is therefore script.iter().map(|n| n + 1)Countdown::new(0) finishes on its first poll and returns 1.

What poll is allowed to do

poll gets called, does as much work as it can without blocking, and returns. If the answer is not ready yet it returns Pending and will be called again later. There is no coroutine stack, no suspended thread, no continuation object: the future’s own fields are its saved state. left is the whole of this future’s memory of where it got to.

That is why the counter has to live in the struct rather than in a local. Every poll starts at the top of the function.

The line you are told to write without being told why

Before returning Pending, call

cx.waker().wake_by_ref();

Read it, for now, as “tell whoever is driving me that I am worth polling again”. The block_on you were given ignores it completely — it re-polls no matter what — so your tests pass either way. It is still the right thing to write, and item 16.6 explains what the driver on the other end is supposed to do with it. Item 16.7 gives you a driver that does care, and a future that forgets this line will visibly fail there.

Two traps worth naming

Underflow does not panic here. Submissions are compiled with rustc -O, which turns overflow checks off; debug_assertions is false. So self.left -= 1 when left is already 0 does not abort — it wraps to 4294967295 and your future pends four billion times. Check left == 0 first and return Ready before touching the counter, exactly as the skeleton is shaped. (The given block_on gives up after 100 000 polls and panics, which is how you would find out.)

You are mutating through Pin<&mut Self> and it just works. self.polls += 1 compiles because Countdown holds two u32s and nothing that cares where it lives, which makes it Unpin, which makes Pin<&mut Countdown> implement DerefMut. Pin is not a no-op — it is doing real work for types that are not Unpin — but for this one it costs nothing. That is the whole answer to “why does Pin never seem to matter?”, and item 16.10 unpacks it.

The gate has an opinion about your loop

Building the result with for i in 0..script.len() and then indexing script[i] fires clippy::needless_range_loop, and clippy -D warnings is part of the grade. Iterate over the values, or map and collect. Read the suggestion clippy prints; it names the exact replacement.