writeonce/runtime/README.md
shoney.arickathil c0b0dbb846 docs: audit all markdown against the code, fix findings, flatten status folders
- README: shipped concurrency/HTTP/WebSockets sat in the roadmap as "not yet
  available"; "no package manager" contradicted [deps]; the deps example
  would not have compiled (the key IS the module name)
- runtime/README: leads with wovm, wo-rt.c demoted to a historical section;
  dropped 2 nonexistent recipes, crates/rt, @gc refcounting, 13 suites -> 18
- employee + log-watcher READMEs claimed "does not compile"; both are gates
- error catalog: +10 emitted codes incl WO-E250, the only diagnostic the
  shipped query surface raises; recorded why the sweep rotted
- language-surface: group-by parses, then the typechecker refuses it
- 00-code-review + 00-link-audit re-run; history kept, not rewritten
- 48 dead Rust-era exploration links de-linked rather than re-pointed (their
  prose names the retired plan by number); successor map -> discarded.md
- 08-project-structure: compiler/plan/ never existed; corpus has 9 dirs, 5 empty
- releasing.md: dropped a --draft step the workflow never had
- new docs/00-doc-audit.md: findings + disposition, incl one row where the
  audit was wrong and the doc it accused was right
- status folders removed: 34 stories flat, status only in frontmatter; 252
  links recomputed from resolved paths; board/board-views/structure retaught
- story 24 -> in-progress, since frontmatter is now the only truth
- new iteration 38: fs mutation verbs + net.connect, the two capability
  families no iteration owned
- new iteration 39: gofiber/fiber v3.5.0 parity study. The ledger called
  CSRF/sessions unblocked by iteration 34's HMAC, but the runtime has no
  source of randomness at all
- linkcheck skips .dev/.superpowers: 0 broken paths, 0 bad anchors

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 19:20:22 +02:00

218 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# `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`](../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/`](../database/src/CODE-LOGIC.md)) is
statically linked into every `wovm` and every test binary — one binary, no
separate database process.
Reasoning under the code: [`src/CODE-LOGIC.md`](src/CODE-LOGIC.md). Normative
contracts: [`docs/plan/oop-vm/00-wob-format.md`](../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`](../docs/plan/oop-vm/08-builtin-surface.md) (what each
builtin means in source terms).
> `wo-rt.c` in 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
```bash
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,
`switch` under `-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 (`@gc` is 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. `wovm` pumps the collector to quiescence after the
entry returns (`WO_GC_BUDGET` steps per call, default 64; `WO_GC_TRACE=1`
prints 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 with `try`/`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** — `ICALL` binary-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 catchable `WO_T_ACTOR` trap on overflow, `call` parking 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, the
`Float`/`Bytes` bridges, base64, and the digests `sha1`/`sha256`/`hmac_sha256`.
`WO_B_MAX` is 95. Database builtins reach the linked engine directly; the
legacy `DB_STUB` opcode survives only for images emitted against no engine.
- **CLI contract** — `wovm app.wob`: exit 0 = ran; exit 1 = trap, one stderr
line `trap CODE in METHOD at line N: MESSAGE`; exit 2 = usage/load failure.
`wovm --version` prints `wovm <VERSION>`. `WO_HEAP_MB` overrides the 64 MiB
arena; `WO_DATA` opts into durability; `WO_SHARDS` sets the shard count. Run
with no `.wob` argument, `wovm` checks 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`):
1. **wovm: debug hello.wob** — rebuilds `wovm` at `-O0 -g`, regenerates
fixtures, breaks anywhere in the VM.
2. **wovm: debug a .wob file** — same, prompts for the image path.
3. **wovm: debug unit test (ISO dispatch)** — pick one of the ASan test
binaries (already `-g`), step through it.
4. **wo-rt: debug server (1 shard)** — the retired event-loop reference at
`-O0 -g`, `WO_THREADS=1` so 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_*`, plain `switch`) where `next`/`step` behave normally.
For `wovm` itself, build
`make -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]`, `pc`
already rewound to the faulting instruction). `vm_unwind` is 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=1` makes 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.wob` forces early `T_OOM` paths; exit
codes 0/1/2 are stable for scripting.
- **gdb without VS Code:** `gdb --args ./wovm build/hello.wob`, or
`gdb ./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`](../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:
```bash
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`](../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`](../docs/plan/exploration/c-runtime/01-architecture.md)
(the runtime traced through one memory address, plus seven improvement
proposals),
[`c-runtime/02-single-binary.md`](../docs/plan/exploration/c-runtime/02-single-binary.md)
(how a single binary runs on this runtime),
[`blue-green-vm/00-vision.md`](../docs/plan/exploration/blue-green-vm/00-vision.md)
(port-free transports, fibers, embedded source, two-VM hot swap), and the
kernel-primitive cards under
[`exploration/linux/`](../docs/plan/exploration/linux/00-linux.md).