writeonce/runtime/src/gc.c
shoney.arickathil 631d277e0c feat(runtime): wovm VM core (Iteration 2)
- 16 tasks complete: arena, object model, borrow word, containers,
  RC + budgeted cycle collector, wob_build, validating loader,
  interpreter core (dual dispatch), object opcodes, drop-map unwinding,
  builtins + DB_STUB + TRAP, ICALL, wovm CLI + just recipes
- 13 test suites × 2 dispatch flavors (ASan+UBSan) + CLI smoke, all green
- .wob v1 format pinned in src/wob.h + docs/plan/oop-vm/00-wob-format.md
- wo-rt.c reference event-loop preserved for sub-project 2

This is Iteration 2 of the OOP milestone; compiler front (Iteration 3)
is in progress on this branch. They meet at Iteration 4 (emitter+e2e).
2026-08-10 09:35:55 +02:00

296 lines
9.7 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "gc.h"
#include <stdlib.h>
#include "cont.h"
static void multi_free(wo_rt *rt, wo_multi *m) {
for (uint32_t i = 0; i < m->len; i++)
wo_drop_kind(rt, m->elem_kind, m->items[i]);
free(m->items);
wo_arena_free(&rt->arena, m, sizeof(wo_multi));
}
static void map_free(wo_rt *rt, wo_map *m) {
for (uint32_t i = 0; i < m->len; i++) {
wo_drop_kind(rt, m->key_kind, m->keys[i]);
wo_drop_kind(rt, m->val_kind, m->vals[i]);
}
free(m->keys);
free(m->vals);
wo_arena_free(&rt->arena, m, sizeof(wo_map));
}
/* release a class object's contents then the object itself */
static void class_free(wo_rt *rt, wo_hdr *o) {
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++)
wo_drop_kind(rt, c->kinds[i], f[i]);
wo_arena_free(&rt->arena, o, wo_obj_size(c));
}
void wo_drop_obj(wo_rt *rt, wo_hdr *o) {
if (!o) return;
switch (o->class_id) {
case WO_CLS_STR:
wo_str_free(rt, (wo_str *)o);
return;
case WO_CLS_MULTI:
multi_free(rt, (wo_multi *)o);
return;
case WO_CLS_MAP:
map_free(rt, (wo_map *)o);
return;
default:
class_free(rt, o);
return;
}
}
void wo_drop_kind(wo_rt *rt, uint8_t kind, uint64_t v) {
if (!v) return; /* null values ignored for every kind */
switch (kind) {
case WO_K_SCALAR:
return;
case WO_K_OWNED:
case WO_K_MULTI:
case WO_K_MAP:
wo_drop_obj(rt, (wo_hdr *)(uintptr_t)v);
return;
case WO_K_GCREF:
wo_rc_dec(rt, (wo_hdr *)(uintptr_t)v);
return;
case WO_K_TEXT:
wo_str_free(rt, (wo_str *)(uintptr_t)v);
return;
default:
return; /* loader guarantees kinds; defensive no-op */
}
}
void wo_rc_inc(wo_hdr *o) { o->rc++; }
/* ---- cycle-candidate buffer ---- */
/* only classes that can point at other @gc objects can close a cycle */
static int class_possibly_cyclic(const wo_rt *rt, const wo_hdr *o) {
const wo_classdesc *c = &rt->classes[o->class_id];
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) {
if (rt->cycbuf.len == rt->cycbuf.cap) {
size_t ncap = rt->cycbuf.cap ? rt->cycbuf.cap * 2 : 16;
wo_hdr **ni = realloc(rt->cycbuf.items, ncap * sizeof(wo_hdr *));
if (!ni) return; /* can't buffer: conservative leak, never corrupt */
rt->cycbuf.items = ni;
rt->cycbuf.cap = ncap;
}
o->flags |= WO_F_BUF;
rt->cycbuf.items[rt->cycbuf.len++] = o;
}
/* swap-remove a specific object from the buffer (whites purged mid-step) */
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;
}
}
}
void wo_rc_dec(wo_rt *rt, wo_hdr *o) {
o->rc--;
if (o->rc == 0) {
/* zombie guard: a buffered candidate's death belongs to the cycle
* collector — it will notice rc 0 and free it in its own sweep */
if (o->flags & WO_F_BUF) return;
class_free(rt, o);
return;
}
/* survived a decrement and can sit on a cycle: buffer as a candidate,
* deduplicated by the flag (Bacon–Rajan possible-root heuristic) */
if (!(o->flags & WO_F_BUF) && class_possibly_cyclic(rt, o))
buf_push(rt, o);
}
/* ---- budgeted Bacon–Rajan trial deletion ---- */
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);
}
/* visit every @gc edge out of a class object: gcref fields, plus gcref
* elements inside multi/map fields */
typedef void (*child_fn)(wo_rt *rt, wo_hdr *child, void *ctx);
static void visit_children(wo_rt *rt, wo_hdr *o, child_fn fn, void *ctx) {
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) {
if (f[i]) fn(rt, (wo_hdr *)(uintptr_t)f[i], ctx);
} else if (k == WO_K_MULTI) {
wo_multi *m = (wo_multi *)(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);
visit_children(rt, o, scan_black_child, NULL);
}
static void scan(wo_rt *rt, wo_hdr *o, void *ctx);
static void scan_(wo_rt *rt, wo_hdr *o) {
if (color_of(o) != WO_COLOR_GRAY) return;
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) */
typedef struct {
wo_hdr **items;
size_t len, cap;
int oom;
} whites_t;
static void collect_white(wo_rt *rt, wo_hdr *o, void *ctx) {
whites_t *w = ctx;
if (color_of(o) != WO_COLOR_WHITE) return;
set_color(o, WO_COLOR_BLACK); /* dedup: gathered exactly once */
visit_children(rt, o, collect_white, ctx);
if (w->len == w->cap) {
size_t ncap = w->cap ? w->cap * 2 : 16;
wo_hdr **ni = realloc(w->items, ncap * sizeof(wo_hdr *));
if (!ni) {
w->oom = 1;
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
* accounting was already settled by the gray/scan phases, and the pointed-
* at whites die in this same sweep. Containers holding gcref elements free
* only their backing. */
static void white_free(wo_rt *rt, wo_hdr *o) {
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];
uint64_t v = f[i];
if (!v || k == WO_K_SCALAR || k == WO_K_GCREF) continue;
if (k == WO_K_MULTI) {
wo_multi *m = (wo_multi *)(uintptr_t)v;
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);
free(mp->vals);
wo_arena_free(&rt->arena, mp, sizeof(wo_map));
} else {
wo_drop_kind(rt, k, v); /* OWNED subtree, TEXT */
}
}
wo_arena_free(&rt->arena, o, wo_obj_size(c));
}
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;
}