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>
65 lines
3.1 KiB
C
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 */
|