llkv_plan/traversal.rs
1//! Generic iterative traversal utilities for AST-like structures.
2//!
3//! This module provides postorder traversal patterns that avoid stack overflow
4//! on deeply nested expression trees. The approach uses explicit work and result
5//! stacks instead of recursion, which is critical for handling complex SQL queries
6//! (e.g., 55,000+ line files with deeply nested WHERE clauses).
7//!
8//! # Two Approaches
9//!
10//! ## 1. Trait-Based Traversal (for borrowed trees)
11//!
12//! Use [`Traversable`] and [`traverse_postorder`] when you have a tree of borrowed
13//! nodes and want to compute a result without consuming the tree.
14//!
15//! ## 2. Manual Frame Pattern (for owned/consumed trees)
16//!
17//! When transforming owned trees (consuming input, producing output), use the
18//! manual Frame pattern with these helpers:
19//!
20//! - Define a `Frame` enum with `Enter(InputNode)` and `Exit*` variants
21//! - Use `Vec<Frame>` as work_stack, `Vec<OutputNode>` as result_stack
22//! - Process frames in a loop, building results in postorder
23//!
24//! See existing implementations in:
25//! - `llkv-sql/src/sql_engine.rs`: `translate_condition_with_context`
26//! - `llkv-executor/src/translation/expression.rs`: `translate_predicate_with`
27//!
28//! # Example (Trait-Based)
29//!
30//! ```
31//! use llkv_plan::traversal::{Traversable, traverse_postorder};
32//! use llkv_result::Result as LlkvResult;
33//!
34//! enum MyExpr {
35//! Add(Box<MyExpr>, Box<MyExpr>),
36//! Value(i32),
37//! }
38//!
39//! impl Traversable for MyExpr {
40//! type Output = i32;
41//!
42//! fn visit_children(&self) -> LlkvResult<Vec<&Self>> {
43//! match self {
44//! MyExpr::Add(left, right) => Ok(vec![left.as_ref(), right.as_ref()]),
45//! MyExpr::Value(_) => Ok(vec![]),
46//! }
47//! }
48//!
49//! fn construct(&self, children: Vec<i32>) -> LlkvResult<i32> {
50//! match self {
51//! MyExpr::Add(_, _) => Ok(children[0] + children[1]),
52//! MyExpr::Value(v) => Ok(*v),
53//! }
54//! }
55//! }
56//!
57//! let expr = MyExpr::Add(
58//! Box::new(MyExpr::Value(2)),
59//! Box::new(MyExpr::Value(3)),
60//! );
61//! assert_eq!(traverse_postorder(&expr).unwrap(), 5);
62//! ```
63//!
64//! # Example (TransformFrame Pattern)
65//!
66//! ```
67//! use llkv_plan::TransformFrame;
68//!
69//! #[derive(Clone)]
70//! enum InputExpr {
71//! Binary { left: Box<InputExpr>, op: BinaryOp, right: Box<InputExpr> },
72//! Unary { op: UnaryOp, expr: Box<InputExpr> },
73//! Value(i32),
74//! }
75//!
76//! #[derive(Clone)]
77//! enum BinaryOp { Add, Subtract }
78//!
79//! #[derive(Clone)]
80//! enum UnaryOp { Negate }
81//!
82//! enum OutputExpr {
83//! Binary { left: Box<OutputExpr>, op: BinaryOp, right: Box<OutputExpr> },
84//! Unary { op: UnaryOp, expr: Box<OutputExpr> },
85//! Value(i32),
86//! }
87//!
88//! enum ExprExitContext {
89//! Binary(BinaryOp),
90//! Unary,
91//! }
92//!
93//! type ExprFrame<'a> = TransformFrame<'a, InputExpr, OutputExpr, ExprExitContext>;
94//!
95//! fn transform_expr(root_expr: &InputExpr) -> OutputExpr {
96//! let mut work_stack = vec![ExprFrame::Enter(root_expr)];
97//! let mut result_stack = Vec::new();
98//!
99//! while let Some(frame) = work_stack.pop() {
100//! match frame {
101//! ExprFrame::Enter(node) => match node {
102//! InputExpr::Binary { left, op, right } => {
103//! work_stack.push(ExprFrame::Exit(ExprExitContext::Binary(op.clone())));
104//! work_stack.push(ExprFrame::Enter(right));
105//! work_stack.push(ExprFrame::Enter(left));
106//! }
107//! InputExpr::Unary { op: _, expr } => {
108//! work_stack.push(ExprFrame::Exit(ExprExitContext::Unary));
109//! work_stack.push(ExprFrame::Enter(expr));
110//! }
111//! InputExpr::Value(v) => {
112//! work_stack.push(ExprFrame::Leaf(OutputExpr::Value(*v)));
113//! }
114//! },
115//! ExprFrame::Exit(exit_context) => match exit_context {
116//! ExprExitContext::Binary(op) => {
117//! let right = result_stack.pop().unwrap();
118//! let left = result_stack.pop().unwrap();
119//! result_stack.push(OutputExpr::Binary {
120//! left: Box::new(left),
121//! op,
122//! right: Box::new(right),
123//! });
124//! }
125//! ExprExitContext::Unary => {
126//! let expr = result_stack.pop().unwrap();
127//! result_stack.push(OutputExpr::Unary {
128//! op: UnaryOp::Negate,
129//! expr: Box::new(expr),
130//! });
131//! }
132//! },
133//! ExprFrame::Leaf(output) => {
134//! result_stack.push(output);
135//! }
136//! }
137//! }
138//!
139//! result_stack.pop().unwrap()
140//! }
141//! # transform_expr(&InputExpr::Value(42));
142//! ```
143
144use llkv_result::Result as LlkvResult;
145
146/// Internal frame type used by [`traverse_postorder`] for borrowed tree traversal.
147///
148/// This is distinct from [`TransformFrame`] because it's used internally by the
149/// trait-based traversal function and tracks both the node reference and expected
150/// child count for the Exit variant.
151///
152/// Not exported - users should use [`TransformFrame`] for custom traversals.
153enum TraversalFrame<'a, T> {
154 Enter(&'a T),
155 Exit(&'a T, usize), // node and expected child count
156}
157
158/// A frame in the traversal work stack for tree transformations.
159///
160/// Used in manual iterative traversals that consume input nodes and produce
161/// transformed output nodes. This enum provides the common structure while
162/// allowing implementations to define custom exit variants for operation-specific
163/// context (e.g., which binary operator triggered the exit).
164///
165/// The pattern avoids stack overflow on deeply nested trees (50k+ nodes) by using
166/// explicit work and result stacks instead of recursion.
167///
168/// # Type Parameters
169/// * `Input` - The input node type being consumed during traversal
170/// * `Output` - The result type being built on the result stack
171/// * `ExitContext` - Custom enum carrying operation-specific exit state
172///
173/// # Traversal Pattern
174///
175/// ```rust
176/// use llkv_plan::TransformFrame;
177/// # use llkv_result::Result;
178/// # type SqlExpr = String;
179/// # type OutputExpr = i32;
180/// # #[derive(Clone)]
181/// # enum BinaryOp { Add, Subtract }
182///
183/// // Define operation-specific exit variants
184/// enum ScalarExitContext {
185/// BinaryOp { op: BinaryOp },
186/// UnaryMinus,
187/// }
188///
189/// type ScalarTransformFrame<'a> = TransformFrame<'a, SqlExpr, OutputExpr, ScalarExitContext>;
190///
191/// fn transform_scalar(expr: &SqlExpr) -> Result<OutputExpr> {
192/// let mut work_stack: Vec<ScalarTransformFrame> = vec![ScalarTransformFrame::Enter(expr)];
193/// let mut result_stack: Vec<OutputExpr> = Vec::new();
194///
195/// while let Some(frame) = work_stack.pop() {
196/// match frame {
197/// ScalarTransformFrame::Enter(node) => {
198/// // Decompose node, push exit frame, then push children
199/// # work_stack.push(ScalarTransformFrame::Leaf(42));
200/// }
201/// ScalarTransformFrame::Exit(ScalarExitContext::BinaryOp { op }) => {
202/// // Pop children from result_stack, combine, push result
203/// # let right = result_stack.pop().unwrap();
204/// # let left = result_stack.pop().unwrap();
205/// # result_stack.push(left + right);
206/// }
207/// ScalarTransformFrame::Exit(ScalarExitContext::UnaryMinus) => {
208/// // Pop single child, transform, push result
209/// # let val = result_stack.pop().unwrap();
210/// # result_stack.push(-val);
211/// }
212/// ScalarTransformFrame::Leaf(output) => {
213/// result_stack.push(output);
214/// }
215/// }
216/// }
217///
218/// Ok(result_stack.pop().unwrap())
219/// }
220/// # transform_scalar(&"test".to_string()).unwrap();
221/// ```
222///
223/// # See Also
224///
225/// Concrete implementations using this pattern:
226/// - `llkv-sql/src/sql_engine.rs`: `translate_condition_with_context`, `translate_scalar`
227/// - `llkv-executor/src/translation/expression.rs`: `translate_predicate_with`
228pub enum TransformFrame<'a, Input, Output, ExitContext> {
229 /// Begin visiting an input node (descend to children).
230 Enter(&'a Input),
231
232 /// Complete processing of a node (combine child results).
233 ///
234 /// The `ExitContext` carries operation-specific state needed to combine
235 /// child results into the parent node's result. For example, in arithmetic
236 /// expression evaluation, this might carry which operator to apply.
237 Exit(ExitContext),
238
239 /// A leaf node that has been fully transformed to output.
240 ///
241 /// Used when a node can be immediately converted without further traversal
242 /// (e.g., literals, pre-resolved identifiers).
243 Leaf(Output),
244}
245
246/// Trait for types that support iterative postorder traversal.
247///
248/// Implementors define:
249/// 1. How to decompose a node into children (in `visit_children`)
250/// 2. How to reconstruct a result from child results (in `construct`)
251///
252/// The traversal guarantees postorder: children are fully processed before
253/// their parent's `construct` is called.
254pub trait Traversable: Sized {
255 /// The result type produced by traversing this tree.
256 type Output;
257
258 /// Visit each child of this node.
259 ///
260 /// Called when first encountering a node. Implementations should:
261 /// - Return an iterator over child references
262 /// - Return an empty iterator for leaf nodes
263 ///
264 /// # Returns
265 /// An iterator over references to child nodes, or an error if the node is malformed.
266 fn visit_children(&self) -> LlkvResult<Vec<&Self>>;
267
268 /// Reconstruct this node's result from processed children.
269 ///
270 /// Called after all children have been processed. The `children` vec
271 /// contains child results in the same order they were visited.
272 ///
273 /// # Arguments
274 /// * `children` - Results from child nodes, in visit order
275 ///
276 /// # Returns
277 /// The reconstructed result for this node.
278 fn construct(&self, children: Vec<Self::Output>) -> LlkvResult<Self::Output>;
279}
280
281/// Performs iterative postorder traversal of a tree structure.
282///
283/// This function avoids stack overflow by using explicit work and result stacks
284/// instead of recursion. It's designed for deeply nested ASTs common in complex
285/// SQL queries.
286///
287/// # Type Parameters
288/// * `T` - The node type (must implement [`Traversable`])
289///
290/// # Arguments
291/// * `root` - The root node to traverse
292///
293/// # Returns
294/// The result produced by traversing the entire tree.
295///
296/// # Errors
297/// Returns errors from `visit_children` or `construct` implementations, or if the
298/// traversal encounters an inconsistent state (e.g., result stack underflow).
299///
300/// # Performance
301/// Stack usage is O(tree depth), memory usage is O(tree depth + node count).
302/// For very deep trees (50k+ nesting), thread stack size should be >= 16MB.
303pub fn traverse_postorder<T>(root: &T) -> LlkvResult<T::Output>
304where
305 T: Traversable,
306{
307 let mut work_stack: Vec<TraversalFrame<T>> = vec![TraversalFrame::Enter(root)];
308 let mut result_stack: Vec<T::Output> = Vec::new();
309
310 while let Some(frame) = work_stack.pop() {
311 match frame {
312 TraversalFrame::Enter(node) => {
313 let children = node.visit_children()?;
314 let child_count = children.len();
315
316 // Schedule exit frame first
317 work_stack.push(TraversalFrame::Exit(node, child_count));
318
319 // Then push children in reverse order for correct postorder
320 for child in children.into_iter().rev() {
321 work_stack.push(TraversalFrame::Enter(child));
322 }
323 }
324 TraversalFrame::Exit(node, child_count) => {
325 // Collect child results from result stack
326 if result_stack.len() < child_count {
327 return Err(llkv_result::Error::Internal(
328 "traverse_postorder: result stack underflow".into(),
329 ));
330 }
331
332 let start = result_stack.len() - child_count;
333 let child_results: Vec<T::Output> = result_stack.drain(start..).collect();
334
335 // Process this node with its children's results
336 let output = node.construct(child_results)?;
337 result_stack.push(output);
338 }
339 }
340 }
341
342 // Should have exactly one result
343 if result_stack.len() != 1 {
344 return Err(llkv_result::Error::Internal(format!(
345 "traverse_postorder: expected 1 result, got {}",
346 result_stack.len()
347 )));
348 }
349
350 result_stack.pop().ok_or_else(|| {
351 llkv_result::Error::Internal("traverse_postorder: empty result stack".into())
352 })
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 // Simple expression tree for testing
360 #[derive(Debug, Clone, PartialEq)]
361 enum TestExpr {
362 Add(Box<TestExpr>, Box<TestExpr>),
363 Mul(Box<TestExpr>, Box<TestExpr>),
364 Value(i32),
365 }
366
367 impl Traversable for TestExpr {
368 type Output = i32;
369
370 fn visit_children(&self) -> LlkvResult<Vec<&Self>> {
371 match self {
372 TestExpr::Add(left, right) | TestExpr::Mul(left, right) => {
373 Ok(vec![left.as_ref(), right.as_ref()])
374 }
375 TestExpr::Value(_) => Ok(vec![]),
376 }
377 }
378
379 fn construct(&self, children: Vec<i32>) -> LlkvResult<i32> {
380 match self {
381 TestExpr::Add(_, _) => Ok(children[0] + children[1]),
382 TestExpr::Mul(_, _) => Ok(children[0] * children[1]),
383 TestExpr::Value(v) => Ok(*v),
384 }
385 }
386 }
387
388 #[test]
389 fn test_simple_evaluation() {
390 // 2 + 3 = 5
391 let expr = TestExpr::Add(Box::new(TestExpr::Value(2)), Box::new(TestExpr::Value(3)));
392 assert_eq!(traverse_postorder(&expr).unwrap(), 5);
393 }
394
395 #[test]
396 fn test_nested_evaluation() {
397 // (2 + 3) * 4 = 20
398 let expr = TestExpr::Mul(
399 Box::new(TestExpr::Add(
400 Box::new(TestExpr::Value(2)),
401 Box::new(TestExpr::Value(3)),
402 )),
403 Box::new(TestExpr::Value(4)),
404 );
405 assert_eq!(traverse_postorder(&expr).unwrap(), 20);
406 }
407
408 #[test]
409 fn test_deeply_nested() {
410 // Build a deeply nested expression: 1 + 1 + 1 + ... (1000 times)
411 let mut expr = TestExpr::Value(1);
412 for _ in 0..1000 {
413 expr = TestExpr::Add(Box::new(expr), Box::new(TestExpr::Value(1)));
414 }
415 assert_eq!(traverse_postorder(&expr).unwrap(), 1001);
416 }
417}