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

@@ -173,12 +173,31 @@
(= name "case")
(render-to-html (trampoline (eval-expr expr env)) env)
;; letrec — evaluate via CEK, render the result.
;; sf-letrec returns a thunk; the thunk handler in render-value-to-html
;; unwraps it and renders the expression with the letrec's local env.
;; letrec — pre-bind all names (nil), evaluate values, render body.
;; Can't use eval-expr on the whole form because the body contains
;; render expressions (div, lake, etc.) that eval-expr can't handle.
(= name "letrec")
(let ((result (eval-expr expr env)))
(render-value-to-html result env))
(let ((bindings (nth expr 1))
(body (slice expr 2))
(local (env-extend env)))
;; Phase 1: pre-bind all names to nil
(for-each (fn (pair)
(let ((pname (if (= (type-of (first pair)) "symbol")
(symbol-name (first pair))
(str (first pair)))))
(env-bind! local pname nil)))
bindings)
;; Phase 2: evaluate values (all names in scope for mutual recursion)
(for-each (fn (pair)
(let ((pname (if (= (type-of (first pair)) "symbol")
(symbol-name (first pair))
(str (first pair)))))
(env-set! local pname (trampoline (eval-expr (nth pair 1) local)))))
bindings)
;; Phase 3: eval non-last body exprs for side effects, render last
(when (> (len body) 1)
(for-each (fn (e) (trampoline (eval-expr e local))) (init body)))
(render-to-html (last body) local))
;; let / let* — single body: pass through. Multi: join strings.
(or (= name "let") (= name "let*"))