c: vm restores cur_code across CALL/RETURN, fixing JIT named-let hang

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.
This commit is contained in:
russell@unturf.com 2026-06-04 01:11:18 -04:00
parent 88c4b05032
commit 45a90b84e9
No known key found for this signature in database
2 changed files with 5 additions and 0 deletions

View file

@ -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 {

4
c/vm.c
View file

@ -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;