Task 3 of docs/superpowers/plans/2026-08-26-table-residency.md. - NO LAYOUT CHANGE. The plan said to add descriptor fields; the descriptor already had a `flags` u32 with only bit0 used, so both properties ride spare bits (WO_CLASSF_VOLATILE 0x02, WO_CLASSF_RESIDENT_KEYS 0x04). A v7 class record is byte-identical in shape to a v6 one, which is a much smaller and safer change than the plan assumed - both spelled as the NON-default, so a zero flags word means exactly what every pre-v7 image meant: durable, every row resident. A non-@table class has both clear by construction - the loader refuses the meaningless pair (bit1+bit2) independently of woc, on the standing principle that what the loader accepts the interpreter trusts. Verified by FORGING the flags word in an otherwise valid image, since woc will not emit one: flags=6 gives "durable:false with resident:keys", flags=8 still gives "unknown flags" - WOB_VERSION 6 -> 7. Kept because an OLDER runtime reading a v7 image would otherwise treat a volatile table as durable and quietly disagree with its own source. loader.c's check is exact-match, so a v6 image is refused rather than read with the bits clear — verified by patching a v7 header back down to 6 GAP FOUND AND CLOSED: woc ACCEPTED `durable: false, resident: keys`. Task 1's steps covered duplicates and bad values but never the combination, and the plan had only put that refusal in the loader. The spec wants both, so the compiler now refuses it too (WO-E102, checked after the argument list is complete since it is a property of the pair). A compile error is the one a developer can act on. VERSION DRIFT: the constant lives in FOUR places, not one. wob.h, emit.ml:157, disasm.ml:186, and compiler/test/runner.ml:2405 — the last is a deliberately independent reimplementation of the loader battery, and it caught the drift as 14 failures rather than silently passing. Its flags mask and the combination refusal are now in sync too, which is the point of it being independent rather than shared. Gates: woc-test 557/0, 18 runtime suites 0 fail, 18 ISO-flavour suites 0 fail, cli_smoke OK, oop-e2e 118/0, employee 8/0, db-actor 8/0, site 21/0. Zero goldens moved (git diff over golden/ empty). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| bench | ||
| src | ||
| test | ||
| Makefile | ||
| README.md | ||
| wo-rt.c | ||
runtime/ — wovm, the writeonce bytecode VM
The C11 register VM that loads and runs .wob images. Sibling of the OCaml
woc compiler (compiler/README.md): woc emits the
image, wovm executes it, and woc build appends an image to a copy of this
binary to produce one self-contained executable. libc only, direct syscalls, no
libraries.
The embedded database engine (database/) is
statically linked into every wovm and every test binary — one binary, no
separate database process.
Reasoning under the code: src/CODE-LOGIC.md. Normative
contracts: docs/plan/oop-vm/00-wob-format.md
(format, opcodes, builtin ids — src/wob.h is its machine-readable twin) and
08-builtin-surface.md (what each
builtin means in source terms).
wo-rt.cin this directory is not part of that toolchain. It is the retired io_uring event-loop reference prototype the runtime's design was read off, kept for reading. See the "Historical" section at the bottom of this page — nothing in the shipped build compiles it.
Build, test
just wovm-build # -> runtime/wovm (the release binary)
just wovm-test # unit suites, both dispatch flavors, + CLI smoke, ASan+UBSan
# in runtime/ directly:
make wovm # the release binary
make test # unit suites, computed-goto dispatch
make test-iso # the same suites under -DWO_ISO_C (plain switch)
make wovm-asan # a separate sanitized binary, for corpus fixtures needing a leak/UB proof
make wovm-tsan # thread-sanitized, for the fiber/actor demos
Across both halves of the toolchain: just oop-e2e (the woc + wovm
conformance corpus) and just oop-accept (the full milestone gate — compile-time
budget, corpus under ASan, single-binary smoke, both unit suites, one command).
Shipped features
- Register interpreter — fixed 32-bit instructions, Lua-style window-overlap
calls (callee r0 = caller slot A), dual dispatch: computed goto under GNU C,
switchunder-DWO_ISO_C. Both flavors are gated so neither rots. - Owned objects with a runtime borrow word — shared-reader count /
exclusive sentinel in every 16-byte header; violations trap
T_BORROW. The compiler elides provable sites; the VM enforces the residual ones. - Inferred GC, incremental tri-color mark-sweep — GC-ness is a compiler
inference, never an annotation (
@gcis rejected outright, WO-E104). Per-shard traced list, snapshot-at-beginning roots, Yuasa deletion barrier, budgeted mark and sweep slices — no stop-the-world by construction. Reference counting and the Bacon–Rajan trial-deletion collector that shipped in iteration 2 were both removed by iteration 7b.wovmpumps the collector to quiescence after the entry returns (WO_GC_BUDGETsteps per call, default 64;WO_GC_TRACE=1prints one stderr line per step). - Deterministic drops — kind-directed drop plans (scalar/owned/gcref/text/ bytes/float/multi/map), recursive over class fields and container elements.
- Trap unwinding that never leaks — per-method drop tables (pc → owned/gc
register masks); a trap walks every frame and frees what was live; structured
error
{code, line, method, msg}via line tables, catchable withtry/catch. - Validating loader — bounds-checked parse, aligned copies, const-string interning, full static validation (opcodes, registers, indexes, jump targets, terminators, builtin arity, call windows, sorted vtables). What the loader accepts, the interpreter trusts — no UB on any input.
- Structural interfaces —
ICALLbinary-searches sorted (class, slot, method) vtable triples by receiver class. - Fibers and shard actors — reduction-budget preemption, pinned per-core
shards, ownership-move message sends, bounded mailboxes (
WO_MAILBOX, default cap 1024) with a catchableWO_T_ACTORtrap on overflow,callparking the caller for a typed scalar reply, and actor death that traps callers rather than hanging them. Blocking stdlib calls park the fiber; the shard runs someone else. - The systems stdlib and the engine — six module namespaces (
fs,time,env,net,proc,json) plus the free builtins: text and containers, theFloat/Bytesbridges, base64, and the digestssha1/sha256/hmac_sha256.WO_B_MAXis 95. Database builtins reach the linked engine directly; the legacyDB_STUBopcode survives only for images emitted against no engine. - CLI contract —
wovm app.wob: exit 0 = ran; exit 1 = trap, one stderr linetrap CODE in METHOD at line N: MESSAGE; exit 2 = usage/load failure.wovm --versionprintswovm <VERSION>.WO_HEAP_MBoverrides the 64 MiB arena;WO_DATAopts into durability;WO_SHARDSsets the shard count. Run with no.wobargument,wovmchecks its own trailer for an appended image (woc build's single-binary output) — a recognized-but-corrupt trailer fails clearly on exit 2, never a crash.
Interpreter ceilings, all in src/wob.h: 64 registers per frame
(WO_MAX_REGS), a 4096-slot value stack (WO_STACK_SLOTS), 256 frames
(WO_MAX_FRAMES), 64 shards (WO_MAX_SHARDS). The image format is at
WOB_VERSION 6 (iteration 36's bitwise opcodes 42–46 moved it last).
File map
| File | What it is |
|---|---|
src/wob.h |
the .wob contract: opcodes, field kinds, trap codes, builtin ids, limits, the 16-byte object header |
src/obj.h/.c |
arena allocator (16-byte size-class free lists ≤ 1024 B, malloc above), wo_rt context, object creation, wo_str |
src/borrow.h/.c |
the borrow word: acquire shared/exclusive, unconditional releases |
src/cont.h/.c |
native containers: growable multi, linear-scan map (content-compared text keys) — deliberate KISS, and O(n) per lookup |
src/gc.h/.c |
the kind-directed drop dispatcher plus the incremental tri-color mark-sweep for traced objects |
src/loader.h/.c |
.wob parse + full static validation + the mmap file path |
src/vm.h/.c |
the interpreter: dispatch, frames, traps, drop-map unwinding, ICALL, shard/engine startup |
src/builtin.h/.c |
the builtin dispatcher — the single entry point the interpreter calls, forwarding to sysio.c and json.c |
src/sysio.c |
the OS half: fs, time, env, net, proc |
src/json.c |
json.encode / json.decode, driven by class metadata |
src/crypto.c/.h |
SHA-1, SHA-256, HMAC-SHA256 (iteration 34), vector-verified |
src/park.c/.h |
the park plane: raw io_uring ABI (no liburing) and the epoll fallback, fiber parking, per-call deadlines |
src/main.c |
the wovm CLI: find an image (argument or embedded trailer), build argv, call the entry, map its result to an exit code, pump the collector |
test/t.h |
20-line assert harness (no framework) |
test/wob_build.h/.c |
in-memory .wob assembler — a second, independent encoding of the format, so builder/loader disagreements fail tests |
test/test_*.c |
18 suites, one binary each, ASan+UBSan: arena, borrow, builtin, cont, crypto, cycle, fiber, icall, loader, mailbox, obj, objops, rc, table, unwind, vm, wal, wobbuild |
test/mkwob.c |
fixture generator for the CLI smoke |
test/cli_smoke.sh |
end-to-end exit-code/stderr-shape check |
Note where io_uring is and is not: src/park.c drives it for the fiber and
network plane, while the WAL commit path in database/src/wal.c is still a plain
per-commit fdatasync. Moving the WAL onto the rings is iteration 23.
Debugging
VS Code: .vscode/launch.json ships four configs (needs the C/C++ extension,
ms-vscode.cpptools):
- wovm: debug hello.wob — rebuilds
wovmat-O0 -g, regenerates fixtures, breaks anywhere in the VM. - wovm: debug a .wob file — same, prompts for the image path.
- wovm: debug unit test (ISO dispatch) — pick one of the ASan test
binaries (already
-g), step through it. - wo-rt: debug server (1 shard) — the retired event-loop reference at
-O0 -g,WO_THREADS=1so one shard owns everything.
How to work on the VM under a debugger:
- Step the ISO flavor, not the computed-goto one. The goto interpreter jumps
label-to-label and single-stepping is disorienting. Test binaries have an ISO
twin (
build/iso_test_*, plainswitch) wherenext/stepbehave normally. Forwovmitself, buildmake -B wovm CFLAGS='-O0 -g -std=c11 -DWO_ISO_C'. break vm_trap— one breakpoint catches every trap at the moment of failure, with the trapping frame intact (vm->frames[vm->depth-1],pcalready rewound to the faulting instruction).vm_unwindis the next frame down if you're chasing a leak-on-trap.- Other load-bearing breakpoints:
wo_load_buf(validation rejects),recv_check(residual field checks),wo_builtin(all builtins),wo_gc_step(mark-sweep slices). - ASan under gdb:
ASAN_OPTIONS=abort_on_error=1makes the first report SIGABRT so the debugger stops on it with the full stack; without gdb the report alone usually names the exact free you missed. - CLI knobs:
WO_HEAP_MB=1 ./wovm app.wobforces earlyT_OOMpaths; exit codes 0/1/2 are stable for scripting. - gdb without VS Code:
gdb --args ./wovm build/hello.wob, orgdb ./build/iso_test_unwind.
Historical: wo-rt.c
A single-file C server that was the runtime-layer reference prototype —
built to find out which kernel primitives a writeonce runtime should stand on,
by writing them with no abstraction in the way. Its design conclusions are what
src/park.c and the shard model implement. It is not part of the toolchain, no
gate builds it, and it shares no code with wovm.
It reached phase F of its own plan
(docs/plan/exploration/c-runtime/00-plan.md,
phases A→F all shipped): WO_THREADS pinned threads, each owning a raw io_uring
ring (io_uring_setup + mmap'd SQ/CQ rings + io_uring_enter, no liburing), its
own SO_REUSEPORT listener with multishot accept, its own keep-alive
connections, and its own slice of one mlock'd mmap arena — shared-nothing, no
locks, one io_uring_enter per loop tick in steady state. Writes followed the
dual-write order: RAM apply, framed WAL record to a per-shard fallocate'd log,
one group-commit fdatasync per tick, HTTP ack only after the fsync
completes. Boot replayed each shard's snapshot + WAL tail in parallel before
any accept armed; ./wo-rt wal-check <file> validated a log offline.
Build and poke it, if you want to read it running:
make -C runtime wo-rt # cc -O2 -Wall -Wextra -std=c11 -pthread, no libraries
./runtime/wo-rt # 127.0.0.1:8085 (WO_PORT=9000 WO_THREADS=4 to override)
Its measured numbers, kept as the historical record they are — bench client
bench/bench.c (keep-alive, only 2xx counted), Go reference in bench/goref/,
20-core Linux 6.14, tmpfs data dir. These are not wovm numbers; the
shipped VM's measurements live in bench/baseline.json and
docs/plan/perf-targets.md.
| Benchmark | wo-rt-c (8 shards, durable WAL, io_uring group commit) | Go net/http (go1.25.1, 20 cores, no durability) |
|---|---|---|
GET /healthz |
859,033 req/s · p50 71 µs · p99 159 µs | 336,444 req/s · p50 70 µs · p99 1,277 µs |
GET / (JSON) |
671,312 req/s · p99 180 µs | — |
POST write (tmpfs) |
618,343 commits/s — fsync-acked · p99 194 µs | 320,516 req/s — RAM only, no WAL · p99 1,581 µs |
| 10,000 idle conns | 0 errors | 0 errors |
Honest caveats as recorded then: net/http does full general-purpose HTTP and
this parser was minimal; tmpfs makes fsync nearly free, so the durable column
flatters itself. ACID probes: three kill -9 rounds mid-bench at ~2M commits all
showed WAL records ≥ acked; 300 concurrent commits → 300 distinct ids; torn-tail
records dropped whole by CRC. The crash-under-load test found two real bugs the
lighter phase-D test missed — an ack-before-fsync race and an fd-reuse ABA hazard
in ack parking — which is what the phase existed for.
Deliberate simplifications it never outgrew: single-shot RECV re-armed per
request, one outstanding SQE per connection, fixed-size buffers, naive "title"
extraction instead of a JSON parser, no timerfd. Requires kernel ≥ 5.19
(multishot accept).
Design docs it fed:
c-runtime/01-architecture.md
(the runtime traced through one memory address, plus seven improvement
proposals),
c-runtime/02-single-binary.md
(how a single binary runs on this runtime),
blue-green-vm/00-vision.md
(port-free transports, fibers, embedded source, two-VM hot swap), and the
kernel-primitive cards under
exploration/linux/.