;; ========================================================================== ;; test-eval.sx — Tests for the core evaluator and primitives ;; ;; Requires: test-framework.sx loaded first. ;; Modules tested: eval.sx, primitives.sx ;; ========================================================================== ;; -------------------------------------------------------------------------- ;; Literals and types ;; -------------------------------------------------------------------------- (defsuite "literals" (deftest "numbers are numbers" (assert-type "number" 42) (assert-type "number" 3.14) (assert-type "number" -1)) (deftest "strings are strings" (assert-type "string" "hello") (assert-type "string" "")) (deftest "booleans are booleans" (assert-type "boolean" true) (assert-type "boolean" false)) (deftest "nil is nil" (assert-type "nil" nil) (assert-nil nil)) (deftest "lists are lists" (assert-type "list" (list 1 2 3)) (assert-type "list" (list))) (deftest "dicts are dicts" (assert-type "dict" {:a 1 :b 2}))) ;; -------------------------------------------------------------------------- ;; Arithmetic ;; -------------------------------------------------------------------------- (defsuite "arithmetic" (deftest "addition" (assert-equal 3 (+ 1 2)) (assert-equal 0 (+ 0 0)) (assert-equal -1 (+ 1 -2)) (assert-equal 10 (+ 1 2 3 4))) (deftest "subtraction" (assert-equal 1 (- 3 2)) (assert-equal -1 (- 2 3))) (deftest "multiplication" (assert-equal 6 (* 2 3)) (assert-equal 0 (* 0 100)) (assert-equal 24 (* 1 2 3 4))) (deftest "division" (assert-equal 2 (/ 6 3)) (assert-equal 2.5 (/ 5 2))) (deftest "modulo" (assert-equal 1 (mod 7 3)) (assert-equal 0 (mod 6 3)))) ;; -------------------------------------------------------------------------- ;; Comparison ;; -------------------------------------------------------------------------- (defsuite "comparison" (deftest "equality" (assert-true (= 1 1)) (assert-false (= 1 2)) (assert-true (= "a" "a")) (assert-false (= "a" "b"))) (deftest "deep equality" (assert-true (equal? (list 1 2 3) (list 1 2 3))) (assert-false (equal? (list 1 2) (list 1 3))) (assert-true (equal? {:a 1} {:a 1})) (assert-false (equal? {:a 1} {:a 2}))) (deftest "ordering" (assert-true (< 1 2)) (assert-false (< 2 1)) (assert-true (> 2 1)) (assert-true (<= 1 1)) (assert-true (<= 1 2)) (assert-true (>= 2 2)) (assert-true (>= 3 2)))) ;; -------------------------------------------------------------------------- ;; String operations ;; -------------------------------------------------------------------------- (defsuite "strings" (deftest "str concatenation" (assert-equal "abc" (str "a" "b" "c")) (assert-equal "hello world" (str "hello" " " "world")) (assert-equal "42" (str 42)) (assert-equal "" (str))) (deftest "string-length" (assert-equal 5 (string-length "hello")) (assert-equal 0 (string-length ""))) (deftest "substring" (assert-equal "ell" (substring "hello" 1 4)) (assert-equal "hello" (substring "hello" 0 5))) (deftest "string-contains?" (assert-true (string-contains? "hello world" "world")) (assert-false (string-contains? "hello" "xyz"))) (deftest "upcase and downcase" (assert-equal "HELLO" (upcase "hello")) (assert-equal "hello" (downcase "HELLO"))) (deftest "trim" (assert-equal "hello" (trim " hello ")) (assert-equal "hello" (trim "hello"))) (deftest "split and join" (assert-equal (list "a" "b" "c") (split "a,b,c" ",")) (assert-equal "a-b-c" (join "-" (list "a" "b" "c"))))) ;; -------------------------------------------------------------------------- ;; List operations ;; -------------------------------------------------------------------------- (defsuite "lists" (deftest "constructors" (assert-equal (list 1 2 3) (list 1 2 3)) (assert-equal (list) (list)) (assert-length 3 (list 1 2 3))) (deftest "first and rest" (assert-equal 1 (first (list 1 2 3))) (assert-equal (list 2 3) (rest (list 1 2 3))) (assert-nil (first (list))) (assert-equal (list) (rest (list)))) (deftest "nth" (assert-equal 1 (nth (list 1 2 3) 0)) (assert-equal 2 (nth (list 1 2 3) 1)) (assert-equal 3 (nth (list 1 2 3) 2))) (deftest "last" (assert-equal 3 (last (list 1 2 3))) (assert-nil (last (list)))) (deftest "cons and append" (assert-equal (list 0 1 2) (cons 0 (list 1 2))) (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))) (assert-equal (list) (reverse (list)))) (deftest "empty?" (assert-true (empty? (list))) (assert-false (empty? (list 1)))) (deftest "len" (assert-equal 0 (len (list))) (assert-equal 3 (len (list 1 2 3)))) (deftest "contains?" (assert-true (contains? (list 1 2 3) 2)) (assert-false (contains? (list 1 2 3) 4))) (deftest "flatten" (assert-equal (list 1 2 3 4) (flatten (list (list 1 2) (list 3 4)))))) ;; -------------------------------------------------------------------------- ;; Dict operations ;; -------------------------------------------------------------------------- (defsuite "dicts" (deftest "dict literal" (assert-type "dict" {:a 1 :b 2}) (assert-equal 1 (get {:a 1} "a")) (assert-equal 2 (get {:a 1 :b 2} "b"))) (deftest "assoc" (assert-equal {:a 1 :b 2} (assoc {:a 1} "b" 2)) (assert-equal {:a 99} (assoc {:a 1} "a" 99))) (deftest "dissoc" (assert-equal {:b 2} (dissoc {:a 1 :b 2} "a"))) (deftest "keys and vals" (let ((d {:a 1 :b 2})) (assert-length 2 (keys d)) (assert-length 2 (vals d)) (assert-contains "a" (keys d)) (assert-contains "b" (keys d)))) (deftest "has-key?" (assert-true (has-key? {:a 1} "a")) (assert-false (has-key? {:a 1} "b"))) (deftest "merge" (assert-equal {:a 1 :b 2 :c 3} (merge {:a 1 :b 2} {:c 3})) (assert-equal {:a 99 :b 2} (merge {:a 1 :b 2} {:a 99})))) ;; -------------------------------------------------------------------------- ;; Predicates ;; -------------------------------------------------------------------------- (defsuite "predicates" (deftest "nil?" (assert-true (nil? nil)) (assert-false (nil? 0)) (assert-false (nil? false)) (assert-false (nil? ""))) (deftest "number?" (assert-true (number? 42)) (assert-true (number? 3.14)) (assert-false (number? "42"))) (deftest "string?" (assert-true (string? "hello")) (assert-false (string? 42))) (deftest "list?" (assert-true (list? (list 1 2))) (assert-false (list? "not a list"))) (deftest "dict?" (assert-true (dict? {:a 1})) (assert-false (dict? (list 1)))) (deftest "boolean?" (assert-true (boolean? true)) (assert-true (boolean? false)) (assert-false (boolean? nil)) (assert-false (boolean? 0))) (deftest "not" (assert-true (not false)) (assert-true (not nil)) (assert-false (not true)) (assert-false (not 1)) (assert-false (not "x")))) ;; -------------------------------------------------------------------------- ;; Special forms ;; -------------------------------------------------------------------------- (defsuite "special-forms" (deftest "if" (assert-equal "yes" (if true "yes" "no")) (assert-equal "no" (if false "yes" "no")) (assert-equal "no" (if nil "yes" "no")) (assert-nil (if false "yes"))) (deftest "when" (assert-equal "yes" (when true "yes")) (assert-nil (when false "yes"))) (deftest "cond" (assert-equal "a" (cond true "a" :else "b")) (assert-equal "b" (cond false "a" :else "b")) (assert-equal "c" (cond false "a" false "b" :else "c"))) (deftest "cond with 2-element predicate as first test" ;; Regression: cond misclassifies Clojure-style as scheme-style when ;; the first test is a 2-element list like (nil? x) or (empty? x). ;; The evaluator checks: is first arg a 2-element list? If yes, treats ;; as scheme-style ((test body) ...) — returning the arg instead of ;; evaluating the predicate call. (assert-equal 0 (cond (nil? nil) 0 :else 1)) (assert-equal 1 (cond (nil? "x") 0 :else 1)) (assert-equal "empty" (cond (empty? (list)) "empty" :else "not-empty")) (assert-equal "not-empty" (cond (empty? (list 1)) "empty" :else "not-empty")) (assert-equal "yes" (cond (not false) "yes" :else "no")) (assert-equal "no" (cond (not true) "yes" :else "no"))) (deftest "cond with 2-element predicate and no :else" ;; Same bug, but without :else — this is the worst case because the ;; bootstrapper heuristic also breaks (all clauses are 2-element lists). (assert-equal "found" (cond (nil? nil) "found" (nil? "x") "other")) (assert-equal "b" (cond (nil? "x") "a" (not false) "b"))) (deftest "and" (assert-true (and true true)) (assert-false (and true false)) (assert-false (and false true)) (assert-equal 3 (and 1 2 3))) (deftest "or" (assert-equal 1 (or 1 2)) (assert-equal 2 (or false 2)) (assert-equal "fallback" (or nil false "fallback")) (assert-false (or false false))) (deftest "let" (assert-equal 3 (let ((x 1) (y 2)) (+ x y))) (assert-equal "hello world" (let ((a "hello") (b " world")) (str a b)))) (deftest "let clojure-style" (assert-equal 3 (let (x 1 y 2) (+ x y)))) (deftest "do / begin" (assert-equal 3 (do 1 2 3)) (assert-equal "last" (begin "first" "middle" "last"))) (deftest "define" (define x 42) (assert-equal 42 x)) (deftest "set!" (define x 1) (set! x 2) (assert-equal 2 x))) ;; -------------------------------------------------------------------------- ;; Lambda and closures ;; -------------------------------------------------------------------------- (defsuite "lambdas" (deftest "basic lambda" (let ((add (fn (a b) (+ a b)))) (assert-equal 3 (add 1 2)))) (deftest "closure captures env" (let ((x 10)) (let ((add-x (fn (y) (+ x y)))) (assert-equal 15 (add-x 5))))) (deftest "lambda as argument" (assert-equal (list 2 4 6) (map (fn (x) (* x 2)) (list 1 2 3)))) (deftest "recursive lambda via define" (define factorial (fn (n) (if (<= n 1) 1 (* n (factorial (- n 1)))))) (assert-equal 120 (factorial 5))) (deftest "higher-order returns lambda" (let ((make-adder (fn (n) (fn (x) (+ n x))))) (let ((add5 (make-adder 5))) (assert-equal 8 (add5 3))))) (deftest "multi-body lambda returns last value" ;; All body expressions must execute. Return value is the last. ;; Catches: sf-lambda using nth(args,1) instead of rest(args). (let ((f (fn (x) (+ x 1) (+ x 2) (+ x 3)))) (assert-equal 13 (f 10)))) (deftest "multi-body lambda side effects via dict mutation" ;; Verify all body expressions run by mutating a shared dict. (let ((state (dict "a" 0 "b" 0))) (let ((f (fn () (dict-set! state "a" 1) (dict-set! state "b" 2) "done"))) (assert-equal "done" (f)) (assert-equal 1 (get state "a")) (assert-equal 2 (get state "b"))))) (deftest "multi-body lambda two expressions" ;; Simplest case: two body expressions, return value is second. (assert-equal 20 ((fn (x) (+ x 1) (* x 2)) 10)) ;; And with zero-arg lambda (assert-equal 42 ((fn () (+ 1 2) 42))))) ;; -------------------------------------------------------------------------- ;; Higher-order forms ;; -------------------------------------------------------------------------- (defsuite "higher-order" (deftest "map" (assert-equal (list 2 4 6) (map (fn (x) (* x 2)) (list 1 2 3))) (assert-equal (list) (map (fn (x) x) (list)))) (deftest "filter" (assert-equal (list 2 4) (filter (fn (x) (= (mod x 2) 0)) (list 1 2 3 4))) (assert-equal (list) (filter (fn (x) false) (list 1 2 3)))) (deftest "reduce" (assert-equal 10 (reduce (fn (acc x) (+ acc x)) 0 (list 1 2 3 4))) (assert-equal 0 (reduce (fn (acc x) (+ acc x)) 0 (list)))) (deftest "some" (assert-true (some (fn (x) (> x 3)) (list 1 2 3 4 5))) (assert-false (some (fn (x) (> x 10)) (list 1 2 3)))) (deftest "every?" (assert-true (every? (fn (x) (> x 0)) (list 1 2 3))) (assert-false (every? (fn (x) (> x 2)) (list 1 2 3)))) (deftest "map-indexed" (assert-equal (list "0:a" "1:b" "2:c") (map-indexed (fn (i x) (str i ":" x)) (list "a" "b" "c"))))) ;; -------------------------------------------------------------------------- ;; Components ;; -------------------------------------------------------------------------- (defsuite "components" (deftest "defcomp creates component" (defcomp ~test-comp (&key title) (div title)) (assert-true (not (nil? ~test-comp)))) (deftest "component renders with keyword args" (defcomp ~greeting (&key name) (span (str "Hello, " name "!"))) (assert-true (not (nil? ~greeting)))) (deftest "component with children" (defcomp ~box (&key &rest children) (div :class "box" children)) (assert-true (not (nil? ~box)))) (deftest "component with default via or" (defcomp ~label (&key text) (span (or text "default"))) (assert-true (not (nil? ~label)))) (deftest "defcomp default affinity is auto" (defcomp ~aff-default (&key x) (div x)) (assert-equal "auto" (component-affinity ~aff-default))) (deftest "defcomp affinity client" (defcomp ~aff-client (&key x) :affinity :client (div x)) (assert-equal "client" (component-affinity ~aff-client))) (deftest "defcomp affinity server" (defcomp ~aff-server (&key x) :affinity :server (div x)) (assert-equal "server" (component-affinity ~aff-server))) (deftest "defcomp affinity preserves body" (defcomp ~aff-body (&key val) :affinity :client (span val)) ;; Component should still render correctly (assert-equal "client" (component-affinity ~aff-body)) (assert-true (not (nil? ~aff-body))))) ;; -------------------------------------------------------------------------- ;; Macros ;; -------------------------------------------------------------------------- (defsuite "macros" (deftest "defmacro creates macro" (defmacro unless (cond &rest body) `(if (not ,cond) (do ,@body))) (assert-equal "yes" (unless false "yes")) (assert-nil (unless true "no"))) (deftest "quasiquote and unquote" (let ((x 42)) (assert-equal (list 1 42 3) `(1 ,x 3)))) (deftest "splice-unquote" (let ((xs (list 2 3 4))) (assert-equal (list 1 2 3 4 5) `(1 ,@xs 5))))) ;; -------------------------------------------------------------------------- ;; Threading macro ;; -------------------------------------------------------------------------- (defsuite "threading" (deftest "thread-first" (assert-equal 8 (-> 5 (+ 1) (+ 2))) (assert-equal "HELLO" (-> "hello" upcase)) (assert-equal "HELLO WORLD" (-> "hello" (str " world") upcase)))) ;; -------------------------------------------------------------------------- ;; Truthiness ;; -------------------------------------------------------------------------- (defsuite "truthiness" (deftest "truthy values" (assert-true (if 1 true false)) (assert-true (if "x" true false)) (assert-true (if (list 1) true false)) (assert-true (if true true false))) (deftest "falsy values" (assert-false (if false true false)) (assert-false (if nil true false))) ;; NOTE: empty list, zero, and empty string truthiness is ;; platform-dependent. Python treats all three as falsy. ;; JavaScript treats [] as truthy but 0 and "" as falsy. ;; These tests are omitted — each bootstrapper should emit ;; platform-specific truthiness tests instead. ) ;; -------------------------------------------------------------------------- ;; Edge cases and regression tests ;; -------------------------------------------------------------------------- (defsuite "edge-cases" (deftest "nested let scoping" (let ((x 1)) (let ((x 2)) (assert-equal 2 x)) ;; outer x should be unchanged by inner let ;; (this tests that let creates a new scope) )) (deftest "recursive map" (assert-equal (list (list 2 4) (list 6 8)) (map (fn (sub) (map (fn (x) (* x 2)) sub)) (list (list 1 2) (list 3 4))))) (deftest "keyword as value" (assert-equal "class" :class) (assert-equal "id" :id)) (deftest "dict with evaluated values" (let ((x 42)) (assert-equal 42 (get {:val x} "val")))) (deftest "nil propagation" (assert-nil (get {:a 1} "missing")) (assert-equal "default" (or (get {:a 1} "missing") "default"))) (deftest "empty operations" (assert-equal (list) (map (fn (x) x) (list))) (assert-equal (list) (filter (fn (x) true) (list))) (assert-equal 0 (reduce (fn (acc x) (+ acc x)) 0 (list))) (assert-equal 0 (len (list))) (assert-equal "" (str)))) ;; -------------------------------------------------------------------------- ;; Server-only tests — skip in browser (defpage, streaming functions) ;; These require forms.sx which is only loaded server-side. ;; -------------------------------------------------------------------------- (when (get (try-call (fn () stream-chunk-id)) "ok") (defsuite "defpage" (deftest "basic defpage returns page-def" (let ((p (defpage test-basic :path "/test" :auth :public :content (div "hello")))) (assert-true (not (nil? p))) (assert-equal "test-basic" (get p "name")) (assert-equal "/test" (get p "path")) (assert-equal "public" (get p "auth")))) (deftest "defpage content expr is unevaluated AST" (let ((p (defpage test-content :path "/c" :auth :public :content (~my-comp :title "hi")))) (assert-true (not (nil? (get p "content")))))) (deftest "defpage with :stream" (let ((p (defpage test-stream :path "/s" :auth :public :stream true :content (div "x")))) (assert-equal true (get p "stream")))) (deftest "defpage with :shell" (let ((p (defpage test-shell :path "/sh" :auth :public :stream true :shell (~my-layout (~suspense :id "data" :fallback (div "loading..."))) :content (~my-streamed :data data-val)))) (assert-true (not (nil? (get p "shell")))) (assert-true (not (nil? (get p "content")))))) (deftest "defpage with :fallback" (let ((p (defpage test-fallback :path "/f" :auth :public :stream true :fallback (div :class "skeleton" "loading") :content (div "done")))) (assert-true (not (nil? (get p "fallback")))))) (deftest "defpage with :data" (let ((p (defpage test-data :path "/d" :auth :public :data (fetch-items) :content (~items-list :items items)))) (assert-true (not (nil? (get p "data")))))) (deftest "defpage missing fields are nil" (let ((p (defpage test-minimal :path "/m" :auth :public :content (div "x")))) (assert-nil (get p "data")) (assert-nil (get p "filter")) (assert-nil (get p "aside")) (assert-nil (get p "menu")) (assert-nil (get p "shell")) (assert-nil (get p "fallback")) (assert-equal false (get p "stream"))))) ;; -------------------------------------------------------------------------- ;; Multi-stream data protocol (from forms.sx) ;; -------------------------------------------------------------------------- (defsuite "stream-chunk-id" (deftest "extracts stream-id from chunk" (assert-equal "my-slot" (stream-chunk-id {"stream-id" "my-slot" "x" 1}))) (deftest "defaults to stream-content when missing" (assert-equal "stream-content" (stream-chunk-id {"x" 1 "y" 2})))) (defsuite "stream-chunk-bindings" (deftest "removes stream-id from chunk" (let ((bindings (stream-chunk-bindings {"stream-id" "slot" "name" "alice" "age" 30}))) (assert-equal "alice" (get bindings "name")) (assert-equal 30 (get bindings "age")) (assert-nil (get bindings "stream-id")))) (deftest "returns all keys when no stream-id" (let ((bindings (stream-chunk-bindings {"a" 1 "b" 2}))) (assert-equal 1 (get bindings "a")) (assert-equal 2 (get bindings "b"))))) (defsuite "normalize-binding-key" (deftest "converts underscores to hyphens" (assert-equal "my-key" (normalize-binding-key "my_key"))) (deftest "leaves hyphens unchanged" (assert-equal "my-key" (normalize-binding-key "my-key"))) (deftest "handles multiple underscores" (assert-equal "a-b-c" (normalize-binding-key "a_b_c")))) (defsuite "bind-stream-chunk" (deftest "creates fresh env with bindings" (let ((base {"existing" 42}) (chunk {"stream-id" "slot" "user-name" "bob" "count" 5}) (env (bind-stream-chunk chunk base))) ;; Base env bindings are preserved (assert-equal 42 (get env "existing")) ;; Chunk bindings are added (stream-id removed) (assert-equal "bob" (get env "user-name")) (assert-equal 5 (get env "count")) ;; stream-id is not in env (assert-nil (get env "stream-id")))) (deftest "isolates env from base — bindings don't leak to base" (let ((base {"x" 1}) (chunk {"stream-id" "s" "y" 2}) (env (bind-stream-chunk chunk base))) ;; Chunk bindings should not appear in base (assert-nil (get base "y")) ;; Base bindings should be in derived env (assert-equal 1 (get env "x"))))) (defsuite "validate-stream-data" (deftest "valid: list of dicts" (assert-true (validate-stream-data (list {"stream-id" "a" "x" 1} {"stream-id" "b" "y" 2})))) (deftest "valid: empty list" (assert-true (validate-stream-data (list)))) (deftest "invalid: single dict (not a list)" (assert-equal false (validate-stream-data {"x" 1}))) (deftest "invalid: list containing non-dict" (assert-equal false (validate-stream-data (list {"x" 1} "oops" {"y" 2}))))) ;; -------------------------------------------------------------------------- ;; Multi-stream end-to-end scenarios ;; -------------------------------------------------------------------------- (defsuite "multi-stream routing" (deftest "stream-chunk-id routes different chunks to different slots" (let ((chunks (list {"stream-id" "stream-fast" "msg" "quick"} {"stream-id" "stream-medium" "msg" "steady"} {"stream-id" "stream-slow" "msg" "slow"})) (ids (map stream-chunk-id chunks))) (assert-equal "stream-fast" (nth ids 0)) (assert-equal "stream-medium" (nth ids 1)) (assert-equal "stream-slow" (nth ids 2)))) (deftest "bind-stream-chunk creates isolated envs per chunk" (let ((base {"layout" "main"}) (chunk-a {"stream-id" "a" "title" "First" "count" 1}) (chunk-b {"stream-id" "b" "title" "Second" "count" 2}) (env-a (bind-stream-chunk chunk-a base)) (env-b (bind-stream-chunk chunk-b base))) ;; Each env has its own bindings (assert-equal "First" (get env-a "title")) (assert-equal "Second" (get env-b "title")) (assert-equal 1 (get env-a "count")) (assert-equal 2 (get env-b "count")) ;; Both share base (assert-equal "main" (get env-a "layout")) (assert-equal "main" (get env-b "layout")) ;; Neither leaks into base (assert-nil (get base "title")))) (deftest "normalize-binding-key applied to chunk keys" (let ((chunk {"stream-id" "s" "user_name" "alice" "item_count" 3}) (bindings (stream-chunk-bindings chunk))) ;; Keys with underscores need normalizing for SX env (assert-equal "alice" (get bindings "user_name")) ;; normalize-binding-key converts them (assert-equal "user-name" (normalize-binding-key "user_name")) (assert-equal "item-count" (normalize-binding-key "item_count")))) (deftest "defpage stream flag defaults to false" (let ((p (defpage test-no-stream :path "/ns" :auth :public :content (div "x")))) (assert-equal false (get p "stream")))) (deftest "defpage stream true recorded in page-def" (let ((p (defpage test-with-stream :path "/ws" :auth :public :stream true :shell (~layout (~suspense :id "data")) :content (~chunk :val val)))) (assert-equal true (get p "stream")) (assert-true (not (nil? (get p "shell"))))))) ) ;; end (when has-server-forms?)