writeonce/runtime/src/gc.h
shoney.arickathil 841cb41c6b feat(runtime): incremental tri-color mark-sweep replaces RC (7b Phase 3a)
Reference counting and Bacon-Rajan trial deletion are gone from the runtime.
Traced (inferred-gc) objects now die only by the collector; owned values keep
deterministic drops exactly as before.

- wo_hdr: rc retired; borrow and the freed 4 bytes become a union — non-traced
  values keep the borrow word, traced objects use the 8 bytes as the intrusive
  sweep-list link. Header stays exactly 16 bytes. WHITE is now the all-zero
  color (allocations born white by memset); WO_F_BUF retired.
- gc.c rewritten: snapshot-at-beginning tri-color mark-sweep. Roots (frames'
  gc+owned masks) shaded atomically at cycle start; Yuasa deletion barrier
  shades the OLD target of every gcref edge deleted while marking (SETF
  overwrites + every owned-death path, which all funnel through wo_drop_kind's
  GCREF case); allocations mid-cycle born black. Mark AND sweep budgeted
  (WO_GC_BUDGET objects/slice), sweep resumes via a cursor; gray-worklist OOM
  degrades to a blacken-all cycle (frees nothing, never wrong). Owned interiors
  walked eagerly (single-owner trees), pruned by a per-class may-gcref bit
  computed at rt_init (fixpoint over kinds + v2 field_class/field_elem;
  conservative when metadata is absent).
- vm.c: safepoints at NEW (the heap-goal trigger), CALL, and backward JMP;
  root scan follows vm_unwind's governing-pc convention. Unwind's gc-mask
  branch just nulls the register. RC_INC/RC_DEC are accepted as no-ops until
  the emitter stops producing them (next commit) — which also deletes the old
  RC_DEC-on-nil trap that broke `?Node` gcref field stores.
- main.c pump: post-exit, a rootless cycle frees everything unreachable in
  budgeted slices; the trace line moved into wo_gc_slice (one format for pump
  and in-program slices). rt_destroy frees traced remnants (trap paths, tests).
- WO_GC_GOAL joins WO_GC_BUDGET/WO_GC_TRACE as an rt-owned knob (default 256
  KiB; a tiny goal forces mid-program cycles for testing).
- tests: test_cycle.c rewritten (abandoned cycle freed, rooted cycle survives,
  slices bounded, cycle-through-multi, repeated-cycle leak-freedom, and the
  spec's load-bearing DELETION-BARRIER test: an object hidden behind a black
  object mid-mark must survive). test_rc.c re-pinned to owned drops + the
  owned/traced boundary; test_obj.c asserts tracked-white-linked instead of
  rc=1.

Verified: make test + test-iso (all suites, ASan/UBSan; test_cycle 42/0,
test_rc 14/0) + cli_smoke; oop-e2e 79/0 (gc corpus traces unchanged: the new
slice math reproduces steps=1/freed=2 and steps=2/freed=4); employee 8/0;
log-watcher 7/0. THE RING RUNS: docs/examples/gc-cycle prints
`ring a -> b -> c -> a`, is reclaimed post-exit (freed=3 remaining=0), is ASan
clean, and survives an in-program cycle while rooted (WO_GC_GOAL=64: mid-run
slice frees 0, post-exit frees 3).

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

65 lines
3.1 KiB
C

/* gc.h — deterministic destruction + incremental tri-color mark-sweep for
* traced (inferred-gc) objects (iteration 7b, spec 2026-08-11).
*
* Two reclamation systems, one module:
* - OWNED values die deterministically via drop plans (wo_drop_obj /
* wo_drop_kind), exactly as before.
* - TRACED objects (instances of classes the compiler inferred `gc`) die
* only by the collector: every traced allocation links onto the
* per-shard traced list; a cycle marks from the VM's root snapshot and
* sweeps the unmarked.
*
* The algorithm is snapshot-at-beginning: roots (the value/frame stacks,
* read through the per-pc gc/owned masks) are scanned atomically when a
* cycle starts; a Yuasa deletion barrier shades the OLD value of every
* gcref edge deleted while marking (SETF overwrites and owned-value deaths
* both funnel through here); objects allocated mid-cycle are born black.
* Marking and sweeping are budgeted (WO_GC_BUDGET objects per slice) so no
* slice's pause grows with the heap. */
#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. WO_K_GCREF is the deletion
* barrier: while marking, the old target is shaded; otherwise a no-op —
* tracing owns traced lifetimes, so an owned value dying never frees them. */
void wo_drop_kind(wo_rt *rt, uint8_t kind, uint64_t v);
/* Drop any OWNED 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. Never called on a traced object (sweep frees those). */
void wo_drop_obj(wo_rt *rt, wo_hdr *o);
/* Link a freshly allocated traced object onto the traced list and account
* its bytes toward the cycle trigger (called by wo_obj_new). */
void wo_gc_track(wo_rt *rt, wo_hdr *o, size_t size);
/* Yuasa deletion barrier half: shade a traced object gray if it is still
* white. Also the root-shading primitive. Safe on any phase; only MARK
* callers need it. */
void wo_gc_shade(wo_rt *rt, wo_hdr *o);
/* Root visitor: shade a traced object, or walk an owned value's interior
* (fields, container elements) shading every traced object it can reach —
* pruned by the per-class may-gcref bit. Used for the root snapshot and by
* the mark phase when it crosses an owned field. */
void wo_gc_scan_root(wo_rt *rt, wo_hdr *o);
/* 1 = the trigger says a cycle should start (idle + traced bytes past the
* goal). The caller (the VM's safepoints, the post-exit pump) snapshots
* roots and calls wo_gc_begin. */
int wo_gc_want_start(const wo_rt *rt);
/* Enter MARK phase. The caller shades the roots (wo_gc_scan_root over the
* live frames' masks) immediately after — before the mutator resumes. */
void wo_gc_begin(wo_rt *rt);
/* One budgeted slice: pops up to `budget` gray objects and scans them;
* when the worklist drains, switches to SWEEP and frees up to `budget`
* unmarked traced objects per slice, repainting survivors white. Returns
* the number freed by this slice. No-op when idle. */
size_t wo_gc_slice(wo_rt *rt, size_t budget);
#endif /* WO_GC_H */