# Specification: maps (associative). (use ../support/harness) (defspec "map / construct & predicate" ["literal" "{:a 1}" "{:a 1}"] ["hash-map" "{:a 1, :b 2}" "(hash-map :a 1 :b 2)"] ["empty" "{}" "{}"] ["map? true" "true" "(map? {:a 1})"] ["map? false on vector" "false" "(map? [1 2])"] ["count" "2" "(count {:a 1 :b 2})"] ["empty? true" "true" "(empty? {})"] ["equality order-indep" "true" "(= {:a 1 :b 2} {:b 2 :a 1})"]) (defspec "map / access" ["get" "1" "(get {:a 1} :a)"] ["get missing nil" "nil" "(get {:a 1} :z)"] ["get default" ":x" "(get {:a 1} :z :x)"] ["keyword as fn" "1" "(:a {:a 1})"] ["keyword fn default" ":x" "(:z {:a 1} :x)"] ["map as fn" "1" "({:a 1} :a)"] ["get-in" "2" "(get-in {:a {:b 2}} [:a :b])"] ["get-in missing" "nil" "(get-in {:a {}} [:a :b])"] ["contains? key" "true" "(contains? {:a 1} :a)"] ["contains? missing" "false" "(contains? {:a 1} :z)"] ["find returns entry" "[:a 1]" "(find {:a 1} :a)"] ["keys" "true" "(= #{:a :b} (set (keys {:a 1 :b 2})))"] ["vals" "true" "(= #{1 2} (set (vals {:a 1 :b 2})))"]) (defspec "map / update" ["assoc adds" "{:a 1, :b 2}" "(assoc {:a 1} :b 2)"] ["assoc overwrites" "{:a 9}" "(assoc {:a 1} :a 9)"] ["assoc many" "{:a 1, :b 2}" "(assoc {} :a 1 :b 2)"] ["dissoc" "{:a 1}" "(dissoc {:a 1 :b 2} :b)"] ["dissoc many" "{:a 1}" "(dissoc {:a 1 :b 2 :c 3} :b :c)"] ["merge" "{:a 1, :b 2}" "(merge {:a 1} {:b 2})"] ["merge overwrites" "{:a 2}" "(merge {:a 1} {:a 2})"] ["merge lattermost wins" "{:a 3}" "(merge {:a 1} {:a 2} {:a 3})"] ["merge no args -> nil" "nil" "(merge)"] ["merge all nil -> nil" "nil" "(merge nil nil)"] ["merge nil arg no-op" "{:a 1}" "(merge {:a 1} nil)"] ["merge nil then map" "{:a 1}" "(merge nil {:a 1})"] ["merge empty + nil" "{}" "(merge {} nil)"] ["merge map-entry (conj)" "{:a 1}" "(merge {} (first {:a 1}))"] ["merge [k v] vector" "{:foo 1}" "(merge {} [:foo 1])"] ["merge collection key" "true" "(= {[2 3] :foo} (merge {[2 3] :foo} nil {}))"] ["merge-with" "{:a 3}" "(merge-with + {:a 1} {:a 2})"] ["update" "{:a 2}" "(update {:a 1} :a inc)"] ["update missing w/ fnil" "{:a 1}" "(update {} :a (fnil inc 0))"] ["update-in" "{:a {:b 2}}" "(update-in {:a {:b 1}} [:a :b] inc)"] ["assoc-in" "{:a {:b 1}}" "(assoc-in {} [:a :b] 1)"] ["select-keys" "{:a 1}" "(select-keys {:a 1 :b 2} [:a])"] ["into onto map" "{:a 1, :b 2}" "(into {:a 1} [[:b 2]])"] ["zipmap" "{:a 1, :b 2}" "(zipmap [:a :b] [1 2])"]) (defspec "map / iteration & entries" ["map over entries" "true" "(= #{1 2} (set (map val {:a 1 :b 2})))"] ["map keys" "true" "(= #{:a :b} (set (map key {:a 1 :b 2})))"] ["reduce over entries" "6" "(reduce (fn [a e] (+ a (val e))) 0 {:a 1 :b 2 :c 3})"] ["reduce-kv" "6" "(reduce-kv (fn [a k v] (+ a v)) 0 {:a 1 :b 2 :c 3})"] ["destructure entry" "true" "(= [[:a 2]] (into [] (map (fn [[k v]] [k (inc v)]) {:a 1})))"] ["first of map is entry" "true" "(let [e (first {:a 1})] (and (= (key e) :a) (= (val e) 1)))"] ["map-entry?" "true" "(map-entry? (first {:a 1}))"] ["count of nil map" "0" "(count nil)"] ["get from nil" "nil" "(get nil :a)"] ["immutability" "true" "(let [m {:a 1} n (assoc m :b 2)] (and (= m {:a 1}) (= n {:a 1 :b 2})))"]) (defspec "map / collection keys (by value)" ["vector key literal" ":v" "(get {[1 2] :v} [1 2])"] ["map key literal" ":v" "(get {(hash-map :a 1) :v} {:a 1})"] ["assoc vector key" ":v" "(get (assoc {} [1 2] :v) [1 2])"] ["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)]))"]) # A nil element/key/value INSIDE a collection used as a map key must survive key # canonicalization — it used to be dropped, so the nil-containing collection # collided with the nil-free one (jolt-zcm9). (defspec "map / nil inside a collection key (jolt-zcm9)" ["set key w/ nil distinct" "2" "(count {#{nil 1} :a, #{1} :b})"] ["set key w/ nil neg lookup" "nil" "(get {#{nil 1} :a} #{1})"] ["set key w/ nil pos lookup" ":a" "(get {#{nil 1} :a} #{nil 1})"] ["set key just nil distinct" "false" "(= {#{nil} :x} {#{} :x})"] ["map nil-value key distinct" "2" "(count {{:a nil} 1, {} 2})"] ["map nil-value key neg" "nil" "(get {{:a nil} 1} {})"] ["map nil-value key pos" "1" "(get {{:a nil} 1} {:a nil})"] ["map nil-key key distinct" "2" "(count {{nil :a} 1, {} 2})"] ["map nil-key key pos" "1" "(get {{nil :a} 1} {nil :a})"]) # 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 [])"] ["merge empty vector" :throws "(merge {} [])"] ["merge 1-elem vector" :throws "(merge {} [:foo])"] ["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)"]) # A nil value is a PRESENT key in Clojure (distinct from a missing key); Janet # structs drop nil, so jolt builds these maps as a phm. Tested via literals (the # reader path) and the construction/op surface, in every spec mode. (defspec "map / nil values preserved" ["literal contains" "true" "(contains? {:b nil} :b)"] ["literal not= empty" "false" "(= {:b nil} {})"] ["literal get nil" "nil" "(get {:b nil} :b :x)"] ["literal keys incl nil" "true" "(= #{:a :b} (set (keys {:a nil :b 1})))"] ["literal count" "2" "(count {:a nil :b 1})"] ["literal vals incl nil" "2" "(count (vals {:a nil :b 1}))"] ["eval values w/ nil" "3" "(:a {:a (+ 1 2) :b nil})"] ["nil key present" "true" "(contains? {nil :v} nil)"] ["assoc nil present" "true" "(contains? (assoc {:a 1} :b nil) :b)"] ["assoc nil get" "nil" "(get (assoc {:a 1} :b nil) :b :x)"] ["assoc overwrite nil" "nil" "(get (assoc {:a 1} :a nil) :a :x)"] ["hash-map nil" "true" "(contains? (hash-map :b nil) :b)"] ["merge new nil" "true" "(contains? (merge {:a 1} {:b nil}) :b)"] ["merge overwrite nil" "nil" "(get (merge {:a 1} {:a nil}) :a :x)"] ["merge-with present nil" "true" "(= [nil 1] (get (merge-with (fn [a b] [a b]) {:a nil} {:a 1}) :a))"] ["into nil val" "true" "(contains? (into {} [[:a nil]]) :a)"] ["conj map nil" "true" "(contains? (conj {:x 1} {:a nil}) :a)"] ["zipmap nil" "true" "(contains? (zipmap [:a] [nil]) :a)"] ["select-keys nil" "true" "(contains? (select-keys {:a nil} [:a]) :a)"] ["get-in present nil" "nil" "(get-in {:a nil} [:a] :x)"] ["get-in through nil" ":x" "(get-in {:a nil} [:a :b] :x)"] ["dissoc keeps nil" "true" "(contains? (dissoc {:a nil :b 1} :b) :a)"] ["reduce-kv sees nil" "true" "(= #{:a :b} (reduce-kv (fn [acc k v] (conj acc k)) #{} {:a nil :b 2}))"] ["nil-free stays fast" "true" "(= {:a 1 :b 2} {:b 2 :a 1})"]) # Clojure 1.11 map transformers. (defspec "map / update-keys & update-vals (1.11)" ["update-keys" "{\"a\" 1, \"b\" 2}" "(update-keys {:a 1 :b 2} name)"] ["update-keys empty" "{}" "(update-keys {} inc)"] ["update-keys nil" "{}" "(update-keys nil str)"] ["update-keys collide last wins" "1" "(count (update-keys {:a 1 :b 2} (fn [_] :k)))"] ["update-vals" "{:a 2, :b 3}" "(update-vals {:a 1 :b 2} inc)"] ["update-vals empty" "{}" "(update-vals {} inc)"] ["update-vals nil" "{}" "(update-vals nil inc)"] ["update-vals keeps keys" "[:a :b]" "(sort (keys (update-vals {:a 1 :b 2} inc)))"]) # keys/vals/empty? are 00-syntax overlay fns now (jolt-4j3) — expander-called, # so they live in the first tier and get the staged defn recompile. (defspec "maps / keys-vals-empty? as overlay fns" ["keys" "(quote (:a))" "(keys {:a 1})"] ["keys empty map" "nil" "(keys {})"] ["keys nil" "nil" "(keys nil)"] ["vals" "(quote (1))" "(vals {:a 1})"] ["vals empty" "nil" "(vals {})"] ["keys sorted order" "[1 2 3]" "(vec (keys (sorted-map 2 :b 1 :a 3 :c)))"] ["vals sorted order" "[:a :b :c]" "(vec (vals (sorted-map 2 :b 1 :a 3 :c)))"] ["keys/vals zip" "{:a 1 :b 2}" "(zipmap (keys {:a 1 :b 2}) (vals {:a 1 :b 2}))"] ["empty? map" "true" "(empty? {})"] ["empty? vec" "[true false]" "[(empty? []) (empty? [1])]"] ["empty? list" "[true false]" "[(empty? ()) (empty? (list 1))]"] ["empty? string" "[true false]" "[(empty? \"\") (empty? \"a\")]"] ["empty? nil" "true" "(empty? nil)"] ["empty? set" "[true false]" "[(empty? #{}) (empty? #{1})]"] ["empty? lazy" "[true false]" "[(empty? (filter pos? [-1])) (empty? (map inc [1]))]"] ["empty? lazy nil elem" "false" "(empty? (cons nil nil))"] ["empty? sorted" "[true false]" "[(empty? (sorted-map)) (empty? (sorted-set 1))]"] ["empty? number throws" :throws "(empty? 5)"]) # (assoc nil k v) yields a real immutable map, not a raw host table, so # assoc-in into absent keys nests countable/seqable maps (jolt-w4s). (defspec "map / assoc on nil" ["assoc nil is a map" "{:a 1}" "(assoc nil :a 1)"] ["count of assoc nil" "1" "(count (assoc nil :a 1))"] ["assoc-in nested countable" "1" "(count (:a (assoc-in {} [:a :b] 1)))"] ["assoc-in deep get" "9" "(get-in (assoc-in {} [:a :b :c] 9) [:a :b :c])"] ["seq over assoc-nil map" ":a" "(ffirst (seq (assoc nil :a 1)))"] ["keys of assoc-nil map" "[:a]" "(vec (keys (assoc nil :a 1)))"]) # into {} / frequencies / group-by bulk-build the HAMT in one pass from a native # pairs/table accumulator (phm-from-pairs), instead of an assoc per element # (jolt-5vsp collections). Cross the bin(<=16)/array-node(>=17) promotion and a # size that grows the trie a level; check dedup (last-wins), nil keys, and that # assoc/dissoc after a bulk build still land. The structure is identical to the # incremental builder (validated in the PR), so reads agree. (defspec "map / bulk build boundaries" ["into = incr at 17" "true" "(= (into {} (map (fn [i] [i (* i 2)]) (range 17))) (reduce (fn [m p] (assoc m (first p) (second p))) {} (map (fn [i] [i (* i 2)]) (range 17))))"] ["into = incr at 1000" "true" "(= (into {} (map (fn [i] [i (* i 2)]) (range 1000))) (reduce (fn [m p] (assoc m (first p) (second p))) {} (map (fn [i] [i (* i 2)]) (range 1000))))"] ["into count 1000" "1000" "(count (into {} (map (fn [i] [i i]) (range 1000))))"] ["into reads back" "999" "(get (into {} (map (fn [i] [i (* i 3)]) (range 1000))) 333)"] ["into onto non-empty" "9" "(get (into {:a 1} [[:a 9] [:b 2]]) :a)"] ["into dup last wins" "9" "(get (into {} [[:k 1] [:k 9]]) :k)"] ["into nil key" ":x" "(get (into {} [[nil :x] [:a 1]]) nil)"] ["assoc after bulk" "7" "(get (assoc (into {} (map (fn [i] [i i]) (range 100))) :new 7) :new)"] ["dissoc after bulk" "nil" "(get (dissoc (into {} (map (fn [i] [i i]) (range 100))) 50) 50)"] ["frequencies count" "3" "(get (frequencies [1 2 2 1 2 1]) 1)"] ["frequencies coll-key" "2" "(get (frequencies [[1 2] [1 2] [3 4]]) [1 2])"] # nil is a legal map key, but a Janet table drops a nil key — the transient # map (canon-keyed table) used to silently lose the nil-key entry, so # group-by/frequencies/assoc dropped the whole nil bucket. tbl-key routes nil # to a sentinel; phm keeps its own has-nil slot. ["frequencies nil key" "2" "(get (frequencies [nil nil 1]) nil)"] ["group-by nil key" "[nil nil]" "(get (group-by identity [nil nil 1]) nil)"] ["group-by nil count" "2" "(count (group-by identity [nil nil 1]))"] ["transient nil key" ":x" "(let [t (transient {})] (assoc! t nil :x) (get (persistent! t) nil))"] ["transient nil get" "true" "(let [t (transient {})] (assoc! t nil :x) (contains? t nil))"] ["transient nil dissoc" ":gone" "(let [t (transient {})] (assoc! t nil :x) (dissoc! t nil) (get (persistent! t) nil :gone))"] # group-by buckets are built on transient vectors (O(1) push) and frozen once, # rather than an O(log n) persistent conj per element. Result is identical: # bucket contents and order match the persistent build across a coarse # grouping (few large buckets — the case bound on the per-bucket conj). ["group-by bucket" "[1 3 5]" "(get (group-by odd? (range 1 6)) true)"] ["group-by big bucket" "true" "(= (group-by even? (range 200)) {true (vec (filter even? (range 200))) false (vec (filter odd? (range 200)))})"] ["group-by order" "[0 3 6 9]" "(get (group-by (fn [x] (mod x 3)) (range 10)) 0)"] ["hash-map bulk = incr" "true" "(= (apply hash-map (mapcat (fn [i] [i i]) (range 50))) (reduce (fn [m i] (assoc m i i)) {} (range 50)))"])