Skip to main content

uqa_core/memory/map/
shared.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Immutable ordered roots share entries and unchanged subtrees under one allowance.
8
9#[cfg(test)]
10mod tests;
11mod tree;
12
13use std::{borrow::Borrow, cmp::Ordering, ops::Bound, sync::Arc};
14
15use super::{Budgeted, MemoryBudget, MemoryError, MAX_HEIGHT};
16
17type Entry<K, V> = Arc<Budgeted<(K, V)>>;
18type SharedNode<K, V> = Arc<Budgeted<Node<K, V>>>;
19type Link<K, V> = Option<SharedNode<K, V>>;
20
21struct Node<K, V> {
22    entry: Entry<K, V>,
23    left: Link<K, V>,
24    right: Link<K, V>,
25    height: u8,
26}
27
28/// A persistent ordered map with logarithmic lookup and insertion. Cloning shares a root without copying entries or allocating. Updates reserve the complete insertion before mutation, reuse unique nodes and copy shared paths; unchanged entries and subtrees keep their original reservations until their last root is dropped. Keys and values retain their separately owned payloads.
29pub struct BudgetedSharedMap<K, V> {
30    root: Link<K, V>,
31    len: usize,
32    memory: MemoryBudget,
33}
34
35/// An immutable ordered root and its cardinality. Its nodes retain their original allocation leases; an empty root owns no allocations. Cloning does not copy entries or allocate, and restoring into a mutable map requires its original allowance for a nonempty root.
36pub struct BudgetedSharedMapSnapshot<K, V> {
37    root: Link<K, V>,
38    len: usize,
39}
40
41impl<K, V> Clone for BudgetedSharedMapSnapshot<K, V> {
42    fn clone(&self) -> Self {
43        Self {
44            root: self.root.clone(),
45            len: self.len,
46        }
47    }
48}
49
50impl<K, V> BudgetedSharedMapSnapshot<K, V> {
51    pub fn len(&self) -> usize {
52        self.len
53    }
54
55    pub fn is_empty(&self) -> bool {
56        self.len == 0
57    }
58}
59
60impl<K: Ord, V> BudgetedSharedMapSnapshot<K, V> {
61    pub fn get<Q: Ord + ?Sized>(&self, key: &Q) -> Option<&V>
62    where
63        K: Borrow<Q>,
64    {
65        lookup(&self.root, key)
66    }
67}
68
69impl<K, V> Clone for BudgetedSharedMap<K, V> {
70    fn clone(&self) -> Self {
71        Self {
72            root: self.root.clone(),
73            len: self.len,
74            memory: self.memory.clone(),
75        }
76    }
77}
78
79impl<K, V> BudgetedSharedMap<K, V> {
80    pub fn new(memory: &MemoryBudget) -> Self {
81        Self {
82            root: None,
83            len: 0,
84            memory: memory.clone(),
85        }
86    }
87
88    pub fn len(&self) -> usize {
89        self.len
90    }
91
92    pub fn is_empty(&self) -> bool {
93        self.len == 0
94    }
95
96    pub fn budget(&self) -> &MemoryBudget {
97        &self.memory
98    }
99
100    /// Retain this immutable root without another mutable allowance handle or any allocation.
101    pub fn snapshot(&self) -> BudgetedSharedMapSnapshot<K, V> {
102        BudgetedSharedMapSnapshot {
103            root: self.root.clone(),
104            len: self.len,
105        }
106    }
107
108    /// Restore a retained root without allocating. Panics before mutation if a nonempty root belongs to another allowance; empty roots own no allocations and can be restored into any allowance.
109    pub fn restore(&mut self, snapshot: &BudgetedSharedMapSnapshot<K, V>) {
110        if let Some(root) = &snapshot.root {
111            assert!(
112                self.memory.shares_allowance(root.budget()),
113                "different memory allowances"
114            );
115        }
116        self.root.clone_from(&snapshot.root);
117        self.len = snapshot.len;
118    }
119
120    pub fn iter(&self) -> BudgetedSharedMapIter<'_, K, V> {
121        let mut iter = BudgetedSharedMapIter::empty();
122        iter.push_left(self.root.as_ref().map(|node| &***node));
123        iter
124    }
125}
126
127impl<K: Ord, V> BudgetedSharedMap<K, V> {
128    /// Select the greatest entry at an upper bound in logarithmic time, without allocating or traversing preceding entries.
129    pub fn last_before<Q: Ord + ?Sized>(&self, end: Bound<&Q>) -> Option<(&K, &V)>
130    where
131        K: Borrow<Q>,
132    {
133        let mut link = &self.root;
134        let mut found = None;
135        while let Some(node) = link {
136            let included = match end {
137                Bound::Unbounded => true,
138                Bound::Included(key) => node.entry.0.borrow() <= key,
139                Bound::Excluded(key) => node.entry.0.borrow() < key,
140            };
141            if included {
142                found = Some((&node.entry.0, &node.entry.1));
143                link = &node.right;
144            } else {
145                link = &node.left;
146            }
147        }
148        found
149    }
150
151    pub fn get<Q: Ord + ?Sized>(&self, key: &Q) -> Option<&V>
152    where
153        K: Borrow<Q>,
154    {
155        lookup(&self.root, key)
156    }
157
158    /// Return a new root containing the supplied key and value, leaving this root unchanged even if reservation fails. Matching keys are replaced together with their values; neither type needs to implement `Clone`.
159    pub fn with_insert(&self, key: K, value: V) -> Result<Self, MemoryError> {
160        let mut candidate = self.clone();
161        candidate.try_insert(key, value)?;
162        Ok(candidate)
163    }
164
165    /// Reserve the complete insertion before changing this root, reusing unique nodes and copying only shared paths. Reservation failure preserves this root; existing clones remain unchanged. Matching keys and values are replaced together without requiring `Clone`.
166    pub fn try_insert(&mut self, key: K, value: V) -> Result<(), MemoryError> {
167        let added = tree::insert(&mut self.root, key, value, &self.memory)?;
168        self.len += usize::from(added);
169        Ok(())
170    }
171
172    /// Visit keys in order, seeking the lower bound without traversing earlier entries or allocating a traversal buffer.
173    pub fn range_from<Q: Ord + ?Sized>(&self, start: Bound<&Q>) -> BudgetedSharedMapIter<'_, K, V>
174    where
175        K: Borrow<Q>,
176    {
177        let mut iter = BudgetedSharedMapIter::empty();
178        let mut link = &self.root;
179        while let Some(node) = link {
180            let included = match start {
181                Bound::Unbounded => true,
182                Bound::Included(key) => node.entry.0.borrow() >= key,
183                Bound::Excluded(key) => node.entry.0.borrow() > key,
184            };
185            if included {
186                iter.stack[iter.depth] = Some(node);
187                iter.depth += 1;
188                link = &node.left;
189            } else {
190                link = &node.right;
191            }
192        }
193        iter
194    }
195}
196
197fn lookup<'a, K: Borrow<Q>, V, Q: Ord + ?Sized>(
198    mut link: &'a Link<K, V>,
199    key: &Q,
200) -> Option<&'a V> {
201    while let Some(node) = link {
202        match key.cmp(node.entry.0.borrow()) {
203            Ordering::Less => link = &node.left,
204            Ordering::Greater => link = &node.right,
205            Ordering::Equal => return Some(&node.entry.1),
206        }
207    }
208    None
209}
210
211pub struct BudgetedSharedMapIter<'a, K, V> {
212    stack: [Option<&'a Node<K, V>>; MAX_HEIGHT],
213    depth: usize,
214}
215
216impl<'a, K, V> BudgetedSharedMapIter<'a, K, V> {
217    fn empty() -> Self {
218        Self {
219            stack: [None; MAX_HEIGHT],
220            depth: 0,
221        }
222    }
223
224    fn push_left(&mut self, mut node: Option<&'a Node<K, V>>) {
225        while let Some(current) = node {
226            self.stack[self.depth] = Some(current);
227            self.depth += 1;
228            node = current.left.as_ref().map(|node| &***node);
229        }
230    }
231}
232
233impl<'a, K, V> Iterator for BudgetedSharedMapIter<'a, K, V> {
234    type Item = (&'a K, &'a V);
235
236    fn next(&mut self) -> Option<Self::Item> {
237        self.depth = self.depth.checked_sub(1)?;
238        let node = self.stack[self.depth].take().expect("retained map node");
239        self.push_left(node.right.as_ref().map(|node| &***node));
240        Some((&node.entry.0, &node.entry.1))
241    }
242}
243
244impl<K, V> std::iter::FusedIterator for BudgetedSharedMapIter<'_, K, V> {}
245
246impl<'a, K, V> IntoIterator for &'a BudgetedSharedMap<K, V> {
247    type Item = (&'a K, &'a V);
248    type IntoIter = BudgetedSharedMapIter<'a, K, V>;
249
250    fn into_iter(self) -> Self::IntoIter {
251        self.iter()
252    }
253}