feat(strictness): first/rseq shape checks, assoc even-args + non-associative

- first throws on scalars (numbers/keywords/booleans/char & symbol structs)
- rseq is vector/sorted-only (throws on strings/maps/numbers/seqs)
- assoc requires an even kv count and a map/vector/nil receiver

first clean; rseq 12/1; assoc 39/3. clojure-test-suite pass 3864->3874.
spec: seq/more-strictness (11). jpm test green.
This commit is contained in:
Yogthos 2026-06-05 13:50:11 -04:00
parent ff3cc7d6c0
commit 2ca3fa4348
3 changed files with 36 additions and 5 deletions

View file

@ -342,6 +342,13 @@
result)))))))))))
(defn core-assoc [m & kvs]
(when (odd? (length kvs))
(error "assoc expects an even number of key/value arguments"))
# assoc is defined on maps, vectors and nil; reject other shapes
(when (or (number? m) (string? m) (buffer? m) (keyword? m) (boolean? m)
(plist? m) (set? m) (core-transient? m)
(and (struct? m) (get m :jolt/type)))
(error (string "assoc requires a map or vector, got " (type m))))
(cond
(phm? m)
(do (var result m) (var i 0) (while (< i (length kvs)) (set result (phm-assoc result (kvs i) (kvs (+ i 1)))) (+= i 2)) result)
@ -527,9 +534,14 @@
# maps and sets: first of their seq (an entry / element)
(phm? coll) (let [e (phm-entries coll)] (if (= 0 (length e)) nil (in e 0)))
(set? coll) (let [s (phs-seq coll)] (if (= 0 (length s)) nil (in s 0)))
(struct? coll) (let [ks (keys coll)] (if (= 0 (length ks)) nil (tuple (in ks 0) (get coll (in ks 0)))))
(or (nil? coll) (= 0 (length coll))) nil
(string? coll) (make-char (in coll 0))
(and (struct? coll) (nil? (get coll :jolt/type)))
(let [ks (keys coll)] (if (= 0 (length ks)) nil (tuple (in ks 0) (get coll (in ks 0)))))
(nil? coll) nil
(string? coll) (if (= 0 (length coll)) nil (make-char (in coll 0)))
# scalars aren't seqable
(or (number? coll) (boolean? coll) (keyword? coll) (and (struct? coll) (get coll :jolt/type)))
(error (string "first not supported on " (type coll)))
(= 0 (length coll)) nil
(in coll 0)))
(defn core-rest [coll]
@ -2972,8 +2984,13 @@
(defn core-not-empty [coll]
(if (or (nil? coll) (= 0 (core-count coll))) nil coll))
# rseq is defined only on vectors and sorted collections (Reversible).
(defn core-rseq [coll]
(let [c (realize-for-iteration coll)] (tuple/slice (tuple ;(reverse c)))))
(cond
(pvec? coll) (tuple/slice (tuple ;(reverse (pv->array coll))))
(core-sorted-map? coll) (tuple/slice (tuple ;(reverse (sorted-map-entries coll))))
(core-sorted-set? coll) (tuple/slice (tuple ;(reverse (coll :items))))
(error (string "rseq requires a vector or sorted collection, got " (type coll)))))
(defn core-shuffle [coll]
(let [c (array/slice (realize-for-iteration coll))]

View file

@ -18,7 +18,7 @@
# Baseline: assertions Jolt currently passes across the suite. Raise as Jolt
# improves so a regression (previously-passing assertion breaking) is caught.
(def baseline-pass 3860)
(def baseline-pass 3870)
# A file is "clean" when it ran with zero failures AND zero errors.
(def baseline-clean-files 45)
# Per-file wall-clock budget (seconds). Normal files finish in well under 1s;

View file

@ -165,3 +165,17 @@
["val on number" :throws "(val 0)"]
["key of entry" ":a" "(key (first {:a 1}))"]
["val of entry" "1" "(val (first {:a 1}))"])
# More strictness: first/rseq on the right shapes, assoc even-arg requirement.
(defspec "seq / more strictness"
["first on number" :throws "(first 42)"]
["first on keyword" :throws "(first :a)"]
["first ok vec" "1" "(first [1 2])"]
["first ok nil" "nil" "(first nil)"]
["rseq vector" "[3 2 1]" "(rseq [1 2 3])"]
["rseq on string" :throws "(rseq \"ab\")"]
["rseq on map" :throws "(rseq {:a 1})"]
["rseq on number" :throws "(rseq 0)"]
["assoc odd args" :throws "(assoc {:a 1} :b)"]
["assoc on number" :throws "(assoc 5 :a 1)"]
["assoc on set" :throws "(assoc #{} :a 1)"])