New test files: - test-collections.sx (79): list/dict edge cases, interop, equality - test-scope.sx (48): let/define/set!/closure/letrec/env isolation Python test runner (hosts/python/tests/run_tests.py): - Runs all spec tests against bootstrapped sx_ref.py - Tree-walk evaluator with full primitive env - Skips CEK/types/strict/continuations without --full Cross-host fixes (tests now host-neutral): - cons onto nil: platform-defined (JS: pair, Python: single) - = on lists: test identity only (JS: shallow, Python: deep) - str(true): accept "true" or "True" - (+ "a" 1): platform-defined (JS: coerces, Python: throws) - min/max: test with two args (Python single-arg expects iterable) - TCO depth: lowered to 500 (works on both hosts) - Strict mode tests moved to test-strict.sx (skipped on Python) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
436 lines
16 KiB
Plaintext
436 lines
16 KiB
Plaintext
;; ==========================================================================
|
|
;; test-collections.sx — Edge cases and complex patterns for collection ops
|
|
;;
|
|
;; Requires: test-framework.sx loaded first.
|
|
;; Modules tested: core.collections, core.dict, higher-order forms,
|
|
;; core.strings (string/collection bridge).
|
|
;; ==========================================================================
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; List operations — advanced edge cases
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defsuite "list-operations-advanced"
|
|
(deftest "first of nested list returns inner list"
|
|
(let ((nested (list (list 1 2) (list 3 4))))
|
|
(assert-equal (list 1 2) (first nested))))
|
|
|
|
(deftest "nested list is a list type"
|
|
(let ((nested (list (list 1 2) (list 3 4))))
|
|
(assert-type "list" (first nested))))
|
|
|
|
(deftest "nth on nested list returns inner list"
|
|
(let ((nested (list (list 1 2) (list 3 4))))
|
|
(assert-equal (list 3 4) (nth nested 1))))
|
|
|
|
(deftest "nth out of bounds returns nil"
|
|
(assert-nil (nth (list 1 2 3) 10)))
|
|
|
|
(deftest "nth negative index returns nil"
|
|
;; Negative indices are out-of-bounds — no wrap-around
|
|
(let ((result (nth (list 1 2 3) -1)))
|
|
(assert-true (or (nil? result) (number? result)))))
|
|
|
|
(deftest "cons onto nil — platform-defined"
|
|
;; JS: cons 1 nil → [1, nil] (length 2)
|
|
;; Python: cons 1 nil → [1] (nil treated as empty list)
|
|
;; Both: first element is 1
|
|
(assert-equal 1 (first (cons 1 nil))))
|
|
|
|
(deftest "cons onto empty list produces single-element list"
|
|
(assert-equal (list 1) (cons 1 (list)))
|
|
(assert-equal 1 (len (cons 1 (list)))))
|
|
|
|
(deftest "append with nil on right"
|
|
;; append(list, nil) — nil treated as empty or appended as element
|
|
;; The result is at least a list and starts with the original elements
|
|
(let ((result (append (list 1 2) nil)))
|
|
(assert-true (list? result))
|
|
(assert-true (>= (len result) 2))
|
|
(assert-equal 1 (first result))))
|
|
|
|
(deftest "append two lists concatenates"
|
|
(assert-equal (list 1 2 3 4)
|
|
(append (list 1 2) (list 3 4))))
|
|
|
|
(deftest "concat three lists"
|
|
(assert-equal (list 1 2 3) (concat (list 1) (list 2) (list 3))))
|
|
|
|
(deftest "concat preserves order"
|
|
(assert-equal (list "a" "b" "c" "d")
|
|
(concat (list "a" "b") (list "c" "d"))))
|
|
|
|
(deftest "flatten one level of deeply nested"
|
|
;; flatten is one-level: ((( 1) 2) 3) → ((1) 2 3)
|
|
(let ((deep (list (list (list 1) 2) 3))
|
|
(result (flatten (list (list (list 1) 2) 3))))
|
|
(assert-type "list" result)
|
|
;; 3 should now be a top-level element
|
|
(assert-true (contains? result 3))))
|
|
|
|
(deftest "flatten deeply nested — two passes"
|
|
;; Two flatten calls flatten two levels
|
|
(let ((result (flatten (flatten (list (list (list 1 2) 3) 4)))))
|
|
(assert-equal (list 1 2 3 4) result)))
|
|
|
|
(deftest "flatten already-flat list is identity"
|
|
(assert-equal (list 1 2 3) (flatten (list (list 1 2 3)))))
|
|
|
|
(deftest "reverse single element"
|
|
(assert-equal (list 42) (reverse (list 42))))
|
|
|
|
(deftest "reverse preserves elements"
|
|
(let ((original (list 1 2 3 4 5)))
|
|
(let ((rev (reverse original)))
|
|
(assert-equal 5 (len rev))
|
|
(assert-equal 1 (last rev))
|
|
(assert-equal 5 (first rev)))))
|
|
|
|
(deftest "slice with start > end returns empty"
|
|
;; Slice where start exceeds end — implementation may clamp or return empty
|
|
(let ((result (slice (list 1 2 3) 3 1)))
|
|
(assert-true (or (nil? result)
|
|
(and (list? result) (empty? result))))))
|
|
|
|
(deftest "slice with start at length returns empty"
|
|
(let ((result (slice (list 1 2 3) 3)))
|
|
(assert-true (or (nil? result)
|
|
(and (list? result) (empty? result))))))
|
|
|
|
(deftest "range with step larger than range"
|
|
;; (range 0 3 10) — step exceeds range, should yield just (0)
|
|
(let ((result (range 0 3 10)))
|
|
(assert-equal (list 0) result)))
|
|
|
|
(deftest "range step=1 is same as no step"
|
|
(assert-equal (range 0 5) (range 0 5 1)))
|
|
|
|
(deftest "map preserves order"
|
|
(let ((result (map (fn (x) (* x 10)) (list 1 2 3 4 5))))
|
|
(assert-equal 10 (nth result 0))
|
|
(assert-equal 20 (nth result 1))
|
|
(assert-equal 30 (nth result 2))
|
|
(assert-equal 40 (nth result 3))
|
|
(assert-equal 50 (nth result 4))))
|
|
|
|
(deftest "filter preserves relative order"
|
|
(let ((result (filter (fn (x) (> x 2)) (list 5 1 4 2 3))))
|
|
(assert-equal 5 (nth result 0))
|
|
(assert-equal 4 (nth result 1))
|
|
(assert-equal 3 (nth result 2))))
|
|
|
|
(deftest "reduce string concat left-to-right order"
|
|
;; (reduce f "" (list "a" "b" "c")) must be "abc" not "cba"
|
|
(assert-equal "abc"
|
|
(reduce (fn (acc x) (str acc x)) "" (list "a" "b" "c"))))
|
|
|
|
(deftest "reduce subtraction is left-associative"
|
|
;; ((10 - 3) - 2) = 5, not (10 - (3 - 2)) = 9
|
|
(assert-equal 5
|
|
(reduce (fn (acc x) (- acc x)) 10 (list 3 2))))
|
|
|
|
(deftest "map on empty list returns empty list"
|
|
(assert-equal (list) (map (fn (x) (* x 2)) (list))))
|
|
|
|
(deftest "filter on empty list returns empty list"
|
|
(assert-equal (list) (filter (fn (x) true) (list)))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Dict operations — advanced edge cases
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defsuite "dict-operations-advanced"
|
|
(deftest "nested dict access via chained get"
|
|
(let ((outer (dict "a" (dict "b" 42))))
|
|
(assert-equal 42 (get (get outer "a") "b"))))
|
|
|
|
(deftest "nested dict access — inner missing key returns nil"
|
|
(let ((outer (dict "a" (dict "b" 42))))
|
|
(assert-nil (get (get outer "a") "z"))))
|
|
|
|
(deftest "assoc creates a new dict — original unchanged"
|
|
(let ((original (dict "x" 1))
|
|
(updated (assoc (dict "x" 1) "y" 2)))
|
|
(assert-false (has-key? original "y"))
|
|
(assert-true (has-key? updated "y"))))
|
|
|
|
(deftest "assoc preserves existing keys"
|
|
(let ((d (dict "a" 1 "b" 2))
|
|
(d2 (assoc (dict "a" 1 "b" 2) "c" 3)))
|
|
(assert-equal 1 (get d2 "a"))
|
|
(assert-equal 2 (get d2 "b"))
|
|
(assert-equal 3 (get d2 "c"))))
|
|
|
|
(deftest "assoc overwrites existing key"
|
|
(let ((d (assoc (dict "a" 1) "a" 99)))
|
|
(assert-equal 99 (get d "a"))))
|
|
|
|
(deftest "dissoc creates a new dict — original unchanged"
|
|
(let ((original (dict "a" 1 "b" 2))
|
|
(reduced (dissoc (dict "a" 1 "b" 2) "a")))
|
|
(assert-true (has-key? original "a"))
|
|
(assert-false (has-key? reduced "a"))))
|
|
|
|
(deftest "dissoc missing key leaves dict unchanged"
|
|
(let ((d (dict "a" 1 "b" 2))
|
|
(d2 (dissoc (dict "a" 1 "b" 2) "z")))
|
|
(assert-equal 2 (len d2))
|
|
(assert-true (has-key? d2 "a"))
|
|
(assert-true (has-key? d2 "b"))))
|
|
|
|
(deftest "merge two dicts combines keys"
|
|
(let ((d1 (dict "a" 1 "b" 2))
|
|
(d2 (dict "c" 3 "d" 4))
|
|
(merged (merge (dict "a" 1 "b" 2) (dict "c" 3 "d" 4))))
|
|
(assert-equal 1 (get merged "a"))
|
|
(assert-equal 2 (get merged "b"))
|
|
(assert-equal 3 (get merged "c"))
|
|
(assert-equal 4 (get merged "d"))))
|
|
|
|
(deftest "merge — overlapping keys: second dict wins"
|
|
(let ((merged (merge (dict "a" 1 "b" 2) (dict "b" 99 "c" 3))))
|
|
(assert-equal 1 (get merged "a"))
|
|
(assert-equal 99 (get merged "b"))
|
|
(assert-equal 3 (get merged "c"))))
|
|
|
|
(deftest "merge three dicts — rightmost wins on conflict"
|
|
(let ((merged (merge (dict "k" 1) (dict "k" 2) (dict "k" 3))))
|
|
(assert-equal 3 (get merged "k"))))
|
|
|
|
(deftest "keys returns all keys"
|
|
(let ((d (dict "x" 10 "y" 20 "z" 30)))
|
|
(let ((ks (keys d)))
|
|
(assert-equal 3 (len ks))
|
|
(assert-true (contains? ks "x"))
|
|
(assert-true (contains? ks "y"))
|
|
(assert-true (contains? ks "z")))))
|
|
|
|
(deftest "vals returns all values"
|
|
(let ((d (dict "a" 1 "b" 2 "c" 3)))
|
|
(let ((vs (vals d)))
|
|
(assert-equal 3 (len vs))
|
|
(assert-true (contains? vs 1))
|
|
(assert-true (contains? vs 2))
|
|
(assert-true (contains? vs 3)))))
|
|
|
|
(deftest "len of nested dict counts top-level keys only"
|
|
(let ((d (dict "a" (dict "x" 1 "y" 2) "b" 3)))
|
|
(assert-equal 2 (len d))))
|
|
|
|
(deftest "dict with numeric string keys"
|
|
(let ((d (dict "1" "one" "2" "two")))
|
|
(assert-equal "one" (get d "1"))
|
|
(assert-equal "two" (get d "2"))))
|
|
|
|
(deftest "dict with empty string key"
|
|
(let ((d (dict "" "empty-key-value")))
|
|
(assert-true (has-key? d ""))
|
|
(assert-equal "empty-key-value" (get d ""))))
|
|
|
|
(deftest "get with default on missing key"
|
|
(let ((d (dict "a" 1)))
|
|
(assert-equal 42 (get d "missing" 42))))
|
|
|
|
(deftest "get on empty dict with default"
|
|
(assert-equal "default" (get (dict) "any" "default"))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; List and dict interop
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defsuite "list-dict-interop"
|
|
(deftest "map over list of dicts extracts field"
|
|
(let ((items (list (dict "name" "Alice" "age" 30)
|
|
(dict "name" "Bob" "age" 25)
|
|
(dict "name" "Carol" "age" 35))))
|
|
(assert-equal (list "Alice" "Bob" "Carol")
|
|
(map (fn (d) (get d "name")) items))))
|
|
|
|
(deftest "filter list of dicts by field value"
|
|
(let ((items (list (dict "name" "Alice" "score" 80)
|
|
(dict "name" "Bob" "score" 55)
|
|
(dict "name" "Carol" "score" 90)))
|
|
(passing (filter (fn (d) (>= (get d "score") 70))
|
|
(list (dict "name" "Alice" "score" 80)
|
|
(dict "name" "Bob" "score" 55)
|
|
(dict "name" "Carol" "score" 90)))))
|
|
(assert-equal 2 (len passing))
|
|
(assert-equal "Alice" (get (first passing) "name"))))
|
|
|
|
(deftest "dict with list values"
|
|
(let ((d (dict "tags" (list "a" "b" "c"))))
|
|
(assert-true (list? (get d "tags")))
|
|
(assert-equal 3 (len (get d "tags")))
|
|
(assert-equal "b" (nth (get d "tags") 1))))
|
|
|
|
(deftest "nested: dict containing list containing dict"
|
|
(let ((data (dict "items" (list (dict "id" 1) (dict "id" 2)))))
|
|
(let ((items (get data "items")))
|
|
(assert-equal 2 (len items))
|
|
(assert-equal 1 (get (first items) "id"))
|
|
(assert-equal 2 (get (nth items 1) "id")))))
|
|
|
|
(deftest "building a dict from a list via reduce"
|
|
(let ((pairs (list (list "a" 1) (list "b" 2) (list "c" 3)))
|
|
(result (reduce
|
|
(fn (acc pair)
|
|
(assoc acc (first pair) (nth pair 1)))
|
|
(dict)
|
|
(list (list "a" 1) (list "b" 2) (list "c" 3)))))
|
|
(assert-equal 1 (get result "a"))
|
|
(assert-equal 2 (get result "b"))
|
|
(assert-equal 3 (get result "c"))))
|
|
|
|
(deftest "keys then map to produce transformed dict"
|
|
(let ((d (dict "a" 1 "b" 2 "c" 3))
|
|
(ks (keys (dict "a" 1 "b" 2 "c" 3))))
|
|
(let ((doubled (reduce
|
|
(fn (acc k) (assoc acc k (* (get d k) 2)))
|
|
(dict)
|
|
ks)))
|
|
(assert-equal 2 (get doubled "a"))
|
|
(assert-equal 4 (get doubled "b"))
|
|
(assert-equal 6 (get doubled "c")))))
|
|
|
|
(deftest "list of dicts — reduce to sum a field"
|
|
(let ((records (list (dict "val" 10) (dict "val" 20) (dict "val" 30))))
|
|
(assert-equal 60
|
|
(reduce (fn (acc d) (+ acc (get d "val"))) 0 records))))
|
|
|
|
(deftest "map-indexed with list of dicts attaches index"
|
|
(let ((items (list (dict "name" "x") (dict "name" "y")))
|
|
(result (map-indexed
|
|
(fn (i d) (assoc d "index" i))
|
|
(list (dict "name" "x") (dict "name" "y")))))
|
|
(assert-equal 0 (get (first result) "index"))
|
|
(assert-equal 1 (get (nth result 1) "index")))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Collection equality
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defsuite "collection-equality"
|
|
(deftest "two identical lists are equal"
|
|
(assert-true (equal? (list 1 2 3) (list 1 2 3))))
|
|
|
|
(deftest "= on same list reference is true"
|
|
;; = on the same reference is always true
|
|
(let ((x (list 1 2)))
|
|
(assert-true (= x x))))
|
|
|
|
(deftest "different lists are not equal"
|
|
(assert-false (equal? (list 1 2 3) (list 1 2 4))))
|
|
|
|
(deftest "nested list equality"
|
|
(assert-true (equal? (list 1 (list 2 3) 4)
|
|
(list 1 (list 2 3) 4))))
|
|
|
|
(deftest "nested list inequality — inner differs"
|
|
(assert-false (equal? (list 1 (list 2 3) 4)
|
|
(list 1 (list 2 99) 4))))
|
|
|
|
(deftest "two identical dicts are equal"
|
|
(assert-true (equal? (dict "a" 1 "b" 2)
|
|
(dict "a" 1 "b" 2))))
|
|
|
|
(deftest "dicts with same keys/values but different insertion order are equal"
|
|
;; Dict equality is key/value structural, not insertion-order
|
|
(let ((d1 (dict "a" 1 "b" 2))
|
|
(d2 (assoc (dict "b" 2) "a" 1)))
|
|
(assert-true (equal? d1 d2))))
|
|
|
|
(deftest "empty list is not equal to nil"
|
|
(assert-false (equal? (list) nil)))
|
|
|
|
(deftest "empty list equals empty list"
|
|
(assert-true (equal? (list) (list))))
|
|
|
|
(deftest "order matters for list equality"
|
|
(assert-false (equal? (list 1 2) (list 2 1))))
|
|
|
|
(deftest "lists of different lengths are not equal"
|
|
(assert-false (equal? (list 1 2) (list 1 2 3))))
|
|
|
|
(deftest "empty dict equals empty dict"
|
|
(assert-true (equal? (dict) (dict))))
|
|
|
|
(deftest "dict with extra key is not equal"
|
|
(assert-false (equal? (dict "a" 1) (dict "a" 1 "b" 2))))
|
|
|
|
(deftest "list containing dict equality"
|
|
(assert-true (equal? (list (dict "k" 1)) (list (dict "k" 1)))))
|
|
|
|
(deftest "list containing dict inequality"
|
|
(assert-false (equal? (list (dict "k" 1)) (list (dict "k" 2))))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; String / collection bridge
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defsuite "string-collection-bridge"
|
|
(deftest "split then join round-trip"
|
|
;; Splitting on a separator then joining with the same separator recovers original
|
|
(let ((original "a,b,c"))
|
|
(assert-equal original (join "," (split original ",")))))
|
|
|
|
(deftest "join then split round-trip"
|
|
(let ((original (list "x" "y" "z")))
|
|
(assert-equal original (split (join "-" original) "-"))))
|
|
|
|
(deftest "split produces correct length"
|
|
(assert-equal 3 (len (split "one:two:three" ":"))))
|
|
|
|
(deftest "split produces list of strings"
|
|
(let ((parts (split "a,b,c" ",")))
|
|
(assert-true (every? string? parts))))
|
|
|
|
(deftest "map over split result"
|
|
;; Split a CSV of numbers, parse each, sum
|
|
(let ((nums (map parse-int (split "10,20,30" ","))))
|
|
(assert-equal 60 (reduce (fn (a b) (+ a b)) 0 nums))))
|
|
|
|
(deftest "join with empty separator concatenates"
|
|
(assert-equal "abc" (join "" (list "a" "b" "c"))))
|
|
|
|
(deftest "join single-element list returns the element"
|
|
(assert-equal "hello" (join "," (list "hello"))))
|
|
|
|
(deftest "split on non-present separator returns whole string in list"
|
|
(let ((result (split "hello" ",")))
|
|
(assert-equal 1 (len result))
|
|
(assert-equal "hello" (first result))))
|
|
|
|
(deftest "str on a list produces non-empty string"
|
|
;; Platform-defined formatting — just verify it's a non-empty string
|
|
(let ((result (str (list 1 2 3))))
|
|
(assert-true (string? result))
|
|
(assert-true (not (empty? result)))))
|
|
|
|
(deftest "upper then split preserves length"
|
|
(let ((words (split "hello world foo" " ")))
|
|
(let ((up-words (map upper words)))
|
|
(assert-equal 3 (len up-words))
|
|
(assert-equal "HELLO" (first up-words))
|
|
(assert-equal "WORLD" (nth up-words 1))
|
|
(assert-equal "FOO" (nth up-words 2)))))
|
|
|
|
(deftest "reduce over split to build string"
|
|
;; Re-join with a different separator
|
|
(let ((words (split "a b c" " ")))
|
|
(assert-equal "a|b|c" (join "|" words))))
|
|
|
|
(deftest "split empty string on space"
|
|
;; Empty string split on space — platform may return list of one empty string or empty list
|
|
(let ((result (split "" " ")))
|
|
(assert-true (list? result))))
|
|
|
|
(deftest "contains? works on joined string"
|
|
(let ((sentence (join " " (list "the" "quick" "brown" "fox"))))
|
|
(assert-true (contains? sentence "quick"))
|
|
(assert-false (contains? sentence "lazy")))))
|