Skip to main content

cljrs_value/collections/
hash_set.rs

1use crate::Value;
2use crate::collections::share_charge::{ShareCharge, hamt_path_bytes, rb_path_bytes};
3use rpds::{HashTrieMapSync, RedBlackTreeMapSync};
4
5/// An immutable hash set that preserves insertion order when iterated.
6///
7/// Uses the same index-plus-ordered-log technique as `PersistentHashMap`:
8/// a `rpds::HashTrieMap` from value to insertion sequence number, and a
9/// `rpds::RedBlackTreeMap` from sequence number to value that is iterated in
10/// order. This keeps iteration deterministic and matching insertion order,
11/// rather than depending on hash-bucket layout — `rpds` seeds its hasher
12/// randomly per instance, so raw hash-order iteration would otherwise vary
13/// from run to run.
14///
15/// A set derived by `conj` or `disj` from a GC-allocated set reports only its
16/// path-copied nodes from `gc_size_extra`; see `collections::share_charge`
17/// for the full rule.
18#[derive(Debug, Clone)]
19pub struct PersistentHashSet {
20    index: HashTrieMapSync<Value, u64>,
21    order: RedBlackTreeMapSync<u64, Value>,
22    next_seq: u64,
23    charge: ShareCharge,
24    keys_inert: bool,
25}
26
27impl PersistentHashSet {
28    pub fn empty() -> Self {
29        Self {
30            index: HashTrieMapSync::new_sync(),
31            order: RedBlackTreeMapSync::new_sync(),
32            next_seq: 0,
33            charge: ShareCharge::full(),
34            keys_inert: true,
35        }
36    }
37
38    pub fn count(&self) -> usize {
39        self.index.size()
40    }
41
42    pub fn is_empty(&self) -> bool {
43        self.index.is_empty()
44    }
45
46    pub(crate) fn storage_entry(&self, key: &Value) -> Option<(usize, Value)> {
47        self.index
48            .get_key_value(key)
49            .map(|(stored, sequence)| (*sequence as usize, stored.clone()))
50    }
51    pub(crate) fn keys_inert(&self) -> bool {
52        self.keys_inert
53    }
54
55    pub fn contains(&self, val: &Value) -> bool {
56        self.index.contains_key(val)
57    }
58
59    /// Return a new set with `val` added.
60    ///
61    /// If `val` is already present, its original insertion position is kept.
62    pub fn conj(&self, val: Value) -> Self {
63        let keys_inert = self.keys_inert && crate::realization::is_inert(&val);
64        if self.index.contains_key(&val) {
65            return self.unchanged();
66        }
67        let seq = self.next_seq;
68        let n = self.count();
69        Self {
70            index: self.index.insert(val.clone(), seq),
71            order: self.order.insert(seq, val),
72            next_seq: seq + 1,
73            charge: self.charge.derive(n, n + 1, Self::path_bytes(n + 1)),
74            keys_inert,
75        }
76    }
77
78    pub fn conj_mut(&mut self, val: Value) -> &mut Self {
79        self.keys_inert &= crate::realization::is_inert(&val);
80        if !self.index.contains_key(&val) {
81            let seq = self.next_seq;
82            self.index.insert_mut(val.clone(), seq);
83            self.order.insert_mut(seq, val);
84            self.next_seq = seq + 1;
85            self.charge = ShareCharge::full();
86        }
87        self
88    }
89
90    pub(crate) fn disj_mut(&mut self, value: &Value) {
91        if let Some(seq) = self.index.get(value).copied() {
92            self.index.remove_mut(value);
93            self.order.remove_mut(&seq);
94            self.charge = ShareCharge::full();
95        }
96    }
97
98    /// Return a new set with `val` removed.
99    pub fn disj(&self, val: &Value) -> Self {
100        match self.index.get(val) {
101            Some(seq) => {
102                let n = self.count();
103                Self {
104                    index: self.index.remove(val),
105                    order: self.order.remove(seq),
106                    next_seq: self.next_seq,
107                    charge: self.charge.derive(n, n - 1, Self::path_bytes(n)),
108                    keys_inert: self.keys_inert,
109                }
110            }
111            None => self.unchanged(),
112        }
113    }
114
115    /// A copy returned by an update that changed nothing; it shares every node.
116    fn unchanged(&self) -> Self {
117        let n = self.count();
118        Self {
119            index: self.index.clone(),
120            order: self.order.clone(),
121            next_seq: self.next_seq,
122            charge: self.charge.derive(n, n, 0),
123            keys_inert: self.keys_inert,
124        }
125    }
126
127    /// Bytes path-copied in both trees by inserting or removing one element.
128    fn path_bytes(n: usize) -> usize {
129        hamt_path_bytes::<(Value, u64)>(n) + rb_path_bytes::<(u64, Value)>(n)
130    }
131
132    /// Estimated bytes owned by this set if it shared no nodes.
133    pub(crate) fn full_size_extra(&self) -> usize {
134        // Per entry: index HashTrieMap entry (~40) + order RedBlackTree node
135        // (~48), value stored inline in the order tree (not behind GcPtr).
136        self.count() * (88 + std::mem::size_of::<Value>())
137    }
138
139    /// What `gc_size_extra` would report, without marking this version
140    /// allocated.
141    #[cfg(test)]
142    fn charged(&self) -> usize {
143        self.charge.size_extra(self.full_size_extra())
144    }
145
146    /// Iterate over all elements in insertion order.
147    pub fn iter(&self) -> impl Iterator<Item = &Value> {
148        self.order.iter().map(|(_, v)| v)
149    }
150}
151
152impl FromIterator<Value> for PersistentHashSet {
153    fn from_iter<I: IntoIterator<Item = Value>>(iter: I) -> Self {
154        let mut s = PersistentHashSet::empty();
155        for v in iter {
156            s.conj_mut(v);
157        }
158        s
159    }
160}
161
162impl PartialEq for PersistentHashSet {
163    fn eq(&self, other: &Self) -> bool {
164        if self.count() != other.count() {
165            return false;
166        }
167        self.iter().all(|k| other.contains(k))
168    }
169}
170
171impl cljrs_gc::Trace for PersistentHashSet {
172    #[cfg(feature = "no-gc")]
173    fn immutable_children(&self) -> bool {
174        true
175    }
176    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
177        for (_, v) in self.order.iter() {
178            v.trace(visitor);
179        }
180    }
181
182    fn gc_size_extra(&self) -> usize {
183        self.charge.on_alloc(self.full_size_extra())
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190    use crate::Value;
191
192    fn int(n: i64) -> Value {
193        Value::Long(n)
194    }
195
196    #[test]
197    fn test_basic() {
198        let s = PersistentHashSet::empty();
199        let s = s.conj(int(1)).conj(int(2)).conj(int(3));
200        assert_eq!(s.count(), 3);
201        assert!(s.contains(&int(1)));
202        assert!(s.contains(&int(2)));
203        assert!(!s.contains(&int(99)));
204    }
205
206    #[test]
207    fn test_idempotent_conj() {
208        let s = PersistentHashSet::empty().conj(int(1)).conj(int(1));
209        assert_eq!(s.count(), 1);
210    }
211
212    #[test]
213    fn test_disj() {
214        let s = PersistentHashSet::empty().conj(int(1)).conj(int(2));
215        let s2 = s.disj(&int(1));
216        assert!(!s2.contains(&int(1)));
217        assert!(s2.contains(&int(2)));
218        assert_eq!(s2.count(), 1);
219    }
220
221    #[test]
222    fn test_equality_order_independent() {
223        let a = PersistentHashSet::from_iter([int(1), int(2), int(3)]);
224        let b = PersistentHashSet::from_iter([int(3), int(1), int(2)]);
225        assert_eq!(a, b);
226    }
227
228    #[test]
229    fn test_iteration_order_matches_insertion_order() {
230        let s = PersistentHashSet::empty()
231            .conj(int(10))
232            .conj(int(3))
233            .conj(int(7))
234            .conj(int(1));
235        let iterated: Vec<Value> = s.iter().cloned().collect();
236        assert_eq!(iterated, vec![int(10), int(3), int(7), int(1)]);
237    }
238
239    #[test]
240    fn conj_on_allocated_set_charges_path_bytes() {
241        let _alloc = crate::collections::share_charge::alloc_scope();
242        use cljrs_gc::GcPtr;
243
244        let s = GcPtr::new(PersistentHashSet::from_iter((0..10_000).map(int)));
245        let added = s.get().conj(int(-1));
246        assert_eq!(added.charged(), PersistentHashSet::path_bytes(10_001));
247        assert!(added.charged() * 100 < s.get().full_size_extra());
248        // A no-op conj shares every node.
249        assert_eq!(s.get().conj(int(5)).charged(), 1);
250        // A set built by `conj_mut` owns all of its nodes.
251        let mut built = PersistentHashSet::empty();
252        built.conj_mut(int(1));
253        assert_eq!(built.charged(), built.full_size_extra());
254    }
255}