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>
96 lines
4.5 KiB
C
96 lines
4.5 KiB
C
/* obj.h — per-shard arena allocator (spec section 4). Object model and
|
|
* strings extend this header in the next task. */
|
|
#ifndef WO_OBJ_H
|
|
#define WO_OBJ_H
|
|
|
|
#include "wob.h"
|
|
|
|
/* Arena: one malloc'd region, bump allocation, 16-byte-granular size-class
|
|
* free lists up to 1024 bytes (freed blocks chain through their first word).
|
|
* Larger sizes use plain malloc/free — the caller always passes the size
|
|
* back on free, so no size headers exist anywhere. Exhaustion returns NULL;
|
|
* the VM maps that to WO_T_OOM, never an abort. */
|
|
#define WO_ARENA_MAX_CLASS 1024u
|
|
#define WO_ARENA_NCLASSES (WO_ARENA_MAX_CLASS / 16u) /* 64 lists: 16..1024 */
|
|
|
|
typedef struct wo_arena {
|
|
uint8_t *base; /* malloc'd region */
|
|
size_t cap; /* region capacity in bytes */
|
|
size_t used; /* bump offset */
|
|
void *freelist[WO_ARENA_NCLASSES]; /* index = size/16 - 1 */
|
|
} wo_arena;
|
|
|
|
int wo_arena_init(wo_arena *a, size_t cap); /* 0 ok, -1 malloc failure */
|
|
void wo_arena_destroy(wo_arena *a);
|
|
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);
|
|
|
|
/* 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
|
|
* milestone 1): the arena, the loaded class table, the tracing collector's
|
|
* state (gc.c), and the output stream builtin print writes to (tests point
|
|
* it at a temp file to capture output). */
|
|
typedef struct wo_rt {
|
|
wo_arena arena;
|
|
const wo_classdesc *classes;
|
|
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 {
|
|
wo_hdr **items; /* gray worklist: traced objects awaiting a scan */
|
|
size_t len, cap;
|
|
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 */
|
|
/* the database engine's handles (database/src), opaque here so the VM
|
|
core needn't include engine headers: db = wo_db*, wal = wo_wal*.
|
|
NULL = engine absent (test binaries) / durability off (no WO_DATA).
|
|
Set by main.c at boot; db.c casts. */
|
|
void *db;
|
|
void *wal;
|
|
} wo_rt;
|
|
|
|
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 */
|
|
void wo_rt_destroy(wo_rt *rt);
|
|
|
|
/* New zeroed instance of a class-table class. A traced (inferred-gc) class
|
|
* 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);
|
|
|
|
/* Strings: header + length + inline bytes, class id WO_CLS_STR. */
|
|
typedef struct wo_str {
|
|
wo_hdr h;
|
|
uint32_t len;
|
|
char data[]; /* len bytes, no NUL */
|
|
} wo_str;
|
|
|
|
wo_str *wo_str_new(wo_rt *rt, const char *bytes, uint32_t len); /* NULL=OOM */
|
|
/* A Text of [len] UNINITIALIZED bytes for a caller that writes them itself
|
|
* (the systems-stdlib `join`, which knows the total length up front and
|
|
* would otherwise need one allocation per element). NULL = OOM. */
|
|
wo_str *wo_str_alloc(wo_rt *rt, uint32_t len);
|
|
wo_str *wo_str_concat(wo_rt *rt, const wo_str *a, const wo_str *b);
|
|
int wo_str_eq(const wo_str *a, const wo_str *b); /* content equality */
|
|
void wo_str_free(wo_rt *rt, wo_str *s); /* no-op on WO_F_CONST */
|
|
|
|
#endif /* WO_OBJ_H */
|