Skip to content

← Async From First Principles step 5 of 25

Easy Primitives

Countdown futures and the shape of Pending

Implement a future that counts how many times it is polled. For each value in script, create a fresh Countdown, run it with the supplied block_on, and collect its result in input order.

pub fn poll_trace(script: Vec<u32>) -> Vec<u32>

Countdown behavior

Countdown::new(n) starts with left = n and polls = 0. Each call to poll must:

  1. Increment polls.
  2. If left is zero, return Poll::Ready(polls).
  3. Otherwise decrement left, call cx.waker().wake_by_ref(), and return Poll::Pending.

This produces exactly n Pending results followed by Ready(n + 1). For n = 2, the three calls return Pending, Pending, then Ready(3). For n = 0, the first call returns Ready(1).

An empty script returns an empty vector. For example, [0, 3, 1] produces [1, 4, 2]. Keep the supplied driver and implement the Future behavior rather than replacing the exercise with direct addition.

Inputs are in 0..=99_999: the driver allows at most 100,000 polls for each future. A countdown of 99,999 finishes on the final allowed poll. A countdown of 100,000 would still be Pending at that point, so the driver panics. The limit applies separately to each countdown, not to the entire script.

Pending does not schedule itself

poll should return promptly without blocking. Pending means the result is not ready; returning it does not automatically arrange another poll. A future must arrange an appropriate wakeup when it can make progress.

This artificial countdown can make progress immediately on its next poll, so it wakes its own task before returning Pending. A wakeup requests another opportunity to poll; it does not synchronously call poll or guarantee that the next result will be Ready. Real I/O futures usually arrange a wakeup when an external event occurs instead of repeatedly waking themselves.

The supplied teaching driver uses Waker::noop() and polls in a tight loop. It does not wait for wakeups, so its output tests cannot detect a missing wake_by_ref call. The call is still required by this exercise. This driver is deliberately limited and is not a general-purpose async executor.

Stop polling a future after Ready. The Future trait does not promise a useful result from polling a completed future again.

State must survive between calls

In this handwritten poll method, each call starts at the beginning. Keep left and polls in the Countdown fields so their updated values survive the return. A local counter initialized inside poll would reset each time.

Check for zero before subtracting. Subtracting one from zero as u32 can panic when overflow checks are enabled or wrap to 4,294,967,295 when they are not. The daemon currently builds with rustc -O, whose default disables these checks; an explicit compiler option can enable them even in optimized code. Correct zero handling works in both configurations.

Mutating through Pin

The Future trait takes self: Pin<&mut Self>. Countdown contains only u32 fields, so it automatically implements Unpin. That permits safe mutable access through this pinned reference, including updates to both counters. No unsafe code or custom pin projection is needed for this type.

Pin has different implications for types that are not Unpin. The fact that these field updates compile does not mean pinning can always be ignored.

Iterate over values

The starter indexes script through a range loop. Clippy can flag that pattern with needless_range_loop. Iterate over script's values directly, or use an iterator with map and collect. Submissions must compile, pass the output tests and pass Clippy with warnings treated as errors.