diff --git a/Cargo.toml b/Cargo.toml index 8b9d32cf..28c5f2ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -118,9 +118,22 @@ harness = false name = "product_zipper" harness = false +[[bench]] +name = "catamorphism" +harness = false + +[[bench]] +name = "merkleize" +harness = false + +[[example]] +name = "merkleize_sharing" +required-features = ["viz"] + [[bench]] name = "sla" harness = false +required-features = ["viz"] [[bench]] name = "multiplicities" diff --git a/benches/act_paths.rs b/benches/act_paths.rs index b95691b4..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::zipper::ZipperMoving; +use pathmap::morphisms::CatamorphismCachedIterative; use rand::{Rng, SeedableRng, rngs::StdRng}; use std::path::Path; diff --git a/benches/binary_keys.rs b/benches/binary_keys.rs index dbfd3e83..57ef95f7 100644 --- a/benches/binary_keys.rs +++ b/benches/binary_keys.rs @@ -203,20 +203,82 @@ fn binary_descend_until_max_bytes(bencher: Bencher, n: u64) { }); } -#[divan::bench(args = [125, 250, 500, 1000, 2000, 4000])] -fn binary_val_count_bench(bencher: Bencher, n: u64) { +// Identical fixtures for native recursive, native iterative, and ACT traversal. +// Map/tree construction and zipper creation are outside the timed closures. +mod val_count { + use divan::{Bencher, black_box}; + use pathmap::PathMap; + + fn build_map(n: u64) -> PathMap { + let keys = super::make_keys(n as usize, 1); + let mut map = PathMap::new(); + for (i, key) in keys.iter().enumerate() { + map.set_val_at(key, i as u64); + } + map + } - let keys = make_keys(n as usize, 1); + #[divan::bench(args = [125, 250, 500, 1000, 2000, 4000, 100000])] + fn recursive(bencher: Bencher, n: u64) { + use pathmap::morphisms::CatamorphismCached; + + let map = build_map(n); + let expected = map.iter().count(); + let zipper = map.read_zipper(); + let mut sink = 0; + bencher.bench_local(|| { + *black_box(&mut sink) = zipper.val_count(); + }); + assert_eq!(sink, expected); + } - let mut map: PathMap = PathMap::new(); - for i in 0..n { map.set_val_at(&keys[i as usize], i); } + #[divan::bench(args = [125, 250, 500, 1000, 2000, 4000, 100000])] + fn iterative(bencher: Bencher, n: u64) { + use pathmap::morphisms::CatamorphismCachedIterative; + + let map = build_map(n); + let expected = map.iter().count(); + let zipper = map.read_zipper(); + let mut sink = 0; + bencher.bench_local(|| { + *black_box(&mut sink) = zipper.val_count(); + }); + assert_eq!(sink, expected); + } - //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.val_count() - }); - assert_eq!(sink, n as usize); + #[cfg(feature = "arena_compact")] + #[divan::bench(args = [125, 250, 500, 1000, 2000, 4000, 100000])] + fn act_recursive(bencher: Bencher, n: u64) { + use pathmap::arena_compact::ArenaCompactTree; + use pathmap::morphisms::CatamorphismCached; + + let map = build_map(n); + let expected = map.iter().count(); + let tree = ArenaCompactTree::from_zipper(map.read_zipper(), |&value| value); + let zipper = tree.read_zipper_u64(); + let mut sink = 0; + bencher.bench_local(|| { + *black_box(&mut sink) = zipper.val_count(); + }); + assert_eq!(sink, expected); + } + + #[cfg(feature = "arena_compact")] + #[divan::bench(args = [125, 250, 500, 1000, 2000, 4000, 100000])] + fn act_iterative(bencher: Bencher, n: u64) { + use pathmap::arena_compact::ArenaCompactTree; + use pathmap::morphisms::CatamorphismCachedIterative; + + let map = build_map(n); + let expected = map.iter().count(); + let tree = ArenaCompactTree::from_zipper(map.read_zipper(), |&value| value); + let zipper = tree.read_zipper_u64(); + let mut sink = 0; + bencher.bench_local(|| { + *black_box(&mut sink) = zipper.val_count(); + }); + assert_eq!(sink, expected); + } } #[divan::bench(args = [50, 100, 200, 400, 800, 1600])] diff --git a/benches/catamorphism.rs b/benches/catamorphism.rs new file mode 100644 index 00000000..34e1eaef --- /dev/null +++ b/benches/catamorphism.rs @@ -0,0 +1,140 @@ +use divan::{Divan, Bencher, black_box}; +use core::convert::Infallible; +use pathmap::alloc::GlobalAlloc; +use pathmap::morphisms::CatamorphismCached; +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; + +// 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 factored_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) = 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)), + ).unwrap(); + }); + assert_eq!(sink, MAP_COUNT as usize); +} + +#[divan::bench()] +fn factored_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) = CatamorphismCached::<(), GlobalAlloc> + ::factored_cata_jumping::<_, _, Infallible, _, _, _, false>(&rz, + |_| 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, BINARY_TREE_LEAF_COUNT); +} + +#[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) = 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; + } + sum + }); + }); + assert_eq!(sink, MAP_COUNT as usize); +} + +#[divan::bench()] +fn factored_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) = CatamorphismCached::<(), GlobalAlloc>::factored_cata_jumping::<_, _, Infallible, _, _, _, true>(&rz, + |_| Ok((0usize, 0usize)), + |_mask: &ByteMask, w: (usize, usize), acc: &mut (usize, usize)| { + acc.0 += w.0; + // 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| { + let (count, total_len) = acc.unwrap_or((0, 0)); + let count = count + val.is_some() as usize; + Ok((count, total_len + count * prefix.len())) + }, + ).unwrap(); + }); + assert_eq!(sink, (MAP_COUNT as usize, MAP_COUNT as usize * 8)); +} + +#[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) = 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(); + if val.is_some() { + count += 1; + total_len += prefix_len; + } + for (_byte, child) in mask.iter().zip(children.iter_mut()) { + count += child.0; + // 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, (MAP_COUNT as usize, MAP_COUNT as usize * 8)); +} diff --git a/benches/cities.rs b/benches/cities.rs index 64a1ddf5..7d396659 100644 --- a/benches/cities.rs +++ b/benches/cities.rs @@ -1,6 +1,6 @@ use divan::{Divan, Bencher, black_box}; -use pathmap::{PathMap, zipper::*}; +use pathmap::{PathMap, zipper::ZipperValuesAt}; use std::fs::File; use std::io::BufReader; @@ -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/merkleize.rs b/benches/merkleize.rs new file mode 100644 index 00000000..36513e20 --- /dev/null +++ b/benches/merkleize.rs @@ -0,0 +1,64 @@ +use divan::{Divan, Bencher}; +use pathmap::PathMap; +use pathmap::zipper::*; + +fn main() { + Divan::from_args().sample_count(50).main(); +} + +struct XorShift(u64); +impl XorShift { + fn next(&mut self) -> u64 { self.0 ^= self.0 << 13; self.0 ^= self.0 >> 7; self.0 ^= self.0 << 17; self.0 } + fn shuffle(&mut self, items: &mut [T]) { + for i in (1..items.len()).rev() { let j = (self.next() % (i as u64 + 1)) as usize; items.swap(i, j); } + } +} + +fn random_suffixes(count: usize, len: usize, seed: u64) -> Vec> { + let mut rng = XorShift(seed); + (0..count).map(|_| (0..len).map(|_| (rng.next() >> 32) as u8).collect()).collect() +} + +/// `copies` one-byte prefixes over the same suffixes, each copy inserted in the same or its own order +fn build(suffixes: &[Vec], copies: usize, shuffle: bool) -> PathMap<()> { + let mut map = PathMap::<()>::new(); + let mut rng = XorShift(0x2545F4914F6CDD1D); + for copy in 0..copies { + let mut order: Vec<&Vec> = suffixes.iter().collect(); + if shuffle { rng.shuffle(&mut order); } + let prefix = [copy as u8]; + let mut wz = map.write_zipper_at_path(&prefix); + for suffix in order { wz.descend_to(suffix); wz.set_val(()); wz.reset(); } + } + map +} + +fn bench_merkleize(bencher: Bencher, map: PathMap<()>) { + bencher + .with_inputs(|| map.clone()) + .bench_local_values(|mut map| map.merkleize()); +} + +#[divan::bench] +fn same_order_16x2000(bencher: Bencher) { + bench_merkleize(bencher, build(&random_suffixes(2000, 6, 0x9E3779B97F4A7C15), 16, false)); +} + +#[divan::bench] +fn shuffled_order_16x2000(bencher: Bencher) { + bench_merkleize(bencher, build(&random_suffixes(2000, 6, 0x9E3779B97F4A7C15), 16, true)); +} + +#[divan::bench] +fn no_sharing_random_50k(bencher: Bencher) { + let mut map = PathMap::<()>::new(); + for key in random_suffixes(50_000, 8, 0x1234_5678_9ABC_DEF1) { map.insert(&key, ()); } + bench_merkleize(bencher, map); +} + +#[divan::bench] +fn already_merkleized_16x2000(bencher: Bencher) { + let mut map = build(&random_suffixes(2000, 6, 0x9E3779B97F4A7C15), 16, true); + map.merkleize(); + bench_merkleize(bencher, map); +} 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 41816cb0..a179b79b 100644 --- a/benches/oeis.rs +++ b/benches/oeis.rs @@ -1,6 +1,6 @@ use std::io::Read; 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/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/shakespeare.rs b/benches/shakespeare.rs index 90dba53e..cdd73970 100644 --- a/benches/shakespeare.rs +++ b/benches/shakespeare.rs @@ -1,6 +1,6 @@ use divan::{Divan, Bencher, black_box}; -use pathmap::{PathMap, zipper::*}; +use pathmap::{PathMap, zipper::ZipperValuesAt}; use std::fs::File; use std::io::prelude::*; @@ -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/sla.rs b/benches/sla.rs index b6f04325..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}; @@ -643,4 +642,4 @@ fn main() { tipover_attention_bob(); tipover_attention_weave(0.02); -} \ No newline at end of file +} diff --git a/benches/sparse_keys.rs b/benches/sparse_keys.rs index 265f9d4a..bc9a2f67 100644 --- a/benches/sparse_keys.rs +++ b/benches/sparse_keys.rs @@ -134,24 +134,85 @@ fn sparse_descend_until_max_bytes(bencher: Bencher, n: u64) { }); } -#[divan::bench(args = [125, 250, 500, 1000, 2000, 4000])] -fn sparse_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.val_count() - }); - assert_eq!(sink, n as usize); +// Identical fixtures for native recursive, native iterative, and ACT traversal. +// Map/tree construction and zipper creation are outside the timed closures. +mod val_count { + use divan::{Bencher, black_box}; + use pathmap::PathMap; + use rand::{Rng, SeedableRng, rngs::StdRng}; + + fn build_map(n: u64) -> PathMap { + let mut rng = StdRng::seed_from_u64(1); + let mut map = PathMap::new(); + for i in 0..n { + let len = (rng.random::() % 18) + 3; + let key: Vec = (0..len).map(|_| rng.random::()).collect(); + map.set_val_at(key, i); + } + map + } + + #[divan::bench(args = [125, 250, 500, 1000, 2000, 4000, 20_000, 100_000])] + fn recursive(bencher: Bencher, n: u64) { + use pathmap::morphisms::CatamorphismCached; + + let map = build_map(n); + let expected = map.iter().count(); + let zipper = map.read_zipper(); + let mut sink = 0; + bencher.bench_local(|| { + *black_box(&mut sink) = zipper.val_count(); + }); + assert_eq!(sink, expected); + } + + #[divan::bench(args = [125, 250, 500, 1000, 2000, 4000, 20_000, 100_000])] + fn iterative(bencher: Bencher, n: u64) { + use pathmap::morphisms::CatamorphismCachedIterative; + + let map = build_map(n); + let expected = map.iter().count(); + let zipper = map.read_zipper(); + let mut sink = 0; + bencher.bench_local(|| { + *black_box(&mut sink) = zipper.val_count(); + }); + assert_eq!(sink, expected); + } + + #[cfg(feature = "arena_compact")] + #[divan::bench(args = [125, 250, 500, 1000, 2000, 4000, 20_000, 100_000])] + fn act_recursive(bencher: Bencher, n: u64) { + use pathmap::arena_compact::ArenaCompactTree; + use pathmap::morphisms::CatamorphismCached; + + let map = build_map(n); + let expected = map.iter().count(); + let tree = ArenaCompactTree::from_zipper(map.read_zipper(), |&value| value); + let zipper = tree.read_zipper_u64(); + let mut sink = 0; + bencher.bench_local(|| { + *black_box(&mut sink) = zipper.val_count(); + }); + assert_eq!(sink, expected); + } + + #[cfg(feature = "arena_compact")] + #[divan::bench(args = [125, 250, 500, 1000, 2000, 4000, 20_000, 100_000])] + fn act_iterative(bencher: Bencher, n: u64) { + use pathmap::arena_compact::ArenaCompactTree; + use pathmap::morphisms::CatamorphismCachedIterative; + + let map = build_map(n); + let expected = map.iter().count(); + let tree = ArenaCompactTree::from_zipper(map.read_zipper(), |&value| value); + let zipper = tree.read_zipper_u64(); + let mut sink = 0; + bencher.bench_local(|| { + *black_box(&mut sink) = zipper.val_count(); + }); + assert_eq!(sink, expected); + } } #[divan::bench(args = [50, 100, 200, 400, 800, 1600])] diff --git a/benches/superdense_keys.rs b/benches/superdense_keys.rs index 97d2ae48..a47ee422 100644 --- a/benches/superdense_keys.rs +++ b/benches/superdense_keys.rs @@ -357,35 +357,79 @@ 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])] -fn superdense_val_count_bench(bencher: Bencher, n: u64) { +// Identical fixtures for native recursive, native iterative, and ACT traversal. +// Map/tree construction and zipper creation are outside the timed closures. +mod val_count { + use divan::{Bencher, black_box}; + use pathmap::PathMap; - let mut map: PathMap = PathMap::new(); - for i in 0..n { map.set_val_at(prefix_key(&i), i); } + fn build_map(n: u64) -> PathMap { + let mut map = PathMap::new(); + for i in 0..n { + map.set_val_at(super::prefix_key(&i), i); + } + map + } - //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.val_count() - }); - assert_eq!(sink, n as usize); -} + #[divan::bench(sample_size = 1, args = [100, 200, 400, 800, 1600, 3200, 20_000, 100_000])] + fn recursive(bencher: Bencher, n: u64) { + use pathmap::morphisms::CatamorphismCached; + + let map = build_map(n); + let expected = map.iter().count(); + let zipper = map.read_zipper(); + let mut sink = 0; + bencher.bench_local(|| { + *black_box(&mut sink) = zipper.val_count(); + }); + assert_eq!(sink, expected); + } -#[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) { - use pathmap::{ - arena_compact::ArenaCompactTree, - zipper::ZipperMoving, - }; - 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); - let zipper = act.read_zipper(); - //Benchmark the time taken to count the number of values in the map - let mut sink = 0; - bencher.bench_local(|| { - *black_box(&mut sink) = zipper.val_count(); - }); - assert_eq!(sink, n as usize); + #[divan::bench(sample_size = 1, args = [100, 200, 400, 800, 1600, 3200, 20_000, 100_000])] + fn iterative(bencher: Bencher, n: u64) { + use pathmap::morphisms::CatamorphismCachedIterative; + + let map = build_map(n); + let expected = map.iter().count(); + let zipper = map.read_zipper(); + let mut sink = 0; + bencher.bench_local(|| { + *black_box(&mut sink) = zipper.val_count(); + }); + assert_eq!(sink, expected); + } + + #[cfg(feature = "arena_compact")] + #[divan::bench(sample_size = 1, args = [100, 200, 400, 800, 1600, 3200, 20_000, 100_000])] + fn act_recursive(bencher: Bencher, n: u64) { + use pathmap::arena_compact::ArenaCompactTree; + use pathmap::morphisms::CatamorphismCached; + + let map = build_map(n); + let expected = map.iter().count(); + let tree = ArenaCompactTree::from_zipper(map.read_zipper(), |&value| value); + let zipper = tree.read_zipper_u64(); + let mut sink = 0; + bencher.bench_local(|| { + *black_box(&mut sink) = zipper.val_count(); + }); + assert_eq!(sink, expected); + } + + #[cfg(feature = "arena_compact")] + #[divan::bench(sample_size = 1, args = [100, 200, 400, 800, 1600, 3200, 20_000, 100_000])] + fn act_iterative(bencher: Bencher, n: u64) { + use pathmap::arena_compact::ArenaCompactTree; + use pathmap::morphisms::CatamorphismCachedIterative; + + let map = build_map(n); + let expected = map.iter().count(); + let tree = ArenaCompactTree::from_zipper(map.read_zipper(), |&value| value); + let zipper = tree.read_zipper_u64(); + let mut sink = 0; + bencher.bench_local(|| { + *black_box(&mut sink) = zipper.val_count(); + }); + assert_eq!(sink, expected); + } } diff --git a/differential/src/act.rs b/differential/src/act.rs index 58f7a705..febb7f40 100644 --- a/differential/src/act.rs +++ b/differential/src/act.rs @@ -10,6 +10,14 @@ use pathmap::zipper::*; use crate::harness::*; impl<'t, S: AsRef<[u8]>> ReadSource for ACTZipper<'t, S, u64> { + fn hash_probe(&self) -> Option { + Some(pathmap::morphisms::CatamorphismCached::::hash_with_scheme( + self, &pathmap::morphisms::trie_hash::Fnv1a64Scheme)) + } + fn hash_iter_probe(&self) -> Option { + Some(pathmap::morphisms::CatamorphismCachedIterative::::hash_with_scheme( + self, &pathmap::morphisms::trie_hash::Fnv1a64Scheme)) + } fn dump_fork(&self) -> String { dump_fork(self) } diff --git a/differential/src/bin/act_bug_repros.rs b/differential/src/bin/act_bug_repros.rs index ce81e55a..1faa316b 100644 --- a/differential/src/bin/act_bug_repros.rs +++ b/differential/src/bin/act_bug_repros.rs @@ -44,9 +44,9 @@ fn main() { fn run(name: &str) { match name { // `val_count` is documented as "the total number of values contained at - // and below the zipper's focus". ACT's implementation clones the zipper, - // calls `reset()` -- moving to the zipper's root -- and counts from there, - // so the focus is ignored entirely. + // and below the zipper's focus". The former ACT implementation called + // `reset()` and counted from the root. The cached catamorphism should + // now agree with PathMap at every focus. "val_count_ignores_focus" => { let m = src(); let t = ArenaCompactTree::from_zipper(m.read_zipper(), |&v| v); @@ -55,11 +55,13 @@ fn run(name: &str) { let mut pz = m.read_zipper(); az.descend_to(path); pz.descend_to(path); + let pathmap_count = pz.val_count(); + let act_count = az.val_count(); println!( " focus {:8?}: exists={:<5} PathMap val_count={} ACT val_count={}{}", String::from_utf8_lossy(path), pz.path_exists(), - pz.val_count(), az.val_count(), - if pz.val_count() == az.val_count() { "" } else { " <-- differs" }, + pathmap_count, act_count, + if pathmap_count == act_count { "" } else { " <-- differs" }, ); } } diff --git a/differential/src/harness.rs b/differential/src/harness.rs index c2863849..7fc45371 100644 --- a/differential/src/harness.rs +++ b/differential/src/harness.rs @@ -14,6 +14,8 @@ //! `lean/PathMapModel/Fuzz.lean`; any change here must be mirrored there. use pathmap::PathMap; +use pathmap::alloc::GlobalAlloc; +use pathmap::morphisms::{CatamorphismCached, CatamorphismCachedIterative, trie_hash::Fnv1a64Scheme}; use pathmap::ring::AlgebraicStatus; use pathmap::utils::ByteMask; use pathmap::zipper::*; @@ -26,7 +28,7 @@ use core::fmt::Write as _; /// Selector count for newly generated inputs. Keep in sync with `lean/input_format.py`. /// Keep operation IDs permanently assigned. -pub const NOPS: usize = 61; +pub const NOPS: usize = 65; /// Unversioned inputs retain their original operation decoding. pub const LEGACY_NOPS: usize = 56; /// Input header marker, followed by a little-endian u16 selector count (1..=256). @@ -147,7 +149,7 @@ pub fn show_byte_opt(b: Option) -> String { /// leave it, so a mismatch is tagged `ESCAPED-ROOT` — the model can never /// produce that tag, which lets `differential.py` recognise the escape bug /// instead of reporting it as an unexplained state divergence. -pub fn fingerprint + ZipperAbsolutePath>( +pub fn fingerprint + ZipperAbsolutePath + CatamorphismCached>( z: &Z, expected_root: &[u8], ) -> String { @@ -239,10 +241,17 @@ pub fn dump_fork>(z: &Z) -> String { /// Keeping this behind a trait means there is still exactly one operation table, /// so the two front ends cannot drift apart. pub trait ReadSource: - Zipper + ZipperMoving + ZipperPath + ZipperValues + ZipperValuesAt + ZipperAbsolutePath + ZipperIteration + Zipper + ZipperMoving + ZipperPath + ZipperValues + ZipperValuesAt + ZipperAbsolutePath + ZipperIteration + CatamorphismCached { /// Depth-first dump of everything below the focus (`fork_read_zipper` + walk). fn dump_fork(&self) -> String; + /// `CatamorphismCached::hash_with_scheme(Fnv1a64Scheme)` at the focus through the recursive + /// engine, or `None` if the read source has no concrete subtries to recurse over. + fn hash_probe(&self) -> Option { None } + /// The same hash through the zipper-driven engine (`CatamorphismCachedIterative`), or `None` + /// if the read source cannot drive it. + fn hash_iter_probe(&self) -> Option { None } + /// `make_map().val_count()`, or `None` if subtries cannot be materialised. fn make_map_val_count(&self) -> Option; @@ -277,6 +286,12 @@ impl<'a, 'p> ReadSource for ReadZipperUntracked<'a, 'p, u64> { fn dump_fork(&self) -> String { dump_fork(self) } + fn hash_probe(&self) -> Option { + Some(CatamorphismCached::::hash_with_scheme(self, &Fnv1a64Scheme)) + } + fn hash_iter_probe(&self) -> Option { + Some(CatamorphismCachedIterative::::hash_with_scheme(self, &Fnv1a64Scheme)) + } fn make_map_val_count(&self) -> Option { Some(self.make_map().val_count()) } @@ -364,7 +379,7 @@ macro_rules! tgt { /// trait documentation or forced by the meaning of the accessors. pub fn check_zipper(z: &Z, label: &str, expected_root: &[u8]) where - Z: ZipperMoving + ZipperPath + ZipperValues + ZipperAbsolutePath, + Z: ZipperMoving + ZipperPath + ZipperValues + ZipperAbsolutePath + CatamorphismCached, { // `at_root` is defined as "the path back to the root is empty". assert_eq!( @@ -962,6 +977,51 @@ pub fn run_ops( let v = get!(d.u8()) as u64; ("set_val_at", show_val(wz.set_val_at(&p, v).as_ref())) } + 61 => { + // The logical trie hash at the focus, through the recursive cata engine. The + // model computes the same function over the logical trie, so this pins down + // that the hash is independent of node layout, and that a value at the focus, + // in a run, or in a parent's slot is layered on in the same place. + let t = get!(d.modn(2)); + let h = if t == 0 { + Some(CatamorphismCached::::hash_with_scheme(&wz, &Fnv1a64Scheme)) + } else { + (*rz).hash_probe() + }; + ("hash", h.map(show_hash).unwrap_or_else(|| "skip".to_string())) + } + 62 => { + // The same hash of the read zipper through the zipper-driven engine. The + // model emits the same value as for `hash`, so the two engines are held to + // agree with the model and hence with each other. + ("hash_iter", (*rz).hash_iter_probe().map(show_hash).unwrap_or_else(|| "skip".to_string())) + } + 63 => { + // The hash of the whole write map from its root, including the root value. + // Release the write zipper to hash the map, then restore its root and focus. + let path = wz.path().to_vec(); + drop(wz); + let h = CatamorphismCached::::hash_with_scheme(&*map0, &Fnv1a64Scheme); + wz = map0.write_zipper_at_path(root0); + wz.descend_to(&path); + ("map_hash", show_hash(h)) + } + 64 => { + // Merkleize the write map. Merkleize always uses the production (gxhash) + // scheme, which the model cannot compute, so what is reported is whether the + // hash it returns equals the map's hash under that same scheme -- the model + // says it always does. Merkleize is the identity on the logical trie, so the + // model does nothing else: the fingerprints, the final dump and a later + // `map_hash` hold it to changing no path, value or hash. The write zipper + // borrows the map, so it is re-created at the same logical position. + let path = wz.path().to_vec(); + drop(wz); + let r = map0.merkleize(); + let h = CatamorphismCached::::hash(&*map0); + wz = map0.write_zipper_at_path(root0); + wz.descend_to(&path); + ("merkleize", show_bool(r.hash == h).to_string()) + } 47 => { let t = get!(d.modn(2)); // The blind-zipper addition: `descend_until` reporting the @@ -1008,7 +1068,7 @@ mod input_format_tests { #[test] fn input_header_count_controls_decoding_independently_of_current_operation_count() { - for count in [1u16, 32, 56, 57, 58, 256] { + for count in [1u16, 32, 56, 57, 58, 61, 65, 256] { let mut bytes = WIRE_MAGIC.to_vec(); bytes.extend_from_slice(&count.to_le_bytes()); bytes.extend(0..=255); @@ -1099,8 +1159,7 @@ mod input_format_tests { 57, 1, // Initialize reverse value iteration at [2]. 59, 1, 59, 1, 59, 1, 59, 1, // Iterate, exhaust at the valued root, wrap. ]); - // ACT's known val_count defect prevents --check at a missing focus. - for trace in [run(&bytes, true), run_act(&bytes, false)] { + for trace in [run(&bytes, true), run_act(&bytes, true)] { for expected in [ "1 to_prev_step ret=1 W=02", "2 to_prev_step ret=1 W=00", @@ -1167,4 +1226,32 @@ mod input_format_tests { assert!(repro.contains("wz.remove_subtrie(true);")); assert!(repro.contains("wz.set_val(14);")); } + + #[test] + fn hash_probes_and_merkleize_preserve_the_write_focus_and_decoder_alignment() { + let mut bytes = input_header(); + bytes.extend_from_slice(&[ + 2, 1, 0, 9, 2, 0, 1, 10, // map0: [0], [0,1]. + 1, 1, 2, 11, // map1: [2]. + 1, 0, 0, // Write root [0], read root []. + 0, 0, 1, 1, // Write focus [0,1]. + 61, 0, 61, 1, 62, 63, 64, // Hash both foci and the map; merkleize. + 27, 14, // The restored focus must still be [0,1]. + ]); + let native = run(&bytes, true); + assert_eq!(native, run_act(&bytes, true)); + for expected in ["1 hash ret=", "2 hash ret=", "3 hash_iter ret=", "4 map_hash ret=", "5 merkleize ret=1"] { + assert!(native.contains(expected), "missing {expected}:\n{native}"); + } + assert!(native.contains("6 set_val ret=10 W=01 o0001 e1 v14"), "{native}"); + assert!(native.contains("MAP0 _:-,00:9,0001:14"), "{native}"); + let repro = emit_repro(&bytes, 256); + assert_eq!(repro.matches("hash_with_scheme").count(), 4); + assert!(repro.contains("map0.merkleize();")); + assert!(repro.contains("wz.set_val(14);")); + } +} + +fn show_hash(h: u128) -> String { + format!("{:016x}", h as u64) } diff --git a/differential/src/repro.rs b/differential/src/repro.rs index 98bc1fba..4b70446c 100644 --- a/differential/src/repro.rs +++ b/differential/src/repro.rs @@ -221,6 +221,11 @@ pub fn emit_repro(bytes: &[u8], upto: usize) -> String { 59 => { let _t = g!(d.modn(2)); "rz.to_prev_val();".to_string() } 60 => { let p = g!(d.path(6)); let v = g!(d.u8()) as u64; format!("wz.set_val_at({}, {v});", rs_bytes(&p)) } + 61 => { let t = g!(d.modn(2)); + format!("pathmap::morphisms::CatamorphismCached::::hash_with_scheme(&{}, &pathmap::morphisms::trie_hash::Fnv1a64Scheme);", z!(t)) } + 62 => "pathmap::morphisms::CatamorphismCachedIterative::::hash_with_scheme(&rz, &pathmap::morphisms::trie_hash::Fnv1a64Scheme);".to_string(), + 63 => format!("{{ let path = wz.path().to_vec(); drop(wz); pathmap::morphisms::CatamorphismCached::::hash_with_scheme(&map0, &pathmap::morphisms::trie_hash::Fnv1a64Scheme); wz = map0.write_zipper_at_path({}); wz.descend_to(&path); }}", rs_bytes(&r0)), + 64 => format!("{{ let path = wz.path().to_vec(); drop(wz); map0.merkleize(); wz = map0.write_zipper_at_path({}); wz.descend_to(&path); }}", rs_bytes(&r0)), _ => "// nop".to_string(), }; o.push_str(&format!(" /* {step:3} */ {line}\n")); diff --git a/examples/merkleize_sharing.rs b/examples/merkleize_sharing.rs new file mode 100644 index 00000000..2858580c --- /dev/null +++ b/examples/merkleize_sharing.rs @@ -0,0 +1,95 @@ +//! Measures how much merkleization shares between logically equal subtries that were built with +//! different physical layouts. Run with `--features viz`. +use std::time::Instant; +use pathmap::PathMap; +use pathmap::morphisms::CatamorphismCached; +use pathmap::viz::{viz_maps, DrawConfig, VizMode}; +use pathmap::zipper::*; + +fn physical_nodes(map: &PathMap<()>) -> usize { + let dc = DrawConfig { mode: VizMode::Mermaid, ascii_path: false, hide_value_paths: true, minimize_values: true, logical: false, color: false }; + let mut out = Vec::new(); + viz_maps(std::slice::from_ref(map), &dc, &mut out).unwrap(); + String::from_utf8(out).unwrap().lines().filter(|l| l.contains("shape: rect")).count() +} + +struct XorShift(u64); +impl XorShift { + fn next(&mut self) -> u64 { self.0 ^= self.0 << 13; self.0 ^= self.0 >> 7; self.0 ^= self.0 << 17; self.0 } + fn shuffle(&mut self, items: &mut [T]) { + for i in (1..items.len()).rev() { let j = (self.next() % (i as u64 + 1)) as usize; items.swap(i, j); } + } +} + +fn random_suffixes(count: usize, len: usize, seed: u64) -> Vec> { + let mut rng = XorShift(seed); + (0..count).map(|_| (0..len).map(|_| (rng.next() >> 32) as u8).collect()).collect() +} + +fn cities() -> Vec> { + let text = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/benches/cities5000.txt")).expect("benches/cities5000.txt"); + text.lines().filter_map(|line| line.split('\t').nth(1)).map(|name| name.as_bytes().to_vec()).collect() +} + +/// Inserts `suffixes` under each of `copies` one-byte prefixes. `shuffle` inserts every copy in +/// its own random order. With `assemble_at > 0`, every odd copy is assembled from parts instead of +/// inserted path by path: the suffixes are grouped by their first `assemble_at` bytes and each group +/// is built as its own map and grafted under that stem, which leaves a node boundary at the stem +/// that direct insertion would not have. +fn build(suffixes: &[Vec], copies: usize, shuffle: bool, assemble_at: usize) -> PathMap<()> { + let mut map = PathMap::<()>::new(); + let mut rng = XorShift(0x2545F4914F6CDD1D); + for copy in 0..copies { + let mut order: Vec<&Vec> = suffixes.iter().collect(); + if shuffle { rng.shuffle(&mut order); } + let prefix = [copy as u8]; + let mut wz = map.write_zipper_at_path(&prefix); + if assemble_at > 0 && copy % 2 == 1 { + let mut groups = std::collections::BTreeMap::<&[u8], Vec<&[u8]>>::new(); + for suffix in order { + let split = assemble_at.min(suffix.len()); + groups.entry(&suffix[..split]).or_default().push(&suffix[split..]); + } + for (stem, rests) in groups { + let sub: PathMap<()> = rests.into_iter().map(|rest| (rest, ())).collect(); + wz.descend_to(stem); + wz.graft_map(sub); + wz.reset(); + } + } else { + for suffix in order { wz.descend_to(suffix); wz.set_val(()); wz.reset(); } + } + } + map +} + +fn report(name: &str, mut map: PathMap<()>, single_copy: &PathMap<()>) { + let paths = map.val_count(); + let before = physical_nodes(&map); + let hash = CatamorphismCached::hash(&map); + let one_copy = physical_nodes(single_copy); + let t = Instant::now(); + let result = map.merkleize(); + let elapsed = t.elapsed(); + let after = physical_nodes(&map); + assert_eq!(map.val_count(), paths); + println!("{name:<24} paths {paths:>7} nodes before {before:>7} after {after:>7} one copy {one_copy:>6} reused {:>6} replaced {:>6} cloned {:>6} hash matches cata: {} {:.2?}", + result.reused, result.replaced, result.cloned, if result.hash == hash { "yes" } else { "NO" }, elapsed); +} + +fn main() { + let suffixes = random_suffixes(2000, 6, 0x9E3779B97F4A7C15); + let copies = 16; + let one = build(&suffixes, 1, false, 0); + report("same order", build(&suffixes, copies, false, 0), &one); + report("shuffled order", build(&suffixes, copies, true, 0), &one); + report("half assembled at 3", build(&suffixes, copies, false, 3), &one); + report("shuffled + assembled", build(&suffixes, copies, true, 3), &one); + + let names = cities(); + let one = build(&names, 1, false, 0); + report("cities same order", build(&names, 8, false, 0), &one); + report("cities shuffled", build(&names, 8, true, 0), &one); + report("cities half assembled", build(&names, 8, false, 4), &one); + report("cities shuffled+assembled", build(&names, 8, true, 4), &one); +} diff --git a/lean/FINDINGS.md b/lean/FINDINGS.md index 3ce6d531..139986f2 100644 --- a/lean/FINDINGS.md +++ b/lean/FINDINGS.md @@ -560,3 +560,28 @@ so the empty prefix never validates and the source zipper's focus value is invisible. The two disagree exactly when the source has a value at its focus. Both behaviours are modelled (`Map.restrict` versus `Zip.restrict`); it is the kind of divergence worth either documenting or unifying. + +--- + +## 17. The recursive cached cata reads the refcount of the empty-node sentinel + +`case`: `morphisms::tests::recursive_cata_dangling_child_in_two_child_node` -- **memory unsafety; +segfault in release, "misaligned pointer dereference ... 0xbaadf00d" in debug** + +Found by the `hash` / `merkleize` ops of the differential harness on the first run, fixed on the +same branch (`CataCache::skips` for the ordinary cache, in `src/trie_node.rs`). + +```rust +let mut map = PathMap::<()>::new(); +map.insert(&[0, 0], ()); +map.insert(&[0, 1], ()); +map.create_path(&[1]); // root: two 1-byte keys, slot 1 holds the empty-node sentinel +map.val_count(); // or CatamorphismCached::hash(&map), or merkleize() +``` + +A dangling path is a child slot holding the empty-node sentinel, whose pointer is a tag, not an +address. `LineListNode::node_recursive_cata_1b_keys` asks the cache whether the second child can +be traversed directly, and the ordinary cache answered with `node.refcount() <= 1`, which +dereferences the sentinel. `caches` guarded the same read with `is_empty`; `skips` did not. The +fix makes `skips` accept the sentinel without reading it, documents that contract on the trait, +and adds a `debug_assert` to `refcount` so any other caller shows up in debug builds. diff --git a/lean/PathMapModel.lean b/lean/PathMapModel.lean index d2354b43..bc866a84 100644 --- a/lean/PathMapModel.lean +++ b/lean/PathMapModel.lean @@ -4,5 +4,8 @@ import PathMapModel.Zipper import PathMapModel.Write import PathMapModel.Map import PathMapModel.Spec +import PathMapModel.Hash +import PathMapModel.HashSecurity +import PathMapModel.GraftNoop import PathMapModel.Fuzz import PathMapModel.Check diff --git a/lean/PathMapModel/Check.lean b/lean/PathMapModel/Check.lean index d3213b0a..cd0313ad 100644 --- a/lean/PathMapModel/Check.lean +++ b/lean/PathMapModel/Check.lean @@ -1,5 +1,7 @@ import PathMapModel.Spec import PathMapModel.Fuzz +import PathMapModel.Hash +import PathMapModel.GraftNoop /-! # Build-time checks @@ -308,5 +310,56 @@ real `prestrict` bug: a location that both carries a value and branches. -/ [([0], 0), ([0,1], 1), ([0,1,2], 2), ([0,1,3], 3), ([9,9], 4)] ].all (fun es => restrictSelf ops (mk es)) +/-! ## The logical hash + +`Hash.hashU64` is the model of the crate's `Fnv1a64Scheme` trie hash. These pin the definition +to the algebra the crate implements in `CatamorphismCached::hash_with_scheme`: a run steps one +byte at a time, a leaf value sits on the empty node, a value with children is layered on the +hash of the node below it, and the hash at a position sees nothing above it. -/ + +-- The empty trie is the empty node. +#guard Hash.hashU64 fEmpty [] == Hash.leaf + +-- A dangling position hashes as the empty node, exactly like the empty trie. +#guard Hash.hashU64 (PathMap.empty.addPath [0,1]) [0,1] == Hash.leaf + +-- A single run: one `step` per byte above a leaf value. +#guard Hash.hashU64 fRun [] == + Hash.step 0 (Hash.step 0 (Hash.step 0 (Hash.step 0 (Hash.withValue (Hash.value 7) Hash.leaf)))) + +-- The hash at a position is the hash of the subtrie below it, independent of what is above. +#guard Hash.hashU64 fRun [0,0] == Hash.step 0 (Hash.step 0 (Hash.withValue (Hash.value 7) Hash.leaf)) + +-- A value with children: layered on the node below, whose children fold in ascending order. +#guard Hash.hashU64 fBranch [0] == + Hash.withValue (Hash.value 1) + (Hash.node [0, 1] [Hash.withValue (Hash.value 2) Hash.leaf, Hash.withValue (Hash.value 3) Hash.leaf]) + +-- The root value is layered on like any other. +#guard Hash.hashU64 fBranch [] == + Hash.withValue (Hash.value 0) (Hash.node [0, 1] [Hash.hashU64 fBranch [0], Hash.withValue (Hash.value 4) Hash.leaf]) + +-- Different values, and a value present or absent at an interior position, change the hash. +#guard Hash.hashU64 (mk [([0,0], 1)]) [] != Hash.hashU64 (mk [([0,0], 2)]) [] +#guard Hash.hashU64 (mk [([0,0], 1)]) [] != Hash.hashU64 (mk [([0], 1), ([0,0], 1)]) [] +#guard Hash.hashU64 (mk [([0,0], 1)]) [] != Hash.hashU64 (PathMap.empty.addPath [0,0]) [] + +-- The mask bitmap: byte `b` sets bit `b % 8` of byte `b / 8`. +#guard Hash.maskBytes [0, 9, 255] == [1, 2] ++ List.replicate 29 0 ++ [128] +#guard Hash.maskBytes [] == List.replicate 32 0 + +-- `GraftNoop.lean`, executed: grafting a hash-equal subtrie (here, the same contents built in a +-- different order, at a focus inside the trie) leaves every entry as it was. +def fBranchAgain : T := mk [([1], 4), ([0,1], 3), ([0,0], 2), ([0], 1), ([], 0)] +#guard Hash.hashU64 fBranch [0] == Hash.hashU64 fBranchAgain [0] +#guard ((zipAt fBranch [] [0]).graft (zipAt fBranchAgain [] [0])).trie.entries == fBranch.entries +#guard ((zipAt fBranch [] []).graft (zipAt fBranchAgain [] [])).trie.entries == fBranch.entries +-- and a graft that is not hash-equal does change it +#guard ((zipAt fBranch [] [0]).graft (zipAt fRun [] [])).trie.entries != fBranch.entries + +-- Rendering matches `format!("{:016x}")`. +#guard Hash.hex64 0 == "0000000000000000" +#guard Hash.hex64 0xcbf29ce484222325 == "cbf29ce484222325" + end Check end PathMapModel diff --git a/lean/PathMapModel/Fuzz.lean b/lean/PathMapModel/Fuzz.lean index 99091931..cb6186b6 100644 --- a/lean/PathMapModel/Fuzz.lean +++ b/lean/PathMapModel/Fuzz.lean @@ -1,3 +1,4 @@ +import PathMapModel.Hash import PathMapModel.Spec /-! @@ -348,6 +349,18 @@ def step (s : St) (d : Dec) : Option (St × Dec) := do | some fork => dumpAt fork.trie fork.root | none => "_:-" some (emit s "dump" result, d) + -- Hashing (see `Hash.lean`). The crate hashes with `Fnv1a64Scheme` for these ops; the model + -- computes the same function over the logical trie, which knows nothing about node layout. + | 61 => do let (t, d) ← d.mod 2 + let z := getTarget s t + some (emit s "hash" (Hash.hex64 (Hash.hashU64 z.trie z.focus)), d) + | 62 => -- The zipper-driven engine on the read zipper; ACT drives it too + some (emit s "hash_iter" (Hash.hex64 (Hash.hashU64 s.rz.trie s.rz.focus)), d) + | 63 => -- The whole write map from its root, root value included + some (emit s "map_hash" (Hash.hex64 (Hash.hashU64 s.wz.trie [])), d) + | 64 => -- `merkleize` is the identity on the logical trie, and the hash it reports is the + -- map's hash under the production scheme, which the crate checks and reports as `1` + some (emit s "merkleize" "1", d) | 27 => do let (v, d) ← d.u8 let (old, z) := s.wz.setVal (UInt64.ofNat v.toNat) some (emit { s with wz := z } "set_val" (showVal old), d) diff --git a/lean/PathMapModel/GraftNoop.lean b/lean/PathMapModel/GraftNoop.lean new file mode 100644 index 00000000..e5bdf97c --- /dev/null +++ b/lean/PathMapModel/GraftNoop.lean @@ -0,0 +1,890 @@ +import PathMapModel.HashSecurity +import PathMapModel.Write + +/-! +# Grafting a subtrie with the same hash is a no-op + +The operation `merkleize` performs, and the one a user performs with +`wz.graft(&rz)`, is `Zip.graft`: replace everything at and below the write zipper's focus by +the subtrie at the read zipper's focus, root value included. This file proves that when the +two foci hash alike, the graft changes nothing: + +* `graft_noop_of_hash_eq`: for an ideal primitive (injective `H`, injective value hash), if + `hash(wz) = hash(rz)` then `(wz.graft rz).trie` has the same `Entry` as `wz.trie` at + every path. +* `collision_of_graft_ne`: the classical contrapositive, with no assumption on the primitive. + A hash-equal graft that changes the trie exhibits a collision of `H` or of the value hash. +* `graft_noop_of_hashU64_eq`: the instance for the harness's `PathMap` under + `Fnv1a64Scheme`, whose hypothesis is literally `hashU64 wz.trie wz.focus = hashU64 + rz.trie rz.focus`. + +The hash part is `HashSecurity.lean`'s soundness theorem. Most of this file is instead about +the model's normalizer `mk'`, which every write goes through: what `valAt` and `pathExists` +report on its output (`valAt_mk'`, `pathExists_mk'`), and from that what `removeBelow`, +`graftBelow`, `setVal`, `removeVal` and `subtrie` do to a trie, as observations rather than +as list manipulations. Those lemmas are independent of hashing and may serve `Spec.lean`. + +Two hypotheses on the write zipper's trie are the canonical-form invariants every constructor +maintains: it is prefix-closed and its root exists. The read zipper's trie needs only +prefix-closure. +-/ + +namespace PathMapModel +namespace Hash + +open Function + +/-! ## The prefix order -/ + +theorem isPrefixOf_iff : ∀ (p q : Path), Path.isPrefixOf p q = true ↔ ∃ k, q = p ++ k + | [], q => by simp [Path.isPrefixOf] + | _ :: _, [] => by simp [Path.isPrefixOf] + | a :: p, c :: q => by + simp only [Path.isPrefixOf, Bool.and_eq_true, beq_iff_eq, List.cons_append, List.cons.injEq] + constructor + · rintro ⟨rfl, h⟩ + obtain ⟨k, rfl⟩ := (isPrefixOf_iff p q).mp h + exact ⟨k, rfl, rfl⟩ + · rintro ⟨k, rfl, rfl⟩ + exact ⟨rfl, (isPrefixOf_iff p (p ++ k)).mpr ⟨k, rfl⟩⟩ + +theorem isPrefixOf_append (p k : Path) : Path.isPrefixOf p (p ++ k) = true := + (isPrefixOf_iff p (p ++ k)).mpr ⟨k, rfl⟩ + +theorem isPrefixOf_refl (p : Path) : Path.isPrefixOf p p = true := by + simpa using isPrefixOf_append p [] + +theorem isPrefixOf_antisymm {p q : Path} (h₁ : Path.isPrefixOf p q = true) + (h₂ : Path.isPrefixOf q p = true) : p = q := by + obtain ⟨k, rfl⟩ := (isPrefixOf_iff p q).mp h₁ + obtain ⟨k', hk'⟩ := (isPrefixOf_iff (p ++ k) p).mp h₂ + have : p ++ [] = p ++ (k ++ k') := by simpa using hk' + have := List.append_cancel_left this + have hk : k = [] := by + cases k with + | nil => rfl + | cons _ _ => simp at this + simp [hk] + +theorem isPrefixOf_trans {p q r : Path} (h₁ : Path.isPrefixOf p q = true) + (h₂ : Path.isPrefixOf q r = true) : Path.isPrefixOf p r = true := by + obtain ⟨k, rfl⟩ := (isPrefixOf_iff p q).mp h₁ + obtain ⟨k', rfl⟩ := (isPrefixOf_iff (p ++ k) r).mp h₂ + exact (isPrefixOf_iff _ _).mpr ⟨k ++ k', by simp⟩ + +theorem isPrefixOf_nil_right {q : Path} (h : Path.isPrefixOf q [] = true) : q = [] := by + obtain ⟨k, hk⟩ := (isPrefixOf_iff q []).mp h + exact (List.append_eq_nil_iff.mp hk.symm).1 + +/-- A prefix of `p ++ r` is a prefix of `p`, or extends `p` by a prefix of `r`. -/ +theorem isPrefixOf_append_iff : ∀ (q p r : Path), + Path.isPrefixOf q (p ++ r) = true ↔ + Path.isPrefixOf q p = true ∨ ∃ k, q = p ++ k ∧ Path.isPrefixOf k r = true + | q, [], r => by + simp only [List.nil_append] + constructor + · intro h; exact Or.inr ⟨q, rfl, h⟩ + · rintro (h | ⟨k, rfl, h⟩) + · rw [isPrefixOf_nil_right h]; simp [Path.isPrefixOf] + · simpa using h + | [], _ :: _, _ => by simp [Path.isPrefixOf] + | c :: q, a :: p, r => by + simp only [List.cons_append, Path.isPrefixOf, Bool.and_eq_true, beq_iff_eq, List.cons.injEq] + rw [isPrefixOf_append_iff q p r] + constructor + · rintro ⟨rfl, h | ⟨k, rfl, h⟩⟩ + · exact Or.inl ⟨rfl, h⟩ + · exact Or.inr ⟨k, ⟨rfl, rfl⟩, h⟩ + · rintro (⟨rfl, h⟩ | ⟨k, ⟨rfl, rfl⟩, h⟩) + · exact ⟨rfl, Or.inl h⟩ + · exact ⟨rfl, Or.inr ⟨k, rfl, h⟩⟩ + +theorem mem_prefixes (q r : Path) : q ∈ r.prefixes ↔ Path.isPrefixOf q r = true := by + simp only [Path.prefixes, List.mem_map, List.mem_range] + constructor + · rintro ⟨n, _, rfl⟩ + exact (isPrefixOf_iff _ _).mpr ⟨r.drop n, (List.take_append_drop n r).symm⟩ + · intro h + obtain ⟨k, rfl⟩ := (isPrefixOf_iff q r).mp h + exact ⟨q.length, by simp; omega, by rw [List.take_append_of_le_length (Nat.le_refl _)]; simp⟩ + +/-- Prefix closure in the `≼` form: a prefix of an existing path exists. -/ +theorem present_of_prefix {V : Type} {t : PathMap V} (pc : PrefixClosed t) {p r : Path} + (hr : t.pathExists r = true) (hp : Path.isPrefixOf p r = true) : t.pathExists p = true := by + obtain ⟨k, rfl⟩ := (isPrefixOf_iff p r).mp hp + cases h : t.pathExists p with + | true => rfl + | false => rw [not_present_below pc h k] at hr; cases hr + +/-! ## Membership through the sorters -/ + +theorem mem_insertSorted (a p : Path) : ∀ (l : List Path), a ∈ Path.insertSorted p l ↔ a = p ∨ a ∈ l + | [] => by simp [Path.insertSorted] + | q :: qs => by + simp only [Path.insertSorted] + split + · rename_i h; simp only [beq_iff_eq] at h; subst h; simp + · split + · simp + · simp [mem_insertSorted a p qs] + constructor + · rintro (h | h | h) <;> simp [h] + · rintro (h | h | h) <;> simp [h] + +theorem mem_sortDedup (a : Path) (l : List Path) : a ∈ Path.sortDedup l ↔ a ∈ l := by + suffices ∀ acc, a ∈ l.foldl (fun acc p => Path.insertSorted p acc) acc ↔ a ∈ l ∨ a ∈ acc by + simpa [Path.sortDedup] using this [] + induction l with + | nil => simp + | cons p l ih => + intro acc + simp only [List.foldl_cons, ih, mem_insertSorted, List.mem_cons] + constructor + · rintro (h | h | h) <;> simp [h] + · rintro ((h | h) | h) <;> simp [h] + +/-! ## Lookups -/ + +theorem lookup_map_pair {β : Type} (f : Path → β) (q : Path) : + ∀ (l : List Path), List.lookup q (l.map fun p => (p, f p)) = if q ∈ l then some (f q) else none + | [] => by simp + | p :: l => by + simp only [List.map_cons, List.lookup_cons, List.mem_cons] + by_cases h : q = p + · subst h; simp + · have : (q == p) = false := beq_eq_false_iff_ne.mpr h + simp [this, h, lookup_map_pair f q l] + +theorem lookup_filter_key {V : Type} (f : Path → Bool) (q : Path) : + ∀ (l : List (Path × V)), + List.lookup q (l.filter fun kv => f kv.1) = if f q then List.lookup q l else none + | [] => by simp + | (k, v) :: l => by + simp only [List.filter_cons] + by_cases hq : q = k + · rw [hq] + cases hf : f k <;> simp [hf, List.lookup_cons, lookup_filter_key f k l] + · have hb : (q == k) = false := beq_eq_false_iff_ne.mpr hq + cases hf : f k <;> simp [hf, List.lookup_cons, hb, lookup_filter_key f q l] + +/-- `lookup` over `vals` is `valAt`, for any trie. -/ +theorem lookup_filterMap_vals {V : Type} (t : PathMap V) (q : Path) : + ∀ (l : List Path), + List.lookup q (l.filterMap fun p => (t.entryAt p).val.map fun v => (p, v)) = + if q ∈ l then t.valAt q else none + | [] => by simp + | p :: l => by + simp only [List.filterMap_cons, List.mem_cons] + by_cases hq : q = p + · rw [hq] + cases hv : (t.entryAt p).val with + | none => simp [lookup_filterMap_vals t p l, PathMap.valAt, hv] + | some v => simp [List.lookup_cons, PathMap.valAt, hv] + · have hb : (q == p) = false := beq_eq_false_iff_ne.mpr hq + cases hv : (t.entryAt p).val <;> simp [List.lookup_cons, hb, hq, lookup_filterMap_vals t q l] + +theorem lookup_vals {V : Type} (t : PathMap V) (q : Path) : t.vals.lookup q = t.valAt q := by + rw [PathMap.vals, lookup_filterMap_vals] + split + · rfl + · rename_i hq + have : t.pathExists q = false := by + cases h : t.pathExists q with + | false => rfl + | true => exact absurd ((mem_paths_iff_present t q).mpr h) hq + exact (valAt_eq_none_of_absent this).symm + +/-- A valued path exists. -/ +theorem present_of_valAt {V : Type} {t : PathMap V} {q : Path} {v : V} (h : t.valAt q = some v) : + t.pathExists q = true := + PathMap.Entry.present_of_val h + +/-- The value list `graftBelow` appends: `s`'s non-root values moved under `p`. -/ +def shiftVals {V : Type} (p : Path) (l : List (Path × V)) : List (Path × V) := + l.filterMap fun kv => if kv.1 == ([] : Path) then none else some (p ++ kv.1, kv.2) + +theorem lookup_shiftVals_below {V : Type} (p k : Path) (hk : k ≠ []) : + ∀ (l : List (Path × V)), List.lookup (p ++ k) (shiftVals p l) = List.lookup k l + | [] => by simp [shiftVals] + | (k', v) :: l => by + simp only [shiftVals, List.filterMap_cons] + by_cases h0 : k' = [] + · have hb : (k == k') = false := beq_eq_false_iff_ne.mpr (by rw [h0]; exact hk) + have hb' : (k' == ([] : Path)) = true := beq_iff_eq.mpr h0 + have ih := lookup_shiftVals_below p k hk l + simp only [shiftVals] at ih + simp only [hb', if_true, List.lookup_cons, hb] + exact ih + · have hb : (k' == ([] : Path)) = false := beq_eq_false_iff_ne.mpr h0 + by_cases hkk : k = k' + · rw [hkk]; simp [hb, List.lookup_cons] + · have h1 : (p ++ k == p ++ k') = false := + beq_eq_false_iff_ne.mpr fun h => hkk (List.append_cancel_left h) + have h2 : (k == k') = false := beq_eq_false_iff_ne.mpr hkk + simp only [hb, Bool.false_eq_true, if_false, List.lookup_cons, h1, h2] + exact lookup_shiftVals_below p k hk l + +theorem lookup_shiftVals_other {V : Type} (p q : Path) (hq : ∀ k, k ≠ [] → q ≠ p ++ k) : + ∀ (l : List (Path × V)), List.lookup q (shiftVals p l) = none + | [] => by simp [shiftVals] + | (k', v) :: l => by + simp only [shiftVals, List.filterMap_cons] + by_cases h0 : k' = [] + · have hb' : (k' == ([] : Path)) = true := beq_iff_eq.mpr h0 + simp only [hb', if_true] + exact lookup_shiftVals_other p q hq l + · have hb : (k' == ([] : Path)) = false := beq_eq_false_iff_ne.mpr h0 + have h1 : (q == p ++ k') = false := beq_eq_false_iff_ne.mpr (hq k' h0) + simp only [hb, Bool.false_eq_true, if_false, List.lookup_cons, h1] + exact lookup_shiftVals_other p q hq l + +/-- The value list `subtrie` is built from: the values under `f`, with `f` stripped. -/ +def stripVals {V : Type} (f : Path) (l : List (Path × V)) : List (Path × V) := + l.filterMap fun kv => (Path.stripPrefix f kv.1).map fun r => (r, kv.2) + +theorem lookup_stripVals {V : Type} (f k : Path) : + ∀ (l : List (Path × V)), List.lookup k (stripVals f l) = List.lookup (f ++ k) l + | [] => by simp [stripVals] + | (k', v) :: l => by + simp only [stripVals, List.filterMap_cons] + cases hs : Path.stripPrefix f k' with + | none => + have h1 : (f ++ k == k') = false := beq_eq_false_iff_ne.mpr fun h => by + have := (stripPrefix_eq_some f k' k).mpr h.symm + rw [hs] at this; cases this + simp only [Option.map_none, List.lookup_cons, h1] + exact lookup_stripVals f k l + | some r => + have hr := (stripPrefix_eq_some f k' r).mp hs + by_cases hkr : k = r + · rw [hkr, hr]; simp [List.lookup_cons] + · have h1 : (k == r) = false := beq_eq_false_iff_ne.mpr hkr + have h2 : (f ++ k == k') = false := beq_eq_false_iff_ne.mpr fun h => hkr (by + rw [hr] at h; exact List.append_cancel_left h) + simp only [Option.map_some, List.lookup_cons, h1, h2] + exact lookup_stripVals f k l + +/-! ## The normalizer `mk'` -/ + +theorem lookup_eq_some_iff_of_nodup {V : Type} (q : Path) (v : V) : + ∀ (l : List (Path × V)), (l.map (·.1)).Nodup → (l.lookup q = some v ↔ (q, v) ∈ l) + | [], _ => by simp + | (k, v') :: l, hn => by + rw [List.map_cons, List.nodup_cons] at hn + simp only [List.lookup_cons, List.mem_cons] + by_cases hq : q = k + · rw [hq] + simp only [beq_self_eq_true, Option.some.injEq, Prod.mk.injEq, true_and] + constructor + · intro h; exact Or.inl h.symm + · rintro (h | h) + · exact h.symm + · exact absurd (List.mem_map.mpr ⟨(k, v), h, rfl⟩) hn.1 + · have hb : (q == k) = false := beq_eq_false_iff_ne.mpr hq + simp only [hb] + rw [lookup_eq_some_iff_of_nodup q v l hn.2] + constructor + · intro h; exact Or.inr h + · rintro (h | h) + · exact absurd (congrArg Prod.fst h) hq + · exact h + +theorem lookup_eq_of_perm {V : Type} (q : Path) {l₁ l₂ : List (Path × V)} (h : l₁.Perm l₂) + (hn : (l₁.map (·.1)).Nodup) : l₁.lookup q = l₂.lookup q := by + have hn₂ : (l₂.map (·.1)).Nodup := ((h.map (·.1)).nodup_iff).mp hn + cases h₁ : l₁.lookup q with + | some v => + have := (lookup_eq_some_iff_of_nodup q v l₁ hn).mp h₁ + exact ((lookup_eq_some_iff_of_nodup q v l₂ hn₂).mpr (h.mem_iff.mp this)).symm + | none => + have h₁' : ¬ (l₁.lookup q).isSome = true := by simp [h₁] + rw [lookup_isSome_iff] at h₁' + have : ¬ (l₂.lookup q).isSome = true := by + rw [lookup_isSome_iff, ← (h.map (·.1)).mem_iff]; exact h₁' + cases h₂ : l₂.lookup q with + | some _ => simp [h₂] at this + | none => rfl + +theorem perm_insertValSorted {V : Type} (kv : Path × V) : + ∀ (l : List (Path × V)), (PathMap.insertValSorted kv l).Perm (kv :: l) + | [] => List.Perm.refl _ + | kv' :: l => by + simp only [PathMap.insertValSorted] + split + · exact List.Perm.refl _ + · exact ((perm_insertValSorted kv l).cons kv').trans (List.Perm.swap kv kv' l) + +theorem perm_foldl_insertValSorted {V : Type} : + ∀ (l acc : List (Path × V)), + (l.foldl (fun acc kv => PathMap.insertValSorted kv acc) acc).Perm (l ++ acc) + | [], acc => by simp [List.Perm.refl] + | kv :: l, acc => by + simp only [List.foldl_cons, List.cons_append] + exact (perm_foldl_insertValSorted l _).trans + (((perm_insertValSorted kv acc).append_left l).trans List.perm_middle) + +theorem lookup_dedupVals {V : Type} (q : Path) (l : List (Path × V)) : + (PathMap.dedupVals l).lookup q = l.lookup q := by + suffices ∀ acc : List (Path × V), + (l.foldl (fun acc kv => if acc.any (fun x => x.1 == kv.1) then acc else acc ++ [kv]) acc).lookup q = + (acc.lookup q).or (l.lookup q) by + simpa [PathMap.dedupVals] using this [] + induction l with + | nil => intro acc; simp + | cons kv l ih => + intro acc + obtain ⟨k, v⟩ := kv + simp only [List.foldl_cons] + rw [ih] + by_cases hq : q = k + · have hb : (q == k) = true := beq_iff_eq.mpr hq + split + · rename_i h + obtain ⟨x, hx, hxq⟩ := List.any_eq_true.mp h + have hs : (acc.lookup q).isSome = true := + (lookup_isSome_iff q acc).mpr + (List.mem_map.mpr ⟨x, hx, by rw [hq]; exact beq_iff_eq.mp hxq⟩) + cases hl : acc.lookup q with + | none => simp [hl] at hs + | some _ => simp + · simp only [List.lookup_append, Option.or_assoc, List.lookup_cons, List.lookup_nil, hb, + Option.some_or] + · have hb : (q == k) = false := beq_eq_false_iff_ne.mpr hq + split + · simp only [List.lookup_cons, hb] + · simp only [List.lookup_append, Option.or_assoc, List.lookup_cons, List.lookup_nil, hb, + Option.none_or] + +theorem nodup_keys_dedupVals {V : Type} (l : List (Path × V)) : + ((PathMap.dedupVals l).map (·.1)).Nodup := by + suffices ∀ acc : List (Path × V), (acc.map (·.1)).Nodup → + ((l.foldl (fun acc kv => if acc.any (fun x => x.1 == kv.1) then acc else acc ++ [kv]) acc).map (·.1)).Nodup by + simpa [PathMap.dedupVals] using this [] (by simp) + induction l with + | nil => intro acc h; simpa using h + | cons kv l ih => + intro acc hacc + simp only [List.foldl_cons] + apply ih + split + · exact hacc + · rename_i h + have hnot : kv.1 ∉ acc.map (·.1) := fun hm => by + obtain ⟨x, hx, hxe⟩ := List.mem_map.mp hm + exact h (List.any_eq_true.mpr ⟨x, hx, beq_iff_eq.mpr hxe⟩) + rw [List.map_append, List.map_cons, List.map_nil] + exact (List.perm_middle (l₂ := [])).nodup_iff.mpr + (List.nodup_cons.mpr ⟨by simpa using hnot, by simpa using hacc⟩) + +theorem lookup_normVals {V : Type} (q : Path) (l : List (Path × V)) : + (PathMap.normVals l).lookup q = l.lookup q := by + rw [← lookup_dedupVals q l] + unfold PathMap.normVals + have hperm := perm_foldl_insertValSorted (PathMap.dedupVals l) [] + rw [List.append_nil] at hperm + exact lookup_eq_of_perm q hperm ((hperm.map (·.1)).nodup_iff.mpr (nodup_keys_dedupVals l)) + +theorem mem_keys_iff_lookup {V : Type} (q : Path) (l : List (Path × V)) : + q ∈ l.map (·.1) ↔ (l.lookup q).isSome = true := (lookup_isSome_iff q l).symm + +/-- The locations `mk' vals paths` creates: every prefix of the root, of a given path, or of a +valued path. -/ +def existingOf {V : Type} (vals : List (Path × V)) (paths : List Path) : List Path := + Path.sortDedup ((([] : Path) :: (paths ++ (PathMap.normVals vals).map (·.1))).flatMap Path.prefixes) + +theorem mk'_eq {V : Type} (vals : List (Path × V)) (paths : List Path) : + PathMap.mk' vals paths = + { entries := (existingOf vals paths).map fun p => (p, (PathMap.normVals vals).lookup p) } := rfl + +theorem mem_existingOf {V : Type} (vals : List (Path × V)) (paths : List Path) (q : Path) : + q ∈ existingOf vals paths ↔ + ∃ r, (r = [] ∨ r ∈ paths ∨ (vals.lookup r).isSome = true) ∧ Path.isPrefixOf q r = true := by + simp only [existingOf, mem_sortDedup, List.mem_flatMap, List.mem_cons, List.mem_append, + mem_prefixes, mem_keys_iff_lookup, lookup_normVals] + +theorem valAt_entries {V : Type} (l : List Path) (g : Path → Option V) (q : Path) : + PathMap.valAt { entries := l.map fun p => (p, g p) } q = if q ∈ l then g q else none := by + unfold PathMap.valAt PathMap.entryAt + rw [lookup_map_pair] + by_cases h : q ∈ l + · simp only [h, if_true]; cases g q <;> rfl + · simp only [h, if_false]; rfl + +theorem pathExists_entries {V : Type} (l : List Path) (g : Path → Option V) (q : Path) : + PathMap.pathExists { entries := l.map fun p => (p, g p) } q = true ↔ q ∈ l := by + unfold PathMap.pathExists PathMap.entryAt + rw [lookup_map_pair] + by_cases h : q ∈ l + · simp only [h, if_true]; cases g q <;> simp [PathMap.Entry.present] + · simp only [h, if_false]; simp [PathMap.Entry.present] + +/-- What `mk'` holds at `q`: the first binding for `q` in `vals`. -/ +theorem valAt_mk' {V : Type} (vals : List (Path × V)) (paths : List Path) (q : Path) : + (PathMap.mk' vals paths).valAt q = vals.lookup q := by + rw [mk'_eq, valAt_entries, lookup_normVals] + split + · rfl + · rename_i hq + cases hl : vals.lookup q with + | none => rfl + | some v => + exfalso + apply hq + rw [mem_existingOf] + exact ⟨q, Or.inr (Or.inr (by rw [hl]; rfl)), isPrefixOf_refl q⟩ + +/-- Where `mk'` has a location: at every prefix of a given path or of a valued path. -/ +theorem pathExists_mk' {V : Type} (vals : List (Path × V)) (paths : List Path) (q : Path) : + (PathMap.mk' vals paths).pathExists q = true ↔ + ∃ r, (r = [] ∨ r ∈ paths ∨ (vals.lookup r).isSome = true) ∧ Path.isPrefixOf q r = true := by + rw [mk'_eq, pathExists_entries, mem_existingOf] + +/-! ## What the trie operations do, as observations + +`keep p q` is the filter `removeBelow` applies to a path: it survives unless it is strictly +below `p`. -/ + +def keep (p q : Path) : Prop := Path.isPrefixOf p q = false ∨ q = p + +instance (p q : Path) : Decidable (keep p q) := inferInstanceAs (Decidable (_ ∨ _)) + +theorem keep_iff (p q : Path) : (!(Path.isPrefixOf p q) || q == p) = true ↔ keep p q := by + simp [keep] + +theorem keep_of_prefix {p q r : Path} (hk : keep p r) (hqr : Path.isPrefixOf q r = true) : keep p q := by + rcases hk with h | rfl + · left + cases hq : Path.isPrefixOf p q with + | false => rfl + | true => rw [isPrefixOf_trans hq hqr] at h; cases h + · by_cases hpq : Path.isPrefixOf r q = true + · right; exact isPrefixOf_antisymm hqr hpq + · left; simpa using hpq + +theorem not_keep_below {p k : Path} (hk : k ≠ []) : ¬ keep p (p ++ k) := by + rintro (h | h) + · rw [isPrefixOf_append] at h; cases h + · exact hk (List.append_cancel_left (h.trans (List.append_nil p).symm)) + +theorem ne_append_of_ne_nil {p k : Path} (hk : k ≠ []) : p ≠ p ++ k := fun h => + hk (List.append_cancel_left ((List.append_nil p).trans h)).symm + +/-! ### `removeBelow` -/ + +theorem removeBelow_eq {V : Type} (t : PathMap V) (p : Path) : + t.removeBelow p = + PathMap.mk' (t.vals.filter fun kv => !(Path.isPrefixOf p kv.1) || kv.1 == p) + (t.paths.filter fun q => !(Path.isPrefixOf p q) || q == p) := rfl + +theorem valAt_removeBelow {V : Type} (t : PathMap V) (p q : Path) : + (t.removeBelow p).valAt q = if keep p q then t.valAt q else none := by + rw [removeBelow_eq, valAt_mk', lookup_filter_key (fun k => !(Path.isPrefixOf p k) || k == p), lookup_vals] + by_cases h : keep p q + · rw [if_pos h, if_pos ((keep_iff p q).mpr h)] + · rw [if_neg h, if_neg (fun h' => h ((keep_iff p q).mp h'))] + +theorem pathExists_removeBelow {V : Type} {t : PathMap V} (pc : PrefixClosed t) (p q : Path) : + (t.removeBelow p).pathExists q = true ↔ q = [] ∨ (t.pathExists q = true ∧ keep p q) := by + rw [removeBelow_eq, pathExists_mk'] + constructor + · rintro ⟨r, hr | hr | hr, hqr⟩ + · subst hr; exact Or.inl (isPrefixOf_nil_right hqr) + · rw [List.mem_filter, keep_iff] at hr + exact Or.inr ⟨present_of_prefix pc ((mem_paths_iff_present t r).mp hr.1) hqr, keep_of_prefix hr.2 hqr⟩ + · rw [lookup_filter_key (fun k => !(Path.isPrefixOf p k) || k == p), lookup_vals] at hr + split at hr + · rename_i hk + rw [keep_iff] at hk + cases hv : t.valAt r with + | none => rw [hv] at hr; cases hr + | some v => + exact Or.inr ⟨present_of_prefix pc (present_of_valAt hv) hqr, keep_of_prefix hk hqr⟩ + · cases hr + · rintro (rfl | ⟨hq, hk⟩) + · exact ⟨[], Or.inl rfl, isPrefixOf_refl []⟩ + · refine ⟨q, Or.inr (Or.inl ?_), isPrefixOf_refl q⟩ + rw [List.mem_filter, keep_iff] + exact ⟨(mem_paths_iff_present t q).mpr hq, hk⟩ + +/-! ### `graftBelow` -/ + +/-- The path list `graftBelow` appends: `s`'s non-root locations moved under `p`. -/ +def shiftPaths (p : Path) (l : List Path) : List Path := + l.filterMap fun q => if q == ([] : Path) then none else some (p ++ q) + +theorem mem_shiftPaths (p r : Path) (l : List Path) : + r ∈ shiftPaths p l ↔ ∃ r₀, r₀ ∈ l ∧ r₀ ≠ [] ∧ r = p ++ r₀ := by + simp only [shiftPaths, List.mem_filterMap] + constructor + · rintro ⟨r₀, h₀, hr⟩ + split at hr + · cases hr + · rename_i hne + cases hr + exact ⟨r₀, h₀, fun h => hne (beq_iff_eq.mpr h), rfl⟩ + · rintro ⟨r₀, h₀, hne, rfl⟩ + refine ⟨r₀, h₀, ?_⟩ + rw [if_neg (fun h => hne (beq_iff_eq.mp h))] + +theorem graftBelow_eq {V : Type} (t : PathMap V) (p : Path) (s : PathMap V) : + t.graftBelow p s = + PathMap.mk' ((t.removeBelow p).vals ++ shiftVals p s.vals) + ((t.removeBelow p).paths ++ shiftPaths p s.paths) := rfl + +theorem valAt_graftBelow_other {V : Type} (t : PathMap V) (p : Path) (s : PathMap V) {q : Path} + (h : Path.isPrefixOf p q = false) : (t.graftBelow p s).valAt q = t.valAt q := by + rw [graftBelow_eq, valAt_mk', List.lookup_append, lookup_vals, valAt_removeBelow, + if_pos (show keep p q from Or.inl h), + lookup_shiftVals_other p q (fun k hk hq => by rw [hq, isPrefixOf_append] at h; cases h), Option.or_none] + +theorem valAt_graftBelow_focus {V : Type} (t : PathMap V) (p : Path) (s : PathMap V) : + (t.graftBelow p s).valAt p = t.valAt p := by + rw [graftBelow_eq, valAt_mk', List.lookup_append, lookup_vals, valAt_removeBelow, + if_pos (show keep p p from Or.inr rfl), + lookup_shiftVals_other p p (fun k hk => ne_append_of_ne_nil hk), Option.or_none] + +theorem valAt_graftBelow_below {V : Type} (t : PathMap V) (p : Path) (s : PathMap V) {k : Path} + (hk : k ≠ []) : (t.graftBelow p s).valAt (p ++ k) = s.valAt k := by + rw [graftBelow_eq, valAt_mk', List.lookup_append, lookup_vals, valAt_removeBelow, + if_neg (not_keep_below hk), lookup_shiftVals_below p k hk, lookup_vals, Option.none_or] + +/-- `s` has a location other than its root. -/ +def nonRoot {V : Type} (s : PathMap V) : Prop := ∃ r, r ∈ s.paths ∧ r ≠ [] + +theorem pathExists_graftBelow {V : Type} {t s : PathMap V} (pct : PrefixClosed t) (pcs : PrefixClosed s) + (p q : Path) : + (t.graftBelow p s).pathExists q = true ↔ + q = [] ∨ (t.pathExists q = true ∧ keep p q) ∨ (nonRoot s ∧ Path.isPrefixOf q p = true) ∨ + (∃ k, q = p ++ k ∧ k ≠ [] ∧ s.pathExists k = true) := by + rw [graftBelow_eq, pathExists_mk'] + -- a location of `t.removeBelow p` reached from `q` gives the second disjunct + have fromCleared : ∀ r, (t.removeBelow p).pathExists r = true → Path.isPrefixOf q r = true → + q = [] ∨ (t.pathExists q = true ∧ keep p q) := by + intro r hr hqr + rcases (pathExists_removeBelow pct p r).mp hr with rfl | ⟨hr, hk⟩ + · exact Or.inl (isPrefixOf_nil_right hqr) + · exact Or.inr ⟨present_of_prefix pct hr hqr, keep_of_prefix hk hqr⟩ + -- a shifted location of `s` reached from `q` gives the third or fourth disjunct + have fromShifted : ∀ r₀, r₀ ∈ s.paths → r₀ ≠ [] → Path.isPrefixOf q (p ++ r₀) = true → + (nonRoot s ∧ Path.isPrefixOf q p = true) ∨ (∃ k, q = p ++ k ∧ k ≠ [] ∧ s.pathExists k = true) := by + intro r₀ h₀ hne hq + rcases (isPrefixOf_append_iff q p r₀).mp hq with h | ⟨k, rfl, hk⟩ + · exact Or.inl ⟨⟨r₀, h₀, hne⟩, h⟩ + · cases k with + | nil => exact Or.inl ⟨⟨r₀, h₀, hne⟩, by simpa using isPrefixOf_refl p⟩ + | cons b k => + exact Or.inr ⟨b :: k, rfl, by simp, + present_of_prefix pcs ((mem_paths_iff_present s r₀).mp h₀) hk⟩ + constructor + · rintro ⟨r, hr | hr | hr, hqr⟩ + · subst hr; exact Or.inl (isPrefixOf_nil_right hqr) + · rw [List.mem_append] at hr + rcases hr with hr | hr + · exact (fromCleared r ((mem_paths_iff_present _ r).mp hr) hqr).elim Or.inl (Or.inr ∘ Or.inl) + · obtain ⟨r₀, h₀, hne, rfl⟩ := (mem_shiftPaths p r s.paths).mp hr + exact Or.inr (Or.inr (fromShifted r₀ h₀ hne hqr)) + · rw [List.lookup_append] at hr + cases hc : (t.removeBelow p).vals.lookup r with + | some v => + rw [lookup_vals] at hc + exact (fromCleared r (present_of_valAt hc) hqr).elim Or.inl (Or.inr ∘ Or.inl) + | none => + rw [hc, Option.none_or] at hr + by_cases hex : ∃ k, k ≠ [] ∧ r = p ++ k + · obtain ⟨k, hk, rfl⟩ := hex + rw [lookup_shiftVals_below p k hk, lookup_vals] at hr + cases hv : s.valAt k with + | none => rw [hv] at hr; cases hr + | some v => + exact Or.inr (Or.inr (fromShifted k + ((mem_paths_iff_present s k).mpr (present_of_valAt hv)) hk hqr)) + · rw [lookup_shiftVals_other p r (fun k hk hr => hex ⟨k, hk, hr⟩)] at hr + cases hr + · rintro (rfl | ⟨hq, hk⟩ | ⟨⟨r₀, h₀, hne⟩, hqp⟩ | ⟨k, rfl, hk, hs⟩) + · exact ⟨[], Or.inl rfl, isPrefixOf_refl []⟩ + · refine ⟨q, Or.inr (Or.inl ?_), isPrefixOf_refl q⟩ + rw [List.mem_append] + exact Or.inl ((mem_paths_iff_present _ q).mpr ((pathExists_removeBelow pct p q).mpr (Or.inr ⟨hq, hk⟩))) + · refine ⟨p ++ r₀, Or.inr (Or.inl ?_), isPrefixOf_trans hqp (isPrefixOf_append p r₀)⟩ + rw [List.mem_append, mem_shiftPaths] + exact Or.inr ⟨r₀, h₀, hne, rfl⟩ + · refine ⟨p ++ k, Or.inr (Or.inl ?_), isPrefixOf_refl _⟩ + rw [List.mem_append, mem_shiftPaths] + exact Or.inr ⟨k, (mem_paths_iff_present s k).mpr hs, hk, rfl⟩ + +/-! ### `setVal`, `removeVal` -/ + +theorem valAt_setVal {V : Type} (t : PathMap V) (p : Path) (v : V) (q : Path) : + (t.setVal p v).2.valAt q = if q = p then some v else t.valAt q := by + simp only [PathMap.setVal] + rw [valAt_mk', List.lookup_cons, lookup_filter_key (fun k => !(k == p)), lookup_vals] + by_cases h : q = p + · rw [if_pos h]; simp [beq_iff_eq.mpr h] + · rw [if_neg h]; simp [beq_eq_false_iff_ne.mpr h] + +theorem pathExists_setVal {V : Type} {t : PathMap V} (pc : PrefixClosed t) (p : Path) (v : V) (q : Path) : + (t.setVal p v).2.pathExists q = true ↔ + q = [] ∨ t.pathExists q = true ∨ Path.isPrefixOf q p = true := by + simp only [PathMap.setVal] + rw [pathExists_mk'] + constructor + · rintro ⟨r, hr | hr | hr, hqr⟩ + · subst hr; exact Or.inl (isPrefixOf_nil_right hqr) + · exact Or.inr (Or.inl (present_of_prefix pc ((mem_paths_iff_present t r).mp hr) hqr)) + · rw [List.lookup_cons] at hr + by_cases hrp : r = p + · subst hrp; exact Or.inr (Or.inr hqr) + · rw [beq_eq_false_iff_ne.mpr hrp] at hr + simp only [] at hr + rw [lookup_filter_key (fun k => !(k == p)), lookup_vals] at hr + split at hr + · cases hv : t.valAt r with + | none => rw [hv] at hr; cases hr + | some _ => exact Or.inr (Or.inl (present_of_prefix pc (present_of_valAt hv) hqr)) + · cases hr + · rintro (rfl | hq | hqp) + · exact ⟨[], Or.inl rfl, isPrefixOf_refl []⟩ + · exact ⟨q, Or.inr (Or.inl ((mem_paths_iff_present t q).mpr hq)), isPrefixOf_refl q⟩ + · refine ⟨p, Or.inr (Or.inr ?_), hqp⟩ + rw [List.lookup_cons]; simp + +theorem valAt_removeVal {V : Type} (t : PathMap V) (p q : Path) : + (t.removeVal p).2.valAt q = if q = p then none else t.valAt q := by + simp only [PathMap.removeVal] + rw [valAt_mk', lookup_filter_key (fun k => !(k == p)), lookup_vals] + by_cases h : q = p + · rw [if_pos h]; simp [beq_iff_eq.mpr h] + · rw [if_neg h]; simp [beq_eq_false_iff_ne.mpr h] + +theorem pathExists_removeVal {V : Type} {t : PathMap V} (pc : PrefixClosed t) (p q : Path) : + (t.removeVal p).2.pathExists q = true ↔ q = [] ∨ t.pathExists q = true := by + simp only [PathMap.removeVal] + rw [pathExists_mk'] + constructor + · rintro ⟨r, hr | hr | hr, hqr⟩ + · subst hr; exact Or.inl (isPrefixOf_nil_right hqr) + · exact Or.inr (present_of_prefix pc ((mem_paths_iff_present t r).mp hr) hqr) + · rw [lookup_filter_key (fun k => !(k == p)), lookup_vals] at hr + split at hr + · cases hv : t.valAt r with + | none => rw [hv] at hr; cases hr + | some _ => exact Or.inr (present_of_prefix pc (present_of_valAt hv) hqr) + · cases hr + · rintro (rfl | hq) + · exact ⟨[], Or.inl rfl, isPrefixOf_refl []⟩ + · exact ⟨q, Or.inr (Or.inl ((mem_paths_iff_present t q).mpr hq)), isPrefixOf_refl q⟩ + +/-! ### `subtrie` (what `make_map` / a read zipper's `graft` source holds) -/ + +theorem subtrie_eq {V : Type} (t : PathMap V) (f : Path) : + t.subtrie f = PathMap.mk' (stripVals f t.vals) (t.paths.filterMap (Path.stripPrefix f)) := rfl + +theorem valAt_subtrie {V : Type} (t : PathMap V) (f k : Path) : + (t.subtrie f).valAt k = t.valAt (f ++ k) := by + rw [subtrie_eq, valAt_mk', lookup_stripVals, lookup_vals] + +theorem isPrefixOf_append_left_iff (f k r : Path) : + Path.isPrefixOf (f ++ k) (f ++ r) = true ↔ Path.isPrefixOf k r = true := by + rw [isPrefixOf_iff, isPrefixOf_iff] + constructor + · rintro ⟨k', h⟩ + exact ⟨k', List.append_cancel_left (by rw [← List.append_assoc]; exact h)⟩ + · rintro ⟨k', rfl⟩ + exact ⟨k', by simp⟩ + +theorem pathExists_subtrie {V : Type} {t : PathMap V} (pc : PrefixClosed t) (f k : Path) : + (t.subtrie f).pathExists k = true ↔ k = [] ∨ t.pathExists (f ++ k) = true := by + rw [subtrie_eq, pathExists_mk'] + constructor + · rintro ⟨r, hr | hr | hr, hkr⟩ + · subst hr; exact Or.inl (isPrefixOf_nil_right hkr) + · obtain ⟨r', hr', hs⟩ := List.mem_filterMap.mp hr + rw [(stripPrefix_eq_some f r' r).mp hs] at hr' + exact Or.inr (present_of_prefix pc ((mem_paths_iff_present t _).mp hr') + ((isPrefixOf_append_left_iff f k r).mpr hkr)) + · rw [lookup_stripVals, lookup_vals] at hr + cases hv : t.valAt (f ++ r) with + | none => rw [hv] at hr; cases hr + | some _ => + exact Or.inr (present_of_prefix pc (present_of_valAt hv) + ((isPrefixOf_append_left_iff f k r).mpr hkr)) + · rintro (rfl | hk) + · exact ⟨[], Or.inl rfl, isPrefixOf_refl []⟩ + · refine ⟨k, Or.inr (Or.inl ?_), isPrefixOf_refl k⟩ + exact List.mem_filterMap.mpr ⟨f ++ k, (mem_paths_iff_present t _).mpr hk, + (stripPrefix_eq_some f (f ++ k) k).mpr rfl⟩ + +theorem nonRoot_subtrie {V : Type} {t : PathMap V} (pc : PrefixClosed t) (f : Path) : + nonRoot (t.subtrie f) ↔ ∃ k, k ≠ [] ∧ t.pathExists (f ++ k) = true := by + constructor + · rintro ⟨r, hr, hne⟩ + rcases (pathExists_subtrie pc f r).mp ((mem_paths_iff_present _ r).mp hr) with h | h + · exact absurd h hne + · exact ⟨r, hne, h⟩ + · rintro ⟨k, hne, hk⟩ + exact ⟨k, (mem_paths_iff_present _ k).mpr ((pathExists_subtrie pc f k).mpr (Or.inr hk)), hne⟩ + +theorem prefixClosed_subtrie {V : Type} {t : PathMap V} (pc : PrefixClosed t) (f : Path) : + PrefixClosed (t.subtrie f) := by + intro r b hrb + rw [pathExists_subtrie pc] at hrb ⊢ + rcases hrb with h | h + · exact absurd h (by simp) + · exact Or.inr (present_of_prefix pc h (by rw [← List.append_assoc]; exact isPrefixOf_append _ _)) + +/-! ## The theorem -/ + +/-- `Zip.graft` unfolded: `graftBelow` at the focus, then the source's root value set or +removed there. -/ +theorem graft_trie_eq {V : Type} (z src : Zip V) : + (z.graft src).trie = + match (src.trie.subtrie src.focus).valAt [] with + | some v => ((z.trie.graftBelow z.focus (src.trie.subtrie src.focus)).setVal z.focus v).2 + | none => ((z.trie.graftBelow z.focus (src.trie.subtrie src.focus)).removeVal z.focus).2 := rfl + +/-- **Grafting a subtrie with the same hash is a no-op**, for an ideal primitive. The write +zipper's trie is prefix-closed with an existing root, and the source's trie is prefix-closed: +the canonical-form invariants every constructor maintains. -/ +theorem graft_noop_of_hash_eq {V : Type} (H : List UInt8 → UInt64) (hH : Injective H) + (vh : V → UInt64) (hvh : Injective vh) (z src : Zip V) + (pcz : PrefixClosed z.trie) (rootz : z.trie.pathExists [] = true) (pcs : PrefixClosed src.trie) + (h : hashWith H vh z.trie (depth z.trie + 1) z.focus = + hashWith H vh src.trie (depth src.trie + 1) src.focus) : + ∀ q, (z.graft src).trie.entryAt q = z.trie.entryAt q := by + intro q + -- the two subtries are logically equal + have L := hashWith_inj_imp_logicalEq H hH vh hvh z.trie src.trie pcz pcs _ _ z.focus src.focus + (by omega) (by omega) h + obtain ⟨LV0, L2⟩ := L + have LV : ∀ k, k ≠ [] → z.trie.valAt (z.focus ++ k) = src.trie.valAt (src.focus ++ k) := + fun k hk => congrArg PathMap.Entry.val (L2 k hk) + have LP : ∀ k, k ≠ [] → z.trie.pathExists (z.focus ++ k) = src.trie.pathExists (src.focus ++ k) := + fun k hk => congrArg PathMap.Entry.present (L2 k hk) + -- the grafted map `m`, and what it holds + have hres := graft_trie_eq z src + generalize hm : src.trie.subtrie src.focus = m at hres + have pcm : PrefixClosed m := by rw [← hm]; exact prefixClosed_subtrie pcs src.focus + have hmval : m.valAt [] = z.trie.valAt z.focus := by + rw [← hm, valAt_subtrie, List.append_nil, LV0] + have hmval' : ∀ k, k ≠ [] → m.valAt k = z.trie.valAt (z.focus ++ k) := fun k hk => by + rw [← hm, valAt_subtrie, LV k hk] + have hmpres : ∀ k, m.pathExists k = true ↔ k = [] ∨ src.trie.pathExists (src.focus ++ k) = true := by + intro k; rw [← hm]; exact pathExists_subtrie pcs src.focus k + have hnonRoot : nonRoot m → z.trie.pathExists z.focus = true := by + intro hn + rw [← hm] at hn + obtain ⟨k, hk, hpres⟩ := (nonRoot_subtrie pcs src.focus).mp hn + rw [← LP k hk] at hpres + exact present_of_prefix pcz hpres (isPrefixOf_append z.focus k) + -- the graft below the focus, `t'`, and what it holds + rw [hmval] at hres + generalize ht' : z.trie.graftBelow z.focus m = t' at hres + have t'below : ∀ k, k ≠ [] → t'.valAt (z.focus ++ k) = z.trie.valAt (z.focus ++ k) := fun k hk => by + rw [← ht', valAt_graftBelow_below z.trie z.focus m hk, hmval' k hk] + have t'other : ∀ q, Path.isPrefixOf z.focus q = false → t'.valAt q = z.trie.valAt q := fun q hq => by + rw [← ht', valAt_graftBelow_other z.trie z.focus m hq] + have t'pres : ∀ q, t'.pathExists q = true ↔ + z.trie.pathExists q = true ∨ (Path.isPrefixOf q z.focus = true ∧ nonRoot m) := by + intro q + rw [← ht', pathExists_graftBelow pcz pcm z.focus q] + constructor + · rintro (rfl | ⟨hq, _⟩ | ⟨hn, hqp⟩ | ⟨k, rfl, hk, hs⟩) + · exact Or.inl rootz + · exact Or.inl hq + · exact Or.inr ⟨hqp, hn⟩ + · left + rcases (hmpres k).mp hs with h | h + · exact absurd h hk + · rw [← LP k hk] at h; exact h + · rintro (hq | ⟨hqp, hn⟩) + · by_cases hpq : Path.isPrefixOf z.focus q = true + · obtain ⟨k, rfl⟩ := (isPrefixOf_iff z.focus q).mp hpq + cases k with + | nil => exact Or.inr (Or.inl ⟨hq, Or.inr (by simp)⟩) + | cons b k => + refine Or.inr (Or.inr (Or.inr ⟨b :: k, rfl, by simp, ?_⟩)) + rw [hmpres] + exact Or.inr (by rw [← LP (b :: k) (by simp)]; exact hq) + · exact Or.inr (Or.inl ⟨hq, Or.inl (by simpa using hpq)⟩) + · exact Or.inr (Or.inr (Or.inl ⟨hn, hqp⟩)) + have hpresP : ∀ q, (z.trie.pathExists q = true ∨ (Path.isPrefixOf q z.focus = true ∧ nonRoot m)) → + z.trie.pathExists q = true := by + rintro q (hq | ⟨hqp, hn⟩) + · exact hq + · exact present_of_prefix pcz (hnonRoot hn) hqp + have pct' : PrefixClosed t' := by + intro r b hrb + have := (t'pres (r ++ [b])).mp hrb + exact (t'pres r).mpr (Or.inl (present_of_prefix pcz (hpresP _ this) (isPrefixOf_append r [b]))) + -- assemble the entry at `q` + apply entry_ext' + · -- presence + show (z.graft src).trie.pathExists q = z.trie.pathExists q + rw [Bool.eq_iff_iff] + cases hv : z.trie.valAt z.focus with + | some v => + rw [hv] at hres + simp only [] at hres + rw [hres, pathExists_setVal pct' z.focus v q] + constructor + · rintro (rfl | hq | hqp) + · exact rootz + · exact hpresP q ((t'pres q).mp hq) + · exact present_of_prefix pcz (present_of_valAt hv) hqp + · intro hq; exact Or.inr (Or.inl ((t'pres q).mpr (Or.inl hq))) + | none => + rw [hv] at hres + simp only [] at hres + rw [hres, pathExists_removeVal pct' z.focus q] + constructor + · rintro (rfl | hq) + · exact rootz + · exact hpresP q ((t'pres q).mp hq) + · intro hq; exact Or.inr ((t'pres q).mpr (Or.inl hq)) + · -- value + show (z.graft src).trie.valAt q = z.trie.valAt q + have hval : (z.graft src).trie.valAt q = if q = z.focus then z.trie.valAt z.focus else t'.valAt q := by + cases hv : z.trie.valAt z.focus with + | some v => rw [hv] at hres; simp only [] at hres; rw [hres, valAt_setVal] + | none => rw [hv] at hres; simp only [] at hres; rw [hres, valAt_removeVal] + rw [hval] + by_cases hqp : q = z.focus + · rw [if_pos hqp, hqp] + · rw [if_neg hqp] + by_cases hpq : Path.isPrefixOf z.focus q = true + · obtain ⟨k, rfl⟩ := (isPrefixOf_iff z.focus q).mp hpq + have hk : k ≠ [] := fun h => hqp (by rw [h, List.append_nil]) + exact t'below k hk + · exact t'other q (by simpa using hpq) + +/-- **The collision reduction.** No assumption on the primitive: a hash-equal graft that +changes the trie exhibits a collision of `H` or of the value hash. -/ +theorem collision_of_graft_ne {V : Type} (H : List UInt8 → UInt64) (vh : V → UInt64) (z src : Zip V) + (pcz : PrefixClosed z.trie) (rootz : z.trie.pathExists [] = true) (pcs : PrefixClosed src.trie) + (h : hashWith H vh z.trie (depth z.trie + 1) z.focus = + hashWith H vh src.trie (depth src.trie + 1) src.focus) + (hne : ∃ q, (z.graft src).trie.entryAt q ≠ z.trie.entryAt q) : + (∃ a b, a ≠ b ∧ H a = H b) ∨ (∃ x y, x ≠ y ∧ vh x = vh y) := by + obtain ⟨q, hq⟩ := hne + refine Classical.byContradiction fun hno => hq ?_ + refine graft_noop_of_hash_eq H ?_ vh ?_ z src pcz rootz pcs h q + · intro a b hab + exact Classical.byContradiction fun hne' => hno (Or.inl ⟨a, b, hne', hab⟩) + · intro x y hxy + exact Classical.byContradiction fun hne' => hno (Or.inr ⟨x, y, hne', hxy⟩) + +/-- The harness's instance: `hash(wz) == hash(rz)` under `Fnv1a64Scheme` makes `wz.graft(&rz)` +a no-op, or exhibits an `fnv` collision. -/ +theorem graft_noop_of_hashU64_eq (z src : Zip UInt64) + (pcz : PrefixClosed z.trie) (rootz : z.trie.pathExists [] = true) (pcs : PrefixClosed src.trie) + (h : hashU64 z.trie z.focus = hashU64 src.trie src.focus) + (hne : ∃ q, (z.graft src).trie.entryAt q ≠ z.trie.entryAt q) : + ∃ a b, a ≠ b ∧ fnv a = fnv b := by + rw [hashU64_eq, hashU64_eq] at h + rcases collision_of_graft_ne fnv value z src pcz rootz pcs h hne with + ⟨a, b, hab, he⟩ | ⟨x, y, hxy, he⟩ + · exact ⟨a, b, hab, he⟩ + · exact ⟨leBytes64 x, leBytes64 y, fun e => hxy (leBytes64_inj e), he⟩ + +end Hash +end PathMapModel diff --git a/lean/PathMapModel/Hash.lean b/lean/PathMapModel/Hash.lean new file mode 100644 index 00000000..fda53167 --- /dev/null +++ b/lean/PathMapModel/Hash.lean @@ -0,0 +1,122 @@ +import PathMapModel.PathMap + +/-! +# The logical trie hash + +`pathmap` hashes a trie (`CatamorphismCached::hash`, and the identity of subtries in +`PathMap::merkleize`) as a Merkle tree over the **logical** trie: the byte-radix tree of its +paths, with no notion of the nodes the crate stores it in. For a position `p`: + +* `below p` hashes the child mask of `p` and, in ascending byte order, the hash of every child + `p ++ [b]`. A byte inside a non-branching run is a mask with one bit; an endpoint with no + children has the empty mask, whose hash is `leaf`. +* `hash p` is `below p` with the value at `p` layered on top, if `p` holds one. + +This file is that definition over the model's `PathMap`, which knows nothing about node +layout, so any dependence of the crate's hash on layout shows up as a divergence in the +differential harness (`Fuzz.lean`, ops `hash`, `hash_iter`, `map_hash`, `merkleize`). + +The crate's production primitive is gxhash, which is not worth transcribing. The harness +instead runs the crate with `pathmap::morphisms::trie_hash::Fnv1a64Scheme`, whose primitives +are the few lines of FNV-1a below. Every constant here mirrors that scheme: + +* `value v` = FNV-1a over the 8 little-endian bytes of `v : UInt64` +* `node mask children` = FNV-1a over `'N'`, the 32-byte little-endian bitmap of `mask`, then + 8 little-endian bytes of each child hash +* `withValue vh below` = FNV-1a over `'V'`, 8 bytes of `below`, then 8 bytes of `vh` + +`HashSecurity.lean` proves that this layering is sound for any primitive: two logically +different tries that hash alike exhibit a collision of the primitive. +-/ + +namespace PathMapModel +namespace Hash + +/-! ## FNV-1a -/ + +def fnvOffset : UInt64 := 0xcbf29ce484222325 +def fnvPrime : UInt64 := 0x100000001b3 + +/-- Absorb one byte. -/ +def absorb (h : UInt64) (b : UInt8) : UInt64 := (h ^^^ b.toUInt64) * fnvPrime + +/-- Absorb a byte string. -/ +def absorbBytes (h : UInt64) (bs : List UInt8) : UInt64 := bs.foldl absorb h + +/-- The 8 bytes of a `UInt64`, least significant first. -/ +def leBytes64 (x : UInt64) : List UInt8 := + [x.toUInt8, (x >>> 8).toUInt8, (x >>> 16).toUInt8, (x >>> 24).toUInt8, + (x >>> 32).toUInt8, (x >>> 40).toUInt8, (x >>> 48).toUInt8, (x >>> 56).toUInt8] + +/-- A byte from its bits, least significant first: `digits [b0, b1, ..] = b0 + 2 * (b1 + 2 * ..)`. -/ +def digits : List Bool → Nat + | [] => 0 + | b :: bs => b.toNat + 2 * digits bs + +/-- The 32-byte little-endian bitmap of a child mask: byte `i` holds bits `8i .. 8i+7`, so byte +`b` of the mask sets bit `b % 8` of byte `b / 8`. This is the in-memory layout of +`pathmap::utils::ByteMask` (`[u64; 4]`) on a little-endian machine, which is what the crate +absorbs. Written through `digits` so that `HashSecurity.lean` can show it is injective. -/ +def maskBytes (m : ByteMask) : List UInt8 := + (List.range 32).map fun i => + UInt8.ofNat (digits ((List.range 8).map fun j => m.contains (UInt8.ofNat (8 * i + j)))) + +/-! ## The scheme (`Fnv1a64Scheme`) -/ + +/-- The primitive: FNV-1a over a whole message. -/ +def fnv (msg : List UInt8) : UInt64 := absorbBytes fnvOffset msg + +/-- `HashScheme::value` for `u64`: what `::hash` writes, little-endian. -/ +def value (v : UInt64) : UInt64 := fnv (leBytes64 v) + +/-- `HashScheme::start`, `child`, `finish` in one: a logical node with the given children, in +the order given (callers pass them in ascending byte order). -/ +def node (m : ByteMask) (children : List UInt64) : UInt64 := + children.foldl (fun h c => absorbBytes h (leBytes64 c)) + (absorbBytes (absorb fnvOffset 0x4E) (maskBytes m)) + +/-- `HashScheme::with_value`. -/ +def withValue (vh below : UInt64) : UInt64 := + absorbBytes (absorbBytes (absorb fnvOffset 0x56) (leBytes64 below)) (leBytes64 vh) + +/-- `HashScheme::leaf`: a position with no children and no value. -/ +def leaf : UInt64 := node [] [] + +/-- `HashScheme::step`: one byte of a non-branching run above `below`. -/ +def step (b : UInt8) (below : UInt64) : UInt64 := node [b] [below] + +/-! ## The hash of a logical trie -/ + +/-- The hash of the subtrie at `p`, given `fuel` at least the depth of the trie below `p`. +Fuel is a bound on recursion depth: with enough of it the result does not depend on it, and +the trie is finite so `logicalHash` always supplies enough. Running out of fuel yields the +hash of the empty message, which no node or value message can produce (see +`HashSecurity.lean`), so an inadequate fuel can never be mistaken for a real hash. -/ +def hashFuel {V : Type} (vh : V → UInt64) (t : PathMap V) : Nat → Path → UInt64 + | 0, _ => fnv [] + | fuel + 1, p => + let m := t.childMask p + let below := node m (m.map fun b => hashFuel vh t fuel (p ++ [b])) + match t.valAt p with + | some v => withValue (vh v) below + | none => below + +/-- The length of the longest existing path: every position in the trie is at most this deep. -/ +def depth {V : Type} (t : PathMap V) : Nat := + t.paths.foldl (fun acc p => max acc p.length) 0 + +/-- `CatamorphismCached::hash_with_scheme` of a zipper whose focus is `p`, and of a `PathMap` +when `p = []`: the hash of the logical trie at and below `p`, including the value at `p`. -/ +def logicalHash {V : Type} (vh : V → UInt64) (t : PathMap V) (p : Path) : UInt64 := + hashFuel vh t (depth t + 1) p + +/-- The harness's instance: `PathMap` under `Fnv1a64Scheme`. -/ +def hashU64 (t : PathMap UInt64) (p : Path) : UInt64 := logicalHash value t p + +/-- 16 lowercase hex digits, matching the crate's `format!("{:016x}")`. -/ +def hex64 (x : UInt64) : String := + let digit (n : Nat) : Char := if n < 10 then Char.ofNat (48 + n) else Char.ofNat (87 + n) + String.ofList <| (List.range 16).reverse.map fun i => digit ((x >>> (UInt64.ofNat (4 * i))).toNat % 16) + +end Hash +end PathMapModel diff --git a/lean/PathMapModel/HashSecurity.lean b/lean/PathMapModel/HashSecurity.lean new file mode 100644 index 00000000..b666ffb4 --- /dev/null +++ b/lean/PathMapModel/HashSecurity.lean @@ -0,0 +1,492 @@ +import Std.Tactic.BVDecide +import PathMapModel.Hash + +/-! +# Soundness of the hashing scheme + +`Hash.lean` defines the logical trie hash as a Merkle tree over an abstract primitive. This +file proves that the *scheme* adds no weakness of its own: if two tries that differ as +logical tries hash alike, then two different **messages** were handed to the primitive and +came back with the same digest. Any collision resistance the primitive has therefore +transfers to the trie hash. Nothing here is about the primitive itself (the crate's gxhash +makes no collision-resistance claim, and the harness's FNV-1a certainly does not); the +primitive is a parameter `H`, and the theorems hold for every `H`. + +The argument is the usual one for Merkle trees, and it rests on the messages being +**decodable**: + +* a node message is `N`, the 32-byte mask bitmap, then 8 bytes per child, so the tag + separates it from a value message, the bitmap determines the child set and hence how + many digests follow, and each digest can be cut out by its fixed width; +* a value message is `V`, 8 bytes of the hash below, 8 bytes of the value hash; +* running out of fuel yields `H []`, and the empty message is neither of the above. + +The exact statements: + +* `hashWith_inj_imp_logicalEq`: for an injective `H` and an injective value hash, equal + hashes at two positions mean the two subtries are observationally equal + (`LogicalEq`: same value at the position, same `Entry` at every path strictly below). +* `collision_of_hash_eq`: the classical contrapositive, with no assumption on `H` -- equal + hashes of unequal subtries exhibit a collision of `H` or of the value hash. +* `fnv_collision_of_hashU64_eq`: the instance for the harness's `hashU64`, where the value + hash is itself `fnv` over the value's bytes, so a collision is always an `fnv` collision. + +Two points the statements make explicit. The hash at a position does not see whether the +position itself exists: a dangling endpoint and an absent one both hash as the empty node, +because a location's existence is recorded in its parent's mask. At the root this is moot +(the root always exists), which is why the root corollary yields full equality. And the +value type's encoding is assumed injective (`vh` injective, or for `hashU64`, `leBytes64` +injective); that is the crate's `Hash` impl, not the scheme. +-/ + +namespace PathMapModel +namespace Hash + +open Function + +/-! ## The scheme over an arbitrary primitive -/ + +/-- The message a logical node with mask `m` and child digests `children` is hashed from. -/ +def nodeMsg (m : ByteMask) (children : List UInt64) : List UInt8 := + 0x4E :: (maskBytes m ++ (children.map leBytes64).flatten) + +/-- The message a value is layered on with. -/ +def valMsg (vh below : UInt64) : List UInt8 := + 0x56 :: (leBytes64 below ++ leBytes64 vh) + +/-- `hashFuel` with an arbitrary primitive `H` in place of `fnv`. -/ +def hashWith {V : Type} (H : List UInt8 → UInt64) (vh : V → UInt64) (t : PathMap V) : + Nat → Path → UInt64 + | 0, _ => H [] + | fuel + 1, p => + let m := t.childMask p + let below := H (nodeMsg m (m.map fun b => hashWith H vh t fuel (p ++ [b]))) + match t.valAt p with + | some v => H (valMsg (vh v) below) + | none => below + +/-- The hash of what lies strictly below `p`, at `fuel + 1`. -/ +def belowOf {V : Type} (H : List UInt8 → UInt64) (vh : V → UInt64) (t : PathMap V) + (fuel : Nat) (p : Path) : UInt64 := + H (nodeMsg (t.childMask p) ((t.childMask p).map fun b => hashWith H vh t fuel (p ++ [b]))) + +theorem hashWith_succ {V : Type} (H : List UInt8 → UInt64) (vh : V → UInt64) (t : PathMap V) + (fuel : Nat) (p : Path) : + hashWith H vh t (fuel + 1) p = + match t.valAt p with + | some v => H (valMsg (vh v) (belowOf H vh t fuel p)) + | none => belowOf H vh t fuel p := rfl + +/-! ## `hashFuel` is `hashWith fnv` -/ + +theorem absorbBytes_cons (h : UInt64) (b : UInt8) (bs : List UInt8) : + absorbBytes h (b :: bs) = absorbBytes (absorb h b) bs := rfl + +theorem absorbBytes_append (h : UInt64) (a b : List UInt8) : + absorbBytes h (a ++ b) = absorbBytes (absorbBytes h a) b := by + simp [absorbBytes, List.foldl_append] + +theorem absorbBytes_flatten (h : UInt64) (cs : List UInt64) : + absorbBytes h (cs.map leBytes64).flatten = + cs.foldl (fun h c => absorbBytes h (leBytes64 c)) h := by + induction cs generalizing h with + | nil => rfl + | cons c cs ih => simp [List.flatten_cons, absorbBytes_append, ih] + +theorem node_eq_fnv (m : ByteMask) (cs : List UInt64) : node m cs = fnv (nodeMsg m cs) := by + simp only [node, fnv, nodeMsg, absorbBytes_cons, absorbBytes_append, absorbBytes_flatten] + +theorem withValue_eq_fnv (vh below : UInt64) : withValue vh below = fnv (valMsg vh below) := by + simp only [withValue, fnv, valMsg, absorbBytes_cons, absorbBytes_append] + +theorem hashFuel_eq_hashWith {V : Type} (vh : V → UInt64) (t : PathMap V) : + ∀ (fuel : Nat) (p : Path), hashFuel vh t fuel p = hashWith fnv vh t fuel p + | 0, _ => rfl + | fuel + 1, p => by + rw [hashWith_succ] + have hk : (t.childMask p).map (fun b => hashFuel vh t fuel (p ++ [b])) = + (t.childMask p).map (fun b => hashWith fnv vh t fuel (p ++ [b])) := + List.map_congr_left fun b _ => hashFuel_eq_hashWith vh t fuel (p ++ [b]) + simp only [hashFuel, belowOf, node_eq_fnv, withValue_eq_fnv, hk] + cases t.valAt p <;> rfl + +/-! ## Decodability of the messages -/ + +theorem length_leBytes64 (x : UInt64) : (leBytes64 x).length = 8 := rfl + +theorem length_maskBytes (m : ByteMask) : (maskBytes m).length = 32 := by + simp [maskBytes] + +/-- Eight little-endian bytes determine the word. -/ +theorem leBytes64_inj : Injective leBytes64 := by + intro x y h + simp only [leBytes64, List.cons.injEq, and_true] at h + bv_decide + +theorem digits_lt : ∀ (l : List Bool), digits l < 2 ^ l.length + | [] => by simp [digits] + | b :: l => by + have := digits_lt l + cases b <;> simp [digits, Nat.pow_succ] <;> omega + +theorem digits_inj : ∀ (l₁ l₂ : List Bool), l₁.length = l₂.length → digits l₁ = digits l₂ → l₁ = l₂ + | [], [], _, _ => rfl + | [], _ :: _, hl, _ => by simp at hl + | _ :: _, [], hl, _ => by simp at hl + | b₁ :: l₁, b₂ :: l₂, hl, hd => by + simp only [digits] at hd + have hl' : l₁.length = l₂.length := by simpa using hl + have hb : b₁ = b₂ ∧ digits l₁ = digits l₂ := by + cases b₁ <;> cases b₂ <;> simp at hd ⊢ <;> omega + rw [hb.1, digits_inj l₁ l₂ hl' hb.2] + +theorem ofNat_inj_of_lt {a b : Nat} (ha : a < 256) (hb : b < 256) + (h : UInt8.ofNat a = UInt8.ofNat b) : a = b := by + have := congrArg UInt8.toNat h + rwa [UInt8.toNat_ofNat', UInt8.toNat_ofNat', Nat.mod_eq_of_lt ha, Nat.mod_eq_of_lt hb] at this + +/-- The bitmap determines membership: bit `b % 8` of byte `b / 8`. -/ +theorem maskBytes_inj_contains (m₁ m₂ : ByteMask) (h : maskBytes m₁ = maskBytes m₂) (b : UInt8) : + m₁.contains b = m₂.contains b := by + have hb : b.toNat < 256 := by have := UInt8.toNat_lt b; omega + unfold maskBytes at h + have hbyte := List.map_inj_left.mp h (b.toNat / 8) (List.mem_range.mpr (by omega)) + have hlen : ∀ (f : Nat → Bool), ((List.range 8).map f).length = 8 := fun f => by simp + have hlt : ∀ (f : Nat → Bool), digits ((List.range 8).map f) < 256 := fun f => by + have := digits_lt ((List.range 8).map f) + rw [hlen] at this + omega + have hd := ofNat_inj_of_lt (hlt _) (hlt _) hbyte + have hl := digits_inj _ _ (by rw [hlen, hlen]) hd + have hj := List.map_inj_left.mp hl (b.toNat % 8) (List.mem_range.mpr (by omega)) + rw [Nat.div_add_mod, UInt8.ofNat_toNat] at hj + exact hj + +theorem mem_ofList (bs : List UInt8) (b : UInt8) : b ∈ ByteMask.ofList bs ↔ b ∈ bs := by + simp only [ByteMask.ofList, List.mem_filterMap, List.mem_range] + constructor + · rintro ⟨i, _, hi⟩ + split at hi + · rename_i hc + cases hi + exact List.contains_iff_mem.mp hc + · cases hi + · intro hb + refine ⟨b.toNat, by have := UInt8.toNat_lt b; omega, ?_⟩ + rw [UInt8.ofNat_toNat, if_pos (List.contains_iff_mem.mpr hb)] + +theorem contains_ofList (bs : List UInt8) (b : UInt8) : + (ByteMask.ofList bs).contains b = bs.contains b := by + rw [Bool.eq_iff_iff, List.contains_iff_mem, List.contains_iff_mem] + exact mem_ofList bs b + +theorem ofList_congr (bs₁ bs₂ : List UInt8) (h : ∀ b, bs₁.contains b = bs₂.contains b) : + ByteMask.ofList bs₁ = ByteMask.ofList bs₂ := by + simp only [ByteMask.ofList, h] + +/-- Canonical masks (every `childMask`) with the same members are equal. -/ +theorem childMask_eq_of_contains {V : Type} (t₁ t₂ : PathMap V) (p₁ p₂ : Path) + (h : ∀ b, (t₁.childMask p₁).contains b = (t₂.childMask p₂).contains b) : + t₁.childMask p₁ = t₂.childMask p₂ := by + unfold PathMap.childMask at h ⊢ + exact ofList_congr _ _ fun b => by + have := h b + rwa [contains_ofList, contains_ofList] at this + +theorem flatten_fixed_inj {α : Type} (w : Nat) : + ∀ (l₁ l₂ : List (List α)), (∀ x ∈ l₁, x.length = w) → (∀ x ∈ l₂, x.length = w) → + l₁.length = l₂.length → l₁.flatten = l₂.flatten → l₁ = l₂ + | [], [], _, _, _, _ => rfl + | [], _ :: _, _, _, hl, _ => by simp at hl + | _ :: _, [], _, _, hl, _ => by simp at hl + | x :: l₁, y :: l₂, h₁, h₂, hl, hf => by + simp only [List.flatten_cons] at hf + have hx : x.length = w := h₁ x (by simp) + have hy : y.length = w := h₂ y (by simp) + obtain ⟨rfl, hrest⟩ := List.append_inj hf (hx.trans hy.symm) + rw [flatten_fixed_inj w l₁ l₂ (fun z hz => h₁ z (by simp [hz])) (fun z hz => h₂ z (by simp [hz])) + (by simpa using hl) hrest] + +theorem nodeMsg_inj {m₁ m₂ : ByteMask} {c₁ c₂ : List UInt64} (h : nodeMsg m₁ c₁ = nodeMsg m₂ c₂) : + maskBytes m₁ = maskBytes m₂ ∧ (c₁.map leBytes64).flatten = (c₂.map leBytes64).flatten := by + simp only [nodeMsg, List.cons.injEq, true_and] at h + exact List.append_inj h (by simp [length_maskBytes]) + +theorem children_inj {c₁ c₂ : List UInt64} (hlen : c₁.length = c₂.length) + (h : (c₁.map leBytes64).flatten = (c₂.map leBytes64).flatten) : c₁ = c₂ := by + have := flatten_fixed_inj 8 _ _ (by simp [length_leBytes64]) (by simp [length_leBytes64]) + (by simpa using hlen) h + exact (List.map_inj_right fun _ _ hxy => leBytes64_inj hxy).mp this + +/-- A node message and a value message never coincide, and neither is empty. -/ +theorem nodeMsg_ne_valMsg (m : ByteMask) (c : List UInt64) (vh below : UInt64) : + nodeMsg m c ≠ valMsg vh below := by + intro h + have := (List.cons.inj h).1 + exact absurd this (by decide) + +theorem nodeMsg_ne_nil (m : ByteMask) (c : List UInt64) : nodeMsg m c ≠ [] := by + simp [nodeMsg] + +theorem valMsg_ne_nil (vh below : UInt64) : valMsg vh below ≠ [] := by + simp [valMsg] + +/-! ## Facts about the trie model -/ + +/-- Every trie the crate can build is prefix-closed; the model's constructors maintain it +(`PathMap.mk'`), and it is what puts "nothing below an absent path" on solid ground. -/ +def PrefixClosed {V : Type} (t : PathMap V) : Prop := + ∀ (p : Path) (b : UInt8), t.pathExists (p ++ [b]) = true → t.pathExists p = true + +theorem entry_ext' {V : Type} {e₁ e₂ : PathMap.Entry V} (hp : e₁.present = e₂.present) + (hv : e₁.val = e₂.val) : e₁ = e₂ := by + cases e₁ <;> cases e₂ <;> simp_all [PathMap.Entry.present, PathMap.Entry.val] + +theorem entryAt_eq_absent_iff {V : Type} (t : PathMap V) (p : Path) : + t.entryAt p = .absent ↔ t.pathExists p = false := by + unfold PathMap.pathExists + cases t.entryAt p <;> simp [PathMap.Entry.present] + +theorem valAt_eq_none_of_absent {V : Type} {t : PathMap V} {p : Path} + (h : t.pathExists p = false) : t.valAt p = none := by + unfold PathMap.valAt + rw [(entryAt_eq_absent_iff t p).mpr h] + rfl + +theorem lookup_isSome_iff {α β : Type} [BEq α] [LawfulBEq α] (a : α) : + ∀ (l : List (α × β)), (l.lookup a).isSome = true ↔ a ∈ l.map (·.1) + | [] => by simp + | (k, v) :: l => by + simp only [List.lookup_cons, List.map_cons, List.mem_cons] + by_cases h : a = k + · subst h; simp + · have hk : (a == k) = false := beq_eq_false_iff_ne.mpr h + simp [hk, h, lookup_isSome_iff a l] + +theorem mem_paths_iff_present {V : Type} (t : PathMap V) (q : Path) : + q ∈ t.paths ↔ t.pathExists q = true := by + rw [PathMap.paths, ← lookup_isSome_iff] + unfold PathMap.pathExists PathMap.entryAt + cases t.entries.lookup q with + | none => simp [PathMap.Entry.present] + | some o => cases o <;> simp [PathMap.Entry.present] + +theorem stripPrefix_eq_some : ∀ (p q r : Path), Path.stripPrefix p q = some r ↔ q = p ++ r + | [], q, r => by simp [Path.stripPrefix] + | _ :: _, [], r => by simp [Path.stripPrefix] + | a :: p, c :: q, r => by + simp only [Path.stripPrefix, List.cons_append, List.cons.injEq] + by_cases hac : a = c + · subst hac; simp [stripPrefix_eq_some p q r] + · have : (a == c) = false := beq_eq_false_iff_ne.mpr hac + simp [this, Ne.symm hac] + +theorem mem_childMask_iff {V : Type} (t : PathMap V) (p : Path) (b : UInt8) : + b ∈ t.childMask p ↔ t.pathExists (p ++ [b]) = true := by + rw [← mem_paths_iff_present] + unfold PathMap.childMask + rw [← List.contains_iff_mem, contains_ofList, List.contains_iff_mem, List.mem_filterMap] + constructor + · rintro ⟨q, hq, hb⟩ + split at hb + · rename_i r hr + cases hb + rw [(stripPrefix_eq_some _ _ _).mp hr] at hq + exact hq + · cases hb + · intro h + refine ⟨p ++ [b], h, ?_⟩ + rw [show Path.stripPrefix p (p ++ [b]) = some [b] from (stripPrefix_eq_some _ _ _).mpr rfl] + +theorem not_present_below {V : Type} {t : PathMap V} (pc : PrefixClosed t) {p : Path} + (hp : t.pathExists p = false) : ∀ q, t.pathExists (p ++ q) = false := by + intro q + induction q generalizing p with + | nil => simpa using hp + | cons b q ih => + have hb : t.pathExists (p ++ [b]) = false := by + cases h : t.pathExists (p ++ [b]) with + | true => rw [pc p b h] at hp; cases hp + | false => rfl + rw [List.append_cons] + exact ih hb + +theorem le_foldl_max (l : List Path) (a : Nat) : + a ≤ l.foldl (fun acc p => max acc p.length) a ∧ + ∀ q ∈ l, q.length ≤ l.foldl (fun acc p => max acc p.length) a := by + induction l generalizing a with + | nil => simp + | cons p l ih => + simp only [List.foldl_cons, List.mem_cons] + obtain ⟨h1, h2⟩ := ih (max a p.length) + refine ⟨by omega, ?_⟩ + rintro q (rfl | hq) + · omega + · exact h2 q hq + +theorem absent_of_deep {V : Type} {t : PathMap V} {q : Path} (h : depth t < q.length) : + t.pathExists q = false := by + cases hq : t.pathExists q with + | false => rfl + | true => + have := (le_foldl_max t.paths 0).2 q ((mem_paths_iff_present t q).mpr hq) + unfold depth at h + omega + +/-! ## The theorems -/ + +/-- The two subtries are observationally the same: the same value at the position, and the +same `Entry` at every path strictly below. (Whether the position itself exists is its +parent's business, so it is not part of the hash; see the module docs.) -/ +def LogicalEq {V : Type} (t₁ : PathMap V) (p₁ : Path) (t₂ : PathMap V) (p₂ : Path) : Prop := + t₁.valAt p₁ = t₂.valAt p₂ ∧ ∀ q, q ≠ [] → t₁.entryAt (p₁ ++ q) = t₂.entryAt (p₂ ++ q) + +theorem logicalEq_of_deep {V : Type} {t₁ t₂ : PathMap V} {p₁ p₂ : Path} + (h₁ : depth t₁ < p₁.length) (h₂ : depth t₂ < p₂.length) : LogicalEq t₁ p₁ t₂ p₂ := by + refine ⟨?_, fun q _ => ?_⟩ + · rw [valAt_eq_none_of_absent (absent_of_deep h₁), valAt_eq_none_of_absent (absent_of_deep h₂)] + · rw [(entryAt_eq_absent_iff _ _).mpr (absent_of_deep (by simp; omega)), + (entryAt_eq_absent_iff _ _).mpr (absent_of_deep (by simp; omega))] + +/-- Peeling the value layer: with `H` injective, the two positions agree on having a value, +on the value, and on the hash below. -/ +theorem top_inj {V : Type} {H : List UInt8 → UInt64} (hH : Injective H) {vh : V → UInt64} + (hvh : Injective vh) {o₁ o₂ : Option V} {m₁ m₂ : ByteMask} {c₁ c₂ : List UInt64} + (h : (match o₁ with | some v => H (valMsg (vh v) (H (nodeMsg m₁ c₁))) | none => H (nodeMsg m₁ c₁)) = + (match o₂ with | some v => H (valMsg (vh v) (H (nodeMsg m₂ c₂))) | none => H (nodeMsg m₂ c₂))) : + o₁ = o₂ ∧ H (nodeMsg m₁ c₁) = H (nodeMsg m₂ c₂) := by + cases o₁ with + | none => + cases o₂ with + | none => exact ⟨rfl, h⟩ + | some v₂ => exact absurd (hH h) (nodeMsg_ne_valMsg _ _ _ _) + | some v₁ => + cases o₂ with + | none => exact absurd (hH h).symm (nodeMsg_ne_valMsg _ _ _ _) + | some v₂ => + have hm := hH h + simp only [valMsg, List.cons.injEq, true_and] at hm + obtain ⟨hbelow, hval⟩ := List.append_inj hm (by simp [length_leBytes64]) + exact ⟨by rw [hvh (leBytes64_inj hval)], leBytes64_inj hbelow⟩ + +/-- Peeling the node layer: equal hashes below two positions, and the children's hashes +already known to determine their subtries, give equal subtries strictly below. -/ +theorem below_inj {V : Type} {H : List UInt8 → UInt64} (hH : Injective H) (vh : V → UInt64) + {t₁ t₂ : PathMap V} (pc₁ : PrefixClosed t₁) (pc₂ : PrefixClosed t₂) {f₁ f₂ : Nat} {p₁ p₂ : Path} + (ih : ∀ b, hashWith H vh t₁ f₁ (p₁ ++ [b]) = hashWith H vh t₂ f₂ (p₂ ++ [b]) → + LogicalEq t₁ (p₁ ++ [b]) t₂ (p₂ ++ [b])) + (h : belowOf H vh t₁ f₁ p₁ = belowOf H vh t₂ f₂ p₂) : + ∀ q, q ≠ [] → t₁.entryAt (p₁ ++ q) = t₂.entryAt (p₂ ++ q) := by + obtain ⟨hmask, hflat⟩ := nodeMsg_inj (hH h) + have hm : t₁.childMask p₁ = t₂.childMask p₂ := + childMask_eq_of_contains t₁ t₂ p₁ p₂ (maskBytes_inj_contains _ _ hmask) + have hkids : ∀ b ∈ t₁.childMask p₁, + hashWith H vh t₁ f₁ (p₁ ++ [b]) = hashWith H vh t₂ f₂ (p₂ ++ [b]) := by + have hc := children_inj (by simp [hm]) hflat + rw [← hm] at hc + exact List.map_inj_left.mp hc + intro q hq + cases q with + | nil => exact absurd rfl hq + | cons b q' => + rw [List.append_cons p₁ b q', List.append_cons p₂ b q'] + by_cases hb : b ∈ t₁.childMask p₁ + · have hle := ih b (hkids b hb) + cases q' with + | nil => + simp only [List.append_nil] + apply entry_ext' + · have e₁ := (mem_childMask_iff t₁ p₁ b).mp hb + have e₂ := (mem_childMask_iff t₂ p₂ b).mp (hm ▸ hb) + exact e₁.trans e₂.symm + · exact hle.1 + | cons c q'' => exact hle.2 (c :: q'') (by simp) + · have a₁ : t₁.pathExists (p₁ ++ [b]) = false := by + cases h₁ : t₁.pathExists (p₁ ++ [b]) with + | false => rfl + | true => exact absurd ((mem_childMask_iff t₁ p₁ b).mpr h₁) hb + have a₂ : t₂.pathExists (p₂ ++ [b]) = false := by + cases h₂ : t₂.pathExists (p₂ ++ [b]) with + | false => rfl + | true => exact absurd (hm ▸ (mem_childMask_iff t₂ p₂ b).mpr h₂) hb + rw [(entryAt_eq_absent_iff _ _).mpr (not_present_below pc₁ a₁ q'), + (entryAt_eq_absent_iff _ _).mpr (not_present_below pc₂ a₂ q')] + +/-- **Soundness for an ideal primitive.** With `H` and the value hash injective, equal +hashes at two positions (each computed with adequate fuel) mean the subtries are the same. -/ +theorem hashWith_inj_imp_logicalEq {V : Type} (H : List UInt8 → UInt64) (hH : Injective H) + (vh : V → UInt64) (hvh : Injective vh) (t₁ t₂ : PathMap V) + (pc₁ : PrefixClosed t₁) (pc₂ : PrefixClosed t₂) : + ∀ (f₁ f₂ : Nat) (p₁ p₂ : Path), depth t₁ < p₁.length + f₁ → depth t₂ < p₂.length + f₂ → + hashWith H vh t₁ f₁ p₁ = hashWith H vh t₂ f₂ p₂ → LogicalEq t₁ p₁ t₂ p₂ + | 0, 0, p₁, p₂, d₁, d₂, _ => logicalEq_of_deep (by omega) (by omega) + | 0, f₂ + 1, p₁, p₂, _, _, h => by + exfalso + rw [hashWith_succ] at h + split at h + · exact valMsg_ne_nil _ _ (hH h).symm + · exact nodeMsg_ne_nil _ _ (hH h).symm + | f₁ + 1, 0, p₁, p₂, _, _, h => by + exfalso + rw [hashWith_succ] at h + split at h + · exact valMsg_ne_nil _ _ (hH h) + · exact nodeMsg_ne_nil _ _ (hH h) + | f₁ + 1, f₂ + 1, p₁, p₂, d₁, d₂, h => by + rw [hashWith_succ, hashWith_succ] at h + obtain ⟨hval, hbelow⟩ := top_inj hH hvh h + have ih : ∀ b, hashWith H vh t₁ f₁ (p₁ ++ [b]) = hashWith H vh t₂ f₂ (p₂ ++ [b]) → + LogicalEq t₁ (p₁ ++ [b]) t₂ (p₂ ++ [b]) := fun b hb => + hashWith_inj_imp_logicalEq H hH vh hvh t₁ t₂ pc₁ pc₂ f₁ f₂ (p₁ ++ [b]) (p₂ ++ [b]) + (by rw [List.length_append, List.length_singleton]; omega) + (by rw [List.length_append, List.length_singleton]; omega) hb + exact ⟨hval, below_inj hH vh pc₁ pc₂ ih hbelow⟩ + +/-- **The collision reduction.** No assumption on the primitive: if two subtries that are +not the same hash alike, then either `H` or the value hash has a collision, and the +colliding messages exist. -/ +theorem collision_of_hash_eq {V : Type} (H : List UInt8 → UInt64) (vh : V → UInt64) + (t₁ t₂ : PathMap V) (pc₁ : PrefixClosed t₁) (pc₂ : PrefixClosed t₂) + (f₁ f₂ : Nat) (p₁ p₂ : Path) (d₁ : depth t₁ < p₁.length + f₁) (d₂ : depth t₂ < p₂.length + f₂) + (h : hashWith H vh t₁ f₁ p₁ = hashWith H vh t₂ f₂ p₂) (hne : ¬ LogicalEq t₁ p₁ t₂ p₂) : + (∃ a b, a ≠ b ∧ H a = H b) ∨ (∃ x y, x ≠ y ∧ vh x = vh y) := by + refine Classical.byContradiction fun hno => hne ?_ + refine hashWith_inj_imp_logicalEq H ?_ vh ?_ t₁ t₂ pc₁ pc₂ f₁ f₂ p₁ p₂ d₁ d₂ h + · intro a b hab + exact Classical.byContradiction fun hne' => hno (Or.inl ⟨a, b, hne', hab⟩) + · intro x y hxy + exact Classical.byContradiction fun hne' => hno (Or.inr ⟨x, y, hne', hxy⟩) + +/-! ## The harness's instance -/ + +/-- `hashU64` is `hashWith fnv` over `fnv ∘ leBytes64`, with adequate fuel. -/ +theorem hashU64_eq (t : PathMap UInt64) (p : Path) : + hashU64 t p = hashWith fnv value t (depth t + 1) p := + hashFuel_eq_hashWith value t (depth t + 1) p + +/-- For the harness's `PathMap` maps: two maps whose root hashes agree but which differ +at some path exhibit an `fnv` collision. (A value-hash collision is one too, since values +are hashed as `fnv` of their eight bytes and the bytes determine the value.) -/ +theorem fnv_collision_of_hashU64_eq (t₁ t₂ : PathMap UInt64) + (pc₁ : PrefixClosed t₁) (pc₂ : PrefixClosed t₂) + (r₁ : t₁.pathExists [] = true) (r₂ : t₂.pathExists [] = true) + (h : hashU64 t₁ [] = hashU64 t₂ []) (hne : ∃ q, t₁.entryAt q ≠ t₂.entryAt q) : + ∃ a b, a ≠ b ∧ fnv a = fnv b := by + rw [hashU64_eq, hashU64_eq] at h + have hne' : ¬ LogicalEq t₁ [] t₂ [] := by + rintro ⟨hv, hq⟩ + obtain ⟨q, hq'⟩ := hne + apply hq' + cases q with + | nil => + apply entry_ext' + · exact r₁.trans r₂.symm + · exact hv + | cons b q => simpa using hq (b :: q) (by simp) + rcases collision_of_hash_eq fnv value t₁ t₂ pc₁ pc₂ _ _ [] [] (by simp) (by simp) h hne' with + ⟨a, b, hab, he⟩ | ⟨x, y, hxy, he⟩ + · exact ⟨a, b, hab, he⟩ + · exact ⟨leBytes64 x, leBytes64 y, fun e => hxy (leBytes64_inj e), he⟩ + +end Hash +end PathMapModel diff --git a/lean/PathMapModel/PathMap.lean b/lean/PathMapModel/PathMap.lean index e07c3157..6e8cfcd4 100644 --- a/lean/PathMapModel/PathMap.lean +++ b/lean/PathMapModel/PathMap.lean @@ -207,7 +207,7 @@ def childMask (t : PathMap V) (p : Path) : ByteMask := /-- `Zipper::child_count`. -/ def childCount (t : PathMap V) (p : Path) : Nat := (t.childMask p).length -/-- `ZipperMoving::val_count`: values at and below `p`. -/ +/-- `CatamorphismCached::val_count`: values at and below `p`. -/ def valCount (t : PathMap V) (p : Path) : Nat := (t.vals.filter (fun kv => p ≼ kv.1)).length diff --git a/lean/PathMapModel/Zipper.lean b/lean/PathMapModel/Zipper.lean index d6d2799f..0825404d 100644 --- a/lean/PathMapModel/Zipper.lean +++ b/lean/PathMapModel/Zipper.lean @@ -146,7 +146,7 @@ def atRoot : Bool := z.path.isEmpty /-- `ZipperMoving::reset`. -/ def reset : Zip V := { z with path := [] } -/-- `ZipperMoving::val_count`: values at and below the focus. -/ +/-- Cached catamorphism `val_count`: values at and below the focus. -/ def valCount : Nat := z.trie.valCount z.focus /-! ## `trait ZipperMoving` — descent -/ diff --git a/lean/README.md b/lean/README.md index e6827ae3..c38157b8 100644 --- a/lean/README.md +++ b/lean/README.md @@ -152,6 +152,9 @@ focus, such that ..." — instead of as a node walk. | `PathMapModel/Write.lean` | the write API: `ZipperWriting` in full | | `PathMapModel/Map.lean` | the `PathMap` surface, which is the zipper API applied at the root — plus `PathMap::restrict`, the one genuinely map-level operation | | `PathMapModel/Spec.lean` | §1 proved laws (the cursor algebra); §2 checkable laws (metamorphic properties) | +| `PathMapModel/Hash.lean` | the logical trie hash (`CatamorphismCached::hash` under `Fnv1a64Scheme`), defined over the flat representation so node layout cannot enter | +| `PathMapModel/HashSecurity.lean` | the scheme's collision reduction: two logically different tries that hash alike exhibit a collision of the primitive, for any primitive | +| `PathMapModel/GraftNoop.lean` | `wz.graft(&rz)` is a no-op when the two foci hash alike (or the primitive collided); on the way, what `mk'`, `removeBelow`, `graftBelow`, `setVal`, `removeVal` and `subtrie` do, as observations | | `PathMapModel/Check.lean` | `#guard`s: regression fixtures transcribed from `src/write_zipper.rs`'s own tests, and the §2 laws over a battery of tries | | `PathMapModel/Fuzz.lean` | the wire format, the operation table, and the trace producer (including `--act` mode) | | `Main.lean` | the `pathmap-oracle` binary | @@ -219,6 +222,13 @@ headers or invalid selector counts. Headerless inputs use 56 selectors. Each input's selector count controls decoding independently of the running software's operation table. +Operation 60 is `set_val_at`, with a path and a value byte, as established on master. +Operations 61–64 are `hash`, `hash_iter`, `map_hash`, and `merkleize`. +`hash` consumes a target byte; the other three have no operands. Both native +and ACT read sources support the recursive and iterative hash probes. +New inputs use 65 selectors. Operation decoding is simply the input byte modulo +the recorded selector count, with no format-specific opcode remapping. + Generators always write the current header, using `NOPS` in `lean/input_format.py`, which must match `differential/src/harness.rs`. Saved failures and shrunken inputs also carry a header, preserving the original @@ -625,6 +635,61 @@ cargo run -p differential --bin sharing_check Both hold wherever they run (266/266 and 214/214, 1464 node references reused) and both abort on tries containing a *shared dangling path* — see finding 16. +## The logical hash + +`CatamorphismCached::hash` (and the subtrie identity behind `PathMap::merkleize`) +is a Merkle tree over the **logical** trie: every byte of a path is a node with +one child, a branch point hashes its child mask and its children in ascending +byte order, and a value is layered on the hash of the subtrie below it. Nothing +about the physical nodes enters, so two layouts of the same paths and values +must hash alike. + +`PathMapModel/Hash.lean` is that definition over the model's `PathMap`, which +has no node layout to leak. The production primitive is gxhash, which is not +worth transcribing; instead the crate exposes the primitive as a +`pathmap::morphisms::trie_hash::HashScheme`, and the harness runs the crate with +`Fnv1a64Scheme`, whose primitives are the few lines of FNV-1a the Lean file +mirrors. `merkleize` itself is fixed to gxhash (a caller-supplied scheme with +collisions would merge subtries that differ), so its op reports whether the hash +it returns equals the map's hash under that scheme, which the model says is +always `1`. + +`HashSecurity.lean` proves the scheme sound relative to its primitive: for any +`H`, if two positions with different subtries (`LogicalEq` fails) have equal +hashes, then two different messages collided under `H` (or two different values +under the value hash), and the proof constructs them +(`collision_of_hash_eq`; `hashWith_inj_imp_logicalEq` is the ideal-primitive +form, `fnv_collision_of_hashU64_eq` the instance for the harness's maps). The +reduction rests on the messages being decodable, which is why both schemes tag +node and value messages (`N`, `V`), absorb the mask as a fixed 32-byte bitmap +whose popcount fixes how many fixed-width digests follow, and hash the empty +message when fuel runs out. It says nothing about the primitive: gxhash makes +no collision-resistance claim, so this is a statement about the scheme, and the +primitive is treated as replaceable. + +`GraftNoop.lean` takes this to the operation merkleize actually performs. +`graft_noop_of_hash_eq`: if `hash(wz) = hash(rz)` under an ideal primitive, then +`wz.graft(&rz)` leaves every `Entry` of the write map as it was; +`collision_of_graft_ne` is the unconditional contrapositive, and +`graft_noop_of_hashU64_eq` the instance for the harness's maps. Most of that +file characterizes the model's normalizer `mk'` (`valAt_mk'`, `pathExists_mk'`) +and, from it, the write operations as observations rather than list +manipulations; those lemmas need no hashing and are there for `Spec.lean` to +use. The hypotheses are the canonical-form invariants: the write map is +prefix-closed with an existing root, the source is prefix-closed. + +Four ops exercise it: `hash` (target zipper's focus, recursive engine), +`hash_iter` (read zipper, zipper-driven engine, also on an ACT source), +`map_hash` (the write map from its root), and `merkleize` (the write map; the +model treats it as the identity, so the fingerprints and the final dump hold it +to changing nothing). `Check.lean` pins the definition with `#guard`s, and +`mutants.toml` carries mutants for the algebra and the engine's value and run +handling. + +The first run found a memory-safety bug (FINDINGS.md #17): the recursive cached +cata read the refcount of the empty-node sentinel when a two-slot node had a +dangling child, a segfault in release. + ## Structural invariants `pathmap_trace --check ` (and `act_trace --check`) asserts structural @@ -643,9 +708,10 @@ invariant should show up as a trace diff rather than as an abort. ## Out of scope `ZipperHead` and the concurrency story, `ProductZipper` / `PrefixZipper` / -`OverlayZipper` / the ACT format, serialisation, `merkleize`, catamorphisms, and +`OverlayZipper` / the ACT format, serialisation, catamorphisms beyond logical hashing, and allocator behaviour. The model covers `PathMap`, `ReadZipper` and `WriteZipper` -over an in-memory trie. +over an in-memory trie. Merkleization is checked for preservation of logical +contents and hash agreement; physical sharing is checked by Rust tests. That said, the containment invariant `--check` asserts (`root_prefix_path()` never changes) is exactly the property `ZipperHead` relies diff --git a/lean/input_format.py b/lean/input_format.py index 3333335f..fcf9aecd 100644 --- a/lean/input_format.py +++ b/lean/input_format.py @@ -3,7 +3,7 @@ WIRE_MAGIC = b"PMFUZZ\x01\x00" LEGACY_NOPS = 56 # Keep in sync with NOPS in differential/src/harness.rs. -NOPS = 61 +NOPS = 65 HEADER_SIZE = len(WIRE_MAGIC) + 2 diff --git a/lean/mutants.toml b/lean/mutants.toml index cd129560..0a1c3953 100644 --- a/lean/mutants.toml +++ b/lean/mutants.toml @@ -203,3 +203,44 @@ find = """ Self::Identity => match b { replace = """ Self::Identity => match b { Self::Element => Self::Identity, Self::Identity => Self::Identity,""" + +# --- The logical trie hash (Hash.lean; ops 61..64) -------------------------- +# The model computes the hash over the logical trie, so these mutate the algebra and the +# recursive engine's handling of values and runs, not the FNV primitive. + +[[mutant]] +name = "hash_leaf_value_not_layered" +file = "src/morphisms.rs" +find = """ Some(v) => scheme.with_value(scheme.value(v), below),""" +replace = """ Some(_v) if bm.count_bits() == 0 => below, + Some(v) => scheme.with_value(scheme.value(v), below),""" + +[[mutant]] +name = "summarize_run_drops_run_bytes" +file = "src/morphisms.rs" +find = """ finalize_f(&mask, val, Some(acc), prefix)""" +replace = """ let _ = prefix; finalize_f(&mask, val, Some(acc), &[])""" + +[[mutant]] +name = "recursive_cata_ignores_parent_slot_value" +file = "src/trie_node.rs" +find = """ recursive_cata_dispatch::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(node, passed_in_val, start_f, fold_child_f, finalize_f, cache)""" +replace = """ recursive_cata_dispatch::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(node, None, start_f, fold_child_f, finalize_f, cache)""" + +[[mutant]] +name = "listnode_case7_drops_deeper_value" +file = "src/line_list_node.rs" +find = """ let child_w = summarize!(Some(val), None, &key1[2..])?;""" +replace = """ let _ = val; let child_w = summarize!(None, None, &key1[2..])?;""" + +[[mutant]] +name = "merkleize_hash_drops_root_value" +file = "src/merkleization.rs" +find = """ result.hash = recursive_cata_cached::<_, _, _, _, Infallible, _, _, _, true>(root, root_val, start_f, fold_child_f, finalize_f, &mut hashes)""" +replace = """ result.hash = recursive_cata_cached::<_, _, _, _, Infallible, _, _, _, true>(root, None, start_f, fold_child_f, finalize_f, &mut hashes)""" + +[[mutant]] +name = "cata_skips_sentinel_refcount" +file = "src/trie_node.rs" +find = """ !node.is_empty() && node.refcount() > 1""" +replace = """ node.refcount() > 1""" diff --git a/lean/test_input_format.py b/lean/test_input_format.py index 300f55ab..73dc2e75 100644 --- a/lean/test_input_format.py +++ b/lean/test_input_format.py @@ -27,7 +27,7 @@ def test_legacy_inputs_get_applicable_count(self): self.assertEqual(versioned_input(payload), input_header(56) + payload) def test_versioned_inputs_keep_recorded_count(self): - for count in [1, 56, 57, 58, 256]: + for count in [1, 56, 57, 58, 61, 64, 65, 256]: blob = input_header(count) + bytes(range(256)) self.assertEqual(versioned_input(blob), blob) diff --git a/pathmap-derive/src/lib.rs b/pathmap-derive/src/lib.rs index 7338dbb2..835f9392 100644 --- a/pathmap-derive/src/lib.rs +++ b/pathmap-derive/src/lib.rs @@ -519,12 +519,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),)* @@ -682,6 +676,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 c5e0970e..5e510e0d 100644 --- a/src/arena_compact.rs +++ b/src/arena_compact.rs @@ -85,7 +85,7 @@ use crate::alloc::{GlobalAlloc, global_alloc}; use crate::timed_span::{TimingEntries::*, COUNTERS, timed_span}; use crate::{ PathMap, - morphisms::Catamorphism, + morphisms::CatamorphismSideEffecting, utils::{BitMask, ByteMask, find_prefix_overlap}, zipper::{ Zipper, ZipperValues, ZipperValuesAt, ZipperForking, ZipperAbsolutePath, ZipperIteration, @@ -897,7 +897,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) @@ -1067,7 +1067,7 @@ impl ArenaCompactTree { ) -> Result where V: Clone + Send + Sync + Unpin, - Z: Catamorphism, + Z: CatamorphismSideEffecting, F: Fn(&V) -> u64, P: AsRef { @@ -1147,7 +1147,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(); @@ -1355,10 +1355,9 @@ 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 -/// [`Catamorphism::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, @@ -1369,7 +1368,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. /// @@ -1628,7 +1627,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, { @@ -2488,6 +2487,104 @@ where Storage: AsRef<[u8]> } } +impl> crate::morphisms::CatamorphismCached for ACTZipper<'_, Storage, u64> { + 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<&u64>, Option, &[u8]) -> Result, + { + if self.invalid > 0 { + return summarize_f(&ByteMask::EMPTY, None, None, &[]); + } + let frame = self.stack.last().unwrap(); + recursive_act_cata::<_, _, _, _, _, _, _, _, _, COMPUTE_PATH>( + self.tree, &self.cur_node, frame.node_depth, + new_acc_f, fold_child_f, summarize_f, |value| value, + ) + } +} + +impl> crate::morphisms::CatamorphismCached<()> for ACTZipper<'_, Storage, ()> { + 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<&()>, Option, &[u8]) -> Result, + { + if self.invalid > 0 { + return summarize_f(&ByteMask::EMPTY, None, None, &[]); + } + let frame = self.stack.last().unwrap(); + recursive_act_cata::<_, _, _, _, _, _, _, _, _, COMPUTE_PATH>( + self.tree, &self.cur_node, frame.node_depth, + new_acc_f, fold_child_f, summarize_f, |_| (), + ) + } +} + +// Match iterative traversal's sharing policy: ACT's shared_node_id() always +// returns None, so neither implementation memoizes results. When ACT gains +// sharing metadata, both traversal strategies should use it. +fn recursive_act_cata( + tree: &ArenaCompactTree, node: &Node, node_depth: usize, + new_acc_f: NewAccF, fold_child_f: FoldChildF, summarize_f: SummarizeF, value_f: ValueF, +) -> Result +where + Storage: AsRef<[u8]>, + NewAccF: Copy + Fn(&ByteMask) -> Result, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, + SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, + ValueF: Copy + Fn(u64) -> V, +{ + let result = match node { + Node::Branch(branch) => { + let value = branch.value.map(value_f); + let mut accumulator = None; + if let Some(mut child_id) = branch.first_child { + let mut acc = new_acc_f(&branch.bytemask)?; + for _ in branch.bytemask.iter() { + let (child, next_id) = tree.get_node(child_id); + let child_result = recursive_act_cata::<_, _, _, _, _, _, _, _, _, COMPUTE_PATH>( + tree, &child, 0, + new_acc_f, fold_child_f, summarize_f, value_f, + )?; + fold_child_f(&branch.bytemask, child_result, &mut acc)?; + child_id = next_id; + } + accumulator = Some(acc); + } + summarize_f(&branch.bytemask, value.as_ref(), accumulator, &[])? + } + Node::Line(line) => { + let prefix = &tree.get_line(line.path)[node_depth..]; + if let Some(child_id) = line.child { + let (child, _) = tree.get_node(child_id); + let child_result = recursive_act_cata::<_, _, _, _, _, _, _, _, _, COMPUTE_PATH>( + tree, &child, 0, + new_acc_f, fold_child_f, summarize_f, value_f, + )?; + crate::morphisms::summarize_run::<_, _, _, _, _, _, _, COMPUTE_PATH>( + None, Some(child_result), prefix, new_acc_f, fold_child_f, summarize_f, + )? + } else { + let value = line.value.map(value_f); + summarize_f( + &ByteMask::EMPTY, value.as_ref(), None, + if COMPUTE_PATH { prefix } else { &[] }, + )? + } + } + }; + Ok(result) +} + impl<'tree, Storage, Value> Zipper for ACTZipper<'tree, Storage, Value> where Storage: AsRef<[u8]> { @@ -2586,6 +2683,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); @@ -2993,11 +3094,12 @@ impl<'tree, Storage, Value> ZipperConcrete for ACTZipper<'tree, Storage, Value> where Storage: AsRef<[u8]> { fn shared_node_id(&self) -> Option { - // TODO: no way to detect now + // TODO: expose ACT sharing metadata. Both cached catamorphism strategies + // currently revisit shared subtries because this information is absent. None } fn is_shared(&self) -> bool { - // TODO: no way to detect now + // TODO: expose ACT sharing metadata. false } } @@ -3030,23 +3132,6 @@ where Storage: AsRef<[u8]> self.invalid = self.origin_invalid; } - /// 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 @@ -3405,7 +3490,8 @@ where mod tests { use super::{ArenaCompactTree, ACTZipper}; use crate::{ - morphisms::Catamorphism, PathMap, zipper::{zipper_iteration_tests, zipper_moving_tests, ZipperIteration, ZipperMoving, ZipperPath, ZipperValues, ZipperValuesAt} + morphisms::CatamorphismSideEffecting, + PathMap, zipper::{zipper_iteration_tests, zipper_moving_tests, ZipperIteration, ZipperMoving, ZipperPath, ZipperValues, ZipperValuesAt} }; zipper_moving_tests::zipper_moving_tests!(arena_compact_zipper, @@ -3478,6 +3564,26 @@ 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(), + CatamorphismCached + ); + + 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(), + CatamorphismCachedIterative + ); + /// 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 @@ -4279,7 +4385,7 @@ mod tests { assert!(fork.at_root(), "{root:?}"); assert!(!fork.path_exists(), "{root:?} after reset"); assert_eq!(fork.val(), None, "{root:?} after reset"); - assert_eq!(fork.val_count(), 0, "{root:?} after reset"); + assert_eq!(crate::morphisms::CatamorphismCachedIterative::val_count(&fork), 0, "{root:?} after reset"); assert_eq!(fork.child_count(), 0, "{root:?} after reset"); assert!(!fork.to_next_val(), "{root:?} after reset"); } diff --git a/src/dense_byte_node.rs b/src/dense_byte_node.rs index 4c4246f4..e1ca9f2c 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; @@ -11,6 +10,7 @@ use crate::utils::ByteMask; use crate::utils::BitMask; use crate::trie_node::*; 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]; @@ -390,6 +390,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; @@ -406,6 +410,46 @@ 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 impl crate::trie_node::CataCache) -> 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 = &self.mask; + let mut ws = start_f(mask)?; + for cf in self.values.iter() { + 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 + // `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_cached::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(rec, Some(val), start_f, fold_child_f, finalize_f, cache)?; + 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)?, &mut ws)?; + }, + (None, Some(val)) => { + 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)?, &mut ws)?; + }, + } + } + finalize_f(mask, passed_in_val, Some(ws), &[]) + } } impl> ByteNode where Self: TrieNodeDowncast { @@ -1081,34 +1125,13 @@ impl> TrieNode let k = k as usize; (next_token, &ALL_BYTES[k..=k], cf.rec(), cf.val()) } - fn node_val_count(&self, cache: &mut 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 - }); + #[inline] + fn node_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/dependent_zipper.rs b/src/dependent_zipper.rs index 1e5cb6c6..100504e7 100644 --- a/src/dependent_zipper.rs +++ b/src/dependent_zipper.rs @@ -220,6 +220,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) } @@ -357,9 +358,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 c59b711f..68fdbff8 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::*; @@ -75,10 +74,7 @@ impl TrieNode for EmptyNode { fn next_items(&self, _token: IterToken, _after_focus: bool) -> (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 6bd351ee..01078ab6 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()); } @@ -125,6 +124,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 a6eb71c7..448ff40e 100644 --- a/src/experimental.rs +++ b/src/experimental.rs @@ -29,6 +29,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) } @@ -47,7 +51,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()); } @@ -120,7 +123,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/experimental/serialization.rs b/src/experimental/serialization.rs index eb692147..2a67b5ce 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/line_list_node.rs b/src/line_list_node.rs index aa85da28..2be0eeb0 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 std::collections::HashMap; use fast_slice_utils::{find_prefix_overlap, starts_with}; use local_or_heap::LocalOrHeap; @@ -11,6 +10,7 @@ use crate::trie_node::*; 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)] @@ -417,7 +417,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 }, @@ -433,7 +433,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 }, @@ -2227,26 +2227,7 @@ impl TrieNode for LineListNode (NODE_ITER_FINISHED, &[], None, None) } #[inline] - fn node_val_count(&self, cache: &mut 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 { + 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. @@ -3082,6 +3063,229 @@ 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 impl crate::trie_node::CataCache) -> 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, + { + macro_rules! summarize { + ($val:expr, $downstream:expr, $prefix:expr) => { + summarize_run::<_, _, _, _, _, _, _, COMPUTE_PATH>($val, $downstream, $prefix, start_f, fold_child_f, finalize_f) + }; + } + //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. + // 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 + // 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::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>(child_node, None, start_f, fold_child_f, finalize_f, cache)?; + let path = unsafe{ self.key_unchecked::<0>() }; + 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 => { + let child_node = unsafe{ self.child_in_slot::<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>() }; + 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>(child_node, None, start_f, fold_child_f, finalize_f, cache)?; + let path = &key0[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::<1>() }; + let path = &key1[1..]; + fold_child_f(&mask, summarize!(Some(val), None, path)?, &mut acc)?; + + finalize_f(&mask, passed_in_val, Some(acc), &[]) + } + }, + //Case 5 (Child, Child) = (1 << 3) + (1 << 2) + (1 << 1) + 1 + 15 => { + 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)) }; + let path0 = &key0[1..]; + let path1 = &key1[1..]; + 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>(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>(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), &[]) + }, + //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>() }; + 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 => { + 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 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 = finalize_f(&mask, Some(val), Some(acc), &[])?; + 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 = 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)?; + + let val = unsafe{ self.val_in_slot::<1>() }; + fold_child_f(&mask, summarize!(Some(val), None, path1)?, &mut 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 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), 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); + let val = unsafe { self.val_in_slot::<0>() }; + 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)?; + let child_w = summarize!(None, Some(child_w), &key1[2..])?; + let mask = ByteMask::from(key1[1]); + let mut acc = start_f(&mask)?; + fold_child_f(&mask, child_w, &mut acc)?; + finalize_f(&mask, Some(val), Some(acc), &[])? + }; + summarize!(passed_in_val, Some(child_w), key0) + } else { + //Case 10 (Val, Child), different key bytes + let mask = ByteMask::from((key0_byte, key1_byte)); + let mut acc = start_f(&mask)?; + + 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), &[]) + } + }, + _ => { unsafe { unreachable_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() }) + } } impl TrieNodeDowncast for LineListNode { diff --git a/src/merkleization.rs b/src/merkleization.rs index 6840abb3..fff45c65 100644 --- a/src/merkleization.rs +++ b/src/merkleization.rs @@ -22,14 +22,19 @@ // we might want to explore something like a "weak" pointer to keep in the memo (semantics wouldn't be exactly // the same as an Rc `weak`, but a similar idea) +use core::convert::Infallible; +use std::collections::hash_map::Entry; + use crate::alloc::Allocator; -use crate::trie_node::*; use crate::gxhash; +use crate::morphisms::trie_hash::HashScheme; +use crate::trie_node::*; +use crate::utils::ByteMask; /// Statistics created after merkleization #[derive(Default, Debug)] pub struct MerkleizeResult { - /// The hash of the entire trie beneath the root + /// The hash of the entire trie beneath the root, equal to [`CatamorphismCached::hash`](crate::morphisms::CatamorphismCached::hash) pub hash: u128, /// The number of shared node references that replaced identical copies during the merkleization pub reused: usize, @@ -39,34 +44,182 @@ pub struct MerkleizeResult { pub replaced: usize, } -pub(crate) fn merkleize_impl( +/// Merkleizes the trie below `root`, returning the statistics and the replacement for the root node +/// if it was rewritten +/// +/// Nodes are identified by the hash of the **logical** trie below them, computed by the cached +/// catamorphism with the same algebra as [`CatamorphismCached::hash`](crate::morphisms::CatamorphismCached::hash). +/// Two nodes therefore merge whenever they hold the same paths and values, regardless of how those +/// paths are laid out in nodes and key segments, and regardless of the value the parent stores for +/// the node's root position. +/// +/// A subtrie's root value is conceptually part of the subtrie. Ignoring the parent-slot value +/// when sharing physical nodes reflects its current storage location, not the subtrie's meaning. +/// FUTURE: When nodes own their root values, the memo should use the complete node hash, and the +/// separate value-free hash and parent-slot value restoration can be removed. The logical hash +/// of a subtrie, including `MerkleizeResult::hash`, already includes the value at its root. +/// +/// The cata records the value-free hash of every node, in the order it finishes them. A second, +/// bottom-up pass over the physical nodes then replaces every child whose hash was seen before with +/// the first node that produced that hash. +/// +/// `scheme` is the [`HashScheme`] the hashes are computed with. Only +/// [`GxHashScheme`](crate::morphisms::trie_hash::GxHashScheme) is used outside of tests: merkleize +/// merges subtries whose hashes are equal, so the scheme must be one whose collisions are negligible +pub(crate) fn merkleize_root(root: &TrieNodeODRc, root_val: Option<&V>, scheme: &S) -> (MerkleizeResult, Option>) + where + V: Clone + Send + Sync, + A: Allocator, + S: HashScheme, +{ + let mut result = MerkleizeResult::default(); + + let mut hashes = NodeHashes { + order: Vec::new(), + shared: gxhash::HashMap::default(), + cursor: 0, + by_address: None, + collapse_value: |v: &V, below| scheme.with_value(scheme.value(v), below), + }; + let start_f = |bm: &ByteMask| Ok::<_, Infallible>(scheme.start(bm)); + let fold_child_f = |_bm: &ByteMask, child: NodeHash, acc: &mut S::Acc| { + scheme.child(acc, child.full); + Ok(()) + }; + let finalize_f = |bm: &ByteMask, value: Option<&V>, acc: Option, prefix: &[u8]| { + let below = scheme.finish(acc.unwrap_or_else(|| scheme.start(bm))); + let mut full = match value { + Some(v) => scheme.with_value(scheme.value(v), below), + None => below, + }; + for byte in prefix.iter().rev() { + full = scheme.step(*byte, full); + } + // A nonempty prefix starts above `value`, at an unvalued position. The two hashes + // differ only when this callback summarizes a value at the start position itself. + Ok(NodeHash { full, without_value: if prefix.is_empty() { below } else { full } }) + }; + result.hash = recursive_cata_cached::<_, _, _, _, Infallible, _, _, _, true>(root, root_val, start_f, fold_child_f, finalize_f, &mut hashes) + .unwrap_or_else(|never| match never {}).full; + + let mut memo = gxhash::HashMap::>::default(); + let mut visited = gxhash::HashMap::>>::default(); + let new_root = merkleize_impl(&mut result, &mut hashes, &mut memo, &mut visited, root); + (result, new_root) +} + +/// Both hashes describe the same starting position. `full` includes its value, while +/// `without_value` excludes only that position's value, retaining every descendant value. +/// The second hash is needed only for sharing physical nodes under the current parent-owned +/// root-value contract. It can be removed when each node owns its root value. +#[derive(Clone, Copy)] +struct NodeHash { + full: u128, + without_value: u128, +} + +/// The cata cache used for merkleization: every node's hash is recorded, in the order the cata +/// finishes nodes, and nodes that are physically shared are additionally indexed by address so +/// the cata summarizes them once +struct NodeHashes { + collapse_value: F, + /// `(address, hash)` for every node the cata finished, in post-order + order: Vec<(u64, u128)>, + /// Hashes of nodes with more than one parent, so the cata can reuse them + shared: gxhash::HashMap, + /// Read position in `order` for the rewrite pass + cursor: usize, + /// Built from `order` if the rewrite pass ever visits nodes in a different order than the cata did + by_address: Option>, +} + +impl u128> CataCache for NodeHashes { + #[inline] + fn caches(&self, node: &TrieNodeODRc) -> bool { + !node.is_empty() + } + #[inline] + fn get(&self, key: u64) -> Option<&NodeHash> { + self.shared.get(&key) + } + #[inline] + fn get_with_value(&self, key: u64, value: &V) -> Option { + let without_value = self.shared.get(&key)?.without_value; + Some(NodeHash { full: (self.collapse_value)(value, without_value), without_value }) + } + #[inline] + fn insert(&mut self, key: u64, node: &TrieNodeODRc, _value: Option<&V>, w: &NodeHash) { + self.order.push((key, w.without_value)); + if node.refcount() > 1 { + // Every parent has its own slot value, so reuse only the value-free summary. + // FUTURE: With node-owned root values, cache the full hash instead. + self.shared.insert(key, NodeHash { full: w.without_value, without_value: w.without_value }); + } + } +} + +impl NodeHashes { + /// Whether the node at `address` had more than one parent when the cata ran + #[inline] + fn is_shared(&self, address: u64) -> bool { + !self.shared.is_empty() && self.shared.contains_key(&address) + } + /// The hash recorded for the node at `address`, which the rewrite pass reaches in the cata's order + #[inline] + fn take(&mut self, address: u64) -> Option { + if let Some(by_address) = &self.by_address { + return by_address.get(&address).copied(); + } + if let Some(&(recorded, hash)) = self.order.get(self.cursor) { + if recorded == address { + self.cursor += 1; + return Some(hash); + } + } + // The traversal orders diverged; the remaining lookups go through an index + let by_address = self.order.iter().copied().collect::>(); + let hash = by_address.get(&address).copied(); + self.by_address = Some(by_address); + hash + } +} + +/// Rewrites the trie below `node`, returning the node the parent must store in its slot instead of +/// `node`, if any +fn merkleize_impl( counters: &mut MerkleizeResult, + hashes: &mut NodeHashes, memo: &mut gxhash::HashMap>, + visited: &mut gxhash::HashMap>>, node: &TrieNodeODRc, - value: Option<&V>, -) -> (u128, Option>) +) -> Option> where - V: Clone + Send + Sync + std::hash::Hash, + V: Clone + Send + Sync, A: Allocator, { - // hash = (value, [(path, child_hash)]) - use std::hash::{Hash}; - use std::collections::hash_map::Entry; - const INITIAL_SEED: i64 = 0; - let mut hasher = gxhash::GxHasher::with_seed(INITIAL_SEED); - value.hash(&mut hasher); - let mut replacement = None; + // The cata does not visit the empty node + if node.is_empty() { + return None; + } + let address = node.shared_node_id(); + // A node with more than one parent is rewritten once, and every parent gets the same answer. Only + // such nodes can be reached twice, so only they are tracked. The cata recorded which nodes those + // are; refcounts are no guide here because rewriting a parent shares its children with the copy + let shared = hashes.is_shared(address); + if shared { + if let Some(previous) = visited.get(&address) { + return previous.clone(); + } + } + let mut replacement = None; let node_ref = node.as_tagged(); let mut it = node_ref.new_iter_token(); while it != NODE_ITER_FINISHED { - let (next, path, child, val) = node_ref.next_items(it, false); + let (next, path, child, _val) = node_ref.next_items(it, false); it = next; - path.hash(&mut hasher); - let (child_hash, replace); if let Some(child) = child { - (child_hash, replace) = merkleize_impl(counters, memo, child, val); - if let Some(replace) = replace { + if let Some(replace) = merkleize_impl(counters, hashes, memo, visited, child) { let node = replacement.get_or_insert_with(|| { counters.cloned += 1; node.clone() @@ -74,36 +227,431 @@ pub(crate) fn merkleize_impl( counters.replaced += 1; node.make_mut().node_replace_child(path, replace); } - } else { - // value and no child -> pretend there's an empty node - let mut hasher = gxhash::GxHasher::with_seed(INITIAL_SEED); - val.hash(&mut hasher); - child_hash = hasher.finish_u128(); } - child_hash.hash(&mut hasher); } - let hash = hasher.finish_u128(); + + let hash = hashes.take(address).expect("the cata visits every non-empty node"); match memo.entry(hash) { Entry::Vacant(entry) => { - counters.cloned += 1; - if let Some(replacement) = &replacement { - entry.insert(replacement.clone()); - } else { - entry.insert(node.clone()); - } - } - // if we've seen the hash before, do the replacement + entry.insert(replacement.clone().unwrap_or_else(|| node.clone())); + }, + // Another node with the same logical contents was seen first: point the parent at it Entry::Occupied(entry) => { counters.reused += 1; replacement = Some(entry.get().clone()); - } + }, } - (hash, replacement) + if shared { + visited.insert(address, replacement.clone()); + } + replacement } #[cfg(test)] mod tests { + /// Regression test: the memo key must not include the value stored in the *parent's* slot for + /// a child, otherwise byte-identical nodes reached through a valued slot and an unvalued slot are + /// never merged. + /// + /// The trie contains every bitstring over {0, 1} of length 0..=4 that ends in `1`, plus the + /// empty path. The subtrie below byte `0` and the subtrie below byte `1` are identical, except + /// that the path `[1]` carries a value, which lives in the root node's slot, not in the child. + /// So after merkleization there should be exactly one physical node per depth: 4 in total. + #[cfg(feature="viz")] + #[test] + fn merkleize_parent_slot_value_should_not_split_identical_children() { + use crate::viz::{viz_maps, DrawConfig, VizMode}; + + // All paths over {0,1} of length 1..=4 whose last byte is 1, plus the empty path + let mut paths: Vec> = vec![vec![]]; + for len in 1..=4usize { + for bits in 0..(1u32 << len) { + let path: Vec = (0..len).map(|i| ((bits >> (len - 1 - i)) & 1) as u8).collect(); + if *path.last().unwrap() == 1 { + paths.push(path); + } + } + } + assert_eq!(paths.len(), 16); + let mut map = crate::PathMap::from_iter(paths.iter().map(|p| (p.as_slice(), ()))); + assert_eq!(map.val_count(), 16); + + let count_physical_nodes = |map: &crate::PathMap<()>| { + let dc = DrawConfig{ mode: VizMode::Mermaid, ascii_path: false, hide_value_paths: true, minimize_values: true, logical: false, color: false }; + let mut out = Vec::new(); + viz_maps(std::slice::from_ref(map), &dc, &mut out).unwrap(); + let out = String::from_utf8(out).unwrap(); + // `viz_node_physical` emits one `shape: rect` line per distinct node address + let n = out.lines().filter(|l| l.contains("shape: rect")).count(); + (n, out) + }; + + let (before, _) = count_physical_nodes(&map); + let result = map.merkleize(); + let (after, rendered) = count_physical_nodes(&map); + eprintln!("physical nodes before merkleize: {before}, after: {after}\nmerkleize result: {result:?}\n```mermaid\n{rendered}```"); + assert_eq!(map.val_count(), 16, "merkleize must not change the logical contents"); + assert_eq!(after, 4, "expected one physical node per depth after merkleize, got {after}"); + } + + use std::collections::HashSet; + use crate::PathMap; + use crate::morphisms::CatamorphismCached; + use crate::trie_node::{TrieNodeODRc, NODE_ITER_FINISHED}; + use crate::zipper::*; + use crate::gxhash; + + // ---- Sharing tests ported from the merkleize-fix branch ---- + + /// Walks every node reachable from `map`'s root and groups them by an + /// independently-computed *structural* key: the sorted list of + /// `(path, edge_value_hash, child_structural_key)` triples a node carries, + /// computed bottom-up without any reference to `merkleize_impl`'s own + /// hashing. Returns, per structural key, every distinct node identity + /// (`shared_node_id`) found with that key -- callers decide what to + /// assert, since "before merkleize" legitimately has multiple identities + /// per class (that's the diversity merkleize exists to remove). + fn structural_classes(map: &PathMap) -> std::collections::HashMap> + where V: Clone + Send + Sync + Unpin + std::hash::Hash + { + use std::hash::Hash; + use std::collections::HashMap; + + fn structural_key( + node: &TrieNodeODRc, + seen: &mut HashMap, + classes: &mut HashMap>, + ) -> u128 + where V: Clone + Send + Sync + std::hash::Hash + { + let id = node.shared_node_id(); + if let Some(key) = seen.get(&id) { + return *key; + } + let mut parts: Vec<(Vec, u128)> = Vec::new(); + let node_ref = node.as_tagged(); + let mut it = node_ref.new_iter_token(); + while it != NODE_ITER_FINISHED { + let (next, path, child, val) = node_ref.next_items(it, false); + it = next; + let edge_key = if let Some(child) = child { + let child_key = structural_key(child, seen, classes); + let mut hasher = gxhash::GxHasher::with_seed(0); + val.hash(&mut hasher); + child_key.hash(&mut hasher); + hasher.finish_u128() + } else { + let mut hasher = gxhash::GxHasher::with_seed(0); + val.hash(&mut hasher); + hasher.finish_u128() + }; + parts.push((path.to_vec(), edge_key)); + } + parts.sort(); + let mut hasher = gxhash::GxHasher::with_seed(0); + parts.hash(&mut hasher); + let key = hasher.finish_u128(); + seen.insert(id, key); + classes.entry(key).or_default().push(id); + key + } + + let mut seen = HashMap::new(); + let mut classes: HashMap> = HashMap::new(); + if let Some(root) = map.root() { + structural_key(root, &mut seen, &mut classes); + } + classes + } + + /// Asserts that every structural class contains exactly one node + /// identity -- i.e. merkleization achieved *maximal* sharing. Meant to + /// be called only on a trie that has already been merkleized; calling it + /// beforehand would spuriously fail, since un-merkleized tries are + /// expected to hold distinct-but-identical-shaped nodes. + /// + /// Returns the number of distinct structural classes found (a proxy for + /// "how many distinct node shapes remain"). + fn assert_maximal_sharing(map: &PathMap) -> usize + where V: Clone + Send + Sync + Unpin + std::hash::Hash + { + let classes = structural_classes(map); + for (key, ids) in &classes { + let mut dedup_ids = ids.clone(); + dedup_ids.sort(); + dedup_ids.dedup(); + assert_eq!( + dedup_ids.len(), 1, + "under-shared structural class {key:#x}: {} distinct node identities \ + ({} references) should have been merged into one", + dedup_ids.len(), ids.len(), + ); + } + classes.len() + } + + /// Regression test for the parent-edge-value-leaking-into-child-hash bug: + /// every bitstring of length 1..=4 over {0,1} ending in `1`, plus the empty + /// path. Before the fix, `merkleize` folded the value reached via a + /// node's *parent* edge into the hash used to memoize the node itself, so + /// the same child node reached through a valued slot and an unvalued slot + /// were never deduplicated. This trie is built so every level has one + /// child ending the path (valued) and one child continuing it + /// (unvalued), reaching an *otherwise identical* subtrie -- which + /// collapses to a single chain of 4 shared nodes once merkleization is + /// correct (was 7 with the bug). + #[test] + fn test_merkleize_dedups_value_vs_no_value_edges() { + let mut paths: Vec> = vec![vec![]]; + for len in 1..=4usize { + for bits in 0..(1u32 << len) { + let p: Vec = (0..len) + .map(|i| ((bits >> (len - 1 - i)) & 1) as u8) + .collect(); + if *p.last().unwrap() == 1 { + paths.push(p); + } + } + } + let mut map = PathMap::from_iter(paths.iter().map(|p| (p.as_slice(), ()))); + let before_ids: usize = structural_classes(&map).values().flatten().collect::>().len(); + + let result = map.merkleize(); + eprintln!("merkleize result: {result:?}"); + + let after_classes = assert_maximal_sharing(&map); + // The whole trie is one repeating shape, so after merkleization there + // should be exactly 4 distinct structural classes: the 4 nesting + // depths (the "()" leaf value counts as its own class, folded into + // the deepest node), each now backed by exactly one node identity. + assert_eq!(after_classes, 4, "expected exactly 4 distinct node shapes after merkleize"); + assert!(after_classes < before_ids, "merkleize should have reduced the number of distinct node identities"); + assert!(result.reused > 0, "merkleize should have found reusable nodes"); + } + + /// Adversarial: two sibling subtries are byte-for-byte identical *except* + /// that one is reached through a valued parent slot and the other + /// through an unvalued (dangling) one. Structurally the two subtries + /// are identical, so they must merge. + #[test] + fn test_merkleize_value_and_dangling_siblings_share() { + let mut map = PathMap::<()>::new(); + // valued edge into a subtrie: [0] itself carries a value + map.insert(&[0u8][..], ()); + map.insert(&[0u8, 1, 0][..], ()); + map.insert(&[0u8, 1, 1][..], ()); + // dangling (no value) edge into the identical subtrie: [1] exists but is unvalued + map.create_path(&[1u8]); + map.insert(&[1u8, 1, 0][..], ()); + map.insert(&[1u8, 1, 1][..], ()); + let result = map.merkleize(); + eprintln!("merkleize result: {result:?}"); + assert_maximal_sharing(&map); + assert!(result.reused > 0); + } + + /// Adversarial: identical subtries reached via *different* values at the + /// parent edge (not just "value" vs "no value"). These must remain + /// distinct (the edge value differs), but the child subtrie beneath each + /// must still be the same shared node. + #[test] + fn test_merkleize_distinguishes_different_edge_values_but_shares_children() { + let mut map = PathMap::::new(); + map.insert(&[0u8][..], 1); + map.insert(&[0u8, 5, 0][..], 9); + map.insert(&[0u8, 5, 1][..], 9); + map.insert(&[1u8][..], 2); // different value at this edge + map.insert(&[1u8, 5, 0][..], 9); + map.insert(&[1u8, 5, 1][..], 9); + let before = crate::merkleization::tests::snapshot(&map); + let result = map.merkleize(); + eprintln!("merkleize result: {result:?}"); + assert_maximal_sharing(&map); + let after = crate::merkleization::tests::snapshot(&map); + assert_eq!(before, after, "merkleize must not change observable content"); + assert!(result.reused > 0); + } + + /// Adversarial: deeply nested, asymmetric repetition -- a chain of + /// dangling paths of increasing depth, where only some branches are + /// dangling and others carry values, all funnelling into the same + /// terminal shape. Exercises multiple levels of the value/no-value + /// distinction stacked on top of each other, rather than just one level. + #[test] + fn test_merkleize_nested_mixed_dangling_and_valued() { + let mut map = PathMap::<()>::new(); + for prefix in [ + &[0u8][..], &[0u8, 0][..], &[1u8][..], &[1u8, 1][..], &[2u8, 0, 0][..], + ] { + // half dangling, half valued at each prefix + map.create_path(prefix); + } + map.insert(&[0u8, 1][..], ()); + map.insert(&[1u8, 0][..], ()); + map.insert(&[2u8, 0, 1][..], ()); + // give every branch the same terminal subtrie shape + for base in [&[0u8, 0][..], &[0u8, 1][..], &[1u8, 0][..], &[1u8, 1][..], &[2u8, 0, 0][..], &[2u8, 0, 1][..]] { + let mut k = base.to_vec(); + k.push(7); + map.insert(&k[..], ()); + k.pop(); + k.push(8); + map.insert(&k[..], ()); + } + let before = crate::merkleization::tests::snapshot(&map); + let result = map.merkleize(); + eprintln!("merkleize result: {result:?}"); + assert_maximal_sharing(&map); + let after = crate::merkleization::tests::snapshot(&map); + assert_eq!(before, after, "merkleize must not change observable content"); + assert!(result.reused > 0); + } + + /// Snapshot the full observable (path, value) contents of a map, so tests + /// can assert `merkleize` never changes what the trie means, only how + /// it's represented. + pub(crate) fn snapshot(map: &PathMap) -> std::collections::BTreeMap, Option> + where V: Send + Sync + Unpin + { + use crate::zipper::*; + let mut rz = map.read_zipper(); + let mut out = std::collections::BTreeMap::new(); + out.insert(Vec::new(), rz.val().cloned()); + while rz.to_next_step() { + out.insert(rz.path().to_vec(), rz.val().cloned()); + } + out + } + + + /// Counts the distinct physical nodes reachable from the map root + fn physical_node_count(map: &PathMap<()>) -> usize { + fn visit(node: &TrieNodeODRc<(), crate::alloc::GlobalAlloc>, seen: &mut HashSet) { + if node.is_empty() || !seen.insert(node.shared_node_id()) { + return; + } + let node_ref = node.as_tagged(); + let mut it = node_ref.new_iter_token(); + while it != NODE_ITER_FINISHED { + let (next, _path, child, _val) = node_ref.next_items(it, false); + it = next; + if let Some(child) = child { + visit(child, seen); + } + } + } + let mut seen = HashSet::new(); + if let Some(root) = map.root() { + visit(root, &mut seen); + } + seen.len() + } + + fn all_paths(map: &PathMap<()>) -> Vec> { + let mut rz = map.read_zipper(); + let mut paths = Vec::new(); + while rz.to_next_val() { + paths.push(rz.path().to_vec()); + } + paths + } + + /// The same four paths, either inserted directly (`abcd`/`abce` become `a` -> `bc` -> `{d, e}`) + /// or assembled by grafting `{cd, ce}` under `ab` (which leaves `a` -> `b` -> `c` -> `{d, e}`) + fn insert_copy(map: &mut PathMap<()>, prefix: &[u8], grafted: bool) { + let mut wz = map.write_zipper_at_path(prefix); + if grafted { + let sub: PathMap<()> = [b"cd".as_slice(), b"ce"].into_iter().map(|leaf| (leaf, ())).collect(); + wz.descend_to(b"ab"); + wz.graft_map(sub); + wz.reset(); + for path in [b"m".as_slice(), b"n"] { + wz.descend_to(path); + wz.set_val(()); + wz.reset(); + } + } else { + for path in [b"abcd".as_slice(), b"abce", b"m", b"n"] { + wz.descend_to(path); + wz.set_val(()); + wz.reset(); + } + } + } + + #[test] + fn merkleize_hash_is_the_cata_hash_and_keeps_the_contents() { + let mut map = PathMap::<()>::new(); + insert_copy(&mut map, b"1", false); + insert_copy(&mut map, b"2", true); + map.set_val_at(b"1", ()); + let paths = all_paths(&map); + let hash = CatamorphismCached::hash(&map); + + let result = map.merkleize(); + + assert_eq!(result.hash, hash); + assert_eq!(all_paths(&map), paths); + assert_eq!(CatamorphismCached::hash(&map), hash); + } + + #[test] + fn merkleize_merges_logically_equal_subtries_with_different_layouts() { + let mut direct = PathMap::<()>::new(); + insert_copy(&mut direct, b"", false); + let mut split = PathMap::<()>::new(); + insert_copy(&mut split, b"", true); + let split = split; + assert_eq!(all_paths(&direct), all_paths(&split)); + let (direct_nodes, grafted_nodes) = (physical_node_count(&direct), physical_node_count(&split)); + assert_ne!(direct_nodes, grafted_nodes, "the two copies should differ in layout for this test to mean anything"); + + let mut map = PathMap::<()>::new(); + insert_copy(&mut map, b"1", false); + insert_copy(&mut map, b"2", true); + let before = physical_node_count(&map); + assert_eq!(before, 1 + direct_nodes + grafted_nodes); + + let result = map.merkleize(); + let after = physical_node_count(&map); + eprintln!("direct copy: {direct_nodes} nodes, grafted copy: {grafted_nodes} nodes, before: {before}, after: {after}, {result:?}"); + // The second copy collapses onto the first, node for node, so only the root is left on top + // of one copy (whose own internal duplicates are merged as well) + direct.merkleize(); + assert_eq!(after, 1 + physical_node_count(&direct)); + assert!(result.reused >= 1); + } + + #[test] + fn merkleize_is_idempotent_and_leaves_shared_nodes_alone() { + let mut map = PathMap::<()>::new(); + for prefix in [b"1".as_slice(), b"2", b"3"] { + insert_copy(&mut map, prefix, prefix == b"2"); + } + let first = map.merkleize(); + assert!(first.reused > 0); + let after_first = physical_node_count(&map); + + let second = map.merkleize(); + assert_eq!(second.hash, first.hash); + assert_eq!((second.reused, second.replaced, second.cloned), (0, 0, 0)); + assert_eq!(physical_node_count(&map), after_first); + + // A trie that already shares its nodes physically is left untouched too + let mut grafted = PathMap::<()>::new(); + let mut shared = PathMap::<()>::new(); + insert_copy(&mut shared, b"", false); + for prefix in [b"1".as_slice(), b"2", b"3"] { + let mut wz = grafted.write_zipper_at_path(prefix); + wz.graft_map(shared.clone()); + } + let before = physical_node_count(&grafted); + let result = grafted.merkleize(); + assert_eq!((result.reused, result.replaced, result.cloned), (0, 0, 0)); + assert_eq!(physical_node_count(&grafted), before); + } + #[test] fn test_btm_merkleize() { let paths: &[&[u8]] = &[ @@ -136,4 +684,195 @@ mod tests { eprintln!("```mermaid\n{}```", std::str::from_utf8(&after).unwrap()); } } + + // ---- Exploration: node topology from IMG_0825 ---- + // + // Requested topology, keys written as byte strings over {0, 1}: + // root [ 0: [00: [0: V, 1: V]], 1: [0: [00: V, 01: V]] ] + // Both halves hold the paths {000, 001} below the root byte, laid out with node boundaries + // in different places. Node 4 in the picture, a ListNode holding two values under keys `00` + // and `01`, is an "unfactored divergent prefix" and is rejected by `validate_node`. + + fn dump_layout(map: &PathMap) -> String { + use crate::trie_node::TaggedNodeRef; + fn kind(t: &TaggedNodeRef<'_, V, A>) -> &'static str { + match t { + TaggedNodeRef::DenseByteNode(_) => "Dense", + TaggedNodeRef::LineListNode(_) => "List", + TaggedNodeRef::CellByteNode(_) => "Cell", + TaggedNodeRef::TinyRefNode(_) => "Tiny", + TaggedNodeRef::EmptyNode => "Empty", + } + } + fn walk(node: &TrieNodeODRc, depth: usize, ids: &mut Vec, out: &mut String) { + let id = node.shared_node_id(); + let idx = match ids.iter().position(|x| *x == id) { Some(i) => i, None => { ids.push(id); ids.len() - 1 } }; + let t = node.as_tagged(); + out.push_str(&format!("{}#{idx} {} rc={}\n", " ".repeat(depth), kind(&t), node.refcount())); + let mut it = t.new_iter_token(); + while it != NODE_ITER_FINISHED { + let (next, path, child, val) = t.next_items(it, false); + it = next; + if val.is_none() && child.is_none() { continue; } + out.push_str(&format!("{} key {:?}{}{}\n", " ".repeat(depth), path, + if val.is_some() { " V" } else { "" }, if child.is_some() { " ->" } else { "" })); + if let Some(child) = child { walk(child, depth + 2, ids, out); } + } + } + let mut out = String::new(); + let mut ids = Vec::new(); + if let Some(root) = map.root() { walk(root, 0, &mut ids, &mut out); } + out.push_str(&format!("distinct nodes: {}\n", ids.len())); + out + } + + fn assert_same_paths_and_report(label: &str, map: &mut PathMap<()>) -> MerkleizeResultView { + let before_paths = all_paths(map); + let before_layout = dump_layout(map); + let before_nodes = physical_node_count(map); + let result = map.merkleize(); + let after_layout = dump_layout(map); + let after_nodes = physical_node_count(map); + eprintln!("==== {label} ====\n-- before ({before_nodes} nodes):\n{before_layout}-- merkleize: {result:?}\n-- after ({after_nodes} nodes):\n{after_layout}"); + assert_eq!(all_paths(map), before_paths, "merkleize must not change contents"); + assert_maximal_sharing(map); + MerkleizeResultView { before_nodes, after_nodes, reused: result.reused } + } + struct MerkleizeResultView { before_nodes: usize, after_nodes: usize, reused: usize } + + /// The topology built by plain insertion, and by grafting so the `1` half gets different + /// node boundaries than the `0` half + #[test] + fn topology_img_0825_legal_layouts() { + // (a) natural layout from direct insertion + let mut natural = PathMap::<()>::new(); + for p in [[0u8, 0, 0, 0], [0, 0, 0, 1], [1, 0, 0, 0], [1, 0, 0, 1]] { natural.insert(&p[..], ()); } + let r = assert_same_paths_and_report("natural insertion", &mut natural); + assert!(r.after_nodes <= r.before_nodes); + + // (b) `0` half by insertion: 0 -> [00 -> {0: V, 1: V}] + // `1` half by grafting: 1 -> [0 -> [0 -> {0: V, 1: V}]] (the legal stand-in for [0 -> [00: V, 01: V]]) + let mut map = PathMap::<()>::new(); + map.insert(&[0u8, 0, 0, 0][..], ()); + map.insert(&[0u8, 0, 0, 1][..], ()); + let leaf: PathMap<()> = [[0u8].as_slice(), &[1u8]].into_iter().map(|k| (k, ())).collect(); + let mut mid = PathMap::<()>::new(); + { let mut wz = mid.write_zipper_at_path(&[0u8]); wz.graft_map(leaf); } + let mut outer = PathMap::<()>::new(); + { let mut wz = outer.write_zipper_at_path(&[0u8]); wz.graft_map(mid); } + { let mut wz = map.write_zipper_at_path(&[1u8]); wz.graft_map(outer); } + let r = assert_same_paths_and_report("grafted: different boundaries under 1", &mut map); + assert!(r.reused >= 1, "the {{000, 001}} subtrie under `1` should collapse onto the one under `0`"); + + // (c) the reverse: `1` half by insertion, `0` half with the boundary directly below the root byte: + // 0 -> [0 -> [00 -> {0: V, 1: V}]] is not constructible either (a node with a single 2-byte + // child key `00` and nothing else gets folded), so use 0 -> [0 -> [0 -> {0, 1}]] vs 1 -> [00 -> {0, 1}] + let mut map = PathMap::<()>::new(); + map.insert(&[1u8, 0, 0, 0][..], ()); + map.insert(&[1u8, 0, 0, 1][..], ()); + let leaf: PathMap<()> = [[0u8].as_slice(), &[1u8]].into_iter().map(|k| (k, ())).collect(); + let mut mid = PathMap::<()>::new(); + { let mut wz = mid.write_zipper_at_path(&[0u8]); wz.graft_map(leaf); } + let mut outer = PathMap::<()>::new(); + { let mut wz = outer.write_zipper_at_path(&[0u8]); wz.graft_map(mid); } + { let mut wz = map.write_zipper_at_path(&[0u8]); wz.graft_map(outer); } + let r = assert_same_paths_and_report("grafted: different boundaries under 0", &mut map); + assert!(r.reused >= 1); + } + + /// Hand-builds the picture's node 4, `[00: V, 01: V]`, which the trie invariant forbids, to see + /// what the validator and merkleize do with it + #[test] + fn topology_img_0825_illegal_node4() { + use crate::alloc::global_alloc; + use crate::line_list_node::LineListNode; + use crate::trie_node::ValOrChild; + + // `0` half: natural {000, 001} layout, taken from a map + let a: PathMap<()> = [[0u8, 0, 0].as_slice(), &[0u8, 0, 1]].into_iter().map(|k| (k, ())).collect(); + let (a_root, _) = a.into_root(); + let a_root = a_root.unwrap(); + + // `1` half: [0 -> [00: V, 01: V]] + let mut node4 = LineListNode::<(), _>::new_in(global_alloc()); + unsafe { + node4.set_payload_owned::<0>(&[0u8, 0], ValOrChild::Val(())); + node4.set_payload_owned::<1>(&[0u8, 1], ValOrChild::Val(())); + } + let node4 = TrieNodeODRc::new_in(node4, global_alloc()); + let mut node3 = LineListNode::<(), _>::new_in(global_alloc()); + unsafe { node3.set_payload_owned::<0>(&[0u8], ValOrChild::Child(node4)); } + let node3 = TrieNodeODRc::new_in(node3, global_alloc()); + + let mut root = LineListNode::<(), _>::new_in(global_alloc()); + unsafe { + root.set_payload_owned::<0>(&[0u8], ValOrChild::Child(a_root)); + root.set_payload_owned::<1>(&[1u8], ValOrChild::Child(node3)); + } + let root = TrieNodeODRc::new_in(root, global_alloc()); + let mut map = PathMap::<()>::new_with_root_in(Some(root), None, global_alloc()); + + eprintln!("-- hand-built layout:\n{}", dump_layout(&map)); + eprintln!("-- paths: {:?}", all_paths(&map)); + let valid = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| crate::trie_node::assert_valid_trie(map.root()))); + eprintln!("-- assert_valid_trie: {}", if valid.is_ok() { "ok" } else { "PANICKED (invariant violated)" }); + + // The cata's ListNode case 7 assumes the invariant (`debug_assert_eq!(key0.len(), 1)`), so + // merkleize on this layout is expected to panic in a debug build + let merk = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let r = assert_same_paths_and_report("illegal node 4", &mut map); + (r.reused, r.before_nodes, r.after_nodes) + })); + match merk { + Ok((reused, before, after)) => eprintln!("-- merkleize survived the illegal node: reused={reused} before={before} after={after}"), + Err(_) => eprintln!("-- merkleize PANICKED on the illegal node (cata assumes the ListNode invariant)"), + } + } +} + +#[cfg(test)] +mod cache_tests { + use super::*; + use crate::{ + PathMap, + morphisms::{CatamorphismCached, trie_hash::GxHashScheme}, + zipper::*, + }; + use std::cell::Cell; + struct CountValues(Cell); + impl HashScheme for CountValues { + type Acc = >::Acc; + fn start(&self, m: &ByteMask) -> Self::Acc { + >::start(&GxHashScheme, m) + } + fn child(&self, a: &mut Self::Acc, h: u128) { + >::child(&GxHashScheme, a, h) + } + fn finish(&self, a: Self::Acc) -> u128 { + >::finish(&GxHashScheme, a) + } + fn value(&self, v: &u64) -> u128 { + self.0.set(self.0.get() + 1); + GxHashScheme.value(v) + } + fn with_value(&self, v: u128, h: u128) -> u128 { + >::with_value(&GxHashScheme, v, h) + } + } + #[test] + fn valued_shared_nodes_are_reused() { + let mut map = PathMap::from_iter([(b"".as_slice(), 1u64), (b"leaf".as_slice(), 2)]); + for _ in 0..10 { + let mut next = PathMap::from_iter([(b"".as_slice(), 1u64)]); + for prefix in [b"a", b"b"] { + next.write_zipper_at_path(prefix).graft_map(map.clone()); + } + map = next; + } + let scheme = CountValues(Cell::new(0)); + let (result, _) = merkleize_root(map.root().unwrap(), map.root_val(), &scheme); + // Each shared node is summarized once; values in its incoming slots are hashed separately. + assert_eq!(scheme.0.get(), 22); + assert_eq!(result.hash, CatamorphismCached::hash(&map)); + } } diff --git a/src/morphisms.rs b/src/morphisms.rs index ab69d21f..2248e8a9 100644 --- a/src/morphisms.rs +++ b/src/morphisms.rs @@ -12,8 +12,8 @@ //! ### Catamorphism //! //! Process a trie from the leaves towards the root. This algorithm proceeds in a depth-first order, -//! working from the leaves upward calling a closure on each path in the trie. A summary "accumulator" -//! type `W` is used to represent the information about the trie and carry it upwards to the next invocation +//! working from the leaves upward calling a closure on each path in the trie. A summary type `W` is +//! used to represent the information about the sub-trie and carry it upwards to the next invocation //! of the closure. //! //! The word "catamorphism" comes from the Greek for "down", because the root is considered the bottom @@ -22,11 +22,11 @@ //! will stick to this convention in-spite of the Greek meaning. //! //! **NOTE**: The traversal order, while depth-first, is subtly different from the order of -//! [`ZipperIteration::to_next_val`] and -//! [`ZipperMoving::to_next_step`]. The -//! zipper methods visit values occurring along a path first before descending to the -//! branches below, while the `cata` methods visit the deepest values first, before -//! returning to higher levels to aggregate information from deeper in the trie. +//! [`ZipperIteration::to_next_val`](crate::zipper::ZipperIteration::to_next_val) and +//! [`ZipperMoving::to_next_step`](crate::zipper::ZipperMoving::to_next_step). The zipper methods +//! visit values occurring along a path first before descending to the branches below, while the `cata` +//! methods visit the deepest values first, before returning to higher levels to aggregate information +//! from deeper in the trie. //! //! ### Anamorphism //! @@ -42,7 +42,7 @@ //! //! In most cases, jumping morphisms will perform substantially better than stepping morphisms, so you should use //! them when it is convenient. It is always possible to re-express an `alg` for a stepping morphism in terms of -//! an `alg` for a jumping morphism, however sometimes the logic become uglier and / or the performance benefit +//! an `alg` for a jumping morphism, however sometimes the logic becomes uglier and / or the performance benefit //! is negligible. Therefore, the stepping methods can be thought of as a convenience API. //! //! ### Side-Effecting vs Cached Iteration @@ -52,12 +52,12 @@ //! //! | side_effect | cached | //! |-------------------------------------------------|---------------------------------------------| -//! | Visits the entire trie | Short-circuits shared branches | +//! | 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) | //! | 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 @@ -66,21 +66,20 @@ //! However, the `side_effect` methods are useful in the implementation of things like serialization, etc. //! use core::convert::Infallible; -use std::hash::Hasher; -use std::ptr::slice_from_raw_parts; use reusing_vec::ReusingQueue; use crate::utils::*; -use crate::alloc::Allocator; +use crate::alloc::{Allocator, GlobalAlloc}; use crate::PathMap; use crate::trie_node::TrieNodeODRc; +use crate::trie_node::recursive_cata_cached; use crate::zipper::*; -use crate::gxhash::{self, HashMap, HashMapExt}; +use crate::gxhash::{HashMap, HashMapExt}; -/// Provides methods to perform a catamorphism on types that can reference or contain a trie -pub trait Catamorphism { - /// Applies a "stepping" catamorphism to the trie descending from the zipper's root, running the `alg_f` at every +/// Provides methods to perform side-effecting catamorphisms appropriate for serialization and full-path operations +pub trait CatamorphismSideEffecting { + /// 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`: @@ -95,12 +94,9 @@ pub trait Catamorphism { /// - `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, @@ -113,11 +109,11 @@ 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; - /// 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`. @@ -128,7 +124,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 @@ -142,121 +138,371 @@ 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; +} - /// 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 into_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 { - Ok(alg_f(mask, children, val)) - }).unwrap() - } +macro_rules! define_cached_cata_trait { + ($(#[$meta:meta])* $trait_name:ident [$($generics:tt)*]) => { + $(#[$meta])* + 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). + /// + /// 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. + /// + fn cata_cached(&self, alg_f: AlgF) -> W + where + W: Clone, + AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> W + { + 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 [Catamorphism::into_cata_cached] - fn into_cata_cached_fallible(self, alg_f: AlgF) -> Result - where - W: Clone, - AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> Result; + /// 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(&ByteMask, &mut [W], Option<&V>) -> Result + { + self.cata_jumping_cached_fallible(|mask, children, val, prefix| { + let mut w = alg_f(mask, children, val)?; + for &byte in prefix.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. + /// Applies a **cached**, **jumping** catamorphism to the subtrie descending from the + /// zipper's current focus. + /// + /// 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>, prefix: &[u8])` + /// + /// `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" + /// ├─ e ─── t → "comet" + /// └─ f ─── o ─── r ─── t → "comfort" + /// ``` + /// + /// This implementation calls `alg_f` 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(&ByteMask, &mut [W], Option<&V>, &[u8]) -> W + { + self.cata_jumping_cached_fallible(|mask, children, val, prefix| -> Result { + Ok(alg_f(mask, children, val, prefix)) + }).unwrap() + } + + /// 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(&ByteMask, &mut [W], Option<&V>, &[u8]) -> Result + { + 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), + }, + ) + } + + /// Hashes the logical trie and all of its values. + fn hash(&self) -> u128 + where + V: std::hash::Hash, + { + self.hash_with_scheme(&trie_hash::GxHashScheme) + } + + /// Hashes the logical trie using the provided function to hash values. + /// + /// A logical node hashes its child mask and its children's hashes; a value hashes together + /// with the hash of the subtrie below it. + fn hash_with(&self, val_hash: F) -> u128 + where + F: Fn(&V) -> u128, + { + self.hash_with_scheme(&trie_hash::GxHashWith(val_hash)) + } + + /// Hashes the logical trie with an explicit [`HashScheme`](trie_hash::HashScheme) + /// + /// The result depends only on the paths and values in the trie, never on how they are laid + /// out in nodes: every byte of a path is hashed as a logical node with one child, a branch + /// point hashes its child mask and its children in ascending byte order, and a value is + /// layered on top of the hash of the subtrie below it. See [`trie_hash`] for the scheme. + fn hash_with_scheme(&self, scheme: &S) -> u128 + where + S: trie_hash::HashScheme, + { + self.factored_cata_jumping::( + |bm| Ok(scheme.start(bm)), + |_bm, child, acc| { scheme.child(acc, child); Ok(()) }, + |bm, value, acc, prefix| { + let below = scheme.finish(acc.unwrap_or_else(|| scheme.start(bm))); + let mut hash = match value { + Some(v) => scheme.with_value(scheme.value(v), below), + None => below, + }; + for byte in prefix.iter().rev() { + hash = scheme.step(*byte, hash); + } + Ok(hash) + }, + ).unwrap_or_else(|never| match never {}) + } + + /// 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. + /// + /// ## Closures + /// + /// `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 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 + /// `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 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. + /// + /// The `SummarizeF` signature is + /// `fn(child_mask: &ByteMask, value: Option<&V>, accumulator: Option, prefix: &[u8]) -> Result`. + /// + /// ## 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 + /// 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; + + /// 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, + 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) + }, + ) + } + } + }; +} + +define_cached_cata_trait! { + /// Cached catamorphisms evaluated with recursive traversal. /// - /// ## Arguments to `alg_f`: - /// `(child_mask: &`[`ByteMask`]`, children: &mut [W], value: Option<&V>, sub_path: &[u8]` + /// 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. /// - /// - `sub_path`: A slice of path bytes for which the `alf_f` will not be called. Consider the - /// trie below: + /// [`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 [V, A = GlobalAlloc] +} + +define_cached_cata_trait! { + /// Cached catamorphisms evaluated with iterative zipper traversal. /// - /// ```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")` + /// 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. /// - /// 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, - Self: Sized - { - self.into_cata_jumping_cached_fallible(|mask, children, val, sub_path| -> Result { - Ok(alg_f(mask, children, val, sub_path)) - }).unwrap() + /// 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 [V] +} + +/// Shared child-result storage used to adapt the factored cached-cata API to a single-function algebra. +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, + } } - /// Allows the closure to return an error, stopping traversal immediately - /// - /// See [Catamorphism::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; + #[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, + } + } - /// 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() - }) + #[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); } - /// 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.into_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] + 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 } } @@ -322,8 +568,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] @@ -382,7 +628,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> + ZipperAbsolutePath + ZipperPathBuffer { fn into_cata_side_effect_fallible(self, mut alg_f: AlgF) -> Result where AlgF: FnMut(&ByteMask, &mut [W], Option<&V>, &[u8]) -> Result, { @@ -398,27 +644,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 { @@ -431,21 +659,116 @@ 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 +} + +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, + NewAccF: Copy + Fn(&ByteMask) -> Result, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, + SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, { - let rz = self.into_read_zipper(&[]); - rz.into_cata_cached_fallible(alg_f) + let focus = self.get_focus(); + 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, self.val(), None, &[]), + } + }; + Ok(w) } - fn into_cata_jumping_cached_fallible(self, alg_f: AlgF) -> Result - where - W: Clone, - AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> Result +} + +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, + NewAccF: Copy + Fn(&ByteMask) -> Result, + FoldChildF: Copy + Fn(&ByteMask, W, &mut Acc) -> Result<(), Err>, + SummarizeF: Copy + Fn(&ByteMask, Option<&V>, Option, &[u8]) -> Result, { - let rz = self.into_read_zipper(&[]); - rz.into_cata_jumping_cached_fallible(alg_f) + summarize_cached_body::<_, V, Acc, _, _, _, _, _, COMPUTE_PATH>( + self.clone(), + new_acc_f, + fold_child_f, + summarize_f, + ) + } +} + +impl CatamorphismCached for PathMap { + 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, + { + let w = match self.root() { + Some(node) => { + let mut cache = HashMap::new(); + 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, self.root_val(), None, &[]), + }; + w + } +} + +impl CatamorphismCachedIterative for PathMap { + 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, + { + summarize_cached_body::<_, V, Acc, _, _, _, _, _, COMPUTE_PATH>( + self.read_zipper(), + new_acc_f, + fold_child_f, + summarize_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, +) -> Result +where + 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), []) => Ok(w), + (val, Some(w), prefix) => { + 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 { + if prefix.is_empty() { prefix } else { &prefix[..prefix.len() - 1] } + } else { + &[] + }; + finalize_f(&mask, val, Some(acc), prefix) + }, + (val, None, prefix) => finalize_f(&ByteMask::EMPTY, val, None, if COMPUTE_PATH { prefix } else { &[] }), } } @@ -456,16 +779,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) } @@ -481,7 +804,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; @@ -507,7 +838,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; @@ -521,8 +860,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(); @@ -531,153 +878,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, } } } @@ -737,108 +1057,339 @@ impl CacheStrategy for DoCache { fn clone(w: &W) -> W { w.clone() } } -/// Internal implementation behind all cached catas -/// -/// AlgF args: (child_mask, children, value, sub_path, 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 +/// Stack frame used in iterative (zipper-based) implementation of Summarization 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, + } + } +} + +/// 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. +#[inline(always)] +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> +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], &[u8]) -> Result, +{ + let witness = zipper.witness(); + let mut child_mask = ByteMask::from(zipper.child_mask()); + + loop { + let old_depth = zipper.depth(); + let old_value = zipper.get_val_with_witness(&witness); + if old_depth == focus_depth { + 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(); + debug_assert!(ascended > 0); + let depth = zipper.depth(); + debug_assert_eq!(old_depth - depth, ascended); + + // 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 + // 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 { &[] }, + debug_path, + ).map(AscendResult::Focus); + } + + let jump_len = if zipper.child_count() != 1 || zipper.is_val() { + ascended - 1 + } else { + ascended + }; + let prefix = if COMPUTE_PATH { + &path[old_depth - jump_len..] + } else { + &[] + }; + + let w = summarize_f(&child_mask, old_value, accumulator, prefix, debug_path)?; + + if zipper.child_count() != 1 || zipper.at_root() { + return Ok(AscendResult::Parent(w)) + } + + 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)?; + accumulator = Some(next_accumulator); + } +} + +/// Iterative cached traversal behind [`CatamorphismCached::factored_cata_jumping`]. +fn summarize_cached_body<'a, Z, V: 'a, Acc, W, E, NewAccF, FoldChildF, SummarizeF, const COMPUTE_PATH: bool>( + zipper: Z, + new_acc_f: NewAccF, + fold_child_f: FoldChildF, + summarize_f: SummarizeF, ) -> Result - where - Cache: CacheStrategy, - Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer, - AlgF: FnMut(&ByteMask, &mut [W], Option<&V>, &[u8], &[u8], &Z) -> Result +where + W: Clone, + 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, { - zipper.reset(); + 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 mut stack = Stack::new(); - let mut children = Vec::::new(); + let root_child_cnt = zipper.child_count(); + if root_child_cnt == 0 { + 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(); + 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, + focus_depth, + None, + debug_path_f, + new_acc_f, + fold_child_f, + summarize_f, + ).map(|result| match result { + AscendResult::Parent(w) | AscendResult::Focus(w) => w, + }) + } + } + 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(); - 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. + .expect("summarization stack is emptied before we returned to root"); + 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()); + zipper.descend_indexed_byte(frame_mut.child_idx as usize); 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); + 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; } - // Descend until leaf or branch let mut is_leaf = false; - 'descend: while zipper.child_count() < 2 { + while zipper.child_count() < 2 { if !zipper.descend_until() { is_leaf = true; - break 'descend; + break; } } 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); + let cur_w = match summarize_ascend_to_fork::( + &mut zipper, + focus_depth, + None, + debug_path_f, + new_acc_f, + fold_child_f, + summarize_f, + )? { + 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()); + fold_child_f(&child_mask, cur_w, &mut frame_mut.accumulator)?; continue 'outer; } - // Enter one recursion step - stack.push_state(&zipper); + let accumulator = new_acc_f(&ByteMask::from(zipper.child_mask()))?; + stack.push(SummarizeStackFrame::new(zipper.child_count(), accumulator)); 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()) + 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, + focus_depth, + Some(frame.accumulator), + debug_path_f, + new_acc_f, + fold_child_f, + summarize_f, + ).map(|result| match result { + AscendResult::Parent(w) | AscendResult::Focus(w) => w, + }) } else { - let debug_path = if DEBUG_PATH { - zipper.origin_path() - } else { - &[] - }; - alg_f(&child_mask, children2, value, &[], debug_path, &zipper) + 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()); + 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 = 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); + 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, + )? { + AscendResult::Parent(w) => w, + AscendResult::Focus(w) => return Ok(w), + }; - // 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); + .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)?; } } +#[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 + W: Clone, + Z: Zipper + ZipperReadOnlyConditionalValues<'a, V> + ZipperConcrete + ZipperAbsolutePath + ZipperPathBuffer, + AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8], &[u8]) -> Result, +{ + 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 // The code is left in for reference/readability, since the unrolled version // is very hard to read. It took several days to debug the unrolled version. @@ -1261,29 +1812,983 @@ impl TrieBuilder { // } // } -// /// An [`Iterator`] type for a [`TrieBuilder`] -// pub struct TrieBuilderIter<'a, W> { -// cb: &'a TrieBuilder, -// cur_mask: u64, -// mask_word: usize, -// i: usize, -// } +// /// An [`Iterator`] type for a [`TrieBuilder`] +// pub struct TrieBuilderIter<'a, W> { +// cb: &'a TrieBuilder, +// cur_mask: u64, +// mask_word: usize, +// i: usize, +// } + +// impl<'a, W> Iterator for TrieBuilderIter<'a, W> { +// type Item = (&'a[u8], &'a W); +// fn next(&mut self) -> Option { +// while self.mask_word < 4 { +// let tz = self.cur_mask.trailing_zeros(); +// if tz < 64 { + +// self.i += 1; +// } else { +// self.mask_word += 1; +// } +// } +// None +// } +// } + +/// 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::utils::{ByteMask, ByteMaskIter}; + use crate::write_zipper::{WriteZipperOwned, ZipperWriting}; + use crate::zipper::{Zipper, ZipperIteration, 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", + 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"]; + + /// 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 { + (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, + $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<()> + ) { + 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 { + 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()); + } + } + + /// Runs both cached-cata engines through the reconstruction probe and checks each result + /// against the expected logical trie as well as against one another. + #[track_caller] + pub(crate) fn assert_reconstructs_like(subject: &Z, expected: &PathMap<()>) + where + Z: crate::morphisms::CatamorphismCached<(), GlobalAlloc> + + crate::morphisms::CatamorphismCachedIterative<()>, + { + let iterative = reconstruct_trie!(CatamorphismCachedIterative, subject); + assert_same_paths(&iterative, expected); + + let recursive = reconstruct_trie!(CatamorphismCached, subject); + 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); + } + + /// 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_map(|key| key.strip_prefix(focus).map(ToOwned::to_owned)) + .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"]); + + assert_reconstructs_like(&map, &map); + } + + // Isolate traversal checks so an abort or timeout is reported as a test + // failure. Under Miri, run the body directly for UB diagnostics. + fn run_cata_test_in_child(name: &str) -> bool { + #[cfg(miri)] + { + let _ = name; + false + } + #[cfg(not(miri))] + { + use std::process::{Command, Stdio}; + use std::time::{Duration, Instant}; + + let test = format!("morphisms::cached_catamorphism_tests::{name}"); + const CHILD_TEST: &str = "PATHMAP_CATA_REGRESSION_CHILD"; + if std::env::var(CHILD_TEST).as_deref() == Ok(test.as_str()) { + return false; + } + let mut child = Command::new(std::env::current_exe().unwrap()) + .args(["--exact", &test, "--nocapture"]) + .env(CHILD_TEST, &test) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + let deadline = Instant::now() + Duration::from_secs(20); + while child.try_wait().unwrap().is_none() { + if Instant::now() >= deadline { + child.kill().unwrap(); + child.wait().unwrap(); + panic!("{test} did not finish within 20 seconds"); + } + std::thread::sleep(Duration::from_millis(10)); + } + let output = child.wait_with_output().unwrap(); + assert!(output.status.success(), "{test} failed: {}\n{}\n{}", + output.status, String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr)); + true + } + } + + // https://github.com/Adam-Vandervorst/PathMap/pull/31#issuecomment-5516518307, case 1. + #[test] + fn cached_catas_preserve_values_above_compressed_child_runs() { + use crate::morphisms::{CatamorphismCached, CatamorphismCachedIterative}; + + let map = map_from_keys(&[b"a", b"abcd", b"abce"]); + assert_reconstructs_like(&map, &map); + assert_eq!(CatamorphismCached::hash(&map), CatamorphismCachedIterative::hash(&map)); + } + + // https://github.com/Adam-Vandervorst/PathMap/pull/31#issuecomment-5516518307, case 2. + #[test] + fn cached_catas_traverse_retained_branches_after_value_removal() { + use crate::morphisms::{CatamorphismCached, CatamorphismCachedIterative}; + + if run_cata_test_in_child("cached_catas_traverse_retained_branches_after_value_removal") { + return; + } + let mut map = map_from_keys(&[b"aa", b"ab", b"ba", b"bb", b"b"]); + map.remove_val_at(b"ba", false); + map.remove_val_at(b"bb", false); + assert_eq!(CatamorphismCached::val_count(&map), 3); + assert_eq!(CatamorphismCachedIterative::val_count(&map), 3); + assert_reconstructs_like(&map, &map_from_keys(&[b"aa", b"ab", b"b"])); + } + + // https://github.com/Adam-Vandervorst/PathMap/pull/31#issuecomment-5516518307, case 3. + #[test] + fn cached_catas_reconstruct_prefixed_subtries() { + use crate::zipper::{PrefixZipper, ZipperInfallibleSubtries}; + + let map = map_from_keys(&[b"ab", b"ac"]); + let zipper = PrefixZipper::new(b"xy".as_slice(), map.read_zipper()); + let expected = map_from_keys(&[b"xyab", b"xyac"]); + assert_same_paths(&zipper.make_map(), &expected); + assert_reconstructs_like(&zipper, &expected); + } + + #[test] + fn cached_catas_follow_prefixed_focus_for_readers_and_writers() { + use crate::morphisms::CatamorphismCached; + use crate::zipper::PrefixZipper; + + let source = map_from_keys(&[b"", b"ab", b"ac"]); + for (prefix, origin) in [ + (b"xy".as_slice(), b"".as_slice()), (b"xy", b"x"), (b"", b""), + ] { + for focus in [b"".as_slice(), b"x", b"xy", b"xya", b"xyab", b"xz", b"xyaz", b"y", b"yab"] { + let absolute_focus = [origin, focus].concat(); + let expected_keys: Vec> = [b"".as_slice(), b"ab", b"ac"].iter() + .filter_map(|key| [prefix, key].concat().strip_prefix(absolute_focus.as_slice()).map(ToOwned::to_owned)) + .collect(); + let expected = map_from_owned_keys(&expected_keys); + let mut reader = PrefixZipper::new(prefix, source.read_zipper()).with_origin(origin).unwrap(); + reader.descend_to(focus); + assert_eq!(CatamorphismCached::hash(&reader), CatamorphismCached::hash(&expected.read_zipper())); + assert_eq!(CatamorphismCached::val_count(&reader), expected_keys.len()); + #[cfg(feature = "graft_root_vals")] + assert_reconstructs_like(&reader, &expected); + + let mut writer = PrefixZipper::new(prefix, source.clone().into_write_zipper(b"")) + .with_origin(origin).unwrap(); + writer.descend_to(focus); + assert_eq!(CatamorphismCached::hash(&writer), CatamorphismCached::hash(&reader)); + assert_eq!(CatamorphismCached::val_count(&writer), expected_keys.len()); + } + } + let inner = PrefixZipper::new(b"xy".as_slice(), source.read_zipper()); + let outer = PrefixZipper::new(b"uv".as_slice(), inner); + let expected = map_from_keys(&[b"uvxy", b"uvxyab", b"uvxyac"]); + assert_eq!(CatamorphismCached::hash(&outer), CatamorphismCached::hash(&expected.read_zipper())); + #[cfg(feature = "graft_root_vals")] + assert_reconstructs_like(&outer, &expected); + } + + #[cfg(feature = "graft_root_vals")] + #[test] + fn grafting_from_a_prefixed_path_preserves_its_subtrie() { + use crate::zipper::PrefixZipper; + + let source = map_from_keys(&[b"", b"ab", b"ac"]); + let zipper = PrefixZipper::new(b"xy".as_slice(), source.read_zipper()); + let full_keys: &[&[u8]] = &[b"xy", b"xyab", b"xyac"]; + for path in [b"".as_slice(), b"x", b"xy", b"xya", b"xyab", b"xz", b"xyaz"] { + let expected_keys: Vec> = full_keys.iter() + .filter_map(|key| key.strip_prefix(path).map(ToOwned::to_owned)).collect(); + let mut grafted = PathMap::new(); + grafted.write_zipper().graft_src_at(&zipper, path); + assert_same_paths(&grafted, &map_from_owned_keys(&expected_keys)); + } + } + + // https://github.com/Adam-Vandervorst/PathMap/pull/31#issuecomment-5518280164. + #[test] + fn cached_catas_traverse_join_k_path_results() { + use crate::morphisms::{CatamorphismCached, CatamorphismCachedIterative}; + + if run_cata_test_in_child("cached_catas_traverse_join_k_path_results") { + return; + } + let mut map = map_from_keys(&[b"aaa", b"bab"]); + assert!(map.write_zipper().join_k_path_into(1, false)); + assert_eq!(map.iter().map(|(path, _)| path).collect::>(), + [b"aa".to_vec(), b"ab".to_vec()]); + + // Indexed traversal and both catamorphisms must agree on the two + // children after removing the leading path byte. + let mut zipper = map.read_zipper(); + zipper.descend_to_byte(b'a'); + assert_eq!(zipper.child_count(), 2); + assert_eq!(zipper.descend_indexed_byte(1), Some(b'b')); + assert_eq!(CatamorphismCached::val_count(&map), 2); + assert_eq!(CatamorphismCachedIterative::val_count(&map), 2); + assert_reconstructs_like(&map, &map_from_keys(&[b"aa", b"ab"])); + } + + /// 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. 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_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_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 + /// 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()], + // 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 { + 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); + } + } + + /// A 256-way logical branch exercises storage configurations that use wide byte-indexed + /// 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); + #[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 + /// node layout. + #[test] + fn recursive_cata_randomized_maps_and_foci() { + 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; + + use rand::{Rng, SeedableRng}; + use rand::rngs::StdRng; + + 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, [$($cata_args:ty),+]) => { + pub(crate) mod $suite_name { + 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<$($cata_args),*>, + { + let error = $cata_trait::<$($cata_args),*> + ::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 = $cata_trait::<$($cata_args),*> + ::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 = $cata_trait::<$($cata_args),*> + ::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: crate::morphisms::$cata_trait<$($cata_args),*>, + { + let count = $cata_trait::<$($cata_args),*>::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: crate::morphisms::$cata_trait<$($cata_args),*>, + { + let count = $cata_trait::<$($cata_args),*>::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: crate::morphisms::$cata_trait<$($cata_args),*>, + { + let count = $cata_trait::<$($cata_args),*> + ::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: crate::morphisms::$cata_trait<$($cata_args),*>, + { + let count = $cata_trait::<$($cata_args),*> + ::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); + } + + /// 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<$($cata_args),*>, + { + zipper.descend_to(b"roman"); + let count = $cata_trait::<$($cata_args),*>::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<$($cata_args),*>, + { + zipper.descend_to(b"roma"); + let count = $cata_trait::<$($cata_args),*>::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<$($cata_args),*>, + { + 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()) + .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: crate::morphisms::$cata_trait<$($cata_args),*>, + { + let longest = $cata_trait::<$($cata_args),*> + ::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: crate::morphisms::$cata_trait<$($cata_args),*>, + { + let values = $cata_trait::<$($cata_args),*>::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: crate::morphisms::$cata_trait<$($cata_args),*>, + { + use std::cell::RefCell; + + let events = RefCell::new(Vec::new()); + let result = $cata_trait::<$($cata_args),*> + ::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: crate::morphisms::$cata_trait<$($cata_args),*>, + { + let result = $cata_trait::<$($cata_args),*> + ::factored_cata_jumping::)>, Vec, Infallible, _, _, _, true>( + &zipper, + |_| 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(); + 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>( + store: &'a mut Store, + make_z: impl Fn(&'a mut Store) -> Z, + test: impl Fn(Z), + ) + where + Z: 'a + crate::morphisms::$cata_trait<$($cata_args),*>, + { + test(make_z(store)); + } -// impl<'a, W> Iterator for TrieBuilderIter<'a, W> { -// type Item = (&'a[u8], &'a W); -// fn next(&mut self) -> Option { -// while self.mask_word < 4 { -// let tz = self.cur_mask.trailing_zeros(); -// if tz < 64 { + } + }; + } -// self.i += 1; -// } else { -// self.mask_word += 1; -// } -// } -// None -// } -// } + 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) => { + paste::paste! { + #[test] + fn [<$z_name _ $implementation _ $test>]() { + let mut temp_store = ($read_keys)(crate::morphisms::cached_catamorphism_tests::$keys); + crate::morphisms::cached_catamorphism_tests::$suite::run_test( + &mut temp_store, + $make_z, + crate::morphisms::cached_catamorphism_tests::$suite::$test, + ); + } + } + }; + } + pub(crate) use cached_catamorphism_case; + + macro_rules! cached_catamorphism_tests { + ($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, 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); + $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; +} #[cfg(test)] mod tests { @@ -1293,10 +2798,106 @@ mod tests { use crate::zipper::ZipperValuesAt; use super::*; + trait TestRecursiveCata: Sized { + fn recursive_cata_cached(&self, alg_f: AlgF) -> W + where + Self: CatamorphismCached, + W: Clone, + AlgF: Fn(&ByteMask, &mut [W], Option<&V>) -> W, + { + CatamorphismCached::::cata_cached(self, alg_f) + } + + fn recursive_cata_jumping_cached(&self, alg_f: AlgF) -> W + where + Self: CatamorphismCached, + W: Clone, + AlgF: Fn(&ByteMask, &mut [W], Option<&V>, &[u8]) -> W, + { + 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: 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, + { + 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: 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, + { + CatamorphismCached::::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_zipper = map.read_zipper(); + let recursive = CatamorphismCached::::cata_cached( + &recursive_zipper, + alg, + ); + let iterative_zipper = map.read_zipper(); + let iterative = CatamorphismCachedIterative::::cata_cached( + &iterative_zipper, + alg, + ); + + assert_eq!(recursive, iterative); + 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 - Z: Clone + Catamorphism, W: Clone, + Z: Clone + CatamorphismSideEffecting, W: Clone, AlgF: FnMut(&ByteMask, &mut [W], usize, Option<&V>, &[u8]) -> W, Assert: FnMut(W, &str), { @@ -1308,32 +2909,32 @@ 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), { - let output = zipper.clone().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().into_cata_jumping_cached( - |bm, ch, v, sub_path| f_pure(bm, ch, v, sub_path)); - assert(output, "into_cata_jumping_cached"); + assert(output, "cata_cached"); + let output = zipper.clone().recursive_cata_jumping_cached( + |bm, ch, v, prefix| f_pure(bm, ch, v, prefix)); + assert(output, "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), { 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); } @@ -1384,19 +2985,20 @@ mod tests { } (val.is_some(), sum) }; - let output = map.read_zipper().into_cata_cached(pure_alg_stepping); + let zipper = map.read_zipper(); + 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 - // 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(); } } @@ -1406,7 +3008,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 @@ -1453,11 +3055,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()) @@ -1466,7 +3068,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 } @@ -1745,8 +3347,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; } )); @@ -1786,8 +3388,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; } )); @@ -1818,17 +3420,17 @@ mod tests { // println!("collapse: {path:?}"); match *v { 1 => assert_eq!(path, &[97, 98]), - 0 => assert_eq!(path, &[]), + 0 => assert!(path.is_empty()), _ => unreachable!(), } }, |_mask, _, path| { // println!("alg: {path:?}"); - assert_eq!(path, &[]); + assert!(path.is_empty()); }, - |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]); } )) @@ -1950,7 +3552,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_cata_cached( |_bm, children, value| { calls_cached.fetch_add(1, Relaxed); Rc::new(Node::new(value, children)) @@ -1968,6 +3570,555 @@ 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_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) { + acc.idx += 1; + if w.0 { + acc.sum += (byte as char).to_digit(10).unwrap(); + } + } + acc.sum += w.1; + Ok(()) + }, + |_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(); + } + } + Ok((val.is_some() && prefix.is_empty(), sum)) + }, + ).unwrap().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_factored_cata::( + |_| Ok(SumAcc::default()), + |mask: &ByteMask, w: (bool, u32), acc: &mut SumAcc| { + 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; + Ok(()) + }, + |_mask, val, acc| { + Ok((val.is_some(), acc.map(|acc| acc.sum).unwrap_or(0))) + }, + ).unwrap().1; + assert_eq!(sum, expected_sum); + } + } + + /// 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().recursive_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().recursive_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_factored_cata_jumping::( + |_| Ok(0), + |_mask, child, total| { *total += child; Ok(()) }, + |_mask, val, children, _prefix| match children { + Some(total) => Ok(total), + None => { + assert!(val.is_some()); + Ok(1) + }, + }, + ); + let stepping = map.recursive_factored_cata::( + |_| Ok(0), + |_mask, child, total| { *total += child; Ok(()) }, + |_mask, val, children| match children { + Some(total) => Ok(total), + None => { + assert!(val.is_some()); + Ok(1) + }, + }, + ); + + assert_eq!(cached_stepping, 11); + assert_eq!(cached_jumping, 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 + /// 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().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)| { + 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_factored_cata_jumping::>, Vec, Infallible, _, _, _, 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() { + 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) + }, + ); + 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); + 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; + } + Ok(()) + }, + |_mask, _val, state| Ok(state.map_or_else(Vec::new, |(_, path)| path)), + ); + 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)| { + 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); + } + + /// 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().recursive_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().recursive_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_factored_cata_jumping::, Vec, Infallible, _, _, _, false>( + |_| Ok(Vec::new()), + |_mask, child, values| { values.extend(child); Ok(()) }, + |_mask, val, children, _prefix| match children { + None => Ok(Vec::new()), + Some(values) => Ok(val.map_or(values, |val| vec![*val])), + }, + ); + let stepping = map.recursive_factored_cata::, Vec, Infallible, _, _, _>( + |_| Ok(Vec::new()), + |_mask, child, values| { values.extend(child); Ok(()) }, + |_mask, val, children| match children { + 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.unwrap(), cached_jumping); + assert_eq!(stepping.unwrap(), cached_stepping); + } + + /// Parallel port of `cata_test_cached`: the input deliberately contains + /// shared subtries, so this exercises `CatamorphismCached`'s factored traversal. + /// A dangling path is a child slot holding the empty-node sentinel, whose "pointer" is not a + /// real address. The recursive cached cata must never read the sentinel's refcount when it + /// decides whether a child needs the cache. Found by the differential fuzzer: on a two-slot + /// node with 1-byte keys whose second child dangles, `hash` and `val_count` read through the + /// sentinel (a segfault in release). + #[test] + fn recursive_cata_dangling_child_in_two_child_node() { + let mut map = PathMap::<()>::new(); + map.insert(&[0u8, 0][..], ()); + map.insert(&[0u8, 1][..], ()); + map.create_path(&[1u8]); + assert_eq!(CatamorphismCached::val_count(&map), 2); + assert_eq!(CatamorphismCached::hash(&map), CatamorphismCachedIterative::hash(&map)); + // and with the dangling child first + let mut map = PathMap::<()>::new(); + map.insert(&[1u8, 0][..], ()); + map.insert(&[1u8, 1][..], ()); + map.create_path(&[0u8]); + assert_eq!(CatamorphismCached::val_count(&map), 2); + assert_eq!(CatamorphismCached::hash(&map), CatamorphismCachedIterative::hash(&map)); + // a dangling path under a value-bearing slot + let mut map = PathMap::::new(); + map.insert(&[0u8][..], 1); + map.insert(&[0u8, 0][..], 2); + map.insert(&[0u8, 1][..], 3); + map.create_path(&[1u8]); + assert_eq!(CatamorphismCached::val_count(&map), 3); + assert_eq!(CatamorphismCached::hash(&map), CatamorphismCachedIterative::hash(&map)); + let mut merk = map.clone(); + assert_eq!(merk.merkleize().hash, CatamorphismCached::hash(&map)); + } + + #[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().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_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_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_factored_cata::>>, Rc>, Infallible, _, _, _>( + |_| Ok(Vec::new()), + |_mask, child, children| { children.push(child); Ok(()) }, + |_mask, value, children| { + summarization_calls.fetch_add(1, Relaxed); + Ok(Rc::new(Node::new(value, children))) + }, + ); + + 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_factored_cata::>>, 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_factored_cata_jumping::( + |_| 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 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` + /// 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 = 200; + #[cfg(not(feature = "all_dense_nodes"))] + 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_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)), + ); + 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_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_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_factored_cata_jumping::<(), (), &'static str, _, _, _, 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_factored_cata_jumping::<(), (), &'static str, _, _, _, 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 #[test] fn ana_test1() { @@ -2248,3 +4399,203 @@ mod tests { } } } + +/// The hashing scheme behind [`CatamorphismCached::hash`](CatamorphismCached::hash) and merkleization +/// +/// A hash is defined over the **logical** trie, so it is the same for every physical layout of the +/// same paths and values. For a position `p` in the trie: +/// +/// * `below(p)` = `finish(start(mask) ∘ child(h(p ++ [b])) for b in mask ascending)`, where `mask` is +/// the set of bytes with a child below `p`. A byte inside a non-branching run is a mask with one +/// bit; an endpoint with no children is the empty mask, whose hash is [`leaf`](trie_hash::HashScheme::leaf). +/// * `h(p)` = `with_value(value(v), below(p))` when `p` holds the value `v`, otherwise `below(p)`. +/// +/// [`GxHashScheme`](trie_hash::GxHashScheme) is the production scheme. [`Fnv1a64Scheme`](trie_hash::Fnv1a64Scheme) exists for the differential fuzzing +/// harness, whose Lean oracle (`lean/PathMapModel/Hash.lean`) computes the same function from the +/// model of the logical trie. +pub mod trie_hash { + use core::hash::{Hash, Hasher}; + use core::ptr::slice_from_raw_parts; + use crate::gxhash; + use crate::utils::ByteMask; + + /// The primitive operations a logical trie hash is built from. See the [module docs](self) + pub trait HashScheme { + /// The running state of a logical node's hash while its children are folded in + type Acc; + /// Begins a logical node's hash from its child mask + fn start(&self, mask: &ByteMask) -> Self::Acc; + /// Folds one child's hash into a logical node. Called in ascending byte order + fn child(&self, acc: &mut Self::Acc, child: u128); + /// Completes a logical node's hash + fn finish(&self, acc: Self::Acc) -> u128; + /// Hashes a value on its own + fn value(&self, v: &V) -> u128; + /// Places a value's hash on top of the hash of the subtrie below it + fn with_value(&self, val_hash: u128, below: u128) -> u128; + /// The hash of a position with no children and no value + #[inline] + fn leaf(&self) -> u128 { + self.finish(self.start(&ByteMask::EMPTY)) + } + /// The hash of one byte of a non-branching run above `below`: a node with a single child + #[inline] + fn step(&self, byte: u8, below: u128) -> u128 { + let mut acc = self.start(&ByteMask::from(byte)); + self.child(&mut acc, below); + self.finish(acc) + } + } + + const SEED: i64 = 0b0100001010101101111110010110100110000010011000100100100111110111i64; + + /// Hashes a value on its own, with the default seed + #[inline(always)] + pub(crate) fn value_hash(v: &V) -> u128 { + let mut hasher = gxhash::GxHasher::with_seed(0); + v.hash(&mut hasher); + hasher.finish_u128() + } + /// Domain tag opening a logical node's message: `N`, then the 32 mask bytes, then the children + pub const NODE_TAG: u8 = b'N'; + /// Domain tag opening a value's message: `V`, then the hash below, then the value hash + pub const VALUE_TAG: u8 = b'V'; + + /// Starts the hash of a logical node from its child mask; the children's hashes are written next + /// + /// The message is `N`, the 32 bytes of the mask as a little-endian 256-bit bitmap, then 16 + /// little-endian bytes per child. The tag separates it from [`with_value`]'s message, and the + /// mask fixes how many children follow, so distinct (mask, children) never share a message. See + /// `lean/PathMapModel/HashSecurity.lean` for the proof that a collision between two logically + /// different tries is therefore a collision of the primitive + #[inline(always)] + pub(crate) fn node_hasher(bm: &ByteMask) -> gxhash::GxHasher { + let mut hasher = gxhash::GxHasher::with_seed(SEED); + hasher.write_u8(NODE_TAG); + hasher.write(unsafe { slice_from_raw_parts(bm.0.as_ptr() as *const u8, 32).as_ref().unwrap_unchecked() }); + hasher + } + /// Places a value's hash on top of the hash of the subtrie below it: the message is `V`, then the + /// 16 bytes of `below`, then the 16 bytes of `val_hash` + #[inline(always)] + pub(crate) fn with_value(val_hash: u128, below: u128) -> u128 { + let mut hasher = gxhash::GxHasher::with_seed(SEED); + hasher.write_u8(VALUE_TAG); + hasher.write_u128(below); + hasher.write_u128(val_hash); + hasher.finish_u128() + } + + /// The production scheme: gxhash over a tag, the mask bytes and the children's hashes (see + /// [`node_hasher`] and [`with_value`] for the messages), values hashed through [`Hash`] with seed 0 + #[derive(Clone, Copy, Default, Debug)] + pub struct GxHashScheme; + + impl HashScheme for GxHashScheme { + type Acc = gxhash::GxHasher; + #[inline(always)] + fn start(&self, mask: &ByteMask) -> Self::Acc { node_hasher(mask) } + #[inline(always)] + fn child(&self, acc: &mut Self::Acc, child: u128) { acc.write_u128(child) } + #[inline(always)] + fn finish(&self, acc: Self::Acc) -> u128 { acc.finish_u128() } + #[inline(always)] + fn value(&self, v: &V) -> u128 { value_hash(v) } + #[inline(always)] + fn with_value(&self, val_hash: u128, below: u128) -> u128 { with_value(val_hash, below) } + } + + /// [`GxHashScheme`] with a caller-supplied value hash, for + /// [`CatamorphismCached::hash_with`](super::CatamorphismCached::hash_with) + #[derive(Clone, Copy)] + pub struct GxHashWith(pub F); + + impl u128> HashScheme for GxHashWith { + type Acc = gxhash::GxHasher; + #[inline(always)] + fn start(&self, mask: &ByteMask) -> Self::Acc { node_hasher(mask) } + #[inline(always)] + fn child(&self, acc: &mut Self::Acc, child: u128) { acc.write_u128(child) } + #[inline(always)] + fn finish(&self, acc: Self::Acc) -> u128 { acc.finish_u128() } + #[inline(always)] + fn value(&self, v: &V) -> u128 { (self.0)(v) } + #[inline(always)] + fn with_value(&self, val_hash: u128, below: u128) -> u128 { with_value(val_hash, below) } + } + + /// A 64-bit FNV-1a state. Integers are absorbed little-endian regardless of the platform, so a + /// trace is comparable across machines + #[derive(Clone, Copy, Debug)] + pub struct Fnv1a64(pub u64); + + impl Fnv1a64 { + pub const OFFSET: u64 = 0xcbf29ce484222325; + pub const PRIME: u64 = 0x100000001b3; + #[inline] + pub fn new() -> Self { Self(Self::OFFSET) } + } + impl Default for Fnv1a64 { + fn default() -> Self { Self::new() } + } + impl Hasher for Fnv1a64 { + #[inline] + fn finish(&self) -> u64 { self.0 } + #[inline] + fn write(&mut self, bytes: &[u8]) { + for b in bytes { + self.0 = (self.0 ^ (*b as u64)).wrapping_mul(Self::PRIME); + } + } + #[inline] fn write_u16(&mut self, i: u16) { self.write(&i.to_le_bytes()) } + #[inline] fn write_u32(&mut self, i: u32) { self.write(&i.to_le_bytes()) } + #[inline] fn write_u64(&mut self, i: u64) { self.write(&i.to_le_bytes()) } + #[inline] fn write_u128(&mut self, i: u128) { self.write(&i.to_le_bytes()) } + #[inline] fn write_usize(&mut self, i: usize) { self.write(&(i as u64).to_le_bytes()) } + } + + /// A scheme built on 64-bit FNV-1a, simple enough to be re-implemented in a few lines elsewhere. + /// Not for production use: it exists so an independent model can compute the same hash and check + /// that the trie hash is a function of the logical trie alone. + /// + /// * `value(v)` = FNV-1a over what `v.hash()` writes (integers little-endian) + /// * `start(mask)` = FNV-1a over `b'N'` then the 32 bytes of the mask as a little-endian 256-bit + /// bitmap (byte `i` holds bits `8i..8i+8`); `child(h)` absorbs the low 64 bits of `h` + /// little-endian; `finish` is the state + /// * `with_value(vh, below)` = FNV-1a over `b'V'`, the low 64 bits of `below`, then of `vh` + /// + /// Every hash this scheme produces fits in 64 bits. + #[derive(Clone, Copy, Default, Debug)] + pub struct Fnv1a64Scheme; + + impl HashScheme for Fnv1a64Scheme { + type Acc = Fnv1a64; + #[inline] + fn start(&self, mask: &ByteMask) -> Self::Acc { + let mut h = Fnv1a64::new(); + h.write_u8(NODE_TAG); + for word in mask.0 { + h.write(&word.to_le_bytes()); + } + h + } + #[inline] + fn child(&self, acc: &mut Self::Acc, child: u128) { acc.write_u64(child as u64) } + #[inline] + fn finish(&self, acc: Self::Acc) -> u128 { acc.0 as u128 } + #[inline] + fn value(&self, v: &V) -> u128 { + let mut h = Fnv1a64::new(); + v.hash(&mut h); + h.0 as u128 + } + #[inline] + fn with_value(&self, val_hash: u128, below: u128) -> u128 { + let mut h = Fnv1a64::new(); + h.write_u8(VALUE_TAG); + h.write_u64(below as u64); + h.write_u64(val_hash as u64); + h.0 as u128 + } + } +} diff --git a/src/overlay_zipper.rs b/src/overlay_zipper.rs index 3cb88a2d..05dfddc5 100644 --- a/src/overlay_zipper.rs +++ b/src/overlay_zipper.rs @@ -173,10 +173,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 6cbef805..f8d92728 100644 --- a/src/path_tracker.rs +++ b/src/path_tracker.rs @@ -72,7 +72,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); @@ -216,6 +215,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 74f6bdf4..687ba8d5 100644 --- a/src/prefix_zipper.rs +++ b/src/prefix_zipper.rs @@ -3,7 +3,7 @@ use fast_slice_utils::{find_prefix_overlap, starts_with}; use crate::alloc::Allocator; use crate::utils::ByteMask; use crate::PathMap; -use crate::trie_node::TaggedNodeRef; +use crate::trie_node::{AbstractNodeRef, TaggedNodeRef}; use crate::zipper::*; #[derive(Clone)] @@ -305,6 +305,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) } @@ -397,10 +401,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; @@ -784,9 +784,46 @@ impl<'prefix, V: Clone + Send + Sync + Unpin, Z, A: Allocator> ZipperInfallibleS TrieRefOwned::new_invalid_in(self.source.alloc()).into() } } - fn get_focus(&self) -> OpaqueAbstractNodeRef<'_, V, A> { self.source.get_focus() } - fn get_focus_at>(&self, path: K) -> OpaqueAbstractNodeRef<'_, V, A> { self.source.get_focus_at(path) } - fn try_borrow_focus(&self) -> Option> { self.source.try_borrow_focus() } + fn get_focus(&self) -> OpaqueAbstractNodeRef<'_, V, A> { + match self.prefix_path_below_focus() { + Some([]) => self.source.get_focus(), + Some(prefix) if self.source.path_exists() => { + // The virtual prefix is part of the logical subtrie. Own the + // prefix nodes while sharing the source's downstream nodes. + let mut map = PathMap::new_in(self.source.alloc()); + { + let mut zipper = map.write_zipper_at_path(prefix); + zipper.graft(&self.source); + #[cfg(not(feature = "graft_root_vals"))] + if let Some(value) = self.source.val() { + zipper.set_val(value.clone()); + } + } + OpaqueAbstractNodeRef(map.into_root().0.map_or(AbstractNodeRef::None, AbstractNodeRef::OwnedRc)) + }, + _ => OpaqueAbstractNodeRef(AbstractNodeRef::None), + } + } + fn get_focus_at>(&self, path: K) -> OpaqueAbstractNodeRef<'_, V, A> { + let path = path.as_ref(); + match self.prefix_path_below_focus() { + Some(prefix) if path.starts_with(prefix) => self.source.get_focus_at(&path[prefix.len()..]), + Some(prefix) if prefix.starts_with(path) => { + let Some(node) = self.get_focus().into_option() else { + return OpaqueAbstractNodeRef(AbstractNodeRef::None); + }; + let focus = node.as_tagged().get_node_at_key(path).into_option(); + OpaqueAbstractNodeRef(focus.map_or(AbstractNodeRef::None, AbstractNodeRef::OwnedRc)) + }, + _ => OpaqueAbstractNodeRef(AbstractNodeRef::None), + } + } + fn try_borrow_focus(&self) -> Option> { + match self.position { + PrefixPos::Source => self.source.try_borrow_focus(), + _ => None, + } + } } impl<'prefix, 'a, V: Clone + Send + Sync + 'a, Z, A: Allocator + 'a> ZipperReadOnlySubtries<'a, V, A> for PrefixZipper<'prefix, Z> where Z: ZipperReadOnlySubtries<'a, V, A>, Self: zipper_priv::ZipperReadOnlyPriv<'a, V, A> + ZipperSubtries { diff --git a/src/product_zipper.rs b/src/product_zipper.rs index ff06bbcd..4ff67a09 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; @@ -391,6 +387,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) } @@ -580,6 +577,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) } @@ -719,10 +717,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; @@ -963,7 +957,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) => { @@ -1751,7 +1745,7 @@ mod tests { pz.reset(); pz.descend_to(&full_path); assert!(pz.ascend_until() > 0); - assert_eq!(pz.path(), []); + assert!(pz.path().is_empty()); assert_eq!(pz.path_exists(), true); assert_eq!(pz.child_count(), 1); assert_eq!(pz.is_val(), false); @@ -1759,7 +1753,7 @@ mod tests { // test ascend_until_branch pz.descend_to(&full_path); assert!(pz.ascend_until_branch() > 0); - assert_eq!(pz.path(), []); + assert!(pz.path().is_empty()); assert_eq!(pz.path_exists(), true); assert_eq!(pz.child_count(), 1); assert_eq!(pz.is_val(), false); diff --git a/src/random.rs b/src/random.rs index ae9ca791..2451a9e9 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::*; @@ -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.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/subtract_zipper.rs b/src/subtract_zipper.rs index 43a5e57b..d2658b39 100644 --- a/src/subtract_zipper.rs +++ b/src/subtract_zipper.rs @@ -1,8 +1,10 @@ -use std::{cell::Cell, num::NonZeroUsize}; +use std::{cell::Cell, convert::Infallible, num::NonZeroUsize}; use smallvec::SmallVec; use crate::{ + alloc::Allocator, + morphisms::CatamorphismCached, ring::{self, AlgebraicResult, DistributiveLattice, DistributiveLatticeRef}, utils::{BitMask, ByteMask}, zipper::{ @@ -832,76 +834,91 @@ where } } -/// Counts values in the materialized subtraction below the current focus. +/// Folds the virtual subtraction without materializing it. Unaffected subtrees use +/// the left zipper's native recursive morphism; overlapping subtrees use the +/// surviving child masks maintained by `SubtractZipper`. /// -/// LHS-only subtrees are counted using the native `val_count()` operation; -/// only overlapping subtrees are traversed explicitly. -/// -/// Both zippers are restored to their original focus before returning. -fn subtract_val_count(lhs: &mut A, rhs: &mut B) -> usize +/// Overlapping paths fall back to stepping (an empty prefix), which is allowed by +/// the jumping morphism contract. No results are cached by left-node identity: +/// the same left node can have different results against different right nodes. +fn subtract_cata( + zipper: &mut SubtractZipper, + new_acc_f: NewAccF, + fold_child_f: FoldChildF, + summarize_f: SummarizeF, +) -> Result where V: DistributiveLattice + Clone, - A: ZipperMoving + ZipperValues, - B: ZipperMoving + ZipperValues, + A: ZipperMoving + ZipperValues + Clone + CatamorphismCached, + B: ZipperMoving + ZipperValues + Clone, + Alloc: Allocator, + 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, { - if !lhs.path_exists() { - return 0; - } - - if !rhs.path_exists() { - return lhs.val_count(); + if !zipper.rhs.path_exists() && zipper.lhs.path_exists() { + return zipper.lhs.factored_cata_jumping::<_, _, _, _, _, _, COMPUTE_PATH>( + new_acc_f, fold_child_f, summarize_f, + ); } - let mut depth = 0; - let mut count = 0; - let mut lhs_mask = lhs.child_mask(); - let mut rhs_mask = rhs.child_mask(); - - 'descend: loop { - count += usize::from(value_survives::(lhs, rhs)); - - let lhs_only = (lhs_mask ^ rhs_mask) & lhs_mask; - for lhs_byte in lhs_only.iter() { - lhs.descend_to_byte(lhs_byte); - count += lhs.val_count(); - lhs.ascend_byte(); + let mask = zipper.child_mask; + let mut accumulator = if mask.is_empty_mask() { + None + } else { + Some(new_acc_f(&mask)?) + }; + if let Some(acc) = &mut accumulator { + for byte in mask.iter() { + zipper.descend_to_byte(byte); + let child = subtract_cata::<_, _, _, Alloc, _, _, _, _, _, _, COMPUTE_PATH>( + zipper, new_acc_f, fold_child_f, summarize_f, + )?; + zipper.ascend_byte(); + fold_child_f(&mask, child, acc)?; } + } + summarize_f(&mask, zipper.val(), accumulator, &[]) +} - let mut combined_mask = lhs_mask & rhs_mask; - let mut next_common_byte = combined_mask.indexed_bit::(0); - loop { - match next_common_byte { - Some(byte) => { - lhs.descend_to_byte(byte); - rhs.descend_to_byte(byte); - - lhs_mask = lhs.child_mask(); - rhs_mask = rhs.child_mask(); - - depth += 1; - continue 'descend; - } - None => { - if depth == 0 { - break 'descend; - } - - let cur_byte = lhs.focus_byte().expect("non-empty path when depth > 0"); - lhs.ascend_byte(); - rhs.ascend_byte(); - - lhs_mask = lhs.child_mask(); - rhs_mask = rhs.child_mask(); - combined_mask = lhs_mask & rhs_mask; - next_common_byte = combined_mask.next_bit(cur_byte); +impl CatamorphismCached for SubtractZipper +where + V: DistributiveLattice + Clone, + A: ZipperMoving + ZipperValues + Clone + CatamorphismCached, + B: ZipperMoving + ZipperValues + Clone, + Alloc: Allocator, +{ + 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, + { + let mut zipper = SubtractZipper::new(self.lhs.clone(), self.rhs.clone()); + subtract_cata::<_, _, _, Alloc, _, _, _, _, _, _, COMPUTE_PATH>( + &mut zipper, new_acc_f, fold_child_f, summarize_f, + ) + } - depth -= 1; - } - } + fn val_count(&self) -> usize { + if let Some(count) = self.val_count.get() { + return count; } + let count = self.factored_cata_jumping::<_, _, Infallible, _, _, _, false>( + |_| Ok(0usize), + |_mask, child, total| { *total += child; Ok(()) }, + |_mask, value, total, _prefix| Ok(usize::from(value.is_some()) + total.unwrap_or(0)), + ).unwrap(); + self.val_count.set(Some(count)); + count } - - count } impl Zipper for SubtractZipper @@ -975,27 +992,6 @@ where // self.lhs.path().len() == self.lhs_root_depth // } - fn val_count(&self) -> usize { - if let Some(count) = self.val_count.get() { - return count; - } - - let count = if !self.path_exists() { - 0 - } else if self.child_mask.is_empty_mask() { - 1 - } else if !self.rhs.path_exists() { - self.lhs.val_count() - } else { - let mut lhs = self.lhs.clone(); - let mut rhs = self.rhs.clone(); - subtract_val_count::(&mut lhs, &mut rhs) - }; - - self.val_count.set(Some(count)); - count - } - fn descend_to>(&mut self, path: K) { let path = path.as_ref(); if path.is_empty() { @@ -1754,6 +1750,25 @@ mod tests { State::Leaf(true).assert(&zipper); } + type CataDescription = Vec<(Vec, Option, Vec)>; + + // Include every byte position and its child mask, so this detects incorrect + // pruning and prefix handling as well as incorrect values. + fn describe_cata_step( + mask: &ByteMask, + children: &mut [CataDescription], + value: Option<&V>, + ) -> CataDescription { + let mut result = vec![(Vec::new(), value.cloned(), mask.iter().collect())]; + for (byte, child) in mask.iter().zip(children) { + for (mut path, value, child_bytes) in std::mem::take(child) { + path.insert(0, byte); + result.push((path, value, child_bytes)); + } + } + result + } + fn assert_subtract_matches_materialized( lhs: &PathMap, rhs: &PathMap, @@ -1769,9 +1784,12 @@ mod tests { expected_z.descend_to(path.as_ref()); let actual = subtract_at(lhs, rhs, path.as_ref()); + // A concrete zipper reports its empty root as existing; the virtual + // subtraction reports existence only when some output survives. + let expected_exists = expected_z.is_val() || expected_z.child_count() != 0; assert_eq!( actual.path_exists(), - expected_z.path_exists(), + expected_exists, "path_exists differs at {path:?}" ); @@ -1801,10 +1819,107 @@ mod tests { "val_count differs at {path:?}" ); + assert_eq!(actual.val_count(), expected_z.val_count(), "cached count at {path:?}"); + assert_eq!( + actual.cata_cached(describe_cata_step), + expected_z.cata_cached(describe_cata_step), + "morphism structure differs at {path:?}" + ); expected_z.reset(); } } + #[test] + fn subtract_cata_handles_empty_results_root_values_and_owned_values() { + use std::collections::HashSet; + let lhs = PathMap::from_iter([ + (b"".as_slice(), HashSet::from([1u8, 2])), + (b"a", HashSet::from([3])), + (b"a/removed", HashSet::from([4])), + (b"b/kept", HashSet::from([5])), + ]); + let rhs = PathMap::from_iter([ + (b"".as_slice(), HashSet::from([1u8])), + (b"a", HashSet::from([3])), + (b"a/removed", HashSet::from([4])), + ]); + let paths = [b"".as_slice(), b"a", b"a/removed", b"b", b"b/kept", b"missing"]; + assert_subtract_matches_materialized(&lhs, &rhs, paths); + assert_subtract_matches_materialized(&lhs, &lhs, paths); + let empty = PathMap::new(); + assert_subtract_matches_materialized(&empty, &lhs, paths); + assert_subtract_matches_materialized(&lhs, &empty, paths); + } + + #[test] + fn subtract_cata_delegates_unaffected_runs_and_invalidates_counts() { + let lhs = PathMap::from_iter([ + (b"a/x".as_slice(), true), + (b"a/y", true), + (b"b/long/unaffected/path", true), + ]); + let rhs = PathMap::from_iter([(b"a/x".as_slice(), true)]); + let mut zipper = subtract_at(&lhs, &rhs, &[]); + let saw_prefix = Cell::new(false); + let count = zipper.factored_cata_jumping::<_, _, Infallible, _, _, _, true>( + |_| Ok(0usize), + |_, child, total| { *total += child; Ok(()) }, + |_, value, total, prefix| { + saw_prefix.set(saw_prefix.get() || !prefix.is_empty()); + Ok(total.unwrap_or(0) + usize::from(value.is_some())) + }, + ).unwrap(); + assert_eq!(count, 2); + assert!(saw_prefix.get(), "left-only runs should use the native jumping morphism"); + let count_without_paths = zipper.factored_cata_jumping::<_, _, Infallible, _, _, _, false>( + |_| Ok(0usize), + |_, child, total| { *total += child; Ok(()) }, + |_, value, total, prefix| { + assert!(prefix.is_empty()); + Ok(total.unwrap_or(0) + usize::from(value.is_some())) + }, + ).unwrap(); + assert_eq!(count_without_paths, count); + assert!(zipper.path().is_empty()); + assert_eq!(zipper.val_count(), 2); + assert_eq!(zipper.val_count.get(), Some(2)); + zipper.descend_to(b"a"); + assert_eq!(zipper.val_count.get(), None); + assert_eq!(zipper.val_count(), 1); + zipper.reset(); + assert_eq!(zipper.val_count(), 2); + } + + #[test] + fn subtract_cata_propagates_callback_errors_without_moving_source() { + let lhs = PathMap::from_iter([(b"a".as_slice(), true), (b"b/long/path", true)]); + let rhs = PathMap::from_iter([(b"a".as_slice(), false)]); + let zipper = subtract_at(&lhs, &rhs, &[]); + for failing_stage in ["start", "fold", "summarize"] { + let failed = Cell::new(false); + let check = |stage| { + assert!(!failed.get(), "callback invoked after error"); + if stage == failing_stage { + failed.set(true); + Err(stage) + } else { + Ok(()) + } + }; + let result = zipper.factored_cata_jumping::<_, _, _, _, _, _, true>( + |_| { check("start")?; Ok(0usize) }, + |_, child, total| { check("fold")?; *total += child; Ok(()) }, + |_, value, total, _| { + check("summarize")?; + Ok(total.unwrap_or(0) + usize::from(value.is_some())) + }, + ); + assert_eq!(result, Err(failing_stage)); + assert!(zipper.path().is_empty()); + assert_eq!(zipper.val_count(), 2); + } + } + #[test] fn subtract_zipper_matches_materialized_subtraction_on_all_prefixes() { let lhs = PathMap::from_iter([ diff --git a/src/tiny_node.rs b/src/tiny_node.rs index 3d3ecb13..db888f7f 100644 --- a/src/tiny_node.rs +++ b/src/tiny_node.rs @@ -9,7 +9,6 @@ use core::mem::MaybeUninit; use core::fmt::{Debug, Formatter}; -use std::collections::HashMap; use fast_slice_utils::{find_prefix_overlap, starts_with}; use crate::utils::ByteMask; @@ -121,6 +120,16 @@ 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, passed_in_val: Option<&V>, start_f: StartF, fold_child_f: FoldChildF, finalize_f: FinalizeF, cache: &mut impl crate::trie_node::CataCache) -> 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>(passed_in_val, start_f, fold_child_f, finalize_f, cache) + } } impl<'a, V: Clone + Send + Sync, A: Allocator> TrieNode for TinyRefNode<'a, V, A> { @@ -218,12 +227,8 @@ impl<'a, V: Clone + Send + Sync, A: Allocator> TrieNode for TinyRefNode<'a fn iter_token_for_path(&self, _key: &[u8]) -> IterToken { unreachable!() } fn ascend_iter_token(&self, _token: IterToken, _byte_count: usize) -> IterToken { unreachable!() } fn next_items(&self, _token: IterToken, _after_focus: bool) -> (IterToken, &'a[u8], Option<&TrieNodeODRc>, Option<&V>) { unreachable!() } - fn node_val_count(&self, cache: &mut 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 49c72936..5566c052 100644 --- a/src/trie_map.rs +++ b/src/trie_map.rs @@ -1,12 +1,12 @@ use core::cell::UnsafeCell; +use core::convert::Infallible; use crate::alloc::{Allocator, GlobalAlloc, global_alloc}; -use crate::morphisms::{new_map_from_ana_in, TrieBuilder}; +use crate::morphisms::{new_map_from_ana_in, CatamorphismCached, TrieBuilder}; use crate::trie_node::*; use crate::zipper::*; -use crate::merkleization::{MerkleizeResult, merkleize_impl}; +use crate::merkleization::{MerkleizeResult, merkleize_root}; use crate::ring::{AlgebraicResult, AlgebraicStatus, COUNTER_IDENT, SELF_IDENT, Lattice, LatticeRef, DistributiveLattice, DistributiveLatticeRef, Quantale}; -use crate::gxhash; /// A map type that uses a trie based on byte slices (`&[u8]`) known as "paths" /// @@ -505,24 +505,18 @@ 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 { - 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_val + Some(_root) => { + 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)), + ) { + Ok(count) => count, + Err(never) => match never {}, + } }, - None => root_val + None => unsafe{ &*self.root_val.get() }.is_some() as usize } } @@ -588,16 +582,16 @@ impl PathMap { } /// Optimize the `PathMap` by factoring shared subtries using a temporary [Merkle Tree](https://en.wikipedia.org/wiki/Merkle_tree) + /// + /// Subtries are identified by their logical hash, so [`MerkleizeResult::hash`] equals + /// [`CatamorphismCached::hash`](crate::morphisms::CatamorphismCached::hash) for this map. pub fn merkleize(&mut self) -> MerkleizeResult where V: core::hash::Hash { let Some(root) = self.root() else { return MerkleizeResult::default(); }; - let mut result = MerkleizeResult::default(); - let mut memo = gxhash::HashMap::default(); - let (hash, new_root) = merkleize_impl(&mut result, &mut memo, root, self.root_val()); - result.hash = hash; + let (result, new_root) = merkleize_root(root, self.root_val(), &crate::morphisms::trie_hash::GxHashScheme); if let Some(new_root) = new_root { *self.root.get_mut() = Some(new_root); } diff --git a/src/trie_node.rs b/src/trie_node.rs index 19b7031b..8f3ffb7c 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; @@ -258,15 +259,9 @@ pub(crate) trait TrieNode: TrieNodeDowncas /// a subsequent call using that token reports exhaustion. fn next_items(&self, token: IterToken, after_focus: bool) -> (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 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 /// @@ -1377,23 +1372,12 @@ mod tagged_node_ref { } #[inline] - pub fn node_val_count(&self, cache: &mut HashMap) -> 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 { + pub fn node_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, } } @@ -2545,89 +2529,115 @@ 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) +/// Storage for summaries at physical node boundaries in the recursive cached cata. +/// +/// Ordinary algebras cache only summaries without a parent-slot value. Merkleization also +/// records unshared nodes and can restore a slot value onto a cached value-free hash. +/// +/// A subtrie's root value is conceptually part of the subtrie. The separation here accommodates +/// the current physical storage of that value in the parent. When nodes own their root values, +/// their complete summaries can be cached by node ID instead. +pub(crate) trait CataCache { + /// Whether to consult and fill the cache for this node. Must be false for the empty sentinel; + /// its refcount must never be read. + fn caches(&self, node: &TrieNodeODRc) -> bool; + /// A summary computed without a value supplied by the parent. + fn get(&self, key: u64) -> Option<&W>; + /// Optionally restore a parent-slot value onto a cached summary. General algebras cannot + /// separate that value from their summary and must recompute instead. + fn get_with_value(&self, _key: u64, _value: &V) -> Option { None } + /// Record a completed summary, together with the parent-slot value used to compute it. + fn insert(&mut self, key: u64, node: &TrieNodeODRc, value: Option<&V>, w: &W); } -pub(crate) fn val_count_below_node(node: &TrieNodeODRc, cache: &mut HashMap) -> usize { - if node.is_empty() { - return 0 +impl CataCache for HashMap { + #[inline] + fn caches(&self, node: &TrieNodeODRc) -> bool { + !node.is_empty() && node.refcount() > 1 } - 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 - }, + #[inline] + fn get(&self, key: u64) -> Option<&W> { + self.get(&key) + } + #[inline] + fn insert(&mut self, key: u64, _node: &TrieNodeODRc, value: Option<&V>, w: &W) { + if value.is_none() { + self.insert(key, w.clone()); } - } else { - node.as_tagged().node_val_count(cache) } } -/// 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 +/// Internal implementation of `CatamorphismCached::factored_cata_jumping` +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 impl CataCache, +) -> Result +where V: Clone + Send + Sync, A: Allocator, - Ctx: Clone + Default, - NodeF: Fn(TaggedNodeRef, Ctx) -> Ctx + Copy, - FoldF: Fn(Ctx, Ctx) -> Ctx + Copy + 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, { - 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(), + // 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 + // + // That guard is now split between the match below and the ordinary cache's `insert` method. + // Merkleization's `get_with_value` is a specialized workaround: it restores the supplied value + // onto a value-free hash. Once nodes own their root values, that workaround can be removed too. + if cache.caches(node) { + let key = node.shared_node_id(); + match passed_in_val { None => { - let ctx = traverse_physical_children_internal(node.as_tagged(), node_f, fold_f, cache); - cache.insert(hash, ctx.clone()); - ctx + if let Some(w) = cache.get(key) { + return Ok(w.clone()); + } + }, + Some(value) => { + if let Some(w) = cache.get_with_value(key, value) { + return Ok(w); + } }, } + let w = recursive_cata_dispatch::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(node, passed_in_val, start_f, fold_child_f, finalize_f, cache)?; + cache.insert(key, node, passed_in_val, &w); + Ok(w) } else { - traverse_physical_children_internal(node.as_tagged(), node_f, fold_f, cache) + recursive_cata_dispatch::<_, _, _, _, _, _, _, _, COMPUTE_PATH>(node, passed_in_val, start_f, fold_child_f, finalize_f, cache) } } -fn traverse_physical_children_internal(node: TaggedNodeRef, node_f: NodeF, fold_f: FoldF, cache: &mut HashMap) -> Ctx - where +#[inline(always)] +fn recursive_cata_dispatch( + node: &TrieNodeODRc, + passed_in_val: Option<&V>, + start_f: StartF, + fold_child_f: FoldChildF, + finalize_f: FinalizeF, + cache: &mut impl CataCache, +) -> Result +where V: Clone + Send + Sync, A: Allocator, - Ctx: Clone + Default, - NodeF: Fn(TaggedNodeRef, Ctx) -> Ctx + Copy, - FoldF: Fn(Ctx, Ctx) -> Ctx + Copy + 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 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.as_tagged() { + 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, &[]) } } - - node_f(node, ctx) } /// Internal function to walk a mut TrieNodeODRc ref along a path @@ -2705,6 +2715,7 @@ 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; + #[cfg(not(feature = "slim_ptrs"))] mod opaque_dyn_rc_trie_node { use std::sync::Arc; @@ -3195,6 +3206,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 } @@ -3237,6 +3249,7 @@ mod opaque_dyn_rc_trie_node { } #[inline] pub(crate) fn refcount(&self) -> usize { + debug_assert!(!self.is_empty(), "refcount of the empty-node sentinel, which is not a real allocation"); let (ptr, _tag) = self.ptr.get_raw_parts(); unsafe{ &*ptr }.load(Acquire) as usize } @@ -3503,5 +3516,4 @@ mod tests { node_ref.make_unique(); drop(cloned); } - } diff --git a/src/utils/debug/diff_zipper.rs b/src/utils/debug/diff_zipper.rs index 9808df12..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); @@ -230,6 +224,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/utils/debug/morphism_debug.rs b/src/utils/debug/morphism_debug.rs index 550f2acd..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 [`into_cata_jumping_cached`](crate::morphisms::Catamorphism::into_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)); } } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 1991cdc5..f49b8e8c 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -461,6 +461,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> for ByteMask { #[inline] fn from(range: Range) -> Self { diff --git a/src/write_zipper.rs b/src/write_zipper.rs index 55c6c3e6..d0544d8c 100644 --- a/src/write_zipper.rs +++ b/src/write_zipper.rs @@ -500,12 +500,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) } @@ -524,6 +530,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) } @@ -685,12 +692,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) } @@ -709,6 +722,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) } @@ -847,6 +861,7 @@ impl Zipper for WriteZipperOwned ZipperValues for WriteZipperOwned { zipper_impl_lens!(ZipperValues self => self.z); } impl ZipperValuesAt for WriteZipperOwned { zipper_impl_lens!(ZipperValuesAt 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); } @@ -1102,6 +1117,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) -> Option> { @@ -1156,15 +1179,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(); @@ -1256,6 +1270,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()); @@ -5967,7 +5991,7 @@ mod tests { assert_eq!(wz.prune_path(), 7); assert_eq!(wz.path_exists(), false); assert_eq!(wz.ascend(7), 7); - assert_eq!(wz.path(), &[]); + assert!(wz.path().is_empty()); assert_eq!(wz.path_exists(), true); assert_eq!(wz.child_count(), 0); assert_eq!(wz.val(), None); @@ -7941,7 +7965,7 @@ mod tests { /// every catamorphism engine (the PR #31 iterative engine loops on it). #[test] fn write_zipper_drop_head_shared_first_byte_is_canonical() { - use crate::morphisms::Catamorphism; + use crate::morphisms::CatamorphismCachedIterative; let mut m = PathMap::<()>::new(); for k in [b"aaa".as_slice(), b"bab"] { m.set_val_at(k, ()); } assert!(m.write_zipper().join_k_path_into(1, false)); @@ -7953,7 +7977,7 @@ mod tests { assert_eq!(rz.child_count(), 2); assert!(rz.descend_indexed_byte(1).is_some()); //the (zipper-driven) cached cata must agree with iteration - let n = m.read_zipper().into_cata_cached(|_m, ws: &mut [usize], v: Option<&()>| ws.iter().sum::() + v.is_some() as usize); + let n = CatamorphismCachedIterative::cata_cached(&m.read_zipper(), |_m, ws: &mut [usize], v: Option<&()>| ws.iter().sum::() + v.is_some() as usize); assert_eq!(n, 2); assert_valid_trie(m.root()); @@ -7963,7 +7987,7 @@ mod tests { for k in [b"".as_slice(), b"a", b"aaaa", b"abab", b"b", b"ba", b"baa", b"bbaa", b"bbb"] { other.set_val_at(k, ()); } { let mut rz = other.read_zipper(); rz.descend_to(b"a"); let mut wz = map.write_zipper(); wz.graft(&rz); } assert!(map.write_zipper().join_k_path_into(1, false)); - let n = map.read_zipper().into_cata_cached(|_m, ws: &mut [usize], v: Option<&()>| ws.iter().sum::() + v.is_some() as usize); + let n = CatamorphismCachedIterative::cata_cached(&map.read_zipper(), |_m, ws: &mut [usize], v: Option<&()>| ws.iter().sum::() + v.is_some() as usize); assert_eq!(n, 3); assert_valid_trie(map.root()); } @@ -8567,4 +8591,3 @@ mod tests { assert_eq!(map.iter().map(|(k, v)| (k, *v)).collect::>(), vec![(vec![1], 1), (vec![2], 2)]); } } - diff --git a/src/zipper.rs b/src/zipper.rs index a4999550..efdddfad 100644 --- a/src/zipper.rs +++ b/src/zipper.rs @@ -29,6 +29,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 { @@ -202,12 +203,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); @@ -1384,6 +1379,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` /// @@ -1470,7 +1471,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) } @@ -1520,6 +1520,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) } @@ -2334,20 +2335,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(); @@ -2796,6 +2783,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()); @@ -5156,7 +5153,7 @@ pub(crate) mod zipper_moving_tests { pub const ZIPPER_BYTES_ITER_TEST3_PATH: &[u8] = &[3, 193, 4, 193, 5, 2, 193]; pub fn zipper_byte_iter_test3(mut zipper: Z) { - assert_eq!(zipper.path(), &[]); + assert!(zipper.path().is_empty()); assert_eq!(zipper.descend_first_byte(), Some(6)); assert_eq!(zipper.path(), &[6]); assert_eq!(zipper.descend_first_byte(), Some(193)); @@ -5955,7 +5952,7 @@ pub(crate) mod zipper_iteration_tests { assert_eq!(zipper.path(), &[1]); assert_eq!(&observed[..], zipper.path()); assert_eq!(zipper.to_next_k_path_observed(1, &mut observed), false); - assert_eq!(zipper.path(), &[]); + assert!(zipper.path().is_empty()); assert_eq!(&observed[..], zipper.path()); } @@ -5976,7 +5973,7 @@ pub(crate) mod zipper_iteration_tests { assert_eq!(zipper.path(), K_PATH_TESTA_KEYS[1]); assert_eq!(&observed[..], zipper.path()); assert_eq!(zipper.to_next_k_path_observed(k, &mut observed), false); - assert_eq!(zipper.path(), []); + assert!(zipper.path().is_empty()); assert_eq!(&observed[..], zipper.path()); } } @@ -5986,6 +5983,20 @@ 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(), + 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 + ); + /// Drives a [TruncatingObserver] with the supplied movements, returning what reached the /// downstream observer along with the observer's final overshoot fn run_truncating(limit: usize, movements: &[&[u8]]) -> (Vec, usize) { @@ -6120,7 +6131,7 @@ mod tests { assert_eq!(moved, false); assert_eq!(pz.path(), b""); assert_eq!(observed, b""); - } +} super::zipper_moving_tests::zipper_moving_tests!(read_zipper, |keys: &[&[u8]]| { @@ -6686,7 +6697,7 @@ mod tests { assert_eq!(rz.origin_path(), root, "root {root:?}"); assert_eq!(rz.val(), None); assert!(rz.at_root()); - assert_eq!(rz.path(), &[]); + assert!(rz.path().is_empty()); //The zipper must still be usable afterwards rz.reset(); assert!(!rz.path_exists()); diff --git a/tests/act_recursive_catamorphism.rs b/tests/act_recursive_catamorphism.rs new file mode 100644 index 00000000..db176008 --- /dev/null +++ b/tests/act_recursive_catamorphism.rs @@ -0,0 +1,136 @@ +#![cfg(feature = "arena_compact")] + +use pathmap::PathMap; +use pathmap::arena_compact::ArenaCompactTree; +use pathmap::morphisms::CatamorphismCached; +use pathmap::zipper::{ZipperAbsolutePath, ZipperMoving, ZipperWriting}; + +// Include unvalued paths as well as values, so compressed runs and dangling +// paths must reconstruct the same logical trie, byte for byte. +fn logical_nodes>(zipper: &Z) -> Vec<(Vec, Option)> { + zipper.cata_cached( + |mask, children: &mut [Vec<(Vec, Option)>], value| { + let mut result = vec![(Vec::new(), value.copied())]; + for (byte, child) in mask.iter().zip(children) { + for (mut path, value) in std::mem::take(child) { + path.insert(0, byte); + result.push((path, value)); + } + } + result + }, + ) +} + +#[test] +fn recursive_act_matches_native_from_every_focus() { + let mut dangling = PathMap::::new(); + dangling.set_val_at(b"dangling", 1); + dangling.write_zipper_at_path(b"dangling").remove_val(false); + let fixtures = [ + PathMap::::new(), + dangling, + PathMap::from_iter([(b"".as_slice(), u64::MAX)]), + PathMap::from_iter([(b"abcdefghij".as_slice(), 248)]), + PathMap::from_iter([ + (b"".as_slice(), 0), + (b"a", 247), + (b"ab", 248), + (b"abcdef", 65535), + (b"abcxyz", u64::MAX), + (b"z", 1), + ]), + PathMap::from_iter((0..=255u64).map(|byte| (vec![byte as u8, 1, 2], byte))), + ]; + for map in fixtures { + let mut focuses = vec![Vec::new(), b"missing".to_vec(), b"dangling".to_vec()]; + for (path, _) in map.iter() { + for depth in 0..=path.len() { + focuses.push(path[..depth].to_vec()); + } + let mut missing = path; + missing.push(255); + focuses.push(missing); + } + focuses.sort(); + focuses.dedup(); + for tree in [ + ArenaCompactTree::from_zipper(map.read_zipper(), |&value| value), + ArenaCompactTree::from_zipper_cached(map.read_zipper(), |&value| value), + ] { + for focus in &focuses { + let native = map.read_zipper_at_path(focus); + let mut act = tree.read_zipper_u64(); + act.descend_to(focus); + let before = act.origin_path().to_vec(); + assert_eq!( + logical_nodes(&act), + logical_nodes(&native), + "focus {focus:?}" + ); + let expected = map + .iter() + .filter(|(path, _)| path.starts_with(focus)) + .count(); + assert_eq!(act.val_count(), expected, "focus {focus:?}"); + assert_eq!( + pathmap::morphisms::CatamorphismCachedIterative::val_count(&act), + expected, + ); + assert_eq!(act.origin_path(), before); + assert_eq!(tree.read_zipper_at_path_u64(focus).val_count(), expected); + assert_eq!(tree.read_zipper_at_path(focus).val_count(), expected); + } + } + } +} + +#[test] +fn both_act_strategies_revisit_shared_subtries() { + use std::cell::Cell; + + let mut map = PathMap::from_iter([(b"leaf".as_slice(), 7u64)]); + for _ in 0..8 { + let mut next = PathMap::new(); + for prefix in [b"left".as_slice(), b"right"] { + next.write_zipper_at_path(prefix).graft(&map.read_zipper()); + } + map = next; + } + let tree = ArenaCompactTree::from_zipper_cached(map.read_zipper(), |&value| value); + let recursive_values = Cell::new(0); + let zipper = tree.read_zipper_u64(); + let count = zipper.cata_jumping_cached(|_, children: &mut [usize], value, _| { + recursive_values.set(recursive_values.get() + value.is_some() as usize); + value.is_some() as usize + children.iter().sum::() + }); + let iterative_values = Cell::new(0); + let iterative_count = pathmap::morphisms::CatamorphismCachedIterative::cata_jumping_cached( + &zipper, + |_, children: &mut [usize], value, _| { + iterative_values.set(iterative_values.get() + value.is_some() as usize); + value.is_some() as usize + children.iter().sum::() + }, + ); + assert_eq!(count, 1 << 8); + assert_eq!(iterative_count, count); + // ACT has no sharing metadata: both strategies currently visit each + // logical value, even when the serialized subtree is shared. + assert_eq!(recursive_values.get(), count); + assert_eq!(iterative_values.get(), count); + assert_eq!(zipper.val_count(), count); +} + +#[test] +fn recursive_act_reads_mapped_storage() { + let map = PathMap::from_iter([(b"a".as_slice(), 248u64), (b"abcdef", u64::MAX)]); + let tree = ArenaCompactTree::from_zipper(map.read_zipper(), |&value| value); + let file = tempfile::NamedTempFile::new().unwrap(); + std::fs::write(file.path(), tree.get_data()).unwrap(); + let mapped = ArenaCompactTree::open_mmap(file.path()).unwrap(); + assert_eq!( + logical_nodes(&mapped.read_zipper_u64()), + logical_nodes(&map.read_zipper()) + ); + assert_eq!(mapped.read_zipper_at_path_u64(b"abc").val_count(), 1); +} diff --git a/tests/merkleization.rs b/tests/merkleization.rs new file mode 100644 index 00000000..2b1f20a5 --- /dev/null +++ b/tests/merkleization.rs @@ -0,0 +1,58 @@ +use pathmap::{ + PathMap, + morphisms::{CatamorphismCached, CatamorphismCachedIterative}, + zipper::*, +}; + +fn snapshot(map: &PathMap) -> Vec<(Vec, Option)> { + let mut z = map.read_zipper(); + let mut result = vec![(vec![], z.val().copied())]; + while z.to_next_step() { + result.push((z.path().to_vec(), z.val().copied())); + } + result +} + +#[test] +fn empty_and_root_only() { + for mut map in [ + PathMap::::new(), + PathMap::from_iter([(b"".as_slice(), 42)]), + ] { + let expected = CatamorphismCached::hash(&map); + assert_eq!(expected, CatamorphismCachedIterative::hash(&map)); + assert_eq!(map.merkleize().hash, expected); + } +} + +#[test] +fn seeded_mixed_paths() { + use rand::{Rng, SeedableRng}; + let mut rng = rand::rngs::StdRng::seed_from_u64(12345); + for case in 0..100 { + let mut map = PathMap::::new(); + for _ in 0..100 { + let len = rng.random_range(0..12); + let path: Vec = (0..len).map(|_| rng.random_range(0..4)).collect(); + if rng.random_bool(0.3) { + map.create_path(&path); + } else { + map.set_val_at(&path, rng.random_range(0..5)); + } + } + let before = snapshot(&map); + let hash = CatamorphismCached::hash(&map); + assert_eq!(hash, CatamorphismCachedIterative::hash(&map), "case {case}"); + let result = map.merkleize(); + assert_eq!(result.hash, hash, "case {case}"); + assert_eq!(snapshot(&map), before, "case {case}"); + assert_eq!(CatamorphismCached::hash(&map), hash, "case {case}"); + assert_eq!(CatamorphismCachedIterative::hash(&map), hash, "case {case}"); + let second = map.merkleize(); + assert_eq!( + (second.reused, second.replaced, second.cloned), + (0, 0, 0), + "case {case}" + ); + } +}