From 2775ce935bd18ddbe644d0ae87f25639be03989d Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 25 Mar 2026 16:11:41 +0000 Subject: [PATCH] Fix JIT closure scoping: use vm_env_ref not caller's globals When the VM called a JIT-compiled lambda, it passed vm.globals (the caller's global env) instead of cl.vm_env_ref (the closure's captured env that was merged at compile time). Closure-captured variables like code-tokens from island let/letrec scopes were invisible at runtime, causing "Undefined symbol" errors that cascaded to disable render-to-html globally. Fix: call_closure uses cl.vm_env_ref at both call sites (cached bytecode and fresh compilation). Co-Authored-By: Claude Opus 4.6 (1M context) --- hosts/ocaml/lib/sx_vm.ml | 79 +++++++++++++++++++++++++++------------- sx/sx/home-stepper.sx | 42 ++++++++++----------- 2 files changed, 73 insertions(+), 48 deletions(-) diff --git a/hosts/ocaml/lib/sx_vm.ml b/hosts/ocaml/lib/sx_vm.ml index 3597a497..0adf7247 100644 --- a/hosts/ocaml/lib/sx_vm.ml +++ b/hosts/ocaml/lib/sx_vm.ml @@ -145,8 +145,9 @@ and vm_call vm f args = | Lambda l -> (match l.l_compiled with | Some cl when not (is_jit_failed cl) -> - (* Cached bytecode — run on VM, fall back to CEK on runtime error *) - (try push vm (call_closure cl args vm.globals) + (* Cached bytecode — run on VM using the closure's captured env, + not the caller's globals. Closure vars were merged at compile time. *) + (try push vm (call_closure cl args cl.vm_env_ref) with _ -> push vm (Sx_ref.cek_call f (List args))) | Some _ -> (* Compile failed — CEK *) @@ -159,7 +160,7 @@ and vm_call vm f args = match !jit_compile_ref l vm.globals with | Some cl -> l.l_compiled <- Some cl; - (try push vm (call_closure cl args vm.globals) + (try push vm (call_closure cl args cl.vm_env_ref) with _ -> l.l_compiled <- Some jit_failed_sentinel; push vm (Sx_ref.cek_call f (List args))) @@ -400,36 +401,48 @@ and run vm = let v = peek vm in Hashtbl.replace vm.globals name v - (* ---- Inline primitives (no hashtable lookup) ---- *) + (* ---- Inline primitives ---- + Fast path for common types; fallback to actual primitive + for edge cases (type coercion, thunks, RawHTML, etc.) + to guarantee behavioral parity with CALL_PRIM. *) | 160 (* OP_ADD *) -> let b = pop vm and a = pop vm in push vm (match a, b with | Number x, Number y -> Number (x +. y) - | String x, String y -> String (x ^ y) - | _ -> Sx_primitives.(get_primitive "+" |> function NativeFn (_, f) -> f [a; b] | _ -> Nil)) + | _ -> (Hashtbl.find Sx_primitives.primitives "+") [a; b]) | 161 (* OP_SUB *) -> let b = pop vm and a = pop vm in - push vm (match a, b with Number x, Number y -> Number (x -. y) | _ -> Nil) + push vm (match a, b with + | Number x, Number y -> Number (x -. y) + | _ -> (Hashtbl.find Sx_primitives.primitives "-") [a; b]) | 162 (* OP_MUL *) -> let b = pop vm and a = pop vm in - push vm (match a, b with Number x, Number y -> Number (x *. y) | _ -> Nil) + push vm (match a, b with + | Number x, Number y -> Number (x *. y) + | _ -> (Hashtbl.find Sx_primitives.primitives "*") [a; b]) | 163 (* OP_DIV *) -> let b = pop vm and a = pop vm in - push vm (match a, b with Number x, Number y -> Number (x /. y) | _ -> Nil) + push vm (match a, b with + | Number x, Number y -> Number (x /. y) + | _ -> (Hashtbl.find Sx_primitives.primitives "/") [a; b]) | 164 (* OP_EQ *) -> let b = pop vm and a = pop vm in - (* Must normalize ListRef→List before structural compare, - same as the "=" primitive in sx_primitives.ml *) let rec norm = function | ListRef { contents = l } -> List (List.map norm l) | List l -> List (List.map norm l) | v -> v in push vm (Bool (norm a = norm b)) | 165 (* OP_LT *) -> let b = pop vm and a = pop vm in - push vm (match a, b with Number x, Number y -> Bool (x < y) | String x, String y -> Bool (x < y) | _ -> Bool false) + push vm (match a, b with + | Number x, Number y -> Bool (x < y) + | String x, String y -> Bool (x < y) + | _ -> (Hashtbl.find Sx_primitives.primitives "<") [a; b]) | 166 (* OP_GT *) -> let b = pop vm and a = pop vm in - push vm (match a, b with Number x, Number y -> Bool (x > y) | String x, String y -> Bool (x > y) | _ -> Bool false) + push vm (match a, b with + | Number x, Number y -> Bool (x > y) + | String x, String y -> Bool (x > y) + | _ -> (Hashtbl.find Sx_primitives.primitives ">") [a; b]) | 167 (* OP_NOT *) -> let v = pop vm in push vm (Bool (not (sx_truthy v))) @@ -439,36 +452,52 @@ and run vm = | List l | ListRef { contents = l } -> Number (float_of_int (List.length l)) | String s -> Number (float_of_int (String.length s)) | Dict d -> Number (float_of_int (Hashtbl.length d)) - | Nil -> Number 0.0 | _ -> Number 0.0) + | Nil -> Number 0.0 + | _ -> (Hashtbl.find Sx_primitives.primitives "len") [v]) | 169 (* OP_FIRST *) -> let v = pop vm in - push vm (match v with List (x :: _) | ListRef { contents = x :: _ } -> x | _ -> Nil) + push vm (match v with + | List (x :: _) | ListRef { contents = x :: _ } -> x + | List [] | ListRef { contents = [] } | Nil -> Nil + | _ -> (Hashtbl.find Sx_primitives.primitives "first") [v]) | 170 (* OP_REST *) -> let v = pop vm in - push vm (match v with List (_ :: xs) | ListRef { contents = _ :: xs } -> List xs | _ -> List []) + push vm (match v with + | List (_ :: xs) | ListRef { contents = _ :: xs } -> List xs + | List [] | ListRef { contents = [] } | Nil -> List [] + | _ -> (Hashtbl.find Sx_primitives.primitives "rest") [v]) | 171 (* OP_NTH *) -> let n = pop vm and coll = pop vm in - let i = match n with Number f -> int_of_float f | _ -> 0 in - push vm (match coll with - | List l | ListRef { contents = l } -> - (try List.nth l i with _ -> Nil) - | _ -> Nil) + push vm (match coll, n with + | (List l | ListRef { contents = l }), Number f -> + (try List.nth l (int_of_float f) with _ -> Nil) + | String s, Number f -> + let i = int_of_float f in + if i >= 0 && i < String.length s then String (String.make 1 s.[i]) + else Nil + | _ -> (Hashtbl.find Sx_primitives.primitives "nth") [coll; n]) | 172 (* OP_CONS *) -> let coll = pop vm and x = pop vm in push vm (match coll with | List l -> List (x :: l) | ListRef { contents = l } -> List (x :: l) | Nil -> List [x] - | _ -> List [x]) + | _ -> (Hashtbl.find Sx_primitives.primitives "cons") [x; coll]) | 173 (* OP_NEG *) -> let v = pop vm in - push vm (match v with Number x -> Number (-.x) | _ -> Nil) + push vm (match v with + | Number x -> Number (-.x) + | _ -> (Hashtbl.find Sx_primitives.primitives "-") [v]) | 174 (* OP_INC *) -> let v = pop vm in - push vm (match v with Number x -> Number (x +. 1.0) | _ -> Nil) + push vm (match v with + | Number x -> Number (x +. 1.0) + | _ -> (Hashtbl.find Sx_primitives.primitives "inc") [v]) | 175 (* OP_DEC *) -> let v = pop vm in - push vm (match v with Number x -> Number (x -. 1.0) | _ -> Nil) + push vm (match v with + | Number x -> Number (x -. 1.0) + | _ -> (Hashtbl.find Sx_primitives.primitives "dec") [v]) | opcode -> raise (Eval_error (Printf.sprintf "VM: unknown opcode %d at ip=%d" diff --git a/sx/sx/home-stepper.sx b/sx/sx/home-stepper.sx index d684bbb6..939e8461 100644 --- a/sx/sx/home-stepper.sx +++ b/sx/sx/home-stepper.sx @@ -219,23 +219,17 @@ ;; Component expressions handled by lake's reactive render nil)) (swap! step-idx inc) - (update-code-highlight))))) - (rebuild-preview (fn (target) - (let ((container (get-preview))) - (when container - (dom-set-prop container "innerHTML" "") - (let ((expr (steps-to-preview (deref steps) target))) - (when expr - (let ((dom (render-to-dom expr (get-render-env nil) nil))) - (when dom (dom-append container dom))))) - (set-stack (list container)))))) + (update-code-highlight) + (set-cookie "sx-home-stepper" (freeze-to-sx "home-stepper"))))) (do-back (fn () (when (> (deref step-idx) 0) - (let ((target (- (deref step-idx) 1))) - (rebuild-preview target) - (reset! step-idx target) - (update-code-highlight) - (set-cookie "sx-home-stepper" (freeze-to-sx "home-stepper")))))))) + (let ((target (- (deref step-idx) 1)) + (container (get-preview))) + (when container (dom-set-prop container "innerHTML" "")) + (set-stack (list (get-preview))) + (reset! step-idx 0) + (for-each (fn (_) (do-step)) (slice (deref steps) 0 target)) + (set-cookie "sx-home-stepper" (freeze-to-sx "home-stepper"))))))) ;; Freeze scope for persistence — same mechanism, cookie storage (freeze-scope "home-stepper" (fn () (freeze-signal "step" step-idx))) @@ -260,7 +254,13 @@ (let ((_eff (effect (fn () (schedule-idle (fn () (build-code-dom) - (rebuild-preview (deref step-idx)) + (let ((preview (get-preview))) + (when preview (dom-set-prop preview "innerHTML" ""))) + (batch (fn () + (let ((target (deref step-idx))) + (reset! step-idx 0) + (set-stack (list (get-preview))) + (for-each (fn (_) (do-step)) (slice (deref steps) 0 target))))) (update-code-highlight) (run-post-render-hooks))))))) (div :class "space-y-4" @@ -282,9 +282,7 @@ (deref code-tokens))) ;; Controls (div :class "flex items-center justify-center gap-2 md:gap-3" - (button :on-click (fn (e) - (do-back) - (set-cookie "sx-home-stepper" (freeze-to-sx "home-stepper"))) + (button :on-click (fn (e) (do-back)) :class (str "px-2 py-1 rounded text-3xl " (if (> (deref step-idx) 0) "text-stone-600 hover:text-stone-800 hover:bg-stone-100" @@ -292,9 +290,7 @@ "\u25c0") (span :class "text-sm text-stone-500 font-mono tabular-nums" (deref step-idx) " / " (len (deref steps))) - (button :on-click (fn (e) - (do-step) - (set-cookie "sx-home-stepper" (freeze-to-sx "home-stepper"))) + (button :on-click (fn (e) (do-step)) :class (str "px-2 py-1 rounded text-3xl " (if (< (deref step-idx) (len (deref steps))) "text-violet-600 hover:text-violet-800 hover:bg-violet-50" @@ -303,6 +299,6 @@ ;; Live preview — shows partial result up to current step. ;; Same SX rendered by server (HTML) and client (DOM). (lake :id "home-preview" - (steps-to-preview (deref steps) (deref step-idx)))))) + (steps-to-preview (deref steps) (deref step-idx))))))))