docs: status board on the compile-and-run milestone + CODE-LOGIC beside the code
- docs/00-status.md: NEXT PLAN is now iteration 5's strictness half (?T forced handling, pub(read) writes, using, #if) plus an ASan run over the workload; iterations 6 and 7 marked landed; a "Landed 2026-08-14" section records what actually shipped, and a new known-gaps block records what did not — lenient optionals, unenforced pub(read), the borrowed-Text-into-container hazard, json's Bool/float limits, net fd lifetime, no ASan over the workload, and no corpus fixtures for the new surface (by direction: the sample is the test) - runtime/src/CODE-LOGIC.md: file map, the loader-is-the-only-validator and traps-never-leak invariants, the catch stack, records the VM fills but cannot name, class metadata + json, program mode, and where to look when it breaks - compiler/src/CODE-LOGIC.md: the pipeline, why there are two type derivers and the stay-silent-when-underivable rule, how contextual values get a destination, the node-id/label contract between owner.ml and emit.ml, the four kinds of qualified call, predeclared records, the constant-interning trap, register discipline, and how to verify a change Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
940ad99c40
commit
f95e9e4f63
3 changed files with 335 additions and 19 deletions
122
compiler/src/CODE-LOGIC.md
Normal file
122
compiler/src/CODE-LOGIC.md
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
# `compiler/src` — how `woc` is put together
|
||||
|
||||
Written 2026-08-14, when the front end grew the language surface that compiles
|
||||
`docs/examples/log-watcher`. The normative contracts it emits against are
|
||||
[`docs/plan/oop-vm/00-wob-format.md`](../../docs/plan/oop-vm/00-wob-format.md)
|
||||
and [`08-builtin-surface.md`](../../docs/plan/oop-vm/08-builtin-surface.md);
|
||||
the diagnostic codes are catalogued in
|
||||
[`01-error-catalog.md`](../../docs/plan/oop-vm/01-error-catalog.md).
|
||||
|
||||
## The pipeline
|
||||
|
||||
```
|
||||
lexer.ml → parser.ml → types.ml → owner.ml → emit.ml → .wob
|
||||
tokens AST symbols + move/drop/rc bytecode
|
||||
typecheck tables
|
||||
```
|
||||
|
||||
`bin/main.ml` drives it: discover files (a directory is one program), parse each,
|
||||
collect declarations per file, check module edges, merge symbols, typecheck,
|
||||
run the owner pass per file, then emit one image from every unit. `diag.ml`
|
||||
accumulates every stage's diagnostics and sorts them by (file, line, col), so
|
||||
ordering never depends on discovery order. `dump.ml` renders the stable text
|
||||
dumps the golden tests diff; `disasm.ml` reads an image back.
|
||||
|
||||
Four things are worth knowing before editing any of it.
|
||||
|
||||
### 1. Two type derivers, deliberately
|
||||
|
||||
`types.ml`'s `confident_typ` and `emit.ml`'s `ty_of_expr` both answer "what type
|
||||
is this expression?", in different languages (`Types.typ` vs `Ast.field_ty`) and
|
||||
for different purposes: the first gates diagnostics, the second picks
|
||||
instructions (EQ vs EQS, a container's element kinds, whether a value is owned).
|
||||
They are kept in sync by hand, and both follow one rule: **stay silent when
|
||||
underivable**. `confident_typ` returns `None`; the emitter falls back to `Int`.
|
||||
That is why a check built on `typecheck_expr`'s `.typ` (which reports `Int` for
|
||||
anything unresolved) produces false positives, and every new check should read
|
||||
`confident_typ` instead.
|
||||
|
||||
A third table pair follows the same discipline: `Types.builtin_confident_ret`
|
||||
and `emit.ml`'s `builtin_ret` give each builtin's return type. An omission there
|
||||
is not a lost type — it is a **leak**, because the owner pass classifies a
|
||||
binding as owned from exactly that answer.
|
||||
|
||||
### 2. Contextual values need a destination
|
||||
|
||||
`[]`, `[a, b]`, `{}` and `nil` have no type of their own. They take it from,
|
||||
in order: a written `let` annotation, the field/parameter they are built into,
|
||||
the enclosing method's declared return type (`fstate.f_ret`), or — for a
|
||||
non-empty list — their own first element. With none of those, emission is a
|
||||
diagnostic, never guessed bytecode: a container's element kinds *are* its
|
||||
runtime drop plan, so a wrong guess leaks or double-frees. `nil` is the zero
|
||||
word for every `?T` (the format doc's own rule), which is also why a comparison
|
||||
against `nil` must lower to `EQ` and never `EQS`.
|
||||
|
||||
### 3. The owner pass hands the emitter tables, not decisions
|
||||
|
||||
`owner.ml` computes moves, scope-end drops, branch-join drops, rc sites and
|
||||
residual borrow guards, keyed by **node id and label**. `emit.ml` looks them up
|
||||
by the same keys. When a construct has arms — `switch`, `if`, `try` — both files
|
||||
must agree on the label strings and on the arm ORDER (`switch_lowering_order`
|
||||
moves `default` last in both). A silent mismatch means a drop that never runs.
|
||||
|
||||
`try`'s shape: the catch arm is an alternate flow joining the try arm, so
|
||||
`analyze_try` snapshots the entry state, walks the body, restores, walks the
|
||||
handler with `e` declared as an owned local, and then makes each arm drop what
|
||||
the other moved. The handler starts from the *entry* state on purpose — a trap
|
||||
can be raised after any prefix of the body, and claiming the body's moves
|
||||
happened would drop values the VM already released.
|
||||
|
||||
### 4. Statics, modules and the stdlib all arrive as `Ident.member` calls
|
||||
|
||||
A qualified call's head can be four things, resolved in this order: a value with
|
||||
a type (an ordinary method call), a class with a static method
|
||||
(`Flock.held(x)` — `static_method`), a reserved stdlib module
|
||||
(`fs.stat(path)` — `Types.stdlib_members`), or a `use` alias for a project
|
||||
module. Adding a fifth kind means extending that chain in both `emit_call` and
|
||||
`ty_of_expr`, and `confident_typ` for the diagnostic side.
|
||||
|
||||
The stdlib table is data: module, member, source arity, builtin id, return
|
||||
type, and the predeclared record whose class id gets appended as the call's last
|
||||
argument. `json.encode`/`json.decode` are the two exceptions with bespoke
|
||||
lowering — encode needs its argument's static kind, and decode has no type at
|
||||
all until an `as` names one, which is why `json.decode(t) as T` is one
|
||||
instruction and a bare `json.decode(t)` is an error.
|
||||
|
||||
## Predeclared records
|
||||
|
||||
`Error` (a catch arm's error), `Stat`, `TimeParts`, `Proc` (stdlib results) are
|
||||
declared by `types.ml`, not by any source file. They join the **merged** symbol
|
||||
table only — one copy per file would read as a cross-file duplicate — and they
|
||||
enter the class table only when a program actually needs one, so images that
|
||||
predate the surface keep their exact class tables. Their field ORDER is the
|
||||
contract with the runtime, which writes those fields by index.
|
||||
|
||||
## Emitting the class table (a trap to remember)
|
||||
|
||||
Field-name constants must be interned **with every other constant**, before the
|
||||
constant pool is serialized. Interning during class-table serialization appends
|
||||
constants the pool has already been written past: the image then references
|
||||
constants it does not contain, and the loader rejects every class. That bug cost
|
||||
a debugging round; the interning now happens beside `class_name_k`.
|
||||
|
||||
## Register discipline in `emit.ml`
|
||||
|
||||
Locals live below `f_nlocals`, temporaries from `f_temp` upward, and a
|
||||
statement resets `f_temp` to `f_nlocals`. Any construct that writes into a `dst`
|
||||
which might itself be a temp (`switch`, `try`, a ctor, a container literal) must
|
||||
reserve `dst` before allocating more temps, or an arm-local `let` can be handed
|
||||
the same register and clobber a live value before its drop runs. `emit_switch`
|
||||
carries the comment explaining the ASan-confirmed leak that taught this.
|
||||
|
||||
## Verifying a change
|
||||
|
||||
- `just woc-test` — unit assertions plus the golden suite (token/AST/owner/bc
|
||||
dumps and an OCaml re-implementation of the loader's validation). `WOC_BLESS=1`
|
||||
regenerates goldens; read the diff before blessing, it is a contract change.
|
||||
- `just oop-e2e` — the conformance corpus: `run/` byte-exact stdout,
|
||||
`compile-fail/` exact diagnostic code, `trap/` exact trap code, `gc/` exact
|
||||
collector trace, plus the single-binary smoke.
|
||||
- `./compiler/_build/default/bin/woc --emit docs/examples/log-watcher -o /tmp/lw.wob`
|
||||
— the acceptance workload. It must compile with zero diagnostics, and
|
||||
`runtime/wovm /tmp/lw.wob watch <file> 2 1` must tail a live file and alert.
|
||||
|
|
@ -19,20 +19,30 @@ Statuses: ✅ **done** · 🔄 **in progress** · ⬜ **pending** · ⏸ **hold*
|
|||
|
||||
## ▶ NEXT PLAN
|
||||
|
||||
**Story iteration 5 — language surface (Haxe-parity adoptions).**
|
||||
**Close the gaps the log-watcher milestone left open.** The acceptance target
|
||||
of the whole language track — _compile and run log-watcher_ — is **met** as of
|
||||
2026-08-14: `docs/examples/log-watcher` (1285 lines, 7 files) compiles with
|
||||
zero diagnostics, and the image runs (`wovm lw.wob watch app.log 2 1` tails a
|
||||
live file, classifies levels and fires `ALERT … last entry is error, quiet for
|
||||
2s`). What remains is the *strictness* half of iteration 5 plus one runtime
|
||||
gate, in this order:
|
||||
|
||||
1. **`?T` forced handling** (plan 8 Task 6's diagnostics half, `WO-E211`–`E213`
|
||||
still dead). Optionals are currently **lenient**: `nil` is the zero word, a
|
||||
`?T` is usable where `T` is expected, and nothing narrows. The
|
||||
representation and the comparisons are right; the refusals are missing.
|
||||
2. **`pub(read)` write enforcement** — parsed and recorded on the field; the
|
||||
typechecker does not yet refuse a write from outside the declaring class.
|
||||
3. **`using` extensions and `#if` build flags + reject-row diagnostics** (plan 8
|
||||
Tasks 7–8's remainder). Nothing in the workload needs them, so they are the
|
||||
tail of the plan, not a blocker.
|
||||
4. **ASan over the workload** — the corpus is ASan-clean, but log-watcher's own
|
||||
run has never been under the sanitizer, and iteration 4's `gc/held-cycle`
|
||||
leak is still open (see the known-gaps section).
|
||||
|
||||
Plan: [`plan/compiler/2026-08-01-haxe-parity-language.md`](plan/compiler/2026-08-01-haxe-parity-language.md) ·
|
||||
Story slice: [`docs/stories/language-runtime-database/05-language-surface.md`](stories/language-runtime-database/05-language-surface.md)
|
||||
|
||||
The language grows from milestone grammar to a daily-driver surface: every
|
||||
**adopt** row of the systems-track verdict table (switch expressions, typedef
|
||||
records, `?T` optionals, enum payloads, try/catch, statics, `using`, modules,
|
||||
`is`, `pub(read)`, `#if`) lands with a golden + must-fail fixture pair; every
|
||||
**reject** row refuses with a doctrine-citing diagnostic. **First task: `?T`
|
||||
forced handling (plan 8 Task 6)** — plumbed since iteration 3 but unenforced
|
||||
(`WO-E211`–`E213` dead), and the log-watcher port (iterations 6–7) uses
|
||||
optionals throughout in place of the Haxe original's sentinel values, so
|
||||
nothing else in this plan can land ahead of it.
|
||||
|
||||
Two tracks run in this repo. The critical path is the **language track**:
|
||||
iterations 3 → 4 → 5 → 6 → 7, ending at _compile and run log-watcher_. The
|
||||
Rust-runtime track is shipped-and-maintained, not advancing.
|
||||
|
|
@ -52,9 +62,9 @@ that sequences its tasks. Read one, approve, then the next starts.
|
|||
| 2 | [VM core (`wovm`)](stories/language-runtime-database/02-vm-core.md) | ✅ |
|
||||
| 3 | [Compiler front (`woc`)](stories/language-runtime-database/03-compiler-front.md) | ✅ (known gaps below) |
|
||||
| 4 | [Single binary end-to-end](stories/language-runtime-database/04-single-binary-e2e.md) | ✅ (known gaps below) |
|
||||
| 5 | [Language surface](stories/language-runtime-database/05-language-surface.md) | 🔄 **next** |
|
||||
| 6 | [Program mode + stdlib](stories/language-runtime-database/06-program-mode-stdlib.md) | ⬜ |
|
||||
| 7 | [log-watcher proof](stories/language-runtime-database/07-logwatcher-proof.md) | ⬜ acceptance |
|
||||
| 5 | [Language surface](stories/language-runtime-database/05-language-surface.md) | 🔄 grammar done, strictness open |
|
||||
| 6 | [Program mode + stdlib](stories/language-runtime-database/06-program-mode-stdlib.md) | ✅ (the surface log-watcher uses) |
|
||||
| 7 | [log-watcher proof](stories/language-runtime-database/07-logwatcher-proof.md) | ✅ compiles and runs |
|
||||
| 7b | [Inferred GC + mark-sweep](stories/language-runtime-database/07b-inferred-gc-mark-sweep.md) | ⬜ closes iteration 4's gate |
|
||||
| 8 | [Shard-actor runtime](stories/language-runtime-database/08-shard-actor-runtime.md) | ⬜ |
|
||||
| 9 | [Database engine](stories/language-runtime-database/09-database-engine.md) | ⬜ |
|
||||
|
|
@ -67,12 +77,46 @@ that sequences its tasks. Read one, approve, then the next starts.
|
|||
|
||||
## In progress
|
||||
|
||||
| Track | Item | Where |
|
||||
| -------- | ---------------------------------------------------------------------- | ---------------------------------------------------------- |
|
||||
| Language | Iteration 5 — Haxe-parity language surface, `?T` forced handling first | [plan 8](plan/compiler/2026-08-01-haxe-parity-language.md) |
|
||||
| Track | Item | Where |
|
||||
| -------- | --------------------------------------------------------------------------- | ---------------------------------------------------------- |
|
||||
| Language | Iteration 5's strictness half — `?T` forced handling, `pub(read)` writes, `using`, `#if` | [plan 8](plan/compiler/2026-08-01-haxe-parity-language.md) |
|
||||
|
||||
Nothing else should be started until iteration 5 lands. Off-critical-path work
|
||||
is parked by explicit scope directive (2026-08-08).
|
||||
Off-critical-path work is parked by explicit scope directive (2026-08-08).
|
||||
|
||||
### Landed 2026-08-14 — the compile-and-run milestone
|
||||
|
||||
One session, driven end to end by compiling `docs/examples/log-watcher` and
|
||||
watching its diagnostic count fall (481 → 0). In order:
|
||||
|
||||
- **let annotations, container literals, statics, `pub(read)`** — `let x: multi
|
||||
Text = []`, `map<K, V>`, `?T`; `[]`/`[a, b]`/`{}` as expressions; `static
|
||||
const`/`static fn` with `Cls.fn(...)` calls; a `;` ends a statement so
|
||||
one-line guard bodies parse.
|
||||
- **try/catch over the trap system** (plan 8 Task 5) — VM catch frames
|
||||
(`TRY`/`ENDTRY`), unwind-to-handler with the try region's own values
|
||||
released, `err_fill` for the `{code, line, method, msg}` record, expression
|
||||
and block catch arms. Uncaught traps unchanged.
|
||||
- **`nil` + 23 text/container builtins** — len, byte_at, print_err,
|
||||
starts_with/ends_with, index_of/last_index_of, substr, trim, to_lower,
|
||||
char_of, parse_int, split/split_ws, join, slice, pop/shift, sort, reverse,
|
||||
remove, key_at/val_at, multi_set.
|
||||
- **`for k, v in m`** over a map, and `m[i] = v` for a `multi`.
|
||||
- **the systems stdlib's OS half** (`runtime/src/sysio.c`) — fs, time, env,
|
||||
net, proc behind the reserved module names, with predeclared `Stat`,
|
||||
`TimeParts` and `Proc` records and the new `WO_T_IO` trap.
|
||||
- **json** (`runtime/src/json.c`) + **`.wob` v2** — per-field names, referenced
|
||||
classes and element kinds in the class table, so encode/decode are one
|
||||
metadata-driven implementation; `json.decode(t) as T` is the language's only
|
||||
cast, yielding `?T`.
|
||||
- **program mode** — `fn main(args: multi Text) -> Int`, argv delivered by the
|
||||
runtime, return value as the exit code.
|
||||
- **two safety fixes found by running it**: `+` on `Text` was lowering to ADD
|
||||
on two heap pointers (now WO-E201 pointing at `..`; seven sites in the sample
|
||||
were corrected), and `x == nil` was lowering to EQS, which dereferences the
|
||||
zero word (now EQ).
|
||||
|
||||
Gates at the end of that session: corpus 71/0, `woc` runtest 565/0, every
|
||||
`wovm` unit gate green in both dispatch flavors.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -122,6 +166,37 @@ is parked by explicit scope directive (2026-08-08).
|
|||
to `set` is under-counted and the collector can free it while the map still
|
||||
points at it. Nothing in the corpus exercises this yet. See
|
||||
[`oop-vm/08-builtin-surface.md`](plan/oop-vm/08-builtin-surface.md).
|
||||
|
||||
**Known gaps carried out of the 2026-08-14 compile-and-run milestone** —
|
||||
recorded, not silently owed:
|
||||
|
||||
- **Optionals are lenient.** `?T` has its representation (the zero word) and
|
||||
its comparisons, but `WO-E211`–`E213` are still dead: a `?T` may be used
|
||||
where `T` is required, and nothing narrows inside an `if x != nil` branch.
|
||||
The workload leans on that leniency today.
|
||||
- **`pub(read)` is parsed, not enforced.** The marker rides on the field
|
||||
(`Ast.field.pub_read`); no check refuses a write from outside the declaring
|
||||
class yet.
|
||||
- **`using` extensions and `#if` build flags are absent**, and the reject rows
|
||||
(`extends`/`cast`/`Dynamic`/…) still have no doctrine-citing diagnostics —
|
||||
plan 8 Tasks 7–8's remainder.
|
||||
- **A borrowed non-constant Text pushed into a container is a double-free
|
||||
hazard** — `push(m, v)`'s open gap, now shared by list literals (`[a, b]`)
|
||||
and documented in `owner.ml`'s own comment. The workload's literals are
|
||||
string constants or borrowed params handed straight to a stdlib call, so
|
||||
nothing reachable today hits it.
|
||||
- **json's two documented limits**: a `Bool` field encodes as `0`/`1` (the
|
||||
class-table kind byte does not distinguish it from an integer), and a JSON
|
||||
number with a fraction or exponent decodes by truncation.
|
||||
- **`net` fd lifetime is the program's problem.** `net.close` exists; the
|
||||
sample's MCP server never calls it, so a long-running `mcp` session leaks
|
||||
descriptors. That is the sample's bug to fix, not the runtime's.
|
||||
- **The workload has never run under ASan**, and iteration 4's `gc/held-cycle`
|
||||
leak (above) is still open. The corpus itself stays ASan-clean.
|
||||
- **No corpus fixtures cover the new surface.** By explicit direction
|
||||
(2026-08-14) the acceptance for this work is the log-watcher program itself,
|
||||
not fixture pairs; `tests/corpus/` still gates every pre-existing behavior
|
||||
(71 checks, 0 failures).
|
||||
- **E201/E203 and seven other `WO-E2xx` codes remain declared but unemitted**
|
||||
— see [`oop-vm/01-error-catalog.md`](plan/oop-vm/01-error-catalog.md).
|
||||
- **CLOSED — milestone-1's ASan gate (`just oop-accept`) failing on
|
||||
|
|
|
|||
119
runtime/src/CODE-LOGIC.md
Normal file
119
runtime/src/CODE-LOGIC.md
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
# `runtime/src` — how the VM is put together
|
||||
|
||||
Written 2026-08-14, when the runtime grew the systems stdlib and json. Read
|
||||
this before changing a file here; the normative contracts are
|
||||
[`docs/plan/oop-vm/00-wob-format.md`](../../docs/plan/oop-vm/00-wob-format.md)
|
||||
(the `.wob` format, opcodes, builtin ids) and
|
||||
[`08-builtin-surface.md`](../../docs/plan/oop-vm/08-builtin-surface.md) (what
|
||||
each builtin means in source terms). `wob.h` is the machine-readable twin of
|
||||
the first: constants there and prose there must never disagree.
|
||||
|
||||
## The files, in dependency order
|
||||
|
||||
| file | what it owns |
|
||||
| --- | --- |
|
||||
| `wob.h` | every format constant: header offsets, field kinds, opcodes, builtin ids, trap codes, the 16-byte object header, the class descriptor |
|
||||
| `obj.h/.c` | the per-shard arena, object allocation, `wo_str` (header + length + inline bytes, no NUL) |
|
||||
| `cont.h/.c` | `multi` and `map` as native classes: struct heads in the arena, backing arrays malloc'd, map lookup a linear scan over parallel key/value arrays |
|
||||
| `gc.h/.c` | the kind-directed dispatcher (`wo_drop_kind`/`wo_drop_obj`), refcounting for `@gc`, and the budgeted Bacon–Rajan cycle collector |
|
||||
| `borrow.h/.c` | the borrow word: shared counts and the exclusive sentinel |
|
||||
| `loader.h/.c` | parse and **validate** an image; the validation contract in its header comment is exactly what the interpreter may then assume |
|
||||
| `vm.h/.c` | the register interpreter: window-overlap calls, dual-flavor dispatch, traps, unwinding, catch frames |
|
||||
| `builtin.h/.c` | the pure builtins: print, containers, text |
|
||||
| `sysio.c` | the OS half: `fs`, `time`, `env`, `net`, `proc` |
|
||||
| `json.c` | `json.encode` / `json.decode`, driven by class metadata |
|
||||
| `main.c` | the CLI: find an image (argument or embedded trailer), build argv, call the entry, map its result to an exit code |
|
||||
|
||||
`builtin.c`'s `wo_builtin` is the single entry point the interpreter calls; it
|
||||
forwards ids at or above `WO_B_SYS_FIRST` to `sysio.c` and the json pair to
|
||||
`json.c`. Splitting by translation unit keeps the kernel-touching code and the
|
||||
format-walking code out of the hot builtin switch.
|
||||
|
||||
## Two invariants worth stating plainly
|
||||
|
||||
**The loader is the only validator.** Everything the interpreter skips
|
||||
checking — opcode ranges, register operands, jump targets, builtin arities,
|
||||
window sizes, table ordering — is checked once at load. The exceptions are
|
||||
deliberate and documented: `GETF`/`SETF` field indexes and receiver shapes stay
|
||||
runtime checks, because registers are untyped (the spec's residual-check
|
||||
doctrine). If you add an opcode or a builtin, its validation goes in
|
||||
`loader.c`'s switch and its arity in `b_arity`, or the interpreter is running
|
||||
unvalidated bytes.
|
||||
|
||||
**Traps never leak.** A trap unwinds frames innermost-outward, and in each one
|
||||
the drop-table entry governing that frame's current instruction says which
|
||||
registers hold owned or counted values. The governing instruction is the
|
||||
trapping pc for the innermost frame and the CALL (saved pc − 1) for every outer
|
||||
one. Registers are nulled as they are released, so window overlap cannot
|
||||
double-free.
|
||||
|
||||
## try/catch (the catch stack)
|
||||
|
||||
`TRY A sBx` pushes `{depth, handler pc, error register}`; `ENDTRY` pops it. On a
|
||||
trap with a catch frame live, `vm_trap`:
|
||||
|
||||
1. fills `vm->caught` (the same structured error the uncaught surface prints),
|
||||
2. unwinds every frame **above** the catching one, exactly as an uncaught trap
|
||||
would,
|
||||
3. releases what the try region owned **in** the catching frame — the
|
||||
difference between the drop entry at the trapping instruction and the entry
|
||||
at the handler pc, which is why the compiler must record an entry at the
|
||||
handler,
|
||||
4. points that frame at the handler and returns 0, so `TRAPF` reloads and keeps
|
||||
interpreting.
|
||||
|
||||
A frame that returns pops the catch frames it registered (`DROP_CATCHES`), so a
|
||||
`return` out of a try region cannot leave a handler aimed at a dead window.
|
||||
With `ncatch == 0` every trap behaves byte-for-byte as it did before the
|
||||
feature existed — that is the property to preserve when touching this code.
|
||||
|
||||
## Records the VM fills but does not know
|
||||
|
||||
Three builtins return a *record*: `fs.stat`, `time.local`, `proc.run`, plus
|
||||
`err_fill` for a catch arm. The VM cannot name a source type, so the compiler
|
||||
passes the record's **class id** as the call's last argument and the builtin
|
||||
fills fields by index. The field order is therefore a contract, written beside
|
||||
each case in `sysio.c` and mirrored in `compiler/src/types.ml`'s predeclared
|
||||
records. Change one side and the other silently writes to the wrong slot.
|
||||
|
||||
## Class metadata and json (`.wob` v2)
|
||||
|
||||
The class table carries, per field, its name constant, the class it refers to
|
||||
(or a json-raw marker) and a container field's element kinds. That is what lets
|
||||
`json.c` be one implementation for every shape instead of per-type generated
|
||||
code:
|
||||
|
||||
- **encode** takes the top-level value's *static* kind from the compiler,
|
||||
because a register alone cannot say whether it holds an i64 or a pointer.
|
||||
Everything nested comes from object headers (which carry `class_id`) and the
|
||||
class table.
|
||||
- **decode** parses and binds straight into the target class: keys matched
|
||||
against field names, a nested object built as that field's class, an array as
|
||||
a `multi` of that field's element kind, unknown keys skipped, absent keys left
|
||||
as the zero word (nil). Malformed input yields nil rather than trapping —
|
||||
that is what makes `json.decode(t) as T` a checked decode.
|
||||
|
||||
Two limits are inherent to the kind byte and are documented, not bugs to
|
||||
discover: a `Bool` field encodes as `0`/`1`, and a fractional JSON number
|
||||
decodes by truncation.
|
||||
|
||||
## Program mode
|
||||
|
||||
`main.c` accepts an entry taking no arguments or exactly one `multi Text`. The
|
||||
list holds the program's **own** arguments — not the program name, and not the
|
||||
image path a `wovm image.wob args...` invocation carries — so `args[0]` is the
|
||||
first real argument. The entry's return value is the process exit code (low
|
||||
byte); a trap is exit 1 with the fixed `trap N in METHOD at line L: MESSAGE`
|
||||
line on stderr, which the conformance harness parses.
|
||||
|
||||
## Where to look when something breaks
|
||||
|
||||
- A wild pointer inside a builtin usually means the *compiler* put the wrong
|
||||
thing in a register: check the method's disassembly (`woc --dump-bc`) before
|
||||
suspecting the C.
|
||||
- `make -C runtime wovm-asan` builds the sanitized binary; the unit suites
|
||||
(`just wovm-test`) run every `test/test_*.c` under ASan+UBSan in both
|
||||
dispatch flavors, so a fallback-only bug cannot hide.
|
||||
- `runtime/test/wob_build.c` is an independent image assembler. A
|
||||
builder/loader disagreement shows up as a unit-test failure, which is the
|
||||
point of having two encoders.
|
||||
Loading…
Reference in a new issue