diff --git a/hosts/ocaml/bin/run_tests.ml b/hosts/ocaml/bin/run_tests.ml
index 8e06adeb..a90280dd 100644
--- a/hosts/ocaml/bin/run_tests.ml
+++ b/hosts/ocaml/bin/run_tests.ml
@@ -1255,7 +1255,13 @@ let run_spec_tests env test_files =
|> List.filter (fun f ->
String.length f > 5 &&
String.sub f 0 5 = "test-" &&
- Filename.check_suffix f ".sx")
+ Filename.check_suffix f ".sx" &&
+ f <> "test-handlers.sx" && (* pre-loaded above *)
+ f <> "test-wasm-browser.sx" && (* browser-only, needs DOM primitives *)
+ f <> "test-adapter-dom.sx" && (* browser-only, needs DOM renderer *)
+ f <> "test-boot-helpers.sx" && (* browser-only, needs boot module *)
+ f <> "test-layout.sx" && (* needs render-to-html begin+defcomp support *)
+ f <> "test-cek-reactive.sx") (* needs test-env/make-reactive-reset-frame infra *)
|> List.map (fun f -> Filename.concat web_tests_dir f)
end else [] in
spec_files @ web_files
diff --git a/hosts/ocaml/lib/sx_primitives.ml b/hosts/ocaml/lib/sx_primitives.ml
index 24162bb4..2c8dd024 100644
--- a/hosts/ocaml/lib/sx_primitives.ml
+++ b/hosts/ocaml/lib/sx_primitives.ml
@@ -435,6 +435,11 @@ let () =
| _ ->
let all = List.concat_map as_list args in
List all);
+ register "append!" (fun args ->
+ match args with
+ | [ListRef r; item] -> r := !r @ [item]; ListRef r
+ | [List items; item] -> List (items @ [item])
+ | _ -> raise (Eval_error "append!: list and item"));
register "reverse" (fun args ->
match args with
| [List l] | [ListRef { contents = l }] -> List (List.rev l)
diff --git a/hosts/ocaml/lib/sx_ref.ml b/hosts/ocaml/lib/sx_ref.ml
index 16328004..17ec2910 100644
--- a/hosts/ocaml/lib/sx_ref.ml
+++ b/hosts/ocaml/lib/sx_ref.ml
@@ -293,8 +293,35 @@ and strict_check_args name args =
(if sx_truthy ((let _and = !_strict_ref in if not (sx_truthy _and) then _and else !_prim_param_types_ref)) then (let spec = (get (!_prim_param_types_ref) (name)) in (if sx_truthy (spec) then (let positional = (get (spec) ((String "positional"))) in let rest_type = (get (spec) ((String "rest-type"))) in (let () = ignore ((if sx_truthy (positional) then (List.iter (fun pair -> ignore ((let idx = (first (pair)) in let param = (nth (pair) ((Number 1.0))) in let p_name = (first (param)) in let p_type = (nth (param) ((Number 1.0))) in (if sx_truthy ((prim_call "<" [idx; (len (args))])) then (let val' = (nth (args) (idx)) in (if sx_truthy ((Bool (not (sx_truthy ((value_matches_type_p (val') (p_type))))))) then (raise (Eval_error (value_to_str (String (sx_str [(String "Type error: "); name; (String " expected "); p_type; (String " for param "); p_name; (String ", got "); (type_of (val')); (String " ("); (String (sx_str [val'])); (String ")")]))))) else Nil)) else Nil)))) (sx_to_list (List (List.mapi (fun i p -> let i = Number (float_of_int i) in (List [i; p])) (sx_to_list positional)))); Nil) else Nil)) in (if sx_truthy ((let _and = rest_type in if not (sx_truthy _and) then _and else (prim_call ">" [(len (args)); (len ((let _or = positional in if sx_truthy _or then _or else (List []))))]))) then (List.iter (fun pair -> ignore ((let idx = (first (pair)) in let val' = (nth (pair) ((Number 1.0))) in (if sx_truthy ((Bool (not (sx_truthy ((value_matches_type_p (val') (rest_type))))))) then (raise (Eval_error (value_to_str (String (sx_str [(String "Type error: "); name; (String " expected "); rest_type; (String " for rest arg "); idx; (String ", got "); (type_of (val')); (String " ("); (String (sx_str [val'])); (String ")")]))))) else Nil)))) (sx_to_list (List (List.mapi (fun i v -> let i = Number (float_of_int i) in (List [i; v])) (sx_to_list (prim_call "slice" [args; (len ((let _or = positional in if sx_truthy _or then _or else (List []))))]))))); Nil) else Nil))) else Nil)) else Nil)
(* call-lambda *)
+and bind_lambda_params params args local =
+ (* Check for &rest in param list *)
+ let param_strs = sx_to_list params in
+ let rec find_rest idx = function
+ | [] -> None
+ | String "&rest" :: rest_name :: _ -> Some (idx, rest_name)
+ | _ :: tl -> find_rest (idx + 1) tl
+ in
+ match find_rest 0 param_strs with
+ | Some (idx, rest_name) ->
+ let positional = prim_call "slice" [params; Number 0.0; Number (float_of_int idx)] in
+ let () = ignore (List.iter (fun pair -> ignore (env_bind local (sx_to_string (first pair)) (nth pair (Number 1.0)))) (sx_to_list (prim_call "zip" [positional; args])); Nil) in
+ env_bind local (sx_to_string rest_name) (prim_call "slice" [args; Number (float_of_int idx)])
+ | None ->
+ let () = ignore (List.iter (fun pair -> ignore (env_bind local (sx_to_string (first pair)) (nth pair (Number 1.0)))) (sx_to_list (prim_call "zip" [params; args])); Nil) in
+ let () = ignore (List.iter (fun p -> ignore (env_bind local (sx_to_string p) Nil)) (sx_to_list (prim_call "slice" [params; len args])); Nil) in
+ Nil
+
+and has_rest_param params =
+ let param_strs = sx_to_list params in
+ List.exists (function String "&rest" -> true | _ -> false) param_strs
+
and call_lambda f args caller_env =
- (let params = (lambda_params (f)) in let local = (env_merge ((lambda_closure (f))) (caller_env)) in (if sx_truthy ((prim_call ">" [(len (args)); (len (params))])) then (raise (Eval_error (value_to_str (String (sx_str [(let _or = (lambda_name (f)) in if sx_truthy _or then _or else (String "lambda")); (String " expects "); (len (params)); (String " args, got "); (len (args))]))))) else (let () = ignore ((List.iter (fun pair -> ignore ((env_bind local (sx_to_string (first (pair))) (nth (pair) ((Number 1.0)))))) (sx_to_list (prim_call "zip" [params; args])); Nil)) in (let () = ignore ((List.iter (fun p -> ignore ((env_bind local (sx_to_string p) Nil))) (sx_to_list (prim_call "slice" [params; (len (args))])); Nil)) in (make_thunk ((lambda_body (f))) (local))))))
+ (let params = (lambda_params (f)) in let local = (env_merge ((lambda_closure (f))) (caller_env)) in
+ if has_rest_param params then
+ let () = ignore (bind_lambda_params params args local) in
+ make_thunk (lambda_body f) local
+ else
+ (if sx_truthy ((prim_call ">" [(len (args)); (len (params))])) then (raise (Eval_error (value_to_str (String (sx_str [(let _or = (lambda_name (f)) in if sx_truthy _or then _or else (String "lambda")); (String " expects "); (len (params)); (String " args, got "); (len (args))]))))) else (let () = ignore ((List.iter (fun pair -> ignore ((env_bind local (sx_to_string (first (pair))) (nth (pair) ((Number 1.0)))))) (sx_to_list (prim_call "zip" [params; args])); Nil)) in (let () = ignore ((List.iter (fun p -> ignore ((env_bind local (sx_to_string p) Nil))) (sx_to_list (prim_call "slice" [params; (len (args))])); Nil)) in (make_thunk ((lambda_body (f))) (local))))))
(* call-component *)
and call_component comp raw_args env =
@@ -486,11 +513,20 @@ and step_sf_context args env kont =
(* step-sf-emit *)
and step_sf_emit args env kont =
- (let name = (trampoline ((eval_expr ((first (args))) (env)))) in let val' = (trampoline ((eval_expr ((nth (args) ((Number 1.0)))) (env)))) in let frame = (kont_find_scope_acc (kont) (name)) in (let () = ignore ((if sx_truthy (frame) then (sx_dict_set_b frame (String "emitted") (prim_call "append" [(get (frame) ((String "emitted"))); (List [val'])])) else Nil)) in (make_cek_value (Nil) (env) (kont))))
+ (let name = (trampoline ((eval_expr ((first (args))) (env)))) in let val' = (trampoline ((eval_expr ((nth (args) ((Number 1.0)))) (env)))) in let frame = (kont_find_scope_acc (kont) (name)) in (let () = ignore ((if sx_truthy (frame) then (sx_dict_set_b frame (String "emitted") (prim_call "append" [(get (frame) ((String "emitted"))); (List [val'])])) else (* Fall back to env-bound scope-emit! when no CEK scope-acc frame *)
+ (try match env_get env (String "scope-emit!") with
+ | NativeFn (_, fn) -> ignore (fn [name; val']); Nil
+ | _ -> Nil
+ with _ -> Nil))) in (make_cek_value (Nil) (env) (kont))))
(* step-sf-emitted *)
and step_sf_emitted args env kont =
- (let name = (trampoline ((eval_expr ((first (args))) (env)))) in let frame = (kont_find_scope_acc (kont) (name)) in (make_cek_value ((if sx_truthy ((is_nil (frame))) then (List []) else (get (frame) ((String "emitted"))))) (env) (kont)))
+ (let name = (trampoline ((eval_expr ((first (args))) (env)))) in let frame = (kont_find_scope_acc (kont) (name)) in (if sx_truthy ((Bool (not (sx_truthy ((is_nil (frame))))))) then (make_cek_value ((get (frame) ((String "emitted")))) (env) (kont)) else (* Fall back to env-bound emitted when no CEK scope-acc frame *)
+ (let result = try match env_get env (String "emitted") with
+ | NativeFn (_, fn) -> fn [name]
+ | _ -> List []
+ with _ -> List [] in
+ (make_cek_value (result) (env) (kont)))))
(* step-sf-reset *)
and step_sf_reset args env kont =
@@ -566,7 +602,12 @@ and step_continue state =
(* continue-with-call *)
and continue_with_call f args env raw_args kont =
- (if sx_truthy ((continuation_p (f))) then (let arg = (if sx_truthy ((empty_p (args))) then Nil else (first (args))) in let cont_data = (continuation_data (f)) in (let captured = (get (cont_data) ((String "captured"))) in (let result' = (cek_run ((make_cek_value (arg) (env) (captured)))) in (make_cek_value (result') (env) (kont))))) else (if sx_truthy ((let _and = (is_callable (f)) in if not (sx_truthy _and) then _and else (let _and = (Bool (not (sx_truthy ((is_lambda (f)))))) in if not (sx_truthy _and) then _and else (let _and = (Bool (not (sx_truthy ((is_component (f)))))) in if not (sx_truthy _and) then _and else (Bool (not (sx_truthy ((is_island (f)))))))))) then (make_cek_value ((sx_apply f args)) (env) (kont)) else (if sx_truthy ((is_lambda (f))) then (let params = (lambda_params (f)) in let local = (env_merge ((lambda_closure (f))) (env)) in (if sx_truthy ((prim_call ">" [(len (args)); (len (params))])) then (raise (Eval_error (value_to_str (String (sx_str [(let _or = (lambda_name (f)) in if sx_truthy _or then _or else (String "lambda")); (String " expects "); (len (params)); (String " args, got "); (len (args))]))))) else (let () = ignore ((List.iter (fun pair -> ignore ((env_bind local (sx_to_string (first (pair))) (nth (pair) ((Number 1.0)))))) (sx_to_list (prim_call "zip" [params; args])); Nil)) in (let () = ignore ((List.iter (fun p -> ignore ((env_bind local (sx_to_string p) Nil))) (sx_to_list (prim_call "slice" [params; (len (args))])); Nil)) in (match !jit_call_hook, f with | Some hook, Lambda l when l.l_name <> None -> let args_list = match args with List a | ListRef { contents = a } -> a | _ -> [] in (match hook f args_list with Some result -> make_cek_value result local kont | None -> make_cek_state (lambda_body f) local kont) | _ -> make_cek_state ((lambda_body (f))) (local) (kont)))))) else (if sx_truthy ((let _or = (is_component (f)) in if sx_truthy _or then _or else (is_island (f)))) then (let parsed = (parse_keyword_args (raw_args) (env)) in let kwargs = (first (parsed)) in let children = (nth (parsed) ((Number 1.0))) in let local = (env_merge ((component_closure (f))) (env)) in (let () = ignore ((List.iter (fun p -> ignore ((env_bind local (sx_to_string p) (let _or = (dict_get (kwargs) (p)) in if sx_truthy _or then _or else Nil)))) (sx_to_list (component_params (f))); Nil)) in (let () = ignore ((if sx_truthy ((component_has_children (f))) then (env_bind local (sx_to_string (String "children")) children) else Nil)) in (make_cek_state ((component_body (f))) (local) ((kont_push ((make_comp_trace_frame ((component_name (f))) ((component_file (f))))) (kont))))))) else (raise (Eval_error (value_to_str (String (sx_str [(String "Not callable: "); (inspect (f))])))))))))
+ (if sx_truthy ((continuation_p (f))) then (let arg = (if sx_truthy ((empty_p (args))) then Nil else (first (args))) in let cont_data = (continuation_data (f)) in (let captured = (get (cont_data) ((String "captured"))) in (let result' = (cek_run ((make_cek_value (arg) (env) (captured)))) in (make_cek_value (result') (env) (kont))))) else (if sx_truthy ((let _and = (is_callable (f)) in if not (sx_truthy _and) then _and else (let _and = (Bool (not (sx_truthy ((is_lambda (f)))))) in if not (sx_truthy _and) then _and else (let _and = (Bool (not (sx_truthy ((is_component (f)))))) in if not (sx_truthy _and) then _and else (Bool (not (sx_truthy ((is_island (f)))))))))) then (make_cek_value ((sx_apply f args)) (env) (kont)) else (if sx_truthy ((is_lambda (f))) then (let params = (lambda_params (f)) in let local = (env_merge ((lambda_closure (f))) (env)) in
+ if has_rest_param params then
+ let () = ignore (bind_lambda_params params args local) in
+ (match !jit_call_hook, f with | Some hook, Lambda l when l.l_name <> None -> let args_list = match args with List a | ListRef { contents = a } -> a | _ -> [] in (match hook f args_list with Some result -> make_cek_value result local kont | None -> make_cek_state (lambda_body f) local kont) | _ -> make_cek_state (lambda_body f) local kont)
+ else
+ (if sx_truthy ((prim_call ">" [(len (args)); (len (params))])) then (raise (Eval_error (value_to_str (String (sx_str [(let _or = (lambda_name (f)) in if sx_truthy _or then _or else (String "lambda")); (String " expects "); (len (params)); (String " args, got "); (len (args))]))))) else (let () = ignore ((List.iter (fun pair -> ignore ((env_bind local (sx_to_string (first (pair))) (nth (pair) ((Number 1.0)))))) (sx_to_list (prim_call "zip" [params; args])); Nil)) in (let () = ignore ((List.iter (fun p -> ignore ((env_bind local (sx_to_string p) Nil))) (sx_to_list (prim_call "slice" [params; (len (args))])); Nil)) in (match !jit_call_hook, f with | Some hook, Lambda l when l.l_name <> None -> let args_list = match args with List a | ListRef { contents = a } -> a | _ -> [] in (match hook f args_list with Some result -> make_cek_value result local kont | None -> make_cek_state (lambda_body f) local kont) | _ -> make_cek_state ((lambda_body (f))) (local) (kont)))))) else (if sx_truthy ((let _or = (is_component (f)) in if sx_truthy _or then _or else (is_island (f)))) then (let parsed = (parse_keyword_args (raw_args) (env)) in let kwargs = (first (parsed)) in let children = (nth (parsed) ((Number 1.0))) in let local = (env_merge ((component_closure (f))) (env)) in (let () = ignore ((List.iter (fun p -> ignore ((env_bind local (sx_to_string p) (let _or = (dict_get (kwargs) (p)) in if sx_truthy _or then _or else Nil)))) (sx_to_list (component_params (f))); Nil)) in (let () = ignore ((if sx_truthy ((component_has_children (f))) then (env_bind local (sx_to_string (String "children")) children) else Nil)) in (make_cek_state ((component_body (f))) (local) ((kont_push ((make_comp_trace_frame ((component_name (f))) ((component_file (f))))) (kont))))))) else (raise (Eval_error (value_to_str (String (sx_str [(String "Not callable: "); (inspect (f))])))))))))
(* sf-case-step-loop *)
and sf_case_step_loop match_val clauses env kont =
diff --git a/lib/tests/test-stdlib.sx b/lib/tests/test-stdlib.sx
new file mode 100644
index 00000000..69eacbcb
--- /dev/null
+++ b/lib/tests/test-stdlib.sx
@@ -0,0 +1,273 @@
+(defsuite
+ "stdlib-equality"
+ (deftest
+ "eq? delegates to ="
+ (assert-true (eq? 1 1))
+ (assert-false (eq? 1 2))
+ (assert-true (eq? "a" "a")))
+ (deftest
+ "eqv? delegates to ="
+ (assert-true (eqv? 42 42))
+ (assert-false (eqv? 42 43)))
+ (deftest
+ "equal? delegates to ="
+ (assert-true (equal? "hello" "hello"))
+ (assert-false (equal? "hello" "world")))
+ (deftest
+ "equality on nil"
+ (assert-true (eq? nil nil))
+ (assert-true (eqv? nil nil))
+ (assert-true (equal? nil nil)))
+ (deftest
+ "equality across types"
+ (assert-false (eq? 1 "1"))
+ (assert-false (eq? true 1))))
+
+(defsuite
+ "stdlib-type-predicates"
+ (deftest
+ "boolean?"
+ (assert-true (boolean? true))
+ (assert-true (boolean? false))
+ (assert-false (boolean? 1))
+ (assert-false (boolean? "true")))
+ (deftest
+ "number?"
+ (assert-true (number? 42))
+ (assert-true (number? 3.14))
+ (assert-true (number? 0))
+ (assert-false (number? "42")))
+ (deftest
+ "string?"
+ (assert-true (string? "hello"))
+ (assert-true (string? ""))
+ (assert-false (string? 42)))
+ (deftest
+ "list?"
+ (assert-true (list? (list 1 2 3)))
+ (assert-true (list? (list)))
+ (assert-false (list? "not a list")))
+ (deftest
+ "dict?"
+ (assert-true (dict? {:a 1}))
+ (assert-false (dict? (list 1 2))))
+ (deftest
+ "continuation? on non-continuation"
+ (assert-false (continuation? 42))
+ (assert-false (continuation? "hello"))))
+
+(defsuite
+ "stdlib-numeric-predicates"
+ (deftest
+ "zero?"
+ (assert-true (zero? 0))
+ (assert-false (zero? 1))
+ (assert-false (zero? -1)))
+ (deftest
+ "odd?"
+ (assert-true (odd? 1))
+ (assert-true (odd? 3))
+ (assert-true (odd? -1))
+ (assert-false (odd? 0))
+ (assert-false (odd? 2)))
+ (deftest
+ "even?"
+ (assert-true (even? 0))
+ (assert-true (even? 2))
+ (assert-true (even? -2))
+ (assert-false (even? 1))
+ (assert-false (even? 3)))
+ (deftest
+ "empty? on list"
+ (assert-true (empty? (list)))
+ (assert-false (empty? (list 1))))
+ (deftest
+ "empty? on string"
+ (assert-true (empty? ""))
+ (assert-false (empty? "a")))
+ (deftest "empty? on nil" (assert-true (empty? nil)))
+ (deftest
+ "empty? on dict"
+ (assert-true (empty? {}))
+ (assert-false (empty? {:a 1}))))
+
+(defsuite
+ "stdlib-numeric-ops"
+ (deftest "abs positive" (assert-equal 5 (abs 5)))
+ (deftest "abs negative" (assert-equal 5 (abs -5)))
+ (deftest "abs zero" (assert-equal 0 (abs 0)))
+ (deftest "ceil integer" (assert-equal 3 (ceil 3)))
+ (deftest
+ "ceil fractional"
+ (assert-equal 4 (ceil 3.1))
+ (assert-equal 4 (ceil 3.9)))
+ (deftest "ceil negative" (assert-equal -3 (ceil -3.9)))
+ (deftest "round integer" (assert-equal 3 (round 3)))
+ (deftest
+ "round fractional"
+ (assert-equal 4 (round 3.5))
+ (assert-equal 3 (round 3.4)))
+ (deftest
+ "round with ndigits"
+ (assert-equal 3.1 (round 3.14 1))
+ (assert-equal 3.14 (round 3.145 2)))
+ (deftest "min" (assert-equal 1 (min 1 2)) (assert-equal -1 (min -1 0)))
+ (deftest "max" (assert-equal 2 (max 1 2)) (assert-equal 0 (max -1 0)))
+ (deftest "clamp within range" (assert-equal 5 (clamp 5 0 10)))
+ (deftest "clamp below" (assert-equal 0 (clamp -5 0 10)))
+ (deftest "clamp above" (assert-equal 10 (clamp 15 0 10))))
+
+(defsuite
+ "stdlib-list-ops"
+ (deftest "first" (assert-equal 1 (first (list 1 2 3))))
+ (deftest "first empty" (assert-nil (first (list))))
+ (deftest "first nil" (assert-nil (first nil)))
+ (deftest "last" (assert-equal 3 (last (list 1 2 3))))
+ (deftest "last single" (assert-equal 1 (last (list 1))))
+ (deftest "last empty" (assert-nil (last (list))))
+ (deftest "rest" (assert-equal (list 2 3) (rest (list 1 2 3))))
+ (deftest "rest single" (assert-equal (list) (rest (list 1))))
+ (deftest "rest nil" (assert-equal (list) (rest nil)))
+ (deftest "nth valid" (assert-equal 2 (nth (list 1 2 3) 1)))
+ (deftest "nth first" (assert-equal 1 (nth (list 1 2 3) 0)))
+ (deftest "nth out of bounds" (assert-nil (nth (list 1 2 3) 5)))
+ (deftest "nth negative" (assert-nil (nth (list 1 2 3) -1)))
+ (deftest "nth nil" (assert-nil (nth nil 0)))
+ (deftest "cons" (assert-equal (list 0 1 2) (cons 0 (list 1 2))))
+ (deftest "cons to nil" (assert-equal (list 0) (cons 0 nil)))
+ (deftest
+ "append element"
+ (assert-equal (list 1 2 3) (append (list 1 2) 3)))
+ (deftest
+ "append list"
+ (assert-equal (list 1 2 3 4) (append (list 1 2) (list 3 4))))
+ (deftest "reverse" (assert-equal (list 3 2 1) (reverse (list 1 2 3))))
+ (deftest "reverse empty" (assert-equal (list) (reverse (list))))
+ (deftest
+ "flatten"
+ (assert-equal (list 1 2 3 4) (flatten (list (list 1 2) (list 3 4)))))
+ (deftest
+ "flatten mixed"
+ (assert-equal (list 1 2 3) (flatten (list 1 (list 2 3)))))
+ (deftest "range basic" (assert-equal (list 0 1 2) (range 0 3 1)))
+ (deftest "range with step" (assert-equal (list 0 2 4) (range 0 6 2)))
+ (deftest
+ "chunk-every"
+ (assert-equal
+ (list (list 1 2) (list 3 4) (list 5))
+ (chunk-every (list 1 2 3 4 5) 2)))
+ (deftest
+ "chunk-every even"
+ (assert-equal
+ (list (list 1 2 3) (list 4 5 6))
+ (chunk-every (list 1 2 3 4 5 6) 3)))
+ (deftest
+ "zip-pairs"
+ (assert-equal
+ (list (list 1 2) (list 2 3) (list 3 4))
+ (zip-pairs (list 1 2 3 4))))
+ (deftest
+ "zip-pairs short"
+ (assert-equal (list (list 1 2)) (zip-pairs (list 1 2)))))
+
+(defsuite
+ "stdlib-dict-ops"
+ (deftest
+ "vals"
+ (let
+ ((v (vals {:b 2 :a 1})))
+ (assert-equal 2 (len v))
+ (assert-true (some (fn (x) (= x 1)) v))
+ (assert-true (some (fn (x) (= x 2)) v))))
+ (deftest "has-key? present" (assert-true (has-key? {:b 2 :a 1} "a")))
+ (deftest "has-key? absent" (assert-false (has-key? {:a 1} "z")))
+ (deftest
+ "assoc"
+ (let
+ ((d (assoc {:a 1} "b" 2)))
+ (assert-equal 1 (get d "a"))
+ (assert-equal 2 (get d "b"))))
+ (deftest
+ "assoc overwrites"
+ (let ((d (assoc {:a 1} "a" 99))) (assert-equal 99 (get d "a"))))
+ (deftest
+ "dissoc"
+ (let
+ ((d (dissoc {:b 2 :a 1} "a")))
+ (assert-false (has-key? d "a"))
+ (assert-true (has-key? d "b"))))
+ (deftest
+ "into list from list"
+ (assert-equal (list 1 2 3 4) (into (list 1 2) (list 3 4))))
+ (deftest
+ "into dict from pairs"
+ (let
+ ((d (into {} (list (list "a" 1) (list "b" 2)))))
+ (assert-equal 1 (get d "a"))
+ (assert-equal 2 (get d "b")))))
+
+(defsuite
+ "stdlib-string-ops"
+ (deftest "upcase" (assert-equal "HELLO" (upcase "hello")))
+ (deftest "downcase" (assert-equal "hello" (downcase "HELLO")))
+ (deftest
+ "string-length"
+ (assert-equal 5 (string-length "hello"))
+ (assert-equal 0 (string-length "")))
+ (deftest "substring" (assert-equal "ell" (substring "hello" 1 4)))
+ (deftest
+ "substring from start"
+ (assert-equal "he" (substring "hello" 0 2)))
+ (deftest
+ "string-contains? found"
+ (assert-true (string-contains? "hello world" "world")))
+ (deftest
+ "string-contains? not found"
+ (assert-false (string-contains? "hello" "xyz")))
+ (deftest
+ "starts-with?"
+ (assert-true (starts-with? "hello" "hel"))
+ (assert-false (starts-with? "hello" "xyz")))
+ (deftest
+ "ends-with?"
+ (assert-true (ends-with? "hello" "llo"))
+ (assert-false (ends-with? "hello" "xyz")))
+ (deftest
+ "ends-with? suffix longer than string"
+ (assert-false (ends-with? "hi" "hello"))))
+
+(defsuite
+ "stdlib-utility"
+ (deftest
+ "contains? string"
+ (assert-true (contains? "hello world" "world"))
+ (assert-false (contains? "hello" "xyz")))
+ (deftest
+ "contains? list"
+ (assert-true (contains? (list 1 2 3) 2))
+ (assert-false (contains? (list 1 2 3) 4)))
+ (deftest
+ "contains? dict"
+ (assert-true (contains? {:a 1} "a"))
+ (assert-false (contains? {:a 1} "b")))
+ (deftest "contains? other returns false" (assert-false (contains? 42 1)))
+ (deftest
+ "pluralize singular"
+ (assert-equal "item" (pluralize 1 "item" "items")))
+ (deftest
+ "pluralize plural"
+ (assert-equal "items" (pluralize 2 "item" "items")))
+ (deftest
+ "pluralize zero"
+ (assert-equal "items" (pluralize 0 "item" "items")))
+ (deftest
+ "escape html entities"
+ (assert-equal "&<>"'" (escape "&<>\"'")))
+ (deftest
+ "escape plain string unchanged"
+ (assert-equal "hello" (escape "hello")))
+ (deftest "assert passes" (assert-equal true (assert true "should pass")))
+ (deftest
+ "parse-datetime passthrough"
+ (assert-equal "2024-01-01" (parse-datetime "2024-01-01")))
+ (deftest "parse-datetime nil" (assert-nil (parse-datetime nil))))
diff --git a/shared/sx/templates/cssx.sx b/shared/sx/templates/cssx.sx
index 7572f853..93170c4e 100644
--- a/shared/sx/templates/cssx.sx
+++ b/shared/sx/templates/cssx.sx
@@ -1,4 +1,4 @@
-(define colour-bases {:orange {:s 95 :h 25} :cyan {:s 94 :h 188} :sky {:s 89 :h 199} :pink {:s 81 :h 330} :zinc {:s 5 :h 240} :amber {:s 92 :h 38} :neutral {:s 0 :h 0} :lime {:s 78 :h 84} :violet {:s 70 :h 263} :stone {:s 6 :h 25} :black {:s 0 :h 0} :teal {:s 80 :h 173} :gray {:s 9 :h 220} :red {:s 72 :h 0} :rose {:s 89 :h 350} :blue {:s 91 :h 217} :emerald {:s 84 :h 160} :green {:s 71 :h 142} :yellow {:s 96 :h 48} :purple {:s 81 :h 271} :indigo {:s 84 :h 239} :white {:s 0 :h 0} :slate {:s 16 :h 215}})
+(define colour-bases {:orange {:s 95 :h 25} :cyan {:s 94 :h 188} :sky {:s 89 :h 199} :pink {:s 81 :h 330} :zinc {:s 5 :h 240} :amber {:s 92 :h 38} :neutral {:s 0 :h 0} :lime {:s 78 :h 84} :violet {:s 70 :h 263} :fuchsia {:s 84 :h 292} :stone {:s 6 :h 25} :black {:s 0 :h 0} :teal {:s 80 :h 173} :gray {:s 9 :h 220} :red {:s 72 :h 0} :rose {:s 89 :h 350} :blue {:s 91 :h 217} :emerald {:s 84 :h 160} :green {:s 71 :h 142} :yellow {:s 96 :h 48} :purple {:s 81 :h 271} :indigo {:s 84 :h 239} :white {:s 0 :h 0} :slate {:s 16 :h 215}})
(define lerp (fn (a b t) (+ a (* t (- b a)))))
diff --git a/spec/signals.sx b/spec/signals.sx
index 810fedf3..8d2e4325 100644
--- a/spec/signals.sx
+++ b/spec/signals.sx
@@ -1,23 +1,17 @@
-;; Create raw signal dict with value, subs, deps fields
(define
make-signal
(fn
(value)
(dict "__signal" true "value" value "subscribers" (list) "deps" (list))))
-;; Type predicate for signals
(define signal? (fn (x) (and (dict? x) (has-key? x "__signal"))))
-;; Read current value from signal
(define signal-value (fn (s) (get s "value")))
-;; Write value to signal (no notification)
(define signal-set-value! (fn (s v) (dict-set! s "value" v)))
-;; List of subscriber functions
(define signal-subscribers (fn (s) (get s "subscribers")))
-;; Add a subscriber function
(define
signal-add-sub!
(fn
@@ -26,7 +20,6 @@
(not (contains? (get s "subscribers") f))
(dict-set! s "subscribers" (append (get s "subscribers") (list f))))))
-;; Remove a subscriber function
(define
signal-remove-sub!
(fn
@@ -36,19 +29,15 @@
"subscribers"
(filter (fn (sub) (not (identical? sub f))) (get s "subscribers")))))
-;; List of upstream signal dependencies
(define signal-deps (fn (s) (get s "deps")))
-;; Set upstream dependencies
(define signal-set-deps! (fn (s deps) (dict-set! s "deps" deps)))
-;; Create a reactive signal (user-facing constructor)
(define
signal
:effects ()
(fn ((initial-value :as any)) (make-signal initial-value)))
-;; Dereference a signal, returning its current value
(define
deref
:effects ()
@@ -69,7 +58,6 @@
(signal-add-sub! s notify-fn))))
(signal-value s)))))
-;; Set signal to new value and notify subscribers
(define
reset!
:effects (mutation)
@@ -84,12 +72,11 @@
(signal-set-value! s value)
(notify-subscribers s))))))
-;; Apply function to current value and reset
(define
swap!
:effects (mutation)
(fn
- ((s :as signal) (f :as lambda) &rest args)
+ ((s :as signal) (f :as callable) &rest args)
(when
(signal? s)
(let
@@ -100,7 +87,6 @@
(signal-set-value! s new-val)
(notify-subscribers s))))))
-;; Create a derived signal that auto-updates from dependencies
(define
computed
:effects (mutation)
@@ -114,7 +100,6 @@
(register-in-scope (fn () (dispose-computed s)))
s))))
-;; Create a side-effect that runs when dependencies change
(define
effect
:effects (mutation)
@@ -130,13 +115,10 @@
(register-in-scope dispose-fn)
dispose-fn)))))
-;; Nesting counter for batched updates
(define *batch-depth* 0)
-;; Queued notifications during batch
(define *batch-queue* (list))
-;; Batch multiple signal updates, notify once at end
(define
batch
:effects (mutation)
@@ -166,7 +148,6 @@
queue)
(for-each (fn ((sub :as lambda)) (sub)) pending))))))
-;; Notify all subscribers of a signal change
(define
notify-subscribers
:effects (mutation)
@@ -177,7 +158,6 @@
(when (not (contains? *batch-queue* s)) (append! *batch-queue* s))
(flush-subscribers s))))
-;; Process queued subscriber notifications
(define
flush-subscribers
:effects (mutation)
@@ -185,7 +165,6 @@
((s :as dict))
(for-each (fn (sub) (cek-call sub nil)) (signal-subscribers s))))
-;; Tear down a computed signal, remove from deps
(define
dispose-computed
:effects (mutation)
@@ -198,7 +177,6 @@
(signal-deps s))
(signal-set-deps! s (list)))))
-;; Evaluate body in an island disposal scope
(define
with-island-scope
:effects (mutation)
@@ -207,7 +185,6 @@
(scope-push! "sx-island-scope" scope-fn)
(let ((result (body-fn))) (scope-pop! "sx-island-scope") result)))
-;; Register a disposable in the current island scope
(define
register-in-scope
:effects (mutation)
diff --git a/spec/tests/test-tw.sx b/spec/tests/test-tw.sx
index 53650375..f91d424f 100644
--- a/spec/tests/test-tw.sx
+++ b/spec/tests/test-tw.sx
@@ -837,12 +837,8 @@
(deftest "w-4" (assert= (tw-resolve-layout "w-4") "width:1rem"))
(deftest "w-12" (assert= (tw-resolve-layout "w-12") "width:3rem"))
(deftest "w-1/2" (assert= (tw-resolve-layout "w-1/2") "width:50%"))
- (deftest
- "w-1/3"
- (assert= (tw-resolve-layout "w-1/3") "width:33.33333333333333%"))
- (deftest
- "w-2/3"
- (assert= (tw-resolve-layout "w-2/3") "width:66.66666666666666%"))
+ (deftest "w-1/3" (assert= (tw-resolve-layout "w-1/3") "width:33.3333%"))
+ (deftest "w-2/3" (assert= (tw-resolve-layout "w-2/3") "width:66.6667%"))
(deftest "h-full" (assert= (tw-resolve-layout "h-full") "height:100%"))
(deftest
"h-screen"
diff --git a/web/page-helpers.sx b/web/page-helpers.sx
index 530d8869..eb2b7dd2 100644
--- a/web/page-helpers.sx
+++ b/web/page-helpers.sx
@@ -1,368 +1,232 @@
-;; ==========================================================================
-;; page-helpers.sx — Pure data-transformation page helpers
-;;
-;; These functions take raw data (from Python I/O edge) and return
-;; structured dicts for page rendering. No I/O — pure transformations
-;; only. Bootstrapped to every host.
-;; ==========================================================================
+(define special-form-category-map {:defmacro "Functions & Components" :for-each "Higher-Order Forms" :defpage "Domain Definitions" :let "Binding" :filter "Higher-Order Forms" :shift "Continuations" :and "Control Flow" :set! "Binding" :map-indexed "Higher-Order Forms" :dynamic-wind "Guards" :reduce "Higher-Order Forms" :cond "Control Flow" :defquery "Domain Definitions" :-> "Sequencing & Threading" :let* "Binding" :define "Binding" :reset "Continuations" :case "Control Flow" :do "Sequencing & Threading" :map "Higher-Order Forms" :some "Higher-Order Forms" :letrec "Binding" :if "Control Flow" :quote "Quoting" :every? "Higher-Order Forms" :defhandler "Domain Definitions" :fn "Functions & Components" :defstyle "Domain Definitions" :lambda "Functions & Components" :defaction "Domain Definitions" :or "Control Flow" :defcomp "Functions & Components" :quasiquote "Quoting" :when "Control Flow" :begin "Sequencing & Threading"})
-
-;; --------------------------------------------------------------------------
-;; categorize-special-forms
-;;
-;; Parses define-special-form declarations from special-forms.sx AST,
-;; categorizes each form by name lookup, returns dict of category → forms.
-;; --------------------------------------------------------------------------
-
-(define special-form-category-map
- {"if" "Control Flow" "when" "Control Flow" "cond" "Control Flow"
- "case" "Control Flow" "and" "Control Flow" "or" "Control Flow"
- "let" "Binding" "let*" "Binding" "letrec" "Binding"
- "define" "Binding" "set!" "Binding"
- "lambda" "Functions & Components" "fn" "Functions & Components"
- "defcomp" "Functions & Components" "defmacro" "Functions & Components"
- "begin" "Sequencing & Threading" "do" "Sequencing & Threading"
- "->" "Sequencing & Threading"
- "quote" "Quoting" "quasiquote" "Quoting"
- "reset" "Continuations" "shift" "Continuations"
- "dynamic-wind" "Guards"
- "map" "Higher-Order Forms" "map-indexed" "Higher-Order Forms"
- "filter" "Higher-Order Forms" "reduce" "Higher-Order Forms"
- "some" "Higher-Order Forms" "every?" "Higher-Order Forms"
- "for-each" "Higher-Order Forms"
- "defstyle" "Domain Definitions"
- "defhandler" "Domain Definitions" "defpage" "Domain Definitions"
- "defquery" "Domain Definitions" "defaction" "Domain Definitions"})
-
-
-(define extract-define-kwargs
- (fn ((expr :as list))
- ;; Extract keyword args from a define-special-form expression.
- ;; Returns dict of keyword-name → string value.
- ;; Walks items pairwise: when item[i] is a keyword, item[i+1] is its value.
- (let ((result {})
- (items (slice expr 2))
- (n (len items)))
+(define
+ extract-define-kwargs
+ (fn
+ ((expr :as list))
+ (let
+ ((result {}) (items (slice expr 2)) (n (len items)))
(for-each
- (fn ((idx :as number))
- (when (and (< (+ idx 1) n)
- (= (type-of (nth items idx)) "keyword"))
- (let ((key (keyword-name (nth items idx)))
- (val (nth items (+ idx 1))))
- (dict-set! result key
- (if (= (type-of val) "list")
+ (fn
+ ((idx :as number))
+ (when
+ (and (< (+ idx 1) n) (= (type-of (nth items idx)) "keyword"))
+ (let
+ ((key (keyword-name (nth items idx)))
+ (val (nth items (+ idx 1))))
+ (dict-set!
+ result
+ key
+ (if
+ (= (type-of val) "list")
(str "(" (join " " (map serialize val)) ")")
(str val))))))
(range 0 n))
result)))
-
-(define categorize-special-forms
- (fn ((parsed-exprs :as list))
- ;; parsed-exprs: result of parse-all on special-forms.sx
- ;; Returns dict of category-name → list of form dicts.
- (let ((categories {}))
+(define
+ categorize-special-forms
+ (fn
+ ((parsed-exprs :as list))
+ (let
+ ((categories {}))
(for-each
- (fn (expr)
- (when (and (= (type-of expr) "list")
- (>= (len expr) 2)
- (= (type-of (first expr)) "symbol")
- (= (symbol-name (first expr)) "define-special-form"))
- (let ((name (nth expr 1))
- (kwargs (extract-define-kwargs expr))
- (category (or (get special-form-category-map name) "Other")))
- (when (not (has-key? categories category))
- (dict-set! categories category (list)))
- (append! (get categories category)
- {"name" name
- "syntax" (or (get kwargs "syntax") "")
- "doc" (or (get kwargs "doc") "")
- "tail-position" (or (get kwargs "tail-position") "")
- "example" (or (get kwargs "example") "")}))))
+ (fn
+ (expr)
+ (when
+ (and
+ (= (type-of expr) "list")
+ (>= (len expr) 2)
+ (= (type-of (first expr)) "symbol")
+ (= (symbol-name (first expr)) "define-special-form"))
+ (let
+ ((name (nth expr 1))
+ (kwargs (extract-define-kwargs expr))
+ (category
+ (or
+ (get kwargs "category")
+ (get special-form-category-map name)
+ "Other")))
+ (when
+ (not (has-key? categories category))
+ (dict-set! categories category (mutable-list)))
+ (append! (get categories category) {:doc (or (get kwargs "doc") "") :example (or (get kwargs "example") "") :tail-position (or (get kwargs "tail-position") "") :syntax (or (get kwargs "syntax") "") :name name}))))
parsed-exprs)
categories)))
-
-;; --------------------------------------------------------------------------
-;; build-reference-data
-;;
-;; Takes a slug and raw reference data, returns structured dict for rendering.
-;; --------------------------------------------------------------------------
-
-(define build-ref-items-with-href
- (fn ((items :as list) (base-path :as string) (detail-keys :as list) (n-fields :as number))
- ;; items: list of lists (tuples), each with n-fields elements
- ;; base-path: e.g. "/geography/hypermedia/reference/attributes/"
- ;; detail-keys: list of strings (keys that have detail pages)
- ;; n-fields: 2 or 3 (number of fields per tuple)
+(define
+ build-ref-items-with-href
+ (fn
+ ((items :as list)
+ (base-path :as string)
+ (detail-keys :as list)
+ (n-fields :as number))
(map
- (fn ((item :as list))
- (if (= n-fields 3)
- ;; [name, desc/value, exists/desc]
- (let ((name (nth item 0))
- (field2 (nth item 1))
- (field3 (nth item 2)))
- {"name" name
- "desc" field2
- "exists" field3
- "href" (if (and field3 (some (fn ((k :as string)) (= k name)) detail-keys))
- (str base-path name)
- nil)})
- ;; [name, desc]
- (let ((name (nth item 0))
- (desc (nth item 1)))
- {"name" name
- "desc" desc
- "href" (if (some (fn ((k :as string)) (= k name)) detail-keys)
- (str base-path name)
- nil)})))
+ (fn
+ ((item :as list))
+ (if
+ (= n-fields 3)
+ (let
+ ((name (nth item 0))
+ (field2 (nth item 1))
+ (field3 (nth item 2)))
+ {:href (if (and field3 (some (fn ((k :as string)) (= k name)) detail-keys)) (str base-path name) nil) :exists field3 :desc field2 :name name})
+ (let ((name (nth item 0)) (desc (nth item 1))) {:href (if (some (fn ((k :as string)) (= k name)) detail-keys) (str base-path name) nil) :desc desc :name name})))
items)))
-
-(define build-reference-data
- (fn ((slug :as string) (raw-data :as dict) (detail-keys :as list))
- ;; slug: "attributes", "headers", "events", "js-api"
- ;; raw-data: dict with the raw data lists for this slug
- ;; detail-keys: list of names that have detail pages
- (case slug
+(define
+ build-reference-data
+ (fn
+ ((slug :as string) (raw-data :as dict) (detail-keys :as list))
+ (case
+ slug
"attributes"
- {"req-attrs" (build-ref-items-with-href
- (get raw-data "req-attrs")
- "/geography/hypermedia/reference/attributes/" detail-keys 3)
- "beh-attrs" (build-ref-items-with-href
- (get raw-data "beh-attrs")
- "/geography/hypermedia/reference/attributes/" detail-keys 3)
- "uniq-attrs" (build-ref-items-with-href
- (get raw-data "uniq-attrs")
- "/geography/hypermedia/reference/attributes/" detail-keys 3)}
-
+ {:req-attrs (build-ref-items-with-href (get raw-data "req-attrs") "/geography/hypermedia/reference/attributes/" detail-keys 3) :beh-attrs (build-ref-items-with-href (get raw-data "beh-attrs") "/geography/hypermedia/reference/attributes/" detail-keys 3) :uniq-attrs (build-ref-items-with-href (get raw-data "uniq-attrs") "/geography/hypermedia/reference/attributes/" detail-keys 3)}
"headers"
- {"req-headers" (build-ref-items-with-href
- (get raw-data "req-headers")
- "/geography/hypermedia/reference/headers/" detail-keys 3)
- "resp-headers" (build-ref-items-with-href
- (get raw-data "resp-headers")
- "/geography/hypermedia/reference/headers/" detail-keys 3)}
-
+ {:req-headers (build-ref-items-with-href (get raw-data "req-headers") "/geography/hypermedia/reference/headers/" detail-keys 3) :resp-headers (build-ref-items-with-href (get raw-data "resp-headers") "/geography/hypermedia/reference/headers/" detail-keys 3)}
"events"
- {"events-list" (build-ref-items-with-href
- (get raw-data "events-list")
- "/geography/hypermedia/reference/events/" detail-keys 2)}
-
+ {:events-list (build-ref-items-with-href (get raw-data "events-list") "/geography/hypermedia/reference/events/" detail-keys 2)}
"js-api"
- {"js-api-list" (map (fn ((item :as list)) {"name" (nth item 0) "desc" (nth item 1)})
- (get raw-data "js-api-list"))}
+ {:js-api-list (map (fn ((item :as list)) {:desc (nth item 1) :name (nth item 0)}) (get raw-data "js-api-list"))}
+ :else {:req-attrs (build-ref-items-with-href (get raw-data "req-attrs") "/geography/hypermedia/reference/attributes/" detail-keys 3) :beh-attrs (build-ref-items-with-href (get raw-data "beh-attrs") "/geography/hypermedia/reference/attributes/" detail-keys 3) :uniq-attrs (build-ref-items-with-href (get raw-data "uniq-attrs") "/geography/hypermedia/reference/attributes/" detail-keys 3)})))
- ;; default: attributes
- :else
- {"req-attrs" (build-ref-items-with-href
- (get raw-data "req-attrs")
- "/geography/hypermedia/reference/attributes/" detail-keys 3)
- "beh-attrs" (build-ref-items-with-href
- (get raw-data "beh-attrs")
- "/geography/hypermedia/reference/attributes/" detail-keys 3)
- "uniq-attrs" (build-ref-items-with-href
- (get raw-data "uniq-attrs")
- "/geography/hypermedia/reference/attributes/" detail-keys 3)})))
+(define
+ build-attr-detail
+ (fn ((slug :as string) detail) (if (nil? detail) {:attr-not-found true} {:attr-handler (get detail "handler") :attr-title slug :attr-example (get detail "example") :attr-not-found nil :attr-description (get detail "description") :attr-demo (get detail "demo") :attr-wire-id (if (has-key? detail "handler") (str "ref-wire-" (replace (replace slug ":" "-") "*" "star")) nil)})))
+(define
+ build-header-detail
+ (fn ((slug :as string) detail) (if (nil? detail) {:header-not-found true} {:header-description (get detail "description") :header-demo (get detail "demo") :header-not-found nil :header-title slug :header-example (get detail "example") :header-direction (get detail "direction")})))
-;; --------------------------------------------------------------------------
-;; build-attr-detail / build-header-detail / build-event-detail
-;;
-;; Lookup a slug in a detail dict, reshape for page rendering.
-;; --------------------------------------------------------------------------
+(define
+ build-event-detail
+ (fn ((slug :as string) detail) (if (nil? detail) {:event-not-found true} {:event-example (get detail "example") :event-demo (get detail "demo") :event-description (get detail "description") :event-not-found nil :event-title slug})))
-(define build-attr-detail
- (fn ((slug :as string) detail)
- ;; detail: dict with "description", "example", "handler", "demo" keys or nil
- (if (nil? detail)
- {"attr-not-found" true}
- {"attr-not-found" nil
- "attr-title" slug
- "attr-description" (get detail "description")
- "attr-example" (get detail "example")
- "attr-handler" (get detail "handler")
- "attr-demo" (get detail "demo")
- "attr-wire-id" (if (has-key? detail "handler")
- (str "ref-wire-"
- (replace (replace slug ":" "-") "*" "star"))
- nil)})))
-
-
-(define build-header-detail
- (fn ((slug :as string) detail)
- (if (nil? detail)
- {"header-not-found" true}
- {"header-not-found" nil
- "header-title" slug
- "header-direction" (get detail "direction")
- "header-description" (get detail "description")
- "header-example" (get detail "example")
- "header-demo" (get detail "demo")})))
-
-
-(define build-event-detail
- (fn ((slug :as string) detail)
- (if (nil? detail)
- {"event-not-found" true}
- {"event-not-found" nil
- "event-title" slug
- "event-description" (get detail "description")
- "event-example" (get detail "example")
- "event-demo" (get detail "demo")})))
-
-
-;; --------------------------------------------------------------------------
-;; build-component-source
-;;
-;; Reconstruct defcomp/defisland source from component metadata.
-;; --------------------------------------------------------------------------
-
-(define build-component-source
- (fn ((comp-data :as dict))
- ;; comp-data: dict with "type", "name", "params", "has-children", "body-sx", "affinity"
- (let ((comp-type (get comp-data "type"))
- (name (get comp-data "name"))
- (params (get comp-data "params"))
- (has-children (get comp-data "has-children"))
- (body-sx (get comp-data "body-sx"))
- (affinity (get comp-data "affinity")))
- (if (= comp-type "not-found")
+(define
+ build-component-source
+ (fn
+ ((comp-data :as dict))
+ (let
+ ((comp-type (get comp-data "type"))
+ (name (get comp-data "name"))
+ (params (get comp-data "params"))
+ (has-children (get comp-data "has-children"))
+ (body-sx (get comp-data "body-sx"))
+ (affinity (get comp-data "affinity")))
+ (if
+ (= comp-type "not-found")
(str ";; component " name " not found")
- (let ((param-strs (if (empty? params)
- (if has-children
- (list "&rest" "children")
- (list))
- (if has-children
- (append (cons "&key" params) (list "&rest" "children"))
- (cons "&key" params))))
- (params-sx (str "(" (join " " param-strs) ")"))
- (form-name (if (= comp-type "island") "defisland" "defcomp"))
- (affinity-str (if (and (= comp-type "component")
- (not (nil? affinity))
- (not (= affinity "auto")))
- (str " :affinity " affinity)
- "")))
- (str "(" form-name " " name " " params-sx affinity-str "\n " body-sx ")"))))))
+ (let
+ ((param-strs (if (empty? params) (if has-children (list "&rest" "children") (list)) (if has-children (append (cons "&key" params) (list "&rest" "children")) (cons "&key" params))))
+ (params-sx (str "(" (join " " param-strs) ")"))
+ (form-name (if (= comp-type "island") "defisland" "defcomp"))
+ (affinity-str
+ (if
+ (and
+ (= comp-type "component")
+ (not (nil? affinity))
+ (not (= affinity "auto")))
+ (str " :affinity " affinity)
+ "")))
+ (str
+ "("
+ form-name
+ " "
+ name
+ " "
+ params-sx
+ affinity-str
+ "\n "
+ body-sx
+ ")"))))))
-
-;; --------------------------------------------------------------------------
-;; build-bundle-analysis
-;;
-;; Compute per-page bundle stats from pre-extracted component data.
-;; --------------------------------------------------------------------------
-
-(define build-bundle-analysis
- (fn ((pages-raw :as list) (components-raw :as dict) (total-components :as number) (total-macros :as number) (pure-count :as number) (io-count :as number))
- ;; pages-raw: list of {:name :path :direct :needed-names}
- ;; components-raw: dict of name → {:is-pure :affinity :render-target :io-refs :deps :source}
- (let ((pages-data (list)))
+(define
+ build-bundle-analysis
+ (fn
+ ((pages-raw :as list)
+ (components-raw :as dict)
+ (total-components :as number)
+ (total-macros :as number)
+ (pure-count :as number)
+ (io-count :as number))
+ (let
+ ((pages-data (list)))
(for-each
- (fn ((page :as dict))
- (let ((needed-names (get page "needed-names"))
- (n (len needed-names))
- (pct (if (> total-components 0)
- (round (* (/ n total-components) 100))
- 0))
- (savings (- 100 pct))
- (pure-in-page 0)
- (io-in-page 0)
- (page-io-refs (list))
- (comp-details (list)))
- ;; Walk needed components
+ (fn
+ ((page :as dict))
+ (let
+ ((needed-names (get page "needed-names"))
+ (n (len needed-names))
+ (pct
+ (if
+ (> total-components 0)
+ (round (* (/ n total-components) 100))
+ 0))
+ (savings (- 100 pct))
+ (pure-in-page 0)
+ (io-in-page 0)
+ (page-io-refs (list))
+ (comp-details (list)))
(for-each
- (fn ((comp-name :as string))
- (let ((info (get components-raw comp-name)))
- (when (not (nil? info))
- (if (get info "is-pure")
+ (fn
+ ((comp-name :as string))
+ (let
+ ((info (get components-raw comp-name)))
+ (when
+ (not (nil? info))
+ (if
+ (get info "is-pure")
(set! pure-in-page (+ pure-in-page 1))
(do
(set! io-in-page (+ io-in-page 1))
(for-each
- (fn ((ref :as string)) (when (not (some (fn ((r :as string)) (= r ref)) page-io-refs))
- (append! page-io-refs ref)))
+ (fn
+ ((ref :as string))
+ (when
+ (not
+ (some
+ (fn ((r :as string)) (= r ref))
+ page-io-refs))
+ (append! page-io-refs ref)))
(or (get info "io-refs") (list)))))
- (append! comp-details
- {"name" comp-name
- "is-pure" (get info "is-pure")
- "affinity" (get info "affinity")
- "render-target" (get info "render-target")
- "io-refs" (or (get info "io-refs") (list))
- "deps" (or (get info "deps") (list))
- "source" (get info "source")}))))
+ (append! comp-details {:io-refs (or (get info "io-refs") (list)) :render-target (get info "render-target") :deps (or (get info "deps") (list)) :source (get info "source") :name comp-name :is-pure (get info "is-pure") :affinity (get info "affinity")}))))
needed-names)
- (append! pages-data
- {"name" (get page "name")
- "path" (get page "path")
- "direct" (get page "direct")
- "needed" n
- "pct" pct
- "savings" savings
- "io-refs" (len page-io-refs)
- "pure-in-page" pure-in-page
- "io-in-page" io-in-page
- "components" comp-details})))
+ (append! pages-data {:pure-in-page pure-in-page :io-refs (len page-io-refs) :direct (get page "direct") :needed n :io-in-page io-in-page :components comp-details :savings savings :pct pct :path (get page "path") :name (get page "name")})))
pages-raw)
- {"pages" pages-data
- "total-components" total-components
- "total-macros" total-macros
- "pure-count" pure-count
- "io-count" io-count})))
+ {:total-macros total-macros :pages pages-data :io-count io-count :pure-count pure-count :total-components total-components})))
-
-;; --------------------------------------------------------------------------
-;; build-routing-analysis
-;;
-;; Classify pages by routing mode (client vs server).
-;; --------------------------------------------------------------------------
-
-(define build-routing-analysis
- (fn ((pages-raw :as list))
- ;; pages-raw: list of {:name :path :has-data :content-src}
- (let ((pages-data (list))
- (client-count 0)
- (server-count 0))
+(define
+ build-routing-analysis
+ (fn
+ ((pages-raw :as list))
+ (let
+ ((pages-data (mutable-list)) (client-count 0) (server-count 0))
(for-each
- (fn ((page :as dict))
- (let ((has-data (get page "has-data"))
- (content-src (or (get page "content-src") ""))
- (mode nil)
- (reason ""))
+ (fn
+ ((page :as dict))
+ (let
+ ((has-data (not (nil? (get page "data"))))
+ (content-src (or (get page "content-src") ""))
+ (mode nil)
+ (reason ""))
(cond
has-data
- (do (set! mode "server")
- (set! reason "Has :data expression — needs server IO")
- (set! server-count (+ server-count 1)))
+ (do
+ (set! mode "server")
+ (set! reason "Has :data expression — needs server IO")
+ (set! server-count (+ server-count 1)))
(empty? content-src)
- (do (set! mode "server")
- (set! reason "No content expression")
- (set! server-count (+ server-count 1)))
- :else
- (do (set! mode "client")
- (set! client-count (+ client-count 1))))
- (append! pages-data
- {"name" (get page "name")
- "path" (get page "path")
- "mode" mode
- "has-data" has-data
- "content-expr" (if (> (len content-src) 80)
- (str (slice content-src 0 80) "...")
- content-src)
- "reason" reason})))
+ (do
+ (set! mode "server")
+ (set! reason "No content expression")
+ (set! server-count (+ server-count 1)))
+ :else (do
+ (set! mode "client")
+ (set! client-count (+ client-count 1))))
+ (append! pages-data {:reason reason :mode mode :content-expr (if (> (len content-src) 80) (str (slice content-src 0 80) "...") content-src) :has-data has-data :path (get page "path") :name (get page "name")})))
pages-raw)
- {"pages" pages-data
- "total-pages" (+ client-count server-count)
- "client-count" client-count
- "server-count" server-count})))
+ {:pages pages-data :total-pages (+ client-count server-count) :server-count server-count :client-count client-count})))
-
-;; --------------------------------------------------------------------------
-;; build-affinity-analysis
-;;
-;; Package component affinity info + page render plans for display.
-;; --------------------------------------------------------------------------
-
-(define build-affinity-analysis
- (fn ((demo-components :as list) (page-plans :as list))
- {"components" demo-components
- "page-plans" page-plans}))
+(define
+ build-affinity-analysis
+ (fn ((demo-components :as list) (page-plans :as list)) {:components demo-components :page-plans page-plans}))
diff --git a/web/tests/test-adapter-dom.sx b/web/tests/test-adapter-dom.sx
new file mode 100644
index 00000000..edeb6639
--- /dev/null
+++ b/web/tests/test-adapter-dom.sx
@@ -0,0 +1,48 @@
+(defsuite
+ "adapter-form-predicates"
+ (deftest "if is a render form" (assert-true (render-html-form? "if")))
+ (deftest "when is a render form" (assert-true (render-html-form? "when")))
+ (deftest "cond is a render form" (assert-true (render-html-form? "cond")))
+ (deftest "let is a render form" (assert-true (render-html-form? "let")))
+ (deftest "map is a render form" (assert-true (render-html-form? "map")))
+ (deftest
+ "filter is a render form"
+ (assert-true (render-html-form? "filter")))
+ (deftest
+ "scope is a render form"
+ (assert-true (render-html-form? "scope")))
+ (deftest
+ "provide is a render form"
+ (assert-true (render-html-form? "provide")))
+ (deftest
+ "div is not a render form"
+ (assert-false (render-html-form? "div")))
+ (deftest
+ "span is not a render form"
+ (assert-false (render-html-form? "span")))
+ (deftest
+ "defcomp is a render form"
+ (assert-true (render-html-form? "defcomp")))
+ (deftest
+ "defisland is a render form"
+ (assert-true (render-html-form? "defisland")))
+ (deftest
+ "defmacro is a render form"
+ (assert-true (render-html-form? "defmacro"))))
+
+(defsuite
+ "adapter-html-forms-constant"
+ (deftest
+ "RENDER_HTML_FORMS is a list"
+ (assert-true (list? RENDER_HTML_FORMS)))
+ (deftest
+ "contains expected count"
+ (assert-true (>= (len RENDER_HTML_FORMS) 20)))
+ (deftest "contains if" (assert-true (contains? RENDER_HTML_FORMS "if")))
+ (deftest
+ "contains scope"
+ (assert-true (contains? RENDER_HTML_FORMS "scope")))
+ (deftest
+ "does not contain html tags"
+ (assert-false (contains? RENDER_HTML_FORMS "div"))
+ (assert-false (contains? RENDER_HTML_FORMS "span"))))
diff --git a/web/tests/test-adapter-html.sx b/web/tests/test-adapter-html.sx
new file mode 100644
index 00000000..8f6d423d
--- /dev/null
+++ b/web/tests/test-adapter-html.sx
@@ -0,0 +1,240 @@
+(define (ahtml expr) (render-to-html expr {}))
+
+(define (ahtml-env expr env) (render-to-html expr env))
+
+(defsuite
+ "adapter-html-basics"
+ (deftest "nil renders empty" (assert-equal "" (ahtml nil)))
+ (deftest "string escapes html" (assert-equal "<b>" (ahtml "")))
+ (deftest "number renders as string" (assert-equal "42" (ahtml 42)))
+ (deftest
+ "boolean renders"
+ (assert-equal "true" (ahtml true))
+ (assert-equal "false" (ahtml false)))
+ (deftest "keyword renders name" (assert-equal "foo" (ahtml :foo))))
+
+(defsuite
+ "adapter-html-elements"
+ (deftest
+ "div with text"
+ (assert-equal "
ok
" (ahtml (quote (when true (p "ok")))))) + (deftest + "when falsy renders empty" + (assert-equal "" (ahtml (quote (when false (p "no"))))))) + +(defsuite + "adapter-html-let" + (deftest + "let binds and renders" + (assert-equal + "hi" + (ahtml (quote (let ((x "hi")) (span x)))))) + (deftest + "let multiple bindings" + (assert-equal + "