Implement sx-swap pure tree rewriting and fix handler test infrastructure
Write lib/sx-swap.sx — string-level SX scanner that finds elements by :id and applies swap operations (innerHTML, outerHTML, beforeend, afterbegin, beforebegin, afterend, delete, none). Includes OOB extraction via find-oob-elements/strip-oob/apply-response for out-of-band targeted swaps. Fix &rest varargs bug in test-handlers.sx helper mock — fn doesn't support &rest, so change to positional (name a1 a2) with nil defaults. Fix into branch, add run-handler sx-expr unwrapping. Add missing primitives to run_tests.ml: scope-peek, callable?, make-sx-expr, sx-expr-source, sx-expr?, spread?, call-lambda. These unblock aser-based handler evaluation in tests. Add algebraic integration tests (test-swap-integration.sx) demonstrating the sx1 ⊕(mode,target) sx2 = sx3 pattern with real handler execution. 1219 → 1330 passing tests (+111). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -319,6 +319,12 @@ let make_test_env () =
|
||||
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);
|
||||
bind "scope-emit!" (fun args ->
|
||||
match args with
|
||||
| [String name; value] ->
|
||||
@@ -385,10 +391,89 @@ let make_test_env () =
|
||||
eval_expr m.m_body (Env local)
|
||||
| _ -> raise (Eval_error "expand-macro: expected (macro args env)"));
|
||||
|
||||
bind "callable?" (fun args ->
|
||||
match args with
|
||||
| [NativeFn _] | [Lambda _] | [Component _] | [Island _] -> Bool true
|
||||
| _ -> Bool false);
|
||||
bind "make-sx-expr" (fun args -> match args with [String s] -> SxExpr s | _ -> raise (Eval_error "make-sx-expr: expected string"));
|
||||
bind "sx-expr-source" (fun args -> match args with [SxExpr s] -> String s | [String s] -> String s | _ -> raise (Eval_error "sx-expr-source: expected sx-expr or string"));
|
||||
bind "sx-expr?" (fun args -> match args with [SxExpr _] -> Bool true | _ -> Bool false);
|
||||
bind "spread?" (fun args -> match args with [Spread _] -> Bool true | _ -> Bool false);
|
||||
bind "call-lambda" (fun args ->
|
||||
match args with
|
||||
| [Lambda _ as f; (List a | ListRef { contents = a })] ->
|
||||
let l = match f with Lambda l -> l | _ -> assert false in
|
||||
let local = Sx_types.env_extend l.l_closure in
|
||||
let rec bind_ps ps as' = match ps, as' with
|
||||
| [], _ -> () | p :: pr, a :: ar -> ignore (Sx_types.env_bind local p a); bind_ps pr ar
|
||||
| p :: pr, [] -> ignore (Sx_types.env_bind local p Nil); bind_ps pr [] in
|
||||
bind_ps l.l_params a;
|
||||
eval_expr l.l_body (Env local)
|
||||
| [Lambda _ as f; (List a | ListRef { contents = a }); Env e] ->
|
||||
let l = match f with Lambda l -> l | _ -> assert false in
|
||||
let local = Sx_types.env_merge l.l_closure e in
|
||||
let rec bind_ps ps as' = match ps, as' with
|
||||
| [], _ -> () | p :: pr, a :: ar -> ignore (Sx_types.env_bind local p a); bind_ps pr ar
|
||||
| p :: pr, [] -> ignore (Sx_types.env_bind local p Nil); bind_ps pr [] in
|
||||
bind_ps l.l_params a;
|
||||
eval_expr l.l_body (Env local)
|
||||
| _ -> raise (Eval_error "call-lambda: expected (fn args env?)"));
|
||||
|
||||
(* Declarative type/effect forms — no-ops at runtime *)
|
||||
bind "deftype" (fun _args -> Nil);
|
||||
bind "defeffect" (fun _args -> Nil);
|
||||
|
||||
(* 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 = match remaining with
|
||||
| List p :: b :: _ -> (p, b) | List _p :: [] -> (_p, Nil) | _ -> ([], Nil) 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 "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;
|
||||
} in
|
||||
ignore (Sx_types.env_bind eval_env name island);
|
||||
island
|
||||
| _ -> Nil)));
|
||||
|
||||
(* --- Primitives for canonical.sx / content tests --- *)
|
||||
bind "contains-char?" (fun args ->
|
||||
match args with
|
||||
@@ -854,6 +939,11 @@ let run_spec_tests env test_files =
|
||||
(* Render adapter for test-render-html.sx *)
|
||||
load_module "render.sx" spec_dir;
|
||||
load_module "adapter-html.sx" web_dir;
|
||||
load_module "adapter-sx.sx" web_dir;
|
||||
(* Web modules for web/tests/ *)
|
||||
load_module "engine.sx" web_dir;
|
||||
load_module "page-helpers.sx" web_dir;
|
||||
load_module "request-handler.sx" web_dir;
|
||||
(* Library modules for lib/tests/ *)
|
||||
load_module "bytecode.sx" lib_dir;
|
||||
load_module "compiler.sx" lib_dir;
|
||||
@@ -863,9 +953,33 @@ let run_spec_tests env test_files =
|
||||
load_module "freeze.sx" lib_dir;
|
||||
load_module "content.sx" lib_dir;
|
||||
load_module "types.sx" lib_dir;
|
||||
load_module "sx-swap.sx" lib_dir;
|
||||
(* SX docs site: components, handlers, demos *)
|
||||
let sx_comp_dir = Filename.concat project_dir "sx/sxc" in
|
||||
let sx_sx_dir = Filename.concat project_dir "sx/sx" in
|
||||
let sx_handlers_dir = Filename.concat project_dir "sx/sx/handlers" in
|
||||
let sx_islands_dir = Filename.concat project_dir "sx/sx/reactive-islands" in
|
||||
let sx_geo_dir = Filename.concat project_dir "sx/sx/geography" in
|
||||
(* Components + handlers *)
|
||||
load_module "examples.sx" sx_comp_dir;
|
||||
load_module "docs.sx" sx_sx_dir;
|
||||
load_module "examples.sx" sx_handlers_dir;
|
||||
load_module "ref-api.sx" sx_handlers_dir;
|
||||
load_module "reactive-api.sx" sx_handlers_dir;
|
||||
(* Server-rendered demos *)
|
||||
load_module "scopes.sx" sx_sx_dir;
|
||||
load_module "provide.sx" sx_sx_dir;
|
||||
load_module "spreads.sx" sx_sx_dir;
|
||||
(* Island definitions *)
|
||||
load_module "index.sx" sx_islands_dir;
|
||||
load_module "demo.sx" sx_islands_dir;
|
||||
load_module "marshes.sx" sx_islands_dir;
|
||||
load_module "cek.sx" sx_geo_dir;
|
||||
load_module "reactive-runtime.sx" sx_sx_dir;
|
||||
|
||||
(* Determine test files — scan spec/tests/ and lib/tests/ *)
|
||||
(* Determine test files — scan spec/tests/, lib/tests/, web/tests/ *)
|
||||
let lib_tests_dir = Filename.concat project_dir "lib/tests" in
|
||||
let web_tests_dir = Filename.concat project_dir "web/tests" in
|
||||
let files = if test_files = [] then begin
|
||||
(* Spec tests (core language — always run) *)
|
||||
let spec_entries = Sys.readdir spec_tests_dir in
|
||||
@@ -878,15 +992,28 @@ let run_spec_tests env test_files =
|
||||
f <> "test-framework.sx")
|
||||
|> List.map (fun f -> Filename.concat spec_tests_dir f)
|
||||
in
|
||||
spec_files
|
||||
(* Web tests (orchestration, handlers) *)
|
||||
let web_files = if Sys.file_exists web_tests_dir then begin
|
||||
let entries = Sys.readdir web_tests_dir in
|
||||
Array.sort String.compare entries;
|
||||
Array.to_list entries
|
||||
|> List.filter (fun f ->
|
||||
String.length f > 5 &&
|
||||
String.sub f 0 5 = "test-" &&
|
||||
Filename.check_suffix f ".sx")
|
||||
|> List.map (fun f -> Filename.concat web_tests_dir f)
|
||||
end else [] in
|
||||
spec_files @ web_files
|
||||
end else
|
||||
(* Specific test files — search all test dirs *)
|
||||
List.map (fun name ->
|
||||
let name = if Filename.check_suffix name ".sx" then name else name ^ ".sx" in
|
||||
let spec_path = Filename.concat spec_tests_dir name in
|
||||
let lib_path = Filename.concat lib_tests_dir name in
|
||||
let web_path = Filename.concat web_tests_dir name in
|
||||
if Sys.file_exists spec_path then spec_path
|
||||
else if Sys.file_exists lib_path then lib_path
|
||||
else if Sys.file_exists web_path then web_path
|
||||
else Filename.concat spec_tests_dir name (* will fail with "not found" *)
|
||||
) test_files
|
||||
in
|
||||
|
||||
Reference in New Issue
Block a user