lumbda/c/Makefile
russell@unturf.com b841b30bc4
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
2026-06-07 17:18:45 -04:00

49 lines
1.3 KiB
Makefile

# Makefile for lumbda C interpreter
# Targets: all, test, bench, clean
CC = gcc
CFLAGS = -O2 -Wall -Wextra -Wno-unused-parameter -std=c11 -D_POSIX_C_SOURCE=200809L -D_GNU_SOURCE
LDFLAGS = -lm
# Boehm GC autodetect — enable when /usr/include/gc.h is present.
# Without GC, every allocation leaks (ul_free is a no-op in our header);
# small REPL snippets work but anything iterating past a few thousand
# allocations OOMs the process. The named-let + per-iteration user-fn
# call pattern in tests/regression/named-let-gc.lsp pins this down.
# Override with USE_GC=0 to force the malloc-only path for diagnostics.
USE_GC ?= $(shell test -f /usr/include/gc.h && echo 1 || echo 0)
ifeq ($(USE_GC),1)
CFLAGS += -DUSE_BOEHM_GC
LDFLAGS += -lgc
GC_SRCS = gc.c
else
GC_SRCS =
endif
SRCS = types.c bignum.c reader.c printer.c eval.c builtins.c vm.c jit.c portal.c $(GC_SRCS)
OBJS = $(SRCS:.c=.o)
.PHONY: all clean test bench
all: lumbda
lumbda: main.o $(OBJS)
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
test: test_runner
./test_runner
test_runner: test.c $(OBJS)
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
bench: bench_runner lumbda
ulimit -s 65536 && ./bench_runner
bench_runner: bench.c $(OBJS)
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
%.o: %.c lumbda.h
$(CC) $(CFLAGS) -c -o $@ $<
clean:
rm -f *.o lumbda test_runner bench_runner