Files
rose-ash/hosts/ocaml/bin/run_tests.ml
giles dd604f2bb1 JIT: close CEK gap (817→0) via skip-list + TIMEOUT catch + primitive fallback
JIT-vs-CEK test parity: both now pass 3938/534 (identical failures).

Three fixes in sx_vm.ml + run_tests.ml:

1. OP_CALL_PRIM: fallback to Sx_primitives.get_primitive when vm.globals
   misses. Primitives registered after JIT setup (host-global, host-get,
   etc. bound inside run_spec_tests) become resolvable at call time.

2. jit_compile_lambda: early-exit for anonymous lambdas, nested lambdas
   (closure has parent — recreated per outer call), and a known-broken
   name list: parser combinators, hyperscript parse/compile orchestrators,
   test helpers, compile-timeout functions, and hs loop runtime (which
   uses guard/raise for break/continue). Lives inside jit_compile_lambda
   so both the CEK _jit_try_call_fn hook and VM OP_CALL Lambda path
   honor the skip list.

3. run_tests.ml _jit_try_call_fn: catch TIMEOUT during jit_compile_lambda.
   Sentinel is set before compile, so subsequent calls skip JIT; this
   ensures the first call of a suite also falls back to CEK cleanly when
   compile exceeds the 5s test budget.

Also includes run_tests.ml 'reset' form helpers refactor (form-element
reset command) that was pending in the working tree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:06:00 +00:00

2642 lines
115 KiB
OCaml

(** Test runner — runs the SX spec test suite against the transpiled CEK evaluator.
Provides the 5 platform functions required by test-framework.sx:
try-call, report-pass, report-fail, push-suite, pop-suite
Plus test helpers: sx-parse, cek-eval, env-*, equal?, etc.
Usage:
dune exec bin/run_tests.exe # foundation + spec tests
dune exec bin/run_tests.exe -- test-primitives # specific test
dune exec bin/run_tests.exe -- --foundation # foundation only *)
(* Modules accessed directly — library is unwrapped *)
open Sx_types
open Sx_parser
open Sx_primitives
open Sx_runtime
open Sx_ref
(* ====================================================================== *)
(* Test state *)
(* ====================================================================== *)
let pass_count = ref 0
let fail_count = ref 0
let suite_stack : string list ref = ref []
(* ====================================================================== *)
(* Deep equality — SX structural comparison *)
(* ====================================================================== *)
let rec deep_equal a b =
match a, b with
| Nil, Nil -> true
| Bool a, Bool b -> a = b
| Number a, Number b -> a = b
| String a, String b -> a = b
| Symbol a, Symbol b -> a = b
| Keyword a, Keyword b -> a = b
| (List a | ListRef { contents = a }), (List b | ListRef { contents = b }) ->
List.length a = List.length b &&
List.for_all2 deep_equal a b
| Dict a, Dict b ->
let ka = Hashtbl.fold (fun k _ acc -> k :: acc) a [] in
let kb = Hashtbl.fold (fun k _ acc -> k :: acc) b [] in
List.length ka = List.length kb &&
List.for_all (fun k ->
Hashtbl.mem b k &&
deep_equal
(match Hashtbl.find_opt a k with Some v -> v | None -> Nil)
(match Hashtbl.find_opt b k with Some v -> v | None -> Nil)) ka
| Record a, Record b ->
a.r_type.rt_uid = b.r_type.rt_uid &&
Array.length a.r_fields = Array.length b.r_fields &&
(let eq = ref true in
for i = 0 to Array.length a.r_fields - 1 do
if not (deep_equal a.r_fields.(i) b.r_fields.(i)) then eq := false
done; !eq)
| Lambda _, Lambda _ -> a == b (* identity *)
| NativeFn _, NativeFn _ -> a == b
| _ -> false
(* ====================================================================== *)
(* Build evaluator environment with test platform functions *)
(* ====================================================================== *)
let make_test_env () =
let env = Sx_types.make_env () in
let bind name fn =
ignore (Sx_types.env_bind env name (NativeFn (name, fn)))
in
(* --- platform functions required by test-framework.sx --- *)
bind "cek-try" (fun args ->
match args with
| [thunk; handler] ->
(try Sx_ref.cek_call thunk Nil
with Eval_error msg -> Sx_ref.cek_call handler (List [String msg]))
| [thunk] ->
(try let r = Sx_ref.cek_call thunk Nil in
List [Symbol "ok"; r]
with Eval_error msg -> List [Symbol "error"; String msg])
| _ -> Nil);
(* Two-phase try-call: first attempt runs in-process (fast, state-sharing).
If a test hangs (detected by SIGALRM), retry it in a fork for safe timeout. *)
let _test_timed_out = ref false in
Sys.set_signal Sys.sigalrm (Sys.Signal_handle (fun _ ->
_test_timed_out := true;
(* Set step_limit to trigger check in VM instruction loop *)
Sx_ref.step_limit := 1;
(* Also raise to break native OCaml loops (HS parser etc.) *)
raise (Eval_error "TIMEOUT: test exceeded 5s")));
bind "try-call" (fun args ->
match args with
| [thunk] ->
_test_timed_out := false;
Sx_ref.step_limit := 0;
Sx_ref.step_count := 0;
ignore (Unix.alarm 5);
(try
let result = eval_expr (List [thunk]) (Env env) in
ignore result;
ignore (Unix.alarm 0);
Sx_ref.step_limit := 0;
let d = Hashtbl.create 2 in
Hashtbl.replace d "ok" (Bool true);
Dict d
with
| Eval_error msg ->
ignore (Unix.alarm 0);
Sx_ref.step_limit := 0;
let d = Hashtbl.create 2 in
Hashtbl.replace d "ok" (Bool false);
Hashtbl.replace d "error" (String (if !_test_timed_out then "TIMEOUT: test exceeded 5s" else msg));
Dict d
| exn ->
ignore (Unix.alarm 0);
Sx_ref.step_limit := 0;
let d = Hashtbl.create 2 in
Hashtbl.replace d "ok" (Bool false);
Hashtbl.replace d "error" (String (Printexc.to_string exn));
Dict d)
| _ -> raise (Eval_error "try-call: expected 1 arg"));
bind "report-pass" (fun args ->
match args with
| [String name] ->
incr pass_count;
let ctx = String.concat " > " (List.rev !suite_stack) in
Printf.printf " PASS: %s > %s\n%!" ctx name;
Nil
| [v] ->
incr pass_count;
let ctx = String.concat " > " (List.rev !suite_stack) in
Printf.printf " PASS: %s > %s\n%!" ctx (Sx_types.inspect v);
Nil
| _ -> raise (Eval_error "report-pass: expected 1 arg"));
bind "report-fail" (fun args ->
match args with
| [String name; String error] ->
incr fail_count;
let ctx = String.concat " > " (List.rev !suite_stack) in
Printf.printf " FAIL: %s > %s: %s\n%!" ctx name error;
Nil
| [name_v; error_v] ->
incr fail_count;
let ctx = String.concat " > " (List.rev !suite_stack) in
Printf.printf " FAIL: %s > %s: %s\n%!" ctx
(Sx_types.value_to_string name_v)
(Sx_types.value_to_string error_v);
Nil
| _ -> raise (Eval_error "report-fail: expected 2 args"));
bind "push-suite" (fun args ->
match args with
| [String name] ->
suite_stack := name :: !suite_stack;
let indent = String.make ((List.length !suite_stack - 1) * 2) ' ' in
Printf.printf "%sSuite: %s\n%!" indent name;
Nil
| [v] ->
let name = Sx_types.value_to_string v in
suite_stack := name :: !suite_stack;
let indent = String.make ((List.length !suite_stack - 1) * 2) ' ' in
Printf.printf "%sSuite: %s\n%!" indent name;
Nil
| _ -> raise (Eval_error "push-suite: expected 1 arg"));
bind "pop-suite" (fun _args ->
suite_stack := (match !suite_stack with _ :: t -> t | [] -> []);
Nil);
(* --- Test helpers --- *)
bind "sx-parse" (fun args ->
match args with
| [String s] -> List (parse_all s)
| _ -> raise (Eval_error "sx-parse: expected string"));
bind "sx-parse-loc" (fun args ->
match args with
| [String s] ->
let cst = Sx_parser.parse_all_cst s in
List (Sx_cst.cst_to_ast_loc s cst.nodes)
| _ -> raise (Eval_error "sx-parse-loc: expected string"));
bind "source-loc" (fun args ->
match args with
| [Dict d] ->
let line = try Hashtbl.find d "line" with Not_found -> Nil in
let col = try Hashtbl.find d "col" with Not_found -> Nil in
let ld = Sx_types.make_dict () in
Sx_types.dict_set ld "line" line;
Sx_types.dict_set ld "col" col;
Dict ld
| _ -> Nil);
(* Step 15: bytecode + CEK state serialization *)
bind "bytecode-serialize" (fun args ->
match args with
| [v] -> String ("(sxbc 2 " ^ Sx_types.inspect v ^ ")")
| _ -> raise (Eval_error "bytecode-serialize: expected 1 arg"));
bind "bytecode-deserialize" (fun args ->
match args with
| [String s] ->
let parsed = Sx_parser.parse_all s in
(match parsed with
| [List (Symbol "sxbc" :: Number _ :: payload :: _)] -> payload
| _ -> raise (Eval_error "bytecode-deserialize: invalid sxbc format"))
| _ -> raise (Eval_error "bytecode-deserialize: expected string"));
bind "cek-serialize" (fun args ->
match args with
| [v] -> String ("(cek-state 1 " ^ Sx_types.inspect v ^ ")")
| _ -> raise (Eval_error "cek-serialize: expected 1 arg"));
bind "cek-deserialize" (fun args ->
match args with
| [String s] ->
let parsed = Sx_parser.parse_all s in
(match parsed with
| [List (Symbol "cek-state" :: Number _ :: payload :: _)] -> payload
| _ -> raise (Eval_error "cek-deserialize: invalid cek-state format"))
| _ -> raise (Eval_error "cek-deserialize: expected string"));
bind "sx-parse-one" (fun args ->
match args with
| [String s] ->
let exprs = parse_all s in
(match exprs with e :: _ -> e | [] -> Nil)
| _ -> raise (Eval_error "sx-parse-one: expected string"));
bind "cek-eval" (fun args ->
match args with
| [String s] ->
let exprs = parse_all s in
(match exprs with
| e :: _ -> eval_expr e (Env env)
| [] -> Nil)
| _ -> raise (Eval_error "cek-eval: expected string"));
bind "eval-expr-cek" (fun args ->
match args with
| [expr; e] -> eval_expr expr e
| [expr] -> eval_expr expr (Env env)
| _ -> raise (Eval_error "eval-expr-cek: expected 1-2 args"));
bind "test-env" (fun _args -> Env (Sx_types.env_extend env));
(* --- Environment operations --- *)
(* Env operations — accept both Env and Dict *)
let uw = Sx_runtime.unwrap_env in
bind "env-get" (fun args ->
match args with
| [e; String k] -> Sx_types.env_get (uw e) k
| [e; Keyword k] -> Sx_types.env_get (uw e) k
| _ -> raise (Eval_error "env-get: expected env and string"));
bind "env-has?" (fun args ->
match args with
| [e; String k] -> Bool (Sx_types.env_has (uw e) k)
| [e; Keyword k] -> Bool (Sx_types.env_has (uw e) k)
| _ -> raise (Eval_error "env-has?: expected env and string"));
bind "env-bind!" (fun args ->
match args with
| [Dict d; String k; v] -> Hashtbl.replace d k v; v
| [Dict d; Keyword k; v] -> Hashtbl.replace d k v; v
| [e; String k; v] -> Sx_types.env_bind (uw e) k v
| [e; Keyword k; v] -> Sx_types.env_bind (uw e) k v
| _ -> raise (Eval_error "env-bind!: expected env, key, value"));
bind "env-set!" (fun args ->
match args with
| [e; String k; v] -> Sx_types.env_set (uw e) k v
| [e; Keyword k; v] -> Sx_types.env_set (uw e) k v
| _ -> raise (Eval_error "env-set!: expected env, key, value"));
bind "make-env" (fun _args -> Env (Sx_types.make_env ()));
bind "env-extend" (fun args ->
match args with
| [e] -> Env (Sx_types.env_extend (uw e))
| _ -> raise (Eval_error "env-extend: expected env"));
bind "env-merge" (fun args ->
match args with
| [a; b] -> Sx_runtime.env_merge a b
| _ -> raise (Eval_error "env-merge: expected 2 envs"));
(* --- Equality --- *)
bind "equal?" (fun args ->
match args with
| [a; b] -> Bool (deep_equal a b)
| _ -> raise (Eval_error "equal?: expected 2 args"));
bind "identical?" (fun args ->
match args with
| [a; b] -> Bool (match a, b with
| Number x, Number y -> x = y
| String x, String y -> x = y
| Bool x, Bool y -> x = y
| Nil, Nil -> true
| _ -> a == b)
| _ -> raise (Eval_error "identical?: expected 2 args"));
(* --- Continuation support --- *)
bind "make-continuation" (fun args ->
match args with
| [f] ->
let k v = sx_call f [v] in
Continuation (k, None)
| _ -> raise (Eval_error "make-continuation: expected 1 arg"));
bind "continuation?" (fun args ->
match args with
| [Continuation _] -> Bool true
| [_] -> Bool false
| _ -> raise (Eval_error "continuation?: expected 1 arg"));
bind "continuation-fn" (fun args ->
match args with
| [Continuation (f, _)] -> NativeFn ("continuation-fn-result", fun args ->
match args with [v] -> f v | _ -> f Nil)
| _ -> raise (Eval_error "continuation-fn: expected continuation"));
(* --- Core builtins used by test framework / test code --- *)
bind "assert" (fun args ->
match args with
| [cond] ->
if not (sx_truthy cond) then raise (Eval_error "Assertion failed");
Bool true
| [cond; String msg] ->
if not (sx_truthy cond) then raise (Eval_error ("Assertion error: " ^ msg));
Bool true
| [cond; msg] ->
if not (sx_truthy cond) then
raise (Eval_error ("Assertion error: " ^ Sx_types.value_to_string msg));
Bool true
| _ -> raise (Eval_error "assert: expected 1-2 args"));
bind "append!" (fun args ->
match args with
| [ListRef r; v; Number n] when int_of_float n = 0 ->
r := v :: !r; ListRef r (* prepend *)
| [ListRef r; v] -> r := !r @ [v]; ListRef r (* append in place *)
| [List items; v; Number n] when int_of_float n = 0 ->
List (v :: items) (* immutable prepend *)
| [List items; v] -> List (items @ [v]) (* immutable fallback *)
| _ -> raise (Eval_error "append!: expected list and value"));
(* --- HTML Renderer (from sx_render.ml library module) --- *)
Sx_render.setup_render_env env;
(* HTML tag functions — bind all tags as native fns returning (tag ...args) *)
List.iter (fun tag ->
ignore (Sx_types.env_bind env tag
(NativeFn ("html:" ^ tag, fun args -> List (Symbol tag :: args))))
) Sx_render.html_tags;
bind "is-html-tag?" (fun args -> match args with [String s] -> Bool (Sx_render.is_html_tag s) | _ -> Bool false);
(* Stubs needed by adapter-html.sx when loaded at test time *)
bind "set-render-active!" (fun _args -> Nil);
bind "render-active?" (fun _args -> Bool true);
bind "trampoline" (fun args ->
match args with
| [Thunk (expr, e)] -> eval_expr expr (Env e)
| [v] -> v
| _ -> Nil);
bind "eval-expr" (fun args ->
match args with
| [expr; e] ->
(match e with
| Dict _ -> Printf.eprintf "[EVAL-EXPR] env is Dict! expr=%s\n%!" (Sx_runtime.value_to_str expr)
| Nil -> Printf.eprintf "[EVAL-EXPR] env is Nil! expr=%s\n%!" (Sx_runtime.value_to_str expr)
| _ -> ());
let ue = Sx_runtime.unwrap_env e in
eval_expr expr (Env ue)
| [expr] -> eval_expr expr (Env env)
| _ -> raise (Eval_error "eval-expr: expected (expr env)"));
bind "set-render-active!" (fun _args -> Nil);
(* render-to-sx wrapper: if called with a string, parse and aser it *)
bind "render-to-sx" (fun args ->
match args with
| [String src] ->
let exprs = Sx_parser.parse_all src in
let expr = match exprs with [e] -> e | es -> List (Symbol "do" :: es) in
let result = eval_expr (List [Symbol "aser"; expr; Env env]) (Env env) in
(match result with SxExpr s -> String s | String s -> String s | _ -> String (Sx_runtime.value_to_str result))
| [expr; Env e] ->
let result = eval_expr (List [Symbol "aser"; expr; Env e]) (Env e) in
(match result with SxExpr s -> String s | String s -> String s | _ -> String (Sx_runtime.value_to_str result))
| _ -> String "");
(* Scope primitives — share the same scope stacks as sx_primitives.ml
so that CEK evaluator's scope_push/scope_peek and SX-level scope-push!/scope-peek
operate on the same table. *)
let _scope_stacks = Sx_primitives._scope_stacks in
bind "scope-push!" (fun args ->
match args with
| [String name; value] ->
let stack = try Hashtbl.find _scope_stacks name with Not_found -> [] in
Hashtbl.replace _scope_stacks name (value :: stack); Nil
| [String name] ->
let stack = try Hashtbl.find _scope_stacks name with Not_found -> [] in
Hashtbl.replace _scope_stacks name (Nil :: stack); Nil
| _ -> Nil);
bind "scope-pop!" (fun args ->
match args with
| [String name] ->
let stack = try Hashtbl.find _scope_stacks name with Not_found -> [] in
(match stack with _ :: rest -> Hashtbl.replace _scope_stacks name rest | [] -> ()); Nil
| _ -> Nil);
bind "scope-peek" (fun args ->
match args with
| [String name] ->
let stack = try Hashtbl.find _scope_stacks name with Not_found -> [] in
(match stack with v :: _ -> v | [] -> Nil)
| _ -> Nil);
let context_fn = (fun args ->
match args with
| String name :: rest ->
let stack = try Hashtbl.find _scope_stacks name with Not_found -> [] in
(match stack with v :: _ -> v | [] -> (match rest with d :: _ -> d | [] -> Nil))
| _ -> Nil) in
bind "context" context_fn;
Sx_primitives.register "context" context_fn;
bind "scope-emit!" (fun args ->
match args with
| [String name; value] ->
let stack = try Hashtbl.find _scope_stacks name with Not_found -> [] in
(match stack with
| List items :: rest ->
Hashtbl.replace _scope_stacks name (List (items @ [value]) :: rest)
| _ :: rest ->
Hashtbl.replace _scope_stacks name (List [value] :: rest)
| [] ->
Hashtbl.replace _scope_stacks name [List [value]]);
Nil
| _ -> Nil);
bind "emitted" (fun args ->
match args with
| [String name] ->
let stack = try Hashtbl.find _scope_stacks name with Not_found -> [] in
(match stack with List items :: _ -> List items | _ -> List [])
| _ -> List []);
bind "scope-emitted" (fun args ->
match args with
| [String name] ->
let stack = try Hashtbl.find _scope_stacks name with Not_found -> [] in
(match stack with List items :: _ -> List items | _ -> List [])
| _ -> List []);
bind "provide-push!" (fun args ->
match args with
| [String name; value] ->
let stack = try Hashtbl.find _scope_stacks name with Not_found -> [] in
Hashtbl.replace _scope_stacks name (value :: stack); Nil
| _ -> Nil);
bind "provide-pop!" (fun args ->
match args with
| [String name] ->
let stack = try Hashtbl.find _scope_stacks name with Not_found -> [] in
(match stack with _ :: rest -> Hashtbl.replace _scope_stacks name rest | [] -> ()); Nil
| _ -> Nil);
bind "cond-scheme?" (fun args ->
match args with
| [(List clauses | ListRef { contents = clauses })] ->
Bool (List.for_all (fun c ->
match c with
| List l | ListRef { contents = l } -> List.length l = 2
| _ -> false
) clauses)
| _ -> Bool false);
bind "expand-macro" (fun args ->
match args with
| [Macro m; (List a | ListRef { contents = a }); _] ->
let local = Sx_types.env_extend m.m_closure in
let rec bind_params ps as' =
match ps, as' with
| [], rest ->
(match m.m_rest_param with
| Some rp -> ignore (Sx_types.env_bind local rp (List rest))
| None -> ())
| p :: ps_rest, a :: as_rest ->
ignore (Sx_types.env_bind local p a);
bind_params ps_rest as_rest
| remaining, [] ->
List.iter (fun p -> ignore (Sx_types.env_bind local p Nil)) remaining
in
bind_params m.m_params a;
eval_expr m.m_body (Env local)
| _ -> raise (Eval_error "expand-macro: expected (macro args env)"));
bind "cek-call" (fun args ->
match args with
| [fn_val; List call_args] -> Sx_ref.cek_call fn_val (List call_args)
| [fn_val; ListRef { contents = call_args }] -> Sx_ref.cek_call fn_val (List call_args)
| [fn_val; Nil] -> Sx_ref.cek_call fn_val (List [])
| [fn_val] -> Sx_ref.cek_call fn_val (List [])
| _ -> Nil);
bind "cek-run" (fun args ->
match args with
| [state] -> Sx_ref.cek_run state
| _ -> Nil);
bind "batch-begin!" (fun _args -> Sx_ref.batch_begin_b ());
bind "batch-end!" (fun _args -> Sx_ref.batch_end_b ());
bind "now-ms" (fun _args -> Number 1000.0);
bind "random-int" (fun args -> match args with [Number lo; _] -> Number lo | _ -> Number 0.0);
bind "try-rerender-page" (fun _args -> Nil);
bind "collect!" (fun args ->
match args with
| [String name; value] ->
let stack = try Hashtbl.find _scope_stacks name with Not_found -> [] in
(match stack with
| List items :: rest ->
if List.mem value items then Nil
else (Hashtbl.replace _scope_stacks name (List (items @ [value]) :: rest); Nil)
| _ ->
Hashtbl.replace _scope_stacks name (List [value] :: stack); Nil)
| _ -> Nil);
bind "collected" (fun args ->
match args with
| [String name] ->
let stack = try Hashtbl.find _scope_stacks name with Not_found -> [] in
(match stack with List items :: _ -> List items | _ -> List [])
| _ -> List []);
bind "clear-collected!" (fun args ->
match args with
| [String name] ->
let stack = try Hashtbl.find _scope_stacks name with Not_found -> [] in
(match stack with _ :: rest -> Hashtbl.replace _scope_stacks name (List [] :: rest) | [] -> ()); Nil
| _ -> Nil);
(* regex-find-all now provided by sx_primitives.ml *)
bind "callable?" (fun args ->
match args with
| [NativeFn _] | [Lambda _] | [Component _] | [Island _] -> Bool true
| _ -> Bool false);
bind "make-sx-expr" (fun args -> match args with [String s] -> SxExpr s | _ -> raise (Eval_error "make-sx-expr: expected string"));
bind "sx-expr-source" (fun args -> match args with [SxExpr s] -> String s | [String s] -> String s | _ -> raise (Eval_error "sx-expr-source: expected sx-expr or string"));
bind "sx-expr?" (fun args -> match args with [SxExpr _] -> Bool true | _ -> Bool false);
bind "spread?" (fun args -> match args with [Spread _] -> Bool true | _ -> Bool false);
bind "call-lambda" (fun args ->
match args with
| [Lambda _ as f; (List a | ListRef { contents = a })] ->
let l = match f with Lambda l -> l | _ -> assert false in
let local = Sx_types.env_extend l.l_closure in
let rec bind_ps ps as' = match ps, as' with
| [], _ -> () | p :: pr, a :: ar -> ignore (Sx_types.env_bind local p a); bind_ps pr ar
| p :: pr, [] -> ignore (Sx_types.env_bind local p Nil); bind_ps pr [] in
bind_ps l.l_params a;
eval_expr l.l_body (Env local)
| [Lambda _ as f; (List a | ListRef { contents = a }); Env e] ->
let l = match f with Lambda l -> l | _ -> assert false in
let local = Sx_types.env_merge l.l_closure e in
let rec bind_ps ps as' = match ps, as' with
| [], _ -> () | p :: pr, a :: ar -> ignore (Sx_types.env_bind local p a); bind_ps pr ar
| p :: pr, [] -> ignore (Sx_types.env_bind local p Nil); bind_ps pr [] in
bind_ps l.l_params a;
eval_expr l.l_body (Env local)
| _ -> raise (Eval_error "call-lambda: expected (fn args env?)"));
(* Declarative type/effect forms — no-ops at runtime *)
bind "deftype" (fun _args -> Nil);
bind "defeffect" (fun _args -> Nil);
bind "register-special-form!" (fun args ->
match args with
| [String name; fn_val] ->
(* Don't let SX modules override OCaml-registered defhandler/defisland *)
if name = "defhandler" || name = "defisland" then Nil
else (ignore (Sx_ref.register_special_form (String name) fn_val); Nil)
| _ -> Nil);
(* defhandler — register handler as handler:name in eval env.
Mirrors sx_server.ml's defhandler special form. *)
ignore (Sx_ref.register_special_form (String "defhandler") (NativeFn ("defhandler", fun sf_args ->
let raw_args, eval_env = match sf_args with
| [List a; Env e] | [ListRef { contents = a }; Env e] -> (a, e)
| _ -> ([], env) in
match raw_args with
| name_sym :: rest ->
let name = match name_sym with Symbol s -> s | String s -> s | _ -> Sx_types.inspect name_sym in
let rec parse_opts acc = function
| Keyword k :: v :: rest -> Hashtbl.replace acc k v; parse_opts acc rest
| rest -> (acc, rest) in
let opts = Hashtbl.create 4 in
let (_, remaining) = parse_opts opts rest in
let params, body_forms = match remaining with
| List p :: rest -> (p, rest) | _ -> ([], []) in
(* Wrap multiple body forms in (do ...) *)
let body = match body_forms with
| [] -> Nil | [b] -> b
| forms -> List (Symbol "do" :: forms) in
(* Extract &key param names for binding *)
let key_params =
let rec collect acc in_key = function
| [] -> List.rev acc
| Symbol "&key" :: rest -> collect acc true rest
| Symbol "&rest" :: _ :: rest -> collect acc false rest
| Symbol s :: rest when in_key -> collect (s :: acc) true rest
| _ :: rest -> collect acc in_key rest
in collect [] false params in
let hdef = Hashtbl.create 8 in
Hashtbl.replace hdef "__type" (String "handler");
Hashtbl.replace hdef "name" (String name);
Hashtbl.replace hdef "body" body;
Hashtbl.replace hdef "params" (List (List.map (fun s -> String s) key_params));
Hashtbl.replace hdef "closure" (Env eval_env);
Hashtbl.replace hdef "method" (match Hashtbl.find_opt opts "method" with
| Some (Keyword m) -> String m | Some v -> v | None -> String "get");
ignore (Sx_types.env_bind eval_env ("handler:" ^ name) (Dict hdef));
Dict hdef
| _ -> Nil)));
(* defisland — register island component. Stub: creates a component record. *)
ignore (Sx_ref.register_special_form (String "defisland") (NativeFn ("defisland", fun sf_args ->
let raw_args, eval_env = match sf_args with
| [List a; Env e] | [ListRef { contents = a }; Env e] -> (a, e)
| _ -> ([], env) in
match raw_args with
| name_sym :: rest ->
let name = match name_sym with Symbol s -> s | String s -> s | _ -> Sx_types.inspect name_sym in
let short_name = if String.length name > 1 && name.[0] = '~' then String.sub name 1 (String.length name - 1) else name in
let params, body = match rest with
| List p :: b :: _ -> (p, b) | List p :: [] -> (p, Nil) | _ -> ([], Nil) in
let param_names = List.filter_map (fun p ->
match p with Symbol s -> Some s | _ -> None) params in
let has_children = List.exists (fun p ->
match p with Symbol "&rest" -> true | _ -> false) params in
let island = Island {
i_name = short_name; i_params = param_names;
i_has_children = has_children;
i_body = body; i_closure = eval_env; i_file = None; i_compiled = None;
} in
ignore (Sx_types.env_bind eval_env name island);
island
| _ -> Nil)));
(* IO registry — spec-level defio populates *io-registry* in evaluator.
Bind accessor functions + __io-registry alias for backward compat. *)
ignore (Sx_types.env_bind env "__io-registry" Sx_ref._io_registry_);
bind "io-registered?" (fun args -> match args with [String n] -> Sx_ref.io_registered_p (String n) | _ -> Bool false);
bind "io-lookup" (fun args -> match args with [String n] -> Sx_ref.io_lookup (String n) | _ -> Nil);
bind "io-names" (fun _args -> Sx_ref.io_names ());
bind "io-register!" (fun args -> match args with [String n; spec] -> Sx_ref.io_register_b (String n) spec | _ -> Nil);
(* Foreign registry — spec-level define-foreign populates *foreign-registry*.
Bind accessor functions so test-foreign.sx can inspect the registry. *)
ignore (Sx_types.env_bind env "*foreign-registry*" Sx_ref._foreign_registry_);
bind "foreign-registered?" (fun args -> match args with [String n] -> Sx_ref.foreign_registered_p (String n) | _ -> Bool false);
bind "foreign-lookup" (fun args -> match args with [String n] -> Sx_ref.foreign_lookup (String n) | _ -> Nil);
bind "foreign-names" (fun _args -> Sx_ref.foreign_names ());
bind "foreign-register!" (fun args -> match args with [String n; spec] -> Sx_ref.foreign_register_b (String n) spec | _ -> Nil);
bind "foreign-resolve-binding" (fun args -> match args with [String s] -> Sx_ref.foreign_resolve_binding (String s) | _ -> Nil);
bind "foreign-check-args" (fun args ->
let to_list = function List l -> List l | ListRef r -> List !r | v -> v in
match args with
| [String n; (List _ | ListRef _ as p); (List _ | ListRef _ as a)] ->
Sx_ref.foreign_check_args (String n) (to_list p) (to_list a)
| _ -> Nil);
bind "foreign-build-lambda" (fun args -> match args with [spec] -> Sx_ref.foreign_build_lambda spec | _ -> Nil);
(* Initialize CEK call forward ref — needed by with-capabilities and foreign-dispatch *)
Sx_types._cek_call_ref := Sx_ref.cek_call;
(* --- Primitives for canonical.sx / content tests --- *)
bind "contains-char?" (fun args ->
match args with
| [String s; String c] when String.length c = 1 ->
Bool (String.contains s c.[0])
| _ -> Bool false);
bind "escape-string" (fun args ->
match args with
| [String s] ->
let buf = Buffer.create (String.length s + 4) in
String.iter (fun c -> match c with
| '"' -> Buffer.add_string buf "\\\""
| '\\' -> Buffer.add_string buf "\\\\"
| '\n' -> Buffer.add_string buf "\\n"
| '\t' -> Buffer.add_string buf "\\t"
| c -> Buffer.add_char buf c) s;
String (Buffer.contents buf)
| _ -> raise (Eval_error "escape-string: expected string"));
bind "sha3-256" (fun args ->
match args with
| [String s] ->
(* Stub: use a simple hash for testing — not real SHA3 *)
let h = Hashtbl.hash s in
String (Printf.sprintf "%064x" (abs h))
| _ -> raise (Eval_error "sha3-256: expected string"));
(* --- Missing primitives referenced by tests --- *)
bind "upcase" (fun args ->
match args with
| [String s] -> String (String.uppercase_ascii s)
| _ -> raise (Eval_error "upcase: expected string"));
bind "downcase" (fun args ->
match args with
| [String s] -> String (String.lowercase_ascii s)
| _ -> raise (Eval_error "downcase: expected string"));
bind "make-keyword" (fun args ->
match args with
| [String s] -> Keyword s
| _ -> raise (Eval_error "make-keyword: expected string"));
bind "string-length" (fun args ->
match args with
| [String s] -> Number (float_of_int (String.length s))
| _ -> raise (Eval_error "string-length: expected string"));
bind "dict-get" (fun args ->
match args with
| [Dict d; String k] -> Sx_types.dict_get d k
| [Dict d; Keyword k] -> Sx_types.dict_get d k
| _ -> raise (Eval_error "dict-get: expected dict and key"));
bind "apply" (fun args ->
match args with
| f :: rest ->
let all_args = match List.rev rest with
| List last :: prefix -> List.rev prefix @ last
| _ -> rest
in
sx_call f all_args
| _ -> raise (Eval_error "apply: expected function and args"));
(* --- Type system helpers (for --full tests) --- *)
bind "test-prim-types" (fun _args ->
let d = Hashtbl.create 40 in
List.iter (fun (k, v) -> Hashtbl.replace d k (String v)) [
"+", "number"; "-", "number"; "*", "number"; "/", "number";
"mod", "number"; "inc", "number"; "dec", "number";
"abs", "number"; "min", "number"; "max", "number";
"floor", "number"; "ceil", "number"; "round", "number";
"str", "string"; "upper", "string"; "lower", "string";
"trim", "string"; "join", "string"; "replace", "string";
"format", "string"; "substr", "string";
"=", "boolean"; "<", "boolean"; ">", "boolean";
"<=", "boolean"; ">=", "boolean"; "!=", "boolean";
"not", "boolean"; "nil?", "boolean"; "empty?", "boolean";
"number?", "boolean"; "string?", "boolean"; "boolean?", "boolean";
"list?", "boolean"; "dict?", "boolean"; "symbol?", "boolean";
"keyword?", "boolean"; "contains?", "boolean"; "has-key?", "boolean";
"starts-with?", "boolean"; "ends-with?", "boolean";
"len", "number"; "first", "any"; "rest", "list";
"last", "any"; "nth", "any"; "cons", "list";
"append", "list"; "concat", "list"; "reverse", "list";
"sort", "list"; "slice", "list"; "range", "list";
"flatten", "list"; "keys", "list"; "vals", "list";
"map-dict", "dict"; "assoc", "dict"; "dissoc", "dict";
"merge", "dict"; "dict", "dict";
"get", "any"; "type-of", "string";
];
Dict d);
bind "test-prim-param-types" (fun _args ->
let d = Hashtbl.create 10 in
let pos name typ =
let d2 = Hashtbl.create 2 in
Hashtbl.replace d2 "positional" (List [List [String name; String typ]]);
Hashtbl.replace d2 "rest-type" Nil;
Dict d2
in
let pos_rest name typ rt =
let d2 = Hashtbl.create 2 in
Hashtbl.replace d2 "positional" (List [List [String name; String typ]]);
Hashtbl.replace d2 "rest-type" (String rt);
Dict d2
in
Hashtbl.replace d "+" (pos_rest "a" "number" "number");
Hashtbl.replace d "-" (pos_rest "a" "number" "number");
Hashtbl.replace d "*" (pos_rest "a" "number" "number");
Hashtbl.replace d "/" (pos_rest "a" "number" "number");
Hashtbl.replace d "inc" (pos "n" "number");
Hashtbl.replace d "dec" (pos "n" "number");
Hashtbl.replace d "upper" (pos "s" "string");
Hashtbl.replace d "lower" (pos "s" "string");
Hashtbl.replace d "keys" (pos "d" "dict");
Hashtbl.replace d "vals" (pos "d" "dict");
Dict d);
(* --- Component accessors --- *)
bind "component-param-types" (fun _args -> Nil);
bind "component-set-param-types!" (fun _args -> Nil);
bind "component-file" (fun args -> match args with [v] -> component_file v | _ -> Nil);
bind "component-set-file!" (fun args -> match args with [v; f] -> component_set_file v f | _ -> Nil);
bind "component-params" (fun args ->
match args with
| [Component c] -> List (List.map (fun s -> String s) c.c_params)
| [Island i] -> List (List.map (fun s -> String s) i.i_params)
| _ -> Nil);
bind "component-body" (fun args ->
match args with
| [Component c] -> c.c_body
| [Island i] -> i.i_body
| _ -> Nil);
bind "component-has-children" (fun args ->
match args with
| [Component c] -> Bool c.c_has_children
| [Island i] -> Bool i.i_has_children
| _ -> Bool false);
bind "component-affinity" (fun args ->
match args with
| [Component c] -> String c.c_affinity
| [Island _] -> String "client"
| _ -> String "auto");
(* --- Parser test helpers --- *)
bind "keyword-name" (fun args ->
match args with
| [Keyword k] -> String k
| _ -> raise (Eval_error "keyword-name: expected keyword"));
bind "symbol-name" (fun args ->
match args with
| [Symbol s] -> String s
| _ -> raise (Eval_error "symbol-name: expected symbol"));
bind "sx-serialize" (fun args ->
match args with
| [v] -> String (Sx_types.inspect v)
| _ -> raise (Eval_error "sx-serialize: expected 1 arg"));
(* --- make-symbol --- *)
bind "make-symbol" (fun args ->
match args with
| [String s] -> Symbol s
| [v] -> Symbol (Sx_types.value_to_string v)
| _ -> raise (Eval_error "make-symbol: expected 1 arg"));
(* --- CEK stepping / introspection --- *)
bind "make-cek-state" (fun args ->
match args with
| [ctrl; env'; kont] -> Sx_ref.make_cek_state ctrl env' kont
| _ -> raise (Eval_error "make-cek-state: expected 3 args"));
bind "cek-step" (fun args ->
match args with
| [state] -> Sx_ref.cek_step state
| _ -> raise (Eval_error "cek-step: expected 1 arg"));
bind "cek-phase" (fun args ->
match args with
| [state] -> Sx_ref.cek_phase state
| _ -> raise (Eval_error "cek-phase: expected 1 arg"));
bind "cek-value" (fun args ->
match args with
| [state] -> Sx_ref.cek_value state
| _ -> raise (Eval_error "cek-value: expected 1 arg"));
bind "cek-terminal?" (fun args ->
match args with
| [state] -> Sx_ref.cek_terminal_p state
| _ -> raise (Eval_error "cek-terminal?: expected 1 arg"));
bind "cek-kont" (fun args ->
match args with
| [state] -> Sx_ref.cek_kont state
| _ -> raise (Eval_error "cek-kont: expected 1 arg"));
bind "frame-type" (fun args ->
match args with
| [frame] -> Sx_ref.frame_type frame
| _ -> raise (Eval_error "frame-type: expected 1 arg"));
(* IO suspension primitives — inline until retranspile *)
let is_suspended state =
match get_val state (String "phase") with String "io-suspended" -> true | _ -> false in
let step_loop state =
let s = ref state in
while not (match Sx_ref.cek_terminal_p !s with Bool true -> true | _ -> false)
&& not (is_suspended !s) do
s := Sx_ref.cek_step !s
done;
!s in
bind "cek-step-loop" (fun args ->
match args with
| [state] -> step_loop state
| _ -> raise (Eval_error "cek-step-loop: expected 1 arg"));
bind "cek-resume" (fun args ->
match args with
| [state; result] ->
step_loop (Sx_ref.make_cek_value result (get_val state (String "env")) (get_val state (String "kont")))
| _ -> raise (Eval_error "cek-resume: expected 2 args"));
bind "cek-suspended?" (fun args ->
match args with
| [state] -> Bool (is_suspended state)
| _ -> raise (Eval_error "cek-suspended?: expected 1 arg"));
bind "cek-io-request" (fun args ->
match args with
| [state] -> get_val state (String "request")
| _ -> raise (Eval_error "cek-io-request: expected 1 arg"));
bind "make-cek-suspended" (fun args ->
match args with
| [req; env'; kont] ->
let d = Hashtbl.create 4 in
Hashtbl.replace d "phase" (String "io-suspended");
Hashtbl.replace d "request" req;
Hashtbl.replace d "env" env';
Hashtbl.replace d "kont" kont;
Dict d
| _ -> raise (Eval_error "make-cek-suspended: expected 3 args"));
(* --- Library registry --- *)
let lib_registry = Hashtbl.create 16 in
ignore (Sx_types.env_bind env "*library-registry*" (Dict lib_registry));
bind "library-loaded?" (fun args ->
match args with
| [spec] -> Sx_ref.library_loaded_p spec
| _ -> raise (Eval_error "library-loaded?: expected 1 arg"));
bind "library-exports" (fun args ->
match args with
| [spec] -> Sx_ref.library_exports spec
| _ -> raise (Eval_error "library-exports: expected 1 arg"));
bind "register-library" (fun args ->
match args with
| [spec; exports] -> Sx_ref.register_library spec exports
| _ -> raise (Eval_error "register-library: expected 2 args"));
(* --- Strict mode --- *)
(* *strict* is a plain value in the env, mutated via env_set by set-strict! *)
ignore (Sx_types.env_bind env "*strict*" (Bool false));
ignore (Sx_types.env_bind env "*prim-param-types*" Nil);
bind "set-strict!" (fun args ->
match args with
| [v] ->
Sx_ref._strict_ref := v;
ignore (Sx_types.env_set env "*strict*" v); Nil
| _ -> raise (Eval_error "set-strict!: expected 1 arg"));
bind "set-prim-param-types!" (fun args ->
match args with
| [v] ->
Sx_ref._prim_param_types_ref := v;
ignore (Sx_types.env_set env "*prim-param-types*" v); Nil
| _ -> raise (Eval_error "set-prim-param-types!: expected 1 arg"));
bind "value-matches-type?" (fun args ->
match args with
| [v; String expected] -> Sx_ref.value_matches_type_p v (String expected)
| _ -> raise (Eval_error "value-matches-type?: expected value and type string"));
(* Request primitives — stubs for test environment *)
let _test_state : (string, value) Hashtbl.t = Hashtbl.create 16 in
bind "now" (fun args ->
let fmt = match args with String f :: _ -> f | _ -> "%Y-%m-%d %H:%M:%S" in
let open Unix in let tm = localtime (gettimeofday ()) in
let r = if fmt = "%H:%M:%S" then Printf.sprintf "%02d:%02d:%02d" tm.tm_hour tm.tm_min tm.tm_sec
else Printf.sprintf "%04d-%02d-%02d %02d:%02d:%02d" (tm.tm_year+1900) (tm.tm_mon+1) tm.tm_mday tm.tm_hour tm.tm_min tm.tm_sec in
String r);
bind "state-get" (fun args -> match args with
| String key :: rest -> let default = match rest with v :: _ -> v | [] -> Nil in
(match Hashtbl.find_opt _test_state key with Some v -> v | None -> default)
| _ -> Nil);
bind "state-set!" (fun args -> match args with
| String key :: value :: _ -> Hashtbl.replace _test_state key value; Nil | _ -> Nil);
bind "state-clear!" (fun args -> match args with
| [String key] -> Hashtbl.remove _test_state key; Nil | _ -> Nil);
bind "request-method" (fun _args -> String "GET");
bind "request-body" (fun _args -> String "");
bind "request-form" (fun args -> match args with
| String _ :: rest -> (match rest with v :: _ -> v | [] -> String "") | _ -> String "");
bind "request-arg" (fun args -> match args with
| String _ :: rest -> (match rest with v :: _ -> v | [] -> Nil) | _ -> Nil);
bind "request-form-all" (fun _args -> Dict (Hashtbl.create 0));
bind "request-args-all" (fun _args -> Dict (Hashtbl.create 0));
bind "request-form-list" (fun _args -> List []);
bind "request-json" (fun _args -> String "");
bind "request-header" (fun args -> match args with
| String _ :: rest -> (match rest with v :: _ -> v | [] -> String "") | _ -> String "");
bind "request-headers-all" (fun _args -> Dict (Hashtbl.create 0));
bind "request-content-type" (fun _args -> String "");
bind "request-file-name" (fun _args -> String "");
bind "into" (fun args -> match args with
| [String "list"; Dict d] ->
List (Hashtbl.fold (fun k v acc -> List [String k; v] :: acc) d [])
| [String "dict"; List pairs] | [String "dict"; ListRef { contents = pairs }] ->
let d = Hashtbl.create 8 in
List.iter (fun pair -> match pair with
| List [String k; v] | ListRef { contents = [String k; v] } -> Hashtbl.replace d k v
| _ -> ()) pairs;
Dict d
| _ -> Nil);
(* --- Stubs for offline/IO tests --- *)
bind "log-info" (fun _args -> Nil);
bind "log-warn" (fun _args -> Nil);
bind "log-error" (fun _args -> Nil);
bind "execute-action" (fun _args -> Nil);
(* --- make-page-def for defpage tests --- *)
bind "make-page-def" (fun args ->
let convert_val = function Keyword k -> String k | v -> v in
let make_pdef name slots =
let d = Hashtbl.create 8 in
Hashtbl.replace d "__type" (String "page");
Hashtbl.replace d "name" (String name);
(* Defaults for missing fields *)
Hashtbl.replace d "stream" (Bool false);
Hashtbl.replace d "shell" Nil;
Hashtbl.replace d "fallback" Nil;
Hashtbl.replace d "data" Nil;
(* Override with actual slot values *)
Hashtbl.iter (fun k v -> Hashtbl.replace d k (convert_val v)) slots;
Dict d
in
match args with
| [String name; Dict slots; _env] -> make_pdef name slots
| [String name; Dict slots] -> make_pdef name slots
| _ -> Nil);
(* --- component-io-refs for deps.sx tests --- *)
bind "component-io-refs" (fun args ->
match args with
| [Component c] ->
(* Scan body for IO calls — look for known IO functions *)
let rec scan = function
| List (Symbol s :: _) when
s = "fetch" || s = "fetch-data" || s = "query" || s = "action" ||
s = "state-get" || s = "state-set!" ||
s = "request-arg" || s = "request-form" || s = "request-method" || s = "now" ||
s = "request-header" || s = "request-json" || s = "request-content-type" ||
s = "execute-action" || s = "submit-mutation" -> [s]
| List items | ListRef { contents = items } -> List.concat_map scan items
| _ -> []
in
let refs = scan c.c_body in
let unique = List.sort_uniq String.compare refs in
List (List.map (fun s -> String s) unique)
| _ -> List []);
bind "component-set-io-refs!" (fun _args -> Nil);
(* --- Fragment binding for aser tests --- *)
bind "<>" (fun args -> List args);
(* --- component-deps / component-set-deps! for deps.sx --- *)
let _comp_deps : (string, value) Hashtbl.t = Hashtbl.create 16 in
bind "component-deps" (fun args ->
match args with
| [Component c] -> (match Hashtbl.find_opt _comp_deps c.c_name with Some v -> v | None -> Nil)
| [Island i] -> (match Hashtbl.find_opt _comp_deps i.i_name with Some v -> v | None -> Nil)
| _ -> Nil);
bind "component-set-deps!" (fun args ->
match args with
| [Component c; v] -> Hashtbl.replace _comp_deps c.c_name v; Nil
| [Island i; v] -> Hashtbl.replace _comp_deps i.i_name v; Nil
| _ -> Nil);
(* --- submit-mutation stub for offline tests --- *)
bind "submit-mutation" (fun args ->
match args with
| _ :: _ -> String "confirmed"
| _ -> Nil);
env
(* ====================================================================== *)
(* Foundation tests (direct, no evaluator) *)
(* ====================================================================== *)
let run_foundation_tests () =
Printf.printf "=== SX OCaml Foundation Tests ===\n\n";
let assert_eq name expected actual =
if deep_equal expected actual then begin
incr pass_count;
Printf.printf " PASS: %s\n" name
end else begin
incr fail_count;
Printf.printf " FAIL: %s — expected %s, got %s\n" name
(Sx_types.inspect expected) (Sx_types.inspect actual)
end
in
let assert_true name v =
if sx_truthy v then begin
incr pass_count;
Printf.printf " PASS: %s\n" name
end else begin
incr fail_count;
Printf.printf " FAIL: %s — expected truthy, got %s\n" name (Sx_types.inspect v)
end
in
let call name args =
match Hashtbl.find_opt primitives name with
| Some f -> f args
| None -> failwith ("Unknown primitive: " ^ name)
in
Printf.printf "Suite: parser\n";
assert_eq "number" (Number 42.0) (List.hd (parse_all "42"));
assert_eq "string" (String "hello") (List.hd (parse_all "\"hello\""));
assert_eq "bool true" (Bool true) (List.hd (parse_all "true"));
assert_eq "nil" Nil (List.hd (parse_all "nil"));
assert_eq "keyword" (Keyword "class") (List.hd (parse_all ":class"));
assert_eq "symbol" (Symbol "foo") (List.hd (parse_all "foo"));
assert_eq "list" (List [Symbol "+"; Number 1.0; Number 2.0]) (List.hd (parse_all "(+ 1 2)"));
(match List.hd (parse_all "(div :class \"card\" (p \"hi\"))") with
| List [Symbol "div"; Keyword "class"; String "card"; List [Symbol "p"; String "hi"]] ->
incr pass_count; Printf.printf " PASS: nested list\n"
| v -> incr fail_count; Printf.printf " FAIL: nested list — got %s\n" (Sx_types.inspect v));
(match List.hd (parse_all "'(1 2 3)") with
| List [Symbol "quote"; List [Number 1.0; Number 2.0; Number 3.0]] ->
incr pass_count; Printf.printf " PASS: quote sugar\n"
| v -> incr fail_count; Printf.printf " FAIL: quote sugar — got %s\n" (Sx_types.inspect v));
(match List.hd (parse_all "{:a 1 :b 2}") with
| Dict d when dict_has d "a" && dict_has d "b" ->
incr pass_count; Printf.printf " PASS: dict literal\n"
| v -> incr fail_count; Printf.printf " FAIL: dict literal — got %s\n" (Sx_types.inspect v));
assert_eq "comment" (Number 42.0) (List.hd (parse_all ";; comment\n42"));
assert_eq "string escape" (String "hello\nworld") (List.hd (parse_all "\"hello\\nworld\""));
assert_eq "multiple exprs" (Number 2.0) (Number (float_of_int (List.length (parse_all "(1 2 3) (4 5)"))));
Printf.printf "\nSuite: primitives\n";
assert_eq "+" (Number 6.0) (call "+" [Number 1.0; Number 2.0; Number 3.0]);
assert_eq "-" (Number 3.0) (call "-" [Number 5.0; Number 2.0]);
assert_eq "*" (Number 12.0) (call "*" [Number 3.0; Number 4.0]);
assert_eq "/" (Number 2.5) (call "/" [Number 5.0; Number 2.0]);
assert_eq "mod" (Number 1.0) (call "mod" [Number 5.0; Number 2.0]);
assert_eq "inc" (Number 6.0) (call "inc" [Number 5.0]);
assert_eq "abs" (Number 5.0) (call "abs" [Number (-5.0)]);
assert_true "=" (call "=" [Number 1.0; Number 1.0]);
assert_true "!=" (call "!=" [Number 1.0; Number 2.0]);
assert_true "<" (call "<" [Number 1.0; Number 2.0]);
assert_true ">" (call ">" [Number 2.0; Number 1.0]);
assert_true "nil?" (call "nil?" [Nil]);
assert_true "number?" (call "number?" [Number 1.0]);
assert_true "string?" (call "string?" [String "hi"]);
assert_true "list?" (call "list?" [List [Number 1.0]]);
assert_true "empty? list" (call "empty?" [List []]);
assert_true "empty? string" (call "empty?" [String ""]);
assert_eq "str" (String "hello42") (call "str" [String "hello"; Number 42.0]);
assert_eq "upper" (String "HI") (call "upper" [String "hi"]);
assert_eq "trim" (String "hi") (call "trim" [String " hi "]);
assert_eq "join" (String "a,b,c") (call "join" [String ","; List [String "a"; String "b"; String "c"]]);
assert_true "starts-with?" (call "starts-with?" [String "hello"; String "hel"]);
assert_true "contains?" (call "contains?" [List [Number 1.0; Number 2.0; Number 3.0]; Number 2.0]);
assert_eq "list" (List [Number 1.0; Number 2.0]) (call "list" [Number 1.0; Number 2.0]);
assert_eq "len" (Number 3.0) (call "len" [List [Number 1.0; Number 2.0; Number 3.0]]);
assert_eq "first" (Number 1.0) (call "first" [List [Number 1.0; Number 2.0]]);
assert_eq "rest" (List [Number 2.0; Number 3.0]) (call "rest" [List [Number 1.0; Number 2.0; Number 3.0]]);
assert_eq "nth" (Number 2.0) (call "nth" [List [Number 1.0; Number 2.0]; Number 1.0]);
assert_eq "cons" (List [Number 0.0; Number 1.0]) (call "cons" [Number 0.0; List [Number 1.0]]);
assert_eq "append" (List [Number 1.0; Number 2.0; Number 3.0])
(call "append" [List [Number 1.0]; List [Number 2.0; Number 3.0]]);
assert_eq "reverse" (List [Number 3.0; Number 2.0; Number 1.0])
(call "reverse" [List [Number 1.0; Number 2.0; Number 3.0]]);
assert_eq "range" (List [Number 0.0; Number 1.0; Number 2.0]) (call "range" [Number 3.0]);
assert_eq "slice" (List [Number 2.0; Number 3.0])
(call "slice" [List [Number 1.0; Number 2.0; Number 3.0]; Number 1.0]);
assert_eq "type-of" (String "number") (call "type-of" [Number 1.0]);
assert_eq "type-of nil" (String "nil") (call "type-of" [Nil]);
Printf.printf "\nSuite: env\n";
let e = Sx_types.make_env () in
ignore (Sx_types.env_bind e "x" (Number 42.0));
assert_eq "env-bind + get" (Number 42.0) (Sx_types.env_get e "x");
assert_true "env-has" (Bool (Sx_types.env_has e "x"));
let child = Sx_types.env_extend e in
ignore (Sx_types.env_bind child "y" (Number 10.0));
assert_eq "child sees parent" (Number 42.0) (Sx_types.env_get child "x");
assert_eq "child own binding" (Number 10.0) (Sx_types.env_get child "y");
ignore (Sx_types.env_set child "x" (Number 99.0));
assert_eq "set! walks chain" (Number 99.0) (Sx_types.env_get e "x");
Printf.printf "\nSuite: types\n";
assert_true "sx_truthy true" (Bool (sx_truthy (Bool true)));
assert_true "sx_truthy 0" (Bool (sx_truthy (Number 0.0)));
assert_true "sx_truthy \"\"" (Bool (sx_truthy (String "")));
assert_eq "not truthy nil" (Bool false) (Bool (sx_truthy Nil));
assert_eq "not truthy false" (Bool false) (Bool (sx_truthy (Bool false)));
let l = { l_params = ["x"]; l_body = Symbol "x"; l_closure = Sx_types.make_env (); l_name = None; l_compiled = None } in
assert_true "is_lambda" (Bool (Sx_types.is_lambda (Lambda l)));
ignore (Sx_types.set_lambda_name (Lambda l) "my-fn");
assert_eq "lambda name mutated" (String "my-fn") (lambda_name (Lambda l))
(* ====================================================================== *)
(* Spec test runner *)
(* ====================================================================== *)
(* Called after module loading to sync JIT globals with env *)
let _jit_refresh_globals : (unit -> unit) ref = ref (fun () -> ())
let run_spec_tests env test_files =
(* Find project root: walk up from cwd until we find spec/tests *)
let rec find_root dir =
let candidate = Filename.concat dir "spec/tests" in
if Sys.file_exists candidate then dir
else
let parent = Filename.dirname dir in
if parent = dir then Sys.getcwd () (* reached filesystem root *)
else find_root parent
in
let project_dir = find_root (Sys.getcwd ()) in
let spec_tests_dir = Filename.concat project_dir "spec/tests" in
let framework_path = Filename.concat spec_tests_dir "test-framework.sx" in
if not (Sys.file_exists framework_path) then begin
Printf.eprintf "test-framework.sx not found at %s\n" framework_path;
Printf.eprintf "Run from the project root directory.\n";
exit 1
end;
(* IO-aware evaluation: resolve library paths and handle import suspension *)
let lib_base = Filename.concat project_dir "lib" in
let spec_base = Filename.concat project_dir "spec" in
let web_base = Filename.concat project_dir "web" in
let resolve_library_path lib_spec =
let parts = match lib_spec with List l | ListRef { contents = l } -> l | _ -> [] in
match List.map (fun v -> match v with Symbol s -> s | String s -> s | _ -> "") parts with
| ["sx"; name] ->
let spec_path = Filename.concat spec_base (name ^ ".sx") in
let lib_path = Filename.concat lib_base (name ^ ".sx") in
let web_lib_path = Filename.concat (Filename.concat web_base "lib") (name ^ ".sx") in
if Sys.file_exists spec_path then Some spec_path
else if Sys.file_exists lib_path then Some lib_path
else if Sys.file_exists web_lib_path then Some web_lib_path
else None
| ["web"; name] ->
let path = Filename.concat web_base (name ^ ".sx") in
let lib_path = Filename.concat (Filename.concat web_base "lib") (name ^ ".sx") in
if Sys.file_exists path then Some path
else if Sys.file_exists lib_path then Some lib_path
else None
| [prefix; name] ->
let path = Filename.concat (Filename.concat project_dir prefix) (name ^ ".sx") in
if Sys.file_exists path then Some path else None
| _ -> None
in
(* Run CEK step loop, handling IO suspension for imports *)
let rec eval_with_io expr env_val =
let state = Sx_ref.make_cek_state expr env_val (List []) in
run_with_io state
and load_library_file path =
let exprs = Sx_parser.parse_file path in
List.iter (fun expr -> ignore (eval_with_io expr (Env env))) exprs
and run_with_io state =
let s = ref state in
let is_terminal st = match Sx_ref.cek_terminal_p st with Bool true -> true | _ -> false in
let is_suspended st = match Sx_runtime.get_val st (String "phase") with String "io-suspended" -> true | _ -> false in
(* Check if kont has any handler frames — pure structural scan *)
let kont_has_handler kont =
let k = ref kont in
let found = ref false in
while (match !k with List (_::_) -> true | _ -> false) && not !found do
(match !k with
| List (frame :: rest) ->
(match frame with
| CekFrame f when f.cf_type = "handler" -> found := true
| _ -> ());
k := List rest
| _ -> k := List [])
done;
!found in
let rec loop () =
while not (is_terminal !s) && not (is_suspended !s) do
(try s := Sx_ref.cek_step !s
with Eval_error msg ->
let kont = Sx_ref.cek_kont !s in
if kont_has_handler kont then
(* Convert to CEK-level raise so guard/handler-bind can catch it *)
let env = Sx_ref.cek_env !s in
s := Sx_ref.make_cek_value (String msg) env
(Sx_ref.kont_push (Sx_ref.make_raise_eval_frame env (Bool false)) kont)
else
raise (Eval_error msg))
done;
if is_suspended !s then begin
let request = Sx_runtime.get_val !s (String "request") in
let req_list = match request with List l -> l | ListRef { contents = l } -> l | _ -> [] in
let op = match req_list with
| String o :: _ -> o
| Symbol o :: _ -> o
| _ -> (match Sx_runtime.get_val request (String "op") with String s -> s | _ -> "") in
let response = match op with
| "import" ->
let lib_spec = Sx_runtime.get_val request (String "library") in
if Sx_types.sx_truthy (Sx_ref.library_loaded_p lib_spec) then
Nil
else begin
(match resolve_library_path lib_spec with
| Some path ->
(try load_library_file path
with Sx_types.Eval_error msg ->
Printf.eprintf "[import] Warning loading %s: %s\n%!"
(Sx_runtime.value_to_str lib_spec) msg)
| None -> ()); (* silently skip unresolvable libraries *)
Nil
end
| "text-measure" ->
(* Monospace approximation for tests *)
let args = let a = Sx_runtime.get_val request (String "args") in
(match a with List l -> l | _ -> [a]) in
let size = match args with
| [_font; Number sz; _text] -> sz | _ -> 16.0 in
let text = match args with
| [_font; _sz; String t] -> t | _ -> "" in
let w = size *. 0.6 *. (float_of_int (String.length text)) in
let d = Hashtbl.create 4 in
Hashtbl.replace d "width" (Number w);
Hashtbl.replace d "height" (Number size);
Hashtbl.replace d "ascent" (Number (size *. 0.8));
Hashtbl.replace d "descent" (Number (size *. 0.2));
Dict d
| "io-sleep" | "io-wait" | "io-settle" | "io-wait-for" -> Nil
| "io-fetch" ->
let args = match req_list with _ :: rest -> rest | _ -> [] in
let format = match args with _ :: String f :: _ -> f | _ -> "text" in
(match format with
| "json" ->
let j = Hashtbl.create 2 in
Hashtbl.replace j "foo" (Number 1.0); Dict j
| "response" ->
let resp = Hashtbl.create 4 in
Hashtbl.replace resp "ok" (Bool true);
Hashtbl.replace resp "status" (Number 200.0);
Hashtbl.replace resp "text" (String "yay");
Dict resp
| _ -> String "yay")
| _ -> Nil
in
s := Sx_ref.cek_resume !s response;
loop ()
end else
Sx_ref.cek_value !s
in
loop ()
in
let load_and_eval path =
let ic = open_in path in
let n = in_channel_length ic in
let s = Bytes.create n in
really_input ic s 0 n;
close_in ic;
let src = Bytes.to_string s in
let exprs = parse_all src in
List.iter (fun expr ->
try ignore (eval_with_io expr (Env env))
with Sx_types.Eval_error _ -> () (* skip expressions that fail during load *)
) exprs
in
Printf.printf "\nLoading test framework...\n%!";
load_and_eval framework_path;
(* Load test harness (mock IO platform) *)
let harness_path = Filename.concat (Filename.concat project_dir "spec") "harness.sx" in
if Sys.file_exists harness_path then begin
Printf.printf "Loading test harness...\n%!";
(try load_and_eval harness_path
with e -> Printf.eprintf "Warning: harness.sx: %s\n%!" (Printexc.to_string e))
end;
(* ================================================================== *)
(* Mock DOM — host-* primitives for hyperscript behavioral tests *)
(* ================================================================== *)
(* Mock DOM elements are SX Dicts with special keys:
__mock_type: "element" | "event" | "classList" | "style" | "document"
__mock_el: back-reference to parent element (for classList/style)
tagName, id, className, children, _listeners, attributes, style, ... *)
let mock_el_counter = ref 0 in
let make_mock_element tag =
incr mock_el_counter;
let d = Hashtbl.create 16 in
Hashtbl.replace d "__mock_type" (String "element");
Hashtbl.replace d "__mock_id" (Number (float_of_int !mock_el_counter));
Hashtbl.replace d "tagName" (String (String.uppercase_ascii tag));
Hashtbl.replace d "nodeName" (String (String.uppercase_ascii tag));
Hashtbl.replace d "nodeType" (Number 1.0);
Hashtbl.replace d "id" (String "");
Hashtbl.replace d "className" (String "");
Hashtbl.replace d "textContent" (String "");
Hashtbl.replace d "innerHTML" (String "");
Hashtbl.replace d "outerHTML" (String "");
Hashtbl.replace d "value" (String "");
Hashtbl.replace d "checked" (Bool false);
Hashtbl.replace d "disabled" (Bool false);
Hashtbl.replace d "open" (Bool false);
Hashtbl.replace d "children" (List []);
Hashtbl.replace d "childNodes" (List []);
Hashtbl.replace d "parentElement" Nil;
Hashtbl.replace d "parentNode" Nil;
Hashtbl.replace d "_listeners" (Dict (Hashtbl.create 4));
Hashtbl.replace d "attributes" (Dict (Hashtbl.create 4));
Hashtbl.replace d "dataset" (Dict (Hashtbl.create 4));
(* style is a sub-dict *)
let style_d = Hashtbl.create 4 in
Hashtbl.replace style_d "__mock_type" (String "style");
Hashtbl.replace style_d "__mock_el" (Dict d);
Hashtbl.replace d "style" (Dict style_d);
(* classList is a sub-dict *)
let cl_d = Hashtbl.create 4 in
Hashtbl.replace cl_d "__mock_type" (String "classList");
Hashtbl.replace cl_d "__mock_el" (Dict d);
Hashtbl.replace d "classList" (Dict cl_d);
Dict d
in
let mock_body = match make_mock_element "body" with Dict d -> d | _ -> assert false in
Hashtbl.replace mock_body "tagName" (String "BODY");
Hashtbl.replace mock_body "nodeName" (String "BODY");
let mock_document =
let d = Hashtbl.create 8 in
Hashtbl.replace d "__mock_type" (String "document");
Hashtbl.replace d "body" (Dict mock_body);
Hashtbl.replace d "title" (String "");
Dict d
in
(* Helper: get classes from className string *)
let get_classes d =
match Hashtbl.find_opt d "className" with
| Some (String s) -> String.split_on_char ' ' s |> List.filter (fun s -> s <> "")
| _ -> []
in
(* Helper: set className from class list *)
let set_classes d classes =
Hashtbl.replace d "className" (String (String.concat " " classes))
in
(* Helper: add child to parent *)
let mock_append_child parent child =
match parent, child with
| Dict pd, Dict cd ->
(* Remove from old parent first *)
(match Hashtbl.find_opt cd "parentElement" with
| Some (Dict old_parent) ->
let old_kids = match Hashtbl.find_opt old_parent "children" with
| Some (List l) -> List.filter (fun c -> c != Dict cd) l | _ -> [] in
Hashtbl.replace old_parent "children" (List old_kids);
Hashtbl.replace old_parent "childNodes" (List old_kids)
| _ -> ());
let kids = match Hashtbl.find_opt pd "children" with
| Some (List l) -> l | _ -> [] in
Hashtbl.replace pd "children" (List (kids @ [child]));
Hashtbl.replace pd "childNodes" (List (kids @ [child]));
Hashtbl.replace cd "parentElement" parent;
Hashtbl.replace cd "parentNode" parent;
child
| _ -> child
in
(* Helper: remove child from parent *)
let mock_remove_child parent child =
match parent, child with
| Dict pd, Dict cd ->
let kids = match Hashtbl.find_opt pd "children" with
| Some (List l) -> List.filter (fun c -> c != Dict cd) l | _ -> [] in
Hashtbl.replace pd "children" (List kids);
Hashtbl.replace pd "childNodes" (List kids);
Hashtbl.replace cd "parentElement" Nil;
Hashtbl.replace cd "parentNode" Nil;
child
| _ -> child
in
(* Helper: querySelector - find element matching selector in tree *)
let mock_matches el sel =
match el with
| Dict d ->
let sel = String.trim sel in
if String.length sel > 0 && sel.[0] = '#' then
let id = String.sub sel 1 (String.length sel - 1) in
(match Hashtbl.find_opt d "id" with Some (String i) -> i = id | _ -> false)
else if String.length sel > 0 && sel.[0] = '.' then
let cls = String.sub sel 1 (String.length sel - 1) in
List.mem cls (get_classes d)
else if String.length sel > 0 && sel.[0] = '[' then
(* [attr] or [attr="value"] *)
let inner = String.sub sel 1 (String.length sel - 2) in
(match String.split_on_char '=' inner with
| [attr] ->
let attrs = match Hashtbl.find_opt d "attributes" with Some (Dict a) -> a | _ -> Hashtbl.create 0 in
Hashtbl.mem attrs attr
| [attr; v] ->
let attrs = match Hashtbl.find_opt d "attributes" with Some (Dict a) -> a | _ -> Hashtbl.create 0 in
let v = if String.length v >= 2 && v.[0] = '"' then String.sub v 1 (String.length v - 2) else v in
(match Hashtbl.find_opt attrs attr with Some (String s) -> s = v | _ -> false)
| _ -> false)
else
(* Tag name match *)
(match Hashtbl.find_opt d "tagName" with
| Some (String t) -> String.lowercase_ascii t = String.lowercase_ascii sel
| _ -> false)
| _ -> false
in
let rec mock_query_selector el sel =
match el with
| Dict d ->
let kids = match Hashtbl.find_opt d "children" with Some (List l) -> l | _ -> [] in
let rec search = function
| [] -> Nil
| child :: rest ->
if mock_matches child sel then child
else match mock_query_selector child sel with
| Nil -> search rest
| found -> found
in
search kids
| _ -> Nil
in
let rec mock_query_all el sel =
match el with
| Dict d ->
let kids = match Hashtbl.find_opt d "children" with Some (List l) -> l | _ -> [] in
List.concat_map (fun child ->
(if mock_matches child sel then [child] else []) @ mock_query_all child sel
) kids
| _ -> []
in
(* Helper: dispatch event with bubbling *)
let rec mock_dispatch_event target event =
match event with
| Dict ev ->
let cur_target = match Hashtbl.find_opt ev "target" with Some Nil | None -> target | Some x -> x in
Hashtbl.replace ev "target" cur_target;
Hashtbl.replace ev "currentTarget" target;
(match target with
| Dict td ->
let listeners = match Hashtbl.find_opt td "_listeners" with Some (Dict l) -> l | _ -> Hashtbl.create 0 in
let evt_type = match Hashtbl.find_opt ev "type" with Some (String t) -> t | _ -> "" in
let fns = match Hashtbl.find_opt listeners evt_type with Some (List l) -> l | _ -> [] in
List.iter (fun fn ->
let stopped = match Hashtbl.find_opt ev "_stopImmediate" with Some (Bool true) -> true | _ -> false in
if not stopped then
(try ignore (Sx_ref.cek_call fn (List [Dict ev]))
with _ -> ())
) fns;
(* Bubble *)
let bubbles = match Hashtbl.find_opt ev "bubbles" with Some (Bool true) -> true | _ -> false in
let stopped = match Hashtbl.find_opt ev "_stopped" with Some (Bool true) -> true | _ -> false in
if bubbles && not stopped then
(match Hashtbl.find_opt td "parentElement" with
| Some (Dict _ as parent) -> ignore (mock_dispatch_event parent (Dict ev))
| _ -> ())
| _ -> ());
let dp = match Hashtbl.find_opt ev "defaultPrevented" with Some (Bool true) -> true | _ -> false in
Bool (not dp)
| _ -> Bool true
in
(* Register host-* primitives *)
let reg name fn = Sx_primitives.register name fn in
reg "host-global" (fun args ->
match args with
| [String "document"] -> mock_document
| [String "window"] -> Nil (* self-referential, not needed for tests *)
| [String "console"] ->
let d = Hashtbl.create 4 in
Hashtbl.replace d "__mock_type" (String "console");
Dict d
| [String name] ->
(* Check SX env for globally defined things like "tmp" used in HS tests *)
(try Sx_types.env_get env name with _ -> Nil)
| _ -> Nil);
reg "host-get" (fun args ->
match args with
| [Nil; _] -> Nil
| [Dict d; String key] ->
let mt = match Hashtbl.find_opt d "__mock_type" with Some (String t) -> t | _ -> "" in
(* classList.length *)
if mt = "classList" && key = "length" then
let el = match Hashtbl.find_opt d "__mock_el" with Some (Dict e) -> e | _ -> d in
Number (float_of_int (List.length (get_classes el)))
else
(match Hashtbl.find_opt d key with
| Some v -> v
| None ->
(* For mock elements, return a truthy sentinel for method names
so that guards like (host-get el "setAttribute") pass *)
if mt = "element" then
(match key with
| "setAttribute" | "getAttribute" | "removeAttribute" | "hasAttribute"
| "addEventListener" | "removeEventListener" | "dispatchEvent"
| "appendChild" | "removeChild" | "insertBefore" | "replaceChild"
| "querySelector" | "querySelectorAll" | "closest" | "matches"
| "contains" | "cloneNode" | "remove" | "focus" | "blur" | "click"
| "insertAdjacentHTML" | "prepend" | "showModal" | "show" | "close"
| "getBoundingClientRect" | "getAnimations" | "scrollIntoView"
| "scrollTo" | "scroll" | "reset" -> Bool true
| "firstElementChild" ->
let kids = match Hashtbl.find_opt d "children" with Some (List l) -> l | _ -> [] in
(match kids with c :: _ -> c | [] -> Nil)
| "lastElementChild" ->
let kids = match Hashtbl.find_opt d "children" with Some (List l) -> l | _ -> [] in
(match List.rev kids with c :: _ -> c | [] -> Nil)
| "nextElementSibling" ->
(match Hashtbl.find_opt d "parentElement" with
| Some (Dict p) ->
let kids = match Hashtbl.find_opt p "children" with Some (List l) -> l | _ -> [] in
let rec find_next = function
| [] | [_] -> Nil
| a :: b :: _ when a == Dict d -> b
| _ :: rest -> find_next rest in
find_next kids
| _ -> Nil)
| "previousElementSibling" ->
(match Hashtbl.find_opt d "parentElement" with
| Some (Dict p) ->
let kids = match Hashtbl.find_opt p "children" with Some (List l) -> l | _ -> [] in
let rec find_prev prev = function
| [] -> Nil
| a :: _ when a == Dict d -> prev
| a :: rest -> find_prev a rest in
find_prev Nil kids
| _ -> Nil)
| "ownerDocument" -> mock_document
| _ -> Nil)
else if mt = "document" then
(match key with
| "createElement" | "createElementNS" | "createDocumentFragment"
| "createTextNode" | "createComment" | "getElementById"
| "querySelector" | "querySelectorAll" | "createEvent"
| "addEventListener" | "removeEventListener" -> Bool true
| "head" ->
let head = Hashtbl.create 4 in
Hashtbl.replace head "__mock_type" (String "element");
Hashtbl.replace head "tagName" (String "HEAD");
Dict head
| "activeElement" -> Nil
| _ -> Nil)
else Nil)
| [Dict d; Number n] ->
(* Array index access *)
let i = int_of_float n in
(match Hashtbl.find_opt d "children" with
| Some (List l) when i >= 0 && i < List.length l -> List.nth l i
| _ -> (match Hashtbl.find_opt d (string_of_int i) with Some v -> v | None -> Nil))
| _ -> Nil);
(* Stringify a value for DOM string properties *)
let rec dom_stringify = function
| String s -> String s
| Number n ->
let i = int_of_float n in
if float_of_int i = n then String (string_of_int i) else String (string_of_float n)
| Bool true -> String "true"
| Bool false -> String "false"
| List l -> String (String.concat "," (List.map (fun v -> match dom_stringify v with String s -> s | _ -> "") l))
| Nil -> String ""
| v -> String (Sx_types.inspect v)
in
reg "host-set!" (fun args ->
match args with
| [Nil; _; _] -> Nil
| [Dict d; String key; value] ->
(* DOM string properties: coerce to string like a browser *)
let stored = match key with
| "innerHTML" | "textContent" | "outerHTML" | "value" | "innerText" ->
dom_stringify value
| _ -> value
in
Hashtbl.replace d key stored;
(* Side effects for special keys *)
(match key with
| "className" ->
(match Hashtbl.find_opt d "classList" with
| Some (Dict _cl) -> () (* classes live in className *)
| _ -> ())
| "innerHTML" ->
(* Setting innerHTML clears children and syncs textContent (like a browser) *)
let kids = match Hashtbl.find_opt d "children" with Some (List l) -> l | _ -> [] in
List.iter (fun c -> match c with Dict cd ->
Hashtbl.replace cd "parentElement" Nil;
Hashtbl.replace cd "parentNode" Nil | _ -> ()) kids;
Hashtbl.replace d "children" (List []);
Hashtbl.replace d "childNodes" (List []);
(* Approximate textContent: strip HTML tags from innerHTML *)
(match stored with
| String s ->
let buf = Buffer.create (String.length s) in
let in_tag = ref false in
String.iter (fun c ->
if c = '<' then in_tag := true
else if c = '>' then in_tag := false
else if not !in_tag then Buffer.add_char buf c
) s;
Hashtbl.replace d "textContent" (String (Buffer.contents buf))
| _ -> Hashtbl.replace d "textContent" (String ""))
| "textContent" ->
(* Setting textContent clears children *)
Hashtbl.replace d "children" (List []);
Hashtbl.replace d "childNodes" (List [])
| _ -> ());
stored
| [ListRef r; Number n; value] ->
let idx = int_of_float n in
let lst = !r in
if idx >= 0 && idx < List.length lst then
r := List.mapi (fun i v -> if i = idx then value else v) lst
else if idx = List.length lst then
r := lst @ [value];
value
| [List _; Number _; _value] ->
(* Immutable list — can't set, but don't crash *)
Nil
| _ -> Nil);
reg "host-call" (fun args ->
match args with
| Nil :: String m :: rest ->
(* Global function call *)
(match m with
| "setTimeout" -> (match rest with fn :: _ -> ignore (Sx_ref.cek_call fn (List [])); Nil | _ -> Nil)
| "clearTimeout" -> Nil
| _ -> Nil)
| Dict d :: String m :: rest ->
let mt = match Hashtbl.find_opt d "__mock_type" with Some (String t) -> t | _ -> "" in
if mt = "document" then
(* Document methods *)
(match m with
| "createElement" | "createElementNS" ->
let tag = match rest with [String t] -> t | [_; String t] -> t | _ -> "div" in
make_mock_element tag
| "createDocumentFragment" ->
let el = make_mock_element "fragment" in
(match el with Dict d -> Hashtbl.replace d "nodeType" (Number 11.0); el | _ -> el)
| "createTextNode" ->
let text = match rest with [String t] -> t | _ -> "" in
let d = Hashtbl.create 4 in
Hashtbl.replace d "__mock_type" (String "text");
Hashtbl.replace d "nodeType" (Number 3.0);
Hashtbl.replace d "textContent" (String text);
Hashtbl.replace d "data" (String text);
Dict d
| "createComment" ->
let d = Hashtbl.create 4 in
Hashtbl.replace d "__mock_type" (String "comment");
Hashtbl.replace d "nodeType" (Number 8.0);
Dict d
| "getElementById" ->
let id = match rest with [String i] -> i | _ -> "" in
let body = match Hashtbl.find_opt d "body" with Some b -> b | None -> Nil in
mock_query_selector body ("#" ^ id)
| "querySelector" ->
let sel = match rest with [String s] -> s | _ -> "" in
let body = match Hashtbl.find_opt d "body" with Some b -> b | None -> Nil in
mock_query_selector body sel
| "querySelectorAll" ->
let sel = match rest with [String s] -> s | _ -> "" in
let body = match Hashtbl.find_opt d "body" with Some b -> b | None -> Nil in
List (mock_query_all body sel)
| "createEvent" ->
let ev = Hashtbl.create 4 in
Hashtbl.replace ev "__mock_type" (String "event");
Dict ev
| "addEventListener" | "removeEventListener" -> Nil
| _ -> Nil)
else if mt = "classList" then
let el = match Hashtbl.find_opt d "__mock_el" with Some (Dict e) -> e | _ -> d in
(match m with
| "add" ->
let classes = get_classes el in
let new_classes = List.fold_left (fun acc a ->
match a with String c when not (List.mem c acc) -> acc @ [c] | _ -> acc
) classes rest in
set_classes el new_classes; Nil
| "remove" ->
let classes = get_classes el in
let to_remove = List.filter_map (function String c -> Some c | _ -> None) rest in
let new_classes = List.filter (fun c -> not (List.mem c to_remove)) classes in
set_classes el new_classes; Nil
| "toggle" ->
(match rest with
| [String cls] ->
let classes = get_classes el in
if List.mem cls classes then
(set_classes el (List.filter (fun c -> c <> cls) classes); Bool false)
else
(set_classes el (classes @ [cls]); Bool true)
| [String cls; Bool force] ->
let classes = get_classes el in
if force then
(if not (List.mem cls classes) then set_classes el (classes @ [cls]); Bool true)
else
(set_classes el (List.filter (fun c -> c <> cls) classes); Bool false)
| _ -> Nil)
| "contains" ->
(match rest with
| [String cls] -> Bool (List.mem cls (get_classes el))
| _ -> Bool false)
| _ -> Nil)
else if mt = "event" then
(match m with
| "preventDefault" -> Hashtbl.replace d "defaultPrevented" (Bool true); Nil
| "stopPropagation" -> Hashtbl.replace d "_stopped" (Bool true); Nil
| "stopImmediatePropagation" ->
Hashtbl.replace d "_stopped" (Bool true);
Hashtbl.replace d "_stopImmediate" (Bool true); Nil
| _ -> Nil)
else if mt = "style" then
(match m with
| "setProperty" ->
(match rest with
| [String prop; String value] -> Hashtbl.replace d prop (String value); Nil
| [String prop; value] -> Hashtbl.replace d prop value; Nil
| _ -> Nil)
| "removeProperty" ->
(match rest with [String prop] -> Hashtbl.remove d prop; Nil | _ -> Nil)
| "getPropertyValue" ->
(match rest with
| [String prop] -> (match Hashtbl.find_opt d prop with Some v -> v | None -> String "")
| _ -> String "")
| _ -> Nil)
else if mt = "console" then
(* console.log/debug/error — no-op in tests *)
Nil
else
(* Element methods *)
(match m with
| "setAttribute" ->
(match rest with
| [String name; value] ->
let attrs = match Hashtbl.find_opt d "attributes" with Some (Dict a) -> a | _ ->
let a = Hashtbl.create 4 in Hashtbl.replace d "attributes" (Dict a); a in
let sv = match value with String s -> s | Number n ->
let i = int_of_float n in if float_of_int i = n then string_of_int i
else string_of_float n | _ -> Sx_types.inspect value in
Hashtbl.replace attrs name (String sv);
if name = "id" then Hashtbl.replace d "id" (String sv);
if name = "class" then begin
Hashtbl.replace d "className" (String sv);
end;
if name = "disabled" then Hashtbl.replace d "disabled" (Bool true);
if name = "style" then begin
(* Parse CSS string into the style sub-dict *)
let style_d = match Hashtbl.find_opt d "style" with Some (Dict s) -> s | _ ->
let s = Hashtbl.create 4 in Hashtbl.replace d "style" (Dict s); s in
let parts = String.split_on_char ';' sv in
List.iter (fun part ->
let part = String.trim part in
if String.length part > 0 then
match String.index_opt part ':' with
| Some i ->
let prop = String.trim (String.sub part 0 i) in
let value = String.trim (String.sub part (i+1) (String.length part - i - 1)) in
Hashtbl.replace style_d prop (String value)
| None -> ()
) parts
end;
Nil
| _ -> Nil)
| "getAttribute" ->
(match rest with
| [String name] ->
let attrs = match Hashtbl.find_opt d "attributes" with Some (Dict a) -> a | _ -> Hashtbl.create 0 in
(match Hashtbl.find_opt attrs name with Some v -> v | None -> Nil)
| _ -> Nil)
| "removeAttribute" ->
(match rest with
| [String name] ->
let attrs = match Hashtbl.find_opt d "attributes" with Some (Dict a) -> a | _ -> Hashtbl.create 0 in
Hashtbl.remove attrs name;
if name = "disabled" then Hashtbl.replace d "disabled" (Bool false);
Nil
| _ -> Nil)
| "hasAttribute" ->
(match rest with
| [String name] ->
let attrs = match Hashtbl.find_opt d "attributes" with Some (Dict a) -> a | _ -> Hashtbl.create 0 in
Bool (Hashtbl.mem attrs name)
| _ -> Bool false)
| "addEventListener" ->
(match rest with
| String evt_name :: fn :: _ ->
let listeners = match Hashtbl.find_opt d "_listeners" with Some (Dict l) -> l | _ ->
let l = Hashtbl.create 4 in Hashtbl.replace d "_listeners" (Dict l); l in
let fns = match Hashtbl.find_opt listeners evt_name with Some (List l) -> l | _ -> [] in
Hashtbl.replace listeners evt_name (List (fns @ [fn]));
Nil
| _ -> Nil)
| "removeEventListener" ->
(match rest with
| [String evt_name; fn] ->
let listeners = match Hashtbl.find_opt d "_listeners" with Some (Dict l) -> l | _ -> Hashtbl.create 0 in
let fns = match Hashtbl.find_opt listeners evt_name with Some (List l) -> l | _ -> [] in
Hashtbl.replace listeners evt_name (List (List.filter (fun f -> f != fn) fns));
Nil
| _ -> Nil)
| "dispatchEvent" ->
(match rest with [ev] -> mock_dispatch_event (Dict d) ev | _ -> Nil)
| "appendChild" ->
(match rest with [child] -> mock_append_child (Dict d) child | _ -> Nil)
| "removeChild" ->
(match rest with [child] -> mock_remove_child (Dict d) child | _ -> Nil)
| "insertBefore" ->
(match rest with
| [new_child; ref_child] ->
(* Remove from old parent *)
(match new_child with
| Dict cd -> (match Hashtbl.find_opt cd "parentElement" with
| Some (Dict p) -> ignore (mock_remove_child (Dict p) new_child) | _ -> ())
| _ -> ());
let kids = match Hashtbl.find_opt d "children" with Some (List l) -> l | _ -> [] in
let idx = let rec find_idx i = function [] -> List.length kids | c :: _ when c == ref_child -> i | _ :: rest -> find_idx (i+1) rest in find_idx 0 kids in
let before = List.filteri (fun i _ -> i < idx) kids in
let after = List.filteri (fun i _ -> i >= idx) kids in
let new_kids = before @ [new_child] @ after in
Hashtbl.replace d "children" (List new_kids);
Hashtbl.replace d "childNodes" (List new_kids);
(match new_child with Dict cd ->
Hashtbl.replace cd "parentElement" (Dict d);
Hashtbl.replace cd "parentNode" (Dict d) | _ -> ());
new_child
| _ -> Nil)
| "replaceChild" ->
(match rest with
| [new_child; old_child] ->
let kids = match Hashtbl.find_opt d "children" with Some (List l) -> l | _ -> [] in
let new_kids = List.map (fun c -> if c == old_child then new_child else c) kids in
Hashtbl.replace d "children" (List new_kids);
Hashtbl.replace d "childNodes" (List new_kids);
(match new_child with Dict cd ->
Hashtbl.replace cd "parentElement" (Dict d);
Hashtbl.replace cd "parentNode" (Dict d) | _ -> ());
(match old_child with Dict cd ->
Hashtbl.replace cd "parentElement" Nil;
Hashtbl.replace cd "parentNode" Nil | _ -> ());
old_child
| _ -> Nil)
| "remove" ->
(match Hashtbl.find_opt d "parentElement" with
| Some (Dict p) -> ignore (mock_remove_child (Dict p) (Dict d)) | _ -> ());
Nil
| "querySelector" ->
(match rest with [String sel] -> mock_query_selector (Dict d) sel | _ -> Nil)
| "querySelectorAll" ->
(match rest with [String sel] -> List (mock_query_all (Dict d) sel) | _ -> List [])
| "closest" ->
(match rest with
| [String sel] ->
let rec up = function
| Dict e as el -> if mock_matches el sel then el else
(match Hashtbl.find_opt e "parentElement" with Some (Dict _ as p) -> up p | _ -> Nil)
| _ -> Nil
in up (Dict d)
| _ -> Nil)
| "matches" ->
(match rest with [String sel] -> Bool (mock_matches (Dict d) sel) | _ -> Bool false)
| "contains" ->
(match rest with
| [target] ->
let rec contains_check el =
if el == target then true
else match el with
| Dict dd -> let kids = match Hashtbl.find_opt dd "children" with Some (List l) -> l | _ -> [] in
List.exists contains_check kids
| _ -> false
in Bool (contains_check (Dict d))
| _ -> Bool false)
| "cloneNode" ->
let deep = match rest with [Bool b] -> b | _ -> false in
let rec clone_el el = match el with Dict src ->
let nd = Hashtbl.create 16 in
Hashtbl.iter (fun k v ->
if k <> "parentElement" && k <> "parentNode" && k <> "_listeners" && k <> "children" && k <> "childNodes" then
Hashtbl.replace nd k v
) src;
Hashtbl.replace nd "parentElement" Nil;
Hashtbl.replace nd "parentNode" Nil;
Hashtbl.replace nd "_listeners" (Dict (Hashtbl.create 4));
incr mock_el_counter;
Hashtbl.replace nd "__mock_id" (Number (float_of_int !mock_el_counter));
let new_style = Hashtbl.create 4 in
(match Hashtbl.find_opt src "style" with
| Some (Dict s) -> Hashtbl.iter (fun k v -> if k <> "__mock_el" then Hashtbl.replace new_style k v) s
| _ -> ());
Hashtbl.replace new_style "__mock_type" (String "style");
Hashtbl.replace new_style "__mock_el" (Dict nd);
Hashtbl.replace nd "style" (Dict new_style);
let new_cl = Hashtbl.create 4 in
Hashtbl.replace new_cl "__mock_type" (String "classList");
Hashtbl.replace new_cl "__mock_el" (Dict nd);
Hashtbl.replace nd "classList" (Dict new_cl);
if deep then begin
let kids = match Hashtbl.find_opt src "children" with Some (List l) -> l | _ -> [] in
let cloned_kids = List.map (fun c -> match c with Dict _ -> clone_el c | _ -> c) kids in
List.iter (fun c -> match c with Dict cd ->
Hashtbl.replace cd "parentElement" (Dict nd);
Hashtbl.replace cd "parentNode" (Dict nd) | _ -> ()) cloned_kids;
Hashtbl.replace nd "children" (List cloned_kids);
Hashtbl.replace nd "childNodes" (List cloned_kids)
end else begin
Hashtbl.replace nd "children" (List []);
Hashtbl.replace nd "childNodes" (List [])
end;
Dict nd
| _ -> el
in
(match rest with _ -> clone_el (Dict d))
| "focus" | "blur" | "scrollIntoView" | "scrollTo" | "scroll" -> Nil
| "click" ->
let ev = Hashtbl.create 8 in
Hashtbl.replace ev "__mock_type" (String "event");
Hashtbl.replace ev "type" (String "click");
Hashtbl.replace ev "bubbles" (Bool true);
Hashtbl.replace ev "cancelable" (Bool true);
Hashtbl.replace ev "defaultPrevented" (Bool false);
Hashtbl.replace ev "_stopped" (Bool false);
Hashtbl.replace ev "_stopImmediate" (Bool false);
Hashtbl.replace ev "target" (Dict d);
mock_dispatch_event (Dict d) (Dict ev)
| "getAnimations" -> List []
| "getBoundingClientRect" ->
let r = Hashtbl.create 8 in
Hashtbl.replace r "top" (Number 0.0); Hashtbl.replace r "left" (Number 0.0);
Hashtbl.replace r "width" (Number 100.0); Hashtbl.replace r "height" (Number 100.0);
Hashtbl.replace r "right" (Number 100.0); Hashtbl.replace r "bottom" (Number 100.0);
Dict r
| "insertAdjacentHTML" ->
(* Position-aware insertion, coerce value to string *)
(match rest with
| [String pos; value] ->
let html = match dom_stringify value with String s -> s | _ -> "" in
let cur = match Hashtbl.find_opt d "innerHTML" with Some (String s) -> s | _ -> "" in
let new_html = match pos with
| "afterbegin" -> html ^ cur (* prepend *)
| _ -> cur ^ html (* beforeend / default: append *)
in
Hashtbl.replace d "innerHTML" (String new_html);
(* Sync textContent *)
let buf = Buffer.create (String.length new_html) in
let in_tag = ref false in
String.iter (fun c ->
if c = '<' then in_tag := true
else if c = '>' then in_tag := false
else if not !in_tag then Buffer.add_char buf c
) new_html;
Hashtbl.replace d "textContent" (String (Buffer.contents buf));
Nil
| _ -> Nil)
| "showModal" | "show" ->
Hashtbl.replace d "open" (Bool true);
let attrs = match Hashtbl.find_opt d "attributes" with Some (Dict a) -> a | _ ->
let a = Hashtbl.create 4 in Hashtbl.replace d "attributes" (Dict a); a in
Hashtbl.replace attrs "open" (String ""); Nil
| "close" ->
Hashtbl.replace d "open" (Bool false);
let attrs = match Hashtbl.find_opt d "attributes" with Some (Dict a) -> a | _ -> Hashtbl.create 0 in
Hashtbl.remove attrs "open"; Nil
| "prepend" ->
(match rest with
| [child] ->
let kids = match Hashtbl.find_opt d "children" with Some (List l) -> l | _ -> [] in
Hashtbl.replace d "children" (List (child :: kids));
Hashtbl.replace d "childNodes" (List (child :: kids));
(match child with Dict cd ->
Hashtbl.replace cd "parentElement" (Dict d);
Hashtbl.replace cd "parentNode" (Dict d) | _ -> ());
Nil
| _ -> Nil)
| "reset" ->
(* Reset form elements to their default values *)
let get_attrs (dd : (string, Sx_types.value) Hashtbl.t) =
match Hashtbl.find_opt dd "attributes" with Some (Dict a) -> a | _ -> Hashtbl.create 0
in
let reset_input (ed : (string, Sx_types.value) Hashtbl.t) =
let attrs = get_attrs ed in
let typ = match Hashtbl.find_opt attrs "type" with Some (String t) -> String.lowercase_ascii t | _ -> "text" in
if typ = "checkbox" || typ = "radio" then
Hashtbl.replace ed "checked" (Bool (Hashtbl.mem attrs "checked"))
else
let dv = match Hashtbl.find_opt attrs "value" with Some v -> v | None -> String "" in
Hashtbl.replace ed "value" dv
in
let reset_textarea (ed : (string, Sx_types.value) Hashtbl.t) =
let attrs = get_attrs ed in
let dv = match Hashtbl.find_opt attrs "value" with
| Some v -> v
| None -> (match Hashtbl.find_opt ed "textContent" with Some v -> v | None -> String "")
in
Hashtbl.replace ed "value" dv
in
let reset_select (ed : (string, Sx_types.value) Hashtbl.t) =
let kids = match Hashtbl.find_opt ed "children" with Some (List l) -> l | _ -> [] in
let v = List.fold_left (fun (acc : string) (k : Sx_types.value) ->
match k with
| Dict od ->
let a = get_attrs od in
let ov = match Hashtbl.find_opt a "value" with Some (String s) -> s | _ -> "" in
if acc = "" then ov
else if Hashtbl.mem a "selected" then ov
else acc
| _ -> acc
) "" kids in
Hashtbl.replace ed "value" (String v)
in
let rec reset_el (el : Sx_types.value) =
match el with
| Dict ed ->
let tag = match Hashtbl.find_opt ed "tagName" with Some (String t) -> String.lowercase_ascii t | _ -> "" in
if tag = "input" then reset_input ed
else if tag = "textarea" then reset_textarea ed
else if tag = "select" then reset_select ed
else
let kids = match Hashtbl.find_opt ed "children" with Some (List l) -> l | _ -> [] in
List.iter reset_el kids
| _ -> ()
in
reset_el (Dict d); Nil
| _ -> Nil)
| _ -> Nil);
reg "host-new" (fun args ->
match args with
| [String "CustomEvent"; String typ] ->
let ev = Hashtbl.create 8 in
Hashtbl.replace ev "__mock_type" (String "event");
Hashtbl.replace ev "type" (String typ);
Hashtbl.replace ev "bubbles" (Bool false);
Hashtbl.replace ev "cancelable" (Bool true);
Hashtbl.replace ev "defaultPrevented" (Bool false);
Hashtbl.replace ev "_stopped" (Bool false);
Hashtbl.replace ev "_stopImmediate" (Bool false);
Hashtbl.replace ev "target" Nil;
Hashtbl.replace ev "detail" Nil;
Dict ev
| [String "CustomEvent"; String typ; Dict opts] ->
let ev = Hashtbl.create 8 in
Hashtbl.replace ev "__mock_type" (String "event");
Hashtbl.replace ev "type" (String typ);
Hashtbl.replace ev "bubbles" (match Hashtbl.find_opt opts "bubbles" with Some v -> v | None -> Bool false);
Hashtbl.replace ev "cancelable" (match Hashtbl.find_opt opts "cancelable" with Some v -> v | None -> Bool true);
Hashtbl.replace ev "defaultPrevented" (Bool false);
Hashtbl.replace ev "_stopped" (Bool false);
Hashtbl.replace ev "_stopImmediate" (Bool false);
Hashtbl.replace ev "target" Nil;
Hashtbl.replace ev "detail" (match Hashtbl.find_opt opts "detail" with Some v -> v | None -> Nil);
Dict ev
| [String "Event"; String typ] ->
let ev = Hashtbl.create 8 in
Hashtbl.replace ev "__mock_type" (String "event");
Hashtbl.replace ev "type" (String typ);
Hashtbl.replace ev "bubbles" (Bool false);
Hashtbl.replace ev "defaultPrevented" (Bool false);
Hashtbl.replace ev "_stopped" (Bool false);
Hashtbl.replace ev "_stopImmediate" (Bool false);
Dict ev
| _ -> Nil);
reg "host-callback" (fun args ->
match args with
| [fn] ->
(* Wrap SX function as a NativeFn that calls it via CEK *)
(match fn with
| NativeFn _ -> fn (* already a native fn *)
| Lambda _ | Component _ ->
NativeFn ("host-callback", fun cb_args ->
try Sx_ref.cek_call fn (List cb_args)
with e -> Printf.eprintf "[mock] host-callback error: %s\n%!" (Printexc.to_string e); Nil)
| _ -> NativeFn ("host-callback-noop", fun _ -> Nil))
| _ -> NativeFn ("host-callback-noop", fun _ -> Nil));
reg "host-typeof" (fun args ->
match args with
| [Nil] -> String "nil"
| [Dict d] ->
(match Hashtbl.find_opt d "__mock_type" with
| Some (String "element") -> String "element"
| Some (String "text") -> String "text"
| Some (String "event") -> String "event"
| Some (String "document") -> String "document"
| _ -> String "object")
| [String _] -> String "string"
| [Number _] -> String "number"
| [Bool _] -> String "boolean"
| [NativeFn _] | [Lambda _] -> String "function"
| _ -> String "nil");
reg "host-await" (fun _args -> Nil);
(* Reset mock body — called between tests via hs-cleanup! *)
reg "mock-dom-reset!" (fun _args ->
Hashtbl.replace mock_body "children" (List []);
Hashtbl.replace mock_body "childNodes" (List []);
Hashtbl.replace mock_body "innerHTML" (String "");
Hashtbl.replace mock_body "textContent" (String "");
Nil);
(* IO resolution function — used by both run_with_io and _cek_io_suspend_hook *)
let resolve_io request =
let req_list = match request with List l -> l | ListRef { contents = l } -> l | _ -> [] in
let op, args = match req_list with
| String op :: rest -> op, rest
| Symbol op :: rest -> op, rest
| _ ->
(match request with
| Dict d ->
let op = match Hashtbl.find_opt d "op" with Some (String s) -> s | _ -> "" in
let a = match Hashtbl.find_opt d "args" with Some (List l) -> l | _ -> [] in
op, a
| _ -> "", [])
in
match op with
| "io-sleep" | "io-wait" | "io-settle" | "io-wait-for" -> Nil
| "io-fetch" ->
let url = match args with String u :: _ -> u | _ -> "" in
let format = match args with _ :: String f :: _ -> f | _ -> "text" in
let body = "yay" in
(match format with
| "json" ->
let j = Hashtbl.create 2 in
Hashtbl.replace j "foo" (Number 1.0); Dict j
| "response" ->
let resp = Hashtbl.create 4 in
Hashtbl.replace resp "ok" (Bool true);
Hashtbl.replace resp "status" (Number 200.0);
Hashtbl.replace resp "url" (String url);
Hashtbl.replace resp "text" (String body);
Dict resp
| _ -> String body)
| _ -> Nil
in
(* Use suspend hook (not resolver) — cek_run's resume has a propagation bug.
The hook receives the suspended state and must return the final value. *)
Sx_types._cek_io_resolver := None;
Sx_types._cek_io_suspend_hook := Some (fun suspended ->
let request = Sx_ref.cek_io_request suspended in
let response = resolve_io request in
(* Resume by manually stepping from the resumed state *)
let resumed = Sx_ref.cek_resume suspended response in
let is_terminal st = match Sx_ref.cek_terminal_p st with Bool true -> true | _ -> false in
let is_suspended st = match Sx_runtime.get_val st (String "phase") with String "io-suspended" -> true | _ -> false in
let s = ref resumed in
while not (is_terminal !s) && not (is_suspended !s) do
(try s := Sx_ref.cek_step !s
with Sx_types.CekPerformRequest req ->
let resp = resolve_io req in
s := Sx_ref.cek_resume (Sx_ref.make_cek_suspended req (Sx_ref.cek_env !s) (Sx_ref.cek_kont !s)) resp)
done;
if is_suspended !s then
let req2 = Sx_ref.cek_io_request !s in
let resp2 = resolve_io req2 in
Sx_ref.cek_value (Sx_ref.cek_resume !s resp2)
else
Sx_ref.cek_value !s);
(* Load modules needed by tests *)
let spec_dir = Filename.concat project_dir "spec" in
let lib_dir = Filename.concat project_dir "lib" in
let web_dir = Filename.concat project_dir "web" in
let load_module name dir =
let path = Filename.concat dir name in
if Sys.file_exists path then begin
Printf.printf "Loading %s...\n%!" name;
(try load_and_eval path
with e -> Printf.eprintf "Warning: %s: %s\n%!" name (Printexc.to_string e))
end
in
(* R7RS compatibility library — minimal test version *)
load_module "r7rs.sx" lib_dir;
(* Render adapter for test-render-html.sx *)
load_module "render.sx" spec_dir;
load_module "canonical.sx" spec_dir;
load_module "adapter-html.sx" web_dir;
load_module "adapter-sx.sx" web_dir;
(* Web modules for web/tests/ *)
load_module "forms.sx" web_dir;
load_module "engine.sx" web_dir;
load_module "page-helpers.sx" web_dir;
load_module "request-handler.sx" web_dir;
load_module "router.sx" web_dir;
load_module "deps.sx" web_dir;
load_module "orchestration.sx" web_dir;
(* Library modules for lib/tests/ *)
load_module "bytecode.sx" lib_dir;
load_module "compiler.sx" lib_dir;
load_module "vm.sx" lib_dir;
(* Rebind vm-execute-module and code-from-value to native OCaml implementations.
The SX versions from vm.sx run bytecode step-by-step in the interpreter —
far too slow for the test suite. Native versions use the compiled OCaml VM. *)
(* Wrap SX vm-execute-module to seed empty globals with primitives + env.
The SX VM resolves CALL_PRIM/GLOBAL_GET from globals — without seeding,
even (+ 1 2) fails. We keep the SX version (not native Sx_vm) so
suspension tests work (SX VM suspends via dict, native VM via exception). *)
let sx_vm_execute = try Some (Sx_types.env_get env "vm-execute-module") with _ -> None in
ignore (Sx_types.env_bind env "vm-execute-module" (NativeFn ("vm-execute-module", fun args ->
match args with
| [code; Dict globals] ->
if Hashtbl.length globals = 0 then begin
Hashtbl.iter (fun name fn ->
Hashtbl.replace globals name (NativeFn (name, fn))
) Sx_primitives.primitives;
let rec add_env e =
Hashtbl.iter (fun id v ->
let name = Sx_types.unintern id in
if not (Hashtbl.mem globals name) then
Hashtbl.replace globals name v) e.Sx_types.bindings;
match e.Sx_types.parent with Some p -> add_env p | None -> ()
in add_env env
end;
(* Use native VM for speed — much faster than SX step-by-step *)
let c = Sx_vm.code_from_value code in
(try Sx_vm.execute_module c globals
with Sx_vm.VmSuspended (_request, _saved_vm) ->
(* Fall back to SX version for suspension handling *)
Hashtbl.remove globals "__io_request";
match sx_vm_execute with
| Some fn -> Sx_ref.cek_call fn (List [code; Dict globals])
| None -> Nil)
| _ ->
match sx_vm_execute with
| Some fn -> Sx_ref.cek_call fn (List args)
| None -> Nil)));
load_module "signals.sx" spec_dir; (* core reactive primitives *)
load_module "signals.sx" web_dir; (* web extensions *)
load_module "freeze.sx" lib_dir;
load_module "content.sx" lib_dir;
load_module "parser-combinators.sx" lib_dir;
load_module "graphql.sx" lib_dir;
load_module "graphql-exec.sx" lib_dir;
(* DOM module — provides dom-* wrappers around host-* primitives *)
let web_lib_dir = Filename.concat web_dir "lib" in
load_module "dom.sx" web_lib_dir;
load_module "browser.sx" web_lib_dir;
let hs_dir = Filename.concat lib_dir "hyperscript" in
load_module "tokenizer.sx" hs_dir;
load_module "parser.sx" hs_dir;
load_module "compiler.sx" hs_dir;
load_module "runtime.sx" hs_dir;
load_module "integration.sx" hs_dir;
load_module "htmx.sx" hs_dir;
(* Override console-log to avoid str on circular mock DOM refs *)
ignore (Sx_types.env_bind env "console-log" (NativeFn ("console-log", fun _ -> Nil)));
ignore (Sx_types.env_bind env "console-debug" (NativeFn ("console-debug", fun _ -> Nil)));
ignore (Sx_types.env_bind env "console-error" (NativeFn ("console-error", fun _ -> Nil)));
(* eval-hs: compile hyperscript source to SX and evaluate it.
Used by eval-only behavioral tests (comparisonOperator, mathOperator, etc.) *)
ignore (Sx_types.env_bind env "eval-hs" (NativeFn ("eval-hs", fun args ->
match args with
| [String src] ->
(* Add "return" prefix if source doesn't start with a command keyword *)
let contains s sub = try ignore (String.index s sub.[0]); let rec check i j =
if j >= String.length sub then true
else if i >= String.length s then false
else if s.[i] = sub.[j] then check (i+1) (j+1)
else false in
let rec scan i = if i > String.length s - String.length sub then false
else if check i 0 then true else scan (i+1) in scan 0
with _ -> false in
let wrapped =
let has_cmd = (String.length src > 4 &&
(String.sub src 0 4 = "set " || String.sub src 0 4 = "put " ||
String.sub src 0 4 = "get ")) ||
contains src "return " || contains src "then " in
if has_cmd then src else "return " ^ src
in
let sx_expr = eval_expr (List [Symbol "hs-to-sx-from-source"; String wrapped]) (Env env) in
eval_expr (List [Symbol "eval-expr"; sx_expr; Env env]) (Env env)
| _ -> raise (Eval_error "eval-hs: expected string"))));
load_module "types.sx" lib_dir;
load_module "text-layout.sx" lib_dir;
load_module "sx-swap.sx" lib_dir;
(* Shared templates: TW styling engine *)
let templates_dir = Filename.concat project_dir "shared/sx/templates" in
load_module "tw.sx" templates_dir;
load_module "tw-layout.sx" templates_dir;
load_module "font.sx" templates_dir;
load_module "tw-type.sx" templates_dir;
(* SX docs site: components, handlers, demos *)
let sx_comp_dir = Filename.concat project_dir "sx/sxc" in
let sx_sx_dir = Filename.concat project_dir "sx/sx" in
let sx_handlers_dir = Filename.concat project_dir "sx/sx/handlers" in
let sx_islands_dir = Filename.concat project_dir "sx/sx/reactive-islands" in
let sx_geo_dir = Filename.concat project_dir "sx/sx/geography" in
(* Components + handlers *)
load_module "examples.sx" sx_comp_dir;
load_module "docs.sx" sx_sx_dir;
load_module "examples.sx" sx_handlers_dir;
load_module "ref-api.sx" sx_handlers_dir;
load_module "reactive-api.sx" sx_handlers_dir;
(* Server-rendered demos *)
load_module "scopes.sx" sx_sx_dir;
load_module "provide.sx" sx_sx_dir;
load_module "spreads.sx" sx_sx_dir;
(* Island definitions *)
load_module "index.sx" sx_islands_dir;
load_module "demo.sx" sx_islands_dir;
load_module "marshes.sx" sx_islands_dir;
load_module "cek.sx" sx_geo_dir;
load_module "reactive-runtime.sx" sx_sx_dir;
(* Create short-name aliases for reactive-islands tests *)
let alias short full =
try let v = Sx_types.env_get env full in
ignore (Sx_types.env_bind env short v)
with _ -> () in
alias "~reactive-islands/counter" "~reactive-islands/index/demo-counter";
alias "~reactive-islands/temperature" "~reactive-islands/index/demo-temperature";
alias "~reactive-islands/stopwatch" "~reactive-islands/index/demo-stopwatch";
alias "~reactive-islands/reactive-list" "~reactive-islands/index/demo-reactive-list";
alias "~reactive-islands/input-binding" "~reactive-islands/index/demo-input-binding";
alias "~reactive-islands/error-boundary" "~reactive-islands/index/demo-error-boundary";
alias "~reactive-islands/dynamic-class" "~reactive-islands/index/demo-dynamic-class";
alias "~reactive-islands/store-writer" "~reactive-islands/index/demo-store-writer";
alias "~reactive-islands/store-reader" "~reactive-islands/index/demo-store-reader";
alias "~marshes/demo-marsh-product" "~reactive-islands/marshes/demo-marsh-product";
alias "~marshes/demo-marsh-settle" "~reactive-islands/marshes/demo-marsh-settle";
(* Determine test files — scan spec/tests/, lib/tests/, web/tests/ *)
let lib_tests_dir = Filename.concat project_dir "lib/tests" in
let web_tests_dir = Filename.concat project_dir "web/tests" in
(* Pre-load test-handlers.sx so its mock definitions (reset-mocks!, helper, etc.)
are available to test-examples.sx which loads before it alphabetically *)
load_module "test-handlers.sx" web_tests_dir;
(* Re-bind render-to-sx AFTER adapter-sx.sx has loaded, wrapping the SX version.
The SX render-to-sx handles AST inputs; we add string→parse→aser support. *)
let sx_render_to_sx = try Some (Sx_types.env_get env "render-to-sx") with _ -> None in
ignore (Sx_types.env_bind env "render-to-sx" (NativeFn ("render-to-sx", fun args ->
match args with
| [String src] ->
(* String input: parse then evaluate via aser (quote the parsed AST so aser sees raw structure) *)
let exprs = Sx_parser.parse_all src in
let expr = match exprs with [e] -> e | es -> List (Symbol "do" :: es) in
let result = eval_expr (List [Symbol "aser"; List [Symbol "quote"; expr]; Env env]) (Env env) in
(match result with SxExpr s -> String s | String s -> String s | _ -> String (Sx_runtime.value_to_str result))
| _ ->
(* AST input: delegate to the SX render-to-sx *)
match sx_render_to_sx with
| Some (NativeFn (_, f)) -> f args
| Some (Lambda _ as fn) -> Sx_ref.cek_call fn (List args)
| _ -> String "")));
let files = if test_files = [] then begin
(* Spec tests (core language — always run) *)
let spec_entries = Sys.readdir spec_tests_dir in
Array.sort String.compare spec_entries;
let spec_files = Array.to_list spec_entries
|> List.filter (fun f ->
String.length f > 5 &&
String.sub f 0 5 = "test-" &&
Filename.check_suffix f ".sx" &&
f <> "test-framework.sx")
|> List.map (fun f -> Filename.concat spec_tests_dir f)
in
(* Web tests (orchestration, handlers) *)
let web_files = if Sys.file_exists web_tests_dir then begin
let entries = Sys.readdir web_tests_dir in
Array.sort String.compare entries;
Array.to_list entries
|> List.filter (fun f ->
String.length f > 5 &&
String.sub f 0 5 = "test-" &&
Filename.check_suffix f ".sx" &&
f <> "test-handlers.sx" && (* pre-loaded above *)
f <> "test-wasm-browser.sx" && (* browser-only, needs DOM primitives *)
f <> "test-adapter-dom.sx" && (* browser-only, needs DOM renderer *)
f <> "test-boot-helpers.sx" && (* browser-only, needs boot module *)
f <> "test-layout.sx" && (* needs render-to-html begin+defcomp support *)
f <> "test-cek-reactive.sx") (* needs test-env/make-reactive-reset-frame infra *)
|> List.map (fun f -> Filename.concat web_tests_dir f)
end else [] in
spec_files @ web_files
end else
(* Specific test files — search all test dirs *)
List.map (fun name ->
let name = if Filename.check_suffix name ".sx" then name else name ^ ".sx" in
let spec_path = Filename.concat spec_tests_dir name in
let lib_path = Filename.concat lib_tests_dir name in
let web_path = Filename.concat web_tests_dir name in
if Sys.file_exists spec_path then spec_path
else if Sys.file_exists lib_path then lib_path
else if Sys.file_exists web_path then web_path
else Filename.concat spec_tests_dir name (* will fail with "not found" *)
) test_files
in
(* Refresh JIT globals after all modules loaded — vm-execute-module,
code-from-value, and other late-bound functions must be visible. *)
!_jit_refresh_globals ();
List.iter (fun path ->
if Sys.file_exists path then begin
let name = Filename.basename path in
Printf.printf "\n%s\n" (String.make 60 '=');
Printf.printf "Running %s\n" name;
Printf.printf "%s\n%!" (String.make 60 '=');
(try
load_and_eval path
with
| Eval_error msg ->
incr fail_count;
Printf.printf " ERROR in %s: %s\n%!" name msg
| exn ->
incr fail_count;
Printf.printf " ERROR in %s: %s\n%!" name (Printexc.to_string exn))
end else
Printf.eprintf "Test file not found: %s\n" path
) files
(* ====================================================================== *)
(* Main *)
(* ====================================================================== *)
let () =
let args = Array.to_list Sys.argv |> List.tl in
let foundation_only = List.mem "--foundation" args in
let jit_enabled = List.mem "--jit" args in
let test_files = List.filter (fun a -> not (String.length a > 0 && a.[0] = '-')) args in
(* Always run foundation tests *)
run_foundation_tests ();
if not foundation_only then begin
Printf.printf "\n=== SX Spec Tests (CEK Evaluator + JIT) ===\n%!";
let env = make_test_env () in
(* Load compiler and enable JIT (opt-in via --jit flag) *)
if jit_enabled then begin
let globals = Hashtbl.create 512 in
let rec env_to_globals e =
Hashtbl.iter (fun id v ->
let name = Sx_types.unintern id in
if not (Hashtbl.mem globals name) then
Hashtbl.replace globals name v) e.Sx_types.bindings;
match e.Sx_types.parent with Some p -> env_to_globals p | None -> ()
in
env_to_globals env;
(* Seed VM globals with native primitives — CALL_PRIM resolves from globals *)
Hashtbl.iter (fun name fn ->
Hashtbl.replace globals name (NativeFn (name, fn))
) Sx_primitives.primitives;
_jit_refresh_globals := (fun () -> env_to_globals env);
(try
let compiler_path = if Sys.file_exists "lib/compiler.sx" then "lib/compiler.sx"
else "../../lib/compiler.sx" in
let ic = open_in compiler_path in
let src = really_input_string ic (in_channel_length ic) in
close_in ic; let _ = src in
let exprs = Sx_parser.parse_all src in
List.iter (fun e -> ignore (Sx_ref.eval_expr e (Env env))) exprs;
env_to_globals env;
Sx_runtime._jit_try_call_fn := Some (fun f args ->
match f with
| Lambda l ->
(match l.l_compiled with
| Some cl when not (Sx_vm.is_jit_failed cl) ->
(* VmSuspended = IO perform, Eval_error "VM undefined" = missing
special form. Both fall back to CEK safely — mark as failed
so we don't retry. *)
(try Some (Sx_vm.call_closure cl args cl.vm_env_ref)
with
| Sx_vm.VmSuspended _ ->
l.l_compiled <- Some Sx_vm.jit_failed_sentinel; None
| Eval_error msg when String.length msg > 14
&& String.sub msg 0 14 = "VM undefined: " ->
l.l_compiled <- Some Sx_vm.jit_failed_sentinel; None)
| Some _ -> None
| None ->
if l.l_name = None then None
else begin
l.l_compiled <- Some Sx_vm.jit_failed_sentinel;
(* Catch TIMEOUT during compile so the first test in a suite
doesn't time out just from JIT-compiling a large top-level
function. Sentinel is already set, so subsequent calls skip
JIT; this ensures the FIRST call falls back to CEK too. *)
match (try Sx_vm.jit_compile_lambda l globals
with Eval_error msg when
String.length msg >= 7
&& String.sub msg 0 7 = "TIMEOUT" -> None)
with
| Some cl -> l.l_compiled <- Some cl;
(try Some (Sx_vm.call_closure cl args cl.vm_env_ref)
with
| Sx_vm.VmSuspended _ ->
l.l_compiled <- Some Sx_vm.jit_failed_sentinel; None
| Eval_error msg when String.length msg > 14
&& String.sub msg 0 14 = "VM undefined: " ->
l.l_compiled <- Some Sx_vm.jit_failed_sentinel; None)
| None -> None
end)
| _ -> None);
Printf.printf "[jit] Compiler loaded, JIT enabled\n%!"
with e ->
Printf.printf "[jit] Compiler not loaded: %s\n%!" (Printexc.to_string e));
end;
Sx_runtime.jit_reset_counters ();
run_spec_tests env test_files
end;
(* JIT statistics *)
let jh = !(Sx_runtime._jit_hit) and jm = !(Sx_runtime._jit_miss) and js = !(Sx_runtime._jit_skip) in
let total = jh + jm + js in
if total > 0 then
Printf.printf "\n[jit] calls=%d hit=%d (%.1f%%) miss=%d skip=%d\n"
total jh (100.0 *. float_of_int jh /. float_of_int (max 1 total)) jm js;
(* Summary *)
Printf.printf "\n%s\n" (String.make 60 '=');
Printf.printf "Results: %d passed, %d failed\n" !pass_count !fail_count;
Printf.printf "%s\n" (String.make 60 '=');
if !fail_count > 0 then exit 1