Lazy JIT compilation: lambdas compile to bytecode on first call

Replace AOT adapter compilation with lazy JIT — each named lambda is
compiled to VM bytecode on first call, cached in l_compiled field for
subsequent calls. Compilation failures fall back to CEK gracefully.

VM types (vm_code, vm_upvalue_cell, vm_closure) moved to sx_types.ml
mutual recursion block. Lambda and Component records gain mutable
l_compiled/c_compiled cache fields. jit_compile_lambda in sx_vm.ml
wraps body as (fn (params) body), invokes spec/compiler.sx via CEK,
extracts inner closure from OP_CLOSURE constant.

JIT hooks in both paths:
- vm_call: Lambda calls from compiled VM code
- continue_with_call: Lambda calls from CEK step loop (injected by
  bootstrap.py post-processing)

Pre-mark sentinel prevents re-entrancy (compile function itself was
hanging when JIT'd mid-compilation). VM execution errors caught and
fall back to CEK with sentinel marking.

Also: add kbd/samp/var to HTML_TAGS, rebuild sx-browser.js, add page
URL to sx-page-full-py timing log.

Performance: first page 28s (JIT compiles 17 functions), subsequent
pages 0.31s home / 0.71s wittgenstein (was 2.3s). All 1945 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-23 08:18:44 +00:00
parent 7628659854
commit 318c818728
10 changed files with 296 additions and 197 deletions

View File

@@ -591,7 +591,7 @@ let run_foundation_tests () =
assert_true "sx_truthy \"\"" (Bool (sx_truthy (String ""))); assert_true "sx_truthy \"\"" (Bool (sx_truthy (String "")));
assert_eq "not truthy nil" (Bool false) (Bool (sx_truthy Nil)); assert_eq "not truthy nil" (Bool false) (Bool (sx_truthy Nil));
assert_eq "not truthy false" (Bool false) (Bool (sx_truthy (Bool false))); assert_eq "not truthy false" (Bool false) (Bool (sx_truthy (Bool false)));
let l = { l_params = ["x"]; l_body = Symbol "x"; l_closure = Sx_types.make_env (); l_name = None } in let l = { l_params = ["x"]; l_body = Symbol "x"; l_closure = Sx_types.make_env (); l_name = None; l_compiled = None } in
assert_true "is_lambda" (Bool (Sx_types.is_lambda (Lambda l))); assert_true "is_lambda" (Bool (Sx_types.is_lambda (Lambda l)));
ignore (Sx_types.set_lambda_name (Lambda l) "my-fn"); ignore (Sx_types.set_lambda_name (Lambda l) "my-fn");
assert_eq "lambda name mutated" (String "my-fn") (lambda_name (Lambda l)) assert_eq "lambda name mutated" (String "my-fn") (lambda_name (Lambda l))

View File

@@ -698,96 +698,60 @@ let make_server_env () =
(* ====================================================================== *) (* ====================================================================== *)
(* VM adapter — compiled aser functions in isolated globals *) (* JIT hook registration *)
(* ====================================================================== *) (* ====================================================================== *)
(** Compiled adapter globals — separate from kernel env. (** Register the JIT call hook. Called once after the compiler is loaded
Contains compiled aser functions + reads from kernel env for into the kernel env. The hook handles both cached execution (bytecode
components, helpers, and other runtime bindings. *) already compiled) and first-call compilation (invoke compiler.sx via
let vm_adapter_globals : (string, value) Hashtbl.t option ref = ref None CEK, cache result). cek_call checks this before CEK dispatch. *)
let _jit_compiling = ref false (* re-entrancy guard *)
(** Compile adapter-sx.sx and store in vm_adapter_globals.
Called from vm-compile-adapter command. *)
let compile_adapter env =
if not (Hashtbl.mem env.bindings "compile") then
raise (Eval_error "compiler not loaded")
else begin
let compile_fn = Hashtbl.find env.bindings "compile" in
(* Find and parse adapter-sx.sx *)
let web_dir = try Sys.getenv "SX_WEB_DIR" with Not_found ->
try Filename.concat (Sys.getenv "SX_SPEC_DIR") "../web"
with Not_found -> "web" in
let adapter_path = Filename.concat web_dir "adapter-sx.sx" in
if not (Sys.file_exists adapter_path) then
raise (Eval_error ("adapter-sx.sx not found: " ^ adapter_path));
let exprs = Sx_parser.parse_file adapter_path in
(* Compile each define's body *)
let globals = Hashtbl.create 64 in
(* Seed with kernel env for component/helper lookups *)
Hashtbl.iter (fun k v -> Hashtbl.replace globals k v) env.bindings;
let compiled = ref 0 in
List.iter (fun expr ->
match expr with
| List (Symbol "define" :: Symbol name :: rest) ->
(* Find the body — skip :effects annotations *)
let rec find_body = function
| Keyword _ :: _ :: rest -> find_body rest
| body :: _ -> body
| [] -> Nil
in
let body = find_body rest in
(try
let quoted = List [Symbol "quote"; body] in
let result = Sx_ref.eval_expr (List [compile_fn; quoted]) (Env env) in
match result with
| Dict d when Hashtbl.mem d "bytecode" ->
let outer_code = Sx_vm.code_from_value result in
Printf.eprintf "[vm] %s: outer bc=%d consts=%d inner_type=%s\n%!"
name (Array.length outer_code.Sx_vm.bytecode)
(Array.length outer_code.Sx_vm.constants)
(if Array.length outer_code.Sx_vm.constants > 0 then
type_of outer_code.Sx_vm.constants.(0) else "empty");
let bc = outer_code.Sx_vm.bytecode in
if Array.length bc >= 4 && bc.(0) = 51 then begin
(* The compiled define body is (fn ...) which compiles to
OP_CLOSURE + [upvalue descriptors] + OP_RETURN.
Extract the inner code object from constants[idx]. *)
let idx = bc.(1) lor (bc.(2) lsl 8) in
let code =
if idx < Array.length outer_code.Sx_vm.constants then begin
let inner_val = outer_code.Sx_vm.constants.(idx) in
try Sx_vm.code_from_value inner_val
with e ->
Printf.eprintf "[vm] inner code_from_value failed for %s: %s\n%!"
name (Printexc.to_string e);
raise e
end else outer_code
in
let cl = { Sx_vm.code; upvalues = [||]; name = Some name;
env_ref = globals } in
Hashtbl.replace globals name
(NativeFn ("vm:" ^ name, fun args ->
Sx_vm.call_closure cl args globals));
incr compiled
end else begin
(* Not a lambda — constant expression (e.g. (list ...)).
Execute once and store the resulting value directly. *)
let value = Sx_vm.execute_module outer_code globals in
Hashtbl.replace globals name value;
Printf.eprintf "[vm] %s: constant (type=%s)\n%!" name (type_of value);
incr compiled
end
| _ -> () (* non-dict result — skip *)
with e ->
Printf.eprintf "[vm] FAIL adapter %s: %s\n%!" name (Printexc.to_string e))
| _ -> let register_jit_hook env =
(* Non-define expression — evaluate on CEK to set up constants *) Sx_ref.jit_call_hook := Some (fun f args ->
(try ignore (Sx_ref.eval_expr expr (Env env)) with _ -> ()) match f with
) exprs; | Lambda l ->
vm_adapter_globals := Some globals; (match l.l_compiled with
Printf.eprintf "[vm] Compiled adapter: %d functions\n%!" !compiled | Some cl when not (Sx_vm.is_jit_failed cl) ->
end (* Cached bytecode — execute on VM, fall back to CEK on error *)
(try Some (Sx_vm.call_closure cl args cl.vm_env_ref)
with _ -> l.l_compiled <- Some Sx_vm.jit_failed_sentinel; None)
| Some _ -> None (* failed sentinel *)
| None ->
(* Don't try to compile while already compiling (prevents
infinite recursion: compile calls lambdas internally) *)
if !_jit_compiling then None
else begin
let fn_name = match l.l_name with Some n -> n | None -> "?" in
begin
(* Mark as tried BEFORE compiling — prevents other calls to
the same lambda from starting redundant compilations while
this one is running. If compilation succeeds, overwrite. *)
l.l_compiled <- Some Sx_vm.jit_failed_sentinel;
Printf.eprintf "[jit-hook] compiling %s (body size ~%d)...\n%!"
fn_name (String.length (inspect l.l_body));
_jit_compiling := true;
let t0 = Unix.gettimeofday () in
let compiled = Sx_vm.jit_compile_lambda l env.bindings in
let dt = Unix.gettimeofday () -. t0 in
_jit_compiling := false;
Printf.eprintf "[jit-hook] %s compile %s in %.3fs\n%!"
fn_name (match compiled with Some _ -> "OK" | None -> "FAIL") dt;
match compiled with
| Some cl ->
l.l_compiled <- Some cl;
Printf.eprintf "[jit-hook] executing %s on VM...\n%!" fn_name;
(try
let r = Sx_vm.call_closure cl args cl.vm_env_ref in
Printf.eprintf "[jit-hook] %s execution OK\n%!" fn_name;
Some r
with e ->
Printf.eprintf "[jit-hook] %s VM FAIL: %s\n%!" fn_name (Printexc.to_string e);
l.l_compiled <- Some Sx_vm.jit_failed_sentinel; None)
| None -> None
end end)
| _ -> None)
(* ====================================================================== *) (* ====================================================================== *)
@@ -901,17 +865,15 @@ let rec dispatch env cmd =
| exn -> send_error (Printexc.to_string exn)) | exn -> send_error (Printexc.to_string exn))
| List [Symbol "vm-compile-adapter"] -> | List [Symbol "vm-compile-adapter"] ->
(* Compile adapter-sx.sx to VM bytecode with isolated globals *) (* Legacy command — JIT hook is now registered at startup.
(try Kept for backward compatibility with ocaml_bridge.py. *)
compile_adapter env; register_jit_hook env;
send_ok () Printf.eprintf "[jit] JIT hook registered (lazy compilation active)\n%!";
with send_ok ()
| Eval_error msg -> send_error msg
| exn -> send_error (Printexc.to_string exn))
| List [Symbol "aser-slot"; String src] -> | List [Symbol "aser-slot"; String src] ->
(* Expand ALL components server-side. Uses batch IO mode. (* Expand ALL components server-side. Uses batch IO mode.
Routes through VM if adapter is compiled, else CEK. *) Calls aser via CEK — the JIT hook compiles it on first call. *)
(try (try
let exprs = Sx_parser.parse_all src in let exprs = Sx_parser.parse_all src in
let expr = match exprs with let expr = match exprs with
@@ -925,22 +887,14 @@ let rec dispatch env cmd =
let t0 = Unix.gettimeofday () in let t0 = Unix.gettimeofday () in
let expand_fn = NativeFn ("expand-components?", fun _args -> Bool true) in let expand_fn = NativeFn ("expand-components?", fun _args -> Bool true) in
ignore (env_bind env "expand-components?" expand_fn); ignore (env_bind env "expand-components?" expand_fn);
let result = match !vm_adapter_globals with Printf.eprintf "[aser-slot] starting aser eval...\n%!";
| Some globals -> let result =
Hashtbl.replace globals "expand-components?" expand_fn; let call = List [Symbol "aser";
let aser_fn = try Hashtbl.find globals "aser" List [Symbol "quote"; expr];
with Not_found -> raise (Eval_error "VM: aser not compiled") in Env env] in
let r = match aser_fn with let r = Sx_ref.eval_expr call (Env env) in
| NativeFn (_, fn) -> fn [expr; Env env] Printf.eprintf "[aser-slot] aser eval returned\n%!";
| _ -> raise (Eval_error "VM: aser not a function") r
in
Hashtbl.remove globals "expand-components?";
r
| None ->
let call = List [Symbol "aser";
List [Symbol "quote"; expr];
Env env] in
Sx_ref.eval_expr call (Env env)
in in
let t1 = Unix.gettimeofday () in let t1 = Unix.gettimeofday () in
io_batch_mode := false; io_batch_mode := false;
@@ -990,22 +944,14 @@ let rec dispatch env cmd =
let t0 = Unix.gettimeofday () in let t0 = Unix.gettimeofday () in
let expand_fn = NativeFn ("expand-components?", fun _args -> Bool true) in let expand_fn = NativeFn ("expand-components?", fun _args -> Bool true) in
ignore (env_bind env "expand-components?" expand_fn); ignore (env_bind env "expand-components?" expand_fn);
let body_result = match !vm_adapter_globals with Printf.eprintf "[sx-page-full] starting aser eval...\n%!";
| Some globals -> let body_result =
Hashtbl.replace globals "expand-components?" expand_fn;
let aser_fn = try Hashtbl.find globals "aser"
with Not_found -> raise (Eval_error "VM: aser not compiled") in
let r = match aser_fn with
| NativeFn (_, fn) -> fn [expr; Env env]
| _ -> raise (Eval_error "VM: aser not a function")
in
Hashtbl.remove globals "expand-components?";
r
| None ->
let call = List [Symbol "aser"; let call = List [Symbol "aser";
List [Symbol "quote"; expr]; List [Symbol "quote"; expr];
Env env] in Env env] in
Sx_ref.eval_expr call (Env env) let r = Sx_ref.eval_expr call (Env env) in
Printf.eprintf "[sx-page-full] aser eval returned\n%!";
r
in in
let t1 = Unix.gettimeofday () in let t1 = Unix.gettimeofday () in
io_batch_mode := false; io_batch_mode := false;
@@ -1117,8 +1063,8 @@ let rec dispatch env cmd =
let fn = NativeFn ("vm:" ^ name, fun args -> let fn = NativeFn ("vm:" ^ name, fun args ->
try try
Sx_vm.call_closure Sx_vm.call_closure
{ Sx_vm.code; upvalues = [||]; name = lam.l_name; { vm_code = code; vm_upvalues = [||]; vm_name = lam.l_name;
env_ref = live_env } vm_env_ref = live_env }
args live_env args live_env
with with
| _ -> | _ ->

View File

@@ -55,10 +55,15 @@ let trampoline v = !trampoline_fn v
let _strict_ref = ref (Bool false) let _strict_ref = ref (Bool false)
let _prim_param_types_ref = ref Nil let _prim_param_types_ref = ref Nil
(* JIT call hook — cek_call checks this before CEK dispatch for named
lambdas. Registered by sx_server.ml after compiler loads. Tests
run with hook = None (pure CEK, no compilation dependency). *)
let jit_call_hook : (value -> value list -> value option) option ref = ref None
""" """
# OCaml fixups — wire up trampoline + iterative CEK run # OCaml fixups — wire up trampoline + iterative CEK run + JIT hook
FIXUPS = """\ FIXUPS = """\
(* Wire up trampoline to resolve thunks via the CEK machine *) (* Wire up trampoline to resolve thunks via the CEK machine *)
@@ -75,6 +80,8 @@ let cek_run_iterative state =
done; done;
cek_value !s cek_value !s
""" """
@@ -200,9 +207,6 @@ def compile_spec_to_ml(spec_dir: str | None = None) -> str:
return '\n'.join(fixed) return '\n'.join(fixed)
output = fix_mutable_reads(output) output = fix_mutable_reads(output)
# Fix cek_call: the spec passes (make-env) as the env arg to
# continue_with_call, but the transpiler evaluates it at transpile
# time (it's a primitive), producing Dict instead of Env.
# Fix cek_call: the spec passes (make-env) as the env arg to # Fix cek_call: the spec passes (make-env) as the env arg to
# continue_with_call, but the transpiler evaluates make-env at # continue_with_call, but the transpiler evaluates make-env at
# transpile time (it's a primitive), producing Dict instead of Env. # transpile time (it's a primitive), producing Dict instead of Env.
@@ -211,6 +215,29 @@ def compile_spec_to_ml(spec_dir: str | None = None) -> str:
"(Env (Sx_types.make_env ())) (a) ((List []))", "(Env (Sx_types.make_env ())) (a) ((List []))",
) )
# Inject JIT dispatch into continue_with_call's lambda branch.
# After params are bound, check jit_call_hook before creating CEK state.
lambda_body_pattern = (
'(prim_call "slice" [params; (len (args))])); Nil)) in '
'(make_cek_state ((lambda_body (f))) (local) (kont))'
)
lambda_body_jit = (
'(prim_call "slice" [params; (len (args))])); Nil)) in '
'(match !jit_call_hook, f with '
'| Some hook, Lambda l when l.l_name <> None -> '
'let args_list = match args with '
'List a | ListRef { contents = a } -> a | _ -> [] in '
'(match hook f args_list with '
'Some result -> make_cek_value result local kont '
'| None -> make_cek_state (lambda_body f) local kont) '
'| _ -> make_cek_state ((lambda_body (f))) (local) (kont))'
)
if lambda_body_pattern in output:
output = output.replace(lambda_body_pattern, lambda_body_jit, 1)
else:
import sys
print("WARNING: Could not find lambda body pattern for JIT injection", file=sys.stderr)
return output return output

View File

@@ -18,6 +18,11 @@ let trampoline v = !trampoline_fn v
let _strict_ref = ref (Bool false) let _strict_ref = ref (Bool false)
let _prim_param_types_ref = ref Nil let _prim_param_types_ref = ref Nil
(* JIT call hook — cek_call checks this before CEK dispatch for named
lambdas. Registered by sx_server.ml after compiler loads. Tests
run with hook = None (pure CEK, no compilation dependency). *)
let jit_call_hook : (value -> value list -> value option) option ref = ref None
(* === Transpiled from evaluator (frames + eval + CEK) === *) (* === Transpiled from evaluator (frames + eval + CEK) === *)
@@ -482,7 +487,7 @@ and step_continue state =
(* continue-with-call *) (* continue-with-call *)
and continue_with_call f args env raw_args kont = and continue_with_call f args env raw_args kont =
(if sx_truthy ((continuation_p (f))) then (let arg = (if sx_truthy ((empty_p (args))) then Nil else (first (args))) in let cont_data = (continuation_data (f)) in (let captured = (get (cont_data) ((String "captured"))) in (let result' = (cek_run ((make_cek_value (arg) (env) (captured)))) in (make_cek_value (result') (env) (kont))))) else (if sx_truthy ((let _and = (is_callable (f)) in if not (sx_truthy _and) then _and else (let _and = (Bool (not (sx_truthy ((is_lambda (f)))))) in if not (sx_truthy _and) then _and else (let _and = (Bool (not (sx_truthy ((is_component (f)))))) in if not (sx_truthy _and) then _and else (Bool (not (sx_truthy ((is_island (f)))))))))) then (make_cek_value ((sx_apply f args)) (env) (kont)) else (if sx_truthy ((is_lambda (f))) then (let params = (lambda_params (f)) in let local = (env_merge ((lambda_closure (f))) (env)) in (if sx_truthy ((prim_call ">" [(len (args)); (len (params))])) then (raise (Eval_error (value_to_str (String (sx_str [(let _or = (lambda_name (f)) in if sx_truthy _or then _or else (String "lambda")); (String " expects "); (len (params)); (String " args, got "); (len (args))]))))) else (let () = ignore ((List.iter (fun pair -> ignore ((env_bind local (sx_to_string (first (pair))) (nth (pair) ((Number 1.0)))))) (sx_to_list (prim_call "zip" [params; args])); Nil)) in (let () = ignore ((List.iter (fun p -> ignore ((env_bind local (sx_to_string p) Nil))) (sx_to_list (prim_call "slice" [params; (len (args))])); Nil)) in (make_cek_state ((lambda_body (f))) (local) (kont)))))) else (if sx_truthy ((let _or = (is_component (f)) in if sx_truthy _or then _or else (is_island (f)))) then (let parsed = (parse_keyword_args (raw_args) (env)) in let kwargs = (first (parsed)) in let children = (nth (parsed) ((Number 1.0))) in let local = (env_merge ((component_closure (f))) (env)) in (let () = ignore ((List.iter (fun p -> ignore ((env_bind local (sx_to_string p) (let _or = (dict_get (kwargs) (p)) in if sx_truthy _or then _or else Nil)))) (sx_to_list (component_params (f))); Nil)) in (let () = ignore ((if sx_truthy ((component_has_children (f))) then (env_bind local (sx_to_string (String "children")) children) else Nil)) in (make_cek_state ((component_body (f))) (local) (kont))))) else (raise (Eval_error (value_to_str (String (sx_str [(String "Not callable: "); (inspect (f))]))))))))) (if sx_truthy ((continuation_p (f))) then (let arg = (if sx_truthy ((empty_p (args))) then Nil else (first (args))) in let cont_data = (continuation_data (f)) in (let captured = (get (cont_data) ((String "captured"))) in (let result' = (cek_run ((make_cek_value (arg) (env) (captured)))) in (make_cek_value (result') (env) (kont))))) else (if sx_truthy ((let _and = (is_callable (f)) in if not (sx_truthy _and) then _and else (let _and = (Bool (not (sx_truthy ((is_lambda (f)))))) in if not (sx_truthy _and) then _and else (let _and = (Bool (not (sx_truthy ((is_component (f)))))) in if not (sx_truthy _and) then _and else (Bool (not (sx_truthy ((is_island (f)))))))))) then (make_cek_value ((sx_apply f args)) (env) (kont)) else (if sx_truthy ((is_lambda (f))) then (let params = (lambda_params (f)) in let local = (env_merge ((lambda_closure (f))) (env)) in (if sx_truthy ((prim_call ">" [(len (args)); (len (params))])) then (raise (Eval_error (value_to_str (String (sx_str [(let _or = (lambda_name (f)) in if sx_truthy _or then _or else (String "lambda")); (String " expects "); (len (params)); (String " args, got "); (len (args))]))))) else (let () = ignore ((List.iter (fun pair -> ignore ((env_bind local (sx_to_string (first (pair))) (nth (pair) ((Number 1.0)))))) (sx_to_list (prim_call "zip" [params; args])); Nil)) in (let () = ignore ((List.iter (fun p -> ignore ((env_bind local (sx_to_string p) Nil))) (sx_to_list (prim_call "slice" [params; (len (args))])); Nil)) in (match !jit_call_hook, f with | Some hook, Lambda l when l.l_name <> None -> let args_list = match args with List a | ListRef { contents = a } -> a | _ -> [] in (match hook f args_list with Some result -> make_cek_value result local kont | None -> make_cek_state (lambda_body f) local kont) | _ -> make_cek_state ((lambda_body (f))) (local) (kont)))))) else (if sx_truthy ((let _or = (is_component (f)) in if sx_truthy _or then _or else (is_island (f)))) then (let parsed = (parse_keyword_args (raw_args) (env)) in let kwargs = (first (parsed)) in let children = (nth (parsed) ((Number 1.0))) in let local = (env_merge ((component_closure (f))) (env)) in (let () = ignore ((List.iter (fun p -> ignore ((env_bind local (sx_to_string p) (let _or = (dict_get (kwargs) (p)) in if sx_truthy _or then _or else Nil)))) (sx_to_list (component_params (f))); Nil)) in (let () = ignore ((if sx_truthy ((component_has_children (f))) then (env_bind local (sx_to_string (String "children")) children) else Nil)) in (make_cek_state ((component_body (f))) (local) (kont))))) else (raise (Eval_error (value_to_str (String (sx_str [(String "Not callable: "); (inspect (f))])))))))))
(* sf-case-step-loop *) (* sf-case-step-loop *)
and sf_case_step_loop match_val clauses env kont = and sf_case_step_loop match_val clauses env kont =
@@ -515,3 +520,5 @@ let cek_run_iterative state =
done; done;
cek_value !s cek_value !s

View File

@@ -74,6 +74,7 @@ and lambda = {
l_body : value; l_body : value;
l_closure : env; l_closure : env;
mutable l_name : string option; mutable l_name : string option;
mutable l_compiled : vm_closure option; (** Lazy JIT cache *)
} }
and component = { and component = {
@@ -83,6 +84,7 @@ and component = {
c_body : value; c_body : value;
c_closure : env; c_closure : env;
c_affinity : string; (** "auto" | "client" | "server" *) c_affinity : string; (** "auto" | "client" | "server" *)
mutable c_compiled : vm_closure option; (** Lazy JIT cache *)
} }
and island = { and island = {
@@ -107,6 +109,33 @@ and signal = {
mutable s_deps : signal list; mutable s_deps : signal list;
} }
(** {1 Bytecode VM types}
Defined here (not in sx_vm.ml) because [vm_code.constants] references
[value] and [lambda.l_compiled] references [vm_closure] — mutual
recursion requires all types in one [and] chain. *)
(** Compiled function body — bytecode + constant pool. *)
and vm_code = {
vc_arity : int;
vc_locals : int;
vc_bytecode : int array;
vc_constants : value array;
}
(** Upvalue cell — shared mutable reference to a captured variable. *)
and vm_upvalue_cell = {
mutable uv_value : value;
}
(** Closure — compiled code + captured upvalues + live env reference. *)
and vm_closure = {
vm_code : vm_code;
vm_upvalues : vm_upvalue_cell array;
vm_name : string option;
vm_env_ref : (string, value) Hashtbl.t;
}
(** {1 Errors} *) (** {1 Errors} *)
@@ -202,7 +231,7 @@ let make_lambda params body closure =
| List items -> List.map value_to_string items | List items -> List.map value_to_string items
| _ -> value_to_string_list params | _ -> value_to_string_list params
in in
Lambda { l_params = ps; l_body = body; l_closure = unwrap_env_val closure; l_name = None } Lambda { l_params = ps; l_body = body; l_closure = unwrap_env_val closure; l_name = None; l_compiled = None }
let make_component name params has_children body closure affinity = let make_component name params has_children body closure affinity =
let n = value_to_string name in let n = value_to_string name in
@@ -212,6 +241,7 @@ let make_component name params has_children body closure affinity =
Component { Component {
c_name = n; c_params = ps; c_has_children = hc; c_name = n; c_params = ps; c_has_children = hc;
c_body = body; c_closure = unwrap_env_val closure; c_affinity = aff; c_body = body; c_closure = unwrap_env_val closure; c_affinity = aff;
c_compiled = None;
} }
let make_island name params has_children body closure = let make_island name params has_children body closure =

View File

@@ -5,39 +5,19 @@
no allocation per step (unlike the CEK machine). no allocation per step (unlike the CEK machine).
This is the platform-native execution engine. The same bytecode This is the platform-native execution engine. The same bytecode
runs on all platforms (OCaml, JS, WASM). *) runs on all platforms (OCaml, JS, WASM).
VM types (vm_code, vm_upvalue_cell, vm_closure) are defined in
sx_types.ml to share the mutual recursion block with [value]. *)
open Sx_types open Sx_types
(** Code object — compiled function body. *)
type code = {
arity : int;
locals : int;
bytecode : int array;
constants : value array;
}
(** Upvalue cell — shared mutable reference to a captured variable.
Open when the variable is still on the stack; closed when the
enclosing frame returns and the value is moved to the heap. *)
type upvalue_cell = {
mutable uv_value : value;
}
(** Closure — code + captured upvalues + live env reference. *)
type vm_closure = {
code : code;
upvalues : upvalue_cell array;
name : string option;
env_ref : (string, value) Hashtbl.t; (* live global env — NOT a snapshot *)
}
(** Call frame — one per function invocation. *) (** Call frame — one per function invocation. *)
type frame = { type frame = {
closure : vm_closure; closure : vm_closure;
mutable ip : int; mutable ip : int;
base : int; (* base index in value stack for locals *) base : int; (* base index in value stack for locals *)
local_cells : (int, upvalue_cell) Hashtbl.t; (* slot → shared cell for captured locals *) local_cells : (int, vm_upvalue_cell) Hashtbl.t; (* slot → shared cell for captured locals *)
} }
(** VM state. *) (** VM state. *)
@@ -48,6 +28,19 @@ type vm = {
globals : (string, value) Hashtbl.t; (* live reference to kernel env *) globals : (string, value) Hashtbl.t; (* live reference to kernel env *)
} }
(** Forward reference for JIT compilation — set after definition. *)
let jit_compile_ref : (lambda -> (string, value) Hashtbl.t -> vm_closure option) ref =
ref (fun _ _ -> None)
(** Sentinel closure indicating JIT compilation was attempted and failed.
Prevents retrying compilation on every call. *)
let jit_failed_sentinel = {
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
}
let is_jit_failed cl = cl.vm_code.vc_arity = -1
let create globals = let create globals =
{ stack = Array.make 4096 Nil; sp = 0; frames = []; globals } { stack = Array.make 4096 Nil; sp = 0; frames = []; globals }
@@ -69,12 +62,12 @@ let[@inline] peek vm = vm.stack.(vm.sp - 1)
(** Read operands. *) (** Read operands. *)
let[@inline] read_u8 f = let[@inline] read_u8 f =
let v = f.closure.code.bytecode.(f.ip) in let v = f.closure.vm_code.vc_bytecode.(f.ip) in
f.ip <- f.ip + 1; v f.ip <- f.ip + 1; v
let[@inline] read_u16 f = let[@inline] read_u16 f =
let lo = f.closure.code.bytecode.(f.ip) in let lo = f.closure.vm_code.vc_bytecode.(f.ip) in
let hi = f.closure.code.bytecode.(f.ip + 1) in let hi = f.closure.vm_code.vc_bytecode.(f.ip + 1) in
f.ip <- f.ip + 2; f.ip <- f.ip + 2;
lo lor (hi lsl 8) lo lor (hi lsl 8)
@@ -84,7 +77,7 @@ let[@inline] read_i16 f =
(** Wrap a VM closure as an SX value (NativeFn). *) (** Wrap a VM closure as an SX value (NativeFn). *)
let closure_to_value cl = let closure_to_value cl =
NativeFn ("vm:" ^ (match cl.name with Some n -> n | None -> "anon"), NativeFn ("vm:" ^ (match cl.vm_name with Some n -> n | None -> "anon"),
fun args -> raise (Eval_error ("VM_CLOSURE_CALL:" ^ String.concat "," (List.map Sx_runtime.value_to_str args)))) 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 *) (* Placeholder — actual calls go through vm_call below *)
@@ -93,8 +86,8 @@ let rec run vm =
match vm.frames with match vm.frames with
| [] -> () (* no frame = done *) | [] -> () (* no frame = done *)
| frame :: rest_frames -> | frame :: rest_frames ->
let bc = frame.closure.code.bytecode in let bc = frame.closure.vm_code.vc_bytecode in
let consts = frame.closure.code.constants in let consts = frame.closure.vm_code.vc_constants in
if frame.ip >= Array.length bc then () if frame.ip >= Array.length bc then ()
else else
let saved_ip = frame.ip in let saved_ip = frame.ip in
@@ -139,15 +132,15 @@ let rec run vm =
run vm run vm
| 18 (* OP_UPVALUE_GET *) -> | 18 (* OP_UPVALUE_GET *) ->
let idx = read_u8 frame in let idx = read_u8 frame in
if idx >= Array.length frame.closure.upvalues then if idx >= Array.length frame.closure.vm_upvalues then
raise (Eval_error (Printf.sprintf raise (Eval_error (Printf.sprintf
"VM: UPVALUE_GET idx=%d out of bounds (have %d)" idx "VM: UPVALUE_GET idx=%d out of bounds (have %d)" idx
(Array.length frame.closure.upvalues))); (Array.length frame.closure.vm_upvalues)));
push vm frame.closure.upvalues.(idx).uv_value; push vm frame.closure.vm_upvalues.(idx).uv_value;
run vm run vm
| 19 (* OP_UPVALUE_SET *) -> | 19 (* OP_UPVALUE_SET *) ->
let idx = read_u8 frame in let idx = read_u8 frame in
frame.closure.upvalues.(idx).uv_value <- peek vm; frame.closure.vm_upvalues.(idx).uv_value <- peek vm;
run vm run vm
| 20 (* OP_GLOBAL_GET *) -> | 20 (* OP_GLOBAL_GET *) ->
let idx = read_u16 frame in let idx = read_u16 frame in
@@ -209,9 +202,6 @@ let rec run vm =
raise (Eval_error (Printf.sprintf "VM: CLOSURE idx %d >= consts %d" idx (Array.length consts))); raise (Eval_error (Printf.sprintf "VM: CLOSURE idx %d >= consts %d" idx (Array.length consts)));
let code_val = consts.(idx) in let code_val = consts.(idx) in
let code = code_from_value code_val in let code = code_from_value code_val in
Printf.eprintf "[vm-closure] idx=%d type=%s bc_len=%d consts=%d sp_before=%d\n%!"
idx (type_of code_val)
(Array.length code.bytecode) (Array.length code.constants) vm.sp;
(* Read upvalue descriptors from bytecode *) (* Read upvalue descriptors from bytecode *)
let uv_count = match code_val with let uv_count = match code_val with
| Dict d -> (match Hashtbl.find_opt d "upvalue-count" with | Dict d -> (match Hashtbl.find_opt d "upvalue-count" with
@@ -235,9 +225,9 @@ let rec run vm =
cell cell
end else end else
(* Capture from enclosing frame's upvalue — already a shared cell *) (* Capture from enclosing frame's upvalue — already a shared cell *)
frame.closure.upvalues.(index) frame.closure.vm_upvalues.(index)
) in ) in
let cl = { code; upvalues; name = None; env_ref = vm.globals } in let cl = { vm_code = code; vm_upvalues = upvalues; vm_name = None; vm_env_ref = vm.globals } 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)
@@ -306,26 +296,61 @@ let rec run vm =
raise (Eval_error (Printf.sprintf "VM: unknown opcode %d at ip=%d" raise (Eval_error (Printf.sprintf "VM: unknown opcode %d at ip=%d"
opcode (frame.ip - 1))) opcode (frame.ip - 1)))
with Invalid_argument msg -> with Invalid_argument msg ->
let fn_name = match frame.closure.name with Some n -> n | None -> "?" in let fn_name = match frame.closure.vm_name with Some n -> n | None -> "?" in
raise (Eval_error (Printf.sprintf raise (Eval_error (Printf.sprintf
"VM: %s at ip=%d op=%d in %s (base=%d sp=%d bc_len=%d consts=%d)" "VM: %s at ip=%d op=%d in %s (base=%d sp=%d bc_len=%d consts=%d)"
msg saved_ip op fn_name frame.base vm.sp msg saved_ip op fn_name frame.base vm.sp
(Array.length bc) (Array.length consts)))) (Array.length bc) (Array.length consts))))
(** Call a value as a function — dispatch by type. *) (** Call a value as a function — dispatch by type.
For Lambda values, tries JIT compilation before falling back to CEK. *)
and vm_call vm f args = and vm_call vm f args =
match f with match f with
| NativeFn (_name, fn) -> | NativeFn (_name, fn) ->
let result = fn args in let result = fn args in
push vm result push vm result
| Lambda _ | Component _ | Island _ -> | Lambda l ->
(* Fall back to CEK machine for SX-defined functions *) (* 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 *)
(try push vm (call_closure cl args vm.globals)
with _ ->
l.l_compiled <- Some jit_failed_sentinel;
push vm (Sx_ref.cek_call f (List args)))
| Some _ ->
(* Previously failed or skipped — use 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;
match !jit_compile_ref l vm.globals with
| Some cl ->
l.l_compiled <- Some cl;
(try push vm (call_closure cl args vm.globals)
with _ ->
l.l_compiled <- Some jit_failed_sentinel;
push vm (Sx_ref.cek_call f (List 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)
| Component _ | Island _ ->
(* Components use keyword-arg parsing — CEK handles this *)
let result = Sx_ref.cek_call f (List args) in let result = Sx_ref.cek_call f (List args) in
push vm result push vm result
| _ -> | _ ->
raise (Eval_error ("VM: not callable: " ^ Sx_runtime.value_to_str f)) raise (Eval_error ("VM: not callable: " ^ Sx_runtime.value_to_str f))
(** Convert compiler output (SX dict) to a code object. *) (** Convert compiler output (SX dict) to a vm_code object. *)
and code_from_value v = and code_from_value v =
match v with match v with
| Dict d -> | Dict d ->
@@ -346,35 +371,99 @@ and code_from_value v =
let arity = match Hashtbl.find_opt d "arity" with let arity = match Hashtbl.find_opt d "arity" with
| Some (Number n) -> int_of_float n | _ -> 0 | Some (Number n) -> int_of_float n | _ -> 0
in in
{ arity; locals = arity + 16; bytecode = bc_list; constants } { vc_arity = arity; vc_locals = arity + 16; vc_bytecode = bc_list; vc_constants = constants }
| _ -> { arity = 0; locals = 16; bytecode = [||]; constants = [||] } | _ -> { vc_arity = 0; vc_locals = 16; vc_bytecode = [||]; vc_constants = [||] }
(** Execute a closure with arguments. (** Execute a closure with arguments. *)
If called from within a VM (via NativeFn wrapper from for-each/map),
the upvalue cells already contain the captured values — no parent
frame needed. The fresh VM is fine because upvalues are heap-allocated
cells, not stack references. *)
and call_closure cl args globals = and call_closure cl args globals =
let vm = create globals in let vm = create globals in
let frame = { closure = cl; ip = 0; base = vm.sp; local_cells = Hashtbl.create 4 } in let frame = { closure = cl; ip = 0; base = vm.sp; local_cells = Hashtbl.create 4 } in
List.iter (fun a -> push vm a) args; List.iter (fun a -> push vm a) args;
for _ = List.length args to cl.code.locals - 1 do push vm Nil done; for _ = List.length args to cl.vm_code.vc_locals - 1 do push vm Nil done;
vm.frames <- [frame]; vm.frames <- [frame];
(try run vm (try run vm
with e -> with e ->
Printf.eprintf "[vm-call-closure] FAIL in %s: %s (bc_len=%d args=%d sp=%d)\n%!" Printf.eprintf "[vm-call-closure] FAIL in %s: %s (bc_len=%d args=%d sp=%d)\n%!"
(match cl.name with Some n -> n | None -> "?") (match cl.vm_name with Some n -> n | None -> "?")
(Printexc.to_string e) (Printexc.to_string e)
(Array.length cl.code.bytecode) (List.length args) vm.sp; (Array.length cl.vm_code.vc_bytecode) (List.length args) vm.sp;
raise e); raise e);
pop vm pop vm
(** 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 = { code; upvalues = [||]; name = Some "module"; env_ref = globals } in let cl = { vm_code = code; vm_upvalues = [||]; vm_name = Some "module"; vm_env_ref = globals } 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.locals - 1 do push vm Nil done; for _ = 0 to code.vc_locals - 1 do push vm Nil done;
vm.frames <- [frame]; vm.frames <- [frame];
run vm; run vm;
pop vm pop vm
(** {1 Lazy JIT compilation} *)
(** Compile a lambda or component body to bytecode using the SX compiler.
Invokes [compile] from spec/compiler.sx via the CEK machine.
Returns a [vm_closure] ready for execution, or [None] on failure
(safe fallback to CEK interpretation).
The compilation cost is a single CEK evaluation of the compiler —
microseconds per function. The result is cached in the lambda/component
record so subsequent calls go straight to the VM. *)
let jit_compile_lambda (l : lambda) globals =
let fn_name = match l.l_name with Some n -> n | None -> "<anon>" in
try
let compile_fn = try Hashtbl.find globals "compile"
with Not_found -> raise (Eval_error "JIT: compiler not loaded") in
(* Reconstruct the (fn (params) body) form so the compiler produces
a proper closure. l.l_body is the inner body; we need the full
function form with params so the compiled code binds them. *)
let param_syms = List (List.map (fun s -> Symbol s) l.l_params) in
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
match result with
| Dict d when Hashtbl.mem d "bytecode" ->
let outer_code = code_from_value result in
let bc = outer_code.vc_bytecode in
if Array.length bc >= 4 && bc.(0) = 51 (* OP_CLOSURE *) then begin
let idx = bc.(1) lor (bc.(2) lsl 8) in
if idx < Array.length outer_code.vc_constants then
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 }
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);
None
end
end else begin
(* Not a closure — constant expression, alias, or simple computation.
Execute the bytecode as a module to get the value, then wrap
as a NativeFn if it's callable (so the CEK can dispatch to it). *)
(try
let value = execute_module outer_code globals in
Printf.eprintf "[jit] RESOLVED %s: %s (bc[0]=%d)\n%!"
fn_name (type_of value) (if Array.length bc > 0 then bc.(0) else -1);
(* If the resolved value is a NativeFn, we can't wrap it as a
vm_closure — just let the CEK handle it directly. Return None
so the lambda falls through to CEK, which will find the
resolved value in the env on next lookup. *)
None
with _ ->
Printf.eprintf "[jit] SKIP %s: non-closure execution failed (bc[0]=%d, len=%d)\n%!"
fn_name (if Array.length bc > 0 then bc.(0) else -1) (Array.length bc);
None)
end
| _ ->
Printf.eprintf "[jit] FAIL %s: compiler returned %s\n%!" fn_name (type_of result);
None
with e ->
Printf.eprintf "[jit] FAIL %s: %s\n%!" fn_name (Printexc.to_string e);
None
(* Wire up the forward reference *)
let () = jit_compile_ref := jit_compile_lambda

View File

@@ -14,7 +14,7 @@
// ========================================================================= // =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-22T20:34:05Z"; var SX_VERSION = "2026-03-23T08:09:30Z";
function isNil(x) { return x === NIL || x === null || x === undefined; } function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); } function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -2157,7 +2157,7 @@ PRIMITIVES["thaw-from-cid"] = thawFromCid;
// === Transpiled from render (core) === // === Transpiled from render (core) ===
// HTML_TAGS // HTML_TAGS
var HTML_TAGS = ["html", "head", "body", "title", "meta", "link", "script", "style", "noscript", "header", "nav", "main", "section", "article", "aside", "footer", "h1", "h2", "h3", "h4", "h5", "h6", "hgroup", "div", "p", "blockquote", "pre", "figure", "figcaption", "address", "details", "summary", "a", "span", "em", "strong", "small", "b", "i", "u", "s", "mark", "sub", "sup", "abbr", "cite", "code", "time", "br", "wbr", "hr", "ul", "ol", "li", "dl", "dt", "dd", "table", "thead", "tbody", "tfoot", "tr", "th", "td", "caption", "colgroup", "col", "form", "input", "textarea", "select", "option", "optgroup", "button", "label", "fieldset", "legend", "output", "datalist", "img", "video", "audio", "source", "picture", "canvas", "iframe", "svg", "math", "path", "circle", "ellipse", "rect", "line", "polyline", "polygon", "text", "tspan", "g", "defs", "use", "clipPath", "mask", "pattern", "linearGradient", "radialGradient", "stop", "filter", "feGaussianBlur", "feOffset", "feBlend", "feColorMatrix", "feComposite", "feMerge", "feMergeNode", "feTurbulence", "feComponentTransfer", "feFuncR", "feFuncG", "feFuncB", "feFuncA", "feDisplacementMap", "feFlood", "feImage", "feMorphology", "feSpecularLighting", "feDiffuseLighting", "fePointLight", "feSpotLight", "feDistantLight", "animate", "animateTransform", "foreignObject", "template", "slot", "dialog", "menu"]; var HTML_TAGS = ["html", "head", "body", "title", "meta", "link", "script", "style", "noscript", "header", "nav", "main", "section", "article", "aside", "footer", "h1", "h2", "h3", "h4", "h5", "h6", "hgroup", "div", "p", "blockquote", "pre", "figure", "figcaption", "address", "details", "summary", "a", "span", "em", "strong", "small", "b", "i", "u", "s", "mark", "sub", "sup", "abbr", "cite", "code", "kbd", "samp", "var", "time", "br", "wbr", "hr", "ul", "ol", "li", "dl", "dt", "dd", "table", "thead", "tbody", "tfoot", "tr", "th", "td", "caption", "colgroup", "col", "form", "input", "textarea", "select", "option", "optgroup", "button", "label", "fieldset", "legend", "output", "datalist", "img", "video", "audio", "source", "picture", "canvas", "iframe", "svg", "math", "path", "circle", "ellipse", "rect", "line", "polyline", "polygon", "text", "tspan", "g", "defs", "use", "clipPath", "mask", "pattern", "linearGradient", "radialGradient", "stop", "filter", "feGaussianBlur", "feOffset", "feBlend", "feColorMatrix", "feComposite", "feMerge", "feMergeNode", "feTurbulence", "feComponentTransfer", "feFuncR", "feFuncG", "feFuncB", "feFuncA", "feDisplacementMap", "feFlood", "feImage", "feMorphology", "feSpecularLighting", "feDiffuseLighting", "fePointLight", "feSpotLight", "feDistantLight", "animate", "animateTransform", "foreignObject", "template", "slot", "dialog", "menu"];
PRIMITIVES["HTML_TAGS"] = HTML_TAGS; PRIMITIVES["HTML_TAGS"] = HTML_TAGS;
// VOID_ELEMENTS // VOID_ELEMENTS

View File

@@ -434,13 +434,13 @@ class OcamlBridge:
skipped += 1 skipped += 1
_logger.warning("OCaml load skipped %s: %s", _logger.warning("OCaml load skipped %s: %s",
filepath, e) filepath, e)
# Compile adapter to VM after all files loaded (inside lock) # Register JIT hook — lambdas compile on first call
try: try:
await self._send('(vm-compile-adapter)') await self._send('(vm-compile-adapter)')
await self._read_until_ok(ctx=None) await self._read_until_ok(ctx=None)
_logger.info("VM adapter compiled — aser runs on bytecode VM") _logger.info("JIT hook registered — lambdas compile on first call")
except OcamlBridgeError as e: except OcamlBridgeError as e:
_logger.warning("VM adapter compilation skipped: %s", e) _logger.warning("JIT hook registration skipped: %s", e)
_logger.info("Loaded %d definitions from .sx files into OCaml kernel (%d skipped)", _logger.info("Loaded %d definitions from .sx files into OCaml kernel (%d skipped)",
count, skipped) count, skipped)
except Exception as e: except Exception as e:

View File

@@ -32,7 +32,7 @@
"div" "p" "blockquote" "pre" "figure" "figcaption" "address" "details" "summary" "div" "p" "blockquote" "pre" "figure" "figcaption" "address" "details" "summary"
;; Inline ;; Inline
"a" "span" "em" "strong" "small" "b" "i" "u" "s" "mark" "sub" "sup" "a" "span" "em" "strong" "small" "b" "i" "u" "s" "mark" "sub" "sup"
"abbr" "cite" "code" "time" "br" "wbr" "hr" "abbr" "cite" "code" "kbd" "samp" "var" "time" "br" "wbr" "hr"
;; Lists ;; Lists
"ul" "ol" "li" "dl" "dt" "dd" "ul" "ol" "li" "dl" "dt" "dd"
;; Tables ;; Tables

View File

@@ -230,8 +230,8 @@ async def eval_sx_url(raw_path: str) -> Any:
html = await bridge.sx_page_full( html = await bridge.sx_page_full(
page_source, shell_kwargs, ctx=ocaml_ctx) page_source, shell_kwargs, ctx=ocaml_ctx)
_t3 = _time.monotonic() _t3 = _time.monotonic()
logger.info("[sx-page-full-py] ctx=%.3fs kwargs=%.3fs ocaml=%.3fs total=%.3fs", logger.info("[sx-page-full-py] %s ctx=%.3fs kwargs=%.3fs ocaml=%.3fs total=%.3fs",
_t1-_t0, _t2-_t1, _t3-_t2, _t3-_t0) raw_path, _t1-_t0, _t2-_t1, _t3-_t2, _t3-_t0)
return await make_response(html, 200) return await make_response(html, 200)
else: else:
content_sx = await _eval_slot(wrapped_ast, env, ctx) content_sx = await _eval_slot(wrapped_ast, env, ctx)