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:
2026-04-01 18:00:51 +00:00
parent f5f58ea47e
commit aa508bad77
5 changed files with 1445 additions and 2 deletions

View File

@@ -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