aimx/expressions/equality.rs
1//! Equality expression parsing and evaluation for the AIMX language.
2//!
3//! This module provides parsers and evaluation logic for equality expressions
4//! using the `=` and `!=` operators. The AIMX grammar accepts both `==` and `=`
5//! for equality comparison, and both `!=` and `<>` for inequality comparison.
6//!
7//! Equality expressions form an important part of the expression hierarchy,
8//! handling comparisons between values of the same type. They have lower
9//! precedence than relational operators (`<`, `<=`, `>`, `>=`) but higher
10//! precedence than logical operators (`&`, `|`).
11//!
12//! # Grammar
13//!
14//! The equality operators have left associativity and follow this grammar rule:
15//!
16//! ```text
17//! equality := relational (S? ('=' | "!=" | '==') S? relational)?
18//! ```
19//!
20//! Where `S` represents optional whitespace.
21//!
22//! # Operator Support
23//!
24//! | Operator | Meaning | Aliases |
25//! |----------|---------|---------|
26//! | `=` | Equality | `==` |
27//! | `!=` | Inequality | |
28//!
29//! # Examples
30//!
31//! ```text
32//! 5 = 5 // true (number equality)
33//! "hello" != "world" // true (text inequality)
34//! 1 + 2 = 3 // true (arithmetic result comparison)
35//! x > 0 = y < 10 // (x > 0) = (y < 10) - relational comparison equality
36//! true = false // false (boolean equality)
37//! ```
38//!
39//! # Type Behavior
40//!
41//! Equality operations require operands of compatible types. The evaluation
42//! system uses type promotion via [`evaluate_and_promote`]
43//! to ensure both operands have the same type before comparison:
44//!
45//! - **Boolean**: `true = false` → `false`
46//! - **Number**: `5 = 5.0` → `true` (numeric equality)
47//! - **Text**: `"abc" = "abc"` → `true` (string equality)
48//! - **Date**: DateTime equality comparison
49//! - **Task**: Task status equality comparison
50//!
51//! # AST Flattening Optimization
52//!
53//! Like other expression modules, equality expressions use AST flattening
54//! where expressions that don't contain equality operators are flattened
55//! to [`Primary`] expressions for improved evaluation performance.
56//!
57//! # Error Handling
58//!
59//! Equality evaluation provides detailed error messages when type promotion
60//! fails or incompatible types are compared:
61//!
62//! ```text
63//! "Expected Bool, Date, Number, Task or Text, found Type1 = Type2 ~formula"
64//! ```
65//!
66//! # Related Modules
67//!
68//! - [`relational`](super::relational) - Higher precedence relational operations
69//! - [`logical`](super::logical) - Lower precedence logical operations
70//! - [`expression`](crate::expression) - Top-level expression parsing
71//! - [`evaluate`](crate::evaluate) - Core evaluation traits and type promotion
72
73use crate::{
74 ContextLike,
75 evaluate_and_promote,
76 ExpressionLike,
77 expressions::{Relational, parse_relational},
78 Literal,
79 Primary,
80 Value,
81 Writer,
82};
83use nom::{
84 IResult, Parser, branch::alt, bytes::complete::tag, character::complete::multispace0,
85 combinator::map,
86};
87use std::fmt;
88use anyhow::{anyhow, Result};
89
90/// An equality expression node in the abstract syntax tree.
91///
92/// Represents an equality comparison (`=`, `!=`) or a lower-precedence
93/// expression that has been flattened in the AST. This enum is part of
94/// the AIMX expression hierarchy and implements the [`ExpressionLike`] trait.
95///
96/// # AST Flattening
97///
98/// The `Primary` variant represents an optimization where expressions that
99/// don't contain equality operators are flattened to reduce AST depth and
100/// improve evaluation performance. This flattening occurs during parsing
101/// when an expression doesn't contain any equality operators.
102///
103/// # Variants
104///
105/// ## `Equal(Relational, Relational)`
106/// Represents an equality comparison using the `=` or `==` operator.
107/// The operands are [`Relational`] expressions, which have higher precedence.
108///
109/// ## `NotEqual(Relational, Relational)`
110/// Represents an inequality comparison using the `!=` operator.
111/// The operands are [`Relational`] expressions, which have higher precedence.
112///
113/// ## `Primary(Primary)`
114/// A flattened primary expression for optimization. This variant is used
115/// when the expression doesn't contain any equality operators.
116///
117/// # Examples
118///
119/// ```rust
120/// use aimx::expressions::{
121/// equality::Equality,
122/// relational::Relational,
123/// primary::Primary,
124/// };
125/// use aimx::{Literal, ExpressionLike, Context};
126///
127/// // This demonstrates how equality expressions can be constructed
128/// // In practice, these would be parsed from text expressions
129///
130/// // Create an equality expression conceptually like: 5 = 5
131/// // Create an inequality expression conceptually like: 5 != 10
132/// ```
133///
134/// # See Also
135///
136/// - [`parse_equality`] - Function to parse equality expressions
137/// - [`ExpressionLike`] - Trait for expression evaluation
138/// - [`Relational`] - Higher precedence expression type
139/// - [`Primary`] - Flattened primary expression type
140#[derive(Debug, Clone, PartialEq)]
141pub enum Equality {
142 Equal(Relational, Relational),
143 NotEqual(Relational, Relational),
144 /// Primary flattened AST optimization
145 Primary(Box<Primary>),
146}
147
148/// Internal operator type for equality parsing.
149///
150/// This enum represents the equality operators during the parsing phase.
151/// It's used internally by the parser to distinguish between equality
152/// and inequality operations before constructing the final AST.
153#[derive(Debug, Clone, Copy, PartialEq)]
154enum EqualityOp {
155 /// Equality operator (`=`, `==`)
156 Equal,
157 /// Inequality operator (`!=`)
158 NotEqual,
159}
160
161/// Parse an equality operator `=`/`==` or `!=` and the following relational expression.
162///
163/// This internal helper function parses an equality operator followed by
164/// a relational expression. It handles optional whitespace and operator aliases.
165///
166/// # Grammar
167///
168/// ```text
169/// equality_operator := S? ('=' | "!=" | '==') S? relational
170/// ```
171///
172/// Where `S` represents optional whitespace.
173///
174/// # Returns
175///
176/// Returns an `IResult` containing:
177/// - The remaining input
178/// - A tuple of `(EqualityOp, Relational)` representing the operator and right operand
179fn equality_operator(input: &str) -> IResult<&str, (EqualityOp, Relational)> {
180 let (input, _) = multispace0.parse(input)?;
181 let (input, equality_op) = alt((
182 map(tag("!="), |_| EqualityOp::NotEqual),
183 map(tag("=="), |_| EqualityOp::Equal),
184 map(tag("="), |_| EqualityOp::Equal),
185 ))
186 .parse(input)?;
187 let (input, _) = multispace0.parse(input)?;
188 let (input, relational) = parse_relational(input)?;
189 Ok((input, (equality_op, relational)))
190}
191
192/// Parse an equality expression from a string slice.
193///
194/// This function is the main entry point for parsing equality expressions.
195/// It handles the complete equality grammar rule, including optional whitespace
196/// and operator aliases. The parser follows the AIMX precedence rules, where
197/// equality operators have lower precedence than relational operators.
198///
199/// # Grammar
200///
201/// The function implements this grammar rule:
202/// ```text
203/// equality := relational (S? ('=' | "!=" | '==') S? relational)?
204/// ```
205///
206/// Where `S` represents optional whitespace.
207///
208/// # Parsing Strategy
209///
210/// 1. First attempts to parse a relational expression
211/// 2. If an equality operator is found, parses the second relational expression
212/// 3. If no equality operator is found, flattens the expression to [`Primary`]
213/// 4. Returns the parsed [`Equality`] expression and remaining input
214///
215/// # Operator Support
216///
217/// The parser accepts these equality operators:
218/// - `=` (equality) - also accepts `==` as an alias
219/// - `!=` (inequality)
220///
221/// # AST Flattening
222///
223/// When no equality operator is found, the parser flattens the expression
224/// to optimize evaluation performance. This means expressions like `5` or
225/// `x > y` are represented as [`Equality::Primary`] rather than creating
226/// unnecessary AST nodes.
227///
228/// # Arguments
229///
230/// * `input` - The input string slice to parse
231///
232/// # Returns
233///
234/// Returns an [`IResult`] containing:
235/// - The remaining unparsed input (if any)
236/// - The parsed [`Equality`] expression
237///
238/// # Examples
239///
240/// ## Basic Equality
241///
242/// ```rust
243/// use aimx::expressions::equality::{parse_equality, Equality};
244///
245/// let (remaining, expr) = parse_equality("5 = 5").unwrap();
246/// assert_eq!(remaining, "");
247/// assert!(matches!(expr, Equality::Equal(_, _)));
248/// ```
249///
250/// ## Inequality with Whitespace
251///
252/// ```rust
253/// use aimx::expressions::equality::{parse_equality, Equality};
254///
255/// let (remaining, expr) = parse_equality("5 != 10").unwrap();
256/// assert_eq!(remaining, "");
257/// assert!(matches!(expr, Equality::NotEqual(_, _)));
258/// ```
259///
260/// ## Operator Aliases
261///
262/// ```rust
263/// use aimx::expressions::equality::{parse_equality, Equality};
264///
265/// // Both '=' and '==' are accepted
266/// let (_, expr1) = parse_equality("5 = 5").unwrap();
267/// let (_, expr2) = parse_equality("5 == 5").unwrap();
268/// assert!(matches!(expr1, Equality::Equal(_, _)));
269/// assert!(matches!(expr2, Equality::Equal(_, _)));
270/// ```
271///
272/// ## Complex Expressions
273///
274/// ```rust
275/// use aimx::expressions::equality::{parse_equality, Equality};
276///
277/// // Equality with relational expressions
278/// let (remaining, expr) = parse_equality("x > 0 = y < 10").unwrap();
279/// assert_eq!(remaining, "");
280/// assert!(matches!(expr, Equality::Equal(_, _)));
281///
282/// // Equality with arithmetic expressions
283/// let (remaining, expr) = parse_equality("2 + 3 = 5").unwrap();
284/// assert_eq!(remaining, "");
285/// assert!(matches!(expr, Equality::Equal(_, _)));
286/// ```
287///
288/// ## Flattened Expressions
289///
290/// ```rust
291/// use aimx::expressions::equality::{parse_equality, Equality};
292///
293/// // Expressions without equality operators are flattened
294/// let (remaining, expr) = parse_equality("5").unwrap();
295/// assert_eq!(remaining, "");
296/// assert!(matches!(expr, Equality::Primary(_)));
297///
298/// let (remaining, expr) = parse_equality("x > y").unwrap();
299/// assert_eq!(remaining, "");
300/// assert!(matches!(expr, Equality::Primary(_)));
301/// ```
302///
303/// ## Chaining Behavior
304///
305/// ```rust
306/// use aimx::expressions::equality::{parse_equality, Equality};
307///
308/// // Equality operations are not chained
309/// let (remaining, expr) = parse_equality("1 = 1 = 1").unwrap();
310/// assert_eq!(remaining, "= 1"); // Only first equality is parsed
311/// assert!(matches!(expr, Equality::Equal(_, _)));
312/// ```
313///
314/// # Error Handling
315///
316/// The function returns [`IResult`] which can contain parsing errors.
317/// Common errors include:
318/// - Invalid operator syntax
319/// - Malformed relational expressions
320/// - Unexpected end of input
321///
322/// # See Also
323///
324/// - [`Equality`] - The expression type returned by this function
325/// - [`parse_relational`] - Function for parsing relational expressions
326/// - [`ExpressionLike`] - Trait for expression evaluation
327pub fn parse_equality(input: &str) -> IResult<&str, Equality> {
328 let (input, first) = parse_relational(input)?;
329 if let Ok((input, (equality_op, second))) = equality_operator(input) {
330 let equality = match equality_op {
331 EqualityOp::Equal => Equality::Equal(first, second),
332 EqualityOp::NotEqual => Equality::NotEqual(first, second),
333 };
334 Ok((input, equality))
335 } else {
336 let equality = match first {
337 Relational::Primary(primary) => Equality::Primary(primary),
338 _ => Equality::Primary(Box::new(Primary::Relational(first))),
339 };
340 Ok((input, equality))
341 }
342}
343
344impl ExpressionLike for Equality {
345 /// Evaluate this equality expression within the given context.
346 ///
347 /// This method implements the core evaluation logic for equality expressions.
348 /// It handles both equality (`=`) and inequality (`!=`) comparisons, using
349 /// type promotion to ensure both operands have compatible types.
350 ///
351 /// # Evaluation Strategy
352 ///
353 /// 1. **Equality Comparison** (`Equality::Equal`):
354 /// - Evaluates both operands using [`evaluate_and_promote`]
355 /// - Promotes the right operand to match the left operand's type
356 /// - Compares values of the same type using `==` operator
357 /// - Returns `true` if equal, `false` otherwise
358 ///
359 /// 2. **Inequality Comparison** (`Equality::NotEqual`):
360 /// - Evaluates both operands using [`evaluate_and_promote`]
361 /// - Promotes the right operand to match the left operand's type
362 /// - Compares values of the same type using `!=` operator
363 /// - Returns `true` if not equal, `false` otherwise
364 ///
365 /// 3. **Flattened Expression** (`Equality::Primary`):
366 /// - Delegates evaluation to the underlying primary expression
367 ///
368 /// # Type Support
369 ///
370 /// Equality operations support these value types:
371 /// - `Bool` - Boolean equality comparison
372 /// - `Date` - DateTime equality comparison
373 /// - `Number` - Numeric equality comparison
374 /// - `Text` - String equality comparison
375 /// - `Task` - Task status equality comparison
376 ///
377 /// # Error Handling
378 ///
379 /// Returns an error if:
380 /// - Type promotion fails (incompatible types)
381 /// - Either operand evaluation fails
382 /// - Comparison of unsupported types is attempted
383 ///
384 /// Error messages include type information and the original formula:
385 /// ```text
386 /// "Expected Bool, Date, Number, Task or Text, found Type1 = Type2 ~formula"
387 /// ```
388 ///
389 /// # Arguments
390 ///
391 /// * `context` - The evaluation context providing variable values and function implementations
392 ///
393 /// # Returns
394 ///
395 /// Returns a `Result<Value>` containing:
396 /// - `Value::Literal(Literal::Bool(true))` if comparison evaluates to true
397 /// - `Value::Literal(Literal::Bool(false))` if comparison evaluates to false
398 /// - An error if evaluation fails
399 ///
400 /// # Examples
401 ///
402 /// ```rust
403 /// use aimx::expressions::equality::{Equality, parse_equality};
404 /// use aimx::{ExpressionLike, Context, Literal, Value};
405 ///
406 /// let mut context = Context::new();
407 ///
408 /// // Evaluate equality: 5 = 5
409 /// let expr = parse_equality("5 = 5").unwrap().1;
410 /// let result = expr.evaluate(&mut context).unwrap();
411 /// assert_eq!(result, Value::Literal(Literal::Bool(true)));
412 ///
413 /// // Evaluate inequality: 5 != 10
414 /// let expr = parse_equality("5 != 10").unwrap().1;
415 /// let result = expr.evaluate(&mut context).unwrap();
416 /// assert_eq!(result, Value::Literal(Literal::Bool(true)));
417 ///
418 /// // Evaluate with type promotion: 5 = "5"
419 /// let expr = parse_equality("5 = \"5\"").unwrap().1;
420 /// let result = expr.evaluate(&mut context).unwrap();
421 /// assert_eq!(result, Value::Literal(Literal::Bool(true)));
422 /// ```
423 fn evaluate(&self, context: &mut dyn ContextLike) -> Result<Value> {
424 match self {
425 Equality::Equal(left, right) => {
426 let (left_val, right_val) = evaluate_and_promote(context, left, right)?;
427 // For equality, we can compare values of the same type
428 let result = match (&left_val, &right_val) {
429 (Value::Literal(Literal::Bool(l)), Value::Literal(Literal::Bool(r))) => l == r,
430 (Value::Literal(Literal::Date(l)), Value::Literal(Literal::Date(r))) => l == r,
431 (Value::Literal(Literal::Number(l)), Value::Literal(Literal::Number(r))) => l == r,
432 (Value::Literal(Literal::Text(l)), Value::Literal(Literal::Text(r))) => l == r,
433 (Value::Literal(Literal::Task(_, l)), Value::Literal(Literal::Task(_, r))) => l == r,
434 _ => return Err(anyhow!(
435 "Expected Bool, Date, Number, Task or Text, found {} = {}~{}",
436 left_val.type_as_string(),
437 right_val.type_as_string(),
438 self.to_formula(),
439 )),
440 };
441 Ok(Value::Literal(Literal::Bool(result)))
442 }
443 Equality::NotEqual(left, right) => {
444 let (left_val, right_val) = evaluate_and_promote(context, left, right)?;
445 // For inequality, we can compare values of the same type
446 let result = match (&left_val, &right_val) {
447 (Value::Literal(Literal::Bool(l)), Value::Literal(Literal::Bool(r))) => l != r,
448 (Value::Literal(Literal::Date(l)), Value::Literal(Literal::Date(r))) => l != r,
449 (Value::Literal(Literal::Number(l)), Value::Literal(Literal::Number(r))) => l != r,
450 (Value::Literal(Literal::Text(l)), Value::Literal(Literal::Text(r))) => l != r,
451 (Value::Literal(Literal::Task(_, l)), Value::Literal(Literal::Task(_, r))) => l != r,
452 _ => return Err(anyhow!(
453 "Expected Bool, Date, Number, Task or Text, found {} != {}~{}",
454 left_val.type_as_string(),
455 right_val.type_as_string(),
456 self.to_formula(),
457 )), // Different types are not equal
458 };
459 Ok(Value::Literal(Literal::Bool(result)))
460 }
461 Equality::Primary(primary) => primary.evaluate(context),
462 }
463 }
464
465 /// Write this equality expression to the provided writer.
466 ///
467 /// This method serializes the expression to a text representation using
468 /// the writer's current formatting mode. The writer handles indentation,
469 /// operator spacing, and other formatting concerns.
470 ///
471 /// # Serialization Strategy
472 ///
473 /// - **Equality Comparison** (`Equality::Equal`):
474 /// - Writes left operand
475 /// - Writes `" = "` operator with spaces
476 /// - Writes right operand
477 ///
478 /// - **Inequality Comparison** (`Equality::NotEqual`):
479 /// - Writes left operand
480 /// - Writes `" != "` operator with spaces
481 /// - Writes right operand
482 ///
483 /// - **Flattened Expression** (`Equality::Primary`):
484 /// - Delegates writing to the underlying primary expression
485 ///
486 /// # Arguments
487 ///
488 /// * `writer` - The writer to serialize the expression to
489 ///
490 /// # Examples
491 ///
492 /// ```rust
493 /// use aimx::expressions::equality::{Equality, parse_equality};
494 /// use aimx::{Writer, PrintMode, Prefix};
495 ///
496 /// // This demonstrates the general pattern for writing equality expressions
497 /// // In practice, these would be used internally by the expression evaluator
498 ///
499 /// // Writing an equality expression like "5 = 5"
500 /// // Writing an inequality expression like "5 != 10"
501 /// ```
502 fn write(&self, writer: &mut Writer) {
503 match self {
504 Equality::Equal(left, right) => {
505 writer.write_binary_op(left, " = ", right);
506 }
507 Equality::NotEqual(left, right) => {
508 writer.write_binary_op(left, " != ", right);
509 }
510 Equality::Primary(primary) => primary.write(writer),
511 }
512 }
513
514 /// Convert this equality expression to a sanitized string representation.
515 ///
516 /// This method produces a string with special characters escaped to make it
517 /// safe for various contexts like JSON output, HTML embedding, or other
518 /// contexts where escaping is required.
519 ///
520 /// # Returns
521 ///
522 /// A sanitized string representation of this expression.
523 ///
524 fn to_sanitized(&self) -> String {
525 let mut writer = Writer::sanitizer();
526 self.write(&mut writer);
527 writer.finish()
528 }
529
530 /// Convert this equality expression to a formula string representation.
531 ///
532 /// This method produces a string with proper quoting and escaping for use
533 /// in formulas. The output should be round-trippable, meaning it can be
534 /// parsed back into an equivalent expression.
535 ///
536 /// # Returns
537 ///
538 /// A formula string representation of this expression.
539 ///
540 fn to_formula(&self) -> String {
541 let mut writer = Writer::formulizer();
542 self.write(&mut writer);
543 writer.finish()
544 }
545}
546
547impl fmt::Display for Equality {
548 /// Format this equality expression as a string.
549 ///
550 /// This implementation delegates to the [`write`](ExpressionLike::write) method
551 /// using a string-based writer with default formatting. The output is suitable
552 /// for display purposes and debugging.
553 ///
554 /// # Format
555 ///
556 /// The formatting follows these conventions:
557 /// - Operators are surrounded by spaces: `5 = 5`, `5 != 10`
558 /// - Complex expressions are properly parenthesized
559 /// - Literals are formatted according to their type
560 /// - Variables and references are displayed as-is
561 ///
562 /// # Examples
563 ///
564 /// ```rust
565 /// use aimx::expressions::equality::{Equality, parse_equality};
566 ///
567 /// let expr = parse_equality("5 = 5").unwrap().1;
568 /// assert_eq!(expr.to_string(), "5 = 5");
569 ///
570 /// let expr = parse_equality("x > 0 = y < 10").unwrap().1;
571 /// assert_eq!(expr.to_string(), "x > 0 = y < 10");
572 ///
573 /// let expr = parse_equality("5").unwrap().1;
574 /// assert_eq!(expr.to_string(), "5");
575 /// ```
576 ///
577 /// # See Also
578 ///
579 /// - [`ExpressionLike::write`] - The underlying serialization method
580 /// - [`ExpressionLike::to_formula`] - For round-trippable formula representation
581 /// - [`ExpressionLike::to_sanitized`] - For sanitized output
582 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
583 let mut writer = Writer::stringizer();
584 self.write(&mut writer);
585 write!(f, "{}", writer.finish())
586 }
587}