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