Files
rose-ash/spec/tests/test-strict.sx
giles 8f146cc810 Add strict typing mode + 139 new tests: 749/749 passing
Strict mode (spec/eval.sx):
- *strict* flag, set-strict!, set-prim-param-types!
- value-matches-type? checks values against declared types
- strict-check-args validates primitive call args at runtime
- Injected into eval-call before apply — zero cost when off
- Supports positional params, rest-type, nullable ("string?")

New test files:
- test-strict.sx (25): value-matches-type?, toggle, 12 type error cases
- test-errors.sx (74): undefined symbols, arity, permissive coercion,
  strict type mismatches, nil/empty edge cases, number edge cases,
  string edge cases, recursion patterns
- test-advanced.sx (39): nested special forms, higher-order patterns,
  define patterns, quasiquote advanced, thread-first, letrec, case/cond

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 12:12:48 +00:00

148 lines
5.3 KiB
Plaintext

;; ==========================================================================
;; test-strict.sx — Tests for strict typing mode
;;
;; Requires: test-framework.sx loaded first.
;; Modules tested: eval.sx (strict-check-args, set-strict!, value-matches-type?)
;;
;; When *strict* is true, primitive calls check arg types at runtime.
;; ==========================================================================
;; --------------------------------------------------------------------------
;; value-matches-type? — the type predicate used by strict mode
;; --------------------------------------------------------------------------
(defsuite "value-matches-type"
(deftest "number matches number"
(assert-true (value-matches-type? 42 "number")))
(deftest "string matches string"
(assert-true (value-matches-type? "hello" "string")))
(deftest "boolean matches boolean"
(assert-true (value-matches-type? true "boolean")))
(deftest "nil matches nil"
(assert-true (value-matches-type? nil "nil")))
(deftest "list matches list"
(assert-true (value-matches-type? (list 1 2) "list")))
(deftest "dict matches dict"
(assert-true (value-matches-type? (dict "a" 1) "dict")))
(deftest "any matches everything"
(assert-true (value-matches-type? 42 "any"))
(assert-true (value-matches-type? "s" "any"))
(assert-true (value-matches-type? nil "any"))
(assert-true (value-matches-type? (list) "any")))
(deftest "wrong type fails"
(assert-false (value-matches-type? "hello" "number"))
(assert-false (value-matches-type? 42 "string"))
(assert-false (value-matches-type? nil "number"))
(assert-false (value-matches-type? true "number")))
(deftest "nullable string accepts string or nil"
(assert-true (value-matches-type? "hello" "string?"))
(assert-true (value-matches-type? nil "string?"))
(assert-false (value-matches-type? 42 "string?")))
(deftest "nullable number accepts number or nil"
(assert-true (value-matches-type? 42 "number?"))
(assert-true (value-matches-type? nil "number?"))
(assert-false (value-matches-type? "x" "number?"))))
;; --------------------------------------------------------------------------
;; Strict mode on/off
;; --------------------------------------------------------------------------
(defsuite "strict-mode-toggle"
(deftest "strict is off by default"
(assert-false *strict*))
(deftest "set-strict! enables and disables"
;; Verify by testing behavior: with strict on, bad types throw
(set-strict! true)
(set-prim-param-types! {"inc" {"positional" (list (list "n" "number")) "rest-type" nil}})
(let ((r (try-call (fn () (inc "x")))))
(assert-false (get r "ok")))
;; Turn off: same call should succeed (JS coercion)
(set-strict! false)
(let ((r (try-call (fn () (inc "x")))))
(assert-true (get r "ok")))
(set-prim-param-types! nil)))
;; --------------------------------------------------------------------------
;; Strict mode catches type errors at runtime
;; --------------------------------------------------------------------------
(defsuite "strict-type-errors"
;; Enable strict mode and register param types for these tests
(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"}
"/" {"positional" (list (list "a" "number")) "rest-type" "number"}
"inc" {"positional" (list (list "n" "number")) "rest-type" nil}
"dec" {"positional" (list (list "n" "number")) "rest-type" nil}
"upper" {"positional" (list (list "s" "string")) "rest-type" nil}
"lower" {"positional" (list (list "s" "string")) "rest-type" nil}
"first" {"positional" (list (list "coll" "list")) "rest-type" nil}
"rest" {"positional" (list (list "coll" "list")) "rest-type" nil}
"len" {"positional" (list (list "coll" "any")) "rest-type" nil}
"keys" {"positional" (list (list "d" "dict")) "rest-type" nil}
})
(deftest "correct types pass"
;; These should NOT throw
(assert-equal 3 (+ 1 2))
(assert-equal "HELLO" (upper "hello"))
(assert-equal 1 (first (list 1 2 3))))
(deftest "string + number throws"
(assert-throws (fn () (+ "a" 1))))
(deftest "number + string throws"
(assert-throws (fn () (+ 1 "b"))))
(deftest "subtract string throws"
(assert-throws (fn () (- "hello" 1))))
(deftest "multiply string throws"
(assert-throws (fn () (* 2 "three"))))
(deftest "inc on string throws"
(assert-throws (fn () (inc "x"))))
(deftest "upper on number throws"
(assert-throws (fn () (upper 42))))
(deftest "first on number throws"
(assert-throws (fn () (first 42))))
(deftest "rest on number throws"
(assert-throws (fn () (rest 42))))
(deftest "keys on list throws"
(assert-throws (fn () (keys (list 1 2 3)))))
(deftest "nil is not a number"
(assert-throws (fn () (+ nil 1))))
(deftest "boolean is not a number"
(assert-throws (fn () (* true 2))))
(deftest "correct types after errors still pass"
;; Verify strict mode wasn't broken by previous throws
(assert-equal 10 (+ 5 5))
(assert-equal "HI" (upper "hi")))
;; Clean up — disable strict mode for other tests
(set-strict! false)
(set-prim-param-types! nil))