From 981b041bcf3edab0a0234d003abaeaca782afbfb Mon Sep 17 00:00:00 2001 From: Adam Vandervorst Date: Fri, 9 Jan 2026 21:19:02 +0100 Subject: [PATCH 01/50] Add node-based catamorphism POC --- Cargo.toml | 1 + benches/binary_keys.rs | 16 ++++ benches/cities.rs | 19 +++++ benches/shakespeare.rs | 38 ++++++++++ benches/sla.rs | 2 +- benches/sparse_keys.rs | 20 +++++ benches/superdense_keys.rs | 15 ++++ src/dense_byte_node.rs | 14 +++- src/line_list_node.rs | 23 +++++- src/trie_map.rs | 22 +++++- src/trie_node.rs | 146 +++++++++++++++++++++++++++++++++++-- 11 files changed, 301 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2d006419..0cf1cecc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -114,6 +114,7 @@ harness = false [[bench]] name = "sla" harness = false +required-features = ["viz"] [workspace] members = ["pathmap-derive"] diff --git a/benches/binary_keys.rs b/benches/binary_keys.rs index 2d8ded79..8f3c1e0a 100644 --- a/benches/binary_keys.rs +++ b/benches/binary_keys.rs @@ -77,6 +77,22 @@ fn binary_val_count_bench(bencher: Bencher, n: u64) { assert_eq!(sink, n as usize); } +#[divan::bench(args = [125, 250, 500, 1000, 2000, 4000])] +fn binary_goat_val_count_bench(bencher: Bencher, n: u64) { + + let keys = make_keys(n as usize, 1); + + let mut map: PathMap = PathMap::new(); + for i in 0..n { map.set_val_at(&keys[i as usize], i); } + + //Benchmark the time taken to count the number of values in the map + let mut sink = 0; + bencher.bench_local(|| { + *black_box(&mut sink) = map.goat_val_count() + }); + assert_eq!(sink, n as usize); +} + #[divan::bench(args = [50, 100, 200, 400, 800, 1600])] fn binary_drop_head(bencher: Bencher, n: u64) { diff --git a/benches/cities.rs b/benches/cities.rs index 231cb6e0..cc5cbc94 100644 --- a/benches/cities.rs +++ b/benches/cities.rs @@ -168,6 +168,25 @@ fn cities_val_count(bencher: Bencher) { assert_eq!(sink, unique_count); } +#[divan::bench()] +fn cities_goat_val_count(bencher: Bencher) { + + let pairs = read_data(); + let mut map = PathMap::new(); + let mut unique_count = 0; + for (k, v) in pairs.iter() { + if map.set_val_at(k, *v).is_none() { + unique_count += 1; + } + } + + let mut sink = 0; + bencher.bench_local(|| { + *black_box(&mut sink) = map.goat_val_count(); + }); + assert_eq!(sink, unique_count); +} + #[cfg(feature="arena_compact")] #[divan::bench()] fn cities_val_count_act(bencher: Bencher) { diff --git a/benches/shakespeare.rs b/benches/shakespeare.rs index 2040ba54..5528f739 100644 --- a/benches/shakespeare.rs +++ b/benches/shakespeare.rs @@ -113,6 +113,25 @@ fn shakespeare_words_val_count(bencher: Bencher) { assert_eq!(sink, unique_count); } +#[divan::bench()] +fn shakespeare_words_goat_val_count(bencher: Bencher) { + + let strings = read_data(true); + let mut map = PathMap::new(); + let mut unique_count = 0; + for (v, k) in strings.iter().enumerate() { + if map.set_val_at(k, v).is_none() { + unique_count += 1; + } + } + + let mut sink = 0; + bencher.bench_local(|| { + *black_box(&mut sink) = map.goat_val_count(); + }); + assert_eq!(sink, unique_count); +} + #[divan::bench()] fn shakespeare_sentences_insert(bencher: Bencher) { @@ -168,6 +187,25 @@ fn shakespeare_sentences_val_count(bencher: Bencher) { assert_eq!(sink, unique_count); } +#[divan::bench()] +fn shakespeare_sentences_goat_val_count(bencher: Bencher) { + + let strings = read_data(false); + let mut map = PathMap::new(); + let mut unique_count = 0; + for (v, k) in strings.iter().enumerate() { + if map.set_val_at(k, v).is_none() { + unique_count += 1; + } + } + + let mut sink = 0; + bencher.bench_local(|| { + *black_box(&mut sink) = map.goat_val_count(); + }); + assert_eq!(sink, unique_count); +} + #[cfg(feature="arena_compact")] #[divan::bench()] fn shakespeare_sentences_val_count_act(bencher: Bencher) { diff --git a/benches/sla.rs b/benches/sla.rs index 5d84dbee..92d89f07 100644 --- a/benches/sla.rs +++ b/benches/sla.rs @@ -388,7 +388,7 @@ fn tipover_attention_weave() { // let res = rtq.vF_mut().merkleize(); // println!("{:?}", res.hash); let t0 = Instant::now(); - println!("{:?} {:?}", rtq.vF().read_zipper().into_cata_cached(morphisms::alg::hash), t0.elapsed().as_micros()); + // println!("{:?} {:?}", rtq.vF().read_zipper().into_cata_cached(morphisms::alg::hash), t0.elapsed().as_micros()); return; // rtk.vF_mut().merkleize(); diff --git a/benches/sparse_keys.rs b/benches/sparse_keys.rs index 84893144..42c4d422 100644 --- a/benches/sparse_keys.rs +++ b/benches/sparse_keys.rs @@ -92,6 +92,26 @@ fn sparse_val_count_bench(bencher: Bencher, n: u64) { assert_eq!(sink, n as usize); } +#[divan::bench(args = [125, 250, 500, 1000, 2000, 4000])] +fn sparse_goat_val_count_bench(bencher: Bencher, n: u64) { + + let mut r = StdRng::seed_from_u64(1); + let keys: Vec> = (0..n).into_iter().map(|_| { + let len = (r.random::() % 18) + 3; //length between 3 and 20 chars + (0..len).into_iter().map(|_| r.random::()).collect() + }).collect(); + + let mut map: PathMap = PathMap::new(); + for i in 0..n { map.set_val_at(&keys[i as usize], i); } + + //Benchmark the time taken to count the number of values in the map + let mut sink = 0; + bencher.bench_local(|| { + *black_box(&mut sink) = map.goat_val_count() + }); + assert_eq!(sink, n as usize); +} + #[divan::bench(args = [50, 100, 200, 400, 800, 1600])] fn binary_drop_head(bencher: Bencher, n: u64) { diff --git a/benches/superdense_keys.rs b/benches/superdense_keys.rs index 597954df..34a09ce4 100644 --- a/benches/superdense_keys.rs +++ b/benches/superdense_keys.rs @@ -253,6 +253,21 @@ fn superdense_val_count_bench(bencher: Bencher, n: u64) { assert_eq!(sink, n as usize); } +#[divan::bench(sample_size = 1, args = [100, 200, 400, 800, 1600, 3200, 20_000])] +fn superdense_goat_val_count_bench(bencher: Bencher, n: u64) { + + let mut map: PathMap = PathMap::new(); + for i in 0..n { map.set_val_at(prefix_key(&i), i); } + + //Benchmark the time taken to count the number of values in the map + let mut sink = 0; + bencher.bench_local(|| { + *black_box(&mut sink) = map.goat_val_count() + }); + assert_eq!(sink, n as usize); +} + + #[cfg(feature="arena_compact")] #[divan::bench(sample_size = 1, args = [100, 200, 400, 800, 1600, 3200, 20_000])] fn superdense_val_count_bench_act(bencher: Bencher, n: u64) { diff --git a/src/dense_byte_node.rs b/src/dense_byte_node.rs index dd846a29..defaccff 100644 --- a/src/dense_byte_node.rs +++ b/src/dense_byte_node.rs @@ -29,7 +29,7 @@ pub struct ByteNode { #[cfg(feature = "nightly")] values: Vec, #[cfg(not(feature = "nightly"))] - values: Vec, + pub(crate) values: Vec, alloc: A, } @@ -991,10 +991,18 @@ impl> TrieNode t + cf.has_val() as usize + cf.rec().map(|r| val_count_below_node(r, cache)).unwrap_or(0) }); } - fn node_goat_val_count(&self) -> usize { +/* fn node_goat_val_count(&self) -> usize { return self.values.iter().rfold(0, |t, cf| { - t + cf.has_val() as usize + t + cf.has_val() as usize + cf.rec().map(|r| r.as_tagged().node_goat_val_count()).unwrap_or(0) }); + }*/ + #[inline] + fn node_goat_val_count(&self) -> usize { + let mut result = 0; + for cf in self.values.iter() { + result += cf.has_val() as usize + } + result } fn node_child_iter_start(&self) -> (u64, Option<&TrieNodeODRc>) { for (pos, cf) in self.values.iter().enumerate() { diff --git a/src/line_list_node.rs b/src/line_list_node.rs index 040f7142..66120318 100644 --- a/src/line_list_node.rs +++ b/src/line_list_node.rs @@ -403,7 +403,7 @@ impl LineListNode { } } #[inline] - unsafe fn child_in_slot(&self) -> &TrieNodeODRc { + pub(crate) unsafe fn child_in_slot(&self) -> &TrieNodeODRc { match SLOT { 0 => unsafe{ &*self.val_or_child0.child }, 1 => unsafe{ &*self.val_or_child1.child }, @@ -419,7 +419,7 @@ impl LineListNode { } } #[inline] - unsafe fn val_in_slot(&self) -> &V { + pub(crate) unsafe fn val_in_slot(&self) -> &V { match SLOT { 0 => unsafe{ &**self.val_or_child0.val }, 1 => unsafe{ &**self.val_or_child1.val }, @@ -1986,6 +1986,25 @@ impl TrieNode for LineListNode } result } +/* #[inline] + fn node_goat_val_count(&self) -> usize { + let mut result = 0; + if self.is_used_value_0() { + result += 1; + } + if self.is_used_value_1() { + result += 1; + } + if self.is_used_child_0() { + let child_node = unsafe{ self.child_in_slot::<0>() }; + result += child_node.as_tagged().node_goat_val_count(); + } + if self.is_used_child_1() { + let child_node = unsafe{ self.child_in_slot::<1>() }; + result += child_node.as_tagged().node_goat_val_count(); + } + result + }*/ #[inline] fn node_goat_val_count(&self) -> usize { //Here are 3 alternative implementations. They're basically the same in perf, with a slight edge to the diff --git a/src/trie_map.rs b/src/trie_map.rs index 3c5b0f37..5d211868 100644 --- a/src/trie_map.rs +++ b/src/trie_map.rs @@ -511,9 +511,25 @@ impl PathMap { let root_val = unsafe{ &*self.root_val.get() }.is_some() as usize; match self.root() { Some(root) => { - traverse_physical(root, - |node, ctx: usize| { ctx + node.node_goat_val_count() }, - |ctx, child_ctx| { ctx + child_ctx }, + // root.as_tagged().node_goat_val_count() + root_val + // traverse_physical(root, + // |node, ctx: usize| { ctx + node.node_goat_val_count() }, + // |ctx, child_ctx| { ctx + child_ctx }, + // ) + root_val + + // traverse_split_cata( + // root, + // |v, _| { 1usize }, + // |_, w, _| { 1 + w }, + // |bm, ws: &mut [usize], _| { ws.iter().sum() } + // ) + root_val + // Adam: this doesn't need to be called "traverse_osplit_cata" or be exposed under this interface; it can just live in morphisms + traverse_osplit_cata( + root, + |v, _| { 1usize }, // on leaf values + |_, w, _| { 1 + w }, // on values amongst a path + |bm, w: usize, _, total| { *total += w }, // on merging children into a node + |bm, total: usize, _| { total } // finalizing a node ) + root_val }, None => root_val diff --git a/src/trie_node.rs b/src/trie_node.rs index 111bd5e4..0687c4fa 100644 --- a/src/trie_node.rs +++ b/src/trie_node.rs @@ -7,7 +7,7 @@ use dyn_clone::*; use local_or_heap::LocalOrHeap; use arrayvec::ArrayVec; -use crate::utils::ByteMask; +use crate::utils::{BitMask, ByteMask}; use crate::alloc::Allocator; use crate::dense_byte_node::*; use crate::ring::*; @@ -2422,16 +2422,147 @@ fn traverse_physical_children_internal(node: TaggedNode { let mut ctx = Ctx::default(); - let (mut tok, mut child) = node.node_child_iter_start(); - while let Some(child_node) = child { - let child_ctx = traverse_physical_internal(child_node, node_f, fold_f, cache); - ctx = fold_f(ctx, child_ctx); - (tok, child) = node.node_child_iter_next(tok); + match node { + TaggedNodeRef::DenseByteNode(n) => { + for cf in n.values.iter() { + if let Some(rec) = cf.rec() { + let child_ctx = traverse_physical_internal(rec, node_f, fold_f, cache); + ctx = fold_f(ctx, child_ctx); + } + } + } + TaggedNodeRef::LineListNode(n) => { + if n.is_used_child_0() { + let child_node = unsafe{ n.child_in_slot::<0>() }; + let child_ctx = traverse_physical_internal(child_node, node_f, fold_f, cache); + ctx = fold_f(ctx, child_ctx); + } + if n.is_used_child_1() { + let child_node = unsafe{ n.child_in_slot::<1>() }; + let child_ctx = traverse_physical_internal(child_node, node_f, fold_f, cache); + ctx = fold_f(ctx, child_ctx); + } + } + TaggedNodeRef::CellByteNode(_) => { todo!() } + TaggedNodeRef::TinyRefNode(_) => { todo!() } + TaggedNodeRef::EmptyNode => { todo!() } } node_f(node, ctx) } +// This experiment is still OK, but the `&mut [W]` is awkward to instantiate if you don't actually have +/*pub fn traverse_split_cata<'a, A : Allocator, V : TrieValue, W, MapF, CollapseF, AlgF>(node: &TrieNodeODRc, mut map_f: MapF, mut collapse_f: CollapseF, alg_f: AlgF) -> W +where + MapF: Copy + FnMut(&V, &[u8]) -> W + 'a, + CollapseF: Copy + FnMut(&V, W, &[u8]) -> W + 'a, + AlgF: Copy + Fn(&ByteMask, &mut [W], &[u8]) -> W + 'a, +{ + match node.as_tagged() { + TaggedNodeRef::DenseByteNode(n) => { + let mut ws = [const { std::mem::MaybeUninit::::uninit() }; 256]; + // let mut ws: Vec> = Vec::with_capacity(n.mask.count_bits()); + // unsafe { ws.set_len(n.mask.count_bits()) }; + let mut c = 0; + for cf in n.values.iter() { + if let Some(rec) = cf.rec() { + let w = traverse_split_cata(rec, map_f, collapse_f, alg_f); + if let Some(v) = cf.val() { + ws[c].write(collapse_f(v, w, &[])); + } else { + ws[c].write(w); + } + } else if let Some(v) = cf.val() { + ws[c].write(map_f(v, &[])); + } + c += 1; + } + alg_f(&n.mask, unsafe { std::mem::transmute(&mut ws[..c]) }, &[]) + } + TaggedNodeRef::LineListNode(n) => { + // let mut ws = vec![]; + // if n.is_used_value_0() { + // ws.append(map_f(unsafe { n.val_in_slot::<0>() }, &[])); + // } + // if n.is_used_value_1() { + // ws.append(map_f(unsafe { n.val_in_slot::<1>() }, &[])); + // } + // if n.is_used_child_0() { + // let child_node = unsafe{ n.child_in_slot::<0>() }; + // let child_ctx = traverse_split_cata(child_node, map_f, collapse_f, alg_f); + // + // } + // if n.is_used_child_1() { + // let child_node = unsafe{ n.child_in_slot::<1>() }; + // let child_ctx = traverse_physical_internal(child_node, node_f, fold_f, cache); + // ctx = fold_f(ctx, child_ctx); + // } + alg_f(&ByteMask::new(), &mut [], &[]) + } + TaggedNodeRef::CellByteNode(_) => { todo!() } + TaggedNodeRef::TinyRefNode(_) => { todo!() } + TaggedNodeRef::EmptyNode => { todo!() } + } +} +*/ + +// Adam: This seems to be a winner, though it needs some work, the split alg gives us the opportunity to nicely compose the different calls for the different node types without introducing overhead +pub fn traverse_osplit_cata<'a, A : Allocator, V : TrieValue, Alg : Default, W, MapF, CollapseF, InAlgF, OutAlgF>(node: &TrieNodeODRc, mut map_f: MapF, mut collapse_f: CollapseF, in_alg_f: InAlgF, out_alg_f: OutAlgF) -> W +where + MapF: Copy + FnMut(&V, &[u8]) -> W + 'a, + CollapseF: Copy + FnMut(&V, W, &[u8]) -> W + 'a, + InAlgF: Copy + Fn(&ByteMask, W, &[u8], &mut Alg), + OutAlgF: Copy + Fn(&ByteMask, Alg, &[u8]) -> W + 'a, +{ + match node.as_tagged() { + TaggedNodeRef::DenseByteNode(n) => { + let mut ws = Some(Alg::default()); + for cf in n.values.iter() { + if let Some(rec) = cf.rec() { + let w = traverse_osplit_cata(rec, map_f, collapse_f, in_alg_f, out_alg_f); + if let Some(v) = cf.val() { + in_alg_f(&n.mask, collapse_f(v, w, &[]), &[], unsafe { ws.as_mut().unwrap_unchecked() }); + } else { + in_alg_f(&n.mask, w, &[], unsafe { ws.as_mut().unwrap_unchecked() }); + } + } else if let Some(v) = cf.val() { + in_alg_f(&n.mask, map_f(v, &[]), &[], unsafe { ws.as_mut().unwrap_unchecked() }); + } + } + out_alg_f(&n.mask, unsafe { std::mem::take(&mut ws).unwrap_unchecked() }, &[]) + } + TaggedNodeRef::LineListNode(n) => { + // Adam: I skimped out on the collapse logic here, I assume there are some built-in LineListNode functions I can use for prefixes, or another way to organize the branching based on the mask directly + let mut ws = Some(Alg::default()); + + if n.is_used_value_0() { + in_alg_f(&ByteMask::new(), map_f(unsafe { n.val_in_slot::<0>() }, &[]), &[], unsafe { ws.as_mut().unwrap_unchecked() }); + } + if n.is_used_value_1() { + in_alg_f(&ByteMask::new(), map_f(unsafe { n.val_in_slot::<1>() }, &[]), &[], unsafe { ws.as_mut().unwrap_unchecked() }); + } + if n.is_used_child_0() { + let child_node = unsafe{ n.child_in_slot::<0>() }; + let w = traverse_osplit_cata(child_node, map_f, collapse_f, in_alg_f, out_alg_f); + in_alg_f(&ByteMask::new(), w, &[], unsafe { ws.as_mut().unwrap_unchecked() }); + + } + if n.is_used_child_1() { + let child_node = unsafe{ n.child_in_slot::<1>() }; + let w = traverse_osplit_cata(child_node, map_f, collapse_f, in_alg_f, out_alg_f); + in_alg_f(&ByteMask::new(), w, &[], unsafe { ws.as_mut().unwrap_unchecked() }); + } + + out_alg_f(&ByteMask::new(), unsafe { std::mem::take(&mut ws).unwrap_unchecked() }, &[]) + } + TaggedNodeRef::CellByteNode(_) => { todo!() } + TaggedNodeRef::TinyRefNode(_) => { todo!() } + TaggedNodeRef::EmptyNode => { + out_alg_f(&ByteMask::new(), Alg::default(), &[]) + } + } +} + /// Internal function to walk a mut TrieNodeODRc ref along a path /// /// If `stop_early` is `true`, this function will return the parent node of the path and will never return @@ -2483,6 +2614,9 @@ pub(crate) fn make_cell_node(node: &mut Tr // module come from the visibility of the trait it is derived on. In this case, `TrieNode` //Credit to QuineDot for his ideas on this pattern here: https://users.rust-lang.org/t/inferred-lifetime-for-dyn-trait/112116/7 pub(crate) use opaque_dyn_rc_trie_node::TrieNodeODRc; +use crate::morphisms::SplitCata; +use crate::TrieValue; + #[cfg(not(feature = "slim_ptrs"))] mod opaque_dyn_rc_trie_node { use std::sync::Arc; From 949d2d6b27b2ee6ab90b29d229e52a81e496004b Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Tue, 20 Jan 2026 17:19:11 -0700 Subject: [PATCH 02/50] Thrashing towards fully functional recursive caching cata. --- benches/binary_keys.rs | 4 +- src/dense_byte_node.rs | 49 +++++++++++++++++++++ src/line_list_node.rs | 68 +++++++++++++++++++++++++++++ src/trie_map.rs | 2 +- src/trie_node.rs | 99 +++++++++++++++++++++--------------------- 5 files changed, 170 insertions(+), 52 deletions(-) diff --git a/benches/binary_keys.rs b/benches/binary_keys.rs index 8f3c1e0a..82ffe247 100644 --- a/benches/binary_keys.rs +++ b/benches/binary_keys.rs @@ -61,7 +61,7 @@ fn binary_get(bencher: Bencher, n: u64) { }); } -#[divan::bench(args = [125, 250, 500, 1000, 2000, 4000])] +#[divan::bench(args = [125, 250, 500, 1000, 2000, 4000, 100000])] fn binary_val_count_bench(bencher: Bencher, n: u64) { let keys = make_keys(n as usize, 1); @@ -77,7 +77,7 @@ fn binary_val_count_bench(bencher: Bencher, n: u64) { assert_eq!(sink, n as usize); } -#[divan::bench(args = [125, 250, 500, 1000, 2000, 4000])] +#[divan::bench(args = [125, 250, 500, 1000, 2000, 4000, 100000])] fn binary_goat_val_count_bench(bencher: Bencher, n: u64) { let keys = make_keys(n as usize, 1); diff --git a/src/dense_byte_node.rs b/src/dense_byte_node.rs index defaccff..78b2e176 100644 --- a/src/dense_byte_node.rs +++ b/src/dense_byte_node.rs @@ -310,6 +310,10 @@ impl> ByteNode /// Iterates the entries in `self`, calling `func` for each entry /// The arguments to `func` are: `func(self, key_byte, n)`, where `n` is the number of times /// `func` has been called prior. This corresponds to index of the `CoFree` in the `values` vec + /// + /// PERF GOAT: this method is useful if you know you need the corresponding path byte. If, however, the + /// byte might be unneeded, it's better to let self.values govern the loop, because using the mask + /// in the loop means the bitwise ops to manipulate the mask are always required. #[inline] fn for_each_item(&self, mut func: F) { let mut n = 0; @@ -326,6 +330,51 @@ impl> ByteNode } } } + + #[inline(always)] + pub fn node_recursive_cata(&self, map_f: MapF, collapse_f: CollapseF, in_alg_f: InAlgF, out_alg_f: OutAlgF) -> W + where + Acc: Default, + MapF: Copy + Fn(&V, &[u8]) -> W, + CollapseF: Copy + Fn(&V, W, &[u8]) -> W, + InAlgF: Copy + Fn(&ByteMask, W, &[u8], &mut Acc), + OutAlgF: Copy + Fn(&ByteMask, Acc, &[u8]) -> W, + { + let mut mask_idx = 0; + let mut lm = unsafe{ *self.mask.0.get_unchecked(0) }; + let mut ws = Some(Acc::default()); + for cf in self.values.iter() { + //Compute the key byte. Hopefully this will all be stripped away by the compiler if the path isn't used + //UPDATE: alas, my hopes were dashed. No amount of reorganizing this code, eliminating all traps, + // unrolling the loop, etc., could convince LLVM to elide it. So we have to hit it with the const hammer. + let key_byte; + let path = if COMPUTE_PATH { + while lm == 0 { + mask_idx += 1; + lm = unsafe{ *self.mask.0.get_unchecked(mask_idx) }; + } + let byte_index = lm.trailing_zeros(); + lm ^= 1u64 << byte_index; + key_byte = 64*(mask_idx as u8) + (byte_index as u8); + core::slice::from_ref(&key_byte) + } else { + &[] + }; + + //Do the recursive calling + if let Some(rec) = cf.rec() { + let w = recursive_cata::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(rec, map_f, collapse_f, in_alg_f, out_alg_f); + if let Some(v) = cf.val() { + in_alg_f(&self.mask, collapse_f(v, w, path), path, unsafe { ws.as_mut().unwrap_unchecked() }); + } else { + in_alg_f(&self.mask, w, path, unsafe { ws.as_mut().unwrap_unchecked() }); + } + } else if let Some(v) = cf.val() { + in_alg_f(&self.mask, map_f(v, path), path, unsafe { ws.as_mut().unwrap_unchecked() }); + } + } + out_alg_f(&self.mask, unsafe { std::mem::take(&mut ws).unwrap_unchecked() }, &[]) + } } impl> ByteNode where Self: TrieNodeDowncast { diff --git a/src/line_list_node.rs b/src/line_list_node.rs index 66120318..3a0fd466 100644 --- a/src/line_list_node.rs +++ b/src/line_list_node.rs @@ -2741,6 +2741,74 @@ impl LineListNode { } } } + + #[inline(always)] + pub fn node_recursive_cata(&self, map_f: MapF, collapse_f: CollapseF, in_alg_f: InAlgF, out_alg_f: OutAlgF) -> W + where + Acc: Default, + MapF: Copy + Fn(&V, &[u8]) -> W, + CollapseF: Copy + Fn(&V, W, &[u8]) -> W, + InAlgF: Copy + Fn(&ByteMask, W, &[u8], &mut Acc), + OutAlgF: Copy + Fn(&ByteMask, Acc, &[u8]) -> W, + { + let mut ws = Some(Acc::default()); + +//GOAT, should we remove the path from out_alg?? I can't see when it's ever used... +// A: Either the path doesn't belong on the out_alg or on the in_alg. + +//GOAT, check out whether in_alg should get the path on the ByteNode + +//Cases: +// * There is only one val. Run map_f only +// * There is only one child. Recurse, then Run the in_alg -> out_alg combo +// * Both slots are filled +// GOAT, there is a problem where we have to care about whether it's a Val or child! +// - Is the first byte the same? +// N: +// - Call map_f on slot0 with the whole path +// - Call map_f on slot1 with the whole path +// - Call in_alg (branch_f) on each of the Ws +// - Call out_alg (fold_f) on the context with a composed mask from the first bytes +// Y: +// - Call map_f on slot0 with everything after first byte +// - Call map_f on slot1 with everything after first byte +// - Call in_alg (branch_f) on each of the Ws +// - Call out_alg (fold_f) on the context with a composed mask from the second bytes + + + +//New plan for API. +// 3 closures. +// - closure to deal with straight paths and values. Fn(Option<&V>, Option, &[u8]) -> W +// - closure to deal with one downstream branch from a logical node. Fn(&ByteMask, W, &mut Acc) +// - closure to fold accumulator back into a W for the logical node. Fn(&ByteMask, Acc) -> W +// +//Then, the non-jumping API would take: +// 2 closures. +// - closure to deal with one downstream branch from a logical node. Fn(&ByteMask, W, &mut Acc) +// - closure to deal with each path byte / logical node. Fn(&ByteMask, Option<&V>, Acc) -> W + + + if self.is_used_value_0() { + in_alg_f(&ByteMask::new(), map_f(unsafe { self.val_in_slot::<0>() }, &[]), &[], unsafe { ws.as_mut().unwrap_unchecked() }); + } + if self.is_used_value_1() { + in_alg_f(&ByteMask::new(), map_f(unsafe { self.val_in_slot::<1>() }, &[]), &[], unsafe { ws.as_mut().unwrap_unchecked() }); + } + if self.is_used_child_0() { + let child_node = unsafe{ self.child_in_slot::<0>() }; + let w = recursive_cata::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(child_node, map_f, collapse_f, in_alg_f, out_alg_f); + in_alg_f(&ByteMask::new(), w, &[], unsafe { ws.as_mut().unwrap_unchecked() }); + + } + if self.is_used_child_1() { + let child_node = unsafe{ self.child_in_slot::<1>() }; + let w = recursive_cata::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(child_node, map_f, collapse_f, in_alg_f, out_alg_f); + in_alg_f(&ByteMask::new(), w, &[], unsafe { ws.as_mut().unwrap_unchecked() }); + } + + out_alg_f(&ByteMask::new(), unsafe { std::mem::take(&mut ws).unwrap_unchecked() }, &[]) + } } impl TrieNodeDowncast for LineListNode { diff --git a/src/trie_map.rs b/src/trie_map.rs index 5d211868..9a3d93ab 100644 --- a/src/trie_map.rs +++ b/src/trie_map.rs @@ -524,7 +524,7 @@ impl PathMap { // |bm, ws: &mut [usize], _| { ws.iter().sum() } // ) + root_val // Adam: this doesn't need to be called "traverse_osplit_cata" or be exposed under this interface; it can just live in morphisms - traverse_osplit_cata( + recursive_cata::<_, _, _, _, _, _, _, _, false>( root, |v, _| { 1usize }, // on leaf values |_, w, _| { 1 + w }, // on values amongst a path diff --git a/src/trie_node.rs b/src/trie_node.rs index 0687c4fa..1068a998 100644 --- a/src/trie_node.rs +++ b/src/trie_node.rs @@ -2507,59 +2507,60 @@ where */ // Adam: This seems to be a winner, though it needs some work, the split alg gives us the opportunity to nicely compose the different calls for the different node types without introducing overhead -pub fn traverse_osplit_cata<'a, A : Allocator, V : TrieValue, Alg : Default, W, MapF, CollapseF, InAlgF, OutAlgF>(node: &TrieNodeODRc, mut map_f: MapF, mut collapse_f: CollapseF, in_alg_f: InAlgF, out_alg_f: OutAlgF) -> W +/// Traverse a trie with a split catamorphism and caller-provided aggregation. +/// +/// Closure argument meanings: +/// - `map_f(&V, &[u8]) -> W`: map a leaf value to a result. Args are the value and +/// a path slice (currently always `&[]` in this implementation). +/// - `collapse_f(&V, W, &[u8]) -> W`: combine a value stored along a path with a +/// child result. Args are the value, the child result, and a path slice +/// (currently always `&[]`). +/// - `in_alg_f(&ByteMask, W, &[u8], &mut Alg)`: fold a single slot's result into +/// the node accumulator. Args are the node's mask, the slot result, a path +/// slice (currently always `&[]`), and the mutable accumulator. +/// - `out_alg_f(&ByteMask, Alg, &[u8]) -> W`: finalize a node accumulator into the +/// node's result. Args are the node's mask, the accumulator, and a path slice +/// (currently always `&[]`). + +//GOAT issues +// - The reason it's faster than the other abstraction because it branches on node type once, rather than twice per node. +// +// There is more stuff on the stack, meaning we're more likely to blow the stack +// +// * Needed to add caching +// * The logic in pair node was wrong, because there is no guarantee both sides are at the same level; fixing that added another branch +// +// Observations: +// If we want to count path-ends, MapF wouldn't work, but could pass Option<&V>, which would be fine +// It might be possible to unify MapF and CollapseF, but +// +//GOAT, TODO: Make a test to hit the stack overflow failure case +// +//GOAT: +// * Look at the callbacks in line node +// * Send partial paths in pair node too +// * Come up with new names for in_alg and out_alg... +// * Put caching back +// * See if I can get closer to the other cata API without sacrificing performance +// +pub fn recursive_cata(node: &TrieNodeODRc, map_f: MapF, collapse_f: CollapseF, in_alg_f: InAlgF, out_alg_f: OutAlgF) -> W where - MapF: Copy + FnMut(&V, &[u8]) -> W + 'a, - CollapseF: Copy + FnMut(&V, W, &[u8]) -> W + 'a, - InAlgF: Copy + Fn(&ByteMask, W, &[u8], &mut Alg), - OutAlgF: Copy + Fn(&ByteMask, Alg, &[u8]) -> W + 'a, + V: Clone + Send + Sync, + A: Allocator, + Acc: Default, + MapF: Copy + Fn(&V, &[u8]) -> W, + CollapseF: Copy + Fn(&V, W, &[u8]) -> W, + // InAlgF: called for each + InAlgF: Copy + Fn(&ByteMask, W, &[u8], &mut Acc), + // OutAlgF: collapses all children at the same level + OutAlgF: Copy + Fn(&ByteMask, Acc, &[u8]) -> W, { match node.as_tagged() { - TaggedNodeRef::DenseByteNode(n) => { - let mut ws = Some(Alg::default()); - for cf in n.values.iter() { - if let Some(rec) = cf.rec() { - let w = traverse_osplit_cata(rec, map_f, collapse_f, in_alg_f, out_alg_f); - if let Some(v) = cf.val() { - in_alg_f(&n.mask, collapse_f(v, w, &[]), &[], unsafe { ws.as_mut().unwrap_unchecked() }); - } else { - in_alg_f(&n.mask, w, &[], unsafe { ws.as_mut().unwrap_unchecked() }); - } - } else if let Some(v) = cf.val() { - in_alg_f(&n.mask, map_f(v, &[]), &[], unsafe { ws.as_mut().unwrap_unchecked() }); - } - } - out_alg_f(&n.mask, unsafe { std::mem::take(&mut ws).unwrap_unchecked() }, &[]) - } - TaggedNodeRef::LineListNode(n) => { - // Adam: I skimped out on the collapse logic here, I assume there are some built-in LineListNode functions I can use for prefixes, or another way to organize the branching based on the mask directly - let mut ws = Some(Alg::default()); - - if n.is_used_value_0() { - in_alg_f(&ByteMask::new(), map_f(unsafe { n.val_in_slot::<0>() }, &[]), &[], unsafe { ws.as_mut().unwrap_unchecked() }); - } - if n.is_used_value_1() { - in_alg_f(&ByteMask::new(), map_f(unsafe { n.val_in_slot::<1>() }, &[]), &[], unsafe { ws.as_mut().unwrap_unchecked() }); - } - if n.is_used_child_0() { - let child_node = unsafe{ n.child_in_slot::<0>() }; - let w = traverse_osplit_cata(child_node, map_f, collapse_f, in_alg_f, out_alg_f); - in_alg_f(&ByteMask::new(), w, &[], unsafe { ws.as_mut().unwrap_unchecked() }); - - } - if n.is_used_child_1() { - let child_node = unsafe{ n.child_in_slot::<1>() }; - let w = traverse_osplit_cata(child_node, map_f, collapse_f, in_alg_f, out_alg_f); - in_alg_f(&ByteMask::new(), w, &[], unsafe { ws.as_mut().unwrap_unchecked() }); - } - - out_alg_f(&ByteMask::new(), unsafe { std::mem::take(&mut ws).unwrap_unchecked() }, &[]) - } + TaggedNodeRef::DenseByteNode(node) => { node.node_recursive_cata::<_, _, _, _, _, _, COMPUTE_PATH>(map_f, collapse_f, in_alg_f, out_alg_f) } + TaggedNodeRef::LineListNode(node) => { node.node_recursive_cata::<_, _, _, _, _, _, COMPUTE_PATH>(map_f, collapse_f, in_alg_f, out_alg_f) } TaggedNodeRef::CellByteNode(_) => { todo!() } TaggedNodeRef::TinyRefNode(_) => { todo!() } - TaggedNodeRef::EmptyNode => { - out_alg_f(&ByteMask::new(), Alg::default(), &[]) - } + TaggedNodeRef::EmptyNode => { out_alg_f(&ByteMask::new(), Acc::default(), &[]) } } } @@ -3375,4 +3376,4 @@ mod tests { node_ref.make_unique(); drop(cloned); } -} \ No newline at end of file +} From 54b8daa160134b6ef698ddd6ac9d6fe903565ec0 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Tue, 20 Jan 2026 18:49:47 -0700 Subject: [PATCH 03/50] Implementing new API structure for recursive cata. No loss in perf so far. --- src/dense_byte_node.rs | 27 ++++++++++---------- src/line_list_node.rs | 39 +++++++++++------------------ src/trie_map.rs | 9 +++---- src/trie_node.rs | 56 ++++++++++++++++++++++++------------------ 4 files changed, 64 insertions(+), 67 deletions(-) diff --git a/src/dense_byte_node.rs b/src/dense_byte_node.rs index 78b2e176..362f2d21 100644 --- a/src/dense_byte_node.rs +++ b/src/dense_byte_node.rs @@ -332,13 +332,12 @@ impl> ByteNode } #[inline(always)] - pub fn node_recursive_cata(&self, map_f: MapF, collapse_f: CollapseF, in_alg_f: InAlgF, out_alg_f: OutAlgF) -> W + pub fn node_recursive_cata(&self, collapse_f: CollapseF, branch_f: BranchF, finalize_f: FinalizeF) -> W where Acc: Default, - MapF: Copy + Fn(&V, &[u8]) -> W, - CollapseF: Copy + Fn(&V, W, &[u8]) -> W, - InAlgF: Copy + Fn(&ByteMask, W, &[u8], &mut Acc), - OutAlgF: Copy + Fn(&ByteMask, Acc, &[u8]) -> W, + CollapseF: Copy + Fn(Option<&V>, Option, &[u8]) -> W, + BranchF: Copy + Fn(&ByteMask, W, &mut Acc), + FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, { let mut mask_idx = 0; let mut lm = unsafe{ *self.mask.0.get_unchecked(0) }; @@ -362,18 +361,18 @@ impl> ByteNode }; //Do the recursive calling + //PERF NOTE: The reason we have two code paths around the call to `branch_f` instead of just doing + // `let w = cf.rec().map(|rec| recursive_cata(...))` is that the compiler won't optimize the implemntation + // of `branch_f` around whether `w` is none or not, if we go with one call to `branch_f`. That means we + // often pay for two dependent branches instead of one, and the difference was 25% to the val_count benchmark. if let Some(rec) = cf.rec() { - let w = recursive_cata::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(rec, map_f, collapse_f, in_alg_f, out_alg_f); - if let Some(v) = cf.val() { - in_alg_f(&self.mask, collapse_f(v, w, path), path, unsafe { ws.as_mut().unwrap_unchecked() }); - } else { - in_alg_f(&self.mask, w, path, unsafe { ws.as_mut().unwrap_unchecked() }); - } - } else if let Some(v) = cf.val() { - in_alg_f(&self.mask, map_f(v, path), path, unsafe { ws.as_mut().unwrap_unchecked() }); + let w = recursive_cata::<_, _, _, _, _, _, _, COMPUTE_PATH>(rec, collapse_f, branch_f, finalize_f); + branch_f(&self.mask, collapse_f(cf.val(), Some(w), path), unsafe { ws.as_mut().unwrap_unchecked() }); + } else { + branch_f(&self.mask, collapse_f(cf.val(), None, path), unsafe { ws.as_mut().unwrap_unchecked() }); } } - out_alg_f(&self.mask, unsafe { std::mem::take(&mut ws).unwrap_unchecked() }, &[]) + finalize_f(&self.mask, unsafe { std::mem::take(&mut ws).unwrap_unchecked() }) } } diff --git a/src/line_list_node.rs b/src/line_list_node.rs index 3a0fd466..155218e9 100644 --- a/src/line_list_node.rs +++ b/src/line_list_node.rs @@ -2743,13 +2743,12 @@ impl LineListNode { } #[inline(always)] - pub fn node_recursive_cata(&self, map_f: MapF, collapse_f: CollapseF, in_alg_f: InAlgF, out_alg_f: OutAlgF) -> W + pub fn node_recursive_cata(&self, collapse_f: CollapseF, branch_f: BranchF, finalize_f: FinalizeF) -> W where Acc: Default, - MapF: Copy + Fn(&V, &[u8]) -> W, - CollapseF: Copy + Fn(&V, W, &[u8]) -> W, - InAlgF: Copy + Fn(&ByteMask, W, &[u8], &mut Acc), - OutAlgF: Copy + Fn(&ByteMask, Acc, &[u8]) -> W, + CollapseF: Copy + Fn(Option<&V>, Option, &[u8]) -> W, + BranchF: Copy + Fn(&ByteMask, W, &mut Acc), + FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, { let mut ws = Some(Acc::default()); @@ -2777,37 +2776,29 @@ impl LineListNode { -//New plan for API. -// 3 closures. -// - closure to deal with straight paths and values. Fn(Option<&V>, Option, &[u8]) -> W -// - closure to deal with one downstream branch from a logical node. Fn(&ByteMask, W, &mut Acc) -// - closure to fold accumulator back into a W for the logical node. Fn(&ByteMask, Acc) -> W -// -//Then, the non-jumping API would take: -// 2 closures. -// - closure to deal with one downstream branch from a logical node. Fn(&ByteMask, W, &mut Acc) -// - closure to deal with each path byte / logical node. Fn(&ByteMask, Option<&V>, Acc) -> W - - if self.is_used_value_0() { - in_alg_f(&ByteMask::new(), map_f(unsafe { self.val_in_slot::<0>() }, &[]), &[], unsafe { ws.as_mut().unwrap_unchecked() }); + let downstream = collapse_f(Some(unsafe { self.val_in_slot::<0>() }), None, &[]); + branch_f(&ByteMask::new(), downstream, unsafe { ws.as_mut().unwrap_unchecked() }); } if self.is_used_value_1() { - in_alg_f(&ByteMask::new(), map_f(unsafe { self.val_in_slot::<1>() }, &[]), &[], unsafe { ws.as_mut().unwrap_unchecked() }); + let downstream = collapse_f(Some(unsafe { self.val_in_slot::<1>() }), None, &[]); + branch_f(&ByteMask::new(), downstream, unsafe { ws.as_mut().unwrap_unchecked() }); } if self.is_used_child_0() { let child_node = unsafe{ self.child_in_slot::<0>() }; - let w = recursive_cata::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(child_node, map_f, collapse_f, in_alg_f, out_alg_f); - in_alg_f(&ByteMask::new(), w, &[], unsafe { ws.as_mut().unwrap_unchecked() }); + let w = recursive_cata::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f); + let downstream = collapse_f(None, Some(w), &[]); + branch_f(&ByteMask::new(), downstream, unsafe { ws.as_mut().unwrap_unchecked() }); } if self.is_used_child_1() { let child_node = unsafe{ self.child_in_slot::<1>() }; - let w = recursive_cata::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(child_node, map_f, collapse_f, in_alg_f, out_alg_f); - in_alg_f(&ByteMask::new(), w, &[], unsafe { ws.as_mut().unwrap_unchecked() }); + let w = recursive_cata::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f); + let downstream = collapse_f(None, Some(w), &[]); + branch_f(&ByteMask::new(), downstream, unsafe { ws.as_mut().unwrap_unchecked() }); } - out_alg_f(&ByteMask::new(), unsafe { std::mem::take(&mut ws).unwrap_unchecked() }, &[]) + finalize_f(&ByteMask::new(), unsafe { std::mem::take(&mut ws).unwrap_unchecked() }) } } diff --git a/src/trie_map.rs b/src/trie_map.rs index 9a3d93ab..3b09d143 100644 --- a/src/trie_map.rs +++ b/src/trie_map.rs @@ -524,12 +524,11 @@ impl PathMap { // |bm, ws: &mut [usize], _| { ws.iter().sum() } // ) + root_val // Adam: this doesn't need to be called "traverse_osplit_cata" or be exposed under this interface; it can just live in morphisms - recursive_cata::<_, _, _, _, _, _, _, _, false>( + recursive_cata::<_, _, _, _, _, _, _, false>( root, - |v, _| { 1usize }, // on leaf values - |_, w, _| { 1 + w }, // on values amongst a path - |bm, w: usize, _, total| { *total += w }, // on merging children into a node - |bm, total: usize, _| { total } // finalizing a node + |v, w, _| { (v.is_some() as usize) + w.unwrap_or(0) }, // on values amongst a path + |bm, w: usize, total| { *total += w }, // on merging children into a node + |bm, total: usize| { total } // finalizing a node ) + root_val }, None => root_val diff --git a/src/trie_node.rs b/src/trie_node.rs index 1068a998..f083bef1 100644 --- a/src/trie_node.rs +++ b/src/trie_node.rs @@ -2506,21 +2506,32 @@ where } */ -// Adam: This seems to be a winner, though it needs some work, the split alg gives us the opportunity to nicely compose the different calls for the different node types without introducing overhead -/// Traverse a trie with a split catamorphism and caller-provided aggregation. +/// GOAT recursive caching cata. If this dev branch is successful this should replace the caching cata flavors in the public API +/// This is the JUMPING cata /// -/// Closure argument meanings: -/// - `map_f(&V, &[u8]) -> W`: map a leaf value to a result. Args are the value and -/// a path slice (currently always `&[]` in this implementation). -/// - `collapse_f(&V, W, &[u8]) -> W`: combine a value stored along a path with a -/// child result. Args are the value, the child result, and a path slice -/// (currently always `&[]`). -/// - `in_alg_f(&ByteMask, W, &[u8], &mut Alg)`: fold a single slot's result into -/// the node accumulator. Args are the node's mask, the slot result, a path -/// slice (currently always `&[]`), and the mutable accumulator. -/// - `out_alg_f(&ByteMask, Alg, &[u8]) -> W`: finalize a node accumulator into the -/// node's result. Args are the node's mask, the accumulator, and a path slice -/// (currently always `&[]`). +/// Closures: +/// +/// `CollapseF`: Folds a possible value and a possible downstream continuation, prefixed by a linear sub-path into a single `W` +/// `fn(val: Option<&V>, downstream: Option, prefix: &[u8]) -> W` +/// +/// `BranchF`: Accumulates the `W` representing a downstream branch into an `Acc` accumulator type +/// `fn(branch_mask: &ByteMask, downstream: W, accumulator: &mut Acc)` +/// +/// `FinalizeF`: Converts an `Acc` accumulator into a `W` representing the logical node +/// `fn(branch_mask: &ByteMask, accumulator: Acc) -> W` + +// +//New plan for API. +// 3 closures. +// - closure to deal with straight paths and values. Fn(Option<&V>, Option, &[u8]) -> W +// - closure to deal with one downstream branch from a logical node. Fn(&ByteMask, W, &mut Acc) +// - closure to fold accumulator back into a W for the logical node. Fn(&ByteMask, Acc) -> W +// +//Then, the non-jumping API would take: +// 2 closures. +// - closure to deal with one downstream branch from a logical node. Fn(&ByteMask, W, &mut Acc) +// - closure to deal with each path byte / logical node. Fn(&ByteMask, Option<&V>, Acc) -> W + //GOAT issues // - The reason it's faster than the other abstraction because it branches on node type once, rather than twice per node. @@ -2543,24 +2554,21 @@ where // * Put caching back // * See if I can get closer to the other cata API without sacrificing performance // -pub fn recursive_cata(node: &TrieNodeODRc, map_f: MapF, collapse_f: CollapseF, in_alg_f: InAlgF, out_alg_f: OutAlgF) -> W +pub fn recursive_cata(node: &TrieNodeODRc, collapse_f: CollapseF, branch_f: BranchF, finalize_f: FinalizeF) -> W where V: Clone + Send + Sync, A: Allocator, Acc: Default, - MapF: Copy + Fn(&V, &[u8]) -> W, - CollapseF: Copy + Fn(&V, W, &[u8]) -> W, - // InAlgF: called for each - InAlgF: Copy + Fn(&ByteMask, W, &[u8], &mut Acc), - // OutAlgF: collapses all children at the same level - OutAlgF: Copy + Fn(&ByteMask, Acc, &[u8]) -> W, + CollapseF: Copy + Fn(Option<&V>, Option, &[u8]) -> W, + BranchF: Copy + Fn(&ByteMask, W, &mut Acc), + FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, { match node.as_tagged() { - TaggedNodeRef::DenseByteNode(node) => { node.node_recursive_cata::<_, _, _, _, _, _, COMPUTE_PATH>(map_f, collapse_f, in_alg_f, out_alg_f) } - TaggedNodeRef::LineListNode(node) => { node.node_recursive_cata::<_, _, _, _, _, _, COMPUTE_PATH>(map_f, collapse_f, in_alg_f, out_alg_f) } + TaggedNodeRef::DenseByteNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH>(collapse_f, branch_f, finalize_f) } + TaggedNodeRef::LineListNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH>(collapse_f, branch_f, finalize_f) } TaggedNodeRef::CellByteNode(_) => { todo!() } TaggedNodeRef::TinyRefNode(_) => { todo!() } - TaggedNodeRef::EmptyNode => { out_alg_f(&ByteMask::new(), Acc::default(), &[]) } + TaggedNodeRef::EmptyNode => { finalize_f(&ByteMask::EMPTY, Acc::default()) } } } From cd6ea5cdd728af6d8f990b243ce92d5bd4d0fd32 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Tue, 20 Jan 2026 19:25:46 -0700 Subject: [PATCH 04/50] Slight tweak to node_recursive_cata for byte node, to give the optimizer more to work with. ~5% improvement to saturated val_count benchmark --- src/dense_byte_node.rs | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/dense_byte_node.rs b/src/dense_byte_node.rs index 362f2d21..9fb1d821 100644 --- a/src/dense_byte_node.rs +++ b/src/dense_byte_node.rs @@ -361,15 +361,27 @@ impl> ByteNode }; //Do the recursive calling - //PERF NOTE: The reason we have two code paths around the call to `branch_f` instead of just doing - // `let w = cf.rec().map(|rec| recursive_cata(...))` is that the compiler won't optimize the implemntation - // of `branch_f` around whether `w` is none or not, if we go with one call to `branch_f`. That means we - // often pay for two dependent branches instead of one, and the difference was 25% to the val_count benchmark. - if let Some(rec) = cf.rec() { - let w = recursive_cata::<_, _, _, _, _, _, _, COMPUTE_PATH>(rec, collapse_f, branch_f, finalize_f); - branch_f(&self.mask, collapse_f(cf.val(), Some(w), path), unsafe { ws.as_mut().unwrap_unchecked() }); - } else { - branch_f(&self.mask, collapse_f(cf.val(), None, path), unsafe { ws.as_mut().unwrap_unchecked() }); + //PERF NOTE: The reason we have four code paths around the call to `branch_f` instead of just doing + // `let w = cf.rec().map(|rec| recursive_cata(...))` and then calling branch_f with the appropriate + // pair of `Option<&V>` and `Option` is that the compiler won't optimize the implemntation of + // `branch_f` around whether the options are none or not. That means we often pay for two dependent + // branches instead of one, and the difference was 25% to the val_count benchmark. Breaking out the calls + // like this gives the optimizer a site to specialize for each permutation + match (cf.rec(), cf.val()) { + (Some(rec), Some(val)) => { + let w = recursive_cata::<_, _, _, _, _, _, _, COMPUTE_PATH>(rec, collapse_f, branch_f, finalize_f); + branch_f(&self.mask, collapse_f(Some(val), Some(w), path), unsafe { ws.as_mut().unwrap_unchecked() }); + }, + (Some(rec), None) => { + let w = recursive_cata::<_, _, _, _, _, _, _, COMPUTE_PATH>(rec, collapse_f, branch_f, finalize_f); + branch_f(&self.mask, collapse_f(None, Some(w), path), unsafe { ws.as_mut().unwrap_unchecked() }); + }, + (None, Some(val)) => { + branch_f(&self.mask, collapse_f(Some(val), None, path), unsafe { ws.as_mut().unwrap_unchecked() }); + }, + (None, None) => { + branch_f(&self.mask, collapse_f(None, None, path), unsafe { ws.as_mut().unwrap_unchecked() }); + }, } } finalize_f(&self.mask, unsafe { std::mem::take(&mut ws).unwrap_unchecked() }) From 3fb75fe74f413462bfb315fabc66e91b94f77bb2 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Tue, 20 Jan 2026 20:00:34 -0700 Subject: [PATCH 05/50] Getting rid of dead code --- src/trie_map.rs | 17 +----- src/trie_node.rs | 140 +---------------------------------------------- 2 files changed, 3 insertions(+), 154 deletions(-) diff --git a/src/trie_map.rs b/src/trie_map.rs index 3b09d143..aa835b06 100644 --- a/src/trie_map.rs +++ b/src/trie_map.rs @@ -511,24 +511,11 @@ impl PathMap { let root_val = unsafe{ &*self.root_val.get() }.is_some() as usize; match self.root() { Some(root) => { - // root.as_tagged().node_goat_val_count() + root_val - // traverse_physical(root, - // |node, ctx: usize| { ctx + node.node_goat_val_count() }, - // |ctx, child_ctx| { ctx + child_ctx }, - // ) + root_val - - // traverse_split_cata( - // root, - // |v, _| { 1usize }, - // |_, w, _| { 1 + w }, - // |bm, ws: &mut [usize], _| { ws.iter().sum() } - // ) + root_val - // Adam: this doesn't need to be called "traverse_osplit_cata" or be exposed under this interface; it can just live in morphisms recursive_cata::<_, _, _, _, _, _, _, false>( root, |v, w, _| { (v.is_some() as usize) + w.unwrap_or(0) }, // on values amongst a path - |bm, w: usize, total| { *total += w }, // on merging children into a node - |bm, total: usize| { total } // finalizing a node + |_mask, w: usize, total| { *total += w }, // on merging children into a node + |_mask, total: usize| { total } // finalizing a node ) + root_val }, None => root_val diff --git a/src/trie_node.rs b/src/trie_node.rs index f083bef1..dc5aa2f6 100644 --- a/src/trie_node.rs +++ b/src/trie_node.rs @@ -7,7 +7,7 @@ use dyn_clone::*; use local_or_heap::LocalOrHeap; use arrayvec::ArrayVec; -use crate::utils::{BitMask, ByteMask}; +use crate::utils::ByteMask; use crate::alloc::Allocator; use crate::dense_byte_node::*; use crate::ring::*; @@ -2372,140 +2372,6 @@ pub(crate) fn val_count_below_node(node: & } } -/// Recursively traverses a trie descending from `node`, visiting every physical non-empty node once -pub(crate) fn traverse_physical(node: &TrieNodeODRc, node_f: NodeF, fold_f: FoldF) -> Ctx - where - V: Clone + Send + Sync, - A: Allocator, - Ctx: Clone + Default, - NodeF: Fn(TaggedNodeRef, Ctx) -> Ctx + Copy, - FoldF: Fn(Ctx, Ctx) -> Ctx + Copy -{ - let mut cache = std::collections::HashMap::new(); - traverse_physical_internal(node, node_f, fold_f, &mut cache) -} - -fn traverse_physical_internal(node: &TrieNodeODRc, node_f: NodeF, fold_f: FoldF, cache: &mut HashMap) -> Ctx - where - V: Clone + Send + Sync, - A: Allocator, - Ctx: Clone + Default, - NodeF: Fn(TaggedNodeRef, Ctx) -> Ctx + Copy, - FoldF: Fn(Ctx, Ctx) -> Ctx + Copy -{ - if node.is_empty() { - return Ctx::default() - } - - if node.refcount() > 1 { - let hash = node.shared_node_id(); - match cache.get(&hash) { - Some(cached) => cached.clone(), - None => { - let ctx = traverse_physical_children_internal(node.as_tagged(), node_f, fold_f, cache); - cache.insert(hash, ctx.clone()); - ctx - }, - } - } else { - traverse_physical_children_internal(node.as_tagged(), node_f, fold_f, cache) - } -} - -fn traverse_physical_children_internal(node: TaggedNodeRef, node_f: NodeF, fold_f: FoldF, cache: &mut HashMap) -> Ctx - where - V: Clone + Send + Sync, - A: Allocator, - Ctx: Clone + Default, - NodeF: Fn(TaggedNodeRef, Ctx) -> Ctx + Copy, - FoldF: Fn(Ctx, Ctx) -> Ctx + Copy -{ - let mut ctx = Ctx::default(); - - match node { - TaggedNodeRef::DenseByteNode(n) => { - for cf in n.values.iter() { - if let Some(rec) = cf.rec() { - let child_ctx = traverse_physical_internal(rec, node_f, fold_f, cache); - ctx = fold_f(ctx, child_ctx); - } - } - } - TaggedNodeRef::LineListNode(n) => { - if n.is_used_child_0() { - let child_node = unsafe{ n.child_in_slot::<0>() }; - let child_ctx = traverse_physical_internal(child_node, node_f, fold_f, cache); - ctx = fold_f(ctx, child_ctx); - } - if n.is_used_child_1() { - let child_node = unsafe{ n.child_in_slot::<1>() }; - let child_ctx = traverse_physical_internal(child_node, node_f, fold_f, cache); - ctx = fold_f(ctx, child_ctx); - } - } - TaggedNodeRef::CellByteNode(_) => { todo!() } - TaggedNodeRef::TinyRefNode(_) => { todo!() } - TaggedNodeRef::EmptyNode => { todo!() } - } - - node_f(node, ctx) -} - -// This experiment is still OK, but the `&mut [W]` is awkward to instantiate if you don't actually have -/*pub fn traverse_split_cata<'a, A : Allocator, V : TrieValue, W, MapF, CollapseF, AlgF>(node: &TrieNodeODRc, mut map_f: MapF, mut collapse_f: CollapseF, alg_f: AlgF) -> W -where - MapF: Copy + FnMut(&V, &[u8]) -> W + 'a, - CollapseF: Copy + FnMut(&V, W, &[u8]) -> W + 'a, - AlgF: Copy + Fn(&ByteMask, &mut [W], &[u8]) -> W + 'a, -{ - match node.as_tagged() { - TaggedNodeRef::DenseByteNode(n) => { - let mut ws = [const { std::mem::MaybeUninit::::uninit() }; 256]; - // let mut ws: Vec> = Vec::with_capacity(n.mask.count_bits()); - // unsafe { ws.set_len(n.mask.count_bits()) }; - let mut c = 0; - for cf in n.values.iter() { - if let Some(rec) = cf.rec() { - let w = traverse_split_cata(rec, map_f, collapse_f, alg_f); - if let Some(v) = cf.val() { - ws[c].write(collapse_f(v, w, &[])); - } else { - ws[c].write(w); - } - } else if let Some(v) = cf.val() { - ws[c].write(map_f(v, &[])); - } - c += 1; - } - alg_f(&n.mask, unsafe { std::mem::transmute(&mut ws[..c]) }, &[]) - } - TaggedNodeRef::LineListNode(n) => { - // let mut ws = vec![]; - // if n.is_used_value_0() { - // ws.append(map_f(unsafe { n.val_in_slot::<0>() }, &[])); - // } - // if n.is_used_value_1() { - // ws.append(map_f(unsafe { n.val_in_slot::<1>() }, &[])); - // } - // if n.is_used_child_0() { - // let child_node = unsafe{ n.child_in_slot::<0>() }; - // let child_ctx = traverse_split_cata(child_node, map_f, collapse_f, alg_f); - // - // } - // if n.is_used_child_1() { - // let child_node = unsafe{ n.child_in_slot::<1>() }; - // let child_ctx = traverse_physical_internal(child_node, node_f, fold_f, cache); - // ctx = fold_f(ctx, child_ctx); - // } - alg_f(&ByteMask::new(), &mut [], &[]) - } - TaggedNodeRef::CellByteNode(_) => { todo!() } - TaggedNodeRef::TinyRefNode(_) => { todo!() } - TaggedNodeRef::EmptyNode => { todo!() } - } -} -*/ - /// GOAT recursive caching cata. If this dev branch is successful this should replace the caching cata flavors in the public API /// This is the JUMPING cata /// @@ -2550,9 +2416,7 @@ where //GOAT: // * Look at the callbacks in line node // * Send partial paths in pair node too -// * Come up with new names for in_alg and out_alg... // * Put caching back -// * See if I can get closer to the other cata API without sacrificing performance // pub fn recursive_cata(node: &TrieNodeODRc, collapse_f: CollapseF, branch_f: BranchF, finalize_f: FinalizeF) -> W where @@ -2623,8 +2487,6 @@ pub(crate) fn make_cell_node(node: &mut Tr // module come from the visibility of the trait it is derived on. In this case, `TrieNode` //Credit to QuineDot for his ideas on this pattern here: https://users.rust-lang.org/t/inferred-lifetime-for-dyn-trait/112116/7 pub(crate) use opaque_dyn_rc_trie_node::TrieNodeODRc; -use crate::morphisms::SplitCata; -use crate::TrieValue; #[cfg(not(feature = "slim_ptrs"))] mod opaque_dyn_rc_trie_node { From 69bce4139aeb309c589136bb478da554f27fd7cf Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Tue, 20 Jan 2026 23:30:20 -0700 Subject: [PATCH 06/50] Working through all the cases in pair node (on paper) --- src/line_list_node.rs | 49 ++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/src/line_list_node.rs b/src/line_list_node.rs index 155218e9..08e1ca9f 100644 --- a/src/line_list_node.rs +++ b/src/line_list_node.rs @@ -2752,28 +2752,33 @@ impl LineListNode { { let mut ws = Some(Acc::default()); -//GOAT, should we remove the path from out_alg?? I can't see when it's ever used... -// A: Either the path doesn't belong on the out_alg or on the in_alg. - -//GOAT, check out whether in_alg should get the path on the ByteNode - -//Cases: -// * There is only one val. Run map_f only -// * There is only one child. Recurse, then Run the in_alg -> out_alg combo -// * Both slots are filled -// GOAT, there is a problem where we have to care about whether it's a Val or child! -// - Is the first byte the same? -// N: -// - Call map_f on slot0 with the whole path -// - Call map_f on slot1 with the whole path -// - Call in_alg (branch_f) on each of the Ws -// - Call out_alg (fold_f) on the context with a composed mask from the first bytes -// Y: -// - Call map_f on slot0 with everything after first byte -// - Call map_f on slot1 with everything after first byte -// - Call in_alg (branch_f) on each of the Ws -// - Call out_alg (fold_f) on the context with a composed mask from the second bytes - +//Pair node can have the following permutations: (Slot0, Slot1) +// +// - (Empty, Empty) +// Run only `finalize_f` on default `Acc` +// - (Child, Empty) +// Recursively call on child, then run only `collapse_f` on the result, specifying the path +// - (Child, Val), 1-byte key, same key byte +// Recursively call on child, then run only `collapse_f` on the result, specifying the value and the 1-byte path +// - (Child, Val), different key bytes +// Recursively call on child, run `branch_f(collapse_f())` on the result. Then run `branch_f(collapse_f())` again +// with the value's path. And finally, run `finalize_f()` +// - (Child, Child) +// Recursively call on child0, run `branch_f(collapse_f())` on the result. Do the same for child1. Finally, run +// `finalize_f()` +// - (Val, Empty) +// Run only `collapse_f` on the val +// - (Val, Val), common first byte (meaning slot0 is a 1-byte path) +// run `collapse_f` on the slot1 val, specifying the path, then run `collapse_f` again on the slot0 val, specifying +// the common prefix byte +// - (Val, Val), different first bytes +// Run `branch_f(collapse_f())` on each val, then `finalize_f` at the end +// - (Val, Child), 1-byte key, same key byte (We could eliminate this case by requiring a canonical ordering for identical one-byte keys, but currently we don't) +// See "(Child, Val), 1-byte key, same key byte" +// - (Val, Child), different key bytes +// See (Child, Val), different key bytes +// +//GOAT, It would be nice to refactor the pair_node in order to express each of these permutations as a unique value for the 4 header bits, so we could take the appropriate code path without looking at any path bytes if self.is_used_value_0() { From 3a8e826fe9e1ccb3fbd42ce274ae70234df492ce Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Wed, 21 Jan 2026 18:12:00 -0700 Subject: [PATCH 07/50] Handling every case in the PairNode for recursive_cata (still no paths) --- src/line_list_node.rs | 194 +++++++++++++++++++++++++++++++----------- 1 file changed, 146 insertions(+), 48 deletions(-) diff --git a/src/line_list_node.rs b/src/line_list_node.rs index 08e1ca9f..1dce7438 100644 --- a/src/line_list_node.rs +++ b/src/line_list_node.rs @@ -2750,60 +2750,158 @@ impl LineListNode { BranchF: Copy + Fn(&ByteMask, W, &mut Acc), FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, { - let mut ws = Some(Acc::default()); + //Pair node can have the following permutations: (Slot0, Slot1) + // + // - Case 1 (Empty, Empty) + // Run only `finalize_f` on default `Acc` + // - Case 2 (Child, Empty) + // Recursively call on child, then run only `collapse_f` on the result, specifying the path + // - Case 3 (Child, Val), 1-byte key, same key byte + // Recursively call on child, then run only `collapse_f` on the result, specifying the value and the 1-byte path + // - Case 4 (Child, Val), different key bytes + // Recursively call on child, run `branch_f(collapse_f())` on the result. Then run `branch_f(collapse_f())` again + // with the value's path. And finally, run `finalize_f()` + // - Case 5 (Child, Child) + // Recursively call on child0, run `branch_f(collapse_f())` on the result. Do the same for child1. Finally, run + // `finalize_f()` + // - Case 6 (Val, Empty) + // Run only `collapse_f` on the val + // - Case 7 (Val, Val), common first byte (meaning slot0 is a 1-byte path) + // run `collapse_f` on the slot1 val, specifying the path, then run `collapse_f` again on the slot0 val, specifying + // the common prefix byte + // - Case 8 (Val, Val), different first bytes + // Run `branch_f(collapse_f())` on each val, then `finalize_f` at the end + // - Case 9 (Val, Child), 1-byte key, same key byte (We could eliminate this case by requiring a canonical ordering for identical one-byte keys, but currently we don't) + // See "Case 3 (Child, Val), 1-byte key, same key byte" + // - Case 10 (Val, Child), different key bytes + // See "Case 4 (Child, Val), different key bytes" + // + //GOAT, It would be nice to refactor the pair_node in order to express each of these permutations as a unique value for the 4 header bits, so we could take the appropriate code path without looking at any path bytes + + match self.header >> 12 { + //Case 1 (Empty, Empty) + 0 => finalize_f(&ByteMask::new(), Acc::default()), + //Case 2 (Child, Empty) = (1 << 3) + (1 << 1) | (1 << 3) + (1 << 1) + 1 + 10 | 11 => { + let child_node = unsafe{ self.child_in_slot::<0>() }; + let child_w = recursive_cata::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f); + collapse_f(None, Some(child_w), &[]) + }, + //(Child, Val) = (1 << 3) + (1 << 2) + (1 << 1) + 14 => { + let child_node = unsafe{ self.child_in_slot::<0>() }; + let child_w = recursive_cata::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f); + let (key0, key1) = self.get_both_keys(); + //GOAT, we check the length here to short-circuit checking the key bytes, which is likely a lot slower. But maybe not... Try it both ways + if key1.len() == 1 && unsafe{ key0.get_unchecked(0) == key1.get_unchecked(0) } { + //Case 3 + debug_assert_eq!(key0.len(), 1); + debug_assert_eq!(key1.len(), 1); + let val = unsafe { self.val_in_slot::<1>() }; + collapse_f(Some(val), Some(child_w), &[]) + } else { + //Case 4 + let mut acc = Acc::default(); + branch_f(&ByteMask::new(), collapse_f(None, Some(child_w), &[]), &mut acc); -//Pair node can have the following permutations: (Slot0, Slot1) -// -// - (Empty, Empty) -// Run only `finalize_f` on default `Acc` -// - (Child, Empty) -// Recursively call on child, then run only `collapse_f` on the result, specifying the path -// - (Child, Val), 1-byte key, same key byte -// Recursively call on child, then run only `collapse_f` on the result, specifying the value and the 1-byte path -// - (Child, Val), different key bytes -// Recursively call on child, run `branch_f(collapse_f())` on the result. Then run `branch_f(collapse_f())` again -// with the value's path. And finally, run `finalize_f()` -// - (Child, Child) -// Recursively call on child0, run `branch_f(collapse_f())` on the result. Do the same for child1. Finally, run -// `finalize_f()` -// - (Val, Empty) -// Run only `collapse_f` on the val -// - (Val, Val), common first byte (meaning slot0 is a 1-byte path) -// run `collapse_f` on the slot1 val, specifying the path, then run `collapse_f` again on the slot0 val, specifying -// the common prefix byte -// - (Val, Val), different first bytes -// Run `branch_f(collapse_f())` on each val, then `finalize_f` at the end -// - (Val, Child), 1-byte key, same key byte (We could eliminate this case by requiring a canonical ordering for identical one-byte keys, but currently we don't) -// See "(Child, Val), 1-byte key, same key byte" -// - (Val, Child), different key bytes -// See (Child, Val), different key bytes -// -//GOAT, It would be nice to refactor the pair_node in order to express each of these permutations as a unique value for the 4 header bits, so we could take the appropriate code path without looking at any path bytes + let val = unsafe { self.val_in_slot::<1>() }; + branch_f(&ByteMask::new(), collapse_f(Some(val), None, &[]), &mut acc); + finalize_f(&ByteMask::new(), acc) + } + }, + //Case 5 (Child, Child) = (1 << 3) + (1 << 2) + (1 << 1) + 1 + 15 => { + let mut acc = Acc::default(); + let child_node = unsafe{ self.child_in_slot::<0>() }; + let child_w = recursive_cata::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f); + branch_f(&ByteMask::new(), collapse_f(None, Some(child_w), &[]), &mut acc); + + let child_node = unsafe{ self.child_in_slot::<1>() }; + let child_w = recursive_cata::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f); + branch_f(&ByteMask::new(), collapse_f(None, Some(child_w), &[]), &mut acc); + + finalize_f(&ByteMask::new(), acc) + }, + //Case 6 (Val, Empty) = (1 << 3) | (1 << 3) + 1 + 8 | 9 => { + let val = unsafe { self.val_in_slot::<0>() }; + collapse_f(Some(val), None, &[]) + }, + //(Val, Val) = (1 << 3) + (1 << 2) + 12 => { + let (key0, key1) = self.get_both_keys(); + if unsafe{ key0.get_unchecked(0) == key1.get_unchecked(0) } { + //Case 7 (Val, Val), common first byte (meaning slot0 is a 1-byte path) + debug_assert_eq!(key0.len(), 1); + debug_assert!(key1.len() > 1); + let val = unsafe { self.val_in_slot::<1>() }; + let w1 = collapse_f(Some(val), None, &[]); + let val = unsafe { self.val_in_slot::<0>() }; + collapse_f(Some(val), Some(w1), &[]) + } else { + //Case 8 (Val, Val), different first bytes + let mut acc = Acc::default(); + let val = unsafe{ self.val_in_slot::<0>() }; + branch_f(&ByteMask::new(), collapse_f(Some(val), None, &[]), &mut acc); - if self.is_used_value_0() { - let downstream = collapse_f(Some(unsafe { self.val_in_slot::<0>() }), None, &[]); - branch_f(&ByteMask::new(), downstream, unsafe { ws.as_mut().unwrap_unchecked() }); - } - if self.is_used_value_1() { - let downstream = collapse_f(Some(unsafe { self.val_in_slot::<1>() }), None, &[]); - branch_f(&ByteMask::new(), downstream, unsafe { ws.as_mut().unwrap_unchecked() }); - } - if self.is_used_child_0() { - let child_node = unsafe{ self.child_in_slot::<0>() }; - let w = recursive_cata::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f); - let downstream = collapse_f(None, Some(w), &[]); - branch_f(&ByteMask::new(), downstream, unsafe { ws.as_mut().unwrap_unchecked() }); + let val = unsafe{ self.val_in_slot::<1>() }; + branch_f(&ByteMask::new(), collapse_f(Some(val), None, &[]), &mut acc); + finalize_f(&ByteMask::new(), acc) + } + }, + //(Val, Child) = (1 << 3) + (1 << 2) + 1 + 13 => { + let child_node = unsafe{ self.child_in_slot::<1>() }; + let child_w = recursive_cata::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f); + let (key0, key1) = self.get_both_keys(); + //GOAT, we check the length here to short-circuit checking the key bytes, which is likely a lot slower. But maybe not... Try it both ways + if key1.len() == 1 && unsafe{ key0.get_unchecked(0) == key1.get_unchecked(0) } { + //Case 9 (Val, Child), 1-byte key, same key byte (We could eliminate this case by requiring a canonical ordering for identical one-byte keys, but currently we don't) + debug_assert_eq!(key0.len(), 1); + debug_assert_eq!(key1.len(), 1); + let val = unsafe { self.val_in_slot::<0>() }; + collapse_f(Some(val), Some(child_w), &[]) + } else { + //Case 10 (Val, Child), different key bytes + let mut acc = Acc::default(); + branch_f(&ByteMask::new(), collapse_f(None, Some(child_w), &[]), &mut acc); + + let val = unsafe { self.val_in_slot::<0>() }; + branch_f(&ByteMask::new(), collapse_f(Some(val), None, &[]), &mut acc); + + finalize_f(&ByteMask::new(), acc) + } + }, + _ => { unsafe { unreachable_unchecked() } } } - if self.is_used_child_1() { - let child_node = unsafe{ self.child_in_slot::<1>() }; - let w = recursive_cata::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f); - let downstream = collapse_f(None, Some(w), &[]); - branch_f(&ByteMask::new(), downstream, unsafe { ws.as_mut().unwrap_unchecked() }); - } - finalize_f(&ByteMask::new(), unsafe { std::mem::take(&mut ws).unwrap_unchecked() }) + // let mut ws = Some(Acc::default()); + + // if self.is_used_value_0() { + // let downstream = collapse_f(Some(unsafe { self.val_in_slot::<0>() }), None, &[]); + // branch_f(&ByteMask::new(), downstream, unsafe { ws.as_mut().unwrap_unchecked() }); + // } + // if self.is_used_value_1() { + // let downstream = collapse_f(Some(unsafe { self.val_in_slot::<1>() }), None, &[]); + // branch_f(&ByteMask::new(), downstream, unsafe { ws.as_mut().unwrap_unchecked() }); + // } + // if self.is_used_child_0() { + // let child_node = unsafe{ self.child_in_slot::<0>() }; + // let w = recursive_cata::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f); + // let downstream = collapse_f(None, Some(w), &[]); + // branch_f(&ByteMask::new(), downstream, unsafe { ws.as_mut().unwrap_unchecked() }); + + // } + // if self.is_used_child_1() { + // let child_node = unsafe{ self.child_in_slot::<1>() }; + // let w = recursive_cata::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f); + // let downstream = collapse_f(None, Some(w), &[]); + // branch_f(&ByteMask::new(), downstream, unsafe { ws.as_mut().unwrap_unchecked() }); + // } + + // finalize_f(&ByteMask::new(), unsafe { std::mem::take(&mut ws).unwrap_unchecked() }) } } From 42c35878acfa18ee2dd37d7aecad2e6d37f79705 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Wed, 21 Jan 2026 21:16:07 -0700 Subject: [PATCH 08/50] Adding correct path handling to node_recursive_cata for PairNode --- src/line_list_node.rs | 120 +++++++++++++++++++++++++++++++++--------- src/utils/mod.rs | 20 +++++++ 2 files changed, 114 insertions(+), 26 deletions(-) diff --git a/src/line_list_node.rs b/src/line_list_node.rs index 1dce7438..17e809ab 100644 --- a/src/line_list_node.rs +++ b/src/line_list_node.rs @@ -2785,29 +2785,50 @@ impl LineListNode { 10 | 11 => { let child_node = unsafe{ self.child_in_slot::<0>() }; let child_w = recursive_cata::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f); - collapse_f(None, Some(child_w), &[]) + let path = if COMPUTE_PATH { + unsafe{ self.key_unchecked::<0>() } + } else { + &[] + }; + collapse_f(None, Some(child_w), path) }, //(Child, Val) = (1 << 3) + (1 << 2) + (1 << 1) 14 => { let child_node = unsafe{ self.child_in_slot::<0>() }; let child_w = recursive_cata::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f); - let (key0, key1) = self.get_both_keys(); - //GOAT, we check the length here to short-circuit checking the key bytes, which is likely a lot slower. But maybe not... Try it both ways - if key1.len() == 1 && unsafe{ key0.get_unchecked(0) == key1.get_unchecked(0) } { + let key0 = unsafe{ self.key_unchecked::<0>() }; + let key1 = unsafe{ self.key_unchecked::<1>() }; + let (key0_byte, key1_byte) = unsafe{ (*key0.get_unchecked(0), *key1.get_unchecked(0)) }; + if key0_byte == key1_byte { //Case 3 debug_assert_eq!(key0.len(), 1); debug_assert_eq!(key1.len(), 1); let val = unsafe { self.val_in_slot::<1>() }; - collapse_f(Some(val), Some(child_w), &[]) + let path = if COMPUTE_PATH { + key0 + } else { + &[] + }; + collapse_f(Some(val), Some(child_w), path) } else { //Case 4 let mut acc = Acc::default(); - branch_f(&ByteMask::new(), collapse_f(None, Some(child_w), &[]), &mut acc); + let (path, mask) = if COMPUTE_PATH { + (&key0[1..], ByteMask::from((key0_byte, key1_byte))) + } else { + (&[] as &[u8], ByteMask::new()) + }; + branch_f(&mask, collapse_f(None, Some(child_w), path), &mut acc); let val = unsafe { self.val_in_slot::<1>() }; - branch_f(&ByteMask::new(), collapse_f(Some(val), None, &[]), &mut acc); + let path = if COMPUTE_PATH { + &key1[1..] + } else { + &[] + }; + branch_f(&mask, collapse_f(Some(val), None, path), &mut acc); - finalize_f(&ByteMask::new(), acc) + finalize_f(&mask, acc) } }, //Case 5 (Child, Child) = (1 << 3) + (1 << 2) + (1 << 1) + 1 @@ -2815,63 +2836,110 @@ impl LineListNode { let mut acc = Acc::default(); let child_node = unsafe{ self.child_in_slot::<0>() }; let child_w = recursive_cata::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f); - branch_f(&ByteMask::new(), collapse_f(None, Some(child_w), &[]), &mut acc); + let (path0, path1, mask) = if COMPUTE_PATH { + let key0 = unsafe{ self.key_unchecked::<0>() }; + let key1 = unsafe{ self.key_unchecked::<1>() }; + let (key0_byte, key1_byte) = unsafe{ (*key0.get_unchecked(0), *key1.get_unchecked(0)) }; + (&key0[1..], &key1[1..], ByteMask::from((key0_byte, key1_byte))) + } else { + (&[] as &[u8], &[] as &[u8], ByteMask::new()) + }; + branch_f(&mask, collapse_f(None, Some(child_w), path0), &mut acc); let child_node = unsafe{ self.child_in_slot::<1>() }; let child_w = recursive_cata::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f); - branch_f(&ByteMask::new(), collapse_f(None, Some(child_w), &[]), &mut acc); + branch_f(&mask, collapse_f(None, Some(child_w), path1), &mut acc); - finalize_f(&ByteMask::new(), acc) + finalize_f(&mask, acc) }, //Case 6 (Val, Empty) = (1 << 3) | (1 << 3) + 1 8 | 9 => { let val = unsafe { self.val_in_slot::<0>() }; - collapse_f(Some(val), None, &[]) + let path = if COMPUTE_PATH { + unsafe{ self.key_unchecked::<0>() } + } else { + &[] + }; + collapse_f(Some(val), None, path) }, //(Val, Val) = (1 << 3) + (1 << 2) 12 => { - let (key0, key1) = self.get_both_keys(); - if unsafe{ key0.get_unchecked(0) == key1.get_unchecked(0) } { + let key0 = unsafe{ self.key_unchecked::<0>() }; + let key1 = unsafe{ self.key_unchecked::<1>() }; + let (key0_byte, key1_byte) = unsafe{ (*key0.get_unchecked(0), *key1.get_unchecked(0)) }; + if key0_byte == key1_byte { //Case 7 (Val, Val), common first byte (meaning slot0 is a 1-byte path) debug_assert_eq!(key0.len(), 1); debug_assert!(key1.len() > 1); let val = unsafe { self.val_in_slot::<1>() }; - let w1 = collapse_f(Some(val), None, &[]); + let path = if COMPUTE_PATH { + &key1[1..] + } else { + &[] + }; + let w1 = collapse_f(Some(val), None, path); let val = unsafe { self.val_in_slot::<0>() }; - collapse_f(Some(val), Some(w1), &[]) + let path = if COMPUTE_PATH { + &key1[0..1] + } else { + &[] + }; + collapse_f(Some(val), Some(w1), path) } else { //Case 8 (Val, Val), different first bytes let mut acc = Acc::default(); let val = unsafe{ self.val_in_slot::<0>() }; - branch_f(&ByteMask::new(), collapse_f(Some(val), None, &[]), &mut acc); + let (path0, path1, mask) = if COMPUTE_PATH { + (&key0[1..], &key1[1..], ByteMask::from((key0_byte, key1_byte))) + } else { + (&[] as &[u8], &[] as &[u8], ByteMask::new()) + }; + branch_f(&mask, collapse_f(Some(val), None, path0), &mut acc); let val = unsafe{ self.val_in_slot::<1>() }; - branch_f(&ByteMask::new(), collapse_f(Some(val), None, &[]), &mut acc); + branch_f(&mask, collapse_f(Some(val), None, path1), &mut acc); - finalize_f(&ByteMask::new(), acc) + finalize_f(&mask, acc) } }, //(Val, Child) = (1 << 3) + (1 << 2) + 1 13 => { let child_node = unsafe{ self.child_in_slot::<1>() }; let child_w = recursive_cata::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f); - let (key0, key1) = self.get_both_keys(); - //GOAT, we check the length here to short-circuit checking the key bytes, which is likely a lot slower. But maybe not... Try it both ways - if key1.len() == 1 && unsafe{ key0.get_unchecked(0) == key1.get_unchecked(0) } { + let key0 = unsafe{ self.key_unchecked::<0>() }; + let key1 = unsafe{ self.key_unchecked::<1>() }; + let (key0_byte, key1_byte) = unsafe{ (*key0.get_unchecked(0), *key1.get_unchecked(0)) }; + if key0_byte == key1_byte { //Case 9 (Val, Child), 1-byte key, same key byte (We could eliminate this case by requiring a canonical ordering for identical one-byte keys, but currently we don't) debug_assert_eq!(key0.len(), 1); debug_assert_eq!(key1.len(), 1); let val = unsafe { self.val_in_slot::<0>() }; - collapse_f(Some(val), Some(child_w), &[]) + let path = if COMPUTE_PATH { + key0 + } else { + &[] + }; + collapse_f(Some(val), Some(child_w), path) } else { //Case 10 (Val, Child), different key bytes let mut acc = Acc::default(); - branch_f(&ByteMask::new(), collapse_f(None, Some(child_w), &[]), &mut acc); + + let (path, mask) = if COMPUTE_PATH { + (&key1[1..], ByteMask::from((key0_byte, key1_byte))) + } else { + (&[] as &[u8], ByteMask::new()) + }; + branch_f(&ByteMask::new(), collapse_f(None, Some(child_w), path), &mut acc); let val = unsafe { self.val_in_slot::<0>() }; - branch_f(&ByteMask::new(), collapse_f(Some(val), None, &[]), &mut acc); + let path = if COMPUTE_PATH { + &key0[1..] + } else { + &[] + }; + branch_f(&mask, collapse_f(Some(val), None, path), &mut acc); - finalize_f(&ByteMask::new(), acc) + finalize_f(&mask, acc) } }, _ => { unsafe { unreachable_unchecked() } } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index a5c277cf..4f74b458 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -255,6 +255,26 @@ impl From for ByteMask { } } +impl From<(u8, u8)> for ByteMask { + #[inline] + fn from(byte_pair: (u8, u8)) -> Self { + let mut new_mask = Self::new(); + new_mask.set_bit(byte_pair.0); + new_mask.set_bit(byte_pair.1); + new_mask + } +} + +impl From<[u8; 2]> for ByteMask { + #[inline] + fn from(byte_pair: [u8; 2]) -> Self { + let mut new_mask = Self::new(); + new_mask.set_bit(byte_pair[0]); + new_mask.set_bit(byte_pair[1]); + new_mask + } +} + impl From<[u64; 4]> for ByteMask { #[inline] fn from(mask: [u64; 4]) -> Self { From 542c283e2d5bc850a7484adf66f36ead45b8b969 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Wed, 21 Jan 2026 21:31:13 -0700 Subject: [PATCH 09/50] Re-adding caching to recursive_cata. Slight perf hit, but it's unavoidable, and <5% --- src/dense_byte_node.rs | 7 ++-- src/line_list_node.rs | 15 +++++---- src/trie_node.rs | 75 +++++++++++++++++++++++++++++++----------- 3 files changed, 68 insertions(+), 29 deletions(-) diff --git a/src/dense_byte_node.rs b/src/dense_byte_node.rs index 9fb1d821..87a1f9e0 100644 --- a/src/dense_byte_node.rs +++ b/src/dense_byte_node.rs @@ -332,9 +332,10 @@ impl> ByteNode } #[inline(always)] - pub fn node_recursive_cata(&self, collapse_f: CollapseF, branch_f: BranchF, finalize_f: FinalizeF) -> W + pub fn node_recursive_cata(&self, collapse_f: CollapseF, branch_f: BranchF, finalize_f: FinalizeF, cache: &mut HashMap) -> W where Acc: Default, + W: Clone, CollapseF: Copy + Fn(Option<&V>, Option, &[u8]) -> W, BranchF: Copy + Fn(&ByteMask, W, &mut Acc), FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, @@ -369,11 +370,11 @@ impl> ByteNode // like this gives the optimizer a site to specialize for each permutation match (cf.rec(), cf.val()) { (Some(rec), Some(val)) => { - let w = recursive_cata::<_, _, _, _, _, _, _, COMPUTE_PATH>(rec, collapse_f, branch_f, finalize_f); + let w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH>(rec, collapse_f, branch_f, finalize_f, cache); branch_f(&self.mask, collapse_f(Some(val), Some(w), path), unsafe { ws.as_mut().unwrap_unchecked() }); }, (Some(rec), None) => { - let w = recursive_cata::<_, _, _, _, _, _, _, COMPUTE_PATH>(rec, collapse_f, branch_f, finalize_f); + let w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH>(rec, collapse_f, branch_f, finalize_f, cache); branch_f(&self.mask, collapse_f(None, Some(w), path), unsafe { ws.as_mut().unwrap_unchecked() }); }, (None, Some(val)) => { diff --git a/src/line_list_node.rs b/src/line_list_node.rs index 17e809ab..afa5ac8d 100644 --- a/src/line_list_node.rs +++ b/src/line_list_node.rs @@ -2743,9 +2743,10 @@ impl LineListNode { } #[inline(always)] - pub fn node_recursive_cata(&self, collapse_f: CollapseF, branch_f: BranchF, finalize_f: FinalizeF) -> W + pub fn node_recursive_cata(&self, collapse_f: CollapseF, branch_f: BranchF, finalize_f: FinalizeF, cache: &mut HashMap) -> W where Acc: Default, + W: Clone, CollapseF: Copy + Fn(Option<&V>, Option, &[u8]) -> W, BranchF: Copy + Fn(&ByteMask, W, &mut Acc), FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, @@ -2784,7 +2785,7 @@ impl LineListNode { //Case 2 (Child, Empty) = (1 << 3) + (1 << 1) | (1 << 3) + (1 << 1) + 1 10 | 11 => { let child_node = unsafe{ self.child_in_slot::<0>() }; - let child_w = recursive_cata::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f); + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f, cache); let path = if COMPUTE_PATH { unsafe{ self.key_unchecked::<0>() } } else { @@ -2795,7 +2796,7 @@ impl LineListNode { //(Child, Val) = (1 << 3) + (1 << 2) + (1 << 1) 14 => { let child_node = unsafe{ self.child_in_slot::<0>() }; - let child_w = recursive_cata::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f); + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f, cache); let key0 = unsafe{ self.key_unchecked::<0>() }; let key1 = unsafe{ self.key_unchecked::<1>() }; let (key0_byte, key1_byte) = unsafe{ (*key0.get_unchecked(0), *key1.get_unchecked(0)) }; @@ -2835,7 +2836,7 @@ impl LineListNode { 15 => { let mut acc = Acc::default(); let child_node = unsafe{ self.child_in_slot::<0>() }; - let child_w = recursive_cata::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f); + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f, cache); let (path0, path1, mask) = if COMPUTE_PATH { let key0 = unsafe{ self.key_unchecked::<0>() }; let key1 = unsafe{ self.key_unchecked::<1>() }; @@ -2847,7 +2848,7 @@ impl LineListNode { branch_f(&mask, collapse_f(None, Some(child_w), path0), &mut acc); let child_node = unsafe{ self.child_in_slot::<1>() }; - let child_w = recursive_cata::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f); + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f, cache); branch_f(&mask, collapse_f(None, Some(child_w), path1), &mut acc); finalize_f(&mask, acc) @@ -2905,7 +2906,7 @@ impl LineListNode { //(Val, Child) = (1 << 3) + (1 << 2) + 1 13 => { let child_node = unsafe{ self.child_in_slot::<1>() }; - let child_w = recursive_cata::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f); + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f, cache); let key0 = unsafe{ self.key_unchecked::<0>() }; let key1 = unsafe{ self.key_unchecked::<1>() }; let (key0_byte, key1_byte) = unsafe{ (*key0.get_unchecked(0), *key1.get_unchecked(0)) }; @@ -3586,4 +3587,4 @@ mod tests { // and ZipperMoving // 2. implement a val_count convenience on top of 1. -//GOAT, Paths in caching Cata: https://github.com/Adam-Vandervorst/PathMap/pull/8#discussion_r2004828957 \ No newline at end of file +//GOAT, Paths in caching Cata: https://github.com/Adam-Vandervorst/PathMap/pull/8#discussion_r2004828957 diff --git a/src/trie_node.rs b/src/trie_node.rs index dc5aa2f6..005cd535 100644 --- a/src/trie_node.rs +++ b/src/trie_node.rs @@ -2377,7 +2377,7 @@ pub(crate) fn val_count_below_node(node: & /// /// Closures: /// -/// `CollapseF`: Folds a possible value and a possible downstream continuation, prefixed by a linear sub-path into a single `W` +/// `CollapseF`: Folds a possible value and a possible downstream continuation, prefixed by a linear sub-path, into a single `W` /// `fn(val: Option<&V>, downstream: Option, prefix: &[u8]) -> W` /// /// `BranchF`: Accumulates the `W` representing a downstream branch into an `Acc` accumulator type @@ -2399,37 +2399,74 @@ pub(crate) fn val_count_below_node(node: & // - closure to deal with each path byte / logical node. Fn(&ByteMask, Option<&V>, Acc) -> W -//GOAT issues -// - The reason it's faster than the other abstraction because it branches on node type once, rather than twice per node. -// -// There is more stuff on the stack, meaning we're more likely to blow the stack -// -// * Needed to add caching -// * The logic in pair node was wrong, because there is no guarantee both sides are at the same level; fixing that added another branch -// -// Observations: -// If we want to count path-ends, MapF wouldn't work, but could pass Option<&V>, which would be fine -// It might be possible to unify MapF and CollapseF, but // //GOAT, TODO: Make a test to hit the stack overflow failure case // -//GOAT: -// * Look at the callbacks in line node -// * Send partial paths in pair node too -// * Put caching back -// pub fn recursive_cata(node: &TrieNodeODRc, collapse_f: CollapseF, branch_f: BranchF, finalize_f: FinalizeF) -> W where V: Clone + Send + Sync, A: Allocator, Acc: Default, + W: Clone, + CollapseF: Copy + Fn(Option<&V>, Option, &[u8]) -> W, + BranchF: Copy + Fn(&ByteMask, W, &mut Acc), + FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, +{ + let mut cache = HashMap::new(); + recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH>(node, collapse_f, branch_f, finalize_f, &mut cache) +} + +pub(crate) fn recursive_cata_cached( + node: &TrieNodeODRc, + collapse_f: CollapseF, + branch_f: BranchF, + finalize_f: FinalizeF, + cache: &mut HashMap, +) -> W +where + V: Clone + Send + Sync, + A: Allocator, + Acc: Default, + W: Clone, + CollapseF: Copy + Fn(Option<&V>, Option, &[u8]) -> W, + BranchF: Copy + Fn(&ByteMask, W, &mut Acc), + FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, +{ + if node.refcount() > 1 { + let hash = node.shared_node_id(); + match cache.get(&hash) { + Some(cached) => cached.clone(), + None => { + let w = recursive_cata_dispatch::<_, _, _, _, _, _, _, COMPUTE_PATH>(node, collapse_f, branch_f, finalize_f, cache); + cache.insert(hash, w.clone()); + w + }, + } + } else { + recursive_cata_dispatch::<_, _, _, _, _, _, _, COMPUTE_PATH>(node, collapse_f, branch_f, finalize_f, cache) + } +} + +#[inline(always)] +fn recursive_cata_dispatch( + node: &TrieNodeODRc, + collapse_f: CollapseF, + branch_f: BranchF, + finalize_f: FinalizeF, + cache: &mut HashMap, +) -> W +where + V: Clone + Send + Sync, + A: Allocator, + Acc: Default, + W: Clone, CollapseF: Copy + Fn(Option<&V>, Option, &[u8]) -> W, BranchF: Copy + Fn(&ByteMask, W, &mut Acc), FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, { match node.as_tagged() { - TaggedNodeRef::DenseByteNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH>(collapse_f, branch_f, finalize_f) } - TaggedNodeRef::LineListNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH>(collapse_f, branch_f, finalize_f) } + TaggedNodeRef::DenseByteNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH>(collapse_f, branch_f, finalize_f, cache) } + TaggedNodeRef::LineListNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH>(collapse_f, branch_f, finalize_f, cache) } TaggedNodeRef::CellByteNode(_) => { todo!() } TaggedNodeRef::TinyRefNode(_) => { todo!() } TaggedNodeRef::EmptyNode => { finalize_f(&ByteMask::EMPTY, Acc::default()) } From 32973bfb3f304463b07dfa855239fe30ab1dd2b9 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Wed, 21 Jan 2026 21:40:39 -0700 Subject: [PATCH 10/50] Putting empty-node check back into recursive cata, to avoid reading bad memory when retrieving the refcount of the empty node --- src/trie_node.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/trie_node.rs b/src/trie_node.rs index 005cd535..ccb67460 100644 --- a/src/trie_node.rs +++ b/src/trie_node.rs @@ -2377,7 +2377,7 @@ pub(crate) fn val_count_below_node(node: & /// /// Closures: /// -/// `CollapseF`: Folds a possible value and a possible downstream continuation, prefixed by a linear sub-path, into a single `W` +/// `CollapseF`: Folds a possible value and a possible downstream continuation, prefixed by a linear sub-path into a single `W` /// `fn(val: Option<&V>, downstream: Option, prefix: &[u8]) -> W` /// /// `BranchF`: Accumulates the `W` representing a downstream branch into an `Acc` accumulator type @@ -2432,7 +2432,7 @@ where BranchF: Copy + Fn(&ByteMask, W, &mut Acc), FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, { - if node.refcount() > 1 { + if !node.is_empty() && node.refcount() > 1 { let hash = node.shared_node_id(); match cache.get(&hash) { Some(cached) => cached.clone(), @@ -3015,6 +3015,7 @@ mod opaque_dyn_rc_trie_node { pub(crate) fn new_empty() -> Self { Self { ptr: SlimNodePtr::new_empty(), alloc: MaybeUninit::uninit() } } + #[inline(always)] pub(crate) fn is_empty(&self) -> bool { self.tag() == EMPTY_NODE_TAG } From ce6a8f874422ccc7ec31e20b05432052e56a6547 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Wed, 21 Jan 2026 21:50:40 -0700 Subject: [PATCH 11/50] Filling in remaining node type branches for recursive_cata --- src/tiny_node.rs | 11 +++++++++++ src/trie_node.rs | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/tiny_node.rs b/src/tiny_node.rs index 08d61fc3..f12584ca 100644 --- a/src/tiny_node.rs +++ b/src/tiny_node.rs @@ -121,6 +121,17 @@ impl<'a, V: Clone + Send + Sync, A: Allocator> TinyRefNode<'a, V, A> { fn key(&self) -> &[u8] { unsafe{ core::slice::from_raw_parts(self.key_bytes.as_ptr().cast(), self.key_len()) } } + + pub(crate) fn node_recursive_cata(&self, collapse_f: CollapseF, branch_f: BranchF, finalize_f: FinalizeF, cache: &mut HashMap) -> W + where + Acc: Default, + W: Clone, + CollapseF: Copy + Fn(Option<&V>, Option, &[u8]) -> W, + BranchF: Copy + Fn(&ByteMask, W, &mut Acc), + FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, + { + self.into_full().unwrap().node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH>(collapse_f, branch_f, finalize_f, cache) + } } impl<'a, V: Clone + Send + Sync, A: Allocator> TrieNode for TinyRefNode<'a, V, A> { diff --git a/src/trie_node.rs b/src/trie_node.rs index ccb67460..77d4730c 100644 --- a/src/trie_node.rs +++ b/src/trie_node.rs @@ -2467,8 +2467,8 @@ where match node.as_tagged() { TaggedNodeRef::DenseByteNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH>(collapse_f, branch_f, finalize_f, cache) } TaggedNodeRef::LineListNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH>(collapse_f, branch_f, finalize_f, cache) } - TaggedNodeRef::CellByteNode(_) => { todo!() } - TaggedNodeRef::TinyRefNode(_) => { todo!() } + TaggedNodeRef::CellByteNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH>(collapse_f, branch_f, finalize_f, cache) } + TaggedNodeRef::TinyRefNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH>(collapse_f, branch_f, finalize_f, cache) } TaggedNodeRef::EmptyNode => { finalize_f(&ByteMask::EMPTY, Acc::default()) } } } From 73130eaf6774bccd3893b863344d8e7a13354748 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Wed, 21 Jan 2026 22:02:15 -0700 Subject: [PATCH 12/50] Adding recursive_cata_stack_overflow_smoke test --- src/trie_node.rs | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/trie_node.rs b/src/trie_node.rs index 77d4730c..54ef9666 100644 --- a/src/trie_node.rs +++ b/src/trie_node.rs @@ -2398,10 +2398,6 @@ pub(crate) fn val_count_below_node(node: & // - closure to deal with one downstream branch from a logical node. Fn(&ByteMask, W, &mut Acc) // - closure to deal with each path byte / logical node. Fn(&ByteMask, Option<&V>, Acc) -> W - -// -//GOAT, TODO: Make a test to hit the stack overflow failure case -// pub fn recursive_cata(node: &TrieNodeODRc, collapse_f: CollapseF, branch_f: BranchF, finalize_f: FinalizeF) -> W where V: Clone + Send + Sync, @@ -3252,6 +3248,7 @@ mod tests { use crate::alloc::{GlobalAlloc, global_alloc}; use crate::line_list_node::LineListNode; use crate::trie_node::TrieNodeODRc; + use crate::trie_node::recursive_cata; use crate::PathMap; use crate::zipper::*; @@ -3284,4 +3281,26 @@ mod tests { node_ref.make_unique(); drop(cloned); } + + /// Finds the path_depth at which the recursive cata hits a stack overflow + /// + /// Empirically seems to be somewhere between 8 and 10 KBytes. But more branching, and thus fewer + /// bytes-per-node, will mean it will fail on shorter paths. + #[test] + fn recursive_cata_stack_overflow_smoke() { + const PATH_LEN: usize = 8_000; + + let mut map = PathMap::<()>::new(); + let path = vec![b'a'; PATH_LEN]; + map.set_val_at(&path, ()); + + let root = map.root().unwrap(); + let count = recursive_cata::<_, _, _, _, _, _, _, false>( + root, + |v, w, _| (v.is_some() as usize) + w.unwrap_or(0), + |_mask, w: usize, total| { *total += w }, + |_mask, total: usize| { total }, + ); + assert_eq!(count, 1); + } } From 58ea1f1df1d734cf2cc6dd8b3be544ee8039ae53 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Thu, 22 Jan 2026 23:47:38 -0700 Subject: [PATCH 13/50] Adding a test for recursive cata, and adding the stepping version with a test --- src/trie_node.rs | 160 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 154 insertions(+), 6 deletions(-) diff --git a/src/trie_node.rs b/src/trie_node.rs index 54ef9666..45cf21da 100644 --- a/src/trie_node.rs +++ b/src/trie_node.rs @@ -2393,10 +2393,9 @@ pub(crate) fn val_count_below_node(node: & // - closure to deal with one downstream branch from a logical node. Fn(&ByteMask, W, &mut Acc) // - closure to fold accumulator back into a W for the logical node. Fn(&ByteMask, Acc) -> W // -//Then, the non-jumping API would take: -// 2 closures. -// - closure to deal with one downstream branch from a logical node. Fn(&ByteMask, W, &mut Acc) -// - closure to deal with each path byte / logical node. Fn(&ByteMask, Option<&V>, Acc) -> W +// +//The non-jumping API would be the same, but collapse wouldn't take a prefix path, and instead `finalize_f(branch_f())` would be +// called in reverse order for each path byte pub fn recursive_cata(node: &TrieNodeODRc, collapse_f: CollapseF, branch_f: BranchF, finalize_f: FinalizeF) -> W where @@ -2412,6 +2411,44 @@ where recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH>(node, collapse_f, branch_f, finalize_f, &mut cache) } +/// A stepping (non-jumping) catamorphism for the trie. +/// +/// Use this when you need the cata to evaluate once per path byte, even across non-branching sub-paths. +/// Unlike the jumping version, `branch_f` and `finalize_f` will be called for every path byte. +/// +/// See [`recursive_cata`] for closure semantics; this stepping variant omits the prefix argument from `collapse_f`. +pub fn recursive_cata_stepping( + node: &TrieNodeODRc, + collapse_f: CollapseF, + branch_f: BranchF, + finalize_f: FinalizeF, +) -> W +where + V: Clone + Send + Sync, + A: Allocator, + Acc: Default, + W: Clone, + CollapseF: Copy + Fn(Option<&V>, Option) -> W, + BranchF: Copy + Fn(&ByteMask, W, &mut Acc), + FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, +{ + recursive_cata::<_, _, _, _, _, _, _, true>( + node, + |val, downstream, prefix| { + let mut w = collapse_f(val, downstream); + for byte in prefix.iter().rev() { + let mask = ByteMask::from(*byte); + let mut acc = Acc::default(); + branch_f(&mask, w, &mut acc); + w = finalize_f(&mask, acc); + } + w + }, + branch_f, + finalize_f, + ) +} + pub(crate) fn recursive_cata_cached( node: &TrieNodeODRc, collapse_f: CollapseF, @@ -3247,8 +3284,8 @@ impl DistributiveLat mod tests { use crate::alloc::{GlobalAlloc, global_alloc}; use crate::line_list_node::LineListNode; - use crate::trie_node::TrieNodeODRc; - use crate::trie_node::recursive_cata; + use crate::trie_node::{TrieNodeODRc, recursive_cata, recursive_cata_stepping}; + use crate::utils::ByteMask; use crate::PathMap; use crate::zipper::*; @@ -3282,6 +3319,117 @@ mod tests { drop(cloned); } + /// Adapted from morphisms::cata_test1 for recursive_cata (jumping). + //GOAT, this should be in morphisms, and only use the public API + #[test] + fn recursive_cata_jumping_sum_digits() { + let tests = [ + (vec![], 0), + (vec!["1"], 1), + (vec!["1", "2"], 3), + (vec!["1", "2", "3", "4", "5", "6"], 21), + (vec!["a1", "a2"], 3), + (vec!["a1", "a2", "a3", "a4", "a5", "a6"], 21), + (vec!["12345"], 5), + (vec!["1", "12", "123", "1234", "12345"], 15), + (vec!["123", "123456", "123789"], 18), + (vec!["12", "123", "123456", "123789"], 20), + (vec!["1", "2", "123", "123765", "1234", "12345", "12349"], 29), + ]; + + #[derive(Default)] + struct SumAcc { + idx: usize, + sum: u32, + } + + for (keys, expected_sum) in tests { + let map: PathMap<()> = keys.into_iter().map(|v| (v, ())).collect(); + let sum = match map.root() { + Some(root) => { + let w = recursive_cata::<_, _, _, _, _, _, _, true>( + root, + |val, downstream, prefix| { + let mut sum = downstream.map(|w: (bool, u32)| w.1).unwrap_or(0); + if val.is_some() { + if let Some(byte) = prefix.last() { + sum += (*byte as char).to_digit(10).unwrap(); + } + } + (val.is_some() && prefix.is_empty(), sum) + }, + |mask: &ByteMask, w: (bool, u32), acc: &mut SumAcc| { + let byte = mask.indexed_bit::(acc.idx).unwrap(); + acc.idx += 1; + if w.0 { + acc.sum += (byte as char).to_digit(10).unwrap(); + } + acc.sum += w.1; + }, + |_mask: &ByteMask, acc: SumAcc| { (false, acc.sum) }, + ); + w.1 + }, + None => 0, + }; + assert_eq!(sum, expected_sum); + } + } + + /// Adapted from morphisms::cata_test1 for recursive_cata_stepping (non-jumping). + //GOAT, this should be in morphisms, and only use the public API + #[test] + fn recursive_cata_stepping_sum_digits() { + let tests = [ + (vec![], 0), + (vec!["1"], 1), + (vec!["1", "2"], 3), + (vec!["1", "2", "3", "4", "5", "6"], 21), + (vec!["a1", "a2"], 3), + (vec!["a1", "a2", "a3", "a4", "a5", "a6"], 21), + (vec!["12345"], 5), + (vec!["1", "12", "123", "1234", "12345"], 15), + (vec!["123", "123456", "123789"], 18), + (vec!["12", "123", "123456", "123789"], 20), + (vec!["1", "2", "123", "123765", "1234", "12345", "12349"], 29), + ]; + + #[derive(Default)] + struct SumAcc { + idx: usize, + sum: u32, + } + + for (keys, expected_sum) in tests { + let map: PathMap<()> = keys.into_iter().map(|v| (v, ())).collect(); + let sum = match map.root() { + Some(root) => { + let w = recursive_cata_stepping::<_, _, SumAcc, (bool, u32), _, _, _>( + root, + |val, downstream| { + let sum = downstream.map(|w| w.1).unwrap_or(0); + (val.is_some(), sum) + }, + |mask: &ByteMask, w: (bool, u32), acc: &mut SumAcc| { + let byte = mask.iter().nth(acc.idx).unwrap(); + acc.idx += 1; + if w.0 { + acc.sum += (byte as char).to_digit(10).unwrap(); + } + acc.sum += w.1; + }, + |_mask: &ByteMask, acc: SumAcc| { + (false, acc.sum) + }, + ); + w.1 + }, + None => 0, + }; + assert_eq!(sum, expected_sum); + } + } + /// Finds the path_depth at which the recursive cata hits a stack overflow /// /// Empirically seems to be somewhere between 8 and 10 KBytes. But more branching, and thus fewer From df4e48adabef65050344b2446ebf00e5c0815756 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Fri, 23 Jan 2026 00:51:46 -0700 Subject: [PATCH 14/50] Reorganizing recursive_cata so it can be part of the public API --- src/dense_byte_node.rs | 5 +- src/line_list_node.rs | 4 +- src/morphisms.rs | 263 ++++++++++++++++++++++++++++++++++++++++- src/tiny_node.rs | 4 +- src/trie_map.rs | 7 +- src/trie_node.rs | 223 ++-------------------------------- 6 files changed, 278 insertions(+), 228 deletions(-) diff --git a/src/dense_byte_node.rs b/src/dense_byte_node.rs index 87a1f9e0..5c4144ce 100644 --- a/src/dense_byte_node.rs +++ b/src/dense_byte_node.rs @@ -1,7 +1,6 @@ use core::fmt::{Debug, Formatter}; use core::ptr; -use std::collections::HashMap; use std::hint::unreachable_unchecked; use crate::alloc::Allocator; @@ -12,6 +11,8 @@ use crate::utils::BitMask; use crate::trie_node::*; use crate::line_list_node::LineListNode; +use crate::gxhash::HashMap; + //NOTE: This: `core::array::from_fn(|i| i as u8);` ought to work, but https://github.com/rust-lang/rust/issues/109341 const ALL_BYTES: [u8; 256] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255]; @@ -1028,7 +1029,7 @@ impl> TrieNode } } } - fn node_val_count(&self, cache: &mut HashMap) -> usize { + fn node_val_count(&self, cache: &mut std::collections::HashMap) -> usize { //Discussion: These two implementations do the same thing but with a slightly different ordering of // the operations. In `all_dense_nodes`, the "Branchy" impl wins. But in a mixed-node setting, the // IMPL B is the winner. My suspicion is that the ListNode's heavily branching structure leads to diff --git a/src/line_list_node.rs b/src/line_list_node.rs index afa5ac8d..1e7844e6 100644 --- a/src/line_list_node.rs +++ b/src/line_list_node.rs @@ -1,6 +1,6 @@ use core::hint::unreachable_unchecked; use core::mem::{ManuallyDrop, MaybeUninit}; -use std::collections::HashMap; +use crate::gxhash::HashMap; use fast_slice_utils::{find_prefix_overlap, starts_with}; use local_or_heap::LocalOrHeap; @@ -1968,7 +1968,7 @@ impl TrieNode for LineListNode } } #[inline] - fn node_val_count(&self, cache: &mut HashMap) -> usize { + fn node_val_count(&self, cache: &mut std::collections::HashMap) -> usize { let mut result = 0; if self.is_used_value_0() { result += 1; diff --git a/src/morphisms.rs b/src/morphisms.rs index cc5b1f02..9fe0c88b 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -72,8 +72,10 @@ use crate::utils::*; use crate::alloc::Allocator; use crate::PathMap; use crate::trie_node::TrieNodeODRc; +use crate::trie_node::recursive_cata_cached; use crate::zipper; use crate::zipper::*; +use crate::zipper::zipper_priv::ZipperPriv; use crate::gxhash::{HashMap, HashMapExt}; @@ -213,8 +215,8 @@ pub trait Catamorphism { /// See [into_cata_cached](Catamorphism::into_cata_cached) for explanation of other arguments and behavior fn into_cata_jumping_cached(self, alg_f: AlgF) -> W where - W: Clone, - AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> W, + W: Clone, + AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> W, Self: Sized { self.into_cata_jumping_cached_fallible(|mask, children, val, sub_path| -> Result { @@ -231,6 +233,55 @@ pub trait Catamorphism { AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> Result; } +/// Provides faster catamorphism methods for types backed by an in-memory trie, such as [`PathMap`] +/// and some zipper implementations +pub trait Summarization { + + /// GOAT recursive cached cata. If this dev branch is successful this should replace the caching cata flavors in the public API + /// This is the JUMPING cata + /// + /// Closures: + /// + /// `CollapseF`: Folds a possible value and a possible downstream continuation, prefixed by a linear sub-path into a single `W` + /// `fn(val: Option<&V>, downstream: Option, prefix: &[u8]) -> W` + /// + /// `BranchF`: Accumulates the `W` representing a downstream branch into an `Acc` accumulator type + /// `fn(branch_mask: &ByteMask, downstream: W, accumulator: &mut Acc)` + /// + /// `FinalizeF`: Converts an `Acc` accumulator into a `W` representing the logical node + /// `fn(branch_mask: &ByteMask, accumulator: Acc) -> W` + /// + /// GOAT: The `COMPUTE_PATH` parameter shouldn't be necessary in a perfect world, but unfortunately the compiler + /// isn't very good at getting rid of the dead code, so passing `COMPUTE_PATH=false` gives a considerable speedup + /// at the expense of providing paths and reliable child_masks to the closures. + fn recursive_cata(&self, collapse_f: CollapseF, branch_f: BranchF, finalize_f: FinalizeF) -> W + where + V: Clone + Send + Sync, + Acc: Default, + W: Clone, + CollapseF: Copy + Fn(Option<&V>, Option, &[u8]) -> W, + BranchF: Copy + Fn(&ByteMask, W, &mut Acc), + FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, + Self: Sized; + + /// A stepping (non-jumping) catamorphism for the trie. + /// + /// Use this when you need the cata to evaluate once per path byte, even across non-branching sub-paths. + /// Unlike the jumping version, `branch_f` and `finalize_f` will be called for every path byte. + /// + /// See [`Catamorphism::recursive_cata`] for closure semantics; this stepping variant omits the prefix argument from `collapse_f`. + fn recursive_cata_stepping(&self, collapse_f: CollapseF, branch_f: BranchF, finalize_f: FinalizeF) -> W + where + V: Clone + Send + Sync, + Acc: Default, + W: Clone, + CollapseF: Copy + Fn(Option<&V>, Option) -> W, + BranchF: Copy + Fn(&ByteMask, W, &mut Acc), + FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, + Self: Sized; + +} + //TODO GOAT!!: It would be nice to get rid of this Default bound on all morphism Ws. In this case, the plan // for doing that would be to create a new type called a TakableSlice. It would be able to deref // into a regular mutable slice of `T` so it would work just like an ordinary slice. Additionally @@ -420,6 +471,99 @@ impl Catamorph } } +impl<'a, Z, V> Summarization for Z where Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer + ZipperPriv { + fn recursive_cata(&self, collapse_f: CollapseF, branch_f: BranchF, finalize_f: FinalizeF) -> W + where + V: Clone + Send + Sync, + Acc: Default, + W: Clone, + CollapseF: Copy + Fn(Option<&V>, Option, &[u8]) -> W, + BranchF: Copy + Fn(&ByteMask, W, &mut Acc), + FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, + { + let focus = self.get_focus(); + let w = match focus.borrow() { + Some(node) => { + let mut cache = HashMap::new(); + recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH>(node, collapse_f, branch_f, finalize_f, &mut cache) + }, + None => finalize_f(&ByteMask::EMPTY, Acc::default()), + }; + collapse_f(self.val(), Some(w), &[]) + } + + fn recursive_cata_stepping(&self, collapse_f: CollapseF, branch_f: BranchF, finalize_f: FinalizeF) -> W + where + V: Clone + Send + Sync, + Acc: Default, + W: Clone, + CollapseF: Copy + Fn(Option<&V>, Option) -> W, + BranchF: Copy + Fn(&ByteMask, W, &mut Acc), + FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, + { + self.recursive_cata::<_, _, _, _, _, true>( + |val, downstream, prefix| { + let mut w = collapse_f(val, downstream); + for byte in prefix.iter().rev() { + let mask = ByteMask::from(*byte); + let mut acc = Acc::default(); + branch_f(&mask, w, &mut acc); + w = finalize_f(&mask, acc); + } + w + }, + branch_f, + finalize_f, + ) + } +} + +impl Summarization for PathMap { + fn recursive_cata(&self, collapse_f: CollapseF, branch_f: BranchF, finalize_f: FinalizeF) -> W + where + V: Clone + Send + Sync, + Acc: Default, + W: Clone, + CollapseF: Copy + Fn(Option<&V>, Option, &[u8]) -> W, + BranchF: Copy + Fn(&ByteMask, W, &mut Acc), + FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, + { + let w = match self.root() { + Some(node) => { + let mut cache = HashMap::new(); + recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH>(node, collapse_f, branch_f, finalize_f, &mut cache) + }, + None => finalize_f(&ByteMask::EMPTY, Acc::default()), + }; + collapse_f(self.root_val(), Some(w), &[]) + } + + fn recursive_cata_stepping(&self, collapse_f: CollapseF, branch_f: BranchF, finalize_f: FinalizeF) -> W + where + V: Clone + Send + Sync, + Acc: Default, + W: Clone, + CollapseF: Copy + Fn(Option<&V>, Option) -> W, + BranchF: Copy + Fn(&ByteMask, W, &mut Acc), + FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, + { + self.recursive_cata::<_, _, _, _, _, true>( + |val, downstream, prefix| { + let mut w = collapse_f(val, downstream); + for byte in prefix.iter().rev() { + let mask = ByteMask::from(*byte); + let mut acc = Acc::default(); + branch_f(&mask, w, &mut acc); + w = finalize_f(&mask, acc); + } + w + }, + branch_f, + finalize_f, + ) + } +} + #[inline] fn cata_side_effect_body<'a, Z, V: 'a, W, Err, AlgF, const JUMPING: bool>(mut z: Z, mut alg_f: AlgF) -> Result where @@ -1956,6 +2100,121 @@ mod tests { eprintln!("calls_cached: {calls_cached}\ncalls_side: {calls_side}"); } + /// Adapted from morphisms::cata_test1 for recursive_cata (jumping). + #[test] + fn recursive_cata_jumping_sum_digits() { + let tests = [ + (vec![], 0), + (vec!["1"], 1), + (vec!["1", "2"], 3), + (vec!["1", "2", "3", "4", "5", "6"], 21), + (vec!["a1", "a2"], 3), + (vec!["a1", "a2", "a3", "a4", "a5", "a6"], 21), + (vec!["12345"], 5), + (vec!["1", "12", "123", "1234", "12345"], 15), + (vec!["123", "123456", "123789"], 18), + (vec!["12", "123", "123456", "123789"], 20), + (vec!["1", "2", "123", "123765", "1234", "12345", "12349"], 29), + ]; + + #[derive(Default)] + struct SumAcc { + idx: usize, + sum: u32, + } + + for (keys, expected_sum) in tests { + let map: PathMap<()> = keys.into_iter().map(|v| (v, ())).collect(); + let sum = map.recursive_cata::<_, _, _, _, _, true>( + |val, downstream, prefix| { + let mut sum = downstream.map(|w: (bool, u32)| w.1).unwrap_or(0); + if val.is_some() { + if let Some(byte) = prefix.last() { + sum += (*byte as char).to_digit(10).unwrap(); + } + } + (val.is_some() && prefix.is_empty(), sum) + }, + |mask: &ByteMask, w: (bool, u32), acc: &mut SumAcc| { + let byte = mask.indexed_bit::(acc.idx).unwrap(); + acc.idx += 1; + if w.0 { + acc.sum += (byte as char).to_digit(10).unwrap(); + } + acc.sum += w.1; + }, + |_mask: &ByteMask, acc: SumAcc| { (false, acc.sum) }, + ).1; + assert_eq!(sum, expected_sum); + } + } + + /// Adapted from morphisms::cata_test1 for recursive_cata_stepping (non-jumping). + #[test] + fn recursive_cata_stepping_sum_digits() { + let tests = [ + (vec![], 0), + (vec!["1"], 1), + (vec!["1", "2"], 3), + (vec!["1", "2", "3", "4", "5", "6"], 21), + (vec!["a1", "a2"], 3), + (vec!["a1", "a2", "a3", "a4", "a5", "a6"], 21), + (vec!["12345"], 5), + (vec!["1", "12", "123", "1234", "12345"], 15), + (vec!["123", "123456", "123789"], 18), + (vec!["12", "123", "123456", "123789"], 20), + (vec!["1", "2", "123", "123765", "1234", "12345", "12349"], 29), + ]; + + #[derive(Default)] + struct SumAcc { + idx: usize, + sum: u32, + } + + for (keys, expected_sum) in tests { + let map: PathMap<()> = keys.into_iter().map(|v| (v, ())).collect(); + let sum = map.recursive_cata_stepping::( + |val, downstream| { + let sum = downstream.map(|w| w.1).unwrap_or(0); + (val.is_some(), sum) + }, + |mask: &ByteMask, w: (bool, u32), acc: &mut SumAcc| { + let byte = mask.iter().nth(acc.idx).unwrap(); + acc.idx += 1; + if w.0 { + acc.sum += (byte as char).to_digit(10).unwrap(); + } + acc.sum += w.1; + }, + |_mask: &ByteMask, acc: SumAcc| { + (false, acc.sum) + }, + ).1; + assert_eq!(sum, expected_sum); + } + } + + /// Finds the path_depth at which the recursive cata hits a stack overflow + /// + /// Empirically seems to be somewhere between 8 and 10 KBytes. But more branching, and thus fewer + /// bytes-per-node, will mean it will fail on shorter paths. + #[test] + fn recursive_cata_stack_overflow_smoke() { + const PATH_LEN: usize = 8_000; + + let mut map = PathMap::<()>::new(); + let path = vec![b'a'; PATH_LEN]; + map.set_val_at(&path, ()); + + let count = map.recursive_cata::<_, _, _, _, _, false>( + |v, w, _| (v.is_some() as usize) + w.unwrap_or(0), + |_mask, w: usize, total| { *total += w }, + |_mask, total: usize| { total }, + ); + assert_eq!(count, 1); + } + /// Generate some basic tries using the [TrieBuilder::push_byte] API #[test] fn ana_test1() { diff --git a/src/tiny_node.rs b/src/tiny_node.rs index f12584ca..1c93f1a3 100644 --- a/src/tiny_node.rs +++ b/src/tiny_node.rs @@ -9,7 +9,7 @@ use core::mem::MaybeUninit; use core::fmt::{Debug, Formatter}; -use std::collections::HashMap; +use crate::gxhash::HashMap; use fast_slice_utils::{find_prefix_overlap, starts_with}; use crate::utils::ByteMask; @@ -228,7 +228,7 @@ impl<'a, V: Clone + Send + Sync, A: Allocator> TrieNode for TinyRefNode<'a fn new_iter_token(&self) -> u128 { unreachable!() } fn iter_token_for_path(&self, _key: &[u8]) -> u128 { unreachable!() } fn next_items(&self, _token: u128) -> (u128, &'a[u8], Option<&TrieNodeODRc>, Option<&V>) { unreachable!() } - fn node_val_count(&self, cache: &mut HashMap) -> usize { + fn node_val_count(&self, cache: &mut std::collections::HashMap) -> usize { let temp_node = self.into_full().unwrap(); temp_node.node_val_count(cache) } diff --git a/src/trie_map.rs b/src/trie_map.rs index aa835b06..76311499 100644 --- a/src/trie_map.rs +++ b/src/trie_map.rs @@ -1,7 +1,7 @@ use core::cell::UnsafeCell; use std::ptr::slice_from_raw_parts; use crate::alloc::{Allocator, GlobalAlloc, global_alloc}; -use crate::morphisms::{new_map_from_ana_in, Catamorphism, TrieBuilder}; +use crate::morphisms::{new_map_from_ana_in, Catamorphism, Summarization, TrieBuilder}; use crate::trie_node::*; use crate::zipper::*; use crate::merkleization::{MerkleizeResult, merkleize_impl}; @@ -510,9 +510,8 @@ impl PathMap { pub fn goat_val_count(&self) -> usize { let root_val = unsafe{ &*self.root_val.get() }.is_some() as usize; match self.root() { - Some(root) => { - recursive_cata::<_, _, _, _, _, _, _, false>( - root, + Some(_root) => { + self.recursive_cata::<_, _, _, _, _, false>( |v, w, _| { (v.is_some() as usize) + w.unwrap_or(0) }, // on values amongst a path |_mask, w: usize, total| { *total += w }, // on merging children into a node |_mask, total: usize| { total } // finalizing a node diff --git a/src/trie_node.rs b/src/trie_node.rs index 45cf21da..fb05bd2e 100644 --- a/src/trie_node.rs +++ b/src/trie_node.rs @@ -2,7 +2,6 @@ use core::hint::unreachable_unchecked; use core::mem::ManuallyDrop; use core::ptr::NonNull; -use std::collections::HashMap; use dyn_clone::*; use local_or_heap::LocalOrHeap; use arrayvec::ArrayVec; @@ -14,6 +13,8 @@ use crate::ring::*; use crate::tiny_node::TinyRefNode; use crate::line_list_node::LineListNode; +use crate::gxhash::HashMap; + #[cfg(feature = "bridge_nodes")] use crate::bridge_node::BridgeNode; @@ -220,7 +221,7 @@ pub(crate) trait TrieNode: TrieNodeDowncas /// Returns the total number of leaves contained within the whole subtree defined by the node /// GOAT, this should be deprecated - fn node_val_count(&self, cache: &mut HashMap) -> usize; + fn node_val_count(&self, cache: &mut std::collections::HashMap) -> usize; /// Returns the number of values contained within the node itself, irrespective of the positions within /// the node; does not include onward links @@ -1222,7 +1223,7 @@ mod tagged_node_ref { } #[inline] - pub fn node_val_count(&self, cache: &mut HashMap) -> usize { + pub fn node_val_count(&self, cache: &mut std::collections::HashMap) -> usize { match self { Self::DenseByteNode(node) => node.node_val_count(cache), Self::LineListNode(node) => node.node_val_count(cache), @@ -2353,7 +2354,7 @@ pub(crate) fn val_count_below_root(node: T node.node_val_count(&mut cache) } -pub(crate) fn val_count_below_node(node: &TrieNodeODRc, cache: &mut HashMap) -> usize { +pub(crate) fn val_count_below_node(node: &TrieNodeODRc, cache: &mut std::collections::HashMap) -> usize { if node.is_empty() { return 0 } @@ -2372,83 +2373,7 @@ pub(crate) fn val_count_below_node(node: & } } -/// GOAT recursive caching cata. If this dev branch is successful this should replace the caching cata flavors in the public API -/// This is the JUMPING cata -/// -/// Closures: -/// -/// `CollapseF`: Folds a possible value and a possible downstream continuation, prefixed by a linear sub-path into a single `W` -/// `fn(val: Option<&V>, downstream: Option, prefix: &[u8]) -> W` -/// -/// `BranchF`: Accumulates the `W` representing a downstream branch into an `Acc` accumulator type -/// `fn(branch_mask: &ByteMask, downstream: W, accumulator: &mut Acc)` -/// -/// `FinalizeF`: Converts an `Acc` accumulator into a `W` representing the logical node -/// `fn(branch_mask: &ByteMask, accumulator: Acc) -> W` - -// -//New plan for API. -// 3 closures. -// - closure to deal with straight paths and values. Fn(Option<&V>, Option, &[u8]) -> W -// - closure to deal with one downstream branch from a logical node. Fn(&ByteMask, W, &mut Acc) -// - closure to fold accumulator back into a W for the logical node. Fn(&ByteMask, Acc) -> W -// -// -//The non-jumping API would be the same, but collapse wouldn't take a prefix path, and instead `finalize_f(branch_f())` would be -// called in reverse order for each path byte - -pub fn recursive_cata(node: &TrieNodeODRc, collapse_f: CollapseF, branch_f: BranchF, finalize_f: FinalizeF) -> W -where - V: Clone + Send + Sync, - A: Allocator, - Acc: Default, - W: Clone, - CollapseF: Copy + Fn(Option<&V>, Option, &[u8]) -> W, - BranchF: Copy + Fn(&ByteMask, W, &mut Acc), - FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, -{ - let mut cache = HashMap::new(); - recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH>(node, collapse_f, branch_f, finalize_f, &mut cache) -} - -/// A stepping (non-jumping) catamorphism for the trie. -/// -/// Use this when you need the cata to evaluate once per path byte, even across non-branching sub-paths. -/// Unlike the jumping version, `branch_f` and `finalize_f` will be called for every path byte. -/// -/// See [`recursive_cata`] for closure semantics; this stepping variant omits the prefix argument from `collapse_f`. -pub fn recursive_cata_stepping( - node: &TrieNodeODRc, - collapse_f: CollapseF, - branch_f: BranchF, - finalize_f: FinalizeF, -) -> W -where - V: Clone + Send + Sync, - A: Allocator, - Acc: Default, - W: Clone, - CollapseF: Copy + Fn(Option<&V>, Option) -> W, - BranchF: Copy + Fn(&ByteMask, W, &mut Acc), - FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, -{ - recursive_cata::<_, _, _, _, _, _, _, true>( - node, - |val, downstream, prefix| { - let mut w = collapse_f(val, downstream); - for byte in prefix.iter().rev() { - let mask = ByteMask::from(*byte); - let mut acc = Acc::default(); - branch_f(&mask, w, &mut acc); - w = finalize_f(&mask, acc); - } - w - }, - branch_f, - finalize_f, - ) -} - +/// Internal implementation of recursive_cata pub(crate) fn recursive_cata_cached( node: &TrieNodeODRc, collapse_f: CollapseF, @@ -3284,8 +3209,7 @@ impl DistributiveLat mod tests { use crate::alloc::{GlobalAlloc, global_alloc}; use crate::line_list_node::LineListNode; - use crate::trie_node::{TrieNodeODRc, recursive_cata, recursive_cata_stepping}; - use crate::utils::ByteMask; + use crate::trie_node::TrieNodeODRc; use crate::PathMap; use crate::zipper::*; @@ -3318,137 +3242,4 @@ mod tests { node_ref.make_unique(); drop(cloned); } - - /// Adapted from morphisms::cata_test1 for recursive_cata (jumping). - //GOAT, this should be in morphisms, and only use the public API - #[test] - fn recursive_cata_jumping_sum_digits() { - let tests = [ - (vec![], 0), - (vec!["1"], 1), - (vec!["1", "2"], 3), - (vec!["1", "2", "3", "4", "5", "6"], 21), - (vec!["a1", "a2"], 3), - (vec!["a1", "a2", "a3", "a4", "a5", "a6"], 21), - (vec!["12345"], 5), - (vec!["1", "12", "123", "1234", "12345"], 15), - (vec!["123", "123456", "123789"], 18), - (vec!["12", "123", "123456", "123789"], 20), - (vec!["1", "2", "123", "123765", "1234", "12345", "12349"], 29), - ]; - - #[derive(Default)] - struct SumAcc { - idx: usize, - sum: u32, - } - - for (keys, expected_sum) in tests { - let map: PathMap<()> = keys.into_iter().map(|v| (v, ())).collect(); - let sum = match map.root() { - Some(root) => { - let w = recursive_cata::<_, _, _, _, _, _, _, true>( - root, - |val, downstream, prefix| { - let mut sum = downstream.map(|w: (bool, u32)| w.1).unwrap_or(0); - if val.is_some() { - if let Some(byte) = prefix.last() { - sum += (*byte as char).to_digit(10).unwrap(); - } - } - (val.is_some() && prefix.is_empty(), sum) - }, - |mask: &ByteMask, w: (bool, u32), acc: &mut SumAcc| { - let byte = mask.indexed_bit::(acc.idx).unwrap(); - acc.idx += 1; - if w.0 { - acc.sum += (byte as char).to_digit(10).unwrap(); - } - acc.sum += w.1; - }, - |_mask: &ByteMask, acc: SumAcc| { (false, acc.sum) }, - ); - w.1 - }, - None => 0, - }; - assert_eq!(sum, expected_sum); - } - } - - /// Adapted from morphisms::cata_test1 for recursive_cata_stepping (non-jumping). - //GOAT, this should be in morphisms, and only use the public API - #[test] - fn recursive_cata_stepping_sum_digits() { - let tests = [ - (vec![], 0), - (vec!["1"], 1), - (vec!["1", "2"], 3), - (vec!["1", "2", "3", "4", "5", "6"], 21), - (vec!["a1", "a2"], 3), - (vec!["a1", "a2", "a3", "a4", "a5", "a6"], 21), - (vec!["12345"], 5), - (vec!["1", "12", "123", "1234", "12345"], 15), - (vec!["123", "123456", "123789"], 18), - (vec!["12", "123", "123456", "123789"], 20), - (vec!["1", "2", "123", "123765", "1234", "12345", "12349"], 29), - ]; - - #[derive(Default)] - struct SumAcc { - idx: usize, - sum: u32, - } - - for (keys, expected_sum) in tests { - let map: PathMap<()> = keys.into_iter().map(|v| (v, ())).collect(); - let sum = match map.root() { - Some(root) => { - let w = recursive_cata_stepping::<_, _, SumAcc, (bool, u32), _, _, _>( - root, - |val, downstream| { - let sum = downstream.map(|w| w.1).unwrap_or(0); - (val.is_some(), sum) - }, - |mask: &ByteMask, w: (bool, u32), acc: &mut SumAcc| { - let byte = mask.iter().nth(acc.idx).unwrap(); - acc.idx += 1; - if w.0 { - acc.sum += (byte as char).to_digit(10).unwrap(); - } - acc.sum += w.1; - }, - |_mask: &ByteMask, acc: SumAcc| { - (false, acc.sum) - }, - ); - w.1 - }, - None => 0, - }; - assert_eq!(sum, expected_sum); - } - } - - /// Finds the path_depth at which the recursive cata hits a stack overflow - /// - /// Empirically seems to be somewhere between 8 and 10 KBytes. But more branching, and thus fewer - /// bytes-per-node, will mean it will fail on shorter paths. - #[test] - fn recursive_cata_stack_overflow_smoke() { - const PATH_LEN: usize = 8_000; - - let mut map = PathMap::<()>::new(); - let path = vec![b'a'; PATH_LEN]; - map.set_val_at(&path, ()); - - let root = map.root().unwrap(); - let count = recursive_cata::<_, _, _, _, _, _, _, false>( - root, - |v, w, _| (v.is_some() as usize) + w.unwrap_or(0), - |_mask, w: usize, total| { *total += w }, - |_mask, total: usize| { total }, - ); - assert_eq!(count, 1); - } } From 2567f33bdce20f0084771de8a8e69e1c399d9d6c Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Fri, 23 Jan 2026 01:22:59 -0700 Subject: [PATCH 15/50] Adding catamorphism benchmark to put two cata implementations head-to-head --- Cargo.toml | 4 ++ benches/catamorphism.rs | 105 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 benches/catamorphism.rs diff --git a/Cargo.toml b/Cargo.toml index 0cf1cecc..6982994b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -111,6 +111,10 @@ harness = false name = "product_zipper" harness = false +[[bench]] +name = "catamorphism" +harness = false + [[bench]] name = "sla" harness = false diff --git a/benches/catamorphism.rs b/benches/catamorphism.rs new file mode 100644 index 00000000..11d2fb55 --- /dev/null +++ b/benches/catamorphism.rs @@ -0,0 +1,105 @@ +use divan::{Divan, Bencher, black_box}; +use pathmap::morphisms::{Catamorphism, Summarization}; +use pathmap::utils::ByteMask; +use pathmap::utils::ints::gen_int_range; +use pathmap::PathMap; + +fn main() { + // Run registered benchmarks. + let divan = Divan::from_args() + .sample_count(4000); + + divan.main(); +} + +fn build_map(count: u64) -> PathMap<()> { + // Dense range of u64 keys encoded as paths; sized to keep benches fast and stable. + gen_int_range::<(), 8, u64>(0, count, 1, ()) +} + +const MAP_COUNT: u64 = 20_000_000; + +#[divan::bench()] +fn recursive_cata_jumping_val_count(bencher: Bencher) { + let map = build_map(MAP_COUNT); + let mut sink = 0usize; + bencher.bench_local(|| { + let rz = map.read_zipper(); + *black_box(&mut sink) = rz.recursive_cata::<_, _, _, _, _, false>( + |v, w, _| (v.is_some() as usize) + w.unwrap_or(0), + |_mask, w: usize, total| { *total += w }, + |_mask, total: usize| { total }, + ); + }); + assert_eq!(sink, MAP_COUNT as usize); +} + +#[divan::bench()] +fn cached_jumping_cata_val_count(bencher: Bencher) { + let map = build_map(MAP_COUNT); + let mut sink = 0usize; + bencher.bench_local(|| { + let rz = map.read_zipper(); + *black_box(&mut sink) = rz.into_cata_jumping_cached(|_mask: &ByteMask, children: &mut [usize], val, _sub_path| { + let mut sum: usize = children.iter().sum(); + if val.is_some() { + sum += 1; + } + sum + }); + }); + assert_eq!(sink, MAP_COUNT as usize); +} + +#[divan::bench()] +fn recursive_cata_jumping_total_len(bencher: Bencher) { + let map = build_map(MAP_COUNT); + let mut sink = (0usize, 0usize); + bencher.bench_local(|| { + let rz = map.read_zipper(); + *black_box(&mut sink) = rz.recursive_cata::<_, _, _, _, _, true>( + |val, downstream, prefix| { + let (mut count, mut total_len) = downstream.unwrap_or((0, 0)); + total_len += count * prefix.len(); + if val.is_some() { + count += 1; + total_len += prefix.len(); + } + (count, total_len) + }, + |mask: &ByteMask, w: (usize, usize), acc: &mut (usize, usize, usize)| { + let byte = mask.indexed_bit::(acc.0).unwrap(); + let _ = byte; // byte value unused; only length matters + acc.0 += 1; + acc.1 += w.0; + acc.2 += w.1; + }, + |_mask: &ByteMask, acc: (usize, usize, usize)| { (acc.1, acc.2) }, + ); + }); + assert_eq!(sink.0, MAP_COUNT as usize); +} + +#[divan::bench()] +fn cached_jumping_cata_total_len(bencher: Bencher) { + let map = build_map(MAP_COUNT); + let mut sink = (0usize, 0usize); + bencher.bench_local(|| { + let rz = map.read_zipper(); + *black_box(&mut sink) = rz.into_cata_jumping_cached(|mask: &ByteMask, children: &mut [(usize, usize)], val, sub_path| { + let mut count = 0usize; + let mut total_len = 0usize; + let prefix_len = sub_path.len(); + if val.is_some() { + count += 1; + total_len += prefix_len; + } + for (_byte, child) in mask.iter().zip(children.iter_mut()) { + count += child.0; + total_len += child.1 + child.0 * prefix_len; + } + (count, total_len) + }); + }); + assert_eq!(sink.0, MAP_COUNT as usize); +} From 95cdb38e783f34be51e1a6d12197fb3be12c0d0e Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Tue, 25 Aug 2026 21:03:44 -0600 Subject: [PATCH 16/50] Collapsing recursive_cata_stepping implementation into a trait default --- src/morphisms.rs | 68 ++++++++++++------------------------------------ 1 file changed, 17 insertions(+), 51 deletions(-) diff --git a/src/morphisms.rs b/src/morphisms.rs index 34c869a5..c15caa45 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -307,7 +307,23 @@ pub trait Summarization { CollapseF: Copy + Fn(Option<&V>, Option) -> W, BranchF: Copy + Fn(&ByteMask, W, &mut Acc), FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, - Self: Sized; + Self: Sized + { + self.recursive_cata::<_, _, _, _, _, true>( + |val, downstream, prefix| { + let mut w = collapse_f(val, downstream); + for byte in prefix.iter().rev() { + let mask = ByteMask::from(*byte); + let mut acc = Acc::default(); + branch_f(&mask, w, &mut acc); + w = finalize_f(&mask, acc); + } + w + }, + branch_f, + finalize_f, + ) + } } @@ -520,31 +536,6 @@ impl<'a, Z, V: Clone + Send + Sync, A: Allocator> Summarization for Z wher }; collapse_f(self.val(), Some(w), &[]) } - - fn recursive_cata_stepping(&self, collapse_f: CollapseF, branch_f: BranchF, finalize_f: FinalizeF) -> W - where - V: Clone + Send + Sync, - Acc: Default, - W: Clone, - CollapseF: Copy + Fn(Option<&V>, Option) -> W, - BranchF: Copy + Fn(&ByteMask, W, &mut Acc), - FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, - { - self.recursive_cata::<_, _, _, _, _, true>( - |val, downstream, prefix| { - let mut w = collapse_f(val, downstream); - for byte in prefix.iter().rev() { - let mask = ByteMask::from(*byte); - let mut acc = Acc::default(); - branch_f(&mask, w, &mut acc); - w = finalize_f(&mask, acc); - } - w - }, - branch_f, - finalize_f, - ) - } } impl Summarization for PathMap { @@ -566,31 +557,6 @@ impl Summarization for PathM }; collapse_f(self.root_val(), Some(w), &[]) } - - fn recursive_cata_stepping(&self, collapse_f: CollapseF, branch_f: BranchF, finalize_f: FinalizeF) -> W - where - V: Clone + Send + Sync, - Acc: Default, - W: Clone, - CollapseF: Copy + Fn(Option<&V>, Option) -> W, - BranchF: Copy + Fn(&ByteMask, W, &mut Acc), - FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, - { - self.recursive_cata::<_, _, _, _, _, true>( - |val, downstream, prefix| { - let mut w = collapse_f(val, downstream); - for byte in prefix.iter().rev() { - let mask = ByteMask::from(*byte); - let mut acc = Acc::default(); - branch_f(&mask, w, &mut acc); - w = finalize_f(&mask, acc); - } - w - }, - branch_f, - finalize_f, - ) - } } #[inline] From ee5ce1be0478a28bea0471c84c7c57e88e55ac17 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Wed, 26 Aug 2026 02:01:25 -0600 Subject: [PATCH 17/50] Tweaking the Summarization API to support a cheap(ish) shim for the old cata, and picking up another 5% perf --- benches/catamorphism.rs | 48 ++++++---- src/dense_byte_node.rs | 30 +++---- src/line_list_node.rs | 78 +++++++++-------- src/morphisms.rs | 189 +++++++++++++++++++++++----------------- src/tiny_node.rs | 13 ++- src/trie_map.rs | 13 ++- src/trie_node.rs | 40 ++++----- 7 files changed, 227 insertions(+), 184 deletions(-) diff --git a/benches/catamorphism.rs b/benches/catamorphism.rs index 11d2fb55..b2478659 100644 --- a/benches/catamorphism.rs +++ b/benches/catamorphism.rs @@ -25,10 +25,27 @@ fn recursive_cata_jumping_val_count(bencher: Bencher) { let mut sink = 0usize; bencher.bench_local(|| { let rz = map.read_zipper(); - *black_box(&mut sink) = rz.recursive_cata::<_, _, _, _, _, false>( - |v, w, _| (v.is_some() as usize) + w.unwrap_or(0), + *black_box(&mut sink) = rz.recursive_cata::<_, _, _, _, _, false, false>( + |_| 0usize, |_mask, w: usize, total| { *total += w }, - |_mask, total: usize| { total }, + |_mask, v, total, _| (v.is_some() as usize) + total.unwrap_or(0), + ); + }); + assert_eq!(sink, MAP_COUNT as usize); +} + +/// Same summary as `recursive_cata_jumping_val_count`, but requests real masks. +/// This isolates the cost of the `COMPUTE_MASK` specialization. +#[divan::bench()] +fn recursive_cata_jumping_val_count_with_masks(bencher: Bencher) { + let map = build_map(MAP_COUNT); + let mut sink = 0usize; + bencher.bench_local(|| { + let rz = map.read_zipper(); + *black_box(&mut sink) = rz.recursive_cata::<_, _, _, _, _, false, true>( + |_| 0usize, + |_mask, w: usize, total| { *total += w }, + |_mask, v, total, _| (v.is_some() as usize) + total.unwrap_or(0), ); }); assert_eq!(sink, MAP_COUNT as usize); @@ -57,24 +74,17 @@ fn recursive_cata_jumping_total_len(bencher: Bencher) { let mut sink = (0usize, 0usize); bencher.bench_local(|| { let rz = map.read_zipper(); - *black_box(&mut sink) = rz.recursive_cata::<_, _, _, _, _, true>( - |val, downstream, prefix| { - let (mut count, mut total_len) = downstream.unwrap_or((0, 0)); - total_len += count * prefix.len(); - if val.is_some() { - count += 1; - total_len += prefix.len(); - } - (count, total_len) + *black_box(&mut sink) = rz.recursive_cata::<_, _, _, _, _, true, true>( + |_| (0usize, 0usize), + |_mask: &ByteMask, w: (usize, usize), acc: &mut (usize, usize)| { + acc.0 += w.0; + acc.1 += w.1; }, - |mask: &ByteMask, w: (usize, usize), acc: &mut (usize, usize, usize)| { - let byte = mask.indexed_bit::(acc.0).unwrap(); - let _ = byte; // byte value unused; only length matters - acc.0 += 1; - acc.1 += w.0; - acc.2 += w.1; + |_mask: &ByteMask, val, acc, prefix| { + let (count, total_len) = acc.unwrap_or((0, 0)); + let count = count + val.is_some() as usize; + (count, total_len + count * prefix.len()) }, - |_mask: &ByteMask, acc: (usize, usize, usize)| { (acc.1, acc.2) }, ); }); assert_eq!(sink.0, MAP_COUNT as usize); diff --git a/src/dense_byte_node.rs b/src/dense_byte_node.rs index d2586c1b..48bf8165 100644 --- a/src/dense_byte_node.rs +++ b/src/dense_byte_node.rs @@ -9,9 +9,9 @@ use crate::utils::ByteMask; use crate::utils::BitMask; use crate::trie_node::*; -use crate::line_list_node::LineListNode; - use crate::gxhash::HashMap; +use crate::line_list_node::LineListNode; +use crate::morphisms::summarize_run; //NOTE: This: `core::array::from_fn(|i| i as u8);` ought to work, but https://github.com/rust-lang/rust/issues/109341 const ALL_BYTES: [u8; 256] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255]; @@ -388,17 +388,17 @@ impl> ByteNode } #[inline(always)] - pub fn node_recursive_cata(&self, collapse_f: CollapseF, branch_f: BranchF, finalize_f: FinalizeF, cache: &mut HashMap) -> W + pub(crate) fn node_recursive_cata(&self, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF, cache: &mut HashMap) -> W where - Acc: Default, W: Clone, - CollapseF: Copy + Fn(Option<&V>, Option, &[u8]) -> W, - BranchF: Copy + Fn(&ByteMask, W, &mut Acc), - FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, + StartF: Copy + Fn(&ByteMask) -> Acc, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc), + FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> W, { let mut mask_idx = 0; let mut lm = unsafe{ *self.mask.0.get_unchecked(0) }; - let mut ws = Some(Acc::default()); + let mask = if COMPUTE_MASK { &self.mask } else { &ByteMask::EMPTY }; + let mut ws = Some(start_f(mask)); for cf in self.values.iter() { //Compute the key byte. Hopefully this will all be stripped away by the compiler if the path isn't used //UPDATE: alas, my hopes were dashed. No amount of reorganizing this code, eliminating all traps, @@ -426,22 +426,22 @@ impl> ByteNode // like this gives the optimizer a site to specialize for each permutation match (cf.rec(), cf.val()) { (Some(rec), Some(val)) => { - let w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH>(rec, collapse_f, branch_f, finalize_f, cache); - branch_f(&self.mask, collapse_f(Some(val), Some(w), path), unsafe { ws.as_mut().unwrap_unchecked() }); + let w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(rec, start_f, fold_child_f, finalize_f, cache); + fold_child_f(mask, summarize_run::<_, _, _, _, _, _, COMPUTE_MASK>(Some(val), Some(w), path, start_f, fold_child_f, finalize_f), unsafe { ws.as_mut().unwrap_unchecked() }); }, (Some(rec), None) => { - let w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH>(rec, collapse_f, branch_f, finalize_f, cache); - branch_f(&self.mask, collapse_f(None, Some(w), path), unsafe { ws.as_mut().unwrap_unchecked() }); + let w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(rec, start_f, fold_child_f, finalize_f, cache); + fold_child_f(mask, summarize_run::<_, _, _, _, _, _, COMPUTE_MASK>(None, Some(w), path, start_f, fold_child_f, finalize_f), unsafe { ws.as_mut().unwrap_unchecked() }); }, (None, Some(val)) => { - branch_f(&self.mask, collapse_f(Some(val), None, path), unsafe { ws.as_mut().unwrap_unchecked() }); + fold_child_f(mask, summarize_run::<_, _, _, _, _, _, COMPUTE_MASK>(Some(val), None, path, start_f, fold_child_f, finalize_f), unsafe { ws.as_mut().unwrap_unchecked() }); }, (None, None) => { - branch_f(&self.mask, collapse_f(None, None, path), unsafe { ws.as_mut().unwrap_unchecked() }); + fold_child_f(mask, summarize_run::<_, _, _, _, _, _, COMPUTE_MASK>(None, None, path, start_f, fold_child_f, finalize_f), unsafe { ws.as_mut().unwrap_unchecked() }); }, } } - finalize_f(&self.mask, unsafe { std::mem::take(&mut ws).unwrap_unchecked() }) + finalize_f(mask, None, Some(unsafe { std::mem::take(&mut ws).unwrap_unchecked() }), &[]) } } diff --git a/src/line_list_node.rs b/src/line_list_node.rs index 0ee970e9..04d5e463 100644 --- a/src/line_list_node.rs +++ b/src/line_list_node.rs @@ -1,6 +1,5 @@ use core::hint::unreachable_unchecked; use core::mem::{ManuallyDrop, MaybeUninit}; -use crate::gxhash::HashMap; use fast_slice_utils::{find_prefix_overlap, starts_with}; use local_or_heap::LocalOrHeap; @@ -8,9 +7,11 @@ use local_or_heap::LocalOrHeap; use crate::utils::{BitMask, ByteMask}; use crate::alloc::Allocator; use crate::trie_node::*; +use crate::gxhash::HashMap; use crate::ring::*; use crate::dense_byte_node::{DenseByteNode, ByteNode, CoFree, OrdinaryCoFree, CellCoFree}; use crate::tiny_node::TinyRefNode; +use crate::morphisms::summarize_run; /// A LineListNode stores up to 2 children in a single cache line #[repr(C)] @@ -2780,14 +2781,18 @@ impl LineListNode { } #[inline(always)] - pub fn node_recursive_cata(&self, collapse_f: CollapseF, branch_f: BranchF, finalize_f: FinalizeF, cache: &mut HashMap) -> W + pub(crate) fn node_recursive_cata(&self, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF, cache: &mut HashMap) -> W where - Acc: Default, W: Clone, - CollapseF: Copy + Fn(Option<&V>, Option, &[u8]) -> W, - BranchF: Copy + Fn(&ByteMask, W, &mut Acc), - FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, + StartF: Copy + Fn(&ByteMask) -> Acc, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc), + FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> W, { + macro_rules! summarize { + ($val:expr, $downstream:expr, $prefix:expr) => { + summarize_run::<_, _, _, _, _, _, COMPUTE_MASK>($val, $downstream, $prefix, start_f, fold_child_f, finalize_f) + }; + } //Pair node can have the following permutations: (Slot0, Slot1) // // - Case 1 (Empty, Empty) @@ -2818,22 +2823,22 @@ impl LineListNode { match self.header >> 12 { //Case 1 (Empty, Empty) - 0 => finalize_f(&ByteMask::new(), Acc::default()), + 0 => finalize_f(&ByteMask::EMPTY, None, None, &[]), //Case 2 (Child, Empty) = (1 << 3) + (1 << 1) | (1 << 3) + (1 << 1) + 1 10 | 11 => { let child_node = unsafe{ self.child_in_slot::<0>() }; - let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f, cache); + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, start_f, fold_child_f, finalize_f, cache); let path = if COMPUTE_PATH { unsafe{ self.key_unchecked::<0>() } } else { &[] }; - collapse_f(None, Some(child_w), path) + summarize!(None, Some(child_w), path) }, //(Child, Val) = (1 << 3) + (1 << 2) + (1 << 1) 14 => { let child_node = unsafe{ self.child_in_slot::<0>() }; - let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f, cache); + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, start_f, fold_child_f, finalize_f, cache); let key0 = unsafe{ self.key_unchecked::<0>() }; let key1 = unsafe{ self.key_unchecked::<1>() }; let (key0_byte, key1_byte) = unsafe{ (*key0.get_unchecked(0), *key1.get_unchecked(0)) }; @@ -2847,16 +2852,16 @@ impl LineListNode { } else { &[] }; - collapse_f(Some(val), Some(child_w), path) + summarize!(Some(val), Some(child_w), path) } else { //Case 4 - let mut acc = Acc::default(); let (path, mask) = if COMPUTE_PATH { (&key0[1..], ByteMask::from((key0_byte, key1_byte))) } else { (&[] as &[u8], ByteMask::new()) }; - branch_f(&mask, collapse_f(None, Some(child_w), path), &mut acc); + let mut acc = start_f(&mask); + fold_child_f(&mask, summarize!(None, Some(child_w), path), &mut acc); let val = unsafe { self.val_in_slot::<1>() }; let path = if COMPUTE_PATH { @@ -2864,16 +2869,13 @@ impl LineListNode { } else { &[] }; - branch_f(&mask, collapse_f(Some(val), None, path), &mut acc); + fold_child_f(&mask, summarize!(Some(val), None, path), &mut acc); - finalize_f(&mask, acc) + finalize_f(&mask, None, Some(acc), &[]) } }, //Case 5 (Child, Child) = (1 << 3) + (1 << 2) + (1 << 1) + 1 15 => { - let mut acc = Acc::default(); - let child_node = unsafe{ self.child_in_slot::<0>() }; - let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f, cache); let (path0, path1, mask) = if COMPUTE_PATH { let key0 = unsafe{ self.key_unchecked::<0>() }; let key1 = unsafe{ self.key_unchecked::<1>() }; @@ -2882,13 +2884,16 @@ impl LineListNode { } else { (&[] as &[u8], &[] as &[u8], ByteMask::new()) }; - branch_f(&mask, collapse_f(None, Some(child_w), path0), &mut acc); + let mut acc = start_f(&mask); + let child_node = unsafe{ self.child_in_slot::<0>() }; + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, start_f, fold_child_f, finalize_f, cache); + fold_child_f(&mask, summarize!(None, Some(child_w), path0), &mut acc); let child_node = unsafe{ self.child_in_slot::<1>() }; - let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f, cache); - branch_f(&mask, collapse_f(None, Some(child_w), path1), &mut acc); + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, start_f, fold_child_f, finalize_f, cache); + fold_child_f(&mask, summarize!(None, Some(child_w), path1), &mut acc); - finalize_f(&mask, acc) + finalize_f(&mask, None, Some(acc), &[]) }, //Case 6 (Val, Empty) = (1 << 3) | (1 << 3) + 1 8 | 9 => { @@ -2898,7 +2903,7 @@ impl LineListNode { } else { &[] }; - collapse_f(Some(val), None, path) + summarize!(Some(val), None, path) }, //(Val, Val) = (1 << 3) + (1 << 2) 12 => { @@ -2915,35 +2920,35 @@ impl LineListNode { } else { &[] }; - let w1 = collapse_f(Some(val), None, path); + let w1 = summarize!(Some(val), None, path); let val = unsafe { self.val_in_slot::<0>() }; let path = if COMPUTE_PATH { &key1[0..1] } else { &[] }; - collapse_f(Some(val), Some(w1), path) + summarize!(Some(val), Some(w1), path) } else { //Case 8 (Val, Val), different first bytes - let mut acc = Acc::default(); - let val = unsafe{ self.val_in_slot::<0>() }; let (path0, path1, mask) = if COMPUTE_PATH { (&key0[1..], &key1[1..], ByteMask::from((key0_byte, key1_byte))) } else { (&[] as &[u8], &[] as &[u8], ByteMask::new()) }; - branch_f(&mask, collapse_f(Some(val), None, path0), &mut acc); + let mut acc = start_f(&mask); + let val = unsafe{ self.val_in_slot::<0>() }; + fold_child_f(&mask, summarize!(Some(val), None, path0), &mut acc); let val = unsafe{ self.val_in_slot::<1>() }; - branch_f(&mask, collapse_f(Some(val), None, path1), &mut acc); + fold_child_f(&mask, summarize!(Some(val), None, path1), &mut acc); - finalize_f(&mask, acc) + finalize_f(&mask, None, Some(acc), &[]) } }, //(Val, Child) = (1 << 3) + (1 << 2) + 1 13 => { let child_node = unsafe{ self.child_in_slot::<1>() }; - let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH>(child_node, collapse_f, branch_f, finalize_f, cache); + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, start_f, fold_child_f, finalize_f, cache); let key0 = unsafe{ self.key_unchecked::<0>() }; let key1 = unsafe{ self.key_unchecked::<1>() }; let (key0_byte, key1_byte) = unsafe{ (*key0.get_unchecked(0), *key1.get_unchecked(0)) }; @@ -2957,17 +2962,16 @@ impl LineListNode { } else { &[] }; - collapse_f(Some(val), Some(child_w), path) + summarize!(Some(val), Some(child_w), path) } else { //Case 10 (Val, Child), different key bytes - let mut acc = Acc::default(); - let (path, mask) = if COMPUTE_PATH { (&key1[1..], ByteMask::from((key0_byte, key1_byte))) } else { (&[] as &[u8], ByteMask::new()) }; - branch_f(&ByteMask::new(), collapse_f(None, Some(child_w), path), &mut acc); + let mut acc = start_f(&mask); + fold_child_f(&mask, summarize!(None, Some(child_w), path), &mut acc); let val = unsafe { self.val_in_slot::<0>() }; let path = if COMPUTE_PATH { @@ -2975,9 +2979,9 @@ impl LineListNode { } else { &[] }; - branch_f(&mask, collapse_f(Some(val), None, path), &mut acc); + fold_child_f(&mask, summarize!(Some(val), None, path), &mut acc); - finalize_f(&mask, acc) + finalize_f(&mask, None, Some(acc), &[]) } }, _ => { unsafe { unreachable_unchecked() } } diff --git a/src/morphisms.rs b/src/morphisms.rs index c15caa45..cd78adb5 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -265,66 +265,73 @@ pub trait Catamorphism { /// Provides faster catamorphism methods for types backed by an in-memory trie, such as [`PathMap`] /// and some zipper implementations pub trait Summarization { - - /// GOAT recursive cached cata. If this dev branch is successful this should replace the caching cata flavors in the public API - /// This is the JUMPING cata + /// GOAT recursive cached cata. If this dev branch is successful this should replace the caching cata flavors in the public API. + /// + /// JUMPING catamorphism implemented with recursion, for performance + /// + /// Each invocation of `finalize_f` may represent a whole non-branching + /// run of path bytes, supplied as `prefix`, rather than just one path byte. /// /// Closures: /// - /// `CollapseF`: Folds a possible value and a possible downstream continuation, prefixed by a linear sub-path into a single `W` - /// `fn(val: Option<&V>, downstream: Option, prefix: &[u8]) -> W` + /// `StartF`: Creates an accumulator for a logical node with more than one child branch (for jumping) + /// `fn(child_mask: &ByteMask) -> Acc` /// - /// `BranchF`: Accumulates the `W` representing a downstream branch into an `Acc` accumulator type - /// `fn(branch_mask: &ByteMask, downstream: W, accumulator: &mut Acc)` + /// `FoldChildF`: Folds one downstream child branch's `W` into the accumulator. It is called once for each + /// downstream result, in the same order as the bits in `child_mask`. Each call for a given + /// logical node receives the same full child mask; the callback can use the ordinal of its calls + /// to associate a result with a particular mask bit. + /// `fn(child_mask: &ByteMask, downstream: W, accumulator: &mut Acc)` /// - /// `FinalizeF`: Converts an `Acc` accumulator into a `W` representing the logical node - /// `fn(branch_mask: &ByteMask, accumulator: Acc) -> W` + /// `FinalizeF`: Converts the value (if present) and the optional child accumulator (if one exists) + /// into the `W` which summarizes the subtrie. `accumulator` is `None` when there are no downstream + /// results, indicating that `StartF` and `FoldChildF` were not called. `prefix` is the non-branching + /// sub-path leading to this invocation. + /// `fn(child_mask: &ByteMask, value: Option<&V>, accumulator: Option, prefix: &[u8]) -> W` /// - /// GOAT: The `COMPUTE_PATH` parameter shouldn't be necessary in a perfect world, but unfortunately the compiler - /// isn't very good at getting rid of the dead code, so passing `COMPUTE_PATH=false` gives a considerable speedup - /// at the expense of providing paths and reliable child_masks to the closures. - fn recursive_cata(&self, collapse_f: CollapseF, branch_f: BranchF, finalize_f: FinalizeF) -> W + /// In a callback where `COMPUTE_MASK` is false, `child_mask` is [`ByteMask::EMPTY`]. This avoids + /// materializing masks for callers that do not use them. Similarly, `COMPUTE_PATH=false` avoids + /// materializing path runs and passes an empty `prefix`. These are independent controls: callers + /// which need masks but not paths should use `COMPUTE_PATH=false, COMPUTE_MASK=true`. + fn recursive_cata(&self, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF) -> W where V: Clone + Send + Sync, - Acc: Default, W: Clone, - CollapseF: Copy + Fn(Option<&V>, Option, &[u8]) -> W, - BranchF: Copy + Fn(&ByteMask, W, &mut Acc), - FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, + StartF: Copy + Fn(&ByteMask) -> Acc, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc), + FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> W, Self: Sized; /// A stepping (non-jumping) catamorphism for the trie. /// - /// Use this when you need the cata to evaluate once per path byte, even across non-branching sub-paths. - /// Unlike the jumping version, `branch_f` and `finalize_f` will be called for every path byte. - /// - /// See [`Catamorphism::recursive_cata`] for closure semantics; this stepping variant omits the prefix argument from `collapse_f`. - fn recursive_cata_stepping(&self, collapse_f: CollapseF, branch_f: BranchF, finalize_f: FinalizeF) -> W + /// Use this when the cata must evaluate once per path byte, including bytes in non-branching + /// runs. Unlike the jumping version, `finalize_f` has no `prefix`: it is called once for every + /// path byte. The callback roles and child-mask ordering are otherwise the same as for + /// [`Summarization::recursive_cata`]. + fn recursive_cata_stepping(&self, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF) -> W where V: Clone + Send + Sync, - Acc: Default, W: Clone, - CollapseF: Copy + Fn(Option<&V>, Option) -> W, - BranchF: Copy + Fn(&ByteMask, W, &mut Acc), - FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, + StartF: Copy + Fn(&ByteMask) -> Acc, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc), + FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option) -> W, Self: Sized { - self.recursive_cata::<_, _, _, _, _, true>( - |val, downstream, prefix| { - let mut w = collapse_f(val, downstream); + self.recursive_cata::<_, _, _, _, _, true, COMPUTE_MASK>( + start_f, + fold_child_f, + |mask, val, acc, prefix| { + let mut w = finalize_f(mask, val, acc); for byte in prefix.iter().rev() { - let mask = ByteMask::from(*byte); - let mut acc = Acc::default(); - branch_f(&mask, w, &mut acc); - w = finalize_f(&mask, acc); + let mask = if COMPUTE_MASK { ByteMask::from(*byte) } else { ByteMask::EMPTY }; + let mut acc = start_f(&mask); + fold_child_f(&mask, w, &mut acc); + w = finalize_f(&mask, None, Some(acc)); } w }, - branch_f, - finalize_f, ) } - } //TODO GOAT!!: It would be nice to get rid of this Default bound on all morphism Ws. In this case, the plan @@ -517,45 +524,72 @@ impl Catamorph } impl<'a, Z, V: Clone + Send + Sync, A: Allocator> Summarization for Z where Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer + ZipperInfallibleSubtries { - fn recursive_cata(&self, collapse_f: CollapseF, branch_f: BranchF, finalize_f: FinalizeF) -> W + fn recursive_cata(&self, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF) -> W where V: Clone + Send + Sync, - Acc: Default, W: Clone, - CollapseF: Copy + Fn(Option<&V>, Option, &[u8]) -> W, - BranchF: Copy + Fn(&ByteMask, W, &mut Acc), - FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, + StartF: Copy + Fn(&ByteMask) -> Acc, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc), + FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> W, { let focus = self.get_focus(); let w = match focus.0.borrow() { Some(node) => { let mut cache = HashMap::new(); - recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH>(node, collapse_f, branch_f, finalize_f, &mut cache) + recursive_cata_cached::<_, _, Acc, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(node, start_f, fold_child_f, finalize_f, &mut cache) }, - None => finalize_f(&ByteMask::EMPTY, Acc::default()), + None => finalize_f(&ByteMask::EMPTY, None, None, &[]), }; - collapse_f(self.val(), Some(w), &[]) + summarize_run::<_, _, _, _, _, _, COMPUTE_MASK>(self.val(), Some(w), &[], start_f, fold_child_f, finalize_f) } } impl Summarization for PathMap { - fn recursive_cata(&self, collapse_f: CollapseF, branch_f: BranchF, finalize_f: FinalizeF) -> W + fn recursive_cata(&self, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF) -> W where V: Clone + Send + Sync, - Acc: Default, W: Clone, - CollapseF: Copy + Fn(Option<&V>, Option, &[u8]) -> W, - BranchF: Copy + Fn(&ByteMask, W, &mut Acc), - FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, + StartF: Copy + Fn(&ByteMask) -> Acc, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc), + FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> W, { let w = match self.root() { Some(node) => { let mut cache = HashMap::new(); - recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH>(node, collapse_f, branch_f, finalize_f, &mut cache) + recursive_cata_cached::<_, _, Acc, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(node, start_f, fold_child_f, finalize_f, &mut cache) }, - None => finalize_f(&ByteMask::EMPTY, Acc::default()), + None => finalize_f(&ByteMask::EMPTY, None, None, &[]), }; - collapse_f(self.root_val(), Some(w), &[]) + summarize_run::<_, _, _, _, _, _, COMPUTE_MASK>(self.root_val(), Some(w), &[], start_f, fold_child_f, finalize_f) + } +} + +/// Helper function to summarize one path run (section of non-branching path bytes) +// +//NOTE: #[inline(always)] here leads to a huge bloating of the size of the a stack frame in the recursive cata +// (about 2.5x bloat. but #[inline(never)] costs about 25% performance. Letting the compiler do its thing seems +// to be the sweet spot. +pub(crate) fn summarize_run( + val: Option<&V>, + downstream: Option, + prefix: &[u8], + start_f: StartF, + fold_child_f: FoldChildF, + finalize_f: FinalizeF, +) -> W +where + StartF: Copy + Fn(&ByteMask) -> Acc, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc), + FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> W, +{ + match downstream { + Some(w) => { + let mask = if COMPUTE_MASK && !prefix.is_empty() { ByteMask::from(prefix[0]) } else { ByteMask::EMPTY }; + let mut acc = start_f(&mask); + fold_child_f(&mask, w, &mut acc); + finalize_f(&mask, val, Some(acc), prefix) + }, + None => finalize_f(&ByteMask::EMPTY, val, None, prefix), } } @@ -2120,9 +2154,19 @@ mod tests { for (keys, expected_sum) in tests { let map: PathMap<()> = keys.into_iter().map(|v| (v, ())).collect(); - let sum = map.recursive_cata::<_, _, _, _, _, true>( - |val, downstream, prefix| { - let mut sum = downstream.map(|w: (bool, u32)| w.1).unwrap_or(0); + let sum = map.recursive_cata::<_, _, _, _, _, true, true>( + |_| SumAcc::default(), + |mask: &ByteMask, w: (bool, u32), acc: &mut SumAcc| { + if let Some(byte) = mask.indexed_bit::(acc.idx) { + acc.idx += 1; + if w.0 { + acc.sum += (byte as char).to_digit(10).unwrap(); + } + } + acc.sum += w.1; + }, + |_mask, val, acc, prefix| { + let mut sum = acc.map(|acc| acc.sum).unwrap_or(0); if val.is_some() { if let Some(byte) = prefix.last() { sum += (*byte as char).to_digit(10).unwrap(); @@ -2130,15 +2174,6 @@ mod tests { } (val.is_some() && prefix.is_empty(), sum) }, - |mask: &ByteMask, w: (bool, u32), acc: &mut SumAcc| { - let byte = mask.indexed_bit::(acc.idx).unwrap(); - acc.idx += 1; - if w.0 { - acc.sum += (byte as char).to_digit(10).unwrap(); - } - acc.sum += w.1; - }, - |_mask: &ByteMask, acc: SumAcc| { (false, acc.sum) }, ).1; assert_eq!(sum, expected_sum); } @@ -2169,21 +2204,19 @@ mod tests { for (keys, expected_sum) in tests { let map: PathMap<()> = keys.into_iter().map(|v| (v, ())).collect(); - let sum = map.recursive_cata_stepping::( - |val, downstream| { - let sum = downstream.map(|w| w.1).unwrap_or(0); - (val.is_some(), sum) - }, + let sum = map.recursive_cata_stepping::( + |_| SumAcc::default(), |mask: &ByteMask, w: (bool, u32), acc: &mut SumAcc| { - let byte = mask.iter().nth(acc.idx).unwrap(); - acc.idx += 1; - if w.0 { - acc.sum += (byte as char).to_digit(10).unwrap(); + if let Some(byte) = mask.iter().nth(acc.idx) { + acc.idx += 1; + if w.0 { + acc.sum += (byte as char).to_digit(10).unwrap(); + } } acc.sum += w.1; }, - |_mask: &ByteMask, acc: SumAcc| { - (false, acc.sum) + |_mask, val, acc| { + (val.is_some(), acc.map(|acc| acc.sum).unwrap_or(0)) }, ).1; assert_eq!(sum, expected_sum); @@ -2202,10 +2235,10 @@ mod tests { let path = vec![b'a'; PATH_LEN]; map.set_val_at(&path, ()); - let count = map.recursive_cata::<_, _, _, _, _, false>( - |v, w, _| (v.is_some() as usize) + w.unwrap_or(0), + let count = map.recursive_cata::<_, _, _, _, _, false, false>( + |_| 0usize, |_mask, w: usize, total| { *total += w }, - |_mask, total: usize| { total }, + |_mask, v, total, _| (v.is_some() as usize) + total.unwrap_or(0), ); assert_eq!(count, 1); } diff --git a/src/tiny_node.rs b/src/tiny_node.rs index b59eb4a8..59b0e150 100644 --- a/src/tiny_node.rs +++ b/src/tiny_node.rs @@ -9,12 +9,12 @@ use core::mem::MaybeUninit; use core::fmt::{Debug, Formatter}; -use crate::gxhash::HashMap; use fast_slice_utils::{find_prefix_overlap, starts_with}; use crate::utils::ByteMask; use crate::alloc::Allocator; use crate::trie_node::*; +use crate::gxhash::HashMap; use crate::ring::*; /// A borrowed reference to a payload with a key stored elsewhere, contained in 16 Bytes @@ -122,15 +122,14 @@ impl<'a, V: Clone + Send + Sync, A: Allocator> TinyRefNode<'a, V, A> { unsafe{ core::slice::from_raw_parts(self.key_bytes.as_ptr().cast(), self.key_len()) } } - pub(crate) fn node_recursive_cata(&self, collapse_f: CollapseF, branch_f: BranchF, finalize_f: FinalizeF, cache: &mut HashMap) -> W + pub(crate) fn node_recursive_cata(&self, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF, cache: &mut HashMap) -> W where - Acc: Default, W: Clone, - CollapseF: Copy + Fn(Option<&V>, Option, &[u8]) -> W, - BranchF: Copy + Fn(&ByteMask, W, &mut Acc), - FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, + StartF: Copy + Fn(&ByteMask) -> Acc, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc), + FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> W, { - self.into_full().unwrap().node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH>(collapse_f, branch_f, finalize_f, cache) + self.into_full().unwrap().node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(start_f, fold_child_f, finalize_f, cache) } } diff --git a/src/trie_map.rs b/src/trie_map.rs index 2566b6eb..33b3f06d 100644 --- a/src/trie_map.rs +++ b/src/trie_map.rs @@ -507,16 +507,15 @@ impl PathMap { /// GOAT, temporary method to do side-by-side comparison between abstracted val_count and bespoke version pub fn goat_val_count(&self) -> usize { - let root_val = unsafe{ &*self.root_val.get() }.is_some() as usize; match self.root() { Some(_root) => { - self.recursive_cata::<_, _, _, _, _, false>( - |v, w, _| { (v.is_some() as usize) + w.unwrap_or(0) }, // on values amongst a path - |_mask, w: usize, total| { *total += w }, // on merging children into a node - |_mask, total: usize| { total } // finalizing a node - ) + root_val + self.recursive_cata::<_, _, _, _, _, false, false>( + |_| 0usize, + |_mask, w: usize, total| { *total += w }, + |_mask, v, total, _| { (v.is_some() as usize) + total.unwrap_or(0) }, + ) }, - None => root_val + None => unsafe{ &*self.root_val.get() }.is_some() as usize } } diff --git a/src/trie_node.rs b/src/trie_node.rs index ca00a790..5368666a 100644 --- a/src/trie_node.rs +++ b/src/trie_node.rs @@ -2412,60 +2412,58 @@ pub(crate) fn val_count_below_node(node: & } /// Internal implementation of recursive_cata -pub(crate) fn recursive_cata_cached( +pub(crate) fn recursive_cata_cached( node: &TrieNodeODRc, - collapse_f: CollapseF, - branch_f: BranchF, + start_f: StartF, + fold_child_f: FoldChildF, finalize_f: FinalizeF, cache: &mut HashMap, ) -> W where V: Clone + Send + Sync, A: Allocator, - Acc: Default, W: Clone, - CollapseF: Copy + Fn(Option<&V>, Option, &[u8]) -> W, - BranchF: Copy + Fn(&ByteMask, W, &mut Acc), - FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, + StartF: Copy + Fn(&ByteMask) -> Acc, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc), + FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> W, { if !node.is_empty() && node.refcount() > 1 { let hash = node.shared_node_id(); match cache.get(&hash) { Some(cached) => cached.clone(), None => { - let w = recursive_cata_dispatch::<_, _, _, _, _, _, _, COMPUTE_PATH>(node, collapse_f, branch_f, finalize_f, cache); + let w = recursive_cata_dispatch::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(node, start_f, fold_child_f, finalize_f, cache); cache.insert(hash, w.clone()); w }, } } else { - recursive_cata_dispatch::<_, _, _, _, _, _, _, COMPUTE_PATH>(node, collapse_f, branch_f, finalize_f, cache) + recursive_cata_dispatch::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(node, start_f, fold_child_f, finalize_f, cache) } } #[inline(always)] -fn recursive_cata_dispatch( +fn recursive_cata_dispatch( node: &TrieNodeODRc, - collapse_f: CollapseF, - branch_f: BranchF, + start_f: StartF, + fold_child_f: FoldChildF, finalize_f: FinalizeF, cache: &mut HashMap, ) -> W where V: Clone + Send + Sync, A: Allocator, - Acc: Default, W: Clone, - CollapseF: Copy + Fn(Option<&V>, Option, &[u8]) -> W, - BranchF: Copy + Fn(&ByteMask, W, &mut Acc), - FinalizeF: Copy + Fn(&ByteMask, Acc) -> W, + StartF: Copy + Fn(&ByteMask) -> Acc, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc), + FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> W, { match node.as_tagged() { - TaggedNodeRef::DenseByteNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH>(collapse_f, branch_f, finalize_f, cache) } - TaggedNodeRef::LineListNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH>(collapse_f, branch_f, finalize_f, cache) } - TaggedNodeRef::CellByteNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH>(collapse_f, branch_f, finalize_f, cache) } - TaggedNodeRef::TinyRefNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH>(collapse_f, branch_f, finalize_f, cache) } - TaggedNodeRef::EmptyNode => { finalize_f(&ByteMask::EMPTY, Acc::default()) } + TaggedNodeRef::DenseByteNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(start_f, fold_child_f, finalize_f, cache) } + TaggedNodeRef::LineListNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(start_f, fold_child_f, finalize_f, cache) } + TaggedNodeRef::CellByteNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(start_f, fold_child_f, finalize_f, cache) } + TaggedNodeRef::TinyRefNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(start_f, fold_child_f, finalize_f, cache) } + TaggedNodeRef::EmptyNode => { finalize_f(&ByteMask::EMPTY, None, None, &[]) } } } From c63191a7ca451970442ad94a299b94b64900400a Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Wed, 26 Aug 2026 03:43:54 -0600 Subject: [PATCH 18/50] Adding some more correctness tests for the summarize mechanism --- src/dense_byte_node.rs | 27 +---- src/line_list_node.rs | 80 ++++--------- src/morphisms.rs | 251 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 268 insertions(+), 90 deletions(-) diff --git a/src/dense_byte_node.rs b/src/dense_byte_node.rs index 48bf8165..d9e8057f 100644 --- a/src/dense_byte_node.rs +++ b/src/dense_byte_node.rs @@ -395,27 +395,10 @@ impl> ByteNode FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc), FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> W, { - let mut mask_idx = 0; - let mut lm = unsafe{ *self.mask.0.get_unchecked(0) }; let mask = if COMPUTE_MASK { &self.mask } else { &ByteMask::EMPTY }; let mut ws = Some(start_f(mask)); for cf in self.values.iter() { - //Compute the key byte. Hopefully this will all be stripped away by the compiler if the path isn't used - //UPDATE: alas, my hopes were dashed. No amount of reorganizing this code, eliminating all traps, - // unrolling the loop, etc., could convince LLVM to elide it. So we have to hit it with the const hammer. - let key_byte; - let path = if COMPUTE_PATH { - while lm == 0 { - mask_idx += 1; - lm = unsafe{ *self.mask.0.get_unchecked(mask_idx) }; - } - let byte_index = lm.trailing_zeros(); - lm ^= 1u64 << byte_index; - key_byte = 64*(mask_idx as u8) + (byte_index as u8); - core::slice::from_ref(&key_byte) - } else { - &[] - }; + let path = &[]; //Do the recursive calling //PERF NOTE: The reason we have four code paths around the call to `branch_f` instead of just doing @@ -427,17 +410,17 @@ impl> ByteNode match (cf.rec(), cf.val()) { (Some(rec), Some(val)) => { let w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(rec, start_f, fold_child_f, finalize_f, cache); - fold_child_f(mask, summarize_run::<_, _, _, _, _, _, COMPUTE_MASK>(Some(val), Some(w), path, start_f, fold_child_f, finalize_f), unsafe { ws.as_mut().unwrap_unchecked() }); + fold_child_f(mask, summarize_run::<_, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(Some(val), Some(w), path, start_f, fold_child_f, finalize_f), unsafe { ws.as_mut().unwrap_unchecked() }); }, (Some(rec), None) => { let w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(rec, start_f, fold_child_f, finalize_f, cache); - fold_child_f(mask, summarize_run::<_, _, _, _, _, _, COMPUTE_MASK>(None, Some(w), path, start_f, fold_child_f, finalize_f), unsafe { ws.as_mut().unwrap_unchecked() }); + fold_child_f(mask, summarize_run::<_, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(None, Some(w), path, start_f, fold_child_f, finalize_f), unsafe { ws.as_mut().unwrap_unchecked() }); }, (None, Some(val)) => { - fold_child_f(mask, summarize_run::<_, _, _, _, _, _, COMPUTE_MASK>(Some(val), None, path, start_f, fold_child_f, finalize_f), unsafe { ws.as_mut().unwrap_unchecked() }); + fold_child_f(mask, summarize_run::<_, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(Some(val), None, path, start_f, fold_child_f, finalize_f), unsafe { ws.as_mut().unwrap_unchecked() }); }, (None, None) => { - fold_child_f(mask, summarize_run::<_, _, _, _, _, _, COMPUTE_MASK>(None, None, path, start_f, fold_child_f, finalize_f), unsafe { ws.as_mut().unwrap_unchecked() }); + fold_child_f(mask, summarize_run::<_, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(None, None, path, start_f, fold_child_f, finalize_f), unsafe { ws.as_mut().unwrap_unchecked() }); }, } } diff --git a/src/line_list_node.rs b/src/line_list_node.rs index 04d5e463..9c61ac2b 100644 --- a/src/line_list_node.rs +++ b/src/line_list_node.rs @@ -2790,7 +2790,7 @@ impl LineListNode { { macro_rules! summarize { ($val:expr, $downstream:expr, $prefix:expr) => { - summarize_run::<_, _, _, _, _, _, COMPUTE_MASK>($val, $downstream, $prefix, start_f, fold_child_f, finalize_f) + summarize_run::<_, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>($val, $downstream, $prefix, start_f, fold_child_f, finalize_f) }; } //Pair node can have the following permutations: (Slot0, Slot1) @@ -2828,11 +2828,7 @@ impl LineListNode { 10 | 11 => { let child_node = unsafe{ self.child_in_slot::<0>() }; let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, start_f, fold_child_f, finalize_f, cache); - let path = if COMPUTE_PATH { - unsafe{ self.key_unchecked::<0>() } - } else { - &[] - }; + let path = unsafe{ self.key_unchecked::<0>() }; summarize!(None, Some(child_w), path) }, //(Child, Val) = (1 << 3) + (1 << 2) + (1 << 1) @@ -2847,28 +2843,17 @@ impl LineListNode { debug_assert_eq!(key0.len(), 1); debug_assert_eq!(key1.len(), 1); let val = unsafe { self.val_in_slot::<1>() }; - let path = if COMPUTE_PATH { - key0 - } else { - &[] - }; + let path = key0; summarize!(Some(val), Some(child_w), path) } else { //Case 4 - let (path, mask) = if COMPUTE_PATH { - (&key0[1..], ByteMask::from((key0_byte, key1_byte))) - } else { - (&[] as &[u8], ByteMask::new()) - }; + let path = &key0[1..]; + let mask = if COMPUTE_MASK { ByteMask::from((key0_byte, key1_byte)) } else { ByteMask::EMPTY }; let mut acc = start_f(&mask); fold_child_f(&mask, summarize!(None, Some(child_w), path), &mut acc); let val = unsafe { self.val_in_slot::<1>() }; - let path = if COMPUTE_PATH { - &key1[1..] - } else { - &[] - }; + let path = &key1[1..]; fold_child_f(&mask, summarize!(Some(val), None, path), &mut acc); finalize_f(&mask, None, Some(acc), &[]) @@ -2876,14 +2861,12 @@ impl LineListNode { }, //Case 5 (Child, Child) = (1 << 3) + (1 << 2) + (1 << 1) + 1 15 => { - let (path0, path1, mask) = if COMPUTE_PATH { let key0 = unsafe{ self.key_unchecked::<0>() }; let key1 = unsafe{ self.key_unchecked::<1>() }; let (key0_byte, key1_byte) = unsafe{ (*key0.get_unchecked(0), *key1.get_unchecked(0)) }; - (&key0[1..], &key1[1..], ByteMask::from((key0_byte, key1_byte))) - } else { - (&[] as &[u8], &[] as &[u8], ByteMask::new()) - }; + let path0 = &key0[1..]; + let path1 = &key1[1..]; + let mask = if COMPUTE_MASK { ByteMask::from((key0_byte, key1_byte)) } else { ByteMask::EMPTY }; let mut acc = start_f(&mask); let child_node = unsafe{ self.child_in_slot::<0>() }; let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, start_f, fold_child_f, finalize_f, cache); @@ -2898,11 +2881,7 @@ impl LineListNode { //Case 6 (Val, Empty) = (1 << 3) | (1 << 3) + 1 8 | 9 => { let val = unsafe { self.val_in_slot::<0>() }; - let path = if COMPUTE_PATH { - unsafe{ self.key_unchecked::<0>() } - } else { - &[] - }; + let path = unsafe{ self.key_unchecked::<0>() }; summarize!(Some(val), None, path) }, //(Val, Val) = (1 << 3) + (1 << 2) @@ -2915,26 +2894,16 @@ impl LineListNode { debug_assert_eq!(key0.len(), 1); debug_assert!(key1.len() > 1); let val = unsafe { self.val_in_slot::<1>() }; - let path = if COMPUTE_PATH { - &key1[1..] - } else { - &[] - }; + let path = &key1[1..]; let w1 = summarize!(Some(val), None, path); let val = unsafe { self.val_in_slot::<0>() }; - let path = if COMPUTE_PATH { - &key1[0..1] - } else { - &[] - }; + let path = &key1[0..1]; summarize!(Some(val), Some(w1), path) } else { //Case 8 (Val, Val), different first bytes - let (path0, path1, mask) = if COMPUTE_PATH { - (&key0[1..], &key1[1..], ByteMask::from((key0_byte, key1_byte))) - } else { - (&[] as &[u8], &[] as &[u8], ByteMask::new()) - }; + let path0 = &key0[1..]; + let path1 = &key1[1..]; + let mask = if COMPUTE_MASK { ByteMask::from((key0_byte, key1_byte)) } else { ByteMask::EMPTY }; let mut acc = start_f(&mask); let val = unsafe{ self.val_in_slot::<0>() }; fold_child_f(&mask, summarize!(Some(val), None, path0), &mut acc); @@ -2957,28 +2926,17 @@ impl LineListNode { debug_assert_eq!(key0.len(), 1); debug_assert_eq!(key1.len(), 1); let val = unsafe { self.val_in_slot::<0>() }; - let path = if COMPUTE_PATH { - key0 - } else { - &[] - }; + let path = key0; summarize!(Some(val), Some(child_w), path) } else { //Case 10 (Val, Child), different key bytes - let (path, mask) = if COMPUTE_PATH { - (&key1[1..], ByteMask::from((key0_byte, key1_byte))) - } else { - (&[] as &[u8], ByteMask::new()) - }; + let path = &key1[1..]; + let mask = if COMPUTE_MASK { ByteMask::from((key0_byte, key1_byte)) } else { ByteMask::EMPTY }; let mut acc = start_f(&mask); fold_child_f(&mask, summarize!(None, Some(child_w), path), &mut acc); let val = unsafe { self.val_in_slot::<0>() }; - let path = if COMPUTE_PATH { - &key0[1..] - } else { - &[] - }; + let path = &key0[1..]; fold_child_f(&mask, summarize!(Some(val), None, path), &mut acc); finalize_f(&mask, None, Some(acc), &[]) diff --git a/src/morphisms.rs b/src/morphisms.rs index cd78adb5..a59e13db 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -540,7 +540,7 @@ impl<'a, Z, V: Clone + Send + Sync, A: Allocator> Summarization for Z wher }, None => finalize_f(&ByteMask::EMPTY, None, None, &[]), }; - summarize_run::<_, _, _, _, _, _, COMPUTE_MASK>(self.val(), Some(w), &[], start_f, fold_child_f, finalize_f) + summarize_run::<_, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(self.val(), Some(w), &[], start_f, fold_child_f, finalize_f) } } @@ -560,7 +560,7 @@ impl Summarization for PathM }, None => finalize_f(&ByteMask::EMPTY, None, None, &[]), }; - summarize_run::<_, _, _, _, _, _, COMPUTE_MASK>(self.root_val(), Some(w), &[], start_f, fold_child_f, finalize_f) + summarize_run::<_, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(self.root_val(), Some(w), &[], start_f, fold_child_f, finalize_f) } } @@ -569,7 +569,7 @@ impl Summarization for PathM //NOTE: #[inline(always)] here leads to a huge bloating of the size of the a stack frame in the recursive cata // (about 2.5x bloat. but #[inline(never)] costs about 25% performance. Letting the compiler do its thing seems // to be the sweet spot. -pub(crate) fn summarize_run( +pub(crate) fn summarize_run( val: Option<&V>, downstream: Option, prefix: &[u8], @@ -582,14 +582,20 @@ where FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc), FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> W, { - match downstream { - Some(w) => { - let mask = if COMPUTE_MASK && !prefix.is_empty() { ByteMask::from(prefix[0]) } else { ByteMask::EMPTY }; + match (val, downstream, prefix) { + (None, Some(w), []) => w, + (val, Some(w), prefix) => { + let mask = if COMPUTE_MASK && !prefix.is_empty() { ByteMask::from(*prefix.last().unwrap()) } else { ByteMask::EMPTY }; let mut acc = start_f(&mask); fold_child_f(&mask, w, &mut acc); + let prefix = if COMPUTE_PATH { + if prefix.is_empty() { prefix } else { &prefix[..prefix.len() - 1] } + } else { + &[] + }; finalize_f(&mask, val, Some(acc), prefix) }, - None => finalize_f(&ByteMask::EMPTY, val, None, prefix), + (val, None, prefix) => finalize_f(&ByteMask::EMPTY, val, None, if COMPUTE_PATH { prefix } else { &[] }), } } @@ -2223,6 +2229,237 @@ mod tests { } } + /// Ports the leaf-count portion of `cata_test2` to the summarization API and + /// compares both summarization traversals with the existing cached catas. + #[test] + fn recursive_cata_leaf_count_matches_cached_catas() { + let mut map = PathMap::new(); + let words = ["arrow", "bow", "cannon", "roman", "romane", "romanus", "romulus", "rubens", "ruber", "rubicon", "rubicundus", "rom'i"]; + words.iter().enumerate().for_each(|(i, word)| { map.set_val_at(word.as_bytes(), i); }); + + let cached_stepping = map.read_zipper().into_cata_cached(|_mask, children: &mut [usize], val| { + if children.is_empty() { + assert!(val.is_some()); + 1 + } else { + children.iter().sum() + } + }); + let cached_jumping = map.read_zipper().into_cata_jumping_cached(|_mask, children: &mut [usize], val, _prefix| { + if children.is_empty() { + assert!(val.is_some()); + 1 + } else { + children.iter().sum() + } + }); + + let jumping = map.recursive_cata::( + |_| 0, + |_mask, child, total| *total += child, + |_mask, val, children, _prefix| match children { + Some(total) => total, + None => { + assert!(val.is_some()); + 1 + }, + }, + ); + let stepping = map.recursive_cata_stepping::( + |_| 0, + |_mask, child, total| *total += child, + |_mask, val, children| match children { + Some(total) => total, + None => { + assert!(val.is_some()); + 1 + }, + }, + ); + + assert_eq!(cached_stepping, 11); + assert_eq!(cached_jumping, cached_stepping); + assert_eq!(jumping, cached_jumping); + assert_eq!(stepping, cached_stepping); + } + + /// Ports the pure longest-path calculation from `cata_test2`. The stepping + /// version deliberately uses the fold ordinal to associate each child with + /// the corresponding bit in the shared mask. + #[test] + fn recursive_cata_longest_path_matches_cached_cata() { + let mut map = PathMap::new(); + let words = ["arrow", "bow", "cannon", "roman", "romane", "romanus", "romulus", "rubens", "ruber", "rubicon", "rubicundus", "rom'i"]; + words.iter().enumerate().for_each(|(i, word)| { map.set_val_at(word.as_bytes(), i); }); + + let cached = map.read_zipper().into_cata_jumping_cached(|mask, children: &mut [Vec], _val, prefix| { + let mut longest = mask.iter().zip(children.iter_mut()) + .max_by_key(|(_byte, rest)| rest.len()) + .map_or_else(Vec::new, |(byte, rest)| { + let mut path = std::mem::take(rest); + path.insert(0, byte); + path + }); + let mut path = prefix.to_vec(); + path.append(&mut longest); + path + }); + + // This uses allocation for readability; performance-sensitive code can fold a longest path directly. + let jumping = map.recursive_cata::>, Vec, _, _, _, true, true>( + |_| Vec::new(), + |_mask, child, children| children.push(child), + |mask, _val, children, prefix| { + let mut longest = children.map_or_else(Vec::new, |children| { + if mask.is_empty_mask() { + children.into_iter().max_by_key(|rest| rest.len()).unwrap_or_default() + } else { + mask.iter().zip(children) + .max_by_key(|(_byte, rest)| rest.len()) + .map_or_else(Vec::new, |(byte, mut rest)| { + rest.insert(0, byte); + rest + }) + } + }); + let mut path = prefix.to_vec(); + path.append(&mut longest); + path + }, + ); + let stepping = map.recursive_cata_stepping::<(usize, Vec), Vec, _, _, _, true>( + |_| (0, Vec::new()), + |mask, child, state| { + let mut path = Vec::with_capacity(child.len() + 1); + if let Some(byte) = mask.indexed_bit::(state.0) { + state.0 += 1; + path.push(byte); + } + path.extend(child); + if path.len() > state.1.len() { + state.1 = path; + } + }, + |_mask, _val, state| state.map_or_else(Vec::new, |(_, path)| path), + ); + + assert_eq!(std::str::from_utf8(&cached).unwrap(), "rubicundus"); + assert_eq!(jumping, cached); + assert_eq!(stepping, cached); + } + + /// Ports the branch-value portion of `cata_test2` to both summarization + /// traversals. Values with no downstream results are deliberately omitted. + #[test] + fn recursive_cata_branch_values_matches_cached_catas() { + let mut map = PathMap::new(); + let words = ["arrow", "bow", "cannon", "roman", "romane", "romanus", "romulus", "rubens", "ruber", "rubicon", "rubicundus", "rom'i"]; + words.iter().enumerate().for_each(|(i, word)| { map.set_val_at(word.as_bytes(), i); }); + + let cached_stepping = map.read_zipper().into_cata_cached(|_mask, children: &mut [Vec], val| { + if children.is_empty() { + Vec::new() + } else if let Some(val) = val { + vec![*val] + } else { + let mut values = children.first_mut().map_or_else(Vec::new, std::mem::take); + for child in &mut children[1..] { + values.append(child); + } + values + } + }); + let cached_jumping = map.read_zipper().into_cata_jumping_cached(|_mask, children: &mut [Vec], val, _prefix| { + if children.is_empty() { + Vec::new() + } else if let Some(val) = val { + vec![*val] + } else { + let mut values = children.first_mut().map_or_else(Vec::new, std::mem::take); + for child in &mut children[1..] { + values.append(child); + } + values + } + }); + + let jumping = map.recursive_cata::, Vec, _, _, _, false, true>( + |_| Vec::new(), + |_mask, child, values| values.extend(child), + |_mask, val, children, _prefix| match children { + None => Vec::new(), + Some(values) => val.map_or(values, |val| vec![*val]), + }, + ); + let stepping = map.recursive_cata_stepping::, Vec, _, _, _, true>( + |_| Vec::new(), + |_mask, child, values| values.extend(child), + |_mask, val, children| match children { + None => Vec::new(), + Some(values) => val.map_or(values, |val| vec![*val]), + }, + ); + + assert_eq!(cached_stepping, vec![3]); + assert_eq!(cached_jumping, cached_stepping); + assert_eq!(jumping, cached_jumping); + assert_eq!(stepping, cached_stepping); + } + + /// Parallel port of `cata_test_cached`: the input deliberately contains + /// shared subtries, so this exercises Summarization's cached traversal. + #[test] + fn recursive_cata_cached_dag_matches_cached_cata() { + fn make_map() -> PathMap { + let mut map: PathMap = PathMap::from_iter([([0], 0)]); + for _level in 0..3 { + let previous = map.read_zipper(); + let next = PathMap::new_from_ana(false, |quit, _val, children, _path| { + if quit { return; } + for byte in 0..=2 { + children.graft_at_byte(byte, &previous); + } + }); + drop(previous); + map = next; + } + map + } + + use core::sync::atomic::{AtomicU64, Ordering::Relaxed}; + use std::rc::Rc; + + #[derive(Clone, Debug, PartialEq)] + struct Node { + value: Option, + children: Vec>>, + } + impl Node { + fn new(value: Option<&V>, children: Option>>>) -> Self { + Self { value: value.cloned(), children: children.unwrap_or_default() } + } + } + + let cached_calls = AtomicU64::new(0); + let cached: Rc> = make_map().into_cata_cached(|_mask, children, value| { + cached_calls.fetch_add(1, Relaxed); + Rc::new(Node { value: value.cloned(), children: children.to_vec() }) + }); + + let summarization_calls = AtomicU64::new(0); + let summarized = make_map().recursive_cata_stepping::>>, Rc>, _, _, _, true>( + |_| Vec::new(), + |_mask, child, children| children.push(child), + |_mask, value, children| { + summarization_calls.fetch_add(1, Relaxed); + Rc::new(Node::new(value, children)) + }, + ); + + assert_eq!(summarized, cached); + assert_eq!(summarization_calls.load(Relaxed), cached_calls.load(Relaxed)); + } + /// Finds the path_depth at which the recursive cata hits a stack overflow /// /// Empirically seems to be somewhere between 8 and 10 KBytes. But more branching, and thus fewer From 9699447b42b3c5efb4311d6ea7754edf25efc23a Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Wed, 26 Aug 2026 06:43:19 -0600 Subject: [PATCH 19/50] Fixing bug in new summarize API caused by not carrying value forward from parent node --- src/dense_byte_node.rs | 10 +++++----- src/line_list_node.rs | 41 ++++++++++++++++++++--------------------- src/morphisms.rs | 8 ++++---- src/tiny_node.rs | 4 ++-- src/trie_node.rs | 22 ++++++++++++++-------- 5 files changed, 45 insertions(+), 40 deletions(-) diff --git a/src/dense_byte_node.rs b/src/dense_byte_node.rs index d9e8057f..926878de 100644 --- a/src/dense_byte_node.rs +++ b/src/dense_byte_node.rs @@ -388,7 +388,7 @@ impl> ByteNode } #[inline(always)] - pub(crate) fn node_recursive_cata(&self, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF, cache: &mut HashMap) -> W + pub(crate) fn node_recursive_cata(&self, passed_in_val: Option<&V>, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF, cache: &mut HashMap) -> W where W: Clone, StartF: Copy + Fn(&ByteMask) -> Acc, @@ -409,11 +409,11 @@ impl> ByteNode // like this gives the optimizer a site to specialize for each permutation match (cf.rec(), cf.val()) { (Some(rec), Some(val)) => { - let w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(rec, start_f, fold_child_f, finalize_f, cache); - fold_child_f(mask, summarize_run::<_, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(Some(val), Some(w), path, start_f, fold_child_f, finalize_f), unsafe { ws.as_mut().unwrap_unchecked() }); + let w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(rec, Some(val), start_f, fold_child_f, finalize_f, cache); + fold_child_f(mask, w, unsafe { ws.as_mut().unwrap_unchecked() }); }, (Some(rec), None) => { - let w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(rec, start_f, fold_child_f, finalize_f, cache); + let w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(rec, None, start_f, fold_child_f, finalize_f, cache); fold_child_f(mask, summarize_run::<_, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(None, Some(w), path, start_f, fold_child_f, finalize_f), unsafe { ws.as_mut().unwrap_unchecked() }); }, (None, Some(val)) => { @@ -424,7 +424,7 @@ impl> ByteNode }, } } - finalize_f(mask, None, Some(unsafe { std::mem::take(&mut ws).unwrap_unchecked() }), &[]) + finalize_f(mask, passed_in_val, Some(unsafe { std::mem::take(&mut ws).unwrap_unchecked() }), &[]) } } diff --git a/src/line_list_node.rs b/src/line_list_node.rs index 9c61ac2b..fc5a7d31 100644 --- a/src/line_list_node.rs +++ b/src/line_list_node.rs @@ -2781,7 +2781,7 @@ impl LineListNode { } #[inline(always)] - pub(crate) fn node_recursive_cata(&self, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF, cache: &mut HashMap) -> W + pub(crate) fn node_recursive_cata(&self, passed_in_val: Option<&V>, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF, cache: &mut HashMap) -> W where W: Clone, StartF: Copy + Fn(&ByteMask) -> Acc, @@ -2823,18 +2823,17 @@ impl LineListNode { match self.header >> 12 { //Case 1 (Empty, Empty) - 0 => finalize_f(&ByteMask::EMPTY, None, None, &[]), + 0 => finalize_f(&ByteMask::EMPTY, passed_in_val, None, &[]), //Case 2 (Child, Empty) = (1 << 3) + (1 << 1) | (1 << 3) + (1 << 1) + 1 10 | 11 => { let child_node = unsafe{ self.child_in_slot::<0>() }; - let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, start_f, fold_child_f, finalize_f, cache); + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, None, start_f, fold_child_f, finalize_f, cache); let path = unsafe{ self.key_unchecked::<0>() }; - summarize!(None, Some(child_w), path) + summarize!(passed_in_val, Some(child_w), path) }, //(Child, Val) = (1 << 3) + (1 << 2) + (1 << 1) 14 => { let child_node = unsafe{ self.child_in_slot::<0>() }; - let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, start_f, fold_child_f, finalize_f, cache); let key0 = unsafe{ self.key_unchecked::<0>() }; let key1 = unsafe{ self.key_unchecked::<1>() }; let (key0_byte, key1_byte) = unsafe{ (*key0.get_unchecked(0), *key1.get_unchecked(0)) }; @@ -2843,10 +2842,11 @@ impl LineListNode { debug_assert_eq!(key0.len(), 1); debug_assert_eq!(key1.len(), 1); let val = unsafe { self.val_in_slot::<1>() }; - let path = key0; - summarize!(Some(val), Some(child_w), path) + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, Some(val), start_f, fold_child_f, finalize_f, cache); + summarize!(passed_in_val, Some(child_w), key0) } else { //Case 4 + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, None, start_f, fold_child_f, finalize_f, cache); let path = &key0[1..]; let mask = if COMPUTE_MASK { ByteMask::from((key0_byte, key1_byte)) } else { ByteMask::EMPTY }; let mut acc = start_f(&mask); @@ -2856,7 +2856,7 @@ impl LineListNode { let path = &key1[1..]; fold_child_f(&mask, summarize!(Some(val), None, path), &mut acc); - finalize_f(&mask, None, Some(acc), &[]) + finalize_f(&mask, passed_in_val, Some(acc), &[]) } }, //Case 5 (Child, Child) = (1 << 3) + (1 << 2) + (1 << 1) + 1 @@ -2869,20 +2869,20 @@ impl LineListNode { let mask = if COMPUTE_MASK { ByteMask::from((key0_byte, key1_byte)) } else { ByteMask::EMPTY }; let mut acc = start_f(&mask); let child_node = unsafe{ self.child_in_slot::<0>() }; - let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, start_f, fold_child_f, finalize_f, cache); + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, None, start_f, fold_child_f, finalize_f, cache); fold_child_f(&mask, summarize!(None, Some(child_w), path0), &mut acc); let child_node = unsafe{ self.child_in_slot::<1>() }; - let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, start_f, fold_child_f, finalize_f, cache); + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, None, start_f, fold_child_f, finalize_f, cache); fold_child_f(&mask, summarize!(None, Some(child_w), path1), &mut acc); - finalize_f(&mask, None, Some(acc), &[]) + finalize_f(&mask, passed_in_val, Some(acc), &[]) }, //Case 6 (Val, Empty) = (1 << 3) | (1 << 3) + 1 8 | 9 => { let val = unsafe { self.val_in_slot::<0>() }; let path = unsafe{ self.key_unchecked::<0>() }; - summarize!(Some(val), None, path) + summarize!(passed_in_val, Some(summarize!(Some(val), None, path)), &[]) }, //(Val, Val) = (1 << 3) + (1 << 2) 12 => { @@ -2894,11 +2894,10 @@ impl LineListNode { debug_assert_eq!(key0.len(), 1); debug_assert!(key1.len() > 1); let val = unsafe { self.val_in_slot::<1>() }; - let path = &key1[1..]; - let w1 = summarize!(Some(val), None, path); + let w = summarize!(Some(val), None, &[]); let val = unsafe { self.val_in_slot::<0>() }; - let path = &key1[0..1]; - summarize!(Some(val), Some(w1), path) + let w = summarize!(Some(val), Some(w), &key1[1..]); + summarize!(passed_in_val, Some(w), &key1[0..1]) } else { //Case 8 (Val, Val), different first bytes let path0 = &key0[1..]; @@ -2911,13 +2910,12 @@ impl LineListNode { let val = unsafe{ self.val_in_slot::<1>() }; fold_child_f(&mask, summarize!(Some(val), None, path1), &mut acc); - finalize_f(&mask, None, Some(acc), &[]) + finalize_f(&mask, passed_in_val, Some(acc), &[]) } }, //(Val, Child) = (1 << 3) + (1 << 2) + 1 13 => { let child_node = unsafe{ self.child_in_slot::<1>() }; - let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, start_f, fold_child_f, finalize_f, cache); let key0 = unsafe{ self.key_unchecked::<0>() }; let key1 = unsafe{ self.key_unchecked::<1>() }; let (key0_byte, key1_byte) = unsafe{ (*key0.get_unchecked(0), *key1.get_unchecked(0)) }; @@ -2926,10 +2924,11 @@ impl LineListNode { debug_assert_eq!(key0.len(), 1); debug_assert_eq!(key1.len(), 1); let val = unsafe { self.val_in_slot::<0>() }; - let path = key0; - summarize!(Some(val), Some(child_w), path) + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, Some(val), start_f, fold_child_f, finalize_f, cache); + summarize!(passed_in_val, Some(child_w), key0) } else { //Case 10 (Val, Child), different key bytes + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, None, start_f, fold_child_f, finalize_f, cache); let path = &key1[1..]; let mask = if COMPUTE_MASK { ByteMask::from((key0_byte, key1_byte)) } else { ByteMask::EMPTY }; let mut acc = start_f(&mask); @@ -2939,7 +2938,7 @@ impl LineListNode { let path = &key0[1..]; fold_child_f(&mask, summarize!(Some(val), None, path), &mut acc); - finalize_f(&mask, None, Some(acc), &[]) + finalize_f(&mask, passed_in_val, Some(acc), &[]) } }, _ => { unsafe { unreachable_unchecked() } } diff --git a/src/morphisms.rs b/src/morphisms.rs index a59e13db..e69d7a64 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -536,11 +536,11 @@ impl<'a, Z, V: Clone + Send + Sync, A: Allocator> Summarization for Z wher let w = match focus.0.borrow() { Some(node) => { let mut cache = HashMap::new(); - recursive_cata_cached::<_, _, Acc, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(node, start_f, fold_child_f, finalize_f, &mut cache) + recursive_cata_cached::<_, _, Acc, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(node, self.val(), start_f, fold_child_f, finalize_f, &mut cache) }, None => finalize_f(&ByteMask::EMPTY, None, None, &[]), }; - summarize_run::<_, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(self.val(), Some(w), &[], start_f, fold_child_f, finalize_f) + w } } @@ -556,11 +556,11 @@ impl Summarization for PathM let w = match self.root() { Some(node) => { let mut cache = HashMap::new(); - recursive_cata_cached::<_, _, Acc, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(node, start_f, fold_child_f, finalize_f, &mut cache) + recursive_cata_cached::<_, _, Acc, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(node, self.root_val(), start_f, fold_child_f, finalize_f, &mut cache) }, None => finalize_f(&ByteMask::EMPTY, None, None, &[]), }; - summarize_run::<_, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(self.root_val(), Some(w), &[], start_f, fold_child_f, finalize_f) + w } } diff --git a/src/tiny_node.rs b/src/tiny_node.rs index 59b0e150..e11ab82f 100644 --- a/src/tiny_node.rs +++ b/src/tiny_node.rs @@ -122,14 +122,14 @@ impl<'a, V: Clone + Send + Sync, A: Allocator> TinyRefNode<'a, V, A> { unsafe{ core::slice::from_raw_parts(self.key_bytes.as_ptr().cast(), self.key_len()) } } - pub(crate) fn node_recursive_cata(&self, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF, cache: &mut HashMap) -> W + pub(crate) fn node_recursive_cata(&self, passed_in_val: Option<&V>, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF, cache: &mut HashMap) -> W where W: Clone, StartF: Copy + Fn(&ByteMask) -> Acc, FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc), FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> W, { - self.into_full().unwrap().node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(start_f, fold_child_f, finalize_f, cache) + self.into_full().unwrap().node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(passed_in_val, start_f, fold_child_f, finalize_f, cache) } } diff --git a/src/trie_node.rs b/src/trie_node.rs index 5368666a..e90c32e5 100644 --- a/src/trie_node.rs +++ b/src/trie_node.rs @@ -2414,6 +2414,7 @@ pub(crate) fn val_count_below_node(node: & /// Internal implementation of recursive_cata pub(crate) fn recursive_cata_cached( node: &TrieNodeODRc, + passed_in_val: Option<&V>, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF, @@ -2427,24 +2428,29 @@ where FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc), FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> W, { - if !node.is_empty() && node.refcount() > 1 { + // NOTE: A caller-supplied value can make this trie-node boundary fall inside one Summarization callback, + // so its W is not reusable by node ID alone. + // FUTURE: When the node contract associates values at the root of nodes with the node and not with the parent, + // then the `passed_in_val.is_none()` check can be removed + if passed_in_val.is_none() && !node.is_empty() && node.refcount() > 1 { let hash = node.shared_node_id(); match cache.get(&hash) { Some(cached) => cached.clone(), None => { - let w = recursive_cata_dispatch::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(node, start_f, fold_child_f, finalize_f, cache); + let w = recursive_cata_dispatch::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(node, passed_in_val, start_f, fold_child_f, finalize_f, cache); cache.insert(hash, w.clone()); w }, } } else { - recursive_cata_dispatch::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(node, start_f, fold_child_f, finalize_f, cache) + recursive_cata_dispatch::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(node, passed_in_val, start_f, fold_child_f, finalize_f, cache) } } #[inline(always)] fn recursive_cata_dispatch( node: &TrieNodeODRc, + passed_in_val: Option<&V>, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF, @@ -2459,11 +2465,11 @@ where FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> W, { match node.as_tagged() { - TaggedNodeRef::DenseByteNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(start_f, fold_child_f, finalize_f, cache) } - TaggedNodeRef::LineListNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(start_f, fold_child_f, finalize_f, cache) } - TaggedNodeRef::CellByteNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(start_f, fold_child_f, finalize_f, cache) } - TaggedNodeRef::TinyRefNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(start_f, fold_child_f, finalize_f, cache) } - TaggedNodeRef::EmptyNode => { finalize_f(&ByteMask::EMPTY, None, None, &[]) } + TaggedNodeRef::DenseByteNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(passed_in_val, start_f, fold_child_f, finalize_f, cache) } + TaggedNodeRef::LineListNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(passed_in_val, start_f, fold_child_f, finalize_f, cache) } + TaggedNodeRef::CellByteNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(passed_in_val, start_f, fold_child_f, finalize_f, cache) } + TaggedNodeRef::TinyRefNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(passed_in_val, start_f, fold_child_f, finalize_f, cache) } + TaggedNodeRef::EmptyNode => { finalize_f(&ByteMask::EMPTY, passed_in_val, None, &[]) } } } From 7342db59debd11df081e12e7f9fbfb8ee9368e8b Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Fri, 28 Aug 2026 22:12:06 -0600 Subject: [PATCH 20/50] Improving documentation for `recursive_cata` callback args --- src/morphisms.rs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/morphisms.rs b/src/morphisms.rs index e69d7a64..2cd21ecf 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -274,19 +274,23 @@ pub trait Summarization { /// /// Closures: /// - /// `StartF`: Creates an accumulator for a logical node with more than one child branch (for jumping) + /// `StartF`: Creates an accumulator for a logical trie node with more than one child branch. /// `fn(child_mask: &ByteMask) -> Acc` /// - /// `FoldChildF`: Folds one downstream child branch's `W` into the accumulator. It is called once for each - /// downstream result, in the same order as the bits in `child_mask`. Each call for a given - /// logical node receives the same full child mask; the callback can use the ordinal of its calls - /// to associate a result with a particular mask bit. + /// `FoldChildF`: Folds one downstream child branch's `W` into the accumulator. It is called once + /// for each downstream child branch, in the same order as the bits in `child_mask`. Each call for + /// a given logical node receives the same full child mask; the callback must use the order of its + /// calls to associate a result with a particular byte. /// `fn(child_mask: &ByteMask, downstream: W, accumulator: &mut Acc)` /// - /// `FinalizeF`: Converts the value (if present) and the optional child accumulator (if one exists) - /// into the `W` which summarizes the subtrie. `accumulator` is `None` when there are no downstream - /// results, indicating that `StartF` and `FoldChildF` were not called. `prefix` is the non-branching - /// sub-path leading to this invocation. + /// `FinalizeF`: Produces the `W` for one logical trie node and a non-branching sub-path `prefix` + /// above it. The returned `W` should summarize the subtrie from the start of `prefix`, including + /// the `value` and downstream children. + /// - `child_mask` describes the node's immediate child bytes. + /// - `accumulator` contains the results folded from those child branches. `accumulator` is `None` + /// when the node has no downstream branches. + /// - `prefix` is a non-branching sub-path above the logical node. `prefix` never includes a + /// path position that is also part of a `child_mask` for this or another call to `finalize_f` /// `fn(child_mask: &ByteMask, value: Option<&V>, accumulator: Option, prefix: &[u8]) -> W` /// /// In a callback where `COMPUTE_MASK` is false, `child_mask` is [`ByteMask::EMPTY`]. This avoids From f0104fab2088a4728705ca0f59306947f00abbec Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Fri, 28 Aug 2026 22:33:11 -0600 Subject: [PATCH 21/50] Renaming closures in Summarization --- src/morphisms.rs | 50 ++++++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/morphisms.rs b/src/morphisms.rs index 2cd21ecf..c94cd79e 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -269,12 +269,12 @@ pub trait Summarization { /// /// JUMPING catamorphism implemented with recursion, for performance /// - /// Each invocation of `finalize_f` may represent a whole non-branching + /// Each invocation of `summarize_f` may represent a whole non-branching /// run of path bytes, supplied as `prefix`, rather than just one path byte. /// /// Closures: /// - /// `StartF`: Creates an accumulator for a logical trie node with more than one child branch. + /// `NewAccF`: Creates an accumulator for a logical trie node with more than one child branch. /// `fn(child_mask: &ByteMask) -> Acc` /// /// `FoldChildF`: Folds one downstream child branch's `W` into the accumulator. It is called once @@ -283,54 +283,54 @@ pub trait Summarization { /// calls to associate a result with a particular byte. /// `fn(child_mask: &ByteMask, downstream: W, accumulator: &mut Acc)` /// - /// `FinalizeF`: Produces the `W` for one logical trie node and a non-branching sub-path `prefix` + /// `SummarizeF`: Produces the `W` for one logical trie node and a non-branching sub-path `prefix` /// above it. The returned `W` should summarize the subtrie from the start of `prefix`, including /// the `value` and downstream children. /// - `child_mask` describes the node's immediate child bytes. /// - `accumulator` contains the results folded from those child branches. `accumulator` is `None` /// when the node has no downstream branches. /// - `prefix` is a non-branching sub-path above the logical node. `prefix` never includes a - /// path position that is also part of a `child_mask` for this or another call to `finalize_f` + /// path position that is also part of a `child_mask` for this or another call to `summarize_f` /// `fn(child_mask: &ByteMask, value: Option<&V>, accumulator: Option, prefix: &[u8]) -> W` /// /// In a callback where `COMPUTE_MASK` is false, `child_mask` is [`ByteMask::EMPTY`]. This avoids /// materializing masks for callers that do not use them. Similarly, `COMPUTE_PATH=false` avoids /// materializing path runs and passes an empty `prefix`. These are independent controls: callers /// which need masks but not paths should use `COMPUTE_PATH=false, COMPUTE_MASK=true`. - fn recursive_cata(&self, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF) -> W + fn recursive_cata(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> W where V: Clone + Send + Sync, W: Clone, - StartF: Copy + Fn(&ByteMask) -> Acc, + NewAccF: Copy + Fn(&ByteMask) -> Acc, FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc), - FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> W, + SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> W, Self: Sized; /// A stepping (non-jumping) catamorphism for the trie. /// /// Use this when the cata must evaluate once per path byte, including bytes in non-branching - /// runs. Unlike the jumping version, `finalize_f` has no `prefix`: it is called once for every + /// runs. Unlike the jumping version, `summarize_f` has no `prefix`: it is called once for every /// path byte. The callback roles and child-mask ordering are otherwise the same as for /// [`Summarization::recursive_cata`]. - fn recursive_cata_stepping(&self, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF) -> W + fn recursive_cata_stepping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> W where V: Clone + Send + Sync, W: Clone, - StartF: Copy + Fn(&ByteMask) -> Acc, + NewAccF: Copy + Fn(&ByteMask) -> Acc, FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc), - FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option) -> W, + SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option) -> W, Self: Sized { self.recursive_cata::<_, _, _, _, _, true, COMPUTE_MASK>( - start_f, + new_acc_f, fold_child_f, |mask, val, acc, prefix| { - let mut w = finalize_f(mask, val, acc); + let mut w = summarize_f(mask, val, acc); for byte in prefix.iter().rev() { let mask = if COMPUTE_MASK { ByteMask::from(*byte) } else { ByteMask::EMPTY }; - let mut acc = start_f(&mask); + let mut acc = new_acc_f(&mask); fold_child_f(&mask, w, &mut acc); - w = finalize_f(&mask, None, Some(acc)); + w = summarize_f(&mask, None, Some(acc)); } w }, @@ -528,41 +528,41 @@ impl Catamorph } impl<'a, Z, V: Clone + Send + Sync, A: Allocator> Summarization for Z where Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer + ZipperInfallibleSubtries { - fn recursive_cata(&self, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF) -> W + fn recursive_cata(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> W where V: Clone + Send + Sync, W: Clone, - StartF: Copy + Fn(&ByteMask) -> Acc, + NewAccF: Copy + Fn(&ByteMask) -> Acc, FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc), - FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> W, + SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> W, { let focus = self.get_focus(); let w = match focus.0.borrow() { Some(node) => { let mut cache = HashMap::new(); - recursive_cata_cached::<_, _, Acc, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(node, self.val(), start_f, fold_child_f, finalize_f, &mut cache) + recursive_cata_cached::<_, _, Acc, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(node, self.val(), new_acc_f, fold_child_f, summarize_f, &mut cache) }, - None => finalize_f(&ByteMask::EMPTY, None, None, &[]), + None => summarize_f(&ByteMask::EMPTY, None, None, &[]), }; w } } impl Summarization for PathMap { - fn recursive_cata(&self, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF) -> W + fn recursive_cata(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> W where V: Clone + Send + Sync, W: Clone, - StartF: Copy + Fn(&ByteMask) -> Acc, + NewAccF: Copy + Fn(&ByteMask) -> Acc, FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc), - FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> W, + SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> W, { let w = match self.root() { Some(node) => { let mut cache = HashMap::new(); - recursive_cata_cached::<_, _, Acc, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(node, self.root_val(), start_f, fold_child_f, finalize_f, &mut cache) + recursive_cata_cached::<_, _, Acc, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(node, self.root_val(), new_acc_f, fold_child_f, summarize_f, &mut cache) }, - None => finalize_f(&ByteMask::EMPTY, None, None, &[]), + None => summarize_f(&ByteMask::EMPTY, None, None, &[]), }; w } From 640b82bae4f0491f8d37661673dcf5cbfa92c90b Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Sat, 29 Aug 2026 02:22:55 -0600 Subject: [PATCH 22/50] Adding fallible exit path to recursive cata --- benches/catamorphism.rs | 30 +++--- src/dense_byte_node.rs | 22 ++-- src/line_list_node.rs | 54 +++++----- src/morphisms.rs | 229 +++++++++++++++++++++++++--------------- src/tiny_node.rs | 10 +- src/trie_map.rs | 14 ++- src/trie_node.rs | 36 +++---- 7 files changed, 229 insertions(+), 166 deletions(-) diff --git a/benches/catamorphism.rs b/benches/catamorphism.rs index b2478659..c0d0db9f 100644 --- a/benches/catamorphism.rs +++ b/benches/catamorphism.rs @@ -1,4 +1,5 @@ use divan::{Divan, Bencher, black_box}; +use core::convert::Infallible; use pathmap::morphisms::{Catamorphism, Summarization}; use pathmap::utils::ByteMask; use pathmap::utils::ints::gen_int_range; @@ -25,11 +26,11 @@ fn recursive_cata_jumping_val_count(bencher: Bencher) { let mut sink = 0usize; bencher.bench_local(|| { let rz = map.read_zipper(); - *black_box(&mut sink) = rz.recursive_cata::<_, _, _, _, _, false, false>( - |_| 0usize, - |_mask, w: usize, total| { *total += w }, - |_mask, v, total, _| (v.is_some() as usize) + total.unwrap_or(0), - ); + *black_box(&mut sink) = rz.recursive_cata::<_, _, Infallible, _, _, _, false, false>( + |_| Ok(0usize), + |_mask, w: usize, total| { *total += w; Ok(()) }, + |_mask, v, total, _| Ok((v.is_some() as usize) + total.unwrap_or(0)), + ).unwrap(); }); assert_eq!(sink, MAP_COUNT as usize); } @@ -42,11 +43,11 @@ fn recursive_cata_jumping_val_count_with_masks(bencher: Bencher) { let mut sink = 0usize; bencher.bench_local(|| { let rz = map.read_zipper(); - *black_box(&mut sink) = rz.recursive_cata::<_, _, _, _, _, false, true>( - |_| 0usize, - |_mask, w: usize, total| { *total += w }, - |_mask, v, total, _| (v.is_some() as usize) + total.unwrap_or(0), - ); + *black_box(&mut sink) = rz.recursive_cata::<_, _, Infallible, _, _, _, false, true>( + |_| Ok(0usize), + |_mask, w: usize, total| { *total += w; Ok(()) }, + |_mask, v, total, _| Ok((v.is_some() as usize) + total.unwrap_or(0)), + ).unwrap(); }); assert_eq!(sink, MAP_COUNT as usize); } @@ -74,18 +75,19 @@ fn recursive_cata_jumping_total_len(bencher: Bencher) { let mut sink = (0usize, 0usize); bencher.bench_local(|| { let rz = map.read_zipper(); - *black_box(&mut sink) = rz.recursive_cata::<_, _, _, _, _, true, true>( - |_| (0usize, 0usize), + *black_box(&mut sink) = rz.recursive_cata::<_, _, Infallible, _, _, _, true, true>( + |_| Ok((0usize, 0usize)), |_mask: &ByteMask, w: (usize, usize), acc: &mut (usize, usize)| { acc.0 += w.0; acc.1 += w.1; + Ok(()) }, |_mask: &ByteMask, val, acc, prefix| { let (count, total_len) = acc.unwrap_or((0, 0)); let count = count + val.is_some() as usize; - (count, total_len + count * prefix.len()) + Ok((count, total_len + count * prefix.len())) }, - ); + ).unwrap(); }); assert_eq!(sink.0, MAP_COUNT as usize); } diff --git a/src/dense_byte_node.rs b/src/dense_byte_node.rs index 926878de..fcd34c60 100644 --- a/src/dense_byte_node.rs +++ b/src/dense_byte_node.rs @@ -388,15 +388,15 @@ impl> ByteNode } #[inline(always)] - pub(crate) fn node_recursive_cata(&self, passed_in_val: Option<&V>, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF, cache: &mut HashMap) -> W + pub(crate) fn node_recursive_cata(&self, passed_in_val: Option<&V>, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF, cache: &mut HashMap) -> Result where W: Clone, - StartF: Copy + Fn(&ByteMask) -> Acc, - FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc), - FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> W, + StartF: Copy + Fn(&ByteMask) -> Result, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, + FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, { let mask = if COMPUTE_MASK { &self.mask } else { &ByteMask::EMPTY }; - let mut ws = Some(start_f(mask)); + let mut ws = Some(start_f(mask)?); for cf in self.values.iter() { let path = &[]; @@ -409,18 +409,18 @@ impl> ByteNode // like this gives the optimizer a site to specialize for each permutation match (cf.rec(), cf.val()) { (Some(rec), Some(val)) => { - let w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(rec, Some(val), start_f, fold_child_f, finalize_f, cache); - fold_child_f(mask, w, unsafe { ws.as_mut().unwrap_unchecked() }); + let w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(rec, Some(val), start_f, fold_child_f, finalize_f, cache)?; + fold_child_f(mask, w, unsafe { ws.as_mut().unwrap_unchecked() })?; }, (Some(rec), None) => { - let w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(rec, None, start_f, fold_child_f, finalize_f, cache); - fold_child_f(mask, summarize_run::<_, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(None, Some(w), path, start_f, fold_child_f, finalize_f), unsafe { ws.as_mut().unwrap_unchecked() }); + let w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(rec, None, start_f, fold_child_f, finalize_f, cache)?; + fold_child_f(mask, summarize_run::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(None, Some(w), path, start_f, fold_child_f, finalize_f)?, unsafe { ws.as_mut().unwrap_unchecked() })?; }, (None, Some(val)) => { - fold_child_f(mask, summarize_run::<_, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(Some(val), None, path, start_f, fold_child_f, finalize_f), unsafe { ws.as_mut().unwrap_unchecked() }); + fold_child_f(mask, summarize_run::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(Some(val), None, path, start_f, fold_child_f, finalize_f)?, unsafe { ws.as_mut().unwrap_unchecked() })?; }, (None, None) => { - fold_child_f(mask, summarize_run::<_, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(None, None, path, start_f, fold_child_f, finalize_f), unsafe { ws.as_mut().unwrap_unchecked() }); + fold_child_f(mask, summarize_run::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(None, None, path, start_f, fold_child_f, finalize_f)?, unsafe { ws.as_mut().unwrap_unchecked() })?; }, } } diff --git a/src/line_list_node.rs b/src/line_list_node.rs index fc5a7d31..9e4ce2cd 100644 --- a/src/line_list_node.rs +++ b/src/line_list_node.rs @@ -2781,16 +2781,16 @@ impl LineListNode { } #[inline(always)] - pub(crate) fn node_recursive_cata(&self, passed_in_val: Option<&V>, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF, cache: &mut HashMap) -> W + pub(crate) fn node_recursive_cata(&self, passed_in_val: Option<&V>, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF, cache: &mut HashMap) -> Result where W: Clone, - StartF: Copy + Fn(&ByteMask) -> Acc, - FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc), - FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> W, + StartF: Copy + Fn(&ByteMask) -> Result, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, + FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, { macro_rules! summarize { ($val:expr, $downstream:expr, $prefix:expr) => { - summarize_run::<_, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>($val, $downstream, $prefix, start_f, fold_child_f, finalize_f) + summarize_run::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>($val, $downstream, $prefix, start_f, fold_child_f, finalize_f) }; } //Pair node can have the following permutations: (Slot0, Slot1) @@ -2827,7 +2827,7 @@ impl LineListNode { //Case 2 (Child, Empty) = (1 << 3) + (1 << 1) | (1 << 3) + (1 << 1) + 1 10 | 11 => { let child_node = unsafe{ self.child_in_slot::<0>() }; - let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, None, start_f, fold_child_f, finalize_f, cache); + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, None, start_f, fold_child_f, finalize_f, cache)?; let path = unsafe{ self.key_unchecked::<0>() }; summarize!(passed_in_val, Some(child_w), path) }, @@ -2842,19 +2842,19 @@ impl LineListNode { debug_assert_eq!(key0.len(), 1); debug_assert_eq!(key1.len(), 1); let val = unsafe { self.val_in_slot::<1>() }; - let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, Some(val), start_f, fold_child_f, finalize_f, cache); + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, Some(val), start_f, fold_child_f, finalize_f, cache)?; summarize!(passed_in_val, Some(child_w), key0) } else { //Case 4 - let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, None, start_f, fold_child_f, finalize_f, cache); + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, None, start_f, fold_child_f, finalize_f, cache)?; let path = &key0[1..]; let mask = if COMPUTE_MASK { ByteMask::from((key0_byte, key1_byte)) } else { ByteMask::EMPTY }; - let mut acc = start_f(&mask); - fold_child_f(&mask, summarize!(None, Some(child_w), path), &mut acc); + let mut acc = start_f(&mask)?; + fold_child_f(&mask, summarize!(None, Some(child_w), path)?, &mut acc)?; let val = unsafe { self.val_in_slot::<1>() }; let path = &key1[1..]; - fold_child_f(&mask, summarize!(Some(val), None, path), &mut acc); + fold_child_f(&mask, summarize!(Some(val), None, path)?, &mut acc)?; finalize_f(&mask, passed_in_val, Some(acc), &[]) } @@ -2867,14 +2867,14 @@ impl LineListNode { let path0 = &key0[1..]; let path1 = &key1[1..]; let mask = if COMPUTE_MASK { ByteMask::from((key0_byte, key1_byte)) } else { ByteMask::EMPTY }; - let mut acc = start_f(&mask); + let mut acc = start_f(&mask)?; let child_node = unsafe{ self.child_in_slot::<0>() }; - let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, None, start_f, fold_child_f, finalize_f, cache); - fold_child_f(&mask, summarize!(None, Some(child_w), path0), &mut acc); + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, None, start_f, fold_child_f, finalize_f, cache)?; + fold_child_f(&mask, summarize!(None, Some(child_w), path0)?, &mut acc)?; let child_node = unsafe{ self.child_in_slot::<1>() }; - let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, None, start_f, fold_child_f, finalize_f, cache); - fold_child_f(&mask, summarize!(None, Some(child_w), path1), &mut acc); + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, None, start_f, fold_child_f, finalize_f, cache)?; + fold_child_f(&mask, summarize!(None, Some(child_w), path1)?, &mut acc)?; finalize_f(&mask, passed_in_val, Some(acc), &[]) }, @@ -2882,7 +2882,7 @@ impl LineListNode { 8 | 9 => { let val = unsafe { self.val_in_slot::<0>() }; let path = unsafe{ self.key_unchecked::<0>() }; - summarize!(passed_in_val, Some(summarize!(Some(val), None, path)), &[]) + summarize!(passed_in_val, Some(summarize!(Some(val), None, path)?), &[]) }, //(Val, Val) = (1 << 3) + (1 << 2) 12 => { @@ -2894,21 +2894,21 @@ impl LineListNode { debug_assert_eq!(key0.len(), 1); debug_assert!(key1.len() > 1); let val = unsafe { self.val_in_slot::<1>() }; - let w = summarize!(Some(val), None, &[]); + let w = summarize!(Some(val), None, &[])?; let val = unsafe { self.val_in_slot::<0>() }; - let w = summarize!(Some(val), Some(w), &key1[1..]); + let w = summarize!(Some(val), Some(w), &key1[1..])?; summarize!(passed_in_val, Some(w), &key1[0..1]) } else { //Case 8 (Val, Val), different first bytes let path0 = &key0[1..]; let path1 = &key1[1..]; let mask = if COMPUTE_MASK { ByteMask::from((key0_byte, key1_byte)) } else { ByteMask::EMPTY }; - let mut acc = start_f(&mask); + let mut acc = start_f(&mask)?; let val = unsafe{ self.val_in_slot::<0>() }; - fold_child_f(&mask, summarize!(Some(val), None, path0), &mut acc); + fold_child_f(&mask, summarize!(Some(val), None, path0)?, &mut acc)?; let val = unsafe{ self.val_in_slot::<1>() }; - fold_child_f(&mask, summarize!(Some(val), None, path1), &mut acc); + fold_child_f(&mask, summarize!(Some(val), None, path1)?, &mut acc)?; finalize_f(&mask, passed_in_val, Some(acc), &[]) } @@ -2924,19 +2924,19 @@ impl LineListNode { debug_assert_eq!(key0.len(), 1); debug_assert_eq!(key1.len(), 1); let val = unsafe { self.val_in_slot::<0>() }; - let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, Some(val), start_f, fold_child_f, finalize_f, cache); + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, Some(val), start_f, fold_child_f, finalize_f, cache)?; summarize!(passed_in_val, Some(child_w), key0) } else { //Case 10 (Val, Child), different key bytes - let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, None, start_f, fold_child_f, finalize_f, cache); + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, None, start_f, fold_child_f, finalize_f, cache)?; let path = &key1[1..]; let mask = if COMPUTE_MASK { ByteMask::from((key0_byte, key1_byte)) } else { ByteMask::EMPTY }; - let mut acc = start_f(&mask); - fold_child_f(&mask, summarize!(None, Some(child_w), path), &mut acc); + let mut acc = start_f(&mask)?; + fold_child_f(&mask, summarize!(None, Some(child_w), path)?, &mut acc)?; let val = unsafe { self.val_in_slot::<0>() }; let path = &key0[1..]; - fold_child_f(&mask, summarize!(Some(val), None, path), &mut acc); + fold_child_f(&mask, summarize!(Some(val), None, path)?, &mut acc)?; finalize_f(&mask, passed_in_val, Some(acc), &[]) } diff --git a/src/morphisms.rs b/src/morphisms.rs index c94cd79e..f7b82b4e 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -275,13 +275,13 @@ pub trait Summarization { /// Closures: /// /// `NewAccF`: Creates an accumulator for a logical trie node with more than one child branch. - /// `fn(child_mask: &ByteMask) -> Acc` + /// `fn(child_mask: &ByteMask) -> Result` /// /// `FoldChildF`: Folds one downstream child branch's `W` into the accumulator. It is called once /// for each downstream child branch, in the same order as the bits in `child_mask`. Each call for /// a given logical node receives the same full child mask; the callback must use the order of its /// calls to associate a result with a particular byte. - /// `fn(child_mask: &ByteMask, downstream: W, accumulator: &mut Acc)` + /// `fn(child_mask: &ByteMask, downstream: W, accumulator: &mut Acc) -> Result<(), Err>` /// /// `SummarizeF`: Produces the `W` for one logical trie node and a non-branching sub-path `prefix` /// above it. The returned `W` should summarize the subtrie from the start of `prefix`, including @@ -291,19 +291,21 @@ pub trait Summarization { /// when the node has no downstream branches. /// - `prefix` is a non-branching sub-path above the logical node. `prefix` never includes a /// path position that is also part of a `child_mask` for this or another call to `summarize_f` - /// `fn(child_mask: &ByteMask, value: Option<&V>, accumulator: Option, prefix: &[u8]) -> W` + /// `fn(child_mask: &ByteMask, value: Option<&V>, accumulator: Option, prefix: &[u8]) -> Result` + /// + /// Errors from any callback immediately stop traversal and are returned to the caller. /// /// In a callback where `COMPUTE_MASK` is false, `child_mask` is [`ByteMask::EMPTY`]. This avoids /// materializing masks for callers that do not use them. Similarly, `COMPUTE_PATH=false` avoids /// materializing path runs and passes an empty `prefix`. These are independent controls: callers /// which need masks but not paths should use `COMPUTE_PATH=false, COMPUTE_MASK=true`. - fn recursive_cata(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> W + fn recursive_cata(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where V: Clone + Send + Sync, W: Clone, - NewAccF: Copy + Fn(&ByteMask) -> Acc, - FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc), - SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> W, + NewAccF: Copy + Fn(&ByteMask) -> Result, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, + SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, Self: Sized; /// A stepping (non-jumping) catamorphism for the trie. @@ -312,27 +314,27 @@ pub trait Summarization { /// runs. Unlike the jumping version, `summarize_f` has no `prefix`: it is called once for every /// path byte. The callback roles and child-mask ordering are otherwise the same as for /// [`Summarization::recursive_cata`]. - fn recursive_cata_stepping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> W + fn recursive_cata_stepping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where V: Clone + Send + Sync, W: Clone, - NewAccF: Copy + Fn(&ByteMask) -> Acc, - FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc), - SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option) -> W, + NewAccF: Copy + Fn(&ByteMask) -> Result, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, + SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option) -> Result, Self: Sized { - self.recursive_cata::<_, _, _, _, _, true, COMPUTE_MASK>( + self.recursive_cata::<_, _, _, _, _, _, true, COMPUTE_MASK>( new_acc_f, fold_child_f, |mask, val, acc, prefix| { - let mut w = summarize_f(mask, val, acc); + let mut w = summarize_f(mask, val, acc)?; for byte in prefix.iter().rev() { let mask = if COMPUTE_MASK { ByteMask::from(*byte) } else { ByteMask::EMPTY }; - let mut acc = new_acc_f(&mask); - fold_child_f(&mask, w, &mut acc); - w = summarize_f(&mask, None, Some(acc)); + let mut acc = new_acc_f(&mask)?; + fold_child_f(&mask, w, &mut acc)?; + w = summarize_f(&mask, None, Some(acc))?; } - w + Ok(w) }, ) } @@ -528,19 +530,19 @@ impl Catamorph } impl<'a, Z, V: Clone + Send + Sync, A: Allocator> Summarization for Z where Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer + ZipperInfallibleSubtries { - fn recursive_cata(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> W + fn recursive_cata(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where V: Clone + Send + Sync, W: Clone, - NewAccF: Copy + Fn(&ByteMask) -> Acc, - FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc), - SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> W, + NewAccF: Copy + Fn(&ByteMask) -> Result, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, + SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, { let focus = self.get_focus(); let w = match focus.0.borrow() { Some(node) => { let mut cache = HashMap::new(); - recursive_cata_cached::<_, _, Acc, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(node, self.val(), new_acc_f, fold_child_f, summarize_f, &mut cache) + recursive_cata_cached::<_, _, Acc, _, Err, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(node, self.val(), new_acc_f, fold_child_f, summarize_f, &mut cache) }, None => summarize_f(&ByteMask::EMPTY, None, None, &[]), }; @@ -549,18 +551,18 @@ impl<'a, Z, V: Clone + Send + Sync, A: Allocator> Summarization for Z wher } impl Summarization for PathMap { - fn recursive_cata(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> W + fn recursive_cata(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where V: Clone + Send + Sync, W: Clone, - NewAccF: Copy + Fn(&ByteMask) -> Acc, - FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc), - SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> W, + NewAccF: Copy + Fn(&ByteMask) -> Result, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, + SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, { let w = match self.root() { Some(node) => { let mut cache = HashMap::new(); - recursive_cata_cached::<_, _, Acc, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(node, self.root_val(), new_acc_f, fold_child_f, summarize_f, &mut cache) + recursive_cata_cached::<_, _, Acc, _, Err, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(node, self.root_val(), new_acc_f, fold_child_f, summarize_f, &mut cache) }, None => summarize_f(&ByteMask::EMPTY, None, None, &[]), }; @@ -573,25 +575,25 @@ impl Summarization for PathM //NOTE: #[inline(always)] here leads to a huge bloating of the size of the a stack frame in the recursive cata // (about 2.5x bloat. but #[inline(never)] costs about 25% performance. Letting the compiler do its thing seems // to be the sweet spot. -pub(crate) fn summarize_run( +pub(crate) fn summarize_run( val: Option<&V>, downstream: Option, prefix: &[u8], start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF, -) -> W +) -> Result where - StartF: Copy + Fn(&ByteMask) -> Acc, - FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc), - FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> W, + StartF: Copy + Fn(&ByteMask) -> Result, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, + FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, { match (val, downstream, prefix) { - (None, Some(w), []) => w, + (None, Some(w), []) => Ok(w), (val, Some(w), prefix) => { let mask = if COMPUTE_MASK && !prefix.is_empty() { ByteMask::from(*prefix.last().unwrap()) } else { ByteMask::EMPTY }; - let mut acc = start_f(&mask); - fold_child_f(&mask, w, &mut acc); + let mut acc = start_f(&mask)?; + fold_child_f(&mask, w, &mut acc)?; let prefix = if COMPUTE_PATH { if prefix.is_empty() { prefix } else { &prefix[..prefix.len() - 1] } } else { @@ -2164,8 +2166,8 @@ mod tests { for (keys, expected_sum) in tests { let map: PathMap<()> = keys.into_iter().map(|v| (v, ())).collect(); - let sum = map.recursive_cata::<_, _, _, _, _, true, true>( - |_| SumAcc::default(), + let sum = map.recursive_cata::<_, _, Infallible, _, _, _, true, true>( + |_| Ok(SumAcc::default()), |mask: &ByteMask, w: (bool, u32), acc: &mut SumAcc| { if let Some(byte) = mask.indexed_bit::(acc.idx) { acc.idx += 1; @@ -2174,6 +2176,7 @@ mod tests { } } acc.sum += w.1; + Ok(()) }, |_mask, val, acc, prefix| { let mut sum = acc.map(|acc| acc.sum).unwrap_or(0); @@ -2182,9 +2185,9 @@ mod tests { sum += (*byte as char).to_digit(10).unwrap(); } } - (val.is_some() && prefix.is_empty(), sum) + Ok((val.is_some() && prefix.is_empty(), sum)) }, - ).1; + ).unwrap().1; assert_eq!(sum, expected_sum); } } @@ -2214,8 +2217,8 @@ mod tests { for (keys, expected_sum) in tests { let map: PathMap<()> = keys.into_iter().map(|v| (v, ())).collect(); - let sum = map.recursive_cata_stepping::( - |_| SumAcc::default(), + let sum = map.recursive_cata_stepping::( + |_| Ok(SumAcc::default()), |mask: &ByteMask, w: (bool, u32), acc: &mut SumAcc| { if let Some(byte) = mask.iter().nth(acc.idx) { acc.idx += 1; @@ -2224,11 +2227,12 @@ mod tests { } } acc.sum += w.1; + Ok(()) }, |_mask, val, acc| { - (val.is_some(), acc.map(|acc| acc.sum).unwrap_or(0)) + Ok((val.is_some(), acc.map(|acc| acc.sum).unwrap_or(0))) }, - ).1; + ).unwrap().1; assert_eq!(sum, expected_sum); } } @@ -2258,33 +2262,33 @@ mod tests { } }); - let jumping = map.recursive_cata::( - |_| 0, - |_mask, child, total| *total += child, + let jumping = map.recursive_cata::( + |_| Ok(0), + |_mask, child, total| { *total += child; Ok(()) }, |_mask, val, children, _prefix| match children { - Some(total) => total, + Some(total) => Ok(total), None => { assert!(val.is_some()); - 1 + Ok(1) }, }, ); - let stepping = map.recursive_cata_stepping::( - |_| 0, - |_mask, child, total| *total += child, + let stepping = map.recursive_cata_stepping::( + |_| Ok(0), + |_mask, child, total| { *total += child; Ok(()) }, |_mask, val, children| match children { - Some(total) => total, + Some(total) => Ok(total), None => { assert!(val.is_some()); - 1 + Ok(1) }, }, ); assert_eq!(cached_stepping, 11); assert_eq!(cached_jumping, cached_stepping); - assert_eq!(jumping, cached_jumping); - assert_eq!(stepping, cached_stepping); + assert_eq!(jumping.unwrap(), cached_jumping); + assert_eq!(stepping.unwrap(), cached_stepping); } /// Ports the pure longest-path calculation from `cata_test2`. The stepping @@ -2310,9 +2314,9 @@ mod tests { }); // This uses allocation for readability; performance-sensitive code can fold a longest path directly. - let jumping = map.recursive_cata::>, Vec, _, _, _, true, true>( - |_| Vec::new(), - |_mask, child, children| children.push(child), + let jumping = map.recursive_cata::>, Vec, Infallible, _, _, _, true, true>( + |_| Ok(Vec::new()), + |_mask, child, children| { children.push(child); Ok(()) }, |mask, _val, children, prefix| { let mut longest = children.map_or_else(Vec::new, |children| { if mask.is_empty_mask() { @@ -2328,11 +2332,11 @@ mod tests { }); let mut path = prefix.to_vec(); path.append(&mut longest); - path + Ok(path) }, ); - let stepping = map.recursive_cata_stepping::<(usize, Vec), Vec, _, _, _, true>( - |_| (0, Vec::new()), + let stepping = map.recursive_cata_stepping::<(usize, Vec), Vec, Infallible, _, _, _, true>( + |_| Ok((0, Vec::new())), |mask, child, state| { let mut path = Vec::with_capacity(child.len() + 1); if let Some(byte) = mask.indexed_bit::(state.0) { @@ -2343,13 +2347,14 @@ mod tests { if path.len() > state.1.len() { state.1 = path; } + Ok(()) }, - |_mask, _val, state| state.map_or_else(Vec::new, |(_, path)| path), + |_mask, _val, state| Ok(state.map_or_else(Vec::new, |(_, path)| path)), ); assert_eq!(std::str::from_utf8(&cached).unwrap(), "rubicundus"); - assert_eq!(jumping, cached); - assert_eq!(stepping, cached); + assert_eq!(jumping.unwrap(), cached); + assert_eq!(stepping.unwrap(), cached); } /// Ports the branch-value portion of `cata_test2` to both summarization @@ -2387,27 +2392,27 @@ mod tests { } }); - let jumping = map.recursive_cata::, Vec, _, _, _, false, true>( - |_| Vec::new(), - |_mask, child, values| values.extend(child), + let jumping = map.recursive_cata::, Vec, Infallible, _, _, _, false, true>( + |_| Ok(Vec::new()), + |_mask, child, values| { values.extend(child); Ok(()) }, |_mask, val, children, _prefix| match children { - None => Vec::new(), - Some(values) => val.map_or(values, |val| vec![*val]), + None => Ok(Vec::new()), + Some(values) => Ok(val.map_or(values, |val| vec![*val])), }, ); - let stepping = map.recursive_cata_stepping::, Vec, _, _, _, true>( - |_| Vec::new(), - |_mask, child, values| values.extend(child), + let stepping = map.recursive_cata_stepping::, Vec, Infallible, _, _, _, true>( + |_| Ok(Vec::new()), + |_mask, child, values| { values.extend(child); Ok(()) }, |_mask, val, children| match children { - None => Vec::new(), - Some(values) => val.map_or(values, |val| vec![*val]), + None => Ok(Vec::new()), + Some(values) => Ok(val.map_or(values, |val| vec![*val])), }, ); assert_eq!(cached_stepping, vec![3]); assert_eq!(cached_jumping, cached_stepping); - assert_eq!(jumping, cached_jumping); - assert_eq!(stepping, cached_stepping); + assert_eq!(jumping.unwrap(), cached_jumping); + assert_eq!(stepping.unwrap(), cached_stepping); } /// Parallel port of `cata_test_cached`: the input deliberately contains @@ -2451,16 +2456,16 @@ mod tests { }); let summarization_calls = AtomicU64::new(0); - let summarized = make_map().recursive_cata_stepping::>>, Rc>, _, _, _, true>( - |_| Vec::new(), - |_mask, child, children| children.push(child), + let summarized = make_map().recursive_cata_stepping::>>, Rc>, Infallible, _, _, _, true>( + |_| Ok(Vec::new()), + |_mask, child, children| { children.push(child); Ok(()) }, |_mask, value, children| { summarization_calls.fetch_add(1, Relaxed); - Rc::new(Node::new(value, children)) + Ok(Rc::new(Node::new(value, children))) }, ); - assert_eq!(summarized, cached); + assert_eq!(summarized.unwrap(), cached); assert_eq!(summarization_calls.load(Relaxed), cached_calls.load(Relaxed)); } @@ -2476,12 +2481,64 @@ mod tests { let path = vec![b'a'; PATH_LEN]; map.set_val_at(&path, ()); - let count = map.recursive_cata::<_, _, _, _, _, false, false>( - |_| 0usize, - |_mask, w: usize, total| { *total += w }, - |_mask, v, total, _| (v.is_some() as usize) + total.unwrap_or(0), + let count = map.recursive_cata::<_, _, Infallible, _, _, _, false, false>( + |_| Ok(0usize), + |_mask, w: usize, total| { *total += w; Ok(()) }, + |_mask, v, total, _| Ok((v.is_some() as usize) + total.unwrap_or(0)), + ); + assert_eq!(count.unwrap(), 1); + } + + #[test] + fn recursive_cata_propagates_callback_errors() { + let map: PathMap<()> = [(b"a".as_slice(), ()), (b"b".as_slice(), ())].into_iter().collect(); + + let error = map.recursive_cata::<(), (), &'static str, _, _, _, false, false>( + |_| Err("start"), + |_mask, _child, _acc| Ok(()), + |_mask, _value, _acc, _prefix| Ok(()), ); - assert_eq!(count, 1); + assert_eq!(error, Err("start")); + + let error = map.recursive_cata::<(), (), &'static str, _, _, _, false, false>( + |_| Ok(()), + |_mask, _child, _acc| Err("fold"), + |_mask, _value, _acc, _prefix| Ok(()), + ); + assert_eq!(error, Err("fold")); + + let error = map.recursive_cata::<(), (), &'static str, _, _, _, false, false>( + |_| Ok(()), + |_mask, _child, _acc| Ok(()), + |_mask, _value, _acc, _prefix| Err("summarize"), + ); + assert_eq!(error, Err("summarize")); + } + + #[test] + fn recursive_cata_stops_after_summarize_error() { + use core::sync::atomic::{AtomicUsize, Ordering::Relaxed}; + + let map: PathMap<()> = [ + (b"a".as_slice(), ()), + (b"b".as_slice(), ()), + (b"c".as_slice(), ()), + ] + .into_iter() + .collect(); + let summarize_calls = AtomicUsize::new(0); + + let error = map.recursive_cata::<(), (), &'static str, _, _, _, false, false>( + |_| Ok(()), + |_mask, _child, _acc| Ok(()), + |_mask, _value, _acc, _prefix| { + summarize_calls.fetch_add(1, Relaxed); + Err("summarize") + }, + ); + + assert_eq!(error, Err("summarize")); + assert_eq!(summarize_calls.load(Relaxed), 1); } /// Generate some basic tries using the [TrieBuilder::push_byte] API diff --git a/src/tiny_node.rs b/src/tiny_node.rs index e11ab82f..b09dd2e6 100644 --- a/src/tiny_node.rs +++ b/src/tiny_node.rs @@ -122,14 +122,14 @@ impl<'a, V: Clone + Send + Sync, A: Allocator> TinyRefNode<'a, V, A> { unsafe{ core::slice::from_raw_parts(self.key_bytes.as_ptr().cast(), self.key_len()) } } - pub(crate) fn node_recursive_cata(&self, passed_in_val: Option<&V>, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF, cache: &mut HashMap) -> W + pub(crate) fn node_recursive_cata(&self, passed_in_val: Option<&V>, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF, cache: &mut HashMap) -> Result where W: Clone, - StartF: Copy + Fn(&ByteMask) -> Acc, - FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc), - FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> W, + StartF: Copy + Fn(&ByteMask) -> Result, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, + FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, { - self.into_full().unwrap().node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(passed_in_val, start_f, fold_child_f, finalize_f, cache) + self.into_full().unwrap().node_recursive_cata::<_, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(passed_in_val, start_f, fold_child_f, finalize_f, cache) } } diff --git a/src/trie_map.rs b/src/trie_map.rs index 33b3f06d..162b889a 100644 --- a/src/trie_map.rs +++ b/src/trie_map.rs @@ -1,4 +1,5 @@ use core::cell::UnsafeCell; +use core::convert::Infallible; use crate::alloc::{Allocator, GlobalAlloc, global_alloc}; use crate::morphisms::{new_map_from_ana_in, Summarization, TrieBuilder}; use crate::trie_node::*; @@ -509,11 +510,14 @@ impl PathMap { pub fn goat_val_count(&self) -> usize { match self.root() { Some(_root) => { - self.recursive_cata::<_, _, _, _, _, false, false>( - |_| 0usize, - |_mask, w: usize, total| { *total += w }, - |_mask, v, total, _| { (v.is_some() as usize) + total.unwrap_or(0) }, - ) + match self.recursive_cata::<_, _, Infallible, _, _, _, false, false>( + |_| Ok(0usize), + |_mask, w: usize, total| { *total += w; Ok(()) }, + |_mask, v, total, _| Ok((v.is_some() as usize) + total.unwrap_or(0)), + ) { + Ok(count) => count, + Err(never) => match never {}, + } }, None => unsafe{ &*self.root_val.get() }.is_some() as usize } diff --git a/src/trie_node.rs b/src/trie_node.rs index e90c32e5..817de37b 100644 --- a/src/trie_node.rs +++ b/src/trie_node.rs @@ -2412,21 +2412,21 @@ pub(crate) fn val_count_below_node(node: & } /// Internal implementation of recursive_cata -pub(crate) fn recursive_cata_cached( +pub(crate) fn recursive_cata_cached( node: &TrieNodeODRc, passed_in_val: Option<&V>, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF, cache: &mut HashMap, -) -> W +) -> Result where V: Clone + Send + Sync, A: Allocator, W: Clone, - StartF: Copy + Fn(&ByteMask) -> Acc, - FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc), - FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> W, + StartF: Copy + Fn(&ByteMask) -> Result, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, + FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, { // NOTE: A caller-supplied value can make this trie-node boundary fall inside one Summarization callback, // so its W is not reusable by node ID alone. @@ -2435,40 +2435,40 @@ where if passed_in_val.is_none() && !node.is_empty() && node.refcount() > 1 { let hash = node.shared_node_id(); match cache.get(&hash) { - Some(cached) => cached.clone(), + Some(cached) => Ok(cached.clone()), None => { - let w = recursive_cata_dispatch::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(node, passed_in_val, start_f, fold_child_f, finalize_f, cache); + let w = recursive_cata_dispatch::<_, _, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(node, passed_in_val, start_f, fold_child_f, finalize_f, cache)?; cache.insert(hash, w.clone()); - w + Ok(w) }, } } else { - recursive_cata_dispatch::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(node, passed_in_val, start_f, fold_child_f, finalize_f, cache) + recursive_cata_dispatch::<_, _, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(node, passed_in_val, start_f, fold_child_f, finalize_f, cache) } } #[inline(always)] -fn recursive_cata_dispatch( +fn recursive_cata_dispatch( node: &TrieNodeODRc, passed_in_val: Option<&V>, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF, cache: &mut HashMap, -) -> W +) -> Result where V: Clone + Send + Sync, A: Allocator, W: Clone, - StartF: Copy + Fn(&ByteMask) -> Acc, - FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc), - FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> W, + StartF: Copy + Fn(&ByteMask) -> Result, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, + FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, { match node.as_tagged() { - TaggedNodeRef::DenseByteNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(passed_in_val, start_f, fold_child_f, finalize_f, cache) } - TaggedNodeRef::LineListNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(passed_in_val, start_f, fold_child_f, finalize_f, cache) } - TaggedNodeRef::CellByteNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(passed_in_val, start_f, fold_child_f, finalize_f, cache) } - TaggedNodeRef::TinyRefNode(node) => { node.node_recursive_cata::<_, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(passed_in_val, start_f, fold_child_f, finalize_f, cache) } + TaggedNodeRef::DenseByteNode(node) => { node.node_recursive_cata::<_, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(passed_in_val, start_f, fold_child_f, finalize_f, cache) } + TaggedNodeRef::LineListNode(node) => { node.node_recursive_cata::<_, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(passed_in_val, start_f, fold_child_f, finalize_f, cache) } + TaggedNodeRef::CellByteNode(node) => { node.node_recursive_cata::<_, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(passed_in_val, start_f, fold_child_f, finalize_f, cache) } + TaggedNodeRef::TinyRefNode(node) => { node.node_recursive_cata::<_, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(passed_in_val, start_f, fold_child_f, finalize_f, cache) } TaggedNodeRef::EmptyNode => { finalize_f(&ByteMask::EMPTY, passed_in_val, None, &[]) } } } From 2a3a4f5b41516ba41672901f2afa99f2f45545c0 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Sat, 29 Aug 2026 05:26:25 -0600 Subject: [PATCH 23/50] Removing COMPUTE_MASKS generic constant in recursive cata, because it wasn't offering a meaningful speedup when switched off --- benches/catamorphism.rs | 43 +++++++++++++++++++++++--------- src/dense_byte_node.rs | 14 +++++------ src/line_list_node.rs | 26 +++++++++---------- src/morphisms.rs | 55 ++++++++++++++++++++--------------------- src/tiny_node.rs | 4 +-- src/trie_map.rs | 2 +- src/trie_node.rs | 16 ++++++------ 7 files changed, 89 insertions(+), 71 deletions(-) diff --git a/benches/catamorphism.rs b/benches/catamorphism.rs index c0d0db9f..9fd48a15 100644 --- a/benches/catamorphism.rs +++ b/benches/catamorphism.rs @@ -20,13 +20,29 @@ fn build_map(count: u64) -> PathMap<()> { const MAP_COUNT: u64 = 20_000_000; +// A complete binary trie keeps every internal node as a two-entry LineListNode. +const BINARY_TREE_DEPTH: usize = 18; +const BINARY_TREE_LEAF_COUNT: usize = 1 << BINARY_TREE_DEPTH; + +fn build_binary_tree_map() -> PathMap<()> { + let mut map = PathMap::new(); + for leaf in 0..BINARY_TREE_LEAF_COUNT { + let mut path = [0u8; BINARY_TREE_DEPTH]; + for (level, byte) in path.iter_mut().enumerate() { + *byte = ((leaf >> (BINARY_TREE_DEPTH - level - 1)) & 1) as u8; + } + map.insert(path, ()); + } + map +} + #[divan::bench()] fn recursive_cata_jumping_val_count(bencher: Bencher) { let map = build_map(MAP_COUNT); let mut sink = 0usize; bencher.bench_local(|| { let rz = map.read_zipper(); - *black_box(&mut sink) = rz.recursive_cata::<_, _, Infallible, _, _, _, false, false>( + *black_box(&mut sink) = rz.recursive_cata::<_, _, Infallible, _, _, _, false>( |_| Ok(0usize), |_mask, w: usize, total| { *total += w; Ok(()) }, |_mask, v, total, _| Ok((v.is_some() as usize) + total.unwrap_or(0)), @@ -35,21 +51,24 @@ fn recursive_cata_jumping_val_count(bencher: Bencher) { assert_eq!(sink, MAP_COUNT as usize); } -/// Same summary as `recursive_cata_jumping_val_count`, but requests real masks. -/// This isolates the cost of the `COMPUTE_MASK` specialization. #[divan::bench()] -fn recursive_cata_jumping_val_count_with_masks(bencher: Bencher) { - let map = build_map(MAP_COUNT); +fn recursive_cata_binary_tree_leaf_count(bencher: Bencher) { + let map = build_binary_tree_map(); let mut sink = 0usize; bencher.bench_local(|| { let rz = map.read_zipper(); - *black_box(&mut sink) = rz.recursive_cata::<_, _, Infallible, _, _, _, false, true>( - |_| Ok(0usize), - |_mask, w: usize, total| { *total += w; Ok(()) }, - |_mask, v, total, _| Ok((v.is_some() as usize) + total.unwrap_or(0)), - ).unwrap(); + *black_box(&mut sink) = rz + .recursive_cata::<_, _, Infallible, _, _, _, false>( + |_| Ok(0usize), + |_mask, child_count: usize, total| { + *total += child_count; + Ok(()) + }, + |_mask, value, total, _| Ok((value.is_some() as usize) + total.unwrap_or(0)), + ) + .unwrap(); }); - assert_eq!(sink, MAP_COUNT as usize); + assert_eq!(sink, BINARY_TREE_LEAF_COUNT); } #[divan::bench()] @@ -75,7 +94,7 @@ fn recursive_cata_jumping_total_len(bencher: Bencher) { let mut sink = (0usize, 0usize); bencher.bench_local(|| { let rz = map.read_zipper(); - *black_box(&mut sink) = rz.recursive_cata::<_, _, Infallible, _, _, _, true, true>( + *black_box(&mut sink) = rz.recursive_cata::<_, _, Infallible, _, _, _, true>( |_| Ok((0usize, 0usize)), |_mask: &ByteMask, w: (usize, usize), acc: &mut (usize, usize)| { acc.0 += w.0; diff --git a/src/dense_byte_node.rs b/src/dense_byte_node.rs index fcd34c60..66e88808 100644 --- a/src/dense_byte_node.rs +++ b/src/dense_byte_node.rs @@ -388,14 +388,14 @@ impl> ByteNode } #[inline(always)] - pub(crate) fn node_recursive_cata(&self, passed_in_val: Option<&V>, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF, cache: &mut HashMap) -> Result + pub(crate) fn node_recursive_cata(&self, passed_in_val: Option<&V>, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF, cache: &mut HashMap) -> Result where W: Clone, StartF: Copy + Fn(&ByteMask) -> Result, FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, { - let mask = if COMPUTE_MASK { &self.mask } else { &ByteMask::EMPTY }; + let mask = &self.mask; let mut ws = Some(start_f(mask)?); for cf in self.values.iter() { let path = &[]; @@ -409,18 +409,18 @@ impl> ByteNode // like this gives the optimizer a site to specialize for each permutation match (cf.rec(), cf.val()) { (Some(rec), Some(val)) => { - let w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(rec, Some(val), start_f, fold_child_f, finalize_f, cache)?; + let w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(rec, Some(val), start_f, fold_child_f, finalize_f, cache)?; fold_child_f(mask, w, unsafe { ws.as_mut().unwrap_unchecked() })?; }, (Some(rec), None) => { - let w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(rec, None, start_f, fold_child_f, finalize_f, cache)?; - fold_child_f(mask, summarize_run::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(None, Some(w), path, start_f, fold_child_f, finalize_f)?, unsafe { ws.as_mut().unwrap_unchecked() })?; + let w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(rec, None, start_f, fold_child_f, finalize_f, cache)?; + fold_child_f(mask, summarize_run::<_, _, _, _, _, _, _, COMPUTE_PATH>(None, Some(w), path, start_f, fold_child_f, finalize_f)?, unsafe { ws.as_mut().unwrap_unchecked() })?; }, (None, Some(val)) => { - fold_child_f(mask, summarize_run::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(Some(val), None, path, start_f, fold_child_f, finalize_f)?, unsafe { ws.as_mut().unwrap_unchecked() })?; + fold_child_f(mask, summarize_run::<_, _, _, _, _, _, _, COMPUTE_PATH>(Some(val), None, path, start_f, fold_child_f, finalize_f)?, unsafe { ws.as_mut().unwrap_unchecked() })?; }, (None, None) => { - fold_child_f(mask, summarize_run::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(None, None, path, start_f, fold_child_f, finalize_f)?, unsafe { ws.as_mut().unwrap_unchecked() })?; + fold_child_f(mask, summarize_run::<_, _, _, _, _, _, _, COMPUTE_PATH>(None, None, path, start_f, fold_child_f, finalize_f)?, unsafe { ws.as_mut().unwrap_unchecked() })?; }, } } diff --git a/src/line_list_node.rs b/src/line_list_node.rs index 9e4ce2cd..90d5378f 100644 --- a/src/line_list_node.rs +++ b/src/line_list_node.rs @@ -2781,7 +2781,7 @@ impl LineListNode { } #[inline(always)] - pub(crate) fn node_recursive_cata(&self, passed_in_val: Option<&V>, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF, cache: &mut HashMap) -> Result + pub(crate) fn node_recursive_cata(&self, passed_in_val: Option<&V>, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF, cache: &mut HashMap) -> Result where W: Clone, StartF: Copy + Fn(&ByteMask) -> Result, @@ -2790,7 +2790,7 @@ impl LineListNode { { macro_rules! summarize { ($val:expr, $downstream:expr, $prefix:expr) => { - summarize_run::<_, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>($val, $downstream, $prefix, start_f, fold_child_f, finalize_f) + summarize_run::<_, _, _, _, _, _, _, COMPUTE_PATH>($val, $downstream, $prefix, start_f, fold_child_f, finalize_f) }; } //Pair node can have the following permutations: (Slot0, Slot1) @@ -2827,7 +2827,7 @@ impl LineListNode { //Case 2 (Child, Empty) = (1 << 3) + (1 << 1) | (1 << 3) + (1 << 1) + 1 10 | 11 => { let child_node = unsafe{ self.child_in_slot::<0>() }; - let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, None, start_f, fold_child_f, finalize_f, cache)?; + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(child_node, None, start_f, fold_child_f, finalize_f, cache)?; let path = unsafe{ self.key_unchecked::<0>() }; summarize!(passed_in_val, Some(child_w), path) }, @@ -2842,13 +2842,13 @@ impl LineListNode { debug_assert_eq!(key0.len(), 1); debug_assert_eq!(key1.len(), 1); let val = unsafe { self.val_in_slot::<1>() }; - let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, Some(val), start_f, fold_child_f, finalize_f, cache)?; + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(child_node, Some(val), start_f, fold_child_f, finalize_f, cache)?; summarize!(passed_in_val, Some(child_w), key0) } else { //Case 4 - let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, None, start_f, fold_child_f, finalize_f, cache)?; + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(child_node, None, start_f, fold_child_f, finalize_f, cache)?; let path = &key0[1..]; - let mask = if COMPUTE_MASK { ByteMask::from((key0_byte, key1_byte)) } else { ByteMask::EMPTY }; + let mask = ByteMask::from((key0_byte, key1_byte)); let mut acc = start_f(&mask)?; fold_child_f(&mask, summarize!(None, Some(child_w), path)?, &mut acc)?; @@ -2866,14 +2866,14 @@ impl LineListNode { let (key0_byte, key1_byte) = unsafe{ (*key0.get_unchecked(0), *key1.get_unchecked(0)) }; let path0 = &key0[1..]; let path1 = &key1[1..]; - let mask = if COMPUTE_MASK { ByteMask::from((key0_byte, key1_byte)) } else { ByteMask::EMPTY }; + let mask = ByteMask::from((key0_byte, key1_byte)); let mut acc = start_f(&mask)?; let child_node = unsafe{ self.child_in_slot::<0>() }; - let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, None, start_f, fold_child_f, finalize_f, cache)?; + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(child_node, None, start_f, fold_child_f, finalize_f, cache)?; fold_child_f(&mask, summarize!(None, Some(child_w), path0)?, &mut acc)?; let child_node = unsafe{ self.child_in_slot::<1>() }; - let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, None, start_f, fold_child_f, finalize_f, cache)?; + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(child_node, None, start_f, fold_child_f, finalize_f, cache)?; fold_child_f(&mask, summarize!(None, Some(child_w), path1)?, &mut acc)?; finalize_f(&mask, passed_in_val, Some(acc), &[]) @@ -2902,7 +2902,7 @@ impl LineListNode { //Case 8 (Val, Val), different first bytes let path0 = &key0[1..]; let path1 = &key1[1..]; - let mask = if COMPUTE_MASK { ByteMask::from((key0_byte, key1_byte)) } else { ByteMask::EMPTY }; + let mask = ByteMask::from((key0_byte, key1_byte)); let mut acc = start_f(&mask)?; let val = unsafe{ self.val_in_slot::<0>() }; fold_child_f(&mask, summarize!(Some(val), None, path0)?, &mut acc)?; @@ -2924,13 +2924,13 @@ impl LineListNode { debug_assert_eq!(key0.len(), 1); debug_assert_eq!(key1.len(), 1); let val = unsafe { self.val_in_slot::<0>() }; - let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, Some(val), start_f, fold_child_f, finalize_f, cache)?; + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(child_node, Some(val), start_f, fold_child_f, finalize_f, cache)?; summarize!(passed_in_val, Some(child_w), key0) } else { //Case 10 (Val, Child), different key bytes - let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(child_node, None, start_f, fold_child_f, finalize_f, cache)?; + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(child_node, None, start_f, fold_child_f, finalize_f, cache)?; let path = &key1[1..]; - let mask = if COMPUTE_MASK { ByteMask::from((key0_byte, key1_byte)) } else { ByteMask::EMPTY }; + let mask = ByteMask::from((key0_byte, key1_byte)); let mut acc = start_f(&mask)?; fold_child_f(&mask, summarize!(None, Some(child_w), path)?, &mut acc)?; diff --git a/src/morphisms.rs b/src/morphisms.rs index f7b82b4e..d6c478c7 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -295,11 +295,10 @@ pub trait Summarization { /// /// Errors from any callback immediately stop traversal and are returned to the caller. /// - /// In a callback where `COMPUTE_MASK` is false, `child_mask` is [`ByteMask::EMPTY`]. This avoids - /// materializing masks for callers that do not use them. Similarly, `COMPUTE_PATH=false` avoids - /// materializing path runs and passes an empty `prefix`. These are independent controls: callers - /// which need masks but not paths should use `COMPUTE_PATH=false, COMPUTE_MASK=true`. - fn recursive_cata(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result + /// `COMPUTE_PATH=false` avoids materializing path runs and passes an empty `prefix`. This should + /// only be used when the algebra is agnostic to the path bytes, and only sensitive to values and/or + /// path endpoints. + fn recursive_cata(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where V: Clone + Send + Sync, W: Clone, @@ -314,7 +313,7 @@ pub trait Summarization { /// runs. Unlike the jumping version, `summarize_f` has no `prefix`: it is called once for every /// path byte. The callback roles and child-mask ordering are otherwise the same as for /// [`Summarization::recursive_cata`]. - fn recursive_cata_stepping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result + fn recursive_cata_stepping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where V: Clone + Send + Sync, W: Clone, @@ -323,13 +322,13 @@ pub trait Summarization { SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option) -> Result, Self: Sized { - self.recursive_cata::<_, _, _, _, _, _, true, COMPUTE_MASK>( + self.recursive_cata::<_, _, _, _, _, _, true>( new_acc_f, fold_child_f, |mask, val, acc, prefix| { let mut w = summarize_f(mask, val, acc)?; for byte in prefix.iter().rev() { - let mask = if COMPUTE_MASK { ByteMask::from(*byte) } else { ByteMask::EMPTY }; + let mask = ByteMask::from(*byte); let mut acc = new_acc_f(&mask)?; fold_child_f(&mask, w, &mut acc)?; w = summarize_f(&mask, None, Some(acc))?; @@ -530,7 +529,7 @@ impl Catamorph } impl<'a, Z, V: Clone + Send + Sync, A: Allocator> Summarization for Z where Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer + ZipperInfallibleSubtries { - fn recursive_cata(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result + fn recursive_cata(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where V: Clone + Send + Sync, W: Clone, @@ -542,7 +541,7 @@ impl<'a, Z, V: Clone + Send + Sync, A: Allocator> Summarization for Z wher let w = match focus.0.borrow() { Some(node) => { let mut cache = HashMap::new(); - recursive_cata_cached::<_, _, Acc, _, Err, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(node, self.val(), new_acc_f, fold_child_f, summarize_f, &mut cache) + recursive_cata_cached::<_, _, Acc, _, Err, _, _, _, COMPUTE_PATH>(node, self.val(), new_acc_f, fold_child_f, summarize_f, &mut cache) }, None => summarize_f(&ByteMask::EMPTY, None, None, &[]), }; @@ -551,7 +550,7 @@ impl<'a, Z, V: Clone + Send + Sync, A: Allocator> Summarization for Z wher } impl Summarization for PathMap { - fn recursive_cata(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result + fn recursive_cata(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where V: Clone + Send + Sync, W: Clone, @@ -562,7 +561,7 @@ impl Summarization for PathM let w = match self.root() { Some(node) => { let mut cache = HashMap::new(); - recursive_cata_cached::<_, _, Acc, _, Err, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(node, self.root_val(), new_acc_f, fold_child_f, summarize_f, &mut cache) + recursive_cata_cached::<_, _, Acc, _, Err, _, _, _, COMPUTE_PATH>(node, self.root_val(), new_acc_f, fold_child_f, summarize_f, &mut cache) }, None => summarize_f(&ByteMask::EMPTY, None, None, &[]), }; @@ -575,7 +574,7 @@ impl Summarization for PathM //NOTE: #[inline(always)] here leads to a huge bloating of the size of the a stack frame in the recursive cata // (about 2.5x bloat. but #[inline(never)] costs about 25% performance. Letting the compiler do its thing seems // to be the sweet spot. -pub(crate) fn summarize_run( +pub(crate) fn summarize_run( val: Option<&V>, downstream: Option, prefix: &[u8], @@ -591,7 +590,7 @@ where match (val, downstream, prefix) { (None, Some(w), []) => Ok(w), (val, Some(w), prefix) => { - let mask = if COMPUTE_MASK && !prefix.is_empty() { ByteMask::from(*prefix.last().unwrap()) } else { ByteMask::EMPTY }; + let mask = if !prefix.is_empty() { ByteMask::from(*prefix.last().unwrap()) } else { ByteMask::EMPTY }; let mut acc = start_f(&mask)?; fold_child_f(&mask, w, &mut acc)?; let prefix = if COMPUTE_PATH { @@ -2166,7 +2165,7 @@ mod tests { for (keys, expected_sum) in tests { let map: PathMap<()> = keys.into_iter().map(|v| (v, ())).collect(); - let sum = map.recursive_cata::<_, _, Infallible, _, _, _, true, true>( + let sum = map.recursive_cata::<_, _, Infallible, _, _, _, true>( |_| Ok(SumAcc::default()), |mask: &ByteMask, w: (bool, u32), acc: &mut SumAcc| { if let Some(byte) = mask.indexed_bit::(acc.idx) { @@ -2217,7 +2216,7 @@ mod tests { for (keys, expected_sum) in tests { let map: PathMap<()> = keys.into_iter().map(|v| (v, ())).collect(); - let sum = map.recursive_cata_stepping::( + let sum = map.recursive_cata_stepping::( |_| Ok(SumAcc::default()), |mask: &ByteMask, w: (bool, u32), acc: &mut SumAcc| { if let Some(byte) = mask.iter().nth(acc.idx) { @@ -2262,7 +2261,7 @@ mod tests { } }); - let jumping = map.recursive_cata::( + let jumping = map.recursive_cata::( |_| Ok(0), |_mask, child, total| { *total += child; Ok(()) }, |_mask, val, children, _prefix| match children { @@ -2273,7 +2272,7 @@ mod tests { }, }, ); - let stepping = map.recursive_cata_stepping::( + let stepping = map.recursive_cata_stepping::( |_| Ok(0), |_mask, child, total| { *total += child; Ok(()) }, |_mask, val, children| match children { @@ -2314,7 +2313,7 @@ mod tests { }); // This uses allocation for readability; performance-sensitive code can fold a longest path directly. - let jumping = map.recursive_cata::>, Vec, Infallible, _, _, _, true, true>( + let jumping = map.recursive_cata::>, Vec, Infallible, _, _, _, true>( |_| Ok(Vec::new()), |_mask, child, children| { children.push(child); Ok(()) }, |mask, _val, children, prefix| { @@ -2335,7 +2334,7 @@ mod tests { Ok(path) }, ); - let stepping = map.recursive_cata_stepping::<(usize, Vec), Vec, Infallible, _, _, _, true>( + let stepping = map.recursive_cata_stepping::<(usize, Vec), Vec, Infallible, _, _, _>( |_| Ok((0, Vec::new())), |mask, child, state| { let mut path = Vec::with_capacity(child.len() + 1); @@ -2392,7 +2391,7 @@ mod tests { } }); - let jumping = map.recursive_cata::, Vec, Infallible, _, _, _, false, true>( + let jumping = map.recursive_cata::, Vec, Infallible, _, _, _, false>( |_| Ok(Vec::new()), |_mask, child, values| { values.extend(child); Ok(()) }, |_mask, val, children, _prefix| match children { @@ -2400,7 +2399,7 @@ mod tests { Some(values) => Ok(val.map_or(values, |val| vec![*val])), }, ); - let stepping = map.recursive_cata_stepping::, Vec, Infallible, _, _, _, true>( + let stepping = map.recursive_cata_stepping::, Vec, Infallible, _, _, _>( |_| Ok(Vec::new()), |_mask, child, values| { values.extend(child); Ok(()) }, |_mask, val, children| match children { @@ -2456,7 +2455,7 @@ mod tests { }); let summarization_calls = AtomicU64::new(0); - let summarized = make_map().recursive_cata_stepping::>>, Rc>, Infallible, _, _, _, true>( + let summarized = make_map().recursive_cata_stepping::>>, Rc>, Infallible, _, _, _>( |_| Ok(Vec::new()), |_mask, child, children| { children.push(child); Ok(()) }, |_mask, value, children| { @@ -2481,7 +2480,7 @@ mod tests { let path = vec![b'a'; PATH_LEN]; map.set_val_at(&path, ()); - let count = map.recursive_cata::<_, _, Infallible, _, _, _, false, false>( + let count = map.recursive_cata::<_, _, Infallible, _, _, _, false>( |_| Ok(0usize), |_mask, w: usize, total| { *total += w; Ok(()) }, |_mask, v, total, _| Ok((v.is_some() as usize) + total.unwrap_or(0)), @@ -2493,21 +2492,21 @@ mod tests { fn recursive_cata_propagates_callback_errors() { let map: PathMap<()> = [(b"a".as_slice(), ()), (b"b".as_slice(), ())].into_iter().collect(); - let error = map.recursive_cata::<(), (), &'static str, _, _, _, false, false>( + let error = map.recursive_cata::<(), (), &'static str, _, _, _, false>( |_| Err("start"), |_mask, _child, _acc| Ok(()), |_mask, _value, _acc, _prefix| Ok(()), ); assert_eq!(error, Err("start")); - let error = map.recursive_cata::<(), (), &'static str, _, _, _, false, false>( + let error = map.recursive_cata::<(), (), &'static str, _, _, _, false>( |_| Ok(()), |_mask, _child, _acc| Err("fold"), |_mask, _value, _acc, _prefix| Ok(()), ); assert_eq!(error, Err("fold")); - let error = map.recursive_cata::<(), (), &'static str, _, _, _, false, false>( + let error = map.recursive_cata::<(), (), &'static str, _, _, _, false>( |_| Ok(()), |_mask, _child, _acc| Ok(()), |_mask, _value, _acc, _prefix| Err("summarize"), @@ -2528,7 +2527,7 @@ mod tests { .collect(); let summarize_calls = AtomicUsize::new(0); - let error = map.recursive_cata::<(), (), &'static str, _, _, _, false, false>( + let error = map.recursive_cata::<(), (), &'static str, _, _, _, false>( |_| Ok(()), |_mask, _child, _acc| Ok(()), |_mask, _value, _acc, _prefix| { diff --git a/src/tiny_node.rs b/src/tiny_node.rs index b09dd2e6..41886566 100644 --- a/src/tiny_node.rs +++ b/src/tiny_node.rs @@ -122,14 +122,14 @@ impl<'a, V: Clone + Send + Sync, A: Allocator> TinyRefNode<'a, V, A> { unsafe{ core::slice::from_raw_parts(self.key_bytes.as_ptr().cast(), self.key_len()) } } - pub(crate) fn node_recursive_cata(&self, passed_in_val: Option<&V>, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF, cache: &mut HashMap) -> Result + pub(crate) fn node_recursive_cata(&self, passed_in_val: Option<&V>, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF, cache: &mut HashMap) -> Result where W: Clone, StartF: Copy + Fn(&ByteMask) -> Result, FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, { - self.into_full().unwrap().node_recursive_cata::<_, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(passed_in_val, start_f, fold_child_f, finalize_f, cache) + self.into_full().unwrap().node_recursive_cata::<_, _, _, _, _, _, COMPUTE_PATH>(passed_in_val, start_f, fold_child_f, finalize_f, cache) } } diff --git a/src/trie_map.rs b/src/trie_map.rs index 162b889a..b26b2627 100644 --- a/src/trie_map.rs +++ b/src/trie_map.rs @@ -510,7 +510,7 @@ impl PathMap { pub fn goat_val_count(&self) -> usize { match self.root() { Some(_root) => { - match self.recursive_cata::<_, _, Infallible, _, _, _, false, false>( + match self.recursive_cata::<_, _, Infallible, _, _, _, false>( |_| Ok(0usize), |_mask, w: usize, total| { *total += w; Ok(()) }, |_mask, v, total, _| Ok((v.is_some() as usize) + total.unwrap_or(0)), diff --git a/src/trie_node.rs b/src/trie_node.rs index 817de37b..849044fa 100644 --- a/src/trie_node.rs +++ b/src/trie_node.rs @@ -2412,7 +2412,7 @@ pub(crate) fn val_count_below_node(node: & } /// Internal implementation of recursive_cata -pub(crate) fn recursive_cata_cached( +pub(crate) fn recursive_cata_cached( node: &TrieNodeODRc, passed_in_val: Option<&V>, start_f: StartF, @@ -2437,18 +2437,18 @@ where match cache.get(&hash) { Some(cached) => Ok(cached.clone()), None => { - let w = recursive_cata_dispatch::<_, _, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(node, passed_in_val, start_f, fold_child_f, finalize_f, cache)?; + let w = recursive_cata_dispatch::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(node, passed_in_val, start_f, fold_child_f, finalize_f, cache)?; cache.insert(hash, w.clone()); Ok(w) }, } } else { - recursive_cata_dispatch::<_, _, _, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(node, passed_in_val, start_f, fold_child_f, finalize_f, cache) + recursive_cata_dispatch::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(node, passed_in_val, start_f, fold_child_f, finalize_f, cache) } } #[inline(always)] -fn recursive_cata_dispatch( +fn recursive_cata_dispatch( node: &TrieNodeODRc, passed_in_val: Option<&V>, start_f: StartF, @@ -2465,10 +2465,10 @@ where FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, { match node.as_tagged() { - TaggedNodeRef::DenseByteNode(node) => { node.node_recursive_cata::<_, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(passed_in_val, start_f, fold_child_f, finalize_f, cache) } - TaggedNodeRef::LineListNode(node) => { node.node_recursive_cata::<_, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(passed_in_val, start_f, fold_child_f, finalize_f, cache) } - TaggedNodeRef::CellByteNode(node) => { node.node_recursive_cata::<_, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(passed_in_val, start_f, fold_child_f, finalize_f, cache) } - TaggedNodeRef::TinyRefNode(node) => { node.node_recursive_cata::<_, _, _, _, _, _, COMPUTE_PATH, COMPUTE_MASK>(passed_in_val, start_f, fold_child_f, finalize_f, cache) } + TaggedNodeRef::DenseByteNode(node) => { node.node_recursive_cata::<_, _, _, _, _, _, COMPUTE_PATH>(passed_in_val, start_f, fold_child_f, finalize_f, cache) } + TaggedNodeRef::LineListNode(node) => { node.node_recursive_cata::<_, _, _, _, _, _, COMPUTE_PATH>(passed_in_val, start_f, fold_child_f, finalize_f, cache) } + TaggedNodeRef::CellByteNode(node) => { node.node_recursive_cata::<_, _, _, _, _, _, COMPUTE_PATH>(passed_in_val, start_f, fold_child_f, finalize_f, cache) } + TaggedNodeRef::TinyRefNode(node) => { node.node_recursive_cata::<_, _, _, _, _, _, COMPUTE_PATH>(passed_in_val, start_f, fold_child_f, finalize_f, cache) } TaggedNodeRef::EmptyNode => { finalize_f(&ByteMask::EMPTY, passed_in_val, None, &[]) } } } From d4d3fbae1e3a0c092b82e0bc3fda09661b018295 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Sat, 29 Aug 2026 06:00:30 -0600 Subject: [PATCH 24/50] Increasing val_count benchmark sizes --- benches/sparse_keys.rs | 4 ++-- benches/superdense_keys.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/benches/sparse_keys.rs b/benches/sparse_keys.rs index 9593c0cc..444374ab 100644 --- a/benches/sparse_keys.rs +++ b/benches/sparse_keys.rs @@ -134,7 +134,7 @@ fn sparse_descend_until_max_bytes(bencher: Bencher, n: u64) { }); } -#[divan::bench(args = [125, 250, 500, 1000, 2000, 4000])] +#[divan::bench(args = [125, 250, 500, 1000, 2000, 4000, 20_000, 100_000])] fn sparse_val_count_bench(bencher: Bencher, n: u64) { let mut r = StdRng::seed_from_u64(1); @@ -154,7 +154,7 @@ fn sparse_val_count_bench(bencher: Bencher, n: u64) { assert_eq!(sink, n as usize); } -#[divan::bench(args = [125, 250, 500, 1000, 2000, 4000])] +#[divan::bench(args = [125, 250, 500, 1000, 2000, 4000, 20_000, 100_000])] fn sparse_goat_val_count_bench(bencher: Bencher, n: u64) { let mut r = StdRng::seed_from_u64(1); diff --git a/benches/superdense_keys.rs b/benches/superdense_keys.rs index 5dce63b5..61d8cdca 100644 --- a/benches/superdense_keys.rs +++ b/benches/superdense_keys.rs @@ -311,7 +311,7 @@ fn from_prefix_key(k: Vec) -> u64 { u64::from_le_bytes(buf) & (!0u64 >> shift) } -#[divan::bench(sample_size = 1, args = [100, 200, 400, 800, 1600, 3200, 20_000])] +#[divan::bench(sample_size = 1, args = [100, 200, 400, 800, 1600, 3200, 20_000, 100_000])] fn superdense_val_count_bench(bencher: Bencher, n: u64) { let mut map: PathMap = PathMap::new(); @@ -325,7 +325,7 @@ fn superdense_val_count_bench(bencher: Bencher, n: u64) { assert_eq!(sink, n as usize); } -#[divan::bench(sample_size = 1, args = [100, 200, 400, 800, 1600, 3200, 20_000])] +#[divan::bench(sample_size = 1, args = [100, 200, 400, 800, 1600, 3200, 20_000, 100_000])] fn superdense_goat_val_count_bench(bencher: Bencher, n: u64) { let mut map: PathMap = PathMap::new(); @@ -341,7 +341,7 @@ fn superdense_goat_val_count_bench(bencher: Bencher, n: u64) { #[cfg(feature="arena_compact")] -#[divan::bench(sample_size = 1, args = [100, 200, 400, 800, 1600, 3200, 20_000])] +#[divan::bench(sample_size = 1, args = [100, 200, 400, 800, 1600, 3200, 20_000, 100_000])] fn superdense_val_count_bench_act(bencher: Bencher, n: u64) { use pathmap::{ arena_compact::ArenaCompactTree, From 939a817c4f05aedd41196aa74c5a9e0b411a63ac Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Sun, 30 Aug 2026 21:22:11 -0600 Subject: [PATCH 25/50] Adding iterative zipper-backed implementation of Summarization trait --- src/morphisms.rs | 324 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 321 insertions(+), 3 deletions(-) diff --git a/src/morphisms.rs b/src/morphisms.rs index d6c478c7..5f218b02 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -265,9 +265,9 @@ pub trait Catamorphism { /// Provides faster catamorphism methods for types backed by an in-memory trie, such as [`PathMap`] /// and some zipper implementations pub trait Summarization { - /// GOAT recursive cached cata. If this dev branch is successful this should replace the caching cata flavors in the public API. + /// GOAT cached cata. If this dev branch is successful this should replace the caching cata flavors in the public API. /// - /// JUMPING catamorphism implemented with recursion, for performance + /// JUMPING catamorphism implemented as an iterative zipper traversal. /// /// Each invocation of `summarize_f` may represent a whole non-branching /// run of path bytes, supplied as `prefix`, rather than just one path byte. @@ -528,6 +528,28 @@ impl Catamorph } } +//GOAT temporary impl using zipers +// impl<'a, Z, V: Clone + Send + Sync + Unpin, A: Allocator> Summarization for Z where Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer + ZipperInfallibleSubtries { +// fn recursive_cata(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result +// where +// V: Clone + Send + Sync, +// W: Clone, +// NewAccF: Copy + Fn(&ByteMask) -> Result, +// FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, +// SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, +// { +// let trie_ref = self.get_trie_ref(); +// let zipper = trie_ref.fork_read_zipper(); +// summarize_cached_body::<_, V, Acc, _, _, _, _, _, COMPUTE_PATH>( +// zipper, +// new_acc_f, +// fold_child_f, +// summarize_f, +// ) +// } +// } + +//GOAT, Recursive impl impl<'a, Z, V: Clone + Send + Sync, A: Allocator> Summarization for Z where Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer + ZipperInfallibleSubtries { fn recursive_cata(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where @@ -912,6 +934,207 @@ impl CacheStrategy for DoCache { fn clone(w: &W) -> W { w.clone() } } +/// Stack frame used in iterative (zipper-based) implementation of Summarizatino trait +struct SummarizeStackFrame { + child_idx: u16, + child_cnt: u16, + child_addr: Option, + accumulator: Acc, +} + +impl SummarizeStackFrame { + #[inline] + fn new(child_cnt: usize, accumulator: Acc) -> Self { + Self { + child_idx: 0, + child_cnt: child_cnt as u16, + child_addr: None, + accumulator, + } + } +} + +/// Ascend from a leaf or completed fork, summarizing each value and non-branching path run on the +/// way to the parent fork. This is the three-closure counterpart to [`ascend_to_fork`]. +#[inline(always)] +fn summarize_ascend_to_fork<'a, Z, V: 'a, Acc, W, E, NewAccF, FoldChildF, SummarizeF, const COMPUTE_PATH: bool>( + zipper: &mut Z, + mut accumulator: Option, + new_acc_f: NewAccF, + fold_child_f: FoldChildF, + summarize_f: SummarizeF, +) -> Result +where + Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperAbsolutePath + ZipperPathBuffer, + NewAccF: Copy + Fn(&ByteMask) -> Result, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), E>, + SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, +{ + let witness = zipper.witness(); + let mut child_mask = ByteMask::from(zipper.child_mask()); + + loop { + let old_path_len = zipper.origin_path().len(); + let old_value = zipper.get_val_with_witness(&witness); + let ascended = zipper.ascend_until(); + debug_assert!(ascended); + + let origin_path = unsafe { zipper.origin_path_assert_len(old_path_len) }; + let jump_len = if zipper.child_count() != 1 || zipper.is_val() { + old_path_len - (zipper.origin_path().len() + 1) + } else { + old_path_len - zipper.origin_path().len() + }; + let prefix = if COMPUTE_PATH { + &origin_path[origin_path.len() - jump_len..] + } else { + &[] + }; + + let w = summarize_f(&child_mask, old_value, accumulator, prefix)?; + + if zipper.child_count() != 1 || zipper.at_root() { + return Ok(w) + } + + // SAFETY: The path buffer still contains the path we just ascended through. + let byte = *unsafe { zipper.origin_path_assert_len(old_path_len - jump_len) } + .last() + .unwrap(); + child_mask = ByteMask::from(byte); + let mut next_accumulator = new_acc_f(&child_mask)?; + fold_child_f(&child_mask, w, &mut next_accumulator)?; + accumulator = Some(next_accumulator); + } +} + +/// Iterative cached traversal behind [`Summarization::recursive_cata`]. +/// +/// This follows [`into_cata_cached_body`] closely, but completes a logical node with the three +/// summarization closures instead of collecting a mutable child slice for one algebra closure. +fn summarize_cached_body<'a, Z, V: 'a, Acc, W, E, NewAccF, FoldChildF, SummarizeF, const COMPUTE_PATH: bool>( + mut zipper: Z, + new_acc_f: NewAccF, + fold_child_f: FoldChildF, + summarize_f: SummarizeF, +) -> Result +where + W: Clone, + Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer, + NewAccF: Copy + Fn(&ByteMask) -> Result, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), E>, + SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, +{ + zipper.reset(); + zipper.prepare_buffers(); + + let root_child_cnt = zipper.child_count(); + if root_child_cnt == 0 { + return summarize_f(&ByteMask::EMPTY, zipper.val(), None, &[]) + } + + let passthrough_root = root_child_cnt == 1 && !zipper.is_val(); + let mut stack = Vec::>::with_capacity(12); + if passthrough_root { + // A unary root without a value passes its child's W through unchanged, so it has no Acc. + zipper.descend_indexed_byte(0); + while zipper.child_count() < 2 { + if !zipper.descend_until() { + return summarize_ascend_to_fork::( + &mut zipper, + None, + new_acc_f, + fold_child_f, + summarize_f, + ) + } + } + let accumulator = new_acc_f(&ByteMask::from(zipper.child_mask()))?; + stack.push(SummarizeStackFrame::new(zipper.child_count(), accumulator)); + } else { + let accumulator = new_acc_f(&ByteMask::from(zipper.child_mask()))?; + stack.push(SummarizeStackFrame::new(zipper.child_count(), accumulator)); + } + + let mut cache = HashMap::::new(); + 'outer: loop { + let frame_mut = stack.last_mut() + .expect("summarization stack is emptied before we returned to root"); + + if frame_mut.child_idx < frame_mut.child_cnt { + zipper.descend_indexed_byte(frame_mut.child_idx as usize); + frame_mut.child_idx += 1; + frame_mut.child_addr = zipper.shared_node_id(); + + if let Some(cached) = DoCache::get(&cache, frame_mut.child_addr) { + zipper.ascend_byte(); + let child_mask = ByteMask::from(zipper.child_mask()); + fold_child_f(&child_mask, cached, &mut frame_mut.accumulator)?; + continue 'outer; + } + + let mut is_leaf = false; + while zipper.child_count() < 2 { + if !zipper.descend_until() { + is_leaf = true; + break; + } + } + + if is_leaf { + let cur_w = summarize_ascend_to_fork::( + &mut zipper, + None, + new_acc_f, + fold_child_f, + summarize_f, + )?; + DoCache::insert(&mut cache, frame_mut.child_addr, &cur_w); + let child_mask = ByteMask::from(zipper.child_mask()); + fold_child_f(&child_mask, cur_w, &mut frame_mut.accumulator)?; + continue 'outer; + } + + let accumulator = new_acc_f(&ByteMask::from(zipper.child_mask()))?; + stack.push(SummarizeStackFrame::new(zipper.child_count(), accumulator)); + continue 'outer; + } + + let frame = stack.pop() + .expect("we just checked that the summarization stack is not empty"); + + if stack.is_empty() { + return if passthrough_root { + summarize_ascend_to_fork::( + &mut zipper, + Some(frame.accumulator), + new_acc_f, + fold_child_f, + summarize_f, + ) + } else { + debug_assert!(zipper.at_root(), "must be at root when summarization is done"); + let child_mask = ByteMask::from(zipper.child_mask()); + summarize_f(&child_mask, zipper.val(), Some(frame.accumulator), &[]) + }; + } + + let cur_w = summarize_ascend_to_fork::( + &mut zipper, + Some(frame.accumulator), + new_acc_f, + fold_child_f, + summarize_f, + )?; + + let frame_mut = stack.last_mut() + .expect("when we're not at root, expect a parent summarization stack frame"); + DoCache::insert(&mut cache, frame_mut.child_addr, &cur_w); + let child_mask = ByteMask::from(zipper.child_mask()); + fold_child_f(&child_mask, cur_w, &mut frame_mut.accumulator)?; + } +} + /// Internal implementation behind all cached catas /// /// AlgF args: (child_mask, children, value, sub_path, debug_path, zipper) @@ -2466,6 +2689,101 @@ mod tests { assert_eq!(summarized.unwrap(), cached); assert_eq!(summarization_calls.load(Relaxed), cached_calls.load(Relaxed)); + + let iterative_calls = AtomicU64::new(0); + let iterative_map = make_map(); + let iterative = iterative_map.read_zipper().recursive_cata_stepping::>>, Rc>, Infallible, _, _, _>( + |_| Ok(Vec::new()), + |_mask, child, children| { children.push(child); Ok(()) }, + |_mask, value, children| { + iterative_calls.fetch_add(1, Relaxed); + Ok(Rc::new(Node::new(value, children))) + }, + ); + + assert_eq!(iterative.unwrap(), cached); + assert_eq!(iterative_calls.load(Relaxed), cached_calls.load(Relaxed)); + } + + #[test] + fn recursive_cata_on_zipper_uses_its_focus_as_root() { + let map: PathMap<()> = [ + (b"a".as_slice(), ()), + (b"ab".as_slice(), ()), + (b"ac".as_slice(), ()), + (b"z".as_slice(), ()), + ] + .into_iter() + .collect(); + let mut zipper = map.read_zipper(); + zipper.descend_to(b"a"); + + let count = zipper.recursive_cata::( + |_| Ok(0), + |_mask, child, total| { *total += child; Ok(()) }, + |_mask, value, children, _prefix| { + Ok(value.is_some() as usize + children.unwrap_or(0)) + }, + ); + + assert_eq!(count.unwrap(), 3); + assert_eq!(zipper.path(), b"a"); + } + + #[test] + fn iterative_summarization_folds_each_child_immediately() { + use std::cell::RefCell; + + let map: PathMap = [ + (b"a".as_slice(), 1), + (b"b".as_slice(), 2), + ] + .into_iter() + .collect(); + let events = RefCell::new(Vec::new()); + + let result = map.read_zipper().recursive_cata::, usize, Infallible, _, _, _, false>( + |_mask| { + events.borrow_mut().push("new"); + Ok(Vec::new()) + }, + |_mask, child, accumulator| { + events.borrow_mut().push(if child == 1 { "fold 1" } else { "fold 2" }); + accumulator.push(child); + Ok(()) + }, + |_mask, value, accumulator, _prefix| { + match value { + Some(1) => events.borrow_mut().push("summarize 1"), + Some(2) => events.borrow_mut().push("summarize 2"), + _ => events.borrow_mut().push("summarize root"), + } + Ok(value.copied().unwrap_or_else(|| accumulator.unwrap().into_iter().sum())) + }, + ); + + assert_eq!(result.unwrap(), 3); + assert_eq!( + events.into_inner(), + ["new", "summarize 1", "fold 1", "summarize 2", "fold 2", "summarize root"], + ); + } + + #[test] + fn iterative_summarization_does_not_accumulate_at_passthrough_root() { + let map: PathMap<()> = [(b"abc".as_slice(), ())].into_iter().collect(); + + let result = map.read_zipper().recursive_cata::<(), Vec, Infallible, _, _, _, true>( + |_| panic!("a unary valueless root must not create an accumulator"), + |_mask, _child, _accumulator| panic!("a unary valueless root must not fold a child"), + |_mask, value, accumulator, prefix| { + assert!(value.is_some()); + assert!(accumulator.is_none()); + Ok(prefix.to_vec()) + }, + ); + + assert_eq!(result.unwrap(), b"abc"); } /// Finds the path_depth at which the recursive cata hits a stack overflow @@ -2473,7 +2791,7 @@ mod tests { /// Empirically seems to be somewhere between 8 and 10 KBytes. But more branching, and thus fewer /// bytes-per-node, will mean it will fail on shorter paths. #[test] - fn recursive_cata_stack_overflow_smoke() { + fn recursive_cata_deep_path_smoke() { const PATH_LEN: usize = 8_000; let mut map = PathMap::<()>::new(); From c34f5098e9456a72530bdd4401244687493959ae Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Sun, 30 Aug 2026 23:10:47 -0600 Subject: [PATCH 26/50] Adding adaptor for single-function algebra on top of summarization trait --- src/morphisms.rs | 121 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/src/morphisms.rs b/src/morphisms.rs index 5f218b02..8262227a 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -339,6 +339,99 @@ pub trait Summarization { } } +/// Shared child-result storage used to adapt [`Summarization`] to the single-function-algebra cata API. +struct CataChildren { + children: Vec, + #[cfg(debug_assertions)] + allocations: Vec<(usize, usize)>, + #[cfg(debug_assertions)] + next_allocation: usize, +} + +struct CataChildrenAcc { + start: usize, + #[cfg(debug_assertions)] + allocation: usize, +} + +impl CataChildren { + #[inline] + fn new() -> Self { + Self { + children: Vec::new(), + #[cfg(debug_assertions)] + allocations: Vec::new(), + #[cfg(debug_assertions)] + next_allocation: 0, + } + } + + #[inline] + fn new_acc(&mut self, capacity: usize) -> CataChildrenAcc { + let start = self.children.len(); + self.children.reserve(capacity); + #[cfg(debug_assertions)] + let allocation = { + let allocation = self.next_allocation; + self.next_allocation += 1; + self.allocations.push((start, allocation)); + allocation + }; + CataChildrenAcc { + start, + #[cfg(debug_assertions)] + allocation, + } + } + + #[inline(always)] + fn push(&mut self, _acc: &CataChildrenAcc, child: W) { + #[cfg(debug_assertions)] + debug_assert_eq!(self.allocations.last(), Some(&(_acc.start, _acc.allocation))); + self.children.push(child); + } + + #[inline] + fn summarize(&mut self, acc: CataChildrenAcc, child_count: usize, summarize_f: impl FnOnce(&mut [W]) -> R) -> R { + #[cfg(debug_assertions)] + debug_assert_eq!(self.allocations.pop(), Some((acc.start, acc.allocation))); + debug_assert!(acc.start <= self.children.len()); + debug_assert_eq!(self.children.len() - acc.start, child_count); + + let result = summarize_f(&mut self.children[acc.start..]); + self.children.truncate(acc.start); + result + } +} + +//GOAT Implementation of adapted single-function-algebra cata +fn into_cata_jumping_cached_from_summarization(source: &S, alg_f: AlgF) -> Result +where + V: Clone + Send + Sync, + A: Allocator, + S: Summarization, + W: Clone, + AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> Result, +{ + let children = std::cell::RefCell::new(CataChildren::::new()); + let children = &children; + let alg_f = &alg_f; + + source.recursive_cata::( + move |mask| Ok(children.borrow_mut().new_acc(mask.count_bits())), + move |_mask, child, acc| { + children.borrow_mut().push(acc, child); + Ok(()) + }, + move |mask, value, acc, prefix| match acc { + Some(acc) => children.borrow_mut().summarize(acc, mask.count_bits(), |children| { + alg_f(mask, children, value, prefix) + }), + None => alg_f(mask, &mut [], value, prefix), + }, + ) +} + //TODO GOAT!!: It would be nice to get rid of this Default bound on all morphism Ws. In this case, the plan // for doing that would be to create a new type called a TakableSlice. It would be able to deref // into a regular mutable slice of `T` so it would work just like an ordinary slice. Additionally @@ -2573,8 +2666,21 @@ mod tests { }, |_mask, _val, state| Ok(state.map_or_else(Vec::new, |(_, path)| path)), ); + let adapted = map.into_cata_jumping_cached(|mask, children: &mut [Vec], _val, prefix| { + let mut longest = mask.iter().zip(children.iter_mut()) + .max_by_key(|(_byte, rest)| rest.len()) + .map_or_else(Vec::new, |(byte, rest)| { + let mut path = std::mem::take(rest); + path.insert(0, byte); + path + }); + let mut path = prefix.to_vec(); + path.append(&mut longest); + path + }); assert_eq!(std::str::from_utf8(&cached).unwrap(), "rubicundus"); + assert_eq!(adapted, cached); assert_eq!(jumping.unwrap(), cached); assert_eq!(stepping.unwrap(), cached); } @@ -2677,6 +2783,21 @@ mod tests { Rc::new(Node { value: value.cloned(), children: children.to_vec() }) }); + let jumping_calls = AtomicU64::new(0); + let jumping: Rc> = make_map().read_zipper().into_cata_jumping_cached(|_mask, children, value, _prefix| { + jumping_calls.fetch_add(1, Relaxed); + Rc::new(Node { value: value.cloned(), children: children.to_vec() }) + }); + + let adapted_calls = AtomicU64::new(0); + let adapted: Rc> = make_map().into_cata_jumping_cached(|_mask, children, value, _prefix| { + adapted_calls.fetch_add(1, Relaxed); + Rc::new(Node { value: value.cloned(), children: children.to_vec() }) + }); + + assert_eq!(adapted, jumping); + assert_eq!(adapted_calls.load(Relaxed), jumping_calls.load(Relaxed)); + let summarization_calls = AtomicU64::new(0); let summarized = make_map().recursive_cata_stepping::>>, Rc>, Infallible, _, _, _>( |_| Ok(Vec::new()), From ec0cf15b26f6a8bafa8755f31b1f2354e0ffe545 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Mon, 31 Aug 2026 03:34:32 -0600 Subject: [PATCH 27/50] Splitting Catamorphism trait in two, where the `_cached` flavors land in the old `Summarization` trait which is now renamed `CatamorphismCached`, while the original Catamorphism trait is now called `CatamorphismSideEffecting` --- benches/catamorphism.rs | 8 +- benches/product_zipper.rs | 8 +- benches/sla.rs | 3 +- src/arena_compact.rs | 14 +- src/experimental/serialization.rs | 4 +- src/experimental/tree_serialization.rs | 6 +- src/morphisms.rs | 256 +++++++++++++------------ src/product_zipper.rs | 2 +- src/random.rs | 7 +- src/trie_map.rs | 4 +- src/trie_node.rs | 6 +- src/utils/debug/morphism_debug.rs | 2 +- 12 files changed, 162 insertions(+), 158 deletions(-) diff --git a/benches/catamorphism.rs b/benches/catamorphism.rs index 9fd48a15..3aa93f44 100644 --- a/benches/catamorphism.rs +++ b/benches/catamorphism.rs @@ -1,6 +1,6 @@ use divan::{Divan, Bencher, black_box}; use core::convert::Infallible; -use pathmap::morphisms::{Catamorphism, Summarization}; +use pathmap::morphisms::CatamorphismCached; use pathmap::utils::ByteMask; use pathmap::utils::ints::gen_int_range; use pathmap::PathMap; @@ -42,7 +42,7 @@ fn recursive_cata_jumping_val_count(bencher: Bencher) { let mut sink = 0usize; bencher.bench_local(|| { let rz = map.read_zipper(); - *black_box(&mut sink) = rz.recursive_cata::<_, _, Infallible, _, _, _, false>( + *black_box(&mut sink) = rz.factored_cata_jumping::<_, _, Infallible, _, _, _, false>( |_| Ok(0usize), |_mask, w: usize, total| { *total += w; Ok(()) }, |_mask, v, total, _| Ok((v.is_some() as usize) + total.unwrap_or(0)), @@ -58,7 +58,7 @@ fn recursive_cata_binary_tree_leaf_count(bencher: Bencher) { bencher.bench_local(|| { let rz = map.read_zipper(); *black_box(&mut sink) = rz - .recursive_cata::<_, _, Infallible, _, _, _, false>( + .factored_cata_jumping::<_, _, Infallible, _, _, _, false>( |_| Ok(0usize), |_mask, child_count: usize, total| { *total += child_count; @@ -94,7 +94,7 @@ fn recursive_cata_jumping_total_len(bencher: Bencher) { let mut sink = (0usize, 0usize); bencher.bench_local(|| { let rz = map.read_zipper(); - *black_box(&mut sink) = rz.recursive_cata::<_, _, Infallible, _, _, _, true>( + *black_box(&mut sink) = rz.factored_cata_jumping::<_, _, Infallible, _, _, _, true>( |_| Ok((0usize, 0usize)), |_mask: &ByteMask, w: (usize, usize), acc: &mut (usize, usize)| { acc.0 += w.0; diff --git a/benches/product_zipper.rs b/benches/product_zipper.rs index fcac057f..c128e66d 100644 --- a/benches/product_zipper.rs +++ b/benches/product_zipper.rs @@ -89,7 +89,7 @@ fn val_count_cata(_bm: &ByteMask, vals: &mut[usize], _val: Option<&V>, _path: fn introspecting_pathmap_pathmap(bencher: Bencher) { use pathmap::{ PathMap, - morphisms::Catamorphism, + morphisms::CatamorphismSideEffecting, zipper::{ProductZipper}, }; let mut sink = 0; @@ -106,7 +106,7 @@ fn introspecting_pathmap_pathmap(bencher: Bencher) { fn generic_pathmap_pathmap(bencher: Bencher) { use pathmap::{ PathMap, - morphisms::Catamorphism, + morphisms::CatamorphismSideEffecting, zipper::{ProductZipperG}, }; let mut sink = 0; @@ -125,7 +125,7 @@ fn generic_act_act(bencher: Bencher) { use pathmap::{ PathMap, arena_compact::{ArenaCompactTree}, - morphisms::Catamorphism, + morphisms::CatamorphismSideEffecting, zipper::{ProductZipperG}, }; let mut sink = 0; @@ -146,7 +146,7 @@ fn generic_pathmap_act(bencher: Bencher) { use pathmap::{ PathMap, arena_compact::{ArenaCompactTree}, - morphisms::Catamorphism, + morphisms::CatamorphismSideEffecting, zipper::{ProductZipperG}, }; let mut sink = 0; diff --git a/benches/sla.rs b/benches/sla.rs index f4e967c4..02b37635 100644 --- a/benches/sla.rs +++ b/benches/sla.rs @@ -1,4 +1,3 @@ -#![allow(unused)] use std::hash::{Hasher, Hash}; use std::time::Instant; use num_traits::Zero; @@ -7,7 +6,7 @@ use rand::prelude::StdRng; use rand::{Rng, SeedableRng}; use rand_distr::Distribution; use pathmap::*; -use pathmap::morphisms::Catamorphism; +use pathmap::morphisms::CatamorphismCached; use pathmap::ring::{AlgebraicResult, Lattice}; use pathmap::utils::{BitMask, ByteMask, ints::{indices_to_weave, indices_to_bob}}; use pathmap::zipper::{ReadZipperUntracked, WriteZipperUntracked, Zipper, ZipperMoving, ZipperValues, ZipperWriting}; diff --git a/src/arena_compact.rs b/src/arena_compact.rs index 086ea772..e725af1d 100644 --- a/src/arena_compact.rs +++ b/src/arena_compact.rs @@ -84,7 +84,7 @@ use fast_slice_utils::starts_with; use crate::alloc::{GlobalAlloc, global_alloc}; use crate::{ PathMap, - morphisms::Catamorphism, + morphisms::{CatamorphismSideEffecting, CatamorphismCached}, utils::{BitMask, ByteMask, find_prefix_overlap}, zipper::{ Zipper, ZipperValues, ZipperForking, ZipperAbsolutePath, ZipperIteration, @@ -895,7 +895,7 @@ impl ArenaCompactTree> { pub fn from_zipper(zipper: Z, map: M) -> Self where V: Clone + Send + Sync + Unpin, - Z: Catamorphism, + Z: CatamorphismSideEffecting, M: Fn(&V) -> u64, { build_arena_tree(zipper, map) @@ -1065,7 +1065,7 @@ impl ArenaCompactTree { ) -> Result where V: Clone + Send + Sync + Unpin, - Z: Catamorphism, + Z: CatamorphismSideEffecting, F: Fn(&V) -> u64, P: AsRef { @@ -1145,7 +1145,7 @@ impl NodeBranch { fn build_arena_tree(zipper: Z, map_val: F) -> ArenaCompactTree> where V: Clone + Send + Sync + Unpin, - Z: Catamorphism, + Z: CatamorphismSideEffecting, F: Fn(&V) -> u64, { let mut arena = ArenaCompactTree::new(); @@ -1356,7 +1356,7 @@ struct CachedFrame { /// The traversal itself is the jumping catamorphism, unrolled (see /// `morphisms::into_cata_cached_body`, which this follows closely). It is /// spelled out here rather than delegating to -/// [`Catamorphism::into_cata_jumping_cached`] for two reasons: +/// [`CatamorphismCached::into_cata_jumping_cached`] for two reasons: /// - the cached cata only consults its cache one byte below a fork, whereas we /// also consult it at the fork we land on after jumping over a chain of /// bytes. That is where a subtrie grafted under a multi-byte path shows up, @@ -1575,7 +1575,7 @@ fn dump_arena_tree( ) -> Result, std::io::Error> where V: Clone + Send + Sync + Unpin, - Z: Catamorphism, + Z: CatamorphismSideEffecting, F: Fn(&V) -> u64, P: AsRef, { @@ -3264,7 +3264,7 @@ where mod tests { use super::{ArenaCompactTree, ACTZipper}; use crate::{ - morphisms::Catamorphism, PathMap, zipper::{zipper_iteration_tests, zipper_moving_tests, ZipperIteration, ZipperMoving, ZipperValues} + morphisms::CatamorphismSideEffecting, PathMap, zipper::{zipper_iteration_tests, zipper_moving_tests, ZipperIteration, ZipperMoving, ZipperValues} }; zipper_moving_tests::zipper_moving_tests!(arena_compact_zipper, diff --git a/src/experimental/serialization.rs b/src/experimental/serialization.rs index 423fe939..6b7ddfa1 100644 --- a/src/experimental/serialization.rs +++ b/src/experimental/serialization.rs @@ -2,7 +2,7 @@ use std::{any::type_name, hash::Hasher, io::{BufRead, BufReader, BufWriter, Read, Seek, Write}, path::PathBuf}; -use crate::{morphisms::Catamorphism, PathMap, zipper::{ZipperMoving, ZipperWriting}}; +use crate::{morphisms::CatamorphismSideEffecting, PathMap, zipper::{ZipperMoving, ZipperWriting}}; use crate::TrieValue; extern crate alloc; use alloc::collections::BTreeMap; @@ -83,7 +83,7 @@ pub const META_DATA_FILENAME : &'static str = "meta.json"; /// Filename of the zero compressesed data file at the `out_dir_path` formal parameter in [`write_trie`] pub const ZERO_COMPRESSED_HEX_DATA_FILENAME : &'static str = "zero_compressed_hex.data"; -pub fn write_trie ,V: TrieValue>( +pub fn write_trie ,V: TrieValue>( memo : impl AsRef, cata : C, serialize_value : impl for<'read, 'encode> Fn(&'read V, &'encode mut Vec)->ValueSlice<'read, 'encode>, diff --git a/src/experimental/tree_serialization.rs b/src/experimental/tree_serialization.rs index 658c0b6c..3622f7c8 100644 --- a/src/experimental/tree_serialization.rs +++ b/src/experimental/tree_serialization.rs @@ -3,12 +3,12 @@ use std::io::Write; use std::ptr::slice_from_raw_parts; use crate::alloc::Allocator; use crate::{zipper, TrieValue}; -use crate::morphisms::{Catamorphism, new_map_from_ana_jumping}; +use crate::morphisms::{CatamorphismSideEffecting, new_map_from_ana_jumping}; use crate::utils::{BitMask, ByteMask}; use crate::write_zipper::ZipperWriting; /// WIP -pub fn serialize_fork, F: FnMut(usize, &[u8], &V) -> ()>(rz: RZ, target: &mut Vec, _fv: F) -> std::io::Result { +pub fn serialize_fork, F: FnMut(usize, &[u8], &V) -> ()>(rz: RZ, target: &mut Vec, _fv: F) -> std::io::Result { unsafe { thread_local! { static WRITTEN: UnsafeCell = UnsafeCell::new(0) @@ -57,7 +57,7 @@ pub fn deserialize_fork { +/// Provides methods to perform side-effecting catamorphisms appropriate for serialization and full-path operations +pub trait CatamorphismSideEffecting { /// Applies a "stepping" catamorphism to the trie descending from the zipper's root, running the `alg_f` at every /// step (at every byte) /// @@ -115,7 +115,7 @@ pub trait Catamorphism { /// Allows the closure to return an error, stopping traversal immediately /// - /// See [Catamorphism::into_cata_side_effect] + /// See [CatamorphismSideEffecting::into_cata_side_effect] fn into_cata_side_effect_fallible(self, alg_f: AlgF) -> Result where AlgF: FnMut(&ByteMask, &mut [W], Option<&V>, &[u8]) -> Result; @@ -130,7 +130,7 @@ pub trait Catamorphism { /// - `jumped_byte_cnt`: The number of bytes before the `alg_f` will be called again. The "jumped" substring /// is equal to `path[path.len()-jumped_byte_cnt..]` /// - /// See [into_cata_side_effect](Catamorphism::into_cata_side_effect) for explanation of other arguments and + /// See [into_cata_side_effect](CatamorphismSideEffecting::into_cata_side_effect) for explanation of other arguments and /// behavior fn into_cata_jumping_side_effect(self, mut alg_f: AlgF) -> W where @@ -144,9 +144,13 @@ pub trait Catamorphism { /// Allows the closure to return an error, stopping traversal immediately /// - /// See [Catamorphism::into_cata_jumping_side_effect] + /// See [CatamorphismSideEffecting::into_cata_jumping_side_effect] fn into_cata_jumping_side_effect_fallible(self, alg_f: AlgF) -> Result where AlgF: FnMut(&ByteMask, &mut [W], usize, Option<&V>, &[u8]) -> Result; +} + +/// Provides methods for cached (pure) catamorphisms appropriate for trie summarization operations +pub trait CatamorphismCached { /// Applies a **cached**, **stepping**, catamorphism to the trie descending from the zipper's /// root, running the `alg_f` at every step (at every byte) @@ -182,7 +186,8 @@ pub trait Catamorphism { /// Allows the closure to return an error, stopping traversal immediately /// - /// See [Catamorphism::into_cata_cached] + /// See [CatamorphismCached::into_cata_cached] +//GOAT, write this in terms of `into_cata_jumping_cached_fallible` fn into_cata_cached_fallible(self, alg_f: AlgF) -> Result where W: Clone, @@ -213,7 +218,7 @@ pub trait Catamorphism { /// 3. `alg_f(ByteMask::EMPTY, &[], Some(&()), b"ort")` /// 4. `alg_f(ByteMask::from_iter([b'b', b'e', b'f']), &[..], None, b"com")` /// - /// See [into_cata_cached](Catamorphism::into_cata_cached) for explanation of other arguments and behavior + /// See [into_cata_cached](CatamorphismCached::into_cata_cached) for explanation of other arguments and behavior fn into_cata_jumping_cached(self, alg_f: AlgF) -> W where W: Clone, @@ -227,11 +232,31 @@ pub trait Catamorphism { /// Allows the closure to return an error, stopping traversal immediately /// - /// See [Catamorphism::into_cata_jumping_cached] + /// See [CatamorphismCached::into_cata_jumping_cached] fn into_cata_jumping_cached_fallible(self, alg_f: AlgF) -> Result where W: Clone, - AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> Result; + AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> Result, + Self: Sized //GOAT, remove uneeded when we take &self + { + let children = std::cell::RefCell::new(CataChildren::::new()); + let children = &children; + let alg_f = &alg_f; + + self.factored_cata_jumping::( + move |mask| Ok(children.borrow_mut().new_acc(mask.count_bits())), + move |_mask, child, acc| { + children.borrow_mut().push(acc, child); + Ok(()) + }, + move |mask, value, acc, prefix| match acc { + Some(acc) => children.borrow_mut().summarize(acc, mask.count_bits(), |children| { + alg_f(mask, children, value, prefix) + }), + None => alg_f(mask, &mut [], value, prefix), + }, + ) + } /// Hash the logical `PathMap` and all its values fn hash(self) -> u128 @@ -260,19 +285,11 @@ pub trait Catamorphism { hasher.finish_u128() }) } -} -/// Provides faster catamorphism methods for types backed by an in-memory trie, such as [`PathMap`] -/// and some zipper implementations -pub trait Summarization { - /// GOAT cached cata. If this dev branch is successful this should replace the caching cata flavors in the public API. + /// A low-level catamorphism API that decomposes the algebra into multiple functions and allows + /// redundant path computation to be disabled /// - /// JUMPING catamorphism implemented as an iterative zipper traversal. - /// - /// Each invocation of `summarize_f` may represent a whole non-branching - /// run of path bytes, supplied as `prefix`, rather than just one path byte. - /// - /// Closures: + /// ## Closures: /// /// `NewAccF`: Creates an accumulator for a logical trie node with more than one child branch. /// `fn(child_mask: &ByteMask) -> Result` @@ -298,7 +315,7 @@ pub trait Summarization { /// `COMPUTE_PATH=false` avoids materializing path runs and passes an empty `prefix`. This should /// only be used when the algebra is agnostic to the path bytes, and only sensitive to values and/or /// path endpoints. - fn recursive_cata(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result + fn factored_cata_jumping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where V: Clone + Send + Sync, W: Clone, @@ -312,8 +329,8 @@ pub trait Summarization { /// Use this when the cata must evaluate once per path byte, including bytes in non-branching /// runs. Unlike the jumping version, `summarize_f` has no `prefix`: it is called once for every /// path byte. The callback roles and child-mask ordering are otherwise the same as for - /// [`Summarization::recursive_cata`]. - fn recursive_cata_stepping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result + /// [`CatamorphismCached::factored_cata_jumping`]. + fn factored_cata(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where V: Clone + Send + Sync, W: Clone, @@ -322,7 +339,7 @@ pub trait Summarization { SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option) -> Result, Self: Sized { - self.recursive_cata::<_, _, _, _, _, _, true>( + self.factored_cata_jumping::<_, _, _, _, _, _, true>( new_acc_f, fold_child_f, |mask, val, acc, prefix| { @@ -339,7 +356,7 @@ pub trait Summarization { } } -/// Shared child-result storage used to adapt [`Summarization`] to the single-function-algebra cata API. +/// Shared child-result storage used to adapt [`CatamorphismCached`] to the single-function-algebra cata API. struct CataChildren { children: Vec, #[cfg(debug_assertions)] @@ -404,33 +421,33 @@ impl CataChildren { } } -//GOAT Implementation of adapted single-function-algebra cata -fn into_cata_jumping_cached_from_summarization(source: &S, alg_f: AlgF) -> Result -where - V: Clone + Send + Sync, - A: Allocator, - S: Summarization, - W: Clone, - AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> Result, -{ - let children = std::cell::RefCell::new(CataChildren::::new()); - let children = &children; - let alg_f = &alg_f; - - source.recursive_cata::( - move |mask| Ok(children.borrow_mut().new_acc(mask.count_bits())), - move |_mask, child, acc| { - children.borrow_mut().push(acc, child); - Ok(()) - }, - move |mask, value, acc, prefix| match acc { - Some(acc) => children.borrow_mut().summarize(acc, mask.count_bits(), |children| { - alg_f(mask, children, value, prefix) - }), - None => alg_f(mask, &mut [], value, prefix), - }, - ) -} +// //GOAT Implementation of adapted single-function-algebra cata +// fn into_cata_jumping_cached_from_summarization(source: &S, alg_f: AlgF) -> Result +// where +// V: Clone + Send + Sync, +// A: Allocator, +// S: Summarization, +// W: Clone, +// AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> Result, +// { +// let children = std::cell::RefCell::new(CataChildren::::new()); +// let children = &children; +// let alg_f = &alg_f; + +// source.recursive_cata::( +// move |mask| Ok(children.borrow_mut().new_acc(mask.count_bits())), +// move |_mask, child, acc| { +// children.borrow_mut().push(acc, child); +// Ok(()) +// }, +// move |mask, value, acc, prefix| match acc { +// Some(acc) => children.borrow_mut().summarize(acc, mask.count_bits(), |children| { +// alg_f(mask, children, value, prefix) +// }), +// None => alg_f(mask, &mut [], value, prefix), +// }, +// ) +// } //TODO GOAT!!: It would be nice to get rid of this Default bound on all morphism Ws. In this case, the plan // for doing that would be to create a new type called a TakableSlice. It would be able to deref @@ -554,7 +571,7 @@ impl SplitCataJumping { } } -impl<'a, Z, V: 'a> Catamorphism for Z where Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer { +impl<'a, Z, V: 'a> CatamorphismSideEffecting for Z where Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer { fn into_cata_side_effect_fallible(self, mut alg_f: AlgF) -> Result where AlgF: FnMut(&ByteMask, &mut [W], Option<&V>, &[u8]) -> Result, { @@ -570,27 +587,9 @@ impl<'a, Z, V: 'a> Catamorphism for Z where Z: Zipper + ZipperReadOnlyConditi alg_f(mask, children, jump_len, val, path) }) } - fn into_cata_cached_fallible(self, alg_f: AlgF) -> Result - where - W: Clone, - AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> Result - { - into_cata_cached_body::(self, |mask, children, val, sub_path, _debug_path, _z| { - debug_assert_eq!(sub_path.len(), 0); - alg_f(mask, children, val) - }) - } - fn into_cata_jumping_cached_fallible(self, alg_f: AlgF) -> Result - where - W: Clone, - AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> Result - { - into_cata_cached_body::(self, - |mask, children, val, sub_path, _debug_path, _z| alg_f(mask, children, val, sub_path)) - } } -impl Catamorphism for PathMap { +impl CatamorphismSideEffecting for PathMap { fn into_cata_side_effect_fallible(self, alg_f: AlgF) -> Result where AlgF: FnMut(&ByteMask, &mut [W], Option<&V>, &[u8]) -> Result { @@ -603,22 +602,6 @@ impl Catamorph let rz = self.into_read_zipper(&[]); rz.into_cata_jumping_side_effect_fallible(alg_f) } - fn into_cata_cached_fallible(self, alg_f: AlgF) -> Result - where - W: Clone, - AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> Result - { - let rz = self.into_read_zipper(&[]); - rz.into_cata_cached_fallible(alg_f) - } - fn into_cata_jumping_cached_fallible(self, alg_f: AlgF) -> Result - where - W: Clone, - AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> Result - { - let rz = self.into_read_zipper(&[]); - rz.into_cata_jumping_cached_fallible(alg_f) - } } //GOAT temporary impl using zipers @@ -643,8 +626,18 @@ impl Catamorph // } //GOAT, Recursive impl -impl<'a, Z, V: Clone + Send + Sync, A: Allocator> Summarization for Z where Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer + ZipperInfallibleSubtries { - fn recursive_cata(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result +impl<'a, Z, V: Clone + Send + Sync + 'a, A: Allocator> CatamorphismCached for Z where Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer + ZipperInfallibleSubtries { + fn into_cata_cached_fallible(self, alg_f: AlgF) -> Result + where + W: Clone, + AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> Result + { + into_cata_cached_body::(self, |mask, children, val, sub_path, _debug_path, _z| { + debug_assert_eq!(sub_path.len(), 0); + alg_f(mask, children, val) + }) + } + fn factored_cata_jumping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where V: Clone + Send + Sync, W: Clone, @@ -664,8 +657,19 @@ impl<'a, Z, V: Clone + Send + Sync, A: Allocator> Summarization for Z wher } } -impl Summarization for PathMap { - fn recursive_cata(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result +impl CatamorphismCached for PathMap { + fn into_cata_cached_fallible(self, alg_f: AlgF) -> Result + where + W: Clone, + AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> Result + { + let rz = self.read_zipper(); + into_cata_cached_body::<_, V, W, E, _, DoCache, false, false>(rz, |mask, children, val, sub_path, _debug_path, _z| { + debug_assert_eq!(sub_path.len(), 0); + alg_f(mask, children, val) + }) + } + fn factored_cata_jumping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where V: Clone + Send + Sync, W: Clone, @@ -1027,7 +1031,7 @@ impl CacheStrategy for DoCache { fn clone(w: &W) -> W { w.clone() } } -/// Stack frame used in iterative (zipper-based) implementation of Summarizatino trait +/// Stack frame used in iterative (zipper-based) implementation of Summarization trait struct SummarizeStackFrame { child_idx: u16, child_cnt: u16, @@ -1101,7 +1105,7 @@ where } } -/// Iterative cached traversal behind [`Summarization::recursive_cata`]. +/// Iterative cached traversal behind [`CatamorphismCached::factored_cata_jumping`]. /// /// This follows [`into_cata_cached_body`] closely, but completes a logical node with the three /// summarization closures instead of collecting a mutable child slice for one algebra closure. @@ -1784,7 +1788,7 @@ mod tests { fn check_side_effect_catas<'a, W, V, Z, AlgF, Assert>( zipper: Z, mut f_side: AlgF, mut assert: Assert) where - Z: Clone + Catamorphism, W: Clone, + Z: Clone + CatamorphismSideEffecting, W: Clone, AlgF: FnMut(&ByteMask, &mut [W], usize, Option<&V>, &[u8]) -> W, Assert: FnMut(W, &str), { @@ -1796,10 +1800,10 @@ mod tests { assert(output, "into_cata_jumping_side_effect"); } - fn check_pure_catas<'a, W, V, Z, AlgFP, Assert>( + fn check_pure_catas<'a, W, V: Clone + Send + Sync, Z, AlgFP, Assert>( zipper: Z, f_pure: AlgFP, mut assert: Assert) where - Z: Clone + Catamorphism, W: Clone, + Z: Clone + CatamorphismCached, W: Clone, AlgFP: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> W, Assert: FnMut(W, &str), { @@ -1811,10 +1815,10 @@ mod tests { assert(output, "into_cata_jumping_cached"); } - fn check_all_catas<'a, W, V, Z, AlgF, Assert>( + fn check_all_catas<'a, W, V: Clone + Send + Sync, Z, AlgF, Assert>( zipper: Z, alg_f: AlgF, mut assert: Assert) where - Z: Clone + Catamorphism, W: Clone, + Z: Clone + CatamorphismSideEffecting + CatamorphismCached, W: Clone, AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> W, Assert: FnMut(W, &str), { @@ -2481,7 +2485,7 @@ mod tests { for (keys, expected_sum) in tests { let map: PathMap<()> = keys.into_iter().map(|v| (v, ())).collect(); - let sum = map.recursive_cata::<_, _, Infallible, _, _, _, true>( + let sum = map.factored_cata_jumping::<_, _, Infallible, _, _, _, true>( |_| Ok(SumAcc::default()), |mask: &ByteMask, w: (bool, u32), acc: &mut SumAcc| { if let Some(byte) = mask.indexed_bit::(acc.idx) { @@ -2532,7 +2536,7 @@ mod tests { for (keys, expected_sum) in tests { let map: PathMap<()> = keys.into_iter().map(|v| (v, ())).collect(); - let sum = map.recursive_cata_stepping::( + let sum = map.factored_cata::( |_| Ok(SumAcc::default()), |mask: &ByteMask, w: (bool, u32), acc: &mut SumAcc| { if let Some(byte) = mask.iter().nth(acc.idx) { @@ -2577,7 +2581,7 @@ mod tests { } }); - let jumping = map.recursive_cata::( + let jumping = map.factored_cata_jumping::( |_| Ok(0), |_mask, child, total| { *total += child; Ok(()) }, |_mask, val, children, _prefix| match children { @@ -2588,7 +2592,7 @@ mod tests { }, }, ); - let stepping = map.recursive_cata_stepping::( + let stepping = map.factored_cata::( |_| Ok(0), |_mask, child, total| { *total += child; Ok(()) }, |_mask, val, children| match children { @@ -2629,7 +2633,7 @@ mod tests { }); // This uses allocation for readability; performance-sensitive code can fold a longest path directly. - let jumping = map.recursive_cata::>, Vec, Infallible, _, _, _, true>( + let jumping = map.factored_cata_jumping::>, Vec, Infallible, _, _, _, true>( |_| Ok(Vec::new()), |_mask, child, children| { children.push(child); Ok(()) }, |mask, _val, children, prefix| { @@ -2650,7 +2654,7 @@ mod tests { Ok(path) }, ); - let stepping = map.recursive_cata_stepping::<(usize, Vec), Vec, Infallible, _, _, _>( + let stepping = map.factored_cata::<(usize, Vec), Vec, Infallible, _, _, _>( |_| Ok((0, Vec::new())), |mask, child, state| { let mut path = Vec::with_capacity(child.len() + 1); @@ -2720,7 +2724,7 @@ mod tests { } }); - let jumping = map.recursive_cata::, Vec, Infallible, _, _, _, false>( + let jumping = map.factored_cata_jumping::, Vec, Infallible, _, _, _, false>( |_| Ok(Vec::new()), |_mask, child, values| { values.extend(child); Ok(()) }, |_mask, val, children, _prefix| match children { @@ -2728,7 +2732,7 @@ mod tests { Some(values) => Ok(val.map_or(values, |val| vec![*val])), }, ); - let stepping = map.recursive_cata_stepping::, Vec, Infallible, _, _, _>( + let stepping = map.factored_cata::, Vec, Infallible, _, _, _>( |_| Ok(Vec::new()), |_mask, child, values| { values.extend(child); Ok(()) }, |_mask, val, children| match children { @@ -2744,7 +2748,7 @@ mod tests { } /// Parallel port of `cata_test_cached`: the input deliberately contains - /// shared subtries, so this exercises Summarization's cached traversal. + /// shared subtries, so this exercises `CatamorphismCached`'s factored traversal. #[test] fn recursive_cata_cached_dag_matches_cached_cata() { fn make_map() -> PathMap { @@ -2799,7 +2803,7 @@ mod tests { assert_eq!(adapted_calls.load(Relaxed), jumping_calls.load(Relaxed)); let summarization_calls = AtomicU64::new(0); - let summarized = make_map().recursive_cata_stepping::>>, Rc>, Infallible, _, _, _>( + let summarized = make_map().factored_cata::>>, Rc>, Infallible, _, _, _>( |_| Ok(Vec::new()), |_mask, child, children| { children.push(child); Ok(()) }, |_mask, value, children| { @@ -2813,7 +2817,7 @@ mod tests { let iterative_calls = AtomicU64::new(0); let iterative_map = make_map(); - let iterative = iterative_map.read_zipper().recursive_cata_stepping::>>, Rc>, Infallible, _, _, _>( + let iterative = iterative_map.read_zipper().factored_cata::>>, Rc>, Infallible, _, _, _>( |_| Ok(Vec::new()), |_mask, child, children| { children.push(child); Ok(()) }, |_mask, value, children| { @@ -2839,7 +2843,7 @@ mod tests { let mut zipper = map.read_zipper(); zipper.descend_to(b"a"); - let count = zipper.recursive_cata::( + let count = zipper.factored_cata_jumping::( |_| Ok(0), |_mask, child, total| { *total += child; Ok(()) }, |_mask, value, children, _prefix| { @@ -2863,7 +2867,7 @@ mod tests { .collect(); let events = RefCell::new(Vec::new()); - let result = map.read_zipper().recursive_cata::, usize, Infallible, _, _, _, false>( + let result = map.read_zipper().factored_cata_jumping::, usize, Infallible, _, _, _, false>( |_mask| { events.borrow_mut().push("new"); Ok(Vec::new()) @@ -2894,7 +2898,7 @@ mod tests { fn iterative_summarization_does_not_accumulate_at_passthrough_root() { let map: PathMap<()> = [(b"abc".as_slice(), ())].into_iter().collect(); - let result = map.read_zipper().recursive_cata::<(), Vec, Infallible, _, _, _, true>( + let result = map.read_zipper().factored_cata_jumping::<(), Vec, Infallible, _, _, _, true>( |_| panic!("a unary valueless root must not create an accumulator"), |_mask, _child, _accumulator| panic!("a unary valueless root must not fold a child"), |_mask, value, accumulator, prefix| { @@ -2919,7 +2923,7 @@ mod tests { let path = vec![b'a'; PATH_LEN]; map.set_val_at(&path, ()); - let count = map.recursive_cata::<_, _, Infallible, _, _, _, false>( + let count = map.factored_cata_jumping::<_, _, Infallible, _, _, _, false>( |_| Ok(0usize), |_mask, w: usize, total| { *total += w; Ok(()) }, |_mask, v, total, _| Ok((v.is_some() as usize) + total.unwrap_or(0)), @@ -2931,21 +2935,21 @@ mod tests { fn recursive_cata_propagates_callback_errors() { let map: PathMap<()> = [(b"a".as_slice(), ()), (b"b".as_slice(), ())].into_iter().collect(); - let error = map.recursive_cata::<(), (), &'static str, _, _, _, false>( + let error = map.factored_cata_jumping::<(), (), &'static str, _, _, _, false>( |_| Err("start"), |_mask, _child, _acc| Ok(()), |_mask, _value, _acc, _prefix| Ok(()), ); assert_eq!(error, Err("start")); - let error = map.recursive_cata::<(), (), &'static str, _, _, _, false>( + let error = map.factored_cata_jumping::<(), (), &'static str, _, _, _, false>( |_| Ok(()), |_mask, _child, _acc| Err("fold"), |_mask, _value, _acc, _prefix| Ok(()), ); assert_eq!(error, Err("fold")); - let error = map.recursive_cata::<(), (), &'static str, _, _, _, false>( + let error = map.factored_cata_jumping::<(), (), &'static str, _, _, _, false>( |_| Ok(()), |_mask, _child, _acc| Ok(()), |_mask, _value, _acc, _prefix| Err("summarize"), @@ -2966,7 +2970,7 @@ mod tests { .collect(); let summarize_calls = AtomicUsize::new(0); - let error = map.recursive_cata::<(), (), &'static str, _, _, _, false>( + let error = map.factored_cata_jumping::<(), (), &'static str, _, _, _, false>( |_| Ok(()), |_mask, _child, _acc| Ok(()), |_mask, _value, _acc, _prefix| { diff --git a/src/product_zipper.rs b/src/product_zipper.rs index dc350bd5..29c997c7 100644 --- a/src/product_zipper.rs +++ b/src/product_zipper.rs @@ -905,7 +905,7 @@ mod tests { use crate::utils::ByteMask; use crate::zipper::*; use crate::PathMap; - use crate::morphisms::Catamorphism; + use crate::morphisms::CatamorphismSideEffecting; macro_rules! impl_product_zipper_tests { ($mod:ident, $ProductZipper:ident, $convert:ident) => { diff --git a/src/random.rs b/src/random.rs index d01ee19b..280cee19 100644 --- a/src/random.rs +++ b/src/random.rs @@ -140,14 +140,15 @@ pub struct FairTriePath { } impl Distribution<(Vec, Option)> for FairTriePath { fn sample(&self, rng: &mut R) -> (Vec, Option) { - use crate::morphisms::Catamorphism; + //TODO: There has to be a more efficient way to implement this than making two passes through the whole trie + use crate::morphisms::{CatamorphismSideEffecting, CatamorphismCached}; // it's much cheaper to draw many samples at once, but the current Distribution API is broken - let size = Catamorphism::into_cata_cached(self.source.clone(), |_: &ByteMask, ws: &mut [usize], _mv: Option<&T>| { + let size = self.source.clone().into_cata_cached(|_: &ByteMask, ws: &mut [usize], _mv: Option<&T>| { ws.iter().sum::() + 1 }); let target = rng.random_range(0..size); let mut i = 0; - Catamorphism::into_cata_side_effect_fallible(self.source.clone(), |_: &ByteMask, _, mv: Option<&T>, path: &[u8]| { + self.source.clone().into_cata_side_effect_fallible(|_: &ByteMask, _, mv: Option<&T>, path: &[u8]| { if i == target { Err((path.to_vec(), mv.cloned())) } else { i += 1; Ok(()) } }).unwrap_err() } diff --git a/src/trie_map.rs b/src/trie_map.rs index b26b2627..b24cce7d 100644 --- a/src/trie_map.rs +++ b/src/trie_map.rs @@ -1,7 +1,7 @@ use core::cell::UnsafeCell; use core::convert::Infallible; use crate::alloc::{Allocator, GlobalAlloc, global_alloc}; -use crate::morphisms::{new_map_from_ana_in, Summarization, TrieBuilder}; +use crate::morphisms::{new_map_from_ana_in, CatamorphismCached, TrieBuilder}; use crate::trie_node::*; use crate::zipper::*; use crate::merkleization::{MerkleizeResult, merkleize_impl}; @@ -510,7 +510,7 @@ impl PathMap { pub fn goat_val_count(&self) -> usize { match self.root() { Some(_root) => { - match self.recursive_cata::<_, _, Infallible, _, _, _, false>( + match self.factored_cata_jumping::<_, _, Infallible, _, _, _, false>( |_| Ok(0usize), |_mask, w: usize, total| { *total += w; Ok(()) }, |_mask, v, total, _| Ok((v.is_some() as usize) + total.unwrap_or(0)), diff --git a/src/trie_node.rs b/src/trie_node.rs index 849044fa..bfbb55e4 100644 --- a/src/trie_node.rs +++ b/src/trie_node.rs @@ -2411,7 +2411,7 @@ pub(crate) fn val_count_below_node(node: & } } -/// Internal implementation of recursive_cata +/// Internal implementation of `CatamorphismCached::factored_cata_jumping` pub(crate) fn recursive_cata_cached( node: &TrieNodeODRc, passed_in_val: Option<&V>, @@ -2428,8 +2428,8 @@ where FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, { - // NOTE: A caller-supplied value can make this trie-node boundary fall inside one Summarization callback, - // so its W is not reusable by node ID alone. + // NOTE: A caller-supplied value can make this trie-node boundary fall within a factored_cata callback, + // therefore the W is not cacheable based on node ID alone. // FUTURE: When the node contract associates values at the root of nodes with the node and not with the parent, // then the `passed_in_val.is_none()` check can be removed if passed_in_val.is_none() && !node.is_empty() && node.refcount() > 1 { diff --git a/src/utils/debug/morphism_debug.rs b/src/utils/debug/morphism_debug.rs index 550f2acd..c4cda1ae 100644 --- a/src/utils/debug/morphism_debug.rs +++ b/src/utils/debug/morphism_debug.rs @@ -11,7 +11,7 @@ use crate::morphisms::{into_cata_cached_body, DoCache}; /// This trait provides debug-only catamorphism methods that may expose additional /// information useful for debugging and development. pub trait CatamorphismDebug { - /// A version of [`into_cata_jumping_cached`](crate::morphisms::Catamorphism::into_cata_jumping_cached) where + /// A version of [`into_cata_jumping_cached`](crate::morphisms::CatamorphismCached::into_cata_jumping_cached) where /// the full path is available to the closure; **For debugging purposes only** /// /// Using data from the full path for your algorithm **will** lead to incorrect behavior. From d9ff9d4bca8e1f52ba4b26391953bb71dfc1371a Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Mon, 31 Aug 2026 04:50:45 -0600 Subject: [PATCH 28/50] Implementing default into_cata_cached_fallible in terms of into_cata_jumping_cached_fallible, so all calls can easily funnel back through one core implementation --- src/arena_compact.rs | 2 +- src/morphisms.rs | 41 +++++++++++++++-------------------------- 2 files changed, 16 insertions(+), 27 deletions(-) diff --git a/src/arena_compact.rs b/src/arena_compact.rs index e725af1d..e8c9d21c 100644 --- a/src/arena_compact.rs +++ b/src/arena_compact.rs @@ -84,7 +84,7 @@ use fast_slice_utils::starts_with; use crate::alloc::{GlobalAlloc, global_alloc}; use crate::{ PathMap, - morphisms::{CatamorphismSideEffecting, CatamorphismCached}, + morphisms::CatamorphismSideEffecting, utils::{BitMask, ByteMask, find_prefix_overlap}, zipper::{ Zipper, ZipperValues, ZipperForking, ZipperAbsolutePath, ZipperIteration, diff --git a/src/morphisms.rs b/src/morphisms.rs index 5139d80d..ce4c9622 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -187,11 +187,21 @@ pub trait CatamorphismCached /// Allows the closure to return an error, stopping traversal immediately /// /// See [CatamorphismCached::into_cata_cached] -//GOAT, write this in terms of `into_cata_jumping_cached_fallible` fn into_cata_cached_fallible(self, alg_f: AlgF) -> Result where W: Clone, - AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> Result; + AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> Result, + Self: Sized, + { + self.into_cata_jumping_cached_fallible(|mask, children, val, sub_path| { + let mut w = alg_f(mask, children, val)?; + for &byte in sub_path.iter().rev() { + let child_mask = ByteMask::from(byte); + w = alg_f(&child_mask, core::slice::from_mut(&mut w), None)?; + } + Ok(w) + }) + } /// Applies a "jumping" catamorphism to the trie /// @@ -604,9 +614,9 @@ impl Catamorph } } -//GOAT temporary impl using zipers -// impl<'a, Z, V: Clone + Send + Sync + Unpin, A: Allocator> Summarization for Z where Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer + ZipperInfallibleSubtries { -// fn recursive_cata(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result +// //GOAT temporary impl using zipers +// impl<'a, Z, V: Clone + Send + Sync + Unpin, A: Allocator> CatamorphismCached for Z where Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer + ZipperInfallibleSubtries { +// fn factored_cata_jumping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result // where // V: Clone + Send + Sync, // W: Clone, @@ -627,16 +637,6 @@ impl Catamorph //GOAT, Recursive impl impl<'a, Z, V: Clone + Send + Sync + 'a, A: Allocator> CatamorphismCached for Z where Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer + ZipperInfallibleSubtries { - fn into_cata_cached_fallible(self, alg_f: AlgF) -> Result - where - W: Clone, - AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> Result - { - into_cata_cached_body::(self, |mask, children, val, sub_path, _debug_path, _z| { - debug_assert_eq!(sub_path.len(), 0); - alg_f(mask, children, val) - }) - } fn factored_cata_jumping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where V: Clone + Send + Sync, @@ -658,17 +658,6 @@ impl<'a, Z, V: Clone + Send + Sync + 'a, A: Allocator> CatamorphismCached } impl CatamorphismCached for PathMap { - fn into_cata_cached_fallible(self, alg_f: AlgF) -> Result - where - W: Clone, - AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> Result - { - let rz = self.read_zipper(); - into_cata_cached_body::<_, V, W, E, _, DoCache, false, false>(rz, |mask, children, val, sub_path, _debug_path, _z| { - debug_assert_eq!(sub_path.len(), 0); - alg_f(mask, children, val) - }) - } fn factored_cata_jumping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where V: Clone + Send + Sync, From a4bc2cd2e2173cef23fbefa4624fc170982605a9 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Mon, 31 Aug 2026 05:09:10 -0600 Subject: [PATCH 29/50] Halving the tax imposed by the compatibility shim between the single-function caching cata and the factored caching cata by skipping RefCell runtime borrow checks --- src/morphisms.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/morphisms.rs b/src/morphisms.rs index ce4c9622..554df40a 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -254,15 +254,22 @@ pub trait CatamorphismCached let alg_f = &alg_f; self.factored_cata_jumping::( - move |mask| Ok(children.borrow_mut().new_acc(mask.count_bits())), + move |mask| { + debug_assert!(children.try_borrow_mut().is_ok()); + Ok(unsafe{&mut *children.as_ptr()}.new_acc(mask.count_bits()) ) + }, move |_mask, child, acc| { - children.borrow_mut().push(acc, child); + debug_assert!(children.try_borrow_mut().is_ok()); + unsafe{&mut *children.as_ptr()}.push(acc, child); Ok(()) }, move |mask, value, acc, prefix| match acc { - Some(acc) => children.borrow_mut().summarize(acc, mask.count_bits(), |children| { - alg_f(mask, children, value, prefix) - }), + Some(acc) => { + debug_assert!(children.try_borrow_mut().is_ok()); + unsafe{&mut *children.as_ptr()}.summarize(acc, mask.count_bits(), |children| { + alg_f(mask, children, value, prefix) + }) + }, None => alg_f(mask, &mut [], value, prefix), }, ) From 8d826ba1425ea7b9047fa43baac49026fd7a0cad Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Mon, 31 Aug 2026 07:44:19 -0600 Subject: [PATCH 30/50] Creating trait with selectable "engines" to dictate whether to use recursion or a zipper Wrapping CatamorphismCached trait so each type gets a default engine implementation Adding test macro so we can be sure all cached catas work equivalently --- benches/catamorphism.rs | 15 +- src/arena_compact.rs | 25 +- src/morphisms.rs | 829 +++++++++++++++++++++++++++++++++++++--- src/zipper.rs | 25 ++ 4 files changed, 826 insertions(+), 68 deletions(-) diff --git a/benches/catamorphism.rs b/benches/catamorphism.rs index 3aa93f44..05be1fa7 100644 --- a/benches/catamorphism.rs +++ b/benches/catamorphism.rs @@ -1,6 +1,7 @@ use divan::{Divan, Bencher, black_box}; use core::convert::Infallible; -use pathmap::morphisms::CatamorphismCached; +use pathmap::alloc::GlobalAlloc; +use pathmap::morphisms::{CatamorphismCachedWithEngine, RecursiveCata}; use pathmap::utils::ByteMask; use pathmap::utils::ints::gen_int_range; use pathmap::PathMap; @@ -42,7 +43,7 @@ fn recursive_cata_jumping_val_count(bencher: Bencher) { let mut sink = 0usize; bencher.bench_local(|| { let rz = map.read_zipper(); - *black_box(&mut sink) = rz.factored_cata_jumping::<_, _, Infallible, _, _, _, false>( + *black_box(&mut sink) = CatamorphismCachedWithEngine::<(), GlobalAlloc, RecursiveCata>::factored_cata_jumping::<_, _, Infallible, _, _, _, false>(&rz, |_| Ok(0usize), |_mask, w: usize, total| { *total += w; Ok(()) }, |_mask, v, total, _| Ok((v.is_some() as usize) + total.unwrap_or(0)), @@ -57,8 +58,8 @@ fn recursive_cata_binary_tree_leaf_count(bencher: Bencher) { let mut sink = 0usize; bencher.bench_local(|| { let rz = map.read_zipper(); - *black_box(&mut sink) = rz - .factored_cata_jumping::<_, _, Infallible, _, _, _, false>( + *black_box(&mut sink) = CatamorphismCachedWithEngine::<(), GlobalAlloc, RecursiveCata> + ::factored_cata_jumping::<_, _, Infallible, _, _, _, false>(&rz, |_| Ok(0usize), |_mask, child_count: usize, total| { *total += child_count; @@ -77,7 +78,7 @@ fn cached_jumping_cata_val_count(bencher: Bencher) { let mut sink = 0usize; bencher.bench_local(|| { let rz = map.read_zipper(); - *black_box(&mut sink) = rz.into_cata_jumping_cached(|_mask: &ByteMask, children: &mut [usize], val, _sub_path| { + *black_box(&mut sink) = CatamorphismCachedWithEngine::<(), GlobalAlloc, RecursiveCata>::into_cata_jumping_cached(rz, |_mask: &ByteMask, children: &mut [usize], val, _sub_path| { let mut sum: usize = children.iter().sum(); if val.is_some() { sum += 1; @@ -94,7 +95,7 @@ fn recursive_cata_jumping_total_len(bencher: Bencher) { let mut sink = (0usize, 0usize); bencher.bench_local(|| { let rz = map.read_zipper(); - *black_box(&mut sink) = rz.factored_cata_jumping::<_, _, Infallible, _, _, _, true>( + *black_box(&mut sink) = CatamorphismCachedWithEngine::<(), GlobalAlloc, RecursiveCata>::factored_cata_jumping::<_, _, Infallible, _, _, _, true>(&rz, |_| Ok((0usize, 0usize)), |_mask: &ByteMask, w: (usize, usize), acc: &mut (usize, usize)| { acc.0 += w.0; @@ -117,7 +118,7 @@ fn cached_jumping_cata_total_len(bencher: Bencher) { let mut sink = (0usize, 0usize); bencher.bench_local(|| { let rz = map.read_zipper(); - *black_box(&mut sink) = rz.into_cata_jumping_cached(|mask: &ByteMask, children: &mut [(usize, usize)], val, sub_path| { + *black_box(&mut sink) = CatamorphismCachedWithEngine::<(), GlobalAlloc, RecursiveCata>::into_cata_jumping_cached(rz, |mask: &ByteMask, children: &mut [(usize, usize)], val, sub_path| { let mut count = 0usize; let mut total_len = 0usize; let prefix_len = sub_path.len(); diff --git a/src/arena_compact.rs b/src/arena_compact.rs index e8c9d21c..45a8b563 100644 --- a/src/arena_compact.rs +++ b/src/arena_compact.rs @@ -2419,6 +2419,18 @@ where Storage: AsRef<[u8]> } } +crate::morphisms::impl_catamorphism_cached!( + crate::morphisms::IterativeCata; + impl<'tree, Storage> for ACTZipper<'tree, Storage, ()> as (), GlobalAlloc + where [Storage: AsRef<[u8]>]; +); + +crate::morphisms::impl_catamorphism_cached!( + crate::morphisms::IterativeCata; + impl<'tree, Storage> for ACTZipper<'tree, Storage, u64> as u64, GlobalAlloc + where [Storage: AsRef<[u8]>]; +); + impl<'tree, Storage, Value> Zipper for ACTZipper<'tree, Storage, Value> where Storage: AsRef<[u8]> { @@ -3264,7 +3276,8 @@ where mod tests { use super::{ArenaCompactTree, ACTZipper}; use crate::{ - morphisms::CatamorphismSideEffecting, PathMap, zipper::{zipper_iteration_tests, zipper_moving_tests, ZipperIteration, ZipperMoving, ZipperValues} + morphisms::CatamorphismSideEffecting, + PathMap, zipper::{zipper_iteration_tests, zipper_moving_tests, ZipperIteration, ZipperMoving, ZipperValues} }; zipper_moving_tests::zipper_moving_tests!(arena_compact_zipper, @@ -3323,6 +3336,16 @@ mod tests { } } + crate::morphisms::cached_catamorphism_tests::cached_catamorphism_tests!( + act_zipper, + |keys: &[&[u8]]| { + let map = keys.iter().enumerate().map(|(idx, path)| (*path, idx as u64)).collect::>(); + ArenaCompactTree::from_zipper(map.read_zipper(), |&value| value) + }, + |tree: &mut ArenaCompactTree>| tree.read_zipper_u64(), + crate::morphisms::IterativeCata + ); + /// Build `map` both ways and check the results describe the same trie. /// /// When there is nothing to re-use the two builders must agree byte for diff --git a/src/morphisms.rs b/src/morphisms.rs index 554df40a..a5ba3a0d 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -149,8 +149,19 @@ pub trait CatamorphismSideEffecting { where AlgF: FnMut(&ByteMask, &mut [W], usize, Option<&V>, &[u8]) -> Result; } -/// Provides methods for cached (pure) catamorphisms appropriate for trie summarization operations -pub trait CatamorphismCached { +/// Selects the recursive implementation of [`CatamorphismCached`]. This tends to be about 10x +/// faster on average, vs. [`IterativeCata`] +pub struct RecursiveCata; + +/// Selects the iterative zipper-based implementation of [`CatamorphismCached`]. Use this to avoid +/// stack overflows caused by [`RecursiveCata`] +pub struct IterativeCata; + +/// Provides cached (pure) catamorphisms using an explicitly selected implementation engine +/// +/// Set `Engine` to either [`RecursiveCata`] or [`IterativeCata`] depending on the implementation +/// desired / available +pub trait CatamorphismCachedWithEngine { /// Applies a **cached**, **stepping**, catamorphism to the trie descending from the zipper's /// root, running the `alg_f` at every step (at every byte) @@ -373,7 +384,234 @@ pub trait CatamorphismCached } } -/// Shared child-result storage used to adapt [`CatamorphismCached`] to the single-function-algebra cata API. +/// The ordinary cached-cata interface. Method calls through this trait use the default strategy for +/// the zipper type. +/// +/// [`Self::Engine`] selects that strategy. [`CatamorphismCachedWithEngine`] is the lower-level +/// interface for explicitly selecting a different catamorphism engine. +pub trait CatamorphismCached { + /// The catamorphism engine selected as the default for this zipper type. + type Engine; + + /// Calls [`CatamorphismCachedWithEngine::into_cata_cached`] with this zipper type's selected + /// [`Self::Engine`]. + fn into_cata_cached(self, alg_f: AlgF) -> W + where + W: Clone, + AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> W, + Self: Sized, + ; + + /// Calls [`CatamorphismCachedWithEngine::into_cata_cached_fallible`] with this zipper type's + /// selected [`Self::Engine`]. + fn into_cata_cached_fallible(self, alg_f: AlgF) -> Result + where + W: Clone, + AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> Result, + Self: Sized, + ; + + /// Calls [`CatamorphismCachedWithEngine::into_cata_jumping_cached`] with this zipper type's + /// selected [`Self::Engine`]. + fn into_cata_jumping_cached(self, alg_f: AlgF) -> W + where + W: Clone, + AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> W, + Self: Sized, + ; + + /// Calls [`CatamorphismCachedWithEngine::into_cata_jumping_cached_fallible`] with this zipper + /// type's selected [`Self::Engine`]. + fn into_cata_jumping_cached_fallible(self, alg_f: AlgF) -> Result + where + W: Clone, + AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> Result, + Self: Sized, + ; + + /// Calls [`CatamorphismCachedWithEngine::hash`] with this zipper type's selected + /// [`Self::Engine`]. + fn hash(self) -> u128 + where + Self: Sized, + V: std::hash::Hash, + ; + + /// Calls [`CatamorphismCachedWithEngine::hash_with`] with this zipper type's selected + /// [`Self::Engine`]. + fn hash_with(self, val_hash: F) -> u128 + where + Self: Sized, + F: Fn(&V) -> u128, + ; + + /// Calls [`CatamorphismCachedWithEngine::factored_cata_jumping`] with this zipper type's + /// selected [`Self::Engine`]. + fn factored_cata_jumping( + &self, + new_acc_f: NewAccF, + fold_child_f: FoldChildF, + summarize_f: SummarizeF, + ) -> Result + where + W: Clone, + NewAccF: Copy + Fn(&ByteMask) -> Result, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, + SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, + Self: Sized, + ; + + /// Calls [`CatamorphismCachedWithEngine::factored_cata`] with this zipper type's selected + /// [`Self::Engine`]. + fn factored_cata( + &self, + new_acc_f: NewAccF, + fold_child_f: FoldChildF, + summarize_f: SummarizeF, + ) -> Result + where + W: Clone, + NewAccF: Copy + Fn(&ByteMask) -> Result, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, + SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option) -> Result, + Self: Sized, + ; +} + +/// Implements [`CatamorphismCached`] for one zipper type by selecting an implementation engine. +/// +/// This is deliberately invoked for each supported zipper type: a blanket implementation could +/// not give `ACTZipper` its iterative default while giving native zippers their recursive default. +/// +/// ```ignore +/// crate::morphisms::impl_catamorphism_cached!( +/// IterativeCata; +/// impl<'tree, Storage> for ACTZipper<'tree, Storage, u64> as u64, GlobalAlloc +/// where [Storage: AsRef<[u8]>]; +/// ); +/// ``` +macro_rules! impl_catamorphism_cached { + ( + $engine:ty; + impl<$($generic:tt),*> for $zipper:ty as $value:ty, $allocator:ty + where [$($where_clause:tt)*]; + ) => { + impl<$($generic),*> $crate::morphisms::CatamorphismCached<$value, $allocator> for $zipper + where + $($where_clause)* + { + type Engine = $engine; + + #[inline] + fn into_cata_cached(self, alg_f: AlgF) -> W + where + W: Clone, + AlgF: Fn(&$crate::utils::ByteMask, &mut [W], Option<&$value>) -> W, + Self: Sized, + { + $crate::morphisms::CatamorphismCachedWithEngine::<$value, $allocator, $engine> + ::into_cata_cached(self, alg_f) + } + + #[inline] + fn into_cata_cached_fallible(self, alg_f: AlgF) -> Result + where + W: Clone, + AlgF: Fn(&$crate::utils::ByteMask, &mut [W], Option<&$value>) -> Result, + Self: Sized, + { + $crate::morphisms::CatamorphismCachedWithEngine::<$value, $allocator, $engine> + ::into_cata_cached_fallible(self, alg_f) + } + + #[inline] + fn into_cata_jumping_cached(self, alg_f: AlgF) -> W + where + W: Clone, + AlgF: Fn(&$crate::utils::ByteMask, &mut [W], Option<&$value>, &[u8]) -> W, + Self: Sized, + { + $crate::morphisms::CatamorphismCachedWithEngine::<$value, $allocator, $engine> + ::into_cata_jumping_cached(self, alg_f) + } + + #[inline] + fn into_cata_jumping_cached_fallible(self, alg_f: AlgF) -> Result + where + W: Clone, + AlgF: Fn(&$crate::utils::ByteMask, &mut [W], Option<&$value>, &[u8]) -> Result, + Self: Sized, + { + $crate::morphisms::CatamorphismCachedWithEngine::<$value, $allocator, $engine> + ::into_cata_jumping_cached_fallible(self, alg_f) + } + + #[inline] + fn hash(self) -> u128 + where + Self: Sized, + $value: std::hash::Hash, + { + $crate::morphisms::CatamorphismCachedWithEngine::<$value, $allocator, $engine> + ::hash(self) + } + + #[inline] + fn hash_with(self, val_hash: F) -> u128 + where + Self: Sized, + F: Fn(&$value) -> u128, + { + $crate::morphisms::CatamorphismCachedWithEngine::<$value, $allocator, $engine> + ::hash_with(self, val_hash) + } + + #[inline] + fn factored_cata_jumping( + &self, + new_acc_f: NewAccF, + fold_child_f: FoldChildF, + summarize_f: SummarizeF, + ) -> Result + where + W: Clone, + NewAccF: Copy + Fn(&$crate::utils::ByteMask) -> Result, + FoldChildF: Copy + Fn(&$crate::utils::ByteMask, W, &mut Acc) -> Result<(), Err>, + SummarizeF: Copy + Fn(&$crate::utils::ByteMask, Option<&$value>, Option, &[u8]) -> Result, + Self: Sized, + { + $crate::morphisms::CatamorphismCachedWithEngine::<$value, $allocator, $engine> + ::factored_cata_jumping::( + self, + new_acc_f, + fold_child_f, + summarize_f, + ) + } + + #[inline] + fn factored_cata( + &self, + new_acc_f: NewAccF, + fold_child_f: FoldChildF, + summarize_f: SummarizeF, + ) -> Result + where + W: Clone, + NewAccF: Copy + Fn(&$crate::utils::ByteMask) -> Result, + FoldChildF: Copy + Fn(&$crate::utils::ByteMask, W, &mut Acc) -> Result<(), Err>, + SummarizeF: Copy + Fn(&$crate::utils::ByteMask, Option<&$value>, Option) -> Result, + Self: Sized, + { + $crate::morphisms::CatamorphismCachedWithEngine::<$value, $allocator, $engine> + ::factored_cata(self, new_acc_f, fold_child_f, summarize_f) + } + } + }; +} +pub(crate) use impl_catamorphism_cached; + +/// Shared child-result storage used to adapt [`CatamorphismCachedWithEngine`] to the single-function-algebra cata API. struct CataChildren { children: Vec, #[cfg(debug_assertions)] @@ -621,29 +859,7 @@ impl Catamorph } } -// //GOAT temporary impl using zipers -// impl<'a, Z, V: Clone + Send + Sync + Unpin, A: Allocator> CatamorphismCached for Z where Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer + ZipperInfallibleSubtries { -// fn factored_cata_jumping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result -// where -// V: Clone + Send + Sync, -// W: Clone, -// NewAccF: Copy + Fn(&ByteMask) -> Result, -// FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, -// SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, -// { -// let trie_ref = self.get_trie_ref(); -// let zipper = trie_ref.fork_read_zipper(); -// summarize_cached_body::<_, V, Acc, _, _, _, _, _, COMPUTE_PATH>( -// zipper, -// new_acc_f, -// fold_child_f, -// summarize_f, -// ) -// } -// } - -//GOAT, Recursive impl -impl<'a, Z, V: Clone + Send + Sync + 'a, A: Allocator> CatamorphismCached for Z where Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer + ZipperInfallibleSubtries { +impl<'a, Z, V: Clone + Send + Sync + 'a, A: Allocator> CatamorphismCachedWithEngine for Z where Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer + ZipperInfallibleSubtries { fn factored_cata_jumping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where V: Clone + Send + Sync, @@ -664,7 +880,25 @@ impl<'a, Z, V: Clone + Send + Sync + 'a, A: Allocator> CatamorphismCached } } -impl CatamorphismCached for PathMap { +impl<'a, Z, V: Clone + Send + Sync + 'a, A: Allocator> CatamorphismCachedWithEngine for Z where Z: Clone + Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer { + fn factored_cata_jumping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result + where + V: Clone + Send + Sync, + W: Clone, + NewAccF: Copy + Fn(&ByteMask) -> Result, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, + SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, + { + summarize_cached_body::<_, V, Acc, _, _, _, _, _, COMPUTE_PATH>( + self.clone(), + new_acc_f, + fold_child_f, + summarize_f, + ) + } +} + +impl CatamorphismCachedWithEngine for PathMap { fn factored_cata_jumping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where V: Clone + Send + Sync, @@ -684,6 +918,70 @@ impl CatamorphismCached for } } +impl CatamorphismCachedWithEngine for PathMap { + fn factored_cata_jumping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result + where + V: Clone + Send + Sync, + W: Clone, + NewAccF: Copy + Fn(&ByteMask) -> Result, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, + SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, + { + summarize_cached_body::<_, V, Acc, _, _, _, _, _, COMPUTE_PATH>( + self.read_zipper(), + new_acc_f, + fold_child_f, + summarize_f, + ) + } +} + +impl_catamorphism_cached!( + RecursiveCata; + impl for PathMap as V, A + where [V: Clone + Send + Sync + Unpin, A: Allocator]; +); + +impl_catamorphism_cached!( + RecursiveCata; + impl<'prefix, V, A, Z> for PrefixZipper<'prefix, Z> as V, A + where [ + V: Clone + Send + Sync, + A: Allocator, + PrefixZipper<'prefix, Z>: CatamorphismCachedWithEngine + ]; +); + +impl_catamorphism_cached!( + RecursiveCata; + impl for OneFactor as V, A + where [ + V: Clone + Send + Sync, + A: Allocator, + OneFactor: CatamorphismCachedWithEngine + ]; +); + +impl_catamorphism_cached!( + RecursiveCata; + impl for Box as V, A + where [ + V: Clone + Send + Sync, + A: Allocator, + Box: CatamorphismCachedWithEngine + ]; +); + +impl_catamorphism_cached!( + RecursiveCata; + impl<'zipper, V, A, Z> for &'zipper mut Z as V, A + where [ + V: Clone + Send + Sync, + A: Allocator, + &'zipper mut Z: CatamorphismCachedWithEngine + ]; +); + /// Helper function to summarize one path run (section of non-branching path bytes) // //NOTE: #[inline(always)] here leads to a huge bloating of the size of the a stack frame in the recursive cata @@ -1774,6 +2072,347 @@ impl TrieBuilder { // } // } +/// Shared cached-cata conformance tests for zipper implementations. +#[cfg(test)] +pub(crate) mod cached_catamorphism_tests { + use core::convert::Infallible; + + use crate::alloc::GlobalAlloc; + use crate::morphisms::{CatamorphismCached, CatamorphismCachedWithEngine}; + use crate::utils::BitMask; + + pub const CACHED_CATA_TEST_KEYS: &[&[u8]] = &[ + b"arrow", b"bow", b"cannon", b"roman", b"romane", b"romanus", b"romulus", + b"rubens", b"ruber", b"rubicon", b"rubicundus", b"rom'i", + ]; + pub const CACHED_CATA_FOLD_ORDER_KEYS: &[&[u8]] = &[b"a", b"b"]; + pub const CACHED_CATA_PASSTHROUGH_KEYS: &[&[u8]] = &[b"abc"]; + + // Keep the explicit-engine trait out of this module's scope so this exercises the same + // unqualified method call clients make after importing only `CatamorphismCached`. + pub(crate) mod default_facade_tests { + use crate::alloc::GlobalAlloc; + use crate::morphisms::CatamorphismCached; + + /// Exercises the ordinary facade rather than an explicitly selected engine. + pub fn leaf_count_stepping(zipper: Z) + where + Z: CatamorphismCached, + { + let count = zipper.into_cata_cached(|_mask, children: &mut [usize], value| { + if children.is_empty() { + assert!(value.is_some()); + 1 + } else { + children.iter().sum() + } + }); + assert_eq!(count, 11); + } + } + + pub fn factored_cata_propagates_callback_errors(zipper: Z) + where + Z: CatamorphismCachedWithEngine, + { + let error = CatamorphismCachedWithEngine:: + ::factored_cata_jumping::<(), (), &'static str, _, _, _, false>( + &zipper, + |_| Err("new"), + |_mask, _child, _acc| Ok(()), + |_mask, _value, _acc, _prefix| Ok(()), + ); + assert_eq!(error, Err("new")); + + let error = CatamorphismCachedWithEngine:: + ::factored_cata_jumping::<(), (), &'static str, _, _, _, false>( + &zipper, + |_| Ok(()), + |_mask, _child, _acc| Err("fold"), + |_mask, _value, _acc, _prefix| Ok(()), + ); + assert_eq!(error, Err("fold")); + + let error = CatamorphismCachedWithEngine:: + ::factored_cata_jumping::<(), (), &'static str, _, _, _, false>( + &zipper, + |_| Ok(()), + |_mask, _child, _acc| Ok(()), + |_mask, _value, _acc, _prefix| Err("summarize"), + ); + assert_eq!(error, Err("summarize")); + } + + pub fn leaf_count_stepping(zipper: Z) + where + Z: CatamorphismCachedWithEngine, + { + let count = CatamorphismCachedWithEngine::::into_cata_cached( + zipper, + |_mask, children: &mut [usize], value| { + if children.is_empty() { + assert!(value.is_some()); + 1 + } else { + children.iter().sum() + } + }, + ); + assert_eq!(count, 11); + } + + pub fn leaf_count_jumping(zipper: Z) + where + Z: CatamorphismCachedWithEngine, + { + let count = CatamorphismCachedWithEngine::::into_cata_jumping_cached( + zipper, + |_mask, children: &mut [usize], value, _prefix| { + if children.is_empty() { + assert!(value.is_some()); + 1 + } else { + children.iter().sum() + } + }, + ); + assert_eq!(count, 11); + } + + pub fn leaf_count_factored_jumping(zipper: Z) + where + Z: CatamorphismCachedWithEngine, + { + let count = CatamorphismCachedWithEngine:: + ::factored_cata_jumping::( + &zipper, + |_| Ok(0), + |_mask, child, total| { *total += child; Ok(()) }, + |_mask, value, total, _prefix| match total { + Some(total) => Ok(total), + None => { + assert!(value.is_some()); + Ok(1) + } + }, + ) + .unwrap(); + assert_eq!(count, 11); + } + + pub fn leaf_count_factored_stepping(zipper: Z) + where + Z: CatamorphismCachedWithEngine, + { + let count = CatamorphismCachedWithEngine:: + ::factored_cata::( + &zipper, + |_| Ok(0), + |_mask, child, total| { *total += child; Ok(()) }, + |_mask, value, total| match total { + Some(total) => Ok(total), + None => { + assert!(value.is_some()); + Ok(1) + } + }, + ) + .unwrap(); + assert_eq!(count, 11); + } + + pub fn longest_path_jumping(zipper: Z) + where + Z: CatamorphismCachedWithEngine, + { + let longest = CatamorphismCachedWithEngine::::into_cata_jumping_cached( + zipper, + |mask, children: &mut [Vec], _value, prefix| { + let mut longest = mask.iter().zip(children.iter_mut()) + .max_by_key(|(_byte, rest)| rest.len()) + .map_or_else(Vec::new, |(byte, rest)| { + let mut path = core::mem::take(rest); + path.insert(0, byte); + path + }); + let mut path = prefix.to_vec(); + path.append(&mut longest); + path + }, + ); + assert_eq!(longest, b"rubicundus"); + } + + pub fn longest_path_factored_jumping(zipper: Z) + where + Z: CatamorphismCachedWithEngine, + { + let longest = CatamorphismCachedWithEngine:: + ::factored_cata_jumping::>, Vec, Infallible, _, _, _, true>( + &zipper, + |_| Ok(Vec::new()), + |_mask, child, children| { children.push(child); Ok(()) }, + |mask, _value, children, prefix| { + let mut longest = children.map_or_else(Vec::new, |children| { + if mask.is_empty_mask() { + children.into_iter().max_by_key(|rest| rest.len()).unwrap_or_default() + } else { + mask.iter().zip(children).max_by_key(|(_byte, rest)| rest.len()) + .map_or_else(Vec::new, |(byte, mut rest)| { + rest.insert(0, byte); + rest + }) + } + }); + let mut path = prefix.to_vec(); + path.append(&mut longest); + Ok(path) + }, + ) + .unwrap(); + assert_eq!(longest, b"rubicundus"); + } + + pub fn branch_values_stepping(zipper: Z) + where + Z: CatamorphismCachedWithEngine, + { + let values = CatamorphismCachedWithEngine::::into_cata_cached( + zipper, + |_mask, children: &mut [Vec], value| { + if children.is_empty() { + Vec::new() + } else if let Some(value) = value { + vec![*value] + } else { + let mut values = children.first_mut().map_or_else(Vec::new, core::mem::take); + for child in &mut children[1..] { + values.append(child); + } + values + } + }, + ); + assert_eq!(values, vec![3]); + } + + pub fn factored_cata_folds_each_child_immediately(zipper: Z) + where + Z: CatamorphismCachedWithEngine, + { + use std::cell::RefCell; + + let events = RefCell::new(Vec::new()); + let result = CatamorphismCachedWithEngine:: + ::factored_cata_jumping::, u64, Infallible, _, _, _, false>( + &zipper, + |_mask| { + events.borrow_mut().push("new"); + Ok(Vec::new()) + }, + |_mask, child, accumulator| { + events.borrow_mut().push(if child == 0 { "fold 0" } else { "fold 1" }); + accumulator.push(child); + Ok(()) + }, + |_mask, value, accumulator, _prefix| { + match value { + Some(0) => events.borrow_mut().push("summarize 0"), + Some(1) => events.borrow_mut().push("summarize 1"), + _ => events.borrow_mut().push("summarize root"), + } + Ok(value.copied().unwrap_or_else(|| accumulator.unwrap().into_iter().sum())) + }, + ) + .unwrap(); + assert_eq!(result, 1); + assert_eq!(events.into_inner(), ["new", "summarize 0", "fold 0", "summarize 1", "fold 1", "summarize root"]); + } + + pub fn factored_cata_passthrough_root(zipper: Z) + where + Z: CatamorphismCachedWithEngine, + { + let result = CatamorphismCachedWithEngine:: + ::factored_cata_jumping::<(), Vec, Infallible, _, _, _, true>( + &zipper, + |_| panic!("a unary valueless root must not create an accumulator"), + |_mask, _child, _accumulator| panic!("a unary valueless root must not fold a child"), + |_mask, value, accumulator, prefix| { + assert_eq!(value, Some(&0)); + assert!(accumulator.is_none()); + Ok(prefix.to_vec()) + }, + ) + .unwrap(); + assert_eq!(result, b"abc"); + } + + /// Internal helper that gives the zipper constructor the lifetime of the test store. + pub fn run_test<'a, Z, Store, Engine>( + store: &'a mut Store, + make_z: impl Fn(&'a mut Store) -> Z, + test: impl Fn(Z), + ) + where + Z: 'a + CatamorphismCached + + CatamorphismCachedWithEngine, + { + test(make_z(store)); + } + + macro_rules! cached_catamorphism_case { + ($z_name:ident, $read_keys:expr, $make_z:expr, $engine:ty, $keys:ident, $test:ident) => { + paste::paste! { + #[test] + fn [<$z_name _ $test>]() { + let mut temp_store = ($read_keys)(crate::morphisms::cached_catamorphism_tests::$keys); + crate::morphisms::cached_catamorphism_tests::run_test::<_, _, $engine>( + &mut temp_store, + $make_z, + crate::morphisms::cached_catamorphism_tests::$test::<_, $engine>, + ); + } + } + }; + } + pub(crate) use cached_catamorphism_case; + + macro_rules! cached_catamorphism_default_case { + ($z_name:ident, $read_keys:expr, $make_z:expr, $engine:ty, $keys:ident, $test:ident) => { + paste::paste! { + #[test] + fn [<$z_name _ default_ $test>]() { + let mut temp_store = ($read_keys)(crate::morphisms::cached_catamorphism_tests::$keys); + crate::morphisms::cached_catamorphism_tests::run_test::<_, _, $engine>( + &mut temp_store, + $make_z, + crate::morphisms::cached_catamorphism_tests::default_facade_tests::$test, + ); + } + } + }; + } + pub(crate) use cached_catamorphism_default_case; + + macro_rules! cached_catamorphism_tests { + ($z_name:ident, $read_keys:expr, $make_z:expr, $engine:ty) => { + $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_default_case!($z_name, $read_keys, $make_z, $engine, CACHED_CATA_TEST_KEYS, leaf_count_stepping); + $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $read_keys, $make_z, $engine, CACHED_CATA_TEST_KEYS, leaf_count_stepping); + $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $read_keys, $make_z, $engine, CACHED_CATA_TEST_KEYS, leaf_count_jumping); + $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $read_keys, $make_z, $engine, CACHED_CATA_TEST_KEYS, leaf_count_factored_jumping); + $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $read_keys, $make_z, $engine, CACHED_CATA_TEST_KEYS, leaf_count_factored_stepping); + $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $read_keys, $make_z, $engine, CACHED_CATA_TEST_KEYS, longest_path_jumping); + $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $read_keys, $make_z, $engine, CACHED_CATA_TEST_KEYS, longest_path_factored_jumping); + $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $read_keys, $make_z, $engine, CACHED_CATA_TEST_KEYS, branch_values_stepping); + $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $read_keys, $make_z, $engine, CACHED_CATA_FOLD_ORDER_KEYS, factored_cata_folds_each_child_immediately); + $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $read_keys, $make_z, $engine, CACHED_CATA_PASSTHROUGH_KEYS, factored_cata_passthrough_root); + $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $read_keys, $make_z, $engine, CACHED_CATA_TEST_KEYS, factored_cata_propagates_callback_errors); + }; + } + pub(crate) use cached_catamorphism_tests; +} + #[cfg(test)] mod tests { use std::ops::Range; @@ -1781,6 +2420,76 @@ mod tests { use crate::utils::BitMask; use super::*; + trait TestRecursiveCata: Sized { + fn recursive_into_cata_cached(self, alg_f: AlgF) -> W + where + Self: CatamorphismCachedWithEngine, + W: Clone, + AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> W, + { + CatamorphismCachedWithEngine::::into_cata_cached(self, alg_f) + } + + fn recursive_into_cata_jumping_cached(self, alg_f: AlgF) -> W + where + Self: CatamorphismCachedWithEngine, + W: Clone, + AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> W, + { + CatamorphismCachedWithEngine::::into_cata_jumping_cached(self, alg_f) + } + + fn recursive_factored_cata_jumping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result + where + Self: CatamorphismCachedWithEngine, + W: Clone, + NewAccF: Copy + Fn(&ByteMask) -> Result, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, + SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, + { + CatamorphismCachedWithEngine::::factored_cata_jumping::(self, new_acc_f, fold_child_f, summarize_f) + } + + fn recursive_factored_cata(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result + where + Self: CatamorphismCachedWithEngine, + W: Clone, + NewAccF: Copy + Fn(&ByteMask) -> Result, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, + SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option) -> Result, + { + CatamorphismCachedWithEngine::::factored_cata(self, new_acc_f, fold_child_f, summarize_f) + } + } + + impl TestRecursiveCata for Z {} + + #[test] + fn recursive_and_iterative_cached_catas_agree() { + let map: PathMap = [ + (b"abc".as_slice(), 1), + (b"abd".as_slice(), 2), + (b"ax".as_slice(), 3), + ] + .into_iter() + .collect(); + + let alg = |_mask: &ByteMask, children: &mut [usize], value: Option<&usize>| { + children.iter().sum::() + value.copied().unwrap_or(0) + }; + let recursive = CatamorphismCachedWithEngine::::into_cata_cached( + map.read_zipper(), + alg, + ); + let iterative = CatamorphismCachedWithEngine::::into_cata_cached( + map.read_zipper(), + alg, + ); + + assert_eq!(recursive, iterative); + assert_eq!(recursive, 6); + } + fn check_side_effect_catas<'a, W, V, Z, AlgF, Assert>( zipper: Z, mut f_side: AlgF, mut assert: Assert) where @@ -1799,14 +2508,14 @@ mod tests { fn check_pure_catas<'a, W, V: Clone + Send + Sync, Z, AlgFP, Assert>( zipper: Z, f_pure: AlgFP, mut assert: Assert) where - Z: Clone + CatamorphismCached, W: Clone, + Z: Clone + CatamorphismCached + CatamorphismCachedWithEngine, W: Clone, AlgFP: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> W, Assert: FnMut(W, &str), { - let output = zipper.clone().into_cata_cached( + let output = zipper.clone().recursive_into_cata_cached( |bm, ch, v| f_pure(bm, ch, v, &[])); assert(output, "into_cata_cached"); - let output = zipper.clone().into_cata_jumping_cached( + let output = zipper.clone().recursive_into_cata_jumping_cached( |bm, ch, v, sub_path| f_pure(bm, ch, v, sub_path)); assert(output, "into_cata_jumping_cached"); } @@ -1814,7 +2523,7 @@ mod tests { fn check_all_catas<'a, W, V: Clone + Send + Sync, Z, AlgF, Assert>( zipper: Z, alg_f: AlgF, mut assert: Assert) where - Z: Clone + CatamorphismSideEffecting + CatamorphismCached, W: Clone, + Z: Clone + CatamorphismSideEffecting + CatamorphismCached + CatamorphismCachedWithEngine, W: Clone, AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> W, Assert: FnMut(W, &str), { @@ -1872,7 +2581,7 @@ mod tests { } (val.is_some(), sum) }; - let output = map.read_zipper().into_cata_cached(pure_alg_stepping); + let output = CatamorphismCachedWithEngine::<(), GlobalAlloc, RecursiveCata>::into_cata_cached(map.read_zipper(), pure_alg_stepping); assert_eq!(output.1, expected_sum); //The pure jumping cata is a variant on the above, but we also need to care about @@ -2438,7 +3147,7 @@ mod tests { // println!("tree: {:#?}", visit(&mut make_map().read_zipper())); use core::sync::atomic::{AtomicU64, Ordering::*}; let calls_cached = AtomicU64::new(0); - let tree_cached: Rc::> = make_map().into_cata_cached( + let tree_cached: Rc::> = make_map().recursive_into_cata_cached( |_bm, children, value| { calls_cached.fetch_add(1, Relaxed); Rc::new(Node::new(value, children)) @@ -2481,7 +3190,7 @@ mod tests { for (keys, expected_sum) in tests { let map: PathMap<()> = keys.into_iter().map(|v| (v, ())).collect(); - let sum = map.factored_cata_jumping::<_, _, Infallible, _, _, _, true>( + let sum = map.recursive_factored_cata_jumping::<_, _, Infallible, _, _, _, true>( |_| Ok(SumAcc::default()), |mask: &ByteMask, w: (bool, u32), acc: &mut SumAcc| { if let Some(byte) = mask.indexed_bit::(acc.idx) { @@ -2532,7 +3241,7 @@ mod tests { for (keys, expected_sum) in tests { let map: PathMap<()> = keys.into_iter().map(|v| (v, ())).collect(); - let sum = map.factored_cata::( + let sum = map.recursive_factored_cata::( |_| Ok(SumAcc::default()), |mask: &ByteMask, w: (bool, u32), acc: &mut SumAcc| { if let Some(byte) = mask.iter().nth(acc.idx) { @@ -2560,7 +3269,7 @@ mod tests { let words = ["arrow", "bow", "cannon", "roman", "romane", "romanus", "romulus", "rubens", "ruber", "rubicon", "rubicundus", "rom'i"]; words.iter().enumerate().for_each(|(i, word)| { map.set_val_at(word.as_bytes(), i); }); - let cached_stepping = map.read_zipper().into_cata_cached(|_mask, children: &mut [usize], val| { + let cached_stepping = map.read_zipper().recursive_into_cata_cached(|_mask, children: &mut [usize], val| { if children.is_empty() { assert!(val.is_some()); 1 @@ -2568,7 +3277,7 @@ mod tests { children.iter().sum() } }); - let cached_jumping = map.read_zipper().into_cata_jumping_cached(|_mask, children: &mut [usize], val, _prefix| { + let cached_jumping = map.read_zipper().recursive_into_cata_jumping_cached(|_mask, children: &mut [usize], val, _prefix| { if children.is_empty() { assert!(val.is_some()); 1 @@ -2577,7 +3286,7 @@ mod tests { } }); - let jumping = map.factored_cata_jumping::( + let jumping = map.recursive_factored_cata_jumping::( |_| Ok(0), |_mask, child, total| { *total += child; Ok(()) }, |_mask, val, children, _prefix| match children { @@ -2588,7 +3297,7 @@ mod tests { }, }, ); - let stepping = map.factored_cata::( + let stepping = map.recursive_factored_cata::( |_| Ok(0), |_mask, child, total| { *total += child; Ok(()) }, |_mask, val, children| match children { @@ -2615,7 +3324,7 @@ mod tests { let words = ["arrow", "bow", "cannon", "roman", "romane", "romanus", "romulus", "rubens", "ruber", "rubicon", "rubicundus", "rom'i"]; words.iter().enumerate().for_each(|(i, word)| { map.set_val_at(word.as_bytes(), i); }); - let cached = map.read_zipper().into_cata_jumping_cached(|mask, children: &mut [Vec], _val, prefix| { + let cached = map.read_zipper().recursive_into_cata_jumping_cached(|mask, children: &mut [Vec], _val, prefix| { let mut longest = mask.iter().zip(children.iter_mut()) .max_by_key(|(_byte, rest)| rest.len()) .map_or_else(Vec::new, |(byte, rest)| { @@ -2629,7 +3338,7 @@ mod tests { }); // This uses allocation for readability; performance-sensitive code can fold a longest path directly. - let jumping = map.factored_cata_jumping::>, Vec, Infallible, _, _, _, true>( + let jumping = map.recursive_factored_cata_jumping::>, Vec, Infallible, _, _, _, true>( |_| Ok(Vec::new()), |_mask, child, children| { children.push(child); Ok(()) }, |mask, _val, children, prefix| { @@ -2650,7 +3359,7 @@ mod tests { Ok(path) }, ); - let stepping = map.factored_cata::<(usize, Vec), Vec, Infallible, _, _, _>( + let stepping = map.recursive_factored_cata::<(usize, Vec), Vec, Infallible, _, _, _>( |_| Ok((0, Vec::new())), |mask, child, state| { let mut path = Vec::with_capacity(child.len() + 1); @@ -2666,7 +3375,7 @@ mod tests { }, |_mask, _val, state| Ok(state.map_or_else(Vec::new, |(_, path)| path)), ); - let adapted = map.into_cata_jumping_cached(|mask, children: &mut [Vec], _val, prefix| { + let adapted = map.recursive_into_cata_jumping_cached(|mask, children: &mut [Vec], _val, prefix| { let mut longest = mask.iter().zip(children.iter_mut()) .max_by_key(|(_byte, rest)| rest.len()) .map_or_else(Vec::new, |(byte, rest)| { @@ -2693,7 +3402,7 @@ mod tests { let words = ["arrow", "bow", "cannon", "roman", "romane", "romanus", "romulus", "rubens", "ruber", "rubicon", "rubicundus", "rom'i"]; words.iter().enumerate().for_each(|(i, word)| { map.set_val_at(word.as_bytes(), i); }); - let cached_stepping = map.read_zipper().into_cata_cached(|_mask, children: &mut [Vec], val| { + let cached_stepping = map.read_zipper().recursive_into_cata_cached(|_mask, children: &mut [Vec], val| { if children.is_empty() { Vec::new() } else if let Some(val) = val { @@ -2706,7 +3415,7 @@ mod tests { values } }); - let cached_jumping = map.read_zipper().into_cata_jumping_cached(|_mask, children: &mut [Vec], val, _prefix| { + let cached_jumping = map.read_zipper().recursive_into_cata_jumping_cached(|_mask, children: &mut [Vec], val, _prefix| { if children.is_empty() { Vec::new() } else if let Some(val) = val { @@ -2720,7 +3429,7 @@ mod tests { } }); - let jumping = map.factored_cata_jumping::, Vec, Infallible, _, _, _, false>( + let jumping = map.recursive_factored_cata_jumping::, Vec, Infallible, _, _, _, false>( |_| Ok(Vec::new()), |_mask, child, values| { values.extend(child); Ok(()) }, |_mask, val, children, _prefix| match children { @@ -2728,7 +3437,7 @@ mod tests { Some(values) => Ok(val.map_or(values, |val| vec![*val])), }, ); - let stepping = map.factored_cata::, Vec, Infallible, _, _, _>( + let stepping = map.recursive_factored_cata::, Vec, Infallible, _, _, _>( |_| Ok(Vec::new()), |_mask, child, values| { values.extend(child); Ok(()) }, |_mask, val, children| match children { @@ -2778,19 +3487,19 @@ mod tests { } let cached_calls = AtomicU64::new(0); - let cached: Rc> = make_map().into_cata_cached(|_mask, children, value| { + let cached: Rc> = make_map().recursive_into_cata_cached(|_mask, children, value| { cached_calls.fetch_add(1, Relaxed); Rc::new(Node { value: value.cloned(), children: children.to_vec() }) }); let jumping_calls = AtomicU64::new(0); - let jumping: Rc> = make_map().read_zipper().into_cata_jumping_cached(|_mask, children, value, _prefix| { + let jumping: Rc> = make_map().read_zipper().recursive_into_cata_jumping_cached(|_mask, children, value, _prefix| { jumping_calls.fetch_add(1, Relaxed); Rc::new(Node { value: value.cloned(), children: children.to_vec() }) }); let adapted_calls = AtomicU64::new(0); - let adapted: Rc> = make_map().into_cata_jumping_cached(|_mask, children, value, _prefix| { + let adapted: Rc> = make_map().recursive_into_cata_jumping_cached(|_mask, children, value, _prefix| { adapted_calls.fetch_add(1, Relaxed); Rc::new(Node { value: value.cloned(), children: children.to_vec() }) }); @@ -2799,7 +3508,7 @@ mod tests { assert_eq!(adapted_calls.load(Relaxed), jumping_calls.load(Relaxed)); let summarization_calls = AtomicU64::new(0); - let summarized = make_map().factored_cata::>>, Rc>, Infallible, _, _, _>( + let summarized = make_map().recursive_factored_cata::>>, Rc>, Infallible, _, _, _>( |_| Ok(Vec::new()), |_mask, child, children| { children.push(child); Ok(()) }, |_mask, value, children| { @@ -2813,7 +3522,7 @@ mod tests { let iterative_calls = AtomicU64::new(0); let iterative_map = make_map(); - let iterative = iterative_map.read_zipper().factored_cata::>>, Rc>, Infallible, _, _, _>( + let iterative = iterative_map.read_zipper().recursive_factored_cata::>>, Rc>, Infallible, _, _, _>( |_| Ok(Vec::new()), |_mask, child, children| { children.push(child); Ok(()) }, |_mask, value, children| { @@ -2839,7 +3548,7 @@ mod tests { let mut zipper = map.read_zipper(); zipper.descend_to(b"a"); - let count = zipper.factored_cata_jumping::( + let count = zipper.recursive_factored_cata_jumping::( |_| Ok(0), |_mask, child, total| { *total += child; Ok(()) }, |_mask, value, children, _prefix| { @@ -2863,7 +3572,7 @@ mod tests { .collect(); let events = RefCell::new(Vec::new()); - let result = map.read_zipper().factored_cata_jumping::, usize, Infallible, _, _, _, false>( + let result = map.read_zipper().recursive_factored_cata_jumping::, usize, Infallible, _, _, _, false>( |_mask| { events.borrow_mut().push("new"); Ok(Vec::new()) @@ -2894,7 +3603,7 @@ mod tests { fn iterative_summarization_does_not_accumulate_at_passthrough_root() { let map: PathMap<()> = [(b"abc".as_slice(), ())].into_iter().collect(); - let result = map.read_zipper().factored_cata_jumping::<(), Vec, Infallible, _, _, _, true>( + let result = map.read_zipper().recursive_factored_cata_jumping::<(), Vec, Infallible, _, _, _, true>( |_| panic!("a unary valueless root must not create an accumulator"), |_mask, _child, _accumulator| panic!("a unary valueless root must not fold a child"), |_mask, value, accumulator, prefix| { @@ -2919,7 +3628,7 @@ mod tests { let path = vec![b'a'; PATH_LEN]; map.set_val_at(&path, ()); - let count = map.factored_cata_jumping::<_, _, Infallible, _, _, _, false>( + let count = map.recursive_factored_cata_jumping::<_, _, Infallible, _, _, _, false>( |_| Ok(0usize), |_mask, w: usize, total| { *total += w; Ok(()) }, |_mask, v, total, _| Ok((v.is_some() as usize) + total.unwrap_or(0)), @@ -2931,21 +3640,21 @@ mod tests { fn recursive_cata_propagates_callback_errors() { let map: PathMap<()> = [(b"a".as_slice(), ()), (b"b".as_slice(), ())].into_iter().collect(); - let error = map.factored_cata_jumping::<(), (), &'static str, _, _, _, false>( + let error = map.recursive_factored_cata_jumping::<(), (), &'static str, _, _, _, false>( |_| Err("start"), |_mask, _child, _acc| Ok(()), |_mask, _value, _acc, _prefix| Ok(()), ); assert_eq!(error, Err("start")); - let error = map.factored_cata_jumping::<(), (), &'static str, _, _, _, false>( + let error = map.recursive_factored_cata_jumping::<(), (), &'static str, _, _, _, false>( |_| Ok(()), |_mask, _child, _acc| Err("fold"), |_mask, _value, _acc, _prefix| Ok(()), ); assert_eq!(error, Err("fold")); - let error = map.factored_cata_jumping::<(), (), &'static str, _, _, _, false>( + let error = map.recursive_factored_cata_jumping::<(), (), &'static str, _, _, _, false>( |_| Ok(()), |_mask, _child, _acc| Ok(()), |_mask, _value, _acc, _prefix| Err("summarize"), @@ -2966,7 +3675,7 @@ mod tests { .collect(); let summarize_calls = AtomicUsize::new(0); - let error = map.factored_cata_jumping::<(), (), &'static str, _, _, _, false>( + let error = map.recursive_factored_cata_jumping::<(), (), &'static str, _, _, _, false>( |_| Ok(()), |_mask, _child, _acc| Ok(()), |_mask, _value, _acc, _prefix| { diff --git a/src/zipper.rs b/src/zipper.rs index 95314fe9..9faf31bb 100644 --- a/src/zipper.rs +++ b/src/zipper.rs @@ -980,6 +980,12 @@ impl<'trie, V: Clone + Send + Sync + Unpin + 'trie, A: Allocator + 'trie> Zipper impl<'trie, V: Clone + Send + Sync + Unpin + 'trie, A: Allocator + 'trie> ZipperReadOnlyConditionalIteration<'trie, V> for ReadZipperTracked<'trie, '_, V, A> { } impl<'trie, V: Clone + Send + Sync + Unpin + 'trie, A: Allocator + 'trie> ZipperAbsolutePath for ReadZipperTracked<'trie, '_, V, A> { zipper_impl_lens!(ZipperAbsolutePath self => self.z); } +crate::morphisms::impl_catamorphism_cached!( + crate::morphisms::RecursiveCata; + impl<'trie, 'path, V, A> for ReadZipperTracked<'trie, 'path, V, A> as V, A + where [V: Clone + Send + Sync + Unpin + 'trie, A: Allocator + 'trie]; +); + impl ZipperForking for ReadZipperTracked<'_, '_, V, A>{ type ReadZipperT<'a> = ReadZipperUntracked<'a, 'a, V, A> where Self: 'a; @@ -1067,6 +1073,12 @@ impl ZipperInfallibleSubtries ZipperMoving for ReadZipperUntracked<'trie, '_, V, A> { zipper_impl_lens!(ZipperMoving self => self.z); } impl ZipperConcrete for ReadZipperUntracked<'_, '_, V, A> { zipper_impl_lens!(ZipperConcrete self => self.z); } +crate::morphisms::impl_catamorphism_cached!( + crate::morphisms::RecursiveCata; + impl<'trie, 'path, V, A> for ReadZipperUntracked<'trie, 'path, V, A> as V, A + where [V: Clone + Send + Sync + Unpin + 'trie, A: Allocator + 'trie]; +); + impl ZipperForking for ReadZipperUntracked<'_, '_, V, A> { type ReadZipperT<'a> = ReadZipperUntracked<'a, 'a, V, A> where Self: 'a; fn fork_read_zipper<'a>(&'a self) -> Self::ReadZipperT<'a> { @@ -1229,6 +1241,12 @@ impl<'trie, V: Clone + Send + Sync + Unpin + 'trie, A: Allocator + 'trie> Zipper impl<'trie, V: Clone + Send + Sync + Unpin + 'trie, A: Allocator + 'trie> ZipperReadOnlyConditionalIteration<'trie, V> for ReadZipperOwned { } impl<'trie, V: Clone + Send + Sync + Unpin + 'trie, A: Allocator + 'trie> ZipperAbsolutePath for ReadZipperOwned { zipper_impl_lens!(ZipperAbsolutePath self => self.z); } +crate::morphisms::impl_catamorphism_cached!( + crate::morphisms::RecursiveCata; + impl for ReadZipperOwned as V, A + where [V: Clone + Send + Sync + Unpin + 'static, A: Allocator + 'static]; +); + impl ZipperValues for ReadZipperOwned { fn val(&self) -> Option<&V> { unsafe{ self.z.get_val() } } @@ -4471,6 +4489,13 @@ mod tests { use crate::{alloc::global_alloc, PathMap}; use super::*; + crate::morphisms::cached_catamorphism_tests::cached_catamorphism_tests!( + read_zipper, + |keys: &[&[u8]]| keys.iter().enumerate().map(|(idx, path)| (*path, idx as u64)).collect::>(), + |map: &mut PathMap| map.read_zipper(), + crate::morphisms::IterativeCata + ); + super::zipper_moving_tests::zipper_moving_tests!(read_zipper, |keys: &[&[u8]]| { let mut btm = PathMap::new(); From f5912a9068aead4b03dc176b31aa0d609b3a0df0 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Mon, 31 Aug 2026 19:36:16 -0600 Subject: [PATCH 31/50] Getting rid of "into" semantics that take ownership for CatamorphismCached API --- benches/catamorphism.rs | 4 +- src/arena_compact.rs | 4 +- src/morphisms.rs | 180 ++++++++++++++++-------------- src/random.rs | 2 +- src/utils/debug/morphism_debug.rs | 2 +- 5 files changed, 105 insertions(+), 87 deletions(-) diff --git a/benches/catamorphism.rs b/benches/catamorphism.rs index 05be1fa7..b2510ea0 100644 --- a/benches/catamorphism.rs +++ b/benches/catamorphism.rs @@ -78,7 +78,7 @@ fn cached_jumping_cata_val_count(bencher: Bencher) { let mut sink = 0usize; bencher.bench_local(|| { let rz = map.read_zipper(); - *black_box(&mut sink) = CatamorphismCachedWithEngine::<(), GlobalAlloc, RecursiveCata>::into_cata_jumping_cached(rz, |_mask: &ByteMask, children: &mut [usize], val, _sub_path| { + *black_box(&mut sink) = CatamorphismCachedWithEngine::<(), GlobalAlloc, RecursiveCata>::cata_jumping_cached(&rz, |_mask: &ByteMask, children: &mut [usize], val, _sub_path| { let mut sum: usize = children.iter().sum(); if val.is_some() { sum += 1; @@ -118,7 +118,7 @@ fn cached_jumping_cata_total_len(bencher: Bencher) { let mut sink = (0usize, 0usize); bencher.bench_local(|| { let rz = map.read_zipper(); - *black_box(&mut sink) = CatamorphismCachedWithEngine::<(), GlobalAlloc, RecursiveCata>::into_cata_jumping_cached(rz, |mask: &ByteMask, children: &mut [(usize, usize)], val, sub_path| { + *black_box(&mut sink) = CatamorphismCachedWithEngine::<(), GlobalAlloc, RecursiveCata>::cata_jumping_cached(&rz, |mask: &ByteMask, children: &mut [(usize, usize)], val, sub_path| { let mut count = 0usize; let mut total_len = 0usize; let prefix_len = sub_path.len(); diff --git a/src/arena_compact.rs b/src/arena_compact.rs index 45a8b563..2a227ba6 100644 --- a/src/arena_compact.rs +++ b/src/arena_compact.rs @@ -1356,7 +1356,7 @@ struct CachedFrame { /// The traversal itself is the jumping catamorphism, unrolled (see /// `morphisms::into_cata_cached_body`, which this follows closely). It is /// spelled out here rather than delegating to -/// [`CatamorphismCached::into_cata_jumping_cached`] for two reasons: +/// [`CatamorphismCached::cata_jumping_cached`] for two reasons: /// - the cached cata only consults its cache one byte below a fork, whereas we /// also consult it at the fork we land on after jumping over a chain of /// bytes. That is where a subtrie grafted under a multi-byte path shows up, @@ -1367,7 +1367,7 @@ struct CachedFrame { /// - the cached cata's algebra is an `Fn`, so writing to the arena from it /// would need interior mutability. /// -/// TODO: GOAT: introduce an abstraction/modify `into_cata_jumping_cached`, +/// TODO: GOAT: introduce an abstraction/modify `cata_jumping_cached`, /// to address the problems listed above. The suggested API is to have /// `FnMut` variant for the caching catamorphism. /// diff --git a/src/morphisms.rs b/src/morphisms.rs index a5ba3a0d..dea60d2d 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -154,14 +154,15 @@ pub trait CatamorphismSideEffecting { pub struct RecursiveCata; /// Selects the iterative zipper-based implementation of [`CatamorphismCached`]. Use this to avoid -/// stack overflows caused by [`RecursiveCata`] +/// stack overflows caused by [`RecursiveCata`]. This engine requires that a zipper can be created +/// or cloned because it uses a zipper for traversal. pub struct IterativeCata; /// Provides cached (pure) catamorphisms using an explicitly selected implementation engine /// /// Set `Engine` to either [`RecursiveCata`] or [`IterativeCata`] depending on the implementation -/// desired / available -pub trait CatamorphismCachedWithEngine { +/// desired / available. +pub trait CatamorphismCachedWithEngine { /// Applies a **cached**, **stepping**, catamorphism to the trie descending from the zipper's /// root, running the `alg_f` at every step (at every byte) @@ -184,27 +185,27 @@ pub trait CatamorphismCachedWithEngine(self, alg_f: AlgF) -> W + fn cata_cached(&self, alg_f: AlgF) -> W where W: Clone, AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> W, Self: Sized { - self.into_cata_cached_fallible(|mask, children, val| -> Result { + self.cata_cached_fallible(|mask, children, val| -> Result { Ok(alg_f(mask, children, val)) }).unwrap() } /// Allows the closure to return an error, stopping traversal immediately /// - /// See [CatamorphismCached::into_cata_cached] - fn into_cata_cached_fallible(self, alg_f: AlgF) -> Result + /// See [CatamorphismCached::cata_cached] + fn cata_cached_fallible(&self, alg_f: AlgF) -> Result where W: Clone, AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> Result, Self: Sized, { - self.into_cata_jumping_cached_fallible(|mask, children, val, sub_path| { + self.cata_jumping_cached_fallible(|mask, children, val, sub_path| { let mut w = alg_f(mask, children, val)?; for &byte in sub_path.iter().rev() { let child_mask = ByteMask::from(byte); @@ -239,26 +240,26 @@ pub trait CatamorphismCachedWithEngine(self, alg_f: AlgF) -> W + /// See [cata_cached](CatamorphismCached::cata_cached) for explanation of other arguments and behavior + fn cata_jumping_cached(&self, alg_f: AlgF) -> W where W: Clone, AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> W, Self: Sized { - self.into_cata_jumping_cached_fallible(|mask, children, val, sub_path| -> Result { + self.cata_jumping_cached_fallible(|mask, children, val, sub_path| -> Result { Ok(alg_f(mask, children, val, sub_path)) }).unwrap() } /// Allows the closure to return an error, stopping traversal immediately /// - /// See [CatamorphismCached::into_cata_jumping_cached] - fn into_cata_jumping_cached_fallible(self, alg_f: AlgF) -> Result + /// See [CatamorphismCached::cata_jumping_cached] + fn cata_jumping_cached_fallible(&self, alg_f: AlgF) -> Result where W: Clone, AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> Result, - Self: Sized //GOAT, remove uneeded when we take &self + Self: Sized { let children = std::cell::RefCell::new(CataChildren::::new()); let children = &children; @@ -287,7 +288,7 @@ pub trait CatamorphismCachedWithEngine u128 + fn hash(&self) -> u128 where Self: Sized, V: std::hash::Hash @@ -300,12 +301,12 @@ pub trait CatamorphismCachedWithEngine(self, val_hash: F) -> u128 + fn hash_with(&self, val_hash: F) -> u128 where Self: Sized, F: Fn(&V) -> u128 { - self.into_cata_cached(|bm, hs, mv| { + self.cata_cached(|bm, hs, mv| { let mut hasher = gxhash::GxHasher::with_seed(0b0100001010101101111110010110100110000010011000100100100111110111i64); hasher.write(unsafe { slice_from_raw_parts(bm.0.as_ptr() as *const u8, 32).as_ref().unwrap_unchecked() }); hasher.write(unsafe { slice_from_raw_parts(hs.as_ptr() as *const u8, 16*hs.len()).as_ref().unwrap_unchecked() }); @@ -345,7 +346,6 @@ pub trait CatamorphismCachedWithEngine(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where - V: Clone + Send + Sync, W: Clone, NewAccF: Copy + Fn(&ByteMask) -> Result, FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, @@ -360,7 +360,6 @@ pub trait CatamorphismCachedWithEngine(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where - V: Clone + Send + Sync, W: Clone, NewAccF: Copy + Fn(&ByteMask) -> Result, FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, @@ -389,40 +388,40 @@ pub trait CatamorphismCachedWithEngine { +pub trait CatamorphismCached { /// The catamorphism engine selected as the default for this zipper type. type Engine; - /// Calls [`CatamorphismCachedWithEngine::into_cata_cached`] with this zipper type's selected + /// Calls [`CatamorphismCachedWithEngine::cata_cached`] with this zipper type's selected /// [`Self::Engine`]. - fn into_cata_cached(self, alg_f: AlgF) -> W + fn cata_cached(&self, alg_f: AlgF) -> W where W: Clone, AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> W, Self: Sized, ; - /// Calls [`CatamorphismCachedWithEngine::into_cata_cached_fallible`] with this zipper type's + /// Calls [`CatamorphismCachedWithEngine::cata_cached_fallible`] with this zipper type's /// selected [`Self::Engine`]. - fn into_cata_cached_fallible(self, alg_f: AlgF) -> Result + fn cata_cached_fallible(&self, alg_f: AlgF) -> Result where W: Clone, AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> Result, Self: Sized, ; - /// Calls [`CatamorphismCachedWithEngine::into_cata_jumping_cached`] with this zipper type's + /// Calls [`CatamorphismCachedWithEngine::cata_jumping_cached`] with this zipper type's /// selected [`Self::Engine`]. - fn into_cata_jumping_cached(self, alg_f: AlgF) -> W + fn cata_jumping_cached(&self, alg_f: AlgF) -> W where W: Clone, AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> W, Self: Sized, ; - /// Calls [`CatamorphismCachedWithEngine::into_cata_jumping_cached_fallible`] with this zipper + /// Calls [`CatamorphismCachedWithEngine::cata_jumping_cached_fallible`] with this zipper /// type's selected [`Self::Engine`]. - fn into_cata_jumping_cached_fallible(self, alg_f: AlgF) -> Result + fn cata_jumping_cached_fallible(&self, alg_f: AlgF) -> Result where W: Clone, AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> Result, @@ -431,7 +430,7 @@ pub trait CatamorphismCached /// Calls [`CatamorphismCachedWithEngine::hash`] with this zipper type's selected /// [`Self::Engine`]. - fn hash(self) -> u128 + fn hash(&self) -> u128 where Self: Sized, V: std::hash::Hash, @@ -439,7 +438,7 @@ pub trait CatamorphismCached /// Calls [`CatamorphismCachedWithEngine::hash_with`] with this zipper type's selected /// [`Self::Engine`]. - fn hash_with(self, val_hash: F) -> u128 + fn hash_with(&self, val_hash: F) -> u128 where Self: Sized, F: Fn(&V) -> u128, @@ -503,51 +502,51 @@ macro_rules! impl_catamorphism_cached { type Engine = $engine; #[inline] - fn into_cata_cached(self, alg_f: AlgF) -> W + fn cata_cached(&self, alg_f: AlgF) -> W where W: Clone, AlgF: Fn(&$crate::utils::ByteMask, &mut [W], Option<&$value>) -> W, Self: Sized, { $crate::morphisms::CatamorphismCachedWithEngine::<$value, $allocator, $engine> - ::into_cata_cached(self, alg_f) + ::cata_cached(self, alg_f) } #[inline] - fn into_cata_cached_fallible(self, alg_f: AlgF) -> Result + fn cata_cached_fallible(&self, alg_f: AlgF) -> Result where W: Clone, AlgF: Fn(&$crate::utils::ByteMask, &mut [W], Option<&$value>) -> Result, Self: Sized, { $crate::morphisms::CatamorphismCachedWithEngine::<$value, $allocator, $engine> - ::into_cata_cached_fallible(self, alg_f) + ::cata_cached_fallible(self, alg_f) } #[inline] - fn into_cata_jumping_cached(self, alg_f: AlgF) -> W + fn cata_jumping_cached(&self, alg_f: AlgF) -> W where W: Clone, AlgF: Fn(&$crate::utils::ByteMask, &mut [W], Option<&$value>, &[u8]) -> W, Self: Sized, { $crate::morphisms::CatamorphismCachedWithEngine::<$value, $allocator, $engine> - ::into_cata_jumping_cached(self, alg_f) + ::cata_jumping_cached(self, alg_f) } #[inline] - fn into_cata_jumping_cached_fallible(self, alg_f: AlgF) -> Result + fn cata_jumping_cached_fallible(&self, alg_f: AlgF) -> Result where W: Clone, AlgF: Fn(&$crate::utils::ByteMask, &mut [W], Option<&$value>, &[u8]) -> Result, Self: Sized, { $crate::morphisms::CatamorphismCachedWithEngine::<$value, $allocator, $engine> - ::into_cata_jumping_cached_fallible(self, alg_f) + ::cata_jumping_cached_fallible(self, alg_f) } #[inline] - fn hash(self) -> u128 + fn hash(&self) -> u128 where Self: Sized, $value: std::hash::Hash, @@ -557,7 +556,7 @@ macro_rules! impl_catamorphism_cached { } #[inline] - fn hash_with(self, val_hash: F) -> u128 + fn hash_with(&self, val_hash: F) -> u128 where Self: Sized, F: Fn(&$value) -> u128, @@ -862,7 +861,6 @@ impl Catamorph impl<'a, Z, V: Clone + Send + Sync + 'a, A: Allocator> CatamorphismCachedWithEngine for Z where Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer + ZipperInfallibleSubtries { fn factored_cata_jumping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where - V: Clone + Send + Sync, W: Clone, NewAccF: Copy + Fn(&ByteMask) -> Result, FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, @@ -880,10 +878,9 @@ impl<'a, Z, V: Clone + Send + Sync + 'a, A: Allocator> CatamorphismCachedWithEng } } -impl<'a, Z, V: Clone + Send + Sync + 'a, A: Allocator> CatamorphismCachedWithEngine for Z where Z: Clone + Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer { +impl<'a, Z, V: 'a, A: Allocator> CatamorphismCachedWithEngine for Z where Z: Clone + Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer { fn factored_cata_jumping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where - V: Clone + Send + Sync, W: Clone, NewAccF: Copy + Fn(&ByteMask) -> Result, FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, @@ -901,7 +898,6 @@ impl<'a, Z, V: Clone + Send + Sync + 'a, A: Allocator> CatamorphismCachedWithEng impl CatamorphismCachedWithEngine for PathMap { fn factored_cata_jumping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where - V: Clone + Send + Sync, W: Clone, NewAccF: Copy + Fn(&ByteMask) -> Result, FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, @@ -921,7 +917,6 @@ impl CatamorphismCachedWithEngine< impl CatamorphismCachedWithEngine for PathMap { fn factored_cata_jumping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where - V: Clone + Send + Sync, W: Clone, NewAccF: Copy + Fn(&ByteMask) -> Result, FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, @@ -946,7 +941,6 @@ impl_catamorphism_cached!( RecursiveCata; impl<'prefix, V, A, Z> for PrefixZipper<'prefix, Z> as V, A where [ - V: Clone + Send + Sync, A: Allocator, PrefixZipper<'prefix, Z>: CatamorphismCachedWithEngine ]; @@ -956,7 +950,6 @@ impl_catamorphism_cached!( RecursiveCata; impl for OneFactor as V, A where [ - V: Clone + Send + Sync, A: Allocator, OneFactor: CatamorphismCachedWithEngine ]; @@ -966,7 +959,6 @@ impl_catamorphism_cached!( RecursiveCata; impl for Box as V, A where [ - V: Clone + Send + Sync, A: Allocator, Box: CatamorphismCachedWithEngine ]; @@ -976,7 +968,6 @@ impl_catamorphism_cached!( RecursiveCata; impl<'zipper, V, A, Z> for &'zipper mut Z as V, A where [ - V: Clone + Send + Sync, A: Allocator, &'zipper mut Z: CatamorphismCachedWithEngine ]; @@ -2099,7 +2090,7 @@ pub(crate) mod cached_catamorphism_tests { where Z: CatamorphismCached, { - let count = zipper.into_cata_cached(|_mask, children: &mut [usize], value| { + let count = zipper.cata_cached(|_mask, children: &mut [usize], value| { if children.is_empty() { assert!(value.is_some()); 1 @@ -2147,8 +2138,8 @@ pub(crate) mod cached_catamorphism_tests { where Z: CatamorphismCachedWithEngine, { - let count = CatamorphismCachedWithEngine::::into_cata_cached( - zipper, + let count = CatamorphismCachedWithEngine::::cata_cached( + &zipper, |_mask, children: &mut [usize], value| { if children.is_empty() { assert!(value.is_some()); @@ -2165,8 +2156,8 @@ pub(crate) mod cached_catamorphism_tests { where Z: CatamorphismCachedWithEngine, { - let count = CatamorphismCachedWithEngine::::into_cata_jumping_cached( - zipper, + let count = CatamorphismCachedWithEngine::::cata_jumping_cached( + &zipper, |_mask, children: &mut [usize], value, _prefix| { if children.is_empty() { assert!(value.is_some()); @@ -2225,8 +2216,8 @@ pub(crate) mod cached_catamorphism_tests { where Z: CatamorphismCachedWithEngine, { - let longest = CatamorphismCachedWithEngine::::into_cata_jumping_cached( - zipper, + let longest = CatamorphismCachedWithEngine::::cata_jumping_cached( + &zipper, |mask, children: &mut [Vec], _value, prefix| { let mut longest = mask.iter().zip(children.iter_mut()) .max_by_key(|(_byte, rest)| rest.len()) @@ -2277,8 +2268,8 @@ pub(crate) mod cached_catamorphism_tests { where Z: CatamorphismCachedWithEngine, { - let values = CatamorphismCachedWithEngine::::into_cata_cached( - zipper, + let values = CatamorphismCachedWithEngine::::cata_cached( + &zipper, |_mask, children: &mut [Vec], value| { if children.is_empty() { Vec::new() @@ -2421,22 +2412,22 @@ mod tests { use super::*; trait TestRecursiveCata: Sized { - fn recursive_into_cata_cached(self, alg_f: AlgF) -> W + fn recursive_cata_cached(&self, alg_f: AlgF) -> W where Self: CatamorphismCachedWithEngine, W: Clone, AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> W, { - CatamorphismCachedWithEngine::::into_cata_cached(self, alg_f) + CatamorphismCachedWithEngine::::cata_cached(self, alg_f) } - fn recursive_into_cata_jumping_cached(self, alg_f: AlgF) -> W + fn recursive_cata_jumping_cached(&self, alg_f: AlgF) -> W where Self: CatamorphismCachedWithEngine, W: Clone, AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> W, { - CatamorphismCachedWithEngine::::into_cata_jumping_cached(self, alg_f) + CatamorphismCachedWithEngine::::cata_jumping_cached(self, alg_f) } fn recursive_factored_cata_jumping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result @@ -2477,12 +2468,14 @@ mod tests { let alg = |_mask: &ByteMask, children: &mut [usize], value: Option<&usize>| { children.iter().sum::() + value.copied().unwrap_or(0) }; - let recursive = CatamorphismCachedWithEngine::::into_cata_cached( - map.read_zipper(), + let recursive_zipper = map.read_zipper(); + let recursive = CatamorphismCachedWithEngine::::cata_cached( + &recursive_zipper, alg, ); - let iterative = CatamorphismCachedWithEngine::::into_cata_cached( - map.read_zipper(), + let iterative_zipper = map.read_zipper(); + let iterative = CatamorphismCachedWithEngine::::cata_cached( + &iterative_zipper, alg, ); @@ -2490,6 +2483,30 @@ mod tests { assert_eq!(recursive, 6); } + #[test] + fn cached_cata_facade_borrows_its_receiver() { + let map: PathMap = [(b"a".as_slice(), 42)].into_iter().collect(); + let stepping = CatamorphismCached::cata_cached(&map, |_mask, children: &mut [u64], value| { + children.iter().sum::() + value.copied().unwrap_or_default() + }); + let jumping = CatamorphismCached::cata_jumping_cached(&map, |_mask, children: &mut [u64], value, _prefix| { + children.iter().sum::() + value.copied().unwrap_or_default() + }); + let fallible = CatamorphismCached::cata_cached_fallible(&map, |_mask, children: &mut [u64], value| { + Ok::<_, Infallible>(children.iter().sum::() + value.copied().unwrap_or_default()) + }); + let jumping_fallible = CatamorphismCached::cata_jumping_cached_fallible(&map, |_mask, children: &mut [u64], value, _prefix| { + Ok::<_, Infallible>(children.iter().sum::() + value.copied().unwrap_or_default()) + }); + + assert_eq!((stepping, jumping, fallible, jumping_fallible), (42, 42, Ok(42), Ok(42))); + assert_eq!(CatamorphismCached::hash(&map), CatamorphismCached::hash(&map)); + assert_eq!( + CatamorphismCached::hash_with(&map, |value| *value as u128), + CatamorphismCached::hash_with(&map, |value| *value as u128), + ); + } + fn check_side_effect_catas<'a, W, V, Z, AlgF, Assert>( zipper: Z, mut f_side: AlgF, mut assert: Assert) where @@ -2512,12 +2529,12 @@ mod tests { AlgFP: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> W, Assert: FnMut(W, &str), { - let output = zipper.clone().recursive_into_cata_cached( + let output = zipper.clone().recursive_cata_cached( |bm, ch, v| f_pure(bm, ch, v, &[])); - assert(output, "into_cata_cached"); - let output = zipper.clone().recursive_into_cata_jumping_cached( + assert(output, "cata_cached"); + let output = zipper.clone().recursive_cata_jumping_cached( |bm, ch, v, sub_path| f_pure(bm, ch, v, sub_path)); - assert(output, "into_cata_jumping_cached"); + assert(output, "cata_jumping_cached"); } fn check_all_catas<'a, W, V: Clone + Send + Sync, Z, AlgF, Assert>( @@ -2581,7 +2598,8 @@ mod tests { } (val.is_some(), sum) }; - let output = CatamorphismCachedWithEngine::<(), GlobalAlloc, RecursiveCata>::into_cata_cached(map.read_zipper(), pure_alg_stepping); + let zipper = map.read_zipper(); + let output = CatamorphismCachedWithEngine::<(), GlobalAlloc, RecursiveCata>::cata_cached(&zipper, pure_alg_stepping); assert_eq!(output.1, expected_sum); //The pure jumping cata is a variant on the above, but we also need to care about @@ -3147,7 +3165,7 @@ mod tests { // println!("tree: {:#?}", visit(&mut make_map().read_zipper())); use core::sync::atomic::{AtomicU64, Ordering::*}; let calls_cached = AtomicU64::new(0); - let tree_cached: Rc::> = make_map().recursive_into_cata_cached( + let tree_cached: Rc::> = make_map().recursive_cata_cached( |_bm, children, value| { calls_cached.fetch_add(1, Relaxed); Rc::new(Node::new(value, children)) @@ -3269,7 +3287,7 @@ mod tests { let words = ["arrow", "bow", "cannon", "roman", "romane", "romanus", "romulus", "rubens", "ruber", "rubicon", "rubicundus", "rom'i"]; words.iter().enumerate().for_each(|(i, word)| { map.set_val_at(word.as_bytes(), i); }); - let cached_stepping = map.read_zipper().recursive_into_cata_cached(|_mask, children: &mut [usize], val| { + let cached_stepping = map.read_zipper().recursive_cata_cached(|_mask, children: &mut [usize], val| { if children.is_empty() { assert!(val.is_some()); 1 @@ -3277,7 +3295,7 @@ mod tests { children.iter().sum() } }); - let cached_jumping = map.read_zipper().recursive_into_cata_jumping_cached(|_mask, children: &mut [usize], val, _prefix| { + let cached_jumping = map.read_zipper().recursive_cata_jumping_cached(|_mask, children: &mut [usize], val, _prefix| { if children.is_empty() { assert!(val.is_some()); 1 @@ -3324,7 +3342,7 @@ mod tests { let words = ["arrow", "bow", "cannon", "roman", "romane", "romanus", "romulus", "rubens", "ruber", "rubicon", "rubicundus", "rom'i"]; words.iter().enumerate().for_each(|(i, word)| { map.set_val_at(word.as_bytes(), i); }); - let cached = map.read_zipper().recursive_into_cata_jumping_cached(|mask, children: &mut [Vec], _val, prefix| { + let cached = map.read_zipper().recursive_cata_jumping_cached(|mask, children: &mut [Vec], _val, prefix| { let mut longest = mask.iter().zip(children.iter_mut()) .max_by_key(|(_byte, rest)| rest.len()) .map_or_else(Vec::new, |(byte, rest)| { @@ -3375,7 +3393,7 @@ mod tests { }, |_mask, _val, state| Ok(state.map_or_else(Vec::new, |(_, path)| path)), ); - let adapted = map.recursive_into_cata_jumping_cached(|mask, children: &mut [Vec], _val, prefix| { + let adapted = map.recursive_cata_jumping_cached(|mask, children: &mut [Vec], _val, prefix| { let mut longest = mask.iter().zip(children.iter_mut()) .max_by_key(|(_byte, rest)| rest.len()) .map_or_else(Vec::new, |(byte, rest)| { @@ -3402,7 +3420,7 @@ mod tests { let words = ["arrow", "bow", "cannon", "roman", "romane", "romanus", "romulus", "rubens", "ruber", "rubicon", "rubicundus", "rom'i"]; words.iter().enumerate().for_each(|(i, word)| { map.set_val_at(word.as_bytes(), i); }); - let cached_stepping = map.read_zipper().recursive_into_cata_cached(|_mask, children: &mut [Vec], val| { + let cached_stepping = map.read_zipper().recursive_cata_cached(|_mask, children: &mut [Vec], val| { if children.is_empty() { Vec::new() } else if let Some(val) = val { @@ -3415,7 +3433,7 @@ mod tests { values } }); - let cached_jumping = map.read_zipper().recursive_into_cata_jumping_cached(|_mask, children: &mut [Vec], val, _prefix| { + let cached_jumping = map.read_zipper().recursive_cata_jumping_cached(|_mask, children: &mut [Vec], val, _prefix| { if children.is_empty() { Vec::new() } else if let Some(val) = val { @@ -3487,19 +3505,19 @@ mod tests { } let cached_calls = AtomicU64::new(0); - let cached: Rc> = make_map().recursive_into_cata_cached(|_mask, children, value| { + let cached: Rc> = make_map().recursive_cata_cached(|_mask, children, value| { cached_calls.fetch_add(1, Relaxed); Rc::new(Node { value: value.cloned(), children: children.to_vec() }) }); let jumping_calls = AtomicU64::new(0); - let jumping: Rc> = make_map().read_zipper().recursive_into_cata_jumping_cached(|_mask, children, value, _prefix| { + let jumping: Rc> = make_map().read_zipper().recursive_cata_jumping_cached(|_mask, children, value, _prefix| { jumping_calls.fetch_add(1, Relaxed); Rc::new(Node { value: value.cloned(), children: children.to_vec() }) }); let adapted_calls = AtomicU64::new(0); - let adapted: Rc> = make_map().recursive_into_cata_jumping_cached(|_mask, children, value, _prefix| { + let adapted: Rc> = make_map().recursive_cata_jumping_cached(|_mask, children, value, _prefix| { adapted_calls.fetch_add(1, Relaxed); Rc::new(Node { value: value.cloned(), children: children.to_vec() }) }); diff --git a/src/random.rs b/src/random.rs index 280cee19..e8494825 100644 --- a/src/random.rs +++ b/src/random.rs @@ -143,7 +143,7 @@ impl Distribution<(Vec, Option)> for FairTriePa //TODO: There has to be a more efficient way to implement this than making two passes through the whole trie use crate::morphisms::{CatamorphismSideEffecting, CatamorphismCached}; // it's much cheaper to draw many samples at once, but the current Distribution API is broken - let size = self.source.clone().into_cata_cached(|_: &ByteMask, ws: &mut [usize], _mv: Option<&T>| { + let size = self.source.cata_cached(|_: &ByteMask, ws: &mut [usize], _mv: Option<&T>| { ws.iter().sum::() + 1 }); let target = rng.random_range(0..size); diff --git a/src/utils/debug/morphism_debug.rs b/src/utils/debug/morphism_debug.rs index c4cda1ae..3485b48d 100644 --- a/src/utils/debug/morphism_debug.rs +++ b/src/utils/debug/morphism_debug.rs @@ -11,7 +11,7 @@ use crate::morphisms::{into_cata_cached_body, DoCache}; /// This trait provides debug-only catamorphism methods that may expose additional /// information useful for debugging and development. pub trait CatamorphismDebug { - /// A version of [`into_cata_jumping_cached`](crate::morphisms::CatamorphismCached::into_cata_jumping_cached) where + /// A version of [`cata_jumping_cached`](crate::morphisms::CatamorphismCached::cata_jumping_cached) where /// the full path is available to the closure; **For debugging purposes only** /// /// Using data from the full path for your algorithm **will** lead to incorrect behavior. From 39b68cae91012f1af5eeb0e451f991adeb3be888 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Mon, 31 Aug 2026 20:53:02 -0600 Subject: [PATCH 32/50] Simplifying the multi-trait CatamorphismCached interface. Getting rid of "Engine" parameter and just generating two traits with a single macro --- benches/catamorphism.rs | 12 +- src/arena_compact.rs | 14 +- src/morphisms.rs | 836 ++++++++++++++-------------------------- src/zipper.rs | 27 +- 4 files changed, 304 insertions(+), 585 deletions(-) diff --git a/benches/catamorphism.rs b/benches/catamorphism.rs index b2510ea0..806efe99 100644 --- a/benches/catamorphism.rs +++ b/benches/catamorphism.rs @@ -1,7 +1,7 @@ use divan::{Divan, Bencher, black_box}; use core::convert::Infallible; use pathmap::alloc::GlobalAlloc; -use pathmap::morphisms::{CatamorphismCachedWithEngine, RecursiveCata}; +use pathmap::morphisms::CatamorphismCached; use pathmap::utils::ByteMask; use pathmap::utils::ints::gen_int_range; use pathmap::PathMap; @@ -43,7 +43,7 @@ fn recursive_cata_jumping_val_count(bencher: Bencher) { let mut sink = 0usize; bencher.bench_local(|| { let rz = map.read_zipper(); - *black_box(&mut sink) = CatamorphismCachedWithEngine::<(), GlobalAlloc, RecursiveCata>::factored_cata_jumping::<_, _, Infallible, _, _, _, false>(&rz, + *black_box(&mut sink) = CatamorphismCached::<(), GlobalAlloc>::factored_cata_jumping::<_, _, Infallible, _, _, _, false>(&rz, |_| Ok(0usize), |_mask, w: usize, total| { *total += w; Ok(()) }, |_mask, v, total, _| Ok((v.is_some() as usize) + total.unwrap_or(0)), @@ -58,7 +58,7 @@ fn recursive_cata_binary_tree_leaf_count(bencher: Bencher) { let mut sink = 0usize; bencher.bench_local(|| { let rz = map.read_zipper(); - *black_box(&mut sink) = CatamorphismCachedWithEngine::<(), GlobalAlloc, RecursiveCata> + *black_box(&mut sink) = CatamorphismCached::<(), GlobalAlloc> ::factored_cata_jumping::<_, _, Infallible, _, _, _, false>(&rz, |_| Ok(0usize), |_mask, child_count: usize, total| { @@ -78,7 +78,7 @@ fn cached_jumping_cata_val_count(bencher: Bencher) { let mut sink = 0usize; bencher.bench_local(|| { let rz = map.read_zipper(); - *black_box(&mut sink) = CatamorphismCachedWithEngine::<(), GlobalAlloc, RecursiveCata>::cata_jumping_cached(&rz, |_mask: &ByteMask, children: &mut [usize], val, _sub_path| { + *black_box(&mut sink) = CatamorphismCached::<(), GlobalAlloc>::cata_jumping_cached(&rz, |_mask: &ByteMask, children: &mut [usize], val, _sub_path| { let mut sum: usize = children.iter().sum(); if val.is_some() { sum += 1; @@ -95,7 +95,7 @@ fn recursive_cata_jumping_total_len(bencher: Bencher) { let mut sink = (0usize, 0usize); bencher.bench_local(|| { let rz = map.read_zipper(); - *black_box(&mut sink) = CatamorphismCachedWithEngine::<(), GlobalAlloc, RecursiveCata>::factored_cata_jumping::<_, _, Infallible, _, _, _, true>(&rz, + *black_box(&mut sink) = CatamorphismCached::<(), GlobalAlloc>::factored_cata_jumping::<_, _, Infallible, _, _, _, true>(&rz, |_| Ok((0usize, 0usize)), |_mask: &ByteMask, w: (usize, usize), acc: &mut (usize, usize)| { acc.0 += w.0; @@ -118,7 +118,7 @@ fn cached_jumping_cata_total_len(bencher: Bencher) { let mut sink = (0usize, 0usize); bencher.bench_local(|| { let rz = map.read_zipper(); - *black_box(&mut sink) = CatamorphismCachedWithEngine::<(), GlobalAlloc, RecursiveCata>::cata_jumping_cached(&rz, |mask: &ByteMask, children: &mut [(usize, usize)], val, sub_path| { + *black_box(&mut sink) = CatamorphismCached::<(), GlobalAlloc>::cata_jumping_cached(&rz, |mask: &ByteMask, children: &mut [(usize, usize)], val, sub_path| { let mut count = 0usize; let mut total_len = 0usize; let prefix_len = sub_path.len(); diff --git a/src/arena_compact.rs b/src/arena_compact.rs index 2a227ba6..bc9094a1 100644 --- a/src/arena_compact.rs +++ b/src/arena_compact.rs @@ -2419,18 +2419,6 @@ where Storage: AsRef<[u8]> } } -crate::morphisms::impl_catamorphism_cached!( - crate::morphisms::IterativeCata; - impl<'tree, Storage> for ACTZipper<'tree, Storage, ()> as (), GlobalAlloc - where [Storage: AsRef<[u8]>]; -); - -crate::morphisms::impl_catamorphism_cached!( - crate::morphisms::IterativeCata; - impl<'tree, Storage> for ACTZipper<'tree, Storage, u64> as u64, GlobalAlloc - where [Storage: AsRef<[u8]>]; -); - impl<'tree, Storage, Value> Zipper for ACTZipper<'tree, Storage, Value> where Storage: AsRef<[u8]> { @@ -3343,7 +3331,7 @@ mod tests { ArenaCompactTree::from_zipper(map.read_zipper(), |&value| value) }, |tree: &mut ArenaCompactTree>| tree.read_zipper_u64(), - crate::morphisms::IterativeCata + CatamorphismCachedIterative ); /// Build `map` both ways and check the results describe the same trie. diff --git a/src/morphisms.rs b/src/morphisms.rs index dea60d2d..138f135c 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -149,423 +149,194 @@ pub trait CatamorphismSideEffecting { where AlgF: FnMut(&ByteMask, &mut [W], usize, Option<&V>, &[u8]) -> Result; } -/// Selects the recursive implementation of [`CatamorphismCached`]. This tends to be about 10x -/// faster on average, vs. [`IterativeCata`] -pub struct RecursiveCata; - -/// Selects the iterative zipper-based implementation of [`CatamorphismCached`]. Use this to avoid -/// stack overflows caused by [`RecursiveCata`]. This engine requires that a zipper can be created -/// or cloned because it uses a zipper for traversal. -pub struct IterativeCata; - -/// Provides cached (pure) catamorphisms using an explicitly selected implementation engine -/// -/// Set `Engine` to either [`RecursiveCata`] or [`IterativeCata`] depending on the implementation -/// desired / available. -pub trait CatamorphismCachedWithEngine { - - /// Applies a **cached**, **stepping**, catamorphism to the trie descending from the zipper's - /// root, running the `alg_f` at every step (at every byte) - /// - /// This method may re-use previous calculations of `W`, if the value for a shared subtrie has - /// been previously computed. - /// - /// ## Arguments to `alg_f`: - /// `(child_mask: &`[`ByteMask`]`, children: &mut [W], val: Option<&V>` - /// - /// - `child_mask`: A [`ByteMask`] indicating the corresponding byte for each downstream branche in - /// `children`. - /// - /// - `children`: A slice containing all the `W` values from previous invocations of `alg_f` for - /// downstream branches. - /// - /// - `value`: A value associated with a given path in the trie, or `None` if the trie has no value at - /// that path. - /// - /// ## Behavior - /// - /// The focus position of the zipper will be ignored and it will be immediately reset to the root. - fn cata_cached(&self, alg_f: AlgF) -> W - where - W: Clone, - AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> W, - Self: Sized - { - self.cata_cached_fallible(|mask, children, val| -> Result { - Ok(alg_f(mask, children, val)) - }).unwrap() - } - - /// Allows the closure to return an error, stopping traversal immediately - /// - /// See [CatamorphismCached::cata_cached] - fn cata_cached_fallible(&self, alg_f: AlgF) -> Result - where - W: Clone, - AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> Result, - Self: Sized, - { - self.cata_jumping_cached_fallible(|mask, children, val, sub_path| { - let mut w = alg_f(mask, children, val)?; - for &byte in sub_path.iter().rev() { - let child_mask = ByteMask::from(byte); - w = alg_f(&child_mask, core::slice::from_mut(&mut w), None)?; - } - Ok(w) - }) - } - - /// Applies a "jumping" catamorphism to the trie - /// - /// A "jumping" catamorphism is a form of catamorphism where the `alg_f` "jumps over" (isn't called for) - /// path bytes in the trie where there isn't either a `value` or a branch where `children.len() > 1`. - /// - /// This method may re-use previous calculations of `W`, if the value for a shared subtrie has - /// been previously computed. - /// - /// ## Arguments to `alg_f`: - /// `(child_mask: &`[`ByteMask`]`, children: &mut [W], value: Option<&V>, sub_path: &[u8]` - /// - /// - `sub_path`: A slice of path bytes for which the `alf_f` will not be called. Consider the - /// trie below: - /// - /// ```txt - /// ─── c ─── o ─── m ─┬─ b ─── o → "combo" - /// ├─ e ─── t → "comet" - /// └─ f ─── o ─── r ─── t → "comfort" - /// ``` - /// The `alg_f` would be called 4 times for this trie. - /// 1. `alg_f(ByteMask::EMPTY, &[], Some(&()), b"o")` - /// 2. `alg_f(ByteMask::EMPTY, &[], Some(&()), b"t")` - /// 3. `alg_f(ByteMask::EMPTY, &[], Some(&()), b"ort")` - /// 4. `alg_f(ByteMask::from_iter([b'b', b'e', b'f']), &[..], None, b"com")` - /// - /// See [cata_cached](CatamorphismCached::cata_cached) for explanation of other arguments and behavior - fn cata_jumping_cached(&self, alg_f: AlgF) -> W - where - W: Clone, - AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> W, - Self: Sized - { - self.cata_jumping_cached_fallible(|mask, children, val, sub_path| -> Result { - Ok(alg_f(mask, children, val, sub_path)) - }).unwrap() - } - - /// Allows the closure to return an error, stopping traversal immediately - /// - /// See [CatamorphismCached::cata_jumping_cached] - fn cata_jumping_cached_fallible(&self, alg_f: AlgF) -> Result - where - W: Clone, - AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> Result, - Self: Sized - { - let children = std::cell::RefCell::new(CataChildren::::new()); - let children = &children; - let alg_f = &alg_f; - - self.factored_cata_jumping::( - move |mask| { - debug_assert!(children.try_borrow_mut().is_ok()); - Ok(unsafe{&mut *children.as_ptr()}.new_acc(mask.count_bits()) ) - }, - move |_mask, child, acc| { - debug_assert!(children.try_borrow_mut().is_ok()); - unsafe{&mut *children.as_ptr()}.push(acc, child); - Ok(()) - }, - move |mask, value, acc, prefix| match acc { - Some(acc) => { - debug_assert!(children.try_borrow_mut().is_ok()); - unsafe{&mut *children.as_ptr()}.summarize(acc, mask.count_bits(), |children| { - alg_f(mask, children, value, prefix) - }) - }, - None => alg_f(mask, &mut [], value, prefix), - }, - ) - } - - /// Hash the logical `PathMap` and all its values - fn hash(&self) -> u128 - where - Self: Sized, - V: std::hash::Hash - { - self.hash_with(|v| { - let mut hasher = gxhash::GxHasher::with_seed(0); - v.hash(&mut hasher); - hasher.finish_u128() - }) - } - - /// Hash the logical `PathMap`, using the provided function to hash values - fn hash_with(&self, val_hash: F) -> u128 - where - Self: Sized, - F: Fn(&V) -> u128 - { - self.cata_cached(|bm, hs, mv| { - let mut hasher = gxhash::GxHasher::with_seed(0b0100001010101101111110010110100110000010011000100100100111110111i64); - hasher.write(unsafe { slice_from_raw_parts(bm.0.as_ptr() as *const u8, 32).as_ref().unwrap_unchecked() }); - hasher.write(unsafe { slice_from_raw_parts(hs.as_ptr() as *const u8, 16*hs.len()).as_ref().unwrap_unchecked() }); - if let Some(v) = mv { hasher.write_u128(val_hash(v)) }; - hasher.finish_u128() - }) - } - - /// A low-level catamorphism API that decomposes the algebra into multiple functions and allows - /// redundant path computation to be disabled - /// - /// ## Closures: - /// - /// `NewAccF`: Creates an accumulator for a logical trie node with more than one child branch. - /// `fn(child_mask: &ByteMask) -> Result` - /// - /// `FoldChildF`: Folds one downstream child branch's `W` into the accumulator. It is called once - /// for each downstream child branch, in the same order as the bits in `child_mask`. Each call for - /// a given logical node receives the same full child mask; the callback must use the order of its - /// calls to associate a result with a particular byte. - /// `fn(child_mask: &ByteMask, downstream: W, accumulator: &mut Acc) -> Result<(), Err>` - /// - /// `SummarizeF`: Produces the `W` for one logical trie node and a non-branching sub-path `prefix` - /// above it. The returned `W` should summarize the subtrie from the start of `prefix`, including - /// the `value` and downstream children. - /// - `child_mask` describes the node's immediate child bytes. - /// - `accumulator` contains the results folded from those child branches. `accumulator` is `None` - /// when the node has no downstream branches. - /// - `prefix` is a non-branching sub-path above the logical node. `prefix` never includes a - /// path position that is also part of a `child_mask` for this or another call to `summarize_f` - /// `fn(child_mask: &ByteMask, value: Option<&V>, accumulator: Option, prefix: &[u8]) -> Result` - /// - /// Errors from any callback immediately stop traversal and are returned to the caller. - /// - /// `COMPUTE_PATH=false` avoids materializing path runs and passes an empty `prefix`. This should - /// only be used when the algebra is agnostic to the path bytes, and only sensitive to values and/or - /// path endpoints. - fn factored_cata_jumping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result - where - W: Clone, - NewAccF: Copy + Fn(&ByteMask) -> Result, - FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, - SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, - Self: Sized; - - /// A stepping (non-jumping) catamorphism for the trie. - /// - /// Use this when the cata must evaluate once per path byte, including bytes in non-branching - /// runs. Unlike the jumping version, `summarize_f` has no `prefix`: it is called once for every - /// path byte. The callback roles and child-mask ordering are otherwise the same as for - /// [`CatamorphismCached::factored_cata_jumping`]. - fn factored_cata(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result - where - W: Clone, - NewAccF: Copy + Fn(&ByteMask) -> Result, - FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, - SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option) -> Result, - Self: Sized - { - self.factored_cata_jumping::<_, _, _, _, _, _, true>( - new_acc_f, - fold_child_f, - |mask, val, acc, prefix| { - let mut w = summarize_f(mask, val, acc)?; - for byte in prefix.iter().rev() { - let mask = ByteMask::from(*byte); - let mut acc = new_acc_f(&mask)?; - fold_child_f(&mask, w, &mut acc)?; - w = summarize_f(&mask, None, Some(acc))?; - } - Ok(w) - }, - ) - } -} - -/// The ordinary cached-cata interface. Method calls through this trait use the default strategy for -/// the zipper type. -/// -/// [`Self::Engine`] selects that strategy. [`CatamorphismCachedWithEngine`] is the lower-level -/// interface for explicitly selecting a different catamorphism engine. -pub trait CatamorphismCached { - /// The catamorphism engine selected as the default for this zipper type. - type Engine; - - /// Calls [`CatamorphismCachedWithEngine::cata_cached`] with this zipper type's selected - /// [`Self::Engine`]. - fn cata_cached(&self, alg_f: AlgF) -> W - where - W: Clone, - AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> W, - Self: Sized, - ; - - /// Calls [`CatamorphismCachedWithEngine::cata_cached_fallible`] with this zipper type's - /// selected [`Self::Engine`]. - fn cata_cached_fallible(&self, alg_f: AlgF) -> Result - where - W: Clone, - AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> Result, - Self: Sized, - ; - - /// Calls [`CatamorphismCachedWithEngine::cata_jumping_cached`] with this zipper type's - /// selected [`Self::Engine`]. - fn cata_jumping_cached(&self, alg_f: AlgF) -> W - where - W: Clone, - AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> W, - Self: Sized, - ; - - /// Calls [`CatamorphismCachedWithEngine::cata_jumping_cached_fallible`] with this zipper - /// type's selected [`Self::Engine`]. - fn cata_jumping_cached_fallible(&self, alg_f: AlgF) -> Result - where - W: Clone, - AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> Result, - Self: Sized, - ; - - /// Calls [`CatamorphismCachedWithEngine::hash`] with this zipper type's selected - /// [`Self::Engine`]. - fn hash(&self) -> u128 - where - Self: Sized, - V: std::hash::Hash, - ; - - /// Calls [`CatamorphismCachedWithEngine::hash_with`] with this zipper type's selected - /// [`Self::Engine`]. - fn hash_with(&self, val_hash: F) -> u128 - where - Self: Sized, - F: Fn(&V) -> u128, - ; - - /// Calls [`CatamorphismCachedWithEngine::factored_cata_jumping`] with this zipper type's - /// selected [`Self::Engine`]. - fn factored_cata_jumping( - &self, - new_acc_f: NewAccF, - fold_child_f: FoldChildF, - summarize_f: SummarizeF, - ) -> Result - where - W: Clone, - NewAccF: Copy + Fn(&ByteMask) -> Result, - FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, - SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, - Self: Sized, - ; - - /// Calls [`CatamorphismCachedWithEngine::factored_cata`] with this zipper type's selected - /// [`Self::Engine`]. - fn factored_cata( - &self, - new_acc_f: NewAccF, - fold_child_f: FoldChildF, - summarize_f: SummarizeF, - ) -> Result - where - W: Clone, - NewAccF: Copy + Fn(&ByteMask) -> Result, - FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, - SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option) -> Result, - Self: Sized, - ; -} - -/// Implements [`CatamorphismCached`] for one zipper type by selecting an implementation engine. -/// -/// This is deliberately invoked for each supported zipper type: a blanket implementation could -/// not give `ACTZipper` its iterative default while giving native zippers their recursive default. -/// -/// ```ignore -/// crate::morphisms::impl_catamorphism_cached!( -/// IterativeCata; -/// impl<'tree, Storage> for ACTZipper<'tree, Storage, u64> as u64, GlobalAlloc -/// where [Storage: AsRef<[u8]>]; -/// ); -/// ``` -macro_rules! impl_catamorphism_cached { - ( - $engine:ty; - impl<$($generic:tt),*> for $zipper:ty as $value:ty, $allocator:ty - where [$($where_clause:tt)*]; - ) => { - impl<$($generic),*> $crate::morphisms::CatamorphismCached<$value, $allocator> for $zipper - where - $($where_clause)* - { - type Engine = $engine; - - #[inline] +macro_rules! define_cached_cata_trait { + ($(#[$meta:meta])* $trait_name:ident) => { + $(#[$meta])* + pub trait $trait_name { + /// Applies a **cached**, **stepping**, catamorphism to the trie descending from the + /// zipper's root, running `alg_f` at every step (every byte). + /// + /// This method may reuse previously calculated `W` values when a shared subtrie has + /// already been computed. + /// + /// ## Arguments to `alg_f` + /// + /// `(child_mask: &`[`ByteMask`]`, children: &mut [W], value: Option<&V>)` + /// + /// - `child_mask` indicates the corresponding byte for every downstream branch in + /// `children`. + /// - `children` contains the `W` values produced for downstream branches. + /// - `value` is the value associated with this path, or `None` when there is none. + /// + /// ## Behavior + /// + /// The zipper's focus is ignored; traversal starts again at the root. fn cata_cached(&self, alg_f: AlgF) -> W where W: Clone, - AlgF: Fn(&$crate::utils::ByteMask, &mut [W], Option<&$value>) -> W, + AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> W, Self: Sized, { - $crate::morphisms::CatamorphismCachedWithEngine::<$value, $allocator, $engine> - ::cata_cached(self, alg_f) + self.cata_cached_fallible(|mask, children, val| -> Result { + Ok(alg_f(mask, children, val)) + }).unwrap() } - #[inline] + /// Allows the closure to return an error, stopping traversal immediately. + /// + /// See [`Self::cata_cached`] for the closure arguments and traversal behavior. fn cata_cached_fallible(&self, alg_f: AlgF) -> Result where W: Clone, - AlgF: Fn(&$crate::utils::ByteMask, &mut [W], Option<&$value>) -> Result, + AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> Result, Self: Sized, { - $crate::morphisms::CatamorphismCachedWithEngine::<$value, $allocator, $engine> - ::cata_cached_fallible(self, alg_f) + self.cata_jumping_cached_fallible(|mask, children, val, sub_path| { + let mut w = alg_f(mask, children, val)?; + for &byte in sub_path.iter().rev() { + let child_mask = ByteMask::from(byte); + w = alg_f(&child_mask, core::slice::from_mut(&mut w), None)?; + } + Ok(w) + }) } - #[inline] + /// Applies a **cached**, **jumping** catamorphism to the trie. + /// + /// A jumping catamorphism does not call `alg_f` for path bytes that have neither a + /// value nor a branch with more than one child. Those omitted bytes are passed as + /// `sub_path` instead. + /// + /// This method may reuse previously calculated `W` values when a shared subtrie has + /// already been computed. + /// + /// ## Arguments to `alg_f` + /// + /// `(child_mask: &`[`ByteMask`]`, children: &mut [W], value: Option<&V>, sub_path: &[u8])` + /// + /// `sub_path` is the sequence of bytes for which `alg_f` was not called. For example, + /// in this trie: + /// + /// ```text + /// ─── c ─── o ─── m ─┬─ b ─── o → "combo" + /// ├─ e ─── t → "comet" + /// └─ f ─── o ─── r ─── t → "comfort" + /// ``` + /// + /// `alg_f` is called four times: + /// + /// 1. `alg_f(ByteMask::EMPTY, &[], Some(&()), b"o")` + /// 2. `alg_f(ByteMask::EMPTY, &[], Some(&()), b"t")` + /// 3. `alg_f(ByteMask::EMPTY, &[], Some(&()), b"ort")` + /// 4. `alg_f(ByteMask::from_iter([b'b', b'e', b'f']), &[..], None, b"com")` + /// + /// See [`Self::cata_cached`] for the other arguments and traversal behavior. fn cata_jumping_cached(&self, alg_f: AlgF) -> W where W: Clone, - AlgF: Fn(&$crate::utils::ByteMask, &mut [W], Option<&$value>, &[u8]) -> W, + AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> W, Self: Sized, { - $crate::morphisms::CatamorphismCachedWithEngine::<$value, $allocator, $engine> - ::cata_jumping_cached(self, alg_f) + self.cata_jumping_cached_fallible(|mask, children, val, sub_path| -> Result { + Ok(alg_f(mask, children, val, sub_path)) + }).unwrap() } - #[inline] + /// Allows the closure to return an error, stopping traversal immediately. + /// + /// See [`Self::cata_jumping_cached`] for the closure arguments and traversal behavior. fn cata_jumping_cached_fallible(&self, alg_f: AlgF) -> Result where W: Clone, - AlgF: Fn(&$crate::utils::ByteMask, &mut [W], Option<&$value>, &[u8]) -> Result, + AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> Result, Self: Sized, { - $crate::morphisms::CatamorphismCachedWithEngine::<$value, $allocator, $engine> - ::cata_jumping_cached_fallible(self, alg_f) + let children = std::cell::RefCell::new(CataChildren::::new()); + let children = &children; + let alg_f = &alg_f; + + self.factored_cata_jumping::( + move |mask| { + debug_assert!(children.try_borrow_mut().is_ok()); + Ok(unsafe { &mut *children.as_ptr() }.new_acc(mask.count_bits())) + }, + move |_mask, child, acc| { + debug_assert!(children.try_borrow_mut().is_ok()); + unsafe { &mut *children.as_ptr() }.push(acc, child); + Ok(()) + }, + move |mask, value, acc, prefix| match acc { + Some(acc) => { + debug_assert!(children.try_borrow_mut().is_ok()); + unsafe { &mut *children.as_ptr() }.summarize(acc, mask.count_bits(), |children| { + alg_f(mask, children, value, prefix) + }) + }, + None => alg_f(mask, &mut [], value, prefix), + }, + ) } - #[inline] + /// Hashes the logical trie and all of its values. fn hash(&self) -> u128 where Self: Sized, - $value: std::hash::Hash, + V: std::hash::Hash, { - $crate::morphisms::CatamorphismCachedWithEngine::<$value, $allocator, $engine> - ::hash(self) + self.hash_with(|v| { + let mut hasher = gxhash::GxHasher::with_seed(0); + v.hash(&mut hasher); + hasher.finish_u128() + }) } - #[inline] + /// Hashes the logical trie using the provided function to hash values. fn hash_with(&self, val_hash: F) -> u128 where Self: Sized, - F: Fn(&$value) -> u128, + F: Fn(&V) -> u128, { - $crate::morphisms::CatamorphismCachedWithEngine::<$value, $allocator, $engine> - ::hash_with(self, val_hash) + self.cata_cached(|bm, hs, mv| { + let mut hasher = gxhash::GxHasher::with_seed(0b0100001010101101111110010110100110000010011000100100100111110111i64); + hasher.write(unsafe { slice_from_raw_parts(bm.0.as_ptr() as *const u8, 32).as_ref().unwrap_unchecked() }); + hasher.write(unsafe { slice_from_raw_parts(hs.as_ptr() as *const u8, 16 * hs.len()).as_ref().unwrap_unchecked() }); + if let Some(v) = mv { hasher.write_u128(val_hash(v)) }; + hasher.finish_u128() + }) } - #[inline] + /// A low-level cached catamorphism API that decomposes the algebra into multiple + /// functions and can avoid redundant path computation. + /// + /// ## Closures + /// + /// `NewAccF` creates an accumulator for a logical trie node with more than one child + /// branch: `fn(child_mask: &ByteMask) -> Result`. + /// + /// `FoldChildF` folds one downstream child's `W` into that accumulator. It is called + /// once per downstream branch, in the same order as the bits in `child_mask`. Every + /// call for a logical node receives its complete child mask, so use call order to + /// associate a child result with its byte: + /// `fn(child_mask: &ByteMask, downstream: W, accumulator: &mut Acc) -> Result<(), Err>`. + /// + /// `SummarizeF` produces the `W` for a logical trie node and a non-branching path + /// `prefix` above it. Its result must summarize the subtrie from the start of `prefix`, + /// including `value` and downstream children. + /// + /// - `child_mask` describes the node's immediate child bytes. + /// - `accumulator` contains results folded from child branches, or is `None` when the + /// node has no downstream branches. + /// - `prefix` is a non-branching path above the logical node. It never includes a path + /// position that is part of a `child_mask` for this or another `summarize_f` call. + /// + /// Its signature is + /// `fn(child_mask: &ByteMask, value: Option<&V>, accumulator: Option, prefix: &[u8]) -> Result`. + /// + /// Errors from any callback stop traversal immediately and are returned to the caller. + /// + /// With `COMPUTE_PATH = false`, path runs are not materialized and `prefix` is always + /// empty. Use that only when the algebra does not depend on path bytes, only on values + /// and/or path endpoints. fn factored_cata_jumping( &self, new_acc_f: NewAccF, @@ -574,21 +345,17 @@ macro_rules! impl_catamorphism_cached { ) -> Result where W: Clone, - NewAccF: Copy + Fn(&$crate::utils::ByteMask) -> Result, - FoldChildF: Copy + Fn(&$crate::utils::ByteMask, W, &mut Acc) -> Result<(), Err>, - SummarizeF: Copy + Fn(&$crate::utils::ByteMask, Option<&$value>, Option, &[u8]) -> Result, - Self: Sized, - { - $crate::morphisms::CatamorphismCachedWithEngine::<$value, $allocator, $engine> - ::factored_cata_jumping::( - self, - new_acc_f, - fold_child_f, - summarize_f, - ) - } - - #[inline] + NewAccF: Copy + Fn(&ByteMask) -> Result, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, + SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, + Self: Sized; + + /// A **stepping** catamorphism based on a similar factored algebra to [`Self::factored_cata_jumping`] + /// + /// Use this when the cata must evaluate once per path byte, including bytes in + /// non-branching runs. Unlike the jumping version, `summarize_f` has no `prefix`: it + /// is called once for every path byte. The callback roles and child-mask ordering are + /// otherwise the same as for [`Self::factored_cata_jumping`]. fn factored_cata( &self, new_acc_f: NewAccF, @@ -597,20 +364,60 @@ macro_rules! impl_catamorphism_cached { ) -> Result where W: Clone, - NewAccF: Copy + Fn(&$crate::utils::ByteMask) -> Result, - FoldChildF: Copy + Fn(&$crate::utils::ByteMask, W, &mut Acc) -> Result<(), Err>, - SummarizeF: Copy + Fn(&$crate::utils::ByteMask, Option<&$value>, Option) -> Result, + NewAccF: Copy + Fn(&ByteMask) -> Result, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, + SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option) -> Result, Self: Sized, { - $crate::morphisms::CatamorphismCachedWithEngine::<$value, $allocator, $engine> - ::factored_cata(self, new_acc_f, fold_child_f, summarize_f) + self.factored_cata_jumping::<_, _, _, _, _, _, true>( + new_acc_f, + fold_child_f, + |mask, val, acc, prefix| { + let mut w = summarize_f(mask, val, acc)?; + for byte in prefix.iter().rev() { + let mask = ByteMask::from(*byte); + let mut acc = new_acc_f(&mask)?; + fold_child_f(&mask, w, &mut acc)?; + w = summarize_f(&mask, None, Some(acc))?; + } + Ok(w) + }, + ) } } }; } -pub(crate) use impl_catamorphism_cached; -/// Shared child-result storage used to adapt [`CatamorphismCachedWithEngine`] to the single-function-algebra cata API. +define_cached_cata_trait! { + /// Cached catamorphisms evaluated with recursive traversal. + /// + /// This is the normal choice: it is substantially faster (about 10x) than + /// [`CatamorphismCachedIterative`] for typical tries and does not require cloning or creating + /// a zipper. It uses the call stack, so prefer [`CatamorphismCachedIterative`] when paths can + /// be deeply nested so there may be a risk of stack overflow. + /// + /// [`CatamorphismCachedIterative`] deliberately has the same method names and signatures so it + /// is perfectly interchangeable. Import one trait for ordinary method syntax. When both + /// strategies are needed in one scope, call the desired trait with fully qualified syntax. + CatamorphismCached +} + +define_cached_cata_trait! { + /// Cached catamorphisms evaluated with iterative zipper traversal. + /// + /// Prefer this trait when the trie can contain unbounded or adversarially deep paths and stack + /// safety matters more than throughput. It traverses a zipper, so an implementation must be + /// able to create or clone a movable zipper; for ordinary bounded-depth tries, + /// [`CatamorphismCached`] is faster. + /// + /// This trait intentionally has the same method names and signatures as [`CatamorphismCached`]. + /// Import one trait for ordinary method syntax. When both strategies are needed in one scope, + /// call the desired trait with fully qualified syntax. such as + /// `CatamorphismCachedIterative::cata_cached(&zipper, algebra)`. + CatamorphismCachedIterative +} + +/// Shared child-result storage used to adapt the factored cached-cata API to a single-function algebra. struct CataChildren { children: Vec, #[cfg(debug_assertions)] @@ -858,7 +665,7 @@ impl Catamorph } } -impl<'a, Z, V: Clone + Send + Sync + 'a, A: Allocator> CatamorphismCachedWithEngine for Z where Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer + ZipperInfallibleSubtries { +impl<'a, Z, V: Clone + Send + Sync + 'a, A: Allocator> CatamorphismCached for Z where Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer + ZipperInfallibleSubtries { fn factored_cata_jumping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where W: Clone, @@ -878,7 +685,7 @@ impl<'a, Z, V: Clone + Send + Sync + 'a, A: Allocator> CatamorphismCachedWithEng } } -impl<'a, Z, V: 'a, A: Allocator> CatamorphismCachedWithEngine for Z where Z: Clone + Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer { +impl<'a, Z, V: 'a, A: Allocator> CatamorphismCachedIterative for Z where Z: Clone + Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer { fn factored_cata_jumping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where W: Clone, @@ -895,7 +702,7 @@ impl<'a, Z, V: 'a, A: Allocator> CatamorphismCachedWithEngine CatamorphismCachedWithEngine for PathMap { +impl CatamorphismCached for PathMap { fn factored_cata_jumping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where W: Clone, @@ -914,7 +721,7 @@ impl CatamorphismCachedWithEngine< } } -impl CatamorphismCachedWithEngine for PathMap { +impl CatamorphismCachedIterative for PathMap { fn factored_cata_jumping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where W: Clone, @@ -931,47 +738,6 @@ impl CatamorphismCachedWithEngine< } } -impl_catamorphism_cached!( - RecursiveCata; - impl for PathMap as V, A - where [V: Clone + Send + Sync + Unpin, A: Allocator]; -); - -impl_catamorphism_cached!( - RecursiveCata; - impl<'prefix, V, A, Z> for PrefixZipper<'prefix, Z> as V, A - where [ - A: Allocator, - PrefixZipper<'prefix, Z>: CatamorphismCachedWithEngine - ]; -); - -impl_catamorphism_cached!( - RecursiveCata; - impl for OneFactor as V, A - where [ - A: Allocator, - OneFactor: CatamorphismCachedWithEngine - ]; -); - -impl_catamorphism_cached!( - RecursiveCata; - impl for Box as V, A - where [ - A: Allocator, - Box: CatamorphismCachedWithEngine - ]; -); - -impl_catamorphism_cached!( - RecursiveCata; - impl<'zipper, V, A, Z> for &'zipper mut Z as V, A - where [ - A: Allocator, - &'zipper mut Z: CatamorphismCachedWithEngine - ]; -); /// Helper function to summarize one path run (section of non-branching path bytes) // @@ -2069,8 +1835,6 @@ pub(crate) mod cached_catamorphism_tests { use core::convert::Infallible; use crate::alloc::GlobalAlloc; - use crate::morphisms::{CatamorphismCached, CatamorphismCachedWithEngine}; - use crate::utils::BitMask; pub const CACHED_CATA_TEST_KEYS: &[&[u8]] = &[ b"arrow", b"bow", b"cannon", b"roman", b"romane", b"romanus", b"romulus", @@ -2079,34 +1843,18 @@ pub(crate) mod cached_catamorphism_tests { pub const CACHED_CATA_FOLD_ORDER_KEYS: &[&[u8]] = &[b"a", b"b"]; pub const CACHED_CATA_PASSTHROUGH_KEYS: &[&[u8]] = &[b"abc"]; - // Keep the explicit-engine trait out of this module's scope so this exercises the same - // unqualified method call clients make after importing only `CatamorphismCached`. - pub(crate) mod default_facade_tests { - use crate::alloc::GlobalAlloc; - use crate::morphisms::CatamorphismCached; - - /// Exercises the ordinary facade rather than an explicitly selected engine. - pub fn leaf_count_stepping(zipper: Z) - where - Z: CatamorphismCached, - { - let count = zipper.cata_cached(|_mask, children: &mut [usize], value| { - if children.is_empty() { - assert!(value.is_some()); - 1 - } else { - children.iter().sum() - } - }); - assert_eq!(count, 11); - } - } + macro_rules! define_cached_catamorphism_test_suite { + ($suite_name:ident, $cata_trait:ident) => { + pub(crate) mod $suite_name { + use super::{GlobalAlloc, Infallible}; + use crate::morphisms::$cata_trait; + use crate::utils::BitMask; - pub fn factored_cata_propagates_callback_errors(zipper: Z) + pub fn factored_cata_propagates_callback_errors(zipper: Z) where - Z: CatamorphismCachedWithEngine, + Z: crate::morphisms::$cata_trait, { - let error = CatamorphismCachedWithEngine:: + let error = $cata_trait:: ::factored_cata_jumping::<(), (), &'static str, _, _, _, false>( &zipper, |_| Err("new"), @@ -2115,7 +1863,7 @@ pub(crate) mod cached_catamorphism_tests { ); assert_eq!(error, Err("new")); - let error = CatamorphismCachedWithEngine:: + let error = $cata_trait:: ::factored_cata_jumping::<(), (), &'static str, _, _, _, false>( &zipper, |_| Ok(()), @@ -2124,7 +1872,7 @@ pub(crate) mod cached_catamorphism_tests { ); assert_eq!(error, Err("fold")); - let error = CatamorphismCachedWithEngine:: + let error = $cata_trait:: ::factored_cata_jumping::<(), (), &'static str, _, _, _, false>( &zipper, |_| Ok(()), @@ -2134,11 +1882,11 @@ pub(crate) mod cached_catamorphism_tests { assert_eq!(error, Err("summarize")); } - pub fn leaf_count_stepping(zipper: Z) + pub fn leaf_count_stepping(zipper: Z) where - Z: CatamorphismCachedWithEngine, + Z: crate::morphisms::$cata_trait, { - let count = CatamorphismCachedWithEngine::::cata_cached( + let count = $cata_trait::::cata_cached( &zipper, |_mask, children: &mut [usize], value| { if children.is_empty() { @@ -2152,11 +1900,11 @@ pub(crate) mod cached_catamorphism_tests { assert_eq!(count, 11); } - pub fn leaf_count_jumping(zipper: Z) + pub fn leaf_count_jumping(zipper: Z) where - Z: CatamorphismCachedWithEngine, + Z: crate::morphisms::$cata_trait, { - let count = CatamorphismCachedWithEngine::::cata_jumping_cached( + let count = $cata_trait::::cata_jumping_cached( &zipper, |_mask, children: &mut [usize], value, _prefix| { if children.is_empty() { @@ -2170,11 +1918,11 @@ pub(crate) mod cached_catamorphism_tests { assert_eq!(count, 11); } - pub fn leaf_count_factored_jumping(zipper: Z) + pub fn leaf_count_factored_jumping(zipper: Z) where - Z: CatamorphismCachedWithEngine, + Z: crate::morphisms::$cata_trait, { - let count = CatamorphismCachedWithEngine:: + let count = $cata_trait:: ::factored_cata_jumping::( &zipper, |_| Ok(0), @@ -2191,11 +1939,11 @@ pub(crate) mod cached_catamorphism_tests { assert_eq!(count, 11); } - pub fn leaf_count_factored_stepping(zipper: Z) + pub fn leaf_count_factored_stepping(zipper: Z) where - Z: CatamorphismCachedWithEngine, + Z: crate::morphisms::$cata_trait, { - let count = CatamorphismCachedWithEngine:: + let count = $cata_trait:: ::factored_cata::( &zipper, |_| Ok(0), @@ -2212,11 +1960,11 @@ pub(crate) mod cached_catamorphism_tests { assert_eq!(count, 11); } - pub fn longest_path_jumping(zipper: Z) + pub fn longest_path_jumping(zipper: Z) where - Z: CatamorphismCachedWithEngine, + Z: crate::morphisms::$cata_trait, { - let longest = CatamorphismCachedWithEngine::::cata_jumping_cached( + let longest = $cata_trait::::cata_jumping_cached( &zipper, |mask, children: &mut [Vec], _value, prefix| { let mut longest = mask.iter().zip(children.iter_mut()) @@ -2234,11 +1982,11 @@ pub(crate) mod cached_catamorphism_tests { assert_eq!(longest, b"rubicundus"); } - pub fn longest_path_factored_jumping(zipper: Z) + pub fn longest_path_factored_jumping(zipper: Z) where - Z: CatamorphismCachedWithEngine, + Z: crate::morphisms::$cata_trait, { - let longest = CatamorphismCachedWithEngine:: + let longest = $cata_trait:: ::factored_cata_jumping::>, Vec, Infallible, _, _, _, true>( &zipper, |_| Ok(Vec::new()), @@ -2264,11 +2012,11 @@ pub(crate) mod cached_catamorphism_tests { assert_eq!(longest, b"rubicundus"); } - pub fn branch_values_stepping(zipper: Z) + pub fn branch_values_stepping(zipper: Z) where - Z: CatamorphismCachedWithEngine, + Z: crate::morphisms::$cata_trait, { - let values = CatamorphismCachedWithEngine::::cata_cached( + let values = $cata_trait::::cata_cached( &zipper, |_mask, children: &mut [Vec], value| { if children.is_empty() { @@ -2287,14 +2035,14 @@ pub(crate) mod cached_catamorphism_tests { assert_eq!(values, vec![3]); } - pub fn factored_cata_folds_each_child_immediately(zipper: Z) + pub fn factored_cata_folds_each_child_immediately(zipper: Z) where - Z: CatamorphismCachedWithEngine, + Z: crate::morphisms::$cata_trait, { use std::cell::RefCell; let events = RefCell::new(Vec::new()); - let result = CatamorphismCachedWithEngine:: + let result = $cata_trait:: ::factored_cata_jumping::, u64, Infallible, _, _, _, false>( &zipper, |_mask| { @@ -2320,11 +2068,11 @@ pub(crate) mod cached_catamorphism_tests { assert_eq!(events.into_inner(), ["new", "summarize 0", "fold 0", "summarize 1", "fold 1", "summarize root"]); } - pub fn factored_cata_passthrough_root(zipper: Z) + pub fn factored_cata_passthrough_root(zipper: Z) where - Z: CatamorphismCachedWithEngine, + Z: crate::morphisms::$cata_trait, { - let result = CatamorphismCachedWithEngine:: + let result = $cata_trait:: ::factored_cata_jumping::<(), Vec, Infallible, _, _, _, true>( &zipper, |_| panic!("a unary valueless root must not create an accumulator"), @@ -2340,65 +2088,59 @@ pub(crate) mod cached_catamorphism_tests { } /// Internal helper that gives the zipper constructor the lifetime of the test store. - pub fn run_test<'a, Z, Store, Engine>( + pub fn run_test<'a, Z, Store>( store: &'a mut Store, make_z: impl Fn(&'a mut Store) -> Z, test: impl Fn(Z), ) where - Z: 'a + CatamorphismCached - + CatamorphismCachedWithEngine, + Z: 'a + crate::morphisms::$cata_trait, { test(make_z(store)); } - macro_rules! cached_catamorphism_case { - ($z_name:ident, $read_keys:expr, $make_z:expr, $engine:ty, $keys:ident, $test:ident) => { - paste::paste! { - #[test] - fn [<$z_name _ $test>]() { - let mut temp_store = ($read_keys)(crate::morphisms::cached_catamorphism_tests::$keys); - crate::morphisms::cached_catamorphism_tests::run_test::<_, _, $engine>( - &mut temp_store, - $make_z, - crate::morphisms::cached_catamorphism_tests::$test::<_, $engine>, - ); - } } }; } - pub(crate) use cached_catamorphism_case; - macro_rules! cached_catamorphism_default_case { - ($z_name:ident, $read_keys:expr, $make_z:expr, $engine:ty, $keys:ident, $test:ident) => { + define_cached_catamorphism_test_suite!(recursive, CatamorphismCached); + define_cached_catamorphism_test_suite!(iterative, CatamorphismCachedIterative); + + macro_rules! cached_catamorphism_case { + ($z_name:ident, $implementation:ident, $suite:ident, $read_keys:expr, $make_z:expr, $keys:ident, $test:ident) => { paste::paste! { #[test] - fn [<$z_name _ default_ $test>]() { + fn [<$z_name _ $implementation _ $test>]() { let mut temp_store = ($read_keys)(crate::morphisms::cached_catamorphism_tests::$keys); - crate::morphisms::cached_catamorphism_tests::run_test::<_, _, $engine>( + crate::morphisms::cached_catamorphism_tests::$suite::run_test( &mut temp_store, $make_z, - crate::morphisms::cached_catamorphism_tests::default_facade_tests::$test, + crate::morphisms::cached_catamorphism_tests::$suite::$test, ); } } }; } - pub(crate) use cached_catamorphism_default_case; + pub(crate) use cached_catamorphism_case; macro_rules! cached_catamorphism_tests { - ($z_name:ident, $read_keys:expr, $make_z:expr, $engine:ty) => { - $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_default_case!($z_name, $read_keys, $make_z, $engine, CACHED_CATA_TEST_KEYS, leaf_count_stepping); - $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $read_keys, $make_z, $engine, CACHED_CATA_TEST_KEYS, leaf_count_stepping); - $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $read_keys, $make_z, $engine, CACHED_CATA_TEST_KEYS, leaf_count_jumping); - $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $read_keys, $make_z, $engine, CACHED_CATA_TEST_KEYS, leaf_count_factored_jumping); - $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $read_keys, $make_z, $engine, CACHED_CATA_TEST_KEYS, leaf_count_factored_stepping); - $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $read_keys, $make_z, $engine, CACHED_CATA_TEST_KEYS, longest_path_jumping); - $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $read_keys, $make_z, $engine, CACHED_CATA_TEST_KEYS, longest_path_factored_jumping); - $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $read_keys, $make_z, $engine, CACHED_CATA_TEST_KEYS, branch_values_stepping); - $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $read_keys, $make_z, $engine, CACHED_CATA_FOLD_ORDER_KEYS, factored_cata_folds_each_child_immediately); - $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $read_keys, $make_z, $engine, CACHED_CATA_PASSTHROUGH_KEYS, factored_cata_passthrough_root); - $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $read_keys, $make_z, $engine, CACHED_CATA_TEST_KEYS, factored_cata_propagates_callback_errors); + ($z_name:ident, $read_keys:expr, $make_z:expr, CatamorphismCached) => { + $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_tests!($z_name, $read_keys, $make_z, recursive, recursive); + }; + ($z_name:ident, $read_keys:expr, $make_z:expr, CatamorphismCachedIterative) => { + $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_tests!($z_name, $read_keys, $make_z, iterative, iterative); + }; + ($z_name:ident, $read_keys:expr, $make_z:expr, $implementation:ident, $suite:ident) => { + $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $implementation, $suite, $read_keys, $make_z, CACHED_CATA_TEST_KEYS, leaf_count_stepping); + $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $implementation, $suite, $read_keys, $make_z, CACHED_CATA_TEST_KEYS, leaf_count_jumping); + $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $implementation, $suite, $read_keys, $make_z, CACHED_CATA_TEST_KEYS, leaf_count_factored_jumping); + $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $implementation, $suite, $read_keys, $make_z, CACHED_CATA_TEST_KEYS, leaf_count_factored_stepping); + $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $implementation, $suite, $read_keys, $make_z, CACHED_CATA_TEST_KEYS, longest_path_jumping); + $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $implementation, $suite, $read_keys, $make_z, CACHED_CATA_TEST_KEYS, longest_path_factored_jumping); + $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $implementation, $suite, $read_keys, $make_z, CACHED_CATA_TEST_KEYS, branch_values_stepping); + $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $implementation, $suite, $read_keys, $make_z, CACHED_CATA_FOLD_ORDER_KEYS, factored_cata_folds_each_child_immediately); + $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $implementation, $suite, $read_keys, $make_z, CACHED_CATA_PASSTHROUGH_KEYS, factored_cata_passthrough_root); + $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $implementation, $suite, $read_keys, $make_z, CACHED_CATA_TEST_KEYS, factored_cata_propagates_callback_errors); }; } pub(crate) use cached_catamorphism_tests; @@ -2414,42 +2156,42 @@ mod tests { trait TestRecursiveCata: Sized { fn recursive_cata_cached(&self, alg_f: AlgF) -> W where - Self: CatamorphismCachedWithEngine, + Self: CatamorphismCached, W: Clone, AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> W, { - CatamorphismCachedWithEngine::::cata_cached(self, alg_f) + CatamorphismCached::::cata_cached(self, alg_f) } fn recursive_cata_jumping_cached(&self, alg_f: AlgF) -> W where - Self: CatamorphismCachedWithEngine, + Self: CatamorphismCached, W: Clone, AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> W, { - CatamorphismCachedWithEngine::::cata_jumping_cached(self, alg_f) + CatamorphismCached::::cata_jumping_cached(self, alg_f) } fn recursive_factored_cata_jumping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where - Self: CatamorphismCachedWithEngine, + Self: CatamorphismCached, W: Clone, NewAccF: Copy + Fn(&ByteMask) -> Result, FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, { - CatamorphismCachedWithEngine::::factored_cata_jumping::(self, new_acc_f, fold_child_f, summarize_f) + CatamorphismCached::::factored_cata_jumping::(self, new_acc_f, fold_child_f, summarize_f) } fn recursive_factored_cata(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where - Self: CatamorphismCachedWithEngine, + Self: CatamorphismCached, W: Clone, NewAccF: Copy + Fn(&ByteMask) -> Result, FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option) -> Result, { - CatamorphismCachedWithEngine::::factored_cata(self, new_acc_f, fold_child_f, summarize_f) + CatamorphismCached::::factored_cata(self, new_acc_f, fold_child_f, summarize_f) } } @@ -2469,12 +2211,12 @@ mod tests { children.iter().sum::() + value.copied().unwrap_or(0) }; let recursive_zipper = map.read_zipper(); - let recursive = CatamorphismCachedWithEngine::::cata_cached( + let recursive = CatamorphismCached::::cata_cached( &recursive_zipper, alg, ); let iterative_zipper = map.read_zipper(); - let iterative = CatamorphismCachedWithEngine::::cata_cached( + let iterative = CatamorphismCachedIterative::::cata_cached( &iterative_zipper, alg, ); @@ -2525,7 +2267,7 @@ mod tests { fn check_pure_catas<'a, W, V: Clone + Send + Sync, Z, AlgFP, Assert>( zipper: Z, f_pure: AlgFP, mut assert: Assert) where - Z: Clone + CatamorphismCached + CatamorphismCachedWithEngine, W: Clone, + Z: Clone + CatamorphismCached, W: Clone, AlgFP: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> W, Assert: FnMut(W, &str), { @@ -2540,7 +2282,7 @@ mod tests { fn check_all_catas<'a, W, V: Clone + Send + Sync, Z, AlgF, Assert>( zipper: Z, alg_f: AlgF, mut assert: Assert) where - Z: Clone + CatamorphismSideEffecting + CatamorphismCached + CatamorphismCachedWithEngine, W: Clone, + Z: Clone + CatamorphismSideEffecting + CatamorphismCached, W: Clone, AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> W, Assert: FnMut(W, &str), { @@ -2599,7 +2341,7 @@ mod tests { (val.is_some(), sum) }; let zipper = map.read_zipper(); - let output = CatamorphismCachedWithEngine::<(), GlobalAlloc, RecursiveCata>::cata_cached(&zipper, pure_alg_stepping); + let output = CatamorphismCached::<(), GlobalAlloc>::cata_cached(&zipper, pure_alg_stepping); assert_eq!(output.1, expected_sum); //The pure jumping cata is a variant on the above, but we also need to care about diff --git a/src/zipper.rs b/src/zipper.rs index 9faf31bb..cdf6838c 100644 --- a/src/zipper.rs +++ b/src/zipper.rs @@ -980,12 +980,6 @@ impl<'trie, V: Clone + Send + Sync + Unpin + 'trie, A: Allocator + 'trie> Zipper impl<'trie, V: Clone + Send + Sync + Unpin + 'trie, A: Allocator + 'trie> ZipperReadOnlyConditionalIteration<'trie, V> for ReadZipperTracked<'trie, '_, V, A> { } impl<'trie, V: Clone + Send + Sync + Unpin + 'trie, A: Allocator + 'trie> ZipperAbsolutePath for ReadZipperTracked<'trie, '_, V, A> { zipper_impl_lens!(ZipperAbsolutePath self => self.z); } -crate::morphisms::impl_catamorphism_cached!( - crate::morphisms::RecursiveCata; - impl<'trie, 'path, V, A> for ReadZipperTracked<'trie, 'path, V, A> as V, A - where [V: Clone + Send + Sync + Unpin + 'trie, A: Allocator + 'trie]; -); - impl ZipperForking for ReadZipperTracked<'_, '_, V, A>{ type ReadZipperT<'a> = ReadZipperUntracked<'a, 'a, V, A> where Self: 'a; @@ -1073,12 +1067,6 @@ impl ZipperInfallibleSubtries ZipperMoving for ReadZipperUntracked<'trie, '_, V, A> { zipper_impl_lens!(ZipperMoving self => self.z); } impl ZipperConcrete for ReadZipperUntracked<'_, '_, V, A> { zipper_impl_lens!(ZipperConcrete self => self.z); } -crate::morphisms::impl_catamorphism_cached!( - crate::morphisms::RecursiveCata; - impl<'trie, 'path, V, A> for ReadZipperUntracked<'trie, 'path, V, A> as V, A - where [V: Clone + Send + Sync + Unpin + 'trie, A: Allocator + 'trie]; -); - impl ZipperForking for ReadZipperUntracked<'_, '_, V, A> { type ReadZipperT<'a> = ReadZipperUntracked<'a, 'a, V, A> where Self: 'a; fn fork_read_zipper<'a>(&'a self) -> Self::ReadZipperT<'a> { @@ -1241,12 +1229,6 @@ impl<'trie, V: Clone + Send + Sync + Unpin + 'trie, A: Allocator + 'trie> Zipper impl<'trie, V: Clone + Send + Sync + Unpin + 'trie, A: Allocator + 'trie> ZipperReadOnlyConditionalIteration<'trie, V> for ReadZipperOwned { } impl<'trie, V: Clone + Send + Sync + Unpin + 'trie, A: Allocator + 'trie> ZipperAbsolutePath for ReadZipperOwned { zipper_impl_lens!(ZipperAbsolutePath self => self.z); } -crate::morphisms::impl_catamorphism_cached!( - crate::morphisms::RecursiveCata; - impl for ReadZipperOwned as V, A - where [V: Clone + Send + Sync + Unpin + 'static, A: Allocator + 'static]; -); - impl ZipperValues for ReadZipperOwned { fn val(&self) -> Option<&V> { unsafe{ self.z.get_val() } } @@ -4493,7 +4475,14 @@ mod tests { read_zipper, |keys: &[&[u8]]| keys.iter().enumerate().map(|(idx, path)| (*path, idx as u64)).collect::>(), |map: &mut PathMap| map.read_zipper(), - crate::morphisms::IterativeCata + CatamorphismCached + ); + + crate::morphisms::cached_catamorphism_tests::cached_catamorphism_tests!( + read_zipper, + |keys: &[&[u8]]| keys.iter().enumerate().map(|(idx, path)| (*path, idx as u64)).collect::>(), + |map: &mut PathMap| map.read_zipper(), + CatamorphismCachedIterative ); super::zipper_moving_tests::zipper_moving_tests!(read_zipper, From 0eb4535538063dcc8a37a07a1bf11927531f0899 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Tue, 1 Sep 2026 20:16:58 -0600 Subject: [PATCH 33/50] Adding tests and a harness to compare recursive vs iterative catamorphism implementations --- src/morphisms.rs | 197 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) diff --git a/src/morphisms.rs b/src/morphisms.rs index f59f5ba8..81dc3085 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -1838,6 +1838,10 @@ pub(crate) mod cached_catamorphism_tests { use core::convert::Infallible; use crate::alloc::GlobalAlloc; + use crate::utils::{ByteMask, ByteMaskIter}; + use crate::write_zipper::{WriteZipperOwned, ZipperWriting}; + use crate::zipper::{ZipperMoving, ZipperPath}; + use crate::PathMap; pub const CACHED_CATA_TEST_KEYS: &[&[u8]] = &[ b"arrow", b"bow", b"cannon", b"roman", b"romane", b"romanus", b"romulus", @@ -1846,6 +1850,199 @@ pub(crate) mod cached_catamorphism_tests { pub const CACHED_CATA_FOLD_ORDER_KEYS: &[&[u8]] = &[b"a", b"b"]; pub const CACHED_CATA_PASSTHROUGH_KEYS: &[&[u8]] = &[b"abc"]; + /// The branch bytes a node owes its `fold_child` calls. + /// + /// The contract is one fold per bit, in ascending mask-bit order. The reconstruction + /// probe uses this to make an invalid callback sequence fail at the first bad fold. + pub(crate) struct BranchBytes(ByteMaskIter); + + impl BranchBytes { + fn of(child_mask: &ByteMask) -> Self { + Self(child_mask.iter()) + } + + fn take(&mut self) -> u8 { + self.0.next().expect( + "cached-cata contract violation: more fold_child calls than child-mask bits", + ) + } + + fn finish(mut self) { + assert!( + self.0.next().is_none(), + "cached-cata contract violation: fewer fold_child calls than child-mask bits", + ); + } + } + + /// Accumulator for the cached-cata reconstruction probe. + /// + /// Every branch owns one write zipper. Child maps are grafted one mask-selected byte below + /// that zipper; `recon_summarize` then places the prefix and optional value around them. + pub(crate) struct ReconAcc { + branch_bytes: BranchBytes, + wz: WriteZipperOwned<()>, + } + + pub(crate) fn recon_start(child_mask: &ByteMask) -> Result { + Ok(ReconAcc { + branch_bytes: BranchBytes::of(child_mask), + wz: PathMap::new().into_write_zipper(b""), + }) + } + + pub(crate) fn recon_fold( + _child_mask: &ByteMask, + child: PathMap<()>, + accumulator: &mut ReconAcc, + ) -> Result<(), Infallible> { + let branch_byte = accumulator.branch_bytes.take(); + accumulator.wz.descend_to_byte(branch_byte); + accumulator.wz.graft_map(child); + accumulator.wz.ascend_byte(); + Ok(()) + } + + pub(crate) fn recon_summarize( + _child_mask: &ByteMask, + value: Option<&()>, + children: Option, + prefix: &[u8], + ) -> Result, Infallible> { + match children { + Some(ReconAcc { branch_bytes, mut wz }) => { + branch_bytes.finish(); + if !prefix.is_empty() { + wz.insert_prefix(prefix); + } + if value.is_some() { + wz.descend_to(prefix); + wz.set_val(()); + } + Ok(wz.into_map()) + } + None => { + let mut map = PathMap::new(); + if value.is_some() { + map.set_val_at(prefix, ()); + } + Ok(map) + } + } + } + + /// Reconstructs a logical trie through a named cached-cata implementation. + /// + /// This is intentionally a macro because the two engines are traits with the same methods, + /// rather than values of a common engine type. + macro_rules! reconstruct_trie { + ($engine:ident, $subject:expr) => {{ + <_ as $crate::morphisms::$engine<(), $crate::alloc::GlobalAlloc>> + ::factored_cata_jumping::<_, _, core::convert::Infallible, _, _, _, true>( + $subject, + $crate::morphisms::cached_catamorphism_tests::recon_start, + $crate::morphisms::cached_catamorphism_tests::recon_fold, + $crate::morphisms::cached_catamorphism_tests::recon_summarize, + ) + .unwrap() + }}; + } + #[allow(unused_imports)] // Exported for conformance tests in other modules. + pub(crate) use reconstruct_trie; + + /// Compares value paths without materializing either map's complete path list. + #[track_caller] + pub(crate) fn assert_same_paths(got: &PathMap<()>, expected: &PathMap<()>, who: &str) { + let mut got = got.iter(); + let mut expected = expected.iter(); + loop { + match (got.next(), expected.next()) { + (None, None) => break, + (got, expected) => assert_eq!( + got.map(|(path, _)| path), + expected.map(|(path, _)| path), + "{who} diverged", + ), + } + } + } + + /// Runs both cached-cata engines through the reconstruction probe and checks each result + /// against the expected logical trie as well as against one another. + /// + /// `subject` may be a map or a zipper. For a focused zipper, pass the map representing the + /// focused subtrie using the zipper's established origin-path convention as `expected`. + #[track_caller] + pub(crate) fn assert_reconstructs_like(subject: &Z, expected: &PathMap<()>) + where + Z: crate::morphisms::CatamorphismCached<(), GlobalAlloc> + + crate::morphisms::CatamorphismCachedIterative<(), GlobalAlloc>, + { + let iterative = reconstruct_trie!(CatamorphismCachedIterative, subject); + assert_same_paths(&iterative, expected, "iterative reconstruction"); + + let recursive = reconstruct_trie!(CatamorphismCached, subject); + assert_same_paths(&recursive, expected, "recursive reconstruction"); + assert_same_paths(&recursive, &iterative, "recursive and iterative reconstruction"); + } + + fn map_from_keys(keys: &[&[u8]]) -> PathMap<()> { + keys.iter().copied().map(|path| (path, ())).collect() + } + + #[test] + fn reconstruction_harness_sanity() { + let map = map_from_keys(&[b"a1", b"a2", b"b"]); + + assert_reconstructs_like(&map, &map); + } + + /// A value and a child with different first bytes must be folded in ascending byte order. + /// + /// The keys are deliberately chosen only through the public map API; their physical node + /// representation is not part of this test's setup or assertion. + #[test] + fn recursive_cata_regression_val_child_fold_order() { + let map = map_from_keys(&[b"a", b"b1", b"b2"]); + + assert_reconstructs_like(&map, &map); + } + + /// A short value sharing its first byte with a longer value must stay at its exact path. + #[test] + fn recursive_cata_regression_shared_byte_value_position() { + let map = map_from_keys(&[b"a", b"abc"]); + + assert_reconstructs_like(&map, &map); + } + + /// A value passed through a non-branching run must not cause its downstream child to be + /// folded through an empty mask. + #[test] + fn recursive_cata_regression_passed_value_child_mask() { + let map = map_from_keys(&[b"x", b"xy", b"xyz", b"xa1", b"xa2", b"q"]); + + assert_reconstructs_like(&map, &map); + } + + /// A focus within a compressed path must include every value below it, including any value + /// held at the focus. The iterative engine preserves the zipper origin in its reconstructed + /// paths, so each source map contains only the focused subtrie and is also its expected map. + #[test] + fn recursive_cata_regression_mid_node_focus() { + let map = map_from_keys(&[b"abc1", b"abc2"]); + let mut zipper = map.read_zipper(); + zipper.descend_to(b"ab"); + assert_eq!(zipper.path(), b"ab"); + assert_reconstructs_like(&zipper, &map); + + let map = map_from_keys(&[b"ab", b"abcd"]); + let mut zipper = map.read_zipper(); + zipper.descend_to(b"ab"); + assert_eq!(zipper.path(), b"ab"); + assert_reconstructs_like(&zipper, &map); + } + macro_rules! define_cached_catamorphism_test_suite { ($suite_name:ident, $cata_trait:ident) => { pub(crate) mod $suite_name { From 6dea77e77f24d957e71a394ce431981c6767047c Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Tue, 1 Sep 2026 20:36:59 -0600 Subject: [PATCH 34/50] Another more comprehensive recursive cata test --- src/morphisms.rs | 101 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 87 insertions(+), 14 deletions(-) diff --git a/src/morphisms.rs b/src/morphisms.rs index 81dc3085..c3414135 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -1840,7 +1840,7 @@ pub(crate) mod cached_catamorphism_tests { use crate::alloc::GlobalAlloc; use crate::utils::{ByteMask, ByteMaskIter}; use crate::write_zipper::{WriteZipperOwned, ZipperWriting}; - use crate::zipper::{ZipperMoving, ZipperPath}; + use crate::zipper::{Zipper, ZipperIteration, ZipperMoving, ZipperPath}; use crate::PathMap; pub const CACHED_CATA_TEST_KEYS: &[&[u8]] = &[ @@ -1952,18 +1952,26 @@ pub(crate) mod cached_catamorphism_tests { /// Compares value paths without materializing either map's complete path list. #[track_caller] - pub(crate) fn assert_same_paths(got: &PathMap<()>, expected: &PathMap<()>, who: &str) { - let mut got = got.iter(); - let mut expected = expected.iter(); + pub(crate) fn assert_same_paths( + got: &PathMap<()>, + expected: &PathMap<()> + ) { + let mut got = got.read_zipper(); + let mut expected = expected.read_zipper(); + + assert_eq!(got.is_val(), expected.is_val()); + if got.is_val() { + assert_eq!(got.path(), expected.path()); + } + loop { - match (got.next(), expected.next()) { - (None, None) => break, - (got, expected) => assert_eq!( - got.map(|(path, _)| path), - expected.map(|(path, _)| path), - "{who} diverged", - ), + let got_has_value = got.to_next_val(); + let expected_has_value = expected.to_next_val(); + assert_eq!(got_has_value, expected_has_value, ); + if !got_has_value { + break; } + assert_eq!(got.path(), expected.path()); } } @@ -1979,17 +1987,35 @@ pub(crate) mod cached_catamorphism_tests { + crate::morphisms::CatamorphismCachedIterative<(), GlobalAlloc>, { let iterative = reconstruct_trie!(CatamorphismCachedIterative, subject); - assert_same_paths(&iterative, expected, "iterative reconstruction"); + assert_same_paths(&iterative, expected); let recursive = reconstruct_trie!(CatamorphismCached, subject); - assert_same_paths(&recursive, expected, "recursive reconstruction"); - assert_same_paths(&recursive, &iterative, "recursive and iterative reconstruction"); + assert_same_paths(&recursive, expected); + assert_same_paths(&recursive, &iterative); } fn map_from_keys(keys: &[&[u8]]) -> PathMap<()> { keys.iter().copied().map(|path| (path, ())).collect() } + fn map_from_owned_keys(keys: &[Vec]) -> PathMap<()> { + keys.iter().map(|path| (path.as_slice(), ())).collect() + } + + /// Checks a logical key set in both insertion orders. This deliberately does not inspect + /// the resulting storage layout: a valid cached cata must honor the same observable contract + /// regardless of how the implementation stores these keys. + #[track_caller] + fn assert_logical_key_case_roundtrips(keys: &[Vec]) { + let map = map_from_owned_keys(keys); + assert_reconstructs_like(&map, &map); + + let mut reversed = keys.to_vec(); + reversed.reverse(); + let map = map_from_owned_keys(&reversed); + assert_reconstructs_like(&map, &map); + } + #[test] fn reconstruction_harness_sanity() { let map = map_from_keys(&[b"a1", b"a2", b"b"]); @@ -2043,6 +2069,53 @@ pub(crate) mod cached_catamorphism_tests { assert_reconstructs_like(&zipper, &map); } + /// Systematically exercises logical value/branch/prefix combinations that may be represented + /// compactly in different ways. The corpus speaks only in paths and values; it neither + /// assumes nor inspects a particular concrete node representation. + #[test] + fn recursive_cata_logical_prefix_and_branch_corpus() { + let mut cases: Vec>> = vec![ + vec![], + vec![b"".to_vec()], + vec![b"abcd".to_vec()], + vec![b"a".to_vec(), b"ab".to_vec(), b"abc".to_vec()], + vec![b"a".to_vec(), b"b".to_vec()], + vec![b"a1".to_vec(), b"a2".to_vec(), b"b1".to_vec(), b"b2".to_vec()], + vec![b"a".to_vec(), b"ab1".to_vec(), b"ab2".to_vec()], + vec![b"".to_vec(), b"q1".to_vec(), b"q2".to_vec()], + ]; + + for tail_len in 1..=4 { + let tail = &b"cdef"[..tail_len]; + + let mut shared_value_tail = b"a".to_vec(); + shared_value_tail.extend_from_slice(tail); + cases.push(vec![b"a".to_vec(), shared_value_tail]); + + let mut high_child_1 = b"b1".to_vec(); + high_child_1.extend_from_slice(tail); + let mut high_child_2 = b"b2".to_vec(); + high_child_2.extend_from_slice(tail); + cases.push(vec![b"a".to_vec(), high_child_1, high_child_2]); + + let mut low_child_1 = b"a1".to_vec(); + low_child_1.extend_from_slice(tail); + let mut low_child_2 = b"a2".to_vec(); + low_child_2.extend_from_slice(tail); + cases.push(vec![low_child_1, low_child_2, b"b".to_vec()]); + + let mut same_byte_child_1 = b"ab1".to_vec(); + same_byte_child_1.extend_from_slice(tail); + let mut same_byte_child_2 = b"ab2".to_vec(); + same_byte_child_2.extend_from_slice(tail); + cases.push(vec![b"a".to_vec(), same_byte_child_1, same_byte_child_2]); + } + + for keys in cases { + assert_logical_key_case_roundtrips(&keys); + } + } + macro_rules! define_cached_catamorphism_test_suite { ($suite_name:ident, $cata_trait:ident) => { pub(crate) mod $suite_name { From a122a883944096c022a07ffd941012e8c387afbf Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Tue, 1 Sep 2026 20:54:22 -0600 Subject: [PATCH 35/50] More recusrive cata testing - fixing all_dense_nodes failure with smoke test --- src/morphisms.rs | 133 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 130 insertions(+), 3 deletions(-) diff --git a/src/morphisms.rs b/src/morphisms.rs index c3414135..f637266f 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -2016,6 +2016,61 @@ pub(crate) mod cached_catamorphism_tests { assert_reconstructs_like(&map, &map); } + /// Finds prefixes where the logical input keys have at least two different following bytes. + /// These are derived from the test data alone, not from the trie's concrete representation. + fn logical_branch_prefixes(keys: &[Vec]) -> Vec> { + let mut prefixes = Vec::new(); + for key in keys { + for len in 0..key.len() { + let prefix = &key[..len]; + let first_child = keys.iter() + .filter(|candidate| candidate.starts_with(prefix)) + .filter_map(|candidate| candidate.get(len)) + .next(); + let is_branch = first_child.is_some_and(|first| { + keys.iter() + .filter(|candidate| candidate.starts_with(prefix)) + .filter_map(|candidate| candidate.get(len)) + .any(|child| child != first) + }); + if is_branch && !prefixes.iter().any(|existing| existing == prefix) { + prefixes.push(prefix.to_vec()); + } + } + } + prefixes + } + + /// Runs the focused-zipper portion of the differential harness. Both the requested focus + /// and the expected subtrie come from the logical source keys, keeping this test independent + /// of the storage representation. + fn assert_logical_focus_roundtrips(keys: &[Vec], focus: &[u8]) { + let map = map_from_owned_keys(keys); + let expected_keys: Vec> = keys.iter() + .filter(|key| key.starts_with(focus)) + .cloned() + .collect(); + let expected = map_from_owned_keys(&expected_keys); + + let mut zipper = map.read_zipper(); + zipper.descend_to(focus); + assert_eq!(zipper.path(), focus); + assert_reconstructs_like(&zipper, &expected); + } + + fn assert_random_case( + seed: &[u8; 32], + round: usize, + focus: Option<&[u8]>, + test: impl FnOnce(), + ) { + if std::panic::catch_unwind(std::panic::AssertUnwindSafe(test)).is_err() { + panic!( + "random cached-cata regression: seed={seed:?}, round={round}, focus={focus:?}", + ); + } + } + #[test] fn reconstruction_harness_sanity() { let map = map_from_keys(&[b"a1", b"a2", b"b"]); @@ -2116,6 +2171,73 @@ pub(crate) mod cached_catamorphism_tests { } } + /// A 256-way logical branch exercises storage configurations that use wide byte-indexed + /// nodes without making the test depend on any particular node representation. + #[test] + fn recursive_cata_wide_logical_branch() { + let keys: Vec> = (0u8..=u8::MAX) + .map(|byte| vec![byte, b'a', byte]) + .collect(); + + assert_logical_key_case_roundtrips(&keys); + for byte in [0, 63, 64, 127, 128, 191, 192, 255] { + assert_logical_focus_roundtrips(&keys, &[byte]); + } + } + + /// Differential coverage over seeded logical maps and root/value/branch/proper-prefix + /// focuses. The seed is fixed so any failure is reproducible without observing concrete + /// node layout. + #[test] + fn recursive_cata_randomized_maps_and_foci() { + use rand::{Rng, SeedableRng}; + use rand::rngs::StdRng; + + const ROUNDS: usize = 64; + const KEYS_PER_ROUND: usize = 48; + const FOCI_PER_ROUND: usize = 8; + + const SEED: [u8; 32] = [31; 32]; + + let mut rng = StdRng::from_seed(SEED); + for round in 0..ROUNDS { + let keys: Vec> = (0..KEYS_PER_ROUND) + .map(|_| { + let len = rng.random_range(0..=6); + (0..len) + .map(|_| b'a' + rng.random_range(0..3)) + .collect() + }) + .collect(); + let branch_prefixes = logical_branch_prefixes(&keys); + + assert_random_case(&SEED, round, None, || { + assert_logical_key_case_roundtrips(&keys); + }); + assert_random_case(&SEED, round, Some(b""), || { + assert_logical_focus_roundtrips(&keys, b""); + }); + + for focus_idx in 0..FOCI_PER_ROUND { + let key = &keys[rng.random_range(0..keys.len())]; + let focus = match focus_idx % 3 { + // An exact value path. + 0 => key.clone(), + // A proper prefix, which may fall inside a compressed run. + 1 if !key.is_empty() => { + key[..rng.random_range(0..key.len())].to_vec() + } + // A logical branch prefix, or root if the generated map has none. + _ if branch_prefixes.is_empty() => Vec::new(), + _ => branch_prefixes[rng.random_range(0..branch_prefixes.len())].clone(), + }; + assert_random_case(&SEED, round, Some(&focus), || { + assert_logical_focus_roundtrips(&keys, &focus); + }); + } + } + } + macro_rules! define_cached_catamorphism_test_suite { ($suite_name:ident, $cata_trait:ident) => { pub(crate) mod $suite_name { @@ -3649,12 +3771,17 @@ mod tests { assert_eq!(result.unwrap(), b"abc"); } - /// Finds the path_depth at which the recursive cata hits a stack overflow + /// A bounded deep-path smoke test for the recursive cata. /// - /// Empirically seems to be somewhere between 8 and 10 KBytes. But more branching, and thus fewer - /// bytes-per-node, will mean it will fail on shorter paths. + /// The implementation uses the Rust call stack once per physical node. `all_dense_nodes` + /// creates more physical nodes for a path, so it needs a smaller safe bound; intentionally + /// testing beyond that bound can abort the whole test process instead of reporting a normal + /// assertion failure. #[test] fn recursive_cata_deep_path_smoke() { + #[cfg(feature = "all_dense_nodes")] + const PATH_LEN: usize = 256; + #[cfg(not(feature = "all_dense_nodes"))] const PATH_LEN: usize = 8_000; let mut map = PathMap::<()>::new(); From ff7c4a44afb758f449b506afa7e62696ebe3540f Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Tue, 1 Sep 2026 21:05:54 -0600 Subject: [PATCH 36/50] Fixing fold order in one pair node case --- src/line_list_node.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/line_list_node.rs b/src/line_list_node.rs index 95dba670..3b5f2af5 100644 --- a/src/line_list_node.rs +++ b/src/line_list_node.rs @@ -2928,16 +2928,17 @@ impl LineListNode { summarize!(passed_in_val, Some(child_w), key0) } else { //Case 10 (Val, Child), different key bytes - let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(child_node, None, start_f, fold_child_f, finalize_f, cache)?; - let path = &key1[1..]; let mask = ByteMask::from((key0_byte, key1_byte)); let mut acc = start_f(&mask)?; - fold_child_f(&mask, summarize!(None, Some(child_w), path)?, &mut acc)?; let val = unsafe { self.val_in_slot::<0>() }; let path = &key0[1..]; fold_child_f(&mask, summarize!(Some(val), None, path)?, &mut acc)?; + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(child_node, None, start_f, fold_child_f, finalize_f, cache)?; + let path = &key1[1..]; + fold_child_f(&mask, summarize!(None, Some(child_w), path)?, &mut acc)?; + finalize_f(&mask, passed_in_val, Some(acc), &[]) } }, From ac0f07bd5fd0a210e37d3dd064ead1d9637f89a8 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Tue, 1 Sep 2026 21:20:23 -0600 Subject: [PATCH 37/50] Fixing another listnode recursive cata case --- src/line_list_node.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/line_list_node.rs b/src/line_list_node.rs index 3b5f2af5..a11f1e5b 100644 --- a/src/line_list_node.rs +++ b/src/line_list_node.rs @@ -2894,9 +2894,13 @@ impl LineListNode { debug_assert_eq!(key0.len(), 1); debug_assert!(key1.len() > 1); let val = unsafe { self.val_in_slot::<1>() }; - let w = summarize!(Some(val), None, &[])?; + let child_w = summarize!(Some(val), None, &key1[2..])?; + let mask = ByteMask::from(key1[1]); + let mut acc = start_f(&mask)?; + fold_child_f(&mask, child_w, &mut acc)?; + let val = unsafe { self.val_in_slot::<0>() }; - let w = summarize!(Some(val), Some(w), &key1[1..])?; + let w = finalize_f(&mask, Some(val), Some(acc), &[])?; summarize!(passed_in_val, Some(w), &key1[0..1]) } else { //Case 8 (Val, Val), different first bytes From f068141ece6bb8a3f38845129f7373c33d4bee92 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Tue, 1 Sep 2026 21:34:03 -0600 Subject: [PATCH 38/50] Fixing another case to the guts of the pairnode recursive cata table of behaviors --- src/line_list_node.rs | 10 +++++++++- src/morphisms.rs | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/line_list_node.rs b/src/line_list_node.rs index a11f1e5b..0383d7f8 100644 --- a/src/line_list_node.rs +++ b/src/line_list_node.rs @@ -2882,7 +2882,15 @@ impl LineListNode { 8 | 9 => { let val = unsafe { self.val_in_slot::<0>() }; let path = unsafe{ self.key_unchecked::<0>() }; - summarize!(passed_in_val, Some(summarize!(Some(val), None, path)?), &[]) + if passed_in_val.is_none() { + summarize!(None, Some(summarize!(Some(val), None, path)?), &[]) + } else { + let child_w = summarize!(Some(val), None, &path[1..])?; + let mask = ByteMask::from(path[0]); + let mut acc = start_f(&mask)?; + fold_child_f(&mask, child_w, &mut acc)?; + finalize_f(&mask, passed_in_val, Some(acc), &[]) + } }, //(Val, Val) = (1 << 3) + (1 << 2) 12 => { diff --git a/src/morphisms.rs b/src/morphisms.rs index f637266f..aca9076b 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -3780,7 +3780,7 @@ mod tests { #[test] fn recursive_cata_deep_path_smoke() { #[cfg(feature = "all_dense_nodes")] - const PATH_LEN: usize = 256; + const PATH_LEN: usize = 200; #[cfg(not(feature = "all_dense_nodes"))] const PATH_LEN: usize = 8_000; From aea18135fd50e604009f90ea95653560ca3569c0 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Tue, 1 Sep 2026 23:15:05 -0600 Subject: [PATCH 39/50] Partial fix for one of the failures when zipper focus starts in the middle of a node. But there is a deeper question about whether the focus should be respected in cata. IMO it should now that we don't have `into` semantics --- src/morphisms.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/morphisms.rs b/src/morphisms.rs index aca9076b..d56c2e67 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -674,14 +674,16 @@ impl<'a, Z, V: Clone + Send + Sync + 'a, A: Allocator> CatamorphismCached SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, { let focus = self.get_focus(); - let w = match focus.0.borrow() { - Some(node) => { - let mut cache = HashMap::new(); - recursive_cata_cached::<_, _, Acc, _, Err, _, _, _, COMPUTE_PATH>(node, self.val(), new_acc_f, fold_child_f, summarize_f, &mut cache) - }, - None => summarize_f(&ByteMask::EMPTY, None, None, &[]), + let mut cache = HashMap::new(); + let w = if let Some(node) = focus.0.borrow() { + recursive_cata_cached::<_, _, Acc, _, Err, _, _, _, COMPUTE_PATH>(node, self.val(), new_acc_f, fold_child_f, summarize_f, &mut cache)? + } else { + match focus.into_option() { + Some(node) => recursive_cata_cached::<_, _, Acc, _, Err, _, _, _, COMPUTE_PATH>(&node, self.val(), new_acc_f, fold_child_f, summarize_f, &mut cache)?, + None => return summarize_f(&ByteMask::EMPTY, None, None, &[]), + } }; - w + Ok(w) } } From 33ccd3737504066dff531192b37a5fa0ce97eda2 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Wed, 2 Sep 2026 00:01:01 -0600 Subject: [PATCH 40/50] Adding `path_assert_len` to ZipperPathBuffer trait Removing some unnecessary trait bounds from CatamorphismCachedIterative and CatamorphismCached --- pathmap-derive/src/lib.rs | 6 ++++++ src/arena_compact.rs | 4 ++++ src/dependent_zipper.rs | 1 + src/empty_zipper.rs | 4 ++++ src/experimental.rs | 4 ++++ src/morphisms.rs | 28 +++++++++++++++------------- src/path_tracker.rs | 4 ++++ src/prefix_zipper.rs | 4 ++++ src/product_zipper.rs | 2 ++ src/utils/debug/diff_zipper.rs | 6 ++++++ src/write_zipper.rs | 12 ++++++++++++ src/zipper.rs | 17 +++++++++++++++++ 12 files changed, 79 insertions(+), 13 deletions(-) diff --git a/pathmap-derive/src/lib.rs b/pathmap-derive/src/lib.rs index 5111c6d4..f3dc8cc0 100644 --- a/pathmap-derive/src/lib.rs +++ b/pathmap-derive/src/lib.rs @@ -653,6 +653,12 @@ fn derive_poly_zipper_with_traits( impl #impl_generics pathmap::zipper::ZipperPathBuffer for #enum_name #ty_generics #zipper_path_buffer_where { + unsafe fn path_assert_len(&self, len: usize) -> &[u8] { + match self { + #(#variant_arms => unsafe { inner.path_assert_len(len) },)* + } + } + unsafe fn origin_path_assert_len(&self, len: usize) -> &[u8] { match self { #(#variant_arms => unsafe { inner.origin_path_assert_len(len) },)* diff --git a/src/arena_compact.rs b/src/arena_compact.rs index ae8628ab..a9774fff 100644 --- a/src/arena_compact.rs +++ b/src/arena_compact.rs @@ -2518,6 +2518,10 @@ where Storage: AsRef<[u8]> impl<'tree, Storage, Value> ZipperPathBuffer for ACTZipper<'tree, Storage, Value> where Storage: AsRef<[u8]> { + unsafe fn path_assert_len(&self, len: usize) -> &[u8] { + assert!(len <= self.path.capacity() - self.origin_depth); + unsafe{ core::slice::from_raw_parts(self.path.as_ptr().add(self.origin_depth), len) } + } unsafe fn origin_path_assert_len(&self, len: usize) -> &[u8] { // Safety: we're not creating a slice larger than capacity assert!(self.path.capacity() >= len); diff --git a/src/dependent_zipper.rs b/src/dependent_zipper.rs index fe328c79..8786a470 100644 --- a/src/dependent_zipper.rs +++ b/src/dependent_zipper.rs @@ -219,6 +219,7 @@ impl<'trie, PrimaryZ, SecondaryZ, V, C, F : Clone + for <'a> FnOnce(C, &'a [u8], PrimaryZ: ZipperMoving + ZipperPath + ZipperPathBuffer, SecondaryZ: ZipperMoving + ZipperPathBuffer, { + unsafe fn path_assert_len(&self, len: usize) -> &[u8] { unsafe{ self.primary.path_assert_len(len) } } unsafe fn origin_path_assert_len(&self, len: usize) -> &[u8] { unsafe{ self.primary.origin_path_assert_len(len) } } fn prepare_buffers(&mut self) { self.primary.prepare_buffers() } fn reserve_buffers(&mut self, path_len: usize, stack_depth: usize) { self.primary.reserve_buffers(path_len, stack_depth) } diff --git a/src/empty_zipper.rs b/src/empty_zipper.rs index 16860fc1..b7635609 100644 --- a/src/empty_zipper.rs +++ b/src/empty_zipper.rs @@ -118,6 +118,10 @@ impl<'a, V: Clone + Send + Sync> ZipperReadOnlyConditionalIteration<'a, V> for E } impl ZipperPathBuffer for EmptyZipper { + unsafe fn path_assert_len(&self, len: usize) -> &[u8] { + assert!(len <= self.path.capacity() - self.path_start_idx); + unsafe{ core::slice::from_raw_parts(self.path.as_ptr().add(self.path_start_idx), len) } + } unsafe fn origin_path_assert_len(&self, len: usize) -> &[u8] { assert!(len <= self.path.capacity()); unsafe{ core::slice::from_raw_parts(self.path.as_ptr(), len) } diff --git a/src/experimental.rs b/src/experimental.rs index f39c2caa..b3269368 100644 --- a/src/experimental.rs +++ b/src/experimental.rs @@ -31,6 +31,10 @@ impl Zipper for FullZipper { } impl ZipperPathBuffer for FullZipper { + unsafe fn path_assert_len(&self, len: usize) -> &[u8] { + assert!(len <= self.path.capacity()); + unsafe{ core::slice::from_raw_parts(self.path.as_ptr(), len) } + } unsafe fn origin_path_assert_len(&self, len: usize) -> &[u8] { assert!(len <= self.path.capacity()); unsafe{ core::slice::from_raw_parts(self.path.as_ptr(), len) } diff --git a/src/morphisms.rs b/src/morphisms.rs index d56c2e67..e2c33cd7 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -665,7 +665,7 @@ impl Catamorph } } -impl<'a, Z, V: Clone + Send + Sync + 'a, A: Allocator> CatamorphismCached for Z where Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer + ZipperInfallibleSubtries { +impl<'a, Z, V: Clone + Send + Sync + 'a, A: Allocator> CatamorphismCached for Z where Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperInfallibleSubtries { fn factored_cata_jumping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where W: Clone, @@ -687,7 +687,7 @@ impl<'a, Z, V: Clone + Send + Sync + 'a, A: Allocator> CatamorphismCached } } -impl<'a, Z, V: 'a, A: Allocator> CatamorphismCachedIterative for Z where Z: Clone + Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer { +impl<'a, Z, V: 'a, A: Allocator> CatamorphismCachedIterative for Z where Z: Clone + Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperPathBuffer { fn factored_cata_jumping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where W: Clone, @@ -1116,7 +1116,7 @@ fn summarize_ascend_to_fork<'a, Z, V: 'a, Acc, W, E, NewAccF, FoldChildF, Summar summarize_f: SummarizeF, ) -> Result where - Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperAbsolutePath + ZipperPathBuffer, + Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperPathBuffer, NewAccF: Copy + Fn(&ByteMask) -> Result, FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), E>, SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, @@ -1125,19 +1125,23 @@ where let mut child_mask = ByteMask::from(zipper.child_mask()); loop { - let old_path_len = zipper.origin_path().len(); + let old_depth = zipper.depth(); let old_value = zipper.get_val_with_witness(&witness); let ascended = zipper.ascend_until(); debug_assert!(ascended > 0); + let depth = zipper.depth(); + debug_assert_eq!(old_depth - depth, ascended); - let origin_path = unsafe { zipper.origin_path_assert_len(old_path_len) }; + // SAFETY: `ascend_until` only shortens the logical path; the bytes it removed remain + // initialized in the zipper's prepared path buffer until the next zipper movement. + let path = unsafe { zipper.path_assert_len(old_depth) }; let jump_len = if zipper.child_count() != 1 || zipper.is_val() { - old_path_len - (zipper.origin_path().len() + 1) + ascended - 1 } else { - old_path_len - zipper.origin_path().len() + ascended }; let prefix = if COMPUTE_PATH { - &origin_path[origin_path.len() - jump_len..] + &path[old_depth - jump_len..] } else { &[] }; @@ -1148,10 +1152,8 @@ where return Ok(w) } - // SAFETY: The path buffer still contains the path we just ascended through. - let byte = *unsafe { zipper.origin_path_assert_len(old_path_len - jump_len) } - .last() - .unwrap(); + debug_assert!(old_depth > jump_len); + let byte = path[old_depth - jump_len - 1]; child_mask = ByteMask::from(byte); let mut next_accumulator = new_acc_f(&child_mask)?; fold_child_f(&child_mask, w, &mut next_accumulator)?; @@ -1171,7 +1173,7 @@ fn summarize_cached_body<'a, Z, V: 'a, Acc, W, E, NewAccF, FoldChildF, Summarize ) -> Result where W: Clone, - Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer, + Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperPathBuffer, NewAccF: Copy + Fn(&ByteMask) -> Result, FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), E>, SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, diff --git a/src/path_tracker.rs b/src/path_tracker.rs index e86ca669..357d65d4 100644 --- a/src/path_tracker.rs +++ b/src/path_tracker.rs @@ -206,6 +206,10 @@ impl<'a, Z: ZipperReadOnlyConditionalValues<'a, V>, V: Clone + Send + Sync> Zipp } impl ZipperPathBuffer for PathTracker { + unsafe fn path_assert_len(&self, len: usize) -> &[u8] { + assert!(len <= self.path.capacity() - self.origin_len); + unsafe { core::slice::from_raw_parts(self.path.as_ptr().add(self.origin_len), len) } + } unsafe fn origin_path_assert_len(&self, len: usize) -> &[u8] { assert!(len <= self.path.capacity()); let ptr = self.path.as_ptr(); diff --git a/src/prefix_zipper.rs b/src/prefix_zipper.rs index e9353594..93cf40c6 100644 --- a/src/prefix_zipper.rs +++ b/src/prefix_zipper.rs @@ -296,6 +296,10 @@ impl<'prefix, 'source, Z, V> ZipperReadOnlyConditionalValues<'source, V> impl<'prefix, Z> ZipperPathBuffer for PrefixZipper<'prefix, Z> where Z: ZipperMoving { + unsafe fn path_assert_len(&self, len: usize) -> &[u8] { + assert!(len <= self.path.capacity() - self.origin_depth); + unsafe{ core::slice::from_raw_parts(self.path.as_ptr().add(self.origin_depth), len) } + } unsafe fn origin_path_assert_len(&self, len: usize) -> &[u8] { assert!(self.path.capacity() >= len); unsafe{ core::slice::from_raw_parts(self.path.as_ptr(), len) } diff --git a/src/product_zipper.rs b/src/product_zipper.rs index 78974199..88e54bd4 100644 --- a/src/product_zipper.rs +++ b/src/product_zipper.rs @@ -364,6 +364,7 @@ impl ZipperConcrete for ProductZip } impl<'trie, V: Clone + Send + Sync + Unpin + 'trie, A: Allocator + 'trie> ZipperPathBuffer for ProductZipper<'_, 'trie, V, A> { + unsafe fn path_assert_len(&self, len: usize) -> &[u8] { unsafe{ self.z.path_assert_len(len) } } unsafe fn origin_path_assert_len(&self, len: usize) -> &[u8] { unsafe{ self.z.origin_path_assert_len(len) } } fn prepare_buffers(&mut self) { self.z.prepare_buffers() } fn reserve_buffers(&mut self, path_len: usize, stack_depth: usize) { self.z.reserve_buffers(path_len, stack_depth) } @@ -552,6 +553,7 @@ impl<'trie, PrimaryZ, SecondaryZ, V> ZipperPathBuffer PrimaryZ: ZipperMoving + ZipperPath + ZipperPathBuffer, SecondaryZ: ZipperMoving + ZipperPath + ZipperPathBuffer, { + unsafe fn path_assert_len(&self, len: usize) -> &[u8] { unsafe{ self.primary.path_assert_len(len) } } unsafe fn origin_path_assert_len(&self, len: usize) -> &[u8] { unsafe{ self.primary.origin_path_assert_len(len) } } fn prepare_buffers(&mut self) { self.primary.prepare_buffers() } fn reserve_buffers(&mut self, path_len: usize, stack_depth: usize) { self.primary.reserve_buffers(path_len, stack_depth) } diff --git a/src/utils/debug/diff_zipper.rs b/src/utils/debug/diff_zipper.rs index 9808df12..c0016f0f 100644 --- a/src/utils/debug/diff_zipper.rs +++ b/src/utils/debug/diff_zipper.rs @@ -230,6 +230,12 @@ impl ZipperAbsol impl ZipperPathBuffer for DiffZipper { + unsafe fn path_assert_len(&self, len: usize) -> &[u8] { + let a = unsafe{ self.a.path_assert_len(len) }; + let b = unsafe{ self.b.path_assert_len(len) }; + assert_eq!(a, b); + a + } unsafe fn origin_path_assert_len(&self, len: usize) -> &[u8] { let a = unsafe{ self.a.origin_path_assert_len(len) }; let b = unsafe{ self.b.origin_path_assert_len(len) }; diff --git a/src/write_zipper.rs b/src/write_zipper.rs index 1ca87c86..2509091f 100644 --- a/src/write_zipper.rs +++ b/src/write_zipper.rs @@ -444,6 +444,7 @@ impl<'a, 'path, V: Clone + Send + Sync + Unpin, A: Allocator + 'a> ZipperPath fo } impl<'a, 'path, V: Clone + Send + Sync + Unpin, A: Allocator + 'a> ZipperPathBuffer for WriteZipperTracked<'a, 'path, V, A> { + unsafe fn path_assert_len(&self, len: usize) -> &[u8] { unsafe{ self.z.path_assert_len(len) } } unsafe fn origin_path_assert_len(&self, len: usize) -> &[u8] { unsafe{ self.z.origin_path_assert_len(len) } } fn prepare_buffers(&mut self) { self.z.prepare_buffers() } fn reserve_buffers(&mut self, path_len: usize, stack_depth: usize) { self.z.reserve_buffers(path_len, stack_depth) } @@ -609,6 +610,7 @@ impl<'a, 'path, V: Clone + Send + Sync + Unpin, A: Allocator + 'a> ZipperPath fo } impl<'a, 'path, V: Clone + Send + Sync + Unpin, A: Allocator + 'a> ZipperPathBuffer for WriteZipperUntracked<'a, 'path, V, A> { + unsafe fn path_assert_len(&self, len: usize) -> &[u8] { unsafe{ self.z.path_assert_len(len) } } unsafe fn origin_path_assert_len(&self, len: usize) -> &[u8] { unsafe{ self.z.origin_path_assert_len(len) } } fn prepare_buffers(&mut self) { self.z.prepare_buffers() } fn reserve_buffers(&mut self, path_len: usize, stack_depth: usize) { self.z.reserve_buffers(path_len, stack_depth) } @@ -1132,6 +1134,16 @@ impl<'a, 'path, V: Clone + Send + Sync + Unpin, A: Allocator + 'a> ZipperAbsolut } impl<'a, 'path, V: Clone + Send + Sync + Unpin, A: Allocator + 'a> ZipperPathBuffer for WriteZipperCore<'a, 'path, V, A> { + unsafe fn path_assert_len(&self, len: usize) -> &[u8] { + let start = self.key.origin_path.len(); + if self.key.prefix_buf.capacity() > 0 { + assert!(len <= self.key.prefix_buf.capacity() - start); + unsafe{ core::slice::from_raw_parts(self.key.prefix_buf.as_ptr().add(start), len) } + } else { + assert_eq!(len, 0); + &[] + } + } unsafe fn origin_path_assert_len(&self, len: usize) -> &[u8] { if self.key.prefix_buf.capacity() > 0 { assert!(len <= self.key.prefix_buf.capacity()); diff --git a/src/zipper.rs b/src/zipper.rs index 15cdddd1..dd61d861 100644 --- a/src/zipper.rs +++ b/src/zipper.rs @@ -1147,6 +1147,12 @@ pub trait ZipperConcrete { /// Provides more direct control over a [ZipperMoving] zipper's path buffer pub trait ZipperPathBuffer: ZipperMoving { + /// Internal method to get the relative path buffer, beyond its current logical length. + /// + /// Panics if `len` exceeds that buffer's capacity. The returned bytes beyond [`ZipperPath::path`] + /// are only valid when the caller knows they were initialized by earlier zipper movement. + unsafe fn path_assert_len(&self, len: usize) -> &[u8]; + /// Internal method to get the path, beyond its length. Panics if `len` > the path's capacity, or /// if the zipper is relative and doesn't have an `origin_path` /// @@ -1278,6 +1284,7 @@ macro_rules! zipper_impl_lens { #[inline] fn is_shared(&$s) -> bool { $e.is_shared() } }; (ZipperPathBuffer $s: ident => $e:expr) => { + unsafe fn path_assert_len(&$s, len: usize) -> &[u8] { unsafe{ $e.path_assert_len(len) } } unsafe fn origin_path_assert_len(&$s, len: usize) -> &[u8] { unsafe{ $e.origin_path_assert_len(len) } } fn prepare_buffers(&mut $s) { $e.prepare_buffers() } fn reserve_buffers(&mut $s, path_len: usize, stack_depth: usize) { $e.reserve_buffers(path_len, stack_depth) } @@ -2415,6 +2422,16 @@ pub(crate) mod read_zipper_core { } impl<'trie, V: Clone + Send + Sync + Unpin + 'trie, A: Allocator + 'trie> ZipperPathBuffer for ReadZipperCore<'trie, '_, V, A> { + unsafe fn path_assert_len(&self, len: usize) -> &[u8] { + let start = self.origin_path.len(); + if self.prefix_buf.capacity() > 0 { + assert!(len <= self.prefix_buf.capacity() - start); + unsafe{ core::slice::from_raw_parts(self.prefix_buf.as_ptr().add(start), len) } + } else { + assert_eq!(len, 0); + &[] + } + } unsafe fn origin_path_assert_len(&self, len: usize) -> &[u8] { if self.prefix_buf.capacity() > 0 { assert!(len <= self.prefix_buf.capacity()); From 7fa79f4bb82df3d60c2ac480cd78319b33ae21d6 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Wed, 2 Sep 2026 01:04:26 -0600 Subject: [PATCH 41/50] Relaxing CatamorphismCached contract to work from the focus, whatever it is, and not be bound to the zipper root --- src/morphisms.rs | 131 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 104 insertions(+), 27 deletions(-) diff --git a/src/morphisms.rs b/src/morphisms.rs index e2c33cd7..20b8195c 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -153,8 +153,8 @@ macro_rules! define_cached_cata_trait { ($(#[$meta:meta])* $trait_name:ident) => { $(#[$meta])* pub trait $trait_name { - /// Applies a **cached**, **stepping**, catamorphism to the trie descending from the - /// zipper's root, running `alg_f` at every step (every byte). + /// Applies a **cached**, **stepping**, catamorphism to the subtrie descending from the + /// zipper's current focus, running `alg_f` at every step (every byte). /// /// This method may reuse previously calculated `W` values when a shared subtrie has /// already been computed. @@ -168,9 +168,6 @@ macro_rules! define_cached_cata_trait { /// - `children` contains the `W` values produced for downstream branches. /// - `value` is the value associated with this path, or `None` when there is none. /// - /// ## Behavior - /// - /// The zipper's focus is ignored; traversal starts again at the root. fn cata_cached(&self, alg_f: AlgF) -> W where W: Clone, @@ -201,7 +198,8 @@ macro_rules! define_cached_cata_trait { }) } - /// Applies a **cached**, **jumping** catamorphism to the trie. + /// Applies a **cached**, **jumping** catamorphism to the subtrie descending from the + /// zipper's current focus. /// /// A jumping catamorphism does not call `alg_f` for path bytes that have neither a /// value nor a branch with more than one child. Those omitted bytes are passed as @@ -680,7 +678,7 @@ impl<'a, Z, V: Clone + Send + Sync + 'a, A: Allocator> CatamorphismCached } else { match focus.into_option() { Some(node) => recursive_cata_cached::<_, _, Acc, _, Err, _, _, _, COMPUTE_PATH>(&node, self.val(), new_acc_f, fold_child_f, summarize_f, &mut cache)?, - None => return summarize_f(&ByteMask::EMPTY, None, None, &[]), + None => return summarize_f(&ByteMask::EMPTY, self.val(), None, &[]), } }; Ok(w) @@ -1105,16 +1103,23 @@ impl SummarizeStackFrame { } } +/// Internal helper type returned by summarize_ascend_to_fork +enum SummarizeAscend { + Parent(W), + Focus(W), +} + /// Ascend from a leaf or completed fork, summarizing each value and non-branching path run on the /// way to the parent fork. This is the three-closure counterpart to [`ascend_to_fork`]. #[inline(always)] fn summarize_ascend_to_fork<'a, Z, V: 'a, Acc, W, E, NewAccF, FoldChildF, SummarizeF, const COMPUTE_PATH: bool>( zipper: &mut Z, + focus_depth: usize, mut accumulator: Option, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF, -) -> Result +) -> Result, E> where Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperPathBuffer, NewAccF: Copy + Fn(&ByteMask) -> Result, @@ -1127,6 +1132,11 @@ where loop { let old_depth = zipper.depth(); let old_value = zipper.get_val_with_witness(&witness); + if old_depth == focus_depth { + return summarize_f(&child_mask, old_value, accumulator, &[]) + .map(SummarizeAscend::Focus); + } + let ascended = zipper.ascend_until(); debug_assert!(ascended > 0); let depth = zipper.depth(); @@ -1135,6 +1145,20 @@ where // SAFETY: `ascend_until` only shortens the logical path; the bytes it removed remain // initialized in the zipper's prepared path buffer until the next zipper movement. let path = unsafe { zipper.path_assert_len(old_depth) }; + + // `ascend_until` can pass the initial focus when that focus is a valueless unary + // position in a compressed run. That position is the traversal root, so preserve the + // entire suffix below it and finish there rather than continuing toward the zipper root. + if depth < focus_depth { + debug_assert!(focus_depth < old_depth); + return summarize_f( + &child_mask, + old_value, + accumulator, + if COMPUTE_PATH { &path[focus_depth..old_depth] } else { &[] }, + ).map(SummarizeAscend::Focus); + } + let jump_len = if zipper.child_count() != 1 || zipper.is_val() { ascended - 1 } else { @@ -1149,7 +1173,7 @@ where let w = summarize_f(&child_mask, old_value, accumulator, prefix)?; if zipper.child_count() != 1 || zipper.at_root() { - return Ok(w) + return Ok(SummarizeAscend::Parent(w)) } debug_assert!(old_depth > jump_len); @@ -1178,7 +1202,7 @@ where FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), E>, SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, { - zipper.reset(); + let focus_depth = zipper.depth(); zipper.prepare_buffers(); let root_child_cnt = zipper.child_count(); @@ -1195,11 +1219,14 @@ where if !zipper.descend_until() { return summarize_ascend_to_fork::( &mut zipper, + focus_depth, None, new_acc_f, fold_child_f, summarize_f, - ) + ).map(|result| match result { + SummarizeAscend::Parent(w) | SummarizeAscend::Focus(w) => w, + }) } } let accumulator = new_acc_f(&ByteMask::from(zipper.child_mask()))?; @@ -1235,13 +1262,17 @@ where } if is_leaf { - let cur_w = summarize_ascend_to_fork::( + let cur_w = match summarize_ascend_to_fork::( &mut zipper, + focus_depth, None, new_acc_f, fold_child_f, summarize_f, - )?; + )? { + SummarizeAscend::Parent(w) => w, + SummarizeAscend::Focus(w) => return Ok(w), + }; DoCache::insert(&mut cache, frame_mut.child_addr, &cur_w); let child_mask = ByteMask::from(zipper.child_mask()); fold_child_f(&child_mask, cur_w, &mut frame_mut.accumulator)?; @@ -1260,25 +1291,32 @@ where return if passthrough_root { summarize_ascend_to_fork::( &mut zipper, + focus_depth, Some(frame.accumulator), new_acc_f, fold_child_f, summarize_f, - ) + ).map(|result| match result { + SummarizeAscend::Parent(w) | SummarizeAscend::Focus(w) => w, + }) } else { - debug_assert!(zipper.at_root(), "must be at root when summarization is done"); + debug_assert_eq!(zipper.depth(), focus_depth, "must be at the initial focus when summarization is done"); let child_mask = ByteMask::from(zipper.child_mask()); summarize_f(&child_mask, zipper.val(), Some(frame.accumulator), &[]) }; } - let cur_w = summarize_ascend_to_fork::( + let cur_w = match summarize_ascend_to_fork::( &mut zipper, + focus_depth, Some(frame.accumulator), new_acc_f, fold_child_f, summarize_f, - )?; + )? { + SummarizeAscend::Parent(w) => w, + SummarizeAscend::Focus(w) => return Ok(w), + }; let frame_mut = stack.last_mut() .expect("when we're not at root, expect a parent summarization stack frame"); @@ -1981,9 +2019,6 @@ pub(crate) mod cached_catamorphism_tests { /// Runs both cached-cata engines through the reconstruction probe and checks each result /// against the expected logical trie as well as against one another. - /// - /// `subject` may be a map or a zipper. For a focused zipper, pass the map representing the - /// focused subtrie using the zipper's established origin-path convention as `expected`. #[track_caller] pub(crate) fn assert_reconstructs_like(subject: &Z, expected: &PathMap<()>) where @@ -2051,8 +2086,7 @@ pub(crate) mod cached_catamorphism_tests { fn assert_logical_focus_roundtrips(keys: &[Vec], focus: &[u8]) { let map = map_from_owned_keys(keys); let expected_keys: Vec> = keys.iter() - .filter(|key| key.starts_with(focus)) - .cloned() + .filter_map(|key| key.strip_prefix(focus).map(ToOwned::to_owned)) .collect(); let expected = map_from_owned_keys(&expected_keys); @@ -2111,21 +2145,26 @@ pub(crate) mod cached_catamorphism_tests { } /// A focus within a compressed path must include every value below it, including any value - /// held at the focus. The iterative engine preserves the zipper origin in its reconstructed - /// paths, so each source map contains only the focused subtrie and is also its expected map. + /// held at the focus. Reconstructed paths are relative to that focus. #[test] fn recursive_cata_regression_mid_node_focus() { let map = map_from_keys(&[b"abc1", b"abc2"]); let mut zipper = map.read_zipper(); zipper.descend_to(b"ab"); assert_eq!(zipper.path(), b"ab"); - assert_reconstructs_like(&zipper, &map); + assert_reconstructs_like(&zipper, &map_from_keys(&[b"c1", b"c2"])); let map = map_from_keys(&[b"ab", b"abcd"]); let mut zipper = map.read_zipper(); zipper.descend_to(b"ab"); assert_eq!(zipper.path(), b"ab"); - assert_reconstructs_like(&zipper, &map); + assert_reconstructs_like(&zipper, &map_from_keys(&[b"", b"cd"])); + + let map = map_from_keys(&[b"abc"]); + let mut zipper = map.read_zipper(); + zipper.descend_to(b"abc"); + assert_eq!(zipper.path(), b"abc"); + assert_reconstructs_like(&zipper, &map_from_keys(&[b""])); } /// Systematically exercises logical value/branch/prefix combinations that may be represented @@ -2245,7 +2284,7 @@ pub(crate) mod cached_catamorphism_tests { macro_rules! define_cached_catamorphism_test_suite { ($suite_name:ident, $cata_trait:ident) => { pub(crate) mod $suite_name { - use super::{GlobalAlloc, Infallible}; + use super::{GlobalAlloc, Infallible, ZipperMoving, ZipperPath}; use crate::morphisms::$cata_trait; use crate::utils::BitMask; @@ -2359,6 +2398,42 @@ pub(crate) mod cached_catamorphism_tests { assert_eq!(count, 11); } + /// A cached cata starts at the zipper's focus: it includes a value held there and all of its + /// descendants, but not values elsewhere in the trie. + pub fn cata_from_value_focus(mut zipper: Z) + where + Z: ZipperMoving + ZipperPath + crate::morphisms::$cata_trait, + { + zipper.descend_to(b"roman"); + let count = $cata_trait::::cata_cached( + &zipper, + |_mask, children: &mut [usize], value| { + value.is_some() as usize + children.iter().sum::() + }, + ); + + assert_eq!(count, 3); + assert_eq!(zipper.path(), b"roman"); + } + + /// The focus may be within a compressed run rather than at an explicit logical branch. + /// Traversal still covers precisely the subtrie below that point. + pub fn cata_from_mid_run_focus(mut zipper: Z) + where + Z: ZipperMoving + ZipperPath + crate::morphisms::$cata_trait, + { + zipper.descend_to(b"roma"); + let count = $cata_trait::::cata_jumping_cached( + &zipper, + |_mask, children: &mut [usize], value, _prefix| { + value.is_some() as usize + children.iter().sum::() + }, + ); + + assert_eq!(count, 3); + assert_eq!(zipper.path(), b"roma"); + } + pub fn longest_path_jumping(zipper: Z) where Z: crate::morphisms::$cata_trait, @@ -2534,6 +2609,8 @@ pub(crate) mod cached_catamorphism_tests { $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $implementation, $suite, $read_keys, $make_z, CACHED_CATA_TEST_KEYS, leaf_count_jumping); $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $implementation, $suite, $read_keys, $make_z, CACHED_CATA_TEST_KEYS, leaf_count_factored_jumping); $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $implementation, $suite, $read_keys, $make_z, CACHED_CATA_TEST_KEYS, leaf_count_factored_stepping); + $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $implementation, $suite, $read_keys, $make_z, CACHED_CATA_TEST_KEYS, cata_from_value_focus); + $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $implementation, $suite, $read_keys, $make_z, CACHED_CATA_TEST_KEYS, cata_from_mid_run_focus); $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $implementation, $suite, $read_keys, $make_z, CACHED_CATA_TEST_KEYS, longest_path_jumping); $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $implementation, $suite, $read_keys, $make_z, CACHED_CATA_TEST_KEYS, longest_path_factored_jumping); $crate::morphisms::cached_catamorphism_tests::cached_catamorphism_case!($z_name, $implementation, $suite, $read_keys, $make_z, CACHED_CATA_TEST_KEYS, branch_values_stepping); From 5a252b8cb0df2d53ede955f9979981bd3fbff6d6 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Wed, 2 Sep 2026 01:15:50 -0600 Subject: [PATCH 42/50] Fixing another recursive cata case in PairNode --- src/line_list_node.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/line_list_node.rs b/src/line_list_node.rs index 0383d7f8..d7af081b 100644 --- a/src/line_list_node.rs +++ b/src/line_list_node.rs @@ -2932,11 +2932,16 @@ impl LineListNode { let key1 = unsafe{ self.key_unchecked::<1>() }; let (key0_byte, key1_byte) = unsafe{ (*key0.get_unchecked(0), *key1.get_unchecked(0)) }; if key0_byte == key1_byte { - //Case 9 (Val, Child), 1-byte key, same key byte (We could eliminate this case by requiring a canonical ordering for identical one-byte keys, but currently we don't) + //Case 9 (Val, Child), same key byte. The value is at the common first byte; + // the child may carry a further compressed suffix below that byte. debug_assert_eq!(key0.len(), 1); - debug_assert_eq!(key1.len(), 1); let val = unsafe { self.val_in_slot::<0>() }; - let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(child_node, Some(val), start_f, fold_child_f, finalize_f, cache)?; + let child_w = if key1.len() == 1 { + recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(child_node, Some(val), start_f, fold_child_f, finalize_f, cache)? + } else { + let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(child_node, None, start_f, fold_child_f, finalize_f, cache)?; + summarize!(Some(val), Some(child_w), &key1[1..])? + }; summarize!(passed_in_val, Some(child_w), key0) } else { //Case 10 (Val, Child), different key bytes From ec135296f6bf628cc4bf3837206f6db108cbdebd Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Wed, 2 Sep 2026 01:32:23 -0600 Subject: [PATCH 43/50] Fixing (hopfully last) PairNode recusrive logic edge case --- src/line_list_node.rs | 14 ++++++++++++-- src/morphisms.rs | 2 ++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/line_list_node.rs b/src/line_list_node.rs index d7af081b..ea361e14 100644 --- a/src/line_list_node.rs +++ b/src/line_list_node.rs @@ -2798,7 +2798,9 @@ impl LineListNode { // - Case 1 (Empty, Empty) // Run only `finalize_f` on default `Acc` // - Case 2 (Child, Empty) - // Recursively call on child, then run only `collapse_f` on the result, specifying the path + // Recursively call on child, then run only `collapse_f` on the result, specifying the path. + // When a value was passed in at the first byte of that path, split out that byte as a + // value-bearing branch before collapsing the remaining run. // - Case 3 (Child, Val), 1-byte key, same key byte // Recursively call on child, then run only `collapse_f` on the result, specifying the value and the 1-byte path // - Case 4 (Child, Val), different key bytes @@ -2829,7 +2831,15 @@ impl LineListNode { let child_node = unsafe{ self.child_in_slot::<0>() }; let child_w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(child_node, None, start_f, fold_child_f, finalize_f, cache)?; let path = unsafe{ self.key_unchecked::<0>() }; - summarize!(passed_in_val, Some(child_w), path) + if passed_in_val.is_none() { + summarize!(None, Some(child_w), path) + } else { + let child_w = summarize!(None, Some(child_w), &path[1..])?; + let mask = ByteMask::from(path[0]); + let mut acc = start_f(&mask)?; + fold_child_f(&mask, child_w, &mut acc)?; + finalize_f(&mask, passed_in_val, Some(acc), &[]) + } }, //(Child, Val) = (1 << 3) + (1 << 2) + (1 << 1) 14 => { diff --git a/src/morphisms.rs b/src/morphisms.rs index 20b8195c..f618769d 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -2181,6 +2181,8 @@ pub(crate) mod cached_catamorphism_tests { vec![b"a1".to_vec(), b"a2".to_vec(), b"b1".to_vec(), b"b2".to_vec()], vec![b"a".to_vec(), b"ab1".to_vec(), b"ab2".to_vec()], vec![b"".to_vec(), b"q1".to_vec(), b"q2".to_vec()], + // A value mid-run with both a deeper child and a sibling branch. + vec![b"accab".to_vec(), b"ac".to_vec(), b"accac".to_vec(), b"abacc".to_vec()], ]; for tail_len in 1..=4 { From bdd31adbe6c85ff2086d771666538ced13b7bec6 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Wed, 2 Sep 2026 02:22:16 -0600 Subject: [PATCH 44/50] Improving documentation around the factored cata, so the agent doesn't misunderstand the contract Harmonizing description of the jumping cata, so we don't have `prefix` and `sub_path` as two ways to refer to the same thing --- src/morphisms.rs | 101 ++++++++++++++++++++++++++--------------------- 1 file changed, 55 insertions(+), 46 deletions(-) diff --git a/src/morphisms.rs b/src/morphisms.rs index f618769d..1505df28 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -57,7 +57,7 @@ //! | Guaranteed and deterministic `alg` exec order | Unpredictable callback `alg` exec order | //! | `alg` may capture and modify environment ([`FnMut`](https://doc.rust-lang.org/std/ops/trait.FnMut.html)) | `alg` must be a pure [`Fn`](https://doc.rust-lang.org/std/ops/trait.Fn.html) | //! | Owned `W` type passed between `alg` invocations | `W` type must implement [`Clone`](https://doc.rust-lang.org/std/clone/trait.Clone.html) | -//! | `alg` is aware of the entire `path` | `alg` only sees path byte, or sub-path slice | +//! | `alg` is aware of the entire `path` | `alg` only sees path byte, or prefix slice | //! //! All else being equal, `cached` methods are likely to be more efficient because structural sharing //! occurs frequently in pathmap tries. At the extreme, `side_effect` methods may produce a combinitoric @@ -188,9 +188,9 @@ macro_rules! define_cached_cata_trait { AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> Result, Self: Sized, { - self.cata_jumping_cached_fallible(|mask, children, val, sub_path| { + self.cata_jumping_cached_fallible(|mask, children, val, prefix| { let mut w = alg_f(mask, children, val)?; - for &byte in sub_path.iter().rev() { + for &byte in prefix.iter().rev() { let child_mask = ByteMask::from(byte); w = alg_f(&child_mask, core::slice::from_mut(&mut w), None)?; } @@ -201,19 +201,20 @@ macro_rules! define_cached_cata_trait { /// Applies a **cached**, **jumping** catamorphism to the subtrie descending from the /// zipper's current focus. /// - /// A jumping catamorphism does not call `alg_f` for path bytes that have neither a - /// value nor a branch with more than one child. Those omitted bytes are passed as - /// `sub_path` instead. + /// A jumping catamorphism may omit calls to `alg_f` for path bytes that have neither + /// a value nor a branch with more than one child, passing those bytes as `prefix` + /// instead. Implementations may also fall back to stepping through some or all such + /// bytes, in which case `prefix` is shorter or empty. /// /// This method may reuse previously calculated `W` values when a shared subtrie has /// already been computed. /// /// ## Arguments to `alg_f` /// - /// `(child_mask: &`[`ByteMask`]`, children: &mut [W], value: Option<&V>, sub_path: &[u8])` + /// `(child_mask: &`[`ByteMask`]`, children: &mut [W], value: Option<&V>, prefix: &[u8])` /// - /// `sub_path` is the sequence of bytes for which `alg_f` was not called. For example, - /// in this trie: + /// `prefix` is the sequence of bytes collapsed into this `alg_f` call. For example, + /// an implementation that collapses every non-branching run in this trie: /// /// ```text /// ─── c ─── o ─── m ─┬─ b ─── o → "combo" @@ -221,7 +222,7 @@ macro_rules! define_cached_cata_trait { /// └─ f ─── o ─── r ─── t → "comfort" /// ``` /// - /// `alg_f` is called four times: + /// This implementation calls `alg_f` four times: /// /// 1. `alg_f(ByteMask::EMPTY, &[], Some(&()), b"o")` /// 2. `alg_f(ByteMask::EMPTY, &[], Some(&()), b"t")` @@ -235,8 +236,8 @@ macro_rules! define_cached_cata_trait { AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> W, Self: Sized, { - self.cata_jumping_cached_fallible(|mask, children, val, sub_path| -> Result { - Ok(alg_f(mask, children, val, sub_path)) + self.cata_jumping_cached_fallible(|mask, children, val, prefix| -> Result { + Ok(alg_f(mask, children, val, prefix)) }).unwrap() } @@ -308,13 +309,13 @@ macro_rules! define_cached_cata_trait { /// /// ## Closures /// - /// `NewAccF` creates an accumulator for a logical trie node with more than one child - /// branch: `fn(child_mask: &ByteMask) -> Result`. + /// `NewAccF` creates an accumulator for a logical trie node with one or more downstream + /// child branches: `fn(child_mask: &ByteMask) -> Result`. /// /// `FoldChildF` folds one downstream child's `W` into that accumulator. It is called /// once per downstream branch, in the same order as the bits in `child_mask`. Every - /// call for a logical node receives its complete child mask, so use call order to - /// associate a child result with its byte: + /// call for a logical trie node receives its complete child mask, so use call + /// order to associate a child result with its byte: /// `fn(child_mask: &ByteMask, downstream: W, accumulator: &mut Acc) -> Result<(), Err>`. /// /// `SummarizeF` produces the `W` for a logical trie node and a non-branching path @@ -322,15 +323,23 @@ macro_rules! define_cached_cata_trait { /// including `value` and downstream children. /// /// - `child_mask` describes the node's immediate child bytes. - /// - `accumulator` contains results folded from child branches, or is `None` when the - /// node has no downstream branches. - /// - `prefix` is a non-branching path above the logical node. It never includes a path - /// position that is part of a `child_mask` for this or another `summarize_f` call. + /// - `accumulator` contains results folded from child branches, or is `None` when no + /// accumulator was needed. + /// - `prefix` is a collapsed non-branching path above the traversal node. It never + /// includes a path position that is part of a `child_mask` for this or another + /// `summarize_f` call. It may be empty when an implementation falls back to stepping. /// - /// Its signature is + /// The `SummarizeF` signature is /// `fn(child_mask: &ByteMask, value: Option<&V>, accumulator: Option, prefix: &[u8]) -> Result`. /// - /// Errors from any callback stop traversal immediately and are returned to the caller. + /// ## Behavior + /// + /// * Errors from any callback stop traversal immediately and are returned to the caller. + /// * Every call to `new_acc_f` will lead to a matching call to `summarize_f` where ownership + /// of the `Acc` object is given to the `SummarizeF` callback, unless an error halts the catamorphism. + /// * `prefix` is **only an optimization** for path bytes with a single child. Your + /// algebra must not rely on a given path byte being represented as a prefix, instead of as + /// a sequence of `new_acc_f`, then `fold_child_f`. /// /// With `COMPUTE_PATH = false`, path runs are not materialized and `prefix` is always /// empty. Use that only when the algebra does not depend on path bytes, only on values @@ -570,8 +579,8 @@ impl SplitCata { /// A compatibility shim to provide a 4-function "jumping" catamorphism API /// -/// - `jump_f`: `FnMut(sub_path: &[u8], w: W, path: &[u8]) -> W` -/// Elevates a result `w` descending from the relative path, `sub_path` to the current position at `path` +/// - `jump_f`: `FnMut(prefix: &[u8], w: W, path: &[u8]) -> W` +/// Elevates a result `w` descending from the relative path, `prefix` to the current position at `path` /// /// See [`SplitCata`] for a description of additional args #[deprecated] @@ -1328,7 +1337,7 @@ where /// Internal implementation behind all cached catas /// -/// AlgF args: (child_mask, children, value, sub_path, debug_path, zipper) +/// AlgF args: (child_mask, children, value, prefix, debug_path, zipper) pub(crate) fn into_cata_cached_body<'a, Z, V: 'a, W, E, AlgF, Cache, const JUMPING: bool, const DEBUG_PATH: bool>( mut zipper: Z, mut alg_f: AlgF ) -> Result @@ -2753,7 +2762,7 @@ mod tests { |bm, ch, v| f_pure(bm, ch, v, &[])); assert(output, "cata_cached"); let output = zipper.clone().recursive_cata_jumping_cached( - |bm, ch, v, sub_path| f_pure(bm, ch, v, sub_path)); + |bm, ch, v, prefix| f_pure(bm, ch, v, prefix)); assert(output, "cata_jumping_cached"); } @@ -2767,7 +2776,7 @@ mod tests { check_side_effect_catas(zipper.clone(), |mask, children, _jmp, val, _path| { alg_f(mask, children, val) }, &mut assert); - check_pure_catas(zipper.clone(), |mask, children, val, _sub_path| { + check_pure_catas(zipper.clone(), |mask, children, val, _prefix| { alg_f(mask, children, val) }, &mut assert); } @@ -2823,15 +2832,15 @@ mod tests { assert_eq!(output.1, expected_sum); //The pure jumping cata is a variant on the above, but we also need to care about - // the sub_path we jump over. So we either count the value associated with the last - // byte of the sub-path, or with the next parent path byte. + // the prefix we jump over. So we either count the value associated with the last + // byte of the prefix, or with the next parent path byte. // //This code works fine for both stepping and jumping, but is a little more complicated // than the stepping-only version - let pure_alg = |child_mask: &ByteMask, children: &mut [(bool, u32)], val: Option<&()>, sub_path: &[u8]| { + let pure_alg = |child_mask: &ByteMask, children: &mut [(bool, u32)], val: Option<&()>, prefix: &[u8]| { let mut sum = 0; if val.is_some() { - if let Some(path_byte) = sub_path.last() { + if let Some(path_byte) = prefix.last() { sum += (*path_byte as char).to_digit(10).unwrap(); } } @@ -2841,7 +2850,7 @@ mod tests { } sum += *downstream_sum; } - (val.is_some() && sub_path.len()==0, sum) + (val.is_some() && prefix.len()==0, sum) }; //Test both stepping and jumping cached catas @@ -2888,11 +2897,11 @@ mod tests { assert_eq!(std::str::from_utf8(longest.as_slice()).unwrap(), "rubicundus")); //================================================================================= - // PureLongestPath - Finds the longest path in the trie by concatenating sub-paths; + // PureLongestPath - Finds the longest path in the trie by concatenating prefix paths; // This is necessary for pure catas because the same subtrie may share multiple base paths - fn longest_partial_path(child_mask: &ByteMask, children: &mut[Vec], sub_path: &[u8]) -> Vec { + fn longest_partial_path(child_mask: &ByteMask, children: &mut[Vec], prefix: &[u8]) -> Vec { if children.len() == 0 { - sub_path.to_vec() + prefix.to_vec() } else { let mut longest_downstream_path = child_mask.iter() .zip(children.iter_mut()).max_by_key(|(_byte, path_rest)| path_rest.len()) @@ -2901,7 +2910,7 @@ mod tests { path_rest.insert(0, byte); path_rest }); - let mut path = sub_path.to_vec(); + let mut path = prefix.to_vec(); path.append(&mut longest_downstream_path); path } @@ -3180,8 +3189,8 @@ mod tests { // println!("alg: \"{}\"", String::from_utf8_lossy(_path)); alg_cnt += 1; }, - |_sub_path, _, _path| { - // println!("jump: over \"{}\" to \"{}\"", String::from_utf8_lossy(_sub_path), String::from_utf8_lossy(_path)); + |_prefix, _, _path| { + // println!("jump: over \"{}\" to \"{}\"", String::from_utf8_lossy(_prefix), String::from_utf8_lossy(_path)); jump_cnt += 1; } )); @@ -3221,8 +3230,8 @@ mod tests { // println!("alg: {_path:?}, mask: {_mask:?}"); alg_cnt += 1; }, - |_sub_path, _, _path| { - // println!("jump: over {_sub_path:?} to {_path:?}"); + |_prefix, _, _path| { + // println!("jump: over {_prefix:?} to {_path:?}"); jump_cnt += 1; } )); @@ -3261,9 +3270,9 @@ mod tests { // println!("alg: {path:?}"); assert_eq!(path, &[]); }, - |sub_path, _, path| { - // println!("jump: over {sub_path:?} to {path:?}"); - assert_eq!(sub_path, &[98]); + |prefix, _, path| { + // println!("jump: over {prefix:?} to {path:?}"); + assert_eq!(prefix, &[98]); assert_eq!(path, &[97]); } )) @@ -3838,12 +3847,12 @@ mod tests { } #[test] - fn iterative_summarization_does_not_accumulate_at_passthrough_root() { + fn recursive_factored_cata_collapses_compressed_passthrough_root() { let map: PathMap<()> = [(b"abc".as_slice(), ())].into_iter().collect(); let result = map.read_zipper().recursive_factored_cata_jumping::<(), Vec, Infallible, _, _, _, true>( - |_| panic!("a unary valueless root must not create an accumulator"), - |_mask, _child, _accumulator| panic!("a unary valueless root must not fold a child"), + |_| panic!("a compressed unary passthrough root must not create an accumulator"), + |_mask, _child, _accumulator| panic!("a compressed unary passthrough root must not fold a child"), |_mask, value, accumulator, prefix| { assert!(value.is_some()); assert!(accumulator.is_none()); From 657fed150bcf18f1b47f370b7fdefc5a91c63fad Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Wed, 2 Sep 2026 02:32:25 -0600 Subject: [PATCH 45/50] Fixing test that was based on a misunderstanding of contract Deleting two crufty tests that are already expressed in the macro --- src/morphisms.rs | 84 ++++++++++++------------------------------------ 1 file changed, 21 insertions(+), 63 deletions(-) diff --git a/src/morphisms.rs b/src/morphisms.rs index 1505df28..4b0f0db6 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -2558,14 +2558,28 @@ pub(crate) mod cached_catamorphism_tests { Z: crate::morphisms::$cata_trait, { let result = $cata_trait:: - ::factored_cata_jumping::<(), Vec, Infallible, _, _, _, true>( + ::factored_cata_jumping::)>, Vec, Infallible, _, _, _, true>( &zipper, - |_| panic!("a unary valueless root must not create an accumulator"), - |_mask, _child, _accumulator| panic!("a unary valueless root must not fold a child"), - |_mask, value, accumulator, prefix| { - assert_eq!(value, Some(&0)); - assert!(accumulator.is_none()); - Ok(prefix.to_vec()) + |_| Ok(Vec::new()), + |mask, child, children| { + let byte = mask.iter().nth(children.len()) + .expect("fold_child called more times than child-mask bits"); + children.push((byte, child)); + Ok(()) + }, + |_mask, value, children, prefix| { + let mut path = prefix.to_vec(); + match (value, children) { + (Some(&0), None) => {}, + (None, Some(mut children)) => { + assert_eq!(children.len(), 1); + let (byte, child) = children.pop().unwrap(); + path.push(byte); + path.extend(child); + }, + _ => panic!("unexpected callback shape for a single value path"), + } + Ok(path) }, ) .unwrap(); @@ -3807,62 +3821,6 @@ mod tests { assert_eq!(zipper.path(), b"a"); } - #[test] - fn iterative_summarization_folds_each_child_immediately() { - use std::cell::RefCell; - - let map: PathMap = [ - (b"a".as_slice(), 1), - (b"b".as_slice(), 2), - ] - .into_iter() - .collect(); - let events = RefCell::new(Vec::new()); - - let result = map.read_zipper().recursive_factored_cata_jumping::, usize, Infallible, _, _, _, false>( - |_mask| { - events.borrow_mut().push("new"); - Ok(Vec::new()) - }, - |_mask, child, accumulator| { - events.borrow_mut().push(if child == 1 { "fold 1" } else { "fold 2" }); - accumulator.push(child); - Ok(()) - }, - |_mask, value, accumulator, _prefix| { - match value { - Some(1) => events.borrow_mut().push("summarize 1"), - Some(2) => events.borrow_mut().push("summarize 2"), - _ => events.borrow_mut().push("summarize root"), - } - Ok(value.copied().unwrap_or_else(|| accumulator.unwrap().into_iter().sum())) - }, - ); - - assert_eq!(result.unwrap(), 3); - assert_eq!( - events.into_inner(), - ["new", "summarize 1", "fold 1", "summarize 2", "fold 2", "summarize root"], - ); - } - - #[test] - fn recursive_factored_cata_collapses_compressed_passthrough_root() { - let map: PathMap<()> = [(b"abc".as_slice(), ())].into_iter().collect(); - - let result = map.read_zipper().recursive_factored_cata_jumping::<(), Vec, Infallible, _, _, _, true>( - |_| panic!("a compressed unary passthrough root must not create an accumulator"), - |_mask, _child, _accumulator| panic!("a compressed unary passthrough root must not fold a child"), - |_mask, value, accumulator, prefix| { - assert!(value.is_some()); - assert!(accumulator.is_none()); - Ok(prefix.to_vec()) - }, - ); - - assert_eq!(result.unwrap(), b"abc"); - } - /// A bounded deep-path smoke test for the recursive cata. /// /// The implementation uses the Rust call stack once per physical node. `all_dense_nodes` From 0b11c23265d09fa43f22c21894b82da5bbecac99 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Wed, 2 Sep 2026 04:46:05 -0600 Subject: [PATCH 46/50] Updating side-effecting cata behavior to respect the zipper focus, rather than always starting from the root Updating CatamorphismDebug trait to use iterative cata traversal Deleting old implementation of caching cata body, since it no longer has any users --- src/arena_compact.rs | 3 +- src/morphisms.rs | 519 ++++++++++++++++-------------- src/utils/debug/morphism_debug.rs | 119 ++++++- 3 files changed, 379 insertions(+), 262 deletions(-) diff --git a/src/arena_compact.rs b/src/arena_compact.rs index a9774fff..d1b4ebfd 100644 --- a/src/arena_compact.rs +++ b/src/arena_compact.rs @@ -1354,8 +1354,7 @@ struct CachedFrame { /// contiguous — and the copy keeps pointing at the original children. So a /// repeated subtrie costs one node, not a subtrie. /// -/// The traversal itself is the jumping catamorphism, unrolled (see -/// `morphisms::into_cata_cached_body`, which this follows closely). It is +/// The traversal itself is an unrolled jumping catamorphism. It is /// spelled out here rather than delegating to /// [`CatamorphismCached::cata_jumping_cached`] for two reasons: /// - the cached cata only consults its cache one byte below a fork, whereas we diff --git a/src/morphisms.rs b/src/morphisms.rs index 4b0f0db6..f92c7727 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -52,7 +52,7 @@ //! //! | side_effect | cached | //! |-------------------------------------------------|---------------------------------------------| -//! | Visits the entire trie | Short-circuits shared subtries | +//! | Visits every path in the trie | Short-circuits shared subtries | //! | Always re-computes subtrie info (e.g. `W`) | Reuses shared subtrie info | //! | Guaranteed and deterministic `alg` exec order | Unpredictable callback `alg` exec order | //! | `alg` may capture and modify environment ([`FnMut`](https://doc.rust-lang.org/std/ops/trait.FnMut.html)) | `alg` must be a pure [`Fn`](https://doc.rust-lang.org/std/ops/trait.Fn.html) | @@ -82,7 +82,7 @@ use crate::gxhash::{self, HashMap, HashMapExt}; /// Provides methods to perform side-effecting catamorphisms appropriate for serialization and full-path operations pub trait CatamorphismSideEffecting { - /// Applies a "stepping" catamorphism to the trie descending from the zipper's root, running the `alg_f` at every + /// Applies a "stepping" catamorphism to the subtrie descending from the zipper's focus, running the `alg_f` at every /// step (at every byte) /// /// ## Arguments to `alg_f`: @@ -97,12 +97,9 @@ pub trait CatamorphismSideEffecting { /// - `value`: A value associated with a given path in the trie, or `None` if the trie has no value at /// that path. /// - /// - `path`: The [`origin_path`](ZipperAbsolutePath::origin_path) for the invocation. The `alg_f` will - /// be run exactly once for each unique path in the trie. + /// - `path`: The absolute [`origin_path`](ZipperAbsolutePath::origin_path) for the invocation. The `alg_f` will + /// be run exactly once for each path in the subtrie rooted at the initial focus. /// - /// ## Behavior - /// - /// The focus position of the zipper will be ignored and it will be immediately reset to the root. fn into_cata_side_effect(self, mut alg_f: AlgF) -> W where AlgF: FnMut(&ByteMask, &mut [W], Option<&V>, &[u8]) -> W, @@ -119,7 +116,7 @@ pub trait CatamorphismSideEffecting { fn into_cata_side_effect_fallible(self, alg_f: AlgF) -> Result where AlgF: FnMut(&ByteMask, &mut [W], Option<&V>, &[u8]) -> Result; - /// Applies a "jumping" catamorphism to the trie + /// Applies a "jumping" catamorphism to the subtrie descending from the zipper's focus. /// /// A "jumping" catamorphism is a form of catamorphism where the `alg_f` "jumps over" (isn't called for) /// path bytes in the trie where there isn't either a `value` or a branch where `children.len() > 1`. @@ -790,16 +787,16 @@ fn cata_side_effect_body<'a, Z, V: 'a, W, Err, AlgF, const JUMPING: bool>(mut z: AlgF: FnMut(&ByteMask, &mut [W], usize, Option<&V>, &[u8], &Z) -> Result { //`stack` holds a "frame" at each forking point above the zipper position. No frames exist for values - let mut stack = Vec::::with_capacity(12); + let mut stack = Vec::::with_capacity(12); let mut children = Vec::::new(); let mut frame_idx = 0; - z.reset(); + let focus_depth = z.depth(); z.prepare_buffers(); - //Push a stack frame for the root, and start on the first branch off the root - stack.push(StackFrame::from(&z)); + // Push a stack frame for the initial focus, and start on its first branch. + stack.push(SideEffectStackFrame::new(&z)); if z.descend_first_byte().is_none() { - //Empty trie is a special case + // A leaf focus is a special case. return alg_f(&ByteMask::EMPTY, &mut [], 0, z.val(), z.origin_path(), &z) } @@ -815,7 +812,15 @@ fn cata_side_effect_body<'a, Z, V: 'a, W, Err, AlgF, const JUMPING: bool>(mut z: if is_leaf { //Ascend back to the last fork point from this leaf - let cur_w = ascend_to_fork::(&mut z, &mut alg_f, &mut [])?; + let cur_w = match ascend_to_focus_or_fork::( + &mut z, + focus_depth, + &mut alg_f, + &mut [], + )? { + AscendResult::Parent(w) => w, + AscendResult::Focus(w) => return Ok(w), + }; children.push(cur_w); stack[frame_idx].child_idx += 1; @@ -841,7 +846,15 @@ fn cata_side_effect_body<'a, Z, V: 'a, W, Err, AlgF, const JUMPING: bool>(mut z: debug_assert_eq!(stack[frame_idx].child_idx, stack[frame_idx].child_cnt); let child_start = children.len() - stack[frame_idx].child_cnt as usize; let children2 = &mut children[child_start..]; - let cur_w = ascend_to_fork::(&mut z, &mut alg_f, children2)?; + let cur_w = match ascend_to_focus_or_fork::( + &mut z, + focus_depth, + &mut alg_f, + children2, + )? { + AscendResult::Parent(w) => w, + AscendResult::Focus(w) => return Ok(w), + }; children.truncate(child_start); frame_idx -= 1; @@ -855,8 +868,16 @@ fn cata_side_effect_body<'a, Z, V: 'a, W, Err, AlgF, const JUMPING: bool>(mut z: let descended = z.descend_indexed_byte(stack[frame_idx].child_idx as usize); debug_assert!(descended.is_some()); } else { - //Push a new stack frame for this branch - Stack::push_state_raw(&mut stack, &mut frame_idx, &z); + // Push a new stack frame for this branch, reusing a frame left by a completed + // sibling branch when possible. + frame_idx += 1; + assert!(frame_idx <= stack.len(), "stack invariant: frame index <= length"); + let frame = SideEffectStackFrame::new(&z); + if frame_idx == stack.len() { + stack.push(frame); + } else { + stack[frame_idx] = frame; + } //Descend the first child branch let descended = z.descend_first_byte(); @@ -865,153 +886,126 @@ fn cata_side_effect_body<'a, Z, V: 'a, W, Err, AlgF, const JUMPING: bool>(mut z: } } +/// Ascends from a completed child to either its parent fork or the initial cata focus. +/// +/// A jumping ascent may otherwise pass a valueless unary focus in one movement, so the focus +/// boundary is checked explicitly rather than relying on `at_root()`. #[inline(always)] -fn ascend_to_fork<'a, Z, V: 'a, W, Err, AlgF, const JUMPING: bool>(z: &mut Z, - alg_f: &mut AlgF, children: &mut [W] -) -> Result - where +fn ascend_to_focus_or_fork<'a, Z, V: 'a, W, Err, AlgF, const JUMPING: bool>( + z: &mut Z, + focus_depth: usize, + alg_f: &mut AlgF, + children: &mut [W], +) -> Result, Err> +where Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperAbsolutePath + ZipperPathBuffer, - AlgF: FnMut(&ByteMask, &mut [W], usize, Option<&V>, &[u8], &Z) -> Result + AlgF: FnMut(&ByteMask, &mut [W], usize, Option<&V>, &[u8], &Z) -> Result, { - let z_witness = z.witness(); - let mut w; + let witness = z.witness(); let mut child_mask = ByteMask::from(z.child_mask()); let mut children = &mut children[..]; + if JUMPING { - //This loop runs until we got to a fork or the root. We will take a spin through the loop - // for each value we encounter along the way while ascending + let mut w; loop { + let old_depth = z.depth(); let old_path_len = z.origin_path().len(); - let old_val = z.get_val_with_witness(&z_witness); + let old_value = z.get_val_with_witness(&witness); let ascended = z.ascend_until(); debug_assert!(ascended > 0); - let origin_path = unsafe{ z.origin_path_assert_len(old_path_len) }; + // SAFETY: `ascend_until` only shortens the path, so the former path remains initialized + // in the prepared buffer until the next movement. + let origin_path = unsafe { z.origin_path_assert_len(old_path_len) }; + + if z.depth() < focus_depth { + // A valueless unary focus is not an `ascend_until` stopping point. Its jumping + // cata is just its child's result, but the preceding callback must report the + // jump relative to this focus rather than to the zipper's root. + debug_assert!(old_depth > focus_depth); + w = alg_f( + &child_mask, + children, + old_depth - focus_depth, + old_value, + origin_path, + z, + )?; + return Ok(AscendResult::Focus(w)) + } + let jump_len = if z.child_count() != 1 || z.is_val() { - old_path_len - (z.origin_path().len()+1) + ascended - 1 } else { - old_path_len - z.origin_path().len() + ascended }; - - w = alg_f(&child_mask, children, jump_len, old_val, origin_path, &z)?; + w = alg_f(&child_mask, children, jump_len, old_value, origin_path, z)?; + + if z.depth() == focus_depth && z.child_count() == 1 { + // A valued unary focus stops `ascend_until`; complete that focus here rather + // than continuing to its parent. A valueless focus is a jumping passthrough. + return if z.is_val() { + let byte = origin_path[old_path_len - jump_len - 1]; + let child_mask = ByteMask::from(byte); + let mut child = [w]; + alg_f(&child_mask, &mut child, 0, z.val(), z.origin_path(), z) + .map(AscendResult::Focus) + } else { + Ok(AscendResult::Focus(w)) + } + } if z.child_count() != 1 || z.at_root() { - return Ok(w) + return Ok(AscendResult::Parent(w)) } children = core::array::from_mut(&mut w); - - // SAFETY: We will never over-read the path buffer because we only get here after we ascended - let byte = *unsafe{ z.origin_path_assert_len(old_path_len-jump_len) }.last().unwrap(); - child_mask = ByteMask::EMPTY; - child_mask.set_bit(byte); + let byte = origin_path[old_path_len - jump_len - 1]; + child_mask = ByteMask::from(byte); } } else { - //This loop runs at each byte step as we ascend + let mut w; loop { let origin_path = z.origin_path(); let byte = origin_path.last().copied().unwrap_or(0); - let val = z.val(); - w = alg_f(&child_mask, children, 0, val, origin_path, &z)?; + let value = z.val(); + w = alg_f(&child_mask, children, 0, value, origin_path, z)?; let ascended = z.ascend_byte(); debug_assert!(ascended); + if z.depth() == focus_depth && z.child_count() == 1 { + let child_mask = ByteMask::from(byte); + let mut child = [w]; + return alg_f(&child_mask, &mut child, 0, z.val(), z.origin_path(), z) + .map(AscendResult::Focus) + } + if z.child_count() != 1 || z.at_root() { - return Ok(w) + return Ok(AscendResult::Parent(w)) } children = core::array::from_mut(&mut w); - child_mask = ByteMask::EMPTY; - child_mask.set_bit(byte); + child_mask = ByteMask::from(byte); } } } -/// Internal structure to hold temporary info used inside morphism apply methods -struct StackFrame { +/// A frame for the side-effecting cata's explicit traversal stack. +struct SideEffectStackFrame { child_idx: u16, child_cnt: u16, - child_addr: Option, -} - -impl StackFrame { - /// Allocates a new StackFrame - fn from(zipper: &Z) -> Self - where Z: Zipper, - { - let mut stack_frame = StackFrame { - child_cnt: 0, - child_idx: 0, - child_addr: None, - }; - stack_frame.reset(zipper); - stack_frame - } - - /// Resets a StackFrame to the state needed to iterate a new forking point - fn reset(&mut self, zipper: &Z) - where Z: Zipper, - { - self.child_cnt = zipper.child_count() as u16; - self.child_idx = 0; - } } -struct Stack { - stack: Vec, - position: usize, -} - -impl Stack { - pub fn new() -> Self { - Self { - stack: Vec::with_capacity(12), - position: !0, - } - } - /// Return the reference to the top stack frame - #[inline] - pub fn last_mut(&mut self) -> Option<&mut StackFrame> { - let idx = self.position; - self.stack.get_mut(idx) - } - - /// Return the reference to the top stack frame - /// and decrease stack pointer. Doesn't free the stack frame. +impl SideEffectStackFrame { #[inline] - pub fn pop_mut(&mut self) -> Option<&mut StackFrame> { - if self.position == !0 { - return None; - } - let idx = self.position; - self.position = self.position.wrapping_sub(1); - self.stack.get_mut(idx) - } - - /// Push stack state for current zipper position - /// - /// This function re-uses allocations for stack frames, - /// to avoid allocator thrashing. - pub fn push_state(&mut self, z: &Z) - where Z: Zipper + ZipperPath, - { - Self::push_state_raw(&mut self.stack, &mut self.position, z); - } - - pub fn push_state_raw<'a, Z>( - stack: &mut Vec, - position: &mut usize, - zipper: &Z) - where Z: Zipper + ZipperPath, + fn new(zipper: &Z) -> Self + where + Z: Zipper, { - *position = position.wrapping_add(1); - assert!(*position <= stack.len(), - "stack invariant: position <= len"); - if *position == stack.len() { - stack.push(StackFrame::from(zipper)); - } else { - stack[*position].reset(zipper); + Self { + child_idx: 0, + child_cnt: zipper.child_count() as u16, } } } @@ -1112,28 +1106,35 @@ impl SummarizeStackFrame { } } -/// Internal helper type returned by summarize_ascend_to_fork -enum SummarizeAscend { +/// Result of ascending from a completed child. +enum AscendResult { Parent(W), Focus(W), } +#[inline(always)] +fn no_debug_path(_zipper: &Z, _depth: usize) -> &[u8] { + &[] +} + /// Ascend from a leaf or completed fork, summarizing each value and non-branching path run on the -/// way to the parent fork. This is the three-closure counterpart to [`ascend_to_fork`]. +/// way to the parent fork. #[inline(always)] -fn summarize_ascend_to_fork<'a, Z, V: 'a, Acc, W, E, NewAccF, FoldChildF, SummarizeF, const COMPUTE_PATH: bool>( +fn summarize_ascend_to_fork<'a, Z, V: 'a, Acc, W, E, DebugPathF, NewAccF, FoldChildF, SummarizeF, const COMPUTE_PATH: bool, const DEBUG_PATH: bool>( zipper: &mut Z, focus_depth: usize, mut accumulator: Option, + debug_path_f: DebugPathF, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF, -) -> Result, E> +) -> Result, E> where Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperPathBuffer, + DebugPathF: Copy + for<'z> Fn(&'z Z, usize) -> &'z [u8], NewAccF: Copy + Fn(&ByteMask) -> Result, FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), E>, - SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, + SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8], &[u8]) -> Result, { let witness = zipper.witness(); let mut child_mask = ByteMask::from(zipper.child_mask()); @@ -1142,8 +1143,9 @@ where let old_depth = zipper.depth(); let old_value = zipper.get_val_with_witness(&witness); if old_depth == focus_depth { - return summarize_f(&child_mask, old_value, accumulator, &[]) - .map(SummarizeAscend::Focus); + let debug_path = if DEBUG_PATH { debug_path_f(zipper, old_depth) } else { &[] }; + return summarize_f(&child_mask, old_value, accumulator, &[], debug_path) + .map(AscendResult::Focus); } let ascended = zipper.ascend_until(); @@ -1154,6 +1156,7 @@ where // SAFETY: `ascend_until` only shortens the logical path; the bytes it removed remain // initialized in the zipper's prepared path buffer until the next zipper movement. let path = unsafe { zipper.path_assert_len(old_depth) }; + let debug_path = if DEBUG_PATH { debug_path_f(zipper, old_depth) } else { &[] }; // `ascend_until` can pass the initial focus when that focus is a valueless unary // position in a compressed run. That position is the traversal root, so preserve the @@ -1165,7 +1168,8 @@ where old_value, accumulator, if COMPUTE_PATH { &path[focus_depth..old_depth] } else { &[] }, - ).map(SummarizeAscend::Focus); + debug_path, + ).map(AscendResult::Focus); } let jump_len = if zipper.child_count() != 1 || zipper.is_val() { @@ -1179,10 +1183,10 @@ where &[] }; - let w = summarize_f(&child_mask, old_value, accumulator, prefix)?; + let w = summarize_f(&child_mask, old_value, accumulator, prefix, debug_path)?; if zipper.child_count() != 1 || zipper.at_root() { - return Ok(SummarizeAscend::Parent(w)) + return Ok(AscendResult::Parent(w)) } debug_assert!(old_depth > jump_len); @@ -1195,11 +1199,8 @@ where } /// Iterative cached traversal behind [`CatamorphismCached::factored_cata_jumping`]. -/// -/// This follows [`into_cata_cached_body`] closely, but completes a logical node with the three -/// summarization closures instead of collecting a mutable child slice for one algebra closure. fn summarize_cached_body<'a, Z, V: 'a, Acc, W, E, NewAccF, FoldChildF, SummarizeF, const COMPUTE_PATH: bool>( - mut zipper: Z, + zipper: Z, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF, @@ -1210,13 +1211,41 @@ where NewAccF: Copy + Fn(&ByteMask) -> Result, FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), E>, SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, +{ + summarize_cached_body_with_debug::( + zipper, + no_debug_path::, + new_acc_f, + fold_child_f, + move |mask, value, accumulator, prefix, _debug_path| { + summarize_f(mask, value, accumulator, prefix) + }, + ) +} + +/// Shared iterative cached traversal used by the ordinary and debug cata adapters. +fn summarize_cached_body_with_debug<'a, Z, V: 'a, Acc, W, E, DebugPathF, NewAccF, FoldChildF, SummarizeF, const COMPUTE_PATH: bool, const DEBUG_PATH: bool>( + mut zipper: Z, + debug_path_f: DebugPathF, + new_acc_f: NewAccF, + fold_child_f: FoldChildF, + summarize_f: SummarizeF, +) -> Result +where + W: Clone, + Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperPathBuffer, + DebugPathF: Copy + for<'z> Fn(&'z Z, usize) -> &'z [u8], + NewAccF: Copy + Fn(&ByteMask) -> Result, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), E>, + SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8], &[u8]) -> Result, { let focus_depth = zipper.depth(); zipper.prepare_buffers(); let root_child_cnt = zipper.child_count(); if root_child_cnt == 0 { - return summarize_f(&ByteMask::EMPTY, zipper.val(), None, &[]) + let debug_path = if DEBUG_PATH { debug_path_f(&zipper, focus_depth) } else { &[] }; + return summarize_f(&ByteMask::EMPTY, zipper.val(), None, &[], debug_path) } let passthrough_root = root_child_cnt == 1 && !zipper.is_val(); @@ -1226,15 +1255,16 @@ where zipper.descend_indexed_byte(0); while zipper.child_count() < 2 { if !zipper.descend_until() { - return summarize_ascend_to_fork::( + return summarize_ascend_to_fork::( &mut zipper, focus_depth, None, + debug_path_f, new_acc_f, fold_child_f, summarize_f, ).map(|result| match result { - SummarizeAscend::Parent(w) | SummarizeAscend::Focus(w) => w, + AscendResult::Parent(w) | AscendResult::Focus(w) => w, }) } } @@ -1271,16 +1301,17 @@ where } if is_leaf { - let cur_w = match summarize_ascend_to_fork::( + let cur_w = match summarize_ascend_to_fork::( &mut zipper, focus_depth, None, + debug_path_f, new_acc_f, fold_child_f, summarize_f, )? { - SummarizeAscend::Parent(w) => w, - SummarizeAscend::Focus(w) => return Ok(w), + AscendResult::Parent(w) => w, + AscendResult::Focus(w) => return Ok(w), }; DoCache::insert(&mut cache, frame_mut.child_addr, &cur_w); let child_mask = ByteMask::from(zipper.child_mask()); @@ -1298,33 +1329,36 @@ where if stack.is_empty() { return if passthrough_root { - summarize_ascend_to_fork::( + summarize_ascend_to_fork::( &mut zipper, focus_depth, Some(frame.accumulator), + debug_path_f, new_acc_f, fold_child_f, summarize_f, ).map(|result| match result { - SummarizeAscend::Parent(w) | SummarizeAscend::Focus(w) => w, + AscendResult::Parent(w) | AscendResult::Focus(w) => w, }) } else { debug_assert_eq!(zipper.depth(), focus_depth, "must be at the initial focus when summarization is done"); let child_mask = ByteMask::from(zipper.child_mask()); - summarize_f(&child_mask, zipper.val(), Some(frame.accumulator), &[]) + let debug_path = if DEBUG_PATH { debug_path_f(&zipper, focus_depth) } else { &[] }; + summarize_f(&child_mask, zipper.val(), Some(frame.accumulator), &[], debug_path) }; } - let cur_w = match summarize_ascend_to_fork::( + let cur_w = match summarize_ascend_to_fork::( &mut zipper, focus_depth, Some(frame.accumulator), + debug_path_f, new_acc_f, fold_child_f, summarize_f, )? { - SummarizeAscend::Parent(w) => w, - SummarizeAscend::Focus(w) => return Ok(w), + AscendResult::Parent(w) => w, + AscendResult::Focus(w) => return Ok(w), }; let frame_mut = stack.last_mut() @@ -1335,106 +1369,54 @@ where } } -/// Internal implementation behind all cached catas -/// -/// AlgF args: (child_mask, children, value, prefix, debug_path, zipper) -pub(crate) fn into_cata_cached_body<'a, Z, V: 'a, W, E, AlgF, Cache, const JUMPING: bool, const DEBUG_PATH: bool>( - mut zipper: Z, mut alg_f: AlgF +#[inline(always)] +fn debug_origin_path<'a, Z>(zipper: &'a Z, depth: usize) -> &'a [u8] +where + Z: Zipper + ZipperAbsolutePath + ZipperPathBuffer, +{ + let root_prefix_len = zipper.origin_path().len() - zipper.depth(); + // SAFETY: the caller supplies a depth that the zipper occupied earlier in this traversal; + // `prepare_buffers` keeps that path initialized until the next movement. + unsafe { zipper.origin_path_assert_len(root_prefix_len + depth) } +} + +/// Debug-only adapter over the iterative cached cata. The extra path is an absolute path borrowed +/// directly from the zipper buffer and must not influence the cached algebra's result. +pub(crate) fn cata_jumping_cached_debug_body<'a, Z, V: 'a, W, E, AlgF>( + zipper: Z, + alg_f: AlgF, ) -> Result - where - Cache: CacheStrategy, +where + W: Clone, Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer, - AlgF: FnMut(&ByteMask, &mut [W], Option<&V>, &[u8], &[u8], &Z) -> Result + AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8], &[u8]) -> Result, { - zipper.reset(); - zipper.prepare_buffers(); - - let mut stack = Stack::new(); - let mut children = Vec::::new(); - let mut cache = HashMap::::new(); - stack.push_state(&zipper); - 'outer: loop { - let frame_mut = stack.last_mut() - .expect("into_cata stack is emptied before we returned to root"); - // This branch represents the body of the for loop. - if frame_mut.child_idx < frame_mut.child_cnt { - let descended = zipper.descend_indexed_byte(frame_mut.child_idx as usize); - debug_assert!(descended.is_some()); - frame_mut.child_idx += 1; - frame_mut.child_addr = zipper.shared_node_id(); - - // Read and reuse value from cache, if exists - if let Some(cache) = Cache::get(&cache, frame_mut.child_addr) { - // DO NOT modify the W from cache - children.push(cache); - zipper.ascend_byte(); - continue 'outer; - } - - // Descend until leaf or branch - let mut is_leaf = false; - 'descend: while zipper.child_count() < 2 { - if !zipper.descend_until() { - is_leaf = true; - break 'descend; - } - } - - if is_leaf { - // If we encounter a leaf, ascend immediately. - // This branch will preserve the current stack frame. - let cur_w = ascend_to_fork::( - &mut zipper, &mut |mask, children, jump, val, path, z| { - alg_f(mask, children, val, &path[path.len()-jump..], path, z) - }, &mut [])?; - // Put value to cache (1) - Cache::insert(&mut cache, frame_mut.child_addr, &cur_w); - children.push(cur_w); - continue 'outer; - } - - // Enter one recursion step - stack.push_state(&zipper); - continue 'outer; - } - - // This branch represents the rest of the function after the loop - let frame_idx = stack.position; - let StackFrame { child_cnt, .. } = stack.pop_mut() - .expect("we just checked that stack is not empty, pop must return Some"); - let child_start = children.len() - *child_cnt as usize; - let children2 = &mut children[child_start..]; - - if frame_idx == 0 { - // Final branch - debug_assert!(zipper.at_root(), "must be at root when cata is done"); - let value = zipper.val(); - let child_mask = ByteMask::from(zipper.child_mask()); - return if JUMPING && *child_cnt == 1 && value.is_none() { - Ok(children.pop().unwrap()) - } else { - let debug_path = if DEBUG_PATH { - zipper.origin_path() - } else { - &[] - }; - alg_f(&child_mask, children2, value, &[], debug_path, &zipper) - }; - } - - let cur_w = ascend_to_fork::( - &mut zipper, &mut |mask, children, jump, val, path, z| { - alg_f(mask, children, val, &path[path.len()-jump..], path, z) - }, children2)?; - children.truncate(child_start); - - // Exit one recursion step - let frame_mut = stack.last_mut() - .expect("when we're not at root, expect parent stack"); - // Put value to cache (2) after recursion - Cache::insert(&mut cache, frame_mut.child_addr, &cur_w); - children.push(cur_w); - } + let children = std::cell::RefCell::new(CataChildren::::new()); + let children = &children; + let alg_f = &alg_f; + + summarize_cached_body_with_debug::( + zipper, + debug_origin_path::, + move |mask| { + debug_assert!(children.try_borrow_mut().is_ok()); + Ok(unsafe { &mut *children.as_ptr() }.new_acc(mask.count_bits())) + }, + move |_mask, child, accumulator| { + debug_assert!(children.try_borrow_mut().is_ok()); + unsafe { &mut *children.as_ptr() }.push(accumulator, child); + Ok(()) + }, + move |mask, value, accumulator, prefix, debug_path| match accumulator { + Some(accumulator) => { + debug_assert!(children.try_borrow_mut().is_ok()); + unsafe { &mut *children.as_ptr() }.summarize(accumulator, mask.count_bits(), |children| { + alg_f(mask, children, value, prefix, debug_path) + }) + }, + None => alg_f(mask, &mut [], value, prefix, debug_path), + }, + ) } // This is a naive implementation of caching/jumping cata @@ -3821,6 +3803,51 @@ mod tests { assert_eq!(zipper.path(), b"a"); } + #[test] + fn side_effecting_cata_uses_its_focus_as_root() { + let map: PathMap<()> = [ + (b"a".as_slice(), ()), + (b"ab".as_slice(), ()), + (b"ac".as_slice(), ()), + (b"z".as_slice(), ()), + ] + .into_iter() + .collect(); + + let mut stepping_zipper = map.read_zipper(); + stepping_zipper.descend_to(b"a"); + let mut stepping_paths = Vec::new(); + let stepping = stepping_zipper.into_cata_side_effect(|_mask, children: &mut [usize], value, path| { + stepping_paths.push(path.to_vec()); + value.is_some() as usize + children.iter().sum::() + }); + + assert_eq!(stepping, 3); + assert!(stepping_paths.iter().all(|path| path.starts_with(b"a"))); + assert!(stepping_paths.contains(&b"a".to_vec())); + + let mut jumping_zipper = map.read_zipper(); + jumping_zipper.descend_to(b"a"); + let jumping = jumping_zipper.into_cata_jumping_side_effect(|_mask, children: &mut [usize], _jump, value, _path| { + value.is_some() as usize + children.iter().sum::() + }); + + assert_eq!(jumping, 3); + + // A valueless unary focus may lie in a compressed path, so the jumping traversal must + // not ascend past it while looking for the next logical callback point. + let unary_map: PathMap<()> = [(b"abc".as_slice(), ()), (b"z".as_slice(), ())] + .into_iter() + .collect(); + let mut unary_zipper = unary_map.read_zipper(); + unary_zipper.descend_to(b"a"); + let unary = unary_zipper.into_cata_jumping_side_effect(|_mask, children: &mut [usize], _jump, value, _path| { + value.is_some() as usize + children.iter().sum::() + }); + + assert_eq!(unary, 1); + } + /// A bounded deep-path smoke test for the recursive cata. /// /// The implementation uses the Rust call stack once per physical node. `all_dense_nodes` diff --git a/src/utils/debug/morphism_debug.rs b/src/utils/debug/morphism_debug.rs index 3485b48d..388c2e45 100644 --- a/src/utils/debug/morphism_debug.rs +++ b/src/utils/debug/morphism_debug.rs @@ -4,44 +4,135 @@ use crate::utils::ByteMask; use crate::alloc::Allocator; use crate::PathMap; use crate::zipper::*; -use crate::morphisms::{into_cata_cached_body, DoCache}; +use crate::morphisms::cata_jumping_cached_debug_body; /// Debug extension trait for catamorphisms /// /// This trait provides debug-only catamorphism methods that may expose additional /// information useful for debugging and development. pub trait CatamorphismDebug { - /// A version of [`cata_jumping_cached`](crate::morphisms::CatamorphismCached::cata_jumping_cached) where - /// the full path is available to the closure; **For debugging purposes only** + /// A debug-only version of [`cata_jumping_cached`](crate::morphisms::CatamorphismCached::cata_jumping_cached) + /// where the full absolute path is available to the closure. /// /// Using data from the full path for your algorithm **will** lead to incorrect behavior. - /// You must either adapt your algorithm not to require full path data or use the one of - /// the `_side_effect` methods. - fn into_cata_jumping_cached_fallible_debug(self, alg_f: AlgF) -> Result + /// You must either adapt your algorithm not to require full path data or use one of the + /// methods in [`crate::morphisms::CatamorphismSideEffecting`]. + fn cata_jumping_cached_debug(&self, alg_f: AlgF) -> W + where + W: Clone, + AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8], &[u8]) -> W, + Self: Sized, + { + self.cata_jumping_cached_fallible_debug(|mask, children, value, prefix, path| { + Ok::<_, core::convert::Infallible>(alg_f(mask, children, value, prefix, path)) + }) + .unwrap() + } + + /// Fallible form of [`Self::cata_jumping_cached_debug`]. + fn cata_jumping_cached_fallible_debug(&self, alg_f: AlgF) -> Result where W: Clone, AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8], &[u8]) -> Result; } -impl<'a, Z, V: 'a> CatamorphismDebug for Z where Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer { - fn into_cata_jumping_cached_fallible_debug(self, alg_f: AlgF) -> Result +impl<'a, Z, V: 'a> CatamorphismDebug for Z where Z: Clone + Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer { + fn cata_jumping_cached_fallible_debug(&self, alg_f: AlgF) -> Result where W: Clone, AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8], &[u8]) -> Result { - into_cata_cached_body::(self, |mask, children, val, sub_path, debug_path, _z| { - alg_f(mask, children, val, sub_path, debug_path) - }) + cata_jumping_cached_debug_body(self.clone(), alg_f) } } impl CatamorphismDebug for PathMap { - fn into_cata_jumping_cached_fallible_debug(self, alg_f: AlgF) -> Result + fn cata_jumping_cached_fallible_debug(&self, alg_f: AlgF) -> Result where W: Clone, AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8], &[u8]) -> Result { - let rz = self.into_read_zipper(&[]); - rz.into_cata_jumping_cached_fallible_debug(alg_f) + self.read_zipper().cata_jumping_cached_fallible_debug(alg_f) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::morphisms::CatamorphismCached; + + #[test] + fn debug_cached_cata_uses_the_zipper_focus_and_absolute_paths() { + let map: PathMap<()> = [ + (b"a".as_slice(), ()), + (b"ab".as_slice(), ()), + (b"ac".as_slice(), ()), + (b"z".as_slice(), ()), + ] + .into_iter() + .collect(); + let mut zipper = map.read_zipper(); + zipper.descend_to(b"a"); + let paths = std::cell::RefCell::new(Vec::new()); + + let count = zipper.cata_jumping_cached_debug(|_mask, children: &mut [usize], value, _prefix, path| { + paths.borrow_mut().push(path.to_vec()); + value.is_some() as usize + children.iter().sum::() + }); + + assert_eq!(count, 3); + assert!(paths.borrow().iter().all(|path| path.starts_with(b"a"))); + assert!(paths.borrow().contains(&b"a".to_vec())); + assert_eq!(zipper.path(), b"a"); + } + + #[test] + fn debug_cached_cata_does_not_ascend_past_a_unary_focus() { + let map: PathMap<()> = [(b"abc".as_slice(), ()), (b"z".as_slice(), ())] + .into_iter() + .collect(); + let mut zipper = map.read_zipper(); + zipper.descend_to(b"a"); + let paths = std::cell::RefCell::new(Vec::new()); + + let count = zipper.cata_jumping_cached_debug(|_mask, children: &mut [usize], value, _prefix, path| { + paths.borrow_mut().push(path.to_vec()); + value.is_some() as usize + children.iter().sum::() + }); + + assert_eq!(count, 1); + assert!(paths.borrow().iter().all(|path| path.starts_with(b"a"))); + assert_eq!(zipper.path(), b"a"); + } + + #[test] + fn debug_cached_cata_short_circuits_shared_subtries() { + let child: PathMap<()> = [(b"c".as_slice(), ()), (b"d".as_slice(), ())] + .into_iter() + .collect(); + let mut map = PathMap::new(); + let mut writer = map.write_zipper(); + for path in [b"a".as_slice(), b"b".as_slice()] { + writer.reset(); + writer.descend_to(path); + writer.graft_map(child.clone()); + } + drop(writer); + + let cached_calls = std::sync::atomic::AtomicUsize::new(0); + let cached = map.cata_jumping_cached(|_mask, children: &mut [usize], value, _prefix| { + cached_calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + value.is_some() as usize + children.iter().sum::() + }); + + let debug_calls = std::sync::atomic::AtomicUsize::new(0); + let debug = map.cata_jumping_cached_debug(|_mask, children: &mut [usize], value, _prefix, _path| { + debug_calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + value.is_some() as usize + children.iter().sum::() + }); + + assert_eq!(cached, 4); + assert_eq!(debug, cached); + assert_eq!(debug_calls.load(std::sync::atomic::Ordering::Relaxed), cached_calls.load(std::sync::atomic::Ordering::Relaxed)); } } From c9f8e8a5078974bd18b3f24cdc41a6d0281514a4 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Wed, 2 Sep 2026 06:38:48 -0600 Subject: [PATCH 47/50] Cleanups and minor fixes --- benches/catamorphism.rs | 11 +++++++---- src/dense_byte_node.rs | 14 +++++++------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/benches/catamorphism.rs b/benches/catamorphism.rs index 806efe99..9292b505 100644 --- a/benches/catamorphism.rs +++ b/benches/catamorphism.rs @@ -99,7 +99,9 @@ fn recursive_cata_jumping_total_len(bencher: Bencher) { |_| Ok((0usize, 0usize)), |_mask: &ByteMask, w: (usize, usize), acc: &mut (usize, usize)| { acc.0 += w.0; - acc.1 += w.1; + // Every folded child hangs below exactly one branch byte. `prefix` accounts + // for compressed runs separately in `summarize_f` below. + acc.1 += w.1 + w.0; Ok(()) }, |_mask: &ByteMask, val, acc, prefix| { @@ -109,7 +111,7 @@ fn recursive_cata_jumping_total_len(bencher: Bencher) { }, ).unwrap(); }); - assert_eq!(sink.0, MAP_COUNT as usize); + assert_eq!(sink, (MAP_COUNT as usize, MAP_COUNT as usize * 8)); } #[divan::bench()] @@ -128,10 +130,11 @@ fn cached_jumping_cata_total_len(bencher: Bencher) { } for (_byte, child) in mask.iter().zip(children.iter_mut()) { count += child.0; - total_len += child.1 + child.0 * prefix_len; + // The child is below one mask byte as well as this callback's prefix. + total_len += child.1 + child.0 * (prefix_len + 1); } (count, total_len) }); }); - assert_eq!(sink.0, MAP_COUNT as usize); + assert_eq!(sink, (MAP_COUNT as usize, MAP_COUNT as usize * 8)); } diff --git a/src/dense_byte_node.rs b/src/dense_byte_node.rs index 66e88808..bdd40060 100644 --- a/src/dense_byte_node.rs +++ b/src/dense_byte_node.rs @@ -30,7 +30,7 @@ pub struct ByteNode { #[cfg(feature = "nightly")] values: Vec, #[cfg(not(feature = "nightly"))] - pub(crate) values: Vec, + values: Vec, alloc: A, } @@ -396,7 +396,7 @@ impl> ByteNode FinalizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, { let mask = &self.mask; - let mut ws = Some(start_f(mask)?); + let mut ws = start_f(mask)?; for cf in self.values.iter() { let path = &[]; @@ -410,21 +410,21 @@ impl> ByteNode match (cf.rec(), cf.val()) { (Some(rec), Some(val)) => { let w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(rec, Some(val), start_f, fold_child_f, finalize_f, cache)?; - fold_child_f(mask, w, unsafe { ws.as_mut().unwrap_unchecked() })?; + fold_child_f(mask, w, &mut ws)?; }, (Some(rec), None) => { let w = recursive_cata_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(rec, None, start_f, fold_child_f, finalize_f, cache)?; - fold_child_f(mask, summarize_run::<_, _, _, _, _, _, _, COMPUTE_PATH>(None, Some(w), path, start_f, fold_child_f, finalize_f)?, unsafe { ws.as_mut().unwrap_unchecked() })?; + fold_child_f(mask, summarize_run::<_, _, _, _, _, _, _, COMPUTE_PATH>(None, Some(w), path, start_f, fold_child_f, finalize_f)?, &mut ws)?; }, (None, Some(val)) => { - fold_child_f(mask, summarize_run::<_, _, _, _, _, _, _, COMPUTE_PATH>(Some(val), None, path, start_f, fold_child_f, finalize_f)?, unsafe { ws.as_mut().unwrap_unchecked() })?; + fold_child_f(mask, summarize_run::<_, _, _, _, _, _, _, COMPUTE_PATH>(Some(val), None, path, start_f, fold_child_f, finalize_f)?, &mut ws)?; }, (None, None) => { - fold_child_f(mask, summarize_run::<_, _, _, _, _, _, _, COMPUTE_PATH>(None, None, path, start_f, fold_child_f, finalize_f)?, unsafe { ws.as_mut().unwrap_unchecked() })?; + fold_child_f(mask, summarize_run::<_, _, _, _, _, _, _, COMPUTE_PATH>(None, None, path, start_f, fold_child_f, finalize_f)?, &mut ws)?; }, } } - finalize_f(mask, passed_in_val, Some(unsafe { std::mem::take(&mut ws).unwrap_unchecked() }), &[]) + finalize_f(mask, passed_in_val, Some(ws), &[]) } } From 45f879176ea240ec2b308a57d19d008ebdbc8e6b Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Wed, 2 Sep 2026 06:55:21 -0600 Subject: [PATCH 48/50] Adding miri-specific input vectors to the to some of the new tests that took too long to run under miri --- src/morphisms.rs | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/morphisms.rs b/src/morphisms.rs index f92c7727..db4041a1 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -2208,32 +2208,54 @@ pub(crate) mod cached_catamorphism_tests { } /// A 256-way logical branch exercises storage configurations that use wide byte-indexed - /// nodes without making the test depend on any particular node representation. + /// nodes without making the test depend on any particular node representation. Miri uses + /// representative mask-word-boundary bytes instead of the full branch fanout. #[test] fn recursive_cata_wide_logical_branch() { + #[cfg(miri)] + let keys: Vec> = [0, 1, 63, 64, 127, 128, 191, 192, 254, 255] + .into_iter() + .map(|byte| vec![byte, b'a', byte]) + .collect(); + #[cfg(not(miri))] let keys: Vec> = (0u8..=u8::MAX) .map(|byte| vec![byte, b'a', byte]) .collect(); assert_logical_key_case_roundtrips(&keys); - for byte in [0, 63, 64, 127, 128, 191, 192, 255] { + #[cfg(miri)] + let focus_bytes = [0, 64, 128, 255]; + #[cfg(not(miri))] + let focus_bytes = [0, 63, 64, 127, 128, 191, 192, 255]; + for byte in focus_bytes { assert_logical_focus_roundtrips(&keys, &[byte]); } } /// Differential coverage over seeded logical maps and root/value/branch/proper-prefix - /// focuses. The seed is fixed so any failure is reproducible without observing concrete + /// focuses. The seed is fixed so any failure is reproducible without observing concrete /// node layout. #[test] fn recursive_cata_randomized_maps_and_foci() { - use rand::{Rng, SeedableRng}; - use rand::rngs::StdRng; + const SEED: [u8; 32] = [31; 32]; + #[cfg(miri)] + const ROUNDS: usize = 1; + #[cfg(not(miri))] const ROUNDS: usize = 64; + + #[cfg(miri)] + const KEYS_PER_ROUND: usize = 12; + #[cfg(not(miri))] const KEYS_PER_ROUND: usize = 48; + + #[cfg(miri)] + const FOCI_PER_ROUND: usize = 3; + #[cfg(not(miri))] const FOCI_PER_ROUND: usize = 8; - const SEED: [u8; 32] = [31; 32]; + use rand::{Rng, SeedableRng}; + use rand::rngs::StdRng; let mut rng = StdRng::from_seed(SEED); for round in 0..ROUNDS { From 67d62249ccf6060fe2f5ff7f7c93314bf38351df Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Wed, 2 Sep 2026 07:45:52 -0600 Subject: [PATCH 49/50] Moving `val_count` method from ZipperMoving trait to CatamorphismCached trait Implementing ZipperConcrete on WriteZipper flavors Dropping a handful of unneeded bounds on catamorphism traits Ripping out parallel val_count (and goat_val_count) implementations and benchmarks --- benches/act_paths.rs | 2 +- benches/binary_keys.rs | 16 ---------- benches/catamorphism.rs | 6 ++-- benches/cities.rs | 19 ------------ benches/multiplicities.rs | 2 +- benches/oeis.rs | 3 +- benches/shakespeare.rs | 38 ------------------------ benches/sparse_keys.rs | 20 ------------- benches/superdense_keys.rs | 15 ---------- pathmap-derive/src/lib.rs | 6 ---- src/arena_compact.rs | 17 ----------- src/dense_byte_node.rs | 31 +------------------ src/dependent_zipper.rs | 3 -- src/empty_node.rs | 6 +--- src/empty_zipper.rs | 1 - src/experimental.rs | 2 -- src/line_list_node.rs | 40 +------------------------ src/morphisms.rs | 31 ++++++++++--------- src/overlay_zipper.rs | 4 --- src/path_tracker.rs | 1 - src/prefix_zipper.rs | 4 --- src/product_zipper.rs | 8 ----- src/random.rs | 2 +- src/tiny_node.rs | 8 ++--- src/trie_map.rs | 9 ------ src/trie_node.rs | 54 ++++------------------------------ src/utils/debug/diff_zipper.rs | 6 ---- src/write_zipper.rs | 34 ++++++++++++++------- src/zipper.rs | 22 +------------- 29 files changed, 59 insertions(+), 351 deletions(-) diff --git a/benches/act_paths.rs b/benches/act_paths.rs index b95691b4..061c5c2d 100644 --- a/benches/act_paths.rs +++ b/benches/act_paths.rs @@ -26,7 +26,7 @@ use divan::{Bencher, Divan, counter::ItemsCount}; use pathmap::PathMap; use pathmap::arena_compact::{ACTOutputStream, ArenaCompactTree}; use pathmap::paths_serialization::{for_each_deserialized_path, serialize_paths}; -use pathmap::zipper::ZipperMoving; +use pathmap::morphisms::CatamorphismCached; use rand::{Rng, SeedableRng, rngs::StdRng}; use std::path::Path; diff --git a/benches/binary_keys.rs b/benches/binary_keys.rs index 350651bc..b886de65 100644 --- a/benches/binary_keys.rs +++ b/benches/binary_keys.rs @@ -131,22 +131,6 @@ fn binary_val_count_bench(bencher: Bencher, n: u64) { assert_eq!(sink, n as usize); } -#[divan::bench(args = [125, 250, 500, 1000, 2000, 4000, 100000])] -fn binary_goat_val_count_bench(bencher: Bencher, n: u64) { - - let keys = make_keys(n as usize, 1); - - let mut map: PathMap = PathMap::new(); - for i in 0..n { map.set_val_at(&keys[i as usize], i); } - - //Benchmark the time taken to count the number of values in the map - let mut sink = 0; - bencher.bench_local(|| { - *black_box(&mut sink) = map.goat_val_count() - }); - assert_eq!(sink, n as usize); -} - #[divan::bench(args = [50, 100, 200, 400, 800, 1600])] fn binary_drop_head(bencher: Bencher, n: u64) { diff --git a/benches/catamorphism.rs b/benches/catamorphism.rs index 9292b505..34e1eaef 100644 --- a/benches/catamorphism.rs +++ b/benches/catamorphism.rs @@ -38,7 +38,7 @@ fn build_binary_tree_map() -> PathMap<()> { } #[divan::bench()] -fn recursive_cata_jumping_val_count(bencher: Bencher) { +fn factored_cata_jumping_val_count(bencher: Bencher) { let map = build_map(MAP_COUNT); let mut sink = 0usize; bencher.bench_local(|| { @@ -53,7 +53,7 @@ fn recursive_cata_jumping_val_count(bencher: Bencher) { } #[divan::bench()] -fn recursive_cata_binary_tree_leaf_count(bencher: Bencher) { +fn factored_cata_binary_tree_leaf_count(bencher: Bencher) { let map = build_binary_tree_map(); let mut sink = 0usize; bencher.bench_local(|| { @@ -90,7 +90,7 @@ fn cached_jumping_cata_val_count(bencher: Bencher) { } #[divan::bench()] -fn recursive_cata_jumping_total_len(bencher: Bencher) { +fn factored_cata_jumping_total_len(bencher: Bencher) { let map = build_map(MAP_COUNT); let mut sink = (0usize, 0usize); bencher.bench_local(|| { diff --git a/benches/cities.rs b/benches/cities.rs index cc5cbc94..231cb6e0 100644 --- a/benches/cities.rs +++ b/benches/cities.rs @@ -168,25 +168,6 @@ fn cities_val_count(bencher: Bencher) { assert_eq!(sink, unique_count); } -#[divan::bench()] -fn cities_goat_val_count(bencher: Bencher) { - - let pairs = read_data(); - let mut map = PathMap::new(); - let mut unique_count = 0; - for (k, v) in pairs.iter() { - if map.set_val_at(k, *v).is_none() { - unique_count += 1; - } - } - - let mut sink = 0; - bencher.bench_local(|| { - *black_box(&mut sink) = map.goat_val_count(); - }); - assert_eq!(sink, unique_count); -} - #[cfg(feature="arena_compact")] #[divan::bench()] fn cities_val_count_act(bencher: Bencher) { diff --git a/benches/multiplicities.rs b/benches/multiplicities.rs index 4bdd753b..99ed9d4c 100644 --- a/benches/multiplicities.rs +++ b/benches/multiplicities.rs @@ -1,5 +1,5 @@ use pathmap::*; -use pathmap::zipper::{ZipperMoving, ZipperWriting}; +use pathmap::zipper::{CatamorphismCached, ZipperWriting}; fn main() { const SILLY_LARGE_COUNTS: bool = false; diff --git a/benches/oeis.rs b/benches/oeis.rs index afb485f4..a179b79b 100644 --- a/benches/oeis.rs +++ b/benches/oeis.rs @@ -1,7 +1,6 @@ use std::io::Read; -use std::usize; use pathmap::PathMap; -use pathmap::zipper::{Zipper, ZipperValues, ZipperMoving, ZipperPath, ZipperWriting, ZipperCreation}; +use pathmap::zipper::{Zipper, ZipperValues, ZipperMoving, ZipperPath, ZipperWriting, ZipperCreation, CatamorphismCached}; use num::BigInt; use divan::{Divan, Bencher, black_box}; diff --git a/benches/shakespeare.rs b/benches/shakespeare.rs index 5528f739..2040ba54 100644 --- a/benches/shakespeare.rs +++ b/benches/shakespeare.rs @@ -113,25 +113,6 @@ fn shakespeare_words_val_count(bencher: Bencher) { assert_eq!(sink, unique_count); } -#[divan::bench()] -fn shakespeare_words_goat_val_count(bencher: Bencher) { - - let strings = read_data(true); - let mut map = PathMap::new(); - let mut unique_count = 0; - for (v, k) in strings.iter().enumerate() { - if map.set_val_at(k, v).is_none() { - unique_count += 1; - } - } - - let mut sink = 0; - bencher.bench_local(|| { - *black_box(&mut sink) = map.goat_val_count(); - }); - assert_eq!(sink, unique_count); -} - #[divan::bench()] fn shakespeare_sentences_insert(bencher: Bencher) { @@ -187,25 +168,6 @@ fn shakespeare_sentences_val_count(bencher: Bencher) { assert_eq!(sink, unique_count); } -#[divan::bench()] -fn shakespeare_sentences_goat_val_count(bencher: Bencher) { - - let strings = read_data(false); - let mut map = PathMap::new(); - let mut unique_count = 0; - for (v, k) in strings.iter().enumerate() { - if map.set_val_at(k, v).is_none() { - unique_count += 1; - } - } - - let mut sink = 0; - bencher.bench_local(|| { - *black_box(&mut sink) = map.goat_val_count(); - }); - assert_eq!(sink, unique_count); -} - #[cfg(feature="arena_compact")] #[divan::bench()] fn shakespeare_sentences_val_count_act(bencher: Bencher) { diff --git a/benches/sparse_keys.rs b/benches/sparse_keys.rs index 444374ab..d8c905fb 100644 --- a/benches/sparse_keys.rs +++ b/benches/sparse_keys.rs @@ -154,26 +154,6 @@ fn sparse_val_count_bench(bencher: Bencher, n: u64) { assert_eq!(sink, n as usize); } -#[divan::bench(args = [125, 250, 500, 1000, 2000, 4000, 20_000, 100_000])] -fn sparse_goat_val_count_bench(bencher: Bencher, n: u64) { - - let mut r = StdRng::seed_from_u64(1); - let keys: Vec> = (0..n).into_iter().map(|_| { - let len = (r.random::() % 18) + 3; //length between 3 and 20 chars - (0..len).into_iter().map(|_| r.random::()).collect() - }).collect(); - - let mut map: PathMap = PathMap::new(); - for i in 0..n { map.set_val_at(&keys[i as usize], i); } - - //Benchmark the time taken to count the number of values in the map - let mut sink = 0; - bencher.bench_local(|| { - *black_box(&mut sink) = map.goat_val_count() - }); - assert_eq!(sink, n as usize); -} - #[divan::bench(args = [50, 100, 200, 400, 800, 1600])] fn binary_drop_head(bencher: Bencher, n: u64) { diff --git a/benches/superdense_keys.rs b/benches/superdense_keys.rs index 61d8cdca..9e258691 100644 --- a/benches/superdense_keys.rs +++ b/benches/superdense_keys.rs @@ -325,21 +325,6 @@ fn superdense_val_count_bench(bencher: Bencher, n: u64) { assert_eq!(sink, n as usize); } -#[divan::bench(sample_size = 1, args = [100, 200, 400, 800, 1600, 3200, 20_000, 100_000])] -fn superdense_goat_val_count_bench(bencher: Bencher, n: u64) { - - let mut map: PathMap = PathMap::new(); - for i in 0..n { map.set_val_at(prefix_key(&i), i); } - - //Benchmark the time taken to count the number of values in the map - let mut sink = 0; - bencher.bench_local(|| { - *black_box(&mut sink) = map.goat_val_count() - }); - assert_eq!(sink, n as usize); -} - - #[cfg(feature="arena_compact")] #[divan::bench(sample_size = 1, args = [100, 200, 400, 800, 1600, 3200, 20_000, 100_000])] fn superdense_val_count_bench_act(bencher: Bencher, n: u64) { diff --git a/pathmap-derive/src/lib.rs b/pathmap-derive/src/lib.rs index f3dc8cc0..05d0a780 100644 --- a/pathmap-derive/src/lib.rs +++ b/pathmap-derive/src/lib.rs @@ -490,12 +490,6 @@ fn derive_poly_zipper_with_traits( } } - fn val_count(&self) -> usize { - match self { - #(#variant_arms => inner.val_count(),)* - } - } - fn descend_to>(&mut self, k: K) { match self { #(#variant_arms => inner.descend_to(k),)* diff --git a/src/arena_compact.rs b/src/arena_compact.rs index d1b4ebfd..fe70d3f6 100644 --- a/src/arena_compact.rs +++ b/src/arena_compact.rs @@ -2918,23 +2918,6 @@ where Storage: AsRef<[u8]> self.invalid = 0; } - /// Returns the total number of values contained at and below the zipper's focus, including the focus itself - /// - /// WARNING: This is not a cheap method. It may have an order-N cost - fn val_count(&self) -> usize { - timed_span!(ValueCount, COUNTERS); - let mut zipper = self.clone(); - zipper.reset(); - let mut count = 0; - if zipper.is_val() { - count += 1; - } - while zipper.to_next_val() { - count += 1; - } - count - } - /// Moves the zipper deeper into the trie, to the `key` specified relative to the current zipper focus /// /// Returns `true` if the zipper points to an existing path within the tree, otherwise `false`. The diff --git a/src/dense_byte_node.rs b/src/dense_byte_node.rs index bdd40060..ed4aeb28 100644 --- a/src/dense_byte_node.rs +++ b/src/dense_byte_node.rs @@ -1057,37 +1057,8 @@ impl> TrieNode let k = k as usize; (next_token, &ALL_BYTES[k..=k], cf.rec(), cf.val()) } - fn node_val_count(&self, cache: &mut std::collections::HashMap) -> usize { - //Discussion: These two implementations do the same thing but with a slightly different ordering of - // the operations. In `all_dense_nodes`, the "Branchy" impl wins. But in a mixed-node setting, the - // IMPL B is the winner. My suspicion is that the ListNode's heavily branching structure leads to - // underutilization elsewhere in the CPU so we get better instruction parallelism with IMPL B. - - //IMPL A "Branchy" - // let mut result = 0; - // for cf in self.values.iter() { - // if cf.value.is_some() { - // result += 1; - // } - // match &cf.rec { - // Some(rec) => result += rec.borrow().node_subtree_len(), - // None => {} - // } - // } - // result - - //IMPL B "Arithmetic" - return self.values.iter().rfold(0, |t, cf| { - t + cf.has_val() as usize + cf.rec().map(|r| val_count_below_node(r, cache)).unwrap_or(0) - }); - } -/* fn node_goat_val_count(&self) -> usize { - return self.values.iter().rfold(0, |t, cf| { - t + cf.has_val() as usize + cf.rec().map(|r| r.as_tagged().node_goat_val_count()).unwrap_or(0) - }); - }*/ #[inline] - fn node_goat_val_count(&self) -> usize { + fn node_val_count(&self) -> usize { let mut result = 0; for cf in self.values.iter() { result += cf.has_val() as usize diff --git a/src/dependent_zipper.rs b/src/dependent_zipper.rs index 8786a470..2dc413ec 100644 --- a/src/dependent_zipper.rs +++ b/src/dependent_zipper.rs @@ -348,9 +348,6 @@ impl<'trie, PrimaryZ, SecondaryZ, V, C, F : Clone + for <'a> FnOnce(C, &'a [u8], self.secondary.clear(); self.primary.reset(); } - fn val_count(&self) -> usize { - unimplemented!("method will probably get removed") - } fn descend_to_existing>(&mut self, path: K) -> usize { let mut path = path.as_ref(); let mut descended = 0; diff --git a/src/empty_node.rs b/src/empty_node.rs index 722952a3..3477cba2 100644 --- a/src/empty_node.rs +++ b/src/empty_node.rs @@ -1,6 +1,5 @@ use core::fmt::Debug; -use std::collections::HashMap; use crate::alloc::Allocator; use crate::trie_node::*; @@ -67,10 +66,7 @@ impl TrieNode for EmptyNode { fn next_items(&self, _token: IterToken) -> (IterToken, &[u8], Option<&TrieNodeODRc>, Option<&V>) { (NODE_ITER_FINISHED, &[], None, None) } - fn node_val_count(&self, _cache: &mut HashMap) -> usize { - 0 - } - fn node_goat_val_count(&self) -> usize { + fn node_val_count(&self) -> usize { 0 } fn node_child_iter_start(&self) -> (u64, Option<&TrieNodeODRc>) { diff --git a/src/empty_zipper.rs b/src/empty_zipper.rs index b7635609..66225d89 100644 --- a/src/empty_zipper.rs +++ b/src/empty_zipper.rs @@ -36,7 +36,6 @@ impl ZipperMoving for EmptyZipper { #[inline] fn focus_byte(&self) -> Option { self.path.last().cloned() } fn reset(&mut self) { self.path.truncate(self.path_start_idx) } - fn val_count(&self) -> usize { 0 } fn descend_to>(&mut self, k: K) { self.path.extend_from_slice(k.as_ref()); } diff --git a/src/experimental.rs b/src/experimental.rs index b3269368..fa38217e 100644 --- a/src/experimental.rs +++ b/src/experimental.rs @@ -53,7 +53,6 @@ impl ZipperMoving for FullZipper { #[inline] fn focus_byte(&self) -> Option { self.path.last().cloned() } fn reset(&mut self) { self.path.clear() } - fn val_count(&self) -> usize { usize::MAX/2 } // usize::MAX is a dangerous default for overflow fn descend_to>(&mut self, k: K) { self.path.extend_from_slice(k.as_ref()); } @@ -126,7 +125,6 @@ impl ZipperMoving for NullZipper { #[inline] fn focus_byte(&self) -> Option { None } fn reset(&mut self) {} - fn val_count(&self) -> usize { 0 } fn descend_to>(&mut self, _k: K) {} fn descend_to_byte(&mut self, _k: u8) {} fn descend_indexed_byte(&mut self, _idx: usize) -> Option { None } diff --git a/src/line_list_node.rs b/src/line_list_node.rs index ea361e14..6e9c5640 100644 --- a/src/line_list_node.rs +++ b/src/line_list_node.rs @@ -1999,45 +1999,7 @@ impl TrieNode for LineListNode } } #[inline] - fn node_val_count(&self, cache: &mut std::collections::HashMap) -> usize { - let mut result = 0; - if self.is_used_value_0() { - result += 1; - } - if self.is_used_value_1() { - result += 1; - } - if self.is_used_child_0() { - let child_node = unsafe{ self.child_in_slot::<0>() }; - result += val_count_below_node(child_node, cache); - } - if self.is_used_child_1() { - let child_node = unsafe{ self.child_in_slot::<1>() }; - result += val_count_below_node(child_node, cache); - } - result - } -/* #[inline] - fn node_goat_val_count(&self) -> usize { - let mut result = 0; - if self.is_used_value_0() { - result += 1; - } - if self.is_used_value_1() { - result += 1; - } - if self.is_used_child_0() { - let child_node = unsafe{ self.child_in_slot::<0>() }; - result += child_node.as_tagged().node_goat_val_count(); - } - if self.is_used_child_1() { - let child_node = unsafe{ self.child_in_slot::<1>() }; - result += child_node.as_tagged().node_goat_val_count(); - } - result - }*/ - #[inline] - fn node_goat_val_count(&self) -> usize { + fn node_val_count(&self) -> usize { //Here are 3 alternative implementations. They're basically the same in perf, with a slight edge to the // inline bitwise arithmetic version. diff --git a/src/morphisms.rs b/src/morphisms.rs index db4041a1..7229b692 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -168,8 +168,7 @@ macro_rules! define_cached_cata_trait { fn cata_cached(&self, alg_f: AlgF) -> W where W: Clone, - AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> W, - Self: Sized, + AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> W { self.cata_cached_fallible(|mask, children, val| -> Result { Ok(alg_f(mask, children, val)) @@ -182,8 +181,7 @@ macro_rules! define_cached_cata_trait { fn cata_cached_fallible(&self, alg_f: AlgF) -> Result where W: Clone, - AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> Result, - Self: Sized, + AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> Result { self.cata_jumping_cached_fallible(|mask, children, val, prefix| { let mut w = alg_f(mask, children, val)?; @@ -230,8 +228,7 @@ macro_rules! define_cached_cata_trait { fn cata_jumping_cached(&self, alg_f: AlgF) -> W where W: Clone, - AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> W, - Self: Sized, + AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> W { self.cata_jumping_cached_fallible(|mask, children, val, prefix| -> Result { Ok(alg_f(mask, children, val, prefix)) @@ -244,8 +241,7 @@ macro_rules! define_cached_cata_trait { fn cata_jumping_cached_fallible(&self, alg_f: AlgF) -> Result where W: Clone, - AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> Result, - Self: Sized, + AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> Result { let children = std::cell::RefCell::new(CataChildren::::new()); let children = &children; @@ -276,7 +272,6 @@ macro_rules! define_cached_cata_trait { /// Hashes the logical trie and all of its values. fn hash(&self) -> u128 where - Self: Sized, V: std::hash::Hash, { self.hash_with(|v| { @@ -289,7 +284,6 @@ macro_rules! define_cached_cata_trait { /// Hashes the logical trie using the provided function to hash values. fn hash_with(&self, val_hash: F) -> u128 where - Self: Sized, F: Fn(&V) -> u128, { self.cata_cached(|bm, hs, mv| { @@ -301,6 +295,16 @@ macro_rules! define_cached_cata_trait { }) } + /// Returns the total number of values contained at and below the zipper's focus, including the + /// focus itself + fn val_count(&self) -> usize { + self.factored_cata_jumping::<_, _, Infallible, _, _, _, false>( + |_| Ok(0usize), + |_mask, w: usize, total| { *total += w; Ok(()) }, + |_mask, v, total, _| Ok((v.is_some() as usize) + total.unwrap_or(0)), + ).unwrap_or(0) + } + /// A low-level cached catamorphism API that decomposes the algebra into multiple /// functions and can avoid redundant path computation. /// @@ -351,8 +355,7 @@ macro_rules! define_cached_cata_trait { W: Clone, NewAccF: Copy + Fn(&ByteMask) -> Result, FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, - SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, - Self: Sized; + SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result; /// A **stepping** catamorphism based on a similar factored algebra to [`Self::factored_cata_jumping`] /// @@ -636,7 +639,7 @@ impl SplitCataJumping { } } -impl<'a, Z, V: 'a> CatamorphismSideEffecting for Z where Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer { +impl<'a, Z, V: 'a> CatamorphismSideEffecting for Z where Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperAbsolutePath + ZipperPathBuffer { fn into_cata_side_effect_fallible(self, mut alg_f: AlgF) -> Result where AlgF: FnMut(&ByteMask, &mut [W], Option<&V>, &[u8]) -> Result, { @@ -669,7 +672,7 @@ impl Catamorph } } -impl<'a, Z, V: Clone + Send + Sync + 'a, A: Allocator> CatamorphismCached for Z where Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperInfallibleSubtries { +impl CatamorphismCached for Z where Z: Zipper + ZipperConcrete + ZipperInfallibleSubtries { fn factored_cata_jumping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where W: Clone, diff --git a/src/overlay_zipper.rs b/src/overlay_zipper.rs index 96b847e8..9aa6dec5 100644 --- a/src/overlay_zipper.rs +++ b/src/overlay_zipper.rs @@ -160,10 +160,6 @@ impl ZipperMoving self.b.reset(); } - fn val_count(&self) -> usize { - todo!() - } - fn descend_to>(&mut self, path: P) { let path = path.as_ref(); self.a.descend_to(path); diff --git a/src/path_tracker.rs b/src/path_tracker.rs index 357d65d4..1a2056b3 100644 --- a/src/path_tracker.rs +++ b/src/path_tracker.rs @@ -74,7 +74,6 @@ impl ZipperMoving for PathTracker { None } } - fn val_count(&self) -> usize { self.zipper.val_count() } fn descend_to>(&mut self, path: K) { let path = path.as_ref(); self.path.extend_from_slice(path); diff --git a/src/prefix_zipper.rs b/src/prefix_zipper.rs index 93cf40c6..f94623d3 100644 --- a/src/prefix_zipper.rs +++ b/src/prefix_zipper.rs @@ -389,10 +389,6 @@ impl<'prefix, Z> ZipperMoving for PrefixZipper<'prefix, Z> self.set_valid(0); } - fn val_count(&self) -> usize { - self.source.val_count() - } - fn descend_to_existing>(&mut self, patho: K) -> usize { if self.position.is_invalid() { return 0; diff --git a/src/product_zipper.rs b/src/product_zipper.rs index 88e54bd4..d93f5fad 100644 --- a/src/product_zipper.rs +++ b/src/product_zipper.rs @@ -174,10 +174,6 @@ impl<'trie, V: Clone + Send + Sync + Unpin + 'trie, A: Allocator + 'trie> Zipper self.factor_paths.clear(); self.z.reset() } - fn val_count(&self) -> usize { - debug_assert!(self.focus_factor() == self.factor_count() - 1); - self.z.val_count() - } fn descend_to_existing>(&mut self, k: K) -> usize { let k = k.as_ref(); let mut descended = 0; @@ -684,10 +680,6 @@ impl<'trie, PrimaryZ, SecondaryZ, V> ZipperMoving for ProductZipperG<'trie, Prim } self.primary.reset(); } - #[inline] - fn val_count(&self) -> usize { - unimplemented!("method will probably get removed") - } fn descend_to_existing>(&mut self, path: K) -> usize { let mut path = path.as_ref(); let mut descended = 0; diff --git a/src/random.rs b/src/random.rs index 829b1720..ab1a89a9 100644 --- a/src/random.rs +++ b/src/random.rs @@ -8,7 +8,7 @@ use std::marker::PhantomData; use rand::distr::Uniform; use crate::TrieValue; use crate::utils::{BitMask, ByteMask}; -use crate::zipper::{ReadZipperUntracked, Zipper, ZipperPath, ZipperReadOnlyIteration, ZipperMoving, ZipperReadOnlyValues}; +use crate::zipper::{ReadZipperUntracked, Zipper, ZipperPath, ZipperReadOnlyIteration, ZipperMoving, ZipperReadOnlyValues, CatamorphismCached}; // Re-export generic combinators pub use distr_combinators::*; diff --git a/src/tiny_node.rs b/src/tiny_node.rs index 41886566..c79b9401 100644 --- a/src/tiny_node.rs +++ b/src/tiny_node.rs @@ -227,12 +227,8 @@ impl<'a, V: Clone + Send + Sync, A: Allocator> TrieNode for TinyRefNode<'a fn new_iter_token(&self) -> IterToken { unreachable!() } fn iter_token_for_path(&self, _key: &[u8]) -> IterToken { unreachable!() } fn next_items(&self, _token: IterToken) -> (IterToken, &'a[u8], Option<&TrieNodeODRc>, Option<&V>) { unreachable!() } - fn node_val_count(&self, cache: &mut std::collections::HashMap) -> usize { - let temp_node = self.into_full().unwrap(); - temp_node.node_val_count(cache) - } - fn node_goat_val_count(&self) -> usize { - self.into_full().unwrap().node_goat_val_count() + fn node_val_count(&self) -> usize { + self.into_full().unwrap().node_val_count() } fn node_child_iter_start(&self) -> (u64, Option<&TrieNodeODRc>) { if self.is_used_child() { diff --git a/src/trie_map.rs b/src/trie_map.rs index b24cce7d..4eb7fa3d 100644 --- a/src/trie_map.rs +++ b/src/trie_map.rs @@ -499,15 +499,6 @@ impl PathMap { /// /// WARNING: This is not a cheap method. It may have an order-N cost pub fn val_count(&self) -> usize { - let root_val = unsafe{ &*self.root_val.get() }.is_some() as usize; - match self.root() { - Some(root) => val_count_below_root(root.as_tagged()) + root_val, - None => root_val - } - } - - /// GOAT, temporary method to do side-by-side comparison between abstracted val_count and bespoke version - pub fn goat_val_count(&self) -> usize { match self.root() { Some(_root) => { match self.factored_cata_jumping::<_, _, Infallible, _, _, _, false>( diff --git a/src/trie_node.rs b/src/trie_node.rs index bfbb55e4..4598b309 100644 --- a/src/trie_node.rs +++ b/src/trie_node.rs @@ -215,15 +215,9 @@ pub(crate) trait TrieNode: TrieNodeDowncas /// - `value` that exists at the path, or `None` fn next_items(&self, token: IterToken) -> (IterToken, &[u8], Option<&TrieNodeODRc>, Option<&V>); - /// Returns the total number of leaves contained within the whole subtree defined by the node - /// GOAT, this should be deprecated - fn node_val_count(&self, cache: &mut std::collections::HashMap) -> usize; - /// Returns the number of values contained within the node itself, irrespective of the positions within /// the node; does not include onward links - /// - /// GOAT, this should replace node_val_count - fn node_goat_val_count(&self) -> usize; + fn node_val_count(&self) -> usize; /// Returns the first downstream child of a node, and a token that can be used to access subsequent children /// @@ -1243,23 +1237,12 @@ mod tagged_node_ref { } #[inline] - pub fn node_val_count(&self, cache: &mut std::collections::HashMap) -> usize { + pub fn node_val_count(&self) -> usize { match self { - Self::DenseByteNode(node) => node.node_val_count(cache), - Self::LineListNode(node) => node.node_val_count(cache), - Self::CellByteNode(node) => node.node_val_count(cache), - Self::TinyRefNode(node) => node.node_val_count(cache), - Self::EmptyNode => 0, - } - } - - #[inline] - pub fn node_goat_val_count(&self) -> usize { - match self { - Self::DenseByteNode(node) => node.node_goat_val_count(), - Self::LineListNode(node) => node.node_goat_val_count(), - Self::CellByteNode(node) => node.node_goat_val_count(), - Self::TinyRefNode(node) => node.node_goat_val_count(), + Self::DenseByteNode(node) => node.node_val_count(), + Self::LineListNode(node) => node.node_val_count(), + Self::CellByteNode(node) => node.node_val_count(), + Self::TinyRefNode(node) => node.node_val_count(), Self::EmptyNode => 0, } } @@ -2386,31 +2369,6 @@ mod tagged_node_ref { } } -/// Returns the count of values in the subtrie descending from the node, caching shared subtries -pub(crate) fn val_count_below_root(node: TaggedNodeRef) -> usize { - let mut cache = std::collections::HashMap::new(); - node.node_val_count(&mut cache) -} - -pub(crate) fn val_count_below_node(node: &TrieNodeODRc, cache: &mut std::collections::HashMap) -> usize { - if node.is_empty() { - return 0 - } - if node.refcount() > 1 { - let hash = node.shared_node_id(); - match cache.get(&hash) { - Some(cached) => *cached, - None => { - let val = node.as_tagged().node_val_count(cache); - cache.insert(hash, val); - val - }, - } - } else { - node.as_tagged().node_val_count(cache) - } -} - /// Internal implementation of `CatamorphismCached::factored_cata_jumping` pub(crate) fn recursive_cata_cached( node: &TrieNodeODRc, diff --git a/src/utils/debug/diff_zipper.rs b/src/utils/debug/diff_zipper.rs index c0016f0f..127fb8ba 100644 --- a/src/utils/debug/diff_zipper.rs +++ b/src/utils/debug/diff_zipper.rs @@ -71,12 +71,6 @@ impl ZipperMoving for DiffZi println!("DiffZipper: reset") } } - fn val_count(&self) -> usize { - let a = self.a.val_count(); - let b = self.b.val_count(); - assert_eq!(a, b); - a - } fn descend_to>(&mut self, path: P) { let path = path.as_ref(); self.a.descend_to(path); diff --git a/src/write_zipper.rs b/src/write_zipper.rs index 2509091f..93bce9de 100644 --- a/src/write_zipper.rs +++ b/src/write_zipper.rs @@ -420,12 +420,18 @@ impl<'a, V: Clone + Send + Sync + Unpin, A: Allocator + 'a> ZipperInfallibleSubt fn try_borrow_focus(&self) -> Option> { self.z.try_borrow_focus() } } +impl ZipperConcrete for WriteZipperTracked<'_, '_, V, A> { + #[inline] + fn shared_node_id(&self) -> Option { None } + #[inline] + fn is_shared(&self) -> bool { false } +} + impl<'a, 'path, V: Clone + Send + Sync + Unpin, A: Allocator + 'a> ZipperMoving for WriteZipperTracked<'a, 'path, V, A> { #[inline] fn depth(&self) -> usize { self.z.depth() } fn at_root(&self) -> bool { self.z.at_root() } #[inline] fn focus_byte(&self) -> Option { self.z.focus_byte() } fn reset(&mut self) { self.z.reset() } - fn val_count(&self) -> usize { self.z.val_count() } fn descend_to>(&mut self, k: K) { self.z.descend_to(k) } fn descend_to_byte(&mut self, k: u8) { self.z.descend_to_byte(k) } fn descend_indexed_byte(&mut self, child_idx: usize) -> Option { self.z.descend_indexed_byte(child_idx) } @@ -586,12 +592,18 @@ impl<'a, V: Clone + Send + Sync + Unpin, A: Allocator + 'a> ZipperInfallibleSubt fn try_borrow_focus(&self) -> Option> { self.z.try_borrow_focus() } } +impl ZipperConcrete for WriteZipperUntracked<'_, '_, V, A> { + #[inline] + fn shared_node_id(&self) -> Option { None } + #[inline] + fn is_shared(&self) -> bool { false } +} + impl<'a, 'path, V: Clone + Send + Sync + Unpin, A: Allocator + 'a> ZipperMoving for WriteZipperUntracked<'a, 'path, V, A> { #[inline] fn depth(&self) -> usize { self.z.depth() } fn at_root(&self) -> bool { self.z.at_root() } #[inline] fn focus_byte(&self) -> Option { self.z.focus_byte() } fn reset(&mut self) { self.z.reset() } - fn val_count(&self) -> usize { self.z.val_count() } fn descend_to>(&mut self, k: K) { self.z.descend_to(k) } fn descend_to_byte(&mut self, k: u8) { self.z.descend_to_byte(k) } fn descend_indexed_byte(&mut self, child_idx: usize) -> Option { self.z.descend_indexed_byte(child_idx) } @@ -734,6 +746,7 @@ impl Clone for WriteZipp impl Zipper for WriteZipperOwned { zipper_impl_lens!(Zipper self => self.z); } impl ZipperValues for WriteZipperOwned { zipper_impl_lens!(ZipperValues self => self.z); } impl ZipperInfallibleSubtries for WriteZipperOwned { zipper_impl_lens!(ZipperInfallibleSubtries self => self.z); } +impl ZipperConcrete for WriteZipperOwned { zipper_impl_lens!(ZipperConcrete self => self.z); } impl ZipperMoving for WriteZipperOwned { zipper_impl_lens!(ZipperMoving self => self.z); } impl ZipperPath for WriteZipperOwned { zipper_impl_lens!(ZipperPath self => self.z); } impl ZipperPathBuffer for WriteZipperOwned { zipper_impl_lens!(ZipperPathBuffer self => self.z); } @@ -983,6 +996,14 @@ impl<'trie, V: Clone + Send + Sync + Unpin, A: Allocator + 'trie> Zipper for Wri } } +impl ZipperConcrete for WriteZipperCore<'_, '_, V, A> { + #[inline] + fn shared_node_id(&self) -> Option { None } + + #[inline] + fn is_shared(&self) -> bool { false } +} + impl<'trie, V: Clone + Send + Sync + Unpin, A: Allocator + 'trie> ZipperForking for WriteZipperCore<'trie, '_, V, A> { type ReadZipperT<'a> = crate::zipper::read_zipper_core::ReadZipperCore<'a, 'a, V, A> where Self: 'a; fn fork_read_zipper<'a>(&'a self) -> Self::ReadZipperT<'a> { @@ -1036,15 +1057,6 @@ impl<'a, 'path, V: Clone + Send + Sync + Unpin, A: Allocator + 'a> ZipperMoving self.key.prefix_idx.clear(); } - fn val_count(&self) -> usize { - let root_val = self.is_val() as usize; - let focus = self.get_focus(); - if focus.is_none() { - root_val - } else { - val_count_below_root(focus.as_tagged()) + root_val - } - } fn descend_to>(&mut self, k: K) { let key = k.as_ref(); self.key.prepare_buffers(); diff --git a/src/zipper.rs b/src/zipper.rs index dd61d861..90ef138f 100644 --- a/src/zipper.rs +++ b/src/zipper.rs @@ -28,6 +28,7 @@ pub use crate::poly_zipper::{PolyZipper, PolyZipperExplicit}; pub use crate::dependent_zipper::DependentProductZipperG; use crate::zipper_tracking::*; +pub use crate::morphisms::CatamorphismCached; //re-exported so `use pathmap::zipper::*` gives the caller access to `val_count`, etc /// The most fundamantal interface for a zipper, compatible with all zipper types pub trait Zipper { @@ -167,12 +168,6 @@ pub trait ZipperMoving: Zipper { } } - /// Returns the total number of values contained at and below the zipper's focus, including the focus itself - /// - /// WARNING: This is not a cheap method. It may have an order-N cost - //GOAT! This doesn't belong here. Should be a function that uses a non-side-effect catamorphism - fn val_count(&self) -> usize; - /// Moves the zipper deeper into the trie, to the `key` specified relative to the current zipper focus fn descend_to>(&mut self, k: K); @@ -1235,7 +1230,6 @@ macro_rules! zipper_impl_lens { fn at_root(&$s) -> bool { $e.at_root() } #[inline] fn focus_byte(&$s) -> Option { $e.focus_byte() } fn reset(&mut $s) { $e.reset() } - fn val_count(&$s) -> usize { $e.val_count() } fn descend_to>(&mut $s, k: K) { $e.descend_to(k) } fn descend_to_check>(&mut $s, k: K) -> bool { $e.descend_to_check(k) } fn descend_to_existing>(&mut $s, k: K) -> usize { $e.descend_to_existing(k) } @@ -1986,20 +1980,6 @@ pub(crate) mod read_zipper_core { self.prefix_buf.truncate(self.origin_path.len()); } - fn val_count(&self) -> usize { - timed_span!(ValueCount, COUNTERS); - let root_val = self.is_val() as usize; - if self.node_key().len() == 0 { - val_count_below_root(*self.focus_node) + root_val - } else { - let focus = self.get_focus(); - if focus.0.is_none() { - root_val - } else { - val_count_below_root(focus.0.as_tagged()) + root_val - } - } - } fn descend_to>(&mut self, k: K) { timed_span!(DescendTo, COUNTERS); let k = k.as_ref(); From 3839f31c798bc4099397a2c105526f71f8209967 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Wed, 2 Sep 2026 08:15:30 -0600 Subject: [PATCH 50/50] Improving ergonomics of using `CatamorphismCachedIterative` trait by removing usused `A: Allocator` parameter Fixing arena_compact benchmarks --- benches/act_paths.rs | 2 +- benches/cities.rs | 5 +- benches/shakespeare.rs | 5 +- benches/superdense_keys.rs | 5 +- src/morphisms.rs | 120 ++++++++++++++++--------------------- 5 files changed, 55 insertions(+), 82 deletions(-) diff --git a/benches/act_paths.rs b/benches/act_paths.rs index 061c5c2d..13b1a58b 100644 --- a/benches/act_paths.rs +++ b/benches/act_paths.rs @@ -26,7 +26,7 @@ use divan::{Bencher, Divan, counter::ItemsCount}; use pathmap::PathMap; use pathmap::arena_compact::{ACTOutputStream, ArenaCompactTree}; use pathmap::paths_serialization::{for_each_deserialized_path, serialize_paths}; -use pathmap::morphisms::CatamorphismCached; +use pathmap::morphisms::CatamorphismCachedIterative; use rand::{Rng, SeedableRng, rngs::StdRng}; use std::path::Path; diff --git a/benches/cities.rs b/benches/cities.rs index 231cb6e0..9c27c9a4 100644 --- a/benches/cities.rs +++ b/benches/cities.rs @@ -171,10 +171,7 @@ fn cities_val_count(bencher: Bencher) { #[cfg(feature="arena_compact")] #[divan::bench()] fn cities_val_count_act(bencher: Bencher) { - use pathmap::{ - arena_compact::ArenaCompactTree, - zipper::ZipperMoving, - }; + use pathmap::{morphisms::CatamorphismCachedIterative, arena_compact::ArenaCompactTree}; let pairs = read_data(); let mut map = PathMap::new(); let mut unique_count = 0; diff --git a/benches/shakespeare.rs b/benches/shakespeare.rs index 2040ba54..67f754d1 100644 --- a/benches/shakespeare.rs +++ b/benches/shakespeare.rs @@ -171,10 +171,7 @@ fn shakespeare_sentences_val_count(bencher: Bencher) { #[cfg(feature="arena_compact")] #[divan::bench()] fn shakespeare_sentences_val_count_act(bencher: Bencher) { - use pathmap::{ - arena_compact::ArenaCompactTree, - zipper::ZipperMoving, - }; + use pathmap::{morphisms::CatamorphismCachedIterative, arena_compact::ArenaCompactTree}; let strings = read_data(false); let mut map = PathMap::new(); let mut unique_count = 0; diff --git a/benches/superdense_keys.rs b/benches/superdense_keys.rs index 9e258691..f53c386f 100644 --- a/benches/superdense_keys.rs +++ b/benches/superdense_keys.rs @@ -328,10 +328,7 @@ fn superdense_val_count_bench(bencher: Bencher, n: u64) { #[cfg(feature="arena_compact")] #[divan::bench(sample_size = 1, args = [100, 200, 400, 800, 1600, 3200, 20_000, 100_000])] fn superdense_val_count_bench_act(bencher: Bencher, n: u64) { - use pathmap::{ - arena_compact::ArenaCompactTree, - zipper::ZipperMoving, - }; + use pathmap::{morphisms::CatamorphismCachedIterative, arena_compact::ArenaCompactTree}; let mut map: PathMap = PathMap::new(); for i in 0..n { map.set_val_at(prefix_key(&i), i); } let act = ArenaCompactTree::from_zipper(map.read_zipper(), |&v| v); diff --git a/src/morphisms.rs b/src/morphisms.rs index 7229b692..a2a34a6b 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -147,9 +147,9 @@ pub trait CatamorphismSideEffecting { } macro_rules! define_cached_cata_trait { - ($(#[$meta:meta])* $trait_name:ident) => { + ($(#[$meta:meta])* $trait_name:ident [$($generics:tt)*]) => { $(#[$meta])* - pub trait $trait_name { + pub trait $trait_name<$($generics)*> { /// Applies a **cached**, **stepping**, catamorphism to the subtrie descending from the /// zipper's current focus, running `alg_f` at every step (every byte). /// @@ -406,7 +406,7 @@ define_cached_cata_trait! { /// [`CatamorphismCachedIterative`] deliberately has the same method names and signatures so it /// is perfectly interchangeable. Import one trait for ordinary method syntax. When both /// strategies are needed in one scope, call the desired trait with fully qualified syntax. - CatamorphismCached + CatamorphismCached [V, A = GlobalAlloc] } define_cached_cata_trait! { @@ -421,7 +421,7 @@ define_cached_cata_trait! { /// Import one trait for ordinary method syntax. When both strategies are needed in one scope, /// call the desired trait with fully qualified syntax. such as /// `CatamorphismCachedIterative::cata_cached(&zipper, algebra)`. - CatamorphismCachedIterative + CatamorphismCachedIterative [V] } /// Shared child-result storage used to adapt the factored cached-cata API to a single-function algebra. @@ -489,34 +489,6 @@ impl CataChildren { } } -// //GOAT Implementation of adapted single-function-algebra cata -// fn into_cata_jumping_cached_from_summarization(source: &S, alg_f: AlgF) -> Result -// where -// V: Clone + Send + Sync, -// A: Allocator, -// S: Summarization, -// W: Clone, -// AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> Result, -// { -// let children = std::cell::RefCell::new(CataChildren::::new()); -// let children = &children; -// let alg_f = &alg_f; - -// source.recursive_cata::( -// move |mask| Ok(children.borrow_mut().new_acc(mask.count_bits())), -// move |_mask, child, acc| { -// children.borrow_mut().push(acc, child); -// Ok(()) -// }, -// move |mask, value, acc, prefix| match acc { -// Some(acc) => children.borrow_mut().summarize(acc, mask.count_bits(), |children| { -// alg_f(mask, children, value, prefix) -// }), -// None => alg_f(mask, &mut [], value, prefix), -// }, -// ) -// } - //TODO GOAT!!: It would be nice to get rid of this Default bound on all morphism Ws. In this case, the plan // for doing that would be to create a new type called a TakableSlice. It would be able to deref // into a regular mutable slice of `T` so it would work just like an ordinary slice. Additionally @@ -694,7 +666,7 @@ impl CatamorphismCached for Z whe } } -impl<'a, Z, V: 'a, A: Allocator> CatamorphismCachedIterative for Z where Z: Clone + Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperPathBuffer { +impl<'a, Z, V: 'a> CatamorphismCachedIterative for Z where Z: Clone + Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperPathBuffer { fn factored_cata_jumping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where W: Clone, @@ -730,7 +702,7 @@ impl CatamorphismCached for } } -impl CatamorphismCachedIterative for PathMap { +impl CatamorphismCachedIterative for PathMap { fn factored_cata_jumping(&self, new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF) -> Result where W: Clone, @@ -1972,8 +1944,18 @@ pub(crate) mod cached_catamorphism_tests { /// This is intentionally a macro because the two engines are traits with the same methods, /// rather than values of a common engine type. macro_rules! reconstruct_trie { - ($engine:ident, $subject:expr) => {{ - <_ as $crate::morphisms::$engine<(), $crate::alloc::GlobalAlloc>> + (CatamorphismCached, $subject:expr) => {{ + <_ as $crate::morphisms::CatamorphismCached<(), $crate::alloc::GlobalAlloc>> + ::factored_cata_jumping::<_, _, core::convert::Infallible, _, _, _, true>( + $subject, + $crate::morphisms::cached_catamorphism_tests::recon_start, + $crate::morphisms::cached_catamorphism_tests::recon_fold, + $crate::morphisms::cached_catamorphism_tests::recon_summarize, + ) + .unwrap() + }}; + (CatamorphismCachedIterative, $subject:expr) => {{ + <_ as $crate::morphisms::CatamorphismCachedIterative<()>> ::factored_cata_jumping::<_, _, core::convert::Infallible, _, _, _, true>( $subject, $crate::morphisms::cached_catamorphism_tests::recon_start, @@ -2017,7 +1999,7 @@ pub(crate) mod cached_catamorphism_tests { pub(crate) fn assert_reconstructs_like(subject: &Z, expected: &PathMap<()>) where Z: crate::morphisms::CatamorphismCached<(), GlobalAlloc> - + crate::morphisms::CatamorphismCachedIterative<(), GlobalAlloc>, + + crate::morphisms::CatamorphismCachedIterative<()>, { let iterative = reconstruct_trie!(CatamorphismCachedIterative, subject); assert_same_paths(&iterative, expected); @@ -2300,17 +2282,17 @@ pub(crate) mod cached_catamorphism_tests { } macro_rules! define_cached_catamorphism_test_suite { - ($suite_name:ident, $cata_trait:ident) => { + ($suite_name:ident, $cata_trait:ident, [$($cata_args:ty),+]) => { pub(crate) mod $suite_name { - use super::{GlobalAlloc, Infallible, ZipperMoving, ZipperPath}; + use super::{Infallible, ZipperMoving, ZipperPath}; use crate::morphisms::$cata_trait; use crate::utils::BitMask; pub fn factored_cata_propagates_callback_errors(zipper: Z) where - Z: crate::morphisms::$cata_trait, + Z: crate::morphisms::$cata_trait<$($cata_args),*>, { - let error = $cata_trait:: + let error = $cata_trait::<$($cata_args),*> ::factored_cata_jumping::<(), (), &'static str, _, _, _, false>( &zipper, |_| Err("new"), @@ -2319,7 +2301,7 @@ pub(crate) mod cached_catamorphism_tests { ); assert_eq!(error, Err("new")); - let error = $cata_trait:: + let error = $cata_trait::<$($cata_args),*> ::factored_cata_jumping::<(), (), &'static str, _, _, _, false>( &zipper, |_| Ok(()), @@ -2328,7 +2310,7 @@ pub(crate) mod cached_catamorphism_tests { ); assert_eq!(error, Err("fold")); - let error = $cata_trait:: + let error = $cata_trait::<$($cata_args),*> ::factored_cata_jumping::<(), (), &'static str, _, _, _, false>( &zipper, |_| Ok(()), @@ -2340,9 +2322,9 @@ pub(crate) mod cached_catamorphism_tests { pub fn leaf_count_stepping(zipper: Z) where - Z: crate::morphisms::$cata_trait, + Z: crate::morphisms::$cata_trait<$($cata_args),*>, { - let count = $cata_trait::::cata_cached( + let count = $cata_trait::<$($cata_args),*>::cata_cached( &zipper, |_mask, children: &mut [usize], value| { if children.is_empty() { @@ -2358,9 +2340,9 @@ pub(crate) mod cached_catamorphism_tests { pub fn leaf_count_jumping(zipper: Z) where - Z: crate::morphisms::$cata_trait, + Z: crate::morphisms::$cata_trait<$($cata_args),*>, { - let count = $cata_trait::::cata_jumping_cached( + let count = $cata_trait::<$($cata_args),*>::cata_jumping_cached( &zipper, |_mask, children: &mut [usize], value, _prefix| { if children.is_empty() { @@ -2376,9 +2358,9 @@ pub(crate) mod cached_catamorphism_tests { pub fn leaf_count_factored_jumping(zipper: Z) where - Z: crate::morphisms::$cata_trait, + Z: crate::morphisms::$cata_trait<$($cata_args),*>, { - let count = $cata_trait:: + let count = $cata_trait::<$($cata_args),*> ::factored_cata_jumping::( &zipper, |_| Ok(0), @@ -2397,9 +2379,9 @@ pub(crate) mod cached_catamorphism_tests { pub fn leaf_count_factored_stepping(zipper: Z) where - Z: crate::morphisms::$cata_trait, + Z: crate::morphisms::$cata_trait<$($cata_args),*>, { - let count = $cata_trait:: + let count = $cata_trait::<$($cata_args),*> ::factored_cata::( &zipper, |_| Ok(0), @@ -2420,10 +2402,10 @@ pub(crate) mod cached_catamorphism_tests { /// descendants, but not values elsewhere in the trie. pub fn cata_from_value_focus(mut zipper: Z) where - Z: ZipperMoving + ZipperPath + crate::morphisms::$cata_trait, + Z: ZipperMoving + ZipperPath + crate::morphisms::$cata_trait<$($cata_args),*>, { zipper.descend_to(b"roman"); - let count = $cata_trait::::cata_cached( + let count = $cata_trait::<$($cata_args),*>::cata_cached( &zipper, |_mask, children: &mut [usize], value| { value.is_some() as usize + children.iter().sum::() @@ -2438,10 +2420,10 @@ pub(crate) mod cached_catamorphism_tests { /// Traversal still covers precisely the subtrie below that point. pub fn cata_from_mid_run_focus(mut zipper: Z) where - Z: ZipperMoving + ZipperPath + crate::morphisms::$cata_trait, + Z: ZipperMoving + ZipperPath + crate::morphisms::$cata_trait<$($cata_args),*>, { zipper.descend_to(b"roma"); - let count = $cata_trait::::cata_jumping_cached( + let count = $cata_trait::<$($cata_args),*>::cata_jumping_cached( &zipper, |_mask, children: &mut [usize], value, _prefix| { value.is_some() as usize + children.iter().sum::() @@ -2454,9 +2436,9 @@ pub(crate) mod cached_catamorphism_tests { pub fn longest_path_jumping(zipper: Z) where - Z: crate::morphisms::$cata_trait, + Z: crate::morphisms::$cata_trait<$($cata_args),*>, { - let longest = $cata_trait::::cata_jumping_cached( + let longest = $cata_trait::<$($cata_args),*>::cata_jumping_cached( &zipper, |mask, children: &mut [Vec], _value, prefix| { let mut longest = mask.iter().zip(children.iter_mut()) @@ -2476,9 +2458,9 @@ pub(crate) mod cached_catamorphism_tests { pub fn longest_path_factored_jumping(zipper: Z) where - Z: crate::morphisms::$cata_trait, + Z: crate::morphisms::$cata_trait<$($cata_args),*>, { - let longest = $cata_trait:: + let longest = $cata_trait::<$($cata_args),*> ::factored_cata_jumping::>, Vec, Infallible, _, _, _, true>( &zipper, |_| Ok(Vec::new()), @@ -2506,9 +2488,9 @@ pub(crate) mod cached_catamorphism_tests { pub fn branch_values_stepping(zipper: Z) where - Z: crate::morphisms::$cata_trait, + Z: crate::morphisms::$cata_trait<$($cata_args),*>, { - let values = $cata_trait::::cata_cached( + let values = $cata_trait::<$($cata_args),*>::cata_cached( &zipper, |_mask, children: &mut [Vec], value| { if children.is_empty() { @@ -2529,12 +2511,12 @@ pub(crate) mod cached_catamorphism_tests { pub fn factored_cata_folds_each_child_immediately(zipper: Z) where - Z: crate::morphisms::$cata_trait, + Z: crate::morphisms::$cata_trait<$($cata_args),*>, { use std::cell::RefCell; let events = RefCell::new(Vec::new()); - let result = $cata_trait:: + let result = $cata_trait::<$($cata_args),*> ::factored_cata_jumping::, u64, Infallible, _, _, _, false>( &zipper, |_mask| { @@ -2562,9 +2544,9 @@ pub(crate) mod cached_catamorphism_tests { pub fn factored_cata_passthrough_root(zipper: Z) where - Z: crate::morphisms::$cata_trait, + Z: crate::morphisms::$cata_trait<$($cata_args),*>, { - let result = $cata_trait:: + let result = $cata_trait::<$($cata_args),*> ::factored_cata_jumping::)>, Vec, Infallible, _, _, _, true>( &zipper, |_| Ok(Vec::new()), @@ -2600,7 +2582,7 @@ pub(crate) mod cached_catamorphism_tests { test: impl Fn(Z), ) where - Z: 'a + crate::morphisms::$cata_trait, + Z: 'a + crate::morphisms::$cata_trait<$($cata_args),*>, { test(make_z(store)); } @@ -2609,8 +2591,8 @@ pub(crate) mod cached_catamorphism_tests { }; } - define_cached_catamorphism_test_suite!(recursive, CatamorphismCached); - define_cached_catamorphism_test_suite!(iterative, CatamorphismCachedIterative); + define_cached_catamorphism_test_suite!(recursive, CatamorphismCached, [u64, crate::alloc::GlobalAlloc]); + define_cached_catamorphism_test_suite!(iterative, CatamorphismCachedIterative, [u64]); macro_rules! cached_catamorphism_case { ($z_name:ident, $implementation:ident, $suite:ident, $read_keys:expr, $make_z:expr, $keys:ident, $test:ident) => { @@ -2724,7 +2706,7 @@ mod tests { alg, ); let iterative_zipper = map.read_zipper(); - let iterative = CatamorphismCachedIterative::::cata_cached( + let iterative = CatamorphismCachedIterative::::cata_cached( &iterative_zipper, alg, );