Skip to main content

anomstream_core/tree/
node.rs

1//! Tree node algebraic type and packed `NodeRef` reference.
2//!
3//! [`NodeRef`] packs an internal-vs-leaf discriminator and a slot
4//! index into a single `u32`. The high bit (`1 << 31`) marks leaves;
5//! the low 31 bits hold the slot index in the corresponding storage
6//! arena owned by [`NodeStore`].
7//!
8//! [`NodeStore`]: crate::tree::NodeStore
9
10use crate::domain::{BoundingBox, Cut};
11
12/// Packed reference to a tree node.
13///
14/// The high bit discriminates internal (`0`) from leaf (`1`) so node
15/// identity is a single 4-byte field — useful for cache-friendly
16/// storage in [`NodeStore`].
17///
18/// [`NodeStore`]: crate::tree::NodeStore
19///
20/// # Examples
21///
22/// ```
23/// use anomstream_core::ForestBuilder;
24///
25/// let forest = ForestBuilder::<2>::new()
26///     .num_trees(50)
27///     .sample_size(8)
28///     .seed(1)
29///     .build()
30///     .unwrap();
31/// for (tree, _, _) in forest.trees() {
32///     assert!(tree.root().is_none()); // empty forest has no root yet
33/// }
34/// ```
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37pub struct NodeRef(u32);
38
39impl NodeRef {
40    /// Bit set on a leaf reference.
41    pub(crate) const LEAF_BIT: u32 = 1 << 31;
42    /// Mask covering the slot-index bits.
43    pub(crate) const INDEX_MASK: u32 = !Self::LEAF_BIT;
44    /// Largest representable slot index (`(1 << 31) − 1`).
45    pub const MAX_INDEX: u32 = Self::INDEX_MASK;
46
47    /// Build an internal reference from a slot index.
48    ///
49    /// # Panics
50    ///
51    /// Panics in debug builds when `idx > MAX_INDEX`. Internal callers
52    /// always size-check first via the store's capacity.
53    #[must_use]
54    pub(crate) fn internal(idx: u32) -> Self {
55        debug_assert!(idx <= Self::MAX_INDEX, "internal index overflow");
56        Self(idx)
57    }
58
59    /// Build a leaf reference from a slot index.
60    ///
61    /// # Panics
62    ///
63    /// Panics in debug builds when `idx > MAX_INDEX`. Internal callers
64    /// always size-check first via the store's capacity.
65    #[must_use]
66    pub(crate) fn leaf(idx: u32) -> Self {
67        debug_assert!(idx <= Self::MAX_INDEX, "leaf index overflow");
68        Self(idx | Self::LEAF_BIT)
69    }
70
71    /// Whether this reference points to a leaf.
72    #[must_use]
73    #[inline]
74    pub fn is_leaf(self) -> bool {
75        self.0 & Self::LEAF_BIT != 0
76    }
77
78    /// Whether this reference points to an internal node.
79    #[must_use]
80    #[inline]
81    pub fn is_internal(self) -> bool {
82        !self.is_leaf()
83    }
84
85    /// Slot index in the corresponding storage arena.
86    #[must_use]
87    #[inline]
88    pub fn index(self) -> usize {
89        (self.0 & Self::INDEX_MASK) as usize
90    }
91
92    /// Raw packed `u32` representation. Used by [`NodeStore`] for
93    /// child-equality comparisons during sibling lookup.
94    ///
95    /// [`NodeStore`]: crate::tree::NodeStore
96    #[must_use]
97    #[inline]
98    pub(crate) fn raw(self) -> u32 {
99        self.0
100    }
101}
102
103/// Raw internal-node record. Lives inline in the
104/// [`crate::NodeStore`] internal arena — one entry per live
105/// internal node. The bounding box is embedded inline so tree
106/// traversal stays cache-resident.
107#[derive(Debug, Clone, PartialEq)]
108#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
109pub struct InternalData<const D: usize> {
110    /// The hyperplane partitioning the subtree.
111    pub cut: Cut,
112    /// Cached union bounding box of the subtree.
113    pub bbox: BoundingBox<D>,
114    /// Left child (`point[cut.dim] <= cut.value`).
115    pub left: NodeRef,
116    /// Right child (`point[cut.dim] > cut.value`).
117    pub right: NodeRef,
118    /// Parent reference (`None` only at the root).
119    pub parent: Option<NodeRef>,
120    /// Number of leaf descendants.
121    pub mass: u64,
122}
123
124/// Raw leaf-node record. Lives inline in the [`crate::NodeStore`]
125/// leaf arena — one entry per live leaf. Kept small (no bounding
126/// box, no cut) so the leaf arena fits many entries per cache line.
127/// Before the v4 split the leaf arena stored the full `Node<D>`
128/// enum with its internal-variant shape, wasting ~300 bytes per
129/// leaf at `D = 16`.
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
132pub struct LeafData {
133    /// Index into the forest point store.
134    pub point_idx: usize,
135    /// Parent reference (`None` only when the tree contains a
136    /// single leaf at the root).
137    pub parent: Option<NodeRef>,
138    /// Number of stored copies of this point. Always `>= 1`.
139    pub mass: u64,
140}
141
142/// Zero-copy immutable view of a tree node. Returned by
143/// [`crate::NodeStore::view`] — pattern-match to branch on
144/// internal-vs-leaf without cloning the underlying record.
145#[derive(Debug)]
146pub enum NodeView<'a, const D: usize> {
147    /// Reference to an internal node's record.
148    Internal(&'a InternalData<D>),
149    /// Reference to a leaf node's record.
150    Leaf(&'a LeafData),
151}
152
153impl<const D: usize> NodeView<'_, D> {
154    /// Mass of the node (leaf count for internals, copy count for
155    /// leaves).
156    #[must_use]
157    #[inline]
158    pub fn mass(&self) -> u64 {
159        match self {
160            Self::Internal(i) => i.mass,
161            Self::Leaf(l) => l.mass,
162        }
163    }
164
165    /// Parent reference (`None` only for the root).
166    #[must_use]
167    #[inline]
168    pub fn parent(&self) -> Option<NodeRef> {
169        match self {
170            Self::Internal(i) => i.parent,
171            Self::Leaf(l) => l.parent,
172        }
173    }
174
175    /// Whether this is an internal node.
176    #[must_use]
177    #[inline]
178    pub fn is_internal(&self) -> bool {
179        matches!(self, Self::Internal(_))
180    }
181
182    /// Whether this is a leaf.
183    #[must_use]
184    #[inline]
185    pub fn is_leaf(&self) -> bool {
186        matches!(self, Self::Leaf(_))
187    }
188}
189
190/// Zero-copy mutable view of a tree node. Mirrors [`NodeView`] but
191/// hands out `&mut` references for in-place field updates.
192#[derive(Debug)]
193pub enum NodeViewMut<'a, const D: usize> {
194    /// Mutable reference to an internal node's record.
195    Internal(&'a mut InternalData<D>),
196    /// Mutable reference to a leaf node's record.
197    Leaf(&'a mut LeafData),
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn ref_internal_round_trip() {
206        let r = NodeRef::internal(42);
207        assert!(r.is_internal());
208        assert!(!r.is_leaf());
209        assert_eq!(r.index(), 42);
210    }
211
212    #[test]
213    fn ref_leaf_round_trip() {
214        let r = NodeRef::leaf(7);
215        assert!(r.is_leaf());
216        assert!(!r.is_internal());
217        assert_eq!(r.index(), 7);
218    }
219
220    #[test]
221    fn ref_internal_and_leaf_with_same_index_differ() {
222        let i = NodeRef::internal(0);
223        let l = NodeRef::leaf(0);
224        assert_ne!(i, l);
225        assert_eq!(i.index(), l.index());
226    }
227
228    #[test]
229    fn ref_max_index_round_trips() {
230        let r = NodeRef::internal(NodeRef::MAX_INDEX);
231        assert_eq!(r.index(), NodeRef::MAX_INDEX as usize);
232        assert!(r.is_internal());
233    }
234
235    #[test]
236    fn leaf_view_mass_and_parent() {
237        let data = LeafData {
238            point_idx: 0,
239            parent: Some(NodeRef::internal(5)),
240            mass: 3,
241        };
242        let view: NodeView<'_, 2> = NodeView::Leaf(&data);
243        assert_eq!(view.mass(), 3);
244        assert!(view.is_leaf());
245        assert!(!view.is_internal());
246        assert_eq!(view.parent(), Some(NodeRef::internal(5)));
247    }
248}