From ff3cc7d6c06b629cc6f034c21b71e2e5e94cee68 Mon Sep 17 00:00:00 2001 From: Yogthos Date: Fri, 5 Jun 2026 13:44:37 -0400 Subject: [PATCH] feat(strictness): assoc bounds, dissoc/count map-only, subvec bounds, numerator/denominator, min-key empty - assoc on a vector bounds-checks the index (0..count); out-of-range/negative throw - dissoc throws on non-maps (numbers/sequences/sets/scalars); nil ok; records/ sorted-maps/meta-maps still handled - count throws on scalars (numbers/keywords/symbols/booleans/chars) - subvec validates vector type and 0<=start<=end<=count - numerator/denominator always throw (Jolt has no ratio type) - min-key/max-key throw on no values count 18/2; dissoc 19/3; assoc 36/6; subvec 26/3/5; numerator/denominator throw cases pass. clojure-test-suite pass 3840->3864. spec: map/strictness (16). jpm test green. --- src/jolt/core.janet | 44 +++++++++++++++---- .../integration/clojure-test-suite-test.janet | 2 +- test/spec/maps-spec.janet | 20 +++++++++ 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/src/jolt/core.janet b/src/jolt/core.janet index eda07ba..3068f00 100644 --- a/src/jolt/core.janet +++ b/src/jolt/core.janet @@ -346,12 +346,20 @@ (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) (pvec? m) - (do (var result m) (var i 0) (while (< i (length kvs)) (set result (pv-assoc result (kvs i) (kvs (+ i 1)))) (+= i 2)) result) + (do (var result m) (var i 0) + (while (< i (length kvs)) + (let [idx (kvs i)] + (when (not (and (number? idx) (= idx (math/floor idx)) (>= idx 0) (<= idx (pv-count result)))) + (error (string "Index " idx " out of bounds for assoc on a vector of length " (pv-count result)))) + (set result (pv-assoc result idx (kvs (+ i 1))))) + (+= i 2)) result) # vector: assoc by integer index (appending at count is allowed); stays a vector (or (tuple? m) (array? m)) (do (var result (array/slice m)) (var i 0) (while (< i (length kvs)) (let [idx (kvs i) v (kvs (+ i 1))] + (when (not (and (number? idx) (= idx (math/floor idx)) (>= idx 0) (<= idx (length result)))) + (error (string "Index " idx " out of bounds for assoc on a vector of length " (length result)))) (if (= idx (length result)) (array/push result v) (put result idx v))) (+= i 2)) (if (tuple? m) (tuple/slice (tuple ;result)) result)) @@ -371,8 +379,15 @@ (if (struct? m) (table/to-struct result) result)))))) (defn core-dissoc [m & ks] - (if (phm? m) - (do (var result m) (each k ks (set result (phm-dissoc result k))) result) + (cond + (nil? m) nil + (phm? m) (do (var result m) (each k ks (set result (phm-dissoc result k))) result) + # reject clearly non-map values (scalars, sequences, sets, symbol/char structs) + (or (number? m) (string? m) (buffer? m) (keyword? m) (boolean? m) + (pvec? m) (plist? m) (tuple? m) (array? m) (set? m) (core-transient? m) + (and (struct? m) (get m :jolt/type))) + (error (string "dissoc requires a map, got " (type m))) + # struct map / sorted-map / record / meta-wrapped map (do (var result @{}) (each k (keys m) (var in-ks false) (each k2 ks (if (deep= k k2) (do (set in-ks true) (break)))) (if (not in-ks) (put result k (m k)))) (if (struct? m) (table/to-struct result) result)))) @@ -498,7 +513,9 @@ (set? coll) (coll :cnt) (phm? coll) (coll :cnt) (and (table? coll) (get coll :jolt/deftype)) (- (length (keys coll)) 1) - (length coll))) + (or (string? coll) (buffer? coll) (struct? coll) (tuple? coll) (array? coll)) (length coll) + # count is undefined on scalars (numbers/keywords/symbols/booleans/chars) + (error (string "count not supported on " (type coll))))) (defn core-first [coll] (cond @@ -1264,8 +1281,15 @@ (error (string "pop not supported on " (type coll))))) (defn core-subvec [v start &opt end] - (let [a (vview v)] - (make-vec (tuple/slice a start (if (nil? end) (length a) 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)))) (defn core-trampoline [f & args] (var result (apply f args)) @@ -2992,6 +3016,7 @@ # extremal item wins (>=/<= update), matching Clojure. (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)))) @@ -2999,6 +3024,7 @@ (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)))) @@ -3193,8 +3219,10 @@ (defn core-rational? [x] (intval? x)) (defn core-infinite? [x] (and (number? x) (= (math/abs x) math/inf))) (defn core-NaN? [x] (and (number? x) (not= x x))) -(defn core-numerator [x] x) -(defn core-denominator [x] 1) +# Jolt has no ratio type, so numerator/denominator have no valid input (Clojure +# requires a Ratio and throws otherwise). +(defn core-numerator [x] (error "numerator requires a ratio (Jolt has no ratios)")) +(defn core-denominator [x] (error "denominator requires a ratio (Jolt has no ratios)")) (defn core-list* [& args] (let [n (length args)] diff --git a/test/integration/clojure-test-suite-test.janet b/test/integration/clojure-test-suite-test.janet index b6fde55..f0488fd 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 3835) +(def baseline-pass 3860) # 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/spec/maps-spec.janet b/test/spec/maps-spec.janet index 7719c42..c24dc3c 100644 --- a/test/spec/maps-spec.janet +++ b/test/spec/maps-spec.janet @@ -71,3 +71,23 @@ ["key across repr" ":v" "(get (assoc {} (vec [1 2]) :v) [1 2])"] ["frequencies of maps" "2" "(get (frequencies [{:a 1} (hash-map :a 1)]) {:a 1})"] ["group-by collection key" "1" "(count (group-by identity [{:a 1} (hash-map :a 1)]))"]) + +# Strictness: assoc bounds-checks vector indices; dissoc requires a map; +# count rejects scalars; numerator/denominator have no ratio type. +(defspec "map / strictness (throws like Clojure)" + ["assoc vec out of bounds" :throws "(assoc [0 1 2] 4 4)"] + ["assoc vec negative" :throws "(assoc [] -1 0)"] + ["assoc vec at count ok" "[1 2 3]" "(assoc [1 2] 2 3)"] + ["dissoc on number" :throws "(dissoc 42 :a)"] + ["dissoc on vector" :throws "(dissoc [1 2] 0)"] + ["dissoc on set" :throws "(dissoc #{:a} :a)"] + ["dissoc nil ok" "nil" "(dissoc nil :a)"] + ["count on number" :throws "(count 1)"] + ["count on keyword" :throws "(count :a)"] + ["count string ok" "3" "(count \"abc\")"] + ["numerator throws" :throws "(numerator 1)"] + ["denominator throws" :throws "(denominator 2)"] + ["subvec out of range" :throws "(subvec [0 1 2 3] 1 5)"] + ["subvec start>end" :throws "(subvec [0 1 2 3] 3 2)"] + ["subvec ok" "[1 2]" "(subvec [0 1 2 3] 1 3)"] + ["min-key empty" :throws "(apply min-key identity [])"])