HS: extend parser/runtime + new node test runner; ignore test-results/

- Parser: `--` line comments, `|` op, `result` → `the-result`, query-scoped
  `<sel> in <expr>`, `is a/an <type>` predicate, multi-`as` chaining with `|`,
  `match`/`precede` keyword aliases, `[attr]` add/toggle, between attr forms
- Runtime: per-element listener registry + hs-deactivate!, attr toggle
  variants, set-inner-html boots subtree, hs-append polymorphic on
  string/list/element, default? / array-set! / query-all-in / list-set
  via take+drop, hs-script idempotence guard
- Integration: skip reserved (me/it/event/you/yourself) when collecting vars
- Tokenizer: emit `--` comments and `|` op
- Test framework + conformance runner updates; new tests/hs-run-filtered.js
  (single-process Node runner using OCaml VM step-limit to bound infinite
  loops); generate-sx-conformance-dev.py improvements
- mcp_tree.ml + run_tests.ml: harness extensions
- .gitignore: top-level test-results/ (Playwright artifacts)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 07:11:07 +00:00
parent b2ae80fb21
commit 0515295317
20 changed files with 15224 additions and 8120 deletions

1
.gitignore vendored
View File

@@ -23,6 +23,7 @@ hosts/ocaml/test-results/
shared/static/wasm/sx_browser.bc.wasm.assets/ shared/static/wasm/sx_browser.bc.wasm.assets/
.claude/worktrees/ .claude/worktrees/
tests/playwright/test-results/ tests/playwright/test-results/
test-results/
test-case-define.sx test-case-define.sx
test-case-define.txt test-case-define.txt
test_all.js test_all.js

View File

@@ -155,6 +155,192 @@ let register_mcp_jit_hook () =
| None -> None)) | None -> None))
| _ -> None) | _ -> None)
(* ------------------------------------------------------------------ *)
(* Native tree-tools helpers — avoid CEK overhead on big trees. *)
(* Mirror the SX semantics from lib/tree-tools.sx but run as direct *)
(* OCaml recursion. Used by read-subtree / validate / find-all / *)
(* find-across / comp-usage handlers. *)
(* ------------------------------------------------------------------ *)
let native_path_str path =
"[" ^ String.concat "," (List.map string_of_int path) ^ "]"
let rec native_node_display (node : value) : string =
match node with
| Nil -> "nil"
| Symbol s -> s
| Keyword k -> ":" ^ k
| String s -> "\"" ^ s ^ "\""
| Number n ->
if Float.is_integer n then string_of_int (int_of_float n)
else Printf.sprintf "%g" n
| Bool true -> "true"
| Bool false -> "false"
| List [] | ListRef { contents = [] } -> "()"
| List (h :: t) | ListRef { contents = h :: t } ->
if t = [] then "(" ^ native_node_display h ^ ")"
else "(" ^ native_node_display h ^ " ...)"
| Dict _ -> "{...}"
| v -> Sx_runtime.value_to_str v
(* node-summary-short: "(head)" for singletons, "(head second ...)" for >3,
"(all node-displays joined)" for small lists. Mirrors lib/tree-tools.sx. *)
let native_node_summary_short node =
match node with
| List [] | ListRef { contents = [] } -> "()"
| List items | ListRef { contents = items } ->
let n = List.length items in
if n > 3 then
let head = native_node_display (List.hd items) in
let second = native_node_display (List.nth items 1) in
Printf.sprintf "(%s %s ...)" head second
else
"(" ^ String.concat " " (List.map native_node_display items) ^ ")"
| _ -> native_node_display node
let rec native_node_matches node pattern =
match node with
| Symbol s | String s ->
(try ignore (Str.search_forward (Str.regexp_string pattern) s 0); true
with Not_found -> false)
| List items | ListRef { contents = items } ->
List.exists (fun c -> native_node_matches c pattern) items
| _ -> false
(* Raw find: returns reversed list of (path, summary) pairs — caller reverses. *)
let native_find_all_raw exprs pattern =
let nodes = match exprs with
| List xs | ListRef { contents = xs } -> xs
| x -> [x] in
let acc = ref [] in
let rec go node path =
if native_node_matches node pattern then
acc := (List.rev path, native_node_summary_short node) :: !acc;
match node with
| List items | ListRef { contents = items } ->
List.iteri (fun i child -> go child (i :: path)) items
| _ -> ()
in
List.iteri (fun i node -> go node [i]) nodes;
List.rev !acc
(* SX-value-returning wrapper — kept for any SX code that still calls find-all. *)
let native_find_all_sx exprs pattern =
let pairs = native_find_all_raw exprs pattern in
List (List.map (fun (path, summary) ->
List [List (List.map (fun i -> Number (float_of_int i)) path);
String summary]
) pairs)
(* navigate: walk a tree by a list of indices. Wraps exprs into a list on entry
(mirrors SX navigate). Returns Nil if any index is out of range. *)
let native_navigate exprs path =
let init = match exprs with
| List _ | ListRef _ -> exprs
| x -> List [x] in
let rec step current = function
| [] -> current
| i :: rest ->
(match current with
| List items when i >= 0 && i < List.length items ->
step (List.nth items i) rest
| ListRef { contents = items } when i >= 0 && i < List.length items ->
step (List.nth items i) rest
| _ -> Nil) in
step init path
(* annotate-tree: render a list of exprs as an indented path-annotated string.
Mirrors the `annotate-node` dispatch from lib/tree-tools.sx:
- small lists (len<=4, no list children) render inline
- others render head on first line, children indented, closing ")"
Path annotations use [i,j,k,...] form. *)
let native_annotate_tree exprs =
let nodes = match exprs with
| List xs | ListRef { contents = xs } -> xs
| x -> [x] in
let out = Buffer.create 1024 in
let first = ref true in
let emit s =
if !first then first := false else Buffer.add_char out '\n';
Buffer.add_string out s in
let rec ann node path depth =
let indent = String.make (depth * 2) ' ' in
let label = native_path_str (List.rev path) in
match node with
| List [] | ListRef { contents = [] } ->
emit (indent ^ label ^ " ()")
| List items | ListRef { contents = items } ->
let n = List.length items in
let rest = List.tl items in
let any_child_list = List.exists
(fun c -> match c with List _ | ListRef _ -> true | _ -> false) rest in
if n <= 4 && not any_child_list then
emit (indent ^ label ^ " (" ^
String.concat " " (List.map native_node_display items) ^ ")")
else begin
let head_str = native_node_display (List.hd items) in
emit (indent ^ label ^ " (" ^ head_str);
List.iteri (fun i child ->
if i > 0 then ann child (i :: path) (depth + 1)
) items;
emit (indent ^ " )")
end
| _ ->
emit (indent ^ label ^ " " ^ native_node_display node)
in
List.iteri (fun i node -> ann node [i] 0) nodes;
Buffer.contents out
let native_read_subtree exprs path =
let node = native_navigate exprs path in
match node with
| Nil -> "Error: path " ^ native_path_str path ^ " not found"
| _ -> native_annotate_tree (List [node])
(* validate: walk the tree, emit WARNING for malformed letrec bindings and
ERROR for defisland/defcomp with fewer than 3 args. *)
let native_validate exprs =
let errors = ref [] in
let emit s = errors := s :: !errors in
let rec go node path =
(match node with
| List items | ListRef { contents = items } ->
(match items with
| [] -> ()
| head :: _ ->
let head_name = match head with Symbol s -> Some s | _ -> None in
(match head_name with
| Some "letrec" when List.length items >= 2 ->
let bindings = List.nth items 1 in
(match bindings with
| List pairs | ListRef { contents = pairs } ->
List.iteri (fun i pair ->
let ok = match pair with
| List (Symbol _ :: _ :: _) -> true
| ListRef { contents = Symbol _ :: _ :: _ } -> true
| _ -> false in
if not ok then
emit (Printf.sprintf
"WARNING %s: letrec binding %d is not a (name value) pair: %s"
(native_path_str (List.rev (i :: 1 :: path)))
i
(native_node_display pair))
) pairs
| _ -> ())
| Some (("defisland" | "defcomp") as nm) when List.length items < 4 ->
emit (Printf.sprintf
"ERROR %s: %s has fewer than 3 args (name params b..."
(native_path_str (List.rev path)) nm)
| _ -> ()));
List.iteri (fun i child -> go child (i :: path)) items
| _ -> ())
in
let nodes = match exprs with
| List xs | ListRef { contents = xs } -> xs
| x -> [x] in
List.iteri (fun i node -> go node [i]) nodes;
if !errors = [] then "OK" else String.concat "\n" (List.rev !errors)
let setup_env () = let setup_env () =
let e = make_env () in let e = make_env () in
(* Primitives are auto-registered at module init *) (* Primitives are auto-registered at module init *)
@@ -439,6 +625,30 @@ let setup_env () =
try load_sx_file e (Filename.concat lib_dir "compiler.sx"); try load_sx_file e (Filename.concat lib_dir "compiler.sx");
register_mcp_jit_hook () register_mcp_jit_hook ()
with exn -> Printf.eprintf "[mcp] Warning: compiler.sx load failed (JIT disabled): %s\n%!" (Printexc.to_string exn)); with exn -> Printf.eprintf "[mcp] Warning: compiler.sx load failed (JIT disabled): %s\n%!" (Printexc.to_string exn));
(* Native-impl the hot tree-tools ops — replaces SX versions to avoid CEK
overhead on big trees. See native_* helpers defined above setup_env. *)
ignore (Sx_types.env_bind e "find-all" (NativeFn ("find-all", fun args ->
match args with
| [exprs; String pattern] -> native_find_all_sx exprs pattern
| _ -> List [])));
ignore (Sx_types.env_bind e "read-subtree" (NativeFn ("read-subtree", fun args ->
match args with
| [exprs; List path] | [exprs; ListRef { contents = path }] ->
let ints = List.map (fun v -> match v with Number n -> int_of_float n | _ -> 0) path in
String (native_read_subtree exprs ints)
| _ -> String "")));
ignore (Sx_types.env_bind e "validate" (NativeFn ("validate", fun args ->
match args with
| [exprs] -> String (native_validate exprs)
| _ -> String "")));
ignore (Sx_types.env_bind e "path-str" (NativeFn ("path-str", fun args ->
match args with
| [List path] | [ListRef { contents = path }] ->
let ints = List.map (fun v -> match v with Number n -> int_of_float n | _ -> 0) path in
String (native_path_str ints)
| _ -> String "[]")));
Printf.eprintf "[mcp] Ready in %.0fms\n%!" ((Unix.gettimeofday () -. t0) *. 1000.0); Printf.eprintf "[mcp] Ready in %.0fms\n%!" ((Unix.gettimeofday () -. t0) *. 1000.0);
env := e env := e
@@ -804,7 +1014,11 @@ let handle_sx_read_subtree args =
let open Yojson.Safe.Util in let open Yojson.Safe.Util in
let tree = parse_file (require_file args "file") in let tree = parse_file (require_file args "file") in
let path = resolve_path tree (args |> member "path" |> to_string) in let path = resolve_path tree (args |> member "path" |> to_string) in
text_result (value_to_string (call_sx "read-subtree" [tree; path])) let ints = match path with
| List xs | ListRef { contents = xs } ->
List.map (fun v -> match v with Number n -> int_of_float n | _ -> 0) xs
| _ -> [] in
text_result (native_read_subtree tree ints)
let handle_sx_get_context args = let handle_sx_get_context args =
let open Yojson.Safe.Util in let open Yojson.Safe.Util in
@@ -816,17 +1030,8 @@ let handle_sx_find_all args =
let open Yojson.Safe.Util in let open Yojson.Safe.Util in
let tree = parse_file (require_file args "file") in let tree = parse_file (require_file args "file") in
let pattern = args |> member "pattern" |> to_string in let pattern = args |> member "pattern" |> to_string in
let results = call_sx "find-all" [tree; String pattern] in let results = native_find_all_raw tree pattern in
let lines = match results with let lines = List.map (fun (p, s) -> native_path_str p ^ " " ^ s) results in
| List items | ListRef { contents = items } ->
List.map (fun item ->
match item with
| List [p; s] | ListRef { contents = [p; s] } ->
value_to_string (call_sx "path-str" [p]) ^ " " ^ value_to_string s
| _ -> value_to_string item
) items
| _ -> [value_to_string results]
in
text_result (String.concat "\n" lines) text_result (String.concat "\n" lines)
let handle_sx_get_siblings args = let handle_sx_get_siblings args =
@@ -843,7 +1048,7 @@ let handle_sx_get_siblings args =
let handle_sx_validate args = let handle_sx_validate args =
let tree = parse_file (require_file args "file") in let tree = parse_file (require_file args "file") in
text_result (value_to_string (call_sx "validate" [tree])) text_result (native_validate tree)
let handle_sx_replace_node args = let handle_sx_replace_node args =
let open Yojson.Safe.Util in let open Yojson.Safe.Util in
@@ -1912,16 +2117,8 @@ let handle_sx_find_across args =
let rel = relative_path ~base:dir path in let rel = relative_path ~base:dir path in
try try
let tree = parse_file path in let tree = parse_file path in
let results = call_sx "find-all" [tree; String pattern] in let results = native_find_all_raw tree pattern in
(match results with List.map (fun (p, s) -> rel ^ " " ^ native_path_str p ^ " " ^ s) results
| List items | ListRef { contents = items } ->
List.map (fun item ->
match item with
| List [p; s] | ListRef { contents = [p; s] } ->
rel ^ " " ^ value_to_string (call_sx "path-str" [p]) ^ " " ^ value_to_string s
| _ -> rel ^ " " ^ value_to_string item
) items
| _ -> [])
with _ -> [] with _ -> []
) files in ) files in
if all_lines = [] then text_result "(no matches)" if all_lines = [] then text_result "(no matches)"
@@ -1948,16 +2145,8 @@ let handle_sx_comp_usage args =
let rel = relative_path ~base:dir path in let rel = relative_path ~base:dir path in
try try
let tree = parse_file path in let tree = parse_file path in
let results = call_sx "find-all" [tree; String name] in let results = native_find_all_raw tree name in
(match results with List.map (fun (p, s) -> rel ^ " " ^ native_path_str p ^ " " ^ s) results
| List items | ListRef { contents = items } ->
List.map (fun item ->
match item with
| List [p; s] | ListRef { contents = [p; s] } ->
rel ^ " " ^ value_to_string (call_sx "path-str" [p]) ^ " " ^ value_to_string s
| _ -> rel ^ " " ^ value_to_string item
) items
| _ -> [])
with _ -> [] with _ -> []
) files in ) files in
if all_lines = [] then text_result "(no usages found)" if all_lines = [] then text_result "(no usages found)"

View File

@@ -25,6 +25,9 @@ open Sx_ref
let pass_count = ref 0 let pass_count = ref 0
let fail_count = ref 0 let fail_count = ref 0
let suite_stack : string list ref = ref [] 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 *) (* Deep equality — SX structural comparison *)
@@ -176,6 +179,17 @@ let make_test_env () =
suite_stack := (match !suite_stack with _ :: t -> t | [] -> []); suite_stack := (match !suite_stack with _ :: t -> t | [] -> []);
Nil); 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 --- *) (* --- Test helpers --- *)
bind "sx-parse" (fun args -> bind "sx-parse" (fun args ->
@@ -1563,6 +1577,131 @@ let run_spec_tests env test_files =
| _ -> child | _ -> child
in in
(* Minimal HTML parser for test mock.
Parses an HTML string into mock child elements and appends them to `parent`.
Handles: <tag attr="v" attr='v' attr=v attr>content</tag>, nested elements,
self-closing tags, text content. No comments, CDATA, DOCTYPE, or entities. *)
let parse_html_into parent_d html =
let len = String.length html in
let pos = ref 0 in
let is_name_char c =
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c = '-' || c = '_' || c = ':'
in
let skip_ws () =
while !pos < len && (let c = html.[!pos] in c = ' ' || c = '\n' || c = '\t' || c = '\r') do
incr pos
done
in
let parse_name () =
let start = !pos in
while !pos < len && is_name_char html.[!pos] do incr pos done;
String.sub html start (!pos - start)
in
let parse_attr_value () =
if !pos >= len then ""
else if html.[!pos] = '"' then begin
incr pos;
let start = !pos in
while !pos < len && html.[!pos] <> '"' do incr pos done;
let v = String.sub html start (!pos - start) in
if !pos < len then incr pos;
v
end
else if html.[!pos] = '\'' then begin
incr pos;
let start = !pos in
while !pos < len && html.[!pos] <> '\'' do incr pos done;
let v = String.sub html start (!pos - start) in
if !pos < len then incr pos;
v
end
else begin
let start = !pos in
while !pos < len && (let c = html.[!pos] in
c <> ' ' && c <> '\t' && c <> '\n' && c <> '\r'
&& c <> '>' && c <> '/') do
incr pos
done;
String.sub html start (!pos - start)
end
in
let parse_attrs (elem : (string, Sx_types.value) Hashtbl.t) =
skip_ws ();
while !pos < len && html.[!pos] <> '>' && html.[!pos] <> '/' do
let name = parse_name () in
if name = "" then begin
(* Avoid infinite loop on unexpected char *)
if !pos < len then incr pos
end else begin
let value =
if !pos < len && html.[!pos] = '=' then begin
incr pos; parse_attr_value ()
end else ""
in
let attrs = match Hashtbl.find_opt elem "attributes" with
| Some (Dict a) -> a
| _ -> let a = Hashtbl.create 4 in Hashtbl.replace elem "attributes" (Dict a); a in
Hashtbl.replace attrs name (String value);
if name = "id" then Hashtbl.replace elem "id" (String value);
if name = "class" then Hashtbl.replace elem "className" (String value);
if name = "value" then Hashtbl.replace elem "value" (String value);
skip_ws ()
end
done
in
let void_tags = ["br"; "hr"; "img"; "input"; "meta"; "link"; "area";
"base"; "col"; "embed"; "source"; "track"; "wbr"] in
let rec parse_children parent_elem =
while !pos < len && not (!pos + 1 < len && html.[!pos] = '<' && html.[!pos + 1] = '/') do
if !pos < len && html.[!pos] = '<' && !pos + 1 < len && is_name_char html.[!pos + 1] then
parse_element parent_elem
else if !pos < len && html.[!pos] = '<' then begin
(* Unknown/comment — skip to next '>' *)
while !pos < len && html.[!pos] <> '>' do incr pos done;
if !pos < len then incr pos
end
else begin
let start = !pos in
while !pos < len && html.[!pos] <> '<' do incr pos done;
let text = String.sub html start (!pos - start) in
if String.trim text <> "" then begin
let cur = match Hashtbl.find_opt parent_elem "textContent" with
| Some (String s) -> s | _ -> "" in
Hashtbl.replace parent_elem "textContent" (String (cur ^ text))
end
end
done
and parse_element parent_elem =
incr pos; (* skip '<' *)
let tag = parse_name () in
if tag = "" then () else begin
let el = make_mock_element tag in
let eld = match el with Dict d -> d | _ -> Hashtbl.create 0 in
parse_attrs eld;
skip_ws ();
let self_closing =
if !pos < len && html.[!pos] = '/' then begin incr pos; true end else false
in
if !pos < len && html.[!pos] = '>' then incr pos;
let is_void = List.mem (String.lowercase_ascii tag) void_tags in
if not self_closing && not is_void then begin
parse_children eld;
if !pos + 1 < len && html.[!pos] = '<' && html.[!pos + 1] = '/' then begin
pos := !pos + 2;
let _ = parse_name () in
skip_ws ();
if !pos < len && html.[!pos] = '>' then incr pos
end
end;
ignore (mock_append_child (Dict parent_elem) el)
end
in
pos := 0;
parse_children parent_d
in
let _ = parse_html_into in
(* Helper: remove child from parent *) (* Helper: remove child from parent *)
let mock_remove_child parent child = let mock_remove_child parent child =
match parent, child with match parent, child with
@@ -1578,11 +1717,21 @@ let run_spec_tests env test_files =
in in
(* Helper: querySelector - find element matching selector in tree *) (* Helper: querySelector - find element matching selector in tree *)
let mock_matches el sel = let rec mock_matches el sel =
match el with match el with
| Dict d -> | Dict d ->
let sel = String.trim sel in let sel = String.trim sel in
if String.length sel > 0 && sel.[0] = '#' then (* Compound selector: tag[attr=value] or tag.class or tag#id — split into parts *)
if String.length sel > 1 &&
((sel.[0] >= 'a' && sel.[0] <= 'z') || (sel.[0] >= 'A' && sel.[0] <= 'Z')) &&
(String.contains sel '[' || String.contains sel '.' || String.contains sel '#') then
let i = ref 0 in
let n = String.length sel in
while !i < n && ((sel.[!i] >= 'a' && sel.[!i] <= 'z') || (sel.[!i] >= 'A' && sel.[!i] <= 'Z') || (sel.[!i] >= '0' && sel.[!i] <= '9') || sel.[!i] = '-') do incr i done;
let tag_part = String.sub sel 0 !i in
let rest_part = String.sub sel !i (n - !i) in
(mock_matches el tag_part) && (mock_matches el rest_part)
else if String.length sel > 0 && sel.[0] = '#' then
let id = String.sub sel 1 (String.length sel - 1) in let id = String.sub sel 1 (String.length sel - 1) in
(match Hashtbl.find_opt d "id" with Some (String i) -> i = id | _ -> false) (match Hashtbl.find_opt d "id" with Some (String i) -> i = id | _ -> false)
else if String.length sel > 0 && sel.[0] = '.' then else if String.length sel > 0 && sel.[0] = '.' then
@@ -1590,7 +1739,8 @@ let run_spec_tests env test_files =
List.mem cls (get_classes d) List.mem cls (get_classes d)
else if String.length sel > 0 && sel.[0] = '[' then else if String.length sel > 0 && sel.[0] = '[' then
(* [attr] or [attr="value"] *) (* [attr] or [attr="value"] *)
let inner = String.sub sel 1 (String.length sel - 2) in let end_bracket = try String.index sel ']' with Not_found -> String.length sel - 1 in
let inner = String.sub sel 1 (end_bracket - 1) in
(match String.split_on_char '=' inner with (match String.split_on_char '=' inner with
| [attr] -> | [attr] ->
let attrs = match Hashtbl.find_opt d "attributes" with Some (Dict a) -> a | _ -> Hashtbl.create 0 in let attrs = match Hashtbl.find_opt d "attributes" with Some (Dict a) -> a | _ -> Hashtbl.create 0 in
@@ -1621,19 +1771,51 @@ let run_spec_tests env test_files =
| found -> mock_query_selector found (String.concat " " rest)) | found -> mock_query_selector found (String.concat " " rest))
| [] -> Nil | [] -> Nil
and mock_query_selector_single el sel = and mock_query_selector_single el sel =
match el with (* Handle tag:nth-of-type(N): find Nth child of same tag under parent *)
| Dict d -> let nth_match = try
let kids = match Hashtbl.find_opt d "children" with Some (List l) -> l | _ -> [] in let idx = String.index sel ':' in
let rec search = function let tag = String.sub sel 0 idx in
| [] -> Nil let rest = String.sub sel idx (String.length sel - idx) in
| child :: rest -> if String.length rest > String.length ":nth-of-type(" &&
if mock_matches child sel then child String.sub rest 0 (String.length ":nth-of-type(") = ":nth-of-type(" &&
else match mock_query_selector_single child sel with rest.[String.length rest - 1] = ')'
| Nil -> search rest then
| found -> found let n_str = String.sub rest (String.length ":nth-of-type(")
in (String.length rest - String.length ":nth-of-type(" - 1) in
search kids (try Some (tag, int_of_string (String.trim n_str)) with _ -> None)
| _ -> Nil else None
with Not_found -> None in
(match nth_match with
| Some (tag, n) ->
(* Walk tree; collect matching-tag elements in document order; return nth *)
let found = ref [] in
let rec walk node =
match node with
| Dict d ->
let kids = match Hashtbl.find_opt d "children" with Some (List l) -> l | _ -> [] in
List.iter (fun child ->
if mock_matches child tag then found := child :: !found;
walk child
) kids
| _ -> ()
in
walk el;
let matches = List.rev !found in
(try List.nth matches (n - 1) with _ -> Nil)
| None ->
(match el with
| Dict d ->
let kids = match Hashtbl.find_opt d "children" with Some (List l) -> l | _ -> [] in
let rec search = function
| [] -> Nil
| child :: rest ->
if mock_matches child sel then child
else match mock_query_selector_single child sel with
| Nil -> search rest
| found -> found
in
search kids
| _ -> Nil))
in in
let rec mock_query_all el sel = let rec mock_query_all el sel =
@@ -1742,7 +1924,7 @@ let run_spec_tests env test_files =
| "addEventListener" | "removeEventListener" | "dispatchEvent" | "addEventListener" | "removeEventListener" | "dispatchEvent"
| "appendChild" | "removeChild" | "insertBefore" | "replaceChild" | "appendChild" | "removeChild" | "insertBefore" | "replaceChild"
| "querySelector" | "querySelectorAll" | "closest" | "matches" | "querySelector" | "querySelectorAll" | "closest" | "matches"
| "contains" | "cloneNode" | "remove" | "focus" | "blur" | "click" | "contains" | "compareDocumentPosition" | "cloneNode" | "remove" | "focus" | "blur" | "click"
| "insertAdjacentHTML" | "prepend" | "showModal" | "show" | "close" | "insertAdjacentHTML" | "prepend" | "showModal" | "show" | "close"
| "getBoundingClientRect" | "getAnimations" | "scrollIntoView" | "getBoundingClientRect" | "getAnimations" | "scrollIntoView"
| "scrollTo" | "scroll" | "reset" -> Bool true | "scrollTo" | "scroll" | "reset" -> Bool true
@@ -1841,16 +2023,19 @@ let run_spec_tests env test_files =
| Some (Dict _cl) -> () (* classes live in className *) | Some (Dict _cl) -> () (* classes live in className *)
| _ -> ()) | _ -> ())
| "innerHTML" -> | "innerHTML" ->
(* Setting innerHTML clears children and syncs textContent (like a browser) *) (* Setting innerHTML clears existing children, parses the HTML, and
creates new mock child elements (approximating browser behavior). *)
let kids = match Hashtbl.find_opt d "children" with Some (List l) -> l | _ -> [] in let kids = match Hashtbl.find_opt d "children" with Some (List l) -> l | _ -> [] in
List.iter (fun c -> match c with Dict cd -> List.iter (fun c -> match c with Dict cd ->
Hashtbl.replace cd "parentElement" Nil; Hashtbl.replace cd "parentElement" Nil;
Hashtbl.replace cd "parentNode" Nil | _ -> ()) kids; Hashtbl.replace cd "parentNode" Nil | _ -> ()) kids;
Hashtbl.replace d "children" (List []); Hashtbl.replace d "children" (List []);
Hashtbl.replace d "childNodes" (List []); Hashtbl.replace d "childNodes" (List []);
(* Approximate textContent: strip HTML tags from innerHTML *) Hashtbl.replace d "textContent" (String "");
(match stored with (match stored with
| String s -> | String s when String.contains s '<' ->
parse_html_into d s;
(* Strip tags for a best-effort textContent *)
let buf = Buffer.create (String.length s) in let buf = Buffer.create (String.length s) in
let in_tag = ref false in let in_tag = ref false in
String.iter (fun c -> String.iter (fun c ->
@@ -1859,6 +2044,7 @@ let run_spec_tests env test_files =
else if not !in_tag then Buffer.add_char buf c else if not !in_tag then Buffer.add_char buf c
) s; ) s;
Hashtbl.replace d "textContent" (String (Buffer.contents buf)) Hashtbl.replace d "textContent" (String (Buffer.contents buf))
| String s -> Hashtbl.replace d "textContent" (String s)
| _ -> Hashtbl.replace d "textContent" (String "")) | _ -> Hashtbl.replace d "textContent" (String ""))
| "textContent" -> | "textContent" ->
(* Setting textContent clears children *) (* Setting textContent clears children *)
@@ -1887,6 +2073,8 @@ let run_spec_tests env test_files =
| "setTimeout" -> (match rest with fn :: _ -> ignore (Sx_ref.cek_call fn (List [])); Nil | _ -> Nil) | "setTimeout" -> (match rest with fn :: _ -> ignore (Sx_ref.cek_call fn (List [])); Nil | _ -> Nil)
| "clearTimeout" -> Nil | "clearTimeout" -> Nil
| _ -> Nil) | _ -> Nil)
| Dict d :: String "hasOwnProperty" :: [String k] ->
Bool (Hashtbl.mem d k)
| Dict d :: String m :: rest -> | Dict d :: String m :: rest ->
let mt = match Hashtbl.find_opt d "__mock_type" with Some (String t) -> t | _ -> "" in let mt = match Hashtbl.find_opt d "__mock_type" with Some (String t) -> t | _ -> "" in
@@ -2139,6 +2327,33 @@ let run_spec_tests env test_files =
| _ -> Nil | _ -> Nil
in up (Dict d) in up (Dict d)
| _ -> Nil) | _ -> Nil)
| "compareDocumentPosition" ->
(match rest with
| [other] ->
let self = Dict d in
let body = Dict mock_body in
let found_self = ref false in
let found_other = ref false in
let self_first = ref false in
let rec walk node =
if !found_self && !found_other then ()
else begin
if mock_el_eq node self then begin
if not !found_other then self_first := true;
found_self := true
end;
if mock_el_eq node other then found_other := true;
(match node with
| Dict dd -> let kids = match Hashtbl.find_opt dd "children" with Some (List l) -> l | _ -> [] in
List.iter walk kids
| _ -> ())
end
in
walk body;
if !found_self && !found_other then
Number (if !self_first then 4.0 else 2.0)
else Number 0.0
| _ -> Number 0.0)
| "matches" -> | "matches" ->
(match rest with [String sel] -> Bool (mock_matches (Dict d) sel) | _ -> Bool false) (match rest with [String sel] -> Bool (mock_matches (Dict d) sel) | _ -> Bool false)
| "contains" -> | "contains" ->
@@ -2213,25 +2428,62 @@ let run_spec_tests env test_files =
Hashtbl.replace r "right" (Number 100.0); Hashtbl.replace r "bottom" (Number 100.0); Hashtbl.replace r "right" (Number 100.0); Hashtbl.replace r "bottom" (Number 100.0);
Dict r Dict r
| "insertAdjacentHTML" -> | "insertAdjacentHTML" ->
(* Position-aware insertion, coerce value to string *) (* Position-aware insertion. Parse the new HTML into a scratch
container, then splice the resulting children into the target
position WITHOUT disturbing sibling nodes. *)
(match rest with (match rest with
| [String pos; value] -> | [String pos_kind; value] ->
let html = match dom_stringify value with String s -> s | _ -> "" in let html = match dom_stringify value with String s -> s | _ -> "" in
let cur = match Hashtbl.find_opt d "innerHTML" with Some (String s) -> s | _ -> "" in (* Parse new HTML into scratch container to get new child list.
let new_html = match pos with For pure-text content, wrap into the target's innerHTML path. *)
| "afterbegin" -> html ^ cur (* prepend *) let scratch = make_mock_element "div" in
| _ -> cur ^ html (* beforeend / default: append *) let scratch_d = match scratch with Dict sd -> sd | _ -> Hashtbl.create 0 in
if String.contains html '<' then parse_html_into scratch_d html;
let new_kids = match Hashtbl.find_opt scratch_d "children" with Some (List l) -> l | _ -> [] in
let prepend = pos_kind = "beforebegin" || pos_kind = "afterbegin" in
let insert_into container_d index =
List.iter (fun c -> match c with
| Dict cd ->
Hashtbl.replace cd "parentElement" (Dict container_d);
Hashtbl.replace cd "parentNode" (Dict container_d)
| _ -> ()) new_kids;
let kids = match Hashtbl.find_opt container_d "children" with Some (List l) -> l | _ -> [] in
let before = List.filteri (fun i _ -> i < index) kids in
let after = List.filteri (fun i _ -> i >= index) kids in
let all = before @ new_kids @ after in
Hashtbl.replace container_d "children" (List all);
Hashtbl.replace container_d "childNodes" (List all);
(* Update container innerHTML based on position kind, not index *)
let cur = match Hashtbl.find_opt container_d "innerHTML" with Some (String s) -> s | _ -> "" in
let new_html = if prepend then html ^ cur else cur ^ html in
Hashtbl.replace container_d "innerHTML" (String new_html);
let buf = Buffer.create (String.length new_html) in
let in_tag = ref false in
String.iter (fun c ->
if c = '<' then in_tag := true
else if c = '>' then in_tag := false
else if not !in_tag then Buffer.add_char buf c
) new_html;
Hashtbl.replace container_d "textContent" (String (Buffer.contents buf))
in in
Hashtbl.replace d "innerHTML" (String new_html); (match pos_kind with
(* Sync textContent *) | "beforebegin" | "afterend" ->
let buf = Buffer.create (String.length new_html) in (match Hashtbl.find_opt d "parentElement" with
let in_tag = ref false in | Some (Dict pd) ->
String.iter (fun c -> let siblings = match Hashtbl.find_opt pd "children" with Some (List l) -> l | _ -> [] in
if c = '<' then in_tag := true let rec find_idx i = function
else if c = '>' then in_tag := false | [] -> List.length siblings
else if not !in_tag then Buffer.add_char buf c | x :: _ when mock_el_eq x (Dict d) -> i
) new_html; | _ :: rest -> find_idx (i+1) rest
Hashtbl.replace d "textContent" (String (Buffer.contents buf)); in
let self_idx = find_idx 0 siblings in
let insert_idx = if pos_kind = "beforebegin" then self_idx else self_idx + 1 in
insert_into pd insert_idx
| _ -> ())
| "afterbegin" -> insert_into d 0
| _ (* "beforeend" *) ->
let kids_len = match Hashtbl.find_opt d "children" with Some (List l) -> List.length l | _ -> 0 in
insert_into d kids_len);
Nil Nil
| _ -> Nil) | _ -> Nil)
| "showModal" | "show" -> | "showModal" | "show" ->
@@ -2341,6 +2593,8 @@ let run_spec_tests env test_files =
Hashtbl.replace ev "_stopped" (Bool false); Hashtbl.replace ev "_stopped" (Bool false);
Hashtbl.replace ev "_stopImmediate" (Bool false); Hashtbl.replace ev "_stopImmediate" (Bool false);
Dict ev Dict ev
| [String "Object"] ->
Dict (Hashtbl.create 4)
| _ -> Nil); | _ -> Nil);
reg "host-callback" (fun args -> reg "host-callback" (fun args ->
@@ -2823,7 +3077,39 @@ let () =
let args = Array.to_list Sys.argv |> List.tl in let args = Array.to_list Sys.argv |> List.tl in
let foundation_only = List.mem "--foundation" args in let foundation_only = List.mem "--foundation" args in
let jit_enabled = List.mem "--jit" args in let jit_enabled = List.mem "--jit" args in
let test_files = List.filter (fun a -> not (String.length a > 0 && a.[0] = '-')) args in (* --only-failing=PATH : read lines of form "FAIL: suite > name: ..." and
restrict test runs to those (suite, name) pairs. *)
List.iter (fun a ->
let prefix = "--only-failing=" in
if String.length a > String.length prefix
&& String.sub a 0 (String.length prefix) = prefix then begin
let path = String.sub a (String.length prefix) (String.length a - String.length prefix) in
let filter = Hashtbl.create 64 in
let ic = open_in path in
(try while true do
let line = input_line ic in
(* Match " FAIL: <suite> > <name>: <err>" or "FAIL: <suite> > <name>: <err>" *)
let line = String.trim line in
if String.length line > 6 && String.sub line 0 6 = "FAIL: " then begin
let rest = String.sub line 6 (String.length line - 6) in
match String.index_opt rest '>' with
| Some gt ->
let suite = String.trim (String.sub rest 0 gt) in
let after = String.sub rest (gt + 1) (String.length rest - gt - 1) in
(match String.index_opt after ':' with
| Some colon ->
let name = String.trim (String.sub after 0 colon) in
Hashtbl.replace filter (suite, name) ()
| None -> ())
| None -> ()
end
done with End_of_file -> ());
close_in ic;
Printf.eprintf "[filter] %d tests loaded from %s\n%!" (Hashtbl.length filter) path;
suite_filter := Some filter
end) args;
let test_files = List.filter (fun a ->
not (String.length a > 0 && a.[0] = '-')) args in
(* Always run foundation tests *) (* Always run foundation tests *)
run_foundation_tests (); run_foundation_tests ();

View File

@@ -77,7 +77,11 @@
((= th (quote ref)) ((= th (quote ref))
(list (quote set!) (make-symbol (nth target 1)) value)) (list (quote set!) (make-symbol (nth target 1)) value))
((= th (quote local)) ((= th (quote local))
(list (quote define) (make-symbol (nth target 1)) value)) (list
(quote hs-scoped-set!)
(quote me)
(nth target 1)
value))
((= th (quote dom-ref)) ((= th (quote dom-ref))
(list (list
(quote hs-dom-set!) (quote hs-dom-set!)
@@ -85,18 +89,18 @@
(nth target 1) (nth target 1)
value)) value))
((= th (quote me)) ((= th (quote me))
(list (quote dom-set-inner-html) (quote me) value)) (list (quote hs-set-inner-html!) (quote me) value))
((= th (quote it)) (list (quote set!) (quote it) value)) ((= th (quote it)) (list (quote set!) (quote it) value))
((= th (quote query)) ((= th (quote query))
(list (quote dom-set-inner-html) (hs-to-sx target) value)) (list (quote hs-set-inner-html!) (hs-to-sx target) value))
((= th (quote array-index)) ((= th (quote array-index))
(list (list
(quote host-set!) (quote hs-array-set!)
(hs-to-sx (nth target 1)) (hs-to-sx (nth target 1))
(hs-to-sx (nth target 2)) (hs-to-sx (nth target 2))
value)) value))
((or (= th (quote next)) (= th (quote previous)) (= th (quote closest))) ((or (= th (quote next)) (= th (quote previous)) (= th (quote closest)))
(list (quote dom-set-inner-html) (hs-to-sx target) value)) (list (quote hs-set-inner-html!) (hs-to-sx target) value))
((= th (quote of)) ((= th (quote of))
(let (let
((prop-ast (nth target 1)) (obj-ast (nth target 2))) ((prop-ast (nth target 1)) (obj-ast (nth target 2)))
@@ -162,10 +166,19 @@
(let (let
((wrapped-body (if catch-info (let ((var (make-symbol (nth catch-info 0))) (catch-body (hs-to-sx (nth catch-info 1)))) (if finally-info (list (quote do) (list (quote guard) (list var (list true catch-body)) compiled-body) (hs-to-sx finally-info)) (list (quote guard) (list var (list true catch-body)) compiled-body))) (if finally-info (list (quote do) compiled-body (hs-to-sx finally-info)) compiled-body))) ((wrapped-body (if catch-info (let ((var (make-symbol (nth catch-info 0))) (catch-body (hs-to-sx (nth catch-info 1)))) (if finally-info (list (quote do) (list (quote guard) (list var (list true catch-body)) compiled-body) (hs-to-sx finally-info)) (list (quote guard) (list var (list true catch-body)) compiled-body))) (if finally-info (list (quote do) compiled-body (hs-to-sx finally-info)) compiled-body)))
(handler (handler
(list (let
(quote fn) ((uses-the-result? (fn (expr) (cond ((= expr (quote the-result)) true) ((list? expr) (some (fn (x) (uses-the-result? x)) expr)) (true false)))))
(list (quote event)) (list
wrapped-body))) (quote fn)
(list (quote event))
(if
(uses-the-result? wrapped-body)
(list
(quote let)
(list
(list (quote the-result) nil))
wrapped-body)
wrapped-body)))))
(if (if
every? every?
(list (list
@@ -443,9 +456,7 @@
(quote __hs-new) (quote __hs-new)
(list (list
(quote +) (quote +)
(list (list (quote nth) var-sym (quote __hs-idx))
(quote hs-to-number)
(list (quote nth) var-sym (quote __hs-idx)))
amount))) amount)))
(list (list
(quote do) (quote do)
@@ -463,10 +474,7 @@
((t (hs-to-sx expr))) ((t (hs-to-sx expr)))
(list (list
(quote let) (quote let)
(list (list (list (quote __hs-new) (list (quote +) t amount)))
(list
(quote __hs-new)
(list (quote +) (list (quote hs-to-number) t) amount)))
(list (list
(quote do) (quote do)
(list (quote set!) t (quote __hs-new)) (list (quote set!) t (quote __hs-new))
@@ -564,9 +572,7 @@
(quote __hs-new) (quote __hs-new)
(list (list
(quote -) (quote -)
(list (list (quote nth) var-sym (quote __hs-idx))
(quote hs-to-number)
(list (quote nth) var-sym (quote __hs-idx)))
amount))) amount)))
(list (list
(quote do) (quote do)
@@ -584,10 +590,7 @@
((t (hs-to-sx expr))) ((t (hs-to-sx expr)))
(list (list
(quote let) (quote let)
(list (list (list (quote __hs-new) (list (quote -) t amount)))
(list
(quote __hs-new)
(list (quote -) (list (quote hs-to-number) t) amount)))
(list (list
(quote do) (quote do)
(list (quote set!) t (quote __hs-new)) (list (quote set!) t (quote __hs-new))
@@ -754,35 +757,53 @@
(hs-to-sx (nth ast 3)))) (hs-to-sx (nth ast 3))))
((= head (quote pick-first)) ((= head (quote pick-first))
(list (list
(quote hs-pick-first) (quote set!)
(hs-to-sx (nth ast 1)) (quote it)
(hs-to-sx (nth ast 2)))) (list
(quote hs-pick-first)
(hs-to-sx (nth ast 1))
(hs-to-sx (nth ast 2)))))
((= head (quote pick-last)) ((= head (quote pick-last))
(list (list
(quote hs-pick-last) (quote set!)
(hs-to-sx (nth ast 1)) (quote it)
(hs-to-sx (nth ast 2)))) (list
(quote hs-pick-last)
(hs-to-sx (nth ast 1))
(hs-to-sx (nth ast 2)))))
((= head (quote pick-random)) ((= head (quote pick-random))
(list (list
(quote hs-pick-random) (quote set!)
(hs-to-sx (nth ast 1)) (quote it)
(if (nil? (nth ast 2)) nil (hs-to-sx (nth ast 2))))) (list
(quote hs-pick-random)
(hs-to-sx (nth ast 1))
(if (nil? (nth ast 2)) nil (hs-to-sx (nth ast 2))))))
((= head (quote pick-items)) ((= head (quote pick-items))
(list (list
(quote hs-pick-items) (quote set!)
(hs-to-sx (nth ast 1)) (quote it)
(hs-to-sx (nth ast 2)) (list
(hs-to-sx (nth ast 3)))) (quote hs-pick-items)
(hs-to-sx (nth ast 1))
(hs-to-sx (nth ast 2))
(hs-to-sx (nth ast 3)))))
((= head (quote pick-match)) ((= head (quote pick-match))
(list (list
(quote regex-match) (quote set!)
(hs-to-sx (nth ast 1)) (quote it)
(hs-to-sx (nth ast 2)))) (list
(quote regex-match)
(hs-to-sx (nth ast 1))
(hs-to-sx (nth ast 2)))))
((= head (quote pick-matches)) ((= head (quote pick-matches))
(list (list
(quote regex-find-all) (quote set!)
(hs-to-sx (nth ast 1)) (quote it)
(hs-to-sx (nth ast 2)))) (list
(quote regex-find-all)
(hs-to-sx (nth ast 1))
(hs-to-sx (nth ast 2)))))
((= head (quote prop-is)) ((= head (quote prop-is))
(list (list
(quote hs-prop-is) (quote hs-prop-is)
@@ -870,6 +891,11 @@
((= head (quote ref)) (make-symbol (nth ast 1))) ((= head (quote ref)) (make-symbol (nth ast 1)))
((= head (quote query)) ((= head (quote query))
(list (quote hs-query-first) (nth ast 1))) (list (quote hs-query-first) (nth ast 1)))
((= head (quote query-scoped))
(list
(quote hs-query-all-in)
(nth ast 1)
(hs-to-sx (nth ast 2))))
((= head (quote attr)) ((= head (quote attr))
(list (list
(quote dom-get-attr) (quote dom-get-attr)
@@ -890,7 +916,8 @@
(quote dom-has-class?) (quote dom-has-class?)
(hs-to-sx (nth ast 1)) (hs-to-sx (nth ast 1))
(nth ast 2))) (nth ast 2)))
((= head (quote local)) (make-symbol (nth ast 1))) ((= head (quote local))
(list (quote hs-scoped-get) (quote me) (nth ast 1)))
((= head (quote array)) ((= head (quote array))
(cons (quote list) (map hs-to-sx (rest ast)))) (cons (quote list) (map hs-to-sx (rest ast))))
((= head (quote not)) ((= head (quote not))
@@ -1163,6 +1190,14 @@
(quote set!) (quote set!)
(hs-to-sx tgt) (hs-to-sx tgt)
(list (quote hs-add-to!) val (hs-to-sx tgt))))) (list (quote hs-add-to!) val (hs-to-sx tgt)))))
((= head (quote add-attr))
(let
((tgt (nth ast 3)))
(list
(quote hs-set-attr!)
(hs-to-sx tgt)
(nth ast 1)
(hs-to-sx (nth ast 2)))))
((= head (quote remove-value)) ((= head (quote remove-value))
(let (let
((val (hs-to-sx (nth ast 1))) (tgt (nth ast 2))) ((val (hs-to-sx (nth ast 1))) (tgt (nth ast 2)))
@@ -1296,6 +1331,20 @@
(nth ast 1) (nth ast 1)
(hs-to-sx (nth ast 2)) (hs-to-sx (nth ast 2))
(hs-to-sx (nth ast 3)))) (hs-to-sx (nth ast 3))))
((= head (quote toggle-attr-val))
(list
(quote hs-toggle-attr-val!)
(hs-to-sx (nth ast 3))
(nth ast 1)
(hs-to-sx (nth ast 2))))
((= head (quote toggle-attr-diff))
(list
(quote hs-toggle-attr-diff!)
(hs-to-sx (nth ast 5))
(nth ast 1)
(hs-to-sx (nth ast 2))
(nth ast 3)
(hs-to-sx (nth ast 4))))
((= head (quote set!)) ((= head (quote set!))
(emit-set (nth ast 1) (hs-to-sx (nth ast 2)))) (emit-set (nth ast 1) (hs-to-sx (nth ast 2))))
((= head (quote put!)) ((= head (quote put!))
@@ -1358,14 +1407,49 @@
nil)) nil))
((= head (quote hide)) ((= head (quote hide))
(let (let
((tgt (hs-to-sx (nth ast 1))) ((tgt (let ((raw-tgt (nth ast 1))) (if (and (list? raw-tgt) (= (first raw-tgt) (quote query))) (list (quote hs-query-all) (nth raw-tgt 1)) (hs-to-sx raw-tgt))))
(strategy (if (> (len ast) 2) (nth ast 2) "display"))) (strategy (if (> (len ast) 2) (nth ast 2) "display"))
(list (quote hs-hide!) tgt strategy))) (when-cond (if (> (len ast) 3) (nth ast 3) nil)))
(if
(nil? when-cond)
(list (quote hs-hide!) tgt strategy)
(list
(quote hs-hide-when!)
tgt
strategy
(list
(quote fn)
(list (quote it))
(hs-to-sx when-cond))))))
((= head (quote show)) ((= head (quote show))
(let (let
((tgt (hs-to-sx (nth ast 1))) ((tgt (let ((raw-tgt (nth ast 1))) (if (and (list? raw-tgt) (= (first raw-tgt) (quote query))) (list (quote hs-query-all) (nth raw-tgt 1)) (hs-to-sx raw-tgt))))
(strategy (if (> (len ast) 2) (nth ast 2) "display"))) (strategy (if (> (len ast) 2) (nth ast 2) "display"))
(list (quote hs-show!) tgt strategy))) (when-cond (if (> (len ast) 3) (nth ast 3) nil)))
(if
(nil? when-cond)
(list (quote hs-show!) tgt strategy)
(list
(quote let)
(list
(list
(quote __hs-show-r)
(list
(quote hs-show-when!)
tgt
strategy
(list
(quote fn)
(list (quote it))
(hs-to-sx when-cond)))))
(list
(quote begin)
(list
(quote set!)
(quote the-result)
(quote __hs-show-r))
(list (quote set!) (quote it) (quote __hs-show-r))
(quote __hs-show-r))))))
((= head (quote transition)) (emit-transition ast)) ((= head (quote transition)) (emit-transition ast))
((= head (quote transition-from)) ((= head (quote transition-from))
(let (let
@@ -1424,6 +1508,14 @@
(list (quote hs-settle) (quote me))) (list (quote hs-settle) (quote me)))
((= head (quote go)) ((= head (quote go))
(list (quote hs-navigate!) (hs-to-sx (nth ast 1)))) (list (quote hs-navigate!) (hs-to-sx (nth ast 1))))
((= head (quote __get-cmd))
(let
((val (hs-to-sx (nth ast 1))))
(list
(quote begin)
(list (quote set!) (quote the-result) val)
(list (quote set!) (quote it) val)
val)))
((= head (quote append!)) ((= head (quote append!))
(let (let
((tgt (hs-to-sx (nth ast 2))) ((tgt (hs-to-sx (nth ast 2)))
@@ -1648,11 +1740,13 @@
(list (quote hs-reset!) (hs-to-sx (nth ast 1)))) (list (quote hs-reset!) (hs-to-sx (nth ast 1))))
((= head (quote default!)) ((= head (quote default!))
(let (let
((t (hs-to-sx (nth ast 1))) (v (hs-to-sx (nth ast 2)))) ((tgt-ast (nth ast 1))
(read (hs-to-sx (nth ast 1)))
(v (hs-to-sx (nth ast 2))))
(list (list
(quote when) (quote when)
(list (quote nil?) t) (list (quote hs-default?) read)
(list (quote set!) t v)))) (emit-set tgt-ast v))))
((= head (quote hs-is)) ((= head (quote hs-is))
(list (list
(quote hs-is) (quote hs-is)

View File

@@ -16,6 +16,14 @@
(fn (fn
(sx) (sx)
(define vars (list)) (define vars (list))
(define
reserved
(list
(quote me)
(quote it)
(quote event)
(quote you)
(quote yourself)))
(define (define
walk walk
(fn (fn
@@ -30,7 +38,9 @@
(let (let
((name (nth node 1))) ((name (nth node 1)))
(when (when
(not (some (fn (v) (= v name)) vars)) (and
(not (some (fn (v) (= v name)) vars))
(not (some (fn (v) (= v name)) reserved)))
(set! vars (cons name vars))))) (set! vars (cons name vars)))))
(for-each walk node)))) (for-each walk node))))
(walk sx) (walk sx)
@@ -67,9 +77,10 @@
(fn (fn
(el) (el)
(let (let
((src (dom-get-attr el "_"))) ((src (dom-get-attr el "_")) (prev (dom-get-data el "hs-script")))
(when (when
(and src (not (dom-get-data el "hs-active"))) (and src (not (= src prev)))
(dom-set-data el "hs-script" src)
(dom-set-data el "hs-active" true) (dom-set-data el "hs-active" true)
(let ((handler (hs-handler src))) (handler el)))))) (let ((handler (hs-handler src))) (handler el))))))
@@ -77,6 +88,21 @@
;; Called once at page load. Finds all elements with _ attribute, ;; Called once at page load. Finds all elements with _ attribute,
;; compiles their hyperscript, and activates them. ;; compiles their hyperscript, and activates them.
(define
hs-deactivate!
(fn
(el)
(let
((unlisteners (or (dom-get-data el "hs-unlisteners") (list))))
(for-each (fn (u) (when u (u))) unlisteners)
(dom-set-data el "hs-unlisteners" (list))
(dom-set-data el "hs-active" false)
(dom-set-data el "hs-script" nil))))
;; ── Boot subtree: for dynamic content ───────────────────────────
;; Called after HTMX swaps or dynamic DOM insertion.
;; Only activates elements within the given root.
(define (define
hs-boot! hs-boot!
(fn (fn
@@ -85,10 +111,6 @@
((elements (dom-query-all (host-get (host-global "document") "body") "[_]"))) ((elements (dom-query-all (host-get (host-global "document") "body") "[_]")))
(for-each (fn (el) (hs-activate! el)) elements)))) (for-each (fn (el) (hs-activate! el)) elements))))
;; ── Boot subtree: for dynamic content ───────────────────────────
;; Called after HTMX swaps or dynamic DOM insertion.
;; Only activates elements within the given root.
(define (define
hs-boot-subtree! hs-boot-subtree!
(fn (fn

View File

@@ -95,6 +95,13 @@
(do (adv!) (list kind (str "." val) (list (quote me))))) (do (adv!) (list kind (str "." val) (list (quote me)))))
((= typ "id") ((= typ "id")
(do (adv!) (list kind (str "#" val) (list (quote me))))) (do (adv!) (list kind (str "#" val) (list (quote me)))))
((= typ "attr")
(do
(adv!)
(list
(quote attr)
val
(list kind (str "[" val "]") (list (quote me))))))
(true (list kind "*" (list (quote me)))))))) (true (list kind "*" (list (quote me))))))))
(define (define
parse-pos-kw parse-pos-kw
@@ -146,8 +153,10 @@
(do (adv!) (list (quote me)))) (do (adv!) (list (quote me))))
((and (= typ "keyword") (= val "I")) ((and (= typ "keyword") (= val "I"))
(do (adv!) (list (quote me)))) (do (adv!) (list (quote me))))
((and (= typ "keyword") (or (= val "it") (= val "result"))) ((and (= typ "keyword") (= val "it"))
(do (adv!) (list (quote it)))) (do (adv!) (list (quote it))))
((and (= typ "keyword") (= val "result"))
(do (adv!) (quote the-result)))
((and (= typ "keyword") (= val "event")) ((and (= typ "keyword") (= val "event"))
(do (adv!) (list (quote event)))) (do (adv!) (list (quote event))))
((and (= typ "keyword") (= val "target")) ((and (= typ "keyword") (= val "target"))
@@ -174,7 +183,18 @@
(do (adv!) (parse-pos-kw (quote last)))) (do (adv!) (parse-pos-kw (quote last))))
((= typ "id") ((= typ "id")
(do (adv!) (list (quote query) (str "#" val)))) (do (adv!) (list (quote query) (str "#" val))))
((= typ "selector") (do (adv!) (list (quote query) val))) ((= typ "selector")
(do
(adv!)
(if
(and (= (tp-type) "keyword") (= (tp-val) "in"))
(do
(adv!)
(list
(quote query-scoped)
val
(parse-cmp (parse-arith (parse-poss (parse-atom))))))
(list (quote query) val))))
((= typ "attr") ((= typ "attr")
(do (adv!) (list (quote attr) val (list (quote me))))) (do (adv!) (list (quote attr) val (list (quote me)))))
((= typ "style") ((= typ "style")
@@ -426,7 +446,7 @@
(list (quote type-check) left type-name))))))) (list (quote type-check) left type-name)))))))
(true (true
(let (let
((right (parse-expr))) ((right (parse-cmp (parse-arith (parse-poss (parse-atom))))))
(if (if
(match-kw "ignoring") (match-kw "ignoring")
(do (do
@@ -530,6 +550,14 @@
(quote and) (quote and)
(list (quote >=) left lo) (list (quote >=) left lo)
(list (quote <=) left hi)))))) (list (quote <=) left hi))))))
((or (and (or (= (tp-val) "a") (= (tp-val) "an")) (do (adv!) true)))
(let
((type-name (tp-val)))
(do
(adv!)
(list
(quote not)
(list (quote type-check) left type-name)))))
(true (true
(let (let
((right (parse-expr))) ((right (parse-expr)))
@@ -546,6 +574,10 @@
(quote and) (quote and)
(list (quote >=) left lo) (list (quote >=) left lo)
(list (quote <=) left hi))))) (list (quote <=) left hi)))))
((or (and (or (= (tp-val) "a") (= (tp-val) "an")) (do (adv!) true)))
(let
((type-name (tp-val)))
(do (adv!) (list (quote type-check) left type-name))))
(true (true
(let (let
((right (parse-expr))) ((right (parse-expr)))
@@ -576,7 +608,7 @@
(match-kw "case") (match-kw "case")
(list (quote ends-with-ic?) left rhs)) (list (quote ends-with-ic?) left rhs))
(list (quote ends-with?) left rhs))))) (list (quote ends-with?) left rhs)))))
((and (= typ "keyword") (= val "matches")) ((and (= typ "keyword") (or (= val "matches") (= val "match")))
(do (do
(adv!) (adv!)
(let (let
@@ -618,7 +650,22 @@
(quote as) (quote as)
left left
(str type-name ":" param))))) (str type-name ":" param)))))
(list (quote as) left type-name)))))) (let
loop
((result (list (quote as) left type-name)))
(if
(and (= (tp-type) "op") (= (tp-val) "|"))
(do
(adv!)
(when
(or (= (tp-val) "a") (= (tp-val) "an"))
(adv!))
(let
((next-type (tp-val)))
(do
(adv!)
(loop (list (quote as) result next-type)))))
result)))))))
((and (= typ "colon")) ((and (= typ "colon"))
(do (do
(adv!) (adv!)
@@ -693,7 +740,7 @@
(list (quote strict-eq) left (parse-expr)))) (list (quote strict-eq) left (parse-expr))))
((and (= typ "keyword") (or (= val "contain") (= val "include") (= val "includes"))) ((and (= typ "keyword") (or (= val "contain") (= val "include") (= val "includes")))
(do (adv!) (list (quote contains?) left (parse-expr)))) (do (adv!) (list (quote contains?) left (parse-expr))))
((and (= typ "keyword") (= val "precedes")) ((and (= typ "keyword") (or (= val "precedes") (= val "precede")))
(do (adv!) (list (quote precedes?) left (parse-atom)))) (do (adv!) (list (quote precedes?) left (parse-atom))))
((and (= typ "keyword") (= val "follows")) ((and (= typ "keyword") (= val "follows"))
(do (adv!) (list (quote follows?) left (parse-atom)))) (do (adv!) (list (quote follows?) left (parse-atom))))
@@ -772,7 +819,7 @@
(= (tp-val) "starts") (= (tp-val) "starts")
(= (tp-val) "ends") (= (tp-val) "ends")
(= (tp-val) "contains") (= (tp-val) "contains")
(= (tp-val) "matches") (or (= (tp-val) "matches") (= (tp-val) "match"))
(= (tp-val) "is") (= (tp-val) "is")
(= (tp-val) "does") (= (tp-val) "does")
(= (tp-val) "in") (= (tp-val) "in")
@@ -892,6 +939,18 @@
(let (let
((tgt (if (match-kw "to") (parse-expr) (list (quote me))))) ((tgt (if (match-kw "to") (parse-expr) (list (quote me)))))
(list (quote set-styles) (reverse pairs) tgt))))) (list (quote set-styles) (reverse pairs) tgt)))))
((and (= (tp-type) "bracket-open") (> (len tokens) (+ p 1)) (= (get (nth tokens (+ p 1)) "type") "attr"))
(do
(adv!)
(let
((attr-name (get (adv!) "value")))
(when (and (= (tp-type) "op") (= (tp-val) "=")) (adv!))
(let
((attr-val (parse-expr)))
(when (= (tp-type) "bracket-close") (adv!))
(let
((tgt (parse-tgt-kw "to" (list (quote me)))))
(list (quote add-attr) attr-name attr-val tgt))))))
(true (true
(let (let
((value (parse-expr))) ((value (parse-expr)))
@@ -978,20 +1037,58 @@
() ()
(cond (cond
((match-kw "between") ((match-kw "between")
(if (cond
(= (tp-type) "class") ((= (tp-type) "class")
(let (let
((cls1 (do (let ((v (tp-val))) (adv!) v)))) ((cls1 (do (let ((v (tp-val))) (adv!) v))))
(expect-kw! "and") (expect-kw! "and")
(if (if
(= (tp-type) "class") (= (tp-type) "class")
(let
((cls2 (do (let ((v (tp-val))) (adv!) v))))
(let (let
((tgt (parse-tgt-kw "on" (list (quote me))))) ((cls2 (do (let ((v (tp-val))) (adv!) v))))
(list (quote toggle-between) cls1 cls2 tgt))) (let
nil)) ((tgt (parse-tgt-kw "on" (list (quote me)))))
nil)) (list (quote toggle-between) cls1 cls2 tgt)))
nil)))
((and (= (tp-type) "bracket-open") (> (len tokens) (+ p 1)) (= (get (nth tokens (+ p 1)) "type") "attr"))
(do
(adv!)
(let
((n1 (get (adv!) "value")))
(when
(and (= (tp-type) "op") (= (tp-val) "="))
(adv!))
(let
((v1 (parse-expr)))
(when (= (tp-type) "bracket-close") (adv!))
(expect-kw! "and")
(when (= (tp-type) "bracket-open") (adv!))
(let
((n2 (get (adv!) "value")))
(when
(and (= (tp-type) "op") (= (tp-val) "="))
(adv!))
(let
((v2 (parse-expr)))
(when (= (tp-type) "bracket-close") (adv!))
(let
((tgt (parse-tgt-kw "on" (list (quote me)))))
(if
(= n1 n2)
(list
(quote toggle-attr-between)
n1
v1
v2
tgt)
(list
(quote toggle-attr-diff)
n1
v1
n2
v2
tgt)))))))))
(true nil)))
((= (tp-type) "class") ((= (tp-type) "class")
(let (let
((cls (do (let ((v (tp-val))) (adv!) v)))) ((cls (do (let ((v (tp-val))) (adv!) v))))
@@ -1012,38 +1109,67 @@
(match-kw "between") (match-kw "between")
(let (let
((val1 (parse-atom))) ((val1 (parse-atom)))
(expect-kw! "and") (do
(let (when (= (tp-type) "comma") (adv!))
((val2 (parse-atom)))
(if (if
(match-kw "and") (and (= (tp-type) "keyword") (= (tp-val) "and"))
(let (adv!)
((val3 (parse-atom))) nil)
(if (let
(match-kw "and") ((val2 (parse-atom)))
(if
(or
(= (tp-type) "comma")
(and
(= (tp-type) "keyword")
(= (tp-val) "and")))
(do
(when (= (tp-type) "comma") (adv!))
(if
(and
(= (tp-type) "keyword")
(= (tp-val) "and"))
(adv!)
nil)
(let (let
((val4 (parse-atom))) ((val3 (parse-atom)))
(list (if
(quote toggle-style-cycle) (or
prop (= (tp-type) "comma")
tgt (and
val1 (= (tp-type) "keyword")
val2 (= (tp-val) "and")))
val3 (do
val4)) (when (= (tp-type) "comma") (adv!))
(list (if
(quote toggle-style-cycle) (and
prop (= (tp-type) "keyword")
tgt (= (tp-val) "and"))
val1 (adv!)
val2 nil)
val3))) (let
(list ((val4 (parse-atom)))
(quote toggle-style-between) (list
prop (quote toggle-style-cycle)
val1 prop
val2 tgt
tgt)))) val1
val2
val3
val4)))
(list
(quote toggle-style-cycle)
prop
tgt
val1
val2
val3))))
(list
(quote toggle-style-between)
prop
val1
val2
tgt)))))
(list (quote toggle-style) prop tgt))))) (list (quote toggle-style) prop tgt)))))
((= (tp-type) "attr") ((= (tp-type) "attr")
(let (let
@@ -1064,6 +1190,18 @@
val2 val2
tgt))) tgt)))
(list (quote toggle-attr) attr-name tgt))))) (list (quote toggle-attr) attr-name tgt)))))
((and (= (tp-type) "bracket-open") (> (len tokens) (+ p 1)) (= (get (nth tokens (+ p 1)) "type") "attr"))
(do
(adv!)
(let
((attr-name (get (adv!) "value")))
(when (and (= (tp-type) "op") (= (tp-val) "=")) (adv!))
(let
((attr-val (parse-expr)))
(when (= (tp-type) "bracket-close") (adv!))
(let
((tgt (parse-tgt-kw "on" (list (quote me)))))
(list (quote toggle-attr-val) attr-name attr-val tgt))))))
((and (= (tp-type) "keyword") (= (tp-val) "my")) ((and (= (tp-type) "keyword") (= (tp-val) "my"))
(do (do
(adv!) (adv!)
@@ -1338,19 +1476,23 @@
(fn (fn
() ()
(let (let
((tgt (cond ((at-end?) (list (quote me))) ((and (= (tp-type) "keyword") (or (= (tp-val) "then") (= (tp-val) "end") (= (tp-val) "with") (= (tp-val) "add") (= (tp-val) "remove") (= (tp-val) "set") (= (tp-val) "put") (= (tp-val) "toggle") (= (tp-val) "hide") (= (tp-val) "show"))) (list (quote me))) (true (parse-expr))))) ((tgt (cond ((at-end?) (list (quote me))) ((and (= (tp-type) "keyword") (or (= (tp-val) "then") (= (tp-val) "end") (= (tp-val) "with") (= (tp-val) "when") (= (tp-val) "add") (= (tp-val) "remove") (= (tp-val) "set") (= (tp-val) "put") (= (tp-val) "toggle") (= (tp-val) "hide") (= (tp-val) "show"))) (list (quote me))) (true (parse-expr)))))
(let (let
((strategy (if (match-kw "with") (if (at-end?) "display" (let ((s (tp-val))) (adv!) s)) "display"))) ((strategy (if (match-kw "with") (if (at-end?) "display" (let ((s (tp-val))) (do (adv!) (cond ((at-end?) s) ((= (tp-type) "colon") (do (adv!) (let ((v (tp-val))) (do (adv!) (str s ":" v))))) ((= (tp-type) "local") (let ((v (tp-val))) (do (adv!) (str s ":" v)))) (true s))))) "display")))
(list (quote hide) tgt strategy))))) (let
((when-cond (if (and (= (tp-type) "keyword") (= (tp-val) "when")) (do (adv!) (parse-expr)) nil)))
(list (quote hide) tgt strategy when-cond))))))
(define (define
parse-show-cmd parse-show-cmd
(fn (fn
() ()
(let (let
((tgt (cond ((at-end?) (list (quote me))) ((and (= (tp-type) "keyword") (or (= (tp-val) "then") (= (tp-val) "end") (= (tp-val) "with") (= (tp-val) "add") (= (tp-val) "remove") (= (tp-val) "set") (= (tp-val) "put") (= (tp-val) "toggle") (= (tp-val) "hide") (= (tp-val) "show"))) (list (quote me))) (true (parse-expr))))) ((tgt (cond ((at-end?) (list (quote me))) ((and (= (tp-type) "keyword") (or (= (tp-val) "then") (= (tp-val) "end") (= (tp-val) "with") (= (tp-val) "when") (= (tp-val) "add") (= (tp-val) "remove") (= (tp-val) "set") (= (tp-val) "put") (= (tp-val) "toggle") (= (tp-val) "hide") (= (tp-val) "show"))) (list (quote me))) (true (parse-expr)))))
(let (let
((strategy (if (match-kw "with") (if (at-end?) "display" (let ((s (tp-val))) (adv!) s)) "display"))) ((strategy (if (match-kw "with") (if (at-end?) "display" (let ((s (tp-val))) (do (adv!) (cond ((at-end?) s) ((= (tp-type) "colon") (do (adv!) (let ((v (tp-val))) (do (adv!) (str s ":" v))))) ((= (tp-type) "local") (let ((v (tp-val))) (do (adv!) (str s ":" v)))) (true s))))) "display")))
(list (quote show) tgt strategy))))) (let
((when-cond (if (and (= (tp-type) "keyword") (= (tp-val) "when")) (do (adv!) (parse-expr)) nil)))
(list (quote show) tgt strategy when-cond))))))
(define (define
parse-transition-cmd parse-transition-cmd
(fn (fn
@@ -1493,7 +1635,7 @@
(ca-collect (append acc (list arg))))))) (ca-collect (append acc (list arg)))))))
(ca-collect (list)))) (ca-collect (list))))
(define parse-call-cmd (fn () (parse-expr))) (define parse-call-cmd (fn () (parse-expr)))
(define parse-get-cmd (fn () (parse-expr))) (define parse-get-cmd (fn () (list (quote __get-cmd) (parse-expr))))
(define (define
parse-take-cmd parse-take-cmd
(fn (fn
@@ -1501,12 +1643,34 @@
(cond (cond
((= (tp-type) "class") ((= (tp-type) "class")
(let (let
((cls (do (let ((v (tp-val))) (adv!) v)))) ((classes (list)))
(let (let
((from-sel (if (match-kw "from") (parse-expr) nil))) ((collect (fn () (when (= (tp-type) "class") (let ((v (tp-val))) (adv!) (set! classes (append classes (list v))) (collect))))))
(collect)
(let (let
((for-tgt (if (match-kw "for") (parse-expr) nil))) ((from-sel (if (match-kw "from") (parse-expr) nil)))
(list (quote take!) "class" cls from-sel for-tgt))))) (let
((for-tgt (if (match-kw "for") (parse-expr) nil)))
(if
(= (len classes) 1)
(list
(quote take!)
"class"
(first classes)
from-sel
for-tgt)
(cons
(quote do)
(map
(fn
(cls)
(list
(quote take!)
"class"
cls
from-sel
for-tgt))
classes))))))))
((= (tp-type) "attr") ((= (tp-type) "attr")
(let (let
((attr-name (get (adv!) "value"))) ((attr-name (get (adv!) "value")))
@@ -1540,7 +1704,9 @@
(let (let
((n (parse-atom))) ((n (parse-atom)))
(do (do
(expect-kw! "of") (if
(not (or (match-kw "of") (match-kw "from")))
(error (str "Expected 'of' or 'from' at position " p)))
(let (let
((coll (parse-expr))) ((coll (parse-expr)))
(list (quote pick-first) coll n)))))) (list (quote pick-first) coll n))))))
@@ -1550,7 +1716,9 @@
(let (let
((n (parse-atom))) ((n (parse-atom)))
(do (do
(expect-kw! "of") (if
(not (or (match-kw "of") (match-kw "from")))
(error (str "Expected 'of' or 'from' at position " p)))
(let (let
((coll (parse-expr))) ((coll (parse-expr)))
(list (quote pick-last) coll n)))))) (list (quote pick-last) coll n))))))
@@ -1558,14 +1726,17 @@
(do (do
(adv!) (adv!)
(if (if
(match-kw "of") (or (match-kw "of") (match-kw "from"))
(let (let
((coll (parse-expr))) ((coll (parse-expr)))
(list (quote pick-random) coll nil)) (list (quote pick-random) coll nil))
(let (let
((n (parse-atom))) ((n (parse-atom)))
(do (do
(expect-kw! "of") (if
(not (or (match-kw "of") (match-kw "from")))
(error
(str "Expected 'of' or 'from' at position " p)))
(let (let
((coll (parse-expr))) ((coll (parse-expr)))
(list (quote pick-random) coll n))))))) (list (quote pick-random) coll n)))))))
@@ -1579,7 +1750,10 @@
(let (let
((end-expr (parse-atom))) ((end-expr (parse-atom)))
(do (do
(expect-kw! "of") (if
(not (or (match-kw "of") (match-kw "from")))
(error
(str "Expected 'of' or 'from' at position " p)))
(let (let
((coll (parse-expr))) ((coll (parse-expr)))
(list (quote pick-items) coll start-expr end-expr)))))))) (list (quote pick-items) coll start-expr end-expr))))))))
@@ -1588,7 +1762,7 @@
(adv!) (adv!)
(expect-kw! "of") (expect-kw! "of")
(let (let
((regex (parse-expr))) ((regex (parse-atom)))
(do (do
(cond (cond
((match-kw "of") nil) ((match-kw "of") nil)
@@ -1606,7 +1780,7 @@
(adv!) (adv!)
(expect-kw! "of") (expect-kw! "of")
(let (let
((regex (parse-expr))) ((regex (parse-atom)))
(do (do
(cond (cond
((match-kw "of") nil) ((match-kw "of") nil)
@@ -1619,10 +1793,26 @@
(let (let
((haystack (parse-expr))) ((haystack (parse-expr)))
(list (quote pick-matches) regex haystack)))))) (list (quote pick-matches) regex haystack))))))
((and (= typ "ident") (= val "item"))
(do
(adv!)
(let
((n (parse-expr)))
(do
(if
(not (or (match-kw "of") (match-kw "from")))
(error (str "Expected 'of' or 'from' at position " p)))
(let
((coll (parse-expr)))
(list
(quote pick-items)
coll
n
(list (quote +) n 1)))))))
(true (true
(error (error
(str (str
"Expected first/last/random/items/match/matches after 'pick' at " "Expected first/last/random/item/items/match/matches after 'pick' at "
p))))))) p)))))))
(define (define
parse-go-cmd parse-go-cmd
@@ -1697,7 +1887,7 @@
(match-kw "of") (match-kw "of")
(list (make-symbol ".") (parse-expr) val) (list (make-symbol ".") (parse-expr) val)
(cond (cond
((= val "result") (list (quote it))) ((= val "result") (quote the-result))
((= val "first") (parse-pos-kw (quote first))) ((= val "first") (parse-pos-kw (quote first)))
((= val "last") (parse-pos-kw (quote last))) ((= val "last") (parse-pos-kw (quote last)))
((= val "closest") (parse-trav (quote closest))) ((= val "closest") (parse-trav (quote closest)))

View File

@@ -22,7 +22,13 @@
;; Stock hyperscript queues by default; "every" disables queuing. ;; Stock hyperscript queues by default; "every" disables queuing.
(define (define
hs-on hs-on
(fn (target event-name handler) (dom-listen target event-name handler))) (fn
(target event-name handler)
(let
((unlisten (dom-listen target event-name handler))
(prev (or (dom-get-data target "hs-unlisteners") (list))))
(dom-set-data target "hs-unlisteners" (append prev (list unlisten)))
unlisten)))
;; Run an initializer function immediately. ;; Run an initializer function immediately.
;; (hs-init thunk) — called at element boot time ;; (hs-init thunk) — called at element boot time
@@ -88,7 +94,7 @@
((or (= prop "display") (= prop "opacity")) ((or (= prop "display") (= prop "opacity"))
(if (if
(or (= cur "none") (= cur "0")) (or (= cur "none") (= cur "0"))
(dom-set-style target prop (if (= prop "opacity") "1" "")) (dom-set-style target prop (if (= prop "opacity") "1" "block"))
(dom-set-style target prop (if (= prop "display") "none" "0")))) (dom-set-style target prop (if (= prop "display") "none" "0"))))
(true (true
(if (if
@@ -167,6 +173,45 @@
(fn (fn
(el name val) (el name val)
(if (nil? val) (dom-remove-attr el name) (dom-set-attr el name val)))) (if (nil? val) (dom-remove-attr el name) (dom-set-attr el name val))))
(define
hs-toggle-attr!
(fn
(el name)
(if
(dom-has-attr? el name)
(dom-remove-attr el name)
(dom-set-attr el name ""))))
(define
hs-toggle-attr-val!
(fn
(el name val)
(if
(= (dom-get-attr el name) val)
(dom-remove-attr el name)
(dom-set-attr el name val))))
(define
hs-toggle-attr-between!
(fn
(el name val1 val2)
(if
(= (dom-get-attr el name) val1)
(dom-set-attr el name val2)
(dom-set-attr el name val1))))
(define
hs-toggle-attr-diff!
(fn
(el n1 v1 n2 v2)
(if
(dom-has-attr? el n1)
(do (dom-remove-attr el n1) (dom-set-attr el n2 v2))
(do
(when (dom-has-attr? el n2) (dom-remove-attr el n2))
(dom-set-attr el n1 v1)))))
(define
hs-set-inner-html!
(fn
(target value)
(do (dom-set-inner-html target value) (hs-boot-subtree! target))))
(define (define
hs-put! hs-put!
(fn (fn
@@ -407,19 +452,24 @@
hs-query-all hs-query-all
(fn (sel) (host-call (dom-body) "querySelectorAll" sel))) (fn (sel) (host-call (dom-body) "querySelectorAll" sel)))
(define
hs-query-all-in
(fn
(sel target)
(if
(nil? target)
(hs-query-all sel)
(host-call target "querySelectorAll" sel))))
(define (define
hs-list-set hs-list-set
(fn (lst idx val) (map-indexed (fn (i x) (if (= i idx) val x)) lst))) (fn
(lst idx val)
(append (take lst idx) (cons val (drop lst (+ idx 1))))))
(define (define
hs-to-number hs-to-number
(fn (fn (v) (if (number? v) v (or (parse-number (str v)) 0))))
(v)
(cond
((number? v) v)
((string? v) (or (parse-number v) 0))
((nil? v) 0)
(true (or (parse-number (str v)) 0)))))
(define (define
hs-query-first hs-query-first
@@ -490,6 +540,10 @@
((= signal "hs-continue") (hs-repeat-while cond-fn thunk)) ((= signal "hs-continue") (hs-repeat-while cond-fn thunk))
(true (hs-repeat-while cond-fn thunk))))))) (true (hs-repeat-while cond-fn thunk)))))))
(define (define
hs-repeat-until hs-repeat-until
(fn (fn
@@ -502,10 +556,6 @@
(if (cond-fn) nil (hs-repeat-until cond-fn thunk))) (if (cond-fn) nil (hs-repeat-until cond-fn thunk)))
(true (if (cond-fn) nil (hs-repeat-until cond-fn thunk))))))) (true (if (cond-fn) nil (hs-repeat-until cond-fn thunk)))))))
(define (define
hs-for-each hs-for-each
(fn (fn
@@ -525,27 +575,38 @@
((= signal "hs-continue") (do-loop (rest remaining))) ((= signal "hs-continue") (do-loop (rest remaining)))
(true (do-loop (rest remaining)))))))) (true (do-loop (rest remaining))))))))
(do-loop items)))) (do-loop items))))
;; ── Sandbox/test runtime additions ──────────────────────────────
;; Property access — dot notation and .length
(begin (begin
(define (define
hs-append hs-append
(fn (fn
(target value) (target value)
(cond (cond
((nil? target) value)
((string? target) (str target value)) ((string? target) (str target value))
((list? target) (append target (list value))) ((list? target) (append target (list value)))
((hs-element? target)
(do
(dom-insert-adjacent-html target "beforeend" (str value))
target))
(true (str target value))))) (true (str target value)))))
(define (define
hs-append! hs-append!
(fn (value target) (dom-insert-adjacent-html target "beforeend" value)))) (fn
;; ── Sandbox/test runtime additions ────────────────────────────── (value target)
;; Property access — dot notation and .length (cond
((nil? target) nil)
((hs-element? target)
(dom-insert-adjacent-html target "beforeend" (str value)))
(true nil)))))
;; DOM query stub — sandbox returns empty list
(define (define
hs-fetch hs-fetch
(fn (fn
(url format) (url format)
(perform (list "io-fetch" url (if format format "text"))))) (perform (list "io-fetch" url (if format format "text")))))
;; DOM query stub — sandbox returns empty list ;; Method dispatch — obj.method(args)
(define (define
hs-coerce hs-coerce
(fn (fn
@@ -636,7 +697,24 @@
(map (fn (k) (list k (get value k))) (keys value)) (map (fn (k) (list k (get value k))) (keys value))
value)) value))
(true value)))) (true value))))
;; Method dispatch — obj.method(args)
;; ── 0.9.90 features ─────────────────────────────────────────────
;; beep! — debug logging, returns value unchanged
(define
hs-default?
(fn
(v)
(cond
((nil? v) true)
((and (string? v) (= v "")) true)
(true false))))
;; Property-based is — check obj.key truthiness
(define
hs-array-set!
(fn
(arr i v)
(if (list? arr) (do (set-nth! arr i v) v) (host-set! arr i v))))
;; Array slicing (inclusive both ends)
(define (define
hs-add hs-add
(fn (fn
@@ -646,9 +724,7 @@
((list? b) (cons a b)) ((list? b) (cons a b))
((or (string? a) (string? b)) (str a b)) ((or (string? a) (string? b)) (str a b))
(true (+ a b))))) (true (+ a b)))))
;; Collection: sorted by
;; ── 0.9.90 features ─────────────────────────────────────────────
;; beep! — debug logging, returns value unchanged
(define (define
hs-make hs-make
(fn (fn
@@ -659,13 +735,13 @@
((= type-name "Set") (list)) ((= type-name "Set") (list))
((= type-name "Map") (dict)) ((= type-name "Map") (dict))
(true (dict))))) (true (dict)))))
;; Property-based is — check obj.key truthiness ;; Collection: sorted by descending
(define hs-install (fn (behavior-fn) (behavior-fn me))) (define hs-install (fn (behavior-fn) (behavior-fn me)))
;; Array slicing (inclusive both ends) ;; Collection: split by
(define (define
hs-measure hs-measure
(fn (target) (perform (list (quote io-measure) target)))) (fn (target) (perform (list (quote io-measure) target))))
;; Collection: sorted by ;; Collection: joined by
(define (define
hs-transition hs-transition
(fn (fn
@@ -678,7 +754,7 @@
(str prop " " (/ duration 1000) "s"))) (str prop " " (/ duration 1000) "s")))
(dom-set-style target prop value) (dom-set-style target prop value)
(when duration (hs-settle target)))) (when duration (hs-settle target))))
;; Collection: sorted by descending
(define (define
hs-transition-from hs-transition-from
(fn (fn
@@ -692,7 +768,7 @@
(str prop " " (/ duration 1000) "s"))) (str prop " " (/ duration 1000) "s")))
(dom-set-style target prop (str to-val)) (dom-set-style target prop (str to-val))
(when duration (hs-settle target)))) (when duration (hs-settle target))))
;; Collection: split by
(define (define
hs-type-check hs-type-check
(fn (fn
@@ -712,7 +788,7 @@
(= (host-typeof value) "element") (= (host-typeof value) "element")
(= (host-typeof value) "text"))) (= (host-typeof value) "text")))
(true (= (host-typeof value) (downcase type-name))))))) (true (= (host-typeof value) (downcase type-name)))))))
;; Collection: joined by
(define (define
hs-type-check-strict hs-type-check-strict
(fn (fn
@@ -745,11 +821,26 @@
((nil? suffix) false) ((nil? suffix) false)
(true (ends-with? (str s) (str suffix)))))) (true (ends-with? (str s) (str suffix))))))
(define
hs-scoped-set!
(fn (el name val) (dom-set-data el (str "hs-local-" name) val)))
(define
hs-scoped-get
(fn (el name) (dom-get-data el (str "hs-local-" name))))
(define (define
hs-precedes? hs-precedes?
(fn (fn
(a b) (a b)
(cond ((nil? a) false) ((nil? b) false) (true (< (str a) (str b)))))) (cond
((nil? a) false)
((nil? b) false)
((and (dict? a) (dict? b))
(let
((pos (host-call a "compareDocumentPosition" b)))
(if (number? pos) (not (= 0 (mod (/ pos 4) 2))) false)))
(true (< (str a) (str b))))))
(define (define
hs-follows? hs-follows?
@@ -840,7 +931,18 @@
(= obj (nth r 1)) (= obj (nth r 1))
(= obj nil))))))) (= obj nil)))))))
(define precedes? (fn (a b) (< (str a) (str b)))) (define
precedes?
(fn
(a b)
(cond
((nil? a) false)
((nil? b) false)
((and (dict? a) (dict? b))
(let
((pos (host-call a "compareDocumentPosition" b)))
(if (number? pos) (not (= 0 (mod (/ pos 4) 2))) false)))
(true (< (str a) (str b))))))
(define (define
hs-empty? hs-empty?
@@ -1124,33 +1226,109 @@
(host-call el "removeAttribute" "open") (host-call el "removeAttribute" "open")
(dom-set-prop el "open" false))))))) (dom-set-prop el "open" false)))))))
(define (begin
hs-hide! (define
(fn hs-hide-one!
(el strategy) (fn
(let (el strategy)
((tag (dom-get-prop el "tagName"))) (let
(cond ((parts (split strategy ":")) (tag (dom-get-prop el "tagName")))
((= tag "DIALOG") (let
(when (dom-has-attr? el "open") (host-call el "close"))) ((prop (first parts))
((= tag "DETAILS") (dom-set-prop el "open" false)) (val (if (> (len parts) 1) (nth parts 1) nil)))
((= strategy "opacity") (dom-set-style el "opacity" "0")) (cond
((= strategy "visibility") (dom-set-style el "visibility" "hidden")) ((= tag "DIALOG")
(true (dom-set-style el "display" "none")))))) (when (dom-has-attr? el "open") (host-call el "close")))
((= tag "DETAILS") (dom-set-prop el "open" false))
((= prop "opacity")
(dom-set-style el "opacity" (if val val "0")))
((= prop "visibility")
(dom-set-style el "visibility" (if val val "hidden")))
((= prop "hidden") (dom-set-attr el "hidden" ""))
((= prop "twDisplay") (dom-add-class el "hidden"))
((= prop "twVisibility") (dom-add-class el "invisible"))
((= prop "twOpacity") (dom-add-class el "opacity-0"))
(true (dom-set-style el "display" (if val val "none"))))))))
(define
hs-hide!
(fn
(target strategy)
(if
(list? target)
(do (for-each (fn (el) (hs-hide-one! el strategy)) target) target)
(do (hs-hide-one! target strategy) target)))))
(begin
(define
hs-show-one!
(fn
(el strategy)
(let
((parts (split strategy ":")) (tag (dom-get-prop el "tagName")))
(let
((prop (first parts))
(val (if (> (len parts) 1) (nth parts 1) nil)))
(cond
((= tag "DIALOG")
(when
(not (dom-has-attr? el "open"))
(host-call el "showModal")))
((= tag "DETAILS") (dom-set-prop el "open" true))
((= prop "opacity")
(dom-set-style el "opacity" (if val val "1")))
((= prop "visibility")
(dom-set-style el "visibility" (if val val "visible")))
((= prop "hidden") (dom-remove-attr el "hidden"))
((= prop "twDisplay") (dom-remove-class el "hidden"))
((= prop "twVisibility") (dom-remove-class el "invisible"))
((= prop "twOpacity") (dom-remove-class el "opacity-0"))
(true (dom-set-style el "display" (if val val "block"))))))))
(define
hs-show!
(fn
(target strategy)
(if
(list? target)
(do (for-each (fn (el) (hs-show-one! el strategy)) target) target)
(do (hs-show-one! target strategy) target)))))
(define (define
hs-show! hs-show-when!
(fn (fn
(el strategy) (target strategy pred)
(let (let
((tag (dom-get-prop el "tagName"))) ((items (if (list? target) target (list target))))
(cond (let
((= tag "DIALOG") ((matched (list)))
(when (not (dom-has-attr? el "open")) (host-call el "showModal"))) (do
((= tag "DETAILS") (dom-set-prop el "open" true)) (for-each
((= strategy "opacity") (dom-set-style el "opacity" "1")) (fn
((= strategy "visibility") (dom-set-style el "visibility" "visible")) (el)
(true (dom-set-style el "display" "")))))) (if
(pred el)
(do (hs-show-one! el strategy) (append! matched el))
(hs-hide-one! el strategy)))
items)
matched)))))
(define
hs-hide-when!
(fn
(target strategy pred)
(let
((items (if (list? target) target (list target))))
(let
((matched (list)))
(do
(for-each
(fn
(el)
(if
(pred el)
(do (hs-hide-one! el strategy) (append! matched el))
(hs-show-one! el strategy)))
items)
matched)))))
(define hs-first (fn (lst) (first lst))) (define hs-first (fn (lst) (first lst)))
@@ -1390,7 +1568,7 @@
false false
(let (let
((store (host-get el "__hs_vars"))) ((store (host-get el "__hs_vars")))
(if (nil? store) false (has-key? store name)))))) (if (nil? store) false (host-call store "hasOwnProperty" name))))))
(define (define
hs-dom-get-var-raw hs-dom-get-var-raw
@@ -1409,7 +1587,7 @@
(do (do
(when (when
(nil? (host-get el "__hs_vars")) (nil? (host-get el "__hs_vars"))
(host-set! el "__hs_vars" (dict))) (host-set! el "__hs_vars" (host-new "Object")))
(host-set! (host-get el "__hs_vars") name val) (host-set! (host-get el "__hs_vars") name val)
(when changed (hs-dom-fire-watchers! el name val)))))) (when changed (hs-dom-fire-watchers! el name val))))))

View File

@@ -436,6 +436,8 @@
(let (let
((ch (hs-cur)) (start pos)) ((ch (hs-cur)) (start pos))
(cond (cond
(and (= ch "-") (< (+ pos 1) src-len) (= (hs-peek 1) "-"))
(do (hs-advance! 2) (skip-comment!) (scan!))
(and (= ch "/") (< (+ pos 1) src-len) (= (hs-peek 1) "/")) (and (= ch "/") (< (+ pos 1) src-len) (= (hs-peek 1) "/"))
(do (hs-advance! 2) (skip-comment!) (scan!)) (do (hs-advance! 2) (skip-comment!) (scan!))
(and (and
@@ -613,6 +615,8 @@
(do (hs-emit! "op" "\\" start) (hs-advance! 1) (scan!)) (do (hs-emit! "op" "\\" start) (hs-advance! 1) (scan!))
(= ch ":") (= ch ":")
(do (hs-emit! "colon" ":" start) (hs-advance! 1) (scan!)) (do (hs-emit! "colon" ":" start) (hs-advance! 1) (scan!))
(= ch "|")
(do (hs-emit! "op" "|" start) (hs-advance! 1) (scan!))
:else (do (hs-advance! 1) (scan!))))))) :else (do (hs-advance! 1) (scan!)))))))
(scan!) (scan!)
(hs-emit! "eof" nil pos) (hs-emit! "eof" nil pos)

View File

@@ -85,18 +85,18 @@
(nth target 1) (nth target 1)
value)) value))
((= th (quote me)) ((= th (quote me))
(list (quote dom-set-inner-html) (quote me) value)) (list (quote hs-set-inner-html!) (quote me) value))
((= th (quote it)) (list (quote set!) (quote it) value)) ((= th (quote it)) (list (quote set!) (quote it) value))
((= th (quote query)) ((= th (quote query))
(list (quote dom-set-inner-html) (hs-to-sx target) value)) (list (quote hs-set-inner-html!) (hs-to-sx target) value))
((= th (quote array-index)) ((= th (quote array-index))
(list (list
(quote host-set!) (quote hs-array-set!)
(hs-to-sx (nth target 1)) (hs-to-sx (nth target 1))
(hs-to-sx (nth target 2)) (hs-to-sx (nth target 2))
value)) value))
((or (= th (quote next)) (= th (quote previous)) (= th (quote closest))) ((or (= th (quote next)) (= th (quote previous)) (= th (quote closest)))
(list (quote dom-set-inner-html) (hs-to-sx target) value)) (list (quote hs-set-inner-html!) (hs-to-sx target) value))
((= th (quote of)) ((= th (quote of))
(let (let
((prop-ast (nth target 1)) (obj-ast (nth target 2))) ((prop-ast (nth target 1)) (obj-ast (nth target 2)))
@@ -162,10 +162,19 @@
(let (let
((wrapped-body (if catch-info (let ((var (make-symbol (nth catch-info 0))) (catch-body (hs-to-sx (nth catch-info 1)))) (if finally-info (list (quote do) (list (quote guard) (list var (list true catch-body)) compiled-body) (hs-to-sx finally-info)) (list (quote guard) (list var (list true catch-body)) compiled-body))) (if finally-info (list (quote do) compiled-body (hs-to-sx finally-info)) compiled-body))) ((wrapped-body (if catch-info (let ((var (make-symbol (nth catch-info 0))) (catch-body (hs-to-sx (nth catch-info 1)))) (if finally-info (list (quote do) (list (quote guard) (list var (list true catch-body)) compiled-body) (hs-to-sx finally-info)) (list (quote guard) (list var (list true catch-body)) compiled-body))) (if finally-info (list (quote do) compiled-body (hs-to-sx finally-info)) compiled-body)))
(handler (handler
(list (let
(quote fn) ((uses-the-result? (fn (expr) (cond ((= expr (quote the-result)) true) ((list? expr) (some (fn (x) (uses-the-result? x)) expr)) (true false)))))
(list (quote event)) (list
wrapped-body))) (quote fn)
(list (quote event))
(if
(uses-the-result? wrapped-body)
(list
(quote let)
(list
(list (quote the-result) nil))
wrapped-body)
wrapped-body)))))
(if (if
every? every?
(list (list
@@ -443,9 +452,7 @@
(quote __hs-new) (quote __hs-new)
(list (list
(quote +) (quote +)
(list (list (quote nth) var-sym (quote __hs-idx))
(quote hs-to-number)
(list (quote nth) var-sym (quote __hs-idx)))
amount))) amount)))
(list (list
(quote do) (quote do)
@@ -463,10 +470,7 @@
((t (hs-to-sx expr))) ((t (hs-to-sx expr)))
(list (list
(quote let) (quote let)
(list (list (list (quote __hs-new) (list (quote +) t amount)))
(list
(quote __hs-new)
(list (quote +) (list (quote hs-to-number) t) amount)))
(list (list
(quote do) (quote do)
(list (quote set!) t (quote __hs-new)) (list (quote set!) t (quote __hs-new))
@@ -564,9 +568,7 @@
(quote __hs-new) (quote __hs-new)
(list (list
(quote -) (quote -)
(list (list (quote nth) var-sym (quote __hs-idx))
(quote hs-to-number)
(list (quote nth) var-sym (quote __hs-idx)))
amount))) amount)))
(list (list
(quote do) (quote do)
@@ -584,10 +586,7 @@
((t (hs-to-sx expr))) ((t (hs-to-sx expr)))
(list (list
(quote let) (quote let)
(list (list (list (quote __hs-new) (list (quote -) t amount)))
(list
(quote __hs-new)
(list (quote -) (list (quote hs-to-number) t) amount)))
(list (list
(quote do) (quote do)
(list (quote set!) t (quote __hs-new)) (list (quote set!) t (quote __hs-new))
@@ -870,6 +869,11 @@
((= head (quote ref)) (make-symbol (nth ast 1))) ((= head (quote ref)) (make-symbol (nth ast 1)))
((= head (quote query)) ((= head (quote query))
(list (quote hs-query-first) (nth ast 1))) (list (quote hs-query-first) (nth ast 1)))
((= head (quote query-scoped))
(list
(quote hs-query-all-in)
(nth ast 1)
(hs-to-sx (nth ast 2))))
((= head (quote attr)) ((= head (quote attr))
(list (list
(quote dom-get-attr) (quote dom-get-attr)
@@ -1163,6 +1167,14 @@
(quote set!) (quote set!)
(hs-to-sx tgt) (hs-to-sx tgt)
(list (quote hs-add-to!) val (hs-to-sx tgt))))) (list (quote hs-add-to!) val (hs-to-sx tgt)))))
((= head (quote add-attr))
(let
((tgt (nth ast 3)))
(list
(quote hs-set-attr!)
(hs-to-sx tgt)
(nth ast 1)
(hs-to-sx (nth ast 2)))))
((= head (quote remove-value)) ((= head (quote remove-value))
(let (let
((val (hs-to-sx (nth ast 1))) (tgt (nth ast 2))) ((val (hs-to-sx (nth ast 1))) (tgt (nth ast 2)))
@@ -1296,6 +1308,20 @@
(nth ast 1) (nth ast 1)
(hs-to-sx (nth ast 2)) (hs-to-sx (nth ast 2))
(hs-to-sx (nth ast 3)))) (hs-to-sx (nth ast 3))))
((= head (quote toggle-attr-val))
(list
(quote hs-toggle-attr-val!)
(hs-to-sx (nth ast 3))
(nth ast 1)
(hs-to-sx (nth ast 2))))
((= head (quote toggle-attr-diff))
(list
(quote hs-toggle-attr-diff!)
(hs-to-sx (nth ast 5))
(nth ast 1)
(hs-to-sx (nth ast 2))
(nth ast 3)
(hs-to-sx (nth ast 4))))
((= head (quote set!)) ((= head (quote set!))
(emit-set (nth ast 1) (hs-to-sx (nth ast 2)))) (emit-set (nth ast 1) (hs-to-sx (nth ast 2))))
((= head (quote put!)) ((= head (quote put!))
@@ -1359,13 +1385,48 @@
((= head (quote hide)) ((= head (quote hide))
(let (let
((tgt (hs-to-sx (nth ast 1))) ((tgt (hs-to-sx (nth ast 1)))
(strategy (if (> (len ast) 2) (nth ast 2) "display"))) (strategy (if (> (len ast) 2) (nth ast 2) "display"))
(list (quote hs-hide!) tgt strategy))) (when-cond (if (> (len ast) 3) (nth ast 3) nil)))
(if
(nil? when-cond)
(list (quote hs-hide!) tgt strategy)
(list
(quote hs-hide-when!)
tgt
strategy
(list
(quote fn)
(list (quote it))
(hs-to-sx when-cond))))))
((= head (quote show)) ((= head (quote show))
(let (let
((tgt (hs-to-sx (nth ast 1))) ((tgt (hs-to-sx (nth ast 1)))
(strategy (if (> (len ast) 2) (nth ast 2) "display"))) (strategy (if (> (len ast) 2) (nth ast 2) "display"))
(list (quote hs-show!) tgt strategy))) (when-cond (if (> (len ast) 3) (nth ast 3) nil)))
(if
(nil? when-cond)
(list (quote hs-show!) tgt strategy)
(list
(quote let)
(list
(list
(quote __hs-show-r)
(list
(quote hs-show-when!)
tgt
strategy
(list
(quote fn)
(list (quote it))
(hs-to-sx when-cond)))))
(list
(quote begin)
(list
(quote set!)
(quote the-result)
(quote __hs-show-r))
(list (quote set!) (quote it) (quote __hs-show-r))
(quote __hs-show-r))))))
((= head (quote transition)) (emit-transition ast)) ((= head (quote transition)) (emit-transition ast))
((= head (quote transition-from)) ((= head (quote transition-from))
(let (let
@@ -1424,6 +1485,14 @@
(list (quote hs-settle) (quote me))) (list (quote hs-settle) (quote me)))
((= head (quote go)) ((= head (quote go))
(list (quote hs-navigate!) (hs-to-sx (nth ast 1)))) (list (quote hs-navigate!) (hs-to-sx (nth ast 1))))
((= head (quote __get-cmd))
(let
((val (hs-to-sx (nth ast 1))))
(list
(quote begin)
(list (quote set!) (quote the-result) val)
(list (quote set!) (quote it) val)
val)))
((= head (quote append!)) ((= head (quote append!))
(let (let
((tgt (hs-to-sx (nth ast 2))) ((tgt (hs-to-sx (nth ast 2)))
@@ -1648,11 +1717,13 @@
(list (quote hs-reset!) (hs-to-sx (nth ast 1)))) (list (quote hs-reset!) (hs-to-sx (nth ast 1))))
((= head (quote default!)) ((= head (quote default!))
(let (let
((t (hs-to-sx (nth ast 1))) (v (hs-to-sx (nth ast 2)))) ((tgt-ast (nth ast 1))
(read (hs-to-sx (nth ast 1)))
(v (hs-to-sx (nth ast 2))))
(list (list
(quote when) (quote when)
(list (quote nil?) t) (list (quote hs-default?) read)
(list (quote set!) t v)))) (emit-set tgt-ast v))))
((= head (quote hs-is)) ((= head (quote hs-is))
(list (list
(quote hs-is) (quote hs-is)

View File

@@ -16,6 +16,14 @@
(fn (fn
(sx) (sx)
(define vars (list)) (define vars (list))
(define
reserved
(list
(quote me)
(quote it)
(quote event)
(quote you)
(quote yourself)))
(define (define
walk walk
(fn (fn
@@ -30,7 +38,9 @@
(let (let
((name (nth node 1))) ((name (nth node 1)))
(when (when
(not (some (fn (v) (= v name)) vars)) (and
(not (some (fn (v) (= v name)) vars))
(not (some (fn (v) (= v name)) reserved)))
(set! vars (cons name vars))))) (set! vars (cons name vars)))))
(for-each walk node)))) (for-each walk node))))
(walk sx) (walk sx)

View File

@@ -95,6 +95,13 @@
(do (adv!) (list kind (str "." val) (list (quote me))))) (do (adv!) (list kind (str "." val) (list (quote me)))))
((= typ "id") ((= typ "id")
(do (adv!) (list kind (str "#" val) (list (quote me))))) (do (adv!) (list kind (str "#" val) (list (quote me)))))
((= typ "attr")
(do
(adv!)
(list
(quote attr)
val
(list kind (str "[" val "]") (list (quote me))))))
(true (list kind "*" (list (quote me)))))))) (true (list kind "*" (list (quote me))))))))
(define (define
parse-pos-kw parse-pos-kw
@@ -146,8 +153,10 @@
(do (adv!) (list (quote me)))) (do (adv!) (list (quote me))))
((and (= typ "keyword") (= val "I")) ((and (= typ "keyword") (= val "I"))
(do (adv!) (list (quote me)))) (do (adv!) (list (quote me))))
((and (= typ "keyword") (or (= val "it") (= val "result"))) ((and (= typ "keyword") (= val "it"))
(do (adv!) (list (quote it)))) (do (adv!) (list (quote it))))
((and (= typ "keyword") (= val "result"))
(do (adv!) (quote the-result)))
((and (= typ "keyword") (= val "event")) ((and (= typ "keyword") (= val "event"))
(do (adv!) (list (quote event)))) (do (adv!) (list (quote event))))
((and (= typ "keyword") (= val "target")) ((and (= typ "keyword") (= val "target"))
@@ -174,7 +183,18 @@
(do (adv!) (parse-pos-kw (quote last)))) (do (adv!) (parse-pos-kw (quote last))))
((= typ "id") ((= typ "id")
(do (adv!) (list (quote query) (str "#" val)))) (do (adv!) (list (quote query) (str "#" val))))
((= typ "selector") (do (adv!) (list (quote query) val))) ((= typ "selector")
(do
(adv!)
(if
(and (= (tp-type) "keyword") (= (tp-val) "in"))
(do
(adv!)
(list
(quote query-scoped)
val
(parse-cmp (parse-arith (parse-poss (parse-atom))))))
(list (quote query) val))))
((= typ "attr") ((= typ "attr")
(do (adv!) (list (quote attr) val (list (quote me))))) (do (adv!) (list (quote attr) val (list (quote me)))))
((= typ "style") ((= typ "style")
@@ -426,7 +446,7 @@
(list (quote type-check) left type-name))))))) (list (quote type-check) left type-name)))))))
(true (true
(let (let
((right (parse-expr))) ((right (parse-cmp (parse-arith (parse-poss (parse-atom))))))
(if (if
(match-kw "ignoring") (match-kw "ignoring")
(do (do
@@ -892,6 +912,18 @@
(let (let
((tgt (if (match-kw "to") (parse-expr) (list (quote me))))) ((tgt (if (match-kw "to") (parse-expr) (list (quote me)))))
(list (quote set-styles) (reverse pairs) tgt))))) (list (quote set-styles) (reverse pairs) tgt)))))
((and (= (tp-type) "bracket-open") (> (len tokens) (+ p 1)) (= (get (nth tokens (+ p 1)) "type") "attr"))
(do
(adv!)
(let
((attr-name (get (adv!) "value")))
(when (and (= (tp-type) "op") (= (tp-val) "=")) (adv!))
(let
((attr-val (parse-expr)))
(when (= (tp-type) "bracket-close") (adv!))
(let
((tgt (parse-tgt-kw "to" (list (quote me)))))
(list (quote add-attr) attr-name attr-val tgt))))))
(true (true
(let (let
((value (parse-expr))) ((value (parse-expr)))
@@ -978,20 +1010,58 @@
() ()
(cond (cond
((match-kw "between") ((match-kw "between")
(if (cond
(= (tp-type) "class") ((= (tp-type) "class")
(let (let
((cls1 (do (let ((v (tp-val))) (adv!) v)))) ((cls1 (do (let ((v (tp-val))) (adv!) v))))
(expect-kw! "and") (expect-kw! "and")
(if (if
(= (tp-type) "class") (= (tp-type) "class")
(let
((cls2 (do (let ((v (tp-val))) (adv!) v))))
(let (let
((tgt (parse-tgt-kw "on" (list (quote me))))) ((cls2 (do (let ((v (tp-val))) (adv!) v))))
(list (quote toggle-between) cls1 cls2 tgt))) (let
nil)) ((tgt (parse-tgt-kw "on" (list (quote me)))))
nil)) (list (quote toggle-between) cls1 cls2 tgt)))
nil)))
((and (= (tp-type) "bracket-open") (> (len tokens) (+ p 1)) (= (get (nth tokens (+ p 1)) "type") "attr"))
(do
(adv!)
(let
((n1 (get (adv!) "value")))
(when
(and (= (tp-type) "op") (= (tp-val) "="))
(adv!))
(let
((v1 (parse-expr)))
(when (= (tp-type) "bracket-close") (adv!))
(expect-kw! "and")
(when (= (tp-type) "bracket-open") (adv!))
(let
((n2 (get (adv!) "value")))
(when
(and (= (tp-type) "op") (= (tp-val) "="))
(adv!))
(let
((v2 (parse-expr)))
(when (= (tp-type) "bracket-close") (adv!))
(let
((tgt (parse-tgt-kw "on" (list (quote me)))))
(if
(= n1 n2)
(list
(quote toggle-attr-between)
n1
v1
v2
tgt)
(list
(quote toggle-attr-diff)
n1
v1
n2
v2
tgt)))))))))
(true nil)))
((= (tp-type) "class") ((= (tp-type) "class")
(let (let
((cls (do (let ((v (tp-val))) (adv!) v)))) ((cls (do (let ((v (tp-val))) (adv!) v))))
@@ -1064,6 +1134,18 @@
val2 val2
tgt))) tgt)))
(list (quote toggle-attr) attr-name tgt))))) (list (quote toggle-attr) attr-name tgt)))))
((and (= (tp-type) "bracket-open") (> (len tokens) (+ p 1)) (= (get (nth tokens (+ p 1)) "type") "attr"))
(do
(adv!)
(let
((attr-name (get (adv!) "value")))
(when (and (= (tp-type) "op") (= (tp-val) "=")) (adv!))
(let
((attr-val (parse-expr)))
(when (= (tp-type) "bracket-close") (adv!))
(let
((tgt (parse-tgt-kw "on" (list (quote me)))))
(list (quote toggle-attr-val) attr-name attr-val tgt))))))
((and (= (tp-type) "keyword") (= (tp-val) "my")) ((and (= (tp-type) "keyword") (= (tp-val) "my"))
(do (do
(adv!) (adv!)
@@ -1338,19 +1420,23 @@
(fn (fn
() ()
(let (let
((tgt (cond ((at-end?) (list (quote me))) ((and (= (tp-type) "keyword") (or (= (tp-val) "then") (= (tp-val) "end") (= (tp-val) "with") (= (tp-val) "add") (= (tp-val) "remove") (= (tp-val) "set") (= (tp-val) "put") (= (tp-val) "toggle") (= (tp-val) "hide") (= (tp-val) "show"))) (list (quote me))) (true (parse-expr))))) ((tgt (cond ((at-end?) (list (quote me))) ((and (= (tp-type) "keyword") (or (= (tp-val) "then") (= (tp-val) "end") (= (tp-val) "with") (= (tp-val) "when") (= (tp-val) "add") (= (tp-val) "remove") (= (tp-val) "set") (= (tp-val) "put") (= (tp-val) "toggle") (= (tp-val) "hide") (= (tp-val) "show"))) (list (quote me))) (true (parse-expr)))))
(let (let
((strategy (if (match-kw "with") (if (at-end?) "display" (let ((s (tp-val))) (adv!) s)) "display"))) ((strategy (if (match-kw "with") (if (at-end?) "display" (let ((s (tp-val))) (adv!) s)) "display")))
(list (quote hide) tgt strategy))))) (let
((when-cond (if (and (= (tp-type) "keyword") (= (tp-val) "when")) (do (adv!) (parse-expr)) nil)))
(list (quote hide) tgt strategy when-cond))))))
(define (define
parse-show-cmd parse-show-cmd
(fn (fn
() ()
(let (let
((tgt (cond ((at-end?) (list (quote me))) ((and (= (tp-type) "keyword") (or (= (tp-val) "then") (= (tp-val) "end") (= (tp-val) "with") (= (tp-val) "add") (= (tp-val) "remove") (= (tp-val) "set") (= (tp-val) "put") (= (tp-val) "toggle") (= (tp-val) "hide") (= (tp-val) "show"))) (list (quote me))) (true (parse-expr))))) ((tgt (cond ((at-end?) (list (quote me))) ((and (= (tp-type) "keyword") (or (= (tp-val) "then") (= (tp-val) "end") (= (tp-val) "with") (= (tp-val) "when") (= (tp-val) "add") (= (tp-val) "remove") (= (tp-val) "set") (= (tp-val) "put") (= (tp-val) "toggle") (= (tp-val) "hide") (= (tp-val) "show"))) (list (quote me))) (true (parse-expr)))))
(let (let
((strategy (if (match-kw "with") (if (at-end?) "display" (let ((s (tp-val))) (adv!) s)) "display"))) ((strategy (if (match-kw "with") (if (at-end?) "display" (let ((s (tp-val))) (adv!) s)) "display")))
(list (quote show) tgt strategy))))) (let
((when-cond (if (and (= (tp-type) "keyword") (= (tp-val) "when")) (do (adv!) (parse-expr)) nil)))
(list (quote show) tgt strategy when-cond))))))
(define (define
parse-transition-cmd parse-transition-cmd
(fn (fn
@@ -1493,7 +1579,7 @@
(ca-collect (append acc (list arg))))))) (ca-collect (append acc (list arg)))))))
(ca-collect (list)))) (ca-collect (list))))
(define parse-call-cmd (fn () (parse-expr))) (define parse-call-cmd (fn () (parse-expr)))
(define parse-get-cmd (fn () (parse-expr))) (define parse-get-cmd (fn () (list (quote __get-cmd) (parse-expr))))
(define (define
parse-take-cmd parse-take-cmd
(fn (fn
@@ -1501,12 +1587,34 @@
(cond (cond
((= (tp-type) "class") ((= (tp-type) "class")
(let (let
((cls (do (let ((v (tp-val))) (adv!) v)))) ((classes (list)))
(let (let
((from-sel (if (match-kw "from") (parse-expr) nil))) ((collect (fn () (when (= (tp-type) "class") (let ((v (tp-val))) (adv!) (set! classes (append classes (list v))) (collect))))))
(collect)
(let (let
((for-tgt (if (match-kw "for") (parse-expr) nil))) ((from-sel (if (match-kw "from") (parse-expr) nil)))
(list (quote take!) "class" cls from-sel for-tgt))))) (let
((for-tgt (if (match-kw "for") (parse-expr) nil)))
(if
(= (len classes) 1)
(list
(quote take!)
"class"
(first classes)
from-sel
for-tgt)
(cons
(quote do)
(map
(fn
(cls)
(list
(quote take!)
"class"
cls
from-sel
for-tgt))
classes))))))))
((= (tp-type) "attr") ((= (tp-type) "attr")
(let (let
((attr-name (get (adv!) "value"))) ((attr-name (get (adv!) "value")))
@@ -1588,7 +1696,7 @@
(adv!) (adv!)
(expect-kw! "of") (expect-kw! "of")
(let (let
((regex (parse-expr))) ((regex (parse-atom)))
(do (do
(cond (cond
((match-kw "of") nil) ((match-kw "of") nil)
@@ -1606,7 +1714,7 @@
(adv!) (adv!)
(expect-kw! "of") (expect-kw! "of")
(let (let
((regex (parse-expr))) ((regex (parse-atom)))
(do (do
(cond (cond
((match-kw "of") nil) ((match-kw "of") nil)
@@ -1697,7 +1805,7 @@
(match-kw "of") (match-kw "of")
(list (make-symbol ".") (parse-expr) val) (list (make-symbol ".") (parse-expr) val)
(cond (cond
((= val "result") (list (quote it))) ((= val "result") (quote the-result))
((= val "first") (parse-pos-kw (quote first))) ((= val "first") (parse-pos-kw (quote first)))
((= val "last") (parse-pos-kw (quote last))) ((= val "last") (parse-pos-kw (quote last)))
((= val "closest") (parse-trav (quote closest))) ((= val "closest") (parse-trav (quote closest)))

File diff suppressed because it is too large Load Diff

View File

@@ -18,11 +18,18 @@
;; 1. Test framework macros ;; 1. Test framework macros
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(defmacro deftest (name &rest body) (defmacro
`(let ((result (try-call (fn () ,@body)))) deftest
(if (get result "ok") (name &rest body)
(report-pass ,name) (quasiquote
(report-fail ,name (get result "error"))))) (when
(test-allowed? (unquote name))
(let
((result (try-call (fn () (splice-unquote body)))))
(if
(get result "ok")
(report-pass (unquote name))
(report-fail (unquote name) (get result "error")))))))
(defmacro defsuite (name &rest items) (defmacro defsuite (name &rest items)
`(do (push-suite ,name) `(do (push-suite ,name)

View File

@@ -4,7 +4,9 @@
"put into #id compiled" "put into #id compiled"
(let (let
((sx (hs-to-sx-from-source "on click put \"foo\" into #d1"))) ((sx (hs-to-sx-from-source "on click put \"foo\" into #d1")))
(assert= (serialize sx) "SHOW"))) (assert=
(serialize sx)
"(hs-on me \"click\" (fn (event) (hs-set-inner-html! (hs-query-first \"#d1\") \"foo\")))")))
(deftest (deftest
"put into #id works" "put into #id works"
(let (let

File diff suppressed because it is too large Load Diff

View File

@@ -40,7 +40,7 @@
"set attribute" "set attribute"
(let (let
((sx (hs-to-sx-from-source "set @title to 'hello'"))) ((sx (hs-to-sx-from-source "set @title to 'hello'")))
(assert= (quote dom-set-attr) (first sx)) (assert= (quote hs-set-attr!) (first sx))
(assert= "title" (nth sx 2)) (assert= "title" (nth sx 2))
(assert= "hello" (nth sx 3)))) (assert= "hello" (nth sx 3))))
(deftest (deftest
@@ -284,12 +284,16 @@
"increment attribute" "increment attribute"
(let (let
((sx (hs-to-sx-from-source "increment @count"))) ((sx (hs-to-sx-from-source "increment @count")))
(assert= (quote dom-set-attr) (first sx)))) (assert= (quote let) (first sx))
(assert= (quote do) (first (nth sx 2)))
(assert= (quote dom-set-attr) (first (nth (nth sx 2) 1)))))
(deftest (deftest
"decrement attribute" "decrement attribute"
(let (let
((sx (hs-to-sx-from-source "decrement @count"))) ((sx (hs-to-sx-from-source "decrement @count")))
(assert= (quote dom-set-attr) (first sx))))) (assert= (quote let) (first sx))
(assert= (quote do) (first (nth sx 2)))
(assert= (quote dom-set-attr) (first (nth (nth sx 2) 1))))))
(defsuite (defsuite
"hs-live-demo-toggle" "hs-live-demo-toggle"

View File

@@ -59,44 +59,73 @@
) )
;; ── collectionExpressions (12 tests) ── ;; ── collectionExpressions (12 tests) ──
(defsuite "hs-dev-collectionExpressions" (defsuite
(deftest "filters an array by condition" "hs-dev-collectionExpressions"
(let ((result (eval-hs "set arr to [{name: \"a\", active: true}, {name: \"b\", active: false}, {name: \"c\", active: true}] then return arr where its active"))) (deftest
"filters an array by condition"
(let
((result (eval-hs "set arr to [{name: \"a\", active: true}, {name: \"b\", active: false}, {name: \"c\", active: true}] then return arr where its active")))
(assert= (list "a" "c") (map (fn (x) (get x "name")) result)))) (assert= (list "a" "c") (map (fn (x) (get x "name")) result))))
(deftest "filters with comparison" (deftest
(assert= (list 4 5) (eval-hs "set arr to [1, 2, 3, 4, 5] then return arr where it > 3")) "filters with comparison"
) (assert=
(deftest "sorts by a property" (list 4 5)
(let ((result (eval-hs "set arr to [{name: \"Charlie\"}, {name: \"Alice\"}, {name: \"Bob\"}] then return arr sorted by its name"))) (eval-hs "set arr to [1, 2, 3, 4, 5] then return arr where it > 3")))
(assert= (list "Alice" "Bob" "Charlie") (map (fn (x) (get x "name")) result)))) (deftest
(deftest "sorts descending" "sorts by a property"
(assert= (list 3 2 1) (eval-hs "set arr to [3, 1, 2] then return arr sorted by it descending")) (let
) ((result (eval-hs "set arr to [{name: \"Charlie\"}, {name: \"Alice\"}, {name: \"Bob\"}] then return arr sorted by its name")))
(deftest "sorts numbers by a computed key" (assert=
(let ((result (eval-hs "set arr to [{name: \"b\", age: 30}, {name: \"a\", age: 20}, {name: \"c\", age: 25}] then return arr sorted by its age"))) (list "Alice" "Bob" "Charlie")
(map (fn (x) (get x "name")) result))))
(deftest
"sorts descending"
(assert=
(list 3 2 1)
(eval-hs "set arr to [3, 1, 2] then return arr sorted by it descending")))
(deftest
"sorts numbers by a computed key"
(let
((result (eval-hs "set arr to [{name: \"b\", age: 30}, {name: \"a\", age: 20}, {name: \"c\", age: 25}] then return arr sorted by its age")))
(assert= (list "a" "c" "b") (map (fn (x) (get x "name")) result)))) (assert= (list "a" "c" "b") (map (fn (x) (get x "name")) result))))
(deftest "maps to a property" (deftest
(assert= (list "Alice" "Bob") (eval-hs "set arr to [{name: \"Alice\"}, {name: \"Bob\"}] then return arr mapped to its name")) "maps to a property"
) (assert=
(deftest "maps with an expression" (list "Alice" "Bob")
(assert= (list 2 4 6) (eval-hs "set arr to [1, 2, 3] then return arr mapped to (it * 2)")) (eval-hs
) "set arr to [{name: \"Alice\"}, {name: \"Bob\"}] then return arr mapped to its name")))
(deftest "where then mapped to" (deftest
(assert= (list "Alice" "Charlie") (eval-hs "set arr to [{name: \"Alice\", active: true}, {name: \"Bob\", active: false}, {name: \"Charlie\", active: true}] then return arr where its active mapped to its name")) "maps with an expression"
) (assert=
(deftest "sorted by then mapped to" (list 2 4 6)
(assert= (list "Alice" "Charlie") (eval-hs "set arr to [{name: \"Charlie\", age: 30}, {name: \"Alice\", age: 20}] then return arr sorted by its age mapped to its name")) (eval-hs "set arr to [1, 2, 3] then return arr mapped to (it * 2)")))
) (deftest
(deftest "where then sorted by then mapped to" "where then mapped to"
(assert= (list "Bob" "Charlie") (eval-hs "set arr to [{name: \"Charlie\", active: true, age: 30}, {name: \"Alice\", active: false, age: 20}, {name: \"Bob\", active: true, age: 25}] then return arr where its active sorted by its age mapped to its name")) (assert=
) (list "Alice" "Charlie")
(deftest "the result inside where refers to previous command result, not current element" (eval-hs
(assert= (list 4 5) (eval-hs "get 3 then set arr to [1, 2, 3, 4, 5] then return arr where it > the result")) "set arr to [{name: \"Alice\", active: true}, {name: \"Bob\", active: false}, {name: \"Charlie\", active: true}] then return arr where its active mapped to its name")))
) (deftest
(deftest "where binds after property access" "sorted by then mapped to"
(assert= (list 3 4) (eval-hs "obj.items where it > 2")) (assert=
) (list "Alice" "Charlie")
) (eval-hs
"set arr to [{name: \"Charlie\", age: 30}, {name: \"Alice\", age: 20}] then return arr sorted by its age mapped to its name")))
(deftest
"where then sorted by then mapped to"
(assert=
(list "Bob" "Charlie")
(eval-hs
"set arr to [{name: \"Charlie\", active: true, age: 30}, {name: \"Alice\", active: false, age: 20}, {name: \"Bob\", active: true, age: 25}] then return arr where its active sorted by its age mapped to its name")))
(deftest
"the result inside where refers to previous command result, not current element"
(assert=
(list 4 5)
(eval-hs
"get 3 then set arr to [1, 2, 3, 4, 5] then return arr where it > the result")))
(deftest
"where binds after property access"
(assert= (list 3 4) (eval-hs "obj.items where it > 2" {:locals {:obj {:items (list 1 2 3 4)}}}))))
;; ── splitJoin (7 tests) ── ;; ── splitJoin (7 tests) ──
(defsuite "hs-dev-splitJoin" (defsuite "hs-dev-splitJoin"

View File

@@ -302,7 +302,7 @@
(let (let
((ast (hs-compile "if result is empty add .hidden end"))) ((ast (hs-compile "if result is empty add .hidden end")))
(assert= (quote if) (first ast)) (assert= (quote if) (first ast))
(assert= (list (quote empty?) (list (quote it))) (nth ast 1)))) (assert= (list (quote empty?) (quote the-result)) (nth ast 1))))
(deftest (deftest
"comparison is not" "comparison is not"
(let (let
@@ -375,7 +375,7 @@
(let (let
((ast (hs-compile "call alert(\"hello\")"))) ((ast (hs-compile "call alert(\"hello\")")))
(assert= (quote call) (first ast)) (assert= (quote call) (first ast))
(assert= "alert" (nth ast 1)) (assert= (list (quote ref) "alert") (nth ast 1))
(assert= "hello" (nth ast 2))))) (assert= "hello" (nth ast 2)))))
;; ── Full expressions (matching tokenizer conformance) ───────────── ;; ── Full expressions (matching tokenizer conformance) ─────────────
@@ -462,7 +462,7 @@
"the as article skip" "the as article skip"
(let (let
((ast (hs-compile "set the result to 5"))) ((ast (hs-compile "set the result to 5")))
(let ((tgt (nth ast 1))) (assert= (quote it) (first tgt)))))) (let ((tgt (nth ast 1))) (assert= (quote the-result) tgt)))))
(defsuite (defsuite
"hs-parse-as-conversion" "hs-parse-as-conversion"

361
tests/hs-run-filtered.js Executable file
View File

@@ -0,0 +1,361 @@
#!/usr/bin/env node
/**
* Run HS behavioral tests — single process, synchronous, with step-limit timeout.
* Uses the OCaml VM's built-in step_limit to break infinite loops.
*/
const fs = require('fs');
const path = require('path');
const PROJECT = path.resolve(__dirname, '..');
const WASM_DIR = path.join(PROJECT, 'shared/static/wasm');
const SX_DIR = path.join(WASM_DIR, 'sx');
// Load WASM kernel
eval(fs.readFileSync(path.join(WASM_DIR, 'sx_browser.bc.js'), 'utf8'));
const K = globalThis.SxKernel;
// Step limit API — exposed from OCaml kernel
const STEP_LIMIT = parseInt(process.env.HS_STEP_LIMIT || '200000');
function setStepLimit(n) { K.setStepLimit(n); }
function resetStepCount() { K.resetStepCount(); }
// ─── DOM mock ──────────────────────────────────────────────────
function mkStyle() { const s={}; s.setProperty=function(p,v){s[p]=v;}; s.getPropertyValue=function(p){return s[p]||'';}; s.removeProperty=function(p){delete s[p];}; return s; }
class El {
constructor(t) { this.tagName=t.toUpperCase(); this.nodeName=this.tagName; this.nodeType=1; this.id=''; this.className=''; this.classList=new CL(this); this.style=mkStyle(); this.attributes={}; this.children=[]; this.childNodes=[]; this.childNodes.item=function(i){return this[i]||null;}; this.parentElement=null; this.parentNode=null; this.textContent=''; this.innerHTML=''; this._listeners={}; this.dataset={}; this.open=false; this.value=''; this.checked=false; this.disabled=false; this.type=''; this.name=''; this.selectedIndex=-1; this.options=[]; }
setAttribute(n,v) { this.attributes[n]=String(v); if(n==='id')this.id=v; if(n==='class'){this.className=v;this.classList._sync(v);} if(n==='value')this.value=v; if(n==='disabled')this.disabled=true; if(n==='style'){const s=String(v);for(const d of s.split(';')){const c=d.indexOf(':');if(c>0){const k=d.slice(0,c).trim();const val=d.slice(c+1).trim();if(k)this.style.setProperty(k,val);}} } }
getAttribute(n) { return this.attributes[n]!==undefined?this.attributes[n]:null; }
removeAttribute(n) { delete this.attributes[n]; if(n==='disabled')this.disabled=false; }
hasAttribute(n) { return n in this.attributes; }
addEventListener(e,f) { if(!this._listeners[e])this._listeners[e]=[]; this._listeners[e].push(f); }
removeEventListener(e,f) { if(this._listeners[e])this._listeners[e]=this._listeners[e].filter(x=>x!==f); }
dispatchEvent(ev) { ev.target=ev.target||this; ev.currentTarget=this; const fns=[...(this._listeners[ev.type]||[])]; for(const f of fns){if(ev._si)break;try{f.call(this,ev);}catch(e){}} if(ev.bubbles&&!ev._sp&&this.parentElement){this.parentElement.dispatchEvent(ev);} return !ev.defaultPrevented; }
appendChild(c) { if(c.parentElement)c.parentElement.removeChild(c); c.parentElement=this; c.parentNode=this; this.children.push(c); this.childNodes.push(c); this._syncText(); return c; }
removeChild(c) { this.children=this.children.filter(x=>x!==c); this.childNodes=this.childNodes.filter(x=>x!==c); c.parentElement=null; c.parentNode=null; this._syncText(); return c; }
insertBefore(n,r) { if(n.parentElement)n.parentElement.removeChild(n); const i=this.children.indexOf(r); if(i>=0){this.children.splice(i,0,n);this.childNodes.splice(i,0,n);}else{this.children.push(n);this.childNodes.push(n);} n.parentElement=this;n.parentNode=this; this._syncText(); return n; }
replaceChild(n,o) { const i=this.children.indexOf(o); if(i>=0){this.children[i]=n;this.childNodes[i]=n;} n.parentElement=this;n.parentNode=this; o.parentElement=null;o.parentNode=null; this._syncText(); return o; }
querySelector(s) { return fnd(this,s); }
querySelectorAll(s) { return fndAll(this,s); }
closest(s) { let e=this; while(e){if(mt(e,s))return e; e=e.parentElement;} return null; }
matches(s) { return mt(this,s); }
contains(o) { if(o===this)return true; for(const c of this.children)if(c===o||c.contains(o))return true; return false; }
cloneNode(d) { const e=new El(this.tagName.toLowerCase()); Object.assign(e.attributes,this.attributes); e.id=this.id; e.className=this.className; e.classList._sync(this.className); for(const k of Object.keys(this.style)){if(typeof this.style[k]!=='function')e.style[k]=this.style[k];} e.textContent=this.textContent; e.innerHTML=this.innerHTML; e.value=this.value; if(d)for(const c of this.children)e.appendChild(c.cloneNode(true)); return e; }
focus(){} blur(){} click(){this.dispatchEvent(new Ev('click',{bubbles:true}));} remove(){if(this.parentElement)this.parentElement.removeChild(this);}
_syncText() {
// Sync textContent from children
const t = this.children.map(c => c.textContent || '').join('');
if (t) this.textContent = t;
}
_setInnerHTML(html) {
// Clear children
for (const c of this.children) { c.parentElement = null; c.parentNode = null; }
this.children = []; this.childNodes = [];
this.innerHTML = html;
// Parse simple HTML and add children
if (html) {
const parsed = parseHTMLFragments(html);
for (const c of parsed) this.appendChild(c);
this.textContent = this.children.map(c => c.textContent || '').join('') || html.replace(/<[^>]*>/g, '');
} else {
this.textContent = '';
}
}
get firstElementChild() { return this.children[0]||null; }
get lastElementChild() { return this.children[this.children.length-1]||null; }
get nextElementSibling() { if(!this.parentElement)return null; const i=this.parentElement.children.indexOf(this); return this.parentElement.children[i+1]||null; }
get previousElementSibling() { if(!this.parentElement)return null; const i=this.parentElement.children.indexOf(this); return i>0?this.parentElement.children[i-1]:null; }
showModal(){this.open=true;this.setAttribute('open','');} show(){this.open=true;} close(){this.open=false;this.removeAttribute('open');}
getAnimations(){return [];} getBoundingClientRect(){return{top:0,left:0,width:100,height:100,right:100,bottom:100};} scrollIntoView(){}
get ownerDocument() { return document; }
get offsetParent() { return this.parentElement; }
get offsetTop() { return 0; } get offsetLeft() { return 0; }
get scrollTop() { return 0; } set scrollTop(v) {} get scrollLeft() { return 0; } set scrollLeft(v) {}
get scrollHeight() { return 100; } get scrollWidth() { return 100; }
get clientHeight() { return 100; } get clientWidth() { return 100; }
insertAdjacentHTML(pos, html) {
// For non-HTML content (plain text/numbers), just append to innerHTML
if (typeof html !== 'string') html = String(html);
if (pos === 'beforeend' || pos === 'beforeEnd') {
this.innerHTML = (this.innerHTML || '') + html;
this.textContent = (this.textContent || '') + html.replace(/<[^>]*>/g, '');
} else if (pos === 'afterbegin' || pos === 'afterBegin') {
this.innerHTML = html + (this.innerHTML || '');
this.textContent = html.replace(/<[^>]*>/g, '') + (this.textContent || '');
} else if (pos === 'beforebegin' || pos === 'beforeBegin') {
if (this.parentElement) { this.parentElement.insertAdjacentHTML('beforeend', html); }
} else if (pos === 'afterend' || pos === 'afterEnd') {
if (this.parentElement) { this.parentElement.insertAdjacentHTML('beforeend', html); }
}
}
}
class CL { constructor(e){this._el=e;this._set=new Set();} _sync(str){this._set=new Set((str||'').split(/\s+/).filter(Boolean));} add(...c){for(const x of c)this._set.add(x);this._el.className=[...this._set].join(' ');this._el.attributes['class']=this._el.className;} remove(...c){for(const x of c)this._set.delete(x);this._el.className=[...this._set].join(' ');this._el.attributes['class']=this._el.className;} toggle(c,f){if(f!==undefined){if(f)this.add(c);else this.remove(c);return f;} if(this._set.has(c)){this.remove(c);return false;}else{this.add(c);return true;}} contains(c){return this._set.has(c);} get length(){return this._set.size;} [Symbol.iterator](){return this._set[Symbol.iterator]();} }
class Ev { constructor(t,o={}){this.type=t;this.bubbles=o.bubbles||false;this.cancelable=o.cancelable!==false;this.defaultPrevented=false;this._sp=false;this._si=false;this.target=null;this.currentTarget=null;this.detail=o.detail||null;} preventDefault(){this.defaultPrevented=true;} stopPropagation(){this._sp=true;} stopImmediatePropagation(){this._sp=true;this._si=true;} }
function parseHTMLFragments(html) {
const results = [];
const re = /<(\w+)([^>]*?)(?:\/>|>([\s\S]*?)<\/\1>)/g;
let m;
let lastIndex = 0;
while ((m = re.exec(html)) !== null) {
// Text before this tag
if (m.index > lastIndex) {
const text = html.slice(lastIndex, m.index).trim();
if (text) {
const tn = {nodeType:3, textContent:text, data:text};
// Can't push text nodes directly to El children; wrap if needed
}
}
const tag = m[1]; const attrs = m[2]; const inner = m[3] || '';
const el = new El(tag);
const attrRe = /([\w-]+)="([^"]*)"/g; let am;
while ((am = attrRe.exec(attrs))) el.setAttribute(am[1], am[2]);
// Also handle boolean attrs like disabled
const boolRe = /\s(\w+)(?=\s|\/|>|$)/g;
if (inner) {
// Recursively parse inner HTML
const innerEls = parseHTMLFragments(inner);
if (innerEls.length > 0) {
for (const c of innerEls) el.appendChild(c);
el.textContent = innerEls.map(c => c.textContent || '').join('');
} else {
el.textContent = inner;
}
el.innerHTML = inner;
}
results.push(el);
lastIndex = re.lastIndex;
}
// If no tags found, treat as text — create a span with textContent
if (results.length === 0 && html.trim()) {
const el = new El('span');
el.textContent = html.replace(/<[^>]*>/g, '');
el.innerHTML = html;
results.push(el);
}
return results;
}
function mt(e,s) {
if(!e||!e.tagName)return false;
s = s.trim();
if(s.startsWith('#'))return e.id===s.slice(1);
if(s.startsWith('.'))return e.classList.contains(s.slice(1));
if(s.startsWith('[')) {
const m = s.match(/^\[([^\]=]+)(?:="([^"]*)")?\]$/);
if(m) return m[2] !== undefined ? e.getAttribute(m[1]) === m[2] : e.hasAttribute(m[1]);
}
if(s.includes('.')) { const [tag, cls] = s.split('.'); return e.tagName.toLowerCase() === tag && e.classList.contains(cls); }
if(s.includes('#')) { const [tag, id] = s.split('#'); return e.tagName.toLowerCase() === tag && e.id === id; }
return e.tagName.toLowerCase() === s.toLowerCase();
}
function fnd(e,s) { for(const c of(e.children||[])){if(mt(c,s))return c;const f=fnd(c,s);if(f)return f;} return null; }
function fndAll(e,s) { const r=[];for(const c of(e.children||[])){if(mt(c,s))r.push(c);r.push(...fndAll(c,s));}r.item=function(i){return r[i]||null;};return r; }
const _body = new El('body');
const _html = new El('html');
_html.appendChild(_body);
const document = {
body: _body, documentElement: _html,
createElement(t){return new El(t);}, createElementNS(n,t){return new El(t);},
createDocumentFragment(){const f=new El('fragment');f.nodeType=11;return f;},
createTextNode(t){return{nodeType:3,textContent:t,data:t};},
getElementById(i){return fnd(_body,'#'+i);},
querySelector(s){return fnd(_body,s);}, querySelectorAll(s){return fndAll(_body,s);},
createEvent(t){return new Ev(t);}, addEventListener(){}, removeEventListener(){},
};
globalThis.document=document; globalThis.window=globalThis; globalThis.HTMLElement=El; globalThis.Element=El;
globalThis.Event=Ev; globalThis.CustomEvent=Ev; globalThis.NodeList=Array; globalThis.HTMLCollection=Array;
globalThis.getComputedStyle=(e)=>e?e.style:{}; globalThis.requestAnimationFrame=(f)=>{f();return 0;};
globalThis.cancelAnimationFrame=()=>{}; globalThis.MutationObserver=class{observe(){}disconnect(){}};
globalThis.ResizeObserver=class{observe(){}disconnect(){}}; globalThis.IntersectionObserver=class{observe(){}disconnect(){}};
globalThis.navigator={userAgent:'node'}; globalThis.location={href:'http://localhost/',pathname:'/',search:'',hash:''};
globalThis.history={pushState(){},replaceState(){},back(){},forward(){}};
const _origLog = console.log;
globalThis.console = { log: () => {}, error: () => {}, warn: () => {}, info: () => {}, debug: () => {} }; // suppress ALL console noise
const _log = _origLog; // keep reference for our own output
// ─── FFI ────────────────────────────────────────────────────────
K.registerNative('host-global',a=>{const n=a[0];return(n in globalThis)?globalThis[n]:null;});
K.registerNative('host-get',a=>{if(a[0]==null)return null;let v=a[0][a[1]];if(v===undefined)return null;if((a[1]==='innerHTML'||a[1]==='textContent'||a[1]==='value'||a[1]==='className')&&typeof v!=='string')v=String(v!=null?v:'');return v;});
K.registerNative('host-set!',a=>{if(a[0]!=null){const v=a[2]; if(a[1]==='innerHTML'&&a[0] instanceof El){const s=String(v!=null?v:'');a[0]._setInnerHTML(s);a[0][a[1]]=a[0].innerHTML;} else if(a[1]==='textContent'&&a[0] instanceof El){const s=String(v!=null?v:'');a[0].textContent=s;a[0].innerHTML=s;for(const c of a[0].children){c.parentElement=null;c.parentNode=null;}a[0].children=[];a[0].childNodes=[];} else{a[0][a[1]]=v;}} return a[2];});
K.registerNative('host-call',a=>{if(_testDeadline&&Date.now()>_testDeadline)throw new Error('TIMEOUT: wall clock exceeded');const[o,m,...r]=a;if(o==null){const f=globalThis[m];return typeof f==='function'?f.apply(null,r):null;}if(o&&typeof o[m]==='function'){try{const v=o[m].apply(o,r);return v===undefined?null:v;}catch(e){return null;}}return null;});
K.registerNative('host-new',a=>{const C=typeof a[0]==='string'?globalThis[a[0]]:a[0];return typeof C==='function'?new C(...a.slice(1)):null;});
K.registerNative('host-callback',a=>{const fn=a[0];if(typeof fn==='function'&&fn.__sx_handle===undefined)return fn;if(fn&&fn.__sx_handle!==undefined)return function(){const r=K.callFn(fn,Array.from(arguments));if(globalThis._driveAsync)globalThis._driveAsync(r);return r;};return function(){};});
K.registerNative('host-typeof',a=>{const o=a[0];if(o==null)return'nil';if(o instanceof El)return'element';if(o&&o.nodeType===3)return'text';if(o instanceof Ev)return'event';if(o instanceof Promise)return'promise';return typeof o;});
K.registerNative('host-await',a=>{});
K.registerNative('load-library!',()=>false);
let _testDeadline = 0;
// Mock fetch routes
const _fetchRoutes = {
'/test': { status: 200, body: 'yay', json: '{"foo":1}', html: '<div>yay</div>', number: '1.2' },
'/test-json': { status: 200, body: '{"foo":1}', json: '{"foo":1}' },
'/404': { status: 404, body: 'the body' },
'/number': { status: 200, body: '1.2' },
'/users/Joe': { status: 200, body: 'Joe', json: '{"name":"Joe"}' },
};
function _mockFetch(url) {
const route = _fetchRoutes[url] || _fetchRoutes['/test'];
return { ok: route.status < 400, status: route.status || 200, url: url || '/test',
_body: route.body || '', _json: route.json || route.body || '', _html: route.html || route.body || '' };
}
globalThis._driveAsync=function driveAsync(r,d){d=d||0;if(d>500||!r||!r.suspended)return;if(_testDeadline && Date.now()>_testDeadline)throw new Error('TIMEOUT: wall clock exceeded');const req=r.request;const items=req&&(req.items||req);const op=items&&items[0];const opName=typeof op==='string'?op:(op&&op.name)||String(op);
function doResume(v){try{const x=r.resume(v);driveAsync(x,d+1);}catch(e){}}
if(opName==='io-sleep'||opName==='wait')doResume(null);
else if(opName==='io-fetch'){
const url=typeof items[1]==='string'?items[1]:'/test';
const fmt=typeof items[2]==='string'?items[2]:'text';
const route=_fetchRoutes[url]||_fetchRoutes['/test'];
if(fmt==='json'){try{doResume(JSON.parse(route.json||route.body||'{}'));}catch(e){doResume(null);}}
else if(fmt==='html'){const frag=new El('fragment');frag.nodeType=11;frag.innerHTML=route.html||route.body||'';frag.textContent=frag.innerHTML.replace(/<[^>]*>/g,'');doResume(frag);}
else if(fmt==='response')doResume({ok:(route.status||200)<400,status:route.status||200,url});
else if(fmt.toLowerCase()==='number')doResume(parseFloat(route.number||route.body||'0'));
else doResume(route.body||'');
}
else if(opName==='io-parse-text'){const resp=items&&items[1];doResume(resp&&resp._body?resp._body:typeof resp==='string'?resp:'');}
else if(opName==='io-parse-json'){const resp=items&&items[1];try{doResume(JSON.parse(typeof resp==='string'?resp:resp&&resp._json?resp._json:'{}'));}catch(e){doResume(null);}}
else if(opName==='io-parse-html'){const frag=new El('fragment');frag.nodeType=11;doResume(frag);}
else if(opName==='io-settle')doResume(null);
else if(opName==='io-wait-event')doResume(null);
else if(opName==='io-transition')doResume(null);
};
K.eval('(define SX_VERSION "hs-test-1.0")');K.eval('(define SX_ENGINE "ocaml-vm-sandbox")');
K.eval('(define parse sx-parse)');K.eval('(define serialize sx-serialize)');
// ─── Load modules ──────────────────────────────────────────────
process.stderr.write('Loading modules...\n');
const t_mod = Date.now();
const WEB=['render','core-signals','signals','deps','router','page-helpers','freeze','dom','browser','adapter-html','adapter-sx','adapter-dom','boot-helpers','hypersx','engine','orchestration','boot'];
const HS=['hs-tokenizer','hs-parser','hs-compiler','hs-runtime','hs-integration'];
K.beginModuleLoad();
for(const mod of[...WEB,...HS]){const sp=path.join(SX_DIR,mod+'.sx');const lp=path.join(PROJECT,'lib/hyperscript',mod.replace(/^hs-/,'')+'.sx');let s;try{s=fs.existsSync(sp)?fs.readFileSync(sp,'utf8'):fs.readFileSync(lp,'utf8');}catch(e){continue;}try{K.load(s);}catch(e){process.stderr.write(`LOAD ERROR: ${mod}: ${e.message}\n`);}}
K.endModuleLoad();
process.stderr.write(`Modules loaded in ${Date.now()-t_mod}ms\n`);
// ─── Test framework ────────────────────────────────────────────
K.eval('(define _test-registry (list))');K.eval('(define _test-suite "")');
K.eval('(define push-suite (fn (name) (set! _test-suite name)))');
K.eval('(define pop-suite (fn () (set! _test-suite "")))');
K.eval('(define _current-test-name "")');
K.eval('(define try-call (fn (thunk) (set! _test-registry (append _test-registry (list {:suite _test-suite :name _current-test-name :thunk thunk}))) {:ok true}))');
K.eval('(define report-pass (fn (n) true))');
K.eval('(define report-fail (fn (n e) true))');
K.eval(`(define _run-test-thunk
(fn (thunk)
(guard (exn
(true {:ok false :error (if (string? exn) exn (str exn))}))
(thunk)
{:ok true})))`);
process.stderr.write('Loading tests...\n');
const t_tests = Date.now();
for(const f of['spec/harness.sx','spec/tests/test-framework.sx','spec/tests/test-hyperscript-behavioral.sx']){
const t0=Date.now();
try{K.load(fs.readFileSync(path.join(PROJECT,f),'utf8'));}catch(e){process.stderr.write(`TEST LOAD ERROR: ${f}: ${e.message}\n`);}
process.stderr.write(` ${path.basename(f)}: ${Date.now()-t0}ms\n`);
if(f==='spec/tests/test-framework.sx'){
K.eval(`(defmacro deftest (name &rest body)
\`(do (set! _current-test-name ,name)
(let ((result (try-call (fn () ,@body))))
(if (get result "ok")
(report-pass ,name)
(report-fail ,name (get result "error"))))))`);
}
}
process.stderr.write(`Tests loaded in ${Date.now()-t_tests}ms\n`);
const testCount = K.eval('(len _test-registry)');
// Pre-read names
const names = [];
for(let i=0;i<testCount;i++) names.push({
s: K.eval(`(get (nth _test-registry ${i}) "suite")`)||'',
n: K.eval(`(get (nth _test-registry ${i}) "name")`)||`test-${i}`,
});
const startTest = parseInt(process.env.HS_START || '0');
const endTest = parseInt(process.env.HS_END || String(testCount));
process.stdout.write(`Running tests ${startTest}-${endTest-1} of ${testCount} (step limit: ${STEP_LIMIT})...\n`);
let passed=0,failed=0;
const cats={};const errTypes={};
const SUITE_FILTER = process.env.HS_SUITE;
for(let i=startTest;i<Math.min(endTest,testCount);i++){
if(SUITE_FILTER && names[i].s!==SUITE_FILTER) continue;
const {s:suite,n:name}=names[i];
if(!cats[suite])cats[suite]={p:0,f:0,errs:[]};
// Reset body
_body.children=[];_body.childNodes=[];_body.innerHTML='';_body.textContent='';
// Enable step limit for timeout protection
setStepLimit(STEP_LIMIT);
_testDeadline = Date.now() + 10000; // 10 second wall-clock timeout per test
if(process.env.HS_VERBOSE)process.stderr.write(`T${i} `);
let ok=false,err=null;
try{
// Use SX-level guard to catch errors, avoiding __sxR side-channel issues
// Returns a dict with :ok and :error keys
K.eval(`(define _test-result (_run-test-thunk (get (nth _test-registry ${i}) "thunk")))`);
const isOk=K.eval('(get _test-result "ok")');
if(isOk===true){ok=true;}
else{
const errMsg=K.eval('(get _test-result "error")');
err=errMsg?String(errMsg).slice(0,150):'unknown error';
}
}catch(e){err=(e.message||'').slice(0,150);}
setStepLimit(0); // disable step limit between tests
const elapsed = Date.now() - (_testDeadline - 3000); // ms since test start
if(ok){passed++;cats[suite].p++;}
else{
failed++;cats[suite].f++;cats[suite].errs.push({name,err});
let t='other';
if(err&&err.includes('TIMEOUT'))t='timeout';
else if(err&&err.includes('NOT IMPLEMENTED'))t='stub';
else if(err&&err.includes('Assertion'))t='assert-fail';
else if(err&&err.includes('Expected'))t='wrong-value';
else if(err&&err.includes('Undefined symbol'))t='undef-sym';
else if(err&&err.includes('Unhandled'))t='unhandled';
errTypes[t]=(errTypes[t]||0)+1;
}
_testDeadline = 0;
if((i+1)%100===0)process.stdout.write(` ${i+1}/${testCount} (${passed} pass, ${failed} fail)\n`);
if(elapsed > 5000)process.stdout.write(` SLOW: test ${i} took ${elapsed}ms [${suite}] ${name}\n`);
if(!ok && err && err.includes('TIMEOUT'))process.stdout.write(` TIMEOUT: test ${i} [${suite}] ${name}\n`);
if(!ok && err && err.includes('Expected') && err.includes(', got '))process.stdout.write(` WRONG: test ${i} [${suite}] ${name}${err}\n`);
if(!ok && err && err.includes("at position"))process.stdout.write(` PARSE: test ${i} [${suite}] ${name}${err}\n`);
}
process.stdout.write(`\nResults: ${passed}/${passed+failed} (${(100*passed/(passed+failed)).toFixed(0)}%)\n\n`);
process.stdout.write('By category:\n');
for(const[cat,s]of Object.entries(cats).sort((a,b)=>{const ra=a[1].p/(a[1].p+a[1].f);const rb=b[1].p/(b[1].p+b[1].f);return rb-ra;})){
const total=s.p+s.f;const pct=(100*s.p/total).toFixed(0);
const mark=s.f===0?`${s.p}`:`${s.p}/${total} (${pct}%)`;
process.stdout.write(` ${cat}: ${mark}\n`);
}
process.stdout.write('\nFailure types:\n');
for(const[t,n]of Object.entries(errTypes).sort((a,b)=>b[1]-a[1])) process.stdout.write(` ${t}: ${n}\n`);
const ue={};
for(const[cat,s]of Object.entries(cats))for(const{err}of s.errs){const e=(err||'').slice(0,100);ue[e]=(ue[e]||0)+1;}
process.stdout.write(`\nUnique errors (${Object.keys(ue).length}):\n`);
for(const[e,n]of Object.entries(ue).sort((a,b)=>b[1]-a[1]).slice(0,30)) process.stdout.write(` [${n}x] ${e}\n`);
// Full failure list
process.stdout.write('\nAll failures:\n');
for(const[cat,s]of Object.entries(cats))for(const{name,err}of s.errs){
process.stdout.write(` [${cat}] ${name}: ${err}\n`);
}
// Test-specific debug dump
if(process.env.HS_DEBUG_TEST){
const target = process.env.HS_DEBUG_TEST;
for(let i=0;i<names.length;i++){
if(names[i].n===target){
console.log('FOUND TARGET INDEX:', i, 'suite:', names[i].s);
break;
}
}
}

View File

@@ -47,9 +47,56 @@ def parse_js_value(s):
m = re.match(r'^\[(.+)\]$', s, re.DOTALL) m = re.match(r'^\[(.+)\]$', s, re.DOTALL)
if m: if m:
return parse_js_array(m.group(1)) return parse_js_array(m.group(1))
# Empty object
if s == '{}':
return '{}'
# Object literal — convert to SX dict {:key val ...}
m = re.match(r'^\{(.+)\}$', s, re.DOTALL)
if m:
return parse_js_object(m.group(1))
return None return None
def parse_js_object(inner):
"""Parse JS object contents into SX dict {:key val ...}. Handles nested."""
pairs = split_js_object(inner)
if pairs is None:
return None
sx_pairs = []
for pair in pairs:
km = re.match(r'\s*["\']?(\w+)["\']?\s*:\s*(.+)$', pair.strip(), re.DOTALL)
if not km:
return None
k = km.group(1)
v = parse_js_value(km.group(2).strip())
if v is None:
return None
sx_pairs.append(f':{k} {v}')
return '{' + ' '.join(sx_pairs) + '}'
def split_js_object(s):
"""Split JS object-content by commas respecting nesting."""
items = []
depth = 0
current = ''
for ch in s:
if ch in '([{':
depth += 1
current += ch
elif ch in ')]}':
depth -= 1
current += ch
elif ch == ',' and depth == 0:
items.append(current)
current = ''
else:
current += ch
if current.strip():
items.append(current)
return items if items else None
def parse_js_array(inner): def parse_js_array(inner):
"""Parse JS array contents into SX (list ...). Handles nested arrays.""" """Parse JS array contents into SX (list ...). Handles nested arrays."""
items = split_js_array(inner) items = split_js_array(inner)