Files
rose-ash/hosts/ocaml/bin/run_tests.ml
giles e3eb46d0dc HS tests: SIGALRM + raise timeout for native OCaml loops
The infinite loops in the HS parser are in transpiled native OCaml code,
not in the VM or CEK step loop. Neither step counters (in cek_step_loop,
cek_step, trampoline) nor VM instruction checks caught them because
the loops are in direct OCaml recursion.

Fix: SIGALRM handler raises Eval_error to break out of native loops.
Also sets step_limit flag to catch VM loops. Combined approach handles
both native OCaml recursion (alarm+raise) and VM bytecode (step check).

The alarm+raise can become unreliable after ~13 timeouts in a single
process, but handles the common case well. Reverts the fork-based
approach which lost inter-test state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 11:57:33 +00:00

2397 lines
104 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] -> r := !r @ [v]; ListRef r (* mutate in place *)
| [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 op = 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
let key = Sx_ref.library_name_key lib_spec in
if Sx_types.sx_truthy (Sx_ref.library_loaded_p key) 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
| _ -> Nil (* Other IO ops return nil in test context *)
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 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" -> 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);
reg "host-set!" (fun args ->
match args with
| [Nil; _; _] -> Nil
| [Dict d; String key; value] ->
Hashtbl.replace d key value;
(* Side effects for special keys *)
(if key = "className" then
match Hashtbl.find_opt d "classList" with
| Some (Dict cl) ->
(* classList sub-dict doesn't store classes — they live in className *)
ignore cl
| _ -> ());
value
| _ -> 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 = "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
(* 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);
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" ->
(* Simplified: just append text to innerHTML *)
(match rest with
| [String _pos; String html] ->
let cur = match Hashtbl.find_opt d "innerHTML" with Some (String s) -> s | _ -> "" in
Hashtbl.replace d "innerHTML" (String (cur ^ html)); 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)
| _ -> 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);
(* 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;
(* 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;
match Sx_vm.jit_compile_lambda l globals 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