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

@ -47,23 +47,33 @@ void lisp_error_with_obj(Value obj, const char *fmt, ...) {
* Value stack
* */
/* Unused slots in s->data are zeroed so Boehm's conservative scan of
* this GC_MALLOC region does not treat stale popped pointers as live
* roots. Without this, popped Scheme values stay pinned for the
* lifetime of the stack every closure ever pushed leaks. */
void vs_init(ValueStack *s, int cap) {
s->data = (Value *)ul_malloc(sizeof(Value) * cap);
s->data = (Value *)ul_malloc_values(sizeof(Value) * cap);
memset(s->data, 0, sizeof(Value) * cap);
s->len = 0;
s->cap = cap;
}
void vs_push(ValueStack *s, Value v) {
if (s->len >= s->cap) {
int old_cap = s->cap;
s->cap = s->cap * 2;
s->data = (Value *)ul_realloc(s->data, sizeof(Value) * s->cap);
s->data = (Value *)ul_realloc_values(s->data, sizeof(Value) * s->cap);
memset(s->data + old_cap, 0, sizeof(Value) * (s->cap - old_cap));
}
s->data[s->len++] = v;
}
Value vs_pop(ValueStack *s) {
if (s->len <= 0) lisp_error("stack underflow");
return s->data[--s->len];
Value v = s->data[--s->len];
s->data[s->len] = 0;
return v;
}
Value vs_peek(ValueStack *s) {
@ -72,6 +82,7 @@ Value vs_peek(ValueStack *s) {
}
void vs_clear(ValueStack *s) {
if (s->len > 0) memset(s->data, 0, sizeof(Value) * s->len);
s->len = 0;
}
@ -96,7 +107,7 @@ Value intern(const char *name) {
}
/* New symbol */
char *copy = ul_strdup(name);
SymbolEntry *ne = (SymbolEntry *)ul_malloc(sizeof(SymbolEntry));
SymbolEntry *ne = (SymbolEntry *)ul_malloc_values(sizeof(SymbolEntry));
ne->name = copy;
ne->value = VAL_SYM_RAW(copy);
ne->next = g_symbols.buckets[h];
@ -113,7 +124,7 @@ const char *sym_name(Value sym) {
* */
Pair *make_pair(Value car, Value cdr) {
Pair *p = (Pair *)ul_malloc(sizeof(Pair));
Pair *p = (Pair *)ul_malloc_values(sizeof(Pair));
p->hdr.type = OBJ_PAIR;
p->car = car;
p->cdr = cdr;
@ -153,7 +164,7 @@ Value make_vector(size_t len, Value fill) {
v->hdr.type = OBJ_VECTOR;
v->len = len;
v->cap = len > 0 ? len : 4;
v->data = (Value *)ul_malloc(sizeof(Value) * v->cap);
v->data = (Value *)ul_malloc_values(sizeof(Value) * v->cap);
for (size_t i = 0; i < len; i++) v->data[i] = fill;
return VAL_PTR(v);
}
@ -163,7 +174,7 @@ Value make_vector_from(Value *items, size_t len) {
v->hdr.type = OBJ_VECTOR;
v->len = len;
v->cap = len > 0 ? len : 4;
v->data = (Value *)ul_malloc(sizeof(Value) * v->cap);
v->data = (Value *)ul_malloc_values(sizeof(Value) * v->cap);
memcpy(v->data, items, sizeof(Value) * len);
return VAL_PTR(v);
}
@ -206,7 +217,7 @@ void ht_set(ULHashTable *ht, Value key, Value val) {
if (values_equal(e->key, key)) { e->value = val; return; }
e = e->next;
}
HTEntry *ne = (HTEntry *)ul_malloc(sizeof(HTEntry));
HTEntry *ne = (HTEntry *)ul_malloc_values(sizeof(HTEntry));
ne->key = key;
ne->value = val;
ne->next = ht->buckets[h];
@ -502,7 +513,7 @@ void env_define(Env *e, Value sym, Value val) {
b = b->next;
}
/* New binding */
EnvBinding *nb = (EnvBinding *)ul_malloc(sizeof(EnvBinding));
EnvBinding *nb = (EnvBinding *)ul_malloc_values(sizeof(EnvBinding));
nb->sym = sym;
nb->val = val;
nb->next = e->buckets[h];
@ -609,7 +620,7 @@ VMFrame *deep_copy_frames(VMFrame *frames, int nframes) {
copy[i].env = deep_copy_env(frames[i].env);
copy[i].stack_len = frames[i].stack_len;
copy[i].stack_cap = frames[i].stack_cap;
copy[i].stack = (Value *)ul_malloc(sizeof(Value) * copy[i].stack_cap);
copy[i].stack = (Value *)ul_malloc_values(sizeof(Value) * copy[i].stack_cap);
memcpy(copy[i].stack, frames[i].stack, sizeof(Value) * frames[i].stack_len);
}
return copy;
@ -621,7 +632,7 @@ FullCont *make_full_cont(VMFrame *frames, int nframes, Value *stack, int stack_l
c->hdr.type = OBJ_CONTINUATION;
c->frames = deep_copy_frames(frames, nframes);
c->nframes = nframes;
c->stack = (Value *)ul_malloc(sizeof(Value) * (stack_len > 0 ? stack_len : 4));
c->stack = (Value *)ul_malloc_values(sizeof(Value) * (stack_len > 0 ? stack_len : 4));
memcpy(c->stack, stack, sizeof(Value) * stack_len);
c->stack_len = stack_len;
c->ip = ip;
@ -646,7 +657,7 @@ Env *deep_copy_env(Env *env) {
EnvBinding *src = env->buckets[i];
EnvBinding **dst = &ne->buckets[i];
while (src) {
EnvBinding *nb = (EnvBinding *)ul_malloc(sizeof(EnvBinding));
EnvBinding *nb = (EnvBinding *)ul_malloc_values(sizeof(EnvBinding));
nb->sym = src->sym;
nb->val = src->val;
nb->next = NULL;
@ -666,13 +677,13 @@ Env *deep_copy_env(Env *env) {
Proc *make_proc(Value *params, int nparams, Value rest,
ExprList body, Env *env, const char *name) {
Proc *p = (Proc *)ul_malloc(sizeof(Proc));
Proc *p = (Proc *)ul_malloc_values(sizeof(Proc));
p->hdr.type = OBJ_PROC;
p->params = (Value *)ul_malloc(sizeof(Value) * nparams);
p->params = (Value *)ul_malloc_values(sizeof(Value) * nparams);
memcpy(p->params, params, sizeof(Value) * nparams);
p->nparams = nparams;
p->rest = rest;
p->body.exprs = (Value *)ul_malloc(sizeof(Value) * body.count);
p->body.exprs = (Value *)ul_malloc_values(sizeof(Value) * body.count);
memcpy(p->body.exprs, body.exprs, sizeof(Value) * body.count);
p->body.count = body.count;
p->env = env;
@ -687,7 +698,7 @@ Proc *make_proc(Value *params, int nparams, Value rest,
* */
Value make_macro(Value transformer) {
ULMacro *m = (ULMacro *)ul_malloc(sizeof(ULMacro));
ULMacro *m = (ULMacro *)ul_malloc_values(sizeof(ULMacro));
m->hdr.type = OBJ_MACRO;
m->transformer = transformer;
return VAL_PTR(m);
@ -703,7 +714,7 @@ Value make_error_object(const char *msg, Value *irritants, int nirr) {
e->message = ul_strdup(msg);
e->nirritants = nirr;
if (nirr > 0) {
e->irritants = (Value *)ul_malloc(sizeof(Value) * nirr);
e->irritants = (Value *)ul_malloc_values(sizeof(Value) * nirr);
memcpy(e->irritants, irritants, sizeof(Value) * nirr);
} else {
e->irritants = NULL;