From 45a90b84e9848ea0a43d16666aff60202299f445 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Thu, 4 Jun 2026 01:11:18 -0400 Subject: [PATCH] c: vm restores cur_code across CALL/RETURN, fixing JIT named-let hang MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five-line fix that ends the F1 hang in foxhop.net ecdsa/tests/unit/probe-c-confirm.lsp. Symptom — under --fast, a defined function whose body is a tail-recursive named-let that calls another user-defined function per iteration loops forever at 100 percent CPU. Trace pins the bytecode dispatch: walk1.body: PUSHE MKCLO DUP BIND LOOKUP TCALL ->loop loop: LOOKUP NULL? JIF LOOKUP CALL ->always-true always-true: CONST RET (returns #t) always-true (!): JIF LOOKUP CDR STAIL -> ip=0 of always-true (!) loop forever cur_code was the call-frame-local register holding the currently executing CodeObj. OP_CALL updated it on entry but neither OP_RETURN nor the builtin-fallback restore path in OP_TAIL_CALL put it back on return. Subsequent OP_SELF_TAIL_CALL read cur_code->self_params from the still-stale callee proc (NULL for always-true since it has no named-let), guard skipped the env rebind, then set ip=0 — without ever updating the loop variable. Loop variable stayed pinned at the initial list and our walk never reached its base case. Fix — VMFrame gains a cur_code field. Three frame-push sites save it on entry (OP_CALL, OP_TAIL_CALL builtin fallback frame-restore, OP_CALL_CC compiled-proc entry); two frame-pop sites restore it on return (OP_RETURN, OP_TAIL_CALL builtin fallback). Verified inside foxhop.net's ecdsa QEMU guest: - foxhop.net/ecdsa/tests/unit/probe-c-confirm.lsp F1..F4 — all pass - foxhop.net/ecdsa/tests/unit/test-sim.lsp under --fast — 21/21 pass - foxhop.net/ecdsa/lumbda/main.lsp under --fast — 6 shots, score 18, byte-identical with our Python tier - make functional-test — 205/205 on Python and C tiers - make regression-named-let-leak — 4/4 across Python, tree-walker, and --fast JIT c/TODO-named-let-bytecode.md can stop applying its `(define (iter ...))` workaround once this lands. --- c/lumbda.h | 1 + c/vm.c | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/c/lumbda.h b/c/lumbda.h index 0533ac5..eebf40e 100644 --- a/c/lumbda.h +++ b/c/lumbda.h @@ -478,6 +478,7 @@ typedef struct VMFrame { Value *stack; int stack_len; int stack_cap; + CodeObj *cur_code; /* saved so RET restores it — needed for SELF_TAIL_CALL after a nested call */ } VMFrame; typedef struct FullCont { diff --git a/c/vm.c b/c/vm.c index 0633a31..83708a3 100644 --- a/c/vm.c +++ b/c/vm.c @@ -798,6 +798,7 @@ Value vm_exec(CodeObj *code, Env *env) { f->instrs = instrs; f->ip = ip; f->n_instrs = n_instrs; f->env = env; f->stack = stack.data; f->stack_len = stack.len; f->stack_cap = stack.cap; + f->cur_code = cur_code; env = env_child(cp->env, cp->params, cp->nparams, cp->rest, args_arr, nargs); cur_code = cp->code; @@ -848,6 +849,7 @@ Value vm_exec(CodeObj *code, Env *env) { env = f->env; ul_free(stack.data); stack.data = f->stack; stack.len = f->stack_len; stack.cap = f->stack_cap; + cur_code = f->cur_code; vs_push(&stack, ret); continue; } @@ -862,6 +864,7 @@ Value vm_exec(CodeObj *code, Env *env) { env = f->env; ul_free(stack.data); stack.data = f->stack; stack.len = f->stack_len; stack.cap = f->stack_cap; + cur_code = f->cur_code; vs_push(&stack, ret); continue; } @@ -915,6 +918,7 @@ Value vm_exec(CodeObj *code, Env *env) { f->instrs = instrs; f->ip = ip; f->n_instrs = n_instrs; f->env = env; f->stack = stack.data; f->stack_len = stack.len; f->stack_cap = stack.cap; + f->cur_code = cur_code; Value kargs[1] = {cont_val}; env = env_child(cp->env, cp->params, cp->nparams, cp->rest, kargs, 1); cur_code = cp->code;