@@ -187,6 +187,93 @@ impl NiblePath {
187187 pub const fn packed ( self ) -> ( u64 , u8 ) {
188188 ( self . path , self . depth )
189189 }
190+
191+ /// Is this path a descendant-or-equal of `other`? — the symmetric form of
192+ /// [`is_ancestor_of`]. `self.is_descendant_of(other)` is equivalent to
193+ /// `other.is_ancestor_of(self)` BUT the form is sometimes more natural at
194+ /// the call site (e.g. iterating over candidate ancestors).
195+ ///
196+ /// Like [`is_ancestor_of`], the empty path is never a descendant of
197+ /// anything.
198+ #[ must_use]
199+ pub const fn is_descendant_of ( self , other : Self ) -> bool {
200+ other. is_ancestor_of ( self )
201+ }
202+
203+ /// Are `self` and `other` siblings — distinct paths that share the SAME
204+ /// parent (and thus the same depth)? Returns `false` if either is the
205+ /// basin (depth 1 — basins have no parent in this tree), if the depths
206+ /// differ, or if the paths are equal.
207+ ///
208+ /// Together with [`is_ancestor_of`] / [`is_descendant_of`] this exposes
209+ /// the three structural relations the Pearl-junction classifier
210+ /// (`crate::pearl_junction`) needs without forcing the caller to do its
211+ /// own bit-shift arithmetic.
212+ #[ must_use]
213+ pub const fn is_sibling_of ( self , other : Self ) -> bool {
214+ if self . depth != other. depth || self . depth <= 1 || self . path == other. path {
215+ return false ;
216+ }
217+ // Same depth + same parent ⇔ matching top (depth−1) nibbles ⇔
218+ // matching all bits except the low 4 (the leaf nibble).
219+ const LEAF_MASK : u64 = !0x0F_u64 ;
220+ ( self . path & LEAF_MASK ) == ( other. path & LEAF_MASK )
221+ }
222+
223+ /// The longest common ancestor path — the longest prefix shared by
224+ /// `self` and `other`. `None` if the two paths share no basin (they
225+ /// live in disjoint DOLCE-facet subtrees, OR either is the empty path).
226+ ///
227+ /// Symmetric in its arguments: `a.common_ancestor(b) == b.common_ancestor(a)`.
228+ ///
229+ /// O(depth) — at most `MAX_DEPTH` nibble-shifts in the worst case.
230+ #[ must_use]
231+ pub const fn common_ancestor ( self , other : Self ) -> Option < Self > {
232+ if self . depth == 0 || other. depth == 0 {
233+ return None ;
234+ }
235+ // Align both paths to the shallower depth, then walk up until the
236+ // packed prefixes agree. Once we reach depth 0 without a match,
237+ // the two paths share no basin.
238+ let mut a_path = self . path ;
239+ let mut a_depth = self . depth ;
240+ let mut b_path = other. path ;
241+ let mut b_depth = other. depth ;
242+ while a_depth > b_depth {
243+ a_path >>= 4 ;
244+ a_depth -= 1 ;
245+ }
246+ while b_depth > a_depth {
247+ b_path >>= 4 ;
248+ b_depth -= 1 ;
249+ }
250+ // Same depth now. Walk up until the bits match.
251+ while a_path != b_path {
252+ if a_depth <= 1 {
253+ // Reaching depth 0 means the paths share no basin; reaching
254+ // depth 1 with no match means the basins themselves differ.
255+ if a_depth == 1 {
256+ return None ;
257+ }
258+ a_path >>= 4 ;
259+ b_path >>= 4 ;
260+ a_depth -= 1 ;
261+ continue ;
262+ }
263+ a_path >>= 4 ;
264+ b_path >>= 4 ;
265+ a_depth -= 1 ;
266+ }
267+ if a_depth == 0 {
268+ None
269+ } else {
270+ Some ( Self {
271+ path : a_path,
272+ depth : a_depth,
273+ } )
274+ }
275+ }
276+
190277}
191278
192279#[ cfg( test) ]
@@ -323,4 +410,76 @@ mod tests {
323410 "out-of-range nibble is None too"
324411 ) ;
325412 }
413+
414+ #[ test]
415+ fn is_descendant_of_inverse_of_is_ancestor_of ( ) {
416+ let mammal = NiblePath :: root ( 0x1 ) ;
417+ let dog = NiblePath :: root ( 0x1 ) . child ( 0x1 ) ;
418+ let cat = NiblePath :: root ( 0x2 ) ;
419+ assert ! ( dog. is_descendant_of( mammal) ) ;
420+ assert ! ( !mammal. is_descendant_of( dog) ) ;
421+ assert ! ( !dog. is_descendant_of( cat) ) ;
422+ // empty path is never a descendant of anything
423+ assert ! ( !NiblePath :: EMPTY . is_descendant_of( mammal) ) ;
424+ }
425+
426+ #[ test]
427+ fn is_sibling_of_requires_same_parent_distinct_paths ( ) {
428+ let dog = NiblePath :: root ( 0x1 ) . child ( 0x1 ) ;
429+ let cat = NiblePath :: root ( 0x1 ) . child ( 0x2 ) ;
430+ let lance = NiblePath :: root ( 0x1 ) . child ( 0x1 ) ;
431+ // siblings: same parent (mammal), distinct leaf nibbles
432+ assert ! ( dog. is_sibling_of( cat) ) ;
433+ assert ! ( cat. is_sibling_of( dog) ) ;
434+ // not siblings: equal paths
435+ assert ! ( !dog. is_sibling_of( lance) ) ;
436+ // not siblings: different depth
437+ let mammal = NiblePath :: root ( 0x1 ) ;
438+ assert ! ( !dog. is_sibling_of( mammal) ) ;
439+ // not siblings: different parent
440+ let plant = NiblePath :: root ( 0x2 ) . child ( 0x1 ) ;
441+ assert ! ( !dog. is_sibling_of( plant) ) ;
442+ // basins themselves are not siblings (depth 1, no parent)
443+ let b1 = NiblePath :: root ( 0x1 ) ;
444+ let b2 = NiblePath :: root ( 0x2 ) ;
445+ assert ! ( !b1. is_sibling_of( b2) ) ;
446+ }
447+
448+ #[ test]
449+ fn common_ancestor_returns_longest_shared_prefix ( ) {
450+ // (1)(2)(3)(4) and (1)(2)(5)(6) share (1)(2)
451+ let a = NiblePath :: root ( 0x1 ) . child ( 0x2 ) . child ( 0x3 ) . child ( 0x4 ) ;
452+ let b = NiblePath :: root ( 0x1 ) . child ( 0x2 ) . child ( 0x5 ) . child ( 0x6 ) ;
453+ let lca = a. common_ancestor ( b) . unwrap ( ) ;
454+ assert_eq ! ( lca. depth( ) , 2 ) ;
455+ assert_eq ! ( lca. basin( ) , Some ( 0x1 ) ) ;
456+ assert_eq ! ( lca. leaf( ) , Some ( 0x2 ) ) ;
457+ // symmetric
458+ assert_eq ! ( b. common_ancestor( a) , Some ( lca) ) ;
459+ }
460+
461+ #[ test]
462+ fn common_ancestor_handles_different_depths ( ) {
463+ // (1)(2) is an ancestor of (1)(2)(3); LCA should be (1)(2)
464+ let shallow = NiblePath :: root ( 0x1 ) . child ( 0x2 ) ;
465+ let deep = NiblePath :: root ( 0x1 ) . child ( 0x2 ) . child ( 0x3 ) ;
466+ assert_eq ! ( shallow. common_ancestor( deep) , Some ( shallow) ) ;
467+ assert_eq ! ( deep. common_ancestor( shallow) , Some ( shallow) ) ;
468+ }
469+
470+ #[ test]
471+ fn common_ancestor_disjoint_basins_returns_none ( ) {
472+ // different basins → no common ancestor in this tree
473+ let a = NiblePath :: root ( 0x1 ) . child ( 0x2 ) ;
474+ let b = NiblePath :: root ( 0x3 ) . child ( 0x4 ) ;
475+ assert_eq ! ( a. common_ancestor( b) , None ) ;
476+ assert_eq ! ( b. common_ancestor( a) , None ) ;
477+ }
478+
479+ #[ test]
480+ fn common_ancestor_empty_path_returns_none ( ) {
481+ let a = NiblePath :: root ( 0x1 ) ;
482+ assert_eq ! ( a. common_ancestor( NiblePath :: EMPTY ) , None ) ;
483+ assert_eq ! ( NiblePath :: EMPTY . common_ancestor( a) , None ) ;
484+ }
326485}
0 commit comments