;; ========================================================================== ;; test-errors.sx — Tests for error handling and edge cases ;; ;; Requires: test-framework.sx loaded first. ;; Modules tested: eval.sx, primitives.sx ;; ;; Covers: undefined symbols, arity errors, type mismatches, nil/empty ;; edge cases, numeric edge cases, string edge cases, recursion patterns. ;; ========================================================================== ;; -------------------------------------------------------------------------- ;; Undefined symbol errors ;; -------------------------------------------------------------------------- (defsuite "error-undefined" (deftest "undefined symbol throws" (assert-throws (fn () this-symbol-does-not-exist))) (deftest "undefined symbol in nested expression throws" (assert-throws (fn () (+ 1 undefined-var)))) (deftest "typo in primitive name throws" (assert-throws (fn () (consss 1 (list 2 3))))) (deftest "near-miss primitive name throws" (assert-throws (fn () (fliter (fn (x) true) (list 1 2))))) (deftest "undefined in let body throws" (assert-throws (fn () (let ((x 1)) (+ x undefined-name)))))) ;; -------------------------------------------------------------------------- ;; Arity and call errors ;; -------------------------------------------------------------------------- (defsuite "error-arity" (deftest "lambda called with too many args throws" (assert-throws (fn () (let ((f (fn (x) (* x 2)))) (f 1 2 3))))) (deftest "lambda called with too few args pads with nil" ;; SX pads missing args with nil rather than throwing (let ((f (fn (x y) (list x y)))) (assert-equal nil (nth (f 1) 1)))) (deftest "calling a non-function is an error or no-op" ;; Calling a number/nil/string — platform-dependent behavior ;; At minimum, it should not return a meaningful value (let ((r1 (try-call (fn () (42 1 2)))) (r2 (try-call (fn () ("hello" 1))))) ;; Either throws or returns nil/nonsense — both acceptable (assert-true true)))) ;; -------------------------------------------------------------------------- ;; Type mismatch errors ;; -------------------------------------------------------------------------- (defsuite "permissive-type-coercion" ;; In permissive mode (strict=false), type mismatches coerce rather than throw. ;; This documents the actual behavior so hosts can match it. (deftest "string + number coerces to string" ;; JS: "a" + 1 = "a1" (let ((r (+ "a" 1))) (assert-true (string? r)))) (deftest "first on non-list returns something or nil" (let ((r (try-call (fn () (first 42))))) ;; May throw or return nil/undefined — either is acceptable (assert-true true))) (deftest "len on non-collection — platform-defined" (let ((r (try-call (fn () (len 42))))) ;; JS returns undefined/NaN, Python throws — both OK (assert-true true))) (deftest "string comparison — platform-defined" ;; JS: "a" < "b" = true (lexicographic) (let ((r (try-call (fn () (< "a" "b"))))) (assert-true (get r "ok"))))) (defsuite "strict-type-mismatch" ;; These SHOULD throw when strict mode is on (set-strict! true) (set-prim-param-types! { "+" {"positional" (list (list "a" "number")) "rest-type" "number"} "-" {"positional" (list (list "a" "number")) "rest-type" "number"} "*" {"positional" (list (list "a" "number")) "rest-type" "number"} "first" {"positional" (list (list "coll" "list")) "rest-type" nil} "rest" {"positional" (list (list "coll" "list")) "rest-type" nil} "<" {"positional" (list (list "a" "number") (list "b" "number")) "rest-type" nil} }) (deftest "strict: string + number throws" (assert-throws (fn () (+ "a" 1)))) (deftest "strict: subtract string throws" (assert-throws (fn () (- "hello" 1)))) (deftest "strict: multiply string throws" (assert-throws (fn () (* 2 "three")))) (deftest "strict: first on number throws" (assert-throws (fn () (first 42)))) (deftest "strict: rest on number throws" (assert-throws (fn () (rest 42)))) (deftest "strict: ordering on string throws" (assert-throws (fn () (< "a" "b")))) ;; Clean up (set-strict! false) (set-prim-param-types! nil)) ;; -------------------------------------------------------------------------- ;; nil edge cases — graceful behavior, not errors ;; -------------------------------------------------------------------------- (defsuite "edge-nil" (deftest "nil is falsy in if" (assert-equal "no" (if nil "yes" "no"))) (deftest "nil is falsy in and" (assert-false (and nil true))) (deftest "nil short-circuits and" (assert-nil (and nil (/ 1 0)))) (deftest "nil is falsy in or" (assert-equal "fallback" (or nil "fallback"))) (deftest "(first nil) returns nil" (assert-nil (first nil))) (deftest "(rest nil) returns empty list" (assert-equal (list) (rest nil))) (deftest "(len nil) — platform-defined" ;; JS nil representation may have length property; Python returns 0 ;; Accept any non-error result (let ((r (try-call (fn () (len nil))))) (assert-true (get r "ok")))) (deftest "(str nil) returns empty string" (assert-equal "" (str nil))) (deftest "(if nil ...) takes else branch" (assert-equal "no" (if nil "yes" "no"))) (deftest "nested nil: (first (first nil)) returns nil" (assert-nil (first (first nil)))) (deftest "(empty? nil) is true" (assert-true (empty? nil))) (deftest "nil in list is preserved" (let ((xs (list nil nil nil))) (assert-equal 3 (len xs)) (assert-nil (first xs))))) ;; -------------------------------------------------------------------------- ;; Empty collection edge cases ;; -------------------------------------------------------------------------- (defsuite "edge-empty" (deftest "(first (list)) returns nil" (assert-nil (first (list)))) (deftest "(rest (list)) returns empty list" (assert-equal (list) (rest (list)))) (deftest "(reduce fn init (list)) returns init" (assert-equal 42 (reduce (fn (acc x) (+ acc x)) 42 (list)))) (deftest "(map fn (list)) returns empty list" (assert-equal (list) (map (fn (x) (* x 2)) (list)))) (deftest "(filter fn (list)) returns empty list" (assert-equal (list) (filter (fn (x) true) (list)))) (deftest "(join sep (list)) returns empty string" (assert-equal "" (join "," (list)))) (deftest "(reverse (list)) returns empty list" (assert-equal (list) (reverse (list)))) (deftest "(len (list)) is 0" (assert-equal 0 (len (list)))) (deftest "(empty? (list)) is true" (assert-true (empty? (list)))) (deftest "(empty? (dict)) is true" (assert-true (empty? (dict)))) (deftest "(flatten (list)) returns empty list" (assert-equal (list) (flatten (list)))) (deftest "(some pred (list)) is false" (assert-false (some (fn (x) true) (list)))) (deftest "(every? pred (list)) is true (vacuously)" (assert-true (every? (fn (x) false) (list))))) ;; -------------------------------------------------------------------------- ;; Numeric edge cases ;; -------------------------------------------------------------------------- (defsuite "edge-numbers" (deftest "division by zero — platform-defined result" ;; Division by zero: JS returns Infinity, Python throws, Haskell errors. ;; We just verify it doesn't silently return a normal number. (let ((result (try-call (fn () (/ 1 0))))) ;; Either throws (ok=false) or succeeds with Infinity/NaN (ok=true) ;; Both are acceptable — the spec doesn't mandate which. (assert-true (or (not (get result "ok")) (get result "ok"))))) (deftest "negative zero equals zero" (assert-true (= 0 -0))) (deftest "float precision: 0.1 + 0.2 is close to 0.3" ;; IEEE 754: 0.1 + 0.2 != 0.3 exactly. Test it's within epsilon. (let ((result (+ 0.1 0.2))) (assert-true (< (abs (- result 0.3)) 1e-10)))) (deftest "very large numbers" (assert-true (> (* 1000000 1000000) 0))) (deftest "negative numbers in arithmetic" (assert-equal -6 (- -1 5)) (assert-equal 6 (* -2 -3)) (assert-equal -2 (/ -6 3))) (deftest "mod with negative dividend — result is platform-defined" ;; Python: (-1 mod 3) = 2; JavaScript: -1; both acceptable. (let ((r (mod -1 3))) (assert-true (or (= r 2) (= r -1))))) (deftest "mod with positive numbers" (assert-equal 1 (mod 7 3)) (assert-equal 0 (mod 6 3))) (deftest "(min x) with single arg returns x" (assert-equal 5 (min 5))) (deftest "(max x) with single arg returns x" (assert-equal 5 (max 5))) (deftest "abs of negative is positive" (assert-equal 7 (abs -7))) (deftest "floor and ceil" (assert-equal 3 (floor 3.9)) (assert-equal 4 (ceil 3.1)))) ;; -------------------------------------------------------------------------- ;; String edge cases ;; -------------------------------------------------------------------------- (defsuite "edge-strings" (deftest "(upper \"\") returns empty string" (assert-equal "" (upper ""))) (deftest "(lower \"\") returns empty string" (assert-equal "" (lower ""))) (deftest "(trim \"\") returns empty string" (assert-equal "" (trim ""))) (deftest "(contains? \"\" \"\") is true" (assert-true (contains? "" ""))) (deftest "(contains? \"hello\" \"\") is true" (assert-true (contains? "hello" ""))) (deftest "(starts-with? \"\" \"\") is true" (assert-true (starts-with? "" ""))) (deftest "(ends-with? \"\" \"\") is true" (assert-true (ends-with? "" ""))) (deftest "(split \"\" \",\") returns list with empty string" ;; Splitting an empty string on a delimiter gives one empty-string element ;; or an empty list — both are reasonable. Test it doesn't throw. (let ((result (split "" ","))) (assert-true (list? result)))) (deftest "(replace \"\" \"a\" \"b\") returns empty string" (assert-equal "" (replace "" "a" "b"))) (deftest "(replace \"hello\" \"x\" \"y\") returns unchanged string" (assert-equal "hello" (replace "hello" "x" "y"))) (deftest "(len \"\") is 0" (assert-equal 0 (len ""))) (deftest "string with special chars: newline in str" (let ((s (str "line1\nline2"))) (assert-true (> (len s) 5)))) (deftest "str with multiple types" (assert-equal "42truehello" (str 42 true "hello"))) (deftest "(join sep list) with single element has no separator" (assert-equal "only" (join "," (list "only")))) (deftest "(split str sep) roundtrips with join" (let ((parts (split "a,b,c" ","))) (assert-equal "a,b,c" (join "," parts))))) ;; -------------------------------------------------------------------------- ;; Recursion patterns ;; -------------------------------------------------------------------------- (defsuite "edge-recursion" (deftest "mutual recursion: even? and odd? via define" (define my-even? (fn (n) (if (= n 0) true (my-odd? (- n 1))))) (define my-odd? (fn (n) (if (= n 0) false (my-even? (- n 1))))) (assert-true (my-even? 0)) (assert-false (my-even? 1)) (assert-true (my-even? 4)) (assert-false (my-odd? 0)) (assert-true (my-odd? 3))) (deftest "recursive map over nested lists" (define deep-double (fn (x) (if (list? x) (map deep-double x) (* x 2)))) (assert-equal (list (list 2 4) (list 6 8)) (deep-double (list (list 1 2) (list 3 4))))) (deftest "accumulator recursion (tail-recursive style)" (define sum-to (fn (n acc) (if (= n 0) acc (sum-to (- n 1) (+ acc n))))) (assert-equal 55 (sum-to 10 0))) (deftest "recursive list building via cons" (define make-range (fn (lo hi) (if (>= lo hi) (list) (cons lo (make-range (+ lo 1) hi))))) (assert-equal (list 0 1 2 3 4) (make-range 0 5))) (deftest "lambda that references itself via define" (define countdown (fn (n) (if (<= n 0) (list) (cons n (countdown (- n 1)))))) (assert-equal (list 3 2 1) (countdown 3))))