Add JIT closure scoping tests

Tests the exact pattern that broke the home stepper: a component
with letrec bindings referenced inside a map callback. The JIT
compiles the callback with closure vars merged into vm_env_ref.
Subsequent renders must use that env, not the caller's globals.

7 tests covering:
- letrec closure var in map callback (fmt function)
- Render, unrelated render, re-render (env not polluted)
- Signal + letrec + map (the stepper pattern)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 16:13:28 +00:00
parent 2775ce935b
commit 57e7ce9fe4

View File

@@ -7,12 +7,7 @@
Usage:
dune exec bin/integration_tests.exe *)
module Sx_types = Sx.Sx_types
module Sx_parser = Sx.Sx_parser
module Sx_primitives = Sx.Sx_primitives
module Sx_runtime = Sx.Sx_runtime
module Sx_ref = Sx.Sx_ref
module Sx_render = Sx.Sx_render
(* Modules accessed directly — library is unwrapped *)
open Sx_types
@@ -513,6 +508,53 @@ let () =
(reset! s (list 1 2 3))
(len (deref s)))");
(* ================================================================== *)
Printf.printf "\nSuite: JIT closure scoping\n%!";
(* The JIT bug: when a lambda captures closure vars (e.g. from let/letrec),
the VM must use the closure's vm_env_ref (which has the merged bindings),
not the caller's globals (which lacks them). This test reproduces the
exact pattern that broke the home stepper: a component with a letrec
binding referenced inside a map callback. *)
(* 1. Define a component whose body uses letrec + map with closure var *)
assert_no_error "defcomp with letrec+map closure var" (fun () ->
ignore (Sx_ref.eval_expr
(List.hd (Sx_parser.parse_all
"(defcomp ~jit-test (&key)
(let ((items (list \"a\" \"b\" \"c\")))
(letrec ((fmt (fn (x) (str \"[\" x \"]\"))))
(div (map (fn (item) (span (fmt item))) items)))))"))
(Env env)));
(* 2. Render it — this triggers JIT compilation of the map callback *)
assert_contains "jit closure: first render"
"[a]" (sx_render_html "(~jit-test)");
(* 3. Render something ELSE — tests that the JIT-compiled closure
still works when called in a different context *)
assert_contains "jit closure: unrelated render between"
"<p>" (sx_render_html "(p \"hello\")");
(* 4. Render the component AGAIN — the JIT-compiled map callback
must still find 'fmt' via its closure env, not the caller's globals *)
assert_contains "jit closure: second render still works"
"[b]" (sx_render_html "(~jit-test)");
(* 5. Test with signal (the actual stepper pattern) *)
assert_no_error "defcomp with signal+map closure" (fun () ->
ignore (Sx_ref.eval_expr
(List.hd (Sx_parser.parse_all
"(defcomp ~jit-signal-test (&key)
(let ((data (signal (list 1 2 3))))
(letrec ((double (fn (x) (* x 2))))
(div (map (fn (item) (span (str (double item)))) (deref data))))))"))
(Env env)));
assert_contains "jit signal closure: renders" "4" (sx_render_html "(~jit-signal-test)");
assert_contains "jit signal closure: after other render"
"4" (let _ = sx_render_html "(div \"break\")" in sx_render_html "(~jit-signal-test)");
(* ================================================================== *)
Printf.printf "\n";
Printf.printf "============================================================\n";