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    /// Return a new map with `key` removed.
98    pub fn dissoc(&self, key: &Value) -> Self {
99        match self.index.get(key) {
100            Some(seq) => {
101                let n = self.count();
102                Self {
103                    index: self.index.remove(key),
104                    order: self.order.remove(seq),
105                    next_seq: self.next_seq,
106                    charge: self.charge.derive(n, n - 1, Self::path_bytes(n)),
107                }
108            }
109            None => {
110                let n = self.count();
111                Self {
112                    index: self.index.clone(),
113                    order: self.order.clone(),
114                    next_seq: self.next_seq,
115                    charge: self.charge.derive(n, n, 0),
116                }
117            }
118        }
119    }
120
121    /// Bytes path-copied in both trees by inserting or removing one entry.
122    fn path_bytes(n: usize) -> usize {
123        hamt_path_bytes::<(Value, u64)>(n) + rb_path_bytes::<OrderEntry>(n)
124    }
125
126    /// Estimated bytes owned by this map if it shared no nodes.
127    pub(crate) fn full_size_extra(&self) -> usize {
128        // Per entry: index HashTrieMap entry (~40) + order RedBlackTree node
129        // (~48) + values stored inline in the order tree (not behind GcPtr).
130        self.count() * (88 + 2 * std::mem::size_of::<Value>())
131    }
132
133    /// What `gc_size_extra` would report, without marking this version
134    /// allocated.
135    #[cfg(test)]
136    fn charged(&self) -> usize {
137        self.charge.size_extra(self.full_size_extra())
138    }
139
140    /// Iterate over all `(key, value)` pairs in insertion order.
141    pub fn iter(&self) -> impl Iterator<Item = (&Value, &Value)> {
142        self.order.iter().map(|(_, (k, v))| (k, v))
143    }
144
145    /// Collect all keys, in insertion order.
146    pub fn keys(&self) -> Vec<Value> {
147        self.iter().map(|(k, _)| k.clone()).collect()
148    }
149
150    /// Collect all values, in insertion order.
151    pub fn vals(&self) -> Vec<Value> {
152        self.iter().map(|(_, v)| v.clone()).collect()
153    }
154
155    /// Merge two maps; right-hand side wins on key collision.
156    pub fn merge(&self, other: &Self) -> Self {
157        let mut result = self.clone();
158        for (k, v) in other.iter() {
159            result = result.assoc(k.clone(), v.clone());
160        }
161        result
162    }
163
164    /// Build from an iterator of `(key, value)` pairs, in the given order.
165    pub fn from_pairs<I: IntoIterator<Item = (Value, Value)>>(iter: I) -> Self {
166        let mut m = Self::empty();
167        for (k, v) in iter {
168            m = m.assoc(k, v);
169        }
170        m
171    }
172
173    /// Promote from a `PersistentArrayMap` when the threshold is exceeded.
174    pub fn from_array_map(am: &PersistentArrayMap) -> Self {
175        Self::from_pairs(am.iter().map(|(k, v)| (k.clone(), v.clone())))
176    }
177}
178
179impl PartialEq for PersistentHashMap {
180    fn eq(&self, other: &Self) -> bool {
181        if self.count() != other.count() {
182            return false;
183        }
184        self.iter().all(|(k, v)| other.get(k) == Some(v))
185    }
186}
187
188impl cljrs_gc::Trace for PersistentHashMap {
189    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
190        for (_, (k, v)) in self.order.iter() {
191            k.trace(visitor);
192            v.trace(visitor);
193        }
194    }
195
196    fn gc_size_extra(&self) -> usize {
197        self.charge.on_alloc(self.full_size_extra())
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204    use crate::Value;
205    use crate::collections::array_map::AssocResult;
206    use cljrs_gc::GcPtr;
207
208    fn kw(s: &str) -> Value {
209        Value::Keyword(GcPtr::new(crate::keyword::Keyword::simple(s)))
210    }
211    fn int(n: i64) -> Value {
212        Value::Long(n)
213    }
214
215    #[test]
216    fn test_basic_ops() {
217        let m = PersistentHashMap::empty();
218        let m = m.assoc(kw("a"), int(1));
219        let m = m.assoc(kw("b"), int(2));
220        assert_eq!(m.count(), 2);
221        assert_eq!(m.get(&kw("a")), Some(&int(1)));
222        assert_eq!(m.get(&kw("b")), Some(&int(2)));
223        assert_eq!(m.get(&kw("c")), None);
224    }
225
226    #[test]
227    fn test_update() {
228        let m = PersistentHashMap::empty()
229            .assoc(kw("a"), int(1))
230            .assoc(kw("a"), int(99));
231        assert_eq!(m.count(), 1);
232        assert_eq!(m.get(&kw("a")), Some(&int(99)));
233    }
234
235    #[test]
236    fn test_dissoc() {
237        let m = PersistentHashMap::empty()
238            .assoc(kw("a"), int(1))
239            .assoc(kw("b"), int(2));
240        let m2 = m.dissoc(&kw("a"));
241        assert_eq!(m2.count(), 1);
242        assert_eq!(m2.get(&kw("a")), None);
243        assert_eq!(m2.get(&kw("b")), Some(&int(2)));
244    }
245
246    #[test]
247    fn test_many_entries() {
248        let mut m = PersistentHashMap::empty();
249        for i in 0i64..200 {
250            m = m.assoc(int(i), int(i * 10));
251        }
252        assert_eq!(m.count(), 200);
253        for i in 0i64..200 {
254            assert_eq!(m.get(&int(i)), Some(&int(i * 10)));
255        }
256    }
257
258    #[test]
259    fn test_merge() {
260        let a = PersistentHashMap::empty()
261            .assoc(kw("a"), int(1))
262            .assoc(kw("b"), int(2));
263        let b = PersistentHashMap::empty()
264            .assoc(kw("b"), int(99))
265            .assoc(kw("c"), int(3));
266        let merged = a.merge(&b);
267        assert_eq!(merged.count(), 3);
268        assert_eq!(merged.get(&kw("a")), Some(&int(1)));
269        assert_eq!(merged.get(&kw("b")), Some(&int(99))); // right wins
270        assert_eq!(merged.get(&kw("c")), Some(&int(3)));
271    }
272
273    #[test]
274    fn test_equality() {
275        let a = PersistentHashMap::empty()
276            .assoc(kw("a"), int(1))
277            .assoc(kw("b"), int(2));
278        let b = PersistentHashMap::empty()
279            .assoc(kw("b"), int(2))
280            .assoc(kw("a"), int(1));
281        assert_eq!(a, b);
282    }
283
284    #[test]
285    fn test_from_array_map() {
286        let mut am = PersistentArrayMap::empty();
287        for i in 0..3i64 {
288            let AssocResult::Array(next) = am.assoc(int(i), int(i * 2)) else {
289                panic!()
290            };
291            am = next;
292        }
293        let hm = PersistentHashMap::from_array_map(&am);
294        assert_eq!(hm.count(), 3);
295        for i in 0..3i64 {
296            assert_eq!(hm.get(&int(i)), Some(&int(i * 2)));
297        }
298    }
299
300    #[test]
301    fn test_iteration_order_matches_insertion_order() {
302        let mut m = PersistentHashMap::empty();
303        let keys = ["j", "i", "h", "g", "f", "e", "d", "c", "b", "a", "z", "y"];
304        for k in keys {
305            m = m.assoc(kw(k), int(0));
306        }
307        let iterated: Vec<Value> = m.keys();
308        let expected: Vec<Value> = keys.iter().map(|k| kw(k)).collect();
309        assert_eq!(iterated, expected);
310    }
311
312    #[test]
313    fn test_reassoc_keeps_original_position() {
314        let m = PersistentHashMap::empty()
315            .assoc(kw("a"), int(1))
316            .assoc(kw("b"), int(2))
317            .assoc(kw("c"), int(3))
318            .assoc(kw("b"), int(99));
319        let keys = m.keys();
320        assert_eq!(keys, vec![kw("a"), kw("b"), kw("c")]);
321        assert_eq!(m.get(&kw("b")), Some(&int(99)));
322    }
323
324    #[test]
325    fn test_dissoc_preserves_remaining_order() {
326        let m = PersistentHashMap::empty()
327            .assoc(kw("a"), int(1))
328            .assoc(kw("b"), int(2))
329            .assoc(kw("c"), int(3))
330            .dissoc(&kw("b"));
331        assert_eq!(m.keys(), vec![kw("a"), kw("c")]);
332    }
333
334    #[test]
335    fn assoc_on_allocated_map_charges_path_bytes() {
336        let _alloc = crate::collections::share_charge::alloc_scope();
337        let m = GcPtr::new(PersistentHashMap::from_pairs(
338            (0..10_000).map(|i| (int(i), int(i))),
339        ));
340        let added = m.get().assoc(int(-1), int(0));
341        assert_eq!(added.charged(), PersistentHashMap::path_bytes(10_001));
342        let replaced = m.get().assoc(int(5), int(0));
343        assert_eq!(replaced.charged(), rb_path_bytes::<OrderEntry>(10_000));
344        let removed = m.get().dissoc(&int(5));
345        assert_eq!(removed.charged(), PersistentHashMap::path_bytes(10_000));
346        assert!(removed.charged() * 100 < m.get().full_size_extra());
347    }
348}