writeonce/docs/stories/language-runtime-database/07b-inferred-gc-mark-sweep.md
shoney.arickathil 28485a271c docs: iteration 7b migration — amend the normative docs (Phase 4)
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) <noreply@anthropic.com>
2026-08-19 17:11:35 +02:00

7.5 KiB
Raw Blame History

Iteration 7b — inferred GC + incremental mark-sweep

Format: product/story-iteration-template. Part of Story — one language, one runtime, one database, one binary.

Inserted 2026-08-11, after the plan was first drawn — hence 7b rather than a renumber. It sits here because the log-watcher critical path (iterations 3–7) must not be delayed, and because the collector should be settled before iteration 8 multiplies shards.

Status (2026-08-18): LANDED (branch inferred-gc; plan 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: 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

  • The developer stops deciding which types are garbage collected. @gc disappears from the language; the compiler infers GC-ness and reports every decision with its reason.
  • Reference counting is replaced by an incremental per-shard tri-color mark-sweep collector, so the compiler no longer has to emit a balanced acquire/release at every alias site — the source of every recorded @gc defect.
  • Milestone 1's acceptance criterion 3 (ASan-clean across the corpus) closes, because the leak blocking it is one of the defects this deletes.

Acceptance Criteria

  • What to achieve?
    • Given a class whose declaration can form a reference cycle, and a separate class that is only ever shared through a long-lived alias,
    • when the program is compiled,
    • then both are classified GC-managed without any annotation, and --dump-gc names the reason for each — a cycle path for the first, the escaping alias site for the second.
  • What to achieve?
    • Given any .wo source containing @gc,
    • when it is compiled,
    • then it is a diagnostic pointing at inference and --dump-gc, not a silently accepted no-op.
  • What to achieve?
    • Given a program that hides a traced object from the collector — storing it into an already-blackened object between marking slices and dropping the original reference,
    • when the collector completes,
    • then the object is still alive; and the same fixture fails loudly if the write barrier is compiled out.
  • What to achieve?
    • Given an abandoned cycle and a cycle still rooted from a live frame,
    • when collection runs,
    • then the abandoned one is freed within budgeted slices with no slice exceeding the configured budget, and the rooted one survives.
  • What to achieve?
    • Given the whole conformance corpus, run repeatedly so several collection cycles occur,
    • when it runs under ASan,
    • then zero leaks and zero errors — the clause that currently fails.
  • What to achieve?
    • Given any emitted .wob image,
    • when it is disassembled,
    • then no RC_INC or RC_DEC appears, and the format doc records opcodes 27–28 as reserved behind a version bump.

Out Of Scope

  • Cross-shard tracing — ownership moves mean no traced object spans shards.
  • Generational collection and compaction. Non-moving is load-bearing: no forwarding pointers, no read barrier. Go's collector is not generational either.
  • Scheduler-integrated pacing beyond the heap-goal trigger; that stays iteration 8's concern, which is part of why this lands first.
  • ref T semantics, unchanged — it is an id, not a pointer, and creates no edge in the inference graph.

Info

  • Governing spec: docs/superpowers/specs/2026-08-11-inferred-gc-mark-sweep-design.md.
  • Gated by the benchmark (2026-08-15): this is the "implement garbage collection" lever of the performance arc — tri-color mark-sweep replacing RC changes the write path's tail latency, so landing it means re-running iteration 9e and recording the delta (does tracing help or hurt p99 under write load?).
  • Constraint added by the database track (2026-08-15): a GC-managed value in a @table field is a compile error (the engine/heap bulkhead — 9b design, section 6). Once GC-ness is inferred rather than annotated, the inference pass must classify every class before table-field validation, and the diagnostic must name the inference reason ("class X is garbage-collected via Y and cannot be stored in a table field") — otherwise the error becomes unactionable exactly when it stops being self-evident.
  • Why the annotation was insufficient, not merely inconvenient: the OOP spec's own example, @gc class PriceCache { entries: map<SKU, Money> }, is acyclic. It needs GC because it is shared, and second-class borrows cannot be stored or returned. So the developer was being asked to reason about type shape and whole-program aliasing at once — hence the hybrid rule (structural SCC plus reported demand promotion).
  • Why tracing rather than better reference counting: all four recorded @gc defects are RC bookkeeping failures — push missing an increment, set still missing one, the mut-@gc clobber, and the held-cycle leak. Inferring GC-ness would widen that population and so widen that bug class. Tracing emits no per-alias bookkeeping at all.
  • Most of what tracing needs already exists. The emitter already produces precise per-pc pointer masks (the drop table's gc mask) and the class table already carries per-field kinds — the two pieces Go gets from stack maps and type maps. Go's dependence on OS threads is incidental; the algorithm needs only per-frame PC→map lookup and the ability to suspend one stack.
  • The one real runtime addition: the arena cannot enumerate objects — bump allocation plus size-class free lists, with large objects on bare malloc and no size headers anywhere. Sweep needs its own list. Retiring rc, plus the borrow word that traced objects never use, frees exactly eight contiguous bytes for an intrusive link, so the 16-byte header survives.
  • This iteration supersedes part of iteration 2's memory model (§4 of the OOP spec) and closes iteration 4's open gate clause. Neither is renumbered; both carry pointers here.

Proposed Solution

  • Write the implementation plan from the approved spec, then execute it: the gcinfer.ml pass (Tarjan SCC over the class-reference graph, then demand promotion to a fixpoint, with a note per decision), the @gc removal and its diagnostic, retiring RC_INC/RC_DEC and the rc machinery from owner.ml/emit.ml, the per-shard traced list and sweep, incremental tri-color marking with roots read from the existing pc masks, the Yuasa deletion barrier inside the VM's store paths, and the doc/golden migration the spec's §8 table enumerates.