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