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>
88 lines
2.9 KiB
C
88 lines
2.9 KiB
C
/* test_obj — object creation (owned vs @gc), strings, const interning. */
|
|
#include "obj.h"
|
|
#include "t.h"
|
|
|
|
/* two test classes: 0 = owned Point{x,y}, 1 = @gc Cache{hits} */
|
|
static const uint8_t point_kinds[] = {WO_K_SCALAR, WO_K_SCALAR};
|
|
static const uint8_t cache_kinds[] = {WO_K_SCALAR};
|
|
static const wo_classdesc CLASSES[] = {
|
|
{.name = 0, .flags = 0, .field_cnt = 2, .kinds = point_kinds},
|
|
{.name = 0, .flags = WO_CLASSF_GC, .field_cnt = 1, .kinds = cache_kinds},
|
|
};
|
|
|
|
static void test_owned_object_zeroed(void) {
|
|
wo_rt rt;
|
|
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 2), 0);
|
|
wo_hdr *o = wo_obj_new(&rt, 0);
|
|
T_CHECK(o != NULL);
|
|
T_EQ(o->class_id, 0);
|
|
T_EQ(o->flags, 0);
|
|
T_EQ(o->borrow, WO_BORROW_FREE);
|
|
T_EQ(wo_fields(o)[0], 0);
|
|
T_EQ(wo_fields(o)[1], 0);
|
|
wo_arena_free(&rt.arena, o, wo_obj_size(&CLASSES[0]));
|
|
wo_rt_destroy(&rt);
|
|
}
|
|
|
|
/* 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;
|
|
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 2), 0);
|
|
wo_hdr *o = wo_obj_new(&rt, 1);
|
|
T_CHECK(o != NULL);
|
|
T_CHECK(o->flags & WO_F_GC);
|
|
T_EQ(o->flags & WO_F_COLOR, WO_COLOR_WHITE);
|
|
T_EQ(rt.gc_traced, o);
|
|
T_EQ(rt.gc_traced_cnt, 1);
|
|
wo_rt_destroy(&rt);
|
|
}
|
|
|
|
static void test_strings(void) {
|
|
wo_rt rt;
|
|
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 2), 0);
|
|
wo_str *a = wo_str_new(&rt, "hello ", 6);
|
|
wo_str *b = wo_str_new(&rt, "world", 5);
|
|
T_CHECK(a && b);
|
|
T_EQ(a->h.class_id, WO_CLS_STR);
|
|
T_EQ(a->len, 6);
|
|
T_CHECK(memcmp(a->data, "hello ", 6) == 0);
|
|
|
|
wo_str *c = wo_str_concat(&rt, a, b);
|
|
T_CHECK(c != NULL);
|
|
T_EQ(c->len, 11);
|
|
T_CHECK(memcmp(c->data, "hello world", 11) == 0);
|
|
|
|
wo_str *c2 = wo_str_new(&rt, "hello world", 11);
|
|
T_CHECK(wo_str_eq(c, c2)); /* content equality, different pointers */
|
|
T_CHECK(!wo_str_eq(a, b));
|
|
|
|
wo_str_free(&rt, a);
|
|
wo_str_free(&rt, b);
|
|
wo_str_free(&rt, c);
|
|
wo_str_free(&rt, c2);
|
|
wo_rt_destroy(&rt);
|
|
}
|
|
|
|
static void test_const_string_survives_free(void) {
|
|
wo_rt rt;
|
|
T_EQ(wo_rt_init(&rt, 1 << 16, CLASSES, 2), 0);
|
|
wo_str *s = wo_str_new(&rt, "interned", 8);
|
|
T_CHECK(s != NULL);
|
|
s->h.flags |= WO_F_CONST; /* as the loader will mark interned constants */
|
|
wo_str_free(&rt, s); /* must be a no-op */
|
|
T_EQ(s->len, 8); /* still readable: free didn't touch it */
|
|
T_CHECK(memcmp(s->data, "interned", 8) == 0);
|
|
s->h.flags &= (uint8_t)~WO_F_CONST;
|
|
wo_str_free(&rt, s); /* real free so ASan sees no leak */
|
|
wo_rt_destroy(&rt);
|
|
}
|
|
|
|
int main(void) {
|
|
test_owned_object_zeroed();
|
|
test_gc_object_tracked();
|
|
test_strings();
|
|
test_const_string_survives_free();
|
|
return t_report("test_obj");
|
|
}
|