How does the compiler use a state machine and a `label` field to resume a suspend function with multiple suspension points?
answer
- Body sliced into N+1 states
- Integer label = jump table via when(label)
- Set label BEFORE calling the suspending callee
- Check COROUTINE_SUSPENDED, return sentinel to unwind
- resumeWith re-enters same function, reads label
basics
~20 sThe compiler splits the function at each pause point into numbered states. A label counter remembers which state to run next, so when the function resumes it jumps straight to the right spot instead of starting over.
solid answer
~40 sFor a suspend function with N suspension points, the compiler generates a state machine: the body is sliced into N+1 blocks, and an integer `label` field tracks the current state. The whole function becomes a `when (label)` (a jump table). Each suspension point sets `label = next` before calling the suspending callee, then checks whether the callee returned `COROUTINE_SUSPENDED`; if so it returns that sentinel up the chain. When the awaited work completes, the saved continuation's `resumeWith` re-invokes the SAME function, which reads `label`, falls into the right `when` branch, and continues. The result of the previous suspending call arrives via the `resume` path. This compile-time transformation turns sequential-looking suspending code into a resumable, allocation-light state machine — far cheaper than nesting callbacks or spawning threads.
code
kotlin · 8 lines// Two suspension points => states 0,1,2
suspend fun pipeline(): Int {
val a = stepOne() // label set to 1 before this call
val b = stepTwo(a) // label set to 2 before this call
return a + b
}
// Resume re-invokes the function; when(label) jumps to the branch
// after the call that suspended, and 'a'/'b' are read from saved state.go deeper
Understands resume continues where it paused rather than restarting, via a remembered position.
Describes the when(label) jump table, slicing into N+1 states, and the COROUTINE_SUSPENDED check after each call.
Explains why label is set before the call and contrasts the fast path (fall-through) with the slow path (re-entry via resumeWith).
Reasons about how this design keeps suspension allocation-light and tail-call-like, and how it interacts with inlining and exception unwinding.
## Why a state machine A `suspend` function may pause at several points (each call to another suspend function is a *suspension point*). After resuming, execution must continue **exactly where it left off**, with local variables intact. The compiler achieves this by rewriting the function into a **state machine**. ## The `label` field The compiler assigns each suspension point a number and stores the 'current position' in an integer field called `label` (it lives on the continuation object — see the dedicated question on `$continuation`). The whole body becomes a single big `when (label)` switch: ```kotlin // You write suspend fun pipeline(): Int { val a = stepOne() // suspension point 0 val b = stepTwo(a) // suspension point 1 return a + b } // Conceptual state-machine form fun pipeline(c: Continuation<Int>): Any? { val sm = c as PipelineSM // (simplified) when (sm.label) { 0 -> { sm.label = 1 val r = stepOne(sm) if (r == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED sm.a = r as Int // fall through if it ran fast } 1 -> { val a = sm.a // restored local sm.label = 2 val r = stepTwo(a, sm) if (r == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED sm.b = r as Int } 2 -> { /* fall here on resume */ } } return sm.a + sm.b } ``` ## The resume cycle 1. At a suspension point the compiler **first sets `label` to the next state**, then calls the suspending callee passing the continuation. 2. It checks the return value: if it equals `COROUTINE_SUSPENDED`, the function returns that sentinel and unwinds — the thread is freed. 3. When the awaited operation finishes, it calls `continuation.resumeWith(result)`. The continuation's `invokeSuspend` re-enters the SAME function. 4. The function reads `label`, the `when` jumps to the matching branch, and the just-arrived result is read out of the saved state. ## Fast path vs slow path If a callee completes **without** suspending (returns a real value, not the sentinel), control simply falls through to the next branch in the same invocation — no re-entry, no extra allocation of the suspended state on the hot path. Only an actual suspension pays the full cost. ## Key APIs / terms to name - `label` — the integer state cursor. - `when (label)` — the generated jump table. - `COROUTINE_SUSPENDED` — sentinel checked after every suspension point. - `invokeSuspend` / `resumeWith` — the re-entry path. This is a pure compile-time rewrite; there is no interpreter at runtime.
- Why is `label` set to the next state BEFORE the suspending call rather than after it returns?Because if the callee suspends, the function unwinds immediately and never executes any code after the call — so the cursor must already point at the resume target before control can leave.
- What happens if a callee does not actually suspend?It returns a real value instead of COROUTINE_SUSPENDED, so the check fails and execution falls straight through to the next state in the same invocation — the fast path with no re-entry.
Like a 'choose your own adventure' book with a bookmark: the label is the page number you flip to when you pick the story back up.
saying these in an interview costs you the question
- Thinks the function restarts from the top on resume
- Doesn't mention the label/when jump table
- Believes each suspension allocates a thread or blocks
- Sets label after the call instead of before
- Confuses the COROUTINE_SUSPENDED check with a null check