SxRational type in OCaml (Rational of int * int, stored reduced, denom>0) and JS (SxRational class with _rational marker). n/d reader syntax in spec/parser.sx. Arithmetic contagion: int op rational → rational, rational op float → float. JS keeps int/int → float for CSS backward compatibility. OCaml as_number + safe_eq extended for cross-type rational equality so (= 2.5 5/2) → true. 62 tests in test-rationals.sx, all pass. JS: 2232 passed. OCaml: 4532 passed (+11 vs pre-fix baseline). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
625 lines
16 KiB
Plaintext
625 lines
16 KiB
Plaintext
;; ==========================================================================
|
|
;; 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" {:b 2 :a 1})))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; 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" {:b 2 :a 1})
|
|
(assert-equal 1 (get {:a 1} "a"))
|
|
(assert-equal 2 (get {:b 2 :a 1} "b")))
|
|
(deftest
|
|
"assoc"
|
|
(assert-equal {:b 2 :a 1} (assoc {:a 1} "b" 2))
|
|
(assert-equal {:a 99} (assoc {:a 1} "a" 99)))
|
|
(deftest "dissoc" (assert-equal {:b 2} (dissoc {:b 2 :a 1} "a")))
|
|
(deftest
|
|
"keys and vals"
|
|
(let
|
|
((d {:b 2 :a 1}))
|
|
(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 {:c 3 :b 2 :a 1} (merge {:b 2 :a 1} {:c 3}))
|
|
(assert-equal {:b 2 :a 99} (merge {:b 2 :a 1} {: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"
|
|
(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"
|
|
(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"
|
|
(let
|
|
((f (fn (x) (+ x 1) (+ x 2) (+ x 3))))
|
|
(assert-equal 13 (f 10))))
|
|
(deftest
|
|
"multi-body lambda side effects via dict mutation"
|
|
(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"
|
|
(assert-equal
|
|
20
|
|
((fn (x) (+ x 1) (* x 2)) 10))
|
|
(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))
|
|
(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)
|
|
(quasiquote (if (not (unquote cond)) (do (splice-unquote 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)
|
|
(quasiquote (1 (unquote x) 3)))))
|
|
(deftest
|
|
"splice-unquote"
|
|
(let
|
|
((xs (list 2 3 4)))
|
|
(assert-equal
|
|
(list 1 2 3 4 5)
|
|
(quasiquote (1 (splice-unquote 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))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Edge cases and regression tests
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defsuite
|
|
"edge-cases"
|
|
(deftest
|
|
"nested let scoping"
|
|
(let
|
|
((x 1))
|
|
(let ((x 2)) (assert-equal 2 x))))
|
|
(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))))
|