Fix JIT server hang: compiled compiler helpers loop on complex ASTs

Root cause: pre-compiled compiler helper functions (compile-expr,
compile-cond, etc.) produce bytecode that loops when processing
deeply nested ASTs like tw-resolve-style. The test suite passes
because _jit_compiling prevents compiled function execution during
compilation — all functions run via CEK. The server pre-compiled
helpers, so they ran as bytecode during compilation, triggering loops.

Fix:
- _jit_compiling guard on the "already compiled" hook branch prevents
  compiled functions from running during JIT compilation. Compilation
  always uses CEK (correct for all AST sizes). Normal execution uses
  bytecode (fast).
- "compile" itself marked as jit_failed_sentinel — never JIT compiled.
  Runs via CEK, while its helpers use bytecode for normal (non-compile)
  execution.
- Server hook uses call_closure (own VM per call) for IO suspension
  safety. MCP uses call_closure_reuse (fast, no IO needed).

The underlying bytecode bug in the compiled helpers remains — fixing
it requires diagnosing which specific helper loops and why. This is
tracked as a separate issue. Server now starts in ~30s (pre-warm)
and serves pages correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 22:17:51 +00:00
parent 03278c640d
commit 4d1079aa5e

View File

@@ -936,11 +936,11 @@ let register_jit_hook env =
| Lambda l -> | Lambda l ->
(match l.l_compiled with (match l.l_compiled with
| Some cl when not (Sx_vm.is_jit_failed cl) -> | Some cl when not (Sx_vm.is_jit_failed cl) ->
(* Cached bytecode — run via VM. Skip during compilation to avoid (* Skip during compilation — compiled helpers loop on complex ASTs.
compiled-compiler bytecode bugs on complex nested forms. *) Normal execution uses bytecode (fast). *)
if !(Sx_vm._jit_compiling) then None if !(Sx_vm._jit_compiling) then None
else else
(try Some (Sx_vm.call_closure_reuse cl args) (try Some (Sx_vm.call_closure cl args cl.vm_env_ref)
with with
| Sx_vm.VmSuspended (request, saved_vm) -> | Sx_vm.VmSuspended (request, saved_vm) ->
Some (make_vm_suspend_marker request saved_vm) Some (make_vm_suspend_marker request saved_vm)
@@ -966,7 +966,7 @@ let register_jit_hook env =
match compiled with match compiled with
| Some cl -> | Some cl ->
l.l_compiled <- Some cl; l.l_compiled <- Some cl;
(try Some (Sx_vm.call_closure_reuse cl args) (try Some (Sx_vm.call_closure cl args cl.vm_env_ref)
with with
| Sx_vm.VmSuspended (request, saved_vm) -> | Sx_vm.VmSuspended (request, saved_vm) ->
Some (make_vm_suspend_marker request saved_vm) Some (make_vm_suspend_marker request saved_vm)
@@ -1185,8 +1185,9 @@ let rec dispatch env cmd =
register_jit_hook env; register_jit_hook env;
let t0 = Unix.gettimeofday () in let t0 = Unix.gettimeofday () in
let count = ref 0 in let count = ref 0 in
(* Pre-compile helpers, NOT "compile" itself (loops on complex ASTs) *)
let compiler_names = [ let compiler_names = [
"compile"; "compile-module"; "compile-expr"; "compile-symbol"; "compile-module"; "compile-expr"; "compile-symbol";
"compile-dict"; "compile-list"; "compile-if"; "compile-when"; "compile-dict"; "compile-list"; "compile-if"; "compile-when";
"compile-and"; "compile-or"; "compile-begin"; "compile-let"; "compile-and"; "compile-or"; "compile-begin"; "compile-let";
"compile-letrec"; "compile-lambda"; "compile-define"; "compile-set"; "compile-letrec"; "compile-lambda"; "compile-define"; "compile-set";
@@ -1208,6 +1209,11 @@ let rec dispatch env cmd =
| None -> ()) | None -> ())
| _ -> () | _ -> ()
) compiler_names; ) compiler_names;
(* Mark "compile" as jit-failed — loops on complex ASTs as bytecode *)
(try match Hashtbl.find_opt env.bindings (Sx_types.intern "compile") with
| Some (Lambda l) -> l.l_compiled <- Some Sx_vm.jit_failed_sentinel
| _ -> ()
with _ -> ());
let dt = Unix.gettimeofday () -. t0 in let dt = Unix.gettimeofday () -. t0 in
Printf.eprintf "[jit] Pre-compiled %d compiler functions in %.3fs (lazy JIT active for all)\n%!" !count dt; Printf.eprintf "[jit] Pre-compiled %d compiler functions in %.3fs (lazy JIT active for all)\n%!" !count dt;
send_ok () send_ok ()
@@ -2664,14 +2670,23 @@ let http_mode port =
| _ -> raise (Eval_error "component-source: expected (name)")); | _ -> raise (Eval_error "component-source: expected (name)"));
let jt0 = Unix.gettimeofday () in let jt0 = Unix.gettimeofday () in
let count = ref 0 in let count = ref 0 in
(* Pre-compile compiler HELPERS but NOT "compile" itself.
"compile" runs via CEK (correct for all AST sizes).
Its internal calls to compile-expr/emit-byte/etc use bytecode (fast).
Pre-compiling "compile" causes it to loop on complex nested forms. *)
let compiler_names = [ let compiler_names = [
"compile"; "compile-module"; "compile-expr"; "compile-symbol"; "compile-module"; "compile-expr"; "compile-symbol";
"compile-dict"; "compile-list"; "compile-if"; "compile-when"; "compile-dict"; "compile-list"; "compile-if"; "compile-when";
"compile-and"; "compile-or"; "compile-begin"; "compile-let"; "compile-and"; "compile-or"; "compile-begin"; "compile-let";
"compile-letrec"; "compile-lambda"; "compile-define"; "compile-set"; "compile-letrec"; "compile-lambda"; "compile-define"; "compile-set";
"compile-quote"; "compile-cond"; "compile-case"; "compile-case-clauses"; "compile-quote"; "compile-cond"; "compile-case"; "compile-case-clauses";
"compile-thread"; "compile-thread-step"; "compile-defcomp"; "compile-thread"; "compile-thread-step"; "compile-defcomp";
"compile-defisland"; "compile-defmacro"; "compile-defisland"; "compile-defmacro";
"compile-quasiquote"; "compile-qq-expr"; "compile-qq-list"; "compile-call";
"make-emitter"; "make-pool"; "make-scope"; "pool-add";
"scope-define-local"; "scope-resolve";
"emit-byte"; "emit-u16"; "emit-i16"; "emit-op"; "emit-const";
"current-offset"; "patch-i16";
] in ] in
List.iter (fun name -> List.iter (fun name ->
try try
@@ -2684,6 +2699,12 @@ let http_mode port =
| _ -> () | _ -> ()
with _ -> () with _ -> ()
) compiler_names; ) compiler_names;
(* Mark "compile" as jit-failed — its compiled bytecode loops on complex
ASTs. It runs via CEK (correct), while its helpers run as bytecode (fast). *)
(try match env_get env "compile" with
| Lambda l -> l.l_compiled <- Some Sx_vm.jit_failed_sentinel
| _ -> ()
with _ -> ());
let jt1 = Unix.gettimeofday () in let jt1 = Unix.gettimeofday () in
Printf.eprintf "[sx-http] JIT pre-compiled %d compiler fns in %.3fs\n%!" !count (jt1 -. jt0); Printf.eprintf "[sx-http] JIT pre-compiled %d compiler fns in %.3fs\n%!" !count (jt1 -. jt0);
(* Re-bind native primitives that stdlib.sx may have overwritten with (* Re-bind native primitives that stdlib.sx may have overwritten with