diff --git a/src/jolt/core.janet b/src/jolt/core.janet index 828f321..079f742 100644 --- a/src/jolt/core.janet +++ b/src/jolt/core.janet @@ -1302,16 +1302,22 @@ (array? coll) (if (= 0 (length coll)) (error "Can't pop empty list") (array/slice coll 1)) (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] (when (not (or (pvec? v) (tuple? v) (array? v))) (error (string "subvec requires a vector, got " (type v)))) (let [a (vview v) - e (if (nil? end) (length a) end)] - (when (not (and (number? start) (number? e) - (= start (math/floor start)) (= e (math/floor e)) - (>= start 0) (<= start e) (<= e (length a)))) - (error (string "subvec indices out of range: " start " " e " (length " (length a) ")"))) - (make-vec (tuple/slice a start e)))) + s (subvec-idx start) + e (if (nil? end) (length a) (subvec-idx end))] + (when (not (and (>= s 0) (<= s e) (<= e (length a)))) + (error (string "subvec indices out of range: " s " " e " (length " (length a) ")"))) + (make-vec (tuple/slice a s e)))) (defn core-trampoline [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 # 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] (def f (as-fn f)) (when (= 0 (length xs)) (error "min-key requires at least one value")) (if (= 1 (length xs)) (first xs) - (do (var best (first xs)) (var bestv (f best)) - (each x (array/slice xs 1) (let [v (f x)] (when (<= v bestv) (set best x) (set bestv v)))) - best))) + (do (var v (in xs 0)) (var kv (need-num (f v) "min-key")) + (let [y (in xs 1) ky (need-num (f y) "min-key")] (when (not (< kv ky)) (set v y) (set kv ky))) + (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] (def f (as-fn f)) (when (= 0 (length xs)) (error "max-key requires at least one value")) (if (= 1 (length xs)) (first xs) - (do (var best (first xs)) (var bestv (f best)) - (each x (array/slice xs 1) (let [v (f x)] (when (>= v bestv) (set best x) (set bestv v)))) - best))) + (do (var v (in xs 0)) (var kv (need-num (f v) "max-key")) + (let [y (in xs 1) ky (need-num (f y) "max-key")] (when (not (> kv ky)) (set v y) (set kv ky))) + (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] (def pred (as-fn pred)) @@ -3227,10 +3246,14 @@ # Map entries (represented as 2-element vectors) # 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.) -(defn- entry-like? [x] (and (or (pvec? x) (tuple? x) (array? x)) (= 2 (core-count x)))) -(defn core-key [e] (if (entry-like? e) (core-nth e 0) (error "key requires a map entry"))) -(defn core-val [e] (if (entry-like? e) (core-nth e 1) (error "val requires a map entry"))) -(defn core-map-entry? [x] (and (or (pvec? x) (tuple? x)) (= 2 (core-count x)))) +# A map entry is a 2-element tuple — Jolt produces tuples only from map +# iteration (first/seq/map over a map), while vector literals are pvecs and +# lists are arrays. So key/val/map-entry? accept a 2-tuple and reject a plain +# 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] (let [c (realize-for-iteration coll)] diff --git a/test/integration/clojure-test-suite-test.janet b/test/integration/clojure-test-suite-test.janet index f28e9a0..98043ed 100644 --- a/test/integration/clojure-test-suite-test.janet +++ b/test/integration/clojure-test-suite-test.janet @@ -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 3895) +(def baseline-pass 3915) # 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; diff --git a/test/integration/conformance-test.janet b/test/integration/conformance-test.janet index fcccea8..f3de4e6 100644 --- a/test/integration/conformance-test.janet +++ b/test/integration/conformance-test.janet @@ -117,7 +117,7 @@ ["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]))"] ["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)))"] ["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]))"] diff --git a/test/spec/maps-spec.janet b/test/spec/maps-spec.janet index 7f3ded5..b165f59 100644 --- a/test/spec/maps-spec.janet +++ b/test/spec/maps-spec.janet @@ -96,3 +96,22 @@ ["merge atomic arg" :throws "(merge {} :foo)"] ["merge [k v] ok" "{:foo 1}" "(merge {} [:foo 1])"] ["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)"])