From 2a0fa2896126ddc56aa37171897720fe61eca6cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Rze=C5=BAnicki?= Date: Mon, 7 Sep 2026 10:26:09 +0200 Subject: [PATCH 01/11] splitting the API into ZipperValue and ZipperValueAt Split zipper_moving_tests to follow the new ValuesAt trait --- pathmap-derive/src/lib.rs | 30 +++++++++++++ src/arena_compact.rs | 21 +++++++++ src/dependent_zipper.rs | 9 ++++ src/empty_zipper.rs | 3 ++ src/experimental/zipper_algebra.rs | 2 +- src/overlay_zipper.rs | 28 +++++++++++- src/path_tracker.rs | 7 +-- src/poly_zipper.rs | 31 ++++++++++++- src/prefix_zipper.rs | 7 +++ src/product_zipper.rs | 33 ++++++++++++++ src/trie_ref.rs | 11 ++++- src/write_zipper.rs | 17 +++++++ src/zipper.rs | 72 +++++++++++++++++++++++++----- 13 files changed, 254 insertions(+), 17 deletions(-) diff --git a/pathmap-derive/src/lib.rs b/pathmap-derive/src/lib.rs index 5111c6d4..2e5576db 100644 --- a/pathmap-derive/src/lib.rs +++ b/pathmap-derive/src/lib.rs @@ -8,6 +8,7 @@ use std::collections::BTreeSet; enum PolyZipperTrait { Zipper, ZipperValues, + ZipperValuesAt, ZipperReadOnlyValues, ZipperReadOnlyConditionalValues, ZipperReadOnlyConditionalIteration, @@ -27,6 +28,7 @@ impl PolyZipperTrait { match ident.to_string().as_str() { "Zipper" => Some(Self::Zipper), "ZipperValues" => Some(Self::ZipperValues), + "ZipperValuesAt" => Some(Self::ZipperValuesAt), "ZipperReadOnlyValues" => Some(Self::ZipperReadOnlyValues), "ZipperReadOnlyConditionalValues" => Some(Self::ZipperReadOnlyConditionalValues), "ZipperReadOnlyConditionalIteration" => Some(Self::ZipperReadOnlyConditionalIteration), @@ -49,6 +51,7 @@ fn all_poly_zipper_traits() -> BTreeSet { BTreeSet::from([ Zipper, ZipperValues, + ZipperValuesAt, ZipperReadOnlyValues, ZipperReadOnlyConditionalValues, ZipperReadOnlyConditionalIteration, @@ -126,6 +129,11 @@ fn add_trait_dependencies(traits: &mut BTreeSet) { } } if traits.contains(&ZipperInfallibleSubtries) { + if traits.insert(ZipperValuesAt) { + changed = true; + } + } + if traits.contains(&ZipperValuesAt) { if traits.insert(ZipperValues) { changed = true; } @@ -320,7 +328,28 @@ fn derive_poly_zipper_with_traits( #(#variant_arms => inner.val(),)* } } + } + }) + } else { + None + }; + // Generate ZipperValuesAt trait implementation + let zipper_values_at_impl = if traits.contains(&PolyZipperTrait::ZipperValuesAt) { + let variant_arms = &variant_arms; + let zipper_values_where = if include_where_clause { + quote! { + where + #(#inner_types: pathmap::zipper::ZipperValuesAt,)* + #where_clause + } + } else { + quote! {} + }; + Some(quote! { + impl #impl_generics pathmap::zipper::ZipperValuesAt for #enum_name #ty_generics + #zipper_values_where + { fn val_at>(&self, path: K) -> Option<&V> { match self { #(#variant_arms => inner.val_at(path),)* @@ -874,6 +903,7 @@ fn derive_poly_zipper_with_traits( #(#from_impls)* #zipper_impl #zipper_values_impl + #zipper_values_at_impl #zipper_read_only_values_impl #zipper_read_only_conditional_values_impl // #zipper_forking_impl diff --git a/src/arena_compact.rs b/src/arena_compact.rs index 9ee4a49b..04e631ae 100644 --- a/src/arena_compact.rs +++ b/src/arena_compact.rs @@ -94,6 +94,7 @@ use crate::{ ZipperConcrete, ZipperReadOnlyConditionalValues, TrieRef }, }; + use crate::gxhash::{GxHasher, HashMap, HashMapExt}; /// The identifier of a node (branch node or line node) @@ -2796,6 +2797,11 @@ where Storage: AsRef<[u8]> fn val(&self) -> Option<&()> { self.get_value().map(|_x| &()) } +} + +impl<'tree, Storage> ZipperValuesAt<()> for ACTZipper<'tree, Storage, ()> +where Storage: AsRef<[u8]> +{ fn val_at>(&self, path: K) -> Option<&()> { self.get_value_at(path.as_ref()).map(|_x| &()) } @@ -2808,6 +2814,11 @@ where Storage: AsRef<[u8]> //GOAT, see soundness discussion in ZipperReadOnlyValues impl below self.get_val() } +} + +impl<'tree, Storage> ZipperValuesAt for ACTZipper<'tree, Storage, u64> +where Storage: AsRef<[u8]> +{ fn val_at>(&self, path: K) -> Option<&u64> { //GOAT, see soundness discussion in ZipperReadOnlyValues impl below self.get_val_at(path) @@ -3328,6 +3339,16 @@ mod tests { } ); + zipper_moving_tests::zipper_val_at_tests!(arena_compact_zipper, + |keys: &[&[u8]]| { + let btm = keys.into_iter().map(|k| (k, ())).collect::>(); + ArenaCompactTree::from_zipper(btm.read_zipper(), |&_v| 0) + }, + |trie: &mut ArenaCompactTree>, path: &[u8]| -> ACTZipper<'_, Vec, ()> { + trie.read_zipper_at_path(path) + } + ); + zipper_iteration_tests::zipper_iteration_tests!(arena_compact_zipper, |keys: &[&[u8]]| { let btm = keys.into_iter().map(|k| (k, ())).collect::>(); diff --git a/src/dependent_zipper.rs b/src/dependent_zipper.rs index fe328c79..5620e2a4 100644 --- a/src/dependent_zipper.rs +++ b/src/dependent_zipper.rs @@ -238,6 +238,15 @@ impl<'trie, PrimaryZ, SecondaryZ, V, C, F : Clone + for <'a> FnOnce(C, &'a [u8], self.primary.val() } } +} + +impl<'trie, PrimaryZ, SecondaryZ, V, C, F : Clone + for <'a> FnOnce(C, &'a [u8], usize) -> (C, Option)> ZipperValuesAt + for DependentProductZipperG<'trie, PrimaryZ, SecondaryZ, V, C, F> + where + V: Clone + Send + Sync, + PrimaryZ: ZipperMoving + ZipperPath + ZipperValuesAt, + SecondaryZ: ZipperMoving + ZipperPath + ZipperValuesAt, +{ fn val_at>(&self, path: K) -> Option<&V> { if let Some(idx) = self.factor_idx(true) { self.secondary[idx].val_at(path) diff --git a/src/empty_zipper.rs b/src/empty_zipper.rs index 16860fc1..99917802 100644 --- a/src/empty_zipper.rs +++ b/src/empty_zipper.rs @@ -90,6 +90,9 @@ impl ZipperIteration for EmptyZipper { impl ZipperValues for EmptyZipper { fn val(&self) -> Option<&V> { None } +} + +impl ZipperValuesAt for EmptyZipper { fn val_at>(&self, _path: K) -> Option<&V> { None } } diff --git a/src/experimental/zipper_algebra.rs b/src/experimental/zipper_algebra.rs index 7efc82ab..986660cd 100644 --- a/src/experimental/zipper_algebra.rs +++ b/src/experimental/zipper_algebra.rs @@ -2854,7 +2854,7 @@ mod zipper_algebra_poly { use pathmap_derive::PolyZipperExplicit; #[derive(PolyZipperExplicit)] - #[poly_zipper_explicit(traits(ZipperMoving, ZipperValues, ZipperConcrete))] + #[poly_zipper_explicit(traits(ZipperMoving, ZipperValues, ZipperValuesAt, ZipperConcrete))] pub(super) enum SomeMutRefZ<'a, 'trie, 'path, V: Clone + Send + Sync + Unpin, A: Allocator> { RZ(&'a mut ReadZipperUntracked<'trie, 'path, V, A>), RZT(&'a mut ReadZipperTracked<'trie, 'path, V, A>), diff --git a/src/overlay_zipper.rs b/src/overlay_zipper.rs index 96b847e8..211c8aa3 100644 --- a/src/overlay_zipper.rs +++ b/src/overlay_zipper.rs @@ -19,7 +19,7 @@ use arrayvec::ArrayVec; use fast_slice_utils::find_prefix_overlap; use crate::utils::{BitMask, ByteMask}; -use crate::zipper::{Zipper, ZipperMoving, ZipperPath, PathObserver, ZipperIteration, ZipperValues}; +use crate::zipper::{Zipper, ZipperMoving, ZipperPath, PathObserver, ZipperIteration, ZipperValues, ZipperValuesAt}; /// Zipper that traverses a virtual trie formed by fusing the tries of two other zippers pub struct OverlayZipper @@ -102,6 +102,15 @@ impl ZipperValues fn val(&self) -> Option<&OutV> { (self.mapping)(self.a.val(), self.b.val()) } +} + +impl ZipperValuesAt + for OverlayZipper + where + AZipper: ZipperValuesAt, + BZipper: ZipperValuesAt, + Mapping: for<'a> Fn(Option<&'a AV>, Option<&'a BV>) -> Option<&'a OutV>, +{ fn val_at>(&self, path: K) -> Option<&OutV> { (self.mapping)(self.a.val_at(&path), self.b.val_at(&path)) } @@ -441,6 +450,23 @@ mod tests { } ); + zipper_moving_tests::zipper_val_at_tests!(overlay_zipper, + |keys: &[&[u8]]| { + let cutoff = keys.len() / 3 * 2; + // eprintln!("keys={:?}", &keys); + eprintln!("a_keys={:?}\nb_keys={:?}", &keys[..cutoff], &keys[cutoff..]); + let a = keys[..cutoff].into_iter().map(|k| (k, ())).collect::>(); + let b = keys[cutoff..].into_iter().map(|k| (k, ())).collect::>(); + (a, b) + }, + |trie: &mut (PathMap<()>, PathMap<()>), path: &[u8]| -> OZ<'_, ()> { + OverlayZipper::new( + trie.0.read_zipper_at_path(path), + trie.1.read_zipper_at_path(path), + ) + } + ); + zipper_iteration_tests::zipper_iteration_tests!(overlay_zipper, |keys: &[&[u8]]| { let cutoff = keys.len() / 3 * 2; diff --git a/src/path_tracker.rs b/src/path_tracker.rs index e86ca669..c646c31f 100644 --- a/src/path_tracker.rs +++ b/src/path_tracker.rs @@ -1,9 +1,7 @@ use crate::{ utils::ByteMask, zipper::{ - PathObserver, Zipper, ZipperAbsolutePath, ZipperMoving, ZipperIteration, - ZipperPath, ZipperPathBuffer, ZipperValues, - ZipperReadOnlyValues, ZipperReadOnlyConditionalValues, + PathObserver, Zipper, ZipperAbsolutePath, ZipperIteration, ZipperMoving, ZipperPath, ZipperPathBuffer, ZipperReadOnlyConditionalValues, ZipperReadOnlyValues, ZipperValues, ZipperValuesAt }, }; @@ -185,6 +183,9 @@ impl ZipperAbsolutePath for PathTracker { impl, V> ZipperValues for PathTracker { fn val(&self) -> Option<&V> { self.zipper.val() } +} + +impl, V> ZipperValuesAt for PathTracker { fn val_at>(&self, path: K) -> Option<&V> { self.zipper.val_at(path) } } diff --git a/src/poly_zipper.rs b/src/poly_zipper.rs index 4655cec0..1675a187 100644 --- a/src/poly_zipper.rs +++ b/src/poly_zipper.rs @@ -109,6 +109,15 @@ mod tests { } ); + crate::zipper::zipper_moving_tests::zipper_val_at_tests!(poly_zipper_pm, + |keys: &[&[u8]]| { + keys.iter().map(|k| (k, ())).collect::>() + }, + |btm: &mut PathMap<()>, path: &[u8]| -> _ { + TestPolyZipper::PathMapU(btm.read_zipper_at_path(path)) + } + ); + crate::zipper::zipper_iteration_tests::zipper_iteration_tests!(poly_zipper_pm, |keys: &[&[u8]]| { keys.iter().map(|k| (k, ())).collect::>() @@ -129,6 +138,17 @@ mod tests { } ); + #[cfg(feature = "arena_compact")] + crate::zipper::zipper_moving_tests::zipper_val_at_tests!(poly_zipper_act, + |keys: &[&[u8]]| { + let btm = keys.iter().map(|k| (k, ())).collect::>(); + ACTVec::from_zipper(btm.read_zipper(), |()| 0) + }, + |act: &mut ACTVec, path: &[u8]| -> _ { + TestPolyZipper::ACTVecPrefix(PrefixZipper::new(&[], act.read_zipper_at_path(path))) + } + ); + #[cfg(feature = "arena_compact")] crate::zipper::zipper_iteration_tests::zipper_iteration_tests!(poly_zipper_act, |keys: &[&[u8]]| { @@ -143,7 +163,7 @@ mod tests { // ====================================================================================== // Cocktail of recursive zipper madness #[derive(PolyZipperExplicit)] - #[poly_zipper_explicit(traits(Zipper, ZipperValues, ZipperMoving, ZipperPath, ZipperIteration))] + #[poly_zipper_explicit(traits(Zipper, ZipperValues, ZipperValuesAt, ZipperMoving, ZipperPath, ZipperIteration))] pub enum ExprFactor<'trie, V: Clone + Send + Sync + Unpin + 'static = ()> { Specific(ReadZipperOwned), Generic(PrefixZipper<'trie, @@ -164,4 +184,13 @@ mod tests { ExprFactor::Specific(btm.clone().into_read_zipper(path)) } ); + + crate::zipper::zipper_moving_tests::zipper_val_at_tests!(recursive_zipper_madness, + |keys: &[&[u8]]| { + keys.iter().map(|k| (k, ())).collect::>() + }, + |btm: &mut PathMap<()>, path: &[u8]| -> _ { + ExprFactor::Specific(btm.clone().into_read_zipper(path)) + } + ); } diff --git a/src/prefix_zipper.rs b/src/prefix_zipper.rs index e9353594..5bd2324f 100644 --- a/src/prefix_zipper.rs +++ b/src/prefix_zipper.rs @@ -254,6 +254,12 @@ impl<'prefix, Z, V> ZipperValues for PrefixZipper<'prefix, Z> } self.source.val() } +} + +impl<'prefix, Z, V> ZipperValuesAt for PrefixZipper<'prefix, Z> + where + Z: ZipperValuesAt +{ fn val_at>(&self, path: K) -> Option<&V> { let path = self.adjust_lookup_path(path.as_ref())?; self.source.val_at(path) @@ -759,6 +765,7 @@ mod tests { use crate::zipper::ZipperPath; use crate::zipper::ZipperReadOnlyValues; use crate::zipper::ZipperValues; + use crate::zipper::ZipperValuesAt; //The whole prefix is the root prefix, so these run the shared suites against a `PrefixZipper` //whose focus begins in the source diff --git a/src/product_zipper.rs b/src/product_zipper.rs index 83bb67f7..9f2be3e9 100644 --- a/src/product_zipper.rs +++ b/src/product_zipper.rs @@ -316,6 +316,9 @@ impl<'trie, V: Clone + Send + Sync + Unpin + 'trie, A: Allocator + 'trie> Zipper fn val(&self) -> Option<&V> { unsafe{ self.z.get_val() } } +} + +impl<'trie, V: Clone + Send + Sync + Unpin + 'trie, A: Allocator + 'trie> ZipperValuesAt for ProductZipper<'_, 'trie, V, A> { fn val_at>(&self, path: K) -> Option<&V> { unsafe{ self.z.get_val_at(path) } } @@ -571,6 +574,15 @@ impl<'trie, PrimaryZ, SecondaryZ, V> ZipperValues self.primary.val() } } +} + +impl<'trie, PrimaryZ, SecondaryZ, V> ZipperValuesAt + for ProductZipperG<'trie, PrimaryZ, SecondaryZ, V> + where + V: Clone + Send + Sync, + PrimaryZ: ZipperMoving + ZipperPath + ZipperValuesAt, + SecondaryZ: ZipperMoving + ZipperPath + ZipperValuesAt, +{ fn val_at>(&self, path: K) -> Option<&V> { if let Some(idx) = self.factor_idx(true) { self.secondary[idx].val_at(path) @@ -907,6 +919,7 @@ impl ZipperMoving for OneFactor { zipper_impl_lens!(Zipper impl ZipperPath for OneFactor { zipper_impl_lens!(ZipperPath self => self.z); } impl ZipperIteration for OneFactor { zipper_impl_lens!(ZipperIteration self => self.z); } impl > ZipperValues for OneFactor { zipper_impl_lens!(ZipperValues self => self.z); } +impl > ZipperValuesAt for OneFactor { zipper_impl_lens!(ZipperValuesAt self => self.z); } impl > ZipperForking for OneFactor { type ReadZipperT<'a> = Z::ReadZipperT<'a> where Z: 'a; zipper_impl_lens!(ZipperForking self => self.z); } impl > ZipperSubtries for OneFactor { zipper_impl_lens!(ZipperSubtries self => self.z); } impl > ZipperInfallibleSubtries for OneFactor { zipper_impl_lens!(ZipperInfallibleSubtries self => self.z); } @@ -1913,6 +1926,16 @@ mod tests { ProductZipper::new::<_, TrieRef<()>, _>(btm.read_zipper_at_path(path), []) }); + crate::zipper::zipper_moving_tests::zipper_val_at_tests!(product_zipper, + |keys: &[&[u8]]| { + let mut btm = PathMap::new(); + keys.iter().for_each(|k| { btm.set_val_at(k, ()); }); + btm + }, + |btm: &mut PathMap<()>, path: &[u8]| -> _ { + ProductZipper::new::<_, TrieRef<()>, _>(btm.read_zipper_at_path(path), []) + }); + crate::zipper::zipper_iteration_tests::zipper_iteration_tests!(product_zipper, |keys: &[&[u8]]| { let mut btm = PathMap::new(); @@ -1933,6 +1956,16 @@ mod tests { ProductZipperG::new::<[ReadZipperUntracked<()>; 0]>(btm.read_zipper_at_path(path), []) }); + crate::zipper::zipper_moving_tests::zipper_val_at_tests!(product_zipper_generic, + |keys: &[&[u8]]| { + let mut btm = PathMap::new(); + keys.iter().for_each(|k| { btm.set_val_at(k, ()); }); + btm + }, + |btm: &mut PathMap<()>, path: &[u8]| -> _ { + ProductZipperG::new::<[ReadZipperUntracked<()>; 0]>(btm.read_zipper_at_path(path), []) + }); + crate::zipper::zipper_iteration_tests::zipper_iteration_tests!(product_zipper_generic, |keys: &[&[u8]]| { let mut btm = PathMap::new(); diff --git a/src/trie_ref.rs b/src/trie_ref.rs index 7aade28e..547e5909 100644 --- a/src/trie_ref.rs +++ b/src/trie_ref.rs @@ -266,6 +266,9 @@ impl ZipperValues for TrieRefBo fn val(&self) -> Option<&V> { self.get_val() } +} + +impl ZipperValuesAt for TrieRefBorrowed<'_, V, A> { fn val_at>(&self, path: K) -> Option<&V> { self.get_val_at(path) } @@ -646,6 +649,9 @@ impl ZipperValues for TrieRefOw None } } +} + +impl ZipperValuesAt for TrieRefOwned { fn val_at>(&self, path: K) -> Option<&V> { if self.is_valid() { TrieRefBorrowed::new_with_key_and_path_in( @@ -844,6 +850,9 @@ impl ZipperValues for TrieRef<' TrieRef::Owned(trie_ref) => trie_ref.val(), } } +} + +impl ZipperValuesAt for TrieRef<'_, V, A> { fn val_at>(&self, path: K) -> Option<&V> { match self { TrieRef::Borrowed(trie_ref) => trie_ref.val_at(path), @@ -1138,7 +1147,7 @@ mod tests { #[test] fn trie_ref_val_at_test() { - fn assert_val_at>(trie_ref: T) { + fn assert_val_at>(trie_ref: T) { assert_eq!(trie_ref.val(), None); assert_eq!(trie_ref.val_at(b"root:a:new_a"), Some(&10)); assert_eq!(trie_ref.val_at(b"root:a:nested:deep"), Some(&11)); diff --git a/src/write_zipper.rs b/src/write_zipper.rs index f2d1edb1..66d39cd0 100644 --- a/src/write_zipper.rs +++ b/src/write_zipper.rs @@ -399,6 +399,9 @@ impl<'a, V: Clone + Send + Sync + Unpin, A: Allocator + 'a> Zipper for WriteZipp impl<'a, V: Clone + Send + Sync + Unpin, A: Allocator + 'a> ZipperValues for WriteZipperTracked<'a, '_, V, A>{ fn val(&self) -> Option<&V> { self.z.val() } +} + +impl<'a, V: Clone + Send + Sync + Unpin, A: Allocator + 'a> ZipperValuesAt for WriteZipperTracked<'a, '_, V, A>{ fn val_at>(&self, path: K) -> Option<&V> { self.z.val_at(path) } } @@ -564,6 +567,9 @@ impl<'a, V: Clone + Send + Sync + Unpin, A: Allocator + 'a> Zipper for WriteZipp impl<'a, V: Clone + Send + Sync + Unpin, A: Allocator + 'a> ZipperValues for WriteZipperUntracked<'a, '_, V, A> { fn val(&self) -> Option<&V> { self.z.val() } +} + +impl<'a, V: Clone + Send + Sync + Unpin, A: Allocator + 'a> ZipperValuesAt for WriteZipperUntracked<'a, '_, V, A> { fn val_at>(&self, path: K) -> Option<&V> { self.z.val_at(path) } } @@ -736,6 +742,7 @@ impl Clone for WriteZipp impl Zipper for WriteZipperOwned { zipper_impl_lens!(Zipper self => self.z); } impl ZipperValues for WriteZipperOwned { zipper_impl_lens!(ZipperValues self => self.z); } +impl ZipperValuesAt for WriteZipperOwned { zipper_impl_lens!(ZipperValuesAt self => self.z); } impl ZipperInfallibleSubtries for WriteZipperOwned { zipper_impl_lens!(ZipperInfallibleSubtries 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); } @@ -6048,6 +6055,16 @@ mod tests { btm.write_zipper_at_path(path) }); + crate::zipper::zipper_moving_tests::zipper_val_at_tests!(write_zipper, + |keys: &[&[u8]]| { + let mut btm = PathMap::new(); + keys.iter().for_each(|k| { btm.set_val_at(k, ()); }); + btm + }, + |btm: &mut PathMap<()>, path: &[u8]| -> WriteZipperUntracked<(), GlobalAlloc> { + btm.write_zipper_at_path(path) + }); + crate::zipper::zipper_iteration_tests::zipper_iteration_tests!(write_zipper_owned, |keys: &[&[u8]]| { let mut btm = PathMap::new(); diff --git a/src/zipper.rs b/src/zipper.rs index 1e1a7757..8e62b57a 100644 --- a/src/zipper.rs +++ b/src/zipper.rs @@ -62,12 +62,6 @@ pub trait ZipperValues { /// will provide a longer-lived reference to the value. fn val(&self) -> Option<&V>; - /// Returns a refernce to the value at `path`, relative to the zipper's focus, or `None` if there is no value - /// - /// If you have a zipper type that implements [ZipperReadOnlyValues] then [ZipperReadOnlyValues::get_val_at] - /// will provide a longer-lived reference to the value. - fn val_at>(&self, path: K) -> Option<&V>; - /// Deprecated alias for [ZipperValues::val] #[deprecated] //GOAT-old-names fn value(&self) -> Option<&V> { @@ -75,6 +69,22 @@ pub trait ZipperValues { } } +/// Provides random access to values below the zipper's current focus. +/// +/// Unlike [ZipperValues::val], this capability requires the zipper to be able +/// to return a stable reference to a value at an arbitrary relative path. +/// Computed or virtual zippers may therefore implement [ZipperValues] without +/// implementing this trait. +pub trait ZipperValuesAt: ZipperValues { + /// Returns a reference to the value at `path`, relative to the zipper's + /// focus, or `None` if there is no value. + /// + /// If you have a zipper type that implements [ZipperReadOnlyValues] then + /// [ZipperReadOnlyValues::get_val_at] will provide a longer-lived reference + /// to the value. + fn val_at>(&self, path: K) -> Option<&V>; +} + /// Method to fork a read zipper from the parent zipper pub trait ZipperForking { /// The read-zipper type returned from [fork_read_zipper](ZipperForking::fork_read_zipper) @@ -842,7 +852,7 @@ impl<'a, V: Clone + Send + Sync, A: Allocator> OpaqueAbstractNodeRef<'a, V, A> { pub struct OpaqueTrieNodeRef<'trie, V: Clone + Send + Sync, A: Allocator>(pub(crate) &'trie TrieNodeODRc); /// Similar to [ZipperSubtries], but with the stronger guarantee that subtrie access will be constant-time and won't fail -pub trait ZipperInfallibleSubtries: ZipperValues + Zipper { +pub trait ZipperInfallibleSubtries: ZipperValuesAt + Zipper { /// Returns a new [PathMap] containing everything below the zipper's focus fn make_map(&self) -> PathMap; @@ -1213,6 +1223,8 @@ macro_rules! zipper_impl_lens { }; (ZipperValues $s: ident => $e:expr) => { fn val(&$s) -> Option<&V> { $e.val() } + }; + (ZipperValuesAt $s: ident => $e:expr) => { fn val_at>(&$s, path: K) -> Option<&V> { $e.val_at(path) } }; (ZipperForking $s: ident => $e:expr) => { @@ -1295,6 +1307,7 @@ impl ZipperMoving for Box { zipper_impl_lens!(ZipperMoving impl ZipperPath for Box { zipper_impl_lens!(ZipperPath self => (**self)); } impl ZipperIteration for Box { zipper_impl_lens!(ZipperIteration self => (**self)); } impl > ZipperValues for Box { zipper_impl_lens!(ZipperValues self => (**self)); } +impl > ZipperValuesAt for Box { zipper_impl_lens!(ZipperValuesAt self => (**self)); } impl > ZipperForking for Box { type ReadZipperT<'a> = Z::ReadZipperT<'a> where Self: 'a; zipper_impl_lens!(ZipperForking self => (**self)); } impl > ZipperSubtries for Box { zipper_impl_lens!(ZipperSubtries self => (**self)); } impl > ZipperInfallibleSubtries for Box { zipper_impl_lens!(ZipperInfallibleSubtries self => (**self)); } @@ -1313,6 +1326,7 @@ impl ZipperMoving for &mut Z { zipper_impl_lens!(ZipperMoving impl ZipperPath for &mut Z { zipper_impl_lens!(ZipperPath self => (**self)); } impl ZipperIteration for &mut Z { zipper_impl_lens!(ZipperIteration self => (**self)); } impl > ZipperValues for &mut Z { zipper_impl_lens!(ZipperValues self => (**self)); } +impl > ZipperValuesAt for &mut Z { zipper_impl_lens!(ZipperValuesAt self => (**self)); } impl > ZipperForking for &mut Z { type ReadZipperT<'a> = Z::ReadZipperT<'a> where Self: 'a; zipper_impl_lens!(ZipperForking self => (**self)); } impl > ZipperSubtries for &mut Z { zipper_impl_lens!(ZipperSubtries self => (**self)); } impl > ZipperInfallibleSubtries for &mut Z { zipper_impl_lens!(ZipperInfallibleSubtries self => (**self)); } @@ -1365,6 +1379,7 @@ impl Drop for ReadZipperTracked<'_, '_, V, impl Zipper for ReadZipperTracked<'_, '_, V, A> { zipper_impl_lens!(Zipper self => self.z); } impl ZipperValues for ReadZipperTracked<'_, '_, V, A>{ zipper_impl_lens!(ZipperValues self => self.z); } +impl ZipperValuesAt for ReadZipperTracked<'_, '_, V, A>{ zipper_impl_lens!(ZipperValuesAt self => self.z); } impl ZipperSubtries for ReadZipperTracked<'_, '_, V, A> { zipper_impl_lens!(ZipperSubtries self => self.z); } impl ZipperInfallibleSubtries for ReadZipperTracked<'_, '_, V, A> { zipper_impl_lens!(ZipperInfallibleSubtries self => self.z); } impl<'trie, V: Clone + Send + Sync + Unpin + 'trie, A: Allocator + 'trie> ZipperMoving for ReadZipperTracked<'trie, '_, V, A> { zipper_impl_lens!(ZipperMoving self => self.z); } @@ -1454,6 +1469,7 @@ pub struct ReadZipperUntracked<'a, 'path, V: Clone + Send + Sync, A: Allocator = impl Zipper for ReadZipperUntracked<'_, '_, V, A> { zipper_impl_lens!(Zipper self => self.z); } impl ZipperValues for ReadZipperUntracked<'_, '_, V, A> { zipper_impl_lens!(ZipperValues self => self.z); } +impl ZipperValuesAt for ReadZipperUntracked<'_, '_, V, A> { zipper_impl_lens!(ZipperValuesAt self => self.z); } impl<'trie, V: Clone + Send + Sync + Unpin + 'trie, A: Allocator + 'trie> ZipperPathBuffer for ReadZipperUntracked<'trie, '_, V, A> { zipper_impl_lens!(ZipperPathBuffer self => self.z); } impl<'trie, V: Clone + Send + Sync + Unpin + 'trie, A: Allocator + 'trie> ZipperIteration for ReadZipperUntracked<'trie, '_, V, A> { zipper_impl_lens!(ZipperIteration self => self.z); } impl<'trie, V: Clone + Send + Sync + Unpin + 'trie, A: Allocator + 'trie> ZipperReadOnlyConditionalIteration<'trie, V> for ReadZipperUntracked<'trie, '_, V, A> { } @@ -1630,6 +1646,9 @@ impl<'trie, V: Clone + Send + Sync + Unpin + 'trie, A: Allocator + 'trie> Zipper impl ZipperValues for ReadZipperOwned { fn val(&self) -> Option<&V> { unsafe{ self.z.get_val() } } +} + +impl ZipperValuesAt for ReadZipperOwned { fn val_at>(&self, path: K) -> Option<&V> { unsafe{ self.z.get_val_at(path) } } } @@ -1868,6 +1887,9 @@ pub(crate) mod read_zipper_core { impl ZipperValues for ReadZipperCore<'_, '_, V, A> { fn val(&self) -> Option<&V> { unsafe{ self.get_val() } } + } + + impl ZipperValuesAt for ReadZipperCore<'_, '_, V, A> { fn val_at>(&self, path: K) -> Option<&V> { unsafe{ self.get_val_at(path) } } } @@ -3677,7 +3699,17 @@ pub(crate) mod zipper_moving_tests { let mut temp_store = $read_keys(crate::zipper::zipper_moving_tests::ZIPPER_BYTES_ITER_TEST5_KEYS); crate::zipper::zipper_moving_tests::run_test(&mut temp_store, $make_z, &[], crate::zipper::zipper_moving_tests::zipper_byte_iter_test5) } + } + } + } + pub(crate) use zipper_moving_tests; + /// `$ident` is a unique identifier for the zipper, so the generated tests don't collide + /// `$read_keys` is a function that will create a store containing all paths, from which a zipper can be created + /// `$make_z` is a function that will create a zipper from a slice of paths + macro_rules! zipper_val_at_tests { + ($z_name:ident, $read_keys:expr, $make_z:expr)=>{ + paste::paste! { #[test] fn [<$z_name _zipper_val_at_test>]() { let mut temp_store = $read_keys(crate::zipper::zipper_moving_tests::ZIPPER_VAL_AT_TEST_KEYS); @@ -3694,7 +3726,7 @@ pub(crate) mod zipper_moving_tests { } } } - pub(crate) use zipper_moving_tests; + pub(crate) use zipper_val_at_tests; /// Internal method to provide a lifetime bound on the macro arguments to the test macro pub fn run_test<'a, T: 'a + ZipperMoving, Store>( @@ -4396,7 +4428,7 @@ pub(crate) mod zipper_moving_tests { b"romulus", b"rubens", b"ruber", b"rubicon", b"rubicundus", b"rom'i", ]; - pub fn zipper_val_at_test>(mut zipper: Z) { + pub fn zipper_val_at_test>(mut zipper: Z) { assert_eq!(zipper.val_at(b""), None); assert_eq!(zipper.val_at(b"roman"), Some(&())); assert_eq!(zipper.val_at(b"romane"), Some(&())); @@ -4439,7 +4471,7 @@ pub(crate) mod zipper_moving_tests { key } - pub fn zipper_val_at_long_path_test>(mut zipper: Z) { + pub fn zipper_val_at_long_path_test>(mut zipper: Z) { let long_key = zipper_val_at_long_path_test_key(); let relative_long_suffix = &long_key[3..]; let almost_full_suffix = &relative_long_suffix[..relative_long_suffix.len()-1]; @@ -5145,6 +5177,16 @@ mod tests { btm.read_zipper_at_path(path) }); + super::zipper_moving_tests::zipper_val_at_tests!(read_zipper, + |keys: &[&[u8]]| { + let mut btm = PathMap::new(); + keys.iter().for_each(|k| { btm.set_val_at(k, ()); }); + btm + }, + |btm: &mut PathMap<()>, path: &[u8]| -> ReadZipperUntracked<()> { + btm.read_zipper_at_path(path) + }); + super::zipper_iteration_tests::zipper_iteration_tests!(read_zipper, |keys: &[&[u8]]| { let mut btm = PathMap::new(); @@ -5165,6 +5207,16 @@ mod tests { core::mem::take(btm).into_read_zipper(path) }); + super::zipper_moving_tests::zipper_val_at_tests!(read_zipper_owned, + |keys: &[&[u8]]| { + let mut btm = PathMap::new(); + keys.iter().for_each(|k| { btm.set_val_at(k, ()); }); + btm + }, + |btm: &mut PathMap<()>, path: &[u8]| -> ReadZipperOwned<()> { + core::mem::take(btm).into_read_zipper(path) + }); + super::zipper_iteration_tests::zipper_iteration_tests!(read_zipper_owned, |keys: &[&[u8]]| { let mut btm = PathMap::new(); From 743afd9a5a0d04a375158f187ba37fc24b016729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Rze=C5=BAnicki?= Date: Mon, 7 Sep 2026 10:39:18 +0200 Subject: [PATCH 02/11] Introduce SubtractZipper --- src/lib.rs | 1 + src/subtract_zipper.rs | 1519 ++++++++++++++++++++++++++++++++++++++++ src/utils/mod.rs | 16 + src/zipper.rs | 1 + 4 files changed, 1537 insertions(+) create mode 100644 src/subtract_zipper.rs diff --git a/src/lib.rs b/src/lib.rs index 771d569d..227a63c1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -134,6 +134,7 @@ mod empty_zipper; mod prefix_zipper; mod overlay_zipper; mod dependent_zipper; +mod subtract_zipper; mod path_tracker; mod trie_ref; mod dense_byte_node; diff --git a/src/subtract_zipper.rs b/src/subtract_zipper.rs new file mode 100644 index 00000000..3eaad688 --- /dev/null +++ b/src/subtract_zipper.rs @@ -0,0 +1,1519 @@ +use std::{cell::Cell, num::NonZeroUsize}; + +use crate::{ + ring::{self, AlgebraicResult, DistributiveLattice, DistributiveLatticeRef}, + utils::{BitMask, ByteMask}, + zipper::{Zipper, ZipperAbsolutePath, ZipperIteration, ZipperMoving, ZipperPath, ZipperValues}, +}; + +pub struct SubtractZipper { + lhs: A, + rhs: B, + + // To maintain the invariant: + // lhs.path()[lhs_root_depth..] + // == self.path() + lhs_root_depth: usize, + + child_mask: ByteMask, + val: CachedVal, + val_count: Cell>, +} + +enum CachedVal { + None, + Lhs, + Owned(V), +} + +enum DescendState { + /// No value at the focus and exactly one surviving child. + Continue(u8), + + /// A value, leaf, or branch was encountered. + Stop(DescendStop), +} + +enum DescendStop { + Value, + Branch(u8), // first surviving child + Leaf, + ByteLimit, +} + +impl SubtractZipper +where + V: DistributiveLattice + Clone, + A: ZipperMoving + ZipperValues, + B: ZipperMoving + ZipperValues, +{ + pub fn new(lhs: A, rhs: B) -> Self { + let lhs_root_depth = lhs.depth(); + let mut this = Self { + lhs, + rhs, + lhs_root_depth, + child_mask: ByteMask::default(), + val: CachedVal::None, + val_count: Cell::new(None), + }; + + this.refresh(); + this + } + + /// Rebuilds the cached virtual state for the current backing-zipper focus. + /// + /// This recomputes the value and child topology of the materialized + /// subtraction `lhs - rhs` and invalidates the cached value count. + fn refresh(&mut self) { + self.val_count.set(None); + + if !self.lhs.path_exists() { + self.child_mask = ByteMask::default(); + self.val = CachedVal::None; + return; + } + + if !self.rhs.path_exists() { + self.child_mask = self.lhs.child_mask(); + self.val = CachedVal::Lhs; + return; + } + + self.val = self.compute_val(); + self.child_mask = self.compute_child_mask(); + } + + /// Descends both backing zippers by one byte without refreshing the cached + /// virtual state. + /// + /// After this call, cached fields such as `path_exists`, `child_mask`, and + /// `val` are stale until `refresh()` is called or the movement is undone. + #[inline] + fn descend_to_byte_raw(&mut self, byte: u8) { + self.lhs.descend_to_byte(byte); + self.rhs.descend_to_byte(byte); + } + + /// Ascends both backing zippers by one byte without refreshing the cached + /// virtual state. + /// + /// Must not be called at the virtual root. Cached virtual state remains stale + /// until `refresh()` is called or the movement is undone. + #[inline] + fn ascend_byte_raw(&mut self) -> bool { + debug_assert!( + self.lhs.depth() > self.lhs_root_depth, + "SubtractZipper attempted to ascend above its root" + ); + + let lhs = self.lhs.ascend_byte(); + let rhs = self.rhs.ascend_byte(); + + debug_assert_eq!(lhs, rhs); + lhs + } + + /// Returns whether the child `at` contains anything in the materialized + /// subtraction. + /// + /// Both backing zippers are temporarily descended into the child and restored + /// to their original focus before this method returns. + #[inline] + fn subtree_survives(&mut self, at: u8) -> bool { + self.descend_to_byte_raw(at); + let survives = subtree_has_difference::(&mut self.lhs, &mut self.rhs); + self.ascend_byte_raw(); + + survives + } + + fn compute_child_mask(&mut self) -> ByteMask { + if !self.lhs.path_exists() { + return ByteMask::default(); + } + + // Nothing below this RHS position can exist either. + if !self.rhs.path_exists() { + return self.lhs.child_mask(); + } + + let mut out = self.lhs.child_mask(); + + for byte in (out & self.rhs.child_mask()).iter() { + // Both tries contain this branch. Look below it to determine + // whether anything survives. + if !self.subtree_survives(byte) { + out.clear_bit(byte); + } + } + + out + } + + fn compute_val(&self) -> CachedVal { + let lhs_val = self.lhs.val(); + match lhs_val.psubtract(&self.rhs.val()) { + AlgebraicResult::None | AlgebraicResult::Element(None) => CachedVal::None, + AlgebraicResult::Identity(mask) if mask == ring::SELF_IDENT => CachedVal::Lhs, + AlgebraicResult::Element(Some(v)) => CachedVal::Owned(v), + _ => unreachable!(), + } + } + + /// Returns whether the current focus has a surviving child other than + /// `cur_byte`. + /// + /// `cur_byte` is assumed to be a child of the materialized subtraction. + fn has_surviving_sibling(&mut self, cur_byte: u8) -> bool { + let mut candidates = self.lhs.child_mask(); + + if !self.rhs.path_exists() { + // The whole LHS subtree survives, so any other LHS child is enough. + return candidates.count_bits() > 1; + } + + candidates.clear_bit(cur_byte); + let rhs_mask = self.rhs.child_mask(); + + // Any LHS-only sibling survives wholesale. + let lhs_only = (candidates ^ rhs_mask) & candidates; + if !lhs_only.is_empty_mask() { + return true; + } + + // Shared siblings need to be inspected. + let common = candidates & rhs_mask; + common.iter().any(|byte| self.subtree_survives(byte)) + } + + /// Classifies the current virtual focus for optimized downward traversal. + /// + /// Returns `DescendState::Continue(byte)` only when there is no surviving value and exactly + /// one surviving child. Otherwise returns the reason traversal must stop. + fn current_descend_state(&mut self) -> DescendState { + if !self.rhs.path_exists() { + // Once RHS no longer contains the current path, subtraction has no + // further effect below this point: the virtual subtree is exactly LHS. + if self.lhs.is_val() { + return DescendState::Stop(DescendStop::Value); + } + + let mask = self.lhs.child_mask(); + + if let Some(first_byte) = mask.indexed_bit::(0) { + if mask.next_bit(first_byte).is_some() { + return DescendState::Stop(DescendStop::Branch(first_byte)); + } + + return DescendState::Continue(first_byte); + } else { + return DescendState::Stop(DescendStop::Leaf); + } + } + + if value_survives::(&self.lhs, &self.rhs) { + // A surviving value at the current focus is a stopping point for + // descend_until(), regardless of the number of children below it. + return DescendState::Stop(DescendStop::Value); + } + + match self.first_surviving_child() { + Some(byte) => { + if self.surviving_sibling::(byte).is_some() { + DescendState::Stop(DescendStop::Branch(byte)) + } else { + DescendState::Continue(byte) + } + } + None => DescendState::Stop(DescendStop::Leaf), + } + } + + /// Descends through a virtual unary path using raw backing-zipper movement. + /// + /// `byte` must be a known surviving child of the current focus. Traversal + /// stops at a value, branch, leaf, or when the optional byte budget is + /// exhausted. + /// + /// This method does not refresh cached virtual state. + fn descend_to_next_stop( + &mut self, + mut byte: u8, + mut remaining: Option, + ) -> DescendStop { + loop { + self.descend_to_byte_raw(byte); + + if let Some(left) = remaining { + remaining = NonZeroUsize::new(left.get() - 1); + + if remaining.is_none() { + return DescendStop::ByteLimit; + } + } + + match self.current_descend_state() { + DescendState::Continue(next_byte) => { + byte = next_byte; + } + DescendState::Stop(stop) => { + return stop; + } + } + } + } + + #[inline] + fn descend_until(&mut self, max_bytes: Option) -> bool { + if let Some(byte) = self.child_mask.indexed_bit::(0) { + if self.child_mask.next_bit(byte).is_some() { + return false; + } + let _ = self.descend_to_next_stop(byte, max_bytes); + self.refresh(); + true + } else { + false + } + } + + /// Finds the next surviving sibling of `cur_byte` in the requested direction. + /// + /// Shared LHS/RHS children are inspected lazily and skipped when their + /// materialized subtree difference is empty. + fn surviving_sibling(&mut self, cur_byte: u8) -> Option { + let lhs_mask = self.lhs.child_mask(); + + let mut candidate = if FORWARD { + lhs_mask.next_bit(cur_byte) + } else { + lhs_mask.prev_bit(cur_byte) + }; + + if !self.rhs.path_exists() { + return candidate; + } + + let rhs_mask = self.rhs.child_mask(); + + while let Some(byte) = candidate { + if !rhs_mask.test_bit(byte) || self.subtree_survives(byte) { + return candidate; + } + candidate = if FORWARD { + lhs_mask.next_bit(byte) + } else { + lhs_mask.prev_bit(byte) + }; + } + + candidate + } + + /// Returns the first child, in trie order, whose materialized subtraction + /// subtree is non-empty. + fn first_surviving_child(&mut self) -> Option { + let lhs_mask = self.lhs.child_mask(); + + if !self.rhs.path_exists() { + return lhs_mask.indexed_bit::(0); + } + + let rhs_mask = self.rhs.child_mask(); + lhs_mask + .iter() + .find(|&byte| !rhs_mask.test_bit(byte) || self.subtree_survives(byte)) + } + + /// Advances raw backing zippers to the next surviving subtree in DFS order, + /// without ascending above `base_idx`. + /// + /// Returns `false` after exhausting the search and leaving the zipper at + /// `base_idx`. + fn advance_to_next_subtree(&mut self, base_idx: usize) -> bool { + loop { + // Reaching the common root means the current DFS subtree has been + // exhausted and there is no later subtree to visit. + if self.lhs.depth() == self.lhs_root_depth + base_idx { + return false; + } + + let cur_byte = self.lhs.focus_byte().expect("path is below base_idx"); + + // Move to the parent without refreshing the virtual zipper state. + // Intermediate nodes are not externally observable during this search. + self.ascend_byte_raw(); + + // Continue DFS from the next surviving sibling, if one exists. + // surviving_sibling() accounts for branches removed by subtraction. + if let Some(byte) = self.surviving_sibling::(cur_byte) { + self.descend_to_byte_raw(byte); + return true; + } + + // No sibling survives at this level. Keep ascending until either a + // later subtree is found or the common root is reached. + } + } + + /// Searches in depth-first order for the first surviving path exactly `k` + /// bytes below `base_idx`. + /// + /// The zipper may be positioned anywhere below `base_idx` on entry. + /// Intermediate movement is raw; the virtual state is refreshed only when + /// a matching path is found. + fn seek_k_path(&mut self, base_idx: usize, k: usize) -> bool { + let target_idx = base_idx + k; + + loop { + // The first path encountered at the requested depth is the result, + // since traversal always prefers the first surviving child. + if self.lhs.depth() == self.lhs_root_depth + target_idx { + return true; + } + + // Continue depth-first through the first surviving child whenever + // possible. + if let Some(byte) = self.first_surviving_child() { + self.descend_to_byte_raw(byte); + continue; + } + + // This branch ended before reaching the requested depth. Backtrack + // until another surviving subtree can continue the DFS. + if !self.advance_to_next_subtree(base_idx) { + return false; + } + } + } +} + +#[inline] +fn value_survives(lhs: &A, rhs: &B) -> bool +where + V: DistributiveLattice + Clone, + A: ZipperValues, + B: ZipperValues, +{ + !matches!(lhs.val().psubtract(&rhs.val()), AlgebraicResult::None) +} + +/// Returns whether the materialized subtraction of the focused subtrees is +/// non-empty. +/// +/// Traverses only the portion shared by both tries and short-circuits as soon +/// as a surviving value or LHS-only branch is found. +/// +/// Both zippers are restored to their original focus before returning. +fn subtree_has_difference(lhs: &mut A, rhs: &mut B) -> bool +where + V: DistributiveLattice + Clone, + A: ZipperMoving + ZipperValues, + B: ZipperMoving + ZipperValues, +{ + let mut depth = 0; + let mut lhs_mask = lhs.child_mask(); + let mut rhs_mask = rhs.child_mask(); + 'descend: loop { + // Entire LHS-only branches survive. + // A value at this exact key survives. + if !((lhs_mask ^ rhs_mask) & lhs_mask).is_empty_mask() + || value_survives::(lhs, rhs) + { + let lhs_ascended = lhs.ascend(depth); + let rhs_ascended = rhs.ascend(depth); + + debug_assert!(lhs_ascended == depth); + debug_assert!(rhs_ascended == depth); + + return true; + } + + let mut combined_mask = lhs_mask & rhs_mask; + let mut next_common_byte = combined_mask.indexed_bit::(0); + 'node: 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); + + depth -= 1; + continue 'node; + } + } + } + } + + false +} + +/// Counts values in the materialized subtraction below the current focus. +/// +/// 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 +where + V: DistributiveLattice + Clone, + A: ZipperMoving + ZipperValues, + B: ZipperMoving + ZipperValues, +{ + if !lhs.path_exists() { + return 0; + } + + if !rhs.path_exists() { + return lhs.val_count(); + } + + 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 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); + + depth -= 1; + } + } + } + } + + count +} + +impl Zipper for SubtractZipper +where + A: Zipper, + B: Zipper, +{ + #[inline] + fn path_exists(&self) -> bool { + self.is_val() || !self.child_mask.is_empty_mask() + } + + #[inline] + fn is_val(&self) -> bool { + match self.val { + CachedVal::None => false, + CachedVal::Lhs => self.lhs.is_val(), + CachedVal::Owned(_) => true, + } + } + + #[inline] + fn child_count(&self) -> usize { + self.child_mask.count_bits() + } + + #[inline] + fn child_mask(&self) -> ByteMask { + self.child_mask + } +} + +impl ZipperValues for SubtractZipper +where + V: DistributiveLattice + Clone, + A: ZipperMoving + ZipperValues, + B: ZipperMoving + ZipperValues, +{ + #[inline] + fn val(&self) -> Option<&V> { + match self.val { + CachedVal::None => None, + CachedVal::Lhs => self.lhs.val(), + CachedVal::Owned(ref v) => Some(v), + } + } +} + +impl ZipperMoving for SubtractZipper +where + V: DistributiveLattice + Clone, + A: ZipperMoving + ZipperValues + Clone, + B: ZipperMoving + ZipperValues + Clone, +{ + #[inline] + fn depth(&self) -> usize { + self.lhs.depth() - self.lhs_root_depth + } + + #[inline] + fn focus_byte(&self) -> Option { + self.lhs.focus_byte() + } + + // #[inline] + // fn at_root(&self) -> bool { + // self.lhs.path().len() == self.lhs_root_depth + // } + + fn val_count(&self) -> usize { + if let Some(count) = self.val_count.get() { + return count; + } + + let mut lhs = self.lhs.clone(); + let mut rhs = self.rhs.clone(); + + let count = 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() { + return; + } + + self.lhs.descend_to(path); + self.rhs.descend_to(path); + + self.refresh(); + } + + fn ascend(&mut self, steps: usize) -> usize { + if steps == 0 { + return 0; + } + + let actual_steps = steps.min(self.depth()); + + self.lhs.ascend(actual_steps); + self.rhs.ascend(actual_steps); + self.refresh(); + + actual_steps + } + + fn ascend_until(&mut self) -> usize { + if self.at_root() { + return 0; + } + + let mut ascended = 0; + loop { + let cur_byte = self.focus_byte().expect("not at root"); + + self.ascend_byte_raw(); + ascended += 1; + + let stop = self.at_root() + || value_survives::(&self.lhs, &self.rhs) + || self.has_surviving_sibling(cur_byte); + + if stop { + self.refresh(); + return ascended; + } + } + } + + fn ascend_until_branch(&mut self) -> usize { + if self.at_root() { + return 0; + } + + let mut ascended = 0; + loop { + let cur_byte = self.focus_byte().expect("not at root"); + + self.ascend_byte_raw(); + ascended += 1; + + if self.at_root() || self.has_surviving_sibling(cur_byte) { + self.refresh(); + return ascended; + } + } + } + + fn reset(&mut self) { + let depth = self.depth(); + + if depth != 0 { + self.lhs.ascend(depth); + self.rhs.ascend(depth); + self.refresh(); + } + + debug_assert!(self.at_root()); + } + + fn descend_to_existing>(&mut self, k: K) -> usize { + let k = k.as_ref(); + + // The cached virtual child mask is valid at the initial focus, so the + // first step can be checked without inspecting the subtraction again. + if k.is_empty() || !self.child_mask.test_bit(k[0]) { + return 0; + } + + self.descend_to_byte_raw(k[0]); + let mut i = 1; + + while i < k.len() { + let byte = k[i]; + + // No such child in A => no such child in A - B. + if !self.lhs.child_mask().test_bit(byte) { + break; + } + + let rhs_gone = !self.rhs.child_mask().test_bit(byte); + + self.descend_to_byte_raw(byte); + + if rhs_gone { + i += 1; + + // An LHS-only child survives wholesale. From this point downward + // A - B is exactly A, so delegate the rest to the native LHS zipper. + let descended = self.lhs.descend_to_existing(&k[i..]); + self.rhs.descend_to(&k[i..i + descended]); + i += descended; + break; + } + + // Both sides contain the child structurally. It belongs to the + // materialized difference iff something survives below it. + if !subtree_has_difference::(&mut self.lhs, &mut self.rhs) { + self.ascend_byte_raw(); + break; + } + + i += 1; + } + + self.refresh(); + i + } + + fn descend_to_val>(&mut self, k: K) -> usize { + let k = k.as_ref(); + + // The cached virtual child mask is valid at the initial focus, so the + // first step can be checked without inspecting the subtraction again. + if k.is_empty() || !self.child_mask.test_bit(k[0]) { + return 0; + } + + self.descend_to_byte_raw(k[0]); + let mut i = 1; + + // If RHS disappeared on the first step, subtraction has no further + // effect below this point. Check the current LHS value first because + // descend_to_val() deliberately skips a value at its initial focus. + if !self.rhs.path_exists() { + if !self.lhs.is_val() { + let descended = self.lhs.descend_to_val(&k[i..]); + self.rhs.descend_to(&k[i..i + descended]); + i += descended; + } + + self.refresh(); + return i; + } + + // The first child is known to exist in the virtual trie from the cached + // child mask. If its value survives subtraction, we are already done. + if value_survives::(&self.lhs, &self.rhs) { + self.refresh(); + return i; + } + + while i < k.len() { + let byte = k[i]; + + // No such LHS child means there cannot be such a child in A - B. + if !self.lhs.child_mask().test_bit(byte) { + break; + } + + let rhs_gone = !self.rhs.child_mask().test_bit(byte); + + self.descend_to_byte_raw(byte); + + // An LHS-only child survives wholesale. From this point downward + // A - B is exactly A, so delegate the rest to the native LHS zipper. + if rhs_gone { + i += 1; + + // The newly reached focus itself may already contain a value. + // The native descend_to_val() would intentionally skip it. + if !self.lhs.is_val() { + let descended = self.lhs.descend_to_val(&k[i..]); + self.rhs.descend_to(&k[i..i + descended]); + i += descended; + } + + break; + } + + // A surviving value also proves that this virtual path exists, so + // avoid the more expensive subtree check in this case. + if value_survives::(&self.lhs, &self.rhs) { + i += 1; + break; + } + + // Both tries contain the structural child, but it exists in the + // materialized difference only if something survives below it. + if !subtree_has_difference::(&mut self.lhs, &mut self.rhs) { + self.ascend_byte_raw(); + break; + } + + i += 1; + } + + self.refresh(); + i + } + + fn descend_to_existing_byte(&mut self, k: u8) -> bool { + if !self.child_mask.test_bit(k) { + return false; + } + + self.descend_to_byte_raw(k); + self.refresh(); + true + } + + fn descend_until(&mut self) -> bool { + self.descend_until(None) + } + + fn descend_until_max_bytes(&mut self, max_bytes: usize) -> bool { + match NonZeroUsize::new(max_bytes) { + Some(limit) => self.descend_until(Some(limit)), + None => false, + } + } + + fn to_next_sibling_byte(&mut self) -> Option { + let cur_byte = self.focus_byte()?; + + // Move both backing zippers to the parent without refreshing the + // virtual state, which is only needed at the final focus. + self.ascend_byte_raw(); + + match self.surviving_sibling::(cur_byte) { + Some(byte) => { + self.descend_to_byte_raw(byte); + self.refresh(); + Some(byte) + } + + None => { + // Restore the original focus. Since we return to exactly the + // same node, the existing cached virtual state is still valid. + self.descend_to_byte_raw(cur_byte); + None + } + } + } + + fn to_prev_sibling_byte(&mut self) -> Option { + let cur_byte = self.focus_byte()?; + + // Move both backing zippers to the parent without refreshing the + // virtual state, which is only needed at the final focus. + self.ascend_byte_raw(); + + match self.surviving_sibling::(cur_byte) { + Some(byte) => { + self.descend_to_byte_raw(byte); + self.refresh(); + Some(byte) + } + + None => { + // Restore the original focus. Since we return to exactly the + // same node, the existing cached virtual state is still valid. + self.descend_to_byte_raw(cur_byte); + None + } + } + } + + fn to_next_step(&mut self) -> bool { + // If there is a child, DFS simply descends into the first one. + if let Some(byte) = self.child_mask.indexed_bit::(0) { + self.descend_to_byte_raw(byte); + self.refresh(); + return true; + } + + // We are at a leaf. Walk upwards without refreshing intermediate + // virtual nodes until a surviving next sibling is found. + loop { + let Some(cur_byte) = self.focus_byte() else { + return false; + }; + + self.ascend_byte_raw(); + + if let Some(byte) = self.surviving_sibling::(cur_byte) { + self.descend_to_byte_raw(byte); + self.refresh(); + return true; + } + + // No sibling at this parent. Unlike to_next_sibling_byte(), we do + // not restore the previous child: DFS is done with that subtree and + // continues ascending from the parent. + if self.at_root() { + self.refresh(); + return false; + } + } + } +} + +impl ZipperPath for SubtractZipper +where + V: DistributiveLattice + Clone, + A: ZipperMoving + ZipperPath + ZipperValues + Clone, + B: ZipperMoving + ZipperValues + Clone, +{ + #[inline] + fn path(&self) -> &[u8] { + &self.lhs.path()[self.lhs_root_depth..] + } + + fn move_to_path>(&mut self, path: K) -> usize { + let path = path.as_ref(); + + let current = self.path(); + let overlap = fast_slice_utils::find_prefix_overlap(path, current); + let to_ascend = current.len() - overlap; + let suffix = &path[overlap..]; + + if to_ascend == 0 && suffix.is_empty() { + return overlap; + } + + if to_ascend != 0 { + self.lhs.ascend(to_ascend); + self.rhs.ascend(to_ascend); + } + if !suffix.is_empty() { + self.lhs.descend_to(suffix); + self.rhs.descend_to(suffix); + } + + self.refresh(); + + debug_assert_eq!(self.path(), path,); + + overlap + } +} + +impl ZipperIteration for SubtractZipper +where + V: DistributiveLattice + Clone + PartialEq, + A: ZipperMoving + ZipperValues + Clone, + B: ZipperMoving + ZipperValues + Clone, +{ + fn to_next_val(&mut self) -> bool { + // The cache is valid at entry, so use it to select the first child + // without recomputing the virtual topology. + let mut next_byte = self.child_mask.indexed_bit::(0); + + 'search: loop { + // Search downward, always taking the first child in DFS order. + while let Some(byte) = next_byte { + match self.descend_to_next_stop(byte, None) { + DescendStop::Value => { + self.refresh(); + return true; + } + DescendStop::Branch(first_child) => { + // There is no value at this branch, so DFS continues + // immediately through its first surviving child. + next_byte = Some(first_child); + } + DescendStop::Leaf => { + // No value was found on this path. Continue by searching + // for the next sibling while walking back up the tree. + next_byte = None; + } + DescendStop::ByteLimit => unreachable!(), + } + } + + // Walk upwards until the next surviving sibling is found. + loop { + let Some(cur_byte) = self.focus_byte() else { + // We are at the root without having moved, so the cached + // virtual state is still valid. + return false; + }; + + self.ascend_byte_raw(); + + if let Some(byte) = self.surviving_sibling::(cur_byte) { + next_byte = Some(byte); + continue 'search; + } + + // No later subtree exists. The traversal has returned to the + // root through raw movement, so rebuild the cached virtual state. + if self.at_root() { + self.refresh(); + return false; + } + } + } + } + + fn descend_first_k_path(&mut self, k: usize) -> bool { + if k == 0 { + return true; + } + + let Some(byte) = self.child_mask.indexed_bit::(0) else { + return false; + }; + + let base_idx = self.depth(); + self.descend_to_byte_raw(byte); + let found = self.seek_k_path(base_idx, k); + + if found { + self.refresh(); + } + // On failure we are back at the original focus, so the old cache + // is still valid. + found + } + + fn to_next_k_path(&mut self, k: usize) -> bool { + let Some(base_idx) = self.depth().checked_sub(k) else { + return false; + }; + + if !self.advance_to_next_subtree(base_idx) { + self.refresh(); + return false; + } + + let found = self.seek_k_path(base_idx, k); + + // Unlike descend_first_k_path(), failure leaves us at the common root, + // not at the original focus, so the cached state is stale either way. + self.refresh(); + + found + } +} + +impl ZipperAbsolutePath for SubtractZipper +where + V: DistributiveLattice + Clone, + A: ZipperMoving + ZipperPath + ZipperValues + Clone, + B: ZipperMoving + ZipperValues + Clone, +{ + fn origin_path(&self) -> &[u8] { + self.lhs.path() + } + + fn root_prefix_path(&self) -> &[u8] { + &self.lhs.path()[0..self.lhs_root_depth] + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::PathMap; + use crate::zipper::ReadZipperUntracked; + use crate::zipper::zipper_iteration_tests::zipper_iteration_tests; + use crate::zipper::zipper_moving_tests::zipper_moving_tests; + use std::fmt::Debug; + + type ZipperT<'a, V> = + SubtractZipper, ReadZipperUntracked<'a, 'static, V>>; + fn subtract_at<'a, V>(a: &'a PathMap, b: &'a PathMap, path: &[u8]) -> ZipperT<'a, V> + where + V: DistributiveLattice + Clone + Send + Sync + Unpin, + { + let lhs = a.read_zipper_at_path(path); + let rhs = b.read_zipper_at_path(path); + + SubtractZipper::new(lhs, rhs) + } + + enum State { + UnaryNode(u8), + NonExistentNode, + Leaf(V), + Branch(ByteMask), + } + + impl State { + fn assert(self, zipper: &ZipperT<'_, V>) { + fn assert_state_impl<'a, V>( + zipper: &ZipperT<'_, V>, + expected_exists: bool, + expected_value: Option, + expected_children: ByteMask, + ) where + V: DistributiveLattice + Clone + Send + Sync + Unpin + PartialEq + Debug, + { + let path = zipper.origin_path(); + + assert_eq!( + zipper.path_exists(), + expected_exists, + "wrong path_exists at {path:?}" + ); + + assert_eq!( + zipper.is_val(), + expected_value.is_some(), + "wrong is_val at {path:?}" + ); + + assert_eq!( + zipper.val(), + expected_value.as_ref(), + "wrong value at {path:?}" + ); + + assert_eq!( + zipper.child_count(), + expected_children.count_bits(), + "wrong child_count at {path:?}" + ); + + let mask = zipper.child_mask(); + for byte in expected_children.iter() { + assert!(mask.test_bit(byte), "unset child {byte} at {path:?}"); + } + let ghosts = mask & !expected_children; + assert!( + ghosts.is_empty_mask(), + "ghost children {ghosts:?} at {path:?}" + ); + } + + match self { + State::UnaryNode(expected_child) => { + assert_state_impl(zipper, true, None, ByteMask::from(expected_child)) + } + State::NonExistentNode => assert_state_impl(zipper, false, None, ByteMask::EMPTY), + State::Leaf(expected_value) => { + assert_state_impl(zipper, true, Some(expected_value), ByteMask::EMPTY) + } + State::Branch(expected_children) => { + assert_state_impl(zipper, true, None, expected_children) + } + } + } + } + + #[test] + fn lhs_survives_unchanged() { + let lhs = PathMap::from_iter([([10, 20], true)]); + let rhs = PathMap::new(); + + let mut zipper = subtract_at(&lhs, &rhs, &[]); + State::UnaryNode(10).assert(&zipper); + + zipper.descend_to_byte(10); + State::UnaryNode(20).assert(&zipper); + + zipper.descend_to_byte(20); + State::Leaf(true).assert(&zipper); + } + + #[test] + fn lhs_not_affected() { + let lhs = PathMap::from_iter([([10], true)]); + let rhs = PathMap::from_iter([(&[20][..], false), (&[30, 40], false)]); + + let mut zipper = subtract_at(&lhs, &rhs, &[]); + State::UnaryNode(10).assert(&zipper); + + zipper.descend_to_byte(10); + State::Leaf(true).assert(&zipper); + + zipper.reset(); + zipper.descend_to_byte(20); + State::NonExistentNode.assert(&zipper); + } + + #[test] + fn lhs_contained_in_rhs() { + let lhs = PathMap::from_iter([([10, 20], true)]); + let rhs = PathMap::from_iter([(&[10, 20][..], true), (&[10, 30], false), (&[40], false)]); + + let mut zipper = subtract_at(&lhs, &rhs, &[]); + State::NonExistentNode.assert(&zipper); + + zipper.descend_to([10, 20]); + State::NonExistentNode.assert(&zipper); + + zipper.reset(); + zipper.descend_to([10, 30]); + State::NonExistentNode.assert(&zipper); + + zipper.reset(); + zipper.descend_to_byte(40); + State::NonExistentNode.assert(&zipper); + } + + #[test] + fn rhs_contained_in_lhs() { + let lhs = PathMap::from_iter([(&[10, 20][..], false), (&[10, 30], false), (&[40], true)]); + let rhs = PathMap::from_iter([([10, 20], false), ([10, 30], false)]); + + let mut zipper = subtract_at(&lhs, &rhs, &[]); + State::UnaryNode(40).assert(&zipper); + + zipper.descend_to_byte(40); + State::Leaf(true).assert(&zipper); + } + + #[test] + fn identical_singletons() { + let lhs = PathMap::from_iter([([10], true)]); + let rhs = PathMap::from_iter([([10], true)]); + + assert!(!subtract_at(&lhs, &rhs, &[]).path_exists()); + } + + #[test] + fn common_prefix_survives_if_difference_below_1() { + let lhs = PathMap::from_iter([([10, 20], true), ([10, 30], true)]); + let rhs = PathMap::from_iter([([10, 20], true)]); + + let mut zipper = subtract_at(&lhs, &rhs, &[]); + State::UnaryNode(10).assert(&zipper); + + zipper.descend_to_byte(10); + State::UnaryNode(30).assert(&zipper); + + zipper.descend_to_byte(20); + State::NonExistentNode.assert(&zipper); + + zipper.ascend_byte(); + zipper.descend_to_byte(30); + State::Leaf(true).assert(&zipper); + } + + #[test] + fn common_prefix_survives_if_difference_below_2() { + let lhs = PathMap::from_iter([([10, 20, 30], true)]); + let rhs = PathMap::from_iter([([10, 20, 40], true)]); + + let mut zipper = subtract_at(&lhs, &rhs, &[]); + State::UnaryNode(10).assert(&zipper); + + zipper.descend_to_byte(10); + State::UnaryNode(20).assert(&zipper); + + zipper.descend_to_byte(20); + State::UnaryNode(30).assert(&zipper); + + zipper.descend_to_byte(30); + State::Leaf(true).assert(&zipper); + } + + #[test] + fn value_is_destroyed_node_survives() { + let lhs = PathMap::from_iter([(&[10][..], true), (&[10, 20], false)]); + let rhs = PathMap::from_iter([([10], true)]); + + let mut zipper = subtract_at(&lhs, &rhs, &[]); + zipper.descend_to([10, 20]); + State::Leaf(false).assert(&zipper); + + zipper.ascend_byte(); + State::UnaryNode(20).assert(&zipper); + } + + #[test] + fn node_survives_children_are_destroyed() { + let lhs = PathMap::from_iter([(&[10][..], true), (&[10, 20], false)]); + let rhs = PathMap::from_iter([([10, 20], false)]); + + let mut zipper = subtract_at(&lhs, &rhs, &[]); + zipper.descend_to([10]); + State::Leaf(true).assert(&zipper); + } + + fn assert_subtract_matches_materialized( + lhs: &PathMap, + rhs: &PathMap, + paths: impl IntoIterator, + ) where + V: DistributiveLattice + Clone + Send + Sync + Unpin + PartialEq + Debug, + K: AsRef<[u8]> + Debug, + { + let expected = lhs.subtract(rhs); + let mut expected_z = expected.read_zipper(); + + for path in paths { + expected_z.descend_to(path.as_ref()); + let actual = subtract_at(lhs, rhs, path.as_ref()); + + assert_eq!( + actual.path_exists(), + expected_z.path_exists(), + "path_exists differs at {path:?}" + ); + + assert_eq!( + actual.is_val(), + expected_z.is_val(), + "is_val differs at {path:?}" + ); + + assert_eq!(actual.val(), expected_z.val(), "val differs at {path:?}"); + + assert_eq!( + actual.child_mask(), + expected_z.child_mask(), + "child_mask differs at {path:?}" + ); + + assert_eq!( + actual.child_count(), + expected_z.child_count(), + "child_count differs at {path:?}" + ); + + assert_eq!( + actual.val_count(), + expected_z.val_count(), + "val_count differs at {path:?}" + ); + + expected_z.reset(); + } + } + + #[test] + fn subtract_zipper_matches_materialized_subtraction_on_all_prefixes() { + let lhs = PathMap::from_iter([ + (&[10][..], 1u16), + (&[10, 20], 2), + (&[10, 20, 30], 3), + (&[10, 40], 4), + (&[50, 60], 5), + (&[70], 6), + ]); + + let rhs = PathMap::from_iter([ + (&[10][..], 1), + (&[10, 20], 7), + (&[10, 20, 30], 3), + (&[50, 80], 8), + (&[90], 9), + ]); + + let paths = [ + &[], + &[10][..], + &[10, 20], + &[10, 20, 30], + &[10, 40], + &[50], + &[50, 60], + &[50, 80], + &[70], + &[90], + ]; + + assert_subtract_matches_materialized(&lhs, &rhs, paths); + } + + #[test] + fn subtract_zipper_matches_materialized_subtraction_on_absent_paths() { + let lhs = PathMap::from_iter([([10, 20], 1u16), ([10, 30], 2u16)]); + + let rhs = PathMap::from_iter([([10, 20], 1)]); + + let paths = [ + &[], + &[10][..], + &[10, 20], + &[10, 30], + // Deliberately absent. + &[11], + &[10, 21], + &[10, 30, 40], + &[200, 201], + ]; + + assert_subtract_matches_materialized(&lhs, &rhs, paths); + } + + #[test] + fn subtract_zipper_matches_lhs_when_rhs_is_disjoint() { + let lhs = PathMap::from_iter([(&[10][..], 1u16), (&[10, 20], 2), (&[30, 40, 50], 3)]); + + let rhs = PathMap::from_iter([(&[100][..], 4), (&[110, 120], 5)]); + + let paths = [ + &[], + &[10][..], + &[10, 20], + &[30], + &[30, 40], + &[30, 40, 50], + &[100], + &[110], + &[110, 120], + ]; + + assert_subtract_matches_materialized(&lhs, &rhs, paths); + } + + zipper_moving_tests!( + subtract_zipper, + |keys: &[&[u8]]| { + let lhs = PathMap::from_iter(keys.iter().zip(std::iter::repeat(true))); + let mut rhs = PathMap::new(); + if let Some(path) = keys.first() { + rhs.set_val_at(path, false); + } + if let Some(path) = keys.last() { + rhs.set_val_at(path, false); + } + (lhs, rhs) + }, + |(lhs, rhs): &mut (PathMap, PathMap), path: &[u8]| -> ZipperT<'_, bool> { + subtract_at(lhs, rhs, path) + } + ); + + zipper_moving_tests!( + subtract_zipper_demanding, + |keys: &[&[u8]]| { + let mut lhs = PathMap::from_iter(keys.iter().zip(std::iter::repeat(true))); + let mut rhs = PathMap::new(); + keys.iter() + .enumerate() + .filter(|(i, _)| i % 2 != 0) + .for_each(|(_, key)| { + rhs.set_val_at(key, false); + if key.len() > 1 { + let _ = lhs.get_val_or_set_mut_at(&[key[0]], false); + rhs.set_val_at(&[key[0]], false); + } + }); + (lhs, rhs) + }, + |(lhs, rhs): &mut (PathMap, PathMap), path: &[u8]| -> ZipperT<'_, bool> { + subtract_at(lhs, rhs, path) + } + ); + + zipper_iteration_tests!( + subtract_zipper, + |keys: &[&[u8]]| { + let lhs = PathMap::from_iter(keys.iter().zip(std::iter::repeat(true))); + let mut rhs = PathMap::new(); + if let Some(path) = keys.first() { + rhs.set_val_at(path, false); + } + if let Some(path) = keys.last() { + rhs.set_val_at(path, false); + } + (lhs, rhs) + }, + |(lhs, rhs): &mut (PathMap, PathMap), path: &[u8]| -> ZipperT<'_, bool> { + subtract_at(lhs, rhs, path) + } + ); + + zipper_iteration_tests!( + subtract_zipper_demanding, + |keys: &[&[u8]]| { + let mut lhs = PathMap::from_iter(keys.iter().zip(std::iter::repeat(true))); + let mut rhs = PathMap::new(); + keys.iter() + .enumerate() + .filter(|(i, _)| i % 2 != 0) + .for_each(|(_, key)| { + rhs.set_val_at(key, false); + if key.len() > 1 { + let _ = lhs.get_val_or_set_mut_at(&[key[0]], false); + rhs.set_val_at(&[key[0]], false); + } + }); + (lhs, rhs) + }, + |(lhs, rhs): &mut (PathMap, PathMap), path: &[u8]| -> ZipperT<'_, bool> { + subtract_at(lhs, rhs, path) + } + ); +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index c100e1f9..cc84ad1f 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -495,6 +495,22 @@ impl core::ops::BitAndAssign for ByteMask { } } +impl core::ops::BitXor for ByteMask { + type Output = ByteMask; + #[inline] + fn bitxor(self, rhs: Self) -> ByteMask { + self.xor(&rhs) + } +} + +impl core::ops::Not for ByteMask { + type Output = ByteMask; + #[inline] + fn not(self) -> ByteMask { + BitMask::not(&self) + } +} + impl Lattice for ByteMask { #[inline] fn pjoin(&self, other: &Self) -> AlgebraicResult { diff --git a/src/zipper.rs b/src/zipper.rs index 8e62b57a..30fdb1ac 100644 --- a/src/zipper.rs +++ b/src/zipper.rs @@ -24,6 +24,7 @@ pub use crate::overlay_zipper::{OverlayZipper}; pub use crate::prefix_zipper::{PrefixZipper}; pub use crate::path_tracker::{PathTracker}; pub use crate::empty_zipper::{EmptyZipper}; +pub use crate::subtract_zipper::{SubtractZipper}; pub use crate::poly_zipper::{PolyZipper, PolyZipperExplicit}; pub use crate::dependent_zipper::DependentProductZipperG; use crate::zipper_tracking::*; From f7a64d4849b2678bc5da066d3da04681f81de984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Rze=C5=BAnicki?= Date: Tue, 8 Sep 2026 04:07:31 +0200 Subject: [PATCH 03/11] SubtractZipper: Implement `_observed` function --- src/subtract_zipper.rs | 113 +++++++++++++++++++++++++++-------------- src/zipper.rs | 39 -------------- 2 files changed, 76 insertions(+), 76 deletions(-) diff --git a/src/subtract_zipper.rs b/src/subtract_zipper.rs index 3eaad688..a166471b 100644 --- a/src/subtract_zipper.rs +++ b/src/subtract_zipper.rs @@ -3,7 +3,10 @@ use std::{cell::Cell, num::NonZeroUsize}; use crate::{ ring::{self, AlgebraicResult, DistributiveLattice, DistributiveLatticeRef}, utils::{BitMask, ByteMask}, - zipper::{Zipper, ZipperAbsolutePath, ZipperIteration, ZipperMoving, ZipperPath, ZipperValues}, + zipper::{ + PathObserver, Zipper, ZipperAbsolutePath, ZipperIteration, ZipperMoving, ZipperPath, + ZipperValues, + }, }; pub struct SubtractZipper { @@ -130,15 +133,6 @@ where } fn compute_child_mask(&mut self) -> ByteMask { - if !self.lhs.path_exists() { - return ByteMask::default(); - } - - // Nothing below this RHS position can exist either. - if !self.rhs.path_exists() { - return self.lhs.child_mask(); - } - let mut out = self.lhs.child_mask(); for byte in (out & self.rhs.child_mask()).iter() { @@ -168,6 +162,9 @@ where /// `cur_byte` is assumed to be a child of the materialized subtraction. fn has_surviving_sibling(&mut self, cur_byte: u8) -> bool { let mut candidates = self.lhs.child_mask(); + if candidates.is_empty_mask() { + return false; + } if !self.rhs.path_exists() { // The whole LHS subtree survives, so any other LHS child is enough. @@ -238,18 +235,20 @@ where /// exhausted. /// /// This method does not refresh cached virtual state. - fn descend_to_next_stop( + fn descend_to_next_stop( &mut self, mut byte: u8, - mut remaining: Option, + obs: &mut P, + mut limit: Option, ) -> DescendStop { loop { self.descend_to_byte_raw(byte); + obs.descend_to_byte(byte); - if let Some(left) = remaining { - remaining = NonZeroUsize::new(left.get() - 1); + if let Some(left) = limit { + limit = NonZeroUsize::new(left.get() - 1); - if remaining.is_none() { + if limit.is_none() { return DescendStop::ByteLimit; } } @@ -266,12 +265,16 @@ where } #[inline] - fn descend_until(&mut self, max_bytes: Option) -> bool { + fn descend_until( + &mut self, + obs: &mut P, + max_bytes: Option, + ) -> bool { if let Some(byte) = self.child_mask.indexed_bit::(0) { if self.child_mask.next_bit(byte).is_some() { return false; } - let _ = self.descend_to_next_stop(byte, max_bytes); + let _ = self.descend_to_next_stop(byte, obs, max_bytes); self.refresh(); true } else { @@ -292,7 +295,7 @@ where lhs_mask.prev_bit(cur_byte) }; - if !self.rhs.path_exists() { + if candidate.is_none() || !self.rhs.path_exists() { return candidate; } @@ -332,11 +335,15 @@ where /// /// Returns `false` after exhausting the search and leaving the zipper at /// `base_idx`. - fn advance_to_next_subtree(&mut self, base_idx: usize) -> bool { + fn advance_to_next_subtree(&mut self, base_idx: usize, obs: &mut P) -> bool { + let mut ascended = 0; loop { // Reaching the common root means the current DFS subtree has been // exhausted and there is no later subtree to visit. if self.lhs.depth() == self.lhs_root_depth + base_idx { + if ascended != 0 { + obs.ascend(ascended); + } return false; } @@ -345,11 +352,14 @@ where // Move to the parent without refreshing the virtual zipper state. // Intermediate nodes are not externally observable during this search. self.ascend_byte_raw(); + ascended += 1; // Continue DFS from the next surviving sibling, if one exists. // surviving_sibling() accounts for branches removed by subtraction. if let Some(byte) = self.surviving_sibling::(cur_byte) { + obs.ascend(ascended); self.descend_to_byte_raw(byte); + obs.descend_to_byte(byte); return true; } @@ -364,9 +374,8 @@ where /// The zipper may be positioned anywhere below `base_idx` on entry. /// Intermediate movement is raw; the virtual state is refreshed only when /// a matching path is found. - fn seek_k_path(&mut self, base_idx: usize, k: usize) -> bool { + fn seek_k_path(&mut self, base_idx: usize, k: usize, obs: &mut P) -> bool { let target_idx = base_idx + k; - loop { // The first path encountered at the requested depth is the result, // since traversal always prefers the first surviving child. @@ -378,12 +387,13 @@ where // possible. if let Some(byte) = self.first_surviving_child() { self.descend_to_byte_raw(byte); + obs.descend_to_byte(byte); continue; } // This branch ended before reaching the requested depth. Backtrack // until another surviving subtree can continue the DFS. - if !self.advance_to_next_subtree(base_idx) { + if !self.advance_to_next_subtree(base_idx, obs) { return false; } } @@ -723,12 +733,7 @@ where } let rhs_gone = !self.rhs.child_mask().test_bit(byte); - - self.descend_to_byte_raw(byte); - if rhs_gone { - i += 1; - // An LHS-only child survives wholesale. From this point downward // A - B is exactly A, so delegate the rest to the native LHS zipper. let descended = self.lhs.descend_to_existing(&k[i..]); @@ -739,6 +744,7 @@ where // Both sides contain the child structurally. It belongs to the // materialized difference iff something survives below it. + self.descend_to_byte_raw(byte); if !subtree_has_difference::(&mut self.lhs, &mut self.rhs) { self.ascend_byte_raw(); break; @@ -843,13 +849,32 @@ where true } + #[inline] fn descend_until(&mut self) -> bool { - self.descend_until(None) + self.descend_until(&mut (), None) + } + + #[inline] + fn descend_until_observed(&mut self, obs: &mut Obs) -> bool { + self.descend_until(obs, None) } + #[inline] fn descend_until_max_bytes(&mut self, max_bytes: usize) -> bool { match NonZeroUsize::new(max_bytes) { - Some(limit) => self.descend_until(Some(limit)), + Some(limit) => self.descend_until(&mut (), Some(limit)), + None => false, + } + } + + #[inline] + fn descend_until_max_bytes_observed( + &mut self, + max_bytes: usize, + obs: &mut Obs, + ) -> bool { + match NonZeroUsize::new(max_bytes) { + Some(limit) => self.descend_until(obs, Some(limit)), None => false, } } @@ -900,14 +925,17 @@ where } } - fn to_next_step(&mut self) -> bool { + fn to_next_step_observed(&mut self, obs: &mut P) -> bool { // If there is a child, DFS simply descends into the first one. if let Some(byte) = self.child_mask.indexed_bit::(0) { self.descend_to_byte_raw(byte); + obs.descend_to_byte(byte); self.refresh(); return true; } + let mut ascended = 0; + // We are at a leaf. Walk upwards without refreshing intermediate // virtual nodes until a surviving next sibling is found. loop { @@ -916,9 +944,14 @@ where }; self.ascend_byte_raw(); + ascended += 1; if let Some(byte) = self.surviving_sibling::(cur_byte) { + obs.ascend(ascended); + self.descend_to_byte_raw(byte); + obs.descend_to_byte(byte); + self.refresh(); return true; } @@ -927,6 +960,7 @@ where // not restore the previous child: DFS is done with that subtree and // continues ascending from the parent. if self.at_root() { + obs.ascend(ascended); self.refresh(); return false; } @@ -980,7 +1014,7 @@ where A: ZipperMoving + ZipperValues + Clone, B: ZipperMoving + ZipperValues + Clone, { - fn to_next_val(&mut self) -> bool { + fn to_next_val_observed(&mut self, obs: &mut P) -> bool { // The cache is valid at entry, so use it to select the first child // without recomputing the virtual topology. let mut next_byte = self.child_mask.indexed_bit::(0); @@ -988,7 +1022,7 @@ where 'search: loop { // Search downward, always taking the first child in DFS order. while let Some(byte) = next_byte { - match self.descend_to_next_stop(byte, None) { + match self.descend_to_next_stop(byte, obs, None) { DescendStop::Value => { self.refresh(); return true; @@ -1008,6 +1042,7 @@ where } // Walk upwards until the next surviving sibling is found. + let mut ascended = 0; loop { let Some(cur_byte) = self.focus_byte() else { // We are at the root without having moved, so the cached @@ -1016,15 +1051,18 @@ where }; self.ascend_byte_raw(); + ascended += 1; if let Some(byte) = self.surviving_sibling::(cur_byte) { next_byte = Some(byte); + obs.ascend(ascended); continue 'search; } // No later subtree exists. The traversal has returned to the // root through raw movement, so rebuild the cached virtual state. if self.at_root() { + obs.ascend(ascended); self.refresh(); return false; } @@ -1032,7 +1070,7 @@ where } } - fn descend_first_k_path(&mut self, k: usize) -> bool { + fn descend_first_k_path_observed(&mut self, k: usize, obs: &mut P) -> bool { if k == 0 { return true; } @@ -1043,7 +1081,8 @@ where let base_idx = self.depth(); self.descend_to_byte_raw(byte); - let found = self.seek_k_path(base_idx, k); + obs.descend_to_byte(byte); + let found = self.seek_k_path(base_idx, k, obs); if found { self.refresh(); @@ -1053,17 +1092,17 @@ where found } - fn to_next_k_path(&mut self, k: usize) -> bool { + fn to_next_k_path_observed(&mut self, k: usize, obs: &mut P) -> bool { let Some(base_idx) = self.depth().checked_sub(k) else { return false; }; - if !self.advance_to_next_subtree(base_idx) { + if !self.advance_to_next_subtree(base_idx, obs) { self.refresh(); return false; } - let found = self.seek_k_path(base_idx, k); + let found = self.seek_k_path(base_idx, k, obs); // Unlike descend_first_k_path(), failure leaves us at the common root, // not at the original focus, so the cached state is stale either way. diff --git a/src/zipper.rs b/src/zipper.rs index 30fdb1ac..408e8d8e 100644 --- a/src/zipper.rs +++ b/src/zipper.rs @@ -4055,45 +4055,6 @@ pub(crate) mod zipper_moving_tests { assert_eq!(zip.ascend_until_branch(), 5); assert_eq!(zip.path(), b""); assert!(zip.at_root()); - - //Try with some actual branches in the trie. - //Some paths encountered will be values only, some will be branches only, and some will be both - let keys = ["1", "123", "12345", "1abc", "1234abc"]; - let map: PathMap<()> = keys.into_iter().map(|v| (v, ())).collect(); - let mut zip = map.read_zipper(); - - zip.descend_to(b"12345"); - assert!(zip.path_exists()); - assert_eq!(zip.path(), b"12345"); - assert_eq!(zip.ascend_until(), 1); - assert_eq!(zip.path(), b"1234"); // "1234" is a branch only - assert_eq!(zip.is_val(), false); - assert_eq!(zip.child_count(), 2); - assert_eq!(zip.ascend_until(), 1); - assert_eq!(zip.path(), b"123"); // "123" is a value only - assert_eq!(zip.child_count(), 1); - assert_eq!(zip.is_val(), true); - assert_eq!(zip.ascend_until(), 2); // Jump over "12" because it's neither a branch nor a value - assert_eq!(zip.path(), b"1"); // "1" is both a branch and a value - assert_eq!(zip.is_val(), true); - assert_eq!(zip.child_count(), 2); - assert_eq!(zip.ascend_until(), 1); - assert_eq!(zip.path(), b""); - assert_eq!(zip.child_count(), 1); - assert_eq!(zip.ascend_until(), 0); - assert!(zip.at_root()); - - //Test that ascend_until_branch skips over all the values - zip.descend_to(b"12345"); - assert!(zip.path_exists()); - assert_eq!(zip.ascend_until_branch(), 1); - assert_eq!(zip.path(), b"1234"); - assert_eq!(zip.ascend_until_branch(), 3); - assert_eq!(zip.path(), b"1"); - assert_eq!(zip.ascend_until_branch(), 1); - assert_eq!(zip.path(), b""); - assert_eq!(zip.ascend_until_branch(), 0); - assert!(zip.at_root()); } /// Test a trie with some actual branches From d74a67f60deb435dc97f21b9eb8b59df2852f3ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Rze=C5=BAnicki?= Date: Tue, 8 Sep 2026 18:25:10 +0200 Subject: [PATCH 04/11] SubtractZipper: couple of small optimizations --- src/subtract_zipper.rs | 47 ++++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/src/subtract_zipper.rs b/src/subtract_zipper.rs index a166471b..e089c0e2 100644 --- a/src/subtract_zipper.rs +++ b/src/subtract_zipper.rs @@ -39,7 +39,7 @@ enum DescendState { enum DescendStop { Value, - Branch(u8), // first surviving child + Branch, Leaf, ByteLimit, } @@ -189,7 +189,7 @@ where /// /// Returns `DescendState::Continue(byte)` only when there is no surviving value and exactly /// one surviving child. Otherwise returns the reason traversal must stop. - fn current_descend_state(&mut self) -> DescendState { + fn current_descend_state(&mut self) -> DescendState { if !self.rhs.path_exists() { // Once RHS no longer contains the current path, subtraction has no // further effect below this point: the virtual subtree is exactly LHS. @@ -200,8 +200,8 @@ where let mask = self.lhs.child_mask(); if let Some(first_byte) = mask.indexed_bit::(0) { - if mask.next_bit(first_byte).is_some() { - return DescendState::Stop(DescendStop::Branch(first_byte)); + if STOP_ON_BRANCH && mask.next_bit(first_byte).is_some() { + return DescendState::Stop(DescendStop::Branch); } return DescendState::Continue(first_byte); @@ -218,8 +218,8 @@ where match self.first_surviving_child() { Some(byte) => { - if self.surviving_sibling::(byte).is_some() { - DescendState::Stop(DescendStop::Branch(byte)) + if STOP_ON_BRANCH && self.surviving_sibling::(byte).is_some() { + DescendState::Stop(DescendStop::Branch) } else { DescendState::Continue(byte) } @@ -235,7 +235,7 @@ where /// exhausted. /// /// This method does not refresh cached virtual state. - fn descend_to_next_stop( + fn descend_to_next_stop( &mut self, mut byte: u8, obs: &mut P, @@ -253,7 +253,7 @@ where } } - match self.current_descend_state() { + match self.current_descend_state::() { DescendState::Continue(next_byte) => { byte = next_byte; } @@ -274,7 +274,7 @@ where if self.child_mask.next_bit(byte).is_some() { return false; } - let _ = self.descend_to_next_stop(byte, obs, max_bytes); + let _ = self.descend_to_next_stop::(byte, obs, max_bytes); self.refresh(); true } else { @@ -623,10 +623,17 @@ where return count; } - let mut lhs = self.lhs.clone(); - let mut rhs = self.rhs.clone(); - - let count = subtract_val_count::(&mut lhs, &mut rhs); + 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 @@ -1022,22 +1029,22 @@ where 'search: loop { // Search downward, always taking the first child in DFS order. while let Some(byte) = next_byte { - match self.descend_to_next_stop(byte, obs, None) { + match self.descend_to_next_stop::(byte, obs, None) { DescendStop::Value => { self.refresh(); return true; } - DescendStop::Branch(first_child) => { - // There is no value at this branch, so DFS continues - // immediately through its first surviving child. - next_byte = Some(first_child); - } DescendStop::Leaf => { // No value was found on this path. Continue by searching // for the next sibling while walking back up the tree. next_byte = None; } - DescendStop::ByteLimit => unreachable!(), + // DescendStop::Branch(first_child) => { + // // There is no value at this branch, so DFS continues + // // immediately through its first surviving child. + // next_byte = Some(first_child); + // } + _ => unreachable!(), } } From ee6e4204499b099f6d785824cfc3cea31d70945e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Rze=C5=BAnicki?= Date: Wed, 9 Sep 2026 04:23:53 +0200 Subject: [PATCH 05/11] SubtractZipper: implement subtree probe caching --- src/subtract_zipper.rs | 97 +++++++++++++++++++++++++++++++----------- src/utils/mod.rs | 7 +++ 2 files changed, 80 insertions(+), 24 deletions(-) diff --git a/src/subtract_zipper.rs b/src/subtract_zipper.rs index e089c0e2..1de1d46b 100644 --- a/src/subtract_zipper.rs +++ b/src/subtract_zipper.rs @@ -21,6 +21,11 @@ pub struct SubtractZipper { child_mask: ByteMask, val: CachedVal, val_count: Cell>, + + // Results of shared-child subtree probes performed at the current focus + // since the last logical movement or refresh. + checked_common: ByteMask, + surviving_common: ByteMask, } enum CachedVal { @@ -59,33 +64,39 @@ where child_mask: ByteMask::default(), val: CachedVal::None, val_count: Cell::new(None), + checked_common: ByteMask::EMPTY, + surviving_common: ByteMask::EMPTY, }; this.refresh(); this } + #[inline(always)] + fn invalidate_child_probes(&mut self) { + self.checked_common = ByteMask::EMPTY; + self.surviving_common = ByteMask::EMPTY; + } + /// Rebuilds the cached virtual state for the current backing-zipper focus. /// - /// This recomputes the value and child topology of the materialized - /// subtraction `lhs - rhs` and invalidates the cached value count. + /// Previously computed shared-child probes are reused while rebuilding the + /// child mask. The probe cache is discarded afterwards. fn refresh(&mut self) { self.val_count.set(None); if !self.lhs.path_exists() { - self.child_mask = ByteMask::default(); + self.child_mask = ByteMask::EMPTY; self.val = CachedVal::None; - return; - } - - if !self.rhs.path_exists() { + } else if !self.rhs.path_exists() { self.child_mask = self.lhs.child_mask(); self.val = CachedVal::Lhs; - return; + } else { + self.val = self.compute_val(); + self.child_mask = self.compute_child_mask(); } - self.val = self.compute_val(); - self.child_mask = self.compute_child_mask(); + self.invalidate_child_probes(); } /// Descends both backing zippers by one byte without refreshing the cached @@ -95,6 +106,7 @@ where /// `val` are stale until `refresh()` is called or the movement is undone. #[inline] fn descend_to_byte_raw(&mut self, byte: u8) { + self.invalidate_child_probes(); self.lhs.descend_to_byte(byte); self.rhs.descend_to_byte(byte); } @@ -111,11 +123,14 @@ where "SubtractZipper attempted to ascend above its root" ); - let lhs = self.lhs.ascend_byte(); - let rhs = self.rhs.ascend_byte(); + let lhs_ascended = self.lhs.ascend_byte(); + let rhs_ascended = self.rhs.ascend_byte(); + + debug_assert_eq!(lhs_ascended, rhs_ascended); + + self.invalidate_child_probes(); - debug_assert_eq!(lhs, rhs); - lhs + lhs_ascended } /// Returns whether the child `at` contains anything in the materialized @@ -124,21 +139,51 @@ where /// Both backing zippers are temporarily descended into the child and restored /// to their original focus before this method returns. #[inline] - fn subtree_survives(&mut self, at: u8) -> bool { - self.descend_to_byte_raw(at); + fn subtree_survives_uncached(&mut self, at: u8) -> bool { + // This is a temporary probe, not a logical focus change, so bypass the + // raw movement helpers in order to preserve the current-focus probe cache. + self.lhs.descend_to_byte(at); + self.rhs.descend_to_byte(at); + let survives = subtree_has_difference::(&mut self.lhs, &mut self.rhs); - self.ascend_byte_raw(); + + let lhs_ascended = self.lhs.ascend_byte(); + let rhs_ascended = self.rhs.ascend_byte(); + + debug_assert!(lhs_ascended); + debug_assert!(rhs_ascended); survives } - fn compute_child_mask(&mut self) -> ByteMask { - let mut out = self.lhs.child_mask(); + #[inline] + fn subtree_survives(&mut self, at: u8) -> bool { + let survives = self.subtree_survives_uncached(at); + + self.checked_common.set_bit(at); + if survives { + self.surviving_common.set_bit(at); + } - for byte in (out & self.rhs.child_mask()).iter() { - // Both tries contain this branch. Look below it to determine - // whether anything survives. - if !self.subtree_survives(byte) { + survives + } + + fn compute_child_mask(&mut self) -> ByteMask { + let lhs_mask = self.lhs.child_mask(); + let common = lhs_mask & self.rhs.child_mask(); + + let mut out = lhs_mask; + // Remove shared children already known not to survive. + // checked_common ^ surviving_common is exactly the set of checked children + // known not to survive. All of them are present in lhs_mask, so XOR removes them. + out ^= self.checked_common ^ self.surviving_common; + + // Since checked_common ⊆ common, XOR gives exactly the unchecked children. + let unchecked = common ^ self.checked_common; + // Only probe shared children whose subtraction subtree has not already + // been inspected at this focus. + for byte in unchecked.iter() { + if !self.subtree_survives_uncached(byte) { out.clear_bit(byte); } } @@ -610,7 +655,11 @@ where #[inline] fn focus_byte(&self) -> Option { - self.lhs.focus_byte() + if self.depth() == 0 { + None + } else { + self.lhs.focus_byte() + } } // #[inline] diff --git a/src/utils/mod.rs b/src/utils/mod.rs index cc84ad1f..4af2b05a 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -503,6 +503,13 @@ impl core::ops::BitXor for ByteMask { } } +impl core::ops::BitXorAssign for ByteMask { + #[inline] + fn bitxor_assign(&mut self, rhs: Self) { + *self = self.xor(&rhs) + } +} + impl core::ops::Not for ByteMask { type Output = ByteMask; #[inline] From 4d4d1990fed01cf5823b0bdc611d6ee061435209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Rze=C5=BAnicki?= Date: Sat, 12 Sep 2026 03:05:04 +0200 Subject: [PATCH 06/11] SubtractZipper: implement child probe propagation during ascend --- src/subtract_zipper.rs | 188 ++++++++++++++++++++++++++++++++--------- 1 file changed, 146 insertions(+), 42 deletions(-) diff --git a/src/subtract_zipper.rs b/src/subtract_zipper.rs index 1de1d46b..d0d193bd 100644 --- a/src/subtract_zipper.rs +++ b/src/subtract_zipper.rs @@ -22,8 +22,11 @@ pub struct SubtractZipper { val: CachedVal, val_count: Cell>, - // Results of shared-child subtree probes performed at the current focus - // since the last logical movement or refresh. + // Shared-child subtree facts for the current backing-zipper focus. + // + // Only bits set in checked_common are authoritative. surviving_common may + // retain stale bits after invalidation; they are ignored unless the matching + // checked_common bit is set. checked_common: ByteMask, surviving_common: ByteMask, } @@ -72,10 +75,23 @@ where this } + /// Invalidates all shared-child probe facts for the current focus. + /// + /// Clearing checked_common is sufficient: surviving_common is only read for + /// children whose checked_common bit is set. #[inline(always)] fn invalidate_child_probes(&mut self) { self.checked_common = ByteMask::EMPTY; - self.surviving_common = ByteMask::EMPTY; + } + + #[inline(always)] + fn set_child_probe(&mut self, at: u8, survives: bool) { + self.checked_common.set_bit(at); + if survives { + self.surviving_common.set_bit(at); + } else { + self.surviving_common.clear_bit(at); + } } /// Rebuilds the cached virtual state for the current backing-zipper focus. @@ -102,8 +118,9 @@ where /// Descends both backing zippers by one byte without refreshing the cached /// virtual state. /// - /// After this call, cached fields such as `path_exists`, `child_mask`, and - /// `val` are stale until `refresh()` is called or the movement is undone. + /// The shared-child probe cache belongs to the old focus and is invalidated. + /// Observable cached fields remain stale until refresh() is called or the + /// movement is undone. #[inline] fn descend_to_byte_raw(&mut self, byte: u8) { self.invalidate_child_probes(); @@ -111,26 +128,82 @@ where self.rhs.descend_to_byte(byte); } - /// Ascends both backing zippers by one byte without refreshing the cached - /// virtual state. + /// Ascends both backing zippers without rebuilding virtual state. /// - /// Must not be called at the virtual root. Cached virtual state remains stale - /// until `refresh()` is called or the movement is undone. + /// Any cached child-probe results are invalid after the focus changes and are + /// discarded. #[inline] - fn ascend_byte_raw(&mut self) -> bool { + fn ascend_raw(&mut self, steps: usize) { + if steps == 0 { + return; + } + debug_assert!( - self.lhs.depth() > self.lhs_root_depth, + self.lhs.depth() >= self.lhs_root_depth + steps, "SubtractZipper attempted to ascend above its root" ); - let lhs_ascended = self.lhs.ascend_byte(); - let rhs_ascended = self.rhs.ascend_byte(); + let lhs_ascended = self.lhs.ascend(steps); + let rhs_ascended = self.rhs.ascend(steps); - debug_assert_eq!(lhs_ascended, rhs_ascended); + debug_assert_eq!(lhs_ascended, steps); + debug_assert_eq!(rhs_ascended, steps); self.invalidate_child_probes(); + } + + /// Ascends both backing zippers without rebuilding the cached virtual state. + /// + /// A surviving subtree proves that every ancestor containing it survives, so + /// `true` may be propagated across multiple levels. + /// + /// A non-surviving subtree only proves that this particular child is empty at + /// its immediate parent, so `false` is valid only for a single-level ascent. + #[inline] + fn ascend_raw_known(&mut self, steps: usize, survives: bool) { + if steps == 0 { + return; + } + let multi_level = steps > 1; + // Survival is monotone upward, so it remains valid after any number of + // ascents. Emptiness describes only the immediate child and therefore + // cannot be carried through a multi-level ascent. + if multi_level && !survives { + self.ascend_raw(steps); + return; + } + + debug_assert!( + self.lhs.depth() >= self.lhs_root_depth + steps, + "SubtractZipper attempted to ascend above its root" + ); + + // For a multi-level surviving ascent, only the child directly below the + // final focus matters. Ascend the prefix in bulk and leave the last step + // separate so that its byte can be recorded. + if multi_level { + let prefix = steps - 1; + let lhs_ascended = self.lhs.ascend(prefix); + let rhs_ascended = self.rhs.ascend(prefix); + + debug_assert_eq!(lhs_ascended, prefix); + debug_assert_eq!(rhs_ascended, prefix); + } + + let child_byte = self + .lhs + .focus_byte() + .expect("path is below SubtractZipper root"); - lhs_ascended + self.ascend_raw(1); + + // Probe facts are meaningful only for children shared by both tries. + // The raw focus being left may itself be structurally absent on either side, + // so record the propagated fact only when child_byte is a common child. + if self.lhs.child_mask().test_bit(child_byte) && self.rhs.child_mask().test_bit(child_byte) + { + self.set_child_probe(child_byte, survives); + } } /// Returns whether the child `at` contains anything in the materialized @@ -156,15 +229,19 @@ where survives } + /// Returns whether shared child `at` survives in the materialized subtraction. + /// + /// The result is memoized for the current focus. Temporary movement performed + /// by the probe itself does not disturb the focus-local cache. #[inline] fn subtree_survives(&mut self, at: u8) -> bool { - let survives = self.subtree_survives_uncached(at); - - self.checked_common.set_bit(at); - if survives { - self.surviving_common.set_bit(at); + if self.checked_common.test_bit(at) { + return self.surviving_common.test_bit(at); } + let survives = self.subtree_survives_uncached(at); + + self.set_child_probe(at, survives); survives } @@ -173,10 +250,10 @@ where let common = lhs_mask & self.rhs.child_mask(); let mut out = lhs_mask; - // Remove shared children already known not to survive. - // checked_common ^ surviving_common is exactly the set of checked children - // known not to survive. All of them are present in lhs_mask, so XOR removes them. - out ^= self.checked_common ^ self.surviving_common; + + // Checked shared children known to be empty can be removed immediately. + // Bits in surviving_common outside checked_common are intentionally ignored. + out ^= self.checked_common & !self.surviving_common; // Since checked_common ⊆ common, XOR gives exactly the unchecked children. let unchecked = common ^ self.checked_common; @@ -396,7 +473,7 @@ where // Move to the parent without refreshing the virtual zipper state. // Intermediate nodes are not externally observable during this search. - self.ascend_byte_raw(); + self.ascend_raw(1); ascended += 1; // Continue DFS from the next surviving sibling, if one exists. @@ -701,14 +778,15 @@ where } fn ascend(&mut self, steps: usize) -> usize { - if steps == 0 { + let actual_steps = steps.min(self.depth()); + if actual_steps == 0 { return 0; } - let actual_steps = steps.min(self.depth()); - - self.lhs.ascend(actual_steps); - self.rhs.ascend(actual_steps); + // The cached state is valid at entry. Survival can be propagated through + // every ancestor; non-survival is useful only for a single-level ascent, + // which ascend_raw_known() handles internally. + self.ascend_raw_known(actual_steps, self.path_exists()); self.refresh(); actual_steps @@ -719,11 +797,20 @@ where return 0; } + // Survival is monotone upward: once the current subtree survives, every + // ancestor reached while ascending survives as well. + // + // If the current subtree does not survive, that remains true for every + // parent through which this loop continues: continuing means that the parent + // has neither a surviving value nor another surviving child. The first parent + // at which the subtraction may become non-empty is therefore exactly where + // this traversal stops. + let survives = self.path_exists(); let mut ascended = 0; loop { let cur_byte = self.focus_byte().expect("not at root"); - self.ascend_byte_raw(); + self.ascend_raw_known(1, survives); ascended += 1; let stop = self.at_root() @@ -742,17 +829,26 @@ where return 0; } + // Survival is monotone upward. A non-surviving subtree may become + // surviving at an ancestor because that ancestor has its own value. + // Unlike ascend_until(), values are not stopping points here, so we must + // detect that transition before continuing farther upward. + let mut survives = self.path_exists(); let mut ascended = 0; loop { let cur_byte = self.focus_byte().expect("not at root"); - self.ascend_byte_raw(); + self.ascend_raw_known(1, survives); ascended += 1; if self.at_root() || self.has_surviving_sibling(cur_byte) { self.refresh(); return ascended; } + + if !survives { + survives = value_survives::(&self.lhs, &self.rhs); + } } } @@ -760,8 +856,10 @@ where let depth = self.depth(); if depth != 0 { - self.lhs.ascend(depth); - self.rhs.ascend(depth); + // A surviving current subtree proves that the root survives along this path. + // For a non-surviving subtree, multi-level propagation is deliberately + // discarded by ascend_raw_known(). + self.ascend_raw_known(depth, self.path_exists()); self.refresh(); } @@ -802,7 +900,7 @@ where // materialized difference iff something survives below it. self.descend_to_byte_raw(byte); if !subtree_has_difference::(&mut self.lhs, &mut self.rhs) { - self.ascend_byte_raw(); + self.ascend_raw_known(1, false); break; } @@ -884,7 +982,7 @@ where // Both tries contain the structural child, but it exists in the // materialized difference only if something survives below it. if !subtree_has_difference::(&mut self.lhs, &mut self.rhs) { - self.ascend_byte_raw(); + self.ascend_raw_known(1, false); break; } @@ -940,7 +1038,7 @@ where // Move both backing zippers to the parent without refreshing the // virtual state, which is only needed at the final focus. - self.ascend_byte_raw(); + self.ascend_raw(1); match self.surviving_sibling::(cur_byte) { Some(byte) => { @@ -963,7 +1061,7 @@ where // Move both backing zippers to the parent without refreshing the // virtual state, which is only needed at the final focus. - self.ascend_byte_raw(); + self.ascend_raw(1); match self.surviving_sibling::(cur_byte) { Some(byte) => { @@ -999,7 +1097,7 @@ where return false; }; - self.ascend_byte_raw(); + self.ascend_raw(1); ascended += 1; if let Some(byte) = self.surviving_sibling::(cur_byte) { @@ -1048,8 +1146,14 @@ where } if to_ascend != 0 { - self.lhs.ascend(to_ascend); - self.rhs.ascend(to_ascend); + if suffix.is_empty() { + // We stop at this ancestor, so any propagated child fact can be + // consumed immediately by refresh(). + self.ascend_raw_known(to_ascend, self.path_exists()); + } else { + // A subsequent descent would invalidate the propagated probe cache. + self.ascend_raw(to_ascend); + } } if !suffix.is_empty() { self.lhs.descend_to(suffix); @@ -1106,7 +1210,7 @@ where return false; }; - self.ascend_byte_raw(); + self.ascend_raw(1); ascended += 1; if let Some(byte) = self.surviving_sibling::(cur_byte) { From 18e88aae0365f5c784053d9db682c16ab501b703 Mon Sep 17 00:00:00 2001 From: Remy Pierre Bushnell Clarke <60509493+ClarkeRemy@users.noreply.github.com> Date: Fri, 11 Sep 2026 13:50:39 +0900 Subject: [PATCH 07/11] fixed the code to not set a bit that was already cleared. --- src/utils/mod.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 4af2b05a..9230d94c 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -598,6 +598,15 @@ pub trait BitMask { fn not(&self) -> Self where Self: Sized; } +#[inline] const fn u64_4_raw_set_bit(mask : &mut [u64;4], k : u8) { + let idx = (k / 64) as usize; + mask[idx] |= 1 << (k % 64); +} +#[inline] const fn u64_4_raw_clear_bit(mask : &mut [u64;4], k : u8) { + let idx = (k / 64) as usize; + mask[idx] &= !(1 << (k % 64)); +} + impl BitMask for [u64; 4] { #[inline] fn count_bits(&self) -> usize { @@ -616,13 +625,11 @@ impl BitMask for [u64; 4] { } #[inline] fn set_bit(&mut self, k: u8) { - let idx = (k / 64) as usize; - self[idx] |= 1 << (k % 64); + u64_4_raw_set_bit(self, k); } #[inline] fn clear_bit(&mut self, k: u8) { - let idx = (k / 64) as usize; - self[idx] ^= 1 << (k % 64); + u64_4_raw_clear_bit(self, k); } #[inline] fn make_empty(&mut self) { From 729906023cc34af4495f5ae3104718c1da3823dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Rze=C5=BAnicki?= Date: Sat, 12 Sep 2026 03:09:00 +0200 Subject: [PATCH 08/11] SubtractZipper: simplify `has_surviving_sibling` --- src/subtract_zipper.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/subtract_zipper.rs b/src/subtract_zipper.rs index d0d193bd..883b1256 100644 --- a/src/subtract_zipper.rs +++ b/src/subtract_zipper.rs @@ -284,16 +284,16 @@ where /// `cur_byte` is assumed to be a child of the materialized subtraction. fn has_surviving_sibling(&mut self, cur_byte: u8) -> bool { let mut candidates = self.lhs.child_mask(); + candidates.clear_bit(cur_byte); + if candidates.is_empty_mask() { return false; } - if !self.rhs.path_exists() { // The whole LHS subtree survives, so any other LHS child is enough. - return candidates.count_bits() > 1; + return true; } - candidates.clear_bit(cur_byte); let rhs_mask = self.rhs.child_mask(); // Any LHS-only sibling survives wholesale. From 07bb9153283fa3d8c59f41d92714ec7b3c5f2ad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Rze=C5=BAnicki?= Date: Sat, 12 Sep 2026 04:50:02 +0200 Subject: [PATCH 09/11] SubtractZipper: add some tests against fuzzer reports --- src/subtract_zipper.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/subtract_zipper.rs b/src/subtract_zipper.rs index 883b1256..5e27bf08 100644 --- a/src/subtract_zipper.rs +++ b/src/subtract_zipper.rs @@ -1715,4 +1715,28 @@ mod tests { subtract_at(lhs, rhs, path) } ); + + #[test] + fn seek_k_path_exhaustion() { + let lhs = PathMap::from_iter([([10, 20], 1u64), ([10, 21], 2)]); + let rhs = PathMap::from_iter([([10, 20], 1u64), ([10, 21], 2)]); + + let mut zipper = subtract_at(&lhs, &rhs, &[]); + zipper.descend_to_byte(10); + assert!(!zipper.descend_first_k_path(2)); + assert!(!zipper.path_exists(), "cache invalid"); + assert!(!zipper.descend_first_k_path(2)); + assert!(!zipper.path_exists(), "state invalid"); + } + + #[test] + fn to_next_k_path_must_refresh_after_advance_to_next_subtree_fails() { + let lhs = PathMap::from_iter([([10, 20], 1u64), ([10, 21], 2)]); + let rhs = PathMap::from_iter([([10, 20], 1u64), ([10, 21], 2)]); + + let mut zipper = subtract_at(&lhs, &rhs, &[]); + zipper.descend_to([10, 20]); + assert!(!zipper.path_exists(), "state invalid"); + assert!(!zipper.to_next_k_path(2)); + } } From 34c9de8f733826844683ce187dd6fd9d5e9f6268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Rze=C5=BAnicki?= Date: Sat, 12 Sep 2026 20:13:40 +0200 Subject: [PATCH 10/11] SubtractZipper: return `false` if `k == 0` in `descend_first_k_path` --- src/subtract_zipper.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/subtract_zipper.rs b/src/subtract_zipper.rs index 5e27bf08..977f554c 100644 --- a/src/subtract_zipper.rs +++ b/src/subtract_zipper.rs @@ -1232,7 +1232,7 @@ where fn descend_first_k_path_observed(&mut self, k: usize, obs: &mut P) -> bool { if k == 0 { - return true; + return false; } let Some(byte) = self.child_mask.indexed_bit::(0) else { @@ -1716,6 +1716,17 @@ mod tests { } ); + #[test] + fn zero_k_must_not_move() { + let lhs = PathMap::from_iter([([2], 7u64)]); + let rhs = PathMap::new(); + + let mut zipper = subtract_at(&lhs, &rhs, &[]); + zipper.descend_to_byte(2); + assert!(!zipper.descend_first_k_path(0), "zero-k must not move"); + assert_eq!(zipper.path(), [2]); + } + #[test] fn seek_k_path_exhaustion() { let lhs = PathMap::from_iter([([10, 20], 1u64), ([10, 21], 2)]); From 6a9b7f52a415c05f262ef3bbf69f65b660747740 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Rze=C5=BAnicki?= Date: Sat, 12 Sep 2026 20:20:24 +0200 Subject: [PATCH 11/11] SubtractZipper: add new handy constructor --- src/subtract_zipper.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/subtract_zipper.rs b/src/subtract_zipper.rs index 977f554c..b7682c0f 100644 --- a/src/subtract_zipper.rs +++ b/src/subtract_zipper.rs @@ -75,6 +75,12 @@ where this } + pub fn new_at_path(mut lhs: A, mut rhs: B, path: &[u8]) -> Self { + lhs.descend_to(path); + rhs.descend_to(path); + Self::new(lhs, rhs) + } + /// Invalidates all shared-child probe facts for the current focus. /// /// Clearing checked_common is sufficient: surviving_common is only read for @@ -1732,8 +1738,7 @@ mod tests { let lhs = PathMap::from_iter([([10, 20], 1u64), ([10, 21], 2)]); let rhs = PathMap::from_iter([([10, 20], 1u64), ([10, 21], 2)]); - let mut zipper = subtract_at(&lhs, &rhs, &[]); - zipper.descend_to_byte(10); + let mut zipper = subtract_at(&lhs, &rhs, &[10]); assert!(!zipper.descend_first_k_path(2)); assert!(!zipper.path_exists(), "cache invalid"); assert!(!zipper.descend_first_k_path(2));