exp_rs/
types.rs

1//! Type definitions for the expression parser and evaluator.
2//!
3//! This module contains the core data structures used throughout the expression parser
4//! and evaluator, including the Abstract Syntax Tree (AST) representation, token types,
5//! function definitions, and other auxiliary types.
6
7extern crate alloc;
8
9// ============================================================================
10// Heapless Migration - Type Aliases and Configuration
11// ============================================================================
12
13use heapless::{FnvIndexMap, String as HeaplessString};
14use alloc::string::ToString;
15
16// Configuration constants - can be adjusted based on target constraints
17pub const EXP_RS_MAX_VARIABLES: usize = 16;
18pub const EXP_RS_MAX_BATCH_PARAMS: usize = 64;
19pub const EXP_RS_MAX_CONSTANTS: usize = 8;
20pub const EXP_RS_MAX_ARRAYS: usize = 4;
21pub const EXP_RS_MAX_ATTRIBUTES: usize = 4;
22pub const EXP_RS_MAX_NESTED_ARRAYS: usize = 2;
23pub const EXP_RS_MAX_AST_CACHE: usize = 16;
24pub const EXP_RS_MAX_NATIVE_FUNCTIONS: usize = 64;
25pub const EXP_RS_MAX_EXPRESSION_FUNCTIONS: usize = 8;
26pub const EXP_RS_MAX_ATTR_KEYS: usize = 4;
27
28// String length limits for embedded efficiency
29pub const EXP_RS_MAX_KEY_LENGTH: usize = 32;
30pub const EXP_RS_MAX_FUNCTION_NAME_LENGTH: usize = 32; // Changed from 24 to 32 for proper alignment
31
32// Error message buffer size for ExprResult
33pub const EXP_RS_ERROR_BUFFER_SIZE: usize = 256;
34
35// Primary type aliases
36pub type HString = HeaplessString<EXP_RS_MAX_KEY_LENGTH>;
37pub type FunctionName = HeaplessString<EXP_RS_MAX_FUNCTION_NAME_LENGTH>;
38
39// Container type aliases - using heapless FnvIndexMap
40pub type VariableMap = FnvIndexMap<HString, crate::Real, EXP_RS_MAX_VARIABLES>;
41pub type ConstantMap = FnvIndexMap<HString, crate::Real, EXP_RS_MAX_CONSTANTS>;
42pub type BatchParamMap = FnvIndexMap<HString, crate::Real, EXP_RS_MAX_BATCH_PARAMS>;
43pub type ArrayMap = FnvIndexMap<HString, alloc::vec::Vec<crate::Real>, EXP_RS_MAX_ARRAYS>;
44pub type AttributeMap = FnvIndexMap<
45    HString,
46    FnvIndexMap<HString, crate::Real, EXP_RS_MAX_ATTR_KEYS>,
47    EXP_RS_MAX_ATTRIBUTES,
48>;
49pub type NestedArrayMap = FnvIndexMap<
50    HString,
51    FnvIndexMap<usize, alloc::vec::Vec<crate::Real>, EXP_RS_MAX_NESTED_ARRAYS>,
52    EXP_RS_MAX_NESTED_ARRAYS,
53>;
54pub type NativeFunctionMap = FnvIndexMap<FunctionName, NativeFunction, EXP_RS_MAX_NATIVE_FUNCTIONS>;
55pub type ExpressionFunctionMap =
56    FnvIndexMap<FunctionName, ExpressionFunction, EXP_RS_MAX_EXPRESSION_FUNCTIONS>;
57
58// AST cache type - defined later after AstExpr is declared
59// pub type AstCacheMap = FnvIndexMap<HString, alloc::rc::Rc<AstExpr>, MAX_AST_CACHE>;
60
61#[cfg(test)]
62use crate::Real;
63#[cfg(not(test))]
64use crate::{Real, String, Vec};
65#[cfg(not(test))]
66use alloc::rc::Rc;
67#[cfg(test)]
68use std::rc::Rc;
69#[cfg(test)]
70use std::string::String;
71#[cfg(test)]
72use std::vec::Vec;
73
74/// Abstract Syntax Tree (AST) node representing an expression.
75///
76/// The AST is the core data structure used for representing parsed expressions.
77/// Each variant of this enum represents a different type of expression node,
78/// forming a tree structure that can be evaluated to produce a result.
79///
80/// This type uses arena allocation for all strings and recursive structures,
81/// eliminating all dynamic allocations during evaluation.
82///
83/// Using repr(C) with explicit discriminant type and alignment to avoid ARM alignment issues
84#[derive(Debug)]
85#[repr(C, align(8))]
86pub enum AstExpr<'arena> {
87    /// A literal numerical value.
88    ///
89    /// Examples: `3.14`, `42`, `-1.5`
90    Constant(Real),
91
92    /// A named variable reference.
93    ///
94    /// Examples: `x`, `temperature`, `result`
95    Variable(&'arena str),
96
97    /// A function call with a name and list of argument expressions.
98    ///
99    /// Examples: `sin(x)`, `max(a, b)`, `sqrt(x*x + y*y)`
100    Function {
101        /// The name of the function being called
102        name: &'arena str,
103        /// The arguments passed to the function
104        args: &'arena [AstExpr<'arena>],
105    },
106
107    /// An array element access.
108    ///
109    /// Examples: `array[0]`, `values[i+1]`
110    Array {
111        /// The name of the array
112        name: &'arena str,
113        /// The expression for the index
114        index: &'arena AstExpr<'arena>,
115    },
116
117    /// An attribute access on an object.
118    ///
119    /// Examples: `point.x`, `settings.value`
120    Attribute {
121        /// The base object name
122        base: &'arena str,
123        /// The attribute name
124        attr: &'arena str,
125    },
126
127    /// A logical operation with short-circuit evaluation.
128    ///
129    /// Represents logical AND (`&&`) and OR (`||`) operations with short-circuit behavior.
130    /// Unlike function-based operators, these operators have special evaluation semantics
131    /// where the right operand may not be evaluated based on the value of the left operand.
132    ///
133    /// # Examples
134    ///
135    /// - `a && b`: Evaluates `a`, then evaluates `b` only if `a` is non-zero (true)
136    /// - `c || d`: Evaluates `c`, then evaluates `d` only if `c` is zero (false)
137    /// - `x > 0 && y < 10`: Checks if both conditions are true, with short-circuit
138    /// - `flag || calculate_value()`: Skips calculation if flag is true
139    ///
140    /// # Boolean Logic
141    ///
142    /// The engine represents boolean values as floating-point numbers:
143    /// - `0.0` is considered `false`
144    /// - Any non-zero value is considered `true`, typically `1.0` is used
145    ///
146    /// # Operator Precedence
147    ///
148    /// `&&` has higher precedence than `||`, consistent with most programming languages:
149    /// - `a || b && c` is interpreted as `a || (b && c)`
150    /// - Use parentheses to override default precedence: `(a || b) && c`
151    LogicalOp {
152        /// The logical operator (AND or OR)
153        op: LogicalOperator,
154        /// The left operand (always evaluated)
155        left: &'arena AstExpr<'arena>,
156        /// The right operand (conditionally evaluated based on left value)
157        right: &'arena AstExpr<'arena>,
158    },
159
160    /// A ternary conditional operation (condition ? true_expr : false_expr).
161    ///
162    /// Represents a conditional expression with three parts: a condition to evaluate,
163    /// an expression to return if the condition is true, and an expression to return
164    /// if the condition is false. This uses short-circuit evaluation, meaning only
165    /// the relevant branch is evaluated.
166    ///
167    /// # Examples
168    ///
169    /// - `x > 0 ? 1 : -1`: Returns 1 if x is positive, -1 otherwise
170    /// - `flag ? value1 : value2`: Chooses between two values based on flag
171    /// - `a > b ? a : b`: Returns the maximum of a and b
172    ///
173    /// # Boolean Logic
174    ///
175    /// Like logical operations, the ternary operator uses floating-point values for boolean logic:
176    /// - `0.0` is considered `false`
177    /// - Any non-zero value is considered `true`
178    ///
179    /// # Short-Circuit Evaluation
180    ///
181    /// Only one branch is evaluated based on the condition result:
182    /// - If condition is non-zero (true), only the true_branch is evaluated
183    /// - If condition is zero (false), only the false_branch is evaluated
184    ///
185    /// # Operator Precedence
186    ///
187    /// The ternary operator has low precedence:
188    /// - `a + b ? c : d * e` is interpreted as `(a + b) ? c : (d * e)`
189    /// - Use parentheses for clarity when nesting operations
190    Conditional {
191        /// The condition expression to evaluate
192        condition: &'arena AstExpr<'arena>,
193        /// Expression to evaluate if condition is true (non-zero)
194        true_branch: &'arena AstExpr<'arena>,
195        /// Expression to evaluate if condition is false (zero)
196        false_branch: &'arena AstExpr<'arena>,
197    },
198}
199
200// AST cache type - REMOVED: Incompatible with arena allocation
201// pub type AstCacheMap = FnvIndexMap<HString, alloc::rc::Rc<AstExpr>, EXP_RS_MAX_AST_CACHE>;
202
203// Helper trait for string conversion to heapless strings
204pub trait TryIntoHeaplessString {
205    fn try_into_heapless(self) -> Result<HString, crate::error::ExprError>;
206}
207
208impl TryIntoHeaplessString for &str {
209    fn try_into_heapless(self) -> Result<HString, crate::error::ExprError> {
210        HString::try_from(self).map_err(|_| {
211            crate::error::ExprError::StringTooLong(self.to_string(), EXP_RS_MAX_KEY_LENGTH)
212        })
213    }
214}
215
216impl TryIntoHeaplessString for alloc::string::String {
217    fn try_into_heapless(self) -> Result<HString, crate::error::ExprError> {
218        HString::try_from(self.as_str())
219            .map_err(|_| crate::error::ExprError::StringTooLong(self, EXP_RS_MAX_KEY_LENGTH))
220    }
221}
222
223// Helper trait for function names
224pub trait TryIntoFunctionName {
225    fn try_into_function_name(self) -> Result<FunctionName, crate::error::ExprError>;
226}
227
228impl TryIntoFunctionName for &str {
229    fn try_into_function_name(self) -> Result<FunctionName, crate::error::ExprError> {
230        FunctionName::try_from(self).map_err(|_| {
231            crate::error::ExprError::StringTooLong(
232                self.to_string(),
233                EXP_RS_MAX_FUNCTION_NAME_LENGTH,
234            )
235        })
236    }
237}
238
239impl TryIntoFunctionName for alloc::string::String {
240    fn try_into_function_name(self) -> Result<FunctionName, crate::error::ExprError> {
241        FunctionName::try_from(self.as_str()).map_err(|_| {
242            crate::error::ExprError::StringTooLong(self, EXP_RS_MAX_FUNCTION_NAME_LENGTH)
243        })
244    }
245}
246
247impl<'arena> AstExpr<'arena> {
248    /// Helper method that raises a constant expression to a power.
249    ///
250    /// This is primarily used in testing to evaluate power operations on constants.
251    /// For non-constant expressions, it returns 0.0 as a default value.
252    ///
253    /// # Parameters
254    ///
255    /// * `exp` - The exponent to raise the constant to
256    ///
257    /// # Returns
258    ///
259    /// The constant raised to the given power, or 0.0 for non-constant expressions
260    pub fn pow(&self, exp: Real) -> Real {
261        match self {
262            AstExpr::Constant(val) => {
263                #[cfg(all(feature = "libm", feature = "f32"))]
264                {
265                    libm::powf(*val, *exp)
266                }
267                #[cfg(all(feature = "libm", not(feature = "f32")))]
268                {
269                    libm::pow(*val, exp)
270                }
271                #[cfg(all(not(feature = "libm"), test))]
272                {
273                    val.powf(*exp)
274                } // Use std::powf when in test mode
275                #[cfg(all(not(feature = "libm"), not(test)))]
276                {
277                    // Without libm and not in tests, limited power implementation
278                    if exp == 0.0 {
279                        1.0
280                    } else if exp == 1.0 {
281                        *val
282                    } else if exp == 2.0 {
283                        *val * *val
284                    } else {
285                        0.0
286                    } // This functionality requires explicit registration
287                }
288            }
289            _ => 0.0, // Default for non-constant expressions
290        }
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use crate::context::EvalContext;
298    use crate::error::ExprError;
299    use crate::eval::eval_ast;
300    use bumpalo::Bump;
301
302    use std::rc::Rc;
303
304    #[test]
305    fn test_eval_ast_array_and_attribute_errors() {
306        let arena = Bump::new();
307
308        // Array not found
309        let index_ast = arena.alloc(AstExpr::Constant(0.0));
310        let ast = AstExpr::Array {
311            name: "arr",
312            index: index_ast,
313        };
314        let err = eval_ast(&ast, None, &arena).unwrap_err();
315        match err {
316            ExprError::UnknownVariable { name } => assert_eq!(name, "arr"),
317            _ => panic!("Expected UnknownVariable error"),
318        }
319        // Attribute not found
320        let ast2 = AstExpr::Attribute {
321            base: "foo",
322            attr: "bar",
323        };
324        let err2 = eval_ast(&ast2, None, &arena).unwrap_err();
325        match err2 {
326            ExprError::AttributeNotFound { base, attr } => {
327                assert_eq!(base, "foo");
328                assert_eq!(attr, "bar");
329            }
330            _ => panic!("Expected AttributeNotFound error"),
331        }
332    }
333
334    #[test]
335    fn test_eval_ast_function_wrong_arity() {
336        let arena = Bump::new();
337
338        // Create a context that has 'sin' registered
339        let mut ctx = EvalContext::new();
340
341        // Register sin function that takes exactly 1 argument
342        let _ = ctx.register_native_function("sin", 1, |args| args[0].sin());
343        let ctx = Rc::new(ctx);
344
345        // Create AST for sin with 2 args (should be 1)
346        let args = arena.alloc([AstExpr::Constant(1.0), AstExpr::Constant(2.0)]);
347        let ast = AstExpr::Function {
348            name: "sin",
349            args: args,
350        };
351
352        // Should give InvalidFunctionCall error because sin takes 1 arg but we gave 2
353        let err = eval_ast(&ast, Some(ctx), &arena).unwrap_err();
354        match err {
355            ExprError::InvalidFunctionCall {
356                name,
357                expected,
358                found,
359            } => {
360                assert_eq!(name, "sin");
361                assert_eq!(expected, 1);
362                assert_eq!(found, 2);
363            }
364            _ => panic!("Expected InvalidFunctionCall error"),
365        }
366    }
367
368    #[test]
369    fn test_eval_ast_unknown_function_and_variable() {
370        let arena = Bump::new();
371
372        // Unknown function
373        let args = arena.alloc([AstExpr::Constant(1.0)]);
374        let ast = AstExpr::Function {
375            name: "notafunc",
376            args: args,
377        };
378        let err = eval_ast(&ast, None, &arena).unwrap_err();
379        match err {
380            ExprError::UnknownFunction { name } => assert_eq!(name, "notafunc"),
381            _ => panic!("Expected UnknownFunction error"),
382        }
383        // Unknown variable
384        let ast2 = AstExpr::Variable("notavar");
385        let err2 = eval_ast(&ast2, None, &arena).unwrap_err();
386        match err2 {
387            ExprError::UnknownVariable { name } => assert_eq!(name, "notavar"),
388            _ => panic!("Expected UnknownVariable error"),
389        }
390    }
391}
392
393/// Classifies the kind of expression node in the AST.
394///
395/// This enum is used to categorize expression nodes at a higher level than the specific
396/// AST node variants, making it easier to determine the general type of an expression
397/// without matching on all variants.
398#[derive(Copy, Clone, PartialEq, Eq, Debug)]
399pub enum ExprKind {
400    /// A constant numerical value.
401    Constant,
402
403    /// A variable reference.
404    Variable,
405
406    /// A function call with a specific arity (number of arguments).
407    Function {
408        /// Number of arguments the function takes
409        arity: usize,
410    },
411
412    /// An array element access.
413    Array,
414
415    /// An object attribute access.
416    Attribute,
417
418    /// A logical operation (AND/OR).
419    LogicalOp,
420
421    /// A conditional (ternary) operation.
422    Conditional,
423}
424
425/// Classifies the kind of token produced during lexical analysis.
426///
427/// These token types are used by the lexer to categorize different elements
428/// in the expression string during the parsing phase.
429#[derive(Copy, Clone, PartialEq, Eq, Debug)]
430pub enum TokenKind {
431    /// A numerical literal.
432    Number,
433
434    /// A variable identifier.
435    Variable,
436
437    /// An operator such as +, -, *, /, ^, etc.
438    Operator,
439
440    /// An opening delimiter like '(' or '['.
441    Open,
442
443    /// A closing delimiter like ')' or ']'.
444    Close,
445
446    /// A separator between items, typically a comma.
447    Separator,
448
449    /// End of the expression.
450    End,
451
452    /// An error token representing invalid input.
453    Error,
454
455    /// A null or placeholder token.
456    Null,
457}
458
459/*
460    All legacy bitmasking, ExprType, and OperatorKind have been removed.
461    All parser and evaluator logic now uses AstExpr and enums only.
462    The old Expr struct and related types are no longer present.
463    Next: Update and simplify the test suite to use the new AST parser and evaluator.
464*/
465
466/// Defines the type of logical operation.
467///
468/// Used by the `LogicalOp` variant of `AstExpr` to specify which logical operation
469/// should be performed with short-circuit evaluation semantics.
470///
471/// # Short-Circuit Evaluation
472///
473/// Short-circuit evaluation is an optimization technique where the second operand
474/// of a logical operation is evaluated only when necessary:
475///
476/// - For `&&` (AND): If the left operand is false, the result is false regardless
477///   of the right operand, so the right operand is not evaluated.
478///
479/// - For `||` (OR): If the left operand is true, the result is true regardless
480///   of the right operand, so the right operand is not evaluated.
481///
482/// This behavior is particularly useful for:
483///
484/// 1. Performance optimization - avoid unnecessary calculation
485/// 2. Conditional execution - control evaluation of expressions
486/// 3. Safe guards - prevent errors (e.g., division by zero)
487///
488/// # Boolean Representation
489///
490/// In this expression engine, boolean values are represented as floating-point numbers:
491///
492/// - `0.0` represents `false`
493/// - Any non-zero value (typically `1.0`) represents `true`
494#[derive(Clone, Debug, PartialEq)]
495pub enum LogicalOperator {
496    /// Logical AND (&&) - evaluates to true only if both operands are true.
497    /// Short-circuits if the left operand is false.
498    ///
499    /// Examples:
500    /// - `1 && 1` evaluates to `1.0` (true)
501    /// - `1 && 0` evaluates to `0.0` (false)
502    /// - `0 && expr` evaluates to `0.0` without evaluating `expr`
503    And,
504
505    /// Logical OR (||) - evaluates to true if either operand is true.
506    /// Short-circuits if the left operand is true.
507    ///
508    /// Examples:
509    /// - `1 || 0` evaluates to `1.0` (true)
510    /// - `0 || 0` evaluates to `0.0` (false)
511    /// - `1 || expr` evaluates to `1.0` without evaluating `expr`
512    Or,
513}
514
515/// Implements Display for LogicalOperator to use in error messages.
516impl core::fmt::Display for LogicalOperator {
517    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
518        match self {
519            LogicalOperator::And => write!(f, "&&"),
520            LogicalOperator::Or => write!(f, "||"),
521        }
522    }
523}
524
525/// Represents a native Rust function that can be registered with the evaluation context.
526///
527/// Native functions allow users to extend the expression evaluator with custom
528/// functionality written in Rust. These functions can be called from within expressions
529/// like any built-in function.
530///
531/// # Example
532///
533/// ```
534/// # use exp_rs::{EvalContext, Real};
535/// # use exp_rs::engine::interp;
536/// # use std::rc::Rc;
537/// let mut ctx = EvalContext::new();
538///
539/// // Register a custom function that calculates the hypotenuse
540/// ctx.register_native_function(
541///     "hypotenuse",     // Function name
542///     2,                // Takes 2 arguments
543///     |args: &[Real]| { // Implementation
544///         (args[0] * args[0] + args[1] * args[1]).sqrt()
545///     }
546/// );
547///
548/// // Use the function in an expression
549/// let result = interp("hypotenuse(3, 4)", Some(Rc::new(ctx))).unwrap();
550/// assert_eq!(result, 5.0);
551/// ```
552#[derive(Clone)]
553pub struct NativeFunction {
554    /// Number of arguments the function takes.
555    pub arity: usize,
556
557    /// The actual implementation of the function as a Rust closure.
558    pub implementation: Rc<dyn Fn(&[Real]) -> Real>,
559
560    /// The name of the function as it will be used in expressions.
561    pub name: FunctionName,
562
563    /// Optional description of what the function does.
564    pub description: Option<String>,
565}
566
567/* We can't derive Clone for NativeFunction because Box<dyn Fn> doesn't implement Clone.
568Instead, we provide a shallow clone in context.rs for EvalContext, which is safe for read-only use.
569Do NOT call .clone() on NativeFunction directly. */
570
571use alloc::borrow::Cow;
572
573/// Represents a function defined by an expression string rather than Rust code.
574///
575/// Expression functions allow users to define custom functions using the expression
576/// language itself. These functions are compiled once when registered and can be called
577/// from other expressions. They support parameters and can access variables from the
578/// evaluation context.
579///
580/// # Example
581///
582/// ```
583/// # use exp_rs::{EvalContext, Real};
584/// # use exp_rs::engine::interp;
585/// # use std::rc::Rc;
586/// let mut ctx = EvalContext::new();
587///
588/// // Note: Expression functions require runtime parsing which is not supported
589/// // in the current arena-based architecture. Use native functions instead:
590/// ctx.register_native_function("circle_area", 1, |args| {
591///     let radius = args[0];
592///     std::f64::consts::PI * radius * radius
593/// }).unwrap();
594///
595/// // Use the function in another expression
596/// let result = interp("circle_area(2)", Some(Rc::new(ctx))).unwrap();
597/// assert!(result > 12.56 && result < 12.57); // π * 4 ≈ 12.566
598/// ```
599pub struct ExpressionFunction {
600    /// The name of the function as it will be used in expressions.
601    pub name: FunctionName,
602
603    /// The parameter names that the function accepts.
604    pub params: Vec<String>,
605
606    /// The original expression string defining the function body.
607    pub expression: String,
608
609    /// Optional description of what the function does.
610    pub description: Option<String>,
611
612    /// Pre-allocated parameter buffer for zero-allocation evaluation.
613    /// When available, this points to an arena-allocated slice that can be reused
614    /// for every function call instead of allocating new parameter storage.
615    /// The slice size matches params.len() and gets filled with actual values during evaluation.
616    pub param_buffer: Option<*mut [(crate::types::HString, crate::Real)]>,
617}
618
619impl Clone for ExpressionFunction {
620    fn clone(&self) -> Self {
621        Self {
622            name: self.name.clone(),
623            params: self.params.clone(),
624            expression: self.expression.clone(),
625            description: self.description.clone(),
626            param_buffer: self.param_buffer, // Share the same buffer pointer
627        }
628    }
629}
630
631/// Internal representation of a variable in the evaluation system.
632///
633/// This is an implementation detail and should not be used directly by library users.
634/// Variables are normally managed through the `EvalContext` interface.
635#[doc(hidden)]
636pub struct Variable<'a> {
637    /// The name of the variable.
638    pub name: Cow<'a, str>,
639
640    /// Internal address/identifier for the variable.
641    pub address: i8,
642
643    /// Function associated with the variable (if any).
644    pub function: fn(Real, Real) -> Real,
645
646    /// Context or associated AST nodes.
647    pub context: Vec<AstExpr<'a>>,
648}
649
650impl<'a> Variable<'a> {
651    /// Creates a new variable with the given name and default values.
652    pub fn new(name: &'a str) -> Variable<'a> {
653        Variable {
654            name: Cow::Borrowed(name),
655            address: 0,
656            function: crate::functions::dummy,
657            context: Vec::<AstExpr<'a>>::new(),
658        }
659    }
660}