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
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use crate::Value;
212
213    fn kw(s: &str) -> Value {
214        Value::Keyword(cljrs_gc::GcPtr::new(crate::keyword::Keyword::simple(s)))
215    }
216
217    fn int(n: i64) -> Value {
218        Value::Long(n)
219    }
220
221    #[test]
222    fn test_empty() {
223        let m = PersistentArrayMap::empty();
224        assert!(m.is_empty());
225        assert_eq!(m.count(), 0);
226    }
227
228    #[test]
229    fn test_assoc_and_get() {
230        let m = PersistentArrayMap::empty();
231        let AssocResult::Array(m) = m.assoc(kw("a"), int(1)) else {
232            panic!()
233        };
234        let AssocResult::Array(m) = m.assoc(kw("b"), int(2)) else {
235            panic!()
236        };
237        assert_eq!(m.get(&kw("a")), Some(&int(1)));
238        assert_eq!(m.get(&kw("b")), Some(&int(2)));
239        assert_eq!(m.get(&kw("c")), None);
240    }
241
242    #[test]
243    fn test_update() {
244        let m = PersistentArrayMap::empty();
245        let AssocResult::Array(m) = m.assoc(kw("a"), int(1)) else {
246            panic!()
247        };
248        let AssocResult::Array(m) = m.assoc(kw("a"), int(99)) else {
249            panic!()
250        };
251        assert_eq!(m.get(&kw("a")), Some(&int(99)));
252        assert_eq!(m.count(), 1);
253    }
254
255    #[test]
256    fn test_dissoc() {
257        let m = PersistentArrayMap::empty();
258        let AssocResult::Array(m) = m.assoc(kw("a"), int(1)) else {
259            panic!()
260        };
261        let AssocResult::Array(m) = m.assoc(kw("b"), int(2)) else {
262            panic!()
263        };
264        let m2 = m.dissoc(&kw("a"));
265        assert_eq!(m2.get(&kw("a")), None);
266        assert_eq!(m2.get(&kw("b")), Some(&int(2)));
267        assert_eq!(m2.count(), 1);
268    }
269
270    #[test]
271    fn test_promotes_at_threshold() {
272        let mut m = PersistentArrayMap::empty();
273        for i in 0..THRESHOLD as i64 {
274            let AssocResult::Array(next) = m.assoc(int(i), int(i * 10)) else {
275                panic!()
276            };
277            m = next;
278        }
279        // Adding one more should trigger promotion.
280        let result = m.assoc(int(THRESHOLD as i64), int(0));
281        assert!(matches!(result, AssocResult::Promote(_)));
282    }
283
284    #[test]
285    fn test_from_pairs_preserves_all_entries_past_threshold() {
286        // Regression: from_pairs used to drop entries after promotion.
287        let pairs: Vec<(Value, Value)> = (0..15i64).map(|i| (int(i), int(i * 10))).collect();
288        let result = PersistentArrayMap::from_pairs(pairs);
289        match result {
290            AssocResult::Promote(pairs) => {
291                assert_eq!(pairs.len(), 15, "all 15 entries must survive promotion");
292                for i in 0..15i64 {
293                    let found = pairs.iter().find(|(k, _)| *k == int(i));
294                    assert!(found.is_some(), "key {i} missing after promotion");
295                    assert_eq!(found.unwrap().1, int(i * 10));
296                }
297            }
298            AssocResult::Array(_) => panic!("expected promotion for 15 entries"),
299        }
300    }
301
302    #[test]
303    fn test_from_pairs_duplicate_keys_after_promotion() {
304        // 10 unique entries triggers promotion; include a duplicate to test last-wins.
305        let mut pairs: Vec<(Value, Value)> = (0..10i64).map(|i| (int(i), int(i))).collect();
306        pairs.push((int(0), int(999))); // duplicate key 0
307        let result = PersistentArrayMap::from_pairs(pairs);
308        match result {
309            AssocResult::Promote(pairs) => {
310                assert_eq!(pairs.len(), 10, "duplicate should not add extra entry");
311                let val = pairs.iter().find(|(k, _)| *k == int(0)).unwrap();
312                assert_eq!(val.1, int(999), "last value should win for duplicate key");
313            }
314            AssocResult::Array(_) => panic!("expected promotion"),
315        }
316    }
317
318    #[test]
319    fn test_equality() {
320        let m1 = PersistentArrayMap::empty();
321        let AssocResult::Array(m1) = m1.assoc(kw("a"), int(1)) else {
322            panic!()
323        };
324        let m2 = PersistentArrayMap::empty();
325        let AssocResult::Array(m2) = m2.assoc(kw("a"), int(1)) else {
326            panic!()
327        };
328        assert_eq!(m1, m2);
329    }
330}