4. Coroutines

Generators as a State Machine

midcompiler-internalsdata-structures

A generator function is, underneath the yield keyword, a state machine: CPython's compiler captures the function's local variables and "where it paused" into an object (a frame) that can be resumed later -- literally what Objects/genobject.c implements in C. This exercise builds the state-machine equivalent of one specific generator, by hand, with no yield at all:

def handshake():
    name = yield "AWAITING_NAME"
    yield f"HELLO {name}"
    ack = yield "AWAITING_ACK"
    return "DONE" if ack == "ACK" else "RETRY"

Calling .send(value) on a real generator resumes it -- the paused yield expression evaluates to value -- and runs until the NEXT yield. When the generator instead hits a return, .send() raises StopIteration whose .value attribute holds the returned value -- this is genuinely how a generator communicates its final result to whatever's driving it.

Implement HandshakeStateMachine.send(value) so it reproduces this exact sequence of behaviors, without a yield anywhere in your code.

The missing link: "state" has to become a real, explicit value

A single accumulator variable isn't a state machine -- it's the same computation every time you call it. A REAL generator's power is that different calls to .send() can do completely different things, because the generator is paused at a different yield each time. Since there's no yield keyword to pause at here, something has to explicitly track "which of the four points in handshake() am I at" -- that's self._state, and it's doing the exact job CPython's real generator frame does with a saved bytecode instruction pointer.

Example

sm = HandshakeStateMachine()
sm.send(None)     # -> "AWAITING_NAME"     (must prime with None first, like next(gen))
sm.send("Ada")    # -> "HELLO Ada"
sm.send(None)     # -> "AWAITING_ACK"
sm.send("ACK")    # raises StopIteration("DONE")

Constraints

  • The very first call must be send(None) -- exactly like a fresh generator, which must be primed with next(gen) (equivalent to send(None)) before it will accept a real value.
  • On the 4th call, raise StopIteration with .value set to "DONE" or "RETRY" depending on whether value == "ACK" -- don't just return a plain value, since nothing would ever see it.
  • Your class's behavior must be indistinguishable, call for call, from driving the real generator above with the same sequence of values -- the test checks this directly against Python's own generator as ground truth.

Real-world grounding

  • CPython's generator object implementation, including how return value inside a generator becomes StopIteration(value) outside it: Objects/genobject.c

Real generator frames also need to save/restore the full local variable set, the exact bytecode instruction pointer, and exception state -- self._state (plus whatever you stash on self between calls, like the captured name) is a much smaller version of exactly that same idea: state that persists across calls because it's stored on an object instead of a call stack that unwinds when a function returns.

hints
handshake_sm.pysign in to run
Loading...
test result

This is a preview. Sign in to run this exercise against the real tests.