Skip to main content

cljrs_value/collections/
hash_map.rs

1use crate::Value;
2use crate::collections::array_map::PersistentArrayMap;
3use crate::collections::share_charge::{ShareCharge, hamt_path_bytes, rb_path_bytes};
4use rpds::{HashTrieMapSync, RedBlackTreeMapSync};
5
6/// An immutable hash map that preserves insertion order when iterated.
7///
8/// Lookups go through a `rpds::HashTrieMap` from key to insertion sequence
9/// number; the sequence number then indexes a `rpds::RedBlackTreeMap` that
10/// holds the actual `(key, value)` pairs in insertion order. Re-associating
11/// an existing key keeps its original position (matching `PersistentArrayMap`
12/// and how most ordered-map implementations behave), so iteration order is
13/// deterministic and matches the order keys were first written, rather than
14/// depending on hash-bucket layout (which — since `rpds` seeds its hasher
15/// randomly per instance — would otherwise vary from run to run).
16///
17/// Small maps (≤8 entries) are represented as `PersistentArrayMap` instead;
18/// the two types share the same `Value::Map` variant.  `PersistentHashMap` is
19/// used once the entry count exceeds the array-map threshold.
20///
21/// A map derived by `assoc` or `dissoc` from a GC-allocated map reports only
22/// its path-copied nodes from `gc_size_extra`; see
23/// `collections::share_charge` for the full rule.
24#[derive(Debug, Clone)]
25pub struct PersistentHashMap {
26    index: HashTrieMapSync<Value, u64>,
27    order: RedBlackTreeMapSync<u64, (Value, Value)>,
28    next_seq: u64,
29    charge: ShareCharge,
30}
31
32/// An entry of the insertion-order tree.
33type OrderEntry = (u64, (Value, Value));
34
35impl PersistentHashMap {
36    pub fn empty() -> Self {
37        Self {
38            index: HashTrieMapSync::new_sync(),
39            next_seq: 0,
40            order: RedBlackTreeMapSync::new_sync(),
41            charge: ShareCharge::full(),
42        }
43    }
44
45    /// Build from a raw `HashTrieMap`, in its (arbitrary) iteration order.
46    ///
47    /// Prefer `from_pairs`/`assoc` when the caller has a meaningful source
48    /// order to preserve.
49    pub fn new(map: HashTrieMapSync<Value, Value>) -> Self {
50        Self::from_pairs(map.iter().map(|(k, v)| (k.clone(), v.clone())))
51    }
52
53    pub fn count(&self) -> usize {
54        self.index.size()
55    }
56
57    pub fn is_empty(&self) -> bool {
58        self.index.is_empty()
59    }
60
61    /// Look up a key.
62    pub fn get(&self, key: &Value) -> Option<&Value> {
63        let seq = self.index.get(key)?;
64        self.order.get(seq).map(|(_, v)| v)
65    }
66
67    pub fn contains_key(&self, key: &Value) -> bool {
68        self.index.contains_key(key)
69    }
70
71    /// Return a new map with `key` → `value`.
72    ///
73    /// Re-associating a key that is already present keeps its original
74    /// insertion position; a brand-new key is appended at the end.
75    pub fn assoc(&self, key: Value, value: Value) -> Self {
76        if let Some(seq) = self.index.get(&key) {
77            let seq = *seq;
78            let n = self.count();
79            Self {
80                index: self.index.clone(),
81                order: self.order.insert(seq, (key, value)),
82                next_seq: self.next_seq,
83                charge: self.charge.derive(n, n, rb_path_bytes::<OrderEntry>(n)),
84            }
85        } else {
86            let seq = self.next_seq;
87            let n = self.count();
88            Self {
89                index: self.index.insert(key.clone(), seq),
90                order: self.order.insert(seq, (key, value)),
91                next_seq: seq + 1,
92                charge: self.charge.derive(n, n + 1, Self::path_bytes(n + 1)),
93            }
94        }
95    }
96
97    // Only used when neither hashing nor equality can realize user code.
98    pub(crate) fn assoc_mut(&mut self, key: Value, value: Value) {
99        if let Some(seq) = self.index.get(&key).copied() {
100            self.order.insert_mut(seq, (key, value));
101        } else {
102            let seq = self.next_seq;
103            self.index.insert_mut(key.clone(), seq);
104            self.order.insert_mut(seq, (key, value));
105            self.next_seq += 1;
106        }
107        self.charge = ShareCharge::full();
108    }
109
110    pub(crate) fn dissoc_mut(&mut self, key: &Value) {
111        if let Some(seq) = self.index.get(key).copied() {
112            self.index.remove_mut(key);
113            self.order.remove_mut(&seq);
114            self.charge = ShareCharge::full();
115        }
116    }
117
118    /// Return a new map with `key` removed.
119    pub fn dissoc(&self, key: &Value) -> Self {
120        match self.index.get(key) {
121            Some(seq) => {
122                let n = self.count();
123                Self {
124                    index: self.index.remove(key),
125                    order: self.order.remove(seq),
126                    next_seq: self.next_seq,
127                    charge: self.charge.derive(n, n - 1, Self::path_bytes(n)),
128                }
129            }
130            None => {
131                let n = self.count();
132                Self {
133                    index: self.index.clone(),
134                    order: self.order.clone(),
135                    next_seq: self.next_seq,
136                    charge: self.charge.derive(n, n, 0),
137                }
138            }
139        }
140    }
141
142    /// Bytes path-copied in both trees by inserting or removing one entry.
143    fn path_bytes(n: usize) -> usize {
144        hamt_path_bytes::<(Value, u64)>(n) + rb_path_bytes::<OrderEntry>(n)
145    }
146
147    /// Estimated bytes owned by this map if it shared no nodes.
148    pub(crate) fn full_size_extra(&self) -> usize {
149        // Per entry: index HashTrieMap entry (~40) + order RedBlackTree node
150        // (~48) + values stored inline in the order tree (not behind GcPtr).
151        self.count() * (88 + 2 * std::mem::size_of::<Value>())
152    }
153
154    /// What `gc_size_extra` would report, without marking this version
155    /// allocated.
156    #[cfg(test)]
157    fn charged(&self) -> usize {
158        self.charge.size_extra(self.full_size_extra())
159    }
160
161    /// Iterate over all `(key, value)` pairs in insertion order.
162    pub fn iter(&self) -> impl Iterator<Item = (&Value, &Value)> {
163        self.order.iter().map(|(_, (k, v))| (k, v))
164    }
165
166    /// Collect all keys, in insertion order.
167    pub fn keys(&self) -> Vec<Value> {
168        self.iter().map(|(k, _)| k.clone()).collect()
169    }
170
171    /// Collect all values, in insertion order.
172    pub fn vals(&self) -> Vec<Value> {
173        self.iter().map(|(_, v)| v.clone()).collect()
174    }
175
176    /// Merge two maps; right-hand side wins on key collision.
177    pub fn merge(&self, other: &Self) -> Self {
178        let mut result = self.clone();
179        for (k, v) in other.iter() {
180            result = result.assoc(k.clone(), v.clone());
181        }
182        result
183    }
184
185    /// Build from an iterator of `(key, value)` pairs, in the given order.
186    pub fn from_pairs<I: IntoIterator<Item = (Value, Value)>>(iter: I) -> Self {
187        let mut m = Self::empty();
188        for (k, v) in iter {
189            m = m.assoc(k, v);
190        }
191        m
192    }
193
194    /// Promote from a `PersistentArrayMap` when the threshold is exceeded.
195    pub fn from_array_map(am: &PersistentArrayMap) -> Self {
196        Self::from_pairs(am.iter().map(|(k, v)| (k.clone(), v.clone())))
197    }
198}
199
200impl PartialEq for PersistentHashMap {
201    fn eq(&self, other: &Self) -> bool {
202        if self.count() != other.count() {
203            return false;
204        }
205        self.iter().all(|(k, v)| other.get(k) == Some(v))
206    }
207}
208
209impl cljrs_gc::Trace for PersistentHashMap {
210    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
211        for (_, (k, v)) in self.order.iter() {
212            k.trace(visitor);
213            v.trace(visitor);
214        }
215    }
216
217    fn gc_size_extra(&self) -> usize {
218        self.charge.on_alloc(self.full_size_extra())
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225    use crate::Value;
226    use crate::collections::array_map::AssocResult;
227    use cljrs_gc::GcPtr;
228
229    fn kw(s: &str) -> Value {
230        Value::Keyword(GcPtr::new(crate::keyword::Keyword::simple(s)))
231    }
232    fn int(n: i64) -> Value {
233        Value::Long(n)
234    }
235
236    #[test]
237    fn test_basic_ops() {
238        let m = PersistentHashMap::empty();
239        let m = m.assoc(kw("a"), int(1));
240        let m = m.assoc(kw("b"), int(2));
241        assert_eq!(m.count(), 2);
242        assert_eq!(m.get(&kw("a")), Some(&int(1)));
243        assert_eq!(m.get(&kw("b")), Some(&int(2)));
244        assert_eq!(m.get(&kw("c")), None);
245    }
246
247    #[test]
248    fn test_update() {
249        let m = PersistentHashMap::empty()
250            .assoc(kw("a"), int(1))
251            .assoc(kw("a"), int(99));
252        assert_eq!(m.count(), 1);
253        assert_eq!(m.get(&kw("a")), Some(&int(99)));
254    }
255
256    #[test]
257    fn test_dissoc() {
258        let m = PersistentHashMap::empty()
259            .assoc(kw("a"), int(1))
260            .assoc(kw("b"), int(2));
261        let m2 = m.dissoc(&kw("a"));
262        assert_eq!(m2.count(), 1);
263        assert_eq!(m2.get(&kw("a")), None);
264        assert_eq!(m2.get(&kw("b")), Some(&int(2)));
265    }
266
267    #[test]
268    fn test_many_entries() {
269        let mut m = PersistentHashMap::empty();
270        for i in 0i64..200 {
271            m = m.assoc(int(i), int(i * 10));
272        }
273        assert_eq!(m.count(), 200);
274        for i in 0i64..200 {
275            assert_eq!(m.get(&int(i)), Some(&int(i * 10)));
276        }
277    }
278
279    #[test]
280    fn test_merge() {
281        let a = PersistentHashMap::empty()
282            .assoc(kw("a"), int(1))
283            .assoc(kw("b"), int(2));
284        let b = PersistentHashMap::empty()
285            .assoc(kw("b"), int(99))
286            .assoc(kw("c"), int(3));
287        let merged = a.merge(&b);
288        assert_eq!(merged.count(), 3);
289        assert_eq!(merged.get(&kw("a")), Some(&int(1)));
290        assert_eq!(merged.get(&kw("b")), Some(&int(99))); // right wins
291        assert_eq!(merged.get(&kw("c")), Some(&int(3)));
292    }
293
294    #[test]
295    fn test_equality() {
296        let a = PersistentHashMap::empty()
297            .assoc(kw("a"), int(1))
298            .assoc(kw("b"), int(2));
299        let b = PersistentHashMap::empty()
300            .assoc(kw("b"), int(2))
301            .assoc(kw("a"), int(1));
302        assert_eq!(a, b);
303    }
304
305    #[test]
306    fn test_from_array_map() {
307        let mut am = PersistentArrayMap::empty();
308        for i in 0..3i64 {
309            let AssocResult::Array(next) = am.assoc(int(i), int(i * 2)) else {
310                panic!()
311            };
312            am = next;
313        }
314        let hm = PersistentHashMap::from_array_map(&am);
315        assert_eq!(hm.count(), 3);
316        for i in 0..3i64 {
317            assert_eq!(hm.get(&int(i)), Some(&int(i * 2)));
318        }
319    }
320
321    #[test]
322    fn test_iteration_order_matches_insertion_order() {
323        let mut m = PersistentHashMap::empty();
324        let keys = ["j", "i", "h", "g", "f", "e", "d", "c", "b", "a", "z", "y"];
325        for k in keys {
326            m = m.assoc(kw(k), int(0));
327        }
328        let iterated: Vec<Value> = m.keys();
329        let expected: Vec<Value> = keys.iter().map(|k| kw(k)).collect();
330        assert_eq!(iterated, expected);
331    }
332
333    #[test]
334    fn test_reassoc_keeps_original_position() {
335        let m = PersistentHashMap::empty()
336            .assoc(kw("a"), int(1))
337            .assoc(kw("b"), int(2))
338            .assoc(kw("c"), int(3))
339            .assoc(kw("b"), int(99));
340        let keys = m.keys();
341        assert_eq!(keys, vec![kw("a"), kw("b"), kw("c")]);
342        assert_eq!(m.get(&kw("b")), Some(&int(99)));
343    }
344
345    #[test]
346    fn test_dissoc_preserves_remaining_order() {
347        let m = PersistentHashMap::empty()
348            .assoc(kw("a"), int(1))
349            .assoc(kw("b"), int(2))
350            .assoc(kw("c"), int(3))
351            .dissoc(&kw("b"));
352        assert_eq!(m.keys(), vec![kw("a"), kw("c")]);
353    }
354
355    #[test]
356    fn assoc_on_allocated_map_charges_path_bytes() {
357        let _alloc = crate::collections::share_charge::alloc_scope();
358        let m = GcPtr::new(PersistentHashMap::from_pairs(
359            (0..10_000).map(|i| (int(i), int(i))),
360        ));
361        let added = m.get().assoc(int(-1), int(0));
362        assert_eq!(added.charged(), PersistentHashMap::path_bytes(10_001));
363        let replaced = m.get().assoc(int(5), int(0));
364        assert_eq!(replaced.charged(), rb_path_bytes::<OrderEntry>(10_000));
365        let removed = m.get().dissoc(&int(5));
366        assert_eq!(removed.charged(), PersistentHashMap::path_bytes(10_000));
367        assert!(removed.charged() * 100 < m.get().full_size_extra());
368    }
369}