feat(runtime): wovm VM core (Iteration 2)
- 16 tasks complete: arena, object model, borrow word, containers, RC + budgeted cycle collector, wob_build, validating loader, interpreter core (dual dispatch), object opcodes, drop-map unwinding, builtins + DB_STUB + TRAP, ICALL, wovm CLI + just recipes - 13 test suites × 2 dispatch flavors (ASan+UBSan) + CLI smoke, all green - .wob v1 format pinned in src/wob.h + docs/plan/oop-vm/00-wob-format.md - wo-rt.c reference event-loop preserved for sub-project 2 This is Iteration 2 of the OOP milestone; compiler front (Iteration 3) is in progress on this branch. They meet at Iteration 4 (emitter+e2e).
This commit is contained in:
parent
2171b2d94b
commit
bab88a53b9
42 changed files with 5191 additions and 0 deletions
59
runtime/Makefile
Normal file
59
runtime/Makefile
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
CC ?= cc
|
||||||
|
CFLAGS ?= -O2 -Wall -Wextra -std=c11
|
||||||
|
LDFLAGS ?= -pthread
|
||||||
|
|
||||||
|
wo-rt: wo-rt.c
|
||||||
|
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
|
||||||
|
|
||||||
|
# ---- wovm VM core (src/) + unit tests (test/) ----
|
||||||
|
# Each test/test_*.c builds into its own ASan+UBSan binary linked against
|
||||||
|
# every src/*.c except main.c; `make test` runs them all.
|
||||||
|
VMSRC := $(filter-out src/main.c,$(wildcard src/*.c))
|
||||||
|
VMHDR := $(wildcard src/*.h) $(wildcard test/*.h)
|
||||||
|
TESTS := $(wildcard test/test_*.c)
|
||||||
|
TESTBIN := $(TESTS:test/%.c=build/%)
|
||||||
|
TCFLAGS := -std=c11 -Wall -Wextra -Werror -g -O1 \
|
||||||
|
-fsanitize=address,undefined -fno-omit-frame-pointer
|
||||||
|
|
||||||
|
build:
|
||||||
|
mkdir -p build
|
||||||
|
|
||||||
|
TESTHELP := $(wildcard test/wob_build.c)
|
||||||
|
|
||||||
|
build/%: test/%.c $(VMSRC) $(TESTHELP) $(VMHDR) | build
|
||||||
|
$(CC) $(TCFLAGS) -Isrc -Itest -o $@ $< $(TESTHELP) $(VMSRC)
|
||||||
|
|
||||||
|
test: $(TESTBIN)
|
||||||
|
@for t in $(TESTBIN); do echo "== $$t"; ./$$t || exit 1; done
|
||||||
|
|
||||||
|
# ISO dispatch flavor (switch instead of computed goto) — gated in every
|
||||||
|
# run so the fallback can never rot
|
||||||
|
ISOBIN := $(TESTS:test/%.c=build/iso_%)
|
||||||
|
|
||||||
|
build/iso_%: test/%.c $(VMSRC) $(TESTHELP) $(VMHDR) | build
|
||||||
|
$(CC) $(TCFLAGS) -DWO_ISO_C -Isrc -Itest -o $@ $< $(TESTHELP) $(VMSRC)
|
||||||
|
|
||||||
|
test-iso: $(ISOBIN)
|
||||||
|
@for t in $(ISOBIN); do echo "== $$t"; ./$$t || exit 1; done
|
||||||
|
|
||||||
|
# the wovm binary (plain optimized build; the test suite is the ASan gate)
|
||||||
|
wovm: src/main.c $(VMSRC) $(VMHDR)
|
||||||
|
$(CC) $(CFLAGS) -Isrc -o $@ src/main.c $(VMSRC)
|
||||||
|
|
||||||
|
# fixture generator for the CLI smoke test
|
||||||
|
build/mkwob: test/mkwob.c test/wob_build.c $(VMHDR) | build
|
||||||
|
$(CC) $(CFLAGS) -Isrc -Itest -o $@ test/mkwob.c test/wob_build.c
|
||||||
|
|
||||||
|
bench/bench: bench/bench.c
|
||||||
|
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
|
||||||
|
|
||||||
|
bench: bench/bench
|
||||||
|
|
||||||
|
run: wo-rt
|
||||||
|
./wo-rt
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f wo-rt bench/bench wovm
|
||||||
|
rm -rf build
|
||||||
|
|
||||||
|
.PHONY: bench run clean test test-iso
|
||||||
136
runtime/README.md
Normal file
136
runtime/README.md
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
# `wo-rt-c` — the writeonce runtime environment, in C
|
||||||
|
|
||||||
|
A single-file C implementation of the **runtime layer** the writeonce language runs on — now at **phase E: a durable RAM database**. Writes follow the dual-write order — RAM apply, framed WAL record (`len|crc32|payload|COMMIT`) to a per-shard `fallocate`'d log, one group-commit `fdatasync` per loop tick, **HTTP ack only after the fsync completion**. Boot performs the **first load, hard drive → RAM**: each shard replays its snapshot + WAL tail into its arena slice in parallel before any accept arms; clean shutdown snapshots each slice and truncates the WAL; a `meta` file pins the shard count so a mismatched `WO_THREADS` refuses to boot. `./wo-rt wal-check <file>` validates a log offline. `WO_THREADS` pinned threads (default = online cores), each owning its own raw io_uring ring (`io_uring_setup` + mmap'd SQ/CQ rings + `io_uring_enter` — **no liburing**), its own `SO_REUSEPORT` listener (multishot accept), its own keep-alive connections, and its own slice of the one mlock'd mmap arena — shared-nothing, no locks. Steady state is one `io_uring_enter` syscall per loop tick. Thread 0 owns the `signalfd`; shutdown broadcasts through per-thread `eventfd`s, both watched via `POLL_ADD` SQEs. **Zero dependencies beyond libc + kernel uapi headers.** The kernel is the runtime.
|
||||||
|
|
||||||
|
This is the runtime-layer sibling of [`prototypes/wo-db/`](../prototypes/wo-db/) (the C++ query-layer prototype): a reference card showing, with no abstraction in the way, exactly which kernel primitives the production Rust runtime (`crates/rt/`) drives through `libc`. Same role, different layer.
|
||||||
|
|
||||||
|
```
|
||||||
|
prototypes/wo-db/ C++ what the LANGUAGE executes (parser, engine, transactions)
|
||||||
|
runtime/ C what the RUNTIME stands on (epoll, signalfd, sockets)
|
||||||
|
crates/rt/ Rust the product — both layers, libc only
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Moved from `prototypes/wo-rt-c/` to root-level `runtime/`** (monorepo layout, per the OOP compiler + VM design spec). `wo-rt.c` is untouched — it stays the event-loop reference described below. The **`wovm` bytecode VM now lives under `runtime/src/`** — see [The wovm bytecode VM](#the-wovm-bytecode-vm-runtimesrc) for what shipped and [Debugging](#debugging) for how to step through it.
|
||||||
|
|
||||||
|
## Build, run, poke
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make # cc -O2 -Wall -Wextra -std=c11 -pthread — no libraries
|
||||||
|
./wo-rt # 127.0.0.1:8085 (WO_PORT=9000 WO_THREADS=4 ./wo-rt to override)
|
||||||
|
|
||||||
|
curl localhost:8085/ # {"runtime":"wo-rt-c","loop":"epoll-et","threads":4,
|
||||||
|
# "shard":2,"shard_requests":[68,36,44,53]}
|
||||||
|
curl -X POST localhost:8085/api/notes -d '{"title":"hello"}'
|
||||||
|
# {"id":2,"title":"hello","shard":1} ← ids interleave per shard
|
||||||
|
curl localhost:8085/api/notes # the connection's shard only — shared-nothing
|
||||||
|
# ctrl-C → signalfd on shard 0 → eventfd broadcast → all shards join
|
||||||
|
```
|
||||||
|
|
||||||
|
Each connection hashes to one shard for life (`SO_REUSEPORT` 4-tuple): a list may land on a different shard than the create that preceded it. That is the architecture, not a bug — cross-shard reads are a later phase / design decision (see the [architecture doc's improvements](../docs/plan/exploration/c-runtime/01-architecture.md)).
|
||||||
|
|
||||||
|
Or from the repo root: `just rt-c-demo`.
|
||||||
|
|
||||||
|
## Module map
|
||||||
|
|
||||||
|
Every block in `wo-rt.c` corresponds one-to-one to a module of the Rust runtime, which in turn mirrors Go's netpoller — the same lineage the docs trace:
|
||||||
|
|
||||||
|
| `wo-rt.c` block | Rust (`crates/rt/src/`) | Go (`.dev/reference/go/src/runtime/`) | Kernel reference card |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `main` event loop (`epoll_create1` / `epoll_wait`, `EPOLLET`) | `runtime/netpoll_epoll.rs` | `netpoll_epoll.go` | [`linux/01-epoll.md`](../docs/plan/exploration/linux/01-epoll.md) |
|
||||||
|
| `sig_setup` (`sigprocmask` + `signalfd`) | `runtime/signalfd.rs` | signal mask handling | [`linux/04-signalfd.md`](../docs/plan/exploration/linux/04-signalfd.md) |
|
||||||
|
| `listener_bind` (`SOCK_NONBLOCK`, `accept4`-to-EAGAIN) | `http/listener.rs` | `net.Listen` + accept loop | `socket(7)` |
|
||||||
|
| `conn_drive` (read-to-EAGAIN, one buffer per fd) | `http/connection.rs` | `conn.Read` loop | the edge-triggered contract |
|
||||||
|
| `notes[]` store | `engine.rs` (BTreeMaps) | — | [`03-inmemory-engine.md`](../docs/runtime/database/03-inmemory-engine.md) |
|
||||||
|
|
||||||
|
## What it demonstrates
|
||||||
|
|
||||||
|
- **One thread owns each shard outright.** Accept, parse, store, respond — no locks, no worker pool, no connection migration. Scaling past one core is more shards ([`09-concurrency-scaleout.md`](../docs/plan/09-concurrency-scaleout.md)), never shared mutable state. The single cross-thread touch is the relaxed-atomic stats counters on `/` — monotonic, never on the data path.
|
||||||
|
- **Edge-triggered discipline.** Every registration sets `EPOLLET`; every readiness event is drained to `EAGAIN` (the accept loop and the read loop both). Get this wrong and connections silently hang — the reason the Rust module documents the same contract at the top of `netpoll_epoll.rs`.
|
||||||
|
- **Signals as fd events.** `SIGINT`/`SIGTERM` are blocked, then read from a `signalfd` on the same epoll — no async-signal-unsafe handler, no self-pipe trick.
|
||||||
|
- **RAM is the read path.** `GET /api/notes` touches a C array. The production engine is the same idea with MVCC and a WAL behind it.
|
||||||
|
|
||||||
|
## Architecture and roadmap
|
||||||
|
|
||||||
|
Documentation lives under `docs/` (repo convention) — this README stays here as the directory's orientation page only:
|
||||||
|
|
||||||
|
- [`docs/plan/exploration/c-runtime/01-architecture.md`](../docs/plan/exploration/c-runtime/01-architecture.md) — the runtime defined by tracing **one memory address** through user space, kernel space, and hardware under a million concurrent connections, plus seven improvement proposals (seqlock reads, registered buffers, zero-copy send, SQPOLL, …).
|
||||||
|
- [`docs/plan/exploration/c-runtime/00-plan.md`](../docs/plan/exploration/c-runtime/00-plan.md) — the phase sequence: **A → B → C → D → E → F, all ✅ shipped.**
|
||||||
|
- [`docs/plan/exploration/blue-green-vm/00-vision.md`](../docs/plan/exploration/blue-green-vm/00-vision.md) — where the runtime goes next: port-free transports, fibers, source embedded in the binary, agent-managed source over MCP, and the Blue/Green two-VM hot-swap deployment model.
|
||||||
|
|
||||||
|
## Measured (phase F, 20-core Linux 6.14, tmpfs data dir, `just rt-c-bench`)
|
||||||
|
|
||||||
|
Same C bench client (`bench/bench.c`, keep-alive, only 2xx counted) against both servers:
|
||||||
|
|
||||||
|
| Benchmark | **wo-rt-c** (8 shards, durable WAL, io_uring + group commit) | **Go `net/http`** (go1.25.1, 20 cores, no durability) | **Rust `wo`** (release, 8 shards, durable WAL, io_uring group commit)¹ |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `GET /healthz` | **859,033 req/s** · p50 71 µs · p99 159 µs | 336,444 req/s · p50 70 µs · p99 1,277 µs | 746,340 req/s · p50 73 µs · p99 186 µs |
|
||||||
|
| `GET /` (JSON) | **671,312 req/s** · p99 180 µs | — | 692,671 req/s · p99 167 µ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 | 330,285 commits/s — fsync-acked · p99 360 µs |
|
||||||
|
| `POST` write (real ext4/NVMe) | — | — | **27,014 commits/s group commit vs 5,765 per-commit (4.7×)** · p50 2.2 ms |
|
||||||
|
| 10,000 idle conns | 0 errors | 0 errors | 0 errors |
|
||||||
|
|
||||||
|
¹ All numbers measured on a clean box with the same client (earlier parasite-contaminated runs superseded). The Rust column's history is the architecture roadmap, measured: 09a global mutex (74.9k writes/s) → 09b sharded engine (+51%) → 09c per-shard WAL (~1% durability cost) → **keep-alive: reads ×3.4 to 770k/s, durable writes ×1.9 to 331k/s, p99 under 350 µs everywhere**. Rust now beats Go on both columns *while fsyncing every write*, and sits within ~10% of the C prototype on reads — converging exactly as the same-architecture argument predicted. The one remaining C advantage is **group commit on io_uring** (one batched fsync + one syscall per tick vs per-commit fsync over epoll), which is the next port. Bonus finding: with `/tmp` accidentally full, the C runtime **refused to ack non-durable writes under ENOSPC** — the durability guarantee holding in an unplanned failure mode.
|
||||||
|
|
||||||
|
wo-rt-c on 8 cores outpaces Go on 20 with ~8× tighter p99 (Go's GC shows there) — while fsyncing every write Go doesn't. Honest caveats: `net/http` does full general-purpose HTTP; our parser is minimal; .NET was not installed on the box. **ACID under load:** three crash rounds (`kill -9` mid-bench at ~2M commits) all showed WAL records ≥ acked; isolation probe: 300 concurrent commits → 300 distinct ids; torn-tail records drop whole by CRC.
|
||||||
|
|
||||||
|
The crash-under-load test **found and fixed two real bugs** the lighter phase-D test missed: an ack-before-fsync race (`conn_continue` armed the send in the same tick the commit was staged) and an fd-reuse ABA hazard in ack parking (fixed with per-connection generation stamps). That is what phase F is for.
|
||||||
|
- [`docs/plan/exploration/c-runtime/02-single-binary.md`](../docs/plan/exploration/c-runtime/02-single-binary.md) — the end goal: how the `wo build` **single binary** runs on this runtime environment — the runtime kernel is statically linked into every writeonce app (Go model, nothing to install), with the catalog/routes/bytecode payload consumed at boot.
|
||||||
|
|
||||||
|
## Deliberate simplifications
|
||||||
|
|
||||||
|
Single-shot RECV re-armed per request (multishot recv + buffer rings are a phase-F improvement), one outstanding SQE per connection, fixed-size buffers, naive `"title"` extraction instead of a JSON parser, no `timerfd`. Requires kernel ≥ 5.19 (multishot accept). This file is for reading; `crates/rt` is for running writeonce.
|
||||||
|
|
||||||
|
## The wovm bytecode VM (runtime/src/)
|
||||||
|
|
||||||
|
Milestone 1 of the OOP track (spec: [`docs/superpowers/specs/2026-08-01-oop-compiler-vm-design.md`](../docs/superpowers/specs/2026-08-01-oop-compiler-vm-design.md), plan 1: [`docs/superpowers/plans/2026-08-01-wob-format-and-vm-core.md`](../docs/superpowers/plans/2026-08-01-wob-format-and-vm-core.md)) — a register VM that executes `.wob` bytecode (format: [`docs/plan/oop-vm/00-wob-format.md`](../docs/plan/oop-vm/00-wob-format.md)) with the full milestone-1 memory model. C11, libc only, same doctrine as `wo-rt.c`.
|
||||||
|
|
||||||
|
**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 gated in CI 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 (hybrid model, spec §4).
|
||||||
|
- **`@gc` reference counting + budgeted cycle collection** — rc at zero frees immediately; possible cycles buffer as candidates (Bacon–Rajan trial deletion), collected in budgeted epochs per shard — no stop-the-world by construction.
|
||||||
|
- **Deterministic drops** — kind-directed drop plans (scalar/owned/gcref/text/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, method, line, message}` via line tables.
|
||||||
|
- **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.
|
||||||
|
- **Native containers + builtins** — `multi`/`map` with element-kind tags; `now/print/print_int/words/multi_*/map_*`; `DB_STUB` traps "engine not linked" until the DB engine binds (plan 5).
|
||||||
|
- **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. `WO_HEAP_MB` overrides the 64 MiB arena.
|
||||||
|
|
||||||
|
**File map:**
|
||||||
|
|
||||||
|
| File | What it is |
|
||||||
|
| --- | --- |
|
||||||
|
| `src/wob.h` | the `.wob` v1 contract: opcodes, field kinds, trap codes, builtin ids, limits, the 16-byte object header (machine-readable twin of the format doc) |
|
||||||
|
| `src/obj.h/.c` | arena allocator (16-byte size-class free lists ≤ 1024 B, malloc above), `wo_rt` context, object creation, strings |
|
||||||
|
| `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) |
|
||||||
|
| `src/gc.h/.c` | kind-directed drop dispatcher, rc inc/dec, cycle-candidate buffer, budgeted Bacon–Rajan collector |
|
||||||
|
| `src/loader.h/.c` | `.wob` parse + full static validation + mmap file path |
|
||||||
|
| `src/vm.h/.c` | the interpreter: dispatch, frames, traps, drop-map unwinding, `ICALL` |
|
||||||
|
| `src/builtin.h/.c` | builtin dispatcher |
|
||||||
|
| `src/main.c` | the `wovm` CLI |
|
||||||
|
| `test/t.h` | 20-line assert harness (no framework) |
|
||||||
|
| `test/wob_build.h/.c` | in-memory `.wob` assembler — the second, independent encoding of the format; builder/loader disagreements fail tests |
|
||||||
|
| `test/test_*.c` | 13 suites, one binary each, ASan+UBSan |
|
||||||
|
| `test/mkwob.c` | fixture generator for the CLI smoke |
|
||||||
|
| `test/cli_smoke.sh` | end-to-end exit-code/stderr-shape check |
|
||||||
|
|
||||||
|
**Gates:** `just wovm-build` · `just wovm-test` (unit suites + ISO flavor + CLI smoke, all ASan-clean) · in `runtime/`: `make test`, `make test-iso`, `make wovm`.
|
||||||
|
|
||||||
|
## 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** — pick one of the 13 ASan test binaries (already `-g`), step through it.
|
||||||
|
4. **wo-rt: debug server** — the 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` (cycle collection epochs).
|
||||||
|
- **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`.
|
||||||
201
runtime/bench/bench.c
Normal file
201
runtime/bench/bench.c
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
/*
|
||||||
|
* bench.c — load client for wo-rt-c (phase F). Zero deps beyond libc.
|
||||||
|
*
|
||||||
|
* T threads, each driving ONE keep-alive connection in a tight request/
|
||||||
|
* response loop (TCP_NODELAY, full-response framing via Content-Length).
|
||||||
|
* Every request is latency-stamped; the run prints req/s, p50, p99, errors.
|
||||||
|
*
|
||||||
|
* usage: ./bench <host> <port> <threads> <seconds> <path> [post-json]
|
||||||
|
* read: ./bench 127.0.0.1 8085 64 5 /api/notes
|
||||||
|
* write: ./bench 127.0.0.1 8085 64 5 /api/notes '{"title":"bench"}'
|
||||||
|
* idle: ./bench 127.0.0.1 8085 10000 0 /healthz # seconds=0: open conns,
|
||||||
|
* # one request each, hold, exit
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <netinet/tcp.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/resource.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#define MAX_SAMPLES 400000 /* per thread; counting continues past it */
|
||||||
|
|
||||||
|
static char g_req[2048];
|
||||||
|
static int g_reqlen;
|
||||||
|
static struct sockaddr_in g_addr;
|
||||||
|
static long g_deadline_us; /* 0 = idle-connection mode */
|
||||||
|
static int g_hold_secs;
|
||||||
|
|
||||||
|
struct worker {
|
||||||
|
pthread_t tid;
|
||||||
|
long reqs, errs;
|
||||||
|
long ok2xx, non2xx; /* honest accounting: only 2xx is success */
|
||||||
|
long *lat; /* µs samples */
|
||||||
|
int nlat;
|
||||||
|
};
|
||||||
|
|
||||||
|
static long now_us(void) {
|
||||||
|
struct timespec ts;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||||
|
return ts.tv_sec * 1000000L + ts.tv_nsec / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int conn_open(void) {
|
||||||
|
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
if (fd < 0) return -1;
|
||||||
|
int one = 1;
|
||||||
|
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof one);
|
||||||
|
if (connect(fd, (struct sockaddr *)&g_addr, sizeof g_addr) < 0) { close(fd); return -1; }
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* One request/response round-trip. Returns HTTP status, or -1 conn-dead. */
|
||||||
|
static int round_trip(int fd) {
|
||||||
|
size_t off = 0;
|
||||||
|
while (off < (size_t)g_reqlen) {
|
||||||
|
ssize_t n = write(fd, g_req + off, (size_t)g_reqlen - off);
|
||||||
|
if (n <= 0) { if (n < 0 && errno == EINTR) continue; return -1; }
|
||||||
|
off += (size_t)n;
|
||||||
|
}
|
||||||
|
static _Thread_local char buf[131072];
|
||||||
|
size_t got = 0, need = 0;
|
||||||
|
for (;;) {
|
||||||
|
ssize_t n = read(fd, buf + got, sizeof buf - 1 - got);
|
||||||
|
if (n <= 0) { if (n < 0 && errno == EINTR) continue; return -1; }
|
||||||
|
got += (size_t)n;
|
||||||
|
buf[got] = 0;
|
||||||
|
if (!need) {
|
||||||
|
char *he = strstr(buf, "\r\n\r\n");
|
||||||
|
if (!he) { if (got >= sizeof buf - 1) return -1; continue; }
|
||||||
|
size_t hdr = (size_t)(he + 4 - buf);
|
||||||
|
long cl = 0;
|
||||||
|
char *p = strcasestr(buf, "Content-Length:");
|
||||||
|
if (p) cl = strtol(p + 15, NULL, 10);
|
||||||
|
need = hdr + (size_t)cl;
|
||||||
|
}
|
||||||
|
if (got >= need) {
|
||||||
|
int code = 0;
|
||||||
|
sscanf(buf, "HTTP/1.1 %d", &code);
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
if (got >= sizeof buf - 1) return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void *worker_main(void *arg) {
|
||||||
|
struct worker *w = arg;
|
||||||
|
int fd = conn_open();
|
||||||
|
if (fd < 0) { w->errs++; return NULL; }
|
||||||
|
|
||||||
|
if (g_deadline_us == 0) { /* idle-connection mode */
|
||||||
|
if (round_trip(fd) > 0) w->reqs++; else w->errs++;
|
||||||
|
sleep((unsigned)g_hold_secs);
|
||||||
|
close(fd);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (now_us() < g_deadline_us) {
|
||||||
|
long t0 = now_us();
|
||||||
|
int code = round_trip(fd);
|
||||||
|
if (code < 0) { /* reconnect once, then count errs */
|
||||||
|
close(fd);
|
||||||
|
fd = conn_open();
|
||||||
|
if (fd < 0) { w->errs++; break; }
|
||||||
|
w->errs++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
long dt = now_us() - t0;
|
||||||
|
if (w->nlat < MAX_SAMPLES) w->lat[w->nlat++] = dt;
|
||||||
|
w->reqs++;
|
||||||
|
if (code >= 200 && code < 300) w->ok2xx++; else w->non2xx++;
|
||||||
|
}
|
||||||
|
close(fd);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int cmp_long(const void *a, const void *b) {
|
||||||
|
long x = *(const long *)a, y = *(const long *)b;
|
||||||
|
return (x > y) - (x < y);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (argc < 6) {
|
||||||
|
fprintf(stderr, "usage: %s <host> <port> <threads> <seconds> <path> [post-json]\n", argv[0]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
const char *host = argv[1];
|
||||||
|
int port = atoi(argv[2]);
|
||||||
|
int threads = atoi(argv[3]);
|
||||||
|
int secs = atoi(argv[4]);
|
||||||
|
const char *path = argv[5];
|
||||||
|
const char *body = argc > 6 ? argv[6] : NULL;
|
||||||
|
|
||||||
|
struct rlimit rl;
|
||||||
|
if (getrlimit(RLIMIT_NOFILE, &rl) == 0 && rl.rlim_cur < rl.rlim_max) {
|
||||||
|
rl.rlim_cur = rl.rlim_max;
|
||||||
|
setrlimit(RLIMIT_NOFILE, &rl);
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(&g_addr, 0, sizeof g_addr);
|
||||||
|
g_addr.sin_family = AF_INET;
|
||||||
|
g_addr.sin_port = htons((uint16_t)port);
|
||||||
|
inet_pton(AF_INET, host, &g_addr.sin_addr);
|
||||||
|
|
||||||
|
if (body)
|
||||||
|
g_reqlen = snprintf(g_req, sizeof g_req,
|
||||||
|
"POST %s HTTP/1.1\r\nHost: %s\r\nContent-Type: application/json\r\n"
|
||||||
|
"Content-Length: %zu\r\nConnection: keep-alive\r\n\r\n%s",
|
||||||
|
path, host, strlen(body), body);
|
||||||
|
else
|
||||||
|
g_reqlen = snprintf(g_req, sizeof g_req,
|
||||||
|
"GET %s HTTP/1.1\r\nHost: %s\r\nConnection: keep-alive\r\n\r\n", path, host);
|
||||||
|
|
||||||
|
g_hold_secs = 3;
|
||||||
|
g_deadline_us = secs > 0 ? now_us() + (long)secs * 1000000L : 0;
|
||||||
|
|
||||||
|
struct worker *ws = calloc((size_t)threads, sizeof *ws);
|
||||||
|
for (int i = 0; i < threads; i++) {
|
||||||
|
ws[i].lat = secs > 0 ? malloc(MAX_SAMPLES * sizeof(long)) : NULL;
|
||||||
|
pthread_create(&ws[i].tid, NULL, worker_main, &ws[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
long t0 = now_us();
|
||||||
|
long total = 0, errs = 0, nlat = 0, ok = 0, bad = 0;
|
||||||
|
for (int i = 0; i < threads; i++) {
|
||||||
|
pthread_join(ws[i].tid, NULL);
|
||||||
|
total += ws[i].reqs;
|
||||||
|
errs += ws[i].errs;
|
||||||
|
nlat += ws[i].nlat;
|
||||||
|
ok += ws[i].ok2xx;
|
||||||
|
bad += ws[i].non2xx;
|
||||||
|
}
|
||||||
|
long wall_us = now_us() - t0;
|
||||||
|
|
||||||
|
if (secs == 0) {
|
||||||
|
printf("idle-conns: opened %ld / %d connections (errs %ld), held %ds, server survived\n",
|
||||||
|
total, threads, errs, g_hold_secs);
|
||||||
|
return errs ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
long *all = malloc((size_t)nlat * sizeof(long));
|
||||||
|
long k = 0;
|
||||||
|
for (int i = 0; i < threads; i++) {
|
||||||
|
memcpy(all + k, ws[i].lat, (size_t)ws[i].nlat * sizeof(long));
|
||||||
|
k += ws[i].nlat;
|
||||||
|
}
|
||||||
|
qsort(all, (size_t)nlat, sizeof(long), cmp_long);
|
||||||
|
|
||||||
|
double rps = (double)ok / ((double)wall_us / 1e6);
|
||||||
|
printf("%-22s %d conns %ds %ld ok (2xx) %.0f ok/s p50 %ld µs p99 %ld µs non-2xx %ld errs %ld\n",
|
||||||
|
body ? "WRITE (POST)" : path, threads, secs, ok, rps,
|
||||||
|
nlat ? all[nlat / 2] : 0, nlat ? all[(long)((double)nlat * 0.99)] : 0, bad, errs);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
3
runtime/bench/goref/go.mod
Normal file
3
runtime/bench/goref/go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
module goref
|
||||||
|
|
||||||
|
go 1.25.1
|
||||||
69
runtime/bench/goref/main.go
Normal file
69
runtime/bench/goref/main.go
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
// goref — the Go net/http comparison server for the phase-F benchmark.
|
||||||
|
// Same endpoints and semantics as wo-rt-c's RAM read path: /healthz, /,
|
||||||
|
// GET/POST /api/notes against an in-memory store. Durability is NOT
|
||||||
|
// implemented here (Go side has no WAL), so only READ benchmarks are
|
||||||
|
// apples-to-apples; the POST comparison measures Go's non-durable path
|
||||||
|
// against wo-rt-c's fsync-backed path and is labeled as such.
|
||||||
|
//
|
||||||
|
// go build -o goref . && ./goref # :8095, GOMAXPROCS = all cores
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type note struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
mu sync.RWMutex
|
||||||
|
notes []note
|
||||||
|
nextID = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
io.WriteString(w, "ok")
|
||||||
|
})
|
||||||
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
io.WriteString(w, `{"runtime":"go-net-http"}`)
|
||||||
|
})
|
||||||
|
http.HandleFunc("/api/notes", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
mu.RLock()
|
||||||
|
b, _ := json.Marshal(notes)
|
||||||
|
mu.RUnlock()
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(b)
|
||||||
|
case http.MethodPost:
|
||||||
|
var in struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
}
|
||||||
|
if json.NewDecoder(r.Body).Decode(&in) != nil || in.Title == "" {
|
||||||
|
http.Error(w, `{"error":"expected {\"title\":\"...\"}"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
n := note{ID: nextID, Title: in.Title}
|
||||||
|
nextID++
|
||||||
|
if len(notes) < 100000 {
|
||||||
|
notes = append(notes, n)
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
json.NewEncoder(w).Encode(n)
|
||||||
|
default:
|
||||||
|
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
fmt.Println("[goref] listening on :8095")
|
||||||
|
http.ListenAndServe("127.0.0.1:8095", nil)
|
||||||
|
}
|
||||||
0
runtime/src/.gitkeep
Normal file
0
runtime/src/.gitkeep
Normal file
17
runtime/src/borrow.c
Normal file
17
runtime/src/borrow.c
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
#include "borrow.h"
|
||||||
|
|
||||||
|
int wo_borrow_shared(wo_hdr *o) {
|
||||||
|
if (o->borrow == WO_BORROW_EXCL) return -1;
|
||||||
|
o->borrow++;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int wo_borrow_excl(wo_hdr *o) {
|
||||||
|
if (o->borrow != WO_BORROW_FREE) return -1;
|
||||||
|
o->borrow = WO_BORROW_EXCL;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void wo_release_shared(wo_hdr *o) { o->borrow--; }
|
||||||
|
|
||||||
|
void wo_release_excl(wo_hdr *o) { o->borrow = WO_BORROW_FREE; }
|
||||||
17
runtime/src/borrow.h
Normal file
17
runtime/src/borrow.h
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
/* borrow.h — the residual-check primitive of the hybrid model (spec §4).
|
||||||
|
* The header's borrow word counts shared readers; WO_BORROW_EXCL means
|
||||||
|
* exclusively borrowed. Acquires return 0 or -1 (the VM maps -1 to
|
||||||
|
* WO_T_BORROW); releases are unconditional — the compiler emits them
|
||||||
|
* balanced. Small on purpose: this module is the semantic heart of the
|
||||||
|
* hybrid model and the compiler's emit rules cite it. */
|
||||||
|
#ifndef WO_BORROW_H
|
||||||
|
#define WO_BORROW_H
|
||||||
|
|
||||||
|
#include "wob.h"
|
||||||
|
|
||||||
|
int wo_borrow_shared(wo_hdr *o); /* fails only against exclusive */
|
||||||
|
int wo_borrow_excl(wo_hdr *o); /* fails unless completely free */
|
||||||
|
void wo_release_shared(wo_hdr *o);
|
||||||
|
void wo_release_excl(wo_hdr *o);
|
||||||
|
|
||||||
|
#endif /* WO_BORROW_H */
|
||||||
157
runtime/src/builtin.c
Normal file
157
runtime/src/builtin.c
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
#define _POSIX_C_SOURCE 199309L /* clock_gettime under -std=c11 */
|
||||||
|
|
||||||
|
#include "builtin.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#include "cont.h"
|
||||||
|
#include "gc.h"
|
||||||
|
|
||||||
|
/* type checks on receiver headers: wrong native class traps BOUNDS */
|
||||||
|
static void *native_check(uint64_t v, uint32_t cls, const char **msg) {
|
||||||
|
if (!v) {
|
||||||
|
*msg = "null receiver";
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
wo_hdr *o = (wo_hdr *)(uintptr_t)v;
|
||||||
|
if (o->class_id != cls) {
|
||||||
|
*msg = "wrong container type";
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
int wo_builtin(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) {
|
||||||
|
wo_rt *rt = &vm->rt;
|
||||||
|
uint8_t A = wo_ins_a(ins), B = wo_ins_b(ins), C = wo_ins_c(ins);
|
||||||
|
switch (C) {
|
||||||
|
case WO_B_NOW: { /* wall-clock milliseconds */
|
||||||
|
struct timespec ts;
|
||||||
|
clock_gettime(CLOCK_REALTIME, &ts);
|
||||||
|
R[A] = (uint64_t)((int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
case WO_B_PRINT: {
|
||||||
|
wo_str *s = native_check(R[B], WO_CLS_STR, msg);
|
||||||
|
if (!s) return WO_T_BOUNDS;
|
||||||
|
FILE *out = rt->out;
|
||||||
|
fwrite(s->data, 1, s->len, out);
|
||||||
|
fputc('\n', out);
|
||||||
|
R[A] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
case WO_B_PRINT_INT: {
|
||||||
|
fprintf((FILE *)rt->out, "%lld\n", (long long)(int64_t)R[B]);
|
||||||
|
R[A] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
case WO_B_WORDS: { /* whitespace-separated token count */
|
||||||
|
wo_str *s = native_check(R[B], WO_CLS_STR, msg);
|
||||||
|
if (!s) return WO_T_BOUNDS;
|
||||||
|
uint64_t n = 0;
|
||||||
|
int in_tok = 0;
|
||||||
|
for (uint32_t i = 0; i < s->len; i++) {
|
||||||
|
char ch = s->data[i];
|
||||||
|
int ws = ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r';
|
||||||
|
if (!ws && !in_tok) n++;
|
||||||
|
in_tok = !ws;
|
||||||
|
}
|
||||||
|
R[A] = n;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
case WO_B_MULTI_NEW: { /* element kind immediate in B */
|
||||||
|
wo_multi *m = wo_multi_new(rt, B);
|
||||||
|
if (!m) {
|
||||||
|
*msg = "out of memory";
|
||||||
|
return WO_T_OOM;
|
||||||
|
}
|
||||||
|
R[A] = (uint64_t)(uintptr_t)m;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
case WO_B_MULTI_PUSH: {
|
||||||
|
wo_multi *m = native_check(R[B], WO_CLS_MULTI, msg);
|
||||||
|
if (!m) return WO_T_BOUNDS;
|
||||||
|
if (wo_multi_push(m, R[B + 1]) != 0) {
|
||||||
|
*msg = "out of memory";
|
||||||
|
return WO_T_OOM;
|
||||||
|
}
|
||||||
|
R[A] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
case WO_B_MULTI_GET: {
|
||||||
|
wo_multi *m = native_check(R[B], WO_CLS_MULTI, msg);
|
||||||
|
if (!m) return WO_T_BOUNDS;
|
||||||
|
if (wo_multi_get(m, R[B + 1], &R[A]) != 0) {
|
||||||
|
*msg = "multi index out of range";
|
||||||
|
return WO_T_BOUNDS;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
case WO_B_COUNT: { /* length of either container */
|
||||||
|
wo_hdr *o = (wo_hdr *)(uintptr_t)R[B];
|
||||||
|
if (R[B] && o->class_id == WO_CLS_MULTI) {
|
||||||
|
R[A] = ((wo_multi *)o)->len;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (R[B] && o->class_id == WO_CLS_MAP) {
|
||||||
|
R[A] = ((wo_map *)o)->len;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
*msg = "count of a non-container";
|
||||||
|
return WO_T_BOUNDS;
|
||||||
|
}
|
||||||
|
case WO_B_LATEST: {
|
||||||
|
wo_multi *m = native_check(R[B], WO_CLS_MULTI, msg);
|
||||||
|
if (!m) return WO_T_BOUNDS;
|
||||||
|
if (m->len == 0) {
|
||||||
|
*msg = "latest of an empty multi";
|
||||||
|
return WO_T_BOUNDS;
|
||||||
|
}
|
||||||
|
R[A] = m->items[m->len - 1];
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
case WO_B_MAP_NEW: { /* key/value kind nibbles immediate in B */
|
||||||
|
wo_map *m = wo_map_new(rt, B & 0x0F, B >> 4);
|
||||||
|
if (!m) {
|
||||||
|
*msg = "out of memory";
|
||||||
|
return WO_T_OOM;
|
||||||
|
}
|
||||||
|
R[A] = (uint64_t)(uintptr_t)m;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
case WO_B_MAP_SET: {
|
||||||
|
wo_map *m = native_check(R[B], WO_CLS_MAP, msg);
|
||||||
|
if (!m) return WO_T_BOUNDS;
|
||||||
|
uint64_t old = 0;
|
||||||
|
int rc = wo_map_set(m, R[B + 1], R[B + 2], &old);
|
||||||
|
if (rc < 0) {
|
||||||
|
*msg = "out of memory";
|
||||||
|
return WO_T_OOM;
|
||||||
|
}
|
||||||
|
/* insert-or-replace hands the displaced value back: drop it here —
|
||||||
|
* closing the loose end cont.c documents */
|
||||||
|
if (rc == 1) wo_drop_kind(rt, m->val_kind, old);
|
||||||
|
R[A] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
case WO_B_MAP_GET: {
|
||||||
|
wo_map *m = native_check(R[B], WO_CLS_MAP, msg);
|
||||||
|
if (!m) return WO_T_BOUNDS;
|
||||||
|
if (wo_map_get(m, R[B + 1], &R[A]) != 0) {
|
||||||
|
*msg = "missing map key";
|
||||||
|
return WO_T_KEY;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
case WO_B_MAP_HAS: {
|
||||||
|
wo_map *m = native_check(R[B], WO_CLS_MAP, msg);
|
||||||
|
if (!m) return WO_T_BOUNDS;
|
||||||
|
R[A] = wo_map_has(m, R[B + 1]) ? 1 : 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
default: /* unreachable: loader validated the id */
|
||||||
|
*msg = "unknown builtin";
|
||||||
|
return WO_T_EXPLICIT;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
runtime/src/builtin.h
Normal file
12
runtime/src/builtin.h
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
/* builtin.h — runtime services bytecode can't express (spec §3/§5):
|
||||||
|
* now/print/print_int/words plus the container bridge to cont.c. One
|
||||||
|
* dispatcher: takes the VM, the current frame's registers, and the
|
||||||
|
* instruction; 0 = ok, else a WO_T_* trap code with *msg set. */
|
||||||
|
#ifndef WO_BUILTIN_H
|
||||||
|
#define WO_BUILTIN_H
|
||||||
|
|
||||||
|
#include "vm.h"
|
||||||
|
|
||||||
|
int wo_builtin(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg);
|
||||||
|
|
||||||
|
#endif /* WO_BUILTIN_H */
|
||||||
86
runtime/src/cont.c
Normal file
86
runtime/src/cont.c
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
#include "cont.h"
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
wo_multi *wo_multi_new(wo_rt *rt, uint8_t elem_kind) {
|
||||||
|
wo_multi *m = wo_arena_alloc(&rt->arena, sizeof(wo_multi));
|
||||||
|
if (!m) return NULL;
|
||||||
|
memset(m, 0, sizeof(*m));
|
||||||
|
m->h.class_id = WO_CLS_MULTI;
|
||||||
|
m->elem_kind = elem_kind;
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
int wo_multi_push(wo_multi *m, uint64_t v) {
|
||||||
|
if (m->len == m->cap) {
|
||||||
|
uint32_t ncap = m->cap ? m->cap * 2 : 8;
|
||||||
|
uint64_t *ni = realloc(m->items, (size_t)ncap * sizeof(uint64_t));
|
||||||
|
if (!ni) return -1;
|
||||||
|
m->items = ni;
|
||||||
|
m->cap = ncap;
|
||||||
|
}
|
||||||
|
m->items[m->len++] = v;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int wo_multi_get(const wo_multi *m, uint64_t idx, uint64_t *out) {
|
||||||
|
if (idx >= m->len) return -1;
|
||||||
|
*out = m->items[idx];
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
wo_map *wo_map_new(wo_rt *rt, uint8_t key_kind, uint8_t val_kind) {
|
||||||
|
wo_map *m = wo_arena_alloc(&rt->arena, sizeof(wo_map));
|
||||||
|
if (!m) return NULL;
|
||||||
|
memset(m, 0, sizeof(*m));
|
||||||
|
m->h.class_id = WO_CLS_MAP;
|
||||||
|
m->key_kind = key_kind;
|
||||||
|
m->val_kind = val_kind;
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int key_eq(const wo_map *m, uint64_t a, uint64_t b) {
|
||||||
|
if (m->key_kind == WO_K_TEXT)
|
||||||
|
return wo_str_eq((const wo_str *)(uintptr_t)a,
|
||||||
|
(const wo_str *)(uintptr_t)b);
|
||||||
|
return a == b;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int map_find(const wo_map *m, uint64_t k) {
|
||||||
|
for (uint32_t i = 0; i < m->len; i++)
|
||||||
|
if (key_eq(m, m->keys[i], k)) return (int)i;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int wo_map_set(wo_map *m, uint64_t k, uint64_t v, uint64_t *old) {
|
||||||
|
int i = map_find(m, k);
|
||||||
|
if (i >= 0) {
|
||||||
|
*old = m->vals[i];
|
||||||
|
m->vals[i] = v;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (m->len == m->cap) {
|
||||||
|
uint32_t ncap = m->cap ? m->cap * 2 : 8;
|
||||||
|
uint64_t *nk = realloc(m->keys, (size_t)ncap * sizeof(uint64_t));
|
||||||
|
if (!nk) return -1;
|
||||||
|
m->keys = nk;
|
||||||
|
uint64_t *nv = realloc(m->vals, (size_t)ncap * sizeof(uint64_t));
|
||||||
|
if (!nv) return -1;
|
||||||
|
m->vals = nv;
|
||||||
|
m->cap = ncap;
|
||||||
|
}
|
||||||
|
m->keys[m->len] = k;
|
||||||
|
m->vals[m->len] = v;
|
||||||
|
m->len++;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int wo_map_get(const wo_map *m, uint64_t k, uint64_t *out) {
|
||||||
|
int i = map_find(m, k);
|
||||||
|
if (i < 0) return -1;
|
||||||
|
*out = m->vals[i];
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int wo_map_has(const wo_map *m, uint64_t k) { return map_find(m, k) >= 0; }
|
||||||
37
runtime/src/cont.h
Normal file
37
runtime/src/cont.h
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
/* cont.h — native containers (spec §3): `multi T` and `map<K,V>` are
|
||||||
|
* runtime-provided native classes, not user generics. Struct heads live in
|
||||||
|
* the arena; backing arrays are malloc/realloc'd. Map lookup is a linear
|
||||||
|
* scan — deliberate milestone-1 KISS. Insert-or-replace hands the replaced
|
||||||
|
* old value back to the caller (dropping needs gc.c; the builtin layer does
|
||||||
|
* it). Element-dropping frees also live in gc.c. */
|
||||||
|
#ifndef WO_CONT_H
|
||||||
|
#define WO_CONT_H
|
||||||
|
|
||||||
|
#include "obj.h"
|
||||||
|
|
||||||
|
typedef struct wo_multi {
|
||||||
|
wo_hdr h; /* class_id WO_CLS_MULTI */
|
||||||
|
uint8_t elem_kind;
|
||||||
|
uint32_t len, cap;
|
||||||
|
uint64_t *items;
|
||||||
|
} wo_multi;
|
||||||
|
|
||||||
|
wo_multi *wo_multi_new(wo_rt *rt, uint8_t elem_kind); /* NULL = OOM */
|
||||||
|
int wo_multi_push(wo_multi *m, uint64_t v); /* 0 ok, -1 OOM */
|
||||||
|
int wo_multi_get(const wo_multi *m, uint64_t idx, uint64_t *out); /* -1 oob */
|
||||||
|
|
||||||
|
typedef struct wo_map {
|
||||||
|
wo_hdr h; /* class_id WO_CLS_MAP */
|
||||||
|
uint8_t key_kind, val_kind;
|
||||||
|
uint32_t len, cap;
|
||||||
|
uint64_t *keys, *vals; /* parallel arrays */
|
||||||
|
} wo_map;
|
||||||
|
|
||||||
|
wo_map *wo_map_new(wo_rt *rt, uint8_t key_kind, uint8_t val_kind);
|
||||||
|
/* 0 = inserted, 1 = replaced (*old set to the displaced value), -1 = OOM.
|
||||||
|
* Keys compare by content when key_kind is TEXT, by bits otherwise. */
|
||||||
|
int wo_map_set(wo_map *m, uint64_t k, uint64_t v, uint64_t *old);
|
||||||
|
int wo_map_get(const wo_map *m, uint64_t k, uint64_t *out); /* -1 = miss */
|
||||||
|
int wo_map_has(const wo_map *m, uint64_t k);
|
||||||
|
|
||||||
|
#endif /* WO_CONT_H */
|
||||||
296
runtime/src/gc.c
Normal file
296
runtime/src/gc.c
Normal file
|
|
@ -0,0 +1,296 @@
|
||||||
|
#include "gc.h"
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#include "cont.h"
|
||||||
|
|
||||||
|
static void multi_free(wo_rt *rt, wo_multi *m) {
|
||||||
|
for (uint32_t i = 0; i < m->len; i++)
|
||||||
|
wo_drop_kind(rt, m->elem_kind, m->items[i]);
|
||||||
|
free(m->items);
|
||||||
|
wo_arena_free(&rt->arena, m, sizeof(wo_multi));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void map_free(wo_rt *rt, wo_map *m) {
|
||||||
|
for (uint32_t i = 0; i < m->len; i++) {
|
||||||
|
wo_drop_kind(rt, m->key_kind, m->keys[i]);
|
||||||
|
wo_drop_kind(rt, m->val_kind, m->vals[i]);
|
||||||
|
}
|
||||||
|
free(m->keys);
|
||||||
|
free(m->vals);
|
||||||
|
wo_arena_free(&rt->arena, m, sizeof(wo_map));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* release a class object's contents then the object itself */
|
||||||
|
static void class_free(wo_rt *rt, wo_hdr *o) {
|
||||||
|
const wo_classdesc *c = &rt->classes[o->class_id];
|
||||||
|
uint64_t *f = wo_fields(o);
|
||||||
|
for (uint32_t i = 0; i < c->field_cnt; i++)
|
||||||
|
wo_drop_kind(rt, c->kinds[i], f[i]);
|
||||||
|
wo_arena_free(&rt->arena, o, wo_obj_size(c));
|
||||||
|
}
|
||||||
|
|
||||||
|
void wo_drop_obj(wo_rt *rt, wo_hdr *o) {
|
||||||
|
if (!o) return;
|
||||||
|
switch (o->class_id) {
|
||||||
|
case WO_CLS_STR:
|
||||||
|
wo_str_free(rt, (wo_str *)o);
|
||||||
|
return;
|
||||||
|
case WO_CLS_MULTI:
|
||||||
|
multi_free(rt, (wo_multi *)o);
|
||||||
|
return;
|
||||||
|
case WO_CLS_MAP:
|
||||||
|
map_free(rt, (wo_map *)o);
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
class_free(rt, o);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void wo_drop_kind(wo_rt *rt, uint8_t kind, uint64_t v) {
|
||||||
|
if (!v) return; /* null values ignored for every kind */
|
||||||
|
switch (kind) {
|
||||||
|
case WO_K_SCALAR:
|
||||||
|
return;
|
||||||
|
case WO_K_OWNED:
|
||||||
|
case WO_K_MULTI:
|
||||||
|
case WO_K_MAP:
|
||||||
|
wo_drop_obj(rt, (wo_hdr *)(uintptr_t)v);
|
||||||
|
return;
|
||||||
|
case WO_K_GCREF:
|
||||||
|
wo_rc_dec(rt, (wo_hdr *)(uintptr_t)v);
|
||||||
|
return;
|
||||||
|
case WO_K_TEXT:
|
||||||
|
wo_str_free(rt, (wo_str *)(uintptr_t)v);
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
return; /* loader guarantees kinds; defensive no-op */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void wo_rc_inc(wo_hdr *o) { o->rc++; }
|
||||||
|
|
||||||
|
/* ---- cycle-candidate buffer ---- */
|
||||||
|
|
||||||
|
/* only classes that can point at other @gc objects can close a cycle */
|
||||||
|
static int class_possibly_cyclic(const wo_rt *rt, const wo_hdr *o) {
|
||||||
|
const wo_classdesc *c = &rt->classes[o->class_id];
|
||||||
|
for (uint32_t i = 0; i < c->field_cnt; i++) {
|
||||||
|
uint8_t k = c->kinds[i];
|
||||||
|
if (k == WO_K_GCREF || k == WO_K_MULTI || k == WO_K_MAP) return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void buf_push(wo_rt *rt, wo_hdr *o) {
|
||||||
|
if (rt->cycbuf.len == rt->cycbuf.cap) {
|
||||||
|
size_t ncap = rt->cycbuf.cap ? rt->cycbuf.cap * 2 : 16;
|
||||||
|
wo_hdr **ni = realloc(rt->cycbuf.items, ncap * sizeof(wo_hdr *));
|
||||||
|
if (!ni) return; /* can't buffer: conservative leak, never corrupt */
|
||||||
|
rt->cycbuf.items = ni;
|
||||||
|
rt->cycbuf.cap = ncap;
|
||||||
|
}
|
||||||
|
o->flags |= WO_F_BUF;
|
||||||
|
rt->cycbuf.items[rt->cycbuf.len++] = o;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* swap-remove a specific object from the buffer (whites purged mid-step) */
|
||||||
|
static void buf_remove(wo_rt *rt, wo_hdr *o) {
|
||||||
|
for (size_t i = 0; i < rt->cycbuf.len; i++) {
|
||||||
|
if (rt->cycbuf.items[i] == o) {
|
||||||
|
rt->cycbuf.items[i] = rt->cycbuf.items[--rt->cycbuf.len];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void wo_rc_dec(wo_rt *rt, wo_hdr *o) {
|
||||||
|
o->rc--;
|
||||||
|
if (o->rc == 0) {
|
||||||
|
/* zombie guard: a buffered candidate's death belongs to the cycle
|
||||||
|
* collector — it will notice rc 0 and free it in its own sweep */
|
||||||
|
if (o->flags & WO_F_BUF) return;
|
||||||
|
class_free(rt, o);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/* survived a decrement and can sit on a cycle: buffer as a candidate,
|
||||||
|
* deduplicated by the flag (Bacon–Rajan possible-root heuristic) */
|
||||||
|
if (!(o->flags & WO_F_BUF) && class_possibly_cyclic(rt, o))
|
||||||
|
buf_push(rt, o);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- budgeted Bacon–Rajan trial deletion ---- */
|
||||||
|
|
||||||
|
static uint8_t color_of(const wo_hdr *o) { return o->flags & WO_F_COLOR; }
|
||||||
|
static void set_color(wo_hdr *o, uint8_t c) {
|
||||||
|
o->flags = (uint8_t)((o->flags & ~WO_F_COLOR) | c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* visit every @gc edge out of a class object: gcref fields, plus gcref
|
||||||
|
* elements inside multi/map fields */
|
||||||
|
typedef void (*child_fn)(wo_rt *rt, wo_hdr *child, void *ctx);
|
||||||
|
static void visit_children(wo_rt *rt, wo_hdr *o, child_fn fn, void *ctx) {
|
||||||
|
const wo_classdesc *c = &rt->classes[o->class_id];
|
||||||
|
uint64_t *f = wo_fields(o);
|
||||||
|
for (uint32_t i = 0; i < c->field_cnt; i++) {
|
||||||
|
uint8_t k = c->kinds[i];
|
||||||
|
if (k == WO_K_GCREF) {
|
||||||
|
if (f[i]) fn(rt, (wo_hdr *)(uintptr_t)f[i], ctx);
|
||||||
|
} else if (k == WO_K_MULTI) {
|
||||||
|
wo_multi *m = (wo_multi *)(uintptr_t)f[i];
|
||||||
|
if (m && m->elem_kind == WO_K_GCREF)
|
||||||
|
for (uint32_t j = 0; j < m->len; j++)
|
||||||
|
if (m->items[j]) fn(rt, (wo_hdr *)(uintptr_t)m->items[j], ctx);
|
||||||
|
} else if (k == WO_K_MAP) {
|
||||||
|
wo_map *mp = (wo_map *)(uintptr_t)f[i];
|
||||||
|
if (!mp) continue;
|
||||||
|
if (mp->key_kind == WO_K_GCREF)
|
||||||
|
for (uint32_t j = 0; j < mp->len; j++)
|
||||||
|
if (mp->keys[j]) fn(rt, (wo_hdr *)(uintptr_t)mp->keys[j], ctx);
|
||||||
|
if (mp->val_kind == WO_K_GCREF)
|
||||||
|
for (uint32_t j = 0; j < mp->len; j++)
|
||||||
|
if (mp->vals[j]) fn(rt, (wo_hdr *)(uintptr_t)mp->vals[j], ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void mark_gray(wo_rt *rt, wo_hdr *o);
|
||||||
|
static void mark_gray_child(wo_rt *rt, wo_hdr *c, void *ctx) {
|
||||||
|
(void)ctx;
|
||||||
|
c->rc--; /* trial-delete this edge */
|
||||||
|
mark_gray(rt, c);
|
||||||
|
}
|
||||||
|
static void mark_gray(wo_rt *rt, wo_hdr *o) {
|
||||||
|
if (color_of(o) == WO_COLOR_GRAY) return;
|
||||||
|
set_color(o, WO_COLOR_GRAY);
|
||||||
|
visit_children(rt, o, mark_gray_child, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void scan_black(wo_rt *rt, wo_hdr *o);
|
||||||
|
static void scan_black_child(wo_rt *rt, wo_hdr *c, void *ctx) {
|
||||||
|
(void)ctx;
|
||||||
|
c->rc++; /* restore the trial-deleted edge */
|
||||||
|
if (color_of(c) != WO_COLOR_BLACK) scan_black(rt, c);
|
||||||
|
}
|
||||||
|
static void scan_black(wo_rt *rt, wo_hdr *o) {
|
||||||
|
set_color(o, WO_COLOR_BLACK);
|
||||||
|
visit_children(rt, o, scan_black_child, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void scan(wo_rt *rt, wo_hdr *o, void *ctx);
|
||||||
|
static void scan_(wo_rt *rt, wo_hdr *o) {
|
||||||
|
if (color_of(o) != WO_COLOR_GRAY) return;
|
||||||
|
if (o->rc > 0) {
|
||||||
|
scan_black(rt, o); /* externally held: restore the whole subgraph */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
set_color(o, WO_COLOR_WHITE);
|
||||||
|
visit_children(rt, o, scan, NULL);
|
||||||
|
}
|
||||||
|
static void scan(wo_rt *rt, wo_hdr *o, void *ctx) {
|
||||||
|
(void)ctx;
|
||||||
|
scan_(rt, o);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* gather whites into a step-local list (post-marking, pre-free) */
|
||||||
|
typedef struct {
|
||||||
|
wo_hdr **items;
|
||||||
|
size_t len, cap;
|
||||||
|
int oom;
|
||||||
|
} whites_t;
|
||||||
|
|
||||||
|
static void collect_white(wo_rt *rt, wo_hdr *o, void *ctx) {
|
||||||
|
whites_t *w = ctx;
|
||||||
|
if (color_of(o) != WO_COLOR_WHITE) return;
|
||||||
|
set_color(o, WO_COLOR_BLACK); /* dedup: gathered exactly once */
|
||||||
|
visit_children(rt, o, collect_white, ctx);
|
||||||
|
if (w->len == w->cap) {
|
||||||
|
size_t ncap = w->cap ? w->cap * 2 : 16;
|
||||||
|
wo_hdr **ni = realloc(w->items, ncap * sizeof(wo_hdr *));
|
||||||
|
if (!ni) {
|
||||||
|
w->oom = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
w->items = ni;
|
||||||
|
w->cap = ncap;
|
||||||
|
}
|
||||||
|
w->items[w->len++] = o;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Free a dead white: release contents but SKIP every @gc edge — all edge
|
||||||
|
* accounting was already settled by the gray/scan phases, and the pointed-
|
||||||
|
* at whites die in this same sweep. Containers holding gcref elements free
|
||||||
|
* only their backing. */
|
||||||
|
static void white_free(wo_rt *rt, wo_hdr *o) {
|
||||||
|
const wo_classdesc *c = &rt->classes[o->class_id];
|
||||||
|
uint64_t *f = wo_fields(o);
|
||||||
|
for (uint32_t i = 0; i < c->field_cnt; i++) {
|
||||||
|
uint8_t k = c->kinds[i];
|
||||||
|
uint64_t v = f[i];
|
||||||
|
if (!v || k == WO_K_SCALAR || k == WO_K_GCREF) continue;
|
||||||
|
if (k == WO_K_MULTI) {
|
||||||
|
wo_multi *m = (wo_multi *)(uintptr_t)v;
|
||||||
|
if (m->elem_kind != WO_K_GCREF)
|
||||||
|
for (uint32_t j = 0; j < m->len; j++)
|
||||||
|
wo_drop_kind(rt, m->elem_kind, m->items[j]);
|
||||||
|
free(m->items);
|
||||||
|
wo_arena_free(&rt->arena, m, sizeof(wo_multi));
|
||||||
|
} else if (k == WO_K_MAP) {
|
||||||
|
wo_map *mp = (wo_map *)(uintptr_t)v;
|
||||||
|
for (uint32_t j = 0; j < mp->len; j++) {
|
||||||
|
if (mp->key_kind != WO_K_GCREF)
|
||||||
|
wo_drop_kind(rt, mp->key_kind, mp->keys[j]);
|
||||||
|
if (mp->val_kind != WO_K_GCREF)
|
||||||
|
wo_drop_kind(rt, mp->val_kind, mp->vals[j]);
|
||||||
|
}
|
||||||
|
free(mp->keys);
|
||||||
|
free(mp->vals);
|
||||||
|
wo_arena_free(&rt->arena, mp, sizeof(wo_map));
|
||||||
|
} else {
|
||||||
|
wo_drop_kind(rt, k, v); /* OWNED subtree, TEXT */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wo_arena_free(&rt->arena, o, wo_obj_size(c));
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t wo_gc_step(wo_rt *rt, size_t budget) {
|
||||||
|
size_t freed = 0, consumed = 0;
|
||||||
|
while (consumed < budget && rt->cycbuf.len > 0) {
|
||||||
|
wo_hdr *root = rt->cycbuf.items[--rt->cycbuf.len];
|
||||||
|
root->flags &= (uint8_t)~WO_F_BUF;
|
||||||
|
consumed++;
|
||||||
|
if (root->rc == 0) {
|
||||||
|
/* died while buffered (zombie guard) — plain deterministic free */
|
||||||
|
class_free(rt, root);
|
||||||
|
freed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
/* trial deletion over this root's component (atomic per component) */
|
||||||
|
mark_gray(rt, root);
|
||||||
|
scan_(rt, root);
|
||||||
|
whites_t w = {0};
|
||||||
|
collect_white(rt, root, &w);
|
||||||
|
if (w.oom) { /* can't track whites: restore and retry next step */
|
||||||
|
scan_black(rt, root);
|
||||||
|
free(w.items);
|
||||||
|
buf_push(rt, root);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
/* purge gathered whites still sitting in the buffer, then free —
|
||||||
|
* deferred freeing removes every dangling-candidate hazard */
|
||||||
|
for (size_t i = 0; i < w.len; i++) {
|
||||||
|
if (w.items[i]->flags & WO_F_BUF) {
|
||||||
|
w.items[i]->flags &= (uint8_t)~WO_F_BUF;
|
||||||
|
buf_remove(rt, w.items[i]);
|
||||||
|
consumed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < w.len; i++) {
|
||||||
|
white_free(rt, w.items[i]);
|
||||||
|
freed++;
|
||||||
|
}
|
||||||
|
free(w.items);
|
||||||
|
}
|
||||||
|
return freed;
|
||||||
|
}
|
||||||
32
runtime/src/gc.h
Normal file
32
runtime/src/gc.h
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
/* gc.h — deterministic destruction + @gc reference counting (spec §4).
|
||||||
|
* Owned objects die deterministically via drop plans; @gc objects die at
|
||||||
|
* refcount zero. One kind-directed dispatcher is the workhorse: scalars
|
||||||
|
* ignored, owned values drop recursively, gc refs decrement, texts free,
|
||||||
|
* containers free element-wise then their backing. The budgeted cycle
|
||||||
|
* collector extends this module (Bacon–Rajan trial deletion). */
|
||||||
|
#ifndef WO_GC_H
|
||||||
|
#define WO_GC_H
|
||||||
|
|
||||||
|
#include "obj.h"
|
||||||
|
|
||||||
|
/* Drop an 8-byte field/register value known to be of field kind `kind`.
|
||||||
|
* Null (0) values are ignored for every kind. */
|
||||||
|
void wo_drop_kind(wo_rt *rt, uint8_t kind, uint64_t v);
|
||||||
|
|
||||||
|
/* Drop any heap value by its header: native sentinels route to their own
|
||||||
|
* frees; class objects walk their kind array over the field slots, then
|
||||||
|
* free themselves. */
|
||||||
|
void wo_drop_obj(wo_rt *rt, wo_hdr *o);
|
||||||
|
|
||||||
|
void wo_rc_inc(wo_hdr *o);
|
||||||
|
/* Decrement; at zero, release contents and free — unless the object sits
|
||||||
|
* in the cycle-candidate buffer (WO_F_BUF): the collector owns its death. */
|
||||||
|
void wo_rc_dec(wo_rt *rt, wo_hdr *o);
|
||||||
|
|
||||||
|
/* Budgeted cycle collection step (Bacon–Rajan trial deletion over the
|
||||||
|
* candidate buffer). Processes up to `budget` buffered roots (whole
|
||||||
|
* strongly-connected components process atomically, so overshoot is
|
||||||
|
* bounded); returns how many objects it freed. */
|
||||||
|
size_t wo_gc_step(wo_rt *rt, size_t budget);
|
||||||
|
|
||||||
|
#endif /* WO_GC_H */
|
||||||
465
runtime/src/loader.c
Normal file
465
runtime/src/loader.c
Normal file
|
|
@ -0,0 +1,465 @@
|
||||||
|
#include "loader.h"
|
||||||
|
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
/* bounds-checked cursor: every read validates remaining length */
|
||||||
|
typedef struct {
|
||||||
|
const uint8_t *p;
|
||||||
|
size_t len, off;
|
||||||
|
} cur_t;
|
||||||
|
|
||||||
|
static int rd(cur_t *c, void *dst, size_t n) {
|
||||||
|
if (n > c->len - c->off) return -1;
|
||||||
|
memcpy(dst, c->p + c->off, n);
|
||||||
|
c->off += n;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
static int rd_u8(cur_t *c, uint8_t *v) { return rd(c, v, 1); }
|
||||||
|
static int rd_u16(cur_t *c, uint16_t *v) { return rd(c, v, 2); }
|
||||||
|
static int rd_u32(cur_t *c, uint32_t *v) { return rd(c, v, 4); }
|
||||||
|
static int rd_u64(cur_t *c, uint64_t *v) { return rd(c, v, 8); }
|
||||||
|
|
||||||
|
/* every rejection frees everything parsed so far and reports a reason */
|
||||||
|
#define BAIL(...) \
|
||||||
|
do { \
|
||||||
|
snprintf(err, errlen, __VA_ARGS__); \
|
||||||
|
wo_module_free(m); \
|
||||||
|
return -1; \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
/* per-builtin fixed arity (args at B..B+arity-1); kind-immediate builtins
|
||||||
|
* (multi_new/map_new) carry kinds in B and take no register args */
|
||||||
|
static const uint8_t b_arity[WO_B_MAX + 1] = {
|
||||||
|
[WO_B_NOW] = 0, [WO_B_PRINT] = 1, [WO_B_PRINT_INT] = 1,
|
||||||
|
[WO_B_WORDS] = 1, [WO_B_MULTI_NEW] = 0, [WO_B_MULTI_PUSH] = 2,
|
||||||
|
[WO_B_MULTI_GET] = 2, [WO_B_COUNT] = 1, [WO_B_LATEST] = 1,
|
||||||
|
[WO_B_MAP_NEW] = 0, [WO_B_MAP_SET] = 3, [WO_B_MAP_GET] = 2,
|
||||||
|
[WO_B_MAP_HAS] = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
static int vtab_cmp(const void *a, const void *b) {
|
||||||
|
const wo_vtabent *x = a, *y = b;
|
||||||
|
if (x->class_id != y->class_id) return x->class_id < y->class_id ? -1 : 1;
|
||||||
|
if (x->slot != y->slot) return x->slot < y->slot ? -1 : 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Validation contract (what the interpreter is allowed to assume forever):
|
||||||
|
* magic/version; register counts 1..64 with args <= registers; every opcode
|
||||||
|
* known; every static register operand < reg_cnt; constant/class/callee/
|
||||||
|
* slot indexes in range; CALL argument windows fit the caller's frame; jump
|
||||||
|
* targets inside the code; the last instruction is a terminator; builtin
|
||||||
|
* ids in range with arity fitting the frame and kind immediates in range;
|
||||||
|
* line/drop tables ascending, in range, masks within reg_cnt; vtable ids in
|
||||||
|
* range, rows unique per (class,slot); entry (if present) a zero-arg free
|
||||||
|
* fn. GETF/SETF field indexes stay RUNTIME checks (registers are untyped) —
|
||||||
|
* the spec's residual-check doctrine. */
|
||||||
|
int wo_load_buf(wo_module *m, const uint8_t *buf, size_t len, char *err,
|
||||||
|
size_t errlen) {
|
||||||
|
memset(m, 0, sizeof(*m));
|
||||||
|
m->entry = WOB_NONE;
|
||||||
|
if (errlen) err[0] = '\0';
|
||||||
|
|
||||||
|
cur_t h = {buf, len, 0};
|
||||||
|
uint32_t magic, ver, coff, ccnt, koff, kcnt, ioff, icnt, moff, mcnt, entry;
|
||||||
|
if (rd_u32(&h, &magic) || rd_u32(&h, &ver) || rd_u32(&h, &coff) ||
|
||||||
|
rd_u32(&h, &ccnt) || rd_u32(&h, &koff) || rd_u32(&h, &kcnt) ||
|
||||||
|
rd_u32(&h, &ioff) || rd_u32(&h, &icnt) || rd_u32(&h, &moff) ||
|
||||||
|
rd_u32(&h, &mcnt) || rd_u32(&h, &entry))
|
||||||
|
BAIL("truncated header");
|
||||||
|
if (magic != WOB_MAGIC) BAIL("bad magic");
|
||||||
|
if (ver != WOB_VERSION) BAIL("unsupported version %u", (unsigned)ver);
|
||||||
|
if (coff > len || koff > len || ioff > len || moff > len)
|
||||||
|
BAIL("section offset out of range");
|
||||||
|
|
||||||
|
/* ---- constants ---- */
|
||||||
|
cur_t c = {buf, len, coff};
|
||||||
|
if (ccnt) {
|
||||||
|
m->consts = calloc(ccnt, sizeof(wo_const));
|
||||||
|
if (!m->consts) BAIL("out of memory");
|
||||||
|
}
|
||||||
|
for (uint32_t i = 0; i < ccnt; i++) {
|
||||||
|
uint8_t tag;
|
||||||
|
if (rd_u8(&c, &tag)) BAIL("constant %u: truncated", (unsigned)i);
|
||||||
|
if (tag == WOB_K_INT) {
|
||||||
|
uint64_t v;
|
||||||
|
if (rd_u64(&c, &v)) BAIL("constant %u: truncated", (unsigned)i);
|
||||||
|
m->consts[i].tag = tag;
|
||||||
|
m->consts[i].i = (int64_t)v;
|
||||||
|
} else if (tag == WOB_K_TEXT) {
|
||||||
|
uint32_t l;
|
||||||
|
if (rd_u32(&c, &l)) BAIL("constant %u: truncated", (unsigned)i);
|
||||||
|
if (l > c.len - c.off)
|
||||||
|
BAIL("constant %u: text overruns buffer", (unsigned)i);
|
||||||
|
wo_str *s = malloc(sizeof(wo_str) + l);
|
||||||
|
if (!s) BAIL("out of memory");
|
||||||
|
memset(&s->h, 0, sizeof(s->h));
|
||||||
|
s->h.class_id = WO_CLS_STR;
|
||||||
|
s->h.flags = WO_F_CONST; /* interned: outlives everything */
|
||||||
|
s->len = l;
|
||||||
|
memcpy(s->data, buf + c.off, l);
|
||||||
|
c.off += l;
|
||||||
|
m->consts[i].tag = tag;
|
||||||
|
m->consts[i].s = s;
|
||||||
|
} else {
|
||||||
|
BAIL("constant %u: unknown tag %u", (unsigned)i, (unsigned)tag);
|
||||||
|
}
|
||||||
|
m->const_cnt = i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- classes (kind bytes pooled; pointers fixed up after parse) ---- */
|
||||||
|
cur_t k = {buf, len, koff};
|
||||||
|
if (kcnt) {
|
||||||
|
m->classes = calloc(kcnt, sizeof(wo_classdesc));
|
||||||
|
if (!m->classes) BAIL("out of memory");
|
||||||
|
}
|
||||||
|
size_t pool_len = 0;
|
||||||
|
for (uint32_t i = 0; i < kcnt; i++) {
|
||||||
|
uint32_t name, flags, fcnt;
|
||||||
|
if (rd_u32(&k, &name) || rd_u32(&k, &flags) || rd_u32(&k, &fcnt))
|
||||||
|
BAIL("class %u: truncated", (unsigned)i);
|
||||||
|
if (name >= m->const_cnt || m->consts[name].tag != WOB_K_TEXT)
|
||||||
|
BAIL("class %u: bad name constant", (unsigned)i);
|
||||||
|
if (flags & ~WO_CLASSF_GC)
|
||||||
|
BAIL("class %u: unknown flags", (unsigned)i);
|
||||||
|
if (fcnt > 65535) BAIL("class %u: too many fields", (unsigned)i);
|
||||||
|
if (fcnt > k.len - k.off) BAIL("class %u: truncated kinds", (unsigned)i);
|
||||||
|
for (uint32_t j = 0; j < fcnt; j++)
|
||||||
|
if (buf[k.off + j] > WO_K_MAX)
|
||||||
|
BAIL("class %u field %u: bad kind", (unsigned)i, (unsigned)j);
|
||||||
|
uint8_t *np = realloc(m->kindpool, pool_len + (fcnt ? fcnt : 1));
|
||||||
|
if (!np) BAIL("out of memory");
|
||||||
|
m->kindpool = np;
|
||||||
|
memcpy(m->kindpool + pool_len, buf + k.off, fcnt);
|
||||||
|
k.off += fcnt;
|
||||||
|
uint32_t pad = (4u - fcnt % 4u) % 4u;
|
||||||
|
if (pad > k.len - k.off) BAIL("class %u: truncated pad", (unsigned)i);
|
||||||
|
k.off += pad;
|
||||||
|
m->classes[i].name = name;
|
||||||
|
m->classes[i].flags = flags;
|
||||||
|
m->classes[i].field_cnt = fcnt;
|
||||||
|
m->classes[i].kinds = (const uint8_t *)(uintptr_t)pool_len; /* offset */
|
||||||
|
pool_len += fcnt ? fcnt : 1;
|
||||||
|
m->class_cnt = i + 1;
|
||||||
|
}
|
||||||
|
for (uint32_t i = 0; i < m->class_cnt; i++)
|
||||||
|
m->classes[i].kinds = m->kindpool + (uintptr_t)m->classes[i].kinds;
|
||||||
|
|
||||||
|
/* ---- interfaces + vtable rows (expanded to sorted triples) ---- */
|
||||||
|
cur_t s = {buf, len, ioff};
|
||||||
|
uint32_t *slotbase = NULL, *imcnt = NULL;
|
||||||
|
#define BAILI(...) \
|
||||||
|
do { \
|
||||||
|
free(slotbase); \
|
||||||
|
free(imcnt); \
|
||||||
|
BAIL(__VA_ARGS__); \
|
||||||
|
} while (0)
|
||||||
|
if (icnt) {
|
||||||
|
slotbase = malloc(icnt * sizeof(uint32_t));
|
||||||
|
imcnt = malloc(icnt * sizeof(uint32_t));
|
||||||
|
if (!slotbase || !imcnt) BAILI("out of memory");
|
||||||
|
}
|
||||||
|
uint32_t slots = 0;
|
||||||
|
for (uint32_t i = 0; i < icnt; i++) {
|
||||||
|
uint32_t name, mc;
|
||||||
|
if (rd_u32(&s, &name) || rd_u32(&s, &mc))
|
||||||
|
BAILI("interface %u: truncated", (unsigned)i);
|
||||||
|
if (name >= m->const_cnt || m->consts[name].tag != WOB_K_TEXT)
|
||||||
|
BAILI("interface %u: bad name constant", (unsigned)i);
|
||||||
|
if (mc == 0 || mc > 1024)
|
||||||
|
BAILI("interface %u: bad method count", (unsigned)i);
|
||||||
|
slotbase[i] = slots;
|
||||||
|
imcnt[i] = mc;
|
||||||
|
slots += mc;
|
||||||
|
}
|
||||||
|
m->slot_cnt = slots;
|
||||||
|
uint32_t vrows;
|
||||||
|
if (rd_u32(&s, &vrows)) BAILI("truncated vtable count");
|
||||||
|
for (uint32_t r = 0; r < vrows; r++) {
|
||||||
|
uint32_t cid, iid;
|
||||||
|
if (rd_u32(&s, &cid) || rd_u32(&s, &iid))
|
||||||
|
BAILI("vtable row %u: truncated", (unsigned)r);
|
||||||
|
if (cid >= m->class_cnt) BAILI("vtable row %u: bad class", (unsigned)r);
|
||||||
|
if (iid >= icnt) BAILI("vtable row %u: bad interface", (unsigned)r);
|
||||||
|
wo_vtabent *nv = realloc(
|
||||||
|
m->vtabs, (m->vtab_cnt + imcnt[iid]) * sizeof(wo_vtabent));
|
||||||
|
if (!nv) BAILI("out of memory");
|
||||||
|
m->vtabs = nv;
|
||||||
|
for (uint32_t j = 0; j < imcnt[iid]; j++) {
|
||||||
|
uint32_t meth;
|
||||||
|
if (rd_u32(&s, &meth)) BAILI("vtable row %u: truncated", (unsigned)r);
|
||||||
|
m->vtabs[m->vtab_cnt].class_id = cid;
|
||||||
|
m->vtabs[m->vtab_cnt].slot = slotbase[iid] + j;
|
||||||
|
m->vtabs[m->vtab_cnt].method = meth; /* range-checked below */
|
||||||
|
m->vtab_cnt++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(slotbase);
|
||||||
|
free(imcnt);
|
||||||
|
#undef BAILI
|
||||||
|
|
||||||
|
/* ---- methods ---- */
|
||||||
|
cur_t t = {buf, len, moff};
|
||||||
|
if (mcnt) {
|
||||||
|
m->methods = calloc(mcnt, sizeof(wo_methodrec));
|
||||||
|
if (!m->methods) BAIL("out of memory");
|
||||||
|
}
|
||||||
|
for (uint32_t i = 0; i < mcnt; i++) {
|
||||||
|
wo_methodrec *mm = &m->methods[i];
|
||||||
|
uint32_t name, cid, clen;
|
||||||
|
uint16_t reserved;
|
||||||
|
if (rd_u32(&t, &name) || rd_u32(&t, &cid) || rd_u8(&t, &mm->arg_cnt) ||
|
||||||
|
rd_u8(&t, &mm->reg_cnt) || rd_u16(&t, &reserved) ||
|
||||||
|
rd_u32(&t, &clen))
|
||||||
|
BAIL("method %u: truncated", (unsigned)i);
|
||||||
|
m->method_cnt = i + 1; /* partial-free safety from here on */
|
||||||
|
if (name >= m->const_cnt || m->consts[name].tag != WOB_K_TEXT)
|
||||||
|
BAIL("method %u: bad name constant", (unsigned)i);
|
||||||
|
if (cid != WOB_NONE && cid >= m->class_cnt)
|
||||||
|
BAIL("method %u: bad class", (unsigned)i);
|
||||||
|
if (mm->reg_cnt < 1 || mm->reg_cnt > WO_MAX_REGS)
|
||||||
|
BAIL("method %u: register count out of range", (unsigned)i);
|
||||||
|
if (mm->arg_cnt > mm->reg_cnt)
|
||||||
|
BAIL("method %u: more args than registers", (unsigned)i);
|
||||||
|
if (clen == 0 || clen % 4)
|
||||||
|
BAIL("method %u: bad code length", (unsigned)i);
|
||||||
|
mm->name = name;
|
||||||
|
mm->class_id = cid;
|
||||||
|
mm->ninstr = clen / 4;
|
||||||
|
mm->code = malloc(clen); /* aligned owned copy */
|
||||||
|
if (!mm->code) BAIL("out of memory");
|
||||||
|
if (rd(&t, mm->code, clen)) BAIL("method %u: truncated code", (unsigned)i);
|
||||||
|
|
||||||
|
if (rd_u32(&t, &mm->line_cnt)) BAIL("method %u: truncated", (unsigned)i);
|
||||||
|
if (mm->line_cnt) {
|
||||||
|
if (mm->line_cnt > mm->ninstr)
|
||||||
|
BAIL("method %u: line table too long", (unsigned)i);
|
||||||
|
mm->lines = malloc(mm->line_cnt * sizeof(wo_lineent));
|
||||||
|
if (!mm->lines) BAIL("out of memory");
|
||||||
|
for (uint32_t j = 0; j < mm->line_cnt; j++) {
|
||||||
|
if (rd_u32(&t, &mm->lines[j].pc) || rd_u32(&t, &mm->lines[j].line))
|
||||||
|
BAIL("method %u: truncated line table", (unsigned)i);
|
||||||
|
if (mm->lines[j].pc >= mm->ninstr ||
|
||||||
|
(j && mm->lines[j].pc <= mm->lines[j - 1].pc))
|
||||||
|
BAIL("method %u: line table not ascending", (unsigned)i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rd_u32(&t, &mm->drop_cnt)) BAIL("method %u: truncated", (unsigned)i);
|
||||||
|
if (mm->drop_cnt) {
|
||||||
|
if (mm->drop_cnt > mm->ninstr)
|
||||||
|
BAIL("method %u: drop table too long", (unsigned)i);
|
||||||
|
mm->drops = malloc(mm->drop_cnt * sizeof(wo_dropent));
|
||||||
|
if (!mm->drops) BAIL("out of memory");
|
||||||
|
for (uint32_t j = 0; j < mm->drop_cnt; j++) {
|
||||||
|
if (rd_u32(&t, &mm->drops[j].pc) ||
|
||||||
|
rd_u64(&t, &mm->drops[j].owned) ||
|
||||||
|
rd_u64(&t, &mm->drops[j].gc))
|
||||||
|
BAIL("method %u: truncated drop table", (unsigned)i);
|
||||||
|
if (mm->drops[j].pc >= mm->ninstr ||
|
||||||
|
(j && mm->drops[j].pc <= mm->drops[j - 1].pc))
|
||||||
|
BAIL("method %u: drop table not ascending", (unsigned)i);
|
||||||
|
if (mm->reg_cnt < 64 &&
|
||||||
|
((mm->drops[j].owned | mm->drops[j].gc) >> mm->reg_cnt))
|
||||||
|
BAIL("method %u: drop mask out of range", (unsigned)i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- static instruction validation ---- */
|
||||||
|
for (uint32_t i = 0; i < m->method_cnt; i++) {
|
||||||
|
wo_methodrec *mm = &m->methods[i];
|
||||||
|
uint32_t regc = mm->reg_cnt;
|
||||||
|
#define RCHK(r) \
|
||||||
|
do { \
|
||||||
|
if ((uint32_t)(r) >= regc) \
|
||||||
|
BAIL("method %u pc %u: register out of range", (unsigned)i, \
|
||||||
|
(unsigned)pc); \
|
||||||
|
} while (0)
|
||||||
|
for (uint32_t pc = 0; pc < mm->ninstr; pc++) {
|
||||||
|
uint32_t ins = mm->code[pc];
|
||||||
|
uint8_t op = wo_ins_op(ins), A = wo_ins_a(ins), B = wo_ins_b(ins),
|
||||||
|
C = wo_ins_c(ins);
|
||||||
|
uint16_t bx = wo_ins_bx(ins);
|
||||||
|
switch (op) {
|
||||||
|
case WOP_NOP:
|
||||||
|
break;
|
||||||
|
case WOP_LOADK:
|
||||||
|
RCHK(A);
|
||||||
|
if (bx >= m->const_cnt)
|
||||||
|
BAIL("method %u pc %u: constant out of range", (unsigned)i,
|
||||||
|
(unsigned)pc);
|
||||||
|
break;
|
||||||
|
case WOP_MOVE:
|
||||||
|
case WOP_NEG:
|
||||||
|
RCHK(A);
|
||||||
|
RCHK(B);
|
||||||
|
break;
|
||||||
|
case WOP_ADD:
|
||||||
|
case WOP_SUB:
|
||||||
|
case WOP_MUL:
|
||||||
|
case WOP_DIV:
|
||||||
|
case WOP_CONCAT:
|
||||||
|
case WOP_EQ:
|
||||||
|
case WOP_LT:
|
||||||
|
case WOP_LE:
|
||||||
|
case WOP_EQS:
|
||||||
|
RCHK(A);
|
||||||
|
RCHK(B);
|
||||||
|
RCHK(C);
|
||||||
|
break;
|
||||||
|
case WOP_JMP:
|
||||||
|
case WOP_JZ: {
|
||||||
|
if (op == WOP_JZ) RCHK(A);
|
||||||
|
int64_t tgt = (int64_t)pc + 1 + wo_ins_sbx(ins);
|
||||||
|
if (tgt < 0 || tgt >= (int64_t)mm->ninstr)
|
||||||
|
BAIL("method %u pc %u: jump out of code", (unsigned)i,
|
||||||
|
(unsigned)pc);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case WOP_CALL: {
|
||||||
|
RCHK(A);
|
||||||
|
if (bx >= m->method_cnt)
|
||||||
|
BAIL("method %u pc %u: callee out of range", (unsigned)i,
|
||||||
|
(unsigned)pc);
|
||||||
|
if ((uint32_t)A + m->methods[bx].arg_cnt > regc)
|
||||||
|
BAIL("method %u pc %u: call window exceeds frame",
|
||||||
|
(unsigned)i, (unsigned)pc);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case WOP_ICALL:
|
||||||
|
RCHK(A);
|
||||||
|
if (bx >= m->slot_cnt)
|
||||||
|
BAIL("method %u pc %u: interface slot out of range",
|
||||||
|
(unsigned)i, (unsigned)pc);
|
||||||
|
break;
|
||||||
|
case WOP_RET:
|
||||||
|
RCHK(A);
|
||||||
|
break;
|
||||||
|
case WOP_RET0:
|
||||||
|
break;
|
||||||
|
case WOP_NEW:
|
||||||
|
RCHK(A);
|
||||||
|
if (bx >= m->class_cnt)
|
||||||
|
BAIL("method %u pc %u: class out of range", (unsigned)i,
|
||||||
|
(unsigned)pc);
|
||||||
|
break;
|
||||||
|
case WOP_GETF:
|
||||||
|
RCHK(A);
|
||||||
|
RCHK(B);
|
||||||
|
break; /* field idx C: runtime residual check */
|
||||||
|
case WOP_SETF:
|
||||||
|
RCHK(A);
|
||||||
|
RCHK(C);
|
||||||
|
break; /* field idx B: runtime residual check */
|
||||||
|
case WOP_DROP:
|
||||||
|
case WOP_BORROW_S:
|
||||||
|
case WOP_BORROW_X:
|
||||||
|
case WOP_RELEASE_S:
|
||||||
|
case WOP_RELEASE_X:
|
||||||
|
case WOP_RC_INC:
|
||||||
|
case WOP_RC_DEC:
|
||||||
|
RCHK(A);
|
||||||
|
break;
|
||||||
|
case WOP_BUILTIN: {
|
||||||
|
RCHK(A);
|
||||||
|
if (C > WO_B_MAX)
|
||||||
|
BAIL("method %u pc %u: builtin out of range", (unsigned)i,
|
||||||
|
(unsigned)pc);
|
||||||
|
if (C == WO_B_MULTI_NEW) {
|
||||||
|
if (B > WO_K_MAX)
|
||||||
|
BAIL("method %u pc %u: bad element kind", (unsigned)i,
|
||||||
|
(unsigned)pc);
|
||||||
|
} else if (C == WO_B_MAP_NEW) {
|
||||||
|
if ((B & 0x0F) > WO_K_MAX || (B >> 4) > WO_K_MAX)
|
||||||
|
BAIL("method %u pc %u: bad key/value kind", (unsigned)i,
|
||||||
|
(unsigned)pc);
|
||||||
|
} else if (b_arity[C]) {
|
||||||
|
RCHK(B);
|
||||||
|
RCHK((uint32_t)B + b_arity[C] - 1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case WOP_DB_STUB:
|
||||||
|
case WOP_TRAP:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
BAIL("method %u pc %u: unknown opcode %u", (unsigned)i,
|
||||||
|
(unsigned)pc, (unsigned)op);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#undef RCHK
|
||||||
|
uint8_t last = wo_ins_op(mm->code[mm->ninstr - 1]);
|
||||||
|
if (last != WOP_RET && last != WOP_RET0 && last != WOP_TRAP &&
|
||||||
|
last != WOP_DB_STUB && last != WOP_JMP)
|
||||||
|
BAIL("method %u: last instruction is not a terminator", (unsigned)i);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vtable method indexes (methods parsed now); sort; reject duplicates */
|
||||||
|
for (uint32_t i = 0; i < m->vtab_cnt; i++)
|
||||||
|
if (m->vtabs[i].method >= m->method_cnt)
|
||||||
|
BAIL("vtable entry %u: method out of range", (unsigned)i);
|
||||||
|
if (m->vtab_cnt)
|
||||||
|
qsort(m->vtabs, m->vtab_cnt, sizeof(wo_vtabent), vtab_cmp);
|
||||||
|
for (uint32_t i = 1; i < m->vtab_cnt; i++)
|
||||||
|
if (m->vtabs[i].class_id == m->vtabs[i - 1].class_id &&
|
||||||
|
m->vtabs[i].slot == m->vtabs[i - 1].slot)
|
||||||
|
BAIL("duplicate vtable entry for class %u",
|
||||||
|
(unsigned)m->vtabs[i].class_id);
|
||||||
|
|
||||||
|
/* entry: a zero-arg free fn */
|
||||||
|
if (entry != WOB_NONE) {
|
||||||
|
if (entry >= m->method_cnt) BAIL("entry method out of range");
|
||||||
|
if (m->methods[entry].arg_cnt != 0 ||
|
||||||
|
m->methods[entry].class_id != WOB_NONE)
|
||||||
|
BAIL("entry must be a zero-arg free fn");
|
||||||
|
}
|
||||||
|
m->entry = entry;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int wo_load_file(wo_module *m, const char *path, char *err, size_t errlen) {
|
||||||
|
memset(m, 0, sizeof(*m));
|
||||||
|
m->entry = WOB_NONE;
|
||||||
|
int fd = open(path, O_RDONLY);
|
||||||
|
if (fd < 0) {
|
||||||
|
snprintf(err, errlen, "cannot open %s", path);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
struct stat st;
|
||||||
|
if (fstat(fd, &st) != 0 || st.st_size <= 0) {
|
||||||
|
close(fd);
|
||||||
|
snprintf(err, errlen, "cannot stat %s (or empty)", path);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
void *p = mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
|
||||||
|
close(fd);
|
||||||
|
if (p == MAP_FAILED) {
|
||||||
|
snprintf(err, errlen, "cannot mmap %s", path);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
int rc = wo_load_buf(m, p, (size_t)st.st_size, err, errlen);
|
||||||
|
munmap(p, (size_t)st.st_size);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
void wo_module_free(wo_module *m) {
|
||||||
|
for (uint32_t i = 0; i < m->const_cnt; i++) free(m->consts[i].s);
|
||||||
|
free(m->consts);
|
||||||
|
free(m->classes);
|
||||||
|
free(m->kindpool);
|
||||||
|
free(m->vtabs);
|
||||||
|
for (uint32_t i = 0; i < m->method_cnt; i++) {
|
||||||
|
free(m->methods[i].code);
|
||||||
|
free(m->methods[i].lines);
|
||||||
|
free(m->methods[i].drops);
|
||||||
|
}
|
||||||
|
free(m->methods);
|
||||||
|
memset(m, 0, sizeof(*m));
|
||||||
|
m->entry = WOB_NONE;
|
||||||
|
}
|
||||||
70
runtime/src/loader.h
Normal file
70
runtime/src/loader.h
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
/* loader.h — .wob loading + the full static-validation contract.
|
||||||
|
* The "no UB on any input" constraint lives here: everything is parsed
|
||||||
|
* through a bounds-checked cursor and COPIED OUT into aligned, malloc'd
|
||||||
|
* structures (misaligned files and input-buffer lifetimes are non-issues);
|
||||||
|
* text constants intern as const-flagged strings. What the loader accepts,
|
||||||
|
* the interpreter trusts forever — the validation list is in loader.c.
|
||||||
|
* Field indexes on GETF/SETF stay runtime checks (registers are untyped):
|
||||||
|
* the spec's residual-check doctrine. */
|
||||||
|
#ifndef WO_LOADER_H
|
||||||
|
#define WO_LOADER_H
|
||||||
|
|
||||||
|
#include "obj.h"
|
||||||
|
|
||||||
|
typedef struct wo_lineent {
|
||||||
|
uint32_t pc, line;
|
||||||
|
} wo_lineent;
|
||||||
|
|
||||||
|
typedef struct wo_dropent {
|
||||||
|
uint32_t pc;
|
||||||
|
uint64_t owned; /* register bitmask: live owned values at pc */
|
||||||
|
uint64_t gc; /* register bitmask: live @gc references at pc */
|
||||||
|
} wo_dropent;
|
||||||
|
|
||||||
|
typedef struct wo_methodrec {
|
||||||
|
uint32_t name; /* constant index of a text constant */
|
||||||
|
uint32_t class_id; /* WOB_NONE = free fn */
|
||||||
|
uint8_t arg_cnt, reg_cnt;
|
||||||
|
uint32_t *code; /* aligned owned copy */
|
||||||
|
uint32_t ninstr;
|
||||||
|
wo_lineent *lines;
|
||||||
|
uint32_t line_cnt;
|
||||||
|
wo_dropent *drops;
|
||||||
|
uint32_t drop_cnt;
|
||||||
|
} wo_methodrec;
|
||||||
|
|
||||||
|
typedef struct wo_const {
|
||||||
|
uint8_t tag; /* WOB_K_INT / WOB_K_TEXT */
|
||||||
|
int64_t i; /* tag INT */
|
||||||
|
wo_str *s; /* tag TEXT: interned, WO_F_CONST, module-owned */
|
||||||
|
} wo_const;
|
||||||
|
|
||||||
|
/* vtable rows expanded flat and sorted by (class_id, slot) for binary
|
||||||
|
* search at ICALL time */
|
||||||
|
typedef struct wo_vtabent {
|
||||||
|
uint32_t class_id, slot, method;
|
||||||
|
} wo_vtabent;
|
||||||
|
|
||||||
|
typedef struct wo_module {
|
||||||
|
wo_const *consts;
|
||||||
|
uint32_t const_cnt;
|
||||||
|
wo_classdesc *classes;
|
||||||
|
uint32_t class_cnt;
|
||||||
|
uint8_t *kindpool; /* pooled field-kind bytes the classes point into */
|
||||||
|
uint32_t slot_cnt; /* total interface slots across all interfaces */
|
||||||
|
wo_vtabent *vtabs;
|
||||||
|
uint32_t vtab_cnt;
|
||||||
|
wo_methodrec *methods;
|
||||||
|
uint32_t method_cnt;
|
||||||
|
uint32_t entry; /* WOB_NONE = none */
|
||||||
|
} wo_module;
|
||||||
|
|
||||||
|
/* 0 ok; -1 reject with a human-readable reason in err (never empty) and
|
||||||
|
* everything parsed so far freed. */
|
||||||
|
int wo_load_buf(wo_module *m, const uint8_t *buf, size_t len, char *err,
|
||||||
|
size_t errlen);
|
||||||
|
/* mmap -> parse -> munmap */
|
||||||
|
int wo_load_file(wo_module *m, const char *path, char *err, size_t errlen);
|
||||||
|
void wo_module_free(wo_module *m);
|
||||||
|
|
||||||
|
#endif /* WO_LOADER_H */
|
||||||
50
runtime/src/main.c
Normal file
50
runtime/src/main.c
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
/* main.c — the wovm CLI. Exit-code contract the toolchain scripts against:
|
||||||
|
* 0 = ran to completion
|
||||||
|
* 1 = trap; one stderr line: "trap CODE in METHOD at line N: MESSAGE"
|
||||||
|
* 2 = usage or load failure (loader's message on stderr)
|
||||||
|
* Heap cap defaults to 64 MiB, overridable via WO_HEAP_MB. */
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#include "vm.h"
|
||||||
|
|
||||||
|
static wo_vm VM; /* 32K value stack: keep it off the C stack */
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (argc != 2) {
|
||||||
|
fprintf(stderr, "usage: wovm <file.wob>\n");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
wo_module mod;
|
||||||
|
char err[256];
|
||||||
|
if (wo_load_file(&mod, argv[1], err, sizeof err) != 0) {
|
||||||
|
fprintf(stderr, "wovm: %s\n", err);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (mod.entry == WOB_NONE) {
|
||||||
|
fprintf(stderr, "wovm: module has no entry method\n");
|
||||||
|
wo_module_free(&mod);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
size_t heap_mb = 64;
|
||||||
|
const char *env = getenv("WO_HEAP_MB");
|
||||||
|
if (env && env[0]) {
|
||||||
|
char *end = NULL;
|
||||||
|
unsigned long v = strtoul(env, &end, 10);
|
||||||
|
if (end && *end == '\0' && v >= 1 && v <= 1048576) heap_mb = v;
|
||||||
|
}
|
||||||
|
if (wo_vm_init(&VM, &mod, heap_mb << 20) != 0) {
|
||||||
|
fprintf(stderr, "wovm: cannot allocate %zu MiB heap\n", heap_mb);
|
||||||
|
wo_module_free(&mod);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
uint64_t ret = 0;
|
||||||
|
wo_err terr;
|
||||||
|
int rc = wo_vm_call(&VM, mod.entry, NULL, 0, &ret, &terr);
|
||||||
|
if (rc != 0)
|
||||||
|
fprintf(stderr, "trap %u in %s at line %u: %s\n", (unsigned)terr.code,
|
||||||
|
terr.method, (unsigned)terr.line, terr.msg);
|
||||||
|
wo_vm_destroy(&VM);
|
||||||
|
wo_module_free(&mod);
|
||||||
|
return rc == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
110
runtime/src/obj.c
Normal file
110
runtime/src/obj.c
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
#include "obj.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static size_t round16(size_t n) { return (n + 15u) & ~(size_t)15u; }
|
||||||
|
|
||||||
|
int wo_arena_init(wo_arena *a, size_t cap) {
|
||||||
|
memset(a, 0, sizeof(*a));
|
||||||
|
a->base = malloc(cap ? cap : 1);
|
||||||
|
if (!a->base) return -1;
|
||||||
|
a->cap = cap;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void wo_arena_destroy(wo_arena *a) {
|
||||||
|
free(a->base);
|
||||||
|
memset(a, 0, sizeof(*a));
|
||||||
|
}
|
||||||
|
|
||||||
|
void *wo_arena_alloc(wo_arena *a, size_t size) {
|
||||||
|
size = round16(size ? size : 1);
|
||||||
|
if (size > WO_ARENA_MAX_CLASS) return malloc(size);
|
||||||
|
size_t cls = size / 16u - 1u;
|
||||||
|
if (a->freelist[cls]) {
|
||||||
|
void *p = a->freelist[cls];
|
||||||
|
memcpy(&a->freelist[cls], p, sizeof(void *));
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
if (a->used + size > a->cap) return NULL; /* region OOM -> trap upstream */
|
||||||
|
void *p = a->base + a->used;
|
||||||
|
a->used += size;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
void wo_arena_free(wo_arena *a, void *p, size_t size) {
|
||||||
|
if (!p) return;
|
||||||
|
size = round16(size ? size : 1);
|
||||||
|
if (size > WO_ARENA_MAX_CLASS) {
|
||||||
|
free(p);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
size_t cls = size / 16u - 1u;
|
||||||
|
memcpy(p, &a->freelist[cls], sizeof(void *));
|
||||||
|
a->freelist[cls] = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
int wo_rt_init(wo_rt *rt, size_t heap_cap, const wo_classdesc *classes,
|
||||||
|
uint32_t class_cnt) {
|
||||||
|
memset(rt, 0, sizeof(*rt));
|
||||||
|
if (wo_arena_init(&rt->arena, heap_cap) != 0) return -1;
|
||||||
|
rt->classes = classes;
|
||||||
|
rt->class_cnt = class_cnt;
|
||||||
|
rt->out = stdout;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void wo_rt_destroy(wo_rt *rt) {
|
||||||
|
free(rt->cycbuf.items);
|
||||||
|
wo_arena_destroy(&rt->arena);
|
||||||
|
memset(rt, 0, sizeof(*rt));
|
||||||
|
}
|
||||||
|
|
||||||
|
wo_hdr *wo_obj_new(wo_rt *rt, uint32_t class_id) {
|
||||||
|
const wo_classdesc *c = &rt->classes[class_id];
|
||||||
|
size_t sz = wo_obj_size(c);
|
||||||
|
wo_hdr *o = wo_arena_alloc(&rt->arena, sz);
|
||||||
|
if (!o) return NULL;
|
||||||
|
memset(o, 0, sz);
|
||||||
|
o->class_id = class_id;
|
||||||
|
if (c->flags & WO_CLASSF_GC) {
|
||||||
|
o->flags = WO_F_GC;
|
||||||
|
o->rc = 1; /* the creating reference */
|
||||||
|
}
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
static wo_str *str_alloc(wo_rt *rt, uint32_t len) {
|
||||||
|
wo_str *s = wo_arena_alloc(&rt->arena, sizeof(wo_str) + len);
|
||||||
|
if (!s) return NULL;
|
||||||
|
memset(&s->h, 0, sizeof(s->h));
|
||||||
|
s->h.class_id = WO_CLS_STR;
|
||||||
|
s->len = len;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
wo_str *wo_str_new(wo_rt *rt, const char *bytes, uint32_t len) {
|
||||||
|
wo_str *s = str_alloc(rt, len);
|
||||||
|
if (!s) return NULL;
|
||||||
|
memcpy(s->data, bytes, len);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
wo_str *wo_str_concat(wo_rt *rt, const wo_str *a, const wo_str *b) {
|
||||||
|
wo_str *s = str_alloc(rt, a->len + b->len);
|
||||||
|
if (!s) return NULL;
|
||||||
|
memcpy(s->data, a->data, a->len);
|
||||||
|
memcpy(s->data + a->len, b->data, b->len);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
int wo_str_eq(const wo_str *a, const wo_str *b) {
|
||||||
|
return a->len == b->len && memcmp(a->data, b->data, a->len) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void wo_str_free(wo_rt *rt, wo_str *s) {
|
||||||
|
if (!s || (s->h.flags & WO_F_CONST)) return; /* interned: outlives all */
|
||||||
|
wo_arena_free(&rt->arena, s, sizeof(wo_str) + s->len);
|
||||||
|
}
|
||||||
63
runtime/src/obj.h
Normal file
63
runtime/src/obj.h
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
/* obj.h — per-shard arena allocator (spec section 4). Object model and
|
||||||
|
* strings extend this header in the next task. */
|
||||||
|
#ifndef WO_OBJ_H
|
||||||
|
#define WO_OBJ_H
|
||||||
|
|
||||||
|
#include "wob.h"
|
||||||
|
|
||||||
|
/* Arena: one malloc'd region, bump allocation, 16-byte-granular size-class
|
||||||
|
* free lists up to 1024 bytes (freed blocks chain through their first word).
|
||||||
|
* Larger sizes use plain malloc/free — the caller always passes the size
|
||||||
|
* back on free, so no size headers exist anywhere. Exhaustion returns NULL;
|
||||||
|
* the VM maps that to WO_T_OOM, never an abort. */
|
||||||
|
#define WO_ARENA_MAX_CLASS 1024u
|
||||||
|
#define WO_ARENA_NCLASSES (WO_ARENA_MAX_CLASS / 16u) /* 64 lists: 16..1024 */
|
||||||
|
|
||||||
|
typedef struct wo_arena {
|
||||||
|
uint8_t *base; /* malloc'd region */
|
||||||
|
size_t cap; /* region capacity in bytes */
|
||||||
|
size_t used; /* bump offset */
|
||||||
|
void *freelist[WO_ARENA_NCLASSES]; /* index = size/16 - 1 */
|
||||||
|
} wo_arena;
|
||||||
|
|
||||||
|
int wo_arena_init(wo_arena *a, size_t cap); /* 0 ok, -1 malloc failure */
|
||||||
|
void wo_arena_destroy(wo_arena *a);
|
||||||
|
void *wo_arena_alloc(wo_arena *a, size_t size); /* NULL = region OOM */
|
||||||
|
void wo_arena_free(wo_arena *a, void *p, size_t size);
|
||||||
|
|
||||||
|
/* Runtime context: what every module needs. One per shard (one total in
|
||||||
|
* milestone 1): the arena, the loaded class table, the cycle-candidate
|
||||||
|
* buffer (filled by gc.c), and the output stream builtin print writes to
|
||||||
|
* (tests point it at a temp file to capture output). */
|
||||||
|
typedef struct wo_rt {
|
||||||
|
wo_arena arena;
|
||||||
|
const wo_classdesc *classes;
|
||||||
|
uint32_t class_cnt;
|
||||||
|
struct {
|
||||||
|
wo_hdr **items;
|
||||||
|
size_t len, cap;
|
||||||
|
} cycbuf;
|
||||||
|
void *out; /* FILE*; kept void* so obj.h needn't pull in stdio */
|
||||||
|
} wo_rt;
|
||||||
|
|
||||||
|
int wo_rt_init(wo_rt *rt, size_t heap_cap, const wo_classdesc *classes,
|
||||||
|
uint32_t class_cnt); /* 0 ok, -1 alloc failure; out = stdout */
|
||||||
|
void wo_rt_destroy(wo_rt *rt);
|
||||||
|
|
||||||
|
/* New zeroed instance of a class-table class. @gc classes get the GC flag
|
||||||
|
* and rc 1 (the creating reference). NULL = OOM (VM traps WO_T_OOM). */
|
||||||
|
wo_hdr *wo_obj_new(wo_rt *rt, uint32_t class_id);
|
||||||
|
|
||||||
|
/* Strings: header + length + inline bytes, class id WO_CLS_STR. */
|
||||||
|
typedef struct wo_str {
|
||||||
|
wo_hdr h;
|
||||||
|
uint32_t len;
|
||||||
|
char data[]; /* len bytes, no NUL */
|
||||||
|
} wo_str;
|
||||||
|
|
||||||
|
wo_str *wo_str_new(wo_rt *rt, const char *bytes, uint32_t len); /* NULL=OOM */
|
||||||
|
wo_str *wo_str_concat(wo_rt *rt, const wo_str *a, const wo_str *b);
|
||||||
|
int wo_str_eq(const wo_str *a, const wo_str *b); /* content equality */
|
||||||
|
void wo_str_free(wo_rt *rt, wo_str *s); /* no-op on WO_F_CONST */
|
||||||
|
|
||||||
|
#endif /* WO_OBJ_H */
|
||||||
474
runtime/src/vm.c
Normal file
474
runtime/src/vm.c
Normal file
|
|
@ -0,0 +1,474 @@
|
||||||
|
#include "vm.h"
|
||||||
|
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "borrow.h"
|
||||||
|
#include "builtin.h"
|
||||||
|
#include "cont.h"
|
||||||
|
#include "gc.h"
|
||||||
|
|
||||||
|
uint32_t wo_vm_depth(const wo_vm *vm) { return vm->depth; }
|
||||||
|
|
||||||
|
int wo_vm_init(wo_vm *vm, const wo_module *mod, size_t heap_cap) {
|
||||||
|
memset(vm, 0, sizeof(*vm));
|
||||||
|
vm->mod = mod;
|
||||||
|
return wo_rt_init(&vm->rt, heap_cap, mod->classes, mod->class_cnt);
|
||||||
|
}
|
||||||
|
|
||||||
|
void wo_vm_destroy(wo_vm *vm) { wo_rt_destroy(&vm->rt); }
|
||||||
|
|
||||||
|
/* Trap unwinding — the spec's "traps never leak" promise (spec §6). Walk
|
||||||
|
* frames innermost to outermost; in each, look up the drop-table entry for
|
||||||
|
* that frame's current instruction (the trap pc for the innermost frame;
|
||||||
|
* the instruction before the saved resume pc — i.e. the CALL — for outer
|
||||||
|
* frames); apply the owned mask by recursive drop and the gc mask by
|
||||||
|
* decrement, nulling registers as they go. A borrow held by a dying
|
||||||
|
* register does not block its drop — the borrower IS the dying frame.
|
||||||
|
* Window overlap is safe: a slot dropped by the callee frame is nulled, so
|
||||||
|
* an outer mask covering the same physical slot sees 0 and skips. */
|
||||||
|
static void vm_unwind(wo_vm *vm) {
|
||||||
|
const wo_module *mod = vm->mod;
|
||||||
|
for (uint32_t d = vm->depth; d > 0; d--) {
|
||||||
|
const wo_frame *f = &vm->frames[d - 1];
|
||||||
|
const wo_methodrec *me = &mod->methods[f->method];
|
||||||
|
uint32_t fpc = (d == vm->depth) ? f->pc : f->pc - 1;
|
||||||
|
const wo_dropent *ent = NULL; /* last entry with pc <= fpc */
|
||||||
|
for (uint32_t i = 0; i < me->drop_cnt && me->drops[i].pc <= fpc; i++)
|
||||||
|
ent = &me->drops[i];
|
||||||
|
if (!ent) continue; /* no entry: nothing live in this frame */
|
||||||
|
uint64_t *R = vm->regs + f->base;
|
||||||
|
for (uint32_t r = 0; r < me->reg_cnt; r++) {
|
||||||
|
uint64_t bit = 1ull << r;
|
||||||
|
if ((ent->owned & bit) && R[r]) {
|
||||||
|
wo_drop_obj(&vm->rt, (wo_hdr *)(uintptr_t)R[r]);
|
||||||
|
R[r] = 0;
|
||||||
|
}
|
||||||
|
if ((ent->gc & bit) && R[r]) {
|
||||||
|
wo_rc_dec(&vm->rt, (wo_hdr *)(uintptr_t)R[r]);
|
||||||
|
R[r] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vm->depth = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Residual runtime checks the loader cannot do statically (registers are
|
||||||
|
* untyped): non-null receiver, an actual class object (not a native
|
||||||
|
* sentinel), field index inside the class. NULL return = trap BOUNDS with
|
||||||
|
* *why naming the reason. */
|
||||||
|
static wo_hdr *recv_check(wo_vm *vm, uint64_t v, uint32_t fidx,
|
||||||
|
const char **why) {
|
||||||
|
if (!v) {
|
||||||
|
*why = "null receiver";
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
wo_hdr *o = (wo_hdr *)(uintptr_t)v;
|
||||||
|
if (o->class_id >= vm->mod->class_cnt) {
|
||||||
|
*why = "native object has no fields";
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (fidx >= vm->mod->classes[o->class_id].field_cnt) {
|
||||||
|
*why = "field index out of range";
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
static wo_str *str_check(uint64_t v, const char **why) {
|
||||||
|
if (!v) {
|
||||||
|
*why = "null text";
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
wo_str *s = (wo_str *)(uintptr_t)v;
|
||||||
|
if (s->h.class_id != WO_CLS_STR) {
|
||||||
|
*why = "not a text value";
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int vm_trap(wo_vm *vm, wo_err *err, uint32_t tcode, const char *fmt,
|
||||||
|
...) {
|
||||||
|
if (err) {
|
||||||
|
const wo_module *mod = vm->mod;
|
||||||
|
const wo_frame *f = &vm->frames[vm->depth - 1];
|
||||||
|
const wo_methodrec *me = &mod->methods[f->method];
|
||||||
|
err->code = tcode;
|
||||||
|
err->line = 0; /* last line-table entry with pc <= trapping pc */
|
||||||
|
for (uint32_t i = 0; i < me->line_cnt && me->lines[i].pc <= f->pc; i++)
|
||||||
|
err->line = me->lines[i].line;
|
||||||
|
const wo_str *nm = mod->consts[me->name].s;
|
||||||
|
int nlen = nm->len < 63 ? (int)nm->len : 63;
|
||||||
|
snprintf(err->method, sizeof(err->method), "%.*s", nlen, nm->data);
|
||||||
|
va_list ap;
|
||||||
|
va_start(ap, fmt);
|
||||||
|
vsnprintf(err->msg, sizeof(err->msg), fmt, ap);
|
||||||
|
va_end(ap);
|
||||||
|
}
|
||||||
|
vm_unwind(vm);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int vm_run(wo_vm *vm, uint64_t *ret, wo_err *err) {
|
||||||
|
const wo_module *mod = vm->mod;
|
||||||
|
const wo_methodrec *me;
|
||||||
|
const uint32_t *code;
|
||||||
|
uint32_t pc;
|
||||||
|
uint64_t *R;
|
||||||
|
uint32_t ins = 0;
|
||||||
|
|
||||||
|
#define RELOAD() \
|
||||||
|
do { \
|
||||||
|
me = &mod->methods[vm->frames[vm->depth - 1].method]; \
|
||||||
|
code = me->code; \
|
||||||
|
pc = vm->frames[vm->depth - 1].pc; \
|
||||||
|
R = vm->regs + vm->frames[vm->depth - 1].base; \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
/* pc is post-incremented at dispatch: the trapping instruction is pc-1 */
|
||||||
|
#define TRAPF(tcode, ...) \
|
||||||
|
do { \
|
||||||
|
vm->frames[vm->depth - 1].pc = pc - 1; \
|
||||||
|
return vm_trap(vm, err, tcode, __VA_ARGS__); \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
RELOAD();
|
||||||
|
|
||||||
|
/* dual-flavor dispatch, one shared case-body text (spec §5): computed
|
||||||
|
* goto under GNU C, plain switch under -DWO_ISO_C — the ISO flavor has
|
||||||
|
* its own make target so the fallback can never rot */
|
||||||
|
#ifndef WO_ISO_C
|
||||||
|
static const void *JT[WOP_MAX + 1] = {
|
||||||
|
[WOP_NOP] = &&L_NOP, [WOP_LOADK] = &&L_LOADK,
|
||||||
|
[WOP_MOVE] = &&L_MOVE, [WOP_ADD] = &&L_ADD,
|
||||||
|
[WOP_SUB] = &&L_SUB, [WOP_MUL] = &&L_MUL,
|
||||||
|
[WOP_DIV] = &&L_DIV, [WOP_NEG] = &&L_NEG,
|
||||||
|
[WOP_CONCAT] = &&L_CONCAT, [WOP_EQ] = &&L_EQ,
|
||||||
|
[WOP_LT] = &&L_LT, [WOP_LE] = &&L_LE,
|
||||||
|
[WOP_EQS] = &&L_EQS, [WOP_JMP] = &&L_JMP,
|
||||||
|
[WOP_JZ] = &&L_JZ, [WOP_CALL] = &&L_CALL,
|
||||||
|
[WOP_ICALL] = &&L_ICALL, [WOP_RET] = &&L_RET,
|
||||||
|
[WOP_RET0] = &&L_RET0, [WOP_NEW] = &&L_NEW,
|
||||||
|
[WOP_GETF] = &&L_GETF, [WOP_SETF] = &&L_SETF,
|
||||||
|
[WOP_DROP] = &&L_DROP, [WOP_BORROW_S] = &&L_BORROW_S,
|
||||||
|
[WOP_BORROW_X] = &&L_BORROW_X, [WOP_RELEASE_S] = &&L_RELEASE_S,
|
||||||
|
[WOP_RELEASE_X] = &&L_RELEASE_X, [WOP_RC_INC] = &&L_RC_INC,
|
||||||
|
[WOP_RC_DEC] = &&L_RC_DEC, [WOP_BUILTIN] = &&L_BUILTIN,
|
||||||
|
[WOP_DB_STUB] = &&L_DB_STUB, [WOP_TRAP] = &&L_TRAP,
|
||||||
|
};
|
||||||
|
#define CASE(name) L_##name
|
||||||
|
#define NEXT() \
|
||||||
|
do { \
|
||||||
|
ins = code[pc++]; \
|
||||||
|
goto *JT[wo_ins_op(ins)]; \
|
||||||
|
} while (0)
|
||||||
|
NEXT();
|
||||||
|
#else
|
||||||
|
#define CASE(name) case WOP_##name
|
||||||
|
#define NEXT() goto dispatch
|
||||||
|
dispatch:
|
||||||
|
ins = code[pc++];
|
||||||
|
switch (wo_ins_op(ins)) {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
CASE(NOP) : NEXT();
|
||||||
|
|
||||||
|
CASE(LOADK) : {
|
||||||
|
const wo_const *k = &mod->consts[wo_ins_bx(ins)];
|
||||||
|
R[wo_ins_a(ins)] = k->tag == WOB_K_INT ? (uint64_t)k->i
|
||||||
|
: (uint64_t)(uintptr_t)k->s;
|
||||||
|
NEXT();
|
||||||
|
}
|
||||||
|
|
||||||
|
CASE(MOVE) : {
|
||||||
|
/* for owned values this IS the move: the compiler guarantees the
|
||||||
|
* source register is dead afterwards */
|
||||||
|
R[wo_ins_a(ins)] = R[wo_ins_b(ins)];
|
||||||
|
NEXT();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* i64 arithmetic: two's-complement wrapping via unsigned math */
|
||||||
|
CASE(ADD) : {
|
||||||
|
R[wo_ins_a(ins)] = R[wo_ins_b(ins)] + R[wo_ins_c(ins)];
|
||||||
|
NEXT();
|
||||||
|
}
|
||||||
|
CASE(SUB) : {
|
||||||
|
R[wo_ins_a(ins)] = R[wo_ins_b(ins)] - R[wo_ins_c(ins)];
|
||||||
|
NEXT();
|
||||||
|
}
|
||||||
|
CASE(MUL) : {
|
||||||
|
R[wo_ins_a(ins)] = R[wo_ins_b(ins)] * R[wo_ins_c(ins)];
|
||||||
|
NEXT();
|
||||||
|
}
|
||||||
|
CASE(DIV) : {
|
||||||
|
int64_t x = (int64_t)R[wo_ins_b(ins)], y = (int64_t)R[wo_ins_c(ins)];
|
||||||
|
if (y == 0) TRAPF(WO_T_DIV0, "division by zero");
|
||||||
|
if (x == INT64_MIN && y == -1)
|
||||||
|
TRAPF(WO_T_DIV0, "INT64_MIN / -1 overflows");
|
||||||
|
R[wo_ins_a(ins)] = (uint64_t)(x / y);
|
||||||
|
NEXT();
|
||||||
|
}
|
||||||
|
CASE(NEG) : {
|
||||||
|
R[wo_ins_a(ins)] = 0u - R[wo_ins_b(ins)];
|
||||||
|
NEXT();
|
||||||
|
}
|
||||||
|
|
||||||
|
CASE(EQ) : {
|
||||||
|
R[wo_ins_a(ins)] = R[wo_ins_b(ins)] == R[wo_ins_c(ins)] ? 1 : 0;
|
||||||
|
NEXT();
|
||||||
|
}
|
||||||
|
CASE(LT) : {
|
||||||
|
R[wo_ins_a(ins)] =
|
||||||
|
(int64_t)R[wo_ins_b(ins)] < (int64_t)R[wo_ins_c(ins)] ? 1 : 0;
|
||||||
|
NEXT();
|
||||||
|
}
|
||||||
|
CASE(LE) : {
|
||||||
|
R[wo_ins_a(ins)] =
|
||||||
|
(int64_t)R[wo_ins_b(ins)] <= (int64_t)R[wo_ins_c(ins)] ? 1 : 0;
|
||||||
|
NEXT();
|
||||||
|
}
|
||||||
|
|
||||||
|
CASE(JMP) : {
|
||||||
|
pc = (uint32_t)((int64_t)pc + wo_ins_sbx(ins));
|
||||||
|
NEXT();
|
||||||
|
}
|
||||||
|
CASE(JZ) : {
|
||||||
|
if (R[wo_ins_a(ins)] == 0)
|
||||||
|
pc = (uint32_t)((int64_t)pc + wo_ins_sbx(ins));
|
||||||
|
NEXT();
|
||||||
|
}
|
||||||
|
|
||||||
|
CASE(CALL) : {
|
||||||
|
/* Lua-style window overlap: callee r0 = caller slot A; args sit at
|
||||||
|
* A..A+argc-1; the return value lands back in slot A */
|
||||||
|
const wo_methodrec *callee = &mod->methods[wo_ins_bx(ins)];
|
||||||
|
uint32_t nbase = vm->frames[vm->depth - 1].base + wo_ins_a(ins);
|
||||||
|
if (vm->depth >= WO_MAX_FRAMES)
|
||||||
|
TRAPF(WO_T_STACK, "frame stack overflow (%u frames)",
|
||||||
|
(unsigned)WO_MAX_FRAMES);
|
||||||
|
if (nbase + callee->reg_cnt > WO_STACK_SLOTS)
|
||||||
|
TRAPF(WO_T_STACK, "value stack overflow");
|
||||||
|
vm->frames[vm->depth - 1].pc = pc;
|
||||||
|
vm->frames[vm->depth].method = wo_ins_bx(ins);
|
||||||
|
vm->frames[vm->depth].pc = 0;
|
||||||
|
vm->frames[vm->depth].base = nbase;
|
||||||
|
vm->depth++;
|
||||||
|
/* zero non-argument registers: drop masks must never see stale bits */
|
||||||
|
memset(vm->regs + nbase + callee->arg_cnt, 0,
|
||||||
|
(size_t)(callee->reg_cnt - callee->arg_cnt) * 8u);
|
||||||
|
RELOAD();
|
||||||
|
NEXT();
|
||||||
|
}
|
||||||
|
|
||||||
|
CASE(RET) : {
|
||||||
|
uint64_t rv = R[wo_ins_a(ins)];
|
||||||
|
vm->regs[vm->frames[vm->depth - 1].base] = rv;
|
||||||
|
vm->depth--;
|
||||||
|
if (vm->depth == 0) {
|
||||||
|
*ret = rv;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
RELOAD();
|
||||||
|
NEXT();
|
||||||
|
}
|
||||||
|
CASE(RET0) : {
|
||||||
|
vm->regs[vm->frames[vm->depth - 1].base] = 0;
|
||||||
|
vm->depth--;
|
||||||
|
if (vm->depth == 0) {
|
||||||
|
*ret = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
RELOAD();
|
||||||
|
NEXT();
|
||||||
|
}
|
||||||
|
|
||||||
|
CASE(NEW) : {
|
||||||
|
wo_hdr *o = wo_obj_new(&vm->rt, wo_ins_bx(ins));
|
||||||
|
if (!o) TRAPF(WO_T_OOM, "out of memory");
|
||||||
|
R[wo_ins_a(ins)] = (uint64_t)(uintptr_t)o;
|
||||||
|
NEXT();
|
||||||
|
}
|
||||||
|
|
||||||
|
CASE(GETF) : {
|
||||||
|
const char *why;
|
||||||
|
wo_hdr *o = recv_check(vm, R[wo_ins_b(ins)], wo_ins_c(ins), &why);
|
||||||
|
if (!o) TRAPF(WO_T_BOUNDS, "%s", why);
|
||||||
|
R[wo_ins_a(ins)] = wo_fields(o)[wo_ins_c(ins)];
|
||||||
|
NEXT();
|
||||||
|
}
|
||||||
|
|
||||||
|
CASE(SETF) : {
|
||||||
|
/* overwriting a non-scalar field does NOT auto-drop the old value:
|
||||||
|
* the compiler emits the drop (format doc) */
|
||||||
|
const char *why;
|
||||||
|
wo_hdr *o = recv_check(vm, R[wo_ins_a(ins)], wo_ins_b(ins), &why);
|
||||||
|
if (!o) TRAPF(WO_T_BOUNDS, "%s", why);
|
||||||
|
wo_fields(o)[wo_ins_b(ins)] = R[wo_ins_c(ins)];
|
||||||
|
NEXT();
|
||||||
|
}
|
||||||
|
|
||||||
|
CASE(DROP) : {
|
||||||
|
uint64_t v = R[wo_ins_a(ins)];
|
||||||
|
if (v) wo_drop_obj(&vm->rt, (wo_hdr *)(uintptr_t)v);
|
||||||
|
R[wo_ins_a(ins)] = 0; /* unwinding must never double-free */
|
||||||
|
NEXT();
|
||||||
|
}
|
||||||
|
|
||||||
|
CASE(BORROW_S) : {
|
||||||
|
uint64_t v = R[wo_ins_a(ins)];
|
||||||
|
if (!v) TRAPF(WO_T_BOUNDS, "null receiver");
|
||||||
|
if (wo_borrow_shared((wo_hdr *)(uintptr_t)v) != 0)
|
||||||
|
TRAPF(WO_T_BORROW, "shared borrow of exclusively borrowed value");
|
||||||
|
NEXT();
|
||||||
|
}
|
||||||
|
CASE(BORROW_X) : {
|
||||||
|
uint64_t v = R[wo_ins_a(ins)];
|
||||||
|
if (!v) TRAPF(WO_T_BOUNDS, "null receiver");
|
||||||
|
if (wo_borrow_excl((wo_hdr *)(uintptr_t)v) != 0)
|
||||||
|
TRAPF(WO_T_BORROW, "exclusive borrow of already borrowed value");
|
||||||
|
NEXT();
|
||||||
|
}
|
||||||
|
CASE(RELEASE_S) : {
|
||||||
|
/* releases are unconditional: the compiler emits them balanced */
|
||||||
|
wo_release_shared((wo_hdr *)(uintptr_t)R[wo_ins_a(ins)]);
|
||||||
|
NEXT();
|
||||||
|
}
|
||||||
|
CASE(RELEASE_X) : {
|
||||||
|
wo_release_excl((wo_hdr *)(uintptr_t)R[wo_ins_a(ins)]);
|
||||||
|
NEXT();
|
||||||
|
}
|
||||||
|
|
||||||
|
CASE(RC_INC) : {
|
||||||
|
uint64_t v = R[wo_ins_a(ins)];
|
||||||
|
if (!v) TRAPF(WO_T_BOUNDS, "null receiver");
|
||||||
|
wo_rc_inc((wo_hdr *)(uintptr_t)v);
|
||||||
|
NEXT();
|
||||||
|
}
|
||||||
|
CASE(RC_DEC) : {
|
||||||
|
uint64_t v = R[wo_ins_a(ins)];
|
||||||
|
if (!v) TRAPF(WO_T_BOUNDS, "null receiver");
|
||||||
|
wo_rc_dec(&vm->rt, (wo_hdr *)(uintptr_t)v);
|
||||||
|
NEXT();
|
||||||
|
}
|
||||||
|
|
||||||
|
CASE(CONCAT) : {
|
||||||
|
const char *why;
|
||||||
|
wo_str *x = str_check(R[wo_ins_b(ins)], &why);
|
||||||
|
if (!x) TRAPF(WO_T_BOUNDS, "%s", why);
|
||||||
|
wo_str *y = str_check(R[wo_ins_c(ins)], &why);
|
||||||
|
if (!y) TRAPF(WO_T_BOUNDS, "%s", why);
|
||||||
|
wo_str *z = wo_str_concat(&vm->rt, x, y);
|
||||||
|
if (!z) TRAPF(WO_T_OOM, "out of memory");
|
||||||
|
R[wo_ins_a(ins)] = (uint64_t)(uintptr_t)z; /* new owned text */
|
||||||
|
NEXT();
|
||||||
|
}
|
||||||
|
CASE(EQS) : {
|
||||||
|
const char *why;
|
||||||
|
wo_str *x = str_check(R[wo_ins_b(ins)], &why);
|
||||||
|
if (!x) TRAPF(WO_T_BOUNDS, "%s", why);
|
||||||
|
wo_str *y = str_check(R[wo_ins_c(ins)], &why);
|
||||||
|
if (!y) TRAPF(WO_T_BOUNDS, "%s", why);
|
||||||
|
R[wo_ins_a(ins)] = wo_str_eq(x, y) ? 1 : 0;
|
||||||
|
NEXT();
|
||||||
|
}
|
||||||
|
|
||||||
|
CASE(BUILTIN) : {
|
||||||
|
const char *bmsg = "builtin failed";
|
||||||
|
int brc = wo_builtin(vm, R, ins, &bmsg);
|
||||||
|
if (brc) TRAPF((uint32_t)brc, "%s", bmsg);
|
||||||
|
NEXT();
|
||||||
|
}
|
||||||
|
|
||||||
|
CASE(ICALL) : {
|
||||||
|
/* structural-interface dispatch (spec §2): binary search the sorted
|
||||||
|
* (class, slot, method) triples by the RECEIVER's class. The
|
||||||
|
* compiler's type checker makes a miss unreachable in compiled
|
||||||
|
* code; the VM keeps the trap as defense (spec §6). */
|
||||||
|
uint64_t v = R[wo_ins_a(ins)];
|
||||||
|
if (!v) TRAPF(WO_T_BOUNDS, "null receiver");
|
||||||
|
wo_hdr *o = (wo_hdr *)(uintptr_t)v;
|
||||||
|
if (o->class_id >= mod->class_cnt)
|
||||||
|
TRAPF(WO_T_BOUNDS, "interface call on a native value");
|
||||||
|
uint32_t slot = wo_ins_bx(ins);
|
||||||
|
const wo_vtabent *hit = NULL;
|
||||||
|
for (uint32_t lo = 0, hi = mod->vtab_cnt; lo < hi;) {
|
||||||
|
uint32_t mid = lo + (hi - lo) / 2;
|
||||||
|
const wo_vtabent *e = &mod->vtabs[mid];
|
||||||
|
if (e->class_id < o->class_id ||
|
||||||
|
(e->class_id == o->class_id && e->slot < slot)) {
|
||||||
|
lo = mid + 1;
|
||||||
|
} else if (e->class_id == o->class_id && e->slot == slot) {
|
||||||
|
hit = e;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
hi = mid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hit) TRAPF(WO_T_BOUNDS, "no vtable entry for receiver class");
|
||||||
|
/* exactly the CALL sequence at the same window base: the receiver
|
||||||
|
* already sits in slot A = callee's self */
|
||||||
|
const wo_methodrec *callee = &mod->methods[hit->method];
|
||||||
|
if ((uint32_t)wo_ins_a(ins) + callee->arg_cnt > me->reg_cnt)
|
||||||
|
TRAPF(WO_T_STACK, "call window exceeds frame"); /* runtime: callee
|
||||||
|
unknown to the loader here */
|
||||||
|
uint32_t nbase = vm->frames[vm->depth - 1].base + wo_ins_a(ins);
|
||||||
|
if (vm->depth >= WO_MAX_FRAMES)
|
||||||
|
TRAPF(WO_T_STACK, "frame stack overflow (%u frames)",
|
||||||
|
(unsigned)WO_MAX_FRAMES);
|
||||||
|
if (nbase + callee->reg_cnt > WO_STACK_SLOTS)
|
||||||
|
TRAPF(WO_T_STACK, "value stack overflow");
|
||||||
|
vm->frames[vm->depth - 1].pc = pc;
|
||||||
|
vm->frames[vm->depth].method = hit->method;
|
||||||
|
vm->frames[vm->depth].pc = 0;
|
||||||
|
vm->frames[vm->depth].base = nbase;
|
||||||
|
vm->depth++;
|
||||||
|
memset(vm->regs + nbase + callee->arg_cnt, 0,
|
||||||
|
(size_t)(callee->reg_cnt - callee->arg_cnt) * 8u);
|
||||||
|
RELOAD();
|
||||||
|
NEXT();
|
||||||
|
}
|
||||||
|
|
||||||
|
CASE(DB_STUB) : { TRAPF(WO_T_DB, "engine not linked"); }
|
||||||
|
|
||||||
|
CASE(TRAP) : { TRAPF(wo_ins_bx(ins), "explicit trap"); }
|
||||||
|
|
||||||
|
#ifdef WO_ISO_C
|
||||||
|
default:
|
||||||
|
TRAPF(WO_T_EXPLICIT, "unknown opcode"); /* unreachable: loader */
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#undef CASE
|
||||||
|
#undef NEXT
|
||||||
|
#undef RELOAD
|
||||||
|
#undef TRAPF
|
||||||
|
}
|
||||||
|
|
||||||
|
int wo_vm_call(wo_vm *vm, uint32_t method_idx, const uint64_t *args,
|
||||||
|
uint32_t argc, uint64_t *ret, wo_err *err) {
|
||||||
|
if (err) memset(err, 0, sizeof(*err));
|
||||||
|
if (method_idx >= vm->mod->method_cnt) {
|
||||||
|
if (err) {
|
||||||
|
err->code = WO_T_EXPLICIT;
|
||||||
|
snprintf(err->msg, sizeof(err->msg), "no such method");
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
const wo_methodrec *me = &vm->mod->methods[method_idx];
|
||||||
|
if (argc != me->arg_cnt) {
|
||||||
|
if (err) {
|
||||||
|
err->code = WO_T_EXPLICIT;
|
||||||
|
snprintf(err->msg, sizeof(err->msg), "bad call arity");
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
vm->depth = 1;
|
||||||
|
vm->frames[0].method = method_idx;
|
||||||
|
vm->frames[0].pc = 0;
|
||||||
|
vm->frames[0].base = 0;
|
||||||
|
if (argc) memcpy(vm->regs, args, (size_t)argc * 8u);
|
||||||
|
memset(vm->regs + argc, 0, (size_t)(me->reg_cnt - argc) * 8u);
|
||||||
|
return vm_run(vm, ret, err);
|
||||||
|
}
|
||||||
44
runtime/src/vm.h
Normal file
44
runtime/src/vm.h
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
/* vm.h — the register interpreter (spec §5): Lua-style window-overlap
|
||||||
|
* calls, dual-flavor dispatch (computed goto / WO_ISO_C switch), structured
|
||||||
|
* trap errors with line lookup, drop-map unwinding on trap. */
|
||||||
|
#ifndef WO_VM_H
|
||||||
|
#define WO_VM_H
|
||||||
|
|
||||||
|
#include "loader.h"
|
||||||
|
|
||||||
|
/* structured trap error (spec §6): one shape forever — the CLI prints it,
|
||||||
|
* the future service layer maps it to HTTP */
|
||||||
|
typedef struct wo_err {
|
||||||
|
uint32_t code; /* WO_T_* */
|
||||||
|
uint32_t line; /* source line at the trapping pc; 0 = unknown */
|
||||||
|
char method[64]; /* name of the trapping method */
|
||||||
|
char msg[96]; /* human-readable reason */
|
||||||
|
} wo_err;
|
||||||
|
|
||||||
|
typedef struct wo_frame {
|
||||||
|
uint32_t method; /* method index */
|
||||||
|
uint32_t pc; /* saved resume pc (next instruction) */
|
||||||
|
uint32_t base; /* register-window base in the value stack */
|
||||||
|
} wo_frame;
|
||||||
|
|
||||||
|
typedef struct wo_vm {
|
||||||
|
const wo_module *mod;
|
||||||
|
wo_rt rt;
|
||||||
|
uint64_t regs[WO_STACK_SLOTS];
|
||||||
|
wo_frame frames[WO_MAX_FRAMES];
|
||||||
|
uint32_t depth;
|
||||||
|
} wo_vm;
|
||||||
|
|
||||||
|
/* heap_cap = arena byte capacity (the CLI's WO_HEAP_MB feeds this) */
|
||||||
|
int wo_vm_init(wo_vm *vm, const wo_module *mod, size_t heap_cap);
|
||||||
|
void wo_vm_destroy(wo_vm *vm);
|
||||||
|
|
||||||
|
/* Call a method with raw argument words. argc must equal the method's
|
||||||
|
* declared arity. 0 = done, *ret filled; -1 = trapped, *err filled and the
|
||||||
|
* stack fully unwound (depth 0). */
|
||||||
|
int wo_vm_call(wo_vm *vm, uint32_t method_idx, const uint64_t *args,
|
||||||
|
uint32_t argc, uint64_t *ret, wo_err *err);
|
||||||
|
|
||||||
|
uint32_t wo_vm_depth(const wo_vm *vm);
|
||||||
|
|
||||||
|
#endif /* WO_VM_H */
|
||||||
177
runtime/src/wob.h
Normal file
177
runtime/src/wob.h
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
/* wob.h — the .wob v1 compiler↔VM contract.
|
||||||
|
* Single source of truth for format constants, opcodes, field kinds, trap
|
||||||
|
* codes, builtin ids, limits, and the 16-byte object header shared by every
|
||||||
|
* runtime module. Normative prose: docs/plan/oop-vm/00-wob-format.md.
|
||||||
|
* All on-disk integers are little-endian; x86-64/ARM64 Linux only (M1).
|
||||||
|
*/
|
||||||
|
#ifndef WO_WOB_H
|
||||||
|
#define WO_WOB_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/* ---- file header (44 bytes, absolute offsets) ---- */
|
||||||
|
#define WOB_MAGIC 0x31424F57u /* "WOB1" read as LE u32 */
|
||||||
|
#define WOB_VERSION 1u
|
||||||
|
#define WOB_HDR_SIZE 44u
|
||||||
|
#define WOB_OFF_MAGIC 0u
|
||||||
|
#define WOB_OFF_VERSION 4u
|
||||||
|
#define WOB_OFF_CONST 8u /* u32 offset, u32 count */
|
||||||
|
#define WOB_OFF_CLASS 16u
|
||||||
|
#define WOB_OFF_IFACE 24u
|
||||||
|
#define WOB_OFF_METHOD 32u
|
||||||
|
#define WOB_OFF_ENTRY 40u
|
||||||
|
#define WOB_NONE 0xFFFFFFFFu /* "no entry method" / "free fn" class id */
|
||||||
|
|
||||||
|
/* ---- constant pool tags ---- */
|
||||||
|
#define WOB_K_INT 0u /* tag byte, then i64 */
|
||||||
|
#define WOB_K_TEXT 1u /* tag byte, then u32 len + bytes (no NUL) */
|
||||||
|
|
||||||
|
/* ---- field kinds (one byte per field in the class table) ---- */
|
||||||
|
enum {
|
||||||
|
WO_K_SCALAR = 0,
|
||||||
|
WO_K_OWNED = 1,
|
||||||
|
WO_K_GCREF = 2,
|
||||||
|
WO_K_TEXT = 3,
|
||||||
|
WO_K_MULTI = 4,
|
||||||
|
WO_K_MAP = 5,
|
||||||
|
};
|
||||||
|
#define WO_K_MAX 5u
|
||||||
|
|
||||||
|
/* ---- loader-enforced limits ---- */
|
||||||
|
#define WO_MAX_REGS 64u
|
||||||
|
#define WO_STACK_SLOTS 4096u
|
||||||
|
#define WO_MAX_FRAMES 256u
|
||||||
|
|
||||||
|
/* ---- object header: every heap value carries this (spec section 4) ---- */
|
||||||
|
typedef struct wo_hdr {
|
||||||
|
uint32_t class_id; /* class-table index or a WO_CLS_* sentinel */
|
||||||
|
uint16_t shard_id; /* always 0 in milestone 1; reserved for sub-project 2 */
|
||||||
|
uint8_t flags;
|
||||||
|
uint8_t pad;
|
||||||
|
uint32_t borrow; /* WO_BORROW_FREE / reader count / WO_BORROW_EXCL */
|
||||||
|
uint32_t rc; /* strong count, @gc objects only */
|
||||||
|
} wo_hdr;
|
||||||
|
_Static_assert(sizeof(wo_hdr) == 16, "object header must be exactly 16 bytes");
|
||||||
|
|
||||||
|
/* header flags */
|
||||||
|
#define WO_F_GC 0x01u /* instance of a @gc class: rc rules apply */
|
||||||
|
#define WO_F_BUF 0x02u /* sitting in the cycle-candidate buffer */
|
||||||
|
#define WO_F_CONST 0x04u /* loader-interned constant (strings): free is a no-op */
|
||||||
|
/* two color bits for Bacon–Rajan trial deletion */
|
||||||
|
#define WO_F_COLOR 0x18u
|
||||||
|
#define WO_COLOR_BLACK 0x00u
|
||||||
|
#define WO_COLOR_GRAY 0x08u
|
||||||
|
#define WO_COLOR_WHITE 0x10u
|
||||||
|
|
||||||
|
/* native class-id sentinels (top of the u32 range; loader rejects user
|
||||||
|
* class counts anywhere near these) */
|
||||||
|
#define WO_CLS_STR 0xFFFFFFFCu
|
||||||
|
#define WO_CLS_MAP 0xFFFFFFFDu
|
||||||
|
#define WO_CLS_MULTI 0xFFFFFFFEu
|
||||||
|
|
||||||
|
/* borrow word states */
|
||||||
|
#define WO_BORROW_FREE 0u
|
||||||
|
#define WO_BORROW_EXCL 0xFFFFFFFFu
|
||||||
|
|
||||||
|
/* ---- trap codes (spec section 6) ---- */
|
||||||
|
enum {
|
||||||
|
WO_T_DIV0 = 1,
|
||||||
|
WO_T_BORROW = 2,
|
||||||
|
WO_T_STACK = 3,
|
||||||
|
WO_T_OOM = 4,
|
||||||
|
WO_T_DB = 5,
|
||||||
|
WO_T_BOUNDS = 6,
|
||||||
|
WO_T_KEY = 7,
|
||||||
|
WO_T_EXPLICIT = 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---- opcodes (spec section 5; semantics in the format doc) ---- */
|
||||||
|
enum {
|
||||||
|
WOP_NOP = 0,
|
||||||
|
WOP_LOADK = 1, /* A Bx : r[A] = const[Bx] */
|
||||||
|
WOP_MOVE = 2, /* A B : r[A] = r[B] (owned move: source is dead) */
|
||||||
|
WOP_ADD = 3, /* A B C: i64, wrapping */
|
||||||
|
WOP_SUB = 4,
|
||||||
|
WOP_MUL = 5,
|
||||||
|
WOP_DIV = 6, /* traps DIV0 on 0 and INT64_MIN / -1 */
|
||||||
|
WOP_NEG = 7, /* A B */
|
||||||
|
WOP_CONCAT = 8, /* A B C: new owned text */
|
||||||
|
WOP_EQ = 9, /* A B C: i64 compare, 0/1 */
|
||||||
|
WOP_LT = 10,
|
||||||
|
WOP_LE = 11,
|
||||||
|
WOP_EQS = 12, /* A B C: text content equality */
|
||||||
|
WOP_JMP = 13, /* sBx */
|
||||||
|
WOP_JZ = 14, /* A sBx: jump when r[A] == 0 */
|
||||||
|
WOP_CALL = 15, /* A Bx : call method Bx, window at caller base + A */
|
||||||
|
WOP_ICALL = 16, /* A Bx : interface call by global slot id Bx */
|
||||||
|
WOP_RET = 17, /* A */
|
||||||
|
WOP_RET0 = 18,
|
||||||
|
WOP_NEW = 19, /* A Bx : zeroed instance of class Bx */
|
||||||
|
WOP_GETF = 20, /* A B C: r[A] = field C of object r[B] */
|
||||||
|
WOP_SETF = 21, /* A B C: field B of object r[A] = r[C]; no auto-drop */
|
||||||
|
WOP_DROP = 22, /* A : recursive owned drop, nulls the register */
|
||||||
|
WOP_BORROW_S = 23,
|
||||||
|
WOP_BORROW_X = 24,
|
||||||
|
WOP_RELEASE_S = 25,
|
||||||
|
WOP_RELEASE_X = 26,
|
||||||
|
WOP_RC_INC = 27,
|
||||||
|
WOP_RC_DEC = 28,
|
||||||
|
WOP_BUILTIN = 29, /* A B C: r[A] = builtin C, args from r[B] */
|
||||||
|
WOP_DB_STUB = 30, /* traps WO_T_DB "engine not linked" */
|
||||||
|
WOP_TRAP = 31, /* Bx: explicit trap */
|
||||||
|
};
|
||||||
|
#define WOP_MAX 31u
|
||||||
|
|
||||||
|
/* ---- builtin ids (WOP_BUILTIN operand C) ---- */
|
||||||
|
enum {
|
||||||
|
WO_B_NOW = 0, /* () -> i64 wall-clock ms */
|
||||||
|
WO_B_PRINT = 1, /* (text) newline-terminated to rt output stream */
|
||||||
|
WO_B_PRINT_INT = 2, /* (i64) */
|
||||||
|
WO_B_WORDS = 3, /* (text) -> i64 whitespace token count */
|
||||||
|
WO_B_MULTI_NEW = 4, /* elem kind immediate in B */
|
||||||
|
WO_B_MULTI_PUSH = 5,
|
||||||
|
WO_B_MULTI_GET = 6,
|
||||||
|
WO_B_COUNT = 7,
|
||||||
|
WO_B_LATEST = 8,
|
||||||
|
WO_B_MAP_NEW = 9, /* key/val kind nibbles immediate in B */
|
||||||
|
WO_B_MAP_SET = 10,
|
||||||
|
WO_B_MAP_GET = 11, /* missing key traps WO_T_KEY */
|
||||||
|
WO_B_MAP_HAS = 12,
|
||||||
|
};
|
||||||
|
#define WO_B_MAX 12u
|
||||||
|
|
||||||
|
/* ---- instruction encode/decode: op:8 A:8 then B:8 C:8 or Bx:16 ---- */
|
||||||
|
static inline uint32_t wo_ins_abc(uint8_t op, uint8_t a, uint8_t b, uint8_t c) {
|
||||||
|
return (uint32_t)op | ((uint32_t)a << 8) | ((uint32_t)b << 16) | ((uint32_t)c << 24);
|
||||||
|
}
|
||||||
|
static inline uint32_t wo_ins_abx(uint8_t op, uint8_t a, uint16_t bx) {
|
||||||
|
return (uint32_t)op | ((uint32_t)a << 8) | ((uint32_t)bx << 16);
|
||||||
|
}
|
||||||
|
/* signed jumps: sBx encodes as Bx - 32768 */
|
||||||
|
static inline uint32_t wo_ins_asbx(uint8_t op, uint8_t a, int32_t sbx) {
|
||||||
|
return wo_ins_abx(op, a, (uint16_t)(sbx + 32768));
|
||||||
|
}
|
||||||
|
static inline uint8_t wo_ins_op(uint32_t i) { return (uint8_t)(i & 0xFFu); }
|
||||||
|
static inline uint8_t wo_ins_a(uint32_t i) { return (uint8_t)((i >> 8) & 0xFFu); }
|
||||||
|
static inline uint8_t wo_ins_b(uint32_t i) { return (uint8_t)((i >> 16) & 0xFFu); }
|
||||||
|
static inline uint8_t wo_ins_c(uint32_t i) { return (uint8_t)((i >> 24) & 0xFFu); }
|
||||||
|
static inline uint16_t wo_ins_bx(uint32_t i) { return (uint16_t)(i >> 16); }
|
||||||
|
static inline int32_t wo_ins_sbx(uint32_t i) { return (int32_t)wo_ins_bx(i) - 32768; }
|
||||||
|
|
||||||
|
/* ---- class descriptor shared by loader and runtime ---- */
|
||||||
|
typedef struct wo_classdesc {
|
||||||
|
uint32_t name; /* constant index of the class name */
|
||||||
|
uint32_t flags; /* bit0: instances are @gc */
|
||||||
|
uint32_t field_cnt;
|
||||||
|
const uint8_t *kinds; /* field_cnt kind bytes, declaration order */
|
||||||
|
} wo_classdesc;
|
||||||
|
#define WO_CLASSF_GC 0x01u
|
||||||
|
|
||||||
|
/* runtime object layout: 16-byte header then one 8-byte slot per field */
|
||||||
|
static inline size_t wo_obj_size(const wo_classdesc *c) {
|
||||||
|
return sizeof(wo_hdr) + (size_t)c->field_cnt * 8u;
|
||||||
|
}
|
||||||
|
static inline uint64_t *wo_fields(wo_hdr *o) { return (uint64_t *)(o + 1); }
|
||||||
|
|
||||||
|
#endif /* WO_WOB_H */
|
||||||
0
runtime/test/.gitkeep
Normal file
0
runtime/test/.gitkeep
Normal file
31
runtime/test/cli_smoke.sh
Executable file
31
runtime/test/cli_smoke.sh
Executable file
|
|
@ -0,0 +1,31 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# cli_smoke — end-to-end check of the wovm CLI exit-code contract:
|
||||||
|
# 0 = success, 1 = trap (one stderr line: "trap CODE in METHOD at line N:
|
||||||
|
# MESSAGE"), 2 = usage/load failure. Fixtures come from mkwob.
|
||||||
|
set -u
|
||||||
|
cd "$(dirname "$0")/.." || exit 1
|
||||||
|
|
||||||
|
make wovm build/mkwob >/dev/null || { echo "cli_smoke: build failed"; exit 1; }
|
||||||
|
./build/mkwob build || { echo "cli_smoke: mkwob failed"; exit 1; }
|
||||||
|
|
||||||
|
# success path: stdout diffed, exit 0
|
||||||
|
out=$(./wovm build/hello.wob)
|
||||||
|
rc=$?
|
||||||
|
[ "$rc" -eq 0 ] || { echo "cli_smoke: hello exit $rc, want 0"; exit 1; }
|
||||||
|
expected=$'hello, wovm\n42'
|
||||||
|
[ "$out" = "$expected" ] || { echo "cli_smoke: unexpected stdout: $out"; exit 1; }
|
||||||
|
|
||||||
|
# trap path: exit 1, fixed stderr shape
|
||||||
|
./wovm build/trap.wob >/dev/null 2>build/trap.err
|
||||||
|
rc=$?
|
||||||
|
[ "$rc" -eq 1 ] || { echo "cli_smoke: trap exit $rc, want 1"; exit 1; }
|
||||||
|
grep -q '^trap 1 in main at line 7: division by zero$' build/trap.err ||
|
||||||
|
{ echo "cli_smoke: bad trap line: $(cat build/trap.err)"; exit 1; }
|
||||||
|
|
||||||
|
# load-failure path: exit 2
|
||||||
|
./wovm build/does-not-exist.wob 2>/dev/null
|
||||||
|
[ $? -eq 2 ] || { echo "cli_smoke: missing-file exit not 2"; exit 1; }
|
||||||
|
./wovm 2>/dev/null
|
||||||
|
[ $? -eq 2 ] || { echo "cli_smoke: usage exit not 2"; exit 1; }
|
||||||
|
|
||||||
|
echo "cli_smoke: OK"
|
||||||
62
runtime/test/mkwob.c
Normal file
62
runtime/test/mkwob.c
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
/* mkwob — fixture generator for the CLI smoke test, built from the
|
||||||
|
* test-side assembler: writes <outdir>/hello.wob and <outdir>/trap.wob. */
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#include "wob_build.h"
|
||||||
|
|
||||||
|
static int write_img(const char *dir, const char *name, uint8_t *img,
|
||||||
|
size_t len) {
|
||||||
|
char path[512];
|
||||||
|
snprintf(path, sizeof path, "%s/%s", dir, name);
|
||||||
|
FILE *f = fopen(path, "wb");
|
||||||
|
if (!f) {
|
||||||
|
fprintf(stderr, "mkwob: cannot write %s\n", path);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
size_t n = fwrite(img, 1, len, f);
|
||||||
|
fclose(f);
|
||||||
|
free(img);
|
||||||
|
return n == len ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (argc != 2) {
|
||||||
|
fprintf(stderr, "usage: mkwob <outdir>\n");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
size_t len;
|
||||||
|
|
||||||
|
/* hello.wob: print "hello, wovm", print_int 42 */
|
||||||
|
wb_t *b = wb_new();
|
||||||
|
uint32_t kh = wb_const_text(b, "hello, wovm");
|
||||||
|
uint32_t k42 = wb_const_int(b, 42);
|
||||||
|
uint32_t kn = wb_const_text(b, "main");
|
||||||
|
uint32_t hello[] = {
|
||||||
|
wo_ins_abx(WOP_LOADK, 1, (uint16_t)kh),
|
||||||
|
wo_ins_abc(WOP_BUILTIN, 0, 1, WO_B_PRINT),
|
||||||
|
wo_ins_abx(WOP_LOADK, 1, (uint16_t)k42),
|
||||||
|
wo_ins_abc(WOP_BUILTIN, 0, 1, WO_B_PRINT_INT),
|
||||||
|
wo_ins_abc(WOP_RET0, 0, 0, 0),
|
||||||
|
};
|
||||||
|
wb_entry(b, wb_method(b, kn, WOB_NONE, 0, 2, hello, 5, NULL, 0, NULL, 0));
|
||||||
|
uint8_t *img = wb_finish(b, &len);
|
||||||
|
if (write_img(argv[1], "hello.wob", img, len) != 0) return 1;
|
||||||
|
|
||||||
|
/* trap.wob: division by zero at source line 7 */
|
||||||
|
b = wb_new();
|
||||||
|
uint32_t k1 = wb_const_int(b, 1);
|
||||||
|
uint32_t k0 = wb_const_int(b, 0);
|
||||||
|
kn = wb_const_text(b, "main");
|
||||||
|
uint32_t trap[] = {
|
||||||
|
wo_ins_abx(WOP_LOADK, 0, (uint16_t)k1),
|
||||||
|
wo_ins_abx(WOP_LOADK, 1, (uint16_t)k0),
|
||||||
|
wo_ins_abc(WOP_DIV, 2, 0, 1),
|
||||||
|
wo_ins_abc(WOP_RET, 2, 0, 0),
|
||||||
|
};
|
||||||
|
uint32_t lines[] = {0, 5, 2, 7};
|
||||||
|
wb_entry(b, wb_method(b, kn, WOB_NONE, 0, 3, trap, 4, lines, 2, NULL, 0));
|
||||||
|
img = wb_finish(b, &len);
|
||||||
|
if (write_img(argv[1], "trap.wob", img, len) != 0) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
50
runtime/test/t.h
Normal file
50
runtime/test/t.h
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
/* t.h — tiny assert harness. No framework, per doctrine: counters + report. */
|
||||||
|
#ifndef WO_T_H
|
||||||
|
#define WO_T_H
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static int t_pass, t_fail;
|
||||||
|
|
||||||
|
#define T_CHECK(cond) \
|
||||||
|
do { \
|
||||||
|
if (cond) { \
|
||||||
|
t_pass++; \
|
||||||
|
} else { \
|
||||||
|
t_fail++; \
|
||||||
|
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
#define T_EQ(got, want) \
|
||||||
|
do { \
|
||||||
|
long long g_ = (long long)(got), w_ = (long long)(want); \
|
||||||
|
if (g_ == w_) { \
|
||||||
|
t_pass++; \
|
||||||
|
} else { \
|
||||||
|
t_fail++; \
|
||||||
|
fprintf(stderr, "FAIL %s:%d: %s == %lld, want %lld\n", __FILE__, \
|
||||||
|
__LINE__, #got, g_, w_); \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
#define T_STREQ(got, want) \
|
||||||
|
do { \
|
||||||
|
const char *g_ = (got), *w_ = (want); \
|
||||||
|
if (g_ && w_ && strcmp(g_, w_) == 0) { \
|
||||||
|
t_pass++; \
|
||||||
|
} else { \
|
||||||
|
t_fail++; \
|
||||||
|
fprintf(stderr, "FAIL %s:%d: %s == \"%s\", want \"%s\"\n", \
|
||||||
|
__FILE__, __LINE__, #got, g_ ? g_ : "(null)", \
|
||||||
|
w_ ? w_ : "(null)"); \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
static int t_report(const char *name) {
|
||||||
|
fprintf(stderr, "%s: %d pass, %d fail\n", name, t_pass, t_fail);
|
||||||
|
return t_fail ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* WO_T_H */
|
||||||
47
runtime/test/test_arena.c
Normal file
47
runtime/test/test_arena.c
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
/* test_arena — size-class reuse, region OOM, malloc fallback above 1024. */
|
||||||
|
#include "obj.h"
|
||||||
|
#include "t.h"
|
||||||
|
|
||||||
|
static void test_size_class_reuse(void) {
|
||||||
|
wo_arena a;
|
||||||
|
T_EQ(wo_arena_init(&a, 4096), 0);
|
||||||
|
void *p = wo_arena_alloc(&a, 48);
|
||||||
|
T_CHECK(p != NULL);
|
||||||
|
wo_arena_free(&a, p, 48);
|
||||||
|
/* same size class: the freed block must come straight back */
|
||||||
|
void *q = wo_arena_alloc(&a, 48);
|
||||||
|
T_CHECK(q == p);
|
||||||
|
wo_arena_free(&a, q, 48);
|
||||||
|
wo_arena_destroy(&a);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_region_oom_returns_null(void) {
|
||||||
|
wo_arena a;
|
||||||
|
T_EQ(wo_arena_init(&a, 256), 0);
|
||||||
|
void *p1 = wo_arena_alloc(&a, 128);
|
||||||
|
void *p2 = wo_arena_alloc(&a, 128);
|
||||||
|
T_CHECK(p1 != NULL && p2 != NULL);
|
||||||
|
/* region exhausted and nothing on the free lists: null, never abort */
|
||||||
|
T_CHECK(wo_arena_alloc(&a, 128) == NULL);
|
||||||
|
/* freeing one block makes its class allocatable again */
|
||||||
|
wo_arena_free(&a, p1, 128);
|
||||||
|
T_CHECK(wo_arena_alloc(&a, 128) == p1);
|
||||||
|
wo_arena_destroy(&a);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_large_bypasses_region(void) {
|
||||||
|
wo_arena a;
|
||||||
|
T_EQ(wo_arena_init(&a, 64), 0); /* tiny region on purpose */
|
||||||
|
/* >1024 goes to plain malloc/free regardless of region capacity */
|
||||||
|
void *big = wo_arena_alloc(&a, 4096);
|
||||||
|
T_CHECK(big != NULL);
|
||||||
|
wo_arena_free(&a, big, 4096);
|
||||||
|
wo_arena_destroy(&a);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
test_size_class_reuse();
|
||||||
|
test_region_oom_returns_null();
|
||||||
|
test_large_bypasses_region();
|
||||||
|
return t_report("test_arena");
|
||||||
|
}
|
||||||
37
runtime/test/test_borrow.c
Normal file
37
runtime/test/test_borrow.c
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
/* test_borrow — the borrow-word state machine (spec section 4). */
|
||||||
|
#include "borrow.h"
|
||||||
|
#include "t.h"
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
wo_hdr o = {0};
|
||||||
|
|
||||||
|
/* free -> shared -> shared: readers stack */
|
||||||
|
T_EQ(wo_borrow_shared(&o), 0);
|
||||||
|
T_EQ(wo_borrow_shared(&o), 0);
|
||||||
|
T_EQ(o.borrow, 2);
|
||||||
|
|
||||||
|
/* shared blocks exclusive */
|
||||||
|
T_EQ(wo_borrow_excl(&o), -1);
|
||||||
|
|
||||||
|
/* release readers back to free */
|
||||||
|
wo_release_shared(&o);
|
||||||
|
wo_release_shared(&o);
|
||||||
|
T_EQ(o.borrow, WO_BORROW_FREE);
|
||||||
|
|
||||||
|
/* free -> exclusive */
|
||||||
|
T_EQ(wo_borrow_excl(&o), 0);
|
||||||
|
T_EQ(o.borrow, WO_BORROW_EXCL);
|
||||||
|
|
||||||
|
/* exclusive blocks both */
|
||||||
|
T_EQ(wo_borrow_excl(&o), -1);
|
||||||
|
T_EQ(wo_borrow_shared(&o), -1);
|
||||||
|
|
||||||
|
/* release restores free; full cycle works again */
|
||||||
|
wo_release_excl(&o);
|
||||||
|
T_EQ(o.borrow, WO_BORROW_FREE);
|
||||||
|
T_EQ(wo_borrow_shared(&o), 0);
|
||||||
|
wo_release_shared(&o);
|
||||||
|
T_EQ(o.borrow, WO_BORROW_FREE);
|
||||||
|
|
||||||
|
return t_report("test_borrow");
|
||||||
|
}
|
||||||
185
runtime/test/test_builtin.c
Normal file
185
runtime/test/test_builtin.c
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
/* test_builtin — builtins driven from bytecode: print output captured and
|
||||||
|
* diffed (the same observation seam the conformance corpus will use),
|
||||||
|
* containers, words, missing-key and empty-latest traps, DB_STUB, and the
|
||||||
|
* text ops CONCAT/EQS. */
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#include "loader.h"
|
||||||
|
#include "t.h"
|
||||||
|
#include "vm.h"
|
||||||
|
#include "wob_build.h"
|
||||||
|
|
||||||
|
static wo_vm VM;
|
||||||
|
static wo_module MOD;
|
||||||
|
|
||||||
|
/* module with one method per scenario; built once */
|
||||||
|
enum { M_PRINT, M_MULTI, M_MAP, M_KEY, M_LATEST, M_WORDS, M_DB, M_CONCAT };
|
||||||
|
|
||||||
|
static uint8_t *build_module(size_t *len) {
|
||||||
|
wb_t *b = wb_new();
|
||||||
|
uint32_t khi = wb_const_text(b, "hi"); /* 0 */
|
||||||
|
uint32_t k5 = wb_const_int(b, 5); /* 1 */
|
||||||
|
uint32_t k9 = wb_const_int(b, 9); /* 2 */
|
||||||
|
uint32_t k100 = wb_const_int(b, 100); /* 3 */
|
||||||
|
uint32_t k1 = wb_const_int(b, 1); /* 4 */
|
||||||
|
uint32_t kwords = wb_const_text(b, " two words \t here\n"); /* 5 */
|
||||||
|
uint32_t kab = wb_const_text(b, "ab"); /* 6 */
|
||||||
|
uint32_t kcd = wb_const_text(b, "cd"); /* 7 */
|
||||||
|
uint32_t kabcd = wb_const_text(b, "abcd"); /* 8 */
|
||||||
|
uint32_t kn = wb_const_text(b, "m"); /* shared method name */
|
||||||
|
|
||||||
|
{ /* M_PRINT: print "hi", print_int 5 */
|
||||||
|
uint32_t code[] = {
|
||||||
|
wo_ins_abx(WOP_LOADK, 1, (uint16_t)khi),
|
||||||
|
wo_ins_abc(WOP_BUILTIN, 0, 1, WO_B_PRINT),
|
||||||
|
wo_ins_abx(WOP_LOADK, 1, (uint16_t)k5),
|
||||||
|
wo_ins_abc(WOP_BUILTIN, 0, 1, WO_B_PRINT_INT),
|
||||||
|
wo_ins_abc(WOP_RET0, 0, 0, 0),
|
||||||
|
};
|
||||||
|
wb_method(b, kn, WOB_NONE, 0, 2, code, 5, NULL, 0, NULL, 0);
|
||||||
|
}
|
||||||
|
{ /* M_MULTI: push 5, push 9 -> count*100 + latest = 209 */
|
||||||
|
uint32_t code[] = {
|
||||||
|
wo_ins_abc(WOP_BUILTIN, 0, WO_K_SCALAR, WO_B_MULTI_NEW),
|
||||||
|
wo_ins_abx(WOP_LOADK, 1, (uint16_t)k5),
|
||||||
|
wo_ins_abc(WOP_BUILTIN, 2, 0, WO_B_MULTI_PUSH),
|
||||||
|
wo_ins_abx(WOP_LOADK, 1, (uint16_t)k9),
|
||||||
|
wo_ins_abc(WOP_BUILTIN, 2, 0, WO_B_MULTI_PUSH),
|
||||||
|
wo_ins_abc(WOP_BUILTIN, 3, 0, WO_B_COUNT),
|
||||||
|
wo_ins_abc(WOP_BUILTIN, 4, 0, WO_B_LATEST),
|
||||||
|
wo_ins_abx(WOP_LOADK, 1, (uint16_t)k100),
|
||||||
|
wo_ins_abc(WOP_MUL, 3, 3, 1),
|
||||||
|
wo_ins_abc(WOP_ADD, 3, 3, 4),
|
||||||
|
wo_ins_abc(WOP_DROP, 0, 0, 0),
|
||||||
|
wo_ins_abc(WOP_RET, 3, 0, 0),
|
||||||
|
};
|
||||||
|
wb_method(b, kn, WOB_NONE, 0, 5, code, 12, NULL, 0, NULL, 0);
|
||||||
|
}
|
||||||
|
{ /* M_MAP: set 1->5; get(1)*10 + has(1)*100 + has(9) = 150 */
|
||||||
|
uint32_t code[] = {
|
||||||
|
wo_ins_abc(WOP_BUILTIN, 0, 0 /* scalar/scalar nibbles */,
|
||||||
|
WO_B_MAP_NEW),
|
||||||
|
wo_ins_abx(WOP_LOADK, 1, (uint16_t)k1),
|
||||||
|
wo_ins_abx(WOP_LOADK, 2, (uint16_t)k5),
|
||||||
|
wo_ins_abc(WOP_BUILTIN, 3, 0, WO_B_MAP_SET),
|
||||||
|
wo_ins_abc(WOP_BUILTIN, 3, 0, WO_B_MAP_GET), /* r3 = 5 */
|
||||||
|
wo_ins_abc(WOP_BUILTIN, 4, 0, WO_B_MAP_HAS), /* r4 = has(1) = 1 */
|
||||||
|
wo_ins_abx(WOP_LOADK, 5, (uint16_t)k100),
|
||||||
|
wo_ins_abc(WOP_MUL, 4, 4, 5),
|
||||||
|
wo_ins_abx(WOP_LOADK, 5, (uint16_t)k9),
|
||||||
|
wo_ins_abc(WOP_MOVE, 1, 5, 0),
|
||||||
|
wo_ins_abc(WOP_BUILTIN, 5, 0, WO_B_MAP_HAS), /* r5 = has(9) = 0 */
|
||||||
|
wo_ins_abx(WOP_LOADK, 6, (uint16_t)k5),
|
||||||
|
/* r3 = r3*10: 10 not in pool — use 5+5 */
|
||||||
|
wo_ins_abc(WOP_ADD, 6, 6, 6),
|
||||||
|
wo_ins_abc(WOP_MUL, 3, 3, 6),
|
||||||
|
wo_ins_abc(WOP_ADD, 3, 3, 4),
|
||||||
|
wo_ins_abc(WOP_ADD, 3, 3, 5),
|
||||||
|
wo_ins_abc(WOP_DROP, 0, 0, 0),
|
||||||
|
wo_ins_abc(WOP_RET, 3, 0, 0),
|
||||||
|
};
|
||||||
|
wb_method(b, kn, WOB_NONE, 0, 7, code, 18, NULL, 0, NULL, 0);
|
||||||
|
}
|
||||||
|
{ /* M_KEY: get of a missing key traps KEY */
|
||||||
|
uint32_t code[] = {
|
||||||
|
wo_ins_abc(WOP_BUILTIN, 0, 0, WO_B_MAP_NEW),
|
||||||
|
wo_ins_abx(WOP_LOADK, 1, (uint16_t)k1),
|
||||||
|
wo_ins_abc(WOP_BUILTIN, 2, 0, WO_B_MAP_GET),
|
||||||
|
wo_ins_abc(WOP_RET0, 0, 0, 0),
|
||||||
|
};
|
||||||
|
wb_drop drops[] = {{.pc = 1, .owned = 1u << 0, .gc = 0}};
|
||||||
|
wb_method(b, kn, WOB_NONE, 0, 3, code, 4, NULL, 0, drops, 1);
|
||||||
|
}
|
||||||
|
{ /* M_LATEST: latest of an empty multi traps BOUNDS */
|
||||||
|
uint32_t code[] = {
|
||||||
|
wo_ins_abc(WOP_BUILTIN, 0, WO_K_SCALAR, WO_B_MULTI_NEW),
|
||||||
|
wo_ins_abc(WOP_BUILTIN, 1, 0, WO_B_LATEST),
|
||||||
|
wo_ins_abc(WOP_RET0, 0, 0, 0),
|
||||||
|
};
|
||||||
|
wb_drop drops[] = {{.pc = 1, .owned = 1u << 0, .gc = 0}};
|
||||||
|
wb_method(b, kn, WOB_NONE, 0, 2, code, 3, NULL, 0, drops, 1);
|
||||||
|
}
|
||||||
|
{ /* M_WORDS: words(" two words \t here\n") = 3 */
|
||||||
|
uint32_t code[] = {
|
||||||
|
wo_ins_abx(WOP_LOADK, 1, (uint16_t)kwords),
|
||||||
|
wo_ins_abc(WOP_BUILTIN, 0, 1, WO_B_WORDS),
|
||||||
|
wo_ins_abc(WOP_RET, 0, 0, 0),
|
||||||
|
};
|
||||||
|
wb_method(b, kn, WOB_NONE, 0, 2, code, 3, NULL, 0, NULL, 0);
|
||||||
|
}
|
||||||
|
{ /* M_DB: DB_STUB traps DB */
|
||||||
|
uint32_t code[] = {wo_ins_abc(WOP_DB_STUB, 0, 0, 0)};
|
||||||
|
wb_method(b, kn, WOB_NONE, 0, 1, code, 1, NULL, 0, NULL, 0);
|
||||||
|
}
|
||||||
|
{ /* M_CONCAT: "ab"+"cd" == "abcd" -> 1 */
|
||||||
|
uint32_t code[] = {
|
||||||
|
wo_ins_abx(WOP_LOADK, 1, (uint16_t)kab),
|
||||||
|
wo_ins_abx(WOP_LOADK, 2, (uint16_t)kcd),
|
||||||
|
wo_ins_abc(WOP_CONCAT, 3, 1, 2),
|
||||||
|
wo_ins_abx(WOP_LOADK, 4, (uint16_t)kabcd),
|
||||||
|
wo_ins_abc(WOP_EQS, 5, 3, 4),
|
||||||
|
wo_ins_abc(WOP_DROP, 3, 0, 0),
|
||||||
|
wo_ins_abc(WOP_RET, 5, 0, 0),
|
||||||
|
};
|
||||||
|
wb_method(b, kn, WOB_NONE, 0, 6, code, 7, NULL, 0, NULL, 0);
|
||||||
|
}
|
||||||
|
return wb_finish(b, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int call(uint32_t mi, uint64_t *ret, wo_err *err, FILE *cap) {
|
||||||
|
T_EQ(wo_vm_init(&VM, &MOD, 1 << 20), 0);
|
||||||
|
if (cap) VM.rt.out = cap;
|
||||||
|
int rc = wo_vm_call(&VM, mi, NULL, 0, ret, err);
|
||||||
|
wo_vm_destroy(&VM);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
size_t len;
|
||||||
|
uint8_t *img = build_module(&len);
|
||||||
|
char lerr[256];
|
||||||
|
if (wo_load_buf(&MOD, img, len, lerr, sizeof lerr) != 0) {
|
||||||
|
fprintf(stderr, "loader rejected: %s\n", lerr);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
uint64_t ret;
|
||||||
|
wo_err err;
|
||||||
|
|
||||||
|
/* print output captured and diffed */
|
||||||
|
FILE *cap = tmpfile();
|
||||||
|
T_EQ(call(M_PRINT, &ret, &err, cap), 0);
|
||||||
|
fflush(cap);
|
||||||
|
rewind(cap);
|
||||||
|
char out[64] = {0};
|
||||||
|
size_t n = fread(out, 1, sizeof out - 1, cap);
|
||||||
|
(void)n;
|
||||||
|
T_STREQ(out, "hi\n5\n");
|
||||||
|
fclose(cap);
|
||||||
|
|
||||||
|
T_EQ(call(M_MULTI, &ret, &err, NULL), 0);
|
||||||
|
T_EQ(ret, 209);
|
||||||
|
|
||||||
|
T_EQ(call(M_MAP, &ret, &err, NULL), 0);
|
||||||
|
T_EQ(ret, 150);
|
||||||
|
|
||||||
|
T_EQ(call(M_KEY, &ret, &err, NULL), -1);
|
||||||
|
T_EQ(err.code, WO_T_KEY);
|
||||||
|
|
||||||
|
T_EQ(call(M_LATEST, &ret, &err, NULL), -1);
|
||||||
|
T_EQ(err.code, WO_T_BOUNDS);
|
||||||
|
|
||||||
|
T_EQ(call(M_WORDS, &ret, &err, NULL), 0);
|
||||||
|
T_EQ(ret, 3);
|
||||||
|
|
||||||
|
T_EQ(call(M_DB, &ret, &err, NULL), -1);
|
||||||
|
T_EQ(err.code, WO_T_DB);
|
||||||
|
T_STREQ(err.msg, "engine not linked");
|
||||||
|
|
||||||
|
T_EQ(call(M_CONCAT, &ret, &err, NULL), 0);
|
||||||
|
T_EQ(ret, 1);
|
||||||
|
|
||||||
|
wo_module_free(&MOD);
|
||||||
|
free(img);
|
||||||
|
return t_report("test_builtin");
|
||||||
|
}
|
||||||
89
runtime/test/test_cont.c
Normal file
89
runtime/test/test_cont.c
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
/* test_cont — native containers: multi growth/bounds, map int + text keys.
|
||||||
|
* Element drops land in the gc task; here backing stores are freed by hand. */
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#include "cont.h"
|
||||||
|
#include "t.h"
|
||||||
|
|
||||||
|
static void free_multi_raw(wo_rt *rt, wo_multi *m) {
|
||||||
|
free(m->items);
|
||||||
|
wo_arena_free(&rt->arena, m, sizeof(wo_multi));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void free_map_raw(wo_rt *rt, wo_map *m) {
|
||||||
|
free(m->keys);
|
||||||
|
free(m->vals);
|
||||||
|
wo_arena_free(&rt->arena, m, sizeof(wo_map));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_multi_push_get_growth(void) {
|
||||||
|
wo_rt rt;
|
||||||
|
T_EQ(wo_rt_init(&rt, 1 << 16, NULL, 0), 0);
|
||||||
|
wo_multi *m = wo_multi_new(&rt, WO_K_SCALAR);
|
||||||
|
T_CHECK(m != NULL);
|
||||||
|
T_EQ(m->h.class_id, WO_CLS_MULTI);
|
||||||
|
for (uint64_t i = 0; i < 100; i++) T_EQ(wo_multi_push(m, i * 7), 0);
|
||||||
|
T_EQ(m->len, 100);
|
||||||
|
uint64_t v = 0;
|
||||||
|
T_EQ(wo_multi_get(m, 0, &v), 0);
|
||||||
|
T_EQ(v, 0);
|
||||||
|
T_EQ(wo_multi_get(m, 99, &v), 0);
|
||||||
|
T_EQ(v, 99 * 7);
|
||||||
|
/* bounds miss */
|
||||||
|
T_EQ(wo_multi_get(m, 100, &v), -1);
|
||||||
|
free_multi_raw(&rt, m);
|
||||||
|
wo_rt_destroy(&rt);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_map_int_keys(void) {
|
||||||
|
wo_rt rt;
|
||||||
|
T_EQ(wo_rt_init(&rt, 1 << 16, NULL, 0), 0);
|
||||||
|
wo_map *m = wo_map_new(&rt, WO_K_SCALAR, WO_K_SCALAR);
|
||||||
|
T_CHECK(m != NULL);
|
||||||
|
T_EQ(m->h.class_id, WO_CLS_MAP);
|
||||||
|
uint64_t old = 0;
|
||||||
|
T_EQ(wo_map_set(m, 1, 10, &old), 0); /* insert */
|
||||||
|
T_EQ(wo_map_set(m, 2, 20, &old), 0);
|
||||||
|
T_EQ(wo_map_set(m, 1, 11, &old), 1); /* replace hands old back */
|
||||||
|
T_EQ(old, 10);
|
||||||
|
uint64_t v = 0;
|
||||||
|
T_EQ(wo_map_get(m, 1, &v), 0);
|
||||||
|
T_EQ(v, 11);
|
||||||
|
T_EQ(wo_map_get(m, 2, &v), 0);
|
||||||
|
T_EQ(v, 20);
|
||||||
|
T_EQ(wo_map_get(m, 3, &v), -1); /* miss */
|
||||||
|
T_CHECK(wo_map_has(m, 2));
|
||||||
|
T_CHECK(!wo_map_has(m, 3));
|
||||||
|
free_map_raw(&rt, m);
|
||||||
|
wo_rt_destroy(&rt);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_map_text_keys_by_content(void) {
|
||||||
|
wo_rt rt;
|
||||||
|
T_EQ(wo_rt_init(&rt, 1 << 16, NULL, 0), 0);
|
||||||
|
wo_map *m = wo_map_new(&rt, WO_K_TEXT, WO_K_SCALAR);
|
||||||
|
wo_str *k1 = wo_str_new(&rt, "sku-1", 5);
|
||||||
|
uint64_t old = 0;
|
||||||
|
T_EQ(wo_map_set(m, (uint64_t)(uintptr_t)k1, 999, &old), 0);
|
||||||
|
/* lookup through a DIFFERENT pointer with equal content must hit */
|
||||||
|
wo_str *k1b = wo_str_new(&rt, "sku-1", 5);
|
||||||
|
T_CHECK(k1 != k1b);
|
||||||
|
uint64_t v = 0;
|
||||||
|
T_EQ(wo_map_get(m, (uint64_t)(uintptr_t)k1b, &v), 0);
|
||||||
|
T_EQ(v, 999);
|
||||||
|
T_CHECK(wo_map_has(m, (uint64_t)(uintptr_t)k1b));
|
||||||
|
wo_str *k2 = wo_str_new(&rt, "sku-2", 5);
|
||||||
|
T_CHECK(!wo_map_has(m, (uint64_t)(uintptr_t)k2));
|
||||||
|
wo_str_free(&rt, k1);
|
||||||
|
wo_str_free(&rt, k1b);
|
||||||
|
wo_str_free(&rt, k2);
|
||||||
|
free_map_raw(&rt, m);
|
||||||
|
wo_rt_destroy(&rt);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
test_multi_push_get_growth();
|
||||||
|
test_map_int_keys();
|
||||||
|
test_map_text_keys_by_content();
|
||||||
|
return t_report("test_cont");
|
||||||
|
}
|
||||||
115
runtime/test/test_cycle.c
Normal file
115
runtime/test/test_cycle.c
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
/* test_cycle — budgeted Bacon–Rajan cycle collection.
|
||||||
|
* Cycle classes use the malloc-path trick (~130 fields) so ASan proves
|
||||||
|
* every free. Budget semantics: a step consumes candidates from the buffer
|
||||||
|
* (roots popped + whites purged) up to `budget`, whole components atomic. */
|
||||||
|
#include "cont.h"
|
||||||
|
#include "gc.h"
|
||||||
|
#include "t.h"
|
||||||
|
|
||||||
|
#define BIG 130
|
||||||
|
|
||||||
|
/* @gc class "GNode": field0 GCREF, field1 MULTI, rest scalars */
|
||||||
|
static uint8_t gnode_kinds[BIG];
|
||||||
|
static wo_classdesc CLASSES[1];
|
||||||
|
|
||||||
|
static void setup(void) {
|
||||||
|
gnode_kinds[0] = WO_K_GCREF;
|
||||||
|
gnode_kinds[1] = WO_K_MULTI;
|
||||||
|
CLASSES[0] = (wo_classdesc){
|
||||||
|
.name = 0, .flags = WO_CLASSF_GC, .field_cnt = BIG, .kinds = gnode_kinds};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* helper: link a->f0 = b, taking a reference on b */
|
||||||
|
static void link(wo_hdr *a, wo_hdr *b) {
|
||||||
|
wo_fields(a)[0] = (uint64_t)(uintptr_t)b;
|
||||||
|
wo_rc_inc(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_two_object_cycle_collects(void) {
|
||||||
|
wo_rt rt;
|
||||||
|
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 1), 0);
|
||||||
|
wo_hdr *a = wo_obj_new(&rt, 0);
|
||||||
|
wo_hdr *b = wo_obj_new(&rt, 0);
|
||||||
|
link(a, b);
|
||||||
|
link(b, a);
|
||||||
|
/* drop both external handles: objects survive on cycle edges alone */
|
||||||
|
wo_rc_dec(&rt, a);
|
||||||
|
wo_rc_dec(&rt, b);
|
||||||
|
T_EQ(rt.cycbuf.len, 2); /* both buffered as candidates */
|
||||||
|
T_EQ(wo_gc_step(&rt, 16), 2); /* whole cycle freed (ASan proves it) */
|
||||||
|
T_EQ(rt.cycbuf.len, 0);
|
||||||
|
wo_rt_destroy(&rt);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_budget_one_cycle_per_step(void) {
|
||||||
|
wo_rt rt;
|
||||||
|
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 1), 0);
|
||||||
|
wo_hdr *a = wo_obj_new(&rt, 0), *b = wo_obj_new(&rt, 0);
|
||||||
|
wo_hdr *c = wo_obj_new(&rt, 0), *d = wo_obj_new(&rt, 0);
|
||||||
|
link(a, b);
|
||||||
|
link(b, a);
|
||||||
|
link(c, d);
|
||||||
|
link(d, c);
|
||||||
|
wo_rc_dec(&rt, a);
|
||||||
|
wo_rc_dec(&rt, b);
|
||||||
|
wo_rc_dec(&rt, c);
|
||||||
|
wo_rc_dec(&rt, d);
|
||||||
|
T_EQ(rt.cycbuf.len, 4);
|
||||||
|
/* budget 2 = one two-object component per step */
|
||||||
|
T_EQ(wo_gc_step(&rt, 2), 2);
|
||||||
|
T_EQ(rt.cycbuf.len, 2);
|
||||||
|
T_EQ(wo_gc_step(&rt, 2), 2);
|
||||||
|
T_EQ(rt.cycbuf.len, 0);
|
||||||
|
/* nothing left: a further step frees nothing */
|
||||||
|
T_EQ(wo_gc_step(&rt, 2), 0);
|
||||||
|
wo_rt_destroy(&rt);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_externally_held_cycle_survives_then_dies(void) {
|
||||||
|
wo_rt rt;
|
||||||
|
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 1), 0);
|
||||||
|
wo_hdr *a = wo_obj_new(&rt, 0), *b = wo_obj_new(&rt, 0);
|
||||||
|
link(a, b);
|
||||||
|
link(b, a);
|
||||||
|
/* keep the external handle on a; drop only b's */
|
||||||
|
wo_rc_dec(&rt, b);
|
||||||
|
T_EQ(rt.cycbuf.len, 1);
|
||||||
|
T_EQ(wo_gc_step(&rt, 16), 0); /* held from outside: survives */
|
||||||
|
T_EQ(rt.cycbuf.len, 0); /* candidate consumed, flag cleared */
|
||||||
|
/* counts fully restored */
|
||||||
|
T_EQ(a->rc, 2);
|
||||||
|
T_EQ(b->rc, 1);
|
||||||
|
T_EQ(a->flags & (WO_F_BUF | WO_F_COLOR), 0);
|
||||||
|
T_EQ(b->flags & (WO_F_BUF | WO_F_COLOR), 0);
|
||||||
|
/* still usable, then truly dead */
|
||||||
|
wo_rc_dec(&rt, a);
|
||||||
|
T_EQ(rt.cycbuf.len, 1); /* re-buffered on the last external decrement */
|
||||||
|
T_EQ(wo_gc_step(&rt, 16), 2);
|
||||||
|
wo_rt_destroy(&rt);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_cycle_through_multi_elements(void) {
|
||||||
|
wo_rt rt;
|
||||||
|
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 1), 0);
|
||||||
|
wo_hdr *a = wo_obj_new(&rt, 0), *b = wo_obj_new(&rt, 0);
|
||||||
|
/* a --(multi element)--> b --(gcref field)--> a */
|
||||||
|
wo_multi *m = wo_multi_new(&rt, WO_K_GCREF);
|
||||||
|
T_EQ(wo_multi_push(m, (uint64_t)(uintptr_t)b), 0);
|
||||||
|
wo_rc_inc(b);
|
||||||
|
wo_fields(a)[1] = (uint64_t)(uintptr_t)m;
|
||||||
|
link(b, a);
|
||||||
|
wo_rc_dec(&rt, a);
|
||||||
|
wo_rc_dec(&rt, b);
|
||||||
|
T_EQ(wo_gc_step(&rt, 16), 2); /* multi head + backing freed with a */
|
||||||
|
T_EQ(rt.cycbuf.len, 0);
|
||||||
|
wo_rt_destroy(&rt);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
setup();
|
||||||
|
test_two_object_cycle_collects();
|
||||||
|
test_budget_one_cycle_per_step();
|
||||||
|
test_externally_held_cycle_survives_then_dies();
|
||||||
|
test_cycle_through_multi_elements();
|
||||||
|
return t_report("test_cycle");
|
||||||
|
}
|
||||||
99
runtime/test/test_icall.c
Normal file
99
runtime/test/test_icall.c
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
/* test_icall — structural-interface dispatch: one ICALL site, two classes,
|
||||||
|
* two different answers chosen by the receiver's class; missing vtable
|
||||||
|
* entry traps with the no-vtable message. */
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#include "loader.h"
|
||||||
|
#include "t.h"
|
||||||
|
#include "vm.h"
|
||||||
|
#include "wob_build.h"
|
||||||
|
|
||||||
|
static wo_vm VM;
|
||||||
|
|
||||||
|
static int run_img(uint8_t *img, size_t len, uint32_t mi, uint64_t *ret,
|
||||||
|
wo_err *err) {
|
||||||
|
wo_module mod;
|
||||||
|
char lerr[256];
|
||||||
|
if (wo_load_buf(&mod, img, len, lerr, sizeof lerr) != 0) {
|
||||||
|
fprintf(stderr, "loader rejected test image: %s\n", lerr);
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
if (wo_vm_init(&VM, &mod, 1 << 20) != 0) {
|
||||||
|
wo_module_free(&mod);
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
int rc = wo_vm_call(&VM, mi, NULL, 0, ret, err);
|
||||||
|
wo_vm_destroy(&VM);
|
||||||
|
wo_module_free(&mod);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t *build(int with_vtabs, size_t *len) {
|
||||||
|
wb_t *b = wb_new();
|
||||||
|
uint32_t kA = wb_const_text(b, "CA");
|
||||||
|
uint32_t kB = wb_const_text(b, "CB");
|
||||||
|
uint32_t kI = wb_const_text(b, "Priced");
|
||||||
|
uint32_t km = wb_const_text(b, "price");
|
||||||
|
uint32_t kmain = wb_const_text(b, "main");
|
||||||
|
uint32_t k10 = wb_const_int(b, 10);
|
||||||
|
uint32_t k20 = wb_const_int(b, 20);
|
||||||
|
uint8_t kinds[] = {WO_K_SCALAR};
|
||||||
|
uint32_t ca = wb_class(b, kA, 0, kinds, 1);
|
||||||
|
uint32_t cb = wb_class(b, kB, 0, kinds, 1);
|
||||||
|
wb_iface(b, kI, 1); /* one method -> global slot 0 */
|
||||||
|
|
||||||
|
/* method 0: CA.price(self) -> 10 ; method 1: CB.price(self) -> 20 */
|
||||||
|
uint32_t code_a[] = {
|
||||||
|
wo_ins_abx(WOP_LOADK, 1, (uint16_t)k10),
|
||||||
|
wo_ins_abc(WOP_RET, 1, 0, 0),
|
||||||
|
};
|
||||||
|
uint32_t ma = wb_method(b, km, ca, 1, 2, code_a, 2, NULL, 0, NULL, 0);
|
||||||
|
uint32_t code_b[] = {
|
||||||
|
wo_ins_abx(WOP_LOADK, 1, (uint16_t)k20),
|
||||||
|
wo_ins_abc(WOP_RET, 1, 0, 0),
|
||||||
|
};
|
||||||
|
uint32_t mb = wb_method(b, km, cb, 1, 2, code_b, 2, NULL, 0, NULL, 0);
|
||||||
|
if (with_vtabs) {
|
||||||
|
wb_vtab(b, ca, 0, &ma, 1);
|
||||||
|
wb_vtab(b, cb, 0, &mb, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* method 2: main — same ICALL site, both classes */
|
||||||
|
uint32_t code_m[] = {
|
||||||
|
wo_ins_abx(WOP_NEW, 1, (uint16_t)ca),
|
||||||
|
wo_ins_abx(WOP_ICALL, 1, 0), /* r1 = priced(r1) via slot 0 */
|
||||||
|
wo_ins_abx(WOP_NEW, 2, (uint16_t)cb),
|
||||||
|
wo_ins_abx(WOP_ICALL, 2, 0), /* r2 = priced(r2) via the SAME slot */
|
||||||
|
wo_ins_abc(WOP_ADD, 0, 1, 2),
|
||||||
|
wo_ins_abc(WOP_RET, 0, 0, 0),
|
||||||
|
};
|
||||||
|
wb_method(b, kmain, WOB_NONE, 0, 3, code_m, 6, NULL, 0, NULL, 0);
|
||||||
|
return wb_finish(b, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_dynamic_dispatch_by_class(void) {
|
||||||
|
size_t len;
|
||||||
|
uint8_t *img = build(1, &len);
|
||||||
|
uint64_t ret = 0;
|
||||||
|
wo_err err;
|
||||||
|
T_EQ(run_img(img, len, 2, &ret, &err), 0);
|
||||||
|
T_EQ(ret, 30); /* 10 + 20: both impls reached through one call site */
|
||||||
|
free(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_missing_vtable_traps(void) {
|
||||||
|
size_t len;
|
||||||
|
uint8_t *img = build(0, &len); /* same classes, no vtable rows */
|
||||||
|
uint64_t ret = 0;
|
||||||
|
wo_err err = {0};
|
||||||
|
T_EQ(run_img(img, len, 2, &ret, &err), -1);
|
||||||
|
T_EQ(err.code, WO_T_BOUNDS);
|
||||||
|
T_CHECK(strstr(err.msg, "no vtable entry") != NULL);
|
||||||
|
free(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
test_dynamic_dispatch_by_class();
|
||||||
|
test_missing_vtable_traps();
|
||||||
|
return t_report("test_icall");
|
||||||
|
}
|
||||||
116
runtime/test/test_loader.c
Normal file
116
runtime/test/test_loader.c
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
/* test_loader — happy path from a builder image, then a rejection battery.
|
||||||
|
* Every reject must produce a nonempty error and leak nothing (ASan). */
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#include "loader.h"
|
||||||
|
#include "t.h"
|
||||||
|
#include "wob_build.h"
|
||||||
|
|
||||||
|
/* a valid two-const, one-class, one-method image */
|
||||||
|
static uint8_t *valid_image(size_t *len) {
|
||||||
|
wb_t *b = wb_new();
|
||||||
|
uint32_t k42 = wb_const_int(b, 42);
|
||||||
|
uint32_t kname = wb_const_text(b, "main");
|
||||||
|
uint8_t kinds[] = {WO_K_SCALAR, WO_K_SCALAR};
|
||||||
|
wb_class(b, kname, 0, kinds, 2);
|
||||||
|
uint32_t code[] = {
|
||||||
|
wo_ins_abx(WOP_LOADK, 0, (uint16_t)k42),
|
||||||
|
wo_ins_abc(WOP_RET, 0, 0, 0),
|
||||||
|
};
|
||||||
|
uint32_t lines[] = {0, 10, 1, 11};
|
||||||
|
uint32_t m = wb_method(b, kname, WOB_NONE, 0, 2, code, 2, lines, 2, NULL, 0);
|
||||||
|
wb_entry(b, m);
|
||||||
|
return wb_finish(b, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_happy_path(void) {
|
||||||
|
size_t len;
|
||||||
|
uint8_t *img = valid_image(&len);
|
||||||
|
wo_module m;
|
||||||
|
char err[256] = "";
|
||||||
|
T_EQ(wo_load_buf(&m, img, len, err, sizeof err), 0);
|
||||||
|
T_EQ(m.const_cnt, 2);
|
||||||
|
T_EQ(m.consts[0].tag, WOB_K_INT);
|
||||||
|
T_EQ(m.consts[0].i, 42);
|
||||||
|
T_EQ(m.consts[1].tag, WOB_K_TEXT);
|
||||||
|
T_CHECK(m.consts[1].s != NULL);
|
||||||
|
T_CHECK(m.consts[1].s->h.flags & WO_F_CONST); /* interned */
|
||||||
|
T_EQ(m.consts[1].s->len, 4);
|
||||||
|
T_CHECK(memcmp(m.consts[1].s->data, "main", 4) == 0);
|
||||||
|
T_EQ(m.class_cnt, 1);
|
||||||
|
T_EQ(m.classes[0].field_cnt, 2);
|
||||||
|
T_EQ(m.method_cnt, 1);
|
||||||
|
T_EQ(m.methods[0].reg_cnt, 2);
|
||||||
|
T_EQ(m.methods[0].ninstr, 2);
|
||||||
|
T_EQ(m.methods[0].line_cnt, 2);
|
||||||
|
T_EQ(m.methods[0].lines[1].pc, 1);
|
||||||
|
T_EQ(m.methods[0].lines[1].line, 11);
|
||||||
|
T_EQ(m.entry, 0);
|
||||||
|
wo_module_free(&m);
|
||||||
|
free(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* build an image with one patched method-code instruction */
|
||||||
|
static uint8_t *image_with_code(uint32_t i0, uint32_t i1, size_t *len) {
|
||||||
|
wb_t *b = wb_new();
|
||||||
|
wb_const_int(b, 42);
|
||||||
|
uint32_t kname = wb_const_text(b, "main");
|
||||||
|
uint32_t code[] = {i0, i1};
|
||||||
|
wb_method(b, kname, WOB_NONE, 0, 2, code, 2, NULL, 0, NULL, 0);
|
||||||
|
return wb_finish(b, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void expect_reject(uint8_t *img, size_t len, const char *what) {
|
||||||
|
wo_module m;
|
||||||
|
char err[256] = "";
|
||||||
|
int rc = wo_load_buf(&m, img, len, err, sizeof err);
|
||||||
|
T_EQ(rc, -1);
|
||||||
|
if (rc != 0 && err[0] == '\0')
|
||||||
|
fprintf(stderr, "empty error for reject case: %s\n", what);
|
||||||
|
T_CHECK(err[0] != '\0');
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_rejects(void) {
|
||||||
|
size_t len;
|
||||||
|
|
||||||
|
/* corrupted magic */
|
||||||
|
uint8_t *img = valid_image(&len);
|
||||||
|
img[0] ^= 0xFF;
|
||||||
|
expect_reject(img, len, "magic");
|
||||||
|
free(img);
|
||||||
|
|
||||||
|
/* unknown opcode */
|
||||||
|
img = image_with_code(wo_ins_abc(200, 0, 0, 0),
|
||||||
|
wo_ins_abc(WOP_RET, 0, 0, 0), &len);
|
||||||
|
expect_reject(img, len, "opcode");
|
||||||
|
free(img);
|
||||||
|
|
||||||
|
/* constant index out of range */
|
||||||
|
img = image_with_code(wo_ins_abx(WOP_LOADK, 0, 99),
|
||||||
|
wo_ins_abc(WOP_RET, 0, 0, 0), &len);
|
||||||
|
expect_reject(img, len, "const oob");
|
||||||
|
free(img);
|
||||||
|
|
||||||
|
/* register out of range (regc = 2) */
|
||||||
|
img = image_with_code(wo_ins_abc(WOP_MOVE, 5, 0, 0),
|
||||||
|
wo_ins_abc(WOP_RET, 0, 0, 0), &len);
|
||||||
|
expect_reject(img, len, "reg oob");
|
||||||
|
free(img);
|
||||||
|
|
||||||
|
/* last instruction is not a terminator */
|
||||||
|
img = image_with_code(wo_ins_abc(WOP_RET, 0, 0, 0),
|
||||||
|
wo_ins_abx(WOP_LOADK, 0, 0), &len);
|
||||||
|
expect_reject(img, len, "non-terminator tail");
|
||||||
|
free(img);
|
||||||
|
|
||||||
|
/* truncated buffer */
|
||||||
|
img = valid_image(&len);
|
||||||
|
expect_reject(img, len / 2, "truncated");
|
||||||
|
free(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
test_happy_path();
|
||||||
|
test_rejects();
|
||||||
|
return t_report("test_loader");
|
||||||
|
}
|
||||||
85
runtime/test/test_obj.c
Normal file
85
runtime/test/test_obj.c
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
/* test_obj — object creation (owned vs @gc), strings, const interning. */
|
||||||
|
#include "obj.h"
|
||||||
|
#include "t.h"
|
||||||
|
|
||||||
|
/* two test classes: 0 = owned Point{x,y}, 1 = @gc Cache{hits} */
|
||||||
|
static const uint8_t point_kinds[] = {WO_K_SCALAR, WO_K_SCALAR};
|
||||||
|
static const uint8_t cache_kinds[] = {WO_K_SCALAR};
|
||||||
|
static const wo_classdesc CLASSES[] = {
|
||||||
|
{.name = 0, .flags = 0, .field_cnt = 2, .kinds = point_kinds},
|
||||||
|
{.name = 0, .flags = WO_CLASSF_GC, .field_cnt = 1, .kinds = cache_kinds},
|
||||||
|
};
|
||||||
|
|
||||||
|
static void test_owned_object_zeroed(void) {
|
||||||
|
wo_rt rt;
|
||||||
|
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 2), 0);
|
||||||
|
wo_hdr *o = wo_obj_new(&rt, 0);
|
||||||
|
T_CHECK(o != NULL);
|
||||||
|
T_EQ(o->class_id, 0);
|
||||||
|
T_EQ(o->flags, 0);
|
||||||
|
T_EQ(o->borrow, WO_BORROW_FREE);
|
||||||
|
T_EQ(o->rc, 0);
|
||||||
|
T_EQ(wo_fields(o)[0], 0);
|
||||||
|
T_EQ(wo_fields(o)[1], 0);
|
||||||
|
wo_arena_free(&rt.arena, o, wo_obj_size(&CLASSES[0]));
|
||||||
|
wo_rt_destroy(&rt);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_gc_object_rc1(void) {
|
||||||
|
wo_rt rt;
|
||||||
|
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 2), 0);
|
||||||
|
wo_hdr *o = wo_obj_new(&rt, 1);
|
||||||
|
T_CHECK(o != NULL);
|
||||||
|
T_CHECK(o->flags & WO_F_GC);
|
||||||
|
T_EQ(o->rc, 1);
|
||||||
|
wo_arena_free(&rt.arena, o, wo_obj_size(&CLASSES[1]));
|
||||||
|
wo_rt_destroy(&rt);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_strings(void) {
|
||||||
|
wo_rt rt;
|
||||||
|
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 2), 0);
|
||||||
|
wo_str *a = wo_str_new(&rt, "hello ", 6);
|
||||||
|
wo_str *b = wo_str_new(&rt, "world", 5);
|
||||||
|
T_CHECK(a && b);
|
||||||
|
T_EQ(a->h.class_id, WO_CLS_STR);
|
||||||
|
T_EQ(a->len, 6);
|
||||||
|
T_CHECK(memcmp(a->data, "hello ", 6) == 0);
|
||||||
|
|
||||||
|
wo_str *c = wo_str_concat(&rt, a, b);
|
||||||
|
T_CHECK(c != NULL);
|
||||||
|
T_EQ(c->len, 11);
|
||||||
|
T_CHECK(memcmp(c->data, "hello world", 11) == 0);
|
||||||
|
|
||||||
|
wo_str *c2 = wo_str_new(&rt, "hello world", 11);
|
||||||
|
T_CHECK(wo_str_eq(c, c2)); /* content equality, different pointers */
|
||||||
|
T_CHECK(!wo_str_eq(a, b));
|
||||||
|
|
||||||
|
wo_str_free(&rt, a);
|
||||||
|
wo_str_free(&rt, b);
|
||||||
|
wo_str_free(&rt, c);
|
||||||
|
wo_str_free(&rt, c2);
|
||||||
|
wo_rt_destroy(&rt);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_const_string_survives_free(void) {
|
||||||
|
wo_rt rt;
|
||||||
|
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 2), 0);
|
||||||
|
wo_str *s = wo_str_new(&rt, "interned", 8);
|
||||||
|
T_CHECK(s != NULL);
|
||||||
|
s->h.flags |= WO_F_CONST; /* as the loader will mark interned constants */
|
||||||
|
wo_str_free(&rt, s); /* must be a no-op */
|
||||||
|
T_EQ(s->len, 8); /* still readable: free didn't touch it */
|
||||||
|
T_CHECK(memcmp(s->data, "interned", 8) == 0);
|
||||||
|
s->h.flags &= (uint8_t)~WO_F_CONST;
|
||||||
|
wo_str_free(&rt, s); /* real free so ASan sees no leak */
|
||||||
|
wo_rt_destroy(&rt);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
test_owned_object_zeroed();
|
||||||
|
test_gc_object_rc1();
|
||||||
|
test_strings();
|
||||||
|
test_const_string_survives_free();
|
||||||
|
return t_report("test_obj");
|
||||||
|
}
|
||||||
95
runtime/test/test_objops.c
Normal file
95
runtime/test/test_objops.c
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
/* test_objops — NEW / GETF / SETF / DROP through bytecode, plus the
|
||||||
|
* residual runtime checks (field bounds, null receiver) trapping BOUNDS. */
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#include "loader.h"
|
||||||
|
#include "t.h"
|
||||||
|
#include "vm.h"
|
||||||
|
#include "wob_build.h"
|
||||||
|
|
||||||
|
static wo_vm VM;
|
||||||
|
|
||||||
|
static int run_img(uint8_t *img, size_t len, uint64_t *ret, wo_err *err) {
|
||||||
|
wo_module mod;
|
||||||
|
char lerr[256];
|
||||||
|
if (wo_load_buf(&mod, img, len, lerr, sizeof lerr) != 0) {
|
||||||
|
fprintf(stderr, "loader rejected test image: %s\n", lerr);
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
if (wo_vm_init(&VM, &mod, 1 << 20) != 0) {
|
||||||
|
wo_module_free(&mod);
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
int rc = wo_vm_call(&VM, 0, NULL, 0, ret, err);
|
||||||
|
wo_vm_destroy(&VM);
|
||||||
|
wo_module_free(&mod);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* image with class Point{x,y} and a single free fn built from `code` */
|
||||||
|
static uint8_t *point_image(const uint32_t *code, uint32_t n, size_t *len) {
|
||||||
|
wb_t *b = wb_new();
|
||||||
|
uint32_t kp = wb_const_text(b, "Point");
|
||||||
|
uint32_t kf = wb_const_text(b, "go");
|
||||||
|
wb_const_int(b, 7); /* constant index 2 */
|
||||||
|
uint8_t kinds[] = {WO_K_SCALAR, WO_K_SCALAR};
|
||||||
|
wb_class(b, kp, 0, kinds, 2);
|
||||||
|
wb_method(b, kf, WOB_NONE, 0, 3, code, n, NULL, 0, NULL, 0);
|
||||||
|
return wb_finish(b, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_new_set_get_drop_roundtrip(void) {
|
||||||
|
uint32_t code[] = {
|
||||||
|
wo_ins_abx(WOP_NEW, 0, 0), /* r0 = new Point */
|
||||||
|
wo_ins_abx(WOP_LOADK, 1, 2), /* r1 = 7 */
|
||||||
|
wo_ins_abc(WOP_SETF, 0, 0, 1), /* r0.f0 = r1 */
|
||||||
|
wo_ins_abc(WOP_GETF, 2, 0, 0), /* r2 = r0.f0 */
|
||||||
|
wo_ins_abc(WOP_DROP, 0, 0, 0), /* drop r0 (nulls the register) */
|
||||||
|
wo_ins_abc(WOP_RET, 2, 0, 0),
|
||||||
|
};
|
||||||
|
size_t len;
|
||||||
|
uint8_t *img = point_image(code, 6, &len);
|
||||||
|
uint64_t ret = 0;
|
||||||
|
wo_err err;
|
||||||
|
T_EQ(run_img(img, len, &ret, &err), 0);
|
||||||
|
T_EQ(ret, 7);
|
||||||
|
free(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_field_index_out_of_range_traps(void) {
|
||||||
|
uint32_t code[] = {
|
||||||
|
wo_ins_abx(WOP_NEW, 0, 0),
|
||||||
|
wo_ins_abc(WOP_GETF, 2, 0, 9), /* Point has 2 fields */
|
||||||
|
wo_ins_abc(WOP_RET, 2, 0, 0),
|
||||||
|
};
|
||||||
|
size_t len;
|
||||||
|
uint8_t *img = point_image(code, 3, &len);
|
||||||
|
uint64_t ret = 0;
|
||||||
|
wo_err err = {0};
|
||||||
|
T_EQ(run_img(img, len, &ret, &err), -1);
|
||||||
|
T_EQ(err.code, WO_T_BOUNDS);
|
||||||
|
T_EQ(wo_vm_depth(&VM), 0);
|
||||||
|
free(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_null_receiver_traps(void) {
|
||||||
|
uint32_t code[] = {
|
||||||
|
/* r0 was never assigned: zeroed by the frame setup */
|
||||||
|
wo_ins_abc(WOP_GETF, 2, 0, 0),
|
||||||
|
wo_ins_abc(WOP_RET, 2, 0, 0),
|
||||||
|
};
|
||||||
|
size_t len;
|
||||||
|
uint8_t *img = point_image(code, 2, &len);
|
||||||
|
uint64_t ret = 0;
|
||||||
|
wo_err err = {0};
|
||||||
|
T_EQ(run_img(img, len, &ret, &err), -1);
|
||||||
|
T_EQ(err.code, WO_T_BOUNDS);
|
||||||
|
free(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
test_new_set_get_drop_roundtrip();
|
||||||
|
test_field_index_out_of_range_traps();
|
||||||
|
test_null_receiver_traps();
|
||||||
|
return t_report("test_objops");
|
||||||
|
}
|
||||||
103
runtime/test/test_rc.c
Normal file
103
runtime/test/test_rc.c
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
/* test_rc — RC + drop plans: deterministic destruction.
|
||||||
|
*
|
||||||
|
* Testing trick used by every memory test from here on: classes get ~130
|
||||||
|
* fields so instances exceed the 1024-byte size-class ceiling and take the
|
||||||
|
* arena's malloc path — any missed free is a hard ASan leak report. */
|
||||||
|
#include "cont.h"
|
||||||
|
#include "gc.h"
|
||||||
|
#include "t.h"
|
||||||
|
|
||||||
|
#define BIG 130
|
||||||
|
|
||||||
|
/* class 0 "Node": field0 OWNED (child Node), field1 TEXT, rest scalars */
|
||||||
|
static uint8_t node_kinds[BIG];
|
||||||
|
/* class 1 "Shared" (@gc): all scalars */
|
||||||
|
static uint8_t shared_kinds[BIG];
|
||||||
|
/* class 2 "Holder": field0 GCREF, field1 MULTI (of TEXT), rest scalars */
|
||||||
|
static uint8_t holder_kinds[BIG];
|
||||||
|
|
||||||
|
static wo_classdesc CLASSES[3];
|
||||||
|
|
||||||
|
static void setup_classes(void) {
|
||||||
|
node_kinds[0] = WO_K_OWNED;
|
||||||
|
node_kinds[1] = WO_K_TEXT;
|
||||||
|
shared_kinds[0] = WO_K_SCALAR;
|
||||||
|
holder_kinds[0] = WO_K_GCREF;
|
||||||
|
holder_kinds[1] = WO_K_MULTI;
|
||||||
|
CLASSES[0] = (wo_classdesc){.name = 0, .flags = 0, .field_cnt = BIG, .kinds = node_kinds};
|
||||||
|
CLASSES[1] = (wo_classdesc){.name = 0, .flags = WO_CLASSF_GC, .field_cnt = BIG, .kinds = shared_kinds};
|
||||||
|
CLASSES[2] = (wo_classdesc){.name = 0, .flags = 0, .field_cnt = BIG, .kinds = holder_kinds};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Owned tree: parent -> child -> grandchild, each with an owned name text.
|
||||||
|
* One drop of the root must free all six allocations (ASan-proven). */
|
||||||
|
static void test_owned_tree_recursive_drop(void) {
|
||||||
|
wo_rt rt;
|
||||||
|
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 3), 0);
|
||||||
|
wo_hdr *grand = wo_obj_new(&rt, 0);
|
||||||
|
wo_fields(grand)[1] = (uint64_t)(uintptr_t)wo_str_new(&rt, "grand", 5);
|
||||||
|
wo_hdr *child = wo_obj_new(&rt, 0);
|
||||||
|
wo_fields(child)[0] = (uint64_t)(uintptr_t)grand;
|
||||||
|
wo_fields(child)[1] = (uint64_t)(uintptr_t)wo_str_new(&rt, "child", 5);
|
||||||
|
wo_hdr *root = wo_obj_new(&rt, 0);
|
||||||
|
wo_fields(root)[0] = (uint64_t)(uintptr_t)child;
|
||||||
|
wo_fields(root)[1] = (uint64_t)(uintptr_t)wo_str_new(&rt, "root", 4);
|
||||||
|
wo_drop_obj(&rt, root);
|
||||||
|
/* nothing to assert beyond "ASan stays silent" — that IS the test */
|
||||||
|
T_CHECK(1);
|
||||||
|
wo_rt_destroy(&rt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A holder's gcref field decrements on drop; the final external decrement
|
||||||
|
* frees the @gc object. */
|
||||||
|
static void test_gcref_field_decrements(void) {
|
||||||
|
wo_rt rt;
|
||||||
|
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 3), 0);
|
||||||
|
wo_hdr *shared = wo_obj_new(&rt, 1); /* rc = 1 (creating ref) */
|
||||||
|
wo_rc_inc(shared); /* holder's reference */
|
||||||
|
T_EQ(shared->rc, 2);
|
||||||
|
wo_hdr *holder = wo_obj_new(&rt, 2);
|
||||||
|
wo_fields(holder)[0] = (uint64_t)(uintptr_t)shared;
|
||||||
|
wo_drop_obj(&rt, holder); /* drops holder, decrements shared to 1 */
|
||||||
|
T_EQ(shared->rc, 1);
|
||||||
|
wo_rc_dec(&rt, shared); /* final ref gone -> freed (ASan-proven) */
|
||||||
|
wo_rt_destroy(&rt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text and multi-of-text fields are freed with the holder. */
|
||||||
|
static void test_container_fields_freed_with_holder(void) {
|
||||||
|
wo_rt rt;
|
||||||
|
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 3), 0);
|
||||||
|
wo_multi *tags = wo_multi_new(&rt, WO_K_TEXT);
|
||||||
|
T_EQ(wo_multi_push(tags, (uint64_t)(uintptr_t)wo_str_new(&rt, "a", 1)), 0);
|
||||||
|
T_EQ(wo_multi_push(tags, (uint64_t)(uintptr_t)wo_str_new(&rt, "b", 1)), 0);
|
||||||
|
wo_hdr *holder = wo_obj_new(&rt, 2);
|
||||||
|
wo_fields(holder)[1] = (uint64_t)(uintptr_t)tags;
|
||||||
|
wo_drop_obj(&rt, holder); /* frees holder + multi + both strings */
|
||||||
|
T_CHECK(1);
|
||||||
|
wo_rt_destroy(&rt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* rc_inc/rc_dec pairing frees exactly at zero. */
|
||||||
|
static void test_rc_zero_frees(void) {
|
||||||
|
wo_rt rt;
|
||||||
|
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 3), 0);
|
||||||
|
wo_hdr *s = wo_obj_new(&rt, 1);
|
||||||
|
wo_rc_inc(s);
|
||||||
|
wo_rc_inc(s);
|
||||||
|
T_EQ(s->rc, 3);
|
||||||
|
wo_rc_dec(&rt, s);
|
||||||
|
wo_rc_dec(&rt, s);
|
||||||
|
T_EQ(s->rc, 1);
|
||||||
|
wo_rc_dec(&rt, s); /* freed here */
|
||||||
|
wo_rt_destroy(&rt);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
setup_classes();
|
||||||
|
test_owned_tree_recursive_drop();
|
||||||
|
test_gcref_field_decrements();
|
||||||
|
test_container_fields_freed_with_holder();
|
||||||
|
test_rc_zero_frees();
|
||||||
|
return t_report("test_rc");
|
||||||
|
}
|
||||||
133
runtime/test/test_unwind.c
Normal file
133
runtime/test/test_unwind.c
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
/* test_unwind — borrow/rc opcodes and drop-map trap unwinding: the spec's
|
||||||
|
* "traps never leak" promise. Classes use the malloc-path trick (~130
|
||||||
|
* fields) so ASan proves every free on the trap paths. */
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#include "loader.h"
|
||||||
|
#include "t.h"
|
||||||
|
#include "vm.h"
|
||||||
|
#include "wob_build.h"
|
||||||
|
|
||||||
|
#define BIG 130
|
||||||
|
|
||||||
|
static wo_vm VM;
|
||||||
|
|
||||||
|
static int run_img(uint8_t *img, size_t len, uint64_t *ret, wo_err *err) {
|
||||||
|
wo_module mod;
|
||||||
|
char lerr[256];
|
||||||
|
if (wo_load_buf(&mod, img, len, lerr, sizeof lerr) != 0) {
|
||||||
|
fprintf(stderr, "loader rejected test image: %s\n", lerr);
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
if (wo_vm_init(&VM, &mod, 1 << 20) != 0) {
|
||||||
|
wo_module_free(&mod);
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
int rc = wo_vm_call(&VM, 0, NULL, 0, ret, err);
|
||||||
|
wo_vm_destroy(&VM);
|
||||||
|
wo_module_free(&mod);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t big_kinds[BIG]; /* all scalars */
|
||||||
|
|
||||||
|
/* Double-exclusive borrow traps BORROW at the right line; the owned object
|
||||||
|
* named in the frame's drop mask is freed during unwinding. */
|
||||||
|
static void test_borrow_violation_frees_owned(void) {
|
||||||
|
wb_t *b = wb_new();
|
||||||
|
uint32_t kc = wb_const_text(b, "Big");
|
||||||
|
uint32_t kf = wb_const_text(b, "main");
|
||||||
|
wb_class(b, kc, 0, big_kinds, BIG);
|
||||||
|
uint32_t code[] = {
|
||||||
|
wo_ins_abx(WOP_NEW, 0, 0),
|
||||||
|
wo_ins_abc(WOP_BORROW_X, 0, 0, 0), /* ok */
|
||||||
|
wo_ins_abc(WOP_BORROW_X, 0, 0, 0), /* violation: line 99 */
|
||||||
|
wo_ins_abc(WOP_RET0, 0, 0, 0),
|
||||||
|
};
|
||||||
|
uint32_t lines[] = {2, 99};
|
||||||
|
wb_drop drops[] = {{.pc = 1, .owned = 1u << 0, .gc = 0}};
|
||||||
|
wb_method(b, kf, WOB_NONE, 0, 2, code, 4, lines, 1, drops, 1);
|
||||||
|
size_t len;
|
||||||
|
uint8_t *img = wb_finish(b, &len);
|
||||||
|
uint64_t ret = 0;
|
||||||
|
wo_err err = {0};
|
||||||
|
T_EQ(run_img(img, len, &ret, &err), -1);
|
||||||
|
T_EQ(err.code, WO_T_BORROW);
|
||||||
|
T_EQ(err.line, 99);
|
||||||
|
T_EQ(wo_vm_depth(&VM), 0);
|
||||||
|
free(img); /* ASan: the Big instance must have been freed by unwinding */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A child method traps mid-body: owned objects in BOTH frames are freed —
|
||||||
|
* the child's argument (moved in), the child's own allocation, and the
|
||||||
|
* caller's local. */
|
||||||
|
static void test_two_frame_unwind_frees_both(void) {
|
||||||
|
wb_t *b = wb_new();
|
||||||
|
uint32_t kc = wb_const_text(b, "Big");
|
||||||
|
uint32_t kchild = wb_const_text(b, "child");
|
||||||
|
uint32_t kmain = wb_const_text(b, "main");
|
||||||
|
wb_class(b, kc, 0, big_kinds, BIG);
|
||||||
|
/* method 0 = child(o): allocates its own Big, then traps */
|
||||||
|
uint32_t ccode[] = {
|
||||||
|
wo_ins_abx(WOP_NEW, 1, 0),
|
||||||
|
wo_ins_abx(WOP_TRAP, 0, WO_T_EXPLICIT),
|
||||||
|
};
|
||||||
|
wb_drop cdrops[] = {{.pc = 1, .owned = (1u << 0) | (1u << 1), .gc = 0}};
|
||||||
|
wb_method(b, kchild, WOB_NONE, 1, 3, ccode, 2, NULL, 0, cdrops, 1);
|
||||||
|
/* method 1 = main: local Big in r0, arg Big in the call window r2 */
|
||||||
|
uint32_t mcode[] = {
|
||||||
|
wo_ins_abx(WOP_NEW, 0, 0),
|
||||||
|
wo_ins_abx(WOP_NEW, 2, 0),
|
||||||
|
wo_ins_abx(WOP_CALL, 2, 0), /* pc 2: ownership of r2 moves in */
|
||||||
|
wo_ins_abc(WOP_DROP, 0, 0, 0),
|
||||||
|
wo_ins_abc(WOP_RET0, 0, 0, 0),
|
||||||
|
};
|
||||||
|
wb_drop mdrops[] = {{.pc = 2, .owned = 1u << 0, .gc = 0}};
|
||||||
|
wb_method(b, kmain, WOB_NONE, 0, 4, mcode, 5, NULL, 0, mdrops, 1);
|
||||||
|
wb_entry(b, 1);
|
||||||
|
size_t len;
|
||||||
|
uint8_t *img = wb_finish(b, &len);
|
||||||
|
|
||||||
|
wo_module mod;
|
||||||
|
char lerr[256];
|
||||||
|
T_EQ(wo_load_buf(&mod, img, len, lerr, sizeof lerr), 0);
|
||||||
|
T_EQ(wo_vm_init(&VM, &mod, 1 << 20), 0);
|
||||||
|
uint64_t ret = 0;
|
||||||
|
wo_err err = {0};
|
||||||
|
T_EQ(wo_vm_call(&VM, 1, NULL, 0, &ret, &err), -1);
|
||||||
|
T_EQ(err.code, WO_T_EXPLICIT);
|
||||||
|
T_STREQ(err.method, "child");
|
||||||
|
T_EQ(wo_vm_depth(&VM), 0);
|
||||||
|
wo_vm_destroy(&VM);
|
||||||
|
wo_module_free(&mod);
|
||||||
|
free(img); /* ASan: all three Big instances freed across both frames */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* rc inc/dec through opcodes frees exactly at zero (@gc malloc-path class) */
|
||||||
|
static void test_rc_opcodes_free_at_zero(void) {
|
||||||
|
wb_t *b = wb_new();
|
||||||
|
uint32_t kc = wb_const_text(b, "GBig");
|
||||||
|
uint32_t kf = wb_const_text(b, "main");
|
||||||
|
wb_class(b, kc, WO_CLASSF_GC, big_kinds, BIG);
|
||||||
|
uint32_t code[] = {
|
||||||
|
wo_ins_abx(WOP_NEW, 0, 0), /* rc 1 */
|
||||||
|
wo_ins_abc(WOP_RC_INC, 0, 0, 0), /* rc 2 */
|
||||||
|
wo_ins_abc(WOP_RC_DEC, 0, 0, 0), /* rc 1 */
|
||||||
|
wo_ins_abc(WOP_RC_DEC, 0, 0, 0), /* rc 0: freed */
|
||||||
|
wo_ins_abc(WOP_RET0, 0, 0, 0),
|
||||||
|
};
|
||||||
|
wb_method(b, kf, WOB_NONE, 0, 1, code, 5, NULL, 0, NULL, 0);
|
||||||
|
size_t len;
|
||||||
|
uint8_t *img = wb_finish(b, &len);
|
||||||
|
uint64_t ret = 0;
|
||||||
|
wo_err err;
|
||||||
|
T_EQ(run_img(img, len, &ret, &err), 0); /* no trap; ASan proves the free */
|
||||||
|
free(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
test_borrow_violation_frees_owned();
|
||||||
|
test_two_frame_unwind_frees_both();
|
||||||
|
test_rc_opcodes_free_at_zero();
|
||||||
|
return t_report("test_unwind");
|
||||||
|
}
|
||||||
126
runtime/test/test_vm.c
Normal file
126
runtime/test/test_vm.c
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
/* test_vm — interpreter core: window calls, fib recursion, div-by-zero
|
||||||
|
* with line lookup, frame-cap stack overflow, depth zero after traps. */
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#include "loader.h"
|
||||||
|
#include "t.h"
|
||||||
|
#include "vm.h"
|
||||||
|
#include "wob_build.h"
|
||||||
|
|
||||||
|
static wo_vm VM; /* 32K value stack: keep it off the C stack */
|
||||||
|
|
||||||
|
/* run one image's entry-shaped method by index with args */
|
||||||
|
static int run(uint8_t *img, size_t len, uint32_t mi, const uint64_t *args,
|
||||||
|
uint32_t argc, uint64_t *ret, wo_err *err) {
|
||||||
|
wo_module mod;
|
||||||
|
char lerr[256];
|
||||||
|
if (wo_load_buf(&mod, img, len, lerr, sizeof lerr) != 0) {
|
||||||
|
fprintf(stderr, "loader rejected test image: %s\n", lerr);
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
if (wo_vm_init(&VM, &mod, 1 << 20) != 0) {
|
||||||
|
wo_module_free(&mod);
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
int rc = wo_vm_call(&VM, mi, args, argc, ret, err);
|
||||||
|
int depth_after = (int)wo_vm_depth(&VM);
|
||||||
|
wo_vm_destroy(&VM);
|
||||||
|
wo_module_free(&mod);
|
||||||
|
return rc == 0 ? 0 : (depth_after == 0 ? -1 : -3);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_add_window_convention(void) {
|
||||||
|
wb_t *b = wb_new();
|
||||||
|
uint32_t kn = wb_const_text(b, "add");
|
||||||
|
uint32_t code[] = {
|
||||||
|
wo_ins_abc(WOP_ADD, 2, 0, 1),
|
||||||
|
wo_ins_abc(WOP_RET, 2, 0, 0),
|
||||||
|
};
|
||||||
|
wb_method(b, kn, WOB_NONE, 2, 3, code, 2, NULL, 0, NULL, 0);
|
||||||
|
size_t len;
|
||||||
|
uint8_t *img = wb_finish(b, &len);
|
||||||
|
uint64_t args[] = {3, 4}, ret = 0;
|
||||||
|
wo_err err;
|
||||||
|
T_EQ(run(img, len, 0, args, 2, &ret, &err), 0);
|
||||||
|
T_EQ(ret, 7);
|
||||||
|
free(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_fib_recursion(void) {
|
||||||
|
wb_t *b = wb_new();
|
||||||
|
uint32_t kn = wb_const_text(b, "fib");
|
||||||
|
uint32_t k1 = wb_const_int(b, 1);
|
||||||
|
uint32_t k2 = wb_const_int(b, 2);
|
||||||
|
/* fib(n): n<2 -> n; else fib(n-1)+fib(n-2). regc 6, argc 1. */
|
||||||
|
uint32_t code[] = {
|
||||||
|
wo_ins_abx(WOP_LOADK, 1, (uint16_t)k2), /* 0: r1 = 2 */
|
||||||
|
wo_ins_abc(WOP_LT, 2, 0, 1), /* 1: r2 = n < 2 */
|
||||||
|
wo_ins_asbx(WOP_JZ, 2, 1), /* 2: if !r2 -> pc 4 */
|
||||||
|
wo_ins_abc(WOP_RET, 0, 0, 0), /* 3: return n */
|
||||||
|
wo_ins_abx(WOP_LOADK, 1, (uint16_t)k1), /* 4: r1 = 1 */
|
||||||
|
wo_ins_abc(WOP_SUB, 4, 0, 1), /* 5: r4 = n-1 */
|
||||||
|
wo_ins_abx(WOP_CALL, 4, 0), /* 6: r4 = fib(r4) */
|
||||||
|
wo_ins_abx(WOP_LOADK, 1, (uint16_t)k2), /* 7: r1 = 2 */
|
||||||
|
wo_ins_abc(WOP_SUB, 5, 0, 1), /* 8: r5 = n-2 */
|
||||||
|
wo_ins_abx(WOP_CALL, 5, 0), /* 9: r5 = fib(r5) */
|
||||||
|
wo_ins_abc(WOP_ADD, 0, 4, 5), /* 10: r0 = r4+r5 */
|
||||||
|
wo_ins_abc(WOP_RET, 0, 0, 0), /* 11 */
|
||||||
|
};
|
||||||
|
wb_method(b, kn, WOB_NONE, 1, 6, code, 12, NULL, 0, NULL, 0);
|
||||||
|
size_t len;
|
||||||
|
uint8_t *img = wb_finish(b, &len);
|
||||||
|
uint64_t args[] = {10}, ret = 0;
|
||||||
|
wo_err err;
|
||||||
|
T_EQ(run(img, len, 0, args, 1, &ret, &err), 0);
|
||||||
|
T_EQ(ret, 55);
|
||||||
|
free(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_div_zero_traps_with_line(void) {
|
||||||
|
wb_t *b = wb_new();
|
||||||
|
uint32_t kn = wb_const_text(b, "divit");
|
||||||
|
uint32_t k10 = wb_const_int(b, 10);
|
||||||
|
uint32_t k0 = wb_const_int(b, 0);
|
||||||
|
uint32_t code[] = {
|
||||||
|
wo_ins_abx(WOP_LOADK, 0, (uint16_t)k10),
|
||||||
|
wo_ins_abx(WOP_LOADK, 1, (uint16_t)k0),
|
||||||
|
wo_ins_abc(WOP_DIV, 2, 0, 1), /* pc 2, line 42 */
|
||||||
|
wo_ins_abc(WOP_RET, 2, 0, 0),
|
||||||
|
};
|
||||||
|
uint32_t lines[] = {0, 40, 2, 42, 3, 43};
|
||||||
|
wb_method(b, kn, WOB_NONE, 0, 3, code, 4, lines, 3, NULL, 0);
|
||||||
|
size_t len;
|
||||||
|
uint8_t *img = wb_finish(b, &len);
|
||||||
|
uint64_t ret = 0;
|
||||||
|
wo_err err = {0};
|
||||||
|
T_EQ(run(img, len, 0, NULL, 0, &ret, &err), -1); /* trap, depth 0 */
|
||||||
|
T_EQ(err.code, WO_T_DIV0);
|
||||||
|
T_EQ(err.line, 42);
|
||||||
|
T_STREQ(err.method, "divit");
|
||||||
|
free(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_stack_overflow_at_frame_cap(void) {
|
||||||
|
wb_t *b = wb_new();
|
||||||
|
uint32_t kn = wb_const_text(b, "boom");
|
||||||
|
uint32_t code[] = {
|
||||||
|
wo_ins_abx(WOP_CALL, 1, 0), /* call self forever, window +1 */
|
||||||
|
wo_ins_abc(WOP_RET0, 0, 0, 0),
|
||||||
|
};
|
||||||
|
wb_method(b, kn, WOB_NONE, 0, 2, code, 2, NULL, 0, NULL, 0);
|
||||||
|
size_t len;
|
||||||
|
uint8_t *img = wb_finish(b, &len);
|
||||||
|
uint64_t ret = 0;
|
||||||
|
wo_err err = {0};
|
||||||
|
T_EQ(run(img, len, 0, NULL, 0, &ret, &err), -1);
|
||||||
|
T_EQ(err.code, WO_T_STACK);
|
||||||
|
free(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
test_add_window_convention();
|
||||||
|
test_fib_recursion();
|
||||||
|
test_div_zero_traps_with_line();
|
||||||
|
test_stack_overflow_at_frame_cap();
|
||||||
|
return t_report("test_vm");
|
||||||
|
}
|
||||||
69
runtime/test/test_wobbuild.c
Normal file
69
runtime/test/test_wobbuild.c
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
/* test_wobbuild — the in-memory .wob assembler emits exactly the format:
|
||||||
|
* raw header bytes, section offsets, first constant at its stated offset. */
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#include "t.h"
|
||||||
|
#include "wob_build.h"
|
||||||
|
|
||||||
|
static uint32_t rd_u32(const uint8_t *p, size_t off) {
|
||||||
|
uint32_t v;
|
||||||
|
memcpy(&v, p + off, 4);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
wb_t *b = wb_new();
|
||||||
|
uint32_t k_int = wb_const_int(b, 42);
|
||||||
|
uint32_t k_name = wb_const_text(b, "main");
|
||||||
|
T_EQ(k_int, 0);
|
||||||
|
T_EQ(k_name, 1);
|
||||||
|
|
||||||
|
uint32_t code[] = {
|
||||||
|
wo_ins_abx(WOP_LOADK, 0, (uint16_t)k_int),
|
||||||
|
wo_ins_abc(WOP_RET, 0, 0, 0),
|
||||||
|
};
|
||||||
|
uint32_t m = wb_method(b, k_name, WOB_NONE, 0, 1, code, 2, NULL, 0, NULL, 0);
|
||||||
|
T_EQ(m, 0);
|
||||||
|
wb_entry(b, m);
|
||||||
|
|
||||||
|
size_t len = 0;
|
||||||
|
uint8_t *img = wb_finish(b, &len);
|
||||||
|
T_CHECK(img != NULL);
|
||||||
|
T_CHECK(len > WOB_HDR_SIZE);
|
||||||
|
|
||||||
|
/* header */
|
||||||
|
T_EQ(rd_u32(img, WOB_OFF_MAGIC), WOB_MAGIC);
|
||||||
|
T_EQ(rd_u32(img, WOB_OFF_VERSION), WOB_VERSION);
|
||||||
|
uint32_t const_off = rd_u32(img, WOB_OFF_CONST);
|
||||||
|
T_EQ(const_off, WOB_HDR_SIZE); /* constants sit right after the header */
|
||||||
|
T_EQ(rd_u32(img, WOB_OFF_CONST + 4), 2); /* const count */
|
||||||
|
T_EQ(rd_u32(img, WOB_OFF_CLASS + 4), 0);
|
||||||
|
T_EQ(rd_u32(img, WOB_OFF_IFACE + 4), 0);
|
||||||
|
T_EQ(rd_u32(img, WOB_OFF_METHOD + 4), 1);
|
||||||
|
T_EQ(rd_u32(img, WOB_OFF_ENTRY), 0);
|
||||||
|
|
||||||
|
/* first constant: tag 0 (int), i64 42 */
|
||||||
|
T_EQ(img[const_off], WOB_K_INT);
|
||||||
|
int64_t iv;
|
||||||
|
memcpy(&iv, img + const_off + 1, 8);
|
||||||
|
T_EQ(iv, 42);
|
||||||
|
|
||||||
|
/* second constant: tag 1 (text), len 4, "main" */
|
||||||
|
size_t t2 = const_off + 1 + 8;
|
||||||
|
T_EQ(img[t2], WOB_K_TEXT);
|
||||||
|
T_EQ(rd_u32(img, t2 + 1), 4);
|
||||||
|
T_CHECK(memcmp(img + t2 + 5, "main", 4) == 0);
|
||||||
|
|
||||||
|
/* method section: name, class, argc/regc, code length */
|
||||||
|
uint32_t moff = rd_u32(img, WOB_OFF_METHOD);
|
||||||
|
T_EQ(rd_u32(img, moff), k_name);
|
||||||
|
T_EQ(rd_u32(img, moff + 4), WOB_NONE);
|
||||||
|
T_EQ(img[moff + 8], 0); /* argc */
|
||||||
|
T_EQ(img[moff + 9], 1); /* regc */
|
||||||
|
T_EQ(rd_u32(img, moff + 12), 8); /* code_len bytes */
|
||||||
|
T_EQ(rd_u32(img, moff + 16), code[0]);
|
||||||
|
T_EQ(rd_u32(img, moff + 20), code[1]);
|
||||||
|
|
||||||
|
free(img);
|
||||||
|
return t_report("test_wobbuild");
|
||||||
|
}
|
||||||
143
runtime/test/wob_build.c
Normal file
143
runtime/test/wob_build.c
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
#include "wob_build.h"
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* growable byte buffer */
|
||||||
|
typedef struct {
|
||||||
|
uint8_t *p;
|
||||||
|
size_t len, cap;
|
||||||
|
} buf_t;
|
||||||
|
|
||||||
|
static void put(buf_t *b, const void *src, size_t n) {
|
||||||
|
if (n == 0) return;
|
||||||
|
if (b->len + n > b->cap) {
|
||||||
|
size_t ncap = b->cap ? b->cap : 64;
|
||||||
|
while (ncap < b->len + n) ncap *= 2;
|
||||||
|
b->p = realloc(b->p, ncap); /* test helper: abort-on-OOM is fine */
|
||||||
|
b->cap = ncap;
|
||||||
|
}
|
||||||
|
memcpy(b->p + b->len, src, n);
|
||||||
|
b->len += n;
|
||||||
|
}
|
||||||
|
static void put_u8(buf_t *b, uint8_t v) { put(b, &v, 1); }
|
||||||
|
static void put_u16(buf_t *b, uint16_t v) { put(b, &v, 2); }
|
||||||
|
static void put_u32(buf_t *b, uint32_t v) { put(b, &v, 4); }
|
||||||
|
static void put_u64(buf_t *b, uint64_t v) { put(b, &v, 8); }
|
||||||
|
|
||||||
|
struct wb_t {
|
||||||
|
buf_t consts, classes, ifaces, vtabs, methods;
|
||||||
|
uint32_t const_cnt, class_cnt, iface_cnt, vtab_cnt, method_cnt;
|
||||||
|
uint32_t next_slot; /* global interface slot ids accumulate */
|
||||||
|
uint32_t entry;
|
||||||
|
};
|
||||||
|
|
||||||
|
wb_t *wb_new(void) {
|
||||||
|
wb_t *b = calloc(1, sizeof(wb_t));
|
||||||
|
b->entry = WOB_NONE;
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t wb_const_int(wb_t *b, int64_t v) {
|
||||||
|
put_u8(&b->consts, WOB_K_INT);
|
||||||
|
put_u64(&b->consts, (uint64_t)v);
|
||||||
|
return b->const_cnt++;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t wb_const_text(wb_t *b, const char *s) {
|
||||||
|
uint32_t len = (uint32_t)strlen(s);
|
||||||
|
put_u8(&b->consts, WOB_K_TEXT);
|
||||||
|
put_u32(&b->consts, len);
|
||||||
|
put(&b->consts, s, len);
|
||||||
|
return b->const_cnt++;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t wb_class(wb_t *b, uint32_t name_const, uint32_t flags,
|
||||||
|
const uint8_t *kinds, uint32_t field_cnt) {
|
||||||
|
put_u32(&b->classes, name_const);
|
||||||
|
put_u32(&b->classes, flags);
|
||||||
|
put_u32(&b->classes, field_cnt);
|
||||||
|
put(&b->classes, kinds, field_cnt);
|
||||||
|
for (uint32_t pad = field_cnt; pad % 4; pad++) put_u8(&b->classes, 0);
|
||||||
|
return b->class_cnt++;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t wb_iface(wb_t *b, uint32_t name_const, uint32_t method_cnt) {
|
||||||
|
put_u32(&b->ifaces, name_const);
|
||||||
|
put_u32(&b->ifaces, method_cnt);
|
||||||
|
b->next_slot += method_cnt;
|
||||||
|
return b->iface_cnt++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void wb_vtab(wb_t *b, uint32_t class_id, uint32_t iface_id,
|
||||||
|
const uint32_t *methods, uint32_t method_cnt) {
|
||||||
|
put_u32(&b->vtabs, class_id);
|
||||||
|
put_u32(&b->vtabs, iface_id);
|
||||||
|
for (uint32_t i = 0; i < method_cnt; i++) put_u32(&b->vtabs, methods[i]);
|
||||||
|
b->vtab_cnt++;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t wb_method(wb_t *b, uint32_t name_const, uint32_t class_id,
|
||||||
|
uint8_t argc, uint8_t regc, const uint32_t *code,
|
||||||
|
uint32_t ninstr, const uint32_t *lines, uint32_t nlines,
|
||||||
|
const wb_drop *drops, uint32_t ndrops) {
|
||||||
|
buf_t *m = &b->methods;
|
||||||
|
put_u32(m, name_const);
|
||||||
|
put_u32(m, class_id);
|
||||||
|
put_u8(m, argc);
|
||||||
|
put_u8(m, regc);
|
||||||
|
put_u16(m, 0); /* reserved */
|
||||||
|
put_u32(m, ninstr * 4u);
|
||||||
|
for (uint32_t i = 0; i < ninstr; i++) put_u32(m, code[i]);
|
||||||
|
put_u32(m, nlines);
|
||||||
|
for (uint32_t i = 0; i < 2 * nlines; i++) put_u32(m, lines[i]);
|
||||||
|
put_u32(m, ndrops);
|
||||||
|
for (uint32_t i = 0; i < ndrops; i++) {
|
||||||
|
put_u32(m, drops[i].pc);
|
||||||
|
put_u64(m, drops[i].owned);
|
||||||
|
put_u64(m, drops[i].gc);
|
||||||
|
}
|
||||||
|
return b->method_cnt++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void wb_entry(wb_t *b, uint32_t method_idx) { b->entry = method_idx; }
|
||||||
|
|
||||||
|
uint8_t *wb_finish(wb_t *b, size_t *len) {
|
||||||
|
/* interface section = interface entries, then vtab count, then rows */
|
||||||
|
buf_t iface_all = {0};
|
||||||
|
put(&iface_all, b->ifaces.p ? (void *)b->ifaces.p : (void *)"", b->ifaces.len);
|
||||||
|
put_u32(&iface_all, b->vtab_cnt);
|
||||||
|
if (b->vtabs.len) put(&iface_all, b->vtabs.p, b->vtabs.len);
|
||||||
|
|
||||||
|
buf_t out = {0};
|
||||||
|
uint32_t off = WOB_HDR_SIZE;
|
||||||
|
put_u32(&out, WOB_MAGIC);
|
||||||
|
put_u32(&out, WOB_VERSION);
|
||||||
|
put_u32(&out, off);
|
||||||
|
put_u32(&out, b->const_cnt);
|
||||||
|
off += (uint32_t)b->consts.len;
|
||||||
|
put_u32(&out, off);
|
||||||
|
put_u32(&out, b->class_cnt);
|
||||||
|
off += (uint32_t)b->classes.len;
|
||||||
|
put_u32(&out, off);
|
||||||
|
put_u32(&out, b->iface_cnt);
|
||||||
|
off += (uint32_t)iface_all.len;
|
||||||
|
put_u32(&out, off);
|
||||||
|
put_u32(&out, b->method_cnt);
|
||||||
|
put_u32(&out, b->entry);
|
||||||
|
if (b->consts.len) put(&out, b->consts.p, b->consts.len);
|
||||||
|
if (b->classes.len) put(&out, b->classes.p, b->classes.len);
|
||||||
|
put(&out, iface_all.p, iface_all.len);
|
||||||
|
if (b->methods.len) put(&out, b->methods.p, b->methods.len);
|
||||||
|
|
||||||
|
free(b->consts.p);
|
||||||
|
free(b->classes.p);
|
||||||
|
free(b->ifaces.p);
|
||||||
|
free(b->vtabs.p);
|
||||||
|
free(b->methods.p);
|
||||||
|
free(iface_all.p);
|
||||||
|
uint8_t *img = out.p;
|
||||||
|
*len = out.len;
|
||||||
|
free(b);
|
||||||
|
return img;
|
||||||
|
}
|
||||||
52
runtime/test/wob_build.h
Normal file
52
runtime/test/wob_build.h
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
/* wob_build.h — in-memory .wob assembler for tests and fixture generation.
|
||||||
|
* Deliberately a SECOND, independent encoding of the format (the loader is
|
||||||
|
* the first): builder/loader disagreements surface as test failures,
|
||||||
|
* cross-checking docs/plan/oop-vm/00-wob-format.md. Test-side only — never
|
||||||
|
* linked into wovm itself. */
|
||||||
|
#ifndef WO_WOB_BUILD_H
|
||||||
|
#define WO_WOB_BUILD_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#include "wob.h"
|
||||||
|
|
||||||
|
typedef struct wb_t wb_t;
|
||||||
|
|
||||||
|
typedef struct wb_drop {
|
||||||
|
uint32_t pc;
|
||||||
|
uint64_t owned; /* registers holding live owned values at pc */
|
||||||
|
uint64_t gc; /* registers holding live @gc references at pc */
|
||||||
|
} wb_drop;
|
||||||
|
|
||||||
|
wb_t *wb_new(void);
|
||||||
|
|
||||||
|
/* constants: returns the constant index */
|
||||||
|
uint32_t wb_const_int(wb_t *b, int64_t v);
|
||||||
|
uint32_t wb_const_text(wb_t *b, const char *s); /* strlen'd, no NUL stored */
|
||||||
|
|
||||||
|
/* classes: returns the class id */
|
||||||
|
uint32_t wb_class(wb_t *b, uint32_t name_const, uint32_t flags,
|
||||||
|
const uint8_t *kinds, uint32_t field_cnt);
|
||||||
|
|
||||||
|
/* interfaces: returns the interface id; global slot ids accumulate in
|
||||||
|
* declaration order (first interface's methods get slots 0..n-1, etc.) */
|
||||||
|
uint32_t wb_iface(wb_t *b, uint32_t name_const, uint32_t method_cnt);
|
||||||
|
/* vtable row: this class implements this interface with these method
|
||||||
|
* indexes (one per interface method, in interface order) */
|
||||||
|
void wb_vtab(wb_t *b, uint32_t class_id, uint32_t iface_id,
|
||||||
|
const uint32_t *methods, uint32_t method_cnt);
|
||||||
|
|
||||||
|
/* methods: returns the method index. lines = flattened ascending pc,line
|
||||||
|
* pairs (2*nlines u32s). drops = ascending drop-table entries. */
|
||||||
|
uint32_t wb_method(wb_t *b, uint32_t name_const, uint32_t class_id,
|
||||||
|
uint8_t argc, uint8_t regc, const uint32_t *code,
|
||||||
|
uint32_t ninstr, const uint32_t *lines, uint32_t nlines,
|
||||||
|
const wb_drop *drops, uint32_t ndrops);
|
||||||
|
|
||||||
|
void wb_entry(wb_t *b, uint32_t method_idx);
|
||||||
|
|
||||||
|
/* concatenate sections, compute header offsets, free the builder;
|
||||||
|
* returns one malloc'd image (caller frees) */
|
||||||
|
uint8_t *wb_finish(wb_t *b, size_t *len);
|
||||||
|
|
||||||
|
#endif /* WO_WOB_BUILD_H */
|
||||||
979
runtime/wo-rt.c
Normal file
979
runtime/wo-rt.c
Normal file
|
|
@ -0,0 +1,979 @@
|
||||||
|
/*
|
||||||
|
* wo-rt.c — the writeonce runtime environment, in C. Phase E: first load.
|
||||||
|
*
|
||||||
|
* Phases A–D: N pinned threads with raw io_uring loops, SO_REUSEPORT
|
||||||
|
* listeners, keep-alive connections, one mlock'd mmap arena, and durable
|
||||||
|
* commits (RAM apply → framed WAL record → per-tick group fdatasync → ack
|
||||||
|
* on the fsync CQE). Phase E closes the loop: at boot — BEFORE any accept
|
||||||
|
* is armed — each shard thread loads its snapshot and replays its WAL into
|
||||||
|
* its arena slice, in parallel, validating every frame's CRC + COMMIT
|
||||||
|
* trailer and truncating at the first torn record. Appends resume at the
|
||||||
|
* validated tail. A clean shutdown writes a per-shard snapshot
|
||||||
|
* (`shard-<t>.data`) and truncates the WAL; boot prefers snapshot + WAL
|
||||||
|
* tail. The data directory carries a `meta` file pinning the shard count —
|
||||||
|
* a restart with a different WO_THREADS refuses to start (resharding is
|
||||||
|
* plan 09f, not silent data loss). Zero deps beyond libc + kernel uapi.
|
||||||
|
*
|
||||||
|
* build: make run: ./wo-rt [WO_PORT=8085 WO_THREADS=4 WO_DATA=./wo-data ./wo-rt]
|
||||||
|
* poke: curl -X POST localhost:8085/api/notes -d '{"title":"hello"}' # acked after fsync
|
||||||
|
* wal: ./wo-rt wal-check wo-data/shard-0.wal # offline frame/CRC validation
|
||||||
|
*
|
||||||
|
* Phase map: docs/plan/exploration/c-runtime/00-plan.md (A ✅ threads,
|
||||||
|
* B ✅ arena, C ✅ io_uring, D ✅ WAL, E this file, F bench). One-address
|
||||||
|
* trace: 01-architecture.md. Single-binary end goal: 02-single-binary.md.
|
||||||
|
*
|
||||||
|
* Module map (C ↔ Rust ↔ kernel reference card):
|
||||||
|
* ring_init/ring_enter ↔ (plan 09 decision 4: per-thread ring) ↔ linux/07-io_uring.md
|
||||||
|
* arena_init ↔ (plan 10 storage foundations) ↔ linux/08-mmap.md
|
||||||
|
* wal_flush / OP_FSYNC ↔ (plan 11 WAL + 09c per-shard WAL) ↔ linux/12-pwrite-fsync.md, 09-fallocate.md
|
||||||
|
* sig/evfd via POLL_ADD↔ runtime/{signalfd,eventfd}.rs ↔ linux/04-signalfd.md, 02-eventfd.md
|
||||||
|
*
|
||||||
|
* Requires IORING_FEAT_SINGLE_MMAP (≥5.4) and multishot accept (≥5.19).
|
||||||
|
* Phase D opens WALs with O_TRUNC (fresh log each boot) — replay-on-boot and
|
||||||
|
* snapshots are phase E; the crash test inspects the WAL offline via
|
||||||
|
* `wal-check` BEFORE any restart. Simplifications: single-shot RECV re-armed
|
||||||
|
* per request, naive JSON extraction, fixed-size WAL payloads.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <linux/io_uring.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <poll.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <sched.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/eventfd.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <sys/resource.h>
|
||||||
|
#include <sys/signalfd.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/syscall.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#ifndef IORING_ACCEPT_MULTISHOT
|
||||||
|
#define IORING_ACCEPT_MULTISHOT (1U << 0)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define MAX_THREADS 64
|
||||||
|
#ifndef MAX_FDS
|
||||||
|
#define MAX_FDS 16384 /* conn slots per shard, fd-indexed */
|
||||||
|
#endif
|
||||||
|
#define IN_CAP 8192
|
||||||
|
#define OUT_CAP 65536
|
||||||
|
#define PAGE 4096
|
||||||
|
#define HUGE_2M (2u * 1024 * 1024)
|
||||||
|
#ifndef SLOT_SIZE
|
||||||
|
#define SLOT_SIZE 256 /* -D overridable for scale runs (phase F) */
|
||||||
|
#endif
|
||||||
|
#ifndef SLOTS_PER_SHARD
|
||||||
|
#define SLOTS_PER_SHARD 256
|
||||||
|
#endif
|
||||||
|
#define RING_ENTRIES 1024
|
||||||
|
#define WAL_PREALLOC (4u * 1024 * 1024) /* fallocate per shard */
|
||||||
|
#define WAL_COMMIT 0xC0FFEE42u /* frame trailer magic */
|
||||||
|
#define WAL_BATCH_CAP 65536 /* staged bytes per group commit */
|
||||||
|
#define WAL_BATCH_CONNS 256 /* acks parked per batch */
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------- crc32 --
|
||||||
|
* Hand-rolled (poly 0xEDB88320), table built once at boot. Zero deps. */
|
||||||
|
|
||||||
|
static uint32_t crc_table[256];
|
||||||
|
|
||||||
|
static void crc32_init(void) {
|
||||||
|
for (uint32_t i = 0; i < 256; i++) {
|
||||||
|
uint32_t c = i;
|
||||||
|
for (int k = 0; k < 8; k++) c = (c & 1) ? 0xEDB88320u ^ (c >> 1) : c >> 1;
|
||||||
|
crc_table[i] = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t crc32(const void *buf, size_t len) {
|
||||||
|
const uint8_t *p = buf;
|
||||||
|
uint32_t c = 0xFFFFFFFFu;
|
||||||
|
while (len--) c = crc_table[(c ^ *p++) & 0xFF] ^ (c >> 8);
|
||||||
|
return c ^ 0xFFFFFFFFu;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------ WAL frame --
|
||||||
|
* [u32 len][u32 crc(payload)][payload][u32 WAL_COMMIT]. A record replays
|
||||||
|
* whole or not at all: bad len, bad crc, or missing trailer = torn tail.
|
||||||
|
* The payload carries the (shard,slot) coordinates phase B made stable. */
|
||||||
|
|
||||||
|
struct wal_payload {
|
||||||
|
uint32_t op; /* 1 = insert note */
|
||||||
|
uint32_t slot;
|
||||||
|
int32_t id;
|
||||||
|
char title[128];
|
||||||
|
};
|
||||||
|
|
||||||
|
#define WAL_FRAME_BYTES (4 + 4 + sizeof(struct wal_payload) + 4)
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------- arena --
|
||||||
|
* Unchanged from phase B: [header page][shard 0: bitmap page + slots]... */
|
||||||
|
|
||||||
|
struct arena_hdr {
|
||||||
|
char magic[8];
|
||||||
|
uint32_t version;
|
||||||
|
uint32_t n_shards;
|
||||||
|
uint32_t slots_per_shard;
|
||||||
|
uint32_t slot_size;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct slot_note { int32_t id; char title[128]; };
|
||||||
|
|
||||||
|
static uint8_t *arena;
|
||||||
|
static size_t arena_bytes, arena_map_bytes, slice_bytes, bitmap_bytes;
|
||||||
|
static int arena_huge = 0, arena_locked = 0;
|
||||||
|
|
||||||
|
static int arena_init(int n_shards) {
|
||||||
|
bitmap_bytes = ((size_t)SLOTS_PER_SHARD / 8 + PAGE - 1) & ~((size_t)PAGE - 1);
|
||||||
|
slice_bytes = bitmap_bytes + (size_t)SLOTS_PER_SHARD * SLOT_SIZE;
|
||||||
|
arena_bytes = PAGE + (size_t)n_shards * slice_bytes;
|
||||||
|
|
||||||
|
arena_map_bytes = (arena_bytes + HUGE_2M - 1) & ~((size_t)HUGE_2M - 1);
|
||||||
|
arena = mmap(NULL, arena_map_bytes, PROT_READ | PROT_WRITE,
|
||||||
|
MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB | MAP_POPULATE, -1, 0);
|
||||||
|
if (arena != MAP_FAILED) {
|
||||||
|
arena_huge = 1;
|
||||||
|
} else {
|
||||||
|
arena_map_bytes = (arena_bytes + PAGE - 1) & ~((size_t)PAGE - 1);
|
||||||
|
arena = mmap(NULL, arena_map_bytes, PROT_READ | PROT_WRITE,
|
||||||
|
MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0);
|
||||||
|
if (arena == MAP_FAILED) { perror("mmap arena"); return -1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
arena_locked = (mlock(arena, arena_map_bytes) == 0);
|
||||||
|
if (!arena_locked)
|
||||||
|
fprintf(stderr, "[wo-rt-c] warn: mlock refused (%s) — arena not pinned\n", strerror(errno));
|
||||||
|
|
||||||
|
struct arena_hdr *hdr = (struct arena_hdr *)arena;
|
||||||
|
memcpy(hdr->magic, "WORTC\0\0", 8);
|
||||||
|
hdr->version = 3; /* phase C */
|
||||||
|
hdr->n_shards = (uint32_t)n_shards;
|
||||||
|
hdr->slots_per_shard = SLOTS_PER_SHARD;
|
||||||
|
hdr->slot_size = SLOT_SIZE;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint64_t *shard_bitmap(int t) { return (uint64_t *)(arena + PAGE + (size_t)t * slice_bytes); }
|
||||||
|
static uint8_t *shard_slots (int t) { return arena + PAGE + (size_t)t * slice_bytes + bitmap_bytes; }
|
||||||
|
static struct slot_note *slot_at(int t, uint32_t i) {
|
||||||
|
return (struct slot_note *)(shard_slots(t) + (size_t)i * SLOT_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------- io_uring --
|
||||||
|
* The raw ring: three pieces of memory shared with the kernel — the SQ/CQ
|
||||||
|
* ring headers+arrays (one mmap, IORING_FEAT_SINGLE_MMAP) and the SQE array.
|
||||||
|
* Submission: fill sqes[tail&mask], publish tail with a release store, tell
|
||||||
|
* the kernel with ONE io_uring_enter that also waits for completions. */
|
||||||
|
|
||||||
|
struct ring {
|
||||||
|
int fd;
|
||||||
|
unsigned *sq_head, *sq_tail, *sq_mask, *sq_array;
|
||||||
|
unsigned *cq_head, *cq_tail, *cq_mask;
|
||||||
|
struct io_uring_sqe *sqes;
|
||||||
|
struct io_uring_cqe *cqes;
|
||||||
|
unsigned local_tail; /* SQEs filled, not yet published */
|
||||||
|
unsigned to_submit;
|
||||||
|
};
|
||||||
|
|
||||||
|
static int ring_init(struct ring *r) {
|
||||||
|
struct io_uring_params p;
|
||||||
|
memset(&p, 0, sizeof p);
|
||||||
|
r->fd = (int)syscall(__NR_io_uring_setup, RING_ENTRIES, &p);
|
||||||
|
if (r->fd < 0) { perror("io_uring_setup"); return -1; }
|
||||||
|
if (!(p.features & IORING_FEAT_SINGLE_MMAP)) {
|
||||||
|
fprintf(stderr, "[wo-rt-c] kernel lacks IORING_FEAT_SINGLE_MMAP (need >= 5.4)\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t sq_sz = p.sq_off.array + p.sq_entries * sizeof(unsigned);
|
||||||
|
size_t cq_sz = p.cq_off.cqes + p.cq_entries * sizeof(struct io_uring_cqe);
|
||||||
|
size_t sz = sq_sz > cq_sz ? sq_sz : cq_sz;
|
||||||
|
uint8_t *sqcq = mmap(NULL, sz, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE,
|
||||||
|
r->fd, IORING_OFF_SQ_RING);
|
||||||
|
if (sqcq == MAP_FAILED) { perror("mmap sq/cq ring"); return -1; }
|
||||||
|
|
||||||
|
r->sq_head = (unsigned *)(sqcq + p.sq_off.head);
|
||||||
|
r->sq_tail = (unsigned *)(sqcq + p.sq_off.tail);
|
||||||
|
r->sq_mask = (unsigned *)(sqcq + p.sq_off.ring_mask);
|
||||||
|
r->sq_array = (unsigned *)(sqcq + p.sq_off.array);
|
||||||
|
r->cq_head = (unsigned *)(sqcq + p.cq_off.head);
|
||||||
|
r->cq_tail = (unsigned *)(sqcq + p.cq_off.tail);
|
||||||
|
r->cq_mask = (unsigned *)(sqcq + p.cq_off.ring_mask);
|
||||||
|
r->cqes = (struct io_uring_cqe *)(sqcq + p.cq_off.cqes);
|
||||||
|
|
||||||
|
r->sqes = mmap(NULL, p.sq_entries * sizeof(struct io_uring_sqe),
|
||||||
|
PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE,
|
||||||
|
r->fd, IORING_OFF_SQES);
|
||||||
|
if (r->sqes == MAP_FAILED) { perror("mmap sqes"); return -1; }
|
||||||
|
|
||||||
|
r->local_tail = *r->sq_tail;
|
||||||
|
r->to_submit = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct io_uring_sqe *sqe_get(struct ring *r) {
|
||||||
|
unsigned idx = r->local_tail & *r->sq_mask;
|
||||||
|
struct io_uring_sqe *s = &r->sqes[idx];
|
||||||
|
memset(s, 0, sizeof *s);
|
||||||
|
r->sq_array[idx] = idx;
|
||||||
|
r->local_tail++;
|
||||||
|
r->to_submit++;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* user_data = (op << 32) | fd-or-batch-index */
|
||||||
|
enum { OP_ACCEPT = 1, OP_RECV, OP_SEND, OP_EVFD, OP_SIGFD, OP_WALWR, OP_FSYNC };
|
||||||
|
static uint64_t ud(int op, int fd) { return ((uint64_t)op << 32) | (uint32_t)fd; }
|
||||||
|
|
||||||
|
static int ring_enter(struct ring *r, unsigned wait) {
|
||||||
|
__atomic_store_n(r->sq_tail, r->local_tail, __ATOMIC_RELEASE);
|
||||||
|
unsigned n = r->to_submit;
|
||||||
|
r->to_submit = 0;
|
||||||
|
for (;;) {
|
||||||
|
int rc = (int)syscall(__NR_io_uring_enter, r->fd, n, wait,
|
||||||
|
IORING_ENTER_GETEVENTS, NULL, 0);
|
||||||
|
if (rc >= 0) return rc;
|
||||||
|
if (errno == EINTR) { n = 0; continue; } /* already submitted */
|
||||||
|
perror("io_uring_enter");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------- connection --
|
||||||
|
* Keep-alive state machine. Exactly one outstanding SQE per connection:
|
||||||
|
* RECV while a request is being assembled, SEND while a response drains.
|
||||||
|
* Leftover bytes after a request (pipelining) are carried over and parsed
|
||||||
|
* before the next RECV is armed. */
|
||||||
|
|
||||||
|
struct conn {
|
||||||
|
char in[IN_CAP]; size_t in_len;
|
||||||
|
char out[OUT_CAP]; size_t out_len, out_off;
|
||||||
|
int in_use;
|
||||||
|
int closing; /* close once the out buffer drains */
|
||||||
|
int await_durable;/* response parked until this tick's fsync CQE */
|
||||||
|
uint64_t gen; /* incarnation stamp — kernel fds get reused */
|
||||||
|
};
|
||||||
|
|
||||||
|
/* One group-commit batch: staged WAL bytes + the connections whose acks ride
|
||||||
|
* its fsync. Double-buffered: while batch[k] is in flight (write→fsync
|
||||||
|
* linked SQEs), new commits stage into batch[k^1].
|
||||||
|
* Acks are parked as (fd, gen) pairs: an fd alone is ABA-unsafe — a parked
|
||||||
|
* connection can die, the kernel reuses its fd for a NEW connection whose
|
||||||
|
* commit sits in the OTHER batch, and a bare-fd release would ack that new
|
||||||
|
* connection before ITS record is durable. Found by the phase-F crash test
|
||||||
|
* (7 acked-but-unwritten records out of ~990k under reconnect churn). */
|
||||||
|
struct wal_batch {
|
||||||
|
char buf[WAL_BATCH_CAP];
|
||||||
|
size_t len;
|
||||||
|
int conns[WAL_BATCH_CONNS];
|
||||||
|
uint64_t gens[WAL_BATCH_CONNS];
|
||||||
|
int n_conns;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct shard {
|
||||||
|
int id;
|
||||||
|
int lfd, evfd;
|
||||||
|
struct ring ring;
|
||||||
|
pthread_t tid;
|
||||||
|
int next_id;
|
||||||
|
_Atomic int used;
|
||||||
|
int wal_fd;
|
||||||
|
_Atomic size_t wal_off; /* owner-written; stats-readable cross-shard */
|
||||||
|
struct wal_batch batch[2];
|
||||||
|
int active; /* batch being staged */
|
||||||
|
int in_flight; /* a write→fsync pair is on the ring */
|
||||||
|
char snap_path[320];
|
||||||
|
struct conn conns[MAX_FDS];
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Snapshot file: [snap_hdr][bitmap page][slot bytes]. Written on clean
|
||||||
|
* shutdown, loaded at boot before WAL replay. */
|
||||||
|
struct snap_hdr {
|
||||||
|
char magic[8]; /* "WOSNAP\0\0" */
|
||||||
|
uint32_t version;
|
||||||
|
int32_t next_id;
|
||||||
|
int32_t used;
|
||||||
|
};
|
||||||
|
|
||||||
|
static struct shard *shards;
|
||||||
|
static int n_threads = 1;
|
||||||
|
static int sigfd = -1;
|
||||||
|
static _Atomic unsigned long reqs[MAX_THREADS];
|
||||||
|
|
||||||
|
static int slot_alloc(struct shard *sh) {
|
||||||
|
uint64_t *bm = shard_bitmap(sh->id);
|
||||||
|
for (uint32_t w = 0; w < SLOTS_PER_SHARD / 64; w++) {
|
||||||
|
if (bm[w] == UINT64_MAX) continue;
|
||||||
|
uint32_t b = (uint32_t)__builtin_ctzll(~bm[w]);
|
||||||
|
uint32_t i = w * 64 + b;
|
||||||
|
if (i >= SLOTS_PER_SHARD) break;
|
||||||
|
bm[w] |= (1ULL << b);
|
||||||
|
atomic_fetch_add_explicit(&sh->used, 1, memory_order_relaxed);
|
||||||
|
return (int)i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------------------------------------------------------- SQE builders -- */
|
||||||
|
|
||||||
|
static void arm_accept(struct shard *sh) { /* multishot: arm once */
|
||||||
|
struct io_uring_sqe *s = sqe_get(&sh->ring);
|
||||||
|
s->opcode = IORING_OP_ACCEPT;
|
||||||
|
s->fd = sh->lfd;
|
||||||
|
s->ioprio = IORING_ACCEPT_MULTISHOT;
|
||||||
|
s->user_data = ud(OP_ACCEPT, sh->lfd);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void arm_poll(struct shard *sh, int fd, int op) {
|
||||||
|
struct io_uring_sqe *s = sqe_get(&sh->ring);
|
||||||
|
s->opcode = IORING_OP_POLL_ADD;
|
||||||
|
s->fd = fd;
|
||||||
|
s->poll_events = POLLIN;
|
||||||
|
s->user_data = ud(op, fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void arm_recv(struct shard *sh, int fd) {
|
||||||
|
struct conn *c = &sh->conns[fd];
|
||||||
|
struct io_uring_sqe *s = sqe_get(&sh->ring);
|
||||||
|
s->opcode = IORING_OP_RECV;
|
||||||
|
s->fd = fd;
|
||||||
|
s->addr = (uint64_t)(uintptr_t)(c->in + c->in_len);
|
||||||
|
s->len = (uint32_t)(IN_CAP - 1 - c->in_len);
|
||||||
|
s->user_data = ud(OP_RECV, fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void arm_send(struct shard *sh, int fd) {
|
||||||
|
struct conn *c = &sh->conns[fd];
|
||||||
|
struct io_uring_sqe *s = sqe_get(&sh->ring);
|
||||||
|
s->opcode = IORING_OP_SEND;
|
||||||
|
s->fd = fd;
|
||||||
|
s->addr = (uint64_t)(uintptr_t)(c->out + c->out_off);
|
||||||
|
s->len = (uint32_t)(c->out_len - c->out_off);
|
||||||
|
s->msg_flags = MSG_NOSIGNAL;
|
||||||
|
s->user_data = ud(OP_SEND, fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------ WAL flush --
|
||||||
|
* Called once per loop tick. If commits were staged and no batch is in
|
||||||
|
* flight, submit ONE write SQE for the whole batch at the shard's tail
|
||||||
|
* offset, hard-linked to ONE fdatasync SQE. Every parked ack in the batch
|
||||||
|
* is released when the fsync CQE arrives — group commit. */
|
||||||
|
|
||||||
|
static void wal_flush(struct shard *sh) {
|
||||||
|
if (sh->in_flight) return;
|
||||||
|
struct wal_batch *b = &sh->batch[sh->active];
|
||||||
|
if (b->len == 0) return;
|
||||||
|
|
||||||
|
size_t off = atomic_load_explicit(&sh->wal_off, memory_order_relaxed);
|
||||||
|
|
||||||
|
struct io_uring_sqe *w = sqe_get(&sh->ring);
|
||||||
|
w->opcode = IORING_OP_WRITE;
|
||||||
|
w->fd = sh->wal_fd;
|
||||||
|
w->addr = (uint64_t)(uintptr_t)b->buf;
|
||||||
|
w->len = (uint32_t)b->len;
|
||||||
|
w->off = off;
|
||||||
|
w->flags = IOSQE_IO_LINK; /* fsync follows the write */
|
||||||
|
w->user_data = ud(OP_WALWR, sh->active);
|
||||||
|
|
||||||
|
struct io_uring_sqe *f = sqe_get(&sh->ring);
|
||||||
|
f->opcode = IORING_OP_FSYNC;
|
||||||
|
f->fd = sh->wal_fd;
|
||||||
|
f->fsync_flags = IORING_FSYNC_DATASYNC;
|
||||||
|
f->user_data = ud(OP_FSYNC, sh->active);
|
||||||
|
|
||||||
|
sh->in_flight = 1;
|
||||||
|
sh->active ^= 1; /* new commits stage in the twin */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stage one commit's frame + park the connection's ack on the active batch.
|
||||||
|
* Returns 0 if the batch has no room (caller responds 503, no RAM apply). */
|
||||||
|
static int wal_append(struct shard *sh, int connfd, uint32_t slot, int32_t id, const char *title) {
|
||||||
|
struct wal_batch *b = &sh->batch[sh->active];
|
||||||
|
if (b->len + WAL_FRAME_BYTES > WAL_BATCH_CAP || b->n_conns >= WAL_BATCH_CONNS)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
struct wal_payload p;
|
||||||
|
memset(&p, 0, sizeof p);
|
||||||
|
p.op = 1;
|
||||||
|
p.slot = slot;
|
||||||
|
p.id = id;
|
||||||
|
snprintf(p.title, sizeof p.title, "%s", title);
|
||||||
|
b->gens[b->n_conns] = sh->conns[connfd].gen;
|
||||||
|
|
||||||
|
uint32_t len = (uint32_t)sizeof p;
|
||||||
|
uint32_t crc = crc32(&p, sizeof p);
|
||||||
|
uint32_t end = WAL_COMMIT;
|
||||||
|
char *dst = b->buf + b->len;
|
||||||
|
memcpy(dst, &len, 4);
|
||||||
|
memcpy(dst + 4, &crc, 4);
|
||||||
|
memcpy(dst + 8, &p, sizeof p);
|
||||||
|
memcpy(dst + 8 + sizeof p, &end, 4);
|
||||||
|
b->len += WAL_FRAME_BYTES;
|
||||||
|
b->conns[b->n_conns++] = connfd;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------- http -- */
|
||||||
|
|
||||||
|
static void respond(struct conn *c, const char *status, const char *ctype, const char *body) {
|
||||||
|
size_t blen = strlen(body);
|
||||||
|
int n = snprintf(c->out + c->out_len, OUT_CAP - c->out_len,
|
||||||
|
"HTTP/1.1 %s\r\nContent-Type: %s\r\nContent-Length: %zu\r\nConnection: %s\r\n\r\n%s",
|
||||||
|
status, ctype, blen, c->closing ? "close" : "keep-alive", body);
|
||||||
|
if (n > 0 && (size_t)n < OUT_CAP - c->out_len) c->out_len += (size_t)n;
|
||||||
|
else c->closing = 1; /* response too big — drop conn */
|
||||||
|
}
|
||||||
|
|
||||||
|
static int json_title(const char *body, char *out, size_t cap) {
|
||||||
|
const char *p = strstr(body, "\"title\"");
|
||||||
|
if (!p) return 0;
|
||||||
|
p = strchr(p + 7, ':'); if (!p) return 0;
|
||||||
|
p = strchr(p, '"'); if (!p) return 0;
|
||||||
|
p++;
|
||||||
|
size_t i = 0;
|
||||||
|
while (*p && *p != '"' && i + 1 < cap) out[i++] = *p++;
|
||||||
|
out[i] = 0;
|
||||||
|
return i > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void route(struct shard *sh, struct conn *c, const char *method, const char *path, const char *body) {
|
||||||
|
atomic_fetch_add_explicit(&reqs[sh->id], 1, memory_order_relaxed);
|
||||||
|
|
||||||
|
if (!strcmp(method, "GET") && !strcmp(path, "/")) {
|
||||||
|
char out[1024];
|
||||||
|
size_t off = (size_t)snprintf(out, sizeof out,
|
||||||
|
"{\"runtime\":\"wo-rt-c\",\"loop\":\"io_uring\",\"threads\":%d,\"shard\":%d,"
|
||||||
|
"\"arena\":{\"bytes\":%zu,\"mapped\":%zu,\"hugepages\":%s,\"mlocked\":%s,"
|
||||||
|
"\"slot_size\":%d,\"slots_per_shard\":%d},\"shard_used\":[",
|
||||||
|
n_threads, sh->id, arena_bytes, arena_map_bytes,
|
||||||
|
arena_huge ? "true" : "false", arena_locked ? "true" : "false",
|
||||||
|
SLOT_SIZE, SLOTS_PER_SHARD);
|
||||||
|
for (int t = 0; t < n_threads; t++)
|
||||||
|
off += (size_t)snprintf(out + off, sizeof out - off, "%s%d", t ? "," : "",
|
||||||
|
atomic_load_explicit(&shards[t].used, memory_order_relaxed));
|
||||||
|
off += (size_t)snprintf(out + off, sizeof out - off, "],\"shard_requests\":[");
|
||||||
|
for (int t = 0; t < n_threads; t++)
|
||||||
|
off += (size_t)snprintf(out + off, sizeof out - off, "%s%lu", t ? "," : "",
|
||||||
|
atomic_load_explicit(&reqs[t], memory_order_relaxed));
|
||||||
|
off += (size_t)snprintf(out + off, sizeof out - off, "],\"wal_bytes\":[");
|
||||||
|
for (int t = 0; t < n_threads; t++)
|
||||||
|
off += (size_t)snprintf(out + off, sizeof out - off, "%s%zu", t ? "," : "",
|
||||||
|
atomic_load_explicit(&shards[t].wal_off, memory_order_relaxed));
|
||||||
|
snprintf(out + off, sizeof out - off, "]}");
|
||||||
|
respond(c, "200 OK", "application/json", out);
|
||||||
|
} else if (!strcmp(method, "GET") && !strcmp(path, "/healthz")) {
|
||||||
|
respond(c, "200 OK", "text/plain", "ok");
|
||||||
|
} else if (!strcmp(method, "GET") && !strcmp(path, "/api/notes")) {
|
||||||
|
static _Thread_local char out[SLOTS_PER_SHARD * 160 + 64];
|
||||||
|
uint64_t *bm = shard_bitmap(sh->id);
|
||||||
|
size_t off = (size_t)snprintf(out, sizeof out, "{\"shard\":%d,\"notes\":[", sh->id);
|
||||||
|
int first = 1;
|
||||||
|
for (uint32_t i = 0; i < SLOTS_PER_SHARD; i++) {
|
||||||
|
if (!(bm[i / 64] & (1ULL << (i % 64)))) continue;
|
||||||
|
struct slot_note *n = slot_at(sh->id, i);
|
||||||
|
off += (size_t)snprintf(out + off, sizeof out - off,
|
||||||
|
"%s{\"id\":%d,\"title\":\"%s\"}", first ? "" : ",", n->id, n->title);
|
||||||
|
first = 0;
|
||||||
|
}
|
||||||
|
snprintf(out + off, sizeof out - off, "]}");
|
||||||
|
respond(c, "200 OK", "application/json", out);
|
||||||
|
} else if (!strcmp(method, "POST") && !strcmp(path, "/api/notes")) {
|
||||||
|
char title[128];
|
||||||
|
int i;
|
||||||
|
if (!json_title(body, title, sizeof title)) {
|
||||||
|
respond(c, "400 Bad Request", "application/json", "{\"error\":\"expected {\\\"title\\\":\\\"...\\\"}\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/* Capacity gates BEFORE the RAM apply — an aborted op writes nothing. */
|
||||||
|
struct wal_batch *b = &sh->batch[sh->active];
|
||||||
|
if (b->len + WAL_FRAME_BYTES > WAL_BATCH_CAP || b->n_conns >= WAL_BATCH_CONNS) {
|
||||||
|
respond(c, "503 Service Unavailable", "application/json", "{\"error\":\"commit batch full, retry\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((i = slot_alloc(sh)) < 0) {
|
||||||
|
respond(c, "507 Insufficient Storage", "application/json", "{\"error\":\"shard full\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
struct slot_note *n = slot_at(sh->id, (uint32_t)i); /* 1. the RAM apply */
|
||||||
|
n->id = sh->next_id;
|
||||||
|
sh->next_id += n_threads;
|
||||||
|
snprintf(n->title, sizeof n->title, "%s", title);
|
||||||
|
char out[224];
|
||||||
|
snprintf(out, sizeof out, "{\"id\":%d,\"title\":\"%s\",\"shard\":%d,\"slot\":%d}",
|
||||||
|
n->id, n->title, sh->id, i);
|
||||||
|
respond(c, "201 Created", "application/json", out); /* built, NOT sent */
|
||||||
|
int connfd = (int)(c - sh->conns); /* conns is fd-indexed */
|
||||||
|
wal_append(sh, connfd, (uint32_t)i, n->id, n->title);/* 2. stage WAL frame */
|
||||||
|
c->await_durable = 1; /* 4. ack rides fsync */
|
||||||
|
} else {
|
||||||
|
respond(c, "404 Not Found", "application/json", "{\"error\":\"no such route\"}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------- state machine ----- */
|
||||||
|
|
||||||
|
static void conn_open(struct shard *sh, int fd) {
|
||||||
|
struct conn *c = &sh->conns[fd];
|
||||||
|
c->in_len = c->out_len = c->out_off = 0;
|
||||||
|
c->closing = 0;
|
||||||
|
c->await_durable = 0;
|
||||||
|
c->gen++; /* new incarnation — stale parked acks won't match */
|
||||||
|
c->in_use = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void conn_close(struct shard *sh, int fd) {
|
||||||
|
if (fd >= 0 && fd < MAX_FDS) sh->conns[fd].in_use = 0;
|
||||||
|
close(fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Try to consume ONE complete request from the in buffer. Returns 1 if a
|
||||||
|
* response was produced (out has bytes), 0 if the request is incomplete. */
|
||||||
|
static int try_process(struct shard *sh, struct conn *c) {
|
||||||
|
c->in[c->in_len] = 0;
|
||||||
|
char *hdr_end = strstr(c->in, "\r\n\r\n");
|
||||||
|
if (!hdr_end) return 0;
|
||||||
|
char *body = hdr_end + 4;
|
||||||
|
size_t total = (size_t)(body - c->in);
|
||||||
|
|
||||||
|
const char *cl = strcasestr(c->in, "Content-Length:");
|
||||||
|
if (cl) {
|
||||||
|
long want = strtol(cl + 15, NULL, 10);
|
||||||
|
if (want < 0) want = 0;
|
||||||
|
if (c->in_len < total + (size_t)want) return 0;
|
||||||
|
total += (size_t)want;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HTTP/1.1 defaults to keep-alive; honor an explicit close. */
|
||||||
|
if (strcasestr(c->in, "connection: close") ||
|
||||||
|
(strstr(c->in, "HTTP/1.0") && !strcasestr(c->in, "connection: keep-alive")))
|
||||||
|
c->closing = 1;
|
||||||
|
|
||||||
|
char method[8] = {0}, path[256] = {0};
|
||||||
|
if (sscanf(c->in, "%7s %255s", method, path) == 2)
|
||||||
|
route(sh, c, method, path, body);
|
||||||
|
else
|
||||||
|
c->closing = 1;
|
||||||
|
|
||||||
|
memmove(c->in, c->in + total, c->in_len - total); /* carry pipelined tail */
|
||||||
|
c->in_len -= total;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Advance a connection: drain out via SEND, else parse, else arm RECV.
|
||||||
|
* A parked commit ack arms nothing — the fsync CQE handler resumes it. */
|
||||||
|
static void conn_continue(struct shard *sh, int fd) {
|
||||||
|
struct conn *c = &sh->conns[fd];
|
||||||
|
if (c->await_durable) { return; }
|
||||||
|
if (c->out_off < c->out_len) { arm_send(sh, fd); return; }
|
||||||
|
c->out_off = c->out_len = 0;
|
||||||
|
if (c->closing) { conn_close(sh, fd); return; }
|
||||||
|
if (try_process(sh, c)) {
|
||||||
|
/* route() may have JUST parked this response (await set inside
|
||||||
|
* try_process) — sending now would race the fsync. The fsync CQE
|
||||||
|
* re-enters here with await cleared and arms the send.
|
||||||
|
* (Found by the phase-F crash-under-load test: ~6 acked-but-
|
||||||
|
* unwritten records per ~750k at the kill instant.) */
|
||||||
|
if (!c->await_durable) arm_send(sh, fd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (c->in_len >= IN_CAP - 1) { conn_close(sh, fd); return; } /* oversize head */
|
||||||
|
arm_recv(sh, fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------------------------------------------------------------- recovery --
|
||||||
|
* First load: hard drive → RAM, per shard, in parallel, BEFORE accept arms.
|
||||||
|
* Snapshot (if any) restores the slice wholesale; the WAL tail replays
|
||||||
|
* commits since that snapshot. Frame validation is wal-check's logic with
|
||||||
|
* the printf swapped for the arena apply. */
|
||||||
|
|
||||||
|
static long now_ms(void) {
|
||||||
|
struct timespec ts;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||||
|
return ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int snap_load(struct shard *sh) {
|
||||||
|
int fd = open(sh->snap_path, O_RDONLY | O_CLOEXEC);
|
||||||
|
if (fd < 0) return 0;
|
||||||
|
struct snap_hdr h;
|
||||||
|
size_t slot_bytes = (size_t)SLOTS_PER_SHARD * SLOT_SIZE;
|
||||||
|
if (read(fd, &h, sizeof h) != (ssize_t)sizeof h ||
|
||||||
|
memcmp(h.magic, "WOSNAP\0", 8) != 0 ||
|
||||||
|
pread(fd, shard_bitmap(sh->id), bitmap_bytes, (off_t)sizeof h) != (ssize_t)bitmap_bytes ||
|
||||||
|
pread(fd, shard_slots(sh->id), slot_bytes, (off_t)(sizeof h + bitmap_bytes)) != (ssize_t)slot_bytes) {
|
||||||
|
fprintf(stderr, "[wo-rt-c] shard %d: snapshot unreadable — starting from WAL only\n", sh->id);
|
||||||
|
memset(shard_bitmap(sh->id), 0, bitmap_bytes + slot_bytes);
|
||||||
|
close(fd);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
sh->next_id = h.next_id;
|
||||||
|
atomic_store_explicit(&sh->used, h.used, memory_order_relaxed);
|
||||||
|
close(fd);
|
||||||
|
return h.used;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int wal_replay(struct shard *sh) {
|
||||||
|
size_t off = 0;
|
||||||
|
int recs = 0;
|
||||||
|
int32_t maxid = 0;
|
||||||
|
for (;;) {
|
||||||
|
uint32_t len, crc, end;
|
||||||
|
struct wal_payload p;
|
||||||
|
if (pread(sh->wal_fd, &len, 4, (off_t)off) != 4 || len == 0) break;
|
||||||
|
if (len != sizeof p ||
|
||||||
|
pread(sh->wal_fd, &crc, 4, (off_t)(off + 4)) != 4 ||
|
||||||
|
pread(sh->wal_fd, &p, sizeof p, (off_t)(off + 8)) != (ssize_t)sizeof p ||
|
||||||
|
pread(sh->wal_fd, &end, 4, (off_t)(off + 8 + sizeof p)) != 4 ||
|
||||||
|
crc32(&p, sizeof p) != crc || end != WAL_COMMIT) {
|
||||||
|
fprintf(stderr, "[wo-rt-c] shard %d: torn WAL record at byte %zu — truncating\n",
|
||||||
|
sh->id, off);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (p.op == 1 && p.slot < SLOTS_PER_SHARD) { /* idempotent apply */
|
||||||
|
uint64_t *bm = shard_bitmap(sh->id);
|
||||||
|
if (!(bm[p.slot / 64] & (1ULL << (p.slot % 64)))) {
|
||||||
|
bm[p.slot / 64] |= (1ULL << (p.slot % 64));
|
||||||
|
atomic_fetch_add_explicit(&sh->used, 1, memory_order_relaxed);
|
||||||
|
}
|
||||||
|
struct slot_note *n = slot_at(sh->id, p.slot);
|
||||||
|
n->id = p.id;
|
||||||
|
snprintf(n->title, sizeof n->title, "%s", p.title);
|
||||||
|
if (p.id > maxid) maxid = p.id;
|
||||||
|
}
|
||||||
|
recs++;
|
||||||
|
off += WAL_FRAME_BYTES;
|
||||||
|
}
|
||||||
|
/* Resume appends at the validated tail; drop torn bytes, re-preallocate. */
|
||||||
|
if (ftruncate(sh->wal_fd, (off_t)off) == 0)
|
||||||
|
(void)!fallocate(sh->wal_fd, 0, 0, off > WAL_PREALLOC ? off : WAL_PREALLOC);
|
||||||
|
atomic_store_explicit(&sh->wal_off, off, memory_order_relaxed);
|
||||||
|
if (maxid > 0 && maxid + n_threads > sh->next_id)
|
||||||
|
sh->next_id = maxid + n_threads; /* interleaved high-water */
|
||||||
|
return recs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Clean-shutdown snapshot: write slice → fsync → atomic rename → truncate WAL. */
|
||||||
|
static void snap_write(struct shard *sh) {
|
||||||
|
char tmp[336];
|
||||||
|
snprintf(tmp, sizeof tmp, "%s.tmp", sh->snap_path);
|
||||||
|
int fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
|
||||||
|
if (fd < 0) { perror("snapshot open"); return; }
|
||||||
|
struct snap_hdr h;
|
||||||
|
memset(&h, 0, sizeof h);
|
||||||
|
memcpy(h.magic, "WOSNAP\0", 8);
|
||||||
|
h.version = 1;
|
||||||
|
h.next_id = sh->next_id;
|
||||||
|
h.used = atomic_load_explicit(&sh->used, memory_order_relaxed);
|
||||||
|
size_t slot_bytes = (size_t)SLOTS_PER_SHARD * SLOT_SIZE;
|
||||||
|
int ok = write(fd, &h, sizeof h) == (ssize_t)sizeof h
|
||||||
|
&& write(fd, shard_bitmap(sh->id), bitmap_bytes) == (ssize_t)bitmap_bytes
|
||||||
|
&& write(fd, shard_slots(sh->id), slot_bytes) == (ssize_t)slot_bytes
|
||||||
|
&& fsync(fd) == 0;
|
||||||
|
close(fd);
|
||||||
|
if (!ok || rename(tmp, sh->snap_path) < 0) { fprintf(stderr, "[wo-rt-c] shard %d: snapshot failed\n", sh->id); unlink(tmp); return; }
|
||||||
|
if (ftruncate(sh->wal_fd, 0) == 0) { /* WAL now redundant */
|
||||||
|
(void)!fallocate(sh->wal_fd, 0, 0, WAL_PREALLOC);
|
||||||
|
fsync(sh->wal_fd);
|
||||||
|
}
|
||||||
|
printf("[wo-rt-c] shard %d: snapshot %d rows → %s, wal truncated\n", sh->id, h.used, sh->snap_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------ shard loop -- */
|
||||||
|
|
||||||
|
static void *shard_main(void *arg) {
|
||||||
|
struct shard *sh = arg;
|
||||||
|
struct ring *r = &sh->ring;
|
||||||
|
|
||||||
|
cpu_set_t set;
|
||||||
|
CPU_ZERO(&set);
|
||||||
|
CPU_SET((unsigned)sh->id % (unsigned)sysconf(_SC_NPROCESSORS_ONLN), &set);
|
||||||
|
pthread_setaffinity_np(pthread_self(), sizeof set, &set);
|
||||||
|
|
||||||
|
/* First load: disk → RAM, before any accept is armed. */
|
||||||
|
long t0 = now_ms();
|
||||||
|
int srows = snap_load(sh);
|
||||||
|
int wrecs = wal_replay(sh);
|
||||||
|
if (srows || wrecs)
|
||||||
|
printf("[wo-rt-c] shard %d: recovered %d snapshot rows + %d wal records in %ld ms\n",
|
||||||
|
sh->id, srows, wrecs, now_ms() - t0);
|
||||||
|
|
||||||
|
arm_accept(sh);
|
||||||
|
arm_poll(sh, sh->evfd, OP_EVFD);
|
||||||
|
if (sh->id == 0) arm_poll(sh, sigfd, OP_SIGFD);
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
if (ring_enter(r, 1) < 0) break; /* ONE syscall per tick */
|
||||||
|
|
||||||
|
unsigned head = *r->cq_head;
|
||||||
|
unsigned tail = __atomic_load_n(r->cq_tail, __ATOMIC_ACQUIRE);
|
||||||
|
for (; head != tail; head++) {
|
||||||
|
struct io_uring_cqe *cqe = &r->cqes[head & *r->cq_mask];
|
||||||
|
int op = (int)(cqe->user_data >> 32);
|
||||||
|
int fd = (int)(uint32_t)cqe->user_data;
|
||||||
|
int res = cqe->res;
|
||||||
|
|
||||||
|
switch (op) {
|
||||||
|
case OP_EVFD: {
|
||||||
|
uint64_t v;
|
||||||
|
(void)!read(sh->evfd, &v, sizeof v);
|
||||||
|
__atomic_store_n(r->cq_head, head + 1, __ATOMIC_RELEASE);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
case OP_SIGFD: { /* shard 0 only */
|
||||||
|
struct signalfd_siginfo si;
|
||||||
|
if (read(sigfd, &si, sizeof si) == sizeof si)
|
||||||
|
printf("\n[wo-rt-c] signal %u — broadcasting shutdown to %d shards\n",
|
||||||
|
si.ssi_signo, n_threads);
|
||||||
|
uint64_t one = 1;
|
||||||
|
for (int t = 0; t < n_threads; t++)
|
||||||
|
(void)!write(shards[t].evfd, &one, sizeof one);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OP_ACCEPT: {
|
||||||
|
if (res >= 0) {
|
||||||
|
int cfd = res;
|
||||||
|
if (cfd >= MAX_FDS) close(cfd);
|
||||||
|
else { conn_open(sh, cfd); arm_recv(sh, cfd); }
|
||||||
|
}
|
||||||
|
if (!(cqe->flags & IORING_CQE_F_MORE)) arm_accept(sh); /* re-arm */
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OP_RECV: {
|
||||||
|
struct conn *c = &sh->conns[fd];
|
||||||
|
if (!c->in_use) break;
|
||||||
|
if (res <= 0) { conn_close(sh, fd); break; }
|
||||||
|
c->in_len += (size_t)res;
|
||||||
|
conn_continue(sh, fd);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OP_SEND: {
|
||||||
|
struct conn *c = &sh->conns[fd];
|
||||||
|
if (!c->in_use) break;
|
||||||
|
if (res <= 0) { conn_close(sh, fd); break; }
|
||||||
|
c->out_off += (size_t)res;
|
||||||
|
conn_continue(sh, fd);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OP_WALWR: { /* fd field carries the batch idx */
|
||||||
|
struct wal_batch *b = &sh->batch[fd];
|
||||||
|
if (res != (int)b->len)
|
||||||
|
fprintf(stderr, "[wo-rt-c] shard %d: WAL write %d != %zu\n", sh->id, res, b->len);
|
||||||
|
else
|
||||||
|
atomic_fetch_add_explicit(&sh->wal_off, b->len, memory_order_relaxed);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OP_FSYNC: { /* group commit lands: release acks */
|
||||||
|
struct wal_batch *b = &sh->batch[fd];
|
||||||
|
int failed = (res < 0); /* incl. -ECANCELED from a failed link */
|
||||||
|
if (failed)
|
||||||
|
fprintf(stderr, "[wo-rt-c] shard %d: fsync failed (%d) — dropping %d acks\n",
|
||||||
|
sh->id, res, b->n_conns);
|
||||||
|
for (int k = 0; k < b->n_conns; k++) {
|
||||||
|
int cfd = b->conns[k];
|
||||||
|
if (cfd < 0 || cfd >= MAX_FDS) continue;
|
||||||
|
struct conn *c = &sh->conns[cfd];
|
||||||
|
if (!c->in_use || !c->await_durable) continue;
|
||||||
|
if (c->gen != b->gens[k]) continue; /* fd reused — not ours */
|
||||||
|
c->await_durable = 0;
|
||||||
|
if (failed) conn_close(sh, cfd); /* never ack non-durable */
|
||||||
|
else conn_continue(sh, cfd);
|
||||||
|
}
|
||||||
|
b->len = 0;
|
||||||
|
b->n_conns = 0;
|
||||||
|
sh->in_flight = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
__atomic_store_n(r->cq_head, head, __ATOMIC_RELEASE);
|
||||||
|
wal_flush(sh); /* one write→fsync pair per tick */
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------- listener -- */
|
||||||
|
|
||||||
|
static int listener_bind(uint16_t port) {
|
||||||
|
int fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
|
||||||
|
if (fd < 0) { perror("socket"); return -1; }
|
||||||
|
int one = 1;
|
||||||
|
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
|
||||||
|
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof one);
|
||||||
|
struct sockaddr_in addr = {0};
|
||||||
|
addr.sin_family = AF_INET;
|
||||||
|
addr.sin_port = htons(port);
|
||||||
|
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||||
|
if (bind(fd, (struct sockaddr *)&addr, sizeof addr) < 0) { perror("bind"); close(fd); return -1; }
|
||||||
|
if (listen(fd, SOMAXCONN) < 0) { perror("listen"); close(fd); return -1; }
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int sig_setup(void) {
|
||||||
|
sigset_t mask;
|
||||||
|
sigemptyset(&mask);
|
||||||
|
sigaddset(&mask, SIGINT);
|
||||||
|
sigaddset(&mask, SIGTERM);
|
||||||
|
if (sigprocmask(SIG_BLOCK, &mask, NULL) < 0) { perror("sigprocmask"); return -1; }
|
||||||
|
int fd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);
|
||||||
|
if (fd < 0) perror("signalfd");
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------ wal-check --
|
||||||
|
* Offline frame walker: validates every record's len/CRC/COMMIT trailer,
|
||||||
|
* reports the count and where (if anywhere) the log tears. This is the
|
||||||
|
* crash test's witness, and the skeleton of phase E's replay loop. */
|
||||||
|
|
||||||
|
static int wal_check(const char *path) {
|
||||||
|
int fd = open(path, O_RDONLY);
|
||||||
|
if (fd < 0) { fprintf(stderr, "wal-check: %s: %s\n", path, strerror(errno)); return 1; }
|
||||||
|
size_t off = 0;
|
||||||
|
int recs = 0;
|
||||||
|
for (;;) {
|
||||||
|
uint32_t len, crc, end;
|
||||||
|
struct wal_payload p;
|
||||||
|
if (pread(fd, &len, 4, (off_t)off) != 4) break;
|
||||||
|
if (len == 0) break; /* fallocate'd tail */
|
||||||
|
if (len != sizeof p) {
|
||||||
|
printf("%s: TORN at byte %zu (bad len %u) — %d whole records before it\n",
|
||||||
|
path, off, len, recs);
|
||||||
|
close(fd);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (pread(fd, &crc, 4, (off_t)(off + 4)) != 4 ||
|
||||||
|
pread(fd, &p, sizeof p, (off_t)(off + 8)) != (ssize_t)sizeof p ||
|
||||||
|
pread(fd, &end, 4, (off_t)(off + 8 + sizeof p)) != 4 ||
|
||||||
|
crc32(&p, sizeof p) != crc || end != WAL_COMMIT) {
|
||||||
|
printf("%s: TORN at byte %zu (bad crc/trailer) — %d whole records before it\n",
|
||||||
|
path, off, recs);
|
||||||
|
close(fd);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
printf("%s: rec %d op=%u slot=%u id=%d title=\"%s\"\n", path, recs, p.op, p.slot, p.id, p.title);
|
||||||
|
recs++;
|
||||||
|
off += WAL_FRAME_BYTES;
|
||||||
|
}
|
||||||
|
printf("%s: %d records, all frames valid, clean tail at byte %zu\n", path, recs, off);
|
||||||
|
close(fd);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------- main -- */
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
crc32_init();
|
||||||
|
|
||||||
|
if (argc >= 3 && !strcmp(argv[1], "wal-check")) {
|
||||||
|
int rc = 0;
|
||||||
|
for (int a = 2; a < argc; a++) rc |= wal_check(argv[a]);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t port = 8085;
|
||||||
|
const char *env = getenv("WO_PORT");
|
||||||
|
if (env && atoi(env) > 0) port = (uint16_t)atoi(env);
|
||||||
|
|
||||||
|
long cores = sysconf(_SC_NPROCESSORS_ONLN);
|
||||||
|
n_threads = (int)cores;
|
||||||
|
env = getenv("WO_THREADS");
|
||||||
|
if (env && atoi(env) > 0) n_threads = atoi(env);
|
||||||
|
if (n_threads < 1) n_threads = 1;
|
||||||
|
if (n_threads > MAX_THREADS) n_threads = MAX_THREADS;
|
||||||
|
|
||||||
|
/* Million-connection posture: lift the fd ceiling to the hard max. */
|
||||||
|
struct rlimit rl;
|
||||||
|
if (getrlimit(RLIMIT_NOFILE, &rl) == 0 && rl.rlim_cur < rl.rlim_max) {
|
||||||
|
rl.rlim_cur = rl.rlim_max;
|
||||||
|
setrlimit(RLIMIT_NOFILE, &rl);
|
||||||
|
}
|
||||||
|
|
||||||
|
sigfd = sig_setup();
|
||||||
|
if (sigfd < 0) return 1;
|
||||||
|
if (arena_init(n_threads) < 0) return 1;
|
||||||
|
|
||||||
|
const char *data_dir = getenv("WO_DATA");
|
||||||
|
if (!data_dir || !*data_dir) data_dir = "./wo-data";
|
||||||
|
if (mkdir(data_dir, 0755) < 0 && errno != EEXIST) { perror("mkdir data dir"); return 1; }
|
||||||
|
|
||||||
|
/* The data dir is sharded for exactly n_threads. A different WO_THREADS
|
||||||
|
* would strand WAL/snapshot files silently — refuse (resharding = 09f). */
|
||||||
|
char mpath[512];
|
||||||
|
snprintf(mpath, sizeof mpath, "%s/meta", data_dir);
|
||||||
|
FILE *mf = fopen(mpath, "r");
|
||||||
|
if (mf) {
|
||||||
|
int prev = 0;
|
||||||
|
if (fscanf(mf, "%d", &prev) == 1 && prev != n_threads) {
|
||||||
|
fprintf(stderr, "[wo-rt-c] %s was written with WO_THREADS=%d — restart with that, or wipe the dir\n",
|
||||||
|
data_dir, prev);
|
||||||
|
fclose(mf);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
fclose(mf);
|
||||||
|
} else if ((mf = fopen(mpath, "w"))) {
|
||||||
|
fprintf(mf, "%d\n", n_threads);
|
||||||
|
fclose(mf);
|
||||||
|
}
|
||||||
|
|
||||||
|
shards = calloc((size_t)n_threads, sizeof(struct shard));
|
||||||
|
if (!shards) { perror("calloc"); return 1; }
|
||||||
|
|
||||||
|
for (int t = 0; t < n_threads; t++) {
|
||||||
|
struct shard *sh = &shards[t];
|
||||||
|
sh->id = t;
|
||||||
|
sh->next_id = t + 1;
|
||||||
|
sh->lfd = listener_bind(port);
|
||||||
|
sh->evfd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
|
||||||
|
if (sh->lfd < 0 || sh->evfd < 0) return 1;
|
||||||
|
if (ring_init(&sh->ring) < 0) return 1;
|
||||||
|
|
||||||
|
/* Per-shard WAL, preallocated, NOT truncated — boot replays it. */
|
||||||
|
char path[512];
|
||||||
|
snprintf(path, sizeof path, "%s/shard-%d.wal", data_dir, t);
|
||||||
|
sh->wal_fd = open(path, O_RDWR | O_CREAT | O_CLOEXEC, 0644);
|
||||||
|
if (sh->wal_fd < 0) { perror("open wal"); return 1; }
|
||||||
|
if (fallocate(sh->wal_fd, 0, 0, WAL_PREALLOC) < 0)
|
||||||
|
fprintf(stderr, "[wo-rt-c] warn: fallocate %s refused (%s)\n", path, strerror(errno));
|
||||||
|
snprintf(sh->snap_path, sizeof sh->snap_path, "%s/shard-%d.data", data_dir, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("[wo-rt-c] %d shard%s on http://127.0.0.1:%u — io_uring loops, keep-alive — arena %zu KB (%s pages, %s) — ctrl-C to stop\n",
|
||||||
|
n_threads, n_threads == 1 ? "" : "s", port,
|
||||||
|
arena_map_bytes / 1024,
|
||||||
|
arena_huge ? "2M huge" : "4K",
|
||||||
|
arena_locked ? "mlocked" : "NOT locked");
|
||||||
|
printf(" GET / runtime + arena info, per-shard stats\n GET /healthz liveness\n");
|
||||||
|
printf(" GET /api/notes list (connection's shard)\n POST /api/notes create {\"title\":\"...\"}\n");
|
||||||
|
fflush(stdout);
|
||||||
|
|
||||||
|
for (int t = 0; t < n_threads; t++)
|
||||||
|
pthread_create(&shards[t].tid, NULL, shard_main, &shards[t]);
|
||||||
|
for (int t = 0; t < n_threads; t++)
|
||||||
|
pthread_join(shards[t].tid, NULL);
|
||||||
|
|
||||||
|
/* Clean shutdown: persist each slice as a snapshot, truncate the WALs.
|
||||||
|
* A kill -9 skips this — that's what boot-time WAL replay is for. */
|
||||||
|
for (int t = 0; t < n_threads; t++)
|
||||||
|
snap_write(&shards[t]);
|
||||||
|
|
||||||
|
printf("[wo-rt-c] all %d shards joined — bye\n", n_threads);
|
||||||
|
for (int t = 0; t < n_threads; t++) {
|
||||||
|
close(shards[t].ring.fd); close(shards[t].lfd); close(shards[t].evfd);
|
||||||
|
close(shards[t].wal_fd);
|
||||||
|
}
|
||||||
|
close(sigfd);
|
||||||
|
munmap(arena, arena_map_bytes);
|
||||||
|
free(shards);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue