c: precise GC tracing for NaN-boxed Values

Boehm's conservative pointer scan cannot recognize lumbda's Value
layout — heap pointers live in the low 48 bits with QNAN + tag bits
in the upper mantissa, so a raw word never looks like a heap address.
Until now main.c neutralized this with GC_disable(): every allocation
leaked, OOMing any long-running workload.

Add precise tracing via a custom Boehm kind:

- New c/gc.c: mark proc walks 8-byte words in mixed mode — when the
  QNAN bits are set with a pointer-bearing tag (0/2/4/5/6) extract
  the low-48 pointer; otherwise fall through to raw-pointer
  validation. GC_set_push_other_roots callback decodes NaN-boxed
  Values on the C stack via setjmp anchor + scan up to the stack
  base captured at process start.

- Allocations holding Values (Pair, Env bindings, ValueStack data,
  ULVector data, HTEntry, Proc params + body, FullCont stack,
  CodeObj instrs, SymbolEntry) route through lumbda_value_malloc.
  Pure-byte sites (bignum limbs, char buffers, source files) stay
  on regular GC_MALLOC.

- main.c / test.c / bench.c capture stack-base then drop GC_disable.

types.c also zeros popped slots on the value stack so stale pointers
do not survive a vs_pop and pin freed objects — independent
correctness fix that pays off once GC actually runs.

Build: USE_GC=1 (default when /usr/include/gc.h exists).

Tests with GC enabled:
- 88/88 c-test
- 4/4 regression-named-let-leak (test that motivated GC_disable)
- 205/205 functional (Python + C)
- zoe-favorites all tiers (Python + C + asm + asm-full)

alloc-test 1M cons drop-loop:
- Before: 0.60s wall, 156 MB RSS, leaks every cell
- After:  0.37s wall,   4 MB RSS, ~1500 GC cycles each freeing ~370 KB
This commit is contained in:
russell@unturf.com 2026-06-07 17:18:45 -04:00
parent f398902cc4
commit b841b30bc4
No known key found for this signature in database
12 changed files with 300 additions and 74 deletions

View file

@ -41,7 +41,7 @@ int value_to_list(Value v, Value **out) {
while (IS_PAIR(cur)) { n++; cur = CDR(cur); }
if (!IS_NIL(cur)) lisp_error("not a list");
*out = (Value *)ul_malloc(sizeof(Value) * n);
*out = (Value *)ul_malloc_values(sizeof(Value) * n);
cur = v;
for (int i = 0; i < n; i++) {
(*out)[i] = CAR(cur);
@ -80,7 +80,7 @@ Formals parse_formals(Value f) {
bool has_rest = !IS_NIL(cur);
result.nparams = n;
result.params = (Value *)ul_malloc(sizeof(Value) * n);
result.params = (Value *)ul_malloc_values(sizeof(Value) * n);
cur = f;
for (int i = 0; i < n; i++) {
if (!IS_SYM(CAR(cur)))
@ -113,7 +113,7 @@ ExprList body_with_env(Value *forms, int count, Env *env) {
/* We may need to splice begin forms */
int cap = count + 64;
Value *expanded = (Value *)ul_malloc(sizeof(Value) * cap);
Value *expanded = (Value *)ul_malloc_values(sizeof(Value) * cap);
memcpy(expanded, forms, sizeof(Value) * count);
int n = count;
@ -133,7 +133,7 @@ ExprList body_with_env(Value *forms, int count, Env *env) {
Value *spliced; int ns = value_to_list(CDR(f), &spliced);
if (n + ns - 1 >= cap) {
cap = (n + ns) * 2;
expanded = (Value *)ul_realloc(expanded, sizeof(Value) * cap);
expanded = (Value *)ul_realloc_values(expanded, sizeof(Value) * cap);
}
memmove(expanded + i + ns, expanded + i + 1, sizeof(Value) * (n - i - 1));
memcpy(expanded + i, spliced, sizeof(Value) * ns);
@ -211,7 +211,7 @@ Value qq_expand(Value tmpl, Env *env, int depth) {
/* Collect parts */
int cap = 64;
Value *parts = (Value *)ul_malloc(sizeof(Value) * cap);
Value *parts = (Value *)ul_malloc_values(sizeof(Value) * cap);
int nparts = 0;
Value n = tmpl;
@ -222,17 +222,17 @@ Value qq_expand(Value tmpl, Env *env, int depth) {
Value spliced = leval(CADR(item), env);
Value *items; int ni = value_to_list(spliced, &items);
for (int i = 0; i < ni; i++) {
if (nparts >= cap) { cap *= 2; parts = (Value *)ul_realloc(parts, sizeof(Value) * cap); }
if (nparts >= cap) { cap *= 2; parts = (Value *)ul_realloc_values(parts, sizeof(Value) * cap); }
parts[nparts++] = items[i];
}
ul_free(items);
} else {
if (nparts >= cap) { cap *= 2; parts = (Value *)ul_realloc(parts, sizeof(Value) * cap); }
if (nparts >= cap) { cap *= 2; parts = (Value *)ul_realloc_values(parts, sizeof(Value) * cap); }
parts[nparts++] = cons(SYM_UNQUOTE_SPLICING,
cons(qq_expand(CADR(item), env, depth - 1), VAL_NIL));
}
} else {
if (nparts >= cap) { cap *= 2; parts = (Value *)ul_realloc(parts, sizeof(Value) * cap); }
if (nparts >= cap) { cap *= 2; parts = (Value *)ul_realloc_values(parts, sizeof(Value) * cap); }
parts[nparts++] = qq_expand(item, env, depth);
}
n = CDR(n);
@ -339,7 +339,7 @@ static Value define_record_type(Value *a, int na, Env *env) {
/* Capture field count and name symbol */
const char *type_tag = sym_name(name);
int nf = nfields;
Value *field_syms = (Value *)ul_malloc(sizeof(Value) * nf);
Value *field_syms = (Value *)ul_malloc_values(sizeof(Value) * nf);
for (int i = 0; i < nf; i++) field_syms[i] = ctor_spec[i + 1];
/* Create a Proc that builds (list 'type-name f1 f2 ...) */
@ -348,10 +348,10 @@ static Value define_record_type(Value *a, int na, Env *env) {
/* Actually, let's just define a Proc that builds the list */
ExprList body;
body.count = 1;
body.exprs = (Value *)ul_malloc(sizeof(Value));
body.exprs = (Value *)ul_malloc_values(sizeof(Value));
/* Build: (list (quote name) f1 f2 ...) */
Value *listargs = (Value *)ul_malloc(sizeof(Value) * (nf + 2));
Value *listargs = (Value *)ul_malloc_values(sizeof(Value) * (nf + 2));
listargs[0] = intern("list");
listargs[1] = cons(SYM_QUOTE, cons(name, VAL_NIL));
for (int i = 0; i < nf; i++) listargs[2 + i] = field_syms[i];
@ -382,7 +382,7 @@ static Value define_record_type(Value *a, int na, Env *env) {
Value arg_sym = intern("x");
ExprList body;
body.count = 1;
body.exprs = (Value *)ul_malloc(sizeof(Value));
body.exprs = (Value *)ul_malloc_values(sizeof(Value));
/* Build: (and (pair? x) (symbol? (car x)) (eq? (car x) 'name)) */
/* Simpler: use a special check that understands subtypes */
/* We'll need a native predicate. Let's put the type name in an env binding. */
@ -541,7 +541,7 @@ static bool sr_match(SyntaxTransformer *st, Value pat, Value form, Env *bindings
ULVector *vec = AS_VECTOR(existing->val);
if (vec->len >= vec->cap) {
vec->cap *= 2;
vec->data = (Value *)ul_realloc(vec->data, sizeof(Value) * vec->cap);
vec->data = (Value *)ul_realloc_values(vec->data, sizeof(Value) * vec->cap);
}
vec->data[vec->len++] = bind->val;
} else {
@ -549,7 +549,7 @@ static bool sr_match(SyntaxTransformer *st, Value pat, Value form, Env *bindings
Value vec = make_vector(0, VAL_NIL);
ULVector *v = AS_VECTOR(vec);
v->cap = n_ell > 0 ? (size_t)n_ell : 4;
v->data = (Value *)ul_realloc(v->data, sizeof(Value) * v->cap);
v->data = (Value *)ul_realloc_values(v->data, sizeof(Value) * v->cap);
v->data[0] = bind->val;
v->len = 1;
env_define(bindings, bind->sym, vec);
@ -615,7 +615,7 @@ static Value sr_expand(SyntaxTransformer *st, Value tmpl, Env *bindings) {
if (n < 0) n = 0;
/* Expand each iteration */
Value *expanded = (Value *)ul_malloc(sizeof(Value) * n);
Value *expanded = (Value *)ul_malloc_values(sizeof(Value) * n);
for (int i = 0; i < n; i++) {
/* Create a sub-binding env where vector vars are replaced by their i-th element */
Env *sb = make_env(NULL);
@ -921,8 +921,8 @@ Value leval(Value expr, Env *env) {
int nbody = na - 2;
Value *body_arr = a + 2;
Value *bps = (Value *)ul_malloc(sizeof(Value) * nb);
Value *bvs = (Value *)ul_malloc(sizeof(Value) * nb);
Value *bps = (Value *)ul_malloc_values(sizeof(Value) * nb);
Value *bvs = (Value *)ul_malloc_values(sizeof(Value) * nb);
for (int i = 0; i < nb; i++) {
Value *bp; int nbp = value_to_list(binds[i], &bp);
bps[i] = bp[0];
@ -1034,7 +1034,7 @@ Value leval(Value expr, Env *env) {
Env *c = make_env(env);
/* Parse variable specs: (var init step) */
typedef struct { Value var; Value step; } DoSpec;
DoSpec *specs = (DoSpec *)ul_malloc(sizeof(DoSpec) * nvc);
DoSpec *specs = (DoSpec *)ul_malloc_values(sizeof(DoSpec) * nvc);
for (int i = 0; i < nvc; i++) {
Value *sp; int nsp = value_to_list(vcs[i], &sp);
specs[i].var = sp[0];
@ -1055,7 +1055,7 @@ Value leval(Value expr, Env *env) {
break;
}
for (int i = 0; i < nbody; i++) leval(body_arr[i], c);
Value *nvs = (Value *)ul_malloc(sizeof(Value) * nvc);
Value *nvs = (Value *)ul_malloc_values(sizeof(Value) * nvc);
for (int i = 0; i < nvc; i++) nvs[i] = leval(specs[i].step, c);
for (int i = 0; i < nvc; i++) env_set(c, specs[i].var, nvs[i]);
ul_free(nvs);
@ -1110,7 +1110,7 @@ Value leval(Value expr, Env *env) {
st->literals = (char **)ul_malloc(sizeof(char *) * nlits);
for (int i = 0; i < nlits; i++) st->literals[i] = ul_strdup(sym_name(lits[i]));
st->nrules = na - 1;
st->rules = (SyntaxRule *)ul_malloc(sizeof(SyntaxRule) * st->nrules);
st->rules = (SyntaxRule *)ul_malloc_values(sizeof(SyntaxRule) * st->nrules);
for (int i = 0; i < st->nrules; i++) {
Value *rl; int nrl = value_to_list(a[i + 1], &rl);
st->rules[i].pattern = rl[0];
@ -1210,13 +1210,13 @@ Value leval(Value expr, Env *env) {
int npre = na - 2;
Value *pre = NULL;
if (npre > 0) {
pre = (Value *)ul_malloc(sizeof(Value) * npre);
pre = (Value *)ul_malloc_values(sizeof(Value) * npre);
for (int i = 0; i < npre; i++) pre[i] = leval(a[i + 1], env);
}
Value last = leval(a[na - 1], env);
Value *lst; int nlst = value_to_list(last, &lst);
Value *all_args = (Value *)ul_malloc(sizeof(Value) * (npre + nlst));
Value *all_args = (Value *)ul_malloc_values(sizeof(Value) * (npre + nlst));
if (pre) memcpy(all_args, pre, sizeof(Value) * npre);
memcpy(all_args + npre, lst, sizeof(Value) * nlst);
int total = npre + nlst;
@ -1260,7 +1260,7 @@ Value leval(Value expr, Env *env) {
Value *irr = NULL;
int nirr = na - 1;
if (nirr > 0) {
irr = (Value *)ul_malloc(sizeof(Value) * nirr);
irr = (Value *)ul_malloc_values(sizeof(Value) * nirr);
for (int i = 0; i < nirr; i++) irr[i] = leval(a[i + 1], env);
}
Value obj = make_error_object(msg, irr, nirr);
@ -1409,7 +1409,7 @@ Value leval(Value expr, Env *env) {
/* Save old values and set new */
typedef struct { Value param; Value old_val; } PBind;
PBind *pb = (PBind *)ul_malloc(sizeof(PBind) * nb);
PBind *pb = (PBind *)ul_malloc_values(sizeof(PBind) * nb);
for (int i = 0; i < nb; i++) {
Value *bp; int nbp = value_to_list(binds[i], &bp);
pb[i].param = leval(bp[0], env);