OCaml runtime: R7RS parameters, VM closure introspection, import suspension

- R7RS parameter primitives (make-parameter, parameter?, parameterize support)
- VM closure get_val introspection (vm-code, vm-upvalues, vm-name, vm-globals)
- Lazy list caching on vm_code for transpiled VM performance
- VM import suspension: check_io_suspension + resume_module for browser lazy loading
- 23 new R7RS tests (parameter-basic, parameterize-basic, syntax-rules-basic)
- Playwright bytecode-loading spec + WASM rebuild

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-04 18:48:51 +00:00
parent 2727577702
commit 4baed1853c
16 changed files with 692 additions and 178 deletions

View File

@@ -376,7 +376,8 @@ let vm_create_closure vm_val frame_val code_val =
(* --- JIT sentinel --- *)
let _jit_failed_sentinel = {
vm_code = { vc_arity = -1; vc_locals = 0; vc_bytecode = [||]; vc_constants = [||] };
vm_code = { vc_arity = -1; vc_locals = 0; vc_bytecode = [||]; vc_constants = [||];
vc_bytecode_list = None; vc_constants_list = None };
vm_upvalues = [||]; vm_name = Some "__jit_failed__"; vm_env_ref = Hashtbl.create 0; vm_closure_env = None
}
let _is_jit_failed cl = cl.vm_code.vc_arity = -1
@@ -574,7 +575,20 @@ let () = _vm_call_fn := vm_call
Public API — matches Sx_vm interface for drop-in replacement
================================================================ *)
(** Execute a compiled module — entry point for load-sxbc, compile-blob. *)
(** Build a suspension dict from __io_request in globals. *)
let check_io_suspension globals vm_val =
match Hashtbl.find_opt globals "__io_request" with
| Some req when sx_truthy req ->
let d = Hashtbl.create 4 in
Hashtbl.replace d "suspended" (Bool true);
Hashtbl.replace d "op" (String "import");
Hashtbl.replace d "request" req;
Hashtbl.replace d "vm" vm_val;
Some (Dict d)
| _ -> None
(** Execute a compiled module — entry point for load-sxbc, compile-blob.
Returns the result value, or a suspension dict if OP_PERFORM fired. *)
let execute_module (code : vm_code) (globals : (string, value) Hashtbl.t) =
let cl = { vm_code = code; vm_upvalues = [||]; vm_name = Some "module";
vm_env_ref = globals; vm_closure_env = None } in
@@ -587,7 +601,25 @@ let execute_module (code : vm_code) (globals : (string, value) Hashtbl.t) =
done;
m.vm_frames <- [frame];
ignore (vm_run vm_val);
vm_pop vm_val
match check_io_suspension globals vm_val with
| Some suspension -> suspension
| None -> vm_pop vm_val
(** Resume a suspended module. Clears __io_request, pushes nil, re-runs. *)
let resume_module (suspended : value) =
match suspended with
| Dict d ->
let vm_val = Hashtbl.find d "vm" in
let globals = match vm_val with
| VmMachine m -> m.vm_globals
| _ -> raise (Eval_error "resume_module: expected VmMachine") in
Hashtbl.replace globals "__io_request" Nil;
ignore (vm_push vm_val Nil);
ignore (vm_run vm_val);
(match check_io_suspension globals vm_val with
| Some suspension -> suspension
| None -> vm_pop vm_val)
| _ -> raise (Eval_error "resume_module: expected suspension dict")
(** Execute a closure with args — entry point for JIT Lambda calls. *)
let call_closure (cl : vm_closure) (args : value list) (globals : (string, value) Hashtbl.t) =

View File

@@ -208,6 +208,8 @@ let () =
for i = 0 to Array.length a.r_fields - 1 do
if not (safe_eq a.r_fields.(i) b.r_fields.(i)) then eq := false
done; !eq)
(* Parameters: same UID = same parameter *)
| Parameter a, Parameter b -> a.pm_uid = b.pm_uid
(* Lambda/Component/Island/Signal/NativeFn: physical only *)
| _ -> false
in
@@ -732,6 +734,7 @@ let () =
String (Printf.sprintf "~%s" i.i_name)
| [Lambda _] -> String "<lambda>"
| [Record r] -> String (Printf.sprintf "#<%s>" r.r_type.rt_name)
| [Parameter p] -> String (Printf.sprintf "#<parameter %s>" p.pm_uid)
| [a] -> String (inspect a) (* used for dedup keys in compiler *)
| _ -> raise (Eval_error "serialize: 1 arg"));
register "make-symbol" (fun args ->
@@ -951,6 +954,39 @@ let () =
register "make-record-mutator" (fun args ->
match args with [idx] -> make_record_mutator idx
| _ -> raise (Eval_error "make-record-mutator: expected (index)"));
(* R7RS parameters — converter stored, applied by parameterize frame *)
register "make-parameter" (fun args ->
match args with
| [init] ->
let uid = !param_counter in
incr param_counter;
Parameter { pm_uid = "__param_" ^ string_of_int uid;
pm_default = init; pm_converter = None }
| [init; converter] ->
let uid = !param_counter in
incr param_counter;
(* Apply converter to init for NativeFn, store raw for Lambda *)
let converted = match converter with
| NativeFn (_, f) -> f [init]
| _ -> init (* Lambda converters applied via CEK at parameterize time *)
in
Parameter { pm_uid = "__param_" ^ string_of_int uid;
pm_default = converted; pm_converter = Some converter }
| _ -> raise (Eval_error "make-parameter: expected 1-2 args"));
register "parameter?" (fun args ->
match args with [Parameter _] -> Bool true | [_] -> Bool false
| _ -> Bool false);
register "parameter-uid" (fun args ->
match args with [Parameter p] -> String p.pm_uid
| _ -> raise (Eval_error "parameter-uid: expected parameter"));
register "parameter-default" (fun args ->
match args with [Parameter p] -> p.pm_default
| _ -> raise (Eval_error "parameter-default: expected parameter"));
register "parameter-converter" (fun args ->
match args with
| [Parameter p] -> (match p.pm_converter with Some c -> c | None -> Nil)
| _ -> raise (Eval_error "parameter-converter: expected parameter"));
register "is-else-clause?" (fun args ->
match args with
| [Keyword "else"] -> Bool true

View File

@@ -131,6 +131,38 @@ let get_val container key =
| "frames" -> List (List.map (fun f -> VmFrame f) m.vm_frames)
| "globals" -> Dict m.vm_globals
| _ -> Nil)
| VmClosure cl, String k ->
(match k with
| "vm-code" ->
(* Return vm_code fields as a Dict. The bytecode and constants arrays
are lazily converted to Lists and cached on the vm_code record so
the transpiled VM loop (which re-derives bc/consts each iteration)
doesn't allocate on every step. *)
let c = cl.vm_code in
let bc = match c.vc_bytecode_list with
| Some l -> l
| None ->
let l = Array.to_list (Array.map (fun i -> Number (float_of_int i)) c.vc_bytecode) in
c.vc_bytecode_list <- Some l; l in
let consts = match c.vc_constants_list with
| Some l -> l
| None ->
let l = Array.to_list c.vc_constants in
c.vc_constants_list <- Some l; l in
let d = Hashtbl.create 4 in
Hashtbl.replace d "vc-bytecode" (List bc);
Hashtbl.replace d "vc-constants" (List consts);
Hashtbl.replace d "vc-arity" (Number (float_of_int c.vc_arity));
Hashtbl.replace d "vc-locals" (Number (float_of_int c.vc_locals));
Dict d
| "vm-upvalues" ->
List (Array.to_list (Array.map (fun uv -> uv.uv_value) cl.vm_upvalues))
| "vm-name" ->
(match cl.vm_name with Some n -> String n | None -> Nil)
| "vm-globals" -> Dict cl.vm_env_ref
| "vm-closure-env" ->
(match cl.vm_closure_env with Some e -> Env e | None -> Nil)
| _ -> Nil)
| Dict d, String k -> dict_get d k
| Dict d, Keyword k -> dict_get d k
| (List l | ListRef { contents = l }), Number n ->

View File

@@ -179,6 +179,8 @@ and vm_code = {
vc_locals : int;
vc_bytecode : int array;
vc_constants : value array;
mutable vc_bytecode_list : value list option; (** Lazy cache for transpiled VM *)
mutable vc_constants_list : value list option; (** Lazy cache for transpiled VM *)
}
(** Upvalue cell — shared mutable reference to a captured variable. *)

View File

@@ -41,7 +41,8 @@ let jit_compile_ref : (lambda -> (string, value) Hashtbl.t -> vm_closure option)
(** Sentinel closure indicating JIT compilation was attempted and failed.
Prevents retrying compilation on every call. *)
let jit_failed_sentinel = {
vm_code = { vc_arity = -1; vc_locals = 0; vc_bytecode = [||]; vc_constants = [||] };
vm_code = { vc_arity = -1; vc_locals = 0; vc_bytecode = [||]; vc_constants = [||];
vc_bytecode_list = None; vc_constants_list = None };
vm_upvalues = [||]; vm_name = Some "__jit_failed__"; vm_env_ref = Hashtbl.create 0; vm_closure_env = None
}
@@ -131,8 +132,10 @@ let code_from_value v =
let arity = match Hashtbl.find_opt d "arity" with
| Some (Number n) -> int_of_float n | _ -> 0
in
{ vc_arity = arity; vc_locals = arity + 16; vc_bytecode = bc_list; vc_constants = constants }
| _ -> { vc_arity = 0; vc_locals = 16; vc_bytecode = [||]; vc_constants = [||] }
{ vc_arity = arity; vc_locals = arity + 16; vc_bytecode = bc_list; vc_constants = constants;
vc_bytecode_list = None; vc_constants_list = None }
| _ -> { vc_arity = 0; vc_locals = 16; vc_bytecode = [||]; vc_constants = [||];
vc_bytecode_list = None; vc_constants_list = None }
(** Call an SX value via CEK, detecting suspension instead of erroring.
Returns the result value, or raises VmSuspended if CEK suspends.

View File

@@ -287,7 +287,8 @@ let vm_create_closure vm_val frame_val code_val =
(* --- JIT sentinel --- *)
let _jit_failed_sentinel = {
vm_code = { vc_arity = -1; vc_locals = 0; vc_bytecode = [||]; vc_constants = [||] };
vm_code = { vc_arity = -1; vc_locals = 0; vc_bytecode = [||]; vc_constants = [||];
vc_bytecode_list = None; vc_constants_list = None };
vm_upvalues = [||]; vm_name = Some "__jit_failed__"; vm_env_ref = Hashtbl.create 0; vm_closure_env = None
}
let _is_jit_failed cl = cl.vm_code.vc_arity = -1

View File

@@ -287,7 +287,8 @@ let vm_create_closure vm_val frame_val code_val =
(* --- JIT sentinel --- *)
let _jit_failed_sentinel = {
vm_code = { vc_arity = -1; vc_locals = 0; vc_bytecode = [||]; vc_constants = [||] };
vm_code = { vc_arity = -1; vc_locals = 0; vc_bytecode = [||]; vc_constants = [||];
vc_bytecode_list = None; vc_constants_list = None };
vm_upvalues = [||]; vm_name = Some "__jit_failed__"; vm_env_ref = Hashtbl.create 0; vm_closure_env = None
}
let _is_jit_failed cl = cl.vm_code.vc_arity = -1
@@ -455,7 +456,20 @@ let () = _vm_call_fn := vm_call
Public API — matches Sx_vm interface for drop-in replacement
================================================================ *)
(** Execute a compiled module — entry point for load-sxbc, compile-blob. *)
(** Build a suspension dict from __io_request in globals. *)
let check_io_suspension globals vm_val =
match Hashtbl.find_opt globals "__io_request" with
| Some req when sx_truthy req ->
let d = Hashtbl.create 4 in
Hashtbl.replace d "suspended" (Bool true);
Hashtbl.replace d "op" (String "import");
Hashtbl.replace d "request" req;
Hashtbl.replace d "vm" vm_val;
Some (Dict d)
| _ -> None
(** Execute a compiled module — entry point for load-sxbc, compile-blob.
Returns the result value, or a suspension dict if OP_PERFORM fired. *)
let execute_module (code : vm_code) (globals : (string, value) Hashtbl.t) =
let cl = { vm_code = code; vm_upvalues = [||]; vm_name = Some "module";
vm_env_ref = globals; vm_closure_env = None } in
@@ -468,7 +482,25 @@ let execute_module (code : vm_code) (globals : (string, value) Hashtbl.t) =
done;
m.vm_frames <- [frame];
ignore (vm_run vm_val);
vm_pop vm_val
match check_io_suspension globals vm_val with
| Some suspension -> suspension
| None -> vm_pop vm_val
(** Resume a suspended module. Clears __io_request, pushes nil, re-runs. *)
let resume_module (suspended : value) =
match suspended with
| Dict d ->
let vm_val = Hashtbl.find d "vm" in
let globals = match vm_val with
| VmMachine m -> m.vm_globals
| _ -> raise (Eval_error "resume_module: expected VmMachine") in
Hashtbl.replace globals "__io_request" Nil;
ignore (vm_push vm_val Nil);
ignore (vm_run vm_val);
(match check_io_suspension globals vm_val with
| Some suspension -> suspension
| None -> vm_pop vm_val)
| _ -> raise (Eval_error "resume_module: expected suspension dict")
(** Execute a closure with args — entry point for JIT Lambda calls. *)
let call_closure (cl : vm_closure) (args : value list) (globals : (string, value) Hashtbl.t) =

File diff suppressed because one or more lines are too long

View File

@@ -1792,7 +1792,7 @@
blake2_js_for_wasm_create: blake2_js_for_wasm_create};
}
(globalThis))
({"link":[["runtime-0db9b496",0],["prelude-d7e4b000",0],["stdlib-23ce0836",[]],["sx-7a3621b8",[2]],["jsoo_runtime-f96b44a8",[2]],["js_of_ocaml-651f6707",[2,4]],["dune__exe__Sx_browser-e2e6110d",[2,3,5]],["std_exit-10fb8830",[2]],["start-f5d3f095",0]],"generated":(b=>{var
({"link":[["runtime-0db9b496",0],["prelude-d7e4b000",0],["stdlib-23ce0836",[]],["sx-d3c0ad56",[2]],["jsoo_runtime-f96b44a8",[2]],["js_of_ocaml-651f6707",[2,4]],["dune__exe__Sx_browser-e2e6110d",[2,3,5]],["std_exit-10fb8830",[2]],["start-f5d3f095",0]],"generated":(b=>{var
c=b,a=b?.module?.export||b;return{"env":{"caml_ba_kind_of_typed_array":()=>{throw new
Error("caml_ba_kind_of_typed_array not implemented")},"caml_exn_with_js_backtrace":()=>{throw new
Error("caml_exn_with_js_backtrace not implemented")},"caml_int64_create_lo_mi_hi":()=>{throw new

View File

@@ -196,3 +196,176 @@
(assert (boolean=? true true))
(assert (boolean=? false false))
(assert (not (boolean=? true false)))))
(defsuite
"parameter-basic"
(deftest
"make-parameter creates parameter"
(let ((p (make-parameter 42))) (assert (parameter? p))))
(deftest
"parameter returns default value"
(let ((p (make-parameter 42))) (assert= 42 (p))))
(deftest
"parameter? false for non-parameters"
(do
(assert= false (parameter? 42))
(assert= false (parameter? "hello"))
(assert= false (parameter? (list 1 2)))))
(deftest
"two parameters are independent"
(let
((p1 (make-parameter 10)) (p2 (make-parameter 20)))
(do (assert= 10 (p1)) (assert= 20 (p2))))))
(defsuite
"parameterize-basic"
(deftest
"parameterize rebinds single parameter"
(let
((p (make-parameter 1)))
(assert= 99 (parameterize ((p 99)) (p)))))
(deftest
"parameterize restores after body"
(let
((p (make-parameter 1)))
(do (parameterize ((p 99)) (p)) (assert= 1 (p)))))
(deftest
"parameterize with multiple bindings"
(let
((p1 (make-parameter 10)) (p2 (make-parameter 20)))
(parameterize
((p1 100) (p2 200))
(do (assert= 100 (p1)) (assert= 200 (p2))))))
(deftest
"nested parameterize"
(let
((p (make-parameter 1)))
(parameterize
((p 10))
(do
(assert= 10 (p))
(parameterize ((p 100)) (assert= 100 (p)))
(assert= 10 (p))))))
(deftest
"parameterize with empty bindings"
(assert= 42 (parameterize () 42)))
(deftest
"parameterize body returns last expr"
(let
((p (make-parameter 0)))
(assert= 3 (parameterize ((p 3)) 1 2 (p))))))
(defsuite
"syntax-rules-basic"
(deftest
"simple constant pattern"
(do
(define-syntax my-const
(syntax-rules ()
((_) 42)))
(assert= 42 (my-const))))
(deftest
"pattern with variable"
(do
(define-syntax my-id
(syntax-rules ()
((_ x) x)))
(assert= 7 (my-id 7))))
(deftest
"variable in template expression"
(do
(define-syntax my-double
(syntax-rules ()
((_ x) (+ x x))))
(assert= 10 (my-double 5))))
(deftest
"multiple clauses by arity"
(do
(define-syntax my-if2
(syntax-rules ()
((_ test then) (if test then nil))
((_ test then else-expr) (if test then else-expr))))
(assert= 1 (my-if2 true 1))
(assert= 2 (my-if2 false 1 2))))
(deftest
"ellipsis collects zero-or-more"
(do
(define-syntax my-list
(syntax-rules ()
((_ x ...) (list x ...))))
(assert= (list 1 2 3) (my-list 1 2 3))
(assert= (list) (my-list))))
(deftest
"nested pattern"
(do
(define-syntax my-let1
(syntax-rules ()
((_ ((var val)) body) (let ((var val)) body))))
(assert= 10 (my-let1 ((x 10)) x))))
(deftest
"literal keyword matching"
(do
(define-syntax my-arrow
(syntax-rules (=>)
((_ x => y) (list x y))))
(assert= (list 1 2) (my-arrow 1 => 2))))
(deftest
"literal keyword no match falls through"
(do
(define-syntax my-cond
(syntax-rules (=>)
((_ x => fn-expr) (fn-expr x))
((_ x y) (list x y))))
(assert= (list 3 4) (my-cond 3 4))))
(deftest
"recursive macro with ellipsis"
(do
(define-syntax my-and
(syntax-rules ()
((_) true)
((_ e) e)
((_ e1 e2 ...) (if e1 (my-and e2 ...) false))))
(assert= true (my-and))
(assert= 5 (my-and 5))
(assert= true (my-and true true true))
(assert= false (my-and true false true))))
(deftest
"swap macro"
(do
(define-syntax my-swap!
(syntax-rules ()
((_ a b) (let ((tmp a)) (set! a b) (set! b tmp)))))
(let ((x 1) (y 2))
(my-swap! x y)
(assert= 2 x)
(assert= 1 y))))
(deftest
"when macro via syntax-rules"
(do
(define-syntax my-when
(syntax-rules ()
((_ test body ...) (if test (do body ...) nil))))
(assert= nil (my-when false 1 2 3))
(assert= 3 (my-when true 1 2 3))))
(deftest
"nested ellipsis in binding pairs"
(do
(define-syntax my-let
(syntax-rules ()
((_ ((var val) ...) body)
(let ((var val) ...) body))))
(assert= 6 (my-let ((a 1) (b 2) (c 3)) (+ a b c)))))
(deftest
"or macro with short-circuit"
(do
(define-syntax my-or
(syntax-rules ()
((_) false)
((_ e) e)
((_ e1 e2 ...)
(let ((t e1)) (if t t (my-or e2 ...))))))
(assert= false (my-or))
(assert= 42 (my-or 42))
(assert= 1 (my-or 1 2 3))
(assert= 3 (my-or false false 3))
(assert= false (my-or false false false)))))

View File

@@ -1,8 +1,7 @@
{
"status": "failed",
"failedTests": [
"f74e9a54851d8fcfaffa-13e0d4c7d93026c85c6c",
"a2add2f401dce5f22243-5b9be27c24aceaec6daa",
"63f6db7ebc03cc4b82b9-ce6ce7a4bd4491603196"
"0ca76506ebddb95b746c-2b2f50f2cbbb858d1272",
"0ca76506ebddb95b746c-8f9d78e488ffc61daf33"
]
}

View File

@@ -0,0 +1,33 @@
# Page snapshot
```yaml
- main [ref=e4]:
- generic [ref=e6]:
- complementary
- generic [ref=e7]:
- generic [ref=e8]:
- generic [ref=e11]:
- link "(<sx>)" [ref=e12] [cursor=pointer]:
- /url: /sx/
- generic [ref=e14]: (<sx>)
- paragraph [ref=e15]: The framework-free reactive hypermedium
- paragraph [ref=e17]: © Giles Bradshaw 2026· /sx/
- generic [ref=e19]:
- link "Geography" [ref=e20] [cursor=pointer]:
- /url: /sx/(geography)
- link "Language" [ref=e21] [cursor=pointer]:
- /url: /sx/(language)
- link "Applications" [ref=e22] [cursor=pointer]:
- /url: /sx/(applications)
- link "Tools" [ref=e23] [cursor=pointer]:
- /url: /sx/(tools)
- link "Etc" [ref=e24] [cursor=pointer]:
- /url: /sx/(etc)
- generic [ref=e29]:
- generic [ref=e30]: (div (~tw :tokens "text-center") (h1 (~tw :tokens "text-3xl font-bold mb-2") (span (~tw :tokens "text-rose-500") "the ") (span (~tw :tokens "text-amber-500") "joy ") (span (~tw :tokens "text-emerald-500") "of ") (span (~tw :tokens "text-violet-600 text-4xl") "sx")))
- generic [ref=e31]:
- button "◀" [ref=e32] [cursor=pointer]
- generic [ref=e33]: 16 / 16
- button "▶" [ref=e34] [cursor=pointer]
- heading "the joy of sx" [level=1] [ref=e37]
```

View File

@@ -0,0 +1,158 @@
# Page snapshot
```yaml
- main [ref=e4]:
- generic [ref=e6]:
- complementary
- generic [ref=e7]:
- generic [ref=e8]:
- generic [ref=e11]:
- link "(<sx>)" [ref=e12] [cursor=pointer]:
- /url: /sx/
- generic [ref=e14]: (<sx>)
- paragraph [ref=e15]: The framework-free reactive hypermedium
- paragraph [ref=e17]: © Giles Bradshaw 2026· /sx/(geography)
- generic [ref=e18]:
- link "← Etc" [ref=e19] [cursor=pointer]:
- /url: /sx/(etc)
- link "Geography" [ref=e20] [cursor=pointer]:
- /url: /sx/(geography)
- link "Language →" [ref=e21] [cursor=pointer]:
- /url: /sx/(language)
- generic [ref=e23]:
- link "Reactive Islands" [ref=e24] [cursor=pointer]:
- /url: /sx/(geography.(reactive))
- link "Hypermedia Lakes" [ref=e25] [cursor=pointer]:
- /url: /sx/(geography.(hypermedia))
- link "Scopes" [ref=e26] [cursor=pointer]:
- /url: /sx/(geography.(scopes))
- link "Provide / Emit!" [ref=e27] [cursor=pointer]:
- /url: /sx/(geography.(provide))
- link "Spreads" [ref=e28] [cursor=pointer]:
- /url: /sx/(geography.(spreads))
- link "Marshes" [ref=e29] [cursor=pointer]:
- /url: /sx/(geography.(marshes))
- link "Isomorphism" [ref=e30] [cursor=pointer]:
- /url: /sx/(geography.(isomorphism))
- link "CEK Machine" [ref=e31] [cursor=pointer]:
- /url: /sx/(geography.(cek))
- link "Capabilities" [ref=e32] [cursor=pointer]:
- /url: /sx/(geography.(capabilities))
- link "Reactive Runtime" [ref=e33] [cursor=pointer]:
- /url: /sx/(geography.(reactive-runtime))
- generic [ref=e36]:
- heading "Geography" [level=2] [ref=e37]
- paragraph [ref=e38]: Where code runs and how it gets there. Geography maps the rendering pipeline from server-side evaluation through wire formats to client-side hydration.
- generic [ref=e39]:
- heading "Rendering Pipeline" [level=3] [ref=e40]
- generic [ref=e41]:
- generic [ref=e42]:
- generic [ref=e43]: OCaml kernel
- generic [ref=e44]:
- paragraph [ref=e45]: The evaluator is a CEK machine written in SX and bootstrapped to OCaml. It evaluates page definitions, expands components, resolves IO (helpers, queries), and serializes the result as SX wire format.
- paragraph [ref=e46]: spec/evaluator.sx → hosts/ocaml/ → aser-slot with batch IO
- generic [ref=e47]:
- generic [ref=e48]: Wire format
- generic [ref=e49]:
- paragraph [ref=e50]:
- text: The aser (async-serialize) mode produces SX text — HTML tags and component calls serialized as s-expressions. Components with server affinity are expanded; client components stay as calls. The wire format is placed in a
- code [ref=e51]: <script type="text/sx">
- text: tag inside the HTML shell.
- paragraph [ref=e52]: web/adapter-sx.sx → SxExpr values pass through serialize unquoted
- generic [ref=e53]:
- generic [ref=e54]: sx-browser.js
- generic [ref=e55]:
- paragraph [ref=e56]: The client engine parses the SX wire format, evaluates component definitions, renders the DOM, and hydrates reactive islands. It includes the same CEK evaluator (transpiled from the spec), the parser, all web adapters, and the orchestration layer for fetch/swap/polling.
- paragraph [ref=e57]: spec/ + web/ → hosts/javascript/cli.py → sx-browser.js (~400KB)
- heading "What lives where" [level=3] [ref=e58]
- generic [ref=e59]:
- generic [ref=e60]:
- heading "Spec (shared)" [level=4] [ref=e61]
- paragraph [ref=e62]: "The canonical SX language, bootstrapped identically to OCaml, JavaScript, and Python:"
- list [ref=e63]:
- listitem [ref=e64]: CEK evaluator — frames, step function, call dispatch
- listitem [ref=e65]: Parser — tokenizer, s-expression reader, serializer
- listitem [ref=e66]: Primitives — ~80 built-in pure functions
- listitem [ref=e67]: Render modes — HTML, SX wire, DOM
- generic [ref=e68]:
- heading "Web Adapters" [level=4] [ref=e69]
- paragraph [ref=e70]: "SX-defined modules that run on both server and client:"
- list [ref=e71]:
- listitem [ref=e72]: adapter-sx.sx — aser wire format (server component expansion)
- listitem [ref=e73]: adapter-html.sx — server HTML rendering
- listitem [ref=e74]: adapter-dom.sx — client DOM rendering
- listitem [ref=e75]: orchestration.sx — fetch, swap, polling, navigation
- listitem [ref=e76]: engine.sx — trigger parsing, request building
- generic [ref=e77]:
- heading "OCaml Kernel (server)" [level=4] [ref=e78]
- paragraph [ref=e79]: "Persistent process connected via a binary pipe protocol:"
- list [ref=e80]:
- listitem [ref=e81]: CEK evaluator + VM bytecode compiler
- listitem [ref=e82]: Batch IO bridge — defers helper/query calls to Python
- listitem [ref=e83]: Length-prefixed blob protocol — no string escaping
- listitem [ref=e84]: Component hot-reload on .sx file changes
- generic [ref=e85]:
- heading "sx-browser.js (client)" [level=4] [ref=e86]
- paragraph [ref=e87]: "Single JS bundle transpiled from spec + web adapters:"
- list [ref=e88]:
- listitem [ref=e89]: Parses SX wire format from script tags
- listitem [ref=e90]: Renders component trees to DOM
- listitem [ref=e91]: Hydrates reactive islands (signals, effects)
- listitem [ref=e92]: Client-side routing with defpage
- listitem [ref=e93]: HTMX-like fetch/swap orchestration
- heading "Rendering Modes" [level=3] [ref=e94]
- table [ref=e96]:
- rowgroup [ref=e97]:
- row "Mode Runs on Components Output" [ref=e98]:
- columnheader "Mode" [ref=e99]
- columnheader "Runs on" [ref=e100]
- columnheader "Components" [ref=e101]
- columnheader "Output" [ref=e102]
- rowgroup [ref=e103]:
- row "render-to-html Server (OCaml) Expanded recursively HTML string" [ref=e104]:
- cell "render-to-html" [ref=e105]
- cell "Server (OCaml)" [ref=e106]
- cell "Expanded recursively" [ref=e107]
- cell "HTML string" [ref=e108]
- row "aser / aser-slot Server (OCaml) Server-affinity expanded; client preserved SX wire format" [ref=e109]:
- cell "aser / aser-slot" [ref=e110]
- cell "Server (OCaml)" [ref=e111]
- cell "Server-affinity expanded; client preserved" [ref=e112]
- cell "SX wire format" [ref=e113]
- row "render-to-dom Client (sx-browser.js) Expanded recursively DOM nodes" [ref=e114]:
- cell "render-to-dom" [ref=e115]
- cell "Client (sx-browser.js)" [ref=e116]
- cell "Expanded recursively" [ref=e117]
- cell "DOM nodes" [ref=e118]
- row "client routing Client (sx-browser.js) defpage content evaluated locally DOM swap" [ref=e119]:
- cell "client routing" [ref=e120]
- cell "Client (sx-browser.js)" [ref=e121]
- cell "defpage content evaluated locally" [ref=e122]
- cell "DOM swap" [ref=e123]
- heading "Topics" [level=3] [ref=e124]
- generic [ref=e125]:
- link "Hypermedia Lakes Server-driven UI with sx-get/post/put/delete — fetch, swap, and the request lifecycle." [ref=e126] [cursor=pointer]:
- /url: /sx/(geography.(hypermedia))
- heading "Hypermedia Lakes" [level=4] [ref=e127]
- paragraph [ref=e128]: Server-driven UI with sx-get/post/put/delete — fetch, swap, and the request lifecycle.
- link "Reactive Islands Client-side signals and effects hydrated from server-rendered HTML. defisland, deref, lakes." [ref=e129] [cursor=pointer]:
- /url: /sx/(geography.(reactive))
- heading "Reactive Islands" [level=4] [ref=e130]
- paragraph [ref=e131]: Client-side signals and effects hydrated from server-rendered HTML. defisland, deref, lakes.
- link "Marshes Where reactivity and hypermedia interpenetrate — server writes to signals, reactive views reshape server content." [ref=e132] [cursor=pointer]:
- /url: /sx/(geography.(marshes))
- heading "Marshes" [level=4] [ref=e133]
- paragraph [ref=e134]: Where reactivity and hypermedia interpenetrate — server writes to signals, reactive views reshape server content.
- link "Scopes Render-time dynamic scope — the primitive beneath provide, collect!, spreads, and islands." [ref=e135] [cursor=pointer]:
- /url: /sx/(geography.(scopes))
- heading "Scopes" [level=4] [ref=e136]
- paragraph [ref=e137]: Render-time dynamic scope — the primitive beneath provide, collect!, spreads, and islands.
- link "CEK Machine The evaluator internals — frames, continuations, tail-call optimization, and the VM bytecode compiler." [ref=e138] [cursor=pointer]:
- /url: /sx/(geography.(cek))
- heading "CEK Machine" [level=4] [ref=e139]
- paragraph [ref=e140]: The evaluator internals — frames, continuations, tail-call optimization, and the VM bytecode compiler.
- link "Isomorphism One spec, multiple hosts — how the same SX code runs on OCaml, JavaScript, and Python." [ref=e141] [cursor=pointer]:
- /url: /sx/(geography.(isomorphism))
- heading "Isomorphism" [level=4] [ref=e142]
- paragraph [ref=e143]: One spec, multiple hosts — how the same SX code runs on OCaml, JavaScript, and Python.
```

View File

@@ -1,63 +0,0 @@
# Page snapshot
```yaml
- main [ref=e4]:
- generic [ref=e6]:
- complementary
- generic [ref=e7]:
- generic [ref=e8]:
- generic [ref=e11]:
- link "(<sx>)" [ref=e12] [cursor=pointer]:
- /url: /sx/
- generic [ref=e14]: (<sx>)
- paragraph [ref=e15]: The framework-free reactive hypermedium
- paragraph [ref=e17]: © Giles Bradshaw 2026· /sx/(geography.(reactive.(examples.reactive-list)))
- generic [ref=e18]:
- link "← Etc" [ref=e19] [cursor=pointer]:
- /url: /sx/(etc)
- link "Geography" [ref=e20] [cursor=pointer]:
- /url: /sx/(geography)
- link "Language →" [ref=e21] [cursor=pointer]:
- /url: /sx/(language)
- generic [ref=e22]:
- link "← Reactive Runtime" [ref=e23] [cursor=pointer]:
- /url: /sx/(geography.(reactive-runtime))
- link "Reactive Islands" [ref=e24] [cursor=pointer]:
- /url: /sx/(geography.(reactive))
- link "Hypermedia Lakes →" [ref=e25] [cursor=pointer]:
- /url: /sx/(geography.(hypermedia))
- generic [ref=e26]:
- link "← Examples" [ref=e27] [cursor=pointer]:
- /url: /sx/(geography.(reactive.(examples)))
- link "Examples" [ref=e28] [cursor=pointer]:
- /url: /sx/(geography.(reactive.(examples)))
- link "Examples →" [ref=e29] [cursor=pointer]:
- /url: /sx/(geography.(reactive.(examples)))
- generic [ref=e30]:
- link "← Imperative" [ref=e31] [cursor=pointer]:
- /url: /sx/(geography.(reactive.(examples.imperative)))
- link "Reactive List" [ref=e32] [cursor=pointer]:
- /url: /sx/(geography.(reactive.(examples.reactive-list)))
- link "Input Binding →" [ref=e33] [cursor=pointer]:
- /url: /sx/(geography.(reactive.(examples.input-binding)))
- generic [ref=e37]:
- paragraph [ref=e38]:
- text: When
- code [ref=e39]: map
- text: is used with
- code [ref=e40]: (deref signal)
- text: inside an island, it auto-upgrades to a reactive list. With
- code [ref=e41]: :key
- text: attributes, existing DOM nodes are reused across updates — only additions, removals, and reorderings touch the DOM.
- generic [ref=e43]:
- generic [ref=e44]:
- button "Add Item" [ref=e45] [cursor=pointer]
- generic [ref=e46]: 0 items
- list
- generic [ref=e48]: (defisland ~reactive-islands/index/demo-reactive-list () (let ((next-id (signal 1)) (items (signal (list))) (add-item (fn (e) (batch (fn () (swap! items (fn (old) (append old (dict "id" (deref next-id) "text" (str "Item " (deref next-id)))))) (swap! next-id inc))))) (remove-item (fn (id) (swap! items (fn (old) (filter (fn (item) (not (= (get item "id") id))) old)))))) (div (~tw :tokens "rounded border border-violet-200 bg-violet-50 p-4 my-4") (div (~tw :tokens "flex items-center gap-3 mb-3") (button (~tw :tokens "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700") :on-click add-item "Add Item") (span (~tw :tokens "text-sm text-stone-500") (deref (computed (fn () (len (deref items))))) " items")) (ul (~tw :tokens "space-y-1") (map (fn (item) (li :key (str (get item "id")) (~tw :tokens "flex items-center justify-between bg-white rounded px-3 py-2 text-sm") (span (get item "text")) (button (~tw :tokens "text-stone-400 hover:text-red-500 text-xs") :on-click (fn (e) (remove-item (get item "id"))) "✕"))) (deref items))))))
- paragraph [ref=e49]:
- code [ref=e50]: :key
- text: identifies each list item. When items change, the reconciler matches old and new keys — reusing existing DOM nodes, inserting new ones, and removing stale ones. Without keys, the list falls back to clear-and-rerender.
- code [ref=e51]: batch
- text: groups the two signal writes into one update pass.
```

View File

@@ -1,82 +0,0 @@
# Page snapshot
```yaml
- main [ref=e4]:
- generic [ref=e6]:
- complementary
- generic [ref=e7]:
- generic [ref=e8]:
- generic [ref=e11]:
- link "(<sx>)" [ref=e12] [cursor=pointer]:
- /url: /sx/
- generic [ref=e14]: (<sx>)
- paragraph [ref=e15]: The framework-free reactive hypermedium
- paragraph [ref=e17]: © Giles Bradshaw 2026· /sx/(geography.(hypermedia.(example.delete-row)))
- generic [ref=e18]:
- link "← Etc" [ref=e19] [cursor=pointer]:
- /url: /sx/(etc)
- link "Geography" [ref=e20] [cursor=pointer]:
- /url: /sx/(geography)
- link "Language →" [ref=e21] [cursor=pointer]:
- /url: /sx/(language)
- generic [ref=e22]:
- link "← Reactive Islands" [ref=e23] [cursor=pointer]:
- /url: /sx/(geography.(reactive))
- link "Hypermedia Lakes" [ref=e24] [cursor=pointer]:
- /url: /sx/(geography.(hypermedia))
- link "Scopes →" [ref=e25] [cursor=pointer]:
- /url: /sx/(geography.(scopes))
- generic [ref=e26]:
- link "← Reference" [ref=e27] [cursor=pointer]:
- /url: /sx/(geography.(hypermedia.(reference)))
- link "Examples" [ref=e28] [cursor=pointer]:
- /url: /sx/(geography.(hypermedia.(example)))
- link "Reference →" [ref=e29] [cursor=pointer]:
- /url: /sx/(geography.(hypermedia.(reference)))
- generic [ref=e30]:
- link "← Polling" [ref=e31] [cursor=pointer]:
- /url: /sx/(geography.(hypermedia.(example.polling)))
- link "Delete Row" [ref=e32] [cursor=pointer]:
- /url: /sx/(geography.(hypermedia.(example.delete-row)))
- link "Inline Edit →" [ref=e33] [cursor=pointer]:
- /url: /sx/(geography.(hypermedia.(example.inline-edit)))
- generic [ref=e37]:
- paragraph [ref=e38]: sx-delete with sx-swap "outerHTML" and an empty response removes the row from the DOM.
- generic [ref=e40]:
- generic [ref=e41]:
- heading "Demo" [level=3] [ref=e42]
- paragraph [ref=e43]: Click delete to remove a row. Uses sx-confirm for confirmation.
- table [ref=e47]:
- rowgroup [ref=e48]:
- row "Item" [ref=e49]:
- columnheader "Item" [ref=e50]
- columnheader [ref=e51]
- rowgroup [ref=e52]:
- row "Fix login bug delete" [ref=e53]:
- cell "Fix login bug" [ref=e54]
- cell "delete" [ref=e55]:
- button "delete" [ref=e56] [cursor=pointer]
- row "Write documentation delete" [ref=e57]:
- cell "Write documentation" [ref=e58]
- cell "delete" [ref=e59]:
- button "delete" [ref=e60] [cursor=pointer]
- row "Deploy to production delete" [ref=e61]:
- cell "Deploy to production" [ref=e62]
- cell "delete" [ref=e63]:
- button "delete" [ref=e64] [cursor=pointer]
- row "Add unit tests delete" [ref=e65]:
- cell "Add unit tests" [ref=e66]
- cell "delete" [ref=e67]:
- button "delete" [ref=e68] [cursor=pointer]
- heading "S-expression" [level=3] [ref=e69]
- code [ref=e73]: (button :sx-delete "/sx/(geography.(hypermedia.(example.(api.(delete.1)))))" :sx-target "#row-1" :sx-swap "outerHTML" :sx-confirm "Delete this item?" "delete")
- heading "Component" [level=3] [ref=e74]
- code [ref=e79]: (defcomp ~examples/delete-row (id name) (tr :id (str "row-" id) (~tw :tokens "border-b border-stone-100 transition-all") (td (~tw :tokens "px-3 py-2 text-stone-700") name) (td (~tw :tokens "px-3 py-2") (button :sx-delete (str "/sx/(geography.(hypermedia.(example.(api.(delete." id ")))))") :sx-target (str "#row-" id) :sx-swap "outerHTML" :sx-confirm "Delete this item?" (~tw :tokens "text-rose-500 hover:text-rose-700 text-sm") "delete"))))
- heading "Server handler" [level=3] [ref=e80]
- code [ref=e84]: (:path "/sx/(geography.(hypermedia.(example.(api.(delete.<sx:item_id>)))))" :method :delete :csrf false ("item-id") (<> (~docs/oob-code :target-id "delete-comp" :text (helper "component-source" "~examples/delete-row")) (~docs/oob-code :target-id "delete-wire" :text "(empty — row removed by outerHTML swap)")))
- generic [ref=e85]:
- heading "Wire response" [level=3] [ref=e86]
- button "Clear component cache" [ref=e87] [cursor=pointer]
- paragraph [ref=e88]: Empty body — outerHTML swap replaces the target element with nothing.
- code [ref=e93]: (empty — row removed by outerHTML swap)
```

View File

@@ -0,0 +1,110 @@
// Bytecode on-demand loading tests
// Verifies .sxbc modules are only downloaded when needed, not all at boot.
const { test, expect } = require('playwright/test');
const { BASE_URL, waitForSxReady } = require('./helpers');
test.describe('Bytecode Loading', () => {
test('manifest is fetched at boot', async ({ page }) => {
const manifestReqs = [];
page.on('request', req => {
if (req.url().includes('module-manifest.json')) manifestReqs.push(req.url());
});
await page.goto(BASE_URL + '/sx/', { waitUntil: 'domcontentloaded' });
await waitForSxReady(page);
expect(manifestReqs.length).toBe(1);
});
test('boot loads only manifest deps, not all modules', async ({ page }) => {
const sxbcReqs = [];
page.on('request', req => {
const url = req.url();
if (url.includes('.sxbc') && !url.includes('manifest')) {
const name = url.split('/').pop().split('?')[0].replace('.sxbc', '');
sxbcReqs.push(name);
}
});
await page.goto(BASE_URL + '/sx/', { waitUntil: 'domcontentloaded' });
await waitForSxReady(page);
// Boot loads manifest deps for hydration — NOT all 26
// 13 boot deps, 13 deferred (compiler, vm, harness, etc.)
expect(sxbcReqs.length).toBeLessThanOrEqual(14);
expect(sxbcReqs.length).toBeGreaterThan(5);
// Boot-critical modules present
expect(sxbcReqs).toContain('boot');
expect(sxbcReqs).toContain('dom');
// Dev/test modules NOT loaded on homepage
const nonBootModules = ['compiler', 'vm', 'harness', 'harness-web', 'harness-reactive'];
for (const mod of nonBootModules) {
expect(sxbcReqs).not.toContain(mod);
}
});
test('no .sx fallback requests when bytecode exists', async ({ page }) => {
const sxFallbackReqs = [];
page.on('request', req => {
const url = req.url();
// .sx source fallback (not .sxbc) in the wasm directory
if (url.match(/\/static\/wasm\/sx\/[^/]+\.sx(\?|$)/) && !url.includes('.sxbc')) {
sxFallbackReqs.push(url);
}
});
await page.goto(BASE_URL + '/sx/', { waitUntil: 'domcontentloaded' });
await waitForSxReady(page);
expect(sxFallbackReqs).toEqual([]);
});
test('all .sxbc requests use cache-busting hash', async ({ page }) => {
const sxbcUrls = [];
page.on('request', req => {
if (req.url().includes('.sxbc')) sxbcUrls.push(req.url());
});
await page.goto(BASE_URL + '/sx/', { waitUntil: 'domcontentloaded' });
await waitForSxReady(page);
// Every .sxbc request should have a ?v= cache-buster
for (const url of sxbcUrls) {
expect(url).toMatch(/[?&]v=/);
}
// All should share the same hash (combined hash from data-sxbc-hash)
const hashes = sxbcUrls.map(u => new URL(u).searchParams.get('v')).filter(Boolean);
const unique = [...new Set(hashes)];
expect(unique.length).toBe(1);
});
test('SPA navigation does not re-fetch boot modules', async ({ page }) => {
await page.goto(BASE_URL + '/sx/', { waitUntil: 'domcontentloaded' });
await waitForSxReady(page);
// Start tracking requests AFTER boot
const postBootSxbc = [];
page.on('request', req => {
if (req.url().includes('.sxbc')) {
const name = req.url().split('/').pop().split('?')[0].replace('.sxbc', '');
postBootSxbc.push(name);
}
});
// Navigate to a different page via SPA
await page.click('a[href*="geography"]');
await page.waitForTimeout(2000);
// Boot modules should NOT be re-fetched
const bootModules = ['dom', 'signals', 'boot', 'engine', 'orchestration'];
for (const mod of bootModules) {
expect(postBootSxbc).not.toContain(mod);
}
});
});