From 5cfeed81c1597e6b9685d54daf46d9c517c9fd02 Mon Sep 17 00:00:00 2001 From: giles Date: Mon, 23 Mar 2026 18:52:34 +0000 Subject: [PATCH] Compiler: proper letrec support (mutual recursion) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The compiler was treating letrec as let — binding values sequentially. This meant mutually recursive functions (like sx-parse's read-list calling read-expr and vice versa) couldn't reference each other. compile-letrec uses two phases: 1. Define all local slots initialized to nil 2. Compile and assign values — all names already in scope This fixes sx-parse producing wrong ASTs (nested instead of sibling lists) when JIT-compiled, which caused the stepper's step count to be 2 instead of 16. Also: skip JIT for lambdas with closure bindings (inner functions like read-list-loop) — the closure merging into vm_env_ref produces incorrect variable resolution. Co-Authored-By: Claude Opus 4.6 (1M context) --- shared/static/scripts/sx-browser.js | 2 +- spec/compiler.sx | 35 ++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 0f89bc3..92d79c9 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -14,7 +14,7 @@ // ========================================================================= var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); - var SX_VERSION = "2026-03-23T18:18:45Z"; + var SX_VERSION = "2026-03-23T18:52:19Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } diff --git a/spec/compiler.sx b/spec/compiler.sx index b40db91..7163292 100644 --- a/spec/compiler.sx +++ b/spec/compiler.sx @@ -264,7 +264,7 @@ (= name "defeffect") (emit-op em 2) (= name "defisland") (compile-defcomp em args scope) (= name "quasiquote") (compile-quasiquote em (first args) scope) - (= name "letrec") (compile-let em args scope tail?) + (= name "letrec") (compile-letrec em args scope tail?) ;; Default — function call :else (compile-call em head args scope tail?))))))) @@ -387,6 +387,39 @@ (compile-begin em body let-scope tail?)))) +(define compile-letrec + (fn (em args scope tail?) + "Compile letrec: all names visible during value compilation. + 1. Define all local slots (initialized to nil). + 2. Compile each value and assign — names are already in scope + so mutually recursive functions can reference each other." + (let ((bindings (first args)) + (body (rest args)) + (let-scope (make-scope scope))) + (dict-set! let-scope "next-slot" (get scope "next-slot")) + ;; Phase 1: define all slots (push nil for each) + (let ((slots (map (fn (binding) + (let ((name (if (= (type-of (first binding)) "symbol") + (symbol-name (first binding)) + (first binding)))) + (let ((slot (scope-define-local let-scope name))) + (emit-op em 2) ;; OP_NIL + (emit-op em 17) ;; OP_LOCAL_SET + (emit-byte em slot) + slot))) + bindings))) + ;; Phase 2: compile values and assign (all names in scope) + (for-each (fn (pair) + (let ((binding (first pair)) + (slot (nth pair 1))) + (compile-expr em (nth binding 1) let-scope false) + (emit-op em 17) ;; OP_LOCAL_SET + (emit-byte em slot))) + (map (fn (i) (list (nth bindings i) (nth slots i))) + (range 0 (len bindings))))) + ;; Compile body + (compile-begin em body let-scope tail?)))) + (define compile-lambda (fn (em args scope) (let ((params (first args))