beck_core/pmap.rs
1//! A persistent ordered map — the language's `Map[K, V]`.
2//!
3//! # Why this exists
4//!
5//! [`docs/19-phase-1-report.md`](../../../../../docs/19-phase-1-report.md) §19.4 item 3: the fold was
6//! `O(events × rows)` because `map_insert` cloned the whole accumulator. `Arc<BTreeMap>` makes
7//! *cloning the handle* cheap and *updating* expensive, which is exactly backwards for a language
8//! whose central construct is `state = fold(f, init, events)`.
9//!
10//! The Phase 1 report proposed uniqueness analysis — let the fold mutate in place when the previous
11//! state is dead. That is worth having eventually, but it is the wrong *first* answer: it makes an
12//! asymptotic guarantee depend on an optimisation firing. A persistent map gives `O(log n)` updates
13//! unconditionally, with or without the analysis, on every backend. Uniqueness analysis then turns
14//! `O(log n)` into `O(1)` amortised for the common case, which is a real but secondary win.
15//!
16//! # Why it is written here rather than depended on
17//!
18//! `im` and `rpds` are both MPL-2.0, which `deny.toml` does not allow. More to the point, a
19//! persistent map is not a third-party concern for a functional language — it is `Map[K, V]`, a
20//! type in the surface language, and its performance characteristics are part of the semantics.
21//! `docs/01-vision-and-premise.md` §1.5's "we do not write a storage engine" is about substrates,
22//! not about the standard library's own data structures.
23//!
24//! # Why *this* structure
25//!
26//! Three requirements fix the answer, and it is worth writing down which:
27//!
28//! 1. **Persistent.** `state = fold(f, init, events)` keeps old states reachable — snapshots hold
29//! them, `replay_to` rebuilds them, the differential harness diffs against them. So updating
30//! cannot mean mutating.
31//! 2. **Ordered by key.** Iteration order reaches the rendered page and the state digest, and
32//! §4.8's replay harness compares both bit for bit.
33//! 3. **Keys are arbitrary [`Value`](crate::Value)s** — not integers, so no Patricia trie; ordered
34//! by comparison, so no hash table.
35//!
36//! (2) and (3) together mean a comparison-based ordered dictionary, whose worst case is
37//! `Ω(log n)` per operation on information-theoretic grounds. `O(log n)` is therefore optimal and
38//! the only question left is *which* balanced search tree. Three are plausible:
39//!
40//! | scheme | height | `len` | used by |
41//! |-----------------|------------|-------|--------------------------------|
42//! | AVL | ≤1.44 lg n | `O(n)`| OCaml `Map` |
43//! | red-black | ≤2 lg n | `O(n)`| Scala, Java `TreeMap` |
44//! | weight-balanced | ≤2.4 lg n | `O(1)`| Haskell `Data.Map`, SML/NJ |
45//!
46//! Weight-balanced wins here for a language-specific reason: `map_len` is a *prim*, so a program
47//! may call it inside a view that already runs once per event. Every node carries its subtree size,
48//! so `len` is a field read rather than a traversal. The same sizes give `O(log n)` rank/select if
49//! indexing is ever added, and admit the join-based `union`/`intersection`/`difference` of Blelloch,
50//! Ferizovic and Sun (2016) at the optimal `O(m log(n/m + 1))` if those become prims.
51//!
52//! A HAMT (Bagwell 2001; CHAMP, Steindorfer and Vinju 2015) would be a constant factor faster —
53//! depth ≤7 rather than ~2.4 lg n — but iterates in *hash* order, which violates (2); recovering
54//! key order would cost a sort on every render, and the digest would then depend on a hash function
55//! staying stable across compiler versions. It buys no asymptotic improvement, so it loses.
56//!
57//! # Cost
58//!
59//! | operation | time | *fresh* nodes |
60//! |------------------------------|------------|---------------|
61//! | `get`, `contains_key` | `O(log n)` | 0 |
62//! | `insert`, `remove` | `O(log n)` | `O(log n)` |
63//! | `len`, `is_empty`, `clone` | `O(1)` | 0 |
64//! | `iter`, `keys`, `values` | `O(n)` | 0 |
65//!
66//! So a fold of `E` events over a map reaching `n` entries costs `O(E log n)` time, against the
67//! `O(E · n)` — quadratic when every event adds a row — that copying cost. Live space is `O(n)`
68//! nodes plus `O(log n)` per retained version: the path a superseded version rebuilt is freed by
69//! its `Arc` the moment the old state is dropped.
70//!
71//! The price of sharing is per-entry overhead: a node is key + value + `usize` + two `Option<Arc>`
72//! plus the `Arc` header, roughly 3–5× a `BTreeMap` entry, which packs ~11 entries to a cache line
73//! group. That is the trade — and it is repaid immediately, because the old code allocated a whole
74//! copy of the map on *every* event.
75//!
76//! The remaining constant-factor win is uniqueness: when the previous state is dead, the path could
77//! be updated in place (`Arc::get_mut` — Clojure's transients) for `O(1)` allocation. That is a
78//! real improvement and it is *not* implemented here, because an asymptotic guarantee should not
79//! depend on an optimisation firing.
80
81use std::cmp::Ordering;
82use std::sync::Arc;
83
84/// The rebalancing constants. `DELTA` bounds how lopsided a node may be; `RATIO` decides single
85/// versus double rotation.
86///
87/// ⟨3, 2⟩ is not a free choice. Adams' "Efficient sets: a balancing act" (1993) published ⟨4, 2⟩,
88/// and Hirai and Yamamoto ("Balancing weight-balanced trees", JFP 21(3), 2011) later proved by
89/// exhaustive machine-checked search that ⟨4, 2⟩ does **not** preserve the invariant under delete —
90/// a real bug that shipped in Haskell's `containers`. ⟨3, 2⟩ is one of the pairs they proved valid
91/// for both insert and delete, which is why it is the pair here.
92const DELTA: usize = 3;
93const RATIO: usize = 2;
94
95#[derive(Debug)]
96struct Node<K, V> {
97 key: K,
98 value: V,
99 size: usize,
100 left: Option<Arc<Node<K, V>>>,
101 right: Option<Arc<Node<K, V>>>,
102}
103
104type Link<K, V> = Option<Arc<Node<K, V>>>;
105
106fn size<K, V>(n: &Link<K, V>) -> usize {
107 n.as_ref().map_or(0, |n| n.size)
108}
109
110/// A persistent ordered map. Cloning is `O(1)` and shares everything.
111#[derive(Debug)]
112pub struct PMap<K, V> {
113 root: Link<K, V>,
114}
115
116impl<K, V> Clone for PMap<K, V> {
117 fn clone(&self) -> Self {
118 PMap {
119 root: self.root.clone(),
120 }
121 }
122}
123
124impl<K, V> Default for PMap<K, V> {
125 fn default() -> Self {
126 PMap { root: None }
127 }
128}
129
130impl<K: Ord + Clone, V: Clone> PMap<K, V> {
131 pub fn new() -> PMap<K, V> {
132 PMap::default()
133 }
134
135 pub fn len(&self) -> usize {
136 size(&self.root)
137 }
138
139 pub fn is_empty(&self) -> bool {
140 self.root.is_none()
141 }
142
143 pub fn get(&self, key: &K) -> Option<&V> {
144 let mut cur = self.root.as_ref();
145 while let Some(n) = cur {
146 cur = match key.cmp(&n.key) {
147 Ordering::Less => n.left.as_ref(),
148 Ordering::Greater => n.right.as_ref(),
149 Ordering::Equal => return Some(&n.value),
150 };
151 }
152 None
153 }
154
155 pub fn contains_key(&self, key: &K) -> bool {
156 self.get(key).is_some()
157 }
158
159 /// Whether two maps are *the same tree*, not merely equal ones.
160 ///
161 /// `O(1)`, and the answer the incremental engine needs when deciding whether an event moved a
162 /// map at all: structural equality would be `O(n)` per event, which is the cost the engine
163 /// exists to avoid. A `false` here means "it may have changed", never "it did".
164 pub fn same_root(&self, other: &PMap<K, V>) -> bool {
165 match (&self.root, &other.root) {
166 (None, None) => true,
167 (Some(a), Some(b)) => Arc::ptr_eq(a, b),
168 _ => false,
169 }
170 }
171
172 /// Insert, returning a new map. Shares every subtree the new key did not pass through.
173 pub fn insert(&self, key: K, value: V) -> PMap<K, V> {
174 PMap {
175 root: insert_node(&self.root, key, value),
176 }
177 }
178
179 /// Remove, returning a new map.
180 pub fn remove(&self, key: &K) -> PMap<K, V> {
181 PMap {
182 root: remove_node(&self.root, key),
183 }
184 }
185
186 /// Entries in key order.
187 pub fn iter(&self) -> Iter<'_, K, V> {
188 let mut it = Iter { stack: Vec::new() };
189 it.push_left(&self.root);
190 it
191 }
192
193 pub fn keys(&self) -> impl Iterator<Item = &K> {
194 self.iter().map(|(k, _)| k)
195 }
196
197 pub fn values(&self) -> impl Iterator<Item = &V> {
198 self.iter().map(|(_, v)| v)
199 }
200}
201
202/// What happened to one key between two versions of a map.
203///
204/// `old` and `new` are both present for an update, one of them for an insert or a remove. Both
205/// absent never occurs.
206#[derive(Clone, Debug, PartialEq, Eq)]
207pub struct Change<K, V> {
208 pub key: K,
209 pub old: Option<V>,
210 pub new: Option<V>,
211}
212
213impl<K: Ord + Clone, V: Clone + PartialEq> PMap<K, V> {
214 /// The entries that differ between two versions, in key order.
215 ///
216 /// # Why this is `O(δ log n)` rather than `O(n)`
217 ///
218 /// This is the operation the whole incremental view engine rests on
219 /// ([`docs/24-incremental-views-report.md`](../../../../../docs/24-incremental-views-report.md)):
220 /// a fold produces a *whole new accumulator* per event, and a dataflow plan consumes *deltas*,
221 /// so something has to turn one into the other. Comparing entry by entry would be `O(n)` per
222 /// event, which is the recount §3.8 exists to abolish — the plan downstream would be
223 /// incremental and the thing feeding it would not.
224 ///
225 /// [`insert`](PMap::insert) rebuilds only the path to the key and shares every subtree that
226 /// path did not pass through, by `Arc`. So two versions of a map that differ by one insert
227 /// share `n - O(log n)` nodes *by pointer*, and a diff that can recognise a shared subtree can
228 /// skip all of its entries at once.
229 ///
230 /// The traversal is an ordered merge of the two trees, with one extra rule: when the heads of
231 /// the two remaining sequences are the same subtree by pointer, both are dropped. That is sound
232 /// for a reason worth stating, because it is the correctness of the engine: pointer-identical
233 /// subtrees hold identical entries, so the two remaining *sorted sequences* share that prefix
234 /// exactly, and a merge over sorted sequences reports nothing for a shared prefix. It holds
235 /// whatever rebalancing did to the position of that subtree in either tree.
236 pub fn diff(&self, next: &PMap<K, V>) -> Vec<Change<K, V>> {
237 let mut out = Vec::new();
238 let mut a = Walk::new(&self.root);
239 let mut b = Walk::new(&next.root);
240 loop {
241 // The pointer rule, applied before either side is expanded into entries.
242 a.skip_shared(&mut b);
243 match (a.peek(), b.peek()) {
244 (None, None) => return out,
245 (Some((k, v)), None) => {
246 out.push(Change {
247 key: k.clone(),
248 old: Some(v.clone()),
249 new: None,
250 });
251 a.bump();
252 }
253 (None, Some((k, v))) => {
254 out.push(Change {
255 key: k.clone(),
256 old: None,
257 new: Some(v.clone()),
258 });
259 b.bump();
260 }
261 (Some((ka, va)), Some((kb, vb))) => match ka.cmp(kb) {
262 Ordering::Less => {
263 out.push(Change {
264 key: ka.clone(),
265 old: Some(va.clone()),
266 new: None,
267 });
268 a.bump();
269 }
270 Ordering::Greater => {
271 out.push(Change {
272 key: kb.clone(),
273 old: None,
274 new: Some(vb.clone()),
275 });
276 b.bump();
277 }
278 Ordering::Equal => {
279 if va != vb {
280 out.push(Change {
281 key: ka.clone(),
282 old: Some(va.clone()),
283 new: Some(vb.clone()),
284 });
285 }
286 a.bump();
287 b.bump();
288 }
289 },
290 }
291 }
292 }
293}
294
295/// An in-order traversal that can be asked whether its next *subtree* is one another traversal is
296/// also about to yield.
297///
298/// The ordinary [`Iter`] pushes the left spine eagerly, which destroys exactly the information the
299/// diff needs: once a subtree has been expanded into a stack of nodes, "these two are the same
300/// subtree" is no longer a question that can be asked. This keeps unexpanded subtrees on the stack
301/// and expands one only when the merge actually needs an entry from it.
302struct Walk<'a, K, V> {
303 stack: Vec<Task<'a, K, V>>,
304}
305
306enum Task<'a, K, V> {
307 Sub(&'a Arc<Node<K, V>>),
308 Ent(&'a K, &'a V),
309}
310
311impl<'a, K, V> Walk<'a, K, V> {
312 fn new(root: &'a Link<K, V>) -> Walk<'a, K, V> {
313 let mut stack = Vec::new();
314 if let Some(n) = root {
315 stack.push(Task::Sub(n));
316 }
317 Walk { stack }
318 }
319
320 /// Drop any subtree both traversals are about to yield.
321 ///
322 /// Repeated, because skipping one shared subtree can expose another underneath it — which is
323 /// what happens on the second and later events, when the two versions share several whole
324 /// branches rather than one.
325 fn skip_shared(&mut self, other: &mut Walk<'a, K, V>) {
326 loop {
327 let (Some(Task::Sub(x)), Some(Task::Sub(y))) = (self.stack.last(), other.stack.last())
328 else {
329 return;
330 };
331 if !Arc::ptr_eq(*x, *y) {
332 return;
333 }
334 self.stack.pop();
335 other.stack.pop();
336 }
337 }
338
339 /// The next entry, expanding subtrees as needed. Leaves it on the stack.
340 fn peek(&mut self) -> Option<(&'a K, &'a V)> {
341 loop {
342 match self.stack.last()? {
343 Task::Ent(k, v) => return Some((*k, *v)),
344 Task::Sub(n) => {
345 let n = *n;
346 self.stack.pop();
347 // In-order: right subtree deepest, then this entry, then the left subtree.
348 if let Some(r) = &n.right {
349 self.stack.push(Task::Sub(r));
350 }
351 self.stack.push(Task::Ent(&n.key, &n.value));
352 if let Some(l) = &n.left {
353 self.stack.push(Task::Sub(l));
354 }
355 }
356 }
357 }
358 }
359
360 fn bump(&mut self) {
361 self.stack.pop();
362 }
363}
364
365impl<K: Ord + Clone, V: Clone> FromIterator<(K, V)> for PMap<K, V> {
366 fn from_iter<I: IntoIterator<Item = (K, V)>>(iter: I) -> Self {
367 iter.into_iter()
368 .fold(PMap::new(), |m, (k, v)| m.insert(k, v))
369 }
370}
371
372fn node<K, V>(key: K, value: V, left: Link<K, V>, right: Link<K, V>) -> Arc<Node<K, V>> {
373 let size = 1 + size(&left) + size(&right);
374 Arc::new(Node {
375 key,
376 value,
377 size,
378 left,
379 right,
380 })
381}
382
383/// Rebuild a node, restoring the weight balance invariant.
384fn balance<K: Clone, V: Clone>(
385 key: K,
386 value: V,
387 left: Link<K, V>,
388 right: Link<K, V>,
389) -> Arc<Node<K, V>> {
390 let (ls, rs) = (size(&left), size(&right));
391 if ls + rs <= 1 {
392 return node(key, value, left, right);
393 }
394 if rs > DELTA * ls {
395 let r = right.as_ref().expect("rs > 0");
396 return if size(&r.left) < RATIO * size(&r.right) {
397 // single left rotation
398 node(
399 r.key.clone(),
400 r.value.clone(),
401 Some(node(key, value, left, r.left.clone())),
402 r.right.clone(),
403 )
404 } else {
405 // double left rotation
406 let rl = r.left.as_ref().expect("checked by the ratio test");
407 node(
408 rl.key.clone(),
409 rl.value.clone(),
410 Some(node(key, value, left, rl.left.clone())),
411 Some(node(
412 r.key.clone(),
413 r.value.clone(),
414 rl.right.clone(),
415 r.right.clone(),
416 )),
417 )
418 };
419 }
420 if ls > DELTA * rs {
421 let l = left.as_ref().expect("ls > 0");
422 return if size(&l.right) < RATIO * size(&l.left) {
423 node(
424 l.key.clone(),
425 l.value.clone(),
426 l.left.clone(),
427 Some(node(key, value, l.right.clone(), right)),
428 )
429 } else {
430 let lr = l.right.as_ref().expect("checked by the ratio test");
431 node(
432 lr.key.clone(),
433 lr.value.clone(),
434 Some(node(
435 l.key.clone(),
436 l.value.clone(),
437 l.left.clone(),
438 lr.left.clone(),
439 )),
440 Some(node(key, value, lr.right.clone(), right)),
441 )
442 };
443 }
444 node(key, value, left, right)
445}
446
447fn insert_node<K: Ord + Clone, V: Clone>(link: &Link<K, V>, key: K, value: V) -> Link<K, V> {
448 match link {
449 None => Some(node(key, value, None, None)),
450 Some(n) => Some(match key.cmp(&n.key) {
451 // Only the path to the key is rebuilt; `n.right`/`n.left` are shared by pointer.
452 Ordering::Less => balance(
453 n.key.clone(),
454 n.value.clone(),
455 insert_node(&n.left, key, value),
456 n.right.clone(),
457 ),
458 Ordering::Greater => balance(
459 n.key.clone(),
460 n.value.clone(),
461 n.left.clone(),
462 insert_node(&n.right, key, value),
463 ),
464 Ordering::Equal => node(key, value, n.left.clone(), n.right.clone()),
465 }),
466 }
467}
468
469fn remove_node<K: Ord + Clone, V: Clone>(link: &Link<K, V>, key: &K) -> Link<K, V> {
470 let n = link.as_ref()?;
471 Some(match key.cmp(&n.key) {
472 Ordering::Less => balance(
473 n.key.clone(),
474 n.value.clone(),
475 remove_node(&n.left, key),
476 n.right.clone(),
477 ),
478 Ordering::Greater => balance(
479 n.key.clone(),
480 n.value.clone(),
481 n.left.clone(),
482 remove_node(&n.right, key),
483 ),
484 Ordering::Equal => match (&n.left, &n.right) {
485 (None, None) => return None,
486 (None, Some(r)) => r.clone(),
487 (Some(l), None) => l.clone(),
488 (Some(_), Some(r)) => {
489 // Replace with the successor, then remove it from the right subtree.
490 let (sk, sv) = min_entry(r);
491 balance(sk, sv, n.left.clone(), remove_min(&n.right))
492 }
493 },
494 })
495}
496
497fn min_entry<K: Clone, V: Clone>(n: &Arc<Node<K, V>>) -> (K, V) {
498 let mut cur = n;
499 while let Some(l) = &cur.left {
500 cur = l;
501 }
502 (cur.key.clone(), cur.value.clone())
503}
504
505fn remove_min<K: Clone, V: Clone>(link: &Link<K, V>) -> Link<K, V> {
506 let n = link.as_ref()?;
507 match &n.left {
508 None => n.right.clone(),
509 Some(_) => Some(balance(
510 n.key.clone(),
511 n.value.clone(),
512 remove_min(&n.left),
513 n.right.clone(),
514 )),
515 }
516}
517
518pub struct Iter<'a, K, V> {
519 stack: Vec<&'a Arc<Node<K, V>>>,
520}
521
522impl<'a, K, V> Iter<'a, K, V> {
523 fn push_left(&mut self, mut link: &'a Link<K, V>) {
524 while let Some(n) = link {
525 self.stack.push(n);
526 link = &n.left;
527 }
528 }
529}
530
531impl<'a, K, V> Iterator for Iter<'a, K, V> {
532 type Item = (&'a K, &'a V);
533
534 fn next(&mut self) -> Option<Self::Item> {
535 let n = self.stack.pop()?;
536 self.push_left(&n.right);
537 Some((&n.key, &n.value))
538 }
539}
540
541// ---- equality, ordering and hashing are structural, over the sorted entries ----
542
543impl<K: Ord + Clone + PartialEq, V: Clone + PartialEq> PartialEq for PMap<K, V> {
544 fn eq(&self, other: &Self) -> bool {
545 self.len() == other.len() && self.iter().eq(other.iter())
546 }
547}
548
549impl<K: Ord + Clone, V: Clone + Eq> Eq for PMap<K, V> {}
550
551impl<K: Ord + Clone, V: Clone + Ord> PartialOrd for PMap<K, V> {
552 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
553 Some(self.cmp(other))
554 }
555}
556
557impl<K: Ord + Clone, V: Clone + Ord> Ord for PMap<K, V> {
558 fn cmp(&self, other: &Self) -> Ordering {
559 self.iter().cmp(other.iter())
560 }
561}
562
563#[cfg(test)]
564mod tests {
565 use super::*;
566
567 #[test]
568 fn insert_get_remove_and_ordered_iteration() {
569 let mut m = PMap::new();
570 for i in [5, 3, 8, 1, 4, 7, 9, 2, 6] {
571 m = m.insert(i, i * 10);
572 }
573 assert_eq!(m.len(), 9);
574 assert_eq!(m.get(&4), Some(&40));
575 assert_eq!(m.get(&99), None);
576 assert_eq!(
577 m.keys().copied().collect::<Vec<_>>(),
578 (1..=9).collect::<Vec<_>>()
579 );
580
581 let without = m.remove(&5);
582 assert_eq!(without.len(), 8);
583 assert_eq!(without.get(&5), None);
584 // …and the original is untouched, which is the whole point.
585 assert_eq!(m.get(&5), Some(&50));
586 assert_eq!(m.len(), 9);
587 }
588
589 #[test]
590 fn replacing_a_key_does_not_grow_the_map() {
591 let m = PMap::new().insert("a", 1).insert("a", 2);
592 assert_eq!(m.len(), 1);
593 assert_eq!(m.get(&"a"), Some(&2));
594 }
595
596 #[test]
597 fn an_insert_shares_all_but_the_path_it_rebuilt() {
598 // The property the fold's asymptotics rest on: inserting into a map of n entries allocates
599 // O(log n) nodes, not n. Counted by pointer identity against the original.
600 let mut base = PMap::new();
601 for i in 0..1024 {
602 base = base.insert(i, i);
603 }
604 let next = base.insert(9999, 9999);
605
606 fn nodes<K, V>(link: &Link<K, V>, out: &mut Vec<*const Node<K, V>>) {
607 if let Some(n) = link {
608 out.push(Arc::as_ptr(n));
609 nodes(&n.left, out);
610 nodes(&n.right, out);
611 }
612 }
613 let (mut a, mut b) = (Vec::new(), Vec::new());
614 nodes(&base.root, &mut a);
615 nodes(&next.root, &mut b);
616 let shared = b.iter().filter(|p| a.contains(p)).count();
617 let fresh = b.len() - shared;
618
619 assert_eq!(base.len(), 1024);
620 assert_eq!(next.len(), 1025);
621 assert!(
622 fresh <= 40,
623 "an insert into a 1024-entry map rebuilt {fresh} nodes; O(log n) is ~10-40 with \
624 rebalancing, O(n) would be ~1024"
625 );
626 assert!(
627 shared > 900,
628 "only {shared} of {} nodes were shared",
629 b.len()
630 );
631 }
632
633 #[test]
634 fn the_tree_stays_balanced_under_sorted_insertion() {
635 // Ascending keys are the worst case for a naive BST and the common case for the fold,
636 // because ids often arrive in order.
637 let mut m = PMap::new();
638 for i in 0..10_000 {
639 m = m.insert(i, i);
640 }
641 fn depth<K, V>(link: &Link<K, V>) -> usize {
642 match link {
643 None => 0,
644 Some(n) => 1 + depth(&n.left).max(depth(&n.right)),
645 }
646 }
647 let d = depth(&m.root);
648 assert_eq!(m.len(), 10_000);
649 assert!(
650 d < 40,
651 "depth {d} for 10,000 ascending inserts is not balanced"
652 );
653 }
654
655 #[test]
656 fn removal_keeps_the_map_ordered_and_balanced() {
657 let mut m: PMap<i32, i32> = (0..2_000).map(|i| (i, i)).collect();
658 for i in (0..2_000).step_by(2) {
659 m = m.remove(&i);
660 }
661 assert_eq!(m.len(), 1_000);
662 let keys: Vec<i32> = m.keys().copied().collect();
663 assert!(
664 keys.windows(2).all(|w| w[0] < w[1]),
665 "iteration is not ordered"
666 );
667 assert_eq!(keys[0], 1);
668 assert!(m.get(&0).is_none() && m.get(&1).is_some());
669 }
670
671 /// Every node's subtree size is right, the keys are in order, and neither child outweighs the
672 /// other by more than `DELTA` — the invariant `balance` exists to maintain. Returns the size so
673 /// the check is one pass.
674 fn check_invariant<K: Ord, V>(link: &Link<K, V>, lo: Option<&K>, hi: Option<&K>) -> usize {
675 let Some(n) = link else { return 0 };
676 if let Some(lo) = lo {
677 assert!(&n.key > lo, "key order violated");
678 }
679 if let Some(hi) = hi {
680 assert!(&n.key < hi, "key order violated");
681 }
682 let ls = check_invariant(&n.left, lo, Some(&n.key));
683 let rs = check_invariant(&n.right, Some(&n.key), hi);
684 assert_eq!(n.size, 1 + ls + rs, "a cached subtree size is stale");
685 if ls + rs > 1 {
686 assert!(
687 ls <= DELTA * rs && rs <= DELTA * ls,
688 "weight invariant violated: {ls} against {rs}"
689 );
690 }
691 n.size
692 }
693
694 /// One pseudo-random history of inserts and removes, checked against `BTreeMap` as an oracle
695 /// with the structural invariant verified after *every* operation.
696 fn random_history(seed: u64, steps: u32, keyspace: u32) {
697 use std::collections::BTreeMap;
698 let mut s = seed | 1; // xorshift dies at zero
699 let mut rand = move || {
700 s ^= s << 13;
701 s ^= s >> 7;
702 s ^= s << 17;
703 s
704 };
705
706 let mut ours: PMap<u32, u32> = PMap::new();
707 let mut oracle: BTreeMap<u32, u32> = BTreeMap::new();
708 for step in 0..steps {
709 let k = (rand() % keyspace as u64) as u32;
710 // Insert-heavy, then delete-heavy, so the tree both grows and shrinks through every
711 // rebalancing path rather than hovering at one size.
712 let insert_pct = if step < steps / 2 { 70 } else { 30 };
713 if rand() % 100 < insert_pct {
714 ours = ours.insert(k, step);
715 oracle.insert(k, step);
716 } else {
717 ours = ours.remove(&k);
718 oracle.remove(&k);
719 }
720 check_invariant(&ours.root, None, None);
721 assert_eq!(
722 ours.len(),
723 oracle.len(),
724 "size diverged at step {step}, seed {seed}"
725 );
726 }
727 assert!(
728 ours.iter()
729 .map(|(k, v)| (*k, *v))
730 .eq(oracle.iter().map(|(k, v)| (*k, *v))),
731 "entries diverged from the oracle, seed {seed}"
732 );
733 }
734
735 #[test]
736 fn random_histories_of_inserts_and_removes_match_a_btreemap() {
737 // The class of bug this catches is the one `an_insert_shares_all_but_the_path_it_rebuilt`
738 // cannot: a rotation that loses an entry or breaks the balance invariant only under some
739 // particular interleaving of inserts and deletes. Seeds are fixed, so a failure is a
740 // reproducible failure rather than a story about one; several of them, because a single
741 // history explores one path through `balance` and there are six.
742 //
743 // This is the test that decides whether `DELTA`/`RATIO` are right. It is *not* strong
744 // enough to have found the published ⟨4, 2⟩ counterexample on its own — see the note on
745 // those constants: the parameters rest on Hirai and Yamamoto's proof, and this test
746 // confirms the implementation maintains what they proved maintainable.
747 for seed in 0..32u64 {
748 random_history(seed.wrapping_mul(0x9E37_79B9_7F4A_7C15), 3_000, 256);
749 }
750 // One much larger keyspace, so the tree is deep and mostly-distinct keys rather than a
751 // small set churned repeatedly.
752 random_history(0xDEAD_BEEF, 20_000, 8_192);
753 }
754
755 #[test]
756 fn a_diff_reports_exactly_what_changed() {
757 let base: PMap<i32, i32> = (0..100).map(|i| (i, i)).collect();
758 assert!(base.diff(&base).is_empty());
759
760 let inserted = base.insert(1000, 7);
761 assert_eq!(
762 base.diff(&inserted),
763 vec![Change {
764 key: 1000,
765 old: None,
766 new: Some(7)
767 }]
768 );
769 assert_eq!(
770 inserted.diff(&base),
771 vec![Change {
772 key: 1000,
773 old: Some(7),
774 new: None
775 }]
776 );
777
778 let updated = base.insert(50, -1);
779 assert_eq!(
780 base.diff(&updated),
781 vec![Change {
782 key: 50,
783 old: Some(50),
784 new: Some(-1)
785 }]
786 );
787 // Re-inserting the value it already has is not a change: the engine downstream must not be
788 // told to redo work for an event that moved nothing.
789 assert!(base.diff(&base.insert(50, 50)).is_empty());
790
791 let removed = base.remove(&3);
792 assert_eq!(
793 base.diff(&removed),
794 vec![Change {
795 key: 3,
796 old: Some(3),
797 new: None
798 }]
799 );
800 }
801
802 #[test]
803 fn a_diff_against_an_empty_map_is_every_entry() {
804 let m: PMap<i32, i32> = (0..10).map(|i| (i, i * 2)).collect();
805 let empty = PMap::new();
806 let inserts = empty.diff(&m);
807 assert_eq!(inserts.len(), 10);
808 assert!(inserts.iter().all(|c| c.old.is_none()));
809 // In key order, so a downstream operator can build an ordered arrangement from it without
810 // sorting.
811 assert!(inserts.windows(2).all(|w| w[0].key < w[1].key));
812 assert_eq!(m.diff(&empty).len(), 10);
813 }
814
815 /// The property the incremental view engine's asymptotics rest on: diffing two versions that
816 /// differ by one insert visits `O(log n)` nodes, not `n`.
817 ///
818 /// Counted rather than timed, because a wall-clock assertion in CI is a flake. The counter is
819 /// the number of *entries* the merge had to look at, which is what an `O(n)` implementation
820 /// would drive to `n`.
821 #[test]
822 fn diffing_two_versions_that_share_structure_visits_a_handful_of_entries() {
823 let mut base: PMap<u32, u32> = PMap::new();
824 for i in 0..8192 {
825 base = base.insert(i, i);
826 }
827 let next = base.insert(4096, 999);
828
829 // `Walk` yields entries; count how many either side had to expand.
830 let mut visited = 0usize;
831 let mut a = Walk::new(&base.root);
832 let mut b = Walk::new(&next.root);
833 loop {
834 a.skip_shared(&mut b);
835 match (a.peek(), b.peek()) {
836 (None, None) => break,
837 (Some(_), None) => {
838 visited += 1;
839 a.bump();
840 }
841 (None, Some(_)) => {
842 visited += 1;
843 b.bump();
844 }
845 (Some((ka, _)), Some((kb, _))) => {
846 visited += 1;
847 match ka.cmp(kb) {
848 Ordering::Less => a.bump(),
849 Ordering::Greater => b.bump(),
850 Ordering::Equal => {
851 a.bump();
852 b.bump();
853 }
854 }
855 }
856 }
857 }
858 // Printed so that docs/24 §24.2's number is reproducible rather than remembered.
859 println!("diffing an 8,192-entry map after one insert looked at {visited} entries");
860 assert!(
861 visited < 64,
862 "diffing an 8192-entry map after one insert looked at {visited} entries; \
863 O(log n) is a few dozen, O(n) would be 8192"
864 );
865 assert_eq!(base.diff(&next).len(), 1);
866 }
867
868 #[test]
869 fn a_diff_matches_a_btreemap_oracle_over_random_histories() {
870 use std::collections::BTreeMap;
871 let mut s = 0x5EED_1234u64;
872 let mut rand = move || {
873 s ^= s << 13;
874 s ^= s >> 7;
875 s ^= s << 17;
876 s
877 };
878 for _ in 0..64 {
879 let mut ours: PMap<u32, u32> = PMap::new();
880 let mut oracle: BTreeMap<u32, u32> = BTreeMap::new();
881 for _ in 0..200 {
882 let k = (rand() % 64) as u32;
883 if rand() % 100 < 60 {
884 ours = ours.insert(k, (rand() % 8) as u32);
885 oracle.insert(k, *ours.get(&k).unwrap());
886 } else {
887 ours = ours.remove(&k);
888 oracle.remove(&k);
889 }
890 }
891 // A second history from the same start, so the two maps differ in many places at once
892 // rather than by a single path.
893 let mut other = ours.clone();
894 let mut other_oracle = oracle.clone();
895 for _ in 0..60 {
896 let k = (rand() % 64) as u32;
897 if rand() % 100 < 50 {
898 other = other.insert(k, (rand() % 8) as u32);
899 other_oracle.insert(k, *other.get(&k).unwrap());
900 } else {
901 other = other.remove(&k);
902 other_oracle.remove(&k);
903 }
904 }
905
906 let mut expected: Vec<Change<u32, u32>> = Vec::new();
907 for k in oracle
908 .keys()
909 .chain(other_oracle.keys())
910 .copied()
911 .collect::<std::collections::BTreeSet<_>>()
912 {
913 let (old, new) = (oracle.get(&k).copied(), other_oracle.get(&k).copied());
914 if old != new {
915 expected.push(Change { key: k, old, new });
916 }
917 }
918 assert_eq!(ours.diff(&other), expected);
919 }
920 }
921
922 #[test]
923 fn equality_and_ordering_are_structural() {
924 let a: PMap<i32, i32> = (0..50).map(|i| (i, i)).collect();
925 // Built in a different order: same map.
926 let b: PMap<i32, i32> = (0..50).rev().map(|i| (i, i)).collect();
927 assert_eq!(a, b);
928 assert_eq!(a.cmp(&b), Ordering::Equal);
929 assert!(a < a.insert(50, 50));
930 }
931}