Stackless

← Technical Notes
Io

Io's iterative evaluator, continuations, and coroutines.

About

Historically Io evaluated messages by recursive descent in C — every Io call nested a real C frame. That model is simple, but it couples the language to the host platform in ways that become painful as the VM grows: the depth of Io recursion is bounded by the host’s C stack; capturing the in-flight computation requires copying raw stack memory; coroutine switching needs platform-specific assembly (setjmp/longjmp, ucontext, Windows fibers); and targets like WebAssembly don’t expose the native stack at all.

A C-stackless evaluator moves all evaluation state into heap-allocated frames. The C code becomes a flat loop that pops the current frame, steps its state machine, and pushes new frames for sub-expressions. The C stack stays shallow — one loop iteration deep — regardless of how deep the Io call graph gets.

Why it matters

  • First-class continuationscallcc becomes a simple matter of snapshotting the frame chain. No makecontext, no copying raw C stack bytes, no fragile reassembly. Continuations become ordinary Io objects that can be stored and invoked later.
  • Portable coroutines — switching coroutines is just swapping which frame chain the eval loop is running. No platform-specific assembly, no ucontext, no fibers. The same C code works on macOS, Linux, Windows, ARM64, and WASM.
  • Tail-call optimization — reusing the current frame instead of pushing a new one is trivial when frames are explicit heap objects. Deep tail-recursive Io programs no longer blow the host stack.
  • Robust exception unwinding — raising an exception becomes a loop that pops frames until a handler is found. No longjmp hopping over C code that expected to run cleanup, and no reliance on C++-style unwinding.
  • Resumable exceptions — because the raising frame is a live, inspectable object, a handler can choose to resume the computation at the point of the raise instead of unwinding past it. This lets you model Smalltalk- or Common-Lisp-style condition systems where the handler decides whether to retry, substitute a value, or abort.
  • Serializable execution state — the full active computation — frame chain, local slots, pending messages — is a reachable graph of Io objects. It can be serialized to disk or sent over the network, then deserialized and resumed in another process, letting a running task migrate hosts, checkpoint and recover, or survive a VM restart.
  • WASM compatibility — WebAssembly intentionally hides the native call stack, which makes classical coroutine implementations impossible. A C-stackless design ports cleanly because it never depended on the C stack in the first place.
  • Introspection and debugging — the entire call stack is a reachable chain of Io objects. A debugger can walk it, print it, modify it, or resume from any point, without needing to reach into native memory.

The cost is some throughput relative to a tightly coded recursive evaluator — the loop carries more explicit state than a C call frame would. In return, Io gets a single, portable, inspectable runtime with capabilities that would be impractical otherwise.

Stackless Evaluator Report

Design and status notes on the heap-allocated frame-based evaluator.

View →

Stackless VM Examples

Patterns enabled by the heap-allocated frame-based evaluator: portable coroutines, TCO, frame introspection, and robust exception handling.

View →