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>
107 lines
4.3 KiB
C
107 lines
4.3 KiB
C
/* 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
|
|
* fields so instances exceed the 1024-byte size-class ceiling and take the
|
|
* arena's malloc path — any missed free is a hard ASan leak report. */
|
|
#include "cont.h"
|
|
#include "gc.h"
|
|
#include "t.h"
|
|
|
|
#define BIG 130
|
|
|
|
/* class 0 "Node": field0 OWNED (child Node), field1 TEXT, rest scalars */
|
|
static uint8_t node_kinds[BIG];
|
|
/* class 1 "Shared" (traced): all scalars */
|
|
static uint8_t shared_kinds[BIG];
|
|
/* class 2 "Holder": field0 GCREF, field1 MULTI (of TEXT), rest scalars */
|
|
static uint8_t holder_kinds[BIG];
|
|
|
|
static wo_classdesc CLASSES[3];
|
|
|
|
static void setup_classes(void) {
|
|
node_kinds[0] = WO_K_OWNED;
|
|
node_kinds[1] = WO_K_TEXT;
|
|
shared_kinds[0] = WO_K_SCALAR;
|
|
holder_kinds[0] = WO_K_GCREF;
|
|
holder_kinds[1] = WO_K_MULTI;
|
|
CLASSES[0] = (wo_classdesc){.name = 0, .flags = 0, .field_cnt = BIG, .kinds = node_kinds};
|
|
CLASSES[1] = (wo_classdesc){.name = 0, .flags = WO_CLASSF_GC, .field_cnt = BIG, .kinds = shared_kinds};
|
|
CLASSES[2] = (wo_classdesc){.name = 0, .flags = 0, .field_cnt = BIG, .kinds = holder_kinds};
|
|
}
|
|
|
|
/* Owned tree: parent -> child -> grandchild, each with an owned name text.
|
|
* One drop of the root must free all six allocations (ASan-proven). */
|
|
static void test_owned_tree_recursive_drop(void) {
|
|
wo_rt rt;
|
|
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 3), 0);
|
|
wo_hdr *grand = wo_obj_new(&rt, 0);
|
|
wo_fields(grand)[1] = (uint64_t)(uintptr_t)wo_str_new(&rt, "grand", 5);
|
|
wo_hdr *child = wo_obj_new(&rt, 0);
|
|
wo_fields(child)[0] = (uint64_t)(uintptr_t)grand;
|
|
wo_fields(child)[1] = (uint64_t)(uintptr_t)wo_str_new(&rt, "child", 5);
|
|
wo_hdr *root = wo_obj_new(&rt, 0);
|
|
wo_fields(root)[0] = (uint64_t)(uintptr_t)child;
|
|
wo_fields(root)[1] = (uint64_t)(uintptr_t)wo_str_new(&rt, "root", 4);
|
|
wo_drop_obj(&rt, root);
|
|
/* nothing to assert beyond "ASan stays silent" — that IS the test */
|
|
T_CHECK(1);
|
|
wo_rt_destroy(&rt);
|
|
}
|
|
|
|
/* An owned holder dying must NOT free the traced object its gcref field
|
|
* points at — tracing owns that lifetime. The object stays on the traced
|
|
* list; a rootless cycle then frees it (and rt_destroy would too). */
|
|
static void test_holder_death_leaves_traced_alive(void) {
|
|
wo_rt rt;
|
|
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 3), 0);
|
|
wo_hdr *shared = wo_obj_new(&rt, 1);
|
|
T_EQ(rt.gc_traced_cnt, 1);
|
|
wo_hdr *holder = wo_obj_new(&rt, 2);
|
|
wo_fields(holder)[0] = (uint64_t)(uintptr_t)shared;
|
|
wo_drop_obj(&rt, holder); /* gcref edge is a no-op: shared survives */
|
|
T_EQ(rt.gc_traced_cnt, 1);
|
|
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);
|
|
}
|
|
|
|
/* Text and multi-of-text fields are freed with the holder. */
|
|
static void test_container_fields_freed_with_holder(void) {
|
|
wo_rt rt;
|
|
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 3), 0);
|
|
wo_multi *tags = wo_multi_new(&rt, WO_K_TEXT);
|
|
T_EQ(wo_multi_push(tags, (uint64_t)(uintptr_t)wo_str_new(&rt, "a", 1)), 0);
|
|
T_EQ(wo_multi_push(tags, (uint64_t)(uintptr_t)wo_str_new(&rt, "b", 1)), 0);
|
|
wo_hdr *holder = wo_obj_new(&rt, 2);
|
|
wo_fields(holder)[1] = (uint64_t)(uintptr_t)tags;
|
|
wo_drop_obj(&rt, holder); /* frees holder + multi + both strings */
|
|
T_CHECK(1);
|
|
wo_rt_destroy(&rt);
|
|
}
|
|
|
|
/* Teardown safety net: traced objects still on the list when the runtime
|
|
* 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;
|
|
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 3), 0);
|
|
(void)wo_obj_new(&rt, 1);
|
|
(void)wo_obj_new(&rt, 1);
|
|
T_EQ(rt.gc_traced_cnt, 2);
|
|
wo_rt_destroy(&rt); /* frees both (ASan-proven) */
|
|
T_CHECK(1);
|
|
}
|
|
|
|
int main(void) {
|
|
setup_classes();
|
|
test_owned_tree_recursive_drop();
|
|
test_holder_death_leaves_traced_alive();
|
|
test_container_fields_freed_with_holder();
|
|
test_rt_destroy_frees_traced_remnants();
|
|
return t_report("test_rc");
|
|
}
|