jit: save/restore loop_slots across nested named-let

When inner (let loop ((i ...))) shadows an outer (let loop ((a ...) (b ...))),
the inner named-let block was overwriting j->loop_slots[] without saving
the outer's slot positions. After the inner block restored loop_sym /
loop_nparams / loop_params, the outer's recursive call (loop new-a new-b)
would write the new args into the inner's stale slot positions instead
of the outer's slots, causing the outer body to see stale binding values
or trigger 'set! undefined' on tail-call args.

loop_slots is a member ARRAY (not pointer) — memcpy'd at line 876 when
setting up loop context — so the existing pointer save/restore for
loop_params didn't cover it.

Verified:
- jit_named_let_factorial still PASS (3628800)
- new nested-shadowing test: outer 3-param + 4 inner 1-param now PASS
- exact replica of squaring.lsp round84-fold structure now PASS

Bug surfaced when investigating test-round84-keep-quotient-product
failure in www.foxhop.net/ecdsa tests. Test still has a deeper substrate
bug beyond this JIT fix (width 0 ancilla in non-fast mode), but this
fix is independently correct + closes the loop_slots scope leak.
This commit is contained in:
russell@unturf.com 2026-06-14 18:33:15 -04:00
parent 11470a5fba
commit 0cf66cc3dd
No known key found for this signature in database

16
c/jit.c
View file

@ -839,6 +839,18 @@ static bool emit_expr(JitCtx *j, Value expr, bool tail) {
int saved_loop_start = j->loop_start;
int saved_loop_nparams = j->loop_nparams;
Value *saved_loop_params = j->loop_params;
/* 2026-06-14: loop_slots is an ARRAY (memcpy'd in below); save it
* for nested named-let restore. Without this, inner (let loop ...)
* overwrites outer's slots and outer recursive call writes args to
* inner's slots outer's bindings appear stale "set! undefined"
* or wrong values at recursion. Bug surfaced in squaring.lsp's
* round84-fold-hi-into-lo-aggregate with outer terms-keyed loop +
* 4 inner (let loop ((i 0))). */
int saved_loop_slots[MAX_JIT_LOCALS];
if (saved_loop_nparams > 0 && saved_loop_nparams <= MAX_JIT_LOCALS) {
memcpy(saved_loop_slots, j->loop_slots,
sizeof(int) * saved_loop_nparams);
}
/* Pre-compute stack slots (but don't register yet — inits use outer scope) */
Value loop_var_syms[MAX_JIT_LOCALS];
@ -899,6 +911,10 @@ static bool emit_expr(JitCtx *j, Value expr, bool tail) {
j->loop_start = saved_loop_start;
j->loop_nparams = saved_loop_nparams;
j->loop_params = saved_loop_params;
if (saved_loop_nparams > 0 && saved_loop_nparams <= MAX_JIT_LOCALS) {
memcpy(j->loop_slots, saved_loop_slots,
sizeof(int) * saved_loop_nparams);
}
return true;
}