JIT: restore re-entrancy guards, compile quasiquote inline, closure env merging

Fix infinite recursion in VM JIT: restore sentinel pre-mark in vm_call
and pre-compile loop so recursive compiler functions don't trigger
unbounded compilation cascades. Runtime VM errors fall back to CEK;
compile errors surface visibly (not silently swallowed).

New: compile-quasiquote emits inline code instead of delegating to
qq-expand-runtime. Closure-captured variables merged into VM globals
so compiled closures resolve outer bindings via GLOBAL_GET.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-23 12:22:54 +00:00
parent 1cc3e761a2
commit 2a5ef0ea09
3 changed files with 118 additions and 47 deletions

View File

@@ -81,6 +81,14 @@ let closure_to_value cl =
fun args -> raise (Eval_error ("VM_CLOSURE_CALL:" ^ String.concat "," (List.map Sx_runtime.value_to_str args))))
(* Placeholder — actual calls go through vm_call below *)
let _vm_insn_count = ref 0
let _vm_call_count = ref 0
let _vm_cek_count = ref 0
let vm_reset_counters () = _vm_insn_count := 0; _vm_call_count := 0; _vm_cek_count := 0
let vm_report_counters () =
Printf.eprintf "[vm-perf] insns=%d calls=%d cek_fallbacks=%d\n%!"
!_vm_insn_count !_vm_call_count !_vm_cek_count
(** Main execution loop. *)
let rec run vm =
match vm.frames with
@@ -310,21 +318,15 @@ and vm_call vm f args =
let result = fn args in
push vm result
| Lambda l ->
(* Try JIT-compiled path first *)
(match l.l_compiled with
| Some cl when not (is_jit_failed cl) ->
(* Execute cached bytecode; fall back to CEK on VM error.
Don't mark as failed — the bytecode is correct, the error
is from runtime data (e.g. type mismatch in get). *)
(* Cached bytecode — run on VM, fall back to CEK on runtime error *)
(try push vm (call_closure cl args vm.globals)
with _ -> push vm (Sx_ref.cek_call f (List args)))
| Some _ ->
(* Previously failed or skipped — use CEK *)
(* Compile failed — CEK *)
push vm (Sx_ref.cek_call f (List args))
| None ->
(* Only JIT-compile named lambdas (from define).
Anonymous lambdas (map/filter callbacks) are usually one-shot —
compiling them costs more than interpreting. *)
if l.l_name <> None then begin
(* Pre-mark before compile attempt to prevent re-entrancy *)
l.l_compiled <- Some jit_failed_sentinel;
@@ -338,13 +340,11 @@ and vm_call vm f args =
| None ->
push vm (Sx_ref.cek_call f (List args))
end
else begin
(* Mark anonymous lambdas as skipped to avoid re-checking *)
l.l_compiled <- Some jit_failed_sentinel;
push vm (Sx_ref.cek_call f (List args))
end)
else
push vm (Sx_ref.cek_call f (List args)))
| Component _ | Island _ ->
(* Components use keyword-arg parsing — CEK handles this *)
incr _vm_cek_count;
let result = Sx_ref.cek_call f (List args) in
push vm result
| _ ->
@@ -417,6 +417,28 @@ let jit_compile_lambda (l : lambda) globals =
let fn_expr = List [Symbol "fn"; param_syms; l.l_body] in
let quoted = List [Symbol "quote"; fn_expr] in
let result = Sx_ref.eval_expr (List [compile_fn; quoted]) (Env (make_env ())) in
(* If the lambda has closure-captured variables, merge them into globals
so the VM can find them via GLOBAL_GET. The compiler doesn't know
about the enclosing scope, so closure vars get compiled as globals. *)
let effective_globals =
let closure = l.l_closure in
if Hashtbl.length closure.bindings = 0 && closure.parent = None then
globals (* no closure vars — use globals directly *)
else begin
(* Merge: closure bindings layered on top of globals.
Use a shallow copy so we don't pollute the real globals. *)
let merged = Hashtbl.copy globals in
let rec inject env =
Hashtbl.iter (fun k v -> Hashtbl.replace merged k v) env.bindings;
match env.parent with Some p -> inject p | None -> ()
in
inject closure;
let n = Hashtbl.length merged - Hashtbl.length globals in
if n > 0 then
Printf.eprintf "[jit] %s: injected %d closure bindings\n%!" fn_name n;
merged
end
in
match result with
| Dict d when Hashtbl.mem d "bytecode" ->
let outer_code = code_from_value result in
@@ -427,7 +449,7 @@ let jit_compile_lambda (l : lambda) globals =
let inner_val = outer_code.vc_constants.(idx) in
let code = code_from_value inner_val in
Some { vm_code = code; vm_upvalues = [||];
vm_name = l.l_name; vm_env_ref = globals }
vm_name = l.l_name; vm_env_ref = effective_globals }
else begin
Printf.eprintf "[jit] FAIL %s: closure index %d out of bounds (pool=%d)\n%!"
fn_name idx (Array.length outer_code.vc_constants);