skip to content

How does the compiler use a state machine and a `label` field to resume a suspend function with multiple suspension points?

level: middleimportance: must knowfreq 60%

answer

  1. Body sliced into N+1 states
  2. Integer label = jump table via when(label)
  3. Set label BEFORE calling the suspending callee
  4. Check COROUTINE_SUSPENDED, return sentinel to unwind
  5. resumeWith re-enters same function, reads label

basics

~20 s

The 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 s

For 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
kotlin
// 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

for a junior

Understands resume continues where it paused rather than restarting, via a remembered position.

for a middle

Describes the when(label) jump table, slicing into N+1 states, and the COROUTINE_SUSPENDED check after each call.

for a senior

Explains why label is set before the call and contrasts the fast path (fall-through) with the slow path (re-entry via resumeWith).

for a principal

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

context