writeonce/runtime/src/gc.c
shoney.arickathil d24c705860 feat: iterations 19 + 17 — Float/Bytes scalars (.wob v5), library kind + internal/
- Float full stack: literals (fraction/exponent; `0..10` still a range), f64
  opcodes 34-41, @table column, WAL bit-exact replay, json fractions in and
  shortest-round-trip out. IEEE-quiet — FDIV never traps where DIV does.
- Bytes: a wo_str with its own class id, so alloc/free/copy are shared but no
  Text builtin accepts one; len/at/slice/eq/concat, base64 both ways, json
  boundary as base64; TEXT_COPY preserves the kind.
- No implicit Int/Float mixing (WO-E201 in the typechecker, not the emitter,
  which picks the opcode from one side and would misread the other).
- One IEEE deviation: float_cmp total order (NaN last, -0.0 == +0.0) for
  indexes and order-by, keys canonicalized to match. `?Float` nil is a
  reserved quiet NaN — the zero word is +0.0, WO_NIL_SCALAR's bits are -2.0.
- Renderer prefers fixed over exponential in 1e-6..1e21: pure shortest makes
  a price of 900.0 read `9e+02`. One renderer for interp/json/float_to_text.
- Fixed en route: lexer double-counted the leading digit; is_scalar_shaped
  took Float/Bytes as Int-shaped; Bytes ownership needed a shared heap-scalar
  predicate or temps never dropped; order-by bit-compared negatives backwards.
- Iteration 17: `kind = "library"` (absent = program; bad value = WO-E109),
  entry-less check mode retiring the `--emit` workaround, Go's `internal/` as
  WO-E108 at the consumer's `use`. Driver-only; VM/.wob/GC untouched.
- Framework reorg: internal/{parse,serve}.wo; http/form.wo split out to keep
  media_type/form_values public (parse.wo had grown public surface).
- Docs: link audit (97 -> 88 broken, conflict markers resolved, 2 duplicate
  stories removed), 00-code-review verified 26/27, iterations re-sequenced.
- Also carries the pre-staged pub(read)/using/#if work from the index.
- Gates: corpus 103/0, test_wal 156/0, web-app 26/0, oop-accept ALL MET.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 19:24:15 +02:00

299 lines
11 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.

/* 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 <stdio.h>
#include <stdlib.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) {
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. 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) {
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));
}
/* arc T6: a drop on the wrong shard routes home — the owner's arena is
* single-threaded by doctrine, so the free travels as an envelope.
* (vm.c owns the engine; this hook keeps gc.c engine-blind.) */
void wo_route_free(wo_hdr *h);
void wo_drop_obj(wo_rt *rt, wo_hdr *o) {
if (o && o->shard_id != rt->shard_id && !(o->flags & WO_F_CONST)) {
wo_route_free(o);
return;
}
if (!o) return;
switch (o->class_id) {
case WO_CLS_STR:
case WO_CLS_BYTES: /* iteration 19: same object shape, same free */
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:
/* 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;
case WO_K_TEXT: {
wo_str *sp = (wo_str *)(uintptr_t)v;
if (sp->h.shard_id != rt->shard_id && !(sp->h.flags & WO_F_CONST)) {
wo_route_free(&sp->h); /* home arena frees it (arc T6) */
return;
}
wo_str_free(rt, sp);
return;
}
default:
return; /* loader guarantees kinds; defensive no-op */
}
}
/* ---- traced list + shading --------------------------------------------- */
void wo_gc_track(wo_rt *rt, wo_hdr *o, size_t size) {
o->gclink = rt->gc_traced;
rt->gc_traced = o;
rt->gc_traced_cnt++;
rt->gc_alloc_bytes += size;
}
void wo_gc_shade(wo_rt *rt, wo_hdr *o) {
if (!o || !is_traced(o)) return;
if (color_of(o) != WO_COLOR_WHITE) return; /* gray or black: already safe */
set_color(o, WO_COLOR_GRAY);
if (rt->gc_gray.len == rt->gc_gray.cap) {
size_t ncap = rt->gc_gray.cap ? rt->gc_gray.cap * 2 : 64;
wo_hdr **ni = realloc(rt->gc_gray.items, ncap * sizeof(wo_hdr *));
if (!ni) {
/* cannot queue: this cycle can no longer prove anything dead.
* Flag it; the next slice blackens the whole traced list and
* the sweep frees nothing — a conservative, safe cycle. */
rt->gc_gray.oom = 1;
set_color(o, WO_COLOR_BLACK);
return;
}
rt->gc_gray.items = ni;
rt->gc_gray.cap = ncap;
}
rt->gc_gray.items[rt->gc_gray.len++] = o;
}
/* may this class's instances transitively hold a gcref? NULL table (or an
* out-of-range id) answers conservatively: traverse. */
static int class_may_gcref(const wo_rt *rt, uint32_t class_id) {
if (!rt->gc_may || class_id >= rt->class_cnt) return 1;
return rt->gc_may[class_id] != 0;
}
/* 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;
}
switch (o->class_id) {
case WO_CLS_STR:
case WO_CLS_BYTES: /* iteration 19: leaf bytes, nothing to scan */
return;
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;
}
}
}
/* ---- the cycle ---------------------------------------------------------- */
int wo_gc_want_start(const wo_rt *rt) {
return rt->gc_phase == WO_GC_IDLE && rt->gc_alloc_bytes >= rt->gc_goal &&
rt->gc_traced != NULL;
}
void wo_gc_begin(wo_rt *rt) {
rt->gc_gray.len = 0;
rt->gc_gray.oom = 0;
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];
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]) wo_gc_shade(rt, (wo_hdr *)(uintptr_t)f[i]);
} else if (k == WO_K_OWNED || k == WO_K_MULTI || k == WO_K_MAP) {
wo_gc_scan_root(rt, (wo_hdr *)(uintptr_t)f[i]);
}
}
set_color(o, WO_COLOR_BLACK);
}
size_t wo_gc_slice(wo_rt *rt, size_t budget) {
size_t freed = 0;
if (rt->gc_phase == WO_GC_IDLE) return 0;
if (rt->gc_phase == WO_GC_MARK) {
if (rt->gc_gray.oom) {
/* worklist allocation failed mid-mark: blacken everything so
* the sweep frees nothing — a wasted cycle, never a wrong one */
for (wo_hdr *o = rt->gc_traced; o; o = o->gclink)
set_color(o, WO_COLOR_BLACK);
rt->gc_gray.len = 0;
}
size_t done = 0;
while (done < budget && rt->gc_gray.len > 0) {
wo_hdr *o = rt->gc_gray.items[--rt->gc_gray.len];
scan_traced(rt, o);
done++;
}
if (rt->gc_gray.len == 0) {
rt->gc_phase = WO_GC_SWEEP;
rt->gc_sweep = &rt->gc_traced;
}
}
if (rt->gc_phase == WO_GC_SWEEP) {
size_t done = 0;
wo_hdr **link = rt->gc_sweep;
while (done < budget && *link) {
wo_hdr *o = *link;
if (color_of(o) == WO_COLOR_WHITE) {
*link = o->gclink; /* unlink, then free contents + object */
rt->gc_traced_cnt--;
class_free(rt, o);
freed++;
} else {
set_color(o, WO_COLOR_WHITE); /* survivor: candidate next cycle */
link = &o->gclink;
}
done++;
}
rt->gc_sweep = link;
if (!*link) {
rt->gc_phase = WO_GC_IDLE;
rt->gc_sweep = NULL;
rt->gc_alloc_bytes = 0;
}
}
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);
return freed;
}