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>
This commit is contained in:
Dmitri Sotnikov 2026-06-17 01:43:23 +00:00 committed by GitHub
parent 2668a76837
commit 630eb2b9d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 43 additions and 7 deletions

View file

@ -33,6 +33,27 @@
["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}))"]