tiny_trie/dyn_trie.rs
1//! Dynamic NibbleTrie — starts with compact u8 arena indices and promotes
2//! to wider types (u16 → u32 → u64) as the trie grows.
3//!
4//! `DynTrie<T>` wraps an enum over concrete `NibbleTrie<Vec<u8>, T, PTR, u16>`
5//! variants. On insert, it checks whether the current PTR type is approaching
6//! capacity and promotes automatically. This gives small tries the memory
7//! efficiency of u8 indices (32-byte nodes) while supporting unbounded growth.
8//!
9//! # Dispatch
10//!
11//! All method calls dispatch through a `match` on the enum variant — no vtable,
12//! no `Box`, no heap allocation on promotion. The compiler can inline concrete
13//! `NibbleTrie` methods per variant.
14
15use crate::nibble_trie::NibbleTrie;
16
17// ---------------------------------------------------------------------------
18// DynInner enum — replaces Box<dyn DynTrie<T>>
19// ---------------------------------------------------------------------------
20
21/// Internal enum holding one of the four concrete `NibbleTrie` variants.
22/// Promotion replaces the variant in-place via `std::mem::replace`.
23enum DynInner<T> {
24 U8 (NibbleTrie<Vec<u8>, T, u8, u16>),
25 U16(NibbleTrie<Vec<u8>, T, u16, u16>),
26 U32(NibbleTrie<Vec<u8>, T, u32, u16>),
27 U64(NibbleTrie<Vec<u8>, T, u64, u16>),
28}
29
30// ---------------------------------------------------------------------------
31// DynTrie
32// ---------------------------------------------------------------------------
33
34/// A nibble trie that starts with compact u8 arena indices and automatically
35/// promotes to u16/u32/u64 as needed.
36///
37/// Node sizes by PTR width:
38/// - u8: 32 bytes (fits half a cache line)
39/// - u16: 48 bytes (fits a cache line)
40/// - u32: 80 bytes
41/// - u64: 152 bytes
42///
43/// Promotion is transparent: insert checks capacity and promotes before the
44/// underlying trie overflows. No vtable dispatch or heap allocation on
45/// promotion — just an enum variant swap.
46pub struct DynTrie<T> {
47 inner: DynInner<T>,
48}
49
50impl<T> DynTrie<T> {
51 /// Create an empty DynTrie starting with u8 arena indices (32-byte nodes).
52 pub fn new() -> Self {
53 DynTrie {
54 inner: DynInner::U8(NibbleTrie::new()),
55 }
56 }
57
58 /// Look up a key. Returns the value if found.
59 pub fn get(&self, key: &[u8]) -> Option<&T> {
60 match &self.inner {
61 DynInner::U8(t) => t.get(key),
62 DynInner::U16(t) => t.get(key),
63 DynInner::U32(t) => t.get(key),
64 DynInner::U64(t) => t.get(key),
65 }
66 }
67
68 /// Look up a key's value for mutation. Returns the value if found.
69 pub fn get_mut(&mut self, key: &[u8]) -> Option<&mut T> {
70 match &mut self.inner {
71 DynInner::U8(t) => t.get_mut(key),
72 DynInner::U16(t) => t.get_mut(key),
73 DynInner::U32(t) => t.get_mut(key),
74 DynInner::U64(t) => t.get_mut(key),
75 }
76 }
77
78 /// Insert a key-value pair. Automatically promotes to a wider PTR type if
79 /// the current one is approaching capacity. Returns the key index on
80 /// success, or `Err(())` on duplicate key.
81 pub fn insert(&mut self, key: Vec<u8>, value: T) -> Result<usize, ()> {
82 // Promote while at capacity (handles chained promotion, e.g. u8→u16→u32
83 // if the capacity threshold is overshot).
84 loop {
85 let near = match &self.inner {
86 DynInner::U8(t) => t.near_capacity(),
87 DynInner::U16(t) => t.near_capacity(),
88 DynInner::U32(t) => t.near_capacity(),
89 DynInner::U64(t) => t.near_capacity(),
90 };
91 if !near {
92 break;
93 }
94 self.promote_inner();
95 }
96 match &mut self.inner {
97 DynInner::U8(t) => t.insert(key, value),
98 DynInner::U16(t) => t.insert(key, value),
99 DynInner::U32(t) => t.insert(key, value),
100 DynInner::U64(t) => t.insert(key, value),
101 }
102 }
103
104 /// Number of entries in the trie.
105 pub fn len(&self) -> usize {
106 match &self.inner {
107 DynInner::U8(t) => t.len(),
108 DynInner::U16(t) => t.len(),
109 DynInner::U32(t) => t.len(),
110 DynInner::U64(t) => t.len(),
111 }
112 }
113
114 /// Returns `true` if the trie is empty.
115 pub fn is_empty(&self) -> bool {
116 match &self.inner {
117 DynInner::U8(t) => t.is_empty(),
118 DynInner::U16(t) => t.is_empty(),
119 DynInner::U32(t) => t.is_empty(),
120 DynInner::U64(t) => t.is_empty(),
121 }
122 }
123
124 /// Optimize the trie's memory layout for cache locality.
125 pub fn optimize(&mut self) {
126 match &mut self.inner {
127 DynInner::U8(t) => t.optimize(),
128 DynInner::U16(t) => t.optimize(),
129 DynInner::U32(t) => t.optimize(),
130 DynInner::U64(t) => t.optimize(),
131 }
132 }
133
134 /// Returns the size of the current PTR type in bytes (1, 2, 4, or 8).
135 /// Useful for debugging and testing promotion.
136 pub fn ptr_size(&self) -> usize {
137 match &self.inner {
138 DynInner::U8(_) => 1,
139 DynInner::U16(_) => 2,
140 DynInner::U32(_) => 4,
141 DynInner::U64(_) => 8,
142 }
143 }
144
145 /// Iterate all key-value pairs in forward (ascending) order.
146 pub fn iter_fwd(&self, f: &mut dyn FnMut(&[u8], &T)) {
147 match &self.inner {
148 DynInner::U8(t) => {
149 let mut it = t.iter();
150 if let Some((k, v)) = it.current() { f(k, v); }
151 while let Some((k, v)) = it.next() { f(k, v); }
152 }
153 DynInner::U16(t) => {
154 let mut it = t.iter();
155 if let Some((k, v)) = it.current() { f(k, v); }
156 while let Some((k, v)) = it.next() { f(k, v); }
157 }
158 DynInner::U32(t) => {
159 let mut it = t.iter();
160 if let Some((k, v)) = it.current() { f(k, v); }
161 while let Some((k, v)) = it.next() { f(k, v); }
162 }
163 DynInner::U64(t) => {
164 let mut it = t.iter();
165 if let Some((k, v)) = it.current() { f(k, v); }
166 while let Some((k, v)) = it.next() { f(k, v); }
167 }
168 }
169 }
170
171 /// Iterate all key-value pairs in reverse (descending) order.
172 pub fn iter_rev(&self, f: &mut dyn FnMut(&[u8], &T)) {
173 match &self.inner {
174 DynInner::U8(t) => {
175 let mut it = t.iter_last();
176 if let Some((k, v)) = it.current() { f(k, v); }
177 while let Some((k, v)) = it.prev() { f(k, v); }
178 }
179 DynInner::U16(t) => {
180 let mut it = t.iter_last();
181 if let Some((k, v)) = it.current() { f(k, v); }
182 while let Some((k, v)) = it.prev() { f(k, v); }
183 }
184 DynInner::U32(t) => {
185 let mut it = t.iter_last();
186 if let Some((k, v)) = it.current() { f(k, v); }
187 while let Some((k, v)) = it.prev() { f(k, v); }
188 }
189 DynInner::U64(t) => {
190 let mut it = t.iter_last();
191 if let Some((k, v)) = it.current() { f(k, v); }
192 while let Some((k, v)) = it.prev() { f(k, v); }
193 }
194 }
195 }
196
197 /// Manually demote to a narrower PTR type if the trie is small enough.
198 /// Returns `Ok(())` on success, `Err(())` if the trie is too large or
199 /// already at the minimum width (u8).
200 pub fn demote(&mut self) -> Result<(), ()> {
201 // Take ownership of inner via replace, then call consuming demote()
202 let inner = std::mem::replace(&mut self.inner, DynInner::U8(NibbleTrie::new()));
203 match inner {
204 DynInner::U8(t) => {
205 // u8 is the minimum — can't demote further
206 self.inner = DynInner::U8(t);
207 Err(())
208 }
209 DynInner::U16(t) => {
210 match t.demote::<u8>() {
211 Ok(smaller) => {
212 self.inner = DynInner::U8(smaller);
213 Ok(())
214 }
215 Err(original) => {
216 self.inner = DynInner::U16(original);
217 Err(())
218 }
219 }
220 }
221 DynInner::U32(t) => {
222 match t.demote::<u16>() {
223 Ok(smaller) => {
224 self.inner = DynInner::U16(smaller);
225 Ok(())
226 }
227 Err(original) => {
228 self.inner = DynInner::U32(original);
229 Err(())
230 }
231 }
232 }
233 DynInner::U64(t) => {
234 match t.demote::<u32>() {
235 Ok(smaller) => {
236 self.inner = DynInner::U32(smaller);
237 Ok(())
238 }
239 Err(original) => {
240 self.inner = DynInner::U64(original);
241 Err(())
242 }
243 }
244 }
245 }
246 }
247
248 /// Promote the inner trie to the next wider PTR type.
249 fn promote_inner(&mut self) {
250 // We need to take ownership of the inner trie, promote it, and replace.
251 // Use a placeholder that will be immediately overwritten.
252 let inner = std::mem::replace(&mut self.inner, DynInner::U8(NibbleTrie::new()));
253 self.inner = match inner {
254 DynInner::U8(t) => DynInner::U16(t.promote()),
255 DynInner::U16(t) => DynInner::U32(t.promote()),
256 DynInner::U32(t) => DynInner::U64(t.promote()),
257 DynInner::U64(t) => {
258 // u64 is the maximum — this should never happen
259 // (near_capacity returns false for u64 in practice)
260 DynInner::U64(t)
261 }
262 };
263 }
264}
265
266impl<T> Default for DynTrie<T> {
267 fn default() -> Self {
268 Self::new()
269 }
270}
271
272#[cfg(test)]
273#[path = "tests/dyn_trie.rs"]
274mod tests;
275