/* * gc.c — precise tracing for NaN-boxed Values under Boehm GC. * * Boehm's conservative scan treats every machine word as a maybe-pointer: * if its bit pattern looks like a heap address, the target stays alive. * Lumbda's Values are NaN-boxed — pointers live in the low 48 bits with * tag bits in the upper mantissa, so the raw word never looks like a * pointer to Boehm. Live Pairs, Strings, Procs, etc. get reclaimed mid * iteration unless we tell Boehm how to walk our Value-bearing buffers. * * We register a custom mark kind. Any allocation that carries Values goes * through lumbda_value_malloc — Boehm tracks the kind on the block & calls * our mark proc when it scans the block. Our proc walks 8-byte words, * decodes the tag, & pushes the underlying pointer for each Value whose * tag identifies a pointer-bearing type. * * Pure-byte allocations (strings, bignum limbs, symbol names) stay on the * normal GC_MALLOC path — Boehm scans them as plain pointers correctly. */ #ifdef USE_BOEHM_GC #include "lumbda.h" #include #include int lumbda_value_kind = -1; static void *lumbda_value_free_list = NULL; /* Stack root scanning: Boehm's conservative scan over the C stack sees * raw 8-byte words & accepts those that fall inside heap bounds. A * NaN-boxed Value living in a C local has its high bits set (QNAN * pattern) so the raw word does NOT look like a valid heap address & * Boehm skips it — the underlying object dies even though the C frame * still holds the Value. We add a second pass via push_other_roots: * walk the same stack range Boehm already tracks, decode any NaN-boxed * Value & push its low-48 payload as a root. main() captures the stack * base at process start; the current SP comes from setjmp inside the * callback. */ static void *g_stack_base = NULL; static GC_push_other_roots_proc g_prev_push_other_roots = NULL; static void lumbda_push_other_roots(void) { if (g_prev_push_other_roots) g_prev_push_other_roots(); if (!g_stack_base) return; /* setjmp serves only to anchor an address (&snap) inside our own * frame — the actual stack walk runs from there up to g_stack_base, * so it covers every C frame above us at the moment GC fired. Lisp * Values held in caller-saved regs are spilled to memory by callers * around any GC-triggering call (cons → make_pair → ul_malloc_values), * so the stack walk catches them too. */ jmp_buf snap; setjmp(snap); Value *sp = (Value *)&snap; Value *base = (Value *)g_stack_base; if (sp > base) { Value *t = sp; sp = base; base = t; } for (Value *p = sp; p < base; p++) { Value v = *p; if ((v & QNAN) != QNAN) continue; uint64_t tag = (v >> TAG_SHIFT) & 7ULL; if (tag == TAG_INT || tag == TAG_SPECIAL) continue; void *ptr = (void *)(uintptr_t)(v & PAYLOAD_MASK); if (!ptr) continue; /* GC_push_all_eager marks the location immediately rather than * deferring to the regular mark stack — safe to point at a * stack-local since the mark happens before we return. */ GC_push_all_eager(&ptr, (char *)&ptr + sizeof(void *)); } } void lumbda_gc_set_stack_base(void *base) { g_stack_base = base; } /* * Mark proc: scan a block of memory containing a mix of NaN-boxed Values * & plain pointer fields (struct headers, embedded raw pointers). * * `addr` points at the head of the block. `GC_size(addr)` gives the block * size in bytes. We treat the block as an array of 8-byte words & decode * each one in two passes: * * 1. NaN-boxed pointer Value: upper QNAN bits set, tag ∈ * {PTR(0), SYM(2), BUILTIN(4), RATIONAL(5), BIGNUM(6)}. Extract the * low 48 bits as the pointer & push that. * 2. Plain raw pointer: anything else. Push the word as-is — * GC_MARK_AND_PUSH validates against heap bounds, so ints, enums, * fixnums, etc. fall outside & are silently skipped. * * Heap addresses on Linux user-space sit below 2^47, so bits 48..63 of a * plain pointer are zero — `(ptr & QNAN) == 0 != QNAN`, never confused * with a NaN-boxed Value. Inversely, a NaN-boxed pointer has bits 48..62 * set & its low 48 hold the real address; treating the raw word as a * pointer in pass 2 would walk into nowhere (heap base + tag bits = bad * address that fails the heap-bounds check anyway). * * This makes the precise kind safe for ANY struct (header + Values + * raw pointers) — strictly a superset of what conservative NORMAL kind * would catch, plus precise NaN-box decoding. */ struct GC_ms_entry * mark_lumbda_value_block(GC_word *addr, struct GC_ms_entry *mark_stack_ptr, struct GC_ms_entry *mark_stack_limit, GC_word env) { (void)env; size_t bytes = GC_size((const void *)addr); size_t nwords = bytes / sizeof(Value); Value *p = (Value *)addr; for (size_t i = 0; i < nwords; i++) { Value v = p[i]; if ((v & QNAN) == QNAN) { uint64_t tag = (v >> TAG_SHIFT) & 7ULL; if (tag == TAG_INT || tag == TAG_SPECIAL) continue; /* NaN-boxed pointer-bearing tag — decode low 48 bits. */ void *ptr = (void *)(uintptr_t)(v & PAYLOAD_MASK); if (!ptr) continue; mark_stack_ptr = GC_MARK_AND_PUSH(ptr, mark_stack_ptr, mark_stack_limit, (void **)&p[i]); } else { /* Not NaN-boxed: treat as raw pointer (or non-pointer int that * the heap-bounds check inside GC_MARK_AND_PUSH will reject). */ mark_stack_ptr = GC_MARK_AND_PUSH((void *)(uintptr_t)v, mark_stack_ptr, mark_stack_limit, (void **)&p[i]); } } return mark_stack_ptr; } void lumbda_gc_init(void) { if (lumbda_value_kind != -1) return; int proc_idx = GC_new_proc(mark_lumbda_value_block); /* GC_new_kind(free_list, mark_descriptor, add_size_to_descriptor, * clear_new_objects) * - mark_descriptor: GC_MAKE_PROC(proc_idx, 0) — call our proc. * - add_size_to_descriptor: 0 — DS_PROC descriptors do not survive * the bdwgc per-object `descr += sz` adjustment; bytes would * overflow into proc_idx bits & dispatch through a NULL proc * slot. The proc derives size via GC_size(addr) instead. * - clear_new_objects: 1 — zero-init so we never decode garbage as * a stale pointer before the caller writes the first Value. */ lumbda_value_kind = GC_new_kind(&lumbda_value_free_list, GC_MAKE_PROC(proc_idx, 0), 0, 1); g_prev_push_other_roots = GC_get_push_other_roots(); GC_set_push_other_roots(lumbda_push_other_roots); } void *lumbda_value_malloc(size_t sz) { if (lumbda_value_kind == -1) lumbda_gc_init(); return GC_generic_malloc(sz, lumbda_value_kind); } #else /* !USE_BOEHM_GC */ #include "lumbda.h" int lumbda_value_kind = -1; void lumbda_gc_init(void) { /* no-op */ } void lumbda_gc_set_stack_base(void *base) { (void)base; } void *lumbda_value_malloc(size_t sz) { return malloc(sz); } #endif