From 9ef84515e5f8341b34dda6cdcc7ea59803c863d9 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Wed, 19 Aug 2026 17:11:05 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20iteration=207b=20migration=20=E2=80=94?= =?UTF-8?q?=20amend=20the=20normative=20docs=20(Phase=204)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The spec's §8 migration table, applied: - 00-principles.md P3: "@gc is a per-class opt-in, reference-counted" -> GC-ness is inferred; incremental per-shard mark-sweep in budgeted slices; still no global pause by construction. - OOP spec: decision-table GC row -> inferred (hybrid rule named); §3 rule 5 -> traced classes alias freely, which classes is inferred; §4 memory model -> the RC + Bacon-Rajan paragraph replaced by tracing (snapshot roots, Yuasa barrier, born-black, budgeted slices); header rc comment -> union'd sweep link; mixing rule restated for tracing. - 00-wob-format.md: header says version 4; opcodes 27-28 -> reserved (loader rejects); the owned-temporary rule's @gc exclusion restated for tracing. - 08-builtin-surface.md: the push RC_INC special case and the set(m,k,v) retention gap DELETED — neither exists without RC; the corpus cycle is collected by tracing. - story 07b: status -> LANDED 2026-08-18 (with the historical note kept); board: 7b row ✅ (supersedes iteration 2's RC memory model), pending row removed. - gc-cycle README: Phase 3 flipped to landed (the ring runs, is reclaimed, ASan-clean; the ?Node RC_DEC-on-nil trap no longer exists); the barrier prose corrected to the as-built design (snapshot-at-beginning + deletion barrier + born-black, not per-slice root re-reads). - plan 2026-08-18: all checkboxes ticked + a completion banner recording the four deviations from the plan as written. (Error catalog was already amended with the keyword-removal commit: WO-E104 added, WO-W201 retired.) Co-Authored-By: Claude Opus 5 (1M context) --- docs/00-principles.md | 8 +- docs/00-status.md | 3 +- docs/examples/gc-cycle/README.md | 40 +++++----- docs/plan/oop-vm/00-wob-format.md | 8 +- docs/plan/oop-vm/08-builtin-surface.md | 24 ++---- .../07b-inferred-gc-mark-sweep.md | 23 ++++-- .../2026-08-18-inferred-gc-mark-sweep.md | 74 ++++++++++++------- .../2026-08-01-oop-compiler-vm-design.md | 12 +-- 8 files changed, 109 insertions(+), 83 deletions(-) diff --git a/docs/00-principles.md b/docs/00-principles.md index 2714852..24ee993 100644 --- a/docs/00-principles.md +++ b/docs/00-principles.md @@ -28,9 +28,11 @@ the dependency doctrine in [the OOP spec](superpowers/specs/2026-08-01-oop-compi Objects are owned values: one owner, moves on assignment, second-class borrows checked mostly at compile time (mutable value semantics — the -Rust-borrow shape without lifetime inference). `@gc` is a per-class opt-in, -reference-counted with budgeted per-shard cycle collection — no global -pause exists by construction. +Rust-borrow shape without lifetime inference). GC-ness is **inferred** by +the compiler (iteration 7b): a class in a reference cycle, or one whose +values must escape as long-lived aliases, is traced by an incremental +per-shard tri-color mark-sweep collector in budgeted slices — the developer +writes no memory annotation, and no global pause exists by construction. *Why:* deterministic memory for the default case, aliasing freedom where the design wants it, and never a stop-the-world in a runtime that is also the database. diff --git a/docs/00-status.md b/docs/00-status.md index e6a314d..b57ddc2 100644 --- a/docs/00-status.md +++ b/docs/00-status.md @@ -114,7 +114,7 @@ that sequences its tasks. Read one, approve, then the next starts. | 5 | [Language surface](stories/language-runtime-database/05-language-surface.md) | 🔄 grammar done, strictness ⏸ deferred | | 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) | 🔄 **runs; executable in progress** | -| 7b | [Inferred GC + mark-sweep](stories/language-runtime-database/07b-inferred-gc-mark-sweep.md) | ⏸ off the workload's path (no `@gc`) | +| 7b | [Inferred GC + mark-sweep](stories/language-runtime-database/07b-inferred-gc-mark-sweep.md) | ✅ **landed 2026-08-18** — `@gc` gone (WO-E104), GC-ness inferred, RC replaced by incremental mark-sweep, `.wob` v4; supersedes iteration 2's RC memory model | | 8 | [Shard-actor runtime](stories/language-runtime-database/08-shard-actor-runtime.md) | ⬜ | | 9 | [Database engine](stories/language-runtime-database/09-database-engine.md) | 🔄 engine complete (storage/WAL/indexes/insert-update-delete); reads land with 9b | | 9b | [`@table`, relations, query](stories/language-runtime-database/09b-table-relations-query.md) | 🔄 query surface + relations + FK done (branch query-surface); group-by parked | @@ -309,7 +309,6 @@ The C proving-ground work (`exploration/c-runtime/`, phases A–F: 859k reads/s, | 5 | Haxe-parity language surface — **`?T` forced handling first**, then switch expressions, records, enum payloads, try/catch, statics, `using`, modules, `is`, `pub(read)`, `#if` | [plan 8](plan/compiler/2026-08-01-haxe-parity-language.md) | | 6 | Program mode + systems stdlib — `fn main`, exit codes, `fs`/`proc`/`net`/`time`/`json` | [plan 9](superpowers/plans/2026-08-01-program-mode-stdlib.md) | | 7 | log-watcher proof — the sample compiles and detects a silent death live | [plan 10](superpowers/plans/2026-08-01-log-watcher-sample.md) | -| 7b | Inferred GC + incremental mark-sweep — `@gc` removed, GC-ness inferred, RC retired | [spec](superpowers/specs/2026-08-11-inferred-gc-mark-sweep-design.md) — plan to be written | | 8 | Shard-actor runtime | [plan 4](superpowers/plans/2026-08-01-shard-actor-vm-runtime.md) | | 9 | Database engine binding | [plan 5](superpowers/plans/2026-08-01-db-engine-binding.md) | | 9b | `@table` + relations + language-integrated query — comprehension queries, `ref`/`backlink` navigation, GroupBy aggregates; acceptance: new `docs/examples/employee` sample | [spec](superpowers/specs/2026-08-15-table-relations-query-design.md) · [plan](plan/compiler/2026-08-15-employee-relations-query.md) | diff --git a/docs/examples/gc-cycle/README.md b/docs/examples/gc-cycle/README.md index 0de6cac..efe6d21 100644 --- a/docs/examples/gc-cycle/README.md +++ b/docs/examples/gc-cycle/README.md @@ -20,7 +20,7 @@ A pass between `types` and `owner` (`compiler/src/gcinfer.ml`) builds a is a row id, `Copy`). Tarjan's SCC over that graph: any class in a non-trivial SCC, or with a self-loop, is **traced (`gc`)**. Everything else is **`owned`**. -For this sample, `woc --dump-gc` would print: +For this sample, `woc --dump-gc` prints: ``` Node gc (cycle Node -> Node) @@ -118,7 +118,7 @@ flowchart TD ALLOC["allocate traced object
color = WHITE, link into traced list"] --> LIVE LIVE["mutator runs
(program executes)"] --> TRIG{"traced bytes since last cycle
past heap goal?"} TRIG -- no --> LIVE - TRIG -- yes --> ROOTS["START CYCLE
shade every root GREY
(value/frame slots via pc gc-mask)"] + TRIG -- yes --> ROOTS["START CYCLE (snapshot)
shade every root GREY
(value/frame slots via pc gc-mask)"] ROOTS --> SLICE SLICE["MARK SLICE (budgeted)
pop a GREY object,
scan its GCREF fields +
owned subtrees that may reach a gcref,
shade each WHITE child GREY,
then paint this object BLACK"] --> GREY{"grey set empty?"} GREY -- "no (budget hit)" --> SAFE["yield at next safepoint
(loop back-edge / call)"] @@ -143,14 +143,16 @@ subtree that can reach no traced object at all. **The barrier — why incremental is safe.** Between slices the mutator keeps running and can hide a live object from a half-finished mark: store a white object into an already-**black** object, then drop the original grey/white -reference to it. A **Yuasa deletion barrier** closes this: on any store into a -`GCREF` slot **while marking is active**, shade the slot's **old** value grey -before overwriting it. In the sample, `a.next = b` (and the ring-closing -`c.next = a`) go through the store paths `SETF`/`map_set`/`push` where the -barrier lives — no new opcode, because `SETF` already resolves the field kind -from the class table. Owned stores, scalars, and Text pay nothing. Reading a -shard's roots fresh from its masks each slice is what removes Go's Dijkstra -insertion-half and the stack rescan. +reference to it. A **Yuasa deletion barrier** closes this: on any deletion of a +`GCREF` edge **while marking is active** — a `SETF` overwrite, or an owned +holder dying with a gcref inside (every such path funnels through +`wo_drop_kind`) — the **old** target is shaded grey first. No new opcode: +`SETF` already resolves the field kind from the class table. Owned stores, +scalars, and Text pay nothing. Snapshot-at-beginning completes the argument: +roots are scanned atomically when the cycle starts, and objects allocated +mid-cycle are born black — so anything reachable at the snapshot, or created +after it, survives; that is what removes Go's Dijkstra insertion-half and the +stack rescan. **Trigger & budget.** A cycle starts when the shard's traced bytes since the last cycle cross a heap goal; each slice marks at most `WO_GC_BUDGET` objects; @@ -199,13 +201,17 @@ traced with **no annotation** and *traced classes alias freely* — the ring **compiles** (the old `WO-E301: use of \`a\` after it was moved` at `c.next = a` is gone), and its bytecode is byte-identical to writing `@gc class Node`. -**Not yet: the ring runs.** On today's runtime (RC + Bacon–Rajan, `gc.c`) a -**nullable single-reference gc field** (`next: ?Node`) store/read is -unimplemented — even a one-hop `a.next = b; print(a.next.label)` traps -`null receiver` (the existing gc corpus only exercises `multi` gcref fields, -which do work). Running the ring, and reclaiming it, is **Phase 3**: the -incremental mark-sweep collector + full gcref field paths, the `.wob` -opcode-27/28 retirement, and the sweep list. +**Phase 3 (landed).** The runtime collector is the incremental tri-color +mark-sweep this README describes: RC and trial deletion are gone, the header +carries the sweep-list link, opcodes 27–28 are reserved (`.wob` v4), and the +Yuasa deletion barrier lives in the store paths. **The ring runs and is +reclaimed**: `woc --emit docs/examples/gc-cycle -o gc.wob && WO_GC_TRACE=1 +wovm gc.wob` prints `ring a -> b -> c -> a` then +`gc: step 1 budget=64 freed=3 remaining=0`, ASan-clean; with a tiny goal +(`WO_GC_GOAL=64`) a mid-program cycle runs while the ring is rooted and +correctly frees nothing. (The old RC runtime couldn't even store a `?Node` +gcref field — the unconditional RC_DEC of the nil old value trapped; that +opcode no longer exists.) **Phase 2b (landed).** Demand promotion: the ownership pass, run in collect mode, promotes any class whose value *must escape* (returned, stored where it diff --git a/docs/plan/oop-vm/00-wob-format.md b/docs/plan/oop-vm/00-wob-format.md index c4e34be..914fe71 100644 --- a/docs/plan/oop-vm/00-wob-format.md +++ b/docs/plan/oop-vm/00-wob-format.md @@ -11,7 +11,7 @@ All integers little-endian; offsets are absolute file offsets. -**Header (44 bytes):** magic `"WOB1"`, version 2, then offset/count u32 pairs for the constant pool, class table, interface section, and method table, then a u32 entry-method index (all-ones = none). +**Header (44 bytes):** magic `"WOB1"`, version 4, then offset/count u32 pairs for the constant pool, class table, interface section, and method table, then a u32 entry-method index (all-ones = none). **Constant pool** — sequential entries: one tag byte; tag 0 = i64 follows; tag 1 = text (u32 length + bytes, no NUL). @@ -47,7 +47,7 @@ The metadata exists for exactly one reason: `json.encode`/`json.decode` are runt | 20–21 | GETF / SETF | field read/write with runtime null/native/bounds checks (trap T_BOUNDS); overwriting a non-scalar field does NOT auto-drop the old value — the compiler emits the drop | | 22 | DROP A | recursively drop the owned value in A per its class drop plan, null the register | | 23–26 | BORROW_S/BORROW_X/RELEASE_S/RELEASE_X | borrow-word ops on the object in A; violation traps T_BORROW | -| 27–28 | RC_INC / RC_DEC | refcount ops on the `@gc` object in A | +| 27–28 | *reserved* | were RC_INC/RC_DEC; retired with reference counting in v4 (iteration 7b) — the loader rejects them like any unknown opcode | | 29 | BUILTIN A B C | register A = builtin C applied to args starting at register B (fixed arity per builtin; `multi_new`/`map_new` carry kind immediates in B instead) | | 30 | DB_STUB | trap T_DB "engine not linked" (spec: SQL-layer statements in milestone 1) | | 31 | TRAP Bx | explicit trap with code Bx | @@ -123,8 +123,8 @@ as a borrow argument — a record/class constructor literal, a variant construction, or an owned-returning call (`peek(Pay{})`, `get(Boxed(Pay{}))`) — is copied to a stable register below the call window and `DROP`ped by the caller once the call returns (`take` -arguments are the callee's to drop; places are their scope's; `@gc` and -`Text` temporaries are excluded — the rc system's and the Copy-aliasing +arguments are the callee's to drop; places are their scope's; traced and +`Text` temporaries are excluded — the collector's and the Copy-aliasing story's, respectively). Recursive drop is correct both ways, because a payload the callee moved out left the field nulled. diff --git a/docs/plan/oop-vm/08-builtin-surface.md b/docs/plan/oop-vm/08-builtin-surface.md index 059054d..988e4e1 100644 --- a/docs/plan/oop-vm/08-builtin-surface.md +++ b/docs/plan/oop-vm/08-builtin-surface.md @@ -78,23 +78,15 @@ trapped `BOUNDS "not a text value"`. Copying is the only rule correct for both shapes: a value read out of a place keeps its owner, and a freshly built Text (a call result, a `..` chain, an interpolation) stays the caller's — the compiler emits that drop right after the call (emit.ml's `drop_fresh_text`). -`OWNED`/`GCREF` elements still MOVE: they are not copyable, and the `@gc` -escape below is what keeps their counting right. Only the `@gc` half of the -old hazard remains open. +`OWNED`/`GCREF` elements still MOVE: they are not copyable. -**`push` and `@gc` elements.** `push(m, v)`'s value argument is never a -resolved callee parameter (`push` has no declared signature), so the -owner pass's ordinary Take-gated transfer never reaches it; a `@gc` value -pushed into a `multi` is special-cased in `owner.ml`'s `analyze_call` -(the value escapes into the container exactly like a ctor field, RC_INC -included) specifically so a `multi`-mediated `@gc` cycle can be built -and later collected (`tests/corpus/gc/`, plan 3 task 5). **`set(m, k, v)` -has no equivalent special case** — a `@gc` key or value handed to `set` -is not retained, so a `map<_, SomeGcClass>` (or a `@gc`-keyed map) built -this way will under-count its element's refcount and the collector will -free it while the map still points at it. Nothing in the corpus -exercises this yet; treat it as an open gap, not a proven-safe pattern, -until `set` gets the same fix `push` did. +**Traced elements need no bookkeeping (iteration 7b).** The old `push` +RC_INC special case and its `set(m, k, v)` retention gap are both +**deleted with reference counting itself**: a traced value stored into a +container is found by the mark phase through the container, so there is +no count to keep right and the use-after-free class those paragraphs +guarded against cannot recur. (`tests/corpus/gc/` still builds a +`multi`-mediated cycle and collects it — now by tracing.) ## A fresh container needs a destination of declared type diff --git a/docs/stories/language-runtime-database/07b-inferred-gc-mark-sweep.md b/docs/stories/language-runtime-database/07b-inferred-gc-mark-sweep.md index 55f9b61..df8fb64 100644 --- a/docs/stories/language-runtime-database/07b-inferred-gc-mark-sweep.md +++ b/docs/stories/language-runtime-database/07b-inferred-gc-mark-sweep.md @@ -9,13 +9,22 @@ > settled before iteration 8 multiplies shards. -> **Status (2026-08-14):** **not on the driving workload's path**, measured: -> `docs/examples/log-watcher` declares no `@gc` class — 35 classes in its image, -> none with the gc flag, and 0 `RC_INC` / 0 `RC_DEC` instructions against 78 -> `DROP`s. Its memory story is arena + deterministic drops end to end, so this -> iteration (and iteration 4's open `gc/held-cycle` leak, and `set`'s `@gc` -> retention gap) cannot affect whether log-watcher runs. It stays queued for -> workloads that build cycles; the `gc/` corpus fixtures remain its only users. +> **Status (2026-08-18): LANDED** (branch `inferred-gc`; plan +> [`2026-08-18-inferred-gc-mark-sweep.md`](../../superpowers/plans/2026-08-18-inferred-gc-mark-sweep.md)). +> The front end infers GC-ness (structural SCC + demand promotion, +> `woc --dump-gc`), `@gc` in source is WO-E104, and the runtime's RC + +> Bacon–Rajan collector is replaced by an incremental per-shard tri-color +> mark-sweep with a Yuasa deletion barrier — `.wob` is v4, opcodes 27–28 +> reserved. The worked example is +> [`docs/examples/gc-cycle`](../../examples/gc-cycle/README.md): its ring +> compiles with no annotation, runs, and is reclaimed in budgeted slices, +> ASan-clean. All four recorded `@gc`/RC defects are deleted by +> construction; `just oop-accept` is fully green (criterion 3's ASan clause +> included). +> +> *(Historical status 2026-08-14: not on the log-watcher critical path — 35 +> classes, none gc, arena + deterministic drops end to end. That is still +> true; log-watcher simply never allocates a traced object.)* ## Goals diff --git a/docs/superpowers/plans/2026-08-18-inferred-gc-mark-sweep.md b/docs/superpowers/plans/2026-08-18-inferred-gc-mark-sweep.md index a0b3c63..9dde1ba 100644 --- a/docs/superpowers/plans/2026-08-18-inferred-gc-mark-sweep.md +++ b/docs/superpowers/plans/2026-08-18-inferred-gc-mark-sweep.md @@ -1,5 +1,23 @@ # Inferred GC + incremental mark-sweep — implementation plan +> **Status: COMPLETE (2026-08-18)** — all phases landed on branch +> `inferred-gc`; `just oop-accept` fully green. Deviations from the plan as +> written, recorded honestly: (1) Phase 2 landed in two slices (2a +> inference-first `is_gc_class`, 2b demand promotion) and the `@gc`-keyword +> removal moved AHEAD of Phase 3 once the test helpers ran inference; (2) the +> collector is snapshot-at-beginning — roots scanned atomically at cycle +> start — rather than per-slice re-reads, which is what makes the pure Yuasa +> deletion barrier sufficient; (3) sweep is budgeted too (a resumable +> cursor), which is what keeps the budget-steps fixture's trace shape; (4) +> the barrier fixture lives in runtime/test/test_cycle.c (unit level) rather +> than the corpus — the corpus gc fixtures kept their existing traces +> unchanged; (5) no `--dump-gc` golden fixture was added — the classification +> was verified live against gc-cycle/employee/borrow-escape and the SCC unit +> cases ride the existing suites; (6) Phase 2's "runs on today's Bacon–Rajan +> collector" deliverable was unreachable: the RC runtime's unconditional +> RC_DEC of a nil old `?Node` field value trapped, so the ring first RAN +> under the Phase-3 collector (whose no-RC design deletes that trap). + > **For agentic workers:** use superpowers:executing-plans (inline) or > subagent-driven-development. Steps are checkboxes. Per repo rule, this plan > carries **actions in words + verification commands, no code blocks** — the @@ -47,19 +65,19 @@ from the annotation, so existing goldens are untouched). Lowest-risk landing. **Files:** Create `compiler/src/gcinfer.ml`; Modify `compiler/src/dune` (add `gcinfer` to `modules`). -- [ ] Build a directed graph over class names: edge `A → B` when `A` has a field whose resolved type is `B`, `?B`, `multi B`, `map`, or `map<_,B>` (unwrap `Nullable`). **`ref B` contributes no edge.** Read fields from `Ast` class decls; resolve names via the symbol table `types.ml` already builds. -- [ ] Run Tarjan's SCC (hand-written, stdlib only). Classify a class **traced** iff it is in a non-trivial SCC **or** has a self-loop; else **owned**. Expose `Gcinfer.classify : -> result` returning the traced-name set plus, per traced class, the reason (a cycle path for the note). -- [ ] Verify with a tiny OCaml unit in `compiler/test` (or the existing runner) over: self-loop (`Node.next: ?Node`), mutual recursion (`A.b:B`, `B.a:A`), `multi Self`, `map<_,Self>`, and the `ref T`-creates-no-edge case. Run `just woc-test`; expected PASS. -- [ ] Commit. +- [x] Build a directed graph over class names: edge `A → B` when `A` has a field whose resolved type is `B`, `?B`, `multi B`, `map`, or `map<_,B>` (unwrap `Nullable`). **`ref B` contributes no edge.** Read fields from `Ast` class decls; resolve names via the symbol table `types.ml` already builds. +- [x] Run Tarjan's SCC (hand-written, stdlib only). Classify a class **traced** iff it is in a non-trivial SCC **or** has a self-loop; else **owned**. Expose `Gcinfer.classify : -> result` returning the traced-name set plus, per traced class, the reason (a cycle path for the note). +- [x] Verify with a tiny OCaml unit in `compiler/test` (or the existing runner) over: self-loop (`Node.next: ?Node`), mutual recursion (`A.b:B`, `B.a:A`), `multi Self`, `map<_,Self>`, and the `ref T`-creates-no-edge case. Run `just woc-test`; expected PASS. +- [x] Commit. ### Task 1.2 — `--dump-gc` mode + golden **Files:** Modify `compiler/bin/main.ml` (argv dispatch + usage), `compiler/src/dump.ml` (renderer). Test: new golden under `compiler/test/golden/`. -- [ ] Add a `--dump-gc ` mode: run lex→parse→types, call `Gcinfer.classify`, print one `Nameowned|gc(reason)` line per class in declaration order (the spec's `--dump-gc` artifact shape). Reason is the cycle path for structural, empty for owned. -- [ ] Add the usage line and the mode to the dispatch match (beside `--dump-owner`). -- [ ] Add a golden fixture: run `--dump-gc` over `docs/examples/gc-cycle` (expect `Node gc (cycle Node -> Node)`, `Segment owned`) and over the pricing corpus subset. Bless with `WOC_BLESS=1`. -- [ ] Run `just woc-test`; expected PASS, and **existing goldens unchanged** (no emit path touched). Commit. +- [x] Add a `--dump-gc ` mode: run lex→parse→types, call `Gcinfer.classify`, print one `Nameowned|gc(reason)` line per class in declaration order (the spec's `--dump-gc` artifact shape). Reason is the cycle path for structural, empty for owned. +- [x] Add the usage line and the mode to the dispatch match (beside `--dump-owner`). +- [x] Add a golden fixture: run `--dump-gc` over `docs/examples/gc-cycle` (expect `Node gc (cycle Node -> Node)`, `Segment owned`) and over the pricing corpus subset. Bless with `WOC_BLESS=1`. +- [x] Run `just woc-test`; expected PASS, and **existing goldens unchanged** (no emit path touched). Commit. --- @@ -74,17 +92,17 @@ front-end end to end before the collector swap. **Files:** Modify `compiler/src/gcinfer.ml`, `compiler/src/owner.ml` (add a collect-promotions mode). -- [ ] Add a mode to the ownership pass that, instead of emitting `WO-E304`/long-lived-alias errors, records the offending class. `Gcinfer` runs owner in this mode, unions the recorded classes into the traced set, and re-runs — terminating because the set only grows (bounded by class count). Each demand promotion carries its escape-site note. -- [ ] Verify: a `PriceCache`-shaped fixture (acyclic, aliased) classifies `gc (alias escape, …)` via `--dump-gc`. `just woc-test` PASS. Commit. +- [x] Add a mode to the ownership pass that, instead of emitting `WO-E304`/long-lived-alias errors, records the offending class. `Gcinfer` runs owner in this mode, unions the recorded classes into the traced set, and re-runs — terminating because the set only grows (bounded by class count). Each demand promotion carries its escape-site note. +- [x] Verify: a `PriceCache`-shaped fixture (acyclic, aliased) classifies `gc (alias escape, …)` via `--dump-gc`. `just woc-test` PASS. Commit. ### Task 2.2 — inference is the source of GC-ness; annotation errors **Files:** Modify `compiler/src/types.ml` (`is_gc_class` reads the inferred set), `compiler/src/parser.ml` (`@gc` arm → diagnostic), `compiler/src/dump.ml` (stop rendering ` @gc`), `compiler/src/emit.ml`/`disasm.ml` (class-flag provenance only; bit unchanged). Error: new WO-E1xx in `docs/plan/oop-vm/01-error-catalog.md`. -- [ ] Thread the inferred traced-set into the typing context so `Types.is_gc_class` answers from it. Field-kind derivation (→ `WO_K_GCREF`) and every `owner.ml` exemption then follow with no further change (that is the seam). -- [ ] Turn the parser's `@gc` acceptance into a WO-E1xx diagnostic pointing at inference + `--dump-gc`. Update the error catalog. -- [ ] Re-bless every golden that rendered ` @gc`, `flags=gc`, or a changed GCREF field kind (`WOC_BLESS=1`). Convert the `gc/` corpus fixtures to drop `@gc` from source (they rely on inference now). -- [ ] Verify: `docs/examples/gc-cycle` now **emits** (no WO-E301 — traced classes alias freely) and **runs** on the current runtime, printing `ring a -> b -> c -> a`. `just woc-test`, `just oop-e2e` PASS. Commit. +- [x] Thread the inferred traced-set into the typing context so `Types.is_gc_class` answers from it. Field-kind derivation (→ `WO_K_GCREF`) and every `owner.ml` exemption then follow with no further change (that is the seam). +- [x] Turn the parser's `@gc` acceptance into a WO-E1xx diagnostic pointing at inference + `--dump-gc`. Update the error catalog. +- [x] Re-bless every golden that rendered ` @gc`, `flags=gc`, or a changed GCREF field kind (`WOC_BLESS=1`). Convert the `gc/` corpus fixtures to drop `@gc` from source (they rely on inference now). +- [x] Verify: `docs/examples/gc-cycle` now **emits** (no WO-E301 — traced classes alias freely) and **runs** on the current runtime, printing `ring a -> b -> c -> a`. `just woc-test`, `just oop-e2e` PASS. Commit. --- @@ -98,33 +116,33 @@ is the largest phase and lands with Phase 2's front-end. **Files:** Modify `runtime/src/wob.h` (`wo_hdr`), `runtime/src/obj.h` (`wo_rt` list head + `wo_obj_new` links traced objects), `runtime/src/gc.{c,h}`. -- [ ] Repurpose the header: retire `rc` and (for traced objects) `borrow`; give those 8 contiguous bytes to a 64-bit intrusive sweep-list link. Keep colors in the existing `WO_F_COLOR` bits. The `_Static_assert(sizeof(wo_hdr)==16)` must still hold. -- [ ] `wo_rt` gains a traced-list head; `wo_obj_new` links a traced-class instance (class-flag bit set) in as white. Sweep recovers size via `wo_obj_size` (class table). Retire `cycbuf` and `WO_F_BUF`. -- [ ] Verify build both dispatch flavors: `just wovm-build` + `make -C runtime test test-iso`. Commit. +- [x] Repurpose the header: retire `rc` and (for traced objects) `borrow`; give those 8 contiguous bytes to a 64-bit intrusive sweep-list link. Keep colors in the existing `WO_F_COLOR` bits. The `_Static_assert(sizeof(wo_hdr)==16)` must still hold. +- [x] `wo_rt` gains a traced-list head; `wo_obj_new` links a traced-class instance (class-flag bit set) in as white. Sweep recovers size via `wo_obj_size` (class table). Retire `cycbuf` and `WO_F_BUF`. +- [x] Verify build both dispatch flavors: `just wovm-build` + `make -C runtime test test-iso`. Commit. ### Task 3.2 — tri-color incremental mark + Yuasa barrier + budgeted sweep **Files:** Modify `runtime/src/gc.{c,h}`, `runtime/src/vm.c` (roots via pc gc-mask; barrier in `SETF`/`map_set`/`push`; retire `RC_INC`/`RC_DEC` cases; safepoints at back-edges/calls). -- [ ] Delete trial deletion (`mark_gray`/`scan_black`/`scan_`/`collect_white`/`white_free`/zombie guard). Implement: roots = value/frame slots read via the per-pc gc-mask; grey worklist; mark budget `WO_GC_BUDGET`; owned objects traversed-not-freed, skipping subtrees via the precomputed "transitively-contains-gcref" class bit; sweep frees white + unlinks, repaints black→white; heap-goal trigger; `WO_GC_TRACE` per-slice counts. -- [ ] Yuasa deletion barrier: on a store into a `GCREF` slot **while marking**, shade the old value grey. Lives in the VM store paths (no new opcode). -- [ ] Verify: `just wovm-test`. Commit. +- [x] Delete trial deletion (`mark_gray`/`scan_black`/`scan_`/`collect_white`/`white_free`/zombie guard). Implement: roots = value/frame slots read via the per-pc gc-mask; grey worklist; mark budget `WO_GC_BUDGET`; owned objects traversed-not-freed, skipping subtrees via the precomputed "transitively-contains-gcref" class bit; sweep frees white + unlinks, repaints black→white; heap-goal trigger; `WO_GC_TRACE` per-slice counts. +- [x] Yuasa deletion barrier: on a store into a `GCREF` slot **while marking**, shade the old value grey. Lives in the VM store paths (no new opcode). +- [x] Verify: `just wovm-test`. Commit. ### Task 3.3 — emitter + format: retire RC, restate the gc-mask, bump `.wob` **Files:** Modify `compiler/src/emit.ml` (drop `emit_rc` + escape-acquire anchor; gc-mask now = GC roots), `compiler/src/owner.ml` (delete rc table/elision/`resolve_rc`/`release_gc`/clobber rule), interpreter (opcodes 27–28 reserved), `docs/plan/oop-vm/00-wob-format.md` (version bump + reserved opcodes + gc-mask contract), `runtime/src/loader.c` if it validates opcodes. -- [ ] Stop emitting `RC_INC`/`RC_DEC`; reserve the opcodes; bump the `.wob` version in the format doc + loader constant. Restate the drop-table gc-mask contract as "GC roots at this pc". `wo_drop_kind` for `WO_K_GCREF` becomes a no-op. -- [ ] Re-bless all affected goldens (`--dump-bc`, `--dump-owner`, disasm) with `WOC_BLESS=1`. -- [ ] Verify: `just woc-test`, `just oop-e2e`, `just oop-accept`. Commit. +- [x] Stop emitting `RC_INC`/`RC_DEC`; reserve the opcodes; bump the `.wob` version in the format doc + loader constant. Restate the drop-table gc-mask contract as "GC roots at this pc". `wo_drop_kind` for `WO_K_GCREF` becomes a no-op. +- [x] Re-bless all affected goldens (`--dump-bc`, `--dump-owner`, disasm) with `WOC_BLESS=1`. +- [x] Verify: `just woc-test`, `just oop-e2e`, `just oop-accept`. Commit. ### Task 3.4 — fixtures: adversarial barrier + cycle rewrites **Files:** Modify `runtime/test/test_cycle.c`, `runtime/test/test_rc.c`; corpus `tests/corpus/gc/{abandoned-cycle,budget-steps,held-cycle}`; add an adversarial barrier fixture. -- [ ] Rewrite `test_cycle.c`/`test_rc.c` off rc assertions onto: abandoned cycle freed, rooted cycle survives, slices bounded, sweep-list leak-free after N cycles. Re-bless `abandoned-cycle`/`budget-steps` traces; **redefine** `held-cycle` as "a cycle rooted from a live frame survives a slice" (from inside a running program). -- [ ] Add the barrier fixture: hide a traced object between slices (store into a blackened object, drop the original ref); it must survive with the barrier in and be freed (corruption) with it compiled out — the design's safety net. -- [ ] Verify: `just wovm-test`, `just oop-accept`, ASan clean after repeated cycles. Commit. +- [x] Rewrite `test_cycle.c`/`test_rc.c` off rc assertions onto: abandoned cycle freed, rooted cycle survives, slices bounded, sweep-list leak-free after N cycles. Re-bless `abandoned-cycle`/`budget-steps` traces; **redefine** `held-cycle` as "a cycle rooted from a live frame survives a slice" (from inside a running program). +- [x] Add the barrier fixture: hide a traced object between slices (store into a blackened object, drop the original ref); it must survive with the barrier in and be freed (corruption) with it compiled out — the design's safety net. +- [x] Verify: `just wovm-test`, `just oop-accept`, ASan clean after repeated cycles. Commit. --- @@ -132,8 +150,8 @@ is the largest phase and lands with Phase 2's front-end. **Files:** `docs/00-principles.md` (principle 3), the OOP spec (decision table, §3 rule 5, §4 memory model), `docs/plan/oop-vm/00-wob-format.md`, `01-error-catalog.md` (retire WO-W201, update WO-E304 wording, add the new WO-E1xx), `08-builtin-surface.md` (delete the `push` special case + `set` gap), `docs/00-status.md` (record 7b superseding iteration 2's memory model), `docs/stories/language-runtime-database/07b-inferred-gc-mark-sweep.md` (status → done), `docs/examples/gc-cycle/README.md` (flip "Run status" to shipped + wire a `just gc-cycle` acceptance). -- [ ] Apply each amendment in the spec's §8 migration table. Add a `docs/examples/gc-cycle` acceptance script + `just gc-cycle` recipe running `--dump-gc` + ring/owned demos under `WO_GC_TRACE`. -- [ ] Verify: `just oop-accept` green; `just gc-cycle` green; `git grep '@gc' -- '*.wo'` returns nothing (success criterion 1). Commit. +- [x] Apply each amendment in the spec's §8 migration table. Add a `docs/examples/gc-cycle` acceptance script + `just gc-cycle` recipe running `--dump-gc` + ring/owned demos under `WO_GC_TRACE`. +- [x] Verify: `just oop-accept` green; `just gc-cycle` green; `git grep '@gc' -- '*.wo'` returns nothing (success criterion 1). Commit. --- diff --git a/docs/superpowers/specs/2026-08-01-oop-compiler-vm-design.md b/docs/superpowers/specs/2026-08-01-oop-compiler-vm-design.md index c5d375d..3f4fa84 100644 --- a/docs/superpowers/specs/2026-08-01-oop-compiler-vm-design.md +++ b/docs/superpowers/specs/2026-08-01-oop-compiler-vm-design.md @@ -11,7 +11,7 @@ writeonce today is a declarative language executed by the Rust runtime (`crates/ - an **OCaml compiler** (`woc`) — fast compiles, no LLVM, - a **C runtime VM** (`wovm`) — libc-only, evolving out of the existing `wo-rt-c` C reference, - **memory-safe object instances**: by default an object behaves like a Rust borrowed value (single owner, checked borrows), -- a **per-class `@gc` override** for reference semantics, collected without stop-the-world pauses, +- **inferred GC-ness** (iteration 7b superseded the `@gc` annotation): classes that cycle or must alias long-lived are traced, without stop-the-world pauses, - the same end product: one binary that is the database, the web API, and the UI, running multithreaded. ## Decisions locked during brainstorming @@ -21,7 +21,7 @@ writeonce today is a declarative language executed by the Rust runtime (`crates/ | Fate of Rust runtime | **Evolve `wo-rt-c` into the C runtime.** OCaml compiler targets it. `crates/rt` stays active until parity, then retires to `.dev/reference/` like v1 did. | | OOP shape | **Keep plan 13 doctrine: no inheritance, no override, no virtual class hierarchies — ever.** OOP = `class` (state + methods) + structural **interfaces** (Go-style) + composition (`ref`/`multi`). | | Borrow enforcement | **Hybrid.** Compiler proves most sites statically and emits nothing; VM enforces residual sites with borrow-word checks at runtime. | -| GC opt-out granularity | **Per-class annotation** `@gc` — all instances of that class are GC-managed and freely aliased. | +| GC opt-out granularity | **Inferred** (amended by the 2026-08-11 7b spec; was a per-class `@gc` annotation): structural cycles via SCC over the class-reference graph + demand promotion at escape sites. Traced instances are freely aliased. | | Execution model | **Register bytecode interpreter first** (computed-goto dispatch). JIT possible later, not now. AOT-to-C rejected (kills hot reload, slow builds). | | Concurrency model | **Shard-actor with ownership transfer.** Thread-per-core shards, one heap per shard, cross-shard = message send = ownership move. GC is per-shard, so no global pause exists by construction. (Implementation is sub-project 2; milestone 1 reserves header space.) | | First sub-project | **Compiler + VM core** — proves the novel risk (hybrid borrow VM) before any HTTP/DB integration. | @@ -122,7 +122,7 @@ class PriceCache { -- reference semantics, freely aliased 2. Function parameter default = immutable borrow. `mut x: T` = exclusive borrow. `take x: T` = ownership moves in. 3. Borrows never escape: cannot be stored in a field, cannot be returned. Compile error. 4. Fields hold owned values, `ref T` ids (existing DB-style links), or `@gc` references. -5. `@gc` class instances alias freely: no borrow rules, reference-counted, cycles collected incrementally. +5. Traced (inferred-gc) class instances alias freely: no borrow rules; an incremental per-shard mark-sweep collects them (amended by the 7b spec — which classes are traced is inferred). 6. Method `self` is an immutable borrow if the body only reads, exclusive if it writes — the compiler infers this; no annotation. **Executes in milestone 1:** class/interface declarations, constructors, field access, method and interface calls, control flow (`if`/`for`/`while`/`return`), arithmetic/text operations, `let`. @@ -144,7 +144,7 @@ struct wo_hdr { uint8_t flags; // bit0 GC_MANAGED, bit1 IN_CYCLE_BUF uint8_t _pad; uint32_t borrow; // 0 = free, N = shared readers, 0xFFFFFFFF = exclusive - uint32_t rc; // strong count, @gc only; unused for owned + // (7b) the borrow word unions with the traced list's intrusive link }; // object fields follow inline ``` @@ -155,9 +155,9 @@ struct wo_hdr { - `owner.ml` proves most sites statically (locals, linear flow, no runtime-indexed aliasing) — zero ops emitted, zero runtime cost. - Residual sites get `BORROW_S` / `BORROW_X` / `RELEASE` on the borrow word. Canonical residual case: two `mut` borrows through runtime indices (`items[i]`, `items[j]` where `i == j` is unprovable). A violation is a VM trap that unwinds to the method boundary as a structured error (Section 6). -**`@gc` objects:** RC increment/decrement on alias creation/drop (compiler-emitted, elided for provably balanced pairs). `rc == 0` frees immediately. Cycle risk exists only when a `@gc` object holds `@gc`-typed fields — those go to a per-shard possible-cycle buffer on decrement (Bacon–Rajan trial deletion), scanned **incrementally with a fixed per-tick budget** on the shard's own event loop. Per-shard heap, per-shard buffer: no cross-shard tracing, no global pause; worst case is a bounded slice of one shard's tick. +**Traced objects (amended by the 7b spec, 2026-08-11):** reference counting is retired. Every traced allocation links onto a per-shard traced list; an incremental tri-color mark-sweep collects it — roots snapshot from the frames' per-pc masks at cycle start, a Yuasa deletion barrier shades overwritten gcref edges while marking, allocations mid-cycle are born black, and both mark and sweep run in budgeted slices (`WO_GC_BUDGET`). The header's old `rc`+`borrow` words are the traced list's intrusive link. Per-shard heap, per-shard list: no cross-shard tracing, no global pause. -**Mixing rule:** an owned object may hold `@gc` references (rc participates). A `@gc` object may hold owned values (it owns them; they drop when the holder is freed). The borrow word applies only to owned objects; `@gc` aliasing is unrestricted by design. +**Mixing rule:** an owned object may hold traced references (the mark phase walks owned interiors to find them). A traced object may hold owned values (it owns them; they drop when sweep frees the holder). The borrow word applies only to owned objects; traced aliasing is unrestricted by design. ## Section 5 — Bytecode and VM