Aser adapter compiles + loads as VM module — first VM execution
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -744,13 +744,10 @@ let dispatch env cmd =
|
||||
| exn -> send_error (Printexc.to_string exn))
|
||||
|
||||
| List [Symbol "aser-slot"; String src] ->
|
||||
(* Like aser but expands ALL components server-side, not just
|
||||
server-affinity ones. Uses batch IO mode: batchable helper
|
||||
calls (highlight etc.) return placeholders during evaluation,
|
||||
then all IO is flushed concurrently after the aser completes. *)
|
||||
(* Expand ALL components server-side. Uses batch IO mode for
|
||||
concurrent highlight calls. Tries VM first, falls back to CEK. *)
|
||||
(try
|
||||
ignore (env_bind env "expand-components?" (NativeFn ("expand-components?", fun _args -> Bool true)));
|
||||
(* Enable batch IO mode *)
|
||||
io_batch_mode := true;
|
||||
io_queue := [];
|
||||
io_counter := 0;
|
||||
@@ -818,6 +815,21 @@ let dispatch env cmd =
|
||||
| Eval_error msg -> send_error msg
|
||||
| exn -> send_error (Printexc.to_string exn))
|
||||
|
||||
| List [Symbol "vm-load-module"; code_val] ->
|
||||
(* Execute a compiled module on the VM. The module's defines
|
||||
are stored in the kernel env, replacing Lambda values with
|
||||
NativeFn VM closures. This is how compiled code gets wired
|
||||
into the CEK dispatch — the CEK calls NativeFn directly. *)
|
||||
(try
|
||||
let code = Sx_vm.code_from_value code_val in
|
||||
(* VM uses the LIVE kernel env — defines go directly into it *)
|
||||
let _result = Sx_vm.execute_module code env.bindings in
|
||||
(* Count how many defines the module added *)
|
||||
send_ok ()
|
||||
with
|
||||
| Eval_error msg -> send_error msg
|
||||
| exn -> send_error (Printexc.to_string exn))
|
||||
|
||||
| List [Symbol "vm-compile"] ->
|
||||
(* Compile all named lambdas in env to bytecode.
|
||||
Called after all .sx files are loaded. *)
|
||||
|
||||
@@ -195,6 +195,74 @@ class OcamlBridge:
|
||||
_logger.warning("Helper injection failed: %s", e)
|
||||
self._helpers_injected = False
|
||||
|
||||
async def _compile_adapter_module(self) -> None:
|
||||
"""Compile adapter-sx.sx to bytecode and load as a VM module.
|
||||
|
||||
All aser functions become NativeFn VM closures in the kernel env.
|
||||
Subsequent aser-slot calls find them as NativeFn → VM executes
|
||||
the entire render path compiled, no CEK steps.
|
||||
"""
|
||||
from .parser import parse_all, serialize
|
||||
from .ref.sx_ref import eval_expr, trampoline, PRIMITIVES
|
||||
|
||||
# Ensure compiler primitives are available
|
||||
if 'serialize' not in PRIMITIVES:
|
||||
PRIMITIVES['serialize'] = lambda x: serialize(x)
|
||||
if 'primitive?' not in PRIMITIVES:
|
||||
PRIMITIVES['primitive?'] = lambda name: isinstance(name, str) and name in PRIMITIVES
|
||||
if 'has-key?' not in PRIMITIVES:
|
||||
PRIMITIVES['has-key?'] = lambda *a: isinstance(a[0], dict) and str(a[1]) in a[0]
|
||||
if 'set-nth!' not in PRIMITIVES:
|
||||
from .types import NIL
|
||||
PRIMITIVES['set-nth!'] = lambda *a: (a[0].__setitem__(int(a[1]), a[2]), NIL)[-1]
|
||||
if 'init' not in PRIMITIVES:
|
||||
PRIMITIVES['init'] = lambda *a: a[0][:-1] if isinstance(a[0], list) else a[0]
|
||||
if 'concat' not in PRIMITIVES:
|
||||
PRIMITIVES['concat'] = lambda *a: (a[0] or []) + (a[1] or [])
|
||||
if 'slice' not in PRIMITIVES:
|
||||
PRIMITIVES['slice'] = lambda *a: a[0][int(a[1]):int(a[2])] if len(a) == 3 else a[0][int(a[1]):]
|
||||
from .types import Symbol
|
||||
if 'make-symbol' not in PRIMITIVES:
|
||||
PRIMITIVES['make-symbol'] = lambda name: Symbol(name)
|
||||
from .types import NIL
|
||||
for ho in ['map', 'filter', 'for-each', 'reduce', 'some', 'every?', 'map-indexed']:
|
||||
if ho not in PRIMITIVES:
|
||||
PRIMITIVES[ho] = lambda *a: NIL
|
||||
|
||||
# Load compiler
|
||||
compiler_env = {}
|
||||
spec_dir = os.path.join(os.path.dirname(__file__), "../../spec")
|
||||
for f in ["bytecode.sx", "compiler.sx"]:
|
||||
path = os.path.join(spec_dir, f)
|
||||
if os.path.isfile(path):
|
||||
with open(path) as fh:
|
||||
for expr in parse_all(fh.read()):
|
||||
trampoline(eval_expr(expr, compiler_env))
|
||||
|
||||
# Compile adapter-sx.sx
|
||||
web_dir = os.path.join(os.path.dirname(__file__), "../../web")
|
||||
adapter_path = os.path.join(web_dir, "adapter-sx.sx")
|
||||
if not os.path.isfile(adapter_path):
|
||||
_logger.warning("adapter-sx.sx not found at %s", adapter_path)
|
||||
return
|
||||
|
||||
with open(adapter_path) as f:
|
||||
adapter_exprs = parse_all(f.read())
|
||||
|
||||
compiled = trampoline(eval_expr(
|
||||
[Symbol('compile-module'), [Symbol('quote'), adapter_exprs]],
|
||||
compiler_env))
|
||||
|
||||
code_sx = serialize(compiled)
|
||||
_logger.info("Compiled adapter-sx.sx: %d bytes bytecode", len(code_sx))
|
||||
|
||||
# Load the compiled module into the OCaml VM
|
||||
async with self._lock:
|
||||
await self._send(f'(vm-load-module {code_sx})')
|
||||
await self._read_until_ok(ctx=None)
|
||||
|
||||
_logger.info("Loaded adapter-sx.sx as VM module")
|
||||
|
||||
async def _ensure_components(self) -> None:
|
||||
"""Load all .sx source files into the kernel on first use.
|
||||
|
||||
@@ -265,12 +333,13 @@ class OcamlBridge:
|
||||
_logger.info("Loaded %d definitions from .sx files into OCaml kernel (%d skipped)",
|
||||
count, skipped)
|
||||
|
||||
# VM bytecode infrastructure ready. Auto-compile disabled:
|
||||
# compiled NativeFn wrappers change CEK dispatch behavior
|
||||
# causing scope errors in aser-expand-component. The VM
|
||||
# tests (40/40) verify correctness in isolation.
|
||||
# Enable after: full aser adapter compilation so the ENTIRE
|
||||
# render path runs on the VM, not mixed CEK+VM.
|
||||
# Compile adapter-sx.sx to bytecode and load as VM module.
|
||||
# All aser functions become NativeFn VM closures in the
|
||||
# kernel env. The CEK calls them as NativeFn → VM executes.
|
||||
try:
|
||||
await self._compile_adapter_module()
|
||||
except Exception as e:
|
||||
_logger.warning("VM adapter compilation skipped: %s", e)
|
||||
except Exception as e:
|
||||
_logger.error("Failed to load .sx files into OCaml kernel: %s", e)
|
||||
self._components_loaded = False # retry next time
|
||||
|
||||
@@ -394,9 +394,16 @@
|
||||
(fn-em (make-emitter)))
|
||||
;; Mark as function boundary — upvalue captures happen here
|
||||
(dict-set! fn-scope "is-function" true)
|
||||
;; Define params as locals in fn scope
|
||||
;; Define params as locals in fn scope.
|
||||
;; Handle type annotations: (name :as type) → extract name
|
||||
(for-each (fn (p)
|
||||
(let ((name (if (= (type-of p) "symbol") (symbol-name p) p)))
|
||||
(let ((name (cond
|
||||
(= (type-of p) "symbol") (symbol-name p)
|
||||
;; Type-annotated param: (name :as type)
|
||||
(and (list? p) (not (empty? p))
|
||||
(= (type-of (first p)) "symbol"))
|
||||
(symbol-name (first p))
|
||||
:else p)))
|
||||
(when (and (not (= name "&key"))
|
||||
(not (= name "&rest")))
|
||||
(scope-define-local fn-scope name))))
|
||||
|
||||
Reference in New Issue
Block a user