# Execution Model Deft is a stack-based bytecode VM that runs one mutator thread per runtime. Concurrency comes in two tiers: cooperative coroutines that share a heap, and isolated worker runtimes that exchange values by copy. This entry describes the interpreter, the event loop, and the scheduling model, and places them against Smalltalk/Pharo, Tcl, Lua, Node, and BEAM. For the day-to-day API see Concurrency; for allocation and GC internals see Memory Management. ## Four Things Per Runtime A runtime is a self-contained execution world: one VM, one heap, one event loop, one OS thread. TUI panel apps, `defworker` children, `deftask` children, and cluster workers are each a runtime. Values never cross a runtime boundary by reference. Jobs and results are deep-copied through bounded mailboxes, so runtimes share no mutable state and need no locks. That is the opposite trade from Java or Go (one heap shared by every thread) and close to BEAM (each process owns its heap; messages are copied). Node's `worker_threads` sit in between: separate isolates with structured-clone messaging, but shared `ArrayBuffer`s are possible — Deft has no shared-memory escape hatch. ## The Interpreter - Stack-based bytecode, threaded dispatch. Source compiles to a compact opcode set: literals, arithmetic, branches, calls, collection operations, protocol and method definition, and coroutine control. Dispatch is a labeled switch in Zig's computed-goto idiom: every handler tail-jumps with `continue :main_dispatch`, so there is no per-instruction loop-head test and the switch lowers to a jump table. A synthetic `gate` opcode routes to the slow path (errors, fuel, debugger) once it is armed. There is no JIT today. - NaN-boxed values. Every value is one 64-bit word — doubles pass through untagged, everything else is a tagged pointer. Numbers are IEEE doubles: there are no fixnums or bignums. - Late-bound methods. Functions are closures. `call_method` resolves a receiver type plus method name through a type registry with an inline cache, not a class vtable. `defprotocol` specs are registered for validation and `satisfies?`; routing stays name-based. - Non-moving mark-sweep GC. Stop-the-world, no write barriers, per-kind free lists, objects never move — see Memory Management. Pharo's Cog VM is the mirror image: JIT-compiled sends dispatch through class method dictionaries, and the image is the unit of deployment. Deft gives up send purity and JIT throughput for a small interpreter that boots from source and can be embedded per app. ## The Event Loop Each runtime owns a Tcl-style event loop backed by `epoll`, where a single `epoll_wait` unifies I/O readiness and timer expiry. Every iteration processes sources in priority order: 1. file events — fd readiness 2. timer events — `after` 3. coroutines — the ready queue 4. idle handlers — only when nothing else is pending `vwait` drives the loop until no work remains; servers and TUI hosts pump it continuously. This is the same tier as Tcl's event loop and Node's libuv loop; Tcl is the direct ancestor (`vwait`, `after`, and file events are modelled on it). ## Coroutines: Cooperative and Shallow A coroutine is an explicit state machine — `ready`, `running`, `suspended`, `done`, `failed` — parked on the event loop's ready queue. It suspends explicitly with `yield`, `yield --wait ms`, `await`, a mailbox receive, or a direct hand-off to another coroutine. Suspension is one frame deep. A coroutine saves its instruction pointer, locals, and operand-stack slice for its own body frame only. Suspending from inside a function called by the body is not supported by the save format: - a nested `await` or `sleep` does not park the coroutine — it blocks that call in place while pumping the event loop, so other coroutines, timers, and I/O still progress; - a nested bare `yield` is a no-op. The payoff is no async coloring: a helper function may `await` internally and be called from ordinary code (that is how async library code works from sync-shaped callers). The cost is shallow suspension — you cannot preempt or resume a coroutine mid-helper. The op-budget anti-starvation mechanism deserves a caveat. A budget of 100,000 operations per coroutine tick exists, but the forced yield is currently disabled: real work is done in nested calls, and the save format cannot resume a nested frame, so the budget resets and the coroutine keeps running. Runaway coroutines are bounded by the call-depth limit and by the debugger interrupt (below); full continuation save/restore is a known TODO. The one remaining budget yield is the allocation budget, and it too only fires at the coroutine's base frame. This places Deft between two well-known points: | Model | Suspend from | Examples | |---|---|---| | Stackful coroutines | any call depth | Lua, Go, Erlang processes, Java virtual threads | | Stackless + coloring | only the async function's own frame | JavaScript, Python `async`/`await` | | Deft | the coroutine body frame; nested waits block-and-pump | — | Tcl 8.6's `coroutine`/`yield` is stackful as well, implemented over the non-recursive evaluator: the "stack" is heap-allocated invocation records rather than C frames. ## Preemption and the Debugger The VM can also be stopped from outside. Once a debug session is armed, an interrupt flag is checked at instruction boundaries; while a debug server is attached, the interpreter also polls every 4,096 instructions so even a busy computation can be seized. That is genuine instruction-level preemption, but it is not a scheduler: a pause freezes the whole runtime in place (its own coroutines, timers, and I/O stall) and on continue resumes the same flow. It cannot park one computation on a queue and run another in its place. Pharo's green `Process`es are the contrast: preemptive, priority-ordered, and reified as continuations, so the scheduler can switch any process for another at a checkpoint. That policy layer buys responsiveness under misbehaving code (the UI process keeps running while user code loops), fairness without cooperation, simpler application code with no `await` discipline, and live debugging of any process. It costs VM complexity and failure modes (priority inversion, low-priority starvation). Deft trades that for predictability: cooperative coroutines plus the debugger as an emergency brake. See Debug Server. ## Workers and Parallelism Real parallelism means separate runtimes on separate threads: - `deftask` — one-shot child runtime per call; the body is snapshotted, the result arrives as a future or stream. - `defworker` — a persistent file-loaded runtime with its module graph, `@worker` handlers, and worker-local state; `worker/send` dispatches jobs and `worker/stream` streams results. - cluster — N `SO_REUSEPORT` worker runtimes behind the kernel's connection distribution, orchestrated and hot-reloaded by a parent. Values cross through `CrossHeapRing` mailboxes: the sender reserves a slot and writes a value; the receiver deep-copies it into its own heap. Because the GC is non-moving, the receiving thread can safely read a rooted value from the owner's heap while the owner collects, but allocation only ever happens on the owning thread. This is Node's `worker_threads`/`cluster` shape (minus shared memory) and BEAM's copy-message shape (minus per-process preemption). See defworker, Runtimes, and Concurrency. ## Family Tree | Runtime | Concurrency unit | Heap | Scheduling | Parallel? | |---|---|---|---|---| | Deft | coroutine + worker runtime | one per runtime | cooperative; no preemption | workers only | | Pharo | `Process` | one shared image | preemptive, priorities | no | | Tcl | coroutine + thread/interp | one allocator, refcounted | event loop + NRE coroutines | threads with separate interps | | Lua | coroutine | one heap | cooperative | no | | Node | promise / worker | isolate per thread | event loop | worker threads | | BEAM | process | one per process | preemptive | yes | | Go / Loom | goroutine / virtual thread | one shared heap | preemptive | yes | Tcl is the closest single ancestor: event loop, `vwait`, stackful coroutines, per-thread interpreters, and safe interpreters. Lua donated the coroutine syntax; Node supplied the future/worker/cluster vocabulary. The per-runtime GC heap and the kernel sandbox are Deft's own additions — the sandbox is covered in Sandbox. ## Design Notes - Shallow coroutines keep the suspended state small. Saving one frame keeps `ObjCoroutine` compact, keeps GC roots bounded, and makes suspension cheap. Letting nested waits block-and-pump instead of failing is what removes the coloring requirement. - Two tiers answer different problems. Coroutines share a heap so concurrent app logic can touch the same data without ceremony; workers get separate heaps so a runaway or crash-prone computation cannot corrupt the host. - Deliberate omissions: priorities and time quanta, stackful suspension, shared-memory threads, and an image. Each is a coherent design, but each would add VM machinery Deft does not currently need; the debugger interrupt and the allocation budget cover the runaway-code cases. ## Quick Reference | Question | Answer | |---|---| | Is Deft single-threaded? | One mutator thread per runtime; parallelism is separate runtimes | | Do coroutines share memory? | Yes — same heap, no locks needed | | Do workers share memory? | No — values are deep-copied through `CrossHeapRing` mailboxes | | Can a helper function `await`? | Yes — nested waits block-and-pump; no async coloring required | | Can a coroutine suspend in a helper? | No — suspension is one frame deep, at the coroutine body | | What stops one coroutine hogging the loop? | Today: little — the op-budget yield is disabled (TODO); call depth and the debugger bound runaways | | Can the debugger pause any app? | Yes — instruction-boundary interrupts once armed, but it freezes that runtime in place | | Are there priorities? | No — coroutines are FIFO and cooperative |