Skip to main content

cljrs_value/collections/
array_map.rs

1use std::sync::Arc;
2
3use crate::Value;
4
5/// Maximum number of key-value pairs before promoting to `PersistentHashMap`.
6pub const THRESHOLD: usize = 8;
7
8/// A small immutable map stored as a flat key/value vector.
9///
10/// Linear-scan lookup is fast for small maps (≤8 entries) and avoids the
11/// overhead of hashing.  Once the map exceeds `THRESHOLD` entries an `assoc`
12/// returns a `PersistentHashMap` instead.
13#[derive(Debug, Clone)]
14pub struct PersistentArrayMap {
15    /// Flat: `[k0, v0, k1, v1, …]`.  Always even-length; at most 16 elements.
16    entries: Arc<Vec<Value>>,
17}
18
19/// Result of `assoc` — stays an array-map or promotes to a hash-map.
20pub enum AssocResult {
21    Array(PersistentArrayMap),
22    /// The caller must construct the hash-map; we return the raw entries to
23    /// avoid a circular dependency (HashMapincludes ArrayMap but not vice-versa).
24    Promote(Vec<(Value, Value)>),
25}
26
27impl PersistentArrayMap {
28    /// The canonical empty array-map.
29    pub fn empty() -> Self {
30        Self {
31            entries: Arc::new(Vec::new()),
32        }
33    }
34
35    pub fn count(&self) -> usize {
36        self.entries.len() / 2
37    }
38
39    pub fn is_empty(&self) -> bool {
40        self.entries.is_empty()
41    }
42
43    /// Look up `key` using Clojure value equality.  O(n).
44    pub fn get(&self, key: &Value) -> Option<&Value> {
45        let e = &self.entries;
46        let mut i = 0;
47        while i < e.len() {
48            if &e[i] == key {
49                return Some(&e[i + 1]);
50            }
51            i += 2;
52        }
53        None
54    }
55
56    pub fn contains_key(&self, key: &Value) -> bool {
57        self.get(key).is_some()
58    }
59
60    /// Return a new map with `key` associated to `value`.
61    /// Promotes to `Promote` when the count would exceed `THRESHOLD`.
62    pub fn assoc(&self, key: Value, value: Value) -> AssocResult {
63        // Check for existing key to replace.
64        let mut new_entries = (*self.entries).clone();
65        let mut i = 0;
66        while i < new_entries.len() {
67            if new_entries[i] == key {
68                new_entries[i + 1] = value;
69                return AssocResult::Array(Self {
70                    entries: Arc::new(new_entries),
71                });
72            }
73            i += 2;
74        }
75
76        // New key.
77        new_entries.push(key);
78        new_entries.push(value);
79
80        if new_entries.len() / 2 > THRESHOLD {
81            // Promote: collect pairs and let the caller build a hash-map.
82            let mut pairs = Vec::with_capacity(new_entries.len() / 2);
83            let mut j = 0;
84            while j < new_entries.len() {
85                pairs.push((new_entries[j].clone(), new_entries[j + 1].clone()));
86                j += 2;
87            }
88            AssocResult::Promote(pairs)
89        } else {
90            AssocResult::Array(Self {
91                entries: Arc::new(new_entries),
92            })
93        }
94    }
95
96    /// Return a new map with `key` removed.  O(n).
97    pub fn dissoc(&self, key: &Value) -> Self {
98        let mut new_entries = (*self.entries).clone();
99        let mut i = 0;
100        while i < new_entries.len() {
101            if &new_entries[i] == key {
102                new_entries.remove(i); // value
103                new_entries.remove(i); // key
104                return Self {
105                    entries: Arc::new(new_entries),
106                };
107            }
108            i += 2;
109        }
110        // Key not present → return a clone (new Arc, same data).
111        self.clone()
112    }
113
114    /// Iterate over `(key, value)` pairs.
115    pub fn iter(&self) -> ArrayMapIter<'_> {
116        ArrayMapIter {
117            entries: &self.entries,
118            pos: 0,
119        }
120    }
121
122    /// Build directly from a pre-evaluated flat entries vector `[k0, v0, k1, v1, ...]`.
123    ///
124    /// This avoids intermediate allocations when the entries are already known.
125    /// Caller must ensure even length. Does NOT check for duplicate keys —
126    /// if duplicates are possible, use `from_pairs` instead.
127    pub fn from_flat_entries(entries: Vec<Value>) -> AssocResult {
128        debug_assert!(entries.len().is_multiple_of(2));
129        if entries.len() / 2 > THRESHOLD {
130            let mut pairs = Vec::with_capacity(entries.len() / 2);
131            let mut i = 0;
132            while i < entries.len() {
133                pairs.push((entries[i].clone(), entries[i + 1].clone()));
134                i += 2;
135            }
136            AssocResult::Promote(pairs)
137        } else {
138            AssocResult::Array(Self {
139                entries: Arc::new(entries),
140            })
141        }
142    }
143
144    /// Build from an iterator of `(key, value)` pairs.
145    /// Pairs are inserted left-to-right; later values win on duplicate keys.
146    pub fn from_pairs<I: IntoIterator<Item = (Value, Value)>>(iter: I) -> AssocResult {
147        let mut map = Self::empty();
148        let mut iter = iter.into_iter();
149        for (k, v) in iter.by_ref() {
150            match map.assoc(k, v) {
151                AssocResult::Array(m) => map = m,
152                AssocResult::Promote(pairs) => {
153                    // Continue inserting remaining pairs into the promoted map.
154                    let mut promoted = pairs;
155                    for (k2, v2) in iter {
156                        // Check if key already exists (last wins).
157                        if let Some(pos) = promoted.iter().position(|(pk, _)| *pk == k2) {
158                            promoted[pos].1 = v2;
159                        } else {
160                            promoted.push((k2, v2));
161                        }
162                    }
163                    return AssocResult::Promote(promoted);
164                }
165            }
166        }
167        AssocResult::Array(map)
168    }
169}
170
171pub struct ArrayMapIter<'a> {
172    entries: &'a Vec<Value>,
173    pos: usize,
174}
175
176impl<'a> Iterator for ArrayMapIter<'a> {
177    type Item = (&'a Value, &'a Value);
178
179    fn next(&mut self) -> Option<Self::Item> {
180        if self.pos >= self.entries.len() {
181            return None;
182        }
183        let k = &self.entries[self.pos];
184        let v = &self.entries[self.pos + 1];
185        self.pos += 2;
186        Some((k, v))
187    }
188}
189
190impl PartialEq for PersistentArrayMap {
191    fn eq(&self, other: &Self) -> bool {
192        if self.count() != other.count() {
193            return false;
194        }
195        // Every key in self must map to the same value in other.
196        self.iter().all(|(k, v)| other.get(k) == Some(v))
197    }
198}
199
200impl cljrs_gc::Trace for PersistentArrayMap {
201    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
202        for v in self.entries.iter() {
203            v.trace(visitor);
204        }
205    }
206
207    fn gc_size_extra(&self) -> usize {
208        // Arc<Vec<Value>>: 16 bytes for Arc ref-counts + Vec buffer.
209        16 + self.entries.capacity() * std::mem::size_of::<Value>()
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use crate::Value;
217
218    fn kw(s: &str) -> Value {
219        Value::Keyword(cljrs_gc::GcPtr::new(crate::keyword::Keyword::simple(s)))
220    }
221
222    fn int(n: i64) -> Value {
223        Value::Long(n)
224    }
225
226    #[test]
227    fn test_empty() {
228        let m = PersistentArrayMap::empty();
229        assert!(m.is_empty());
230        assert_eq!(m.count(), 0);
231    }
232
233    #[test]
234    fn test_assoc_and_get() {
235        let m = PersistentArrayMap::empty();
236        let AssocResult::Array(m) = m.assoc(kw("a"), int(1)) else {
237            panic!()
238        };
239        let AssocResult::Array(m) = m.assoc(kw("b"), int(2)) else {
240            panic!()
241        };
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 = PersistentArrayMap::empty();
250        let AssocResult::Array(m) = m.assoc(kw("a"), int(1)) else {
251            panic!()
252        };
253        let AssocResult::Array(m) = m.assoc(kw("a"), int(99)) else {
254            panic!()
255        };
256        assert_eq!(m.get(&kw("a")), Some(&int(99)));
257        assert_eq!(m.count(), 1);
258    }
259
260    #[test]
261    fn test_dissoc() {
262        let m = PersistentArrayMap::empty();
263        let AssocResult::Array(m) = m.assoc(kw("a"), int(1)) else {
264            panic!()
265        };
266        let AssocResult::Array(m) = m.assoc(kw("b"), int(2)) else {
267            panic!()
268        };
269        let m2 = m.dissoc(&kw("a"));
270        assert_eq!(m2.get(&kw("a")), None);
271        assert_eq!(m2.get(&kw("b")), Some(&int(2)));
272        assert_eq!(m2.count(), 1);
273    }
274
275    #[test]
276    fn test_promotes_at_threshold() {
277        let mut m = PersistentArrayMap::empty();
278        for i in 0..THRESHOLD as i64 {
279            let AssocResult::Array(next) = m.assoc(int(i), int(i * 10)) else {
280                panic!()
281            };
282            m = next;
283        }
284        // Adding one more should trigger promotion.
285        let result = m.assoc(int(THRESHOLD as i64), int(0));
286        assert!(matches!(result, AssocResult::Promote(_)));
287    }
288
289    #[test]
290    fn test_from_pairs_preserves_all_entries_past_threshold() {
291        // Regression: from_pairs used to drop entries after promotion.
292        let pairs: Vec<(Value, Value)> = (0..15i64).map(|i| (int(i), int(i * 10))).collect();
293        let result = PersistentArrayMap::from_pairs(pairs);
294        match result {
295            AssocResult::Promote(pairs) => {
296                assert_eq!(pairs.len(), 15, "all 15 entries must survive promotion");
297                for i in 0..15i64 {
298                    let found = pairs.iter().find(|(k, _)| *k == int(i));
299                    assert!(found.is_some(), "key {i} missing after promotion");
300                    assert_eq!(found.unwrap().1, int(i * 10));
301                }
302            }
303            AssocResult::Array(_) => panic!("expected promotion for 15 entries"),
304        }
305    }
306
307    #[test]
308    fn test_from_pairs_duplicate_keys_after_promotion() {
309        // 10 unique entries triggers promotion; include a duplicate to test last-wins.
310        let mut pairs: Vec<(Value, Value)> = (0..10i64).map(|i| (int(i), int(i))).collect();
311        pairs.push((int(0), int(999))); // duplicate key 0
312        let result = PersistentArrayMap::from_pairs(pairs);
313        match result {
314            AssocResult::Promote(pairs) => {
315                assert_eq!(pairs.len(), 10, "duplicate should not add extra entry");
316                let val = pairs.iter().find(|(k, _)| *k == int(0)).unwrap();
317                assert_eq!(val.1, int(999), "last value should win for duplicate key");
318            }
319            AssocResult::Array(_) => panic!("expected promotion"),
320        }
321    }
322
323    #[test]
324    fn test_equality() {
325        let m1 = PersistentArrayMap::empty();
326        let AssocResult::Array(m1) = m1.assoc(kw("a"), int(1)) else {
327            panic!()
328        };
329        let m2 = PersistentArrayMap::empty();
330        let AssocResult::Array(m2) = m2.assoc(kw("a"), int(1)) else {
331            panic!()
332        };
333        assert_eq!(m1, m2);
334    }
335}