feat: distinguish map entries from vectors; min-key NaN ordering; subvec float coercion

- A map entry is a 2-element tuple (Jolt produces tuples only from map iteration;
  vector literals are pvecs, lists are arrays). key/val/map-entry? now accept a
  2-tuple and reject a plain vector, matching Clojure's MapEntry-vs-vector
  distinction — no metadata needed, the representations already differ.
- min-key/max-key reproduce Clojure's NaN-aware folding (2-arg strict </>, then
  <=/>=) and require numeric keys (NaN allowed, strings throw).
- subvec coerces float/NaN indices like (int ...) (truncate, NaN->0) then
  bounds-checks, instead of throwing on non-integers.

min_key 35/14 -> 49/0 (clean); key/val recover the 2-vector cases; subvec floats
fixed. clojure-test-suite pass 3898->3921. Updated conformance-test (key/val now
needs a real entry). spec: map/map-entry-&-key-ordering (14). jpm test green.
This commit is contained in:
Yogthos 2026-06-05 14:22:06 -04:00
parent ace1dcd124
commit 360b23c8af
4 changed files with 60 additions and 18 deletions

View file

@ -1302,16 +1302,22 @@
(array? coll) (if (= 0 (length coll)) (error "Can't pop empty list") (array/slice coll 1)) (array? coll) (if (= 0 (length coll)) (error "Can't pop empty list") (array/slice coll 1))
(error (string "pop not supported on " (type coll))))) (error (string "pop not supported on " (type coll)))))
# Clojure coerces subvec indices with (int ...): floats truncate and NaN -> 0;
# only non-numbers and out-of-range values throw.
(defn- subvec-idx [x]
(cond
(not (number? x)) (error "subvec index must be a number")
(not= x x) 0 # NaN -> 0
(math/trunc x)))
(defn core-subvec [v start &opt end] (defn core-subvec [v start &opt end]
(when (not (or (pvec? v) (tuple? v) (array? v))) (when (not (or (pvec? v) (tuple? v) (array? v)))
(error (string "subvec requires a vector, got " (type v)))) (error (string "subvec requires a vector, got " (type v))))
(let [a (vview v) (let [a (vview v)
e (if (nil? end) (length a) end)] s (subvec-idx start)
(when (not (and (number? start) (number? e) e (if (nil? end) (length a) (subvec-idx end))]
(= start (math/floor start)) (= e (math/floor e)) (when (not (and (>= s 0) (<= s e) (<= e (length a))))
(>= start 0) (<= start e) (<= e (length a)))) (error (string "subvec indices out of range: " s " " e " (length " (length a) ")")))
(error (string "subvec indices out of range: " start " " e " (length " (length a) ")"))) (make-vec (tuple/slice a s e))))
(make-vec (tuple/slice a start e))))
(defn core-trampoline [f & args] (defn core-trampoline [f & args]
(var result (apply f args)) (var result (apply f args))
@ -3056,21 +3062,34 @@
# With a single item, Clojure returns it WITHOUT calling f. On ties, the last # With a single item, Clojure returns it WITHOUT calling f. On ties, the last
# extremal item wins (>=/<= update), matching Clojure. # extremal item wins (>=/<= update), matching Clojure.
# Clojure's min-key/max-key: the 2-arg base compares with strict < / > (so the
# second wins on ties/NaN), and each further item switches on <= / >=. This
# asymmetry reproduces the JVM's NaN-ordering behavior. Janet's < / > are used
# directly (NaN comparisons are false, never throwing).
# keys must be numbers (NaN allowed) — like Clojure, which compares them with </>.
(defn core-min-key [f & xs] (defn core-min-key [f & xs]
(def f (as-fn f)) (def f (as-fn f))
(when (= 0 (length xs)) (error "min-key requires at least one value")) (when (= 0 (length xs)) (error "min-key requires at least one value"))
(if (= 1 (length xs)) (first xs) (if (= 1 (length xs)) (first xs)
(do (var best (first xs)) (var bestv (f best)) (do (var v (in xs 0)) (var kv (need-num (f v) "min-key"))
(each x (array/slice xs 1) (let [v (f x)] (when (<= v bestv) (set best x) (set bestv v)))) (let [y (in xs 1) ky (need-num (f y) "min-key")] (when (not (< kv ky)) (set v y) (set kv ky)))
best))) (var i 2)
(while (< i (length xs))
(let [w (in xs i) kw (need-num (f w) "min-key")] (when (<= kw kv) (set v w) (set kv kw)))
(++ i))
v)))
(defn core-max-key [f & xs] (defn core-max-key [f & xs]
(def f (as-fn f)) (def f (as-fn f))
(when (= 0 (length xs)) (error "max-key requires at least one value")) (when (= 0 (length xs)) (error "max-key requires at least one value"))
(if (= 1 (length xs)) (first xs) (if (= 1 (length xs)) (first xs)
(do (var best (first xs)) (var bestv (f best)) (do (var v (in xs 0)) (var kv (need-num (f v) "max-key"))
(each x (array/slice xs 1) (let [v (f x)] (when (>= v bestv) (set best x) (set bestv v)))) (let [y (in xs 1) ky (need-num (f y) "max-key")] (when (not (> kv ky)) (set v y) (set kv ky)))
best))) (var i 2)
(while (< i (length xs))
(let [w (in xs i) kw (need-num (f w) "max-key")] (when (>= kw kv) (set v w) (set kv kw)))
(++ i))
v)))
(defn core-not-every? [pred coll] (defn core-not-every? [pred coll]
(def pred (as-fn pred)) (def pred (as-fn pred))
@ -3227,10 +3246,14 @@
# Map entries (represented as 2-element vectors) # Map entries (represented as 2-element vectors)
# key/val require a map entry (a 2-element vector/tuple in Jolt); Clojure throws # key/val require a map entry (a 2-element vector/tuple in Jolt); Clojure throws
# otherwise. (Jolt can't distinguish a 2-vector from a real MapEntry.) # otherwise. (Jolt can't distinguish a 2-vector from a real MapEntry.)
(defn- entry-like? [x] (and (or (pvec? x) (tuple? x) (array? x)) (= 2 (core-count x)))) # A map entry is a 2-element tuple — Jolt produces tuples only from map
(defn core-key [e] (if (entry-like? e) (core-nth e 0) (error "key requires a map entry"))) # iteration (first/seq/map over a map), while vector literals are pvecs and
(defn core-val [e] (if (entry-like? e) (core-nth e 1) (error "val requires a map entry"))) # lists are arrays. So key/val/map-entry? accept a 2-tuple and reject a plain
(defn core-map-entry? [x] (and (or (pvec? x) (tuple? x)) (= 2 (core-count x)))) # vector, matching Clojure (where a MapEntry is distinct from a vector).
(defn- entry-like? [x] (and (tuple? x) (= 2 (length x))))
(defn core-key [e] (if (entry-like? e) (in e 0) (error "key requires a map entry")))
(defn core-val [e] (if (entry-like? e) (in e 1) (error "val requires a map entry")))
(defn core-map-entry? [x] (entry-like? x))
(defn core-rand-nth [coll] (defn core-rand-nth [coll]
(let [c (realize-for-iteration coll)] (let [c (realize-for-iteration coll)]

View file

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

View file

@ -117,7 +117,7 @@
["filter over map" "true" "(= [[:b 2]] (filterv (fn [[k v]] (> v 1)) {:a 1 :b 2}))"] ["filter over map" "true" "(= [[:b 2]] (filterv (fn [[k v]] (> v 1)) {:a 1 :b 2}))"]
["doall realizes" "(quote (2 3 4))" "(doall (map inc [1 2 3]))"] ["doall realizes" "(quote (2 3 4))" "(doall (map inc [1 2 3]))"]
["tree-seq" "(quote (1 2 3))" "(map (fn [x] x) (filter (complement coll?) (tree-seq coll? seq [1 [2 [3]]])))"] ["tree-seq" "(quote (1 2 3))" "(map (fn [x] x) (filter (complement coll?) (tree-seq coll? seq [1 [2 [3]]])))"]
["key/val" "true" "(let [e [:k 9]] (and (= :k (key e)) (= 9 (val e))))"] ["key/val" "true" "(let [e (first {:k 9})] (and (= :k (key e)) (= 9 (val e))))"]
["nat-int?" "true" "(and (nat-int? 0) (nat-int? 5) (not (nat-int? -1)))"] ["nat-int?" "true" "(and (nat-int? 0) (nat-int? 5) (not (nat-int? -1)))"]
["list* prepend" "(quote (1 2 3 4))" "(list* 1 2 [3 4])"] ["list* prepend" "(quote (1 2 3 4))" "(list* 1 2 [3 4])"]
["cycle" "(quote (1 2 3 1 2 3 1))" "(take 7 (cycle [1 2 3]))"] ["cycle" "(quote (1 2 3 1 2 3 1))" "(take 7 (cycle [1 2 3]))"]

View file

@ -96,3 +96,22 @@
["merge atomic arg" :throws "(merge {} :foo)"] ["merge atomic arg" :throws "(merge {} :foo)"]
["merge [k v] ok" "{:foo 1}" "(merge {} [:foo 1])"] ["merge [k v] ok" "{:foo 1}" "(merge {} [:foo 1])"]
["merge maps ok" "{:a 1, :b 2}" "(merge {:a 1} {:b 2})"]) ["merge maps ok" "{:a 1, :b 2}" "(merge {:a 1} {:b 2})"])
# Map entries are distinct from plain vectors (key/val/map-entry? reject a
# vector); min-key/max-key follow Clojure's NaN-aware ordering; subvec coerces
# float/NaN indices like (int ...).
(defspec "map / map-entry & key ordering"
["key of entry" ":a" "(key (first {:a 1}))"]
["val of entry" "1" "(val (first {:a 1}))"]
["key rejects vector" :throws "(key [:a 1])"]
["val rejects vector" :throws "(val [:a 1])"]
["map-entry? entry" "true" "(map-entry? (first {:a 1}))"]
["map-entry? vector" "false" "(map-entry? [:a 1])"]
["min-key NaN first" "1" "(min-key identity ##NaN 1)"]
["min-key NaN last" "true" "(NaN? (min-key identity 1 ##NaN))"]
["min-key NaN three" "true" "(infinite? (min-key identity ##NaN ##-Inf 1))"]
["min-key keys nonnum" :throws "(min-key identity \"x\" \"y\")"]
["max-key picks max" "[1 2 3]" "(max-key count [1] [1 2 3] [1 2])"]
["subvec float trunc" "[0]" "(subvec [0 1 2] 0.5 1.33)"]
["subvec NaN start" "[0 1 2]" "(subvec [0 1 2] ##NaN 3)"]
["subvec NaN end" "[]" "(subvec [0 1 2] 0 ##NaN)"])