jolt/test/spec/sets-spec.janet
Dmitri Sotnikov 630eb2b9d3
Keep a nil element in sets and transient sets (jolt-bn2p) (#156)
Two paths dropped a nil set member. phs-seq read members via
phm-to-struct, whose Janet struct can't hold a nil key, so the nil was
lost on seq/print and on into-an-existing-set even though the backing phm
counted it (count/contains? then disagreed). Re-attach the nil from the
phm's has-nil slot, keeping struct-key order for the rest so set printing
stays stable.

The transient set keyed its native table by canon-key and stored the
member as the value; canon-key returns nil for nil and a nil value also
drops the entry, so conj!/disj!/contains?/persistent! lost a nil member
outright. Key by tbl-key (the same nil sentinel the transient map uses)
and box a nil value through tbl-nil-key, unboxing on persistent!.

A nil-containing set used as a map key still collides with the nil-free
one (canon-key drops nil during key canonicalization) — separate latent
bug, filed as jolt-zcm9.

Co-authored-by: Yogthos <yogthos@gmail.com>
2026-06-17 01:43:23 +00:00

94 lines
5.8 KiB
Text

# Specification: sets, including clojure.set.
(use ../support/harness)
(defspec "set / construct & predicate"
["literal" "#{1 2 3}" "#{1 2 3}"]
["hash-set" "#{1 2 3}" "(hash-set 1 2 3)"]
["set from vector" "#{1 2 3}" "(set [1 2 3 1])"]
["empty" "#{}" "#{}"]
["set? true" "true" "(set? #{1})"]
["set? false on vector" "false" "(set? [1])"]
["count dedups" "3" "(count (set [1 1 2 3]))"]
["equality order-indep" "true" "(= #{1 2 3} #{3 2 1})"]
# jolt-h86: into-conj had no set branch and returned the set unchanged
["into set" "#{:a :b}" "(into #{} [:a :b])"]
["into non-empty set" "#{1 2 3}" "(into #{1} [2 3 2])"])
(defspec "set / operations"
["conj adds" "#{1 2 3}" "(conj #{1 2} 3)"]
["conj dup no-op" "#{1 2}" "(conj #{1 2} 1)"]
["disj removes" "#{1 2}" "(disj #{1 2 3} 3)"]
["disj missing no-op" "#{1 2}" "(disj #{1 2} 9)"]
["contains?" "true" "(contains? #{1 2} 1)"]
["contains? missing" "false" "(contains? #{1 2} 9)"]
["get present" "1" "(get #{1 2} 1)"]
["get missing nil" "nil" "(get #{1 2} 9)"]
["set as fn present" "2" "(#{1 2 3} 2)"]
["set as fn missing" "nil" "(#{1 2 3} 9)"])
(defspec "set / literals & value elements"
["literal evaluates elements" "#{2 4}" "#{(inc 1) (* 2 2)}"]
["map elements by value" "true" "(= #{{:a 1}} #{(hash-map :a 1)})"]
["contains? map by value" "true" "(contains? #{(hash-map :x 1)} {:x 1})"]
["dedup equal maps" "1" "(count (set [{:a 1} (hash-map :a 1)]))"]
["vector elements" "true" "(contains? #{[1 2]} (vec [1 2]))"])
(defspec "set / nil element (jolt-bn2p)"
# canon-key returns nil for nil and Janet tables drop a nil key, so a nil
# member used to be silently lost while count/contains? disagreed.
["set keeps nil" "2" "(count (set [nil 1 nil]))"]
["contains? nil true" "true" "(contains? (set [nil 1]) nil)"]
["contains? nil false" "false" "(contains? #{1} nil)"]
["seq includes nil" "true" "(some nil? (seq (set [nil 1])))"]
["disj nil" "#{1}" "(disj (set [nil 1]) nil)"]
["disj nil count" "1" "(count (disj (set [nil 1]) nil))"]
["conj nil count" "2" "(count (conj #{1} nil))"]
["conj nil contains?" "true" "(contains? (conj #{1} nil) nil)"]
["into #{} keeps nil" "2" "(count (into #{} [nil 1]))"]
["into #{} contains? nil" "true" "(contains? (into #{} [nil 1]) nil)"]
["into keeps existing nil" "true" "(contains? (into #{nil} [1]) nil)"]
# transient set path: tr-conj!/persistent!/disj!/contains?
["transient conj! nil" "2" "(count (persistent! (conj! (transient #{}) nil 1)))"]
["transient contains? nil" "true" "(contains? (persistent! (conj! (transient #{}) nil 1)) nil)"]
["transient disj! nil cnt" "1" "(count (persistent! (disj! (conj! (transient #{}) nil 1) nil)))"]
["transient disj! removes" "false" "(contains? (persistent! (disj! (conj! (transient #{}) nil 1) nil)) nil)"]
["transient of set w/ nil" "true" "(contains? (persistent! (transient (set [nil 1]))) nil)"])
(defspec "clojure.set"
["union" "#{1 2 3 4}" "(do (require (quote [clojure.set :as s])) (s/union #{1 2} #{3 4}))"]
["intersection" "#{2}" "(do (require (quote [clojure.set :as s])) (s/intersection #{1 2} #{2 3}))"]
["difference" "#{1}" "(do (require (quote [clojure.set :as s])) (s/difference #{1 2} #{2 3}))"]
["subset? true" "true" "(do (require (quote [clojure.set :as s])) (s/subset? #{1} #{1 2}))"]
["superset? true" "true" "(do (require (quote [clojure.set :as s])) (s/superset? #{1 2} #{1}))"]
["select" "#{2 4}" "(do (require (quote [clojure.set :as s])) (s/select even? #{1 2 3 4}))"]
["join" "#{{:a 1, :b 2, :c 3}}" "(do (require (quote [clojure.set :as s])) (s/join #{{:a 1 :b 2}} #{{:b 2 :c 3}}))"]
["map-invert" "{1 :a}" "(do (require (quote [clojure.set :as s])) (s/map-invert {:a 1}))"]
["rename-keys" "{:b 1}" "(do (require (quote [clojure.set :as s])) (s/rename-keys {:a 1} {:a :b}))"])
# set? recognizes every set representation (jolt-dpn: sorted sets are tagged
# tables the host set? predicate missed).
(defspec "set / set? across representations"
["literal" "true" "(set? #{1})"]
["empty literal" "true" "(set? #{})"]
["sorted-set" "true" "(set? (sorted-set 1 2))"]
["sorted-set-by" "true" "(set? (sorted-set-by > 1 2))"]
["empty sorted" "true" "(set? (sorted-set))"]
["map is not" "false" "(set? {})"]
["vector is not" "false" "(set? [1])"]
["coll? still true" "true" "(coll? (sorted-set 1))"]
["ifn? sorted-set" "true" "(ifn? (sorted-set 1))"])
# set / into #{} bulk-build the backing HAMT in one pass (phs-from-seq), instead
# of a phs-conj per element (jolt-5vsp collections). Cross the promotion
# boundary, check dedup, collection members, and conj after a bulk build.
(defspec "set / bulk build boundaries"
["set dedup count" "3" "(count (set [1 1 2 3 3 2]))"]
["set big count" "1000" "(count (set (range 1000)))"]
["into #{} count" "500" "(count (into #{} (range 500)))"]
["into #{} onto base" "3" "(count (into #{:a} [:a :b :c]))"]
["set contains" "true" "(contains? (set (range 1000)) 777)"]
["set missing" "false" "(contains? (set (range 1000)) 5000)"]
["set coll members" "true" "(contains? (set [[1 2] [3 4]]) [1 2])"]
["conj after bulk" "true" "(contains? (conj (set (range 100)) :x) :x)"]
["disj after bulk" "false" "(contains? (disj (set (range 100)) 50) 50)"]
["set = literal" "true" "(= #{0 1 2} (set (range 3)))"])