(** 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 []
(* Test filter: when Some, only run tests (suite, name) in the set.
Populated by --only-failing=FILE from lines like "FAIL: suite > name: error". *)
let suite_filter : (string * string, unit) Hashtbl.t option ref = ref None
(* ====================================================================== *)
(* Deep equality — SX structural comparison *)
(* ====================================================================== *)
let rec deep_equal a b =
match a, b with
| Nil, Nil -> true
| Bool a, Bool b -> a = b
| Integer a, Integer b -> a = b
| Number a, Number b -> a = b
| Integer a, Number b -> float_of_int a = b
| Number a, Integer b -> a = float_of_int 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
(* ====================================================================== *)
(* Test extensions for the VM extension registry suite (Phase B) *)
(* ====================================================================== *)
(* Extend the extensible variant from sx_vm_extension.ml so the test
extensions below can carry their own private state. *)
type Sx_vm_extension.extension_state += TestRegState of int ref
(* ====================================================================== *)
(* 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);
bind "test-allowed?" (fun args ->
match !suite_filter with
| None -> Bool true
| Some filter ->
let name = match args with
| [String s] -> s
| [v] -> Sx_types.value_to_string v
| _ -> "" in
let suite = match !suite_stack with [] -> "" | s :: _ -> s in
Bool (Hashtbl.mem filter (suite, name)));
(* --- 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 _ | Integer _) :: 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 _ | Integer _) :: 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
| Integer x, Integer y -> x = y
| Number x, Number y -> x = y
| Integer x, Number y -> float_of_int x = y
| Number x, Integer y -> x = float_of_int y
| String x, String y -> x = y
| Bool x, Bool y -> x = y
| Nil, Nil -> true
| _ -> a == b)
| _ -> raise (Eval_error "identical?: expected 2 args"));
(* --- Continuation support --- *)
bind "make-continuation" (fun args ->
match args with
| [f] ->
let k v = sx_call f [v] in
Continuation (k, None)
| _ -> raise (Eval_error "make-continuation: expected 1 arg"));
bind "continuation?" (fun args ->
match args with
| [Continuation _] -> Bool true
| [_] -> Bool false
| _ -> raise (Eval_error "continuation?: expected 1 arg"));
bind "continuation-fn" (fun args ->
match args with
| [Continuation (f, _)] -> NativeFn ("continuation-fn-result", fun args ->
match args with [v] -> f v | _ -> f Nil)
| _ -> raise (Eval_error "continuation-fn: expected continuation"));
(* --- Core builtins used by test framework / test code --- *)
bind "assert" (fun args ->
match args with
| [cond] ->
if not (sx_truthy cond) then raise (Eval_error "Assertion failed");
Bool true
| [cond; String msg] ->
if not (sx_truthy cond) then raise (Eval_error ("Assertion error: " ^ msg));
Bool true
| [cond; msg] ->
if not (sx_truthy cond) then
raise (Eval_error ("Assertion error: " ^ Sx_types.value_to_string msg));
Bool true
| _ -> raise (Eval_error "assert: expected 1-2 args"));
bind "append!" (fun args ->
match args with
| [ListRef r; v; (Number n)] when int_of_float n = 0 ->
r := v :: !r; ListRef r (* prepend *)
| [ListRef r; v; (Integer 0)] ->
r := v :: !r; ListRef r (* prepend Integer index *)
| [ListRef r; v] -> r := !r @ [v]; ListRef r (* append in place *)
| [List items; v; (Number n)] when int_of_float n = 0 ->
List (v :: items) (* immutable prepend *)
| [List items; v; (Integer 0)] ->
List (v :: items) (* immutable prepend Integer index *)
| [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 "without-io-hook" (fun args ->
match args with
| [thunk] ->
let saved_hook = !Sx_types._cek_io_suspend_hook in
let saved_resolver = !Sx_types._cek_io_resolver in
Sx_types._cek_io_suspend_hook := None;
Sx_types._cek_io_resolver := None;
(try
let r = Sx_ref.cek_call thunk Nil in
Sx_types._cek_io_suspend_hook := saved_hook;
Sx_types._cek_io_resolver := saved_resolver;
r
with e ->
Sx_types._cek_io_suspend_hook := saved_hook;
Sx_types._cek_io_resolver := saved_resolver;
raise e)
| _ -> 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
| [Integer lo; _] -> Integer lo
| _ -> Integer 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 _] | [VmClosure _] -> 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);
bind "values" (fun args ->
match args with
| [v] -> v
| vs ->
let d = Hashtbl.create 2 in
Hashtbl.replace d "_values" (Bool true);
Hashtbl.replace d "_list" (List vs);
Dict d);
bind "call-with-values" (fun args ->
match args with
| [producer; consumer] ->
let result = Sx_ref.cek_call producer (List []) in
let spread = (match result with
| Dict d when (match Hashtbl.find_opt d "_values" with Some (Bool true) -> true | _ -> false) ->
(match Hashtbl.find_opt d "_list" with Some (List l) -> l | _ -> [result])
| _ -> [result])
in
Sx_ref.cek_call consumer (List spread)
| _ -> raise (Eval_error "call-with-values: expected 2 args"));
bind "promise?" (fun args ->
match args with
| [v] -> Bool (Sx_ref.is_promise v)
| _ -> Bool false);
bind "make-promise" (fun args ->
match args with
| [v] ->
let d = Hashtbl.create 4 in
Hashtbl.replace d "_promise" (Bool true);
Hashtbl.replace d "forced" (Bool true);
Hashtbl.replace d "value" v;
Dict d
| _ -> Nil);
bind "force" (fun args ->
match args with
| [p] -> Sx_ref.force_promise p
| _ -> 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" (Integer 42) (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 "+"; Integer 1; Integer 2]) (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 [Integer 1; Integer 2; Integer 3]] ->
incr pass_count; Printf.printf " PASS: quote sugar\n"
| 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" (Integer 42) (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; l_call_count = 0; l_uid = Sx_types.next_lambda_uid () } 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));
Printf.printf "\nSuite: crypto-sha2\n";
(* NIST FIPS 180-4 published vectors. *)
assert_eq "sha256 empty"
(String "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
(call "crypto-sha256" [String ""]);
assert_eq "sha256 abc"
(String "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad")
(call "crypto-sha256" [String "abc"]);
assert_eq "sha256 896-bit"
(String "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1")
(call "crypto-sha256"
[String "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"]);
assert_eq "sha256 1M 'a'"
(String "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0")
(call "crypto-sha256" [String (String.make 1000000 'a')]);
assert_eq "sha512 empty"
(String "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e")
(call "crypto-sha512" [String ""]);
assert_eq "sha512 abc"
(String "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f")
(call "crypto-sha512" [String "abc"]);
assert_eq "sha512 896-bit"
(String "8e959b75dae313da8cf4f72814fc143f8f7779c6eb9f7fa17299aeadb6889018501d289e4900f7e4331b99dec4b5433ac7d329eeb6dd26545e96e55b874be909")
(call "crypto-sha512"
[String ("abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmn"
^ "hijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu")]);
Printf.printf "\nSuite: crypto-sha3\n";
(* NIST FIPS 202 published vectors. *)
assert_eq "sha3-256 empty"
(String "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a")
(call "crypto-sha3-256" [String ""]);
assert_eq "sha3-256 abc"
(String "3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532")
(call "crypto-sha3-256" [String "abc"]);
assert_eq "sha3-256 896-bit"
(String "41c0dba2a9d6240849100376a8235e2c82e1b9998a999e21db32dd97496d3376")
(call "crypto-sha3-256"
[String "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"]);
(* 1600-bit message: 0xa3 * 200 — exercises multi-block absorb (>136B). *)
assert_eq "sha3-256 1600-bit 0xa3"
(String "79f38adec5c20307a98ef76e8324afbfd46cfd81b22e3973c65fa1bd9de31787")
(call "crypto-sha3-256" [String (String.make 200 '\xa3')]);
Printf.printf "\nSuite: dag-cbor\n";
let mkdict pairs =
let d = Sx_types.make_dict () in
List.iter (fun (k, v) -> Hashtbl.replace d k v) pairs;
Dict d
in
let enc v = call "cbor-encode" [v] in
(* RFC 8949 Appendix A — minimal-length deterministic encoding. *)
assert_eq "cbor 0" (String "\x00") (enc (Integer 0));
assert_eq "cbor 23" (String "\x17") (enc (Integer 23));
assert_eq "cbor 24" (String "\x18\x18") (enc (Integer 24));
assert_eq "cbor 100" (String "\x18\x64") (enc (Integer 100));
assert_eq "cbor 1000" (String "\x19\x03\xe8") (enc (Integer 1000));
assert_eq "cbor 1000000"
(String "\x1a\x00\x0f\x42\x40") (enc (Integer 1000000));
assert_eq "cbor -1" (String "\x20") (enc (Integer (-1)));
assert_eq "cbor -100" (String "\x38\x63") (enc (Integer (-100)));
assert_eq "cbor -1000" (String "\x39\x03\xe7") (enc (Integer (-1000)));
assert_eq "cbor false" (String "\xf4") (enc (Bool false));
assert_eq "cbor true" (String "\xf5") (enc (Bool true));
assert_eq "cbor null" (String "\xf6") (enc Nil);
assert_eq "cbor \"\"" (String "\x60") (enc (String ""));
assert_eq "cbor \"a\"" (String "\x61\x61") (enc (String "a"));
assert_eq "cbor \"IETF\"" (String "\x64IETF") (enc (String "IETF"));
assert_eq "cbor []" (String "\x80") (enc (List []));
assert_eq "cbor [1,2,3]"
(String "\x83\x01\x02\x03")
(enc (List [Integer 1; Integer 2; Integer 3]));
assert_eq "cbor [1,[2,3],[4,5]]"
(String "\x83\x01\x82\x02\x03\x82\x04\x05")
(enc (List [Integer 1;
List [Integer 2; Integer 3];
List [Integer 4; Integer 5]]));
assert_eq "cbor {}" (String "\xa0") (enc (mkdict []));
assert_eq "cbor {a:1,b:[2,3]}"
(String "\xa2\x61\x61\x01\x61\x62\x82\x02\x03")
(enc (mkdict ["a", Integer 1; "b", List [Integer 2; Integer 3]]));
assert_eq "cbor {a..e:A..E}"
(String "\xa5\x61\x61\x61\x41\x61\x62\x61\x42\x61\x63\x61\x43\x61\x64\x61\x44\x61\x65\x61\x45")
(enc (mkdict ["a", String "A"; "b", String "B"; "c", String "C";
"d", String "D"; "e", String "E"]));
(* Determinism: insertion order + key length must not change bytes.
Sort is length-then-bytewise → a, c, bb. *)
let d1 = mkdict ["bb", Integer 2; "a", Integer 1; "c", Integer 3] in
let d2 = mkdict ["c", Integer 3; "bb", Integer 2; "a", Integer 1] in
assert_eq "cbor det order-invariant" (enc d1) (enc d2);
assert_eq "cbor det length-then-bytewise"
(String "\xa3\x61\x61\x01\x61\x63\x03\x62\x62\x62\x02")
(enc d1);
(* Round-trip: decode . encode = identity (structural). *)
let roundtrip name v =
assert_eq ("cbor rt " ^ name) v (call "cbor-decode" [enc v])
in
roundtrip "int" (Integer 42);
roundtrip "neg" (Integer (-99999));
roundtrip "str" (String "hello world");
roundtrip "bool" (Bool true);
roundtrip "nil" Nil;
roundtrip "nested"
(List [Integer 1; String "x"; List [Bool false; Nil]]);
roundtrip "dict"
(mkdict ["k", List [Integer 7]; "name", String "z"]);
Printf.printf "\nSuite: cid\n";
let mh_sha256 s = Sx_cid.multihash 0x12 (Sx_cid.unhex (Sx_sha2.sha256_hex s)) in
(* Authoritative vectors (independently derived; match well-known
IPFS CIDs). raw "abc" and raw "" — codec 0x55. *)
assert_eq "cid raw abc"
(String "bafkreif2pall7dybz7vecqka3zo24irdwabwdi4wc55jznaq75q7eaavvu")
(call "cid-from-bytes" [Integer 0x55; String (mh_sha256 "abc")]);
assert_eq "cid raw empty"
(String "bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku")
(call "cid-from-bytes" [Integer 0x55; String (mh_sha256 "")]);
(* dag-cbor {} — canonical empty-map CID (sha2-256, codec 0x71). *)
assert_eq "cid dag-cbor {}"
(String "bafyreigbtj4x7ip5legnfznufuopl4sg4knzc2cof6duas4b3q2fy6swua")
(call "cid-from-sx" [mkdict []]);
(* Determinism: dict key insertion order must not change the CID. *)
let cda = call "cid-from-sx" [mkdict ["b", Integer 2; "a", Integer 1]] in
let cdb = call "cid-from-sx" [mkdict ["a", Integer 1; "b", Integer 2]] in
assert_eq "cid det order-invariant" cda cdb;
assert_true "cid multibase 'b' prefix"
(Bool (match call "cid-from-sx" [mkdict []] with
| String s -> String.length s > 1 && s.[0] = 'b'
| _ -> false));
Printf.printf "\nSuite: ed25519\n";
let hx = Sx_ed25519.unhex in
let edv pk msg sg = call "ed25519-verify"
[String (hx pk); String (hx msg); String (hx sg)] in
(* RFC 8032 §7.1 TEST 1-3 (deterministic; re-derived independently). *)
assert_eq "ed25519 RFC T1"
(Bool true)
(edv "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a"
""
"e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b");
assert_eq "ed25519 RFC T2"
(Bool true)
(edv "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c"
"72"
"92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00");
assert_eq "ed25519 RFC T3"
(Bool true)
(edv "fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025"
"af82"
"6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a");
(* Tampered message -> false. *)
assert_eq "ed25519 tampered msg"
(Bool false)
(edv "fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025"
"af83"
"6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a");
(* Tampered signature -> false. *)
assert_eq "ed25519 tampered sig"
(Bool false)
(edv "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a"
""
"f5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b");
(* Total: wrong-length pubkey / sig -> false, no exception. *)
assert_eq "ed25519 short pubkey"
(Bool false)
(call "ed25519-verify" [String "abc"; String ""; String (String.make 64 '\000')]);
assert_eq "ed25519 short sig"
(Bool false)
(call "ed25519-verify"
[String (hx "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a");
String ""; String "short"]);
assert_eq "ed25519 non-string args"
(Bool false)
(call "ed25519-verify" [Integer 1; Integer 2; Integer 3]);
Printf.printf "\nSuite: rsa-sha256\n";
(* Fixed RSA-2048 vector: one-off python-cryptography keygen +
PKCS1v15/SHA-256 sign of "fed-sx phase F rsa test". *)
let rhx = Sx_rsa.unhex in
let spki = rhx "30820122300d06092a864886f70d01010105000382010f003082010a0282010100a117b573480bce5a08b54a98384001df26d062e9173caaee2e3a2d0045c6d16f99b2a1e7fb60763f65f95f8c39ff82c18b8590338042914331db3440a06d2dbe65a2f82c82f37d293f67a8b57a1f9014b55150a093cfee90257ef3b4a215d5ab002579bd92b6fcb3536777d51b639347d01e307ddafb209073dd9b8d6a507157c44c624a19b3b9275931472462870ae02132630159132a85c1c889adfb358b6bbd3760ce3fffe6285964833a10ee436d5bc33dfab7f9ed630a74e9a32e5688f5a7797f7cc839ad2494dd1c4c4a8fab844cd26208794bf2602c16b9d12bde434066d8c0dd2d20489f4070f883bae2b4508ead4a1b80b44c576e9e37bdb5df69f10203010001" in
let rmsg = rhx "6665642d73782070686173652046207273612074657374" in
let rsig = rhx "5e1593d674ed15c0172546d38efdf1aebd252f4b0c0dfbe1f7996fd569d0bfd9f3e8689ea2b14aa45b5fc3f0a05d4f23c6b02b8820d71f6998ea3b5b0d071bb33142236e388b1226ece3ec447d33b38999f189c37564cf052cf038de94c67b2ddf9a97d5a73554bb88818f615824517209a4083258965adace55658f344104eaa0d5f2f44ea00cfac8754674aade87b40d955cccd1ccd9b7649a08b66ce3bc5dba2de96b3e859488ded3ef9fb3744a1e3495fd14841d8319b3cc08054c729d1c02739ee314eba2b20fac46e463f47eb67183d8455583eca73ba37448164612dd9cd77877135d30d12084c2843f986a5b8ad59c6600f9855b91d7cbdf7c6c4b0e" in
let rsav s m g = call "rsa-sha256-verify" [String s; String m; String g] in
assert_eq "rsa valid" (Bool true) (rsav spki rmsg rsig);
assert_eq "rsa tampered msg" (Bool false)
(rsav spki (rmsg ^ "x") rsig);
assert_eq "rsa tampered sig" (Bool false)
(rsav spki rmsg
(rhx "5f1593d674ed15c0172546d38efdf1aebd252f4b0c0dfbe1f7996fd569d0bfd9f3e8689ea2b14aa45b5fc3f0a05d4f23c6b02b8820d71f6998ea3b5b0d071bb33142236e388b1226ece3ec447d33b38999f189c37564cf052cf038de94c67b2ddf9a97d5a73554bb88818f615824517209a4083258965adace55658f344104eaa0d5f2f44ea00cfac8754674aade87b40d955cccd1ccd9b7649a08b66ce3bc5dba2de96b3e859488ded3ef9fb3744a1e3495fd14841d8319b3cc08054c729d1c02739ee314eba2b20fac46e463f47eb67183d8455583eca73ba37448164612dd9cd77877135d30d12084c2843f986a5b8ad59c6600f9855b91d7cbdf7c6c4b0e"));
assert_eq "rsa garbage spki" (Bool false)
(rsav "not der" rmsg rsig);
assert_eq "rsa non-string args" (Bool false)
(call "rsa-sha256-verify" [Integer 1; Integer 2; Integer 3]);
Printf.printf "\nSuite: file-list-dir\n";
let expect_err nm f =
(try ignore (f ());
incr fail_count; Printf.printf " FAIL: %s — no error\n" nm
with Eval_error _ ->
incr pass_count; Printf.printf " PASS: %s\n" nm
| _ ->
incr fail_count; Printf.printf " FAIL: %s — wrong exn\n" nm)
in
let tmp = Filename.temp_file "fld" "" in
Sys.remove tmp; Unix.mkdir tmp 0o755;
let touch n = let oc = open_out (Filename.concat tmp n) in close_out oc in
touch "b.txt"; touch "a.txt"; touch "c.txt";
assert_eq "file-list-dir sorted"
(List [String "a.txt"; String "b.txt"; String "c.txt"])
(call "file-list-dir" [String tmp]);
expect_err "file-list-dir missing"
(fun () -> call "file-list-dir" [String (Filename.concat tmp "nope")]);
expect_err "file-list-dir not-a-dir"
(fun () -> call "file-list-dir" [String (Filename.concat tmp "a.txt")]);
expect_err "file-list-dir arity"
(fun () -> call "file-list-dir" []);
(* best-effort cleanup *)
(try List.iter (fun n -> Sys.remove (Filename.concat tmp n))
["a.txt"; "b.txt"; "c.txt"]; Unix.rmdir tmp
with _ -> ());
Printf.printf "\nSuite: vm-extension-dispatch\n";
let make_bc op = ({
vc_arity = 0; vc_rest_arity = -1; vc_locals = 0;
vc_bytecode = [| op |]; vc_constants = [||];
vc_bytecode_list = None; vc_constants_list = None;
} : Sx_types.vm_code) in
let expect_invalid_opcode label op =
let globals = Hashtbl.create 1 in
try
let _ = Sx_vm.execute_module (make_bc op) globals in
incr fail_count;
Printf.printf " FAIL: %s — expected Invalid_opcode, got a result\n" label
with
| Sx_vm.Invalid_opcode n when n = op ->
incr pass_count;
Printf.printf " PASS: %s\n" label
| exn ->
incr fail_count;
Printf.printf " FAIL: %s — unexpected: %s\n" label (Printexc.to_string exn)
in
expect_invalid_opcode "opcode 200 raises Invalid_opcode 200" 200;
expect_invalid_opcode "opcode 224 raises Invalid_opcode 224" 224;
expect_invalid_opcode "opcode 247 raises Invalid_opcode 247" 247;
(* Opcode 199 sits just below the extension threshold — should fall to the
catch-all (Eval_error), proving the threshold is at 200, not 199. *)
let globals = Hashtbl.create 1 in
(try
let _ = Sx_vm.execute_module (make_bc 199) globals in
incr fail_count;
Printf.printf " FAIL: opcode 199 — expected Eval_error, got a result\n"
with
| Sx_vm.Invalid_opcode _ ->
incr fail_count;
Printf.printf " FAIL: opcode 199 routed to extension dispatch (threshold wrong)\n"
| Sx_types.Eval_error _ ->
incr pass_count;
Printf.printf " PASS: opcode 199 stays in core (catch-all)\n"
| exn ->
incr fail_count;
Printf.printf " FAIL: opcode 199 — unexpected: %s\n" (Printexc.to_string exn));
Printf.printf "\nSuite: vm-extension-registry\n";
(* Sx_vm_extensions self-installs its dispatcher at module init. Reset
the registry so prior loaded extensions don't interfere with this
test. *)
Sx_vm_extensions._reset_for_tests ();
let module TestExt : Sx_vm_extension.EXTENSION = struct
let name = "test_reg"
let init () = TestRegState (ref 0)
let opcodes _st = [
(210, "test_reg.OP_PUSH_42", (fun vm _frame ->
Sx_vm.push vm (Sx_types.Integer 42)));
(211, "test_reg.OP_DOUBLE_TOS", (fun vm _frame ->
let v = Sx_vm.pop vm in
match v with
| Sx_types.Integer n -> Sx_vm.push vm (Sx_types.Integer (n * 2))
| _ -> failwith "OP_DOUBLE_TOS: not an integer"));
]
end in
Sx_vm_extensions.register (module TestExt);
(match Sx_vm_extensions.id_of_name "test_reg.OP_PUSH_42" with
| Some 210 ->
incr pass_count;
Printf.printf " PASS: id_of_name resolves opcode\n"
| other ->
incr fail_count;
Printf.printf " FAIL: id_of_name: got %s\n"
(match other with Some n -> string_of_int n | None -> "None"));
(match Sx_vm_extensions.id_of_name "nonexistent.OP" with
| None ->
incr pass_count;
Printf.printf " PASS: id_of_name returns None for unknown\n"
| Some _ ->
incr fail_count;
Printf.printf " FAIL: id_of_name should return None for unknown\n");
(match Sx_vm_extensions.state_of_extension "test_reg" with
| Some (TestRegState _) ->
incr pass_count;
Printf.printf " PASS: state_of_extension returns extension state\n"
| _ ->
incr fail_count;
Printf.printf " FAIL: state_of_extension lookup\n");
(match Sx_vm_extensions.state_of_extension "nonexistent" with
| None ->
incr pass_count;
Printf.printf " PASS: state_of_extension None for unknown\n"
| Some _ ->
incr fail_count;
Printf.printf " FAIL: state_of_extension should be None\n");
(* End-to-end dispatch through the VM. Bytecode runs OP_PUSH_42 then
OP_RETURN (50); execute_module pops the result. *)
let make_bc_seq bytes = ({
vc_arity = 0; vc_rest_arity = -1; vc_locals = 0;
vc_bytecode = bytes; vc_constants = [||];
vc_bytecode_list = None; vc_constants_list = None;
} : Sx_types.vm_code) in
(let globals = Hashtbl.create 1 in
try
match Sx_vm.execute_module (make_bc_seq [| 210; 50 |]) globals with
| Integer 42 ->
incr pass_count;
Printf.printf " PASS: dispatch routes opcode 210 -> push 42\n"
| other ->
incr fail_count;
Printf.printf " FAIL: dispatch opcode 210: got %s\n"
(Sx_types.inspect other)
with exn ->
incr fail_count;
Printf.printf " FAIL: dispatch opcode 210 raised: %s\n"
(Printexc.to_string exn));
(* Compose two extension opcodes: PUSH_42 then DOUBLE_TOS then RETURN.
Verifies that successive extension dispatches share VM state. *)
(let globals = Hashtbl.create 1 in
try
match Sx_vm.execute_module (make_bc_seq [| 210; 211; 50 |]) globals with
| Integer 84 ->
incr pass_count;
Printf.printf " PASS: extension opcodes compose (42 -> 84)\n"
| other ->
incr fail_count;
Printf.printf " FAIL: composed opcodes: got %s\n"
(Sx_types.inspect other)
with exn ->
incr fail_count;
Printf.printf " FAIL: composed opcodes raised: %s\n"
(Printexc.to_string exn));
(* Duplicate opcode-id detection. *)
let module DupExt : Sx_vm_extension.EXTENSION = struct
let name = "dup_check"
let init () = TestRegState (ref 0)
let opcodes _st = [
(210, "dup_check.OP_X", (fun _vm _frame -> ()));
]
end in
(try
Sx_vm_extensions.register (module DupExt);
incr fail_count;
Printf.printf " FAIL: duplicate opcode id should have raised\n"
with Failure _ ->
incr pass_count;
Printf.printf " PASS: duplicate opcode id rejected\n");
(* Out-of-range opcode-id detection. *)
let module OutExt : Sx_vm_extension.EXTENSION = struct
let name = "out_of_range"
let init () = TestRegState (ref 0)
let opcodes _st = [
(300, "out_of_range.OP_X", (fun _vm _frame -> ()));
]
end in
(try
Sx_vm_extensions.register (module OutExt);
incr fail_count;
Printf.printf " FAIL: out-of-range opcode should have raised\n"
with Failure _ ->
incr pass_count;
Printf.printf " PASS: out-of-range opcode rejected\n");
(* Duplicate extension-name detection. *)
let module SameNameExt : Sx_vm_extension.EXTENSION = struct
let name = "test_reg" (* same as TestExt above *)
let init () = TestRegState (ref 0)
let opcodes _st = []
end in
(try
Sx_vm_extensions.register (module SameNameExt);
incr fail_count;
Printf.printf " FAIL: duplicate extension name should have raised\n"
with Failure _ ->
incr pass_count;
Printf.printf " PASS: duplicate extension name rejected\n");
Printf.printf "\nSuite: extension-opcode-id primitive\n";
let prim = Hashtbl.find Sx_primitives.primitives "extension-opcode-id" in
(* Known opcode (registered by TestExt above). *)
(match prim [String "test_reg.OP_PUSH_42"] with
| Integer 210 ->
incr pass_count;
Printf.printf " PASS: primitive returns Integer for registered opcode\n"
| other ->
incr fail_count;
Printf.printf " FAIL: registered opcode lookup: got %s\n"
(Sx_types.inspect other));
(* Unknown opcode → Nil. *)
(match prim [String "nonexistent.OP_X"] with
| Nil ->
incr pass_count;
Printf.printf " PASS: primitive returns nil for unknown opcode\n"
| other ->
incr fail_count;
Printf.printf " FAIL: unknown opcode lookup: got %s\n"
(Sx_types.inspect other));
(* Symbol arg also accepted (compilers may pass quoted symbols). *)
(match prim [Symbol "test_reg.OP_DOUBLE_TOS"] with
| Integer 211 ->
incr pass_count;
Printf.printf " PASS: primitive accepts Symbol args\n"
| other ->
incr fail_count;
Printf.printf " FAIL: symbol arg: got %s\n" (Sx_types.inspect other));
(* Wrong arity / type raises Eval_error. *)
(try
let _ = prim [] in
incr fail_count;
Printf.printf " FAIL: zero args should have raised\n"
with Sx_types.Eval_error _ ->
incr pass_count;
Printf.printf " PASS: zero args rejected\n");
(try
let _ = prim [Integer 42] in
incr fail_count;
Printf.printf " FAIL: integer arg should have raised\n"
with Sx_types.Eval_error _ ->
incr pass_count;
Printf.printf " PASS: integer arg rejected\n");
Printf.printf "\nSuite: extensions/test_ext (canonical extension)\n";
(* Phase D: the real test extension lives at lib/extensions/test_ext.ml.
Register it on top of the inline test_reg from earlier suites — the
two use disjoint opcode IDs (210/211 vs 220/221) so they coexist. *)
Test_ext.register ();
(* Lookup via the public primitive should now find OP_TEST_PUSH_42. *)
(match prim [String "test_ext.OP_TEST_PUSH_42"] with
| Integer 220 ->
incr pass_count;
Printf.printf " PASS: extension-opcode-id finds test_ext.OP_TEST_PUSH_42\n"
| other ->
incr fail_count;
Printf.printf " FAIL: opcode lookup: got %s\n" (Sx_types.inspect other));
(* End-to-end: PUSH_42 + DOUBLE_TOS + RETURN. *)
(let globals = Hashtbl.create 1 in
try
match Sx_vm.execute_module (make_bc_seq [| 220; 221; 50 |]) globals with
| Integer 84 ->
incr pass_count;
Printf.printf " PASS: extensions/test_ext bytecode executes (84)\n"
| other ->
incr fail_count;
Printf.printf " FAIL: test_ext bytecode result: got %s\n"
(Sx_types.inspect other)
with exn ->
incr fail_count;
Printf.printf " FAIL: test_ext bytecode raised: %s\n"
(Printexc.to_string exn));
(* Disassembly: opcode_name should resolve 220/221 via the registry,
not fall back to UNKNOWN_220 / UNKNOWN_221. disassemble returns a
Dict; the instruction list lives at key "bytecode". *)
(let code = make_bc_seq [| 220; 221; 50 |] in
let dis = Sx_vm.disassemble code in
let entries = match dis with
| Dict d -> (match Hashtbl.find_opt d "bytecode" with
| Some (List es) -> es
| _ -> [])
| _ -> []
in
let names = List.filter_map (fun entry -> match entry with
| Dict d ->
(match Hashtbl.find_opt d "opcode" with
| Some (String name) -> Some name
| _ -> None)
| _ -> None) entries
in
let has name = List.mem name names in
if has "test_ext.OP_TEST_PUSH_42" && has "test_ext.OP_TEST_DOUBLE_TOS" then begin
incr pass_count;
Printf.printf " PASS: disassemble shows extension opcode names\n"
end else begin
incr fail_count;
Printf.printf " FAIL: disassemble names: [%s]\n" (String.concat ", " names)
end);
(* Sanity: opcode_name on an unregistered extension opcode still
returns UNKNOWN_n. Pick 230 — out of test_ext's range. *)
(match Sx_vm.opcode_name 230 with
| "UNKNOWN_230" ->
incr pass_count;
Printf.printf " PASS: unregistered ext opcode falls back to UNKNOWN_n\n"
| other ->
incr fail_count;
Printf.printf " FAIL: opcode_name 230: got %s\n" other);
(* Per-extension state: invocation_count should reflect the two opcodes
that ran in the dispatch test above. *)
(match Test_ext.invocation_count () with
| Some n when n >= 2 ->
incr pass_count;
Printf.printf " PASS: extension state recorded %d invocations\n" n
| other ->
incr fail_count;
Printf.printf " FAIL: invocation_count: %s\n"
(match other with Some n -> string_of_int n | None -> "None"));
Printf.printf "\nSuite: extensions/erlang_ext (Phase 9h)\n";
(* Register the Erlang opcode namespace. Disjoint id range (200-217)
from test_ext (220/221) so they coexist. *)
Erlang_ext.register ();
(match prim [String "erlang.OP_PATTERN_TUPLE"] with
| Integer 222 ->
incr pass_count;
Printf.printf " PASS: extension-opcode-id erlang.OP_PATTERN_TUPLE = 222\n"
| other ->
incr fail_count;
Printf.printf " FAIL: erlang.OP_PATTERN_TUPLE: got %s\n"
(Sx_types.inspect other));
(match prim [String "erlang.OP_BIF_IS_TUPLE"] with
| Integer 239 ->
incr pass_count;
Printf.printf " PASS: extension-opcode-id erlang.OP_BIF_IS_TUPLE = 239\n"
| other ->
incr fail_count;
Printf.printf " FAIL: erlang.OP_BIF_IS_TUPLE: got %s\n"
(Sx_types.inspect other));
(match prim [String "erlang.OP_NONEXISTENT"] with
| Nil ->
incr pass_count;
Printf.printf " PASS: unknown erlang opcode -> nil\n"
| other ->
incr fail_count;
Printf.printf " FAIL: unknown erlang opcode: got %s\n"
(Sx_types.inspect other));
(* Phase 10b vertical slice: erlang.OP_BIF_LENGTH (230) is a REAL
handler. Build [CONST 0; OP_BIF_LENGTH; RETURN] with an Erlang
list [1,2,3] in the constant pool; expect Integer 3. Proves the
full path: bytecode -> Sx_vm extension fallthrough -> erlang_ext
handler -> correct stack result. *)
(let mk_dict kvs =
let h = Hashtbl.create 4 in
List.iter (fun (k, v) -> Hashtbl.replace h k v) kvs;
Sx_types.Dict h in
let er_nil = mk_dict [("tag", Sx_types.String "nil")] in
let er_cons hd tl =
mk_dict [("tag", Sx_types.String "cons");
("head", hd); ("tail", tl)] in
let lst = er_cons (Sx_types.Integer 1)
(er_cons (Sx_types.Integer 2)
(er_cons (Sx_types.Integer 3) er_nil)) in
let code = ({
vc_arity = 0; vc_rest_arity = -1; vc_locals = 0;
vc_bytecode = [| 1; 0; 0; 230; 50 |];
vc_constants = [| lst |];
vc_bytecode_list = None; vc_constants_list = None;
} : Sx_types.vm_code) in
let globals = Hashtbl.create 1 in
try
match Sx_vm.execute_module code globals with
| Integer 3 ->
incr pass_count;
Printf.printf " PASS: erlang.OP_BIF_LENGTH [1,2,3] -> 3 (real handler, end-to-end)\n"
| other ->
incr fail_count;
Printf.printf " FAIL: OP_BIF_LENGTH result: got %s\n"
(Sx_types.inspect other)
with exn ->
incr fail_count;
Printf.printf " FAIL: OP_BIF_LENGTH raised: %s\n"
(Printexc.to_string exn));
(* More real handlers (Phase 10b batch): build a list/tuple constant
and exercise HD/TL/TUPLE_SIZE/IS_* end-to-end through the VM. *)
(let mk_dict kvs =
let h = Hashtbl.create 4 in
List.iter (fun (k, v) -> Hashtbl.replace h k v) kvs;
Sx_types.Dict h in
let er_nil = mk_dict [("tag", Sx_types.String "nil")] in
let er_cons hd tl = mk_dict [("tag", Sx_types.String "cons");
("head", hd); ("tail", tl)] in
let er_tuple es = mk_dict [("tag", Sx_types.String "tuple");
("elements", Sx_types.List es)] in
let er_atom nm = mk_dict [("tag", Sx_types.String "atom");
("name", Sx_types.String nm)] in
let lst3 = er_cons (Sx_types.Integer 7)
(er_cons (Sx_types.Integer 8)
(er_cons (Sx_types.Integer 9) er_nil)) in
let tup3 = er_tuple [Sx_types.Integer 1; Sx_types.Integer 2;
Sx_types.Integer 3] in
let run consts bc =
let code = ({
vc_arity = 0; vc_rest_arity = -1; vc_locals = 0;
vc_bytecode = bc; vc_constants = consts;
vc_bytecode_list = None; vc_constants_list = None;
} : Sx_types.vm_code) in
Sx_vm.execute_module code (Hashtbl.create 1) in
let nm = function
| Sx_types.Dict d ->
(match Hashtbl.find_opt d "name" with
| Some (Sx_types.String s) -> s | _ -> "?")
| _ -> "?" in
let check label want got =
if got = want then begin
incr pass_count;
Printf.printf " PASS: %s\n" label
end else begin
incr fail_count;
Printf.printf " FAIL: %s: got %s\n" label (Sx_types.inspect got)
end in
(* HD [7,8,9] -> 7 *)
check "OP_BIF_HD [7,8,9] -> 7" (Sx_types.Integer 7)
(run [| lst3 |] [| 1;0;0; 231; 50 |]);
(* TL [7,8,9] -> [8,9], check its HD = 8 *)
check "OP_BIF_TL then HD -> 8" (Sx_types.Integer 8)
(run [| lst3 |] [| 1;0;0; 232; 231; 50 |]);
(* TUPLE_SIZE {1,2,3} -> 3 *)
check "OP_BIF_TUPLE_SIZE {1,2,3} -> 3" (Sx_types.Integer 3)
(run [| tup3 |] [| 1;0;0; 234; 50 |]);
(* IS_INTEGER 42 -> true ; IS_INTEGER [..] -> false *)
(match run [| Sx_types.Integer 42 |] [| 1;0;0; 236; 50 |] with
| v when nm v = "true" ->
incr pass_count; Printf.printf " PASS: OP_BIF_IS_INTEGER 42 -> true\n"
| v -> incr fail_count;
Printf.printf " FAIL: IS_INTEGER 42: got %s\n" (Sx_types.inspect v));
(match run [| lst3 |] [| 1;0;0; 236; 50 |] with
| v when nm v = "false" ->
incr pass_count; Printf.printf " PASS: OP_BIF_IS_INTEGER list -> false\n"
| v -> incr fail_count;
Printf.printf " FAIL: IS_INTEGER list: got %s\n" (Sx_types.inspect v));
(* IS_ATOM atom -> true ; IS_LIST nil -> true ; IS_TUPLE tuple -> true *)
(match run [| er_atom "ok" |] [| 1;0;0; 237; 50 |] with
| v when nm v = "true" ->
incr pass_count; Printf.printf " PASS: OP_BIF_IS_ATOM ok -> true\n"
| v -> incr fail_count;
Printf.printf " FAIL: IS_ATOM: got %s\n" (Sx_types.inspect v));
(match run [| er_nil |] [| 1;0;0; 238; 50 |] with
| v when nm v = "true" ->
incr pass_count; Printf.printf " PASS: OP_BIF_IS_LIST nil -> true\n"
| v -> incr fail_count;
Printf.printf " FAIL: IS_LIST nil: got %s\n" (Sx_types.inspect v));
(match run [| tup3 |] [| 1;0;0; 239; 50 |] with
| v when nm v = "true" ->
incr pass_count; Printf.printf " PASS: OP_BIF_IS_TUPLE {..} -> true\n"
| v -> incr fail_count;
Printf.printf " FAIL: IS_TUPLE: got %s\n" (Sx_types.inspect v));
(match run [| tup3 |] [| 1;0;0; 238; 50 |] with
| v when nm v = "false" ->
incr pass_count; Printf.printf " PASS: OP_BIF_IS_LIST tuple -> false\n"
| v -> incr fail_count;
Printf.printf " FAIL: IS_LIST tuple: got %s\n" (Sx_types.inspect v));
(* ELEMENT: element(2, {1,2,3}) -> 2. Calling convention: push
Index then Tuple; opcode pops Tuple (TOS) then Index. *)
check "OP_BIF_ELEMENT element(2,{1,2,3}) -> 2" (Sx_types.Integer 2)
(run [| Sx_types.Integer 2; tup3 |] [| 1;0;0; 1;1;0; 233; 50 |]);
check "OP_BIF_ELEMENT element(1,{1,2,3}) -> 1" (Sx_types.Integer 1)
(run [| Sx_types.Integer 1; tup3 |] [| 1;0;0; 1;1;0; 233; 50 |]);
(* ELEMENT out of range raises *)
(let raised =
(try ignore (run [| Sx_types.Integer 9; tup3 |]
[| 1;0;0; 1;1;0; 233; 50 |]); false
with Sx_types.Eval_error _ -> true) in
if raised then begin
incr pass_count;
Printf.printf " PASS: OP_BIF_ELEMENT out-of-range raises\n"
end else begin
incr fail_count;
Printf.printf " FAIL: OP_BIF_ELEMENT out-of-range should raise\n"
end);
(* LISTS_REVERSE [7,8,9] -> [9,8,7]; verify HD = 9 then HD of TL = 8 *)
check "OP_BIF_LISTS_REVERSE then HD -> 9" (Sx_types.Integer 9)
(run [| lst3 |] [| 1;0;0; 235; 231; 50 |]);
check "OP_BIF_LISTS_REVERSE then TL,HD -> 8" (Sx_types.Integer 8)
(run [| lst3 |] [| 1;0;0; 235; 232; 231; 50 |]);
(* reverse preserves length *)
check "OP_BIF_LISTS_REVERSE then LENGTH -> 3" (Sx_types.Integer 3)
(run [| lst3 |] [| 1;0;0; 235; 230; 50 |]));
(* A still-stubbed opcode (222 = erlang.OP_PATTERN_TUPLE) raises the
not-wired Eval_error — confirms the honest-failure path remains
for opcodes whose real handlers haven't landed. *)
(let globals = Hashtbl.create 1 in
try
ignore (Sx_vm.execute_module (make_bc_seq [| 222; 50 |]) globals);
incr fail_count;
Printf.printf " FAIL: erlang.OP_PATTERN_TUPLE dispatch should have raised\n"
with
| Sx_types.Eval_error msg
when (let needle = "not yet wired" in
let nl = String.length needle and ml = String.length msg in
let rec scan i =
if i + nl > ml then false
else if String.sub msg i nl = needle then true
else scan (i + 1)
in scan 0) ->
incr pass_count;
Printf.printf " PASS: erlang opcode dispatch raises not-wired error\n"
| exn ->
incr fail_count;
Printf.printf " FAIL: unexpected exn: %s\n" (Printexc.to_string exn));
(match Erlang_ext.dispatch_count () with
| Some n when n >= 1 ->
incr pass_count;
Printf.printf " PASS: erlang_ext state recorded %d dispatch(es)\n" n
| other ->
incr fail_count;
Printf.printf " FAIL: dispatch_count: %s\n"
(match other with Some n -> string_of_int n | None -> "None"));
Printf.printf "\nSuite: jit extension-opcode awareness\n";
let scan = Sx_vm.bytecode_uses_extension_opcodes in
let no_consts = [||] in
(* Pure core ops: scan reports false. *)
(* OP_TRUE OP_RETURN *)
if not (scan [| 3; 50 |] no_consts) then begin
incr pass_count;
Printf.printf " PASS: pure core bytecode is JIT-eligible\n"
end else begin
incr fail_count;
Printf.printf " FAIL: pure core bytecode flagged as extension\n"
end;
(* Extension opcode anywhere → true. *)
if scan [| 220; 50 |] no_consts then begin
incr pass_count;
Printf.printf " PASS: extension opcode detected at head\n"
end else begin
incr fail_count;
Printf.printf " FAIL: extension opcode at head missed\n"
end;
(* Mixed: core + extension → true. *)
if scan [| 3; 220; 50 |] no_consts then begin
incr pass_count;
Printf.printf " PASS: extension opcode detected after core ops\n"
end else begin
incr fail_count;
Printf.printf " FAIL: extension opcode after core ops missed\n"
end;
(* Operand bytes ≥200 must NOT trigger. CONST u16 with index 220
into a synthetic constant pool — the operand is 220 (lo) 0 (hi),
not an opcode. The pool entry at 220 is irrelevant for the scan. *)
let big_consts = Array.make 256 Nil in
if not (scan [| 1; 220; 0; 50 |] big_consts) then begin
incr pass_count;
Printf.printf " PASS: CONST operand ≥200 not a false positive\n"
end else begin
incr fail_count;
Printf.printf " FAIL: CONST operand ≥200 false-positives as ext op\n"
end;
(* CALL_PRIM has 3 operand bytes (u16 + u8); all ≥200 should not
trigger. *)
if not (scan [| 52; 220; 200; 200; 50 |] big_consts) then begin
incr pass_count;
Printf.printf " PASS: CALL_PRIM operands ≥200 not a false positive\n"
end else begin
incr fail_count;
Printf.printf " FAIL: CALL_PRIM operands ≥200 false-positive\n"
end;
(* CLOSURE with upvalue descriptors: scan must skip the 2 + 2*n
dynamic operand bytes. Build a synthetic constant pool with a
Dict at index 0 declaring upvalue-count 1, descriptors that are
≥200 — the scan should skip them and not trigger.
Bytecode layout: CLOSURE 0 0 desc_is_local desc_index RETURN
op lo hi 210 220 50
With upvalue-count = 1, scan must advance past the 2-byte CLOSURE
operand AND the 2 descriptor bytes (210, 220), landing on RETURN. *)
let cl_consts = Array.make 1 Nil in
let dict = Hashtbl.create 1 in
Hashtbl.replace dict "upvalue-count" (Integer 1);
cl_consts.(0) <- Dict dict;
if not (scan [| 51; 0; 0; 210; 220; 50 |] cl_consts) then begin
incr pass_count;
Printf.printf " PASS: CLOSURE upvalue descriptors ≥200 skipped\n"
end else begin
incr fail_count;
Printf.printf " FAIL: CLOSURE upvalue descriptors false-positive\n"
end;
(* Sanity: opcode after CLOSURE+descriptors that IS an extension
opcode triggers correctly. *)
if scan [| 51; 0; 0; 210; 220; 221; 50 |] cl_consts then begin
incr pass_count;
Printf.printf " PASS: extension opcode after CLOSURE detected\n"
end else begin
incr fail_count;
Printf.printf " FAIL: extension opcode after CLOSURE missed\n"
end
(* ====================================================================== *)
(* Spec test runner *)
(* ====================================================================== *)
(* Called after module loading to sync JIT globals with env *)
let _jit_refresh_globals : (unit -> unit) ref = ref (fun () -> ())
let run_spec_tests env test_files =
(* Find project root: walk up from cwd until we find spec/tests *)
let rec find_root dir =
let candidate = Filename.concat dir "spec/tests" in
if Sys.file_exists candidate then dir
else
let parent = Filename.dirname dir in
if parent = dir then Sys.getcwd () (* reached filesystem root *)
else find_root parent
in
let project_dir = find_root (Sys.getcwd ()) in
let spec_tests_dir = Filename.concat project_dir "spec/tests" in
let framework_path = Filename.concat spec_tests_dir "test-framework.sx" in
if not (Sys.file_exists framework_path) then begin
Printf.eprintf "test-framework.sx not found at %s\n" framework_path;
Printf.eprintf "Run from the project root directory.\n";
exit 1
end;
(* IO-aware evaluation: resolve library paths and handle import suspension *)
let lib_base = Filename.concat project_dir "lib" in
let spec_base = Filename.concat project_dir "spec" in
let web_base = Filename.concat project_dir "web" in
let resolve_library_path lib_spec =
let parts = match lib_spec with List l | ListRef { contents = l } -> l | _ -> [] in
match List.map (fun v -> match v with Symbol s -> s | String s -> s | _ -> "") parts with
| ["sx"; name] ->
let spec_path = Filename.concat spec_base (name ^ ".sx") in
let lib_path = Filename.concat lib_base (name ^ ".sx") in
let web_lib_path = Filename.concat (Filename.concat web_base "lib") (name ^ ".sx") in
if Sys.file_exists spec_path then Some spec_path
else if Sys.file_exists lib_path then Some lib_path
else if Sys.file_exists web_lib_path then Some web_lib_path
else None
| ["web"; name] ->
let path = Filename.concat web_base (name ^ ".sx") in
let lib_path = Filename.concat (Filename.concat web_base "lib") (name ^ ".sx") in
if Sys.file_exists path then Some path
else if Sys.file_exists lib_path then Some lib_path
else None
| [prefix; name] ->
let path = Filename.concat (Filename.concat project_dir prefix) (name ^ ".sx") in
if Sys.file_exists path then Some path else None
| _ -> None
in
(* Run CEK step loop, handling IO suspension for imports *)
let rec eval_with_io expr env_val =
let state = Sx_ref.make_cek_state expr env_val (List []) in
run_with_io state
and load_library_file path =
let exprs = Sx_parser.parse_file path in
List.iter (fun expr -> ignore (eval_with_io expr (Env env))) exprs
and run_with_io state =
let s = ref state in
let is_terminal st = match Sx_ref.cek_terminal_p st with Bool true -> true | _ -> false in
let is_suspended st = match Sx_runtime.get_val st (String "phase") with String "io-suspended" -> true | _ -> false in
(* Check if kont has any handler frames — pure structural scan *)
let kont_has_handler kont =
let k = ref kont in
let found = ref false in
while (match !k with List (_::_) -> true | _ -> false) && not !found do
(match !k with
| List (frame :: rest) ->
(match frame with
| CekFrame f when f.cf_type = "handler" -> found := true
| _ -> ());
k := List rest
| _ -> k := List [])
done;
!found in
let rec loop () =
while not (is_terminal !s) && not (is_suspended !s) do
(try s := Sx_ref.cek_step !s
with Eval_error msg ->
let kont = Sx_ref.cek_kont !s in
if kont_has_handler kont then
(* Convert to CEK-level raise so guard/handler-bind can catch it *)
let env = Sx_ref.cek_env !s in
s := Sx_ref.make_cek_value (String msg) env
(Sx_ref.kont_push (Sx_ref.make_raise_eval_frame env (Bool false)) kont)
else
raise (Eval_error msg))
done;
if is_suspended !s then begin
let request = Sx_runtime.get_val !s (String "request") in
let req_list = match request with List l -> l | ListRef { contents = l } -> l | _ -> [] in
let op = match req_list with
| String o :: _ -> o
| Symbol o :: _ -> o
| _ -> (match Sx_runtime.get_val request (String "op") with String s -> s | _ -> "") in
let response = match op with
| "import" ->
let lib_spec = Sx_runtime.get_val request (String "library") in
if Sx_types.sx_truthy (Sx_ref.library_loaded_p lib_spec) then
Nil
else begin
(match resolve_library_path lib_spec with
| Some path ->
(try load_library_file path
with Sx_types.Eval_error msg ->
Printf.eprintf "[import] Warning loading %s: %s\n%!"
(Sx_runtime.value_to_str lib_spec) msg)
| None -> ()); (* silently skip unresolvable libraries *)
Nil
end
| "text-measure" ->
(* Monospace approximation for tests *)
let args = let a = Sx_runtime.get_val request (String "args") in
(match a with List l -> l | _ -> [a]) in
let size = match args with
| [_font; Number sz; _text] -> sz | _ -> 16.0 in
let text = match args with
| [_font; _sz; String t] -> t | _ -> "" in
let w = size *. 0.6 *. (float_of_int (String.length text)) in
let d = Hashtbl.create 4 in
Hashtbl.replace d "width" (Number w);
Hashtbl.replace d "height" (Number size);
Hashtbl.replace d "ascent" (Number (size *. 0.8));
Hashtbl.replace d "descent" (Number (size *. 0.2));
Dict d
| "io-sleep" | "io-wait" | "io-settle" | "io-wait-for" -> Nil
| "io-fetch" ->
let args = match req_list with _ :: rest -> rest | _ -> [] in
let format = match args with _ :: String f :: _ -> f | _ -> "text" in
(match format with
| "json" | "JSON" | "Object" ->
let j = Hashtbl.create 2 in
Hashtbl.replace j "foo" (Number 1.0); Dict j
| "html" | "HTML" ->
String "[object DocumentFragment]"
| "Number" | "Int" | "Integer" | "Float" ->
String "1.2"
| "response" | "Response" ->
let resp = Hashtbl.create 4 in
Hashtbl.replace resp "ok" (Bool true);
Hashtbl.replace resp "status" (Number 200.0);
Hashtbl.replace resp "text" (String "yay");
Dict resp
| _ -> String "yay")
| _ -> Nil
in
s := Sx_ref.cek_resume !s response;
loop ()
end else
Sx_ref.cek_value !s
in
loop ()
in
let load_and_eval path =
let ic = open_in path in
let n = in_channel_length ic in
let s = Bytes.create n in
really_input ic s 0 n;
close_in ic;
let src = Bytes.to_string s in
let exprs = parse_all src in
List.iter (fun expr ->
try ignore (eval_with_io expr (Env env))
with Sx_types.Eval_error _ -> () (* skip expressions that fail during load *)
) exprs
in
(* Path-based name injection for unnamed defcomp/defisland (one-per-file) *)
let def_keywords = ["defcomp"; "defisland"; "defmacro"; "define";
"defhandler"; "defstyle"; "deftype"; "defeffect";
"defrelation"; "deftest"; "defpage"] in
let inject_path_name expr path base_dir =
match expr with
| Sx_types.List (Sx_types.Symbol kw :: rest) when List.mem kw def_keywords ->
(match rest with
| Sx_types.Symbol _ :: _ -> expr
| _ ->
let rel = if String.length path > String.length base_dir + 1
then String.sub path (String.length base_dir + 1)
(String.length path - String.length base_dir - 1)
else Filename.basename path in
let stem = if Filename.check_suffix rel ".sx"
then String.sub rel 0 (String.length rel - 3)
else rel in
let stem = let parts = String.split_on_char '/' stem in
String.concat "/" (List.filter (fun p -> p <> "_islands") parts) in
let name = if Filename.basename stem = "index"
then let d = Filename.dirname stem in
if d = "." then "index" else d
else stem in
let prefixed = if kw = "defcomp" || kw = "defisland"
then "~" ^ name else name in
Sx_types.List (Sx_types.Symbol kw :: Sx_types.Symbol prefixed :: rest))
| _ -> expr
in
let load_with_path path base_dir =
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 ->
let expr' = inject_path_name expr path base_dir in
try ignore (eval_with_io expr' (Env env))
with Sx_types.Eval_error _ -> ()
) exprs
in
let rec load_dir_recursive dir base_dir =
if Sys.file_exists dir && Sys.is_directory dir then
let entries = Sys.readdir dir in
Array.sort compare entries;
Array.iter (fun name ->
let path = Filename.concat dir name in
if Sys.is_directory path then
load_dir_recursive path base_dir
else if Filename.check_suffix name ".sx" then
(try load_with_path path base_dir
with e -> Printf.eprintf "Warning: %s: %s\n%!" path (Printexc.to_string e))
) entries
in
let _ = load_dir_recursive in
let _ = load_with_path 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
(* Physical-identity compare for mock elements via __host_handle. *)
let mock_el_eq a b =
match a, b with
| Dict da, Dict db ->
(match Hashtbl.find_opt da "__host_handle",
Hashtbl.find_opt db "__host_handle" with
| Some (Number ha), Some (Number hb) -> ha = hb
| _ -> false)
| _ -> false
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 "__host_handle" (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 -> not (mock_el_eq c child)) 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
(* Minimal HTML parser for test mock.
Parses an HTML string into mock child elements and appends them to `parent`.
Handles: