Skip to main content

augmented_rbtree/
search.rs

1//! Custom, Zero-Allocation Search and Traversal for Augmented Red-Black Trees.
2//!
3//! This module provides highly optimized primitives for executing targeted queries over an
4//! augmented binary search tree. Instead of performing traditional, unguided tree traversals,
5//! this module abstracts structural pruning logic to allow fast, custom-indexed lookups
6//! (such as range queries, interval intersections, or weight-based selections).
7//!
8//! # Architecture & Pruning Philosophy
9//!
10//! Traditional search algorithms tightly couple tree geometry with search criteria. This module
11//! breaks that coupling using a decoupled, policy-driven architecture via the [`InOrderPruningPolicy`] trait:
12//!
13//! 1. **Tree Geometry**: Managed natively by the stateful [`InOrderIter`].
14//! 2. **Pruning Strategy**: Dictated by an external component implementing [`InOrderPruningPolicy`].
15//!    Before descending into a subtree, the iterator asks the policy whether that branch should
16//!    be evaluated or structurally pruned.
17//!
18//! # Memory & Performance Characteristics
19//!
20//! - **Space Complexity**: `O(1)` auxiliary space. The traversal uses a persistent, re-entrant
21//!   state machine ([`InOrderIter`]) tracking structural geometry and the current [`TraversalPhase`].
22//!   It requires **zero allocations**, avoiding heap-allocated backtracking vectors or call stacks.
23//! - **Time Complexity**: Bounds range from `O(log N)` for highly constrained pruning policies
24//!   (e.g., singular key lookups) up to `O(N)` for exhaustive scans.
25//!
26//! # Subtree Constraints & Re-entrancy
27//!
28//! To support flexible querying, [`InOrderIter`] can be bound to a specific `subtree_root`. The iterator
29//! enforces strict structural boundaries: it will **never drift higher** than or escape the bounds of
30//! the initialized subtree. Because it preserves its exact geometric location across invocations,
31//! it can be safely paused, resumed, or used to build higher-level streaming interfaces.
32//!
33//! # Examples
34//!
35//! ```
36//! # use augmented_rbtree::{InOrderPruningPolicy, InOrderIter};
37//! // Define a policy that skips metadata categories
38//! struct MyCustomSearchPolicy;
39//!
40//! impl InOrderPruningPolicy<u64, String, u32> for MyCustomSearchPolicy {
41//!     fn is_match(&self, _k: &u64, _v: &String, stats: &u32) -> bool {
42//!         *stats > 100 // Only match nodes with high augmented weight
43//!     }
44//!     fn should_explore_left(&self, left: (&u64, &String, &u32), _: (&u64, &String, &u32)) -> bool {
45//!         *left.2 > 50 // Prune left child if its subtree total weight is too low
46//!     }
47//!     fn should_explore_right(&self, right: (&u64, &String, &u32), _: (&u64, &String, &u32)) -> bool {
48//!         true // Always check right
49//!     }
50//! }
51//! ```
52
53use core::{borrow::Borrow, marker::PhantomData};
54
55use crate::{
56    AugmentedRBTreeInt, alloc_proxy::proxy::Allocator, augmented_rbtree::TreeLocation,
57    cursor::NavCursor, node::internal_details::NodeRef, policy,
58};
59
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61enum TraversalPhase {
62    Above,
63    Left,
64    Right,
65}
66
67/// A policy trait that separates structural pruning rules from the tree architecture.
68pub trait InOrderPruningPolicy<K, V, S> {
69    /// Evaluates if the current node satisfies the lookup constraints.
70    fn is_match(&self, key: &K, value: &V, stats: &S) -> bool;
71
72    /// Determines if the left child branch should be explored or pruned.
73    fn should_explore_left(&self, left: (&K, &V, &S), current: (&K, &V, &S)) -> bool;
74
75    /// Determines if the right child branch should be explored or pruned.
76    fn should_explore_right(&self, right: (&K, &V, &S), current: (&K, &V, &S)) -> bool;
77}
78
79/// A stateful, direction-aware iterator that performs an in-order Depth-First Search (DFS)
80/// over an augmented binary search tree.
81///
82/// This iterator leverages the physical geometry of the tree structure combined with a persistent
83/// direction state to avoid allocating a backtracking vector or an internal node call stack.
84/// It is completely re-entrant, allowing it to yield intermediate values safely across successive
85/// invocations of `.next()`.
86///
87/// To protect against drifting out of bounds during a targeted search, it enforces a structural
88/// boundary check that prevents the cursor from overshooting the original subtree root node.
89///
90/// # Type Parameters
91/// * `K` - The tree node key type.
92/// * `V` - The tree node value type.
93/// * `S` - The augmented subtree statistics type used by the pruning policy.
94/// * `P` - A type implementing [`InOrderPruningPolicy`] to dictate matching and pruning criteria.
95
96#[derive(Debug)]
97pub struct InOrderIter<'a, K, V, S, P>
98where
99    P: InOrderPruningPolicy<K, V, S>,
100{
101    cur: Option<NodeRef<K, V, S>>,
102    policy: P,
103    subtree_root: Option<NodeRef<K, V, S>>,
104    direction: TraversalPhase,
105    _marker: PhantomData<&'a (K, V, S)>,
106}
107
108impl<'a, K, V, S, P> Iterator for InOrderIter<'a, K, V, S, P>
109where
110    P: InOrderPruningPolicy<K, V, S>,
111{
112    type Item = (&'a K, &'a V, &'a S);
113
114    fn next(&mut self) -> Option<Self::Item> {
115        loop {
116            // Retrieve current node details. If None, the cursor space has been exhausted.
117            let node = self.cur?;
118
119            let (key, value, stats) = unsafe { (node.key(), node.value(), node.stats()) };
120
121            match self.direction {
122                TraversalPhase::Above => {
123                    // Check if we can traverse the left subtree first
124                    if let Some(left_node) = node.left() {
125                        let (left_key, left_value, left_stats) =
126                            unsafe { (left_node.key(), left_node.value(), left_node.stats()) };
127                        if self.policy.should_explore_left(
128                            (left_key, left_value, left_stats),
129                            (key, value, stats),
130                        ) {
131                            self.cur = Some(left_node);
132                            self.direction = TraversalPhase::Above; // Reset direction for the left sub-hierarchy
133                            continue;
134                        }
135                    }
136                    // Left subtree is absent or pruned. We say that we returned to the current from left
137                    self.direction = TraversalPhase::Left;
138                }
139
140                TraversalPhase::Left => {
141                    // We hit the current node from the left child, so we can evaluate it now
142                    // The policy is to yield the node coming from the bottom
143
144                    let is_matching_node = self.policy.is_match(key, value, stats);
145
146                    if let Some(right_node) = node.right() {
147                        let (right_key, right_value, right_stats) =
148                            unsafe { (right_node.key(), right_node.value(), right_node.stats()) };
149                        if self.policy.should_explore_right(
150                            (right_key, right_value, right_stats),
151                            (key, value, stats),
152                        ) {
153                            // OK this tells me I can resume search in the right subtree.
154                            self.cur = Some(right_node);
155                            self.direction = TraversalPhase::Above; // Reset direction for the right sub-hierarchy
156
157                            // Yield the current matching parent node.
158                            // The cursor is staged inside the fresh right subtree for the next loop.
159                            if is_matching_node {
160                                return Some((key, value, stats));
161                            }
162                            continue;
163                        }
164                    }
165
166                    // I am not able to move to the right subtree, so I need to ascend and update the direction state
167                    if is_matching_node {
168                        self.ascend_and_update_state();
169                        return Some((key, value, stats));
170                    }
171
172                    self.ascend_and_update_state();
173                }
174
175                TraversalPhase::Right => {
176                    // Done with both subtrees, we yielded the current node and now we need to ascend to the parent
177                    self.ascend_and_update_state();
178                }
179            }
180        }
181    }
182}
183
184impl<K, V, S, P> InOrderIter<'_, K, V, S, P>
185where
186    P: InOrderPruningPolicy<K, V, S>,
187{
188    /// Constructs a new `InOrderIter` starting at the provided node position.
189    ///
190    /// This method automatically captures the starting node position as the structural ceiling
191    /// for the traversal, ensuring it does not overshoot into adjacent tree families.
192    pub fn new<A, R, Q>(
193        tree: &AugmentedRBTreeInt<K, V, S, A, R>,
194        location: TreeLocation<&Q>,
195        policy: P,
196    ) -> Self
197    where
198        A: Allocator,
199        R: policy::internal_details::TreePolicy<K = K, V = V, S = S>,
200        K: Borrow<Q> + Ord,
201        Q: Ord,
202    {
203        let cur = tree.get_tree_location(location);
204        Self {
205            cur,
206            policy,
207            subtree_root: cur, // Directly passes the option without conditional blocks
208            direction: TraversalPhase::Above,
209            _marker: PhantomData,
210        }
211    }
212
213    /// Constructs a new `InOrderIter` starting at the node currently pointed to by a [`NavCursor`].
214    ///
215    /// This allows power-users to initialize a highly customized pruning search originating from
216    /// any arbitrary bookmark or position in the tree structure.
217    ///
218    pub fn from_cursor(cursor: &NavCursor<'_, K, V, S>, policy: P) -> Self {
219        // Safe bridge: Unpack the internal Option<NodeRef> from the public cursor
220        let starting_node = cursor.current;
221
222        Self {
223            cur: starting_node,
224            policy,
225            subtree_root: starting_node,
226            direction: TraversalPhase::Above,
227            _marker: PhantomData,
228        }
229    }
230
231    /// Shifts the cursor upward by exactly one level while protecting against overshooting
232    /// the designated subtree root boundary.
233    fn ascend_and_update_state(&mut self) {
234        // Stop instantly if the current node matches our initial subtree ceiling.
235        if self.cur == self.subtree_root {
236            self.cur = None; // O(1) instant termination
237            return;
238        }
239
240        if let Some(node) = self.cur {
241            // Invariant: Because we checked the subtree_root boundary above,
242            // this node is guaranteed to have a parent in a valid tree.
243            let parent = node
244                .parent()
245                .expect("Invariant violation: Node must have a parent");
246
247            if parent.left() == Some(node) {
248                self.direction = TraversalPhase::Left;
249            } else {
250                self.direction = TraversalPhase::Right;
251            }
252
253            self.cur = Some(parent);
254        }
255    }
256}