VM: VmClosure value type + iterative run loop + define hoisting + SSR fixes

Core VM changes:
- Add VmClosure value variant — inner closures created by OP_CLOSURE are
  first-class VM values, not NativeFn wrappers around call_closure
- Convert `run` from recursive to while-loop — zero OCaml stack growth,
  true TCO for VmClosure tail calls
- vm_call handles VmClosure by pushing frame on current VM (no new VM
  allocation per call)
- Forward ref _vm_call_closure_ref for cross-boundary calls (CEK/primitives)

Compiler (spec/compiler.sx):
- Define hoisting in compile-begin: pre-allocate local slots for all
  define forms before compiling any values. Fixes forward references
  between inner functions (e.g. read-expr referencing skip-ws in sx-parse)
- scope-define-local made idempotent (skip if slot already exists)

Server (sx_server.ml):
- JIT fail-once sentinel: mark l_compiled as failed after first VM runtime
  error. Eliminates thousands of retry attempts per page render.
- HTML tag bindings: register all HTML tags as pass-through NativeFns so
  eval-expr can handle (div ...) etc. in island component bodies.
- Log VM FAIL errors with function name before disabling JIT.

SSR fixes:
- adapter-html.sx letrec handler: evaluate bindings in proper letrec scope
  (pre-bind nil, then evaluate), render body with render-to-html instead of
  eval-expr. Fixes island SSR for components using letrec.
- Add `init` primitive to OCaml kernel (all-but-last of list).
- VmClosure handling in sx_runtime.ml sx_call dispatch.

Tests: 971/971 OCaml (+19 new), 0 failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-23 23:39:35 +00:00
parent 8958714c85
commit dd057247a5
9 changed files with 658 additions and 299 deletions

View File

@@ -622,6 +622,14 @@ let make_server_env () =
| [String s] -> Bool (Sx_render.is_html_tag s)
| _ -> Bool false);
(* HTML tag functions — when eval-expr encounters (div :class "foo" ...),
it calls the tag function which returns the list as-is. The render path
then handles it. Same approach as the DOM adapter in the browser. *)
List.iter (fun tag ->
ignore (env_bind env tag
(NativeFn ("html:" ^ tag, fun args -> List (Symbol tag :: args))))
) Sx_render.html_tags;
(* Spec evaluator helpers needed by render.sx when loaded at runtime *)
bind "random-int" (fun args ->
match args with
@@ -907,9 +915,14 @@ let register_jit_hook env =
| Lambda l ->
(match l.l_compiled with
| Some cl when not (Sx_vm.is_jit_failed cl) ->
(* Cached bytecode — run on VM, fall back to CEK on runtime error *)
(* Cached bytecode — run on VM, fall back to CEK on runtime error.
Mark as failed so we don't retry on every call. *)
(try Some (Sx_vm.call_closure cl args cl.vm_env_ref)
with _ -> None)
with e ->
let fn_name = match l.l_name with Some n -> n | None -> "?" in
Printf.eprintf "[jit-hook] VM FAIL %s: %s (disabling JIT)\n%!" fn_name (Printexc.to_string e);
l.l_compiled <- Some Sx_vm.jit_failed_sentinel;
None)
| Some _ -> None (* compile failed — CEK handles *)
| None ->
if !_jit_compiling then None
@@ -927,7 +940,11 @@ let register_jit_hook env =
l.l_compiled <- Some cl;
(* Run on VM, fall back to CEK on runtime error *)
(try Some (Sx_vm.call_closure cl args cl.vm_env_ref)
with _ -> None)
with e ->
let fn_name = match l.l_name with Some n -> n | None -> "?" in
Printf.eprintf "[jit-hook] VM FAIL (first call) %s: %s (disabling JIT)\n%!" fn_name (Printexc.to_string e);
l.l_compiled <- Some Sx_vm.jit_failed_sentinel;
None)
| None -> None
end)
| _ -> None)