Fix letrec thunk resolution + compiler letrec support + closure JIT check

Root cause: sf-letrec returns a thunk (for TCO) but the CEK dispatch
wrapped it as a value without evaluating. The thunk leaked as the
return value of letrec expressions, breaking sx-parse and any function
using letrec.

Fix: step-sf-letrec unwraps the thunk into a CEK state, so the last
letrec body expression is properly evaluated by the CEK machine.

Also:
- compile-letrec: two-phase (nil-init then assign) for mutual recursion
- Skip JIT for inner functions (closure.bindings != globals) in both
  vm_call and JIT hook
- vm-reset-fn for sx-parse removed (no longer needed)
- Parser regression test: letrec with mutable pos + recursive sublists

Test results: JS 943/17, OCaml 955/0, Python 747/0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-23 21:04:47 +00:00
parent ffe849df8e
commit 30cfbf777a
7 changed files with 72 additions and 12 deletions

View File

@@ -896,6 +896,14 @@
;; (call-thunk f env) — call a zero-arg function
;; --------------------------------------------------------------------------
;; step-sf-letrec: sf-letrec evaluates bindings + intermediate body,
;; returns a thunk for the last body expression. Unwrap into CEK state
;; so the last expression is properly evaluated by the CEK machine.
(define step-sf-letrec
(fn (args env kont)
(let ((thk (sf-letrec args env)))
(make-cek-state (thunk-expr thk) (thunk-env thk) kont))))
(define sf-dynamic-wind
(fn ((args :as list) (env :as dict))
(let ((before (trampoline (eval-expr (first args) env)))
@@ -1215,7 +1223,7 @@
(= name "quasiquote") (make-cek-value (qq-expand (first args) env) env kont)
(= name "->") (step-sf-thread-first args env kont)
(= name "set!") (step-sf-set! args env kont)
(= name "letrec") (make-cek-value (sf-letrec args env) env kont)
(= name "letrec") (step-sf-letrec args env kont)
;; Continuations — native in CEK
(= name "reset") (step-sf-reset args env kont)

View File

@@ -535,3 +535,36 @@
(deftest "parse nil is not a symbol"
(let ((result (first (sx-parse "nil"))))
(assert-nil result))))
;; --------------------------------------------------------------------------
;; JIT regression: mutable pos shared via upvalues across recursive calls
;; --------------------------------------------------------------------------
(defsuite "parser-jit-regression"
(deftest "letrec parser with mutable pos — recursive sublists"
;; Minimal reproducer for the sx-parse JIT bug.
;; Uses define inside fn (like sx-parse's read-list-loop pattern).
(let ((parse-fn (fn (src)
(let ((pos 0))
(letrec
((read-list (fn ()
(let ((result (list))
(done false))
(define go (fn ()
(when (and (not done) (< pos (len src)))
(let ((ch (nth src pos)))
(set! pos (inc pos))
(cond
(= ch ")") (set! done true)
(= ch "(") (do (append! result (read-list)) (go))
:else (do (append! result ch) (go)))))))
(go)
result))))
(set! pos 1)
(read-list))))))
(let ((r (parse-fn "(a(b)(c))")))
(assert (list? r) (str "result should be list, got type=" (type-of r)))
(assert-equal 3 (len r))
(assert-equal (list "a" (list "b") (list "c")) r))))
)