lumbda/c/main.c
russell@unturf.com 88c4b05032
c: enable Boehm GC by default, GC_INIT, file ops, regression test
Three coupled changes that unblock the ecdsa quantum-circuit
simulator's run on the C tier from neoblanka.

1. c/Makefile autodetects libgc-dev — if /usr/include/gc.h is
   present, the build links Boehm and defines USE_BOEHM_GC. Without
   GC, ul_free is a no-op (lumbda.h:35) and every allocation leaks;
   small REPL snippets work but workloads with thousands of envs
   OOM the process. Override with USE_GC=0 to force the malloc-only
   path for diagnostics.

2. c/main.c calls GC_INIT before init_symbols, then GC_disable.
   GC_INIT registers the stack base for conservative scan — without
   it some Linux configs miss roots. GC_disable is a deliberate
   stopgap: lumbda Values are NaN-boxed pointers that conservative
   Boehm cannot recognize as pointers, so live targets get reclaimed
   (env binding symbol payloads, SymbolEntry strings) and lookups
   fail with "undefined: <sym>". Reproducing this without GC_disable
   on the GC build: any sim.lsp call chain triggers the corruption
   after ~100 named-let iterations. Until tracing is precise,
   growing the heap is safer than wrong results. Long-running
   workloads run under ulimit -v.

3. c/builtins.c gains rename-file and delete-file matching the
   Python tier (lumbda.py:3468). sim.lsp's write-portal! pattern
   (write to .tmp, rename) needs rename-file to land cross-tier
   identical results.

4. tests/regression-named-let-leak.lsp + .sh pin four shapes that
   blew up ecdsa: the c/TODO-named-let-bytecode.md repro, the F1
   shape from foxhop.net's lumbda-c-tier-leak-SP.md (12-line
   minimum), a 200-iter scaled variant, and a sim.lsp run-ops!
   mirror. Wired into root Makefile as regression-named-let-leak;
   added to test-all. Wrapper caps memory at 256 MB virt and 15s
   per tier so a leak regression fails the run instead of consuming
   host RAM.

Known limits:
- --fast JIT still has the named-let + inner user-fn call hang
  (separate TODO; tree-walker handles this fine).
- GC_disable means the heap grows; workloads must bound their work
  budget. ecdsa's sim runs comfortably in 5 MB.

Verified inside a 2G/2vCPU QEMU guest (foxhop.net ecdsa/vm-runner.sh):
- test-c (tree-walker) — 35/35 PASS
- bench-c (tree-walker) — score 18 matches Python tier byte-identical
- F1 probe (tree-walker) — all four steps PASS
2026-06-04 00:55:31 -04:00

266 lines
9.4 KiB
C

/*
* main.c — REPL, script mode, -e mode
*/
#include "lumbda.h"
#include "jit.h"
/* ═══════════════════════════════════════════════════════════════════════════
* Find stdlib.lsp relative to the executable
* ═══════════════════════════════════════════════════════════════════════════ */
static char *find_stdlib(const char *argv0) {
/* Try relative to executable: ../stdlib.lsp */
char path[4096];
/* Try via /proc/self/exe on Linux */
ssize_t n = readlink("/proc/self/exe", path, sizeof(path) - 1);
if (n > 0) {
path[n] = '\0';
/* Go up one directory (from c/lumbda to lumbda/) */
char *slash = strrchr(path, '/');
if (slash) {
*slash = '\0';
slash = strrchr(path, '/');
if (slash) {
slash[1] = '\0';
strcat(path, "stdlib.lsp");
if (access(path, R_OK) == 0) return ul_strdup(path);
}
}
}
/* Try current directory */
if (access("stdlib.lsp", R_OK) == 0) return ul_strdup("stdlib.lsp");
/* Try relative to argv0 */
if (argv0) {
const char *slash = strrchr(argv0, '/');
if (slash) {
size_t dir_len = slash - argv0;
snprintf(path, sizeof(path), "%.*s/../stdlib.lsp", (int)dir_len, argv0);
if (access(path, R_OK) == 0) return ul_strdup(path);
}
}
return NULL;
}
/* ═══════════════════════════════════════════════════════════════════════════
* REPL
* ═══════════════════════════════════════════════════════════════════════════ */
static void repl(Env *env) {
char *version_str = show(env_lookup(env, intern("*version*")), true);
printf("lumbda %s (C) — (exit) to quit, (load \"file.lsp\") to load\n", version_str);
ul_free(version_str);
char buf[16384];
buf[0] = '\0';
size_t buf_len = 0;
while (1) {
printf(buf_len > 0 ? " " : "λ> ");
fflush(stdout);
char line[4096];
if (!fgets(line, sizeof(line), stdin)) {
if (buf_len > 0) { buf[0] = '\0'; buf_len = 0; printf("\n"); continue; }
printf("\n");
break;
}
size_t line_len = strlen(line);
if (buf_len + line_len >= sizeof(buf) - 1) {
buf[0] = '\0'; buf_len = 0; continue;
}
memcpy(buf + buf_len, line, line_len);
buf_len += line_len;
buf[buf_len] = '\0';
/* Check for balanced parens */
int depth = 0;
for (size_t i = 0; i < buf_len; i++) {
if (buf[i] == '(') depth++;
else if (buf[i] == ')') depth--;
}
if (depth > 0) continue;
/* Try to parse */
int count;
Value *exprs;
TRY(ctx) {
exprs = read_all(buf, &count, false);
} CATCH {
if (depth > 0) continue;
buf[0] = '\0'; buf_len = 0;
continue;
} ENDTRY;
if (count == 0) { buf[0] = '\0'; buf_len = 0; continue; }
for (int i = 0; i < count; i++) {
TRY(ctx) {
Value result = leval(exprs[i], env);
if (!IS_VOID(result)) {
print_value(result, false, stdout);
printf("\n");
}
} CATCH {
fprintf(stderr, "error: %s\n", ctx.message);
} ENDTRY;
}
ul_free(exprs);
buf[0] = '\0'; buf_len = 0;
}
}
/* ═══════════════════════════════════════════════════════════════════════════
* Main
* ═══════════════════════════════════════════════════════════════════════════ */
int main(int argc, char **argv) {
#ifdef USE_BOEHM_GC
/* GC_INIT registers stack base for conservative scan. Without it,
* roots can be missed on some Linux configs.
*
* GC_disable is a deliberate stopgap: lumbda Values are NaN-boxed
* pointers that conservative Boehm cannot recognize as pointers,
* so live targets get reclaimed (env binding symbol payloads,
* SymbolEntry strings) and lookups fail with "undefined: <sym>".
* Until tracing is precise, growing the heap is safer than wrong
* results. Long-running workloads should run under ulimit -v.
*/
GC_INIT();
GC_disable();
#endif
init_symbols();
Env *g = make_global_env();
/* Load prelude */
{
int count;
Value *exprs = read_all(PRELUDE, &count, false);
for (int i = 0; i < count; i++) leval(exprs[i], g);
ul_free(exprs);
}
/* Parse args */
int argi = 1;
/* Default stays tree-walker for now. Flipping to --fast as default
* exposed a cumulative-state buffer overflow in the bytecode
* compiler that only triggers across the full 189-test functional
* suite, not in isolated scripts. Tracked as a TODO. For deeply
* recursive workloads (e.g. ackermann(3,8)) pass --fast explicitly
* or `ulimit -s unlimited` before invoking the tree-walker. */
bool fast = false;
bool help = false;
bool version = false;
bool jit = false;
const char *eval_expr = NULL;
while (argi < argc) {
if (strcmp(argv[argi], "--jit") == 0 || strcmp(argv[argi], "-j") == 0) {
jit = true; argi++;
} else if (strcmp(argv[argi], "--fast") == 0 || strcmp(argv[argi], "-f") == 0) {
fast = true; argi++;
} else if (strcmp(argv[argi], "--help") == 0 || strcmp(argv[argi], "-h") == 0) {
help = true; argi++;
} else if (strcmp(argv[argi], "--version") == 0 || strcmp(argv[argi], "-v") == 0) {
version = true; argi++;
} else if (strcmp(argv[argi], "-e") == 0 && argi + 1 < argc) {
eval_expr = argv[argi + 1]; argi += 2;
} else {
break;
}
}
if (help) {
printf("lumbda — a Scheme interpreter in C\n\n"
"Usage: lumbda [options] [script.lsp] [args...]\n"
" lumbda -e '(+ 1 2)'\n"
" lumbda (interactive REPL)\n\n"
"Options:\n"
" -e EXPR evaluate expression and print result\n"
" -f, --fast auto-compile all defines (bytecode VM;\n"
" required for deep recursion, e.g. ackermann(3,8))\n"
" -j, --jit enable x86_64 JIT compiler for eligible functions\n"
" -h, --help show this help\n"
" -v, --version show version\n\n"
"Features: R7RS core, bytecode compiler, macros, syntax-rules,\n"
" modules, rationals, string ports, x86_64 JIT.\n");
return 0;
}
if (version) {
printf("lumbda 1.0.0 (C)\n");
return 0;
}
if (fast) g_auto_compile = true;
if (jit) g_jit_enabled = true;
/* Load stdlib.lsp if it exists */
char *stdlib_path = find_stdlib(argv[0]);
if (stdlib_path) {
TRY(ctx) {
load_file(stdlib_path, g);
} CATCH {
/* Ignore stdlib load errors — it's optional */
} ENDTRY;
ul_free(stdlib_path);
}
/* -e mode */
if (eval_expr) {
TRY(ctx) {
int count;
Value *exprs = read_all(eval_expr, &count, false);
for (int i = 0; i < count; i++) {
Value result = leval(exprs[i], g);
if (!IS_VOID(result)) {
print_value(result, false, stdout);
printf("\n");
}
}
ul_free(exprs);
} CATCH {
fprintf(stderr, "error: %s\n", g_error_ctx ? g_error_ctx->message : "unknown");
return 1;
} ENDTRY;
return 0;
}
/* Script mode */
if (argi < argc) {
const char *path = argv[argi];
/* Set *argv* */
int script_argc = argc - argi - 1;
Value *script_argv = (Value *)ul_malloc(sizeof(Value) * script_argc);
for (int i = 0; i < script_argc; i++) {
script_argv[i] = make_string_from_cstr(argv[argi + 1 + i]);
}
env_define(g, intern("*argv*"), list_to_value(script_argv, script_argc));
ul_free(script_argv);
TRY(ctx) {
load_file(path, g);
} CATCH {
fprintf(stderr, "error: %s\n", ctx.message);
return 1;
} ENDTRY;
return 0;
}
/* REPL mode */
ErrorContext root_ctx;
root_ctx.call_stack_depth = 0;
root_ctx.error_obj = VAL_NIL;
root_ctx.source_line = 0;
g_error_ctx = &root_ctx;
if (setjmp(root_ctx.jmp) != 0) {
fprintf(stderr, "fatal error: %s\n", root_ctx.message);
return 1;
}
repl(g);
return 0;
}