aimx/evaluate.rs
1//! Core evaluation traits and utilities for AIMX expressions.
2//!
3//! This module defines the core traits and utilities for evaluating expressions
4//! within a context. It provides the foundation for the AIMX evaluation system,
5//! including the main [`ExpressionLike`] trait that all expression types implement,
6//! and utility functions for type promotion and static evaluation.
7//!
8//! The evaluation system follows a recursive descent pattern where each expression
9//! type knows how to evaluate itself within a given context. This design allows for
10//! flexible extension and maintains separation of concerns between parsing and evaluation.
11//!
12//! # Key Components
13//!
14//! - [`ExpressionLike`] - The core trait that all expression types implement
15//! - [`evaluate_and_promote`] - Helper function for binary operator evaluation with type promotion
16//! - [`statically_evaluate`] - Function for evaluating expressions without external context
17//!
18//! # Architecture
19//!
20//! The evaluation system is designed around the [`ExpressionLike`] trait, which provides
21//! a uniform interface for evaluating different types of expressions. Each expression
22//! type (arithmetic, logical, conditional, etc.) implements this trait to provide
23//! its specific evaluation logic.
24//!
25//! ## Type Promotion
26//!
27//! AIMX uses a sophisticated type promotion system where the left operand's type
28//! determines how the right operand is converted. The [`evaluate_and_promote`] function
29//! implements this logic for binary operators.
30//!
31//! ## Static Evaluation
32//!
33//! The [`statically_evaluate`] function allows evaluating expressions that contain
34//! only literals and built-in functions, without requiring external context. This is
35//! useful for constant folding, optimization, and testing.
36//!
37//! # Examples
38//!
39//! ## Basic Expression Evaluation
40//!
41//! ```rust
42//! use aimx::{aimx_parse, ExpressionLike, Context};
43//!
44//! let expression = aimx_parse("2 + 3");
45//! let mut context = Context::new();
46//! let result = expression.evaluate(&mut context).unwrap();
47//! assert_eq!(result.to_string(), "5");
48//! ```
49//!
50//! ## Using Static Evaluation
51//!
52//! ```rust
53//! use aimx::{aimx_parse, evaluate::statically_evaluate};
54//!
55//! let expression = aimx_parse("sqrt(16) + abs(-5)");
56//! let result = statically_evaluate(&expression).unwrap();
57//! assert_eq!(result.to_string(), "9");
58//! ```
59//!
60//! ## Implementing Custom Expression Types
61//!
62//! ```rust
63//! use aimx::{ExpressionLike, ContextLike, Value, Writer};
64//! use anyhow::Result;
65//!
66//! struct CustomExpression {
67//! value: Value,
68//! }
69//!
70//! impl ExpressionLike for CustomExpression {
71//! fn evaluate(&self, _context: &mut dyn ContextLike) -> Result<Value> {
72//! Ok(self.value.clone())
73//! }
74//!
75//! fn write(&self, writer: &mut Writer) {
76//! writer.print_value(&writer.prefix(), &self.value);
77//! }
78//!
79//! fn to_sanitized(&self) -> String {
80//! self.value.to_sanitized()
81//! }
82//!
83//! fn to_formula(&self) -> String {
84//! self.value.to_formula()
85//! }
86//! }
87//! ```
88
89use crate::{
90 ContextLike,
91 Value,
92 Reference,
93 FunctionRegistry,
94 Writer,
95};
96use anyhow::{anyhow, Result};
97use std::mem::replace;
98
99/// Core trait for evaluating expressions and serializing them to text representations.
100///
101/// This trait defines the interface that all expression types in the AIMX grammar must
102/// implement. It provides methods for evaluation within a context and for converting
103/// expressions to various string representations.
104///
105/// The trait is designed to be implemented by all AST node types, allowing the
106/// evaluation engine to work uniformly across different expression types without
107/// needing to know their specific implementation details.
108///
109/// # Implementation Guidelines
110///
111/// When implementing this trait:
112/// - **Evaluation**: Should handle type checking, type promotion, and error handling
113/// - **Serialization**: Should produce representations that can be parsed back
114/// - **Context Usage**: Should properly interact with the context for variable lookup
115///
116/// # Examples
117///
118/// ## Basic Usage
119///
120/// ```rust
121/// use aimx::{aimx_parse, ExpressionLike, Context};
122///
123/// let expression = aimx_parse("2 + 3 * 4");
124/// let mut context = Context::new();
125/// let result = expression.evaluate(&mut context).unwrap();
126/// assert_eq!(result.to_string(), "14");
127/// ```
128///
129/// ## Custom Implementation
130///
131/// ```rust
132/// use aimx::{ExpressionLike, ContextLike, Value, Writer};
133/// use anyhow::Result;
134///
135/// struct ConstantExpression {
136/// value: Value,
137/// }
138///
139/// impl ExpressionLike for ConstantExpression {
140/// fn evaluate(&self, _context: &mut dyn ContextLike) -> Result<Value> {
141/// Ok(self.value.clone())
142/// }
143///
144/// fn write(&self, writer: &mut Writer) {
145/// writer.print_value(&writer.prefix(), &self.value);
146/// }
147///
148/// fn to_sanitized(&self) -> String {
149/// self.value.to_sanitized()
150/// }
151///
152/// fn to_formula(&self) -> String {
153/// self.value.to_formula()
154/// }
155/// }
156/// ```
157pub trait ExpressionLike {
158 /// Write this expression to the provided writer.
159 ///
160 /// This method serializes the expression to a text representation using the
161 /// writer's current formatting mode. The writer handles indentation, quoting,
162 /// and other formatting concerns.
163 ///
164 /// # Arguments
165 ///
166 /// * `writer` - The writer to serialize the expression to
167 ///
168 /// # Examples
169 ///
170 /// ```rust
171 /// use aimx::{aimx_parse, ExpressionLike, Writer, PrintMode, Prefix};
172 ///
173 /// let expression = aimx_parse("2 + 3");
174 /// let mut writer = Writer::new(PrintMode::None, Prefix::None);
175 /// expression.write(&mut writer);
176 /// let result = writer.finish();
177 /// assert_eq!(result, "2 + 3");
178 /// ```
179 fn write(&self, writer: &mut Writer);
180
181 /// Convert this expression to a sanitized string representation.
182 ///
183 /// This method produces a string with special characters escaped to make it
184 /// safe for various contexts like JSON output, HTML embedding, or other
185 /// contexts where escaping is required.
186 ///
187 /// # Returns
188 ///
189 /// A sanitized string representation of this expression.
190 ///
191 /// # Examples
192 ///
193 /// ```rust
194 /// use aimx::{aimx_parse, ExpressionLike};
195 ///
196 /// let expression = aimx_parse(r#""hello" + "world""#);
197 /// let sanitized = expression.to_sanitized();
198 /// // Contains properly escaped characters
199 /// ```
200 fn to_sanitized(&self) -> String;
201
202 /// Convert this expression to a formula string representation.
203 ///
204 /// This method produces a string with proper quoting and escaping for use
205 /// in formulas. The output should be round-trippable, meaning it can be
206 /// parsed back into an equivalent expression.
207 ///
208 /// # Returns
209 ///
210 /// A formula string representation of this expression.
211 ///
212 /// # Examples
213 ///
214 /// ```rust
215 /// use aimx::{aimx_parse, ExpressionLike};
216 ///
217 /// let expression = aimx_parse("2 + 3");
218 /// let formula = expression.to_formula();
219 /// assert_eq!(formula, "2 + 3");
220 /// ```
221 fn to_formula(&self) -> String;
222
223 /// Evaluate the expression within the given context.
224 ///
225 /// This is the core method that performs the actual evaluation of the expression.
226 /// It uses the provided context to resolve variables, call functions, and
227 /// perform other runtime operations.
228 ///
229 /// # Arguments
230 ///
231 /// * `context` - A mutable reference to the evaluation context that provides
232 /// variable values, function implementations, and other runtime
233 /// information needed for evaluation.
234 ///
235 /// # Returns
236 ///
237 /// Returns a `Result<Value>` containing the evaluated result or an error
238 /// if evaluation fails.
239 ///
240 /// # Errors
241 ///
242 /// This method can return various errors including:
243 /// - `Undefined variable` errors when referencing undefined variables
244 /// - `Type mismatch` errors when operations are performed on incompatible types
245 /// - `Function not found` errors when calling undefined functions
246 /// - `Circular reference` errors when detecting infinite recursion
247 /// - `Division by zero` errors in arithmetic operations
248 /// - Other evaluation errors as appropriate for the expression type
249 ///
250 /// # Examples
251 ///
252 /// ```rust
253 /// use aimx::{aimx_parse, ExpressionLike, Context};
254 ///
255 /// let expression = aimx_parse("x + 5");
256 /// let mut context = Context::new().with_value("x", aimx::Value::from_number(10.0));
257 /// let result = expression.evaluate(&mut context).unwrap();
258 /// assert_eq!(result.to_string(), "15");
259 /// ```
260 fn evaluate(&self, context: &mut dyn ContextLike) -> Result<Value>;
261}
262
263/// Helper function to evaluate two expressions and promote the right operand
264/// to match the type of the left operand.
265///
266/// This function implements AIMX's type promotion system for binary operators.
267/// It evaluates both operands, then promotes the right operand's type to match
268/// the left operand's type according to AIMX grammar rules.
269///
270/// # Type Promotion Rules
271///
272/// The type promotion follows these rules:
273/// - If the left operand is `Bool`, the right operand is converted to boolean
274/// - If the left operand is `Date`, the right operand is converted to date
275/// - If the left operand is `Number`, the right operand is converted to number
276/// - If the left operand is `Task`, the right operand is converted to task
277/// - If the left operand is `Text`, the right operand is converted to text
278///
279/// # Usage
280///
281/// This function is primarily used internally by binary operator implementations
282/// (arithmetic, logical, relational, etc.) to ensure type compatibility.
283///
284/// # Arguments
285///
286/// * `context` - The evaluation context used for evaluating both operands
287/// * `left` - The left operand expression
288/// * `right` - The right operand expression
289///
290/// # Returns
291///
292/// Returns a `Result` containing a tuple of `(Value, Value)` where:
293/// - The first value is the evaluated left operand
294/// - The second value is the evaluated right operand, promoted to match the left operand's type
295///
296/// # Errors
297///
298/// This function can return errors if:
299/// - Either operand fails to evaluate
300/// - The right operand cannot be promoted to match the left operand's type
301///
302/// # Examples
303///
304/// ```rust
305/// use aimx::{aimx_parse, evaluate::evaluate_and_promote, Context};
306///
307/// let mut context = Context::new();
308/// let left_expr = aimx_parse("5");
309/// let right_expr = aimx_parse("\"3\"");
310///
311/// let (left_val, right_val) = evaluate_and_promote(
312/// &mut context,
313/// &left_expr,
314/// &right_expr
315/// ).unwrap();
316///
317/// // The string "3" is promoted to number 3.0
318/// assert!(left_val.is_number());
319/// assert!(right_val.is_number());
320/// assert_eq!(right_val.to_number().unwrap(), 3.0);
321/// ```
322pub fn evaluate_and_promote(
323 context: &mut dyn ContextLike,
324 left: &dyn ExpressionLike,
325 right: &dyn ExpressionLike,
326) -> Result<(Value, Value)> {
327 // evaluate left operand
328 let left_val = left.evaluate(context)?;
329 // evaluate right operand
330 let unknown_type = right.evaluate(context)?;
331 // promote right result to left operands' type
332 match unknown_type.as_type(&left_val) {
333 Ok(right_val) => Ok((left_val, right_val)),
334 Err(err) => {
335 let message = format!("{}~{}", err, right.to_formula());
336 Err(anyhow!(message))
337 }
338 }
339 // return matching types
340
341}
342
343/// Evaluate an expression statically without external context.
344///
345/// This function evaluates expressions that contain only literals, operators,
346/// and function calls. It cannot evaluate expressions that reference variables
347/// or perform assignments. The function uses a thread-safe singleton context
348/// with access to all the built-in functions.
349///
350/// # Purpose
351///
352/// Static evaluation is useful for:
353/// - Constant folding and optimization
354/// - Evaluating pure mathematical expressions
355/// - Testing and debugging expression parsing
356/// - Pre-computing values that don't depend on external state
357///
358/// # Supported Expressions
359///
360/// **Allowed:**
361/// - All literal values: `42`, `true`, `"hello"`, `_`
362/// - Arithmetic operations: `1 + 2 * 3`, `(4 - 1) * 5`
363/// - Logical operations: `true & false | true`
364/// - Relational operations: `5 > 3`, `2 == 1 + 1`
365/// - Conditional expressions: `true ? 1 : 2`
366/// - Type casting: `(Number) "123"`, `(Text) 42`
367/// - Unary operations: `-5`, `!true`, `+10`
368/// - Arrays: `(1, 2, 3)`, `(1, "hello", true)`
369/// - Function calls: `abs(-5)`, `max((1, 5, 3))`, `length("hello")`
370/// - Method calls: `"hello".length()`, `3.14.round()`
371/// - Complex nested expressions: `abs(-5) + max((1, 2)) * length("test")`
372///
373/// **Not Allowed:**
374/// - Variable references: `x + 5`, `my_var`
375/// - Assignment operations: `x = 5`
376/// - Any expression requiring external context
377///
378/// # Thread Safety
379///
380/// This function uses a thread-safe singleton pattern with `OnceLock<Mutex<>>`
381/// to ensure that the function registry is initialized only once and can be
382/// safely accessed from multiple threads simultaneously.
383///
384/// # Examples
385///
386/// ```rust
387/// use aimx::{ExpressionLike, evaluate::statically_evaluate};
388/// use aimx::expression::parse_expression;
389///
390/// // Basic arithmetic
391/// let (_, expression) = parse_expression("1 + 2 * 3").unwrap();
392/// let result = statically_evaluate(&expression).unwrap();
393/// // Result: 7
394///
395/// // Function calls
396/// let (_, expression) = parse_expression("abs(-5) + max((1, 2))").unwrap();
397/// let result = statically_evaluate(&expression).unwrap();
398/// // Result: 7 (5 + 2)
399///
400/// // Complex expressions
401/// let (_, expression) = parse_expression("5 > 3 ? \"yes\" : \"no\"").unwrap();
402/// let result = statically_evaluate(&expression).unwrap();
403/// // Result: "yes"
404///
405/// // Array operations
406/// let (_, expression) = parse_expression("max((1, 5, 3))").unwrap();
407/// let result = statically_evaluate(&expression).unwrap();
408/// // Result: 5
409/// ```
410///
411/// # Errors
412///
413/// This function will return an error for:
414/// - Expressions containing variable references
415/// - Assignment operations
416/// - Invalid syntax or parsing errors
417/// - Runtime errors (division by zero, type mismatches, etc.)
418/// - Function calls to undefined functions
419///
420/// ```rust
421/// use aimx::{ExpressionLike, evaluate::statically_evaluate};
422/// use aimx::expression::parse_expression;
423///
424/// // This will fail because it references a variable
425/// let (_, expression) = parse_expression("x + 5").unwrap();
426/// let result = statically_evaluate(&expression);
427/// assert!(result.is_err()); // Error: "Non-static expression"
428///
429/// // This will fail due to division by zero
430/// let (_, expression) = parse_expression("10 / 0").unwrap();
431/// let result = statically_evaluate(&expression);
432/// assert!(result.is_err()); // Error: Division by zero
433/// ```
434///
435/// # Performance
436///
437/// The singleton pattern ensures that function registration happens only once,
438/// making subsequent calls very efficient. The function registry is shared
439/// across all threads, minimizing memory usage.
440///
441/// # Arguments
442///
443/// * `expression` - The evaluatable expression to statically evaluate
444///
445/// # Returns
446///
447/// Returns a `Result<Value>` containing the evaluated result or an error
448/// if the expression cannot be evaluated statically.
449pub fn statically_evaluate(expression: &dyn ExpressionLike) -> Result<Value> {
450 // Create a static context that only supports function calls
451 struct StaticContext {
452 stack: [(String, Value); 2],
453 }
454
455 impl StaticContext {
456 pub fn new() -> Self {
457 Self {
458 stack: [
459 (String::new(), Value::Empty),
460 (String::new(), Value::Empty)
461 ],
462 }
463 }
464 }
465
466 impl ContextLike for StaticContext {
467 fn start_closure(&mut self) -> [(String, Value); 2] {
468 replace(&mut self.stack, [
469 (String::new(), Value::Empty),
470 (String::new(), Value::Empty)
471 ])
472 }
473
474 fn set_key(&mut self, index: usize, identifier: &str) {
475 if self.stack[index].0 != *identifier {
476 self.stack[index].0 = identifier.to_string();
477 }
478 }
479 fn set_value(&mut self, index: usize, value: &Value) {
480 self.stack[index].1 = value.clone();
481 }
482
483 fn end_closure(&mut self, stack: [(String, Value); 2]) {
484 self.stack = stack;
485 }
486 fn get_referenced(&mut self, reference: &Reference) -> Result<Value> {
487 if *reference == self.stack[0].0 {
488 Ok(self.stack[0].1.clone())
489 } else if *reference == self.stack[1].0 {
490 Ok(self.stack[1].1.clone())
491 } else {
492 Err(anyhow!("Non-static expression"))
493 }
494 }
495 fn set_referenced(&mut self, reference: &Reference, value: Value) -> Result<()> {
496 if *reference == self.stack[0].0 {
497 self.stack[0].1 = value;
498 Ok(())
499 } else if *reference == self.stack[1].0 {
500 self.stack[1].1 = value;
501 Ok(())
502 } else {
503 Err(anyhow!("Non-static expression"))
504 }
505 }
506 fn function_call(&mut self, name: &str, arg: Value) -> Result<Value> {
507 let registry = FunctionRegistry::read_lock()?;
508 registry.function_call(self, name, arg)
509 }
510 fn method_call(&mut self, name: &str, value: Value, arg: Value) -> Result<Value> {
511 let registry = FunctionRegistry::read_lock()?;
512 registry.method_call(self, name, value, arg)
513 }
514 fn inference_call(&mut self, _reference: &Reference, _arg: Value) -> Result<Value> {
515 Err(anyhow!("Non-static inference"))
516 }
517 }
518
519 // Use OnceLock for thread-safe one-time initialization
520 use std::sync::OnceLock;
521 use std::sync::Mutex;
522
523 static INSTANCE: OnceLock<Mutex<StaticContext>> = OnceLock::new();
524
525 let context = INSTANCE.get_or_init(|| {
526 Mutex::new(StaticContext::new())
527 });
528
529 let mut context = context.lock().unwrap();
530 expression.evaluate(&mut *context)
531}