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 continuations —
callcc 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.