VM: closure env chain for GLOBAL_GET/SET + remove JIT skip
vm_closure now stores the original closure env (vm_closure_env). GLOBAL_GET walks the closure env chain when the variable isn't in vm.globals. GLOBAL_SET writes to the correct env in the chain. This enables JIT compilation of all named functions regardless of closure depth. No more closure skip check needed. Pre-compile time back to ~7s (was 37s with closure skip). Note: sx-parse sibling list parsing still has issues — the root cause is in how the JIT-compiled letrec + OP_CLOSURE interacts with the upvalue cell mechanism. Investigation ongoing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -913,9 +913,6 @@ let register_jit_hook env =
|
|||||||
| Some _ -> None (* compile failed — CEK handles *)
|
| Some _ -> None (* compile failed — CEK handles *)
|
||||||
| None ->
|
| None ->
|
||||||
if !_jit_compiling then None
|
if !_jit_compiling then None
|
||||||
else if l.l_closure.bindings != env.bindings then
|
|
||||||
(* Skip JIT for inner functions — closure != globals *)
|
|
||||||
None
|
|
||||||
else begin
|
else begin
|
||||||
let fn_name = match l.l_name with Some n -> n | None -> "?" in
|
let fn_name = match l.l_name with Some n -> n | None -> "?" in
|
||||||
_jit_compiling := true;
|
_jit_compiling := true;
|
||||||
@@ -1300,7 +1297,7 @@ let rec dispatch env cmd =
|
|||||||
try
|
try
|
||||||
Sx_vm.call_closure
|
Sx_vm.call_closure
|
||||||
{ vm_code = code; vm_upvalues = [||]; vm_name = lam.l_name;
|
{ vm_code = code; vm_upvalues = [||]; vm_name = lam.l_name;
|
||||||
vm_env_ref = live_env }
|
vm_env_ref = live_env; vm_closure_env = None }
|
||||||
args live_env
|
args live_env
|
||||||
with
|
with
|
||||||
| _ ->
|
| _ ->
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ and vm_closure = {
|
|||||||
vm_upvalues : vm_upvalue_cell array;
|
vm_upvalues : vm_upvalue_cell array;
|
||||||
vm_name : string option;
|
vm_name : string option;
|
||||||
vm_env_ref : (string, value) Hashtbl.t;
|
vm_env_ref : (string, value) Hashtbl.t;
|
||||||
|
vm_closure_env : env option; (** Original closure env for inner functions *)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ let jit_compile_ref : (lambda -> (string, value) Hashtbl.t -> vm_closure option)
|
|||||||
Prevents retrying compilation on every call. *)
|
Prevents retrying compilation on every call. *)
|
||||||
let jit_failed_sentinel = {
|
let jit_failed_sentinel = {
|
||||||
vm_code = { vc_arity = -1; vc_locals = 0; vc_bytecode = [||]; vc_constants = [||] };
|
vm_code = { vc_arity = -1; vc_locals = 0; vc_bytecode = [||]; vc_constants = [||] };
|
||||||
vm_upvalues = [||]; vm_name = Some "__jit_failed__"; vm_env_ref = Hashtbl.create 0
|
vm_upvalues = [||]; vm_name = Some "__jit_failed__"; vm_env_ref = Hashtbl.create 0; vm_closure_env = None
|
||||||
}
|
}
|
||||||
|
|
||||||
let is_jit_failed cl = cl.vm_code.vc_arity = -1
|
let is_jit_failed cl = cl.vm_code.vc_arity = -1
|
||||||
@@ -154,14 +154,35 @@ let rec run vm =
|
|||||||
let idx = read_u16 frame in
|
let idx = read_u16 frame in
|
||||||
let name = match consts.(idx) with String s -> s | _ -> "" in
|
let name = match consts.(idx) with String s -> s | _ -> "" in
|
||||||
let v = try Hashtbl.find vm.globals name with Not_found ->
|
let v = try Hashtbl.find vm.globals name with Not_found ->
|
||||||
try Sx_primitives.get_primitive name
|
(* Walk the closure env chain for inner functions *)
|
||||||
with _ -> raise (Eval_error ("VM undefined: " ^ name))
|
let rec env_lookup e =
|
||||||
|
try Hashtbl.find e.bindings name
|
||||||
|
with Not_found ->
|
||||||
|
match e.parent with Some p -> env_lookup p | None ->
|
||||||
|
try Sx_primitives.get_primitive name
|
||||||
|
with _ -> raise (Eval_error ("VM undefined: " ^ name))
|
||||||
|
in
|
||||||
|
match frame.closure.vm_closure_env with
|
||||||
|
| Some env -> env_lookup env
|
||||||
|
| None ->
|
||||||
|
try Sx_primitives.get_primitive name
|
||||||
|
with _ -> raise (Eval_error ("VM undefined: " ^ name))
|
||||||
in
|
in
|
||||||
push vm v; run vm
|
push vm v; run vm
|
||||||
| 21 (* OP_GLOBAL_SET *) ->
|
| 21 (* OP_GLOBAL_SET *) ->
|
||||||
let idx = read_u16 frame in
|
let idx = read_u16 frame in
|
||||||
let name = match consts.(idx) with String s -> s | _ -> "" in
|
let name = match consts.(idx) with String s -> s | _ -> "" in
|
||||||
Hashtbl.replace vm.globals name (peek vm);
|
(* Write to closure env if the name exists there (mutable closure vars) *)
|
||||||
|
let written = match frame.closure.vm_closure_env with
|
||||||
|
| Some env ->
|
||||||
|
let rec find_env e =
|
||||||
|
if Hashtbl.mem e.bindings name then
|
||||||
|
(Hashtbl.replace e.bindings name (peek vm); true)
|
||||||
|
else match e.parent with Some p -> find_env p | None -> false
|
||||||
|
in find_env env
|
||||||
|
| None -> false
|
||||||
|
in
|
||||||
|
if not written then Hashtbl.replace vm.globals name (peek vm);
|
||||||
run vm
|
run vm
|
||||||
|
|
||||||
(* ---- Control flow ---- *)
|
(* ---- Control flow ---- *)
|
||||||
@@ -235,7 +256,7 @@ let rec run vm =
|
|||||||
(* Capture from enclosing frame's upvalue — already a shared cell *)
|
(* Capture from enclosing frame's upvalue — already a shared cell *)
|
||||||
frame.closure.vm_upvalues.(index)
|
frame.closure.vm_upvalues.(index)
|
||||||
) in
|
) in
|
||||||
let cl = { vm_code = code; vm_upvalues = upvalues; vm_name = None; vm_env_ref = vm.globals } in
|
let cl = { vm_code = code; vm_upvalues = upvalues; vm_name = None; vm_env_ref = vm.globals; vm_closure_env = None } in
|
||||||
(* Wrap as NativeFn that calls back into the VM *)
|
(* Wrap as NativeFn that calls back into the VM *)
|
||||||
let fn = NativeFn ("vm-closure", fun args ->
|
let fn = NativeFn ("vm-closure", fun args ->
|
||||||
call_closure cl args vm.globals)
|
call_closure cl args vm.globals)
|
||||||
@@ -328,10 +349,6 @@ and vm_call vm f args =
|
|||||||
push vm (Sx_ref.cek_call f (List args))
|
push vm (Sx_ref.cek_call f (List args))
|
||||||
| None ->
|
| None ->
|
||||||
if l.l_name <> None
|
if l.l_name <> None
|
||||||
(* Skip JIT for inner functions (closure != globals).
|
|
||||||
The closure merging produces incorrect variable resolution
|
|
||||||
for functions that capture letrec/let-local bindings. *)
|
|
||||||
&& l.l_closure.bindings == vm.globals
|
|
||||||
then begin
|
then begin
|
||||||
(* Pre-mark before compile attempt to prevent re-entrancy *)
|
(* Pre-mark before compile attempt to prevent re-entrancy *)
|
||||||
l.l_compiled <- Some jit_failed_sentinel;
|
l.l_compiled <- Some jit_failed_sentinel;
|
||||||
@@ -391,7 +408,7 @@ and call_closure cl args globals =
|
|||||||
|
|
||||||
(** Execute a compiled module (top-level bytecode). *)
|
(** Execute a compiled module (top-level bytecode). *)
|
||||||
let execute_module code globals =
|
let execute_module code globals =
|
||||||
let cl = { vm_code = code; vm_upvalues = [||]; vm_name = Some "module"; vm_env_ref = globals } in
|
let cl = { vm_code = code; vm_upvalues = [||]; vm_name = Some "module"; vm_env_ref = globals; vm_closure_env = None } in
|
||||||
let vm = create globals in
|
let vm = create globals in
|
||||||
let frame = { closure = cl; ip = 0; base = 0; local_cells = Hashtbl.create 4 } in
|
let frame = { closure = cl; ip = 0; base = 0; local_cells = Hashtbl.create 4 } in
|
||||||
for _ = 0 to code.vc_locals - 1 do push vm Nil done;
|
for _ = 0 to code.vc_locals - 1 do push vm Nil done;
|
||||||
@@ -454,7 +471,7 @@ let jit_compile_lambda (l : lambda) globals =
|
|||||||
let inner_val = outer_code.vc_constants.(idx) in
|
let inner_val = outer_code.vc_constants.(idx) in
|
||||||
let code = code_from_value inner_val in
|
let code = code_from_value inner_val in
|
||||||
Some { vm_code = code; vm_upvalues = [||];
|
Some { vm_code = code; vm_upvalues = [||];
|
||||||
vm_name = l.l_name; vm_env_ref = effective_globals }
|
vm_name = l.l_name; vm_env_ref = effective_globals; vm_closure_env = Some l.l_closure }
|
||||||
else begin
|
else begin
|
||||||
Printf.eprintf "[jit] FAIL %s: closure index %d out of bounds (pool=%d)\n%!"
|
Printf.eprintf "[jit] FAIL %s: closure index %d out of bounds (pool=%d)\n%!"
|
||||||
fn_name idx (Array.length outer_code.vc_constants);
|
fn_name idx (Array.length outer_code.vc_constants);
|
||||||
|
|||||||
@@ -440,16 +440,6 @@ class OcamlBridge:
|
|||||||
_logger.warning("OCaml load skipped %s: %s",
|
_logger.warning("OCaml load skipped %s: %s",
|
||||||
filepath, e)
|
filepath, e)
|
||||||
|
|
||||||
# sx-parse has deeply nested letrec + define inside fn
|
|
||||||
# bodies. The JIT closure/upvalue mechanism can't handle
|
|
||||||
# the multiple nesting layers correctly — nested list
|
|
||||||
# parsing produces wrong results. Force CEK.
|
|
||||||
try:
|
|
||||||
await self._send('(vm-reset-fn "sx-parse")')
|
|
||||||
await self._read_until_ok(ctx=None)
|
|
||||||
except OcamlBridgeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# SSR overrides: effect is a no-op on the server (prevents
|
# SSR overrides: effect is a no-op on the server (prevents
|
||||||
# reactive loops during island SSR — effects are DOM side-effects)
|
# reactive loops during island SSR — effects are DOM side-effects)
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user