AI authored to showcase ironpad capabilities.
A gen block is an iterator the compiler writes for you. You write straight-line code with yield in it; rustc compiles it to a state machine that implements Iterator directly, so for loops, .collect(), and every adapter work unchanged. The price is a single rule: no borrow may live across a yield.
The feature is nightly-only (gen_blocks, RFC 3513); ironpad injects the gate automatically when a cell uses the block form, so every cell below runs as-is. This notebook pairs with Pin, coroutines, and structs that point at themselves: that one covers the machinery Pin exists for, this one covers the flavor of coroutine built to avoid needing Pin at all.
112 values, peak 9232
The block above compiled to a struct. Every local that survives a yield became a field (n: u64 here), each yield became an exit point, and the result implements Iterator<Item = u64> with no trait ceremony on your side. The 112 values and the 9232 peak came out of .collect() and .max(), the same calls you would make on any iterator.
Compare the signatures. Iterator::next takes &mut self. Coroutine::resume, the general mechanism underneath, takes Pin<&mut Self>. An ordinary &mut means the value can move between calls, and the next cell proves a started gen block survives exactly that.
Some(1) then [2, 3]
Some(1) then [2, 3]: the block yielded once, moved to a new binding, and kept going. is_unpin compiling is the type-level statement of the same fact. Every gen block implements Unpin, so nothing about it ever needs pinning.
A state machine is only dangerous to move when one of its fields points at another, the self-referential shape the pin-coroutines notebook is about. gen blocks rule that shape out at compile time. The cell below tries to smuggle one in.
error[E0626]: borrow may still be in use when gen block yields. The compiler is pointing at the suspension: at the first yield, first still has a use coming, so the suspended state would have to hold both owned and a pointer into owned. Resume after a move and that pointer dangles. gen blocks keep their movability by refusing to create the state at all.
Both escapes are mechanical: own the data (.to_string() the slice), or finish every use of the borrow before the yield. The next cell does both.
["hello", "hello world"]
The same borrow is legal in a coroutine, with one keyword. A plain #[coroutine] closure obeys the identical rule and fails with the identical E0626; adding static lifts it. The static form is allowed to build the self-referential state because it gives up movability: it no longer implements Unpin, and resume demands Pin<&mut Self> as proof you will not move it again.
That is the real division in Rust's coroutine design. Movable state machines (gen blocks, plain coroutine closures) ban borrows across suspension points; static ones allow them and route every use through Pin. async blocks sit in the static camp, which is why Future::poll pins.
[11, 5]
Reach for a gen block anywhere you would otherwise hand-roll a struct with an Iterator impl. The run-length encoder below is 19 lines inside a function returning impl Iterator; the equivalent struct carries the cursor, the pending run, and a next full of edge cases, at about twice the length. The state machine is the same either way; the difference is who writes it.
Reach for a coroutine when you need what Iterator cannot express: resume arguments (data flowing in at each suspension), a completion value distinct from the yielded ones, or the self-referential state above. The stabilization outlook differs too: gen has an accepted RFC and a keyword already reserved in edition 2024, while #[coroutine] has been unstable since the 2017 eRFC with no stabilization path.
[('a', 3), ('b', 1), ('c', 4), ('d', 2)]Iterator::next takes no arguments, so a gen block is a one-way pipe: values come out and nothing goes back in. Coroutine::resume takes one, and yield is an expression whose value is whatever the next resume hands it.
The closure parameter binds the first resume argument, and every later yield evaluates to the argument of the call that woke it. That makes the tally below a fold with its accumulator parked across suspension points, stepped one deposit at a time from the caller's side.
[100, 105, 85, 92]
What gen blocks do not give you today: resume arguments, a meaningful completion value (the block finishes with ()), or recursion without boxing. async gen blocks exist behind the same feature for yielding from async code. All of it is nightly; the keyword is reserved, and the iterator you get back is ordinary.
A gen block is the movable, Unpin, plain-Iterator coroutine, and it keeps those properties by banning borrows across yield. E0626 is the compiler telling you your state machine just became self-referential. Either restructure (own the data, or shorten the borrow) or step up to a static coroutine and let Pin carry the proof.