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}