From 0ce23521b7c7a3898aeaa61cfa6adeee2759e377 Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 19 Mar 2026 21:18:34 +0000 Subject: [PATCH] =?UTF-8?q?Aser=20adapter=20compiles=20+=20loads=20as=20VM?= =?UTF-8?q?=20module=20=E2=80=94=20first=20VM=20execution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- hosts/ocaml/bin/sx_server.ml | 22 +++++++--- shared/sx/ocaml_bridge.py | 81 +++++++++++++++++++++++++++++++++--- spec/compiler.sx | 11 ++++- 3 files changed, 101 insertions(+), 13 deletions(-) diff --git a/hosts/ocaml/bin/sx_server.ml b/hosts/ocaml/bin/sx_server.ml index c65b40f..ecb806e 100644 --- a/hosts/ocaml/bin/sx_server.ml +++ b/hosts/ocaml/bin/sx_server.ml @@ -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. *) diff --git a/shared/sx/ocaml_bridge.py b/shared/sx/ocaml_bridge.py index d281b06..4e6c277 100644 --- a/shared/sx/ocaml_bridge.py +++ b/shared/sx/ocaml_bridge.py @@ -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 diff --git a/spec/compiler.sx b/spec/compiler.sx index 8a6d162..9a11a03 100644 --- a/spec/compiler.sx +++ b/spec/compiler.sx @@ -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))))