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>
This commit is contained in:
parent
3f842f854b
commit
841cb41c6b
10 changed files with 598 additions and 369 deletions
372
runtime/src/gc.c
372
runtime/src/gc.c
|
|
@ -1,9 +1,43 @@
|
||||||
|
/* gc.c — deterministic drops + incremental tri-color mark-sweep (iteration
|
||||||
|
* 7b, spec 2026-08-11). Replaces RC + Bacon–Rajan trial deletion outright:
|
||||||
|
* no reference counts exist anywhere; traced objects (inferred-gc classes)
|
||||||
|
* die only by sweep, owned values die deterministically exactly as before.
|
||||||
|
*
|
||||||
|
* Snapshot-at-beginning correctness argument, in one place:
|
||||||
|
* 1. When a cycle begins, the caller shades every root (the frames' gc
|
||||||
|
* and owned masks) before the mutator resumes — the snapshot.
|
||||||
|
* 2. While marking, deleting a gcref edge (a SETF overwrite, an owned
|
||||||
|
* value dying with a gcref inside) shades the OLD target first — the
|
||||||
|
* Yuasa deletion barrier. Every path that deletes such an edge
|
||||||
|
* funnels through wo_drop_kind's GCREF case or SETF's store site.
|
||||||
|
* 3. Objects allocated while a cycle runs are born black (live for this
|
||||||
|
* cycle) — a brand-new object can never be swept by the cycle it was
|
||||||
|
* born into.
|
||||||
|
* Together: anything reachable at snapshot time, or created after it,
|
||||||
|
* survives; only garbage that was already unreachable is swept. A
|
||||||
|
* pointer the mutator holds mid-cycle was obtained from a root, a field
|
||||||
|
* (protected by 1+2), or an allocation (protected by 3).
|
||||||
|
*
|
||||||
|
* The gray worklist holds TRACED objects only — sweep is the sole freer of
|
||||||
|
* traced objects, so a queued pointer can never dangle. Owned interiors
|
||||||
|
* are walked eagerly (they are single-owner trees: no cycles, no dedup
|
||||||
|
* needed), pruned by the per-class may-gcref bit. */
|
||||||
#include "gc.h"
|
#include "gc.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
|
||||||
#include "cont.h"
|
#include "cont.h"
|
||||||
|
|
||||||
|
static int is_traced(const wo_hdr *o) { return (o->flags & WO_F_GC) != 0; }
|
||||||
|
|
||||||
|
static uint8_t color_of(const wo_hdr *o) { return o->flags & WO_F_COLOR; }
|
||||||
|
static void set_color(wo_hdr *o, uint8_t c) {
|
||||||
|
o->flags = (uint8_t)((o->flags & ~WO_F_COLOR) | c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- deterministic destruction (owned values) -------------------------- */
|
||||||
|
|
||||||
static void multi_free(wo_rt *rt, wo_multi *m) {
|
static void multi_free(wo_rt *rt, wo_multi *m) {
|
||||||
for (uint32_t i = 0; i < m->len; i++)
|
for (uint32_t i = 0; i < m->len; i++)
|
||||||
wo_drop_kind(rt, m->elem_kind, m->items[i]);
|
wo_drop_kind(rt, m->elem_kind, m->items[i]);
|
||||||
|
|
@ -21,7 +55,11 @@ static void map_free(wo_rt *rt, wo_map *m) {
|
||||||
wo_arena_free(&rt->arena, m, sizeof(wo_map));
|
wo_arena_free(&rt->arena, m, sizeof(wo_map));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* release a class object's contents then the object itself */
|
/* release a class object's contents then the object itself. Serves owned
|
||||||
|
* objects (drop paths) and dead traced objects (sweep): a traced object's
|
||||||
|
* gcref edges are no-ops here (their targets die by their own color), its
|
||||||
|
* owned interior is dropped — the traced object was that interior's single
|
||||||
|
* owner. */
|
||||||
static void class_free(wo_rt *rt, wo_hdr *o) {
|
static void class_free(wo_rt *rt, wo_hdr *o) {
|
||||||
const wo_classdesc *c = &rt->classes[o->class_id];
|
const wo_classdesc *c = &rt->classes[o->class_id];
|
||||||
uint64_t *f = wo_fields(o);
|
uint64_t *f = wo_fields(o);
|
||||||
|
|
@ -59,7 +97,12 @@ void wo_drop_kind(wo_rt *rt, uint8_t kind, uint64_t v) {
|
||||||
wo_drop_obj(rt, (wo_hdr *)(uintptr_t)v);
|
wo_drop_obj(rt, (wo_hdr *)(uintptr_t)v);
|
||||||
return;
|
return;
|
||||||
case WO_K_GCREF:
|
case WO_K_GCREF:
|
||||||
wo_rc_dec(rt, (wo_hdr *)(uintptr_t)v);
|
/* the Yuasa deletion barrier: this call site is deleting a gcref
|
||||||
|
* edge (an owned holder dying, a container element dropped). While
|
||||||
|
* marking, the old target must be shaded or the snapshot leaks
|
||||||
|
* reachability; any other time, tracing owns the lifetime and the
|
||||||
|
* edge's death means nothing. */
|
||||||
|
if (rt->gc_phase == WO_GC_MARK) wo_gc_shade(rt, (wo_hdr *)(uintptr_t)v);
|
||||||
return;
|
return;
|
||||||
case WO_K_TEXT:
|
case WO_K_TEXT:
|
||||||
wo_str_free(rt, (wo_str *)(uintptr_t)v);
|
wo_str_free(rt, (wo_str *)(uintptr_t)v);
|
||||||
|
|
@ -69,228 +112,171 @@ void wo_drop_kind(wo_rt *rt, uint8_t kind, uint64_t v) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void wo_rc_inc(wo_hdr *o) { o->rc++; }
|
/* ---- traced list + shading --------------------------------------------- */
|
||||||
|
|
||||||
/* ---- cycle-candidate buffer ---- */
|
void wo_gc_track(wo_rt *rt, wo_hdr *o, size_t size) {
|
||||||
|
o->gclink = rt->gc_traced;
|
||||||
/* only classes that can point at other @gc objects can close a cycle */
|
rt->gc_traced = o;
|
||||||
static int class_possibly_cyclic(const wo_rt *rt, const wo_hdr *o) {
|
rt->gc_traced_cnt++;
|
||||||
const wo_classdesc *c = &rt->classes[o->class_id];
|
rt->gc_alloc_bytes += size;
|
||||||
for (uint32_t i = 0; i < c->field_cnt; i++) {
|
|
||||||
uint8_t k = c->kinds[i];
|
|
||||||
if (k == WO_K_GCREF || k == WO_K_MULTI || k == WO_K_MAP) return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void buf_push(wo_rt *rt, wo_hdr *o) {
|
void wo_gc_shade(wo_rt *rt, wo_hdr *o) {
|
||||||
if (rt->cycbuf.len == rt->cycbuf.cap) {
|
if (!o || !is_traced(o)) return;
|
||||||
size_t ncap = rt->cycbuf.cap ? rt->cycbuf.cap * 2 : 16;
|
if (color_of(o) != WO_COLOR_WHITE) return; /* gray or black: already safe */
|
||||||
wo_hdr **ni = realloc(rt->cycbuf.items, ncap * sizeof(wo_hdr *));
|
set_color(o, WO_COLOR_GRAY);
|
||||||
if (!ni) return; /* can't buffer: conservative leak, never corrupt */
|
if (rt->gc_gray.len == rt->gc_gray.cap) {
|
||||||
rt->cycbuf.items = ni;
|
size_t ncap = rt->gc_gray.cap ? rt->gc_gray.cap * 2 : 64;
|
||||||
rt->cycbuf.cap = ncap;
|
wo_hdr **ni = realloc(rt->gc_gray.items, ncap * sizeof(wo_hdr *));
|
||||||
}
|
if (!ni) {
|
||||||
o->flags |= WO_F_BUF;
|
/* cannot queue: this cycle can no longer prove anything dead.
|
||||||
rt->cycbuf.items[rt->cycbuf.len++] = o;
|
* Flag it; the next slice blackens the whole traced list and
|
||||||
}
|
* the sweep frees nothing — a conservative, safe cycle. */
|
||||||
|
rt->gc_gray.oom = 1;
|
||||||
/* swap-remove a specific object from the buffer (whites purged mid-step) */
|
set_color(o, WO_COLOR_BLACK);
|
||||||
static void buf_remove(wo_rt *rt, wo_hdr *o) {
|
|
||||||
for (size_t i = 0; i < rt->cycbuf.len; i++) {
|
|
||||||
if (rt->cycbuf.items[i] == o) {
|
|
||||||
rt->cycbuf.items[i] = rt->cycbuf.items[--rt->cycbuf.len];
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
rt->gc_gray.items = ni;
|
||||||
|
rt->gc_gray.cap = ncap;
|
||||||
}
|
}
|
||||||
|
rt->gc_gray.items[rt->gc_gray.len++] = o;
|
||||||
}
|
}
|
||||||
|
|
||||||
void wo_rc_dec(wo_rt *rt, wo_hdr *o) {
|
/* may this class's instances transitively hold a gcref? NULL table (or an
|
||||||
o->rc--;
|
* out-of-range id) answers conservatively: traverse. */
|
||||||
if (o->rc == 0) {
|
static int class_may_gcref(const wo_rt *rt, uint32_t class_id) {
|
||||||
/* zombie guard: a buffered candidate's death belongs to the cycle
|
if (!rt->gc_may || class_id >= rt->class_cnt) return 1;
|
||||||
* collector — it will notice rc 0 and free it in its own sweep */
|
return rt->gc_may[class_id] != 0;
|
||||||
if (o->flags & WO_F_BUF) return;
|
}
|
||||||
class_free(rt, o);
|
|
||||||
|
/* Walk an owned value's interior, shading every traced object reachable
|
||||||
|
* through it. Owned graphs are single-owner trees — no cycles, so plain
|
||||||
|
* recursion terminates; depth is data-structure depth, same as class_free.
|
||||||
|
* Traced objects themselves are shaded, never entered: their fields are
|
||||||
|
* scanned when the mark loop pops them gray. */
|
||||||
|
void wo_gc_scan_root(wo_rt *rt, wo_hdr *o) {
|
||||||
|
if (!o) return;
|
||||||
|
if (is_traced(o)) {
|
||||||
|
wo_gc_shade(rt, o);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
/* survived a decrement and can sit on a cycle: buffer as a candidate,
|
switch (o->class_id) {
|
||||||
* deduplicated by the flag (Bacon–Rajan possible-root heuristic) */
|
case WO_CLS_STR:
|
||||||
if (!(o->flags & WO_F_BUF) && class_possibly_cyclic(rt, o))
|
return;
|
||||||
buf_push(rt, o);
|
case WO_CLS_MULTI: {
|
||||||
|
wo_multi *m = (wo_multi *)o;
|
||||||
|
if (m->elem_kind == WO_K_GCREF || m->elem_kind == WO_K_OWNED ||
|
||||||
|
m->elem_kind == WO_K_MULTI || m->elem_kind == WO_K_MAP)
|
||||||
|
for (uint32_t i = 0; i < m->len; i++)
|
||||||
|
wo_gc_scan_root(rt, (wo_hdr *)(uintptr_t)m->items[i]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case WO_CLS_MAP: {
|
||||||
|
wo_map *mp = (wo_map *)o;
|
||||||
|
for (uint32_t i = 0; i < mp->len; i++) {
|
||||||
|
if (mp->key_kind == WO_K_GCREF || mp->key_kind == WO_K_OWNED ||
|
||||||
|
mp->key_kind == WO_K_MULTI || mp->key_kind == WO_K_MAP)
|
||||||
|
wo_gc_scan_root(rt, (wo_hdr *)(uintptr_t)mp->keys[i]);
|
||||||
|
if (mp->val_kind == WO_K_GCREF || mp->val_kind == WO_K_OWNED ||
|
||||||
|
mp->val_kind == WO_K_MULTI || mp->val_kind == WO_K_MAP)
|
||||||
|
wo_gc_scan_root(rt, (wo_hdr *)(uintptr_t)mp->vals[i]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
if (o->class_id >= rt->class_cnt) return; /* defensive */
|
||||||
|
if (!class_may_gcref(rt, o->class_id)) return;
|
||||||
|
const wo_classdesc *c = &rt->classes[o->class_id];
|
||||||
|
uint64_t *f = wo_fields(o);
|
||||||
|
for (uint32_t i = 0; i < c->field_cnt; i++) {
|
||||||
|
uint8_t k = c->kinds[i];
|
||||||
|
if (k == WO_K_GCREF || k == WO_K_OWNED || k == WO_K_MULTI ||
|
||||||
|
k == WO_K_MAP)
|
||||||
|
wo_gc_scan_root(rt, (wo_hdr *)(uintptr_t)f[i]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- budgeted Bacon–Rajan trial deletion ---- */
|
/* ---- the cycle ---------------------------------------------------------- */
|
||||||
|
|
||||||
static uint8_t color_of(const wo_hdr *o) { return o->flags & WO_F_COLOR; }
|
int wo_gc_want_start(const wo_rt *rt) {
|
||||||
static void set_color(wo_hdr *o, uint8_t c) {
|
return rt->gc_phase == WO_GC_IDLE && rt->gc_alloc_bytes >= rt->gc_goal &&
|
||||||
o->flags = (uint8_t)((o->flags & ~WO_F_COLOR) | c);
|
rt->gc_traced != NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* visit every @gc edge out of a class object: gcref fields, plus gcref
|
void wo_gc_begin(wo_rt *rt) {
|
||||||
* elements inside multi/map fields */
|
rt->gc_gray.len = 0;
|
||||||
typedef void (*child_fn)(wo_rt *rt, wo_hdr *child, void *ctx);
|
rt->gc_gray.oom = 0;
|
||||||
static void visit_children(wo_rt *rt, wo_hdr *o, child_fn fn, void *ctx) {
|
rt->gc_phase = WO_GC_MARK;
|
||||||
|
/* the caller shades the roots now, before the mutator resumes */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* scan one gray (traced) object's out-edges, then blacken it */
|
||||||
|
static void scan_traced(wo_rt *rt, wo_hdr *o) {
|
||||||
const wo_classdesc *c = &rt->classes[o->class_id];
|
const wo_classdesc *c = &rt->classes[o->class_id];
|
||||||
uint64_t *f = wo_fields(o);
|
uint64_t *f = wo_fields(o);
|
||||||
for (uint32_t i = 0; i < c->field_cnt; i++) {
|
for (uint32_t i = 0; i < c->field_cnt; i++) {
|
||||||
uint8_t k = c->kinds[i];
|
uint8_t k = c->kinds[i];
|
||||||
if (k == WO_K_GCREF) {
|
if (k == WO_K_GCREF) {
|
||||||
if (f[i]) fn(rt, (wo_hdr *)(uintptr_t)f[i], ctx);
|
if (f[i]) wo_gc_shade(rt, (wo_hdr *)(uintptr_t)f[i]);
|
||||||
} else if (k == WO_K_MULTI) {
|
} else if (k == WO_K_OWNED || k == WO_K_MULTI || k == WO_K_MAP) {
|
||||||
wo_multi *m = (wo_multi *)(uintptr_t)f[i];
|
wo_gc_scan_root(rt, (wo_hdr *)(uintptr_t)f[i]);
|
||||||
if (m && m->elem_kind == WO_K_GCREF)
|
|
||||||
for (uint32_t j = 0; j < m->len; j++)
|
|
||||||
if (m->items[j]) fn(rt, (wo_hdr *)(uintptr_t)m->items[j], ctx);
|
|
||||||
} else if (k == WO_K_MAP) {
|
|
||||||
wo_map *mp = (wo_map *)(uintptr_t)f[i];
|
|
||||||
if (!mp) continue;
|
|
||||||
if (mp->key_kind == WO_K_GCREF)
|
|
||||||
for (uint32_t j = 0; j < mp->len; j++)
|
|
||||||
if (mp->keys[j]) fn(rt, (wo_hdr *)(uintptr_t)mp->keys[j], ctx);
|
|
||||||
if (mp->val_kind == WO_K_GCREF)
|
|
||||||
for (uint32_t j = 0; j < mp->len; j++)
|
|
||||||
if (mp->vals[j]) fn(rt, (wo_hdr *)(uintptr_t)mp->vals[j], ctx);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
static void mark_gray(wo_rt *rt, wo_hdr *o);
|
|
||||||
static void mark_gray_child(wo_rt *rt, wo_hdr *c, void *ctx) {
|
|
||||||
(void)ctx;
|
|
||||||
c->rc--; /* trial-delete this edge */
|
|
||||||
mark_gray(rt, c);
|
|
||||||
}
|
|
||||||
static void mark_gray(wo_rt *rt, wo_hdr *o) {
|
|
||||||
if (color_of(o) == WO_COLOR_GRAY) return;
|
|
||||||
set_color(o, WO_COLOR_GRAY);
|
|
||||||
visit_children(rt, o, mark_gray_child, NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void scan_black(wo_rt *rt, wo_hdr *o);
|
|
||||||
static void scan_black_child(wo_rt *rt, wo_hdr *c, void *ctx) {
|
|
||||||
(void)ctx;
|
|
||||||
c->rc++; /* restore the trial-deleted edge */
|
|
||||||
if (color_of(c) != WO_COLOR_BLACK) scan_black(rt, c);
|
|
||||||
}
|
|
||||||
static void scan_black(wo_rt *rt, wo_hdr *o) {
|
|
||||||
set_color(o, WO_COLOR_BLACK);
|
set_color(o, WO_COLOR_BLACK);
|
||||||
visit_children(rt, o, scan_black_child, NULL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void scan(wo_rt *rt, wo_hdr *o, void *ctx);
|
size_t wo_gc_slice(wo_rt *rt, size_t budget) {
|
||||||
static void scan_(wo_rt *rt, wo_hdr *o) {
|
size_t freed = 0;
|
||||||
if (color_of(o) != WO_COLOR_GRAY) return;
|
if (rt->gc_phase == WO_GC_IDLE) return 0;
|
||||||
if (o->rc > 0) {
|
|
||||||
scan_black(rt, o); /* externally held: restore the whole subgraph */
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
set_color(o, WO_COLOR_WHITE);
|
|
||||||
visit_children(rt, o, scan, NULL);
|
|
||||||
}
|
|
||||||
static void scan(wo_rt *rt, wo_hdr *o, void *ctx) {
|
|
||||||
(void)ctx;
|
|
||||||
scan_(rt, o);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* gather whites into a step-local list (post-marking, pre-free) */
|
if (rt->gc_phase == WO_GC_MARK) {
|
||||||
typedef struct {
|
if (rt->gc_gray.oom) {
|
||||||
wo_hdr **items;
|
/* worklist allocation failed mid-mark: blacken everything so
|
||||||
size_t len, cap;
|
* the sweep frees nothing — a wasted cycle, never a wrong one */
|
||||||
int oom;
|
for (wo_hdr *o = rt->gc_traced; o; o = o->gclink)
|
||||||
} whites_t;
|
set_color(o, WO_COLOR_BLACK);
|
||||||
|
rt->gc_gray.len = 0;
|
||||||
static void collect_white(wo_rt *rt, wo_hdr *o, void *ctx) {
|
}
|
||||||
whites_t *w = ctx;
|
size_t done = 0;
|
||||||
if (color_of(o) != WO_COLOR_WHITE) return;
|
while (done < budget && rt->gc_gray.len > 0) {
|
||||||
set_color(o, WO_COLOR_BLACK); /* dedup: gathered exactly once */
|
wo_hdr *o = rt->gc_gray.items[--rt->gc_gray.len];
|
||||||
visit_children(rt, o, collect_white, ctx);
|
scan_traced(rt, o);
|
||||||
if (w->len == w->cap) {
|
done++;
|
||||||
size_t ncap = w->cap ? w->cap * 2 : 16;
|
}
|
||||||
wo_hdr **ni = realloc(w->items, ncap * sizeof(wo_hdr *));
|
if (rt->gc_gray.len == 0) {
|
||||||
if (!ni) {
|
rt->gc_phase = WO_GC_SWEEP;
|
||||||
w->oom = 1;
|
rt->gc_sweep = &rt->gc_traced;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
w->items = ni;
|
|
||||||
w->cap = ncap;
|
|
||||||
}
|
}
|
||||||
w->items[w->len++] = o;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Free a dead white: release contents but SKIP every @gc edge — all edge
|
if (rt->gc_phase == WO_GC_SWEEP) {
|
||||||
* accounting was already settled by the gray/scan phases, and the pointed-
|
size_t done = 0;
|
||||||
* at whites die in this same sweep. Containers holding gcref elements free
|
wo_hdr **link = rt->gc_sweep;
|
||||||
* only their backing. */
|
while (done < budget && *link) {
|
||||||
static void white_free(wo_rt *rt, wo_hdr *o) {
|
wo_hdr *o = *link;
|
||||||
const wo_classdesc *c = &rt->classes[o->class_id];
|
if (color_of(o) == WO_COLOR_WHITE) {
|
||||||
uint64_t *f = wo_fields(o);
|
*link = o->gclink; /* unlink, then free contents + object */
|
||||||
for (uint32_t i = 0; i < c->field_cnt; i++) {
|
rt->gc_traced_cnt--;
|
||||||
uint8_t k = c->kinds[i];
|
class_free(rt, o);
|
||||||
uint64_t v = f[i];
|
freed++;
|
||||||
if (!v || k == WO_K_SCALAR || k == WO_K_GCREF) continue;
|
} else {
|
||||||
if (k == WO_K_MULTI) {
|
set_color(o, WO_COLOR_WHITE); /* survivor: candidate next cycle */
|
||||||
wo_multi *m = (wo_multi *)(uintptr_t)v;
|
link = &o->gclink;
|
||||||
if (m->elem_kind != WO_K_GCREF)
|
|
||||||
for (uint32_t j = 0; j < m->len; j++)
|
|
||||||
wo_drop_kind(rt, m->elem_kind, m->items[j]);
|
|
||||||
free(m->items);
|
|
||||||
wo_arena_free(&rt->arena, m, sizeof(wo_multi));
|
|
||||||
} else if (k == WO_K_MAP) {
|
|
||||||
wo_map *mp = (wo_map *)(uintptr_t)v;
|
|
||||||
for (uint32_t j = 0; j < mp->len; j++) {
|
|
||||||
if (mp->key_kind != WO_K_GCREF)
|
|
||||||
wo_drop_kind(rt, mp->key_kind, mp->keys[j]);
|
|
||||||
if (mp->val_kind != WO_K_GCREF)
|
|
||||||
wo_drop_kind(rt, mp->val_kind, mp->vals[j]);
|
|
||||||
}
|
}
|
||||||
free(mp->keys);
|
done++;
|
||||||
free(mp->vals);
|
}
|
||||||
wo_arena_free(&rt->arena, mp, sizeof(wo_map));
|
rt->gc_sweep = link;
|
||||||
} else {
|
if (!*link) {
|
||||||
wo_drop_kind(rt, k, v); /* OWNED subtree, TEXT */
|
rt->gc_phase = WO_GC_IDLE;
|
||||||
|
rt->gc_sweep = NULL;
|
||||||
|
rt->gc_alloc_bytes = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
wo_arena_free(&rt->arena, o, wo_obj_size(c));
|
if (rt->gc_trace)
|
||||||
}
|
fprintf(stderr, "gc: step %zu budget=%zu freed=%zu remaining=%zu\n",
|
||||||
|
++rt->gc_step_no, budget, freed, rt->gc_traced_cnt);
|
||||||
size_t wo_gc_step(wo_rt *rt, size_t budget) {
|
|
||||||
size_t freed = 0, consumed = 0;
|
|
||||||
while (consumed < budget && rt->cycbuf.len > 0) {
|
|
||||||
wo_hdr *root = rt->cycbuf.items[--rt->cycbuf.len];
|
|
||||||
root->flags &= (uint8_t)~WO_F_BUF;
|
|
||||||
consumed++;
|
|
||||||
if (root->rc == 0) {
|
|
||||||
/* died while buffered (zombie guard) — plain deterministic free */
|
|
||||||
class_free(rt, root);
|
|
||||||
freed++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
/* trial deletion over this root's component (atomic per component) */
|
|
||||||
mark_gray(rt, root);
|
|
||||||
scan_(rt, root);
|
|
||||||
whites_t w = {0};
|
|
||||||
collect_white(rt, root, &w);
|
|
||||||
if (w.oom) { /* can't track whites: restore and retry next step */
|
|
||||||
scan_black(rt, root);
|
|
||||||
free(w.items);
|
|
||||||
buf_push(rt, root);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
/* purge gathered whites still sitting in the buffer, then free —
|
|
||||||
* deferred freeing removes every dangling-candidate hazard */
|
|
||||||
for (size_t i = 0; i < w.len; i++) {
|
|
||||||
if (w.items[i]->flags & WO_F_BUF) {
|
|
||||||
w.items[i]->flags &= (uint8_t)~WO_F_BUF;
|
|
||||||
buf_remove(rt, w.items[i]);
|
|
||||||
consumed++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (size_t i = 0; i < w.len; i++) {
|
|
||||||
white_free(rt, w.items[i]);
|
|
||||||
freed++;
|
|
||||||
}
|
|
||||||
free(w.items);
|
|
||||||
}
|
|
||||||
return freed;
|
return freed;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,65 @@
|
||||||
/* gc.h — deterministic destruction + @gc reference counting (spec §4).
|
/* gc.h — deterministic destruction + incremental tri-color mark-sweep for
|
||||||
* Owned objects die deterministically via drop plans; @gc objects die at
|
* traced (inferred-gc) objects (iteration 7b, spec 2026-08-11).
|
||||||
* refcount zero. One kind-directed dispatcher is the workhorse: scalars
|
*
|
||||||
* ignored, owned values drop recursively, gc refs decrement, texts free,
|
* Two reclamation systems, one module:
|
||||||
* containers free element-wise then their backing. The budgeted cycle
|
* - OWNED values die deterministically via drop plans (wo_drop_obj /
|
||||||
* collector extends this module (Bacon–Rajan trial deletion). */
|
* 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
|
#ifndef WO_GC_H
|
||||||
#define WO_GC_H
|
#define WO_GC_H
|
||||||
|
|
||||||
#include "obj.h"
|
#include "obj.h"
|
||||||
|
|
||||||
/* Drop an 8-byte field/register value known to be of field kind `kind`.
|
/* Drop an 8-byte field/register value known to be of field kind `kind`.
|
||||||
* Null (0) values are ignored for every 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);
|
void wo_drop_kind(wo_rt *rt, uint8_t kind, uint64_t v);
|
||||||
|
|
||||||
/* Drop any heap value by its header: native sentinels route to their own
|
/* Drop any OWNED heap value by its header: native sentinels route to their
|
||||||
* frees; class objects walk their kind array over the field slots, then
|
* own frees; class objects walk their kind array over the field slots, then
|
||||||
* free themselves. */
|
* free themselves. Never called on a traced object (sweep frees those). */
|
||||||
void wo_drop_obj(wo_rt *rt, wo_hdr *o);
|
void wo_drop_obj(wo_rt *rt, wo_hdr *o);
|
||||||
|
|
||||||
void wo_rc_inc(wo_hdr *o);
|
/* Link a freshly allocated traced object onto the traced list and account
|
||||||
/* Decrement; at zero, release contents and free — unless the object sits
|
* its bytes toward the cycle trigger (called by wo_obj_new). */
|
||||||
* in the cycle-candidate buffer (WO_F_BUF): the collector owns its death. */
|
void wo_gc_track(wo_rt *rt, wo_hdr *o, size_t size);
|
||||||
void wo_rc_dec(wo_rt *rt, wo_hdr *o);
|
|
||||||
|
|
||||||
/* Budgeted cycle collection step (Bacon–Rajan trial deletion over the
|
/* Yuasa deletion barrier half: shade a traced object gray if it is still
|
||||||
* candidate buffer). Processes up to `budget` buffered roots (whole
|
* white. Also the root-shading primitive. Safe on any phase; only MARK
|
||||||
* strongly-connected components process atomically, so overshoot is
|
* callers need it. */
|
||||||
* bounded); returns how many objects it freed. */
|
void wo_gc_shade(wo_rt *rt, wo_hdr *o);
|
||||||
size_t wo_gc_step(wo_rt *rt, size_t budget);
|
|
||||||
|
/* 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 */
|
#endif /* WO_GC_H */
|
||||||
|
|
|
||||||
|
|
@ -93,40 +93,26 @@ static int load_self_embedded(wo_module *mod, char *err, size_t errlen) {
|
||||||
return rc == 0 ? 1 : -1;
|
return rc == 0 ? 1 : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- gc pump -----------------------------------------------------------
|
/* ---- gc pump (iteration 7b) ---------------------------------------------
|
||||||
* Deliberately the simplest possible driver over the plan-1 collector's
|
* After the entry method returns the stack is empty, so a collection cycle
|
||||||
* already-budgeted step interface (wo_gc_step, gc.h): after the entry
|
* has no roots: everything still on the traced list is unreachable and one
|
||||||
* method returns, drain the cycle-candidate buffer in bounded slices until
|
* cycle frees it all, in budgeted slices (WO_GC_BUDGET objects per slice —
|
||||||
* it is empty. This is post-exit-only pacing and nothing more — real
|
* rt owns the knobs now, read at init). WO_GC_TRACE prints one stderr line
|
||||||
* scheduler-integrated pacing (stepping between turns of live work while
|
* per slice (never stdout — the corpus harness diffs stdout byte-for-byte)
|
||||||
* the program keeps running) is sub-project 2's job; this milestone only
|
* so a fixture can assert "collection happened in bounded slices", not just
|
||||||
* proves the budgeted-step interface end to end and makes it observable.
|
* "the leak is gone". A mid-program cycle interrupted by exit is finished
|
||||||
* WO_GC_TRACE prints one stderr line per step (never stdout — the corpus
|
* here the same way. The zero-progress guard turns a would-be hang (a bug)
|
||||||
* harness diffs stdout byte-for-byte) so a fixture can assert "collection
|
* into a leak the ASan gate reports instead. */
|
||||||
* happened in bounded slices", not just "the leak is gone". */
|
|
||||||
static void gc_pump(wo_vm *vm) {
|
static void gc_pump(wo_vm *vm) {
|
||||||
size_t budget = 64; /* candidates per step: no prior art to size this
|
wo_rt *rt = &vm->rt;
|
||||||
against (post-exit draining is new), so picked
|
size_t at_begin = rt->gc_traced_cnt;
|
||||||
to mirror WO_HEAP_MB's default 64 — small
|
while (rt->gc_traced) {
|
||||||
enough that a deliberately oversized abandoned
|
if (rt->gc_phase == WO_GC_IDLE) {
|
||||||
structure visibly takes more than one step,
|
at_begin = rt->gc_traced_cnt;
|
||||||
large enough that ordinary programs clear in
|
wo_gc_begin(rt); /* no roots: the stack is empty at depth 0 */
|
||||||
one or two */
|
}
|
||||||
const char *benv = getenv("WO_GC_BUDGET");
|
wo_gc_slice(rt, rt->gc_budget);
|
||||||
if (benv && benv[0]) {
|
if (rt->gc_phase == WO_GC_IDLE && rt->gc_traced_cnt == at_begin) break;
|
||||||
char *end = NULL;
|
|
||||||
unsigned long v = strtoul(benv, &end, 10);
|
|
||||||
if (end && *end == '\0' && v >= 1 && v <= 1000000) budget = v;
|
|
||||||
}
|
|
||||||
int trace = getenv("WO_GC_TRACE") != NULL;
|
|
||||||
size_t step = 0;
|
|
||||||
while (vm->rt.cycbuf.len > 0) {
|
|
||||||
size_t before = vm->rt.cycbuf.len;
|
|
||||||
size_t freed = wo_gc_step(&vm->rt, budget);
|
|
||||||
step++;
|
|
||||||
if (trace)
|
|
||||||
fprintf(stderr, "gc: step %zu budget=%zu freed=%zu visited=%zu remaining=%zu\n", step,
|
|
||||||
budget, freed, before - vm->rt.cycbuf.len, vm->rt.cycbuf.len);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "gc.h" /* wo_gc_track (traced allocations), wo_drop_obj (teardown) */
|
||||||
|
|
||||||
static size_t round16(size_t n) { return (n + 15u) & ~(size_t)15u; }
|
static size_t round16(size_t n) { return (n + 15u) & ~(size_t)15u; }
|
||||||
|
|
||||||
int wo_arena_init(wo_arena *a, size_t cap) {
|
int wo_arena_init(wo_arena *a, size_t cap) {
|
||||||
|
|
@ -46,6 +48,45 @@ void wo_arena_free(wo_arena *a, void *p, size_t size) {
|
||||||
a->freelist[cls] = p;
|
a->freelist[cls] = p;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Per-class "may transitively hold a gcref" fixpoint (iteration 7b): lets
|
||||||
|
* mark skip owned subtrees that cannot reach a traced object. Field kinds
|
||||||
|
* alone identify direct gcrefs; for owned/container fields the referenced
|
||||||
|
* class comes from the v2 field_class metadata when present — absent or
|
||||||
|
* unknown answers conservatively (may = 1), which is always safe. */
|
||||||
|
static void compute_gc_may(wo_rt *rt) {
|
||||||
|
if (rt->class_cnt == 0) return;
|
||||||
|
rt->gc_may = calloc(rt->class_cnt, 1);
|
||||||
|
if (!rt->gc_may) return; /* NULL = conservative everywhere */
|
||||||
|
int changed = 1;
|
||||||
|
while (changed) {
|
||||||
|
changed = 0;
|
||||||
|
for (uint32_t c = 0; c < rt->class_cnt; c++) {
|
||||||
|
if (rt->gc_may[c]) continue;
|
||||||
|
const wo_classdesc *cd = &rt->classes[c];
|
||||||
|
int may = 0;
|
||||||
|
for (uint32_t i = 0; i < cd->field_cnt && !may; i++) {
|
||||||
|
uint8_t k = cd->kinds[i];
|
||||||
|
if (k == WO_K_GCREF) {
|
||||||
|
may = 1;
|
||||||
|
} else if (k == WO_K_OWNED || k == WO_K_MULTI || k == WO_K_MAP) {
|
||||||
|
if (k != WO_K_OWNED && cd->field_elem &&
|
||||||
|
cd->field_elem[i] == WO_K_GCREF) {
|
||||||
|
may = 1;
|
||||||
|
} else if (cd->field_class && cd->field_class[i] < rt->class_cnt) {
|
||||||
|
if (rt->gc_may[cd->field_class[i]]) may = 1;
|
||||||
|
} else {
|
||||||
|
may = 1; /* referenced class unknown: conservative */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (may) {
|
||||||
|
rt->gc_may[c] = 1;
|
||||||
|
changed = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int wo_rt_init(wo_rt *rt, size_t heap_cap, const wo_classdesc *classes,
|
int wo_rt_init(wo_rt *rt, size_t heap_cap, const wo_classdesc *classes,
|
||||||
uint32_t class_cnt) {
|
uint32_t class_cnt) {
|
||||||
memset(rt, 0, sizeof(*rt));
|
memset(rt, 0, sizeof(*rt));
|
||||||
|
|
@ -53,6 +94,26 @@ int wo_rt_init(wo_rt *rt, size_t heap_cap, const wo_classdesc *classes,
|
||||||
rt->classes = classes;
|
rt->classes = classes;
|
||||||
rt->class_cnt = class_cnt;
|
rt->class_cnt = class_cnt;
|
||||||
rt->out = stdout;
|
rt->out = stdout;
|
||||||
|
/* collector knobs (iteration 7b). WO_GC_BUDGET = objects per slice;
|
||||||
|
* WO_GC_GOAL = traced bytes that trigger a cycle; WO_GC_TRACE = one
|
||||||
|
* stderr line per slice. Defaults: budget 64 (mirrors WO_HEAP_MB's
|
||||||
|
* default; small enough that an oversized abandoned structure visibly
|
||||||
|
* takes more than one slice), goal 256 KiB. */
|
||||||
|
rt->gc_budget = 64;
|
||||||
|
rt->gc_goal = 256u << 10;
|
||||||
|
const char *e;
|
||||||
|
if ((e = getenv("WO_GC_BUDGET")) && e[0]) {
|
||||||
|
char *end = NULL;
|
||||||
|
unsigned long v = strtoul(e, &end, 10);
|
||||||
|
if (end && *end == '\0' && v >= 1 && v <= 1000000) rt->gc_budget = v;
|
||||||
|
}
|
||||||
|
if ((e = getenv("WO_GC_GOAL")) && e[0]) {
|
||||||
|
char *end = NULL;
|
||||||
|
unsigned long v = strtoul(e, &end, 10);
|
||||||
|
if (end && *end == '\0' && v >= 1) rt->gc_goal = v;
|
||||||
|
}
|
||||||
|
rt->gc_trace = getenv("WO_GC_TRACE") != NULL;
|
||||||
|
compute_gc_may(rt);
|
||||||
/* Line-buffered, always: a long-running program (the driving workload's
|
/* Line-buffered, always: a long-running program (the driving workload's
|
||||||
* `watch`/`run`/`mcp` modes) writes progress with `print`, and stdio's
|
* `watch`/`run`/`mcp` modes) writes progress with `print`, and stdio's
|
||||||
* default full buffering when stdout is a file or a pipe meant that output
|
* default full buffering when stdout is a file or a pipe meant that output
|
||||||
|
|
@ -64,7 +125,18 @@ int wo_rt_init(wo_rt *rt, size_t heap_cap, const wo_classdesc *classes,
|
||||||
}
|
}
|
||||||
|
|
||||||
void wo_rt_destroy(wo_rt *rt) {
|
void wo_rt_destroy(wo_rt *rt) {
|
||||||
free(rt->cycbuf.items);
|
/* free whatever the collector still tracks — the post-exit pump should
|
||||||
|
* have emptied the list, but a teardown after a trap or a test that
|
||||||
|
* never pumped must still be leak-free. Sweep-order free is safe: a
|
||||||
|
* traced object's gcref edges are no-ops in class_free (phase is idle),
|
||||||
|
* and its owned interior has exactly one owner — this object. */
|
||||||
|
while (rt->gc_traced) {
|
||||||
|
wo_hdr *o = rt->gc_traced;
|
||||||
|
rt->gc_traced = o->gclink;
|
||||||
|
wo_drop_obj(rt, o);
|
||||||
|
}
|
||||||
|
free(rt->gc_gray.items);
|
||||||
|
free(rt->gc_may);
|
||||||
wo_arena_destroy(&rt->arena);
|
wo_arena_destroy(&rt->arena);
|
||||||
memset(rt, 0, sizeof(*rt));
|
memset(rt, 0, sizeof(*rt));
|
||||||
}
|
}
|
||||||
|
|
@ -74,11 +146,13 @@ wo_hdr *wo_obj_new(wo_rt *rt, uint32_t class_id) {
|
||||||
size_t sz = wo_obj_size(c);
|
size_t sz = wo_obj_size(c);
|
||||||
wo_hdr *o = wo_arena_alloc(&rt->arena, sz);
|
wo_hdr *o = wo_arena_alloc(&rt->arena, sz);
|
||||||
if (!o) return NULL;
|
if (!o) return NULL;
|
||||||
memset(o, 0, sz);
|
memset(o, 0, sz); /* color: WHITE by construction (all-zero) */
|
||||||
o->class_id = class_id;
|
o->class_id = class_id;
|
||||||
if (c->flags & WO_CLASSF_GC) {
|
if (c->flags & WO_CLASSF_GC) {
|
||||||
o->flags = WO_F_GC;
|
o->flags = WO_F_GC;
|
||||||
o->rc = 1; /* the creating reference */
|
/* born black while a cycle runs: live-at-birth for that cycle */
|
||||||
|
if (rt->gc_phase != WO_GC_IDLE) o->flags |= WO_COLOR_BLACK;
|
||||||
|
wo_gc_track(rt, o, sz);
|
||||||
}
|
}
|
||||||
return o;
|
return o;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,18 +25,39 @@ void wo_arena_destroy(wo_arena *a);
|
||||||
void *wo_arena_alloc(wo_arena *a, size_t size); /* NULL = region OOM */
|
void *wo_arena_alloc(wo_arena *a, size_t size); /* NULL = region OOM */
|
||||||
void wo_arena_free(wo_arena *a, void *p, size_t size);
|
void wo_arena_free(wo_arena *a, void *p, size_t size);
|
||||||
|
|
||||||
|
/* Collector phase (iteration 7b, gc.c). IDLE -> MARK at the heap-goal
|
||||||
|
* trigger; MARK -> SWEEP when the gray worklist drains; SWEEP -> IDLE when
|
||||||
|
* the traced-list cursor reaches the end. The Yuasa deletion barrier is
|
||||||
|
* active during MARK only. */
|
||||||
|
enum { WO_GC_IDLE = 0, WO_GC_MARK = 1, WO_GC_SWEEP = 2 };
|
||||||
|
|
||||||
/* Runtime context: what every module needs. One per shard (one total in
|
/* Runtime context: what every module needs. One per shard (one total in
|
||||||
* milestone 1): the arena, the loaded class table, the cycle-candidate
|
* milestone 1): the arena, the loaded class table, the tracing collector's
|
||||||
* buffer (filled by gc.c), and the output stream builtin print writes to
|
* state (gc.c), and the output stream builtin print writes to (tests point
|
||||||
* (tests point it at a temp file to capture output). */
|
* it at a temp file to capture output). */
|
||||||
typedef struct wo_rt {
|
typedef struct wo_rt {
|
||||||
wo_arena arena;
|
wo_arena arena;
|
||||||
const wo_classdesc *classes;
|
const wo_classdesc *classes;
|
||||||
uint32_t class_cnt;
|
uint32_t class_cnt;
|
||||||
|
/* ---- tracing collector (iteration 7b) ---- */
|
||||||
|
wo_hdr *gc_traced; /* per-shard traced list: every live traced object */
|
||||||
|
size_t gc_traced_cnt; /* list length (trace/reporting only) */
|
||||||
struct {
|
struct {
|
||||||
wo_hdr **items;
|
wo_hdr **items; /* gray worklist: traced objects awaiting a scan */
|
||||||
size_t len, cap;
|
size_t len, cap;
|
||||||
} cycbuf;
|
int oom; /* worklist realloc failed: finish the cycle freeing nothing */
|
||||||
|
} gc_gray;
|
||||||
|
int gc_phase; /* WO_GC_IDLE / MARK / SWEEP */
|
||||||
|
wo_hdr **gc_sweep; /* SWEEP: link slot the cursor resumes at */
|
||||||
|
size_t gc_alloc_bytes; /* traced bytes since the last cycle (trigger) */
|
||||||
|
size_t gc_goal; /* start a cycle past this many traced bytes */
|
||||||
|
size_t gc_budget; /* objects processed per slice (WO_GC_BUDGET) */
|
||||||
|
int gc_trace; /* WO_GC_TRACE: one stderr line per slice */
|
||||||
|
size_t gc_step_no; /* slices run so far (the trace's step counter) */
|
||||||
|
uint8_t *gc_may; /* per-class "may transitively hold a gcref" bit —
|
||||||
|
lets mark skip owned subtrees that cannot reach
|
||||||
|
a traced object. Computed at init; NULL =
|
||||||
|
conservative (traverse everything). */
|
||||||
void *out; /* FILE*; kept void* so obj.h needn't pull in stdio */
|
void *out; /* FILE*; kept void* so obj.h needn't pull in stdio */
|
||||||
/* the database engine's handles (database/src), opaque here so the VM
|
/* the database engine's handles (database/src), opaque here so the VM
|
||||||
core needn't include engine headers: db = wo_db*, wal = wo_wal*.
|
core needn't include engine headers: db = wo_db*, wal = wo_wal*.
|
||||||
|
|
@ -50,8 +71,10 @@ int wo_rt_init(wo_rt *rt, size_t heap_cap, const wo_classdesc *classes,
|
||||||
uint32_t class_cnt); /* 0 ok, -1 alloc failure; out = stdout */
|
uint32_t class_cnt); /* 0 ok, -1 alloc failure; out = stdout */
|
||||||
void wo_rt_destroy(wo_rt *rt);
|
void wo_rt_destroy(wo_rt *rt);
|
||||||
|
|
||||||
/* New zeroed instance of a class-table class. @gc classes get the GC flag
|
/* New zeroed instance of a class-table class. A traced (inferred-gc) class
|
||||||
* and rc 1 (the creating reference). NULL = OOM (VM traps WO_T_OOM). */
|
* instance links itself onto the traced list, born white when the collector
|
||||||
|
* is idle and black during a cycle (live-at-birth for that cycle). NULL =
|
||||||
|
* OOM (VM traps WO_T_OOM). */
|
||||||
wo_hdr *wo_obj_new(wo_rt *rt, uint32_t class_id);
|
wo_hdr *wo_obj_new(wo_rt *rt, uint32_t class_id);
|
||||||
|
|
||||||
/* Strings: header + length + inline bytes, class id WO_CLS_STR. */
|
/* Strings: header + length + inline bytes, class id WO_CLS_STR. */
|
||||||
|
|
|
||||||
|
|
@ -53,12 +53,49 @@ static void vm_release_frame(wo_vm *vm, uint32_t d, uint32_t pc, uint32_t keep_p
|
||||||
R[r] = 0;
|
R[r] = 0;
|
||||||
}
|
}
|
||||||
if ((ent->gc & bit) && !(keep_gc & bit) && R[r]) {
|
if ((ent->gc & bit) && !(keep_gc & bit) && R[r]) {
|
||||||
wo_rc_dec(&vm->rt, (wo_hdr *)(uintptr_t)R[r]);
|
/* a traced reference dying with its frame: tracing owns the
|
||||||
|
* lifetime, and a mid-cycle root snapshot already shaded it —
|
||||||
|
* the register just goes away */
|
||||||
R[r] = 0;
|
R[r] = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- collector integration (iteration 7b) -------------------------------
|
||||||
|
* The root snapshot: shade every live frame's gc-masked registers (traced
|
||||||
|
* objects) and walk its owned-masked registers' interiors (owned values
|
||||||
|
* that may hold gcrefs). The governing drop entry per frame follows
|
||||||
|
* vm_unwind's convention — the current instruction for the innermost
|
||||||
|
* frame (the caller synced f->pc first), the CALL for outer ones. Runs
|
||||||
|
* once, atomically, when a cycle begins: bounded by the stack, not the
|
||||||
|
* heap. */
|
||||||
|
static void vm_gc_roots(wo_vm *vm) {
|
||||||
|
for (uint32_t d = vm->depth; d > 0; d--) {
|
||||||
|
const wo_frame *f = &vm->frames[d - 1];
|
||||||
|
const wo_methodrec *me = &vm->mod->methods[f->method];
|
||||||
|
uint32_t gpc = (d == vm->depth) ? f->pc : f->pc - 1;
|
||||||
|
const wo_dropent *ent = vm_dropent(me, gpc);
|
||||||
|
if (!ent) continue;
|
||||||
|
const uint64_t *R = vm->regs + f->base;
|
||||||
|
for (uint32_t r = 0; r < me->reg_cnt; r++) {
|
||||||
|
uint64_t bit = 1ull << r;
|
||||||
|
if (((ent->gc | ent->owned) & bit) && R[r])
|
||||||
|
wo_gc_scan_root(&vm->rt, (wo_hdr *)(uintptr_t)R[r]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* One safepoint: start a cycle when the trigger says so (snapshot the
|
||||||
|
* roots before the mutator resumes), then run one budgeted slice while a
|
||||||
|
* cycle is live. The caller synced the innermost frame's pc first. */
|
||||||
|
static void vm_gc_safepoint(wo_vm *vm) {
|
||||||
|
if (wo_gc_want_start(&vm->rt)) {
|
||||||
|
wo_gc_begin(&vm->rt);
|
||||||
|
vm_gc_roots(vm);
|
||||||
|
}
|
||||||
|
if (vm->rt.gc_phase != WO_GC_IDLE) wo_gc_slice(&vm->rt, vm->rt.gc_budget);
|
||||||
|
}
|
||||||
|
|
||||||
/* Trap unwinding — the spec's "traps never leak" promise (spec §6). Walk
|
/* Trap unwinding — the spec's "traps never leak" promise (spec §6). Walk
|
||||||
* frames innermost to outermost down to (not including) [stop_depth];
|
* frames innermost to outermost down to (not including) [stop_depth];
|
||||||
* in each, the governing instruction is the trap pc for the innermost
|
* in each, the governing instruction is the trap pc for the innermost
|
||||||
|
|
@ -195,6 +232,18 @@ static int vm_run(wo_vm *vm, uint64_t *ret, wo_err *err) {
|
||||||
return -1; \
|
return -1; \
|
||||||
} while (0)
|
} while (0)
|
||||||
|
|
||||||
|
/* Collector safepoint (iteration 7b): placed at allocations, calls, and
|
||||||
|
* loop back-edges — the pcs that already carry drop-table entries, so the
|
||||||
|
* root snapshot's masks are exact. Costs one predictable branch when the
|
||||||
|
* collector is idle and the trigger is cold. */
|
||||||
|
#define GC_SAFEPOINT() \
|
||||||
|
do { \
|
||||||
|
if (vm->rt.gc_phase != WO_GC_IDLE || wo_gc_want_start(&vm->rt)) { \
|
||||||
|
vm->frames[vm->depth - 1].pc = pc - 1; \
|
||||||
|
vm_gc_safepoint(vm); \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
RELOAD();
|
RELOAD();
|
||||||
|
|
||||||
/* dual-flavor dispatch, one shared case-body text (spec §5): computed
|
/* dual-flavor dispatch, one shared case-body text (spec §5): computed
|
||||||
|
|
@ -293,6 +342,7 @@ dispatch:
|
||||||
}
|
}
|
||||||
|
|
||||||
CASE(JMP) : {
|
CASE(JMP) : {
|
||||||
|
if (wo_ins_sbx(ins) < 0) GC_SAFEPOINT(); /* loop back-edge */
|
||||||
pc = (uint32_t)((int64_t)pc + wo_ins_sbx(ins));
|
pc = (uint32_t)((int64_t)pc + wo_ins_sbx(ins));
|
||||||
NEXT();
|
NEXT();
|
||||||
}
|
}
|
||||||
|
|
@ -303,6 +353,7 @@ dispatch:
|
||||||
}
|
}
|
||||||
|
|
||||||
CASE(CALL) : {
|
CASE(CALL) : {
|
||||||
|
GC_SAFEPOINT();
|
||||||
/* Lua-style window overlap: callee r0 = caller slot A; args sit at
|
/* Lua-style window overlap: callee r0 = caller slot A; args sit at
|
||||||
* A..A+argc-1; the return value lands back in slot A */
|
* A..A+argc-1; the return value lands back in slot A */
|
||||||
const wo_methodrec *callee = &mod->methods[wo_ins_bx(ins)];
|
const wo_methodrec *callee = &mod->methods[wo_ins_bx(ins)];
|
||||||
|
|
@ -356,6 +407,7 @@ dispatch:
|
||||||
}
|
}
|
||||||
|
|
||||||
CASE(NEW) : {
|
CASE(NEW) : {
|
||||||
|
GC_SAFEPOINT(); /* allocation is the trigger's natural home */
|
||||||
wo_hdr *o = wo_obj_new(&vm->rt, wo_ins_bx(ins));
|
wo_hdr *o = wo_obj_new(&vm->rt, wo_ins_bx(ins));
|
||||||
if (!o) TRAPF(WO_T_OOM, "out of memory");
|
if (!o) TRAPF(WO_T_OOM, "out of memory");
|
||||||
R[wo_ins_a(ins)] = (uint64_t)(uintptr_t)o;
|
R[wo_ins_a(ins)] = (uint64_t)(uintptr_t)o;
|
||||||
|
|
@ -385,6 +437,15 @@ dispatch:
|
||||||
wo_hdr *o = recv_check(vm, R[wo_ins_a(ins)], wo_ins_b(ins), &why);
|
wo_hdr *o = recv_check(vm, R[wo_ins_a(ins)], wo_ins_b(ins), &why);
|
||||||
if (!o) TRAPF(WO_T_BOUNDS, "%s", why);
|
if (!o) TRAPF(WO_T_BOUNDS, "%s", why);
|
||||||
uint64_t v = R[wo_ins_c(ins)];
|
uint64_t v = R[wo_ins_c(ins)];
|
||||||
|
/* Yuasa deletion barrier (iteration 7b): overwriting a gcref slot
|
||||||
|
* while marking deletes an edge the snapshot may depend on — shade
|
||||||
|
* the OLD target before the store. Inactive outside marking; owned
|
||||||
|
* fields, scalars and text pay nothing. */
|
||||||
|
if (vm->rt.gc_phase == WO_GC_MARK &&
|
||||||
|
vm->mod->classes[o->class_id].kinds[wo_ins_b(ins)] == WO_K_GCREF) {
|
||||||
|
uint64_t old = wo_fields(o)[wo_ins_b(ins)];
|
||||||
|
if (old) wo_gc_shade(&vm->rt, (wo_hdr *)(uintptr_t)old);
|
||||||
|
}
|
||||||
if (v && vm->mod->classes[o->class_id].kinds[wo_ins_b(ins)] == WO_K_TEXT) {
|
if (v && vm->mod->classes[o->class_id].kinds[wo_ins_b(ins)] == WO_K_TEXT) {
|
||||||
const wo_str *src = (const wo_str *)(uintptr_t)v;
|
const wo_str *src = (const wo_str *)(uintptr_t)v;
|
||||||
if (src->h.class_id != WO_CLS_STR) TRAPF(WO_T_BOUNDS, "not a text value");
|
if (src->h.class_id != WO_CLS_STR) TRAPF(WO_T_BOUNDS, "not a text value");
|
||||||
|
|
@ -427,18 +488,13 @@ dispatch:
|
||||||
NEXT();
|
NEXT();
|
||||||
}
|
}
|
||||||
|
|
||||||
CASE(RC_INC) : {
|
/* iteration 7b: reference counting is retired — tracing owns traced
|
||||||
uint64_t v = R[wo_ins_a(ins)];
|
* lifetimes, so alias bookkeeping means nothing. The opcodes stay
|
||||||
if (!v) TRAPF(WO_T_BOUNDS, "null receiver");
|
* accepted as no-ops until the emitter stops producing them and the
|
||||||
wo_rc_inc((wo_hdr *)(uintptr_t)v);
|
* format reserves 27–28 (the .wob version bump); a no-op is also what
|
||||||
NEXT();
|
* deletes the old RC_DEC-on-nil trap that broke `?Node` field stores. */
|
||||||
}
|
CASE(RC_INC) : NEXT();
|
||||||
CASE(RC_DEC) : {
|
CASE(RC_DEC) : NEXT();
|
||||||
uint64_t v = R[wo_ins_a(ins)];
|
|
||||||
if (!v) TRAPF(WO_T_BOUNDS, "null receiver");
|
|
||||||
wo_rc_dec(&vm->rt, (wo_hdr *)(uintptr_t)v);
|
|
||||||
NEXT();
|
|
||||||
}
|
|
||||||
|
|
||||||
CASE(CONCAT) : {
|
CASE(CONCAT) : {
|
||||||
const char *why;
|
const char *why;
|
||||||
|
|
@ -570,6 +626,7 @@ dispatch:
|
||||||
#undef NEXT
|
#undef NEXT
|
||||||
#undef RELOAD
|
#undef RELOAD
|
||||||
#undef TRAPF
|
#undef TRAPF
|
||||||
|
#undef GC_SAFEPOINT
|
||||||
#undef DROP_CATCHES
|
#undef DROP_CATCHES
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,26 +75,35 @@ enum {
|
||||||
#define WO_STACK_SLOTS 4096u
|
#define WO_STACK_SLOTS 4096u
|
||||||
#define WO_MAX_FRAMES 256u
|
#define WO_MAX_FRAMES 256u
|
||||||
|
|
||||||
/* ---- object header: every heap value carries this (spec section 4) ---- */
|
/* ---- object header: every heap value carries this (spec section 4;
|
||||||
typedef struct wo_hdr {
|
* iteration 7b rewrote the second half). Retiring `rc` freed four bytes,
|
||||||
|
* and traced objects are exempt from borrow rules so their borrow word is
|
||||||
|
* dead too — those two adjacent words are one union: non-traced values use
|
||||||
|
* the borrow word, traced objects use the 8 bytes as the intrusive
|
||||||
|
* sweep-list link. The header stays exactly 16 bytes. ---- */
|
||||||
|
typedef struct wo_hdr wo_hdr;
|
||||||
|
struct wo_hdr {
|
||||||
uint32_t class_id; /* class-table index or a WO_CLS_* sentinel */
|
uint32_t class_id; /* class-table index or a WO_CLS_* sentinel */
|
||||||
uint16_t shard_id; /* always 0 in milestone 1; reserved for sub-project 2 */
|
uint16_t shard_id; /* always 0 in milestone 1; reserved for sub-project 2 */
|
||||||
uint8_t flags;
|
uint8_t flags;
|
||||||
uint8_t pad;
|
uint8_t pad;
|
||||||
uint32_t borrow; /* WO_BORROW_FREE / reader count / WO_BORROW_EXCL */
|
union {
|
||||||
uint32_t rc; /* strong count, @gc objects only */
|
uint32_t borrow; /* non-traced: WO_BORROW_FREE / readers / EXCL */
|
||||||
} wo_hdr;
|
wo_hdr *gclink; /* traced: next object on the per-shard traced list */
|
||||||
|
};
|
||||||
|
};
|
||||||
_Static_assert(sizeof(wo_hdr) == 16, "object header must be exactly 16 bytes");
|
_Static_assert(sizeof(wo_hdr) == 16, "object header must be exactly 16 bytes");
|
||||||
|
|
||||||
/* header flags */
|
/* header flags */
|
||||||
#define WO_F_GC 0x01u /* instance of a @gc class: rc rules apply */
|
#define WO_F_GC 0x01u /* instance of a traced (inferred-gc) class */
|
||||||
#define WO_F_BUF 0x02u /* sitting in the cycle-candidate buffer */
|
|
||||||
#define WO_F_CONST 0x04u /* loader-interned constant (strings): free is a no-op */
|
#define WO_F_CONST 0x04u /* loader-interned constant (strings): free is a no-op */
|
||||||
/* two color bits for Bacon–Rajan trial deletion */
|
/* two color bits for tri-color incremental mark-sweep (iteration 7b).
|
||||||
|
* WHITE must be the all-zero value: wo_obj_new memsets the object, so an
|
||||||
|
* allocation outside a marking cycle is born white by construction. */
|
||||||
#define WO_F_COLOR 0x18u
|
#define WO_F_COLOR 0x18u
|
||||||
#define WO_COLOR_BLACK 0x00u
|
#define WO_COLOR_WHITE 0x00u
|
||||||
#define WO_COLOR_GRAY 0x08u
|
#define WO_COLOR_GRAY 0x08u
|
||||||
#define WO_COLOR_WHITE 0x10u
|
#define WO_COLOR_BLACK 0x10u
|
||||||
|
|
||||||
/* native class-id sentinels (top of the u32 range; loader rejects user
|
/* native class-id sentinels (top of the u32 range; loader rejects user
|
||||||
* class counts anywhere near these) */
|
* class counts anywhere near these) */
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
/* test_cycle — budgeted Bacon–Rajan cycle collection.
|
/* test_cycle — incremental tri-color mark-sweep (iteration 7b).
|
||||||
* Cycle classes use the malloc-path trick (~130 fields) so ASan proves
|
* Cycle classes use the malloc-path trick (~130 fields) so ASan proves
|
||||||
* every free. Budget semantics: a step consumes candidates from the buffer
|
* every free. Assertions per the 7b spec: an abandoned cycle is freed, a
|
||||||
* (roots popped + whites purged) up to `budget`, whole components atomic. */
|
* rooted cycle survives, slices are bounded (budget), the traced list is
|
||||||
|
* leak-free after repeated cycles, and — the load-bearing one — the Yuasa
|
||||||
|
* deletion barrier keeps an object alive when the mutator hides it behind
|
||||||
|
* an already-black object between marking slices. */
|
||||||
#include "cont.h"
|
#include "cont.h"
|
||||||
#include "gc.h"
|
#include "gc.h"
|
||||||
#include "t.h"
|
#include "t.h"
|
||||||
|
|
||||||
#define BIG 130
|
#define BIG 130
|
||||||
|
|
||||||
/* @gc class "GNode": field0 GCREF, field1 MULTI, rest scalars */
|
/* traced class "GNode": field0 GCREF, field1 MULTI, rest scalars */
|
||||||
static uint8_t gnode_kinds[BIG];
|
static uint8_t gnode_kinds[BIG];
|
||||||
static wo_classdesc CLASSES[1];
|
static wo_classdesc CLASSES[1];
|
||||||
|
|
||||||
|
|
@ -19,75 +22,70 @@ static void setup(void) {
|
||||||
.name = 0, .flags = WO_CLASSF_GC, .field_cnt = BIG, .kinds = gnode_kinds};
|
.name = 0, .flags = WO_CLASSF_GC, .field_cnt = BIG, .kinds = gnode_kinds};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* helper: link a->f0 = b, taking a reference on b */
|
/* run a whole cycle: shade the given roots, then budgeted slices to idle */
|
||||||
static void link(wo_hdr *a, wo_hdr *b) {
|
static size_t run_cycle(wo_rt *rt, wo_hdr **roots, size_t nroots, size_t budget) {
|
||||||
wo_fields(a)[0] = (uint64_t)(uintptr_t)b;
|
size_t freed = 0;
|
||||||
wo_rc_inc(b);
|
wo_gc_begin(rt);
|
||||||
|
for (size_t i = 0; i < nroots; i++) wo_gc_shade(rt, roots[i]);
|
||||||
|
while (rt->gc_phase != WO_GC_IDLE) freed += wo_gc_slice(rt, budget);
|
||||||
|
return freed;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_two_object_cycle_collects(void) {
|
/* an abandoned two-object cycle is unreachable and collects whole */
|
||||||
|
static void test_abandoned_cycle_collects(void) {
|
||||||
wo_rt rt;
|
wo_rt rt;
|
||||||
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 1), 0);
|
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 1), 0);
|
||||||
wo_hdr *a = wo_obj_new(&rt, 0);
|
wo_hdr *a = wo_obj_new(&rt, 0);
|
||||||
wo_hdr *b = wo_obj_new(&rt, 0);
|
wo_hdr *b = wo_obj_new(&rt, 0);
|
||||||
link(a, b);
|
wo_fields(a)[0] = (uint64_t)(uintptr_t)b;
|
||||||
link(b, a);
|
wo_fields(b)[0] = (uint64_t)(uintptr_t)a; /* a <-> b, no root */
|
||||||
/* drop both external handles: objects survive on cycle edges alone */
|
T_EQ(rt.gc_traced_cnt, 2);
|
||||||
wo_rc_dec(&rt, a);
|
T_EQ(run_cycle(&rt, NULL, 0, 16), 2);
|
||||||
wo_rc_dec(&rt, b);
|
T_EQ(rt.gc_traced_cnt, 0);
|
||||||
T_EQ(rt.cycbuf.len, 2); /* both buffered as candidates */
|
/* nothing left: a further cycle frees nothing */
|
||||||
T_EQ(wo_gc_step(&rt, 16), 2); /* whole cycle freed (ASan proves it) */
|
T_EQ(run_cycle(&rt, NULL, 0, 16), 0);
|
||||||
T_EQ(rt.cycbuf.len, 0);
|
|
||||||
wo_rt_destroy(&rt);
|
wo_rt_destroy(&rt);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_budget_one_cycle_per_step(void) {
|
/* a rooted cycle survives every cycle that names it a root, then dies the
|
||||||
|
* moment it is abandoned */
|
||||||
|
static void test_rooted_cycle_survives(void) {
|
||||||
wo_rt rt;
|
wo_rt rt;
|
||||||
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 1), 0);
|
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 1), 0);
|
||||||
wo_hdr *a = wo_obj_new(&rt, 0), *b = wo_obj_new(&rt, 0);
|
wo_hdr *a = wo_obj_new(&rt, 0), *b = wo_obj_new(&rt, 0);
|
||||||
wo_hdr *c = wo_obj_new(&rt, 0), *d = wo_obj_new(&rt, 0);
|
wo_fields(a)[0] = (uint64_t)(uintptr_t)b;
|
||||||
link(a, b);
|
wo_fields(b)[0] = (uint64_t)(uintptr_t)a;
|
||||||
link(b, a);
|
T_EQ(run_cycle(&rt, &a, 1, 16), 0); /* rooted: survives */
|
||||||
link(c, d);
|
T_EQ(rt.gc_traced_cnt, 2);
|
||||||
link(d, c);
|
T_EQ(run_cycle(&rt, &a, 1, 16), 0); /* survives repeated cycles */
|
||||||
wo_rc_dec(&rt, a);
|
T_EQ(rt.gc_traced_cnt, 2);
|
||||||
wo_rc_dec(&rt, b);
|
T_EQ(run_cycle(&rt, NULL, 0, 16), 2); /* abandoned: dies */
|
||||||
wo_rc_dec(&rt, c);
|
|
||||||
wo_rc_dec(&rt, d);
|
|
||||||
T_EQ(rt.cycbuf.len, 4);
|
|
||||||
/* budget 2 = one two-object component per step */
|
|
||||||
T_EQ(wo_gc_step(&rt, 2), 2);
|
|
||||||
T_EQ(rt.cycbuf.len, 2);
|
|
||||||
T_EQ(wo_gc_step(&rt, 2), 2);
|
|
||||||
T_EQ(rt.cycbuf.len, 0);
|
|
||||||
/* nothing left: a further step frees nothing */
|
|
||||||
T_EQ(wo_gc_step(&rt, 2), 0);
|
|
||||||
wo_rt_destroy(&rt);
|
wo_rt_destroy(&rt);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_externally_held_cycle_survives_then_dies(void) {
|
/* budget = objects per slice: four dead nodes at budget 2 need two sweep
|
||||||
|
* slices; each slice frees at most the budget */
|
||||||
|
static void test_budgeted_slices_bounded(void) {
|
||||||
wo_rt rt;
|
wo_rt rt;
|
||||||
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 1), 0);
|
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 1), 0);
|
||||||
wo_hdr *a = wo_obj_new(&rt, 0), *b = wo_obj_new(&rt, 0);
|
wo_hdr *n[4];
|
||||||
link(a, b);
|
for (int i = 0; i < 4; i++) n[i] = wo_obj_new(&rt, 0);
|
||||||
link(b, a);
|
wo_fields(n[0])[0] = (uint64_t)(uintptr_t)n[1];
|
||||||
/* keep the external handle on a; drop only b's */
|
wo_fields(n[1])[0] = (uint64_t)(uintptr_t)n[0];
|
||||||
wo_rc_dec(&rt, b);
|
wo_fields(n[2])[0] = (uint64_t)(uintptr_t)n[3];
|
||||||
T_EQ(rt.cycbuf.len, 1);
|
wo_fields(n[3])[0] = (uint64_t)(uintptr_t)n[2];
|
||||||
T_EQ(wo_gc_step(&rt, 16), 0); /* held from outside: survives */
|
wo_gc_begin(&rt);
|
||||||
T_EQ(rt.cycbuf.len, 0); /* candidate consumed, flag cleared */
|
size_t f1 = wo_gc_slice(&rt, 2); /* mark drains (nothing gray) + sweep 2 */
|
||||||
/* counts fully restored */
|
T_EQ(f1, 2);
|
||||||
T_EQ(a->rc, 2);
|
T_EQ(rt.gc_traced_cnt, 2);
|
||||||
T_EQ(b->rc, 1);
|
size_t f2 = wo_gc_slice(&rt, 2);
|
||||||
T_EQ(a->flags & (WO_F_BUF | WO_F_COLOR), 0);
|
T_EQ(f2, 2);
|
||||||
T_EQ(b->flags & (WO_F_BUF | WO_F_COLOR), 0);
|
T_EQ(rt.gc_traced_cnt, 0);
|
||||||
/* still usable, then truly dead */
|
T_EQ(rt.gc_phase, WO_GC_IDLE);
|
||||||
wo_rc_dec(&rt, a);
|
|
||||||
T_EQ(rt.cycbuf.len, 1); /* re-buffered on the last external decrement */
|
|
||||||
T_EQ(wo_gc_step(&rt, 16), 2);
|
|
||||||
wo_rt_destroy(&rt);
|
wo_rt_destroy(&rt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* a cycle closed through a multi's gcref elements collects with its nodes */
|
||||||
static void test_cycle_through_multi_elements(void) {
|
static void test_cycle_through_multi_elements(void) {
|
||||||
wo_rt rt;
|
wo_rt rt;
|
||||||
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 1), 0);
|
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 1), 0);
|
||||||
|
|
@ -95,21 +93,77 @@ static void test_cycle_through_multi_elements(void) {
|
||||||
/* a --(multi element)--> b --(gcref field)--> a */
|
/* a --(multi element)--> b --(gcref field)--> a */
|
||||||
wo_multi *m = wo_multi_new(&rt, WO_K_GCREF);
|
wo_multi *m = wo_multi_new(&rt, WO_K_GCREF);
|
||||||
T_EQ(wo_multi_push(m, (uint64_t)(uintptr_t)b), 0);
|
T_EQ(wo_multi_push(m, (uint64_t)(uintptr_t)b), 0);
|
||||||
wo_rc_inc(b);
|
|
||||||
wo_fields(a)[1] = (uint64_t)(uintptr_t)m;
|
wo_fields(a)[1] = (uint64_t)(uintptr_t)m;
|
||||||
link(b, a);
|
wo_fields(b)[0] = (uint64_t)(uintptr_t)a;
|
||||||
wo_rc_dec(&rt, a);
|
T_EQ(run_cycle(&rt, NULL, 0, 16), 2); /* multi head + backing freed with a */
|
||||||
wo_rc_dec(&rt, b);
|
T_EQ(rt.gc_traced_cnt, 0);
|
||||||
T_EQ(wo_gc_step(&rt, 16), 2); /* multi head + backing freed with a */
|
wo_rt_destroy(&rt);
|
||||||
T_EQ(rt.cycbuf.len, 0);
|
}
|
||||||
|
|
||||||
|
/* THE BARRIER TEST (spec §7, the design's safety net). Mid-mark, the
|
||||||
|
* mutator hides a live object: it deletes the only still-white edge to
|
||||||
|
* `victim` after the object holding it was already scanned black. Without
|
||||||
|
* the deletion barrier the victim is swept while reachable — silent
|
||||||
|
* corruption. With it, the delete shades the victim first.
|
||||||
|
*
|
||||||
|
* root -> holder -> victim (holder scanned black in slice 1)
|
||||||
|
* mutator: root.f0 = victim; holder.f0 deleted <- barrier shades victim
|
||||||
|
* remaining slices must NOT free victim.
|
||||||
|
*/
|
||||||
|
static void test_deletion_barrier_keeps_hidden_object(void) {
|
||||||
|
wo_rt rt;
|
||||||
|
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 1), 0);
|
||||||
|
wo_hdr *root = wo_obj_new(&rt, 0);
|
||||||
|
wo_hdr *holder = wo_obj_new(&rt, 0);
|
||||||
|
wo_hdr *victim = wo_obj_new(&rt, 0);
|
||||||
|
wo_fields(root)[0] = (uint64_t)(uintptr_t)holder;
|
||||||
|
wo_fields(holder)[0] = (uint64_t)(uintptr_t)victim;
|
||||||
|
|
||||||
|
wo_gc_begin(&rt);
|
||||||
|
wo_gc_shade(&rt, root);
|
||||||
|
/* slice 1, budget 1: scans root (blackens it, shades holder) */
|
||||||
|
(void)wo_gc_slice(&rt, 1);
|
||||||
|
/* slice 2, budget 1: scans holder (blackens it, shades victim)? No —
|
||||||
|
* order the hide BEFORE holder's scan would shade victim: rewire now,
|
||||||
|
* while holder is still gray but victim is white and only holder-held. */
|
||||||
|
wo_fields(root)[0] = (uint64_t)(uintptr_t)victim; /* hide behind BLACK root */
|
||||||
|
/* delete holder's edge — the mutator's overwrite; the store path's
|
||||||
|
* barrier is wo_drop_kind on the old value */
|
||||||
|
wo_drop_kind(&rt, WO_K_GCREF, wo_fields(holder)[0]); /* shades victim */
|
||||||
|
wo_fields(holder)[0] = 0;
|
||||||
|
/* finish the cycle */
|
||||||
|
while (rt.gc_phase != WO_GC_IDLE) (void)wo_gc_slice(&rt, 1);
|
||||||
|
/* victim survived: still on the traced list, still readable */
|
||||||
|
T_EQ(rt.gc_traced_cnt, 3);
|
||||||
|
T_EQ(wo_fields(root)[0], (uint64_t)(uintptr_t)victim);
|
||||||
|
/* abandon everything: next cycle frees all three */
|
||||||
|
T_EQ(run_cycle(&rt, NULL, 0, 16), 3);
|
||||||
|
wo_rt_destroy(&rt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* leak-freedom across repeated cycles: allocate, abandon, collect, N times;
|
||||||
|
* the traced list must end empty every round (ASan proves the frees) */
|
||||||
|
static void test_repeated_cycles_leak_free(void) {
|
||||||
|
wo_rt rt;
|
||||||
|
T_EQ(wo_rt_init(&rt, 1 << 18, CLASSES, 1), 0);
|
||||||
|
for (int round = 0; round < 8; round++) {
|
||||||
|
wo_hdr *a = wo_obj_new(&rt, 0), *b = wo_obj_new(&rt, 0), *c = wo_obj_new(&rt, 0);
|
||||||
|
wo_fields(a)[0] = (uint64_t)(uintptr_t)b;
|
||||||
|
wo_fields(b)[0] = (uint64_t)(uintptr_t)c;
|
||||||
|
wo_fields(c)[0] = (uint64_t)(uintptr_t)a;
|
||||||
|
T_EQ(run_cycle(&rt, NULL, 0, 2), 3);
|
||||||
|
T_EQ(rt.gc_traced_cnt, 0);
|
||||||
|
}
|
||||||
wo_rt_destroy(&rt);
|
wo_rt_destroy(&rt);
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
setup();
|
setup();
|
||||||
test_two_object_cycle_collects();
|
test_abandoned_cycle_collects();
|
||||||
test_budget_one_cycle_per_step();
|
test_rooted_cycle_survives();
|
||||||
test_externally_held_cycle_survives_then_dies();
|
test_budgeted_slices_bounded();
|
||||||
test_cycle_through_multi_elements();
|
test_cycle_through_multi_elements();
|
||||||
|
test_deletion_barrier_keeps_hidden_object();
|
||||||
|
test_repeated_cycles_leak_free();
|
||||||
return t_report("test_cycle");
|
return t_report("test_cycle");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,21 +18,24 @@ static void test_owned_object_zeroed(void) {
|
||||||
T_EQ(o->class_id, 0);
|
T_EQ(o->class_id, 0);
|
||||||
T_EQ(o->flags, 0);
|
T_EQ(o->flags, 0);
|
||||||
T_EQ(o->borrow, WO_BORROW_FREE);
|
T_EQ(o->borrow, WO_BORROW_FREE);
|
||||||
T_EQ(o->rc, 0);
|
|
||||||
T_EQ(wo_fields(o)[0], 0);
|
T_EQ(wo_fields(o)[0], 0);
|
||||||
T_EQ(wo_fields(o)[1], 0);
|
T_EQ(wo_fields(o)[1], 0);
|
||||||
wo_arena_free(&rt.arena, o, wo_obj_size(&CLASSES[0]));
|
wo_arena_free(&rt.arena, o, wo_obj_size(&CLASSES[0]));
|
||||||
wo_rt_destroy(&rt);
|
wo_rt_destroy(&rt);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_gc_object_rc1(void) {
|
/* iteration 7b: a traced-class instance carries the GC flag, is born white
|
||||||
|
* (collector idle), and links itself onto the traced list — rt_destroy is
|
||||||
|
* what frees it, so no manual arena_free here. */
|
||||||
|
static void test_gc_object_tracked(void) {
|
||||||
wo_rt rt;
|
wo_rt rt;
|
||||||
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 2), 0);
|
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 2), 0);
|
||||||
wo_hdr *o = wo_obj_new(&rt, 1);
|
wo_hdr *o = wo_obj_new(&rt, 1);
|
||||||
T_CHECK(o != NULL);
|
T_CHECK(o != NULL);
|
||||||
T_CHECK(o->flags & WO_F_GC);
|
T_CHECK(o->flags & WO_F_GC);
|
||||||
T_EQ(o->rc, 1);
|
T_EQ(o->flags & WO_F_COLOR, WO_COLOR_WHITE);
|
||||||
wo_arena_free(&rt.arena, o, wo_obj_size(&CLASSES[1]));
|
T_EQ(rt.gc_traced, o);
|
||||||
|
T_EQ(rt.gc_traced_cnt, 1);
|
||||||
wo_rt_destroy(&rt);
|
wo_rt_destroy(&rt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,7 +81,7 @@ static void test_const_string_survives_free(void) {
|
||||||
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
test_owned_object_zeroed();
|
test_owned_object_zeroed();
|
||||||
test_gc_object_rc1();
|
test_gc_object_tracked();
|
||||||
test_strings();
|
test_strings();
|
||||||
test_const_string_survives_free();
|
test_const_string_survives_free();
|
||||||
return t_report("test_obj");
|
return t_report("test_obj");
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
/* test_rc — RC + drop plans: deterministic destruction.
|
/* test_rc — deterministic drops + the owned/traced boundary (iteration 7b:
|
||||||
|
* reference counting is gone; this suite now pins what replaced it on the
|
||||||
|
* owned side, and that owned deaths never free traced objects).
|
||||||
*
|
*
|
||||||
* Testing trick used by every memory test from here on: classes get ~130
|
* Testing trick used by every memory test from here on: classes get ~130
|
||||||
* fields so instances exceed the 1024-byte size-class ceiling and take the
|
* fields so instances exceed the 1024-byte size-class ceiling and take the
|
||||||
|
|
@ -11,7 +13,7 @@
|
||||||
|
|
||||||
/* class 0 "Node": field0 OWNED (child Node), field1 TEXT, rest scalars */
|
/* class 0 "Node": field0 OWNED (child Node), field1 TEXT, rest scalars */
|
||||||
static uint8_t node_kinds[BIG];
|
static uint8_t node_kinds[BIG];
|
||||||
/* class 1 "Shared" (@gc): all scalars */
|
/* class 1 "Shared" (traced): all scalars */
|
||||||
static uint8_t shared_kinds[BIG];
|
static uint8_t shared_kinds[BIG];
|
||||||
/* class 2 "Holder": field0 GCREF, field1 MULTI (of TEXT), rest scalars */
|
/* class 2 "Holder": field0 GCREF, field1 MULTI (of TEXT), rest scalars */
|
||||||
static uint8_t holder_kinds[BIG];
|
static uint8_t holder_kinds[BIG];
|
||||||
|
|
@ -48,19 +50,23 @@ static void test_owned_tree_recursive_drop(void) {
|
||||||
wo_rt_destroy(&rt);
|
wo_rt_destroy(&rt);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* A holder's gcref field decrements on drop; the final external decrement
|
/* An owned holder dying must NOT free the traced object its gcref field
|
||||||
* frees the @gc object. */
|
* points at — tracing owns that lifetime. The object stays on the traced
|
||||||
static void test_gcref_field_decrements(void) {
|
* list; a rootless cycle then frees it (and rt_destroy would too). */
|
||||||
|
static void test_holder_death_leaves_traced_alive(void) {
|
||||||
wo_rt rt;
|
wo_rt rt;
|
||||||
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 3), 0);
|
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 3), 0);
|
||||||
wo_hdr *shared = wo_obj_new(&rt, 1); /* rc = 1 (creating ref) */
|
wo_hdr *shared = wo_obj_new(&rt, 1);
|
||||||
wo_rc_inc(shared); /* holder's reference */
|
T_EQ(rt.gc_traced_cnt, 1);
|
||||||
T_EQ(shared->rc, 2);
|
|
||||||
wo_hdr *holder = wo_obj_new(&rt, 2);
|
wo_hdr *holder = wo_obj_new(&rt, 2);
|
||||||
wo_fields(holder)[0] = (uint64_t)(uintptr_t)shared;
|
wo_fields(holder)[0] = (uint64_t)(uintptr_t)shared;
|
||||||
wo_drop_obj(&rt, holder); /* drops holder, decrements shared to 1 */
|
wo_drop_obj(&rt, holder); /* gcref edge is a no-op: shared survives */
|
||||||
T_EQ(shared->rc, 1);
|
T_EQ(rt.gc_traced_cnt, 1);
|
||||||
wo_rc_dec(&rt, shared); /* final ref gone -> freed (ASan-proven) */
|
T_EQ(rt.gc_traced, shared);
|
||||||
|
/* one rootless cycle reclaims it */
|
||||||
|
wo_gc_begin(&rt);
|
||||||
|
while (rt.gc_phase != WO_GC_IDLE) wo_gc_slice(&rt, 16);
|
||||||
|
T_EQ(rt.gc_traced_cnt, 0);
|
||||||
wo_rt_destroy(&rt);
|
wo_rt_destroy(&rt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,26 +84,24 @@ static void test_container_fields_freed_with_holder(void) {
|
||||||
wo_rt_destroy(&rt);
|
wo_rt_destroy(&rt);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* rc_inc/rc_dec pairing frees exactly at zero. */
|
/* Teardown safety net: traced objects still on the list when the runtime
|
||||||
static void test_rc_zero_frees(void) {
|
* dies are freed by rt_destroy itself (a test or a trap path that never
|
||||||
|
* pumped must still be leak-free — ASan proves the malloc-path frees). */
|
||||||
|
static void test_rt_destroy_frees_traced_remnants(void) {
|
||||||
wo_rt rt;
|
wo_rt rt;
|
||||||
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 3), 0);
|
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 3), 0);
|
||||||
wo_hdr *s = wo_obj_new(&rt, 1);
|
(void)wo_obj_new(&rt, 1);
|
||||||
wo_rc_inc(s);
|
(void)wo_obj_new(&rt, 1);
|
||||||
wo_rc_inc(s);
|
T_EQ(rt.gc_traced_cnt, 2);
|
||||||
T_EQ(s->rc, 3);
|
wo_rt_destroy(&rt); /* frees both (ASan-proven) */
|
||||||
wo_rc_dec(&rt, s);
|
T_CHECK(1);
|
||||||
wo_rc_dec(&rt, s);
|
|
||||||
T_EQ(s->rc, 1);
|
|
||||||
wo_rc_dec(&rt, s); /* freed here */
|
|
||||||
wo_rt_destroy(&rt);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
setup_classes();
|
setup_classes();
|
||||||
test_owned_tree_recursive_drop();
|
test_owned_tree_recursive_drop();
|
||||||
test_gcref_field_decrements();
|
test_holder_death_leaves_traced_alive();
|
||||||
test_container_fields_freed_with_holder();
|
test_container_fields_freed_with_holder();
|
||||||
test_rc_zero_frees();
|
test_rt_destroy_frees_traced_remnants();
|
||||||
return t_report("test_rc");
|
return t_report("test_rc");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue