SBBE has no built-in coroutine or async support. Coroutines are implemented by the frontend through state machine lowering: the frontend transforms each coroutine into a regular function that takes a state struct and a switch on the current state to resume at the right point.

The pattern

Every coroutine becomes:

  1. A state struct in linear memory holding the current state index and any local variables that live across yield points
  2. A resume function that dispatches on the state index using jmp.if chains (or jmpt once branch tables are supported)

Example: a counter generator

Pseudo-code

func countUp(start: Int32) -> Generator<Int32> {
    var i = start
    while true {
        yield i
        i += 1
    }
}

State struct layout

// CounterState:
//   offset 0: i32 state_index (0 = initial, 1 = after yield)
//   offset 4: i32 i (the counter value)
//   offset 8: i32 yielded_value (output slot)

SBBE lowering

// Initialize the coroutine state
func $count_up_init(i32) -> ptr {
    var $state ptr

entry:
    ldi 12
    salloc
    str $state

    // state_index = 0
    ld $state
    ldi 0
    stm i32

    // i = start parameter
    ld $state
    ldi 4
    add.s i64
    ldl 0
    stm i32

    ld $state
    ret
}

// Resume the coroutine. Returns 1 if a value was yielded, 0 if finished.
func $count_up_resume(ptr) -> i32 {
entry:
    // Load state_index
    ldl 0
    ldm i32

    // Dispatch on state
    eqz i32
    jmp.if state_0
    jmp state_1

state_0:
    // First entry: yield the initial value of i
    // Load i
    ldl 0
    ldi 4
    add.s i64
    ldm i32

    // Write to yielded_value slot
    ldl 0
    ldi 8
    add.s i64
    swap
    stm i32

    // Set state_index = 1 for next resume
    ldl 0
    ldi 1
    stm i32

    ldi 1              // yielded = true
    ret

state_1:
    // Resumed after yield: increment i, yield again
    // Load i
    ldl 0
    ldi 4
    add.s i64
    dup                // keep the addr for storing back
    ldm i32
    ldi 1
    add.s i32          // i + 1
    dup                // keep value for yielded_value

    // Store i back
    stm i32

    // Write to yielded_value slot
    ldl 0
    ldi 8
    add.s i64
    swap
    stm i32

    // state_index stays at 1 (loop forever)
    ldi 1              // yielded = true
    ret
}

// Read the last yielded value
func $count_up_value(ptr) -> i32 {
entry:
    ldl 0
    ldi 8
    add.s i64
    ldm i32
    ret
}

Using the generator

func $main() -> i32 {
    var $gen ptr
    var $sum i32

entry:
    // Create generator starting at 1
    ldi 1
    call $count_up_init
    str $gen

    ldi 0
    str $sum

    // Yield 3 values and sum them: 1 + 2 + 3 = 6
    jmp iter

iter:
    ld $gen
    call $count_up_resume
    drop               // we know it always yields for this example

    ld $gen
    call $count_up_value
    ld $sum
    add.s i32
    str $sum

    // Check if we've summed 3 values (sum >= 6)
    ld $sum
    ldi 6
    ge.s i32
    jmp.if done
    jmp iter

done:
    ld $sum
    ret
}

Async/await

Async functions follow the same state machine pattern. The main difference is that instead of yielding a value, the coroutine yields a “pending” status and the runtime scheduler decides when to resume it.

The state struct for an async function includes:

The resume function is called by the async runtime whenever the awaited resource becomes ready. SBBE provides the raw mechanism (function pointers via calli, state structs in memory); the runtime scheduler is implemented in the frontend or as an extern library.

Key takeaways