exp_rs/eval/
iterative.rs

1//! Iterative AST evaluator
2//!
3//! This module implements an iterative (non-recursive) AST evaluator using
4//! an explicit stack. This approach eliminates stack overflow issues and
5//! provides better performance for deeply nested expressions.
6
7use crate::Real;
8use crate::context::EvalContext;
9use crate::error::ExprError;
10use crate::eval::context_stack::ContextStack;
11use crate::eval::stack_ops::EvalOp;
12use crate::eval::types::{FunctionCacheEntry, OwnedNativeFunction};
13use crate::types::{AstExpr, FunctionName, HString};
14use crate::types::{TryIntoFunctionName, TryIntoHeaplessString};
15
16use alloc::collections::BTreeMap;
17use alloc::format;
18use alloc::rc::Rc;
19use alloc::string::ToString;
20use alloc::vec::Vec;
21
22/// Maximum depth of the operation stack (prevents runaway evaluation)
23const MAX_STACK_DEPTH: usize = 1000;
24
25/// Main iterative evaluation function
26pub fn eval_iterative<'arena>(
27    ast: &'arena AstExpr<'arena>,
28    ctx: Option<Rc<EvalContext>>,
29    arena: &'arena bumpalo::Bump,
30) -> Result<Real, ExprError> {
31    let mut engine = EvalEngine::new(arena);
32    engine.eval(ast, ctx)
33}
34
35/// Reusable evaluation engine to avoid allocations
36pub struct EvalEngine<'arena> {
37    /// Optional arena for parsing expression functions on-demand
38    arena: Option<&'arena bumpalo::Bump>,
39
40    /// Operation stack (arena-allocated when arena is available)
41    op_stack: bumpalo::collections::Vec<'arena, EvalOp<'arena>>,
42    /// Value stack for intermediate results (arena-allocated when arena is available)
43    value_stack: bumpalo::collections::Vec<'arena, Real>,
44    /// Shared buffer for function arguments to avoid per-call allocations
45    arg_buffer: bumpalo::collections::Vec<'arena, Real>,
46
47    /// Track high water marks for capacity optimization
48    op_stack_hwm: usize,
49    value_stack_hwm: usize,
50    arg_buffer_hwm: usize,
51
52    /// Context management
53    ctx_stack: ContextStack,
54    /// Function cache
55    func_cache: BTreeMap<HString, Option<FunctionCacheEntry>>,
56    /// Parameter overrides for batch evaluation (avoids context modification)
57    param_overrides: Option<crate::types::BatchParamMap>,
58    /// Optional reference to local expression functions
59    local_functions: Option<&'arena core::cell::RefCell<crate::types::ExpressionFunctionMap>>,
60    /// Cache for parsed expression functions
61    expr_func_cache: BTreeMap<HString, &'arena AstExpr<'arena>>,
62}
63
64// Note: Default trait removed since EvalEngine now requires an arena parameter
65
66impl<'arena> EvalEngine<'arena> {
67    /// Create a new evaluation engine with arena for zero-allocation evaluation
68    pub fn new(arena: &'arena bumpalo::Bump) -> Self {
69        Self {
70            arena: Some(arena),
71
72            op_stack: bumpalo::collections::Vec::new_in(arena),
73            value_stack: bumpalo::collections::Vec::new_in(arena),
74            arg_buffer: bumpalo::collections::Vec::new_in(arena),
75
76            // Initialize high water marks
77            op_stack_hwm: 0,
78            value_stack_hwm: 0,
79            arg_buffer_hwm: 0,
80
81            // Other fields
82            ctx_stack: ContextStack::new(),
83            func_cache: BTreeMap::new(),
84            param_overrides: None,
85            local_functions: None,
86            expr_func_cache: BTreeMap::new(),
87        }
88    }
89
90    /// Set the local expression functions for this evaluation
91    pub fn set_local_functions(
92        &mut self,
93        functions: Option<&'arena core::cell::RefCell<crate::types::ExpressionFunctionMap>>,
94    ) {
95        self.local_functions = functions;
96    }
97
98    /// Arena-aware clearing of internal stacks
99    /// Uses unsafe set_len(0) to avoid triggering Drop on arena-allocated elements
100    fn arena_clear_stacks(&mut self) {
101        // SAFETY: For arena-allocated vectors, we can safely set length to 0
102        // without dropping elements since the arena handles all cleanup
103        unsafe {
104            self.op_stack.set_len(0);
105            self.value_stack.set_len(0);
106            self.arg_buffer.set_len(0);
107        }
108    }
109
110    /// Reset the engine for reuse with new expression
111    /// More comprehensive than arena_clear_stacks - also clears caches and context
112    pub fn arena_reset(&mut self) {
113        self.arena_clear_stacks();
114        self.ctx_stack.clear();
115        self.func_cache.clear();
116        self.expr_func_cache.clear();
117
118        // Reset high water marks
119        self.op_stack_hwm = 0;
120        self.value_stack_hwm = 0;
121        self.arg_buffer_hwm = 0;
122    }
123
124    /// Evaluate an expression
125    pub fn eval(
126        &mut self,
127        ast: &'arena AstExpr<'arena>,
128        ctx: Option<Rc<EvalContext>>,
129    ) -> Result<Real, ExprError> {
130        // Clear stacks efficiently for arena allocation
131        self.arena_clear_stacks();
132        self.ctx_stack.clear();
133        self.func_cache.clear();
134
135        // Initialize with root context
136        let root_ctx_id = self.ctx_stack.push_context(ctx)?;
137
138        // Push initial operation (no clone needed - just use reference!)
139        self.op_stack.push(EvalOp::Eval {
140            expr: ast,
141            ctx_id: root_ctx_id,
142        });
143
144        // Main evaluation loop
145        while let Some(op) = self.op_stack.pop() {
146            // Check depth limit
147            if self.op_stack.len() > MAX_STACK_DEPTH {
148                return Err(ExprError::RecursionLimit(format!(
149                    "Maximum evaluation depth {} exceeded",
150                    MAX_STACK_DEPTH
151                )));
152            }
153
154            self.process_operation(op)?;
155        }
156
157        // Result should be on top of value stack
158        self.value_stack
159            .pop()
160            .ok_or_else(|| ExprError::Other("No result on value stack".to_string()))
161    }
162
163    /// Process a single operation
164    fn process_operation(&mut self, op: EvalOp<'arena>) -> Result<(), ExprError> {
165        match op {
166            EvalOp::Eval { expr, ctx_id } => {
167                self.process_eval(expr, ctx_id)?;
168            }
169
170            EvalOp::ApplyUnary { op } => {
171                let operand = self.pop_value()?;
172                self.value_stack.push(op.apply(operand));
173            }
174
175            EvalOp::CompleteBinary { op } => {
176                let right = self.pop_value()?;
177                let left = self.pop_value()?;
178                self.value_stack.push(op.apply(left, right));
179            }
180
181            EvalOp::ShortCircuitAnd { right_expr, ctx_id } => {
182                let left_val = self.pop_value()?;
183                if left_val == 0.0 {
184                    // Short circuit - left is false, result is false
185                    self.value_stack.push(0.0);
186                } else {
187                    // Need to evaluate right side
188                    self.op_stack.push(EvalOp::CompleteAnd);
189                    self.op_stack.push(EvalOp::Eval {
190                        expr: right_expr,
191                        ctx_id,
192                    });
193                }
194            }
195
196            EvalOp::ShortCircuitOr { right_expr, ctx_id } => {
197                let left_val = self.pop_value()?;
198                if left_val != 0.0 {
199                    // Short circuit - left is true, result is true
200                    self.value_stack.push(1.0);
201                } else {
202                    // Need to evaluate right side
203                    self.op_stack.push(EvalOp::CompleteOr);
204                    self.op_stack.push(EvalOp::Eval {
205                        expr: right_expr,
206                        ctx_id,
207                    });
208                }
209            }
210
211            EvalOp::CompleteAnd => {
212                let right = self.pop_value()?;
213                let left = 1.0; // We know left was true or we wouldn't be here
214                self.value_stack.push(if left != 0.0 && right != 0.0 {
215                    1.0
216                } else {
217                    0.0
218                });
219            }
220
221            EvalOp::CompleteOr => {
222                let right = self.pop_value()?;
223                let left = 0.0; // We know left was false or we wouldn't be here
224                self.value_stack.push(if left != 0.0 || right != 0.0 {
225                    1.0
226                } else {
227                    0.0
228                });
229            }
230
231            EvalOp::LookupVariable { name, ctx_id } => {
232                self.process_variable_lookup(name, ctx_id)?;
233            }
234
235            EvalOp::TernaryCondition {
236                true_branch,
237                false_branch,
238                ctx_id,
239            } => {
240                let condition = self.pop_value()?;
241                if condition != 0.0 {
242                    self.op_stack.push(EvalOp::Eval {
243                        expr: true_branch,
244                        ctx_id,
245                    });
246                } else {
247                    self.op_stack.push(EvalOp::Eval {
248                        expr: false_branch,
249                        ctx_id,
250                    });
251                }
252            }
253
254            EvalOp::AccessArray { array_name, ctx_id } => {
255                let index = self.pop_value()?;
256                self.process_array_access(array_name, index, ctx_id)?;
257            }
258
259            EvalOp::AccessAttribute {
260                object_name,
261                attr_name,
262                ctx_id,
263            } => {
264                self.process_attribute_access(object_name, attr_name, ctx_id)?;
265            }
266
267            EvalOp::ApplyFunction {
268                name,
269                arg_count,
270                ctx_id,
271            } => {
272                self.process_function_call(name, arg_count, ctx_id)?;
273            }
274
275            EvalOp::RestoreFunctionParams { params: _ } => {
276                // No-op: params are scoped to operations on stack
277                // When this operation is popped, the parameters are automatically cleaned up
278            }
279        }
280
281        Ok(())
282    }
283
284    /// Process an Eval operation by converting AST to stack operations
285    fn process_eval(
286        &mut self,
287        expr: &'arena AstExpr<'arena>,
288        ctx_id: usize,
289    ) -> Result<(), ExprError> {
290        match expr {
291            AstExpr::Constant(val) => {
292                self.value_stack.push(*val);
293            }
294
295            AstExpr::Variable(name) => {
296                let hname = name.try_into_heapless()?;
297                self.op_stack.push(EvalOp::LookupVariable {
298                    name: hname,
299                    ctx_id,
300                });
301            }
302
303            AstExpr::LogicalOp { op, left, right } => {
304                // Handle short-circuit operators
305                use crate::types::LogicalOperator;
306                match op {
307                    LogicalOperator::And => {
308                        self.op_stack.push(EvalOp::ShortCircuitAnd {
309                            right_expr: right,
310                            ctx_id,
311                        });
312                        self.op_stack.push(EvalOp::Eval { expr: left, ctx_id });
313                    }
314                    LogicalOperator::Or => {
315                        self.op_stack.push(EvalOp::ShortCircuitOr {
316                            right_expr: right,
317                            ctx_id,
318                        });
319                        self.op_stack.push(EvalOp::Eval { expr: left, ctx_id });
320                    }
321                }
322            }
323
324            AstExpr::Conditional {
325                condition,
326                true_branch,
327                false_branch,
328            } => {
329                self.op_stack.push(EvalOp::TernaryCondition {
330                    true_branch,
331                    false_branch,
332                    ctx_id,
333                });
334                self.op_stack.push(EvalOp::Eval {
335                    expr: condition,
336                    ctx_id,
337                });
338            }
339
340            AstExpr::Function { name, args } => {
341                // Special handling for short-circuit operators
342                match (*name, args.len()) {
343                    ("&&", 2) => {
344                        // Short-circuit AND: evaluate left first, then right only if left is true
345                        self.op_stack.push(EvalOp::ShortCircuitAnd {
346                            right_expr: &args[1],
347                            ctx_id,
348                        });
349                        self.op_stack.push(EvalOp::Eval {
350                            expr: &args[0],
351                            ctx_id,
352                        });
353                    }
354                    ("||", 2) => {
355                        // Short-circuit OR: evaluate left first, then right only if left is false
356                        self.op_stack.push(EvalOp::ShortCircuitOr {
357                            right_expr: &args[1],
358                            ctx_id,
359                        });
360                        self.op_stack.push(EvalOp::Eval {
361                            expr: &args[0],
362                            ctx_id,
363                        });
364                    }
365                    _ => {
366                        // All other function calls go through the same path to support overrides
367                        // The parser represents operators like ^, +, -, etc. as function calls
368                        // So we treat them all uniformly to allow user overrides
369                        let fname = name.try_into_function_name()?;
370
371                        // Push function application operation
372                        // This will execute after all arguments are evaluated
373                        self.op_stack.push(EvalOp::ApplyFunction {
374                            name: fname,
375                            arg_count: args.len(),
376                            ctx_id,
377                        });
378
379                        // Push argument evaluations in reverse order
380                        // (they'll be evaluated left-to-right, results accumulate on value stack)
381                        for arg in args.iter().rev() {
382                            self.op_stack.push(EvalOp::Eval { expr: arg, ctx_id });
383                        }
384                    }
385                }
386            }
387
388            AstExpr::Array { name, index } => {
389                let array_name = name.try_into_heapless()?;
390
391                self.op_stack
392                    .push(EvalOp::AccessArray { array_name, ctx_id });
393                self.op_stack.push(EvalOp::Eval {
394                    expr: index,
395                    ctx_id,
396                });
397            }
398
399            AstExpr::Attribute { base, attr } => {
400                let obj_name = base.try_into_heapless()?;
401                let attr_name = attr.try_into_heapless()?;
402
403                self.op_stack.push(EvalOp::AccessAttribute {
404                    object_name: obj_name,
405                    attr_name,
406                    ctx_id,
407                });
408            }
409        }
410
411        Ok(())
412    }
413
414    /// Process variable lookup
415    fn process_variable_lookup(&mut self, name: HString, ctx_id: usize) -> Result<(), ExprError> {
416        // Check operation stack for function parameters first (walk backwards for shadowing)
417        for op in self.op_stack.iter().rev() {
418            if let EvalOp::RestoreFunctionParams {
419                params: Some(params),
420            } = op
421            {
422                for (param_name, value) in params.iter() {
423                    if param_name == &name {
424                        self.value_stack.push(*value);
425                        return Ok(());
426                    }
427                }
428            }
429        }
430
431        // Check parameter overrides second (batch evaluation parameters)
432        if let Some(ref overrides) = self.param_overrides {
433            if let Some(&value) = overrides.get(&name) {
434                self.value_stack.push(value);
435                return Ok(());
436            }
437        }
438
439        // Try context stack next
440        if let Some(value) = self.ctx_stack.lookup_variable(ctx_id, &name) {
441            self.value_stack.push(value);
442            return Ok(());
443        }
444
445        // Check if it's a built-in constant
446        let value = match name.as_str() {
447            "pi" | "PI" => core::f64::consts::PI as Real,
448            "e" | "E" => core::f64::consts::E as Real,
449            "tau" | "TAU" => 2.0 * core::f64::consts::PI as Real,
450            _ => {
451                // Check if this looks like a function name
452                let is_potential_function_name = match name.as_str() {
453                    "sin" | "cos" | "tan" | "asin" | "acos" | "atan" | "atan2" | "sinh"
454                    | "cosh" | "tanh" | "exp" | "log" | "log10" | "ln" | "sqrt" | "abs"
455                    | "ceil" | "floor" | "pow" | "neg" | "," | "comma" | "+" | "-" | "*" | "/"
456                    | "%" | "^" | "max" | "min" | "<" | ">" | "<=" | ">=" | "==" | "!=" => true,
457                    _ => false,
458                };
459
460                if is_potential_function_name && name.len() > 1 {
461                    return Err(ExprError::Syntax(format!(
462                        "Function '{}' used without arguments",
463                        name
464                    )));
465                }
466
467                return Err(ExprError::UnknownVariable {
468                    name: name.to_string(),
469                });
470            }
471        };
472
473        self.value_stack.push(value);
474        Ok(())
475    }
476
477    /// Process array access
478    fn process_array_access(
479        &mut self,
480        array_name: HString,
481        index: Real,
482        ctx_id: usize,
483    ) -> Result<(), ExprError> {
484        let idx = index as usize;
485
486        if let Some(ctx) = self.ctx_stack.get_context(ctx_id) {
487            if let Some(array) = ctx.arrays.get(&array_name) {
488                if idx < array.len() {
489                    self.value_stack.push(array[idx]);
490                    return Ok(());
491                } else {
492                    return Err(ExprError::ArrayIndexOutOfBounds {
493                        name: array_name.to_string(),
494                        index: idx,
495                        len: array.len(),
496                    });
497                }
498            }
499        }
500
501        Err(ExprError::UnknownVariable {
502            name: array_name.to_string(),
503        })
504    }
505
506    /// Process attribute access
507    fn process_attribute_access(
508        &mut self,
509        object_name: HString,
510        attr_name: HString,
511        ctx_id: usize,
512    ) -> Result<(), ExprError> {
513        if let Some(ctx) = self.ctx_stack.get_context(ctx_id) {
514            if let Some(obj_attrs) = ctx.attributes.get(&object_name) {
515                if let Some(&value) = obj_attrs.get(&attr_name) {
516                    self.value_stack.push(value);
517                    return Ok(());
518                }
519            }
520        }
521
522        Err(ExprError::AttributeNotFound {
523            base: object_name.to_string(),
524            attr: attr_name.to_string(),
525        })
526    }
527
528    /// Process function call
529    fn process_function_call(
530        &mut self,
531        name: FunctionName,
532        arg_count: usize,
533        ctx_id: usize,
534    ) -> Result<(), ExprError> {
535        // Arguments are the last arg_count values on the value stack
536        let args_start = self.value_stack.len().saturating_sub(arg_count);
537
538        // Get context
539        let ctx = self
540            .ctx_stack
541            .get_context(ctx_id)
542            .ok_or_else(|| ExprError::Other("Invalid context ID".to_string()))?;
543
544        // Check local functions first (highest priority)
545        if let Some(local_funcs) = self.local_functions {
546            if let Some(func) = local_funcs.borrow().get(&name).cloned() {
547                return self.process_expression_function(&func, args_start, arg_count, ctx_id);
548            }
549        }
550
551        // Try native function (expression functions no longer exist in context)
552        if let Some(func) = ctx.get_native_function(&name) {
553            if arg_count != func.arity {
554                return Err(ExprError::InvalidFunctionCall {
555                    name: name.to_string(),
556                    expected: func.arity,
557                    found: arg_count,
558                });
559            }
560
561            // Get args slice from value stack
562            let args = &self.value_stack[args_start..];
563            let owned_fn = OwnedNativeFunction::from(func);
564            let result = (owned_fn.implementation)(args);
565
566            // Pop arguments from stack
567            self.value_stack.truncate(args_start);
568
569            // Push result
570            self.value_stack.push(result);
571            return Ok(());
572        }
573
574        Err(ExprError::UnknownFunction {
575            name: name.to_string(),
576        })
577    }
578
579    /// Pop a value from the value stack
580    fn pop_value(&mut self) -> Result<Real, ExprError> {
581        self.value_stack
582            .pop()
583            .ok_or_else(|| ExprError::Other("Value stack underflow".to_string()))
584    }
585
586    /// Set parameter overrides for batch evaluation.
587    /// These take precedence over context variables during lookup.
588    pub fn set_param_overrides(&mut self, params: crate::types::BatchParamMap) {
589        self.param_overrides = Some(params);
590    }
591
592    /// Clear parameter overrides.
593    pub fn clear_param_overrides(&mut self) {
594        self.param_overrides = None;
595    }
596
597    /// Execute a function with parameter overrides, ensuring they are cleared afterwards.
598    /// This provides RAII-style cleanup for safe batch evaluation.
599    pub fn with_param_overrides<F, R>(&mut self, params: crate::types::BatchParamMap, f: F) -> R
600    where
601        F: FnOnce(&mut Self) -> R,
602    {
603        let old_overrides = self.param_overrides.take();
604        self.param_overrides = Some(params);
605        let result = f(self);
606        self.param_overrides = old_overrides;
607        result
608    }
609
610    /// Process an expression function call
611    fn process_expression_function(
612        &mut self,
613        func: &crate::types::ExpressionFunction,
614        args_start: usize,
615        arg_count: usize,
616        ctx_id: usize,
617    ) -> Result<(), ExprError> {
618        use crate::types::TryIntoHeaplessString;
619
620        if arg_count != func.params.len() {
621            return Err(ExprError::InvalidFunctionCall {
622                name: func.name.to_string(),
623                expected: func.params.len(),
624                found: arg_count,
625            });
626        }
627
628        // Use pre-allocated buffer when available, otherwise allocate on-demand
629        let params_slice = if let Some(buffer_ptr) = func.param_buffer {
630            // Use pre-allocated buffer - just update values, names are already set
631            let buffer = unsafe { &mut *buffer_ptr };
632
633            // Update parameter values (names are already pre-filled)
634            for i in 0..func.params.len() {
635                buffer[i].1 = self.value_stack[args_start + i];
636            }
637
638            Some(&*buffer)
639        } else if let Some(arena) = self.arena
640            && !func.params.is_empty()
641        {
642            // Fallback for context functions - allocate on-demand
643            let params_slice: &mut [(crate::types::HString, crate::Real)] =
644                arena.alloc_slice_fill_default(func.params.len());
645
646            // Fill in both names and values
647            for (i, param) in func.params.iter().enumerate() {
648                let value = self.value_stack[args_start + i];
649                let param_key = param.as_str().try_into_heapless()?;
650                params_slice[i] = (param_key, value);
651            }
652
653            Some(&*params_slice)
654        } else {
655            None
656        };
657
658        // Pop arguments from value stack
659        self.value_stack.truncate(args_start);
660
661        // Parse expression function on-demand if we have an arena
662        if let Some(arena) = self.arena {
663            // Check if we've already parsed this function
664            let func_key = func.name.try_into_heapless()?;
665
666            let ast = if let Some(&cached_ast) = self.expr_func_cache.get(&func_key) {
667                cached_ast
668            } else {
669                // Parse the expression function body into the arena
670                let param_names: Vec<crate::String> = func.params.clone();
671                let parsed_ast = crate::engine::parse_expression_with_parameters(
672                    &func.expression,
673                    arena,
674                    &param_names,
675                )?;
676
677                // Allocate the AST in the arena
678                let arena_ast = arena.alloc(parsed_ast);
679
680                // Cache for future use
681                self.expr_func_cache.insert(func_key.clone(), &*arena_ast);
682
683                &*arena_ast
684            };
685
686            // Push operations: restore params first, then eval with SAME context
687            self.op_stack.push(EvalOp::RestoreFunctionParams {
688                params: params_slice,
689            });
690            self.op_stack.push(EvalOp::Eval {
691                expr: ast,
692                ctx_id, // Use SAME context, no new context!
693            });
694            Ok(())
695        } else {
696            // No arena available for expression functions
697            Err(ExprError::Other(
698                "Expression functions require an arena-enabled evaluator".to_string(),
699            ))
700        }
701    }
702}
703
704/// Evaluate an expression using a provided engine (avoids engine allocation).
705///
706/// This function is useful for batch evaluation where the same engine can be
707/// reused across multiple evaluations, avoiding the overhead of creating a
708/// new engine for each evaluation.
709///
710/// # Parameters
711///
712/// * `ast` - The abstract syntax tree to evaluate
713/// * `ctx` - Optional evaluation context containing variables and functions
714/// * `engine` - Mutable reference to an existing evaluation engine
715///
716/// # Returns
717///
718/// The result of evaluating the expression, or an error if evaluation fails
719///
720/// # Example
721///
722/// ```
723/// use exp_rs::eval::iterative::{eval_with_engine, EvalEngine};
724/// use exp_rs::engine::parse_expression;
725/// use exp_rs::context::EvalContext;
726/// use std::rc::Rc;
727/// use bumpalo::Bump;
728///
729/// let arena = Bump::new();
730/// let ast = parse_expression("2 + 3", &arena).unwrap();
731/// let mut engine = EvalEngine::new(&arena);
732/// let result = eval_with_engine(&ast, None, &mut engine).unwrap();
733/// assert_eq!(result, 5.0);
734/// ```
735pub fn eval_with_engine<'arena>(
736    ast: &'arena AstExpr<'arena>,
737    ctx: Option<Rc<EvalContext>>,
738    engine: &mut EvalEngine<'arena>,
739) -> Result<Real, ExprError> {
740    engine.eval(ast, ctx)
741}