aimx/expressions/
logical.rs

1//! Logical expression parsing and evaluation.
2//!
3//! This module handles parsing and evaluating logical expressions using the
4//! `&` (AND) and `|` (OR) operators. Both single (`&`, `|`) and double (`&&`, `||`)
5//! operators are accepted for compatibility with C-style syntax.
6//!
7//! Logical operators have left associativity with AND having higher precedence than OR.
8//! This follows the standard mathematical convention where AND binds tighter than OR.
9//!
10//! # Operator Precedence
11//!
12//! Logical operators fit into the overall AIMX operator precedence hierarchy:
13//!
14//! | Operator Group | Operators | Associativity |
15//! |----------------|-----------|---------------|
16//! | Equality | `=` `!=` | Left to right |
17//! | **Logical AND** | `&` `&&` | **Left to right** |
18//! | **Logical OR** | `|` `||` | **Left to right** |
19//! | Conditional | `?` `:` | Right to left |
20//!
21//! Note that logical AND has higher precedence than logical OR, so expressions like
22//! `a & b | c & d` are evaluated as `(a & b) | (c & d)`.
23//!
24//! # Grammar
25//!
26//! ```text
27//! or := and (S? ('|' | "||") S? and)*
28//! and := equality (S? ('&' | "&&") S? equality)*
29//! ```
30//!
31//! Where `S` represents optional whitespace.
32//!
33//! # Examples
34//!
35//! ## Basic Operations
36//!
37//! ```text
38//! true & false              // false
39//! true | false              // true
40//! true && false             // false (C-style compatibility)
41//! true || false             // true (C-style compatibility)
42//! ```
43//!
44//! ## Complex Expressions
45//!
46//! ```text
47//! x > 0 & y < 10           // Both conditions must be true
48//! x > 0 | y < 10           // Either condition can be true
49//! a & b | c & d            // Evaluated as (a & b) | (c & d)
50//! !x & y | z                // Evaluated as (!x & y) | z
51//! ```
52//!
53//! # Type Safety
54//!
55//! Logical operators require boolean operands. When evaluating expressions:
56//! - Both operands are checked for boolean type compatibility
57//! - Type mismatch errors provide detailed error messages with source locations
58//! - Evaluation follows standard logical rules with short-circuiting semantics
59//!
60//! # Evaluation Behavior
61//!
62//! - **AND (`&`)**: Returns `true` only if both operands are `true`. Implements short-circuiting - if the left operand is `false`, the right operand is not evaluated.
63//! - **OR (`|`)**: Returns `true` if at least one operand is `true`. Implements short-circuiting - if the left operand is `true`, the right operand is not evaluated.
64//! - **Error Handling**: Type mismatches return descriptive error messages with the formula and types involved
65//!
66//! # Related Modules
67//!
68//! - [`equality`](crate::expressions::equality) - Higher precedence equality operations
69//! - [`expression`](crate::expression) - Top-level expression parsing
70//! - [`evaluate`](crate::evaluate) - Core evaluation traits
71//! - [`conditional`](crate::expressions::conditional) - Lower precedence ternary operations
72
73use crate::{
74    ContextLike,
75    ExpressionLike,
76    expressions::{Equality, parse_equality},
77    Literal,
78    Primary,
79    Value,
80    Writer,
81};
82use nom::{
83    IResult, Parser, branch::alt, bytes::complete::tag, character::complete::multispace0,
84    multi::many0,
85};
86use std::fmt;
87use anyhow::{anyhow, Result};
88
89/// A logical AND expression node in the abstract syntax tree.
90///
91/// Represents a logical AND operation (`&` or `&&`) or a lower-precedence
92/// expression that has been flattened in the AST. This enum follows the
93/// recursive descent parsing pattern used throughout the AIMX expression grammar.
94///
95/// # AST Structure
96///
97/// The `LogicalAnd` enum forms part of the recursive AST structure where:
98/// - `And` nodes represent binary AND operations
99/// - `Primary` nodes represent flattened expressions from lower precedence levels
100///
101/// # Variants
102///
103/// ## `And(Box<LogicalAnd>, Equality)`
104/// Represents a binary logical AND operation. The left operand is another
105/// `LogicalAnd` expression (allowing chaining), and the right operand is
106/// an `Equality` expression (higher precedence than logical operators).
107///
108/// ## `Primary(Box<Primary>)`
109/// Represents expressions that don't contain logical AND operations.
110/// This variant is used for AST flattening optimization.
111///
112/// # Examples
113///
114/// ```rust
115/// use aimx::expressions::{
116///     equality::Equality,
117///     logical::LogicalAnd,
118///     primary::Primary,
119/// };
120/// use aimx::Literal;
121///
122/// // Represents the expression: true & false
123/// let and_expr = LogicalAnd::And(
124///     Box::new(LogicalAnd::Primary(Box::new(Primary::Literal(Literal::Bool(true))))),
125///     Equality::Primary(Box::new(Primary::Literal(Literal::Bool(false))))
126/// );
127/// ```
128///
129/// # Evaluation
130///
131/// When evaluated, `LogicalAnd` expressions:
132/// - Require boolean operands for AND operations
133/// - Return `true` only if both operands evaluate to `true`
134/// - Provide detailed error messages for type mismatches
135///
136/// # See Also
137///
138/// - [`LogicalOr`] - Logical OR expressions
139/// - [`Equality`] - Higher precedence operations
140/// - [`Primary`] - Base expression types
141#[derive(Debug, Clone, PartialEq)]
142pub enum LogicalAnd {
143    And(Box<LogicalAnd>, Equality),
144    /// Primary flattened AST optimization
145    Primary(Box<Primary>),
146}
147
148/// Parse a logical AND operator (`&` or `&&`) and the following equality expression.
149fn and_operator(input: &str) -> IResult<&str, Equality> {
150    let (input, _) = multispace0.parse(input)?;
151    let (input, _) = alt((tag("&&"), tag("&"))).parse(input)?;
152    let (input, _) = multispace0.parse(input)?;
153    let (input, equality) = parse_equality(input)?;
154    Ok((input, equality))
155}
156
157/// Parses logical AND operations (`&`, `&&`) according to left associativity,
158/// or falls back to parsing equality expressions.
159///
160/// # Arguments
161///
162/// * `input` - The input string slice to parse
163///
164/// # Returns
165///
166/// A `IResult` containing the remaining input and parsed `LogicalAnd` expression.
167///
168/// # Grammar
169///
170/// ```text
171/// and := equality (S? ('&' | "&&") S? equality)*
172/// ```
173///
174/// Where `S` represents optional whitespace.
175///
176/// # Examples
177///
178/// ```rust
179/// use aimx::expressions::logical::parse_and;
180///
181/// // Simple AND
182/// let result = parse_and("true & false");
183/// assert!(result.is_ok());
184///
185/// // Chained AND operations (left associative)
186/// let result = parse_and("true & false & true");
187/// assert!(result.is_ok());
188/// // Parsed as (true & false) & true
189///
190/// // With whitespace
191/// let result = parse_and("true && false");
192/// assert!(result.is_ok());
193/// ```
194pub fn parse_and(input: &str) -> IResult<&str, LogicalAnd> {
195    let (input, first) = parse_equality(input)?;
196    let (input, logical_chain) = many0(and_operator).parse(input)?;
197
198    let and = logical_chain.into_iter().fold(
199        match first {
200            Equality::Primary(primary) => LogicalAnd::Primary(primary),
201            _ => LogicalAnd::Primary(Box::new(Primary::Equality(first))),
202        },
203        |acc, next| LogicalAnd::And(Box::new(acc), next),
204    );
205
206    Ok((input, and))
207}
208
209/// A logical OR expression node in the abstract syntax tree.
210///
211/// Represents a logical OR operation (`|` or `||`) or a lower-precedence
212/// expression that has been flattened in the AST. This enum follows the
213/// recursive descent parsing pattern used throughout the AIMX expression grammar.
214///
215/// # AST Structure
216///
217/// The `LogicalOr` enum forms part of the recursive AST structure where:
218/// - `Or` nodes represent binary OR operations
219/// - `Primary` nodes represent flattened expressions from lower precedence levels
220///
221/// # Variants
222///
223/// ## `Or(Box<LogicalOr>, LogicalAnd)`
224/// Represents a binary logical OR operation. The left operand is another
225/// `LogicalOr` expression (allowing chaining), and the right operand is
226/// a `LogicalAnd` expression (higher precedence than OR operations).
227///
228/// ## `Primary(Box<Primary>)`
229/// Represents expressions that don't contain logical OR operations.
230/// This variant is used for AST flattening optimization.
231///
232/// # Examples
233///
234/// ```rust
235/// use aimx::expressions::{
236///     logical::LogicalOr,
237///     logical::LogicalAnd,
238///     primary::Primary,
239/// };
240/// use aimx::Literal;
241///
242/// // Represents the expression: true | false
243/// let or_expr = LogicalOr::Or(
244///     Box::new(LogicalOr::Primary(Box::new(Primary::Literal(Literal::Bool(true))))),
245///     LogicalAnd::Primary(Box::new(Primary::Literal(Literal::Bool(false))))
246/// );
247/// ```
248///
249/// # Evaluation
250///
251/// When evaluated, `LogicalOr` expressions:
252/// - Require boolean operands for OR operations
253/// - Return `true` if at least one operand evaluates to `true`
254/// - Provide detailed error messages for type mismatches
255/// - Support short-circuit evaluation (right operand not evaluated if left is `true`)
256///
257/// # See Also
258///
259/// - [`LogicalAnd`] - Logical AND expressions
260/// - [`Primary`] - Base expression types
261#[derive(Debug, Clone, PartialEq)]
262pub enum LogicalOr {
263    Or(Box<LogicalOr>, LogicalAnd),
264    /// Primary flattened AST optimization
265    Primary(Box<Primary>),
266}
267
268/// Parse a logical OR operator (`|` or `||`) and the following AND expression.
269fn or_operator(input: &str) -> IResult<&str, LogicalAnd> {
270    let (input, _) = multispace0.parse(input)?;
271    let (input, _) = alt((tag("||"), tag("|"))).parse(input)?;
272    let (input, _) = multispace0.parse(input)?;
273    let (input, and) = parse_and(input)?;
274    Ok((input, and))
275}
276
277/// Parse a logical OR expression.
278///
279/// Parses logical OR operations (`|`, `||`) according to left associativity,
280/// or falls back to parsing logical AND expressions.
281///
282/// # Arguments
283///
284/// * `input` - The input string slice to parse
285///
286/// # Returns
287///
288/// A `IResult` containing the remaining input and parsed `LogicalOr` expression.
289///
290/// # Grammar
291///
292/// ```text
293/// or := and (S? ('|' | "||") S? and)*
294/// ```
295///
296/// Where `S` represents optional whitespace.
297///
298/// # Examples
299///
300/// ```rust
301/// use aimx::expressions::logical::parse_or;
302///
303/// // Simple OR
304/// let result = parse_or("true | false");
305/// assert!(result.is_ok());
306///
307/// // Chained OR operations (left associative)
308/// let result = parse_or("true | false | true");
309/// assert!(result.is_ok());
310/// // Parsed as (true | false) | true
311///
312/// // With whitespace
313/// let result = parse_or("true || false");
314/// assert!(result.is_ok());
315///
316/// // With higher precedence operations
317/// let result = parse_or("x > 0 | y < 10");
318/// assert!(result.is_ok());
319/// ```
320pub fn parse_or(input: &str) -> IResult<&str, LogicalOr> {
321    let (input, first) = parse_and(input)?;
322    let (input, logical_chain) = many0(or_operator).parse(input)?;
323
324    let or = logical_chain.into_iter().fold(
325        match first {
326            LogicalAnd::Primary(primary) => LogicalOr::Primary(primary),
327            _ => LogicalOr::Primary(Box::new(Primary::LogicalAnd(first))),
328        },
329        |acc, next| LogicalOr::Or(Box::new(acc), next),
330    );
331
332    Ok((input, or))
333}
334
335impl ExpressionLike for LogicalAnd {
336    fn evaluate(&self, context: &mut dyn ContextLike) -> Result<Value> {
337        match self {
338            LogicalAnd::And(left, right) => {
339                // Evaluate both operands
340                let left_val = left.evaluate(context)?;
341                let right_val = right.evaluate(context)?;
342                
343                // Convert both operands to boolean using type promotion rules
344                let left_bool = left_val.clone().as_bool();
345                let right_bool = right_val.clone().as_bool();
346                
347                // Extract boolean values
348                if left_bool.is_bool() && right_bool.is_bool() {
349                    let left_literal = left_bool.to_literal();
350                    let right_literal = right_bool.to_literal();
351                    
352                    if let (Literal::Bool(l), Literal::Bool(r)) = (left_literal, right_literal) {
353                        // Short-circuit: if left is false, result is false without evaluating right
354                        if !*l {
355                            return Ok(Value::Literal(Literal::Bool(false)));
356                        }
357                        // Return the result of left AND right
358                        return Ok(Value::Literal(Literal::Bool(*l && *r)));
359                    }
360                }
361                
362                // Type mismatch error
363                Err(anyhow!(
364                    "Expected Bool, found {} & {}~{}",
365                    left_val.type_as_string(),
366                    right_val.type_as_string(),
367                    self.to_formula(),
368                ))
369            }
370            LogicalAnd::Primary(primary) => primary.evaluate(context),
371        }
372    }
373
374    fn write(&self, writer: &mut Writer) {
375        match self {
376            LogicalAnd::And(left, right) => {
377                writer.write_binary_op(left.as_ref(), " & ", right);
378            }
379            LogicalAnd::Primary(primary) => primary.write(writer),
380        }
381    }
382    fn to_sanitized(&self) -> String {
383        let mut writer = Writer::sanitizer();
384        self.write(&mut writer);
385        writer.finish()
386    }
387    fn to_formula(&self) -> String {
388        let mut writer = Writer::formulizer();
389        self.write(&mut writer);
390        writer.finish()
391    }
392}
393
394impl ExpressionLike for LogicalOr {
395    fn evaluate(&self, context: &mut dyn ContextLike) -> Result<Value> {
396        match self {
397            LogicalOr::Or(left, right) => {
398                // Evaluate both operands
399                let left_val = left.evaluate(context)?;
400                let right_val = right.evaluate(context)?;
401                
402                // Convert both operands to boolean using type promotion rules
403                let left_bool = left_val.clone().as_bool();
404                let right_bool = right_val.clone().as_bool();
405                
406                // Extract boolean values
407                if left_bool.is_bool() && right_bool.is_bool() {
408                    let left_literal = left_bool.to_literal();
409                    let right_literal = right_bool.to_literal();
410                    
411                    if let (Literal::Bool(l), Literal::Bool(r)) = (left_literal, right_literal) {
412                        // Short-circuit: if left is true, result is true without evaluating right
413                        if *l {
414                            return Ok(Value::Literal(Literal::Bool(true)));
415                        }
416                        // Return the result of left OR right
417                        return Ok(Value::Literal(Literal::Bool(*l || *r)));
418                    }
419                }
420                
421                // Type mismatch error
422                Err(anyhow!(
423                    "Expected Bool, found {} | {}~{}",
424                    left_val.type_as_string(),
425                    right_val.type_as_string(),
426                    self.to_formula(),
427                ))
428            }
429            LogicalOr::Primary(primary) => primary.evaluate(context),
430        }
431    }
432    
433   fn write(&self, writer: &mut Writer) {
434        match self {
435            LogicalOr::Or(left, right) => {
436                writer.write_binary_op(left.as_ref(), " | ", right);
437            }
438            LogicalOr::Primary(primary) => primary.write(writer),
439        }
440    }
441    fn to_sanitized(&self) -> String {
442        let mut writer = Writer::sanitizer();
443        self.write(&mut writer);
444        writer.finish()
445    }
446    fn to_formula(&self) -> String {
447        let mut writer = Writer::formulizer();
448        self.write(&mut writer);
449        writer.finish()
450    }
451}
452
453impl fmt::Display for LogicalAnd {
454    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
455        let mut writer = Writer::stringizer();
456        self.write(&mut writer);
457        write!(f, "{}", writer.finish())
458    }
459}
460
461impl fmt::Display for LogicalOr {
462    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
463        let mut writer = Writer::stringizer();
464        self.write(&mut writer);
465        write!(f, "{}", writer.finish())
466    }
467}