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}