aimx/expressions/unary.rs
1//! Unary expression parsing and evaluation for the AIM expression grammar.
2//!
3//! This module provides parsers and evaluation logic for unary expressions
4//! in the AIM expression language. Unary operations include logical NOT,
5//! unary plus and minus, and type casting operations. These operators have
6//! right-to-left associativity and form an important part of the expression
7//! hierarchy.
8//!
9//! # Overview
10//!
11//! Unary expressions operate on a single operand and include:
12//! - **Logical operations**: `!` (NOT)
13//! - **Arithmetic operations**: `+` (unary plus), `-` (unary minus)
14//! - **Type casting**: `(Type)` syntax for explicit type conversion
15//!
16//! # Operator Precedence
17//!
18//! Unary operators have the fourth-highest precedence in the AIMX grammar,
19//! coming after parentheses and postfix operations but before multiplicative
20//! operations. They are **right-to-left associative**, meaning operators group
21//! from right to left when chained.
22//!
23//! # Unary Operators
24//!
25//! | Operator | Description | Example | Result Type |
26//! |----------|-------------|---------|-------------|
27//! | `!` | Logical NOT | `!true` | `Bool` |
28//! | `+` | Unary plus | `+5` | `Number` |
29//! | `-` | Unary minus | `-3.14` | `Number` |
30//! | `(Bool)` | Cast to boolean | `(Bool)0` | `Bool` |
31//! | `(Date)` | Cast to date | `(Date)"2023-01-01"` | `Date` |
32//! | `(Number)` | Cast to number | `(Number)"123"` | `Number` |
33//! | `(Task)` | Cast to task | `(Task)"complete"` | `Task` |
34//! | `(Text)` | Cast to text | `(Text)42` | `Text` |
35//!
36//! # Examples
37//!
38//! ```text
39//! !true // logical NOT → false
40//! +5 // unary plus → 5
41//! -3.14 // unary minus → -3.14
42//! (Number)"123" // cast string to number → 123
43//! !(Bool)0 // cast 0 to bool then negate → true
44//! +-+-5 // multiple unary operators → -5
45//! ```
46//!
47//! # Type Casting Behavior
48//!
49//! Type casting operations convert values between AIMX types:
50//!
51//! - **`(Bool)`**: Converts any value to boolean using truthiness rules
52//! - **`(Date)`**: Parses strings as ISO 8601 dates
53//! - **`(Number)`**: Converts strings and booleans to numbers
54//! - **`(Task)`**: Creates task primitives from values
55//! - **`(Text)`**: Converts any value to string representation
56//!
57//! # Related Modules
58//!
59//! - [`postfix`](crate::expressions::postfix) - Higher precedence operations
60//! - [`multiplicative`](crate::expressions::multiplicative) - Lower precedence operations
61//! - [`literal`](crate::literal) - Literal value parsing
62//! - [`value`](crate::value) - Runtime value representation
63
64use nom::{
65 IResult,
66 Parser,
67 branch::alt,
68 character::complete::{char, multispace0},
69 combinator::map,
70 multi::many1,
71};
72use crate::{
73 ContextLike,
74 ExpressionLike,
75 expressions::{parse_postfix, Postfix},
76 Literal,
77 Primary,
78 Typedef,
79 parse_literal_type,
80 Value,
81 Writer
82};
83use std::fmt;
84use anyhow::{anyhow, Result};
85
86/// Represents a unary expression in the AIM grammar.
87///
88/// Unary expressions perform operations on a single operand, including
89/// logical NOT, unary plus/minus, and type casting. These operations have
90/// right-to-left associativity, meaning they group from right to left.
91///
92/// # AST Flattening Optimization
93///
94/// The `Unary` enum uses AST flattening where it includes variants for all
95/// lower-precedence expression types. This allows for efficient evaluation
96/// without deep recursion and simplifies the implementation of the
97/// [`ExpressionLike`] trait.
98///
99/// # Variants
100///
101/// ## Basic Unary Operations
102///
103/// These variants represent single-operand operations:
104///
105/// - `Not` - Logical NOT operation: `!operand`
106/// - `Positive` - Unary plus operation: `+operand`
107/// - `Negative` - Unary minus operation: `-operand`
108///
109/// ## Type Casting Operations
110///
111/// These variants provide explicit type conversion using the `(Type)` syntax:
112///
113/// - `CastBool` - Cast to boolean type: `(Bool)operand`
114/// - `CastDate` - Cast to date type: `(Date)operand`
115/// - `CastNumber` - Cast to number type: `(Number)operand`
116/// - `CastTask` - Cast to task type: `(Task)operand`
117/// - `CastText` - Cast to text type: `(Text)operand`
118///
119/// ## Flattened Expressions (AST Optimization)
120///
121/// These variants flatten the AST structure for efficient evaluation:
122///
123/// - `Primary` - Flattened primary expression (literals, references, parentheses)
124///
125/// # Examples
126///
127/// ```rust
128/// use aimx::expressions::unary::{Unary, parse_unary};
129///
130/// // Parse and match different Unary variants
131/// let (_, not_expr) = parse_unary("!true").unwrap();
132/// assert!(matches!(not_expr, Unary::Not(_)));
133///
134/// let (_, positive_expr) = parse_unary("+5").unwrap();
135/// assert!(matches!(positive_expr, Unary::Positive(_)));
136///
137/// let (_, cast_expr) = parse_unary("(Number)\"123\"").unwrap();
138/// assert!(matches!(cast_expr, Unary::CastNumber(_)));
139///
140/// // Chained operations demonstrate right-to-left associativity
141/// let (_, chained) = parse_unary("!(Bool)0").unwrap();
142/// // This parses as !((Bool)0) - NOT applied to the result of the cast
143/// ```
144///
145/// # Evaluation Behavior
146///
147/// Each variant has specific evaluation behavior:
148///
149/// - **`Not`**: Evaluates operand to boolean, then applies logical NOT
150/// - **`Positive`/`Negative`**: Evaluates operand to number, then applies sign
151/// - **Cast variants**: Evaluates operand, then converts to target type
152/// - **`Primary`**: Delegates evaluation to the contained primary expression
153#[derive(Debug, Clone, PartialEq)]
154pub enum Unary {
155 /// Logical NOT operation: !operand
156 Not(Box<Unary>),
157 /// Unary plus operation: +operand
158 Positive(Box<Unary>),
159 /// Unary minus operation: -operand
160 Negative(Box<Unary>),
161 /// Cast to boolean type: (Bool)operand
162 CastBool(Box<Unary>),
163 /// Cast to date type: (Date)operand
164 CastDate(Box<Unary>),
165 /// Cast to number type: (Number)operand
166 CastNumber(Box<Unary>),
167 /// Cast to task type: (Task)operand
168 CastTask(Box<Unary>),
169 /// Cast to text type: (Text)operand
170 CastText(Box<Unary>),
171 /// Primary flattened AST optimization
172 Primary(Box<Primary>),
173}
174
175/// Parse a cast operator expression (type).
176///
177/// This function parses type casting syntax in the form `(Type)` where Type
178/// is a valid type name from the AIM type system. The parser handles optional
179/// whitespace around the parentheses and type name.
180///
181/// # Arguments
182///
183/// * `input` - A string slice containing the cast operator to parse
184///
185/// # Returns
186///
187/// * `IResult<&str, Typedef>` - A nom result with remaining input and parsed type definition
188///
189/// # Examples
190///
191/// ```rust
192/// use aimx::expressions::unary::{Unary, parse_unary};
193///
194/// // Parse cast expressions correctly
195/// let result = parse_unary("(Bool) true");
196/// assert!(result.is_ok());
197/// ```
198fn cast_operator(input: &str) -> IResult<&str, Typedef> {
199 let (input, _) = char('(').parse(input)?;
200 let (input, _) = multispace0.parse(input)?;
201 let (input, typedef) = parse_literal_type(input)?;
202 let (input, _) = multispace0.parse(input)?;
203 let (input, _) = char(')').parse(input)?;
204 Ok((input, typedef))
205}
206
207/// Internal representation of unary operators during parsing.
208///
209/// This enum is used internally by the parser to represent different types
210/// of unary operators before they are converted to the final `Unary` AST.
211enum UnaryOp {
212 /// Logical NOT operator: !
213 Not,
214 /// Unary plus operator: +
215 Positive,
216 /// Unary minus operator: -
217 Negative,
218 /// Type cast operator: (type)
219 Cast(Typedef),
220}
221
222/// Parse a unary operator.
223///
224/// This function parses unary operators including logical NOT, unary plus/minus,
225/// and type casting operators. It handles optional whitespace around operators
226/// and uses the `alt` combinator to try each operator type in sequence.
227///
228/// # Arguments
229///
230/// * `input` - A string slice containing the unary operator to parse
231///
232/// # Returns
233///
234/// * `IResult<&str, UnaryOp>` - A nom result with remaining input and parsed unary operator
235///
236/// # Operator Precedence
237///
238/// The parser tries operators in this order:
239/// 1. `!` - Logical NOT
240/// 2. `+` - Unary plus
241/// 3. `-` - Unary minus
242/// 4. `(type)` - Type casting
243///
244/// # Examples
245///
246/// ```rust
247/// use aimx::expressions::unary::{Unary, parse_unary};
248///
249/// // Test parsing different unary operators with operands
250/// let result = parse_unary("!true");
251/// assert!(result.is_ok());
252///
253/// let result = parse_unary("+5");
254/// assert!(result.is_ok());
255///
256/// let result = parse_unary("(Bool) 0");
257/// assert!(result.is_ok());
258/// ```
259fn unary_operator(input: &str) -> IResult<&str, UnaryOp> {
260 let (input, _) = multispace0.parse(input)?;
261 let (input, unary) = alt((
262 map(char('!'), |_| UnaryOp::Not),
263 map(char('+'), |_| UnaryOp::Positive),
264 map(char('-'), |_| UnaryOp::Negative),
265 map(cast_operator, |typedef| UnaryOp::Cast(typedef)),
266 )).parse(input)?;
267 let (input, _) = multispace0.parse(input)?;
268 Ok((input, unary))
269}
270
271impl Unary {
272 /// Create a new unary expression from a unary operator and operand.
273 ///
274 /// This function constructs the appropriate Unary variant based on the
275 /// provided operator and operand. It boxes the operand to enable
276 /// recursive expression structures.
277 ///
278 /// # Arguments
279 ///
280 /// * `op` - The unary operator to apply
281 /// * `unary` - The operand expression
282 ///
283 /// # Returns
284 ///
285 /// * `Unary` - The constructed unary expression
286 ///
287 /// # Examples
288 ///
289 /// ```rust
290 /// use aimx::expressions::unary::{Unary, parse_unary};
291 ///
292 /// // Create operand for demonstration
293 /// let operand = Unary::Primary(Box::new(aimx::Primary::Literal(aimx::Literal::Bool(true))));
294 /// // The Unary::new function is private, so use parse_unary to create expressions
295 /// let parsed_expr = parse_unary("!true").unwrap().1;
296 /// assert!(matches!(parsed_expr, Unary::Not(_)));
297 /// ```
298 fn new(op: UnaryOp, unary: Unary) -> Self {
299 let boxed = Box::new(unary);
300 match op {
301 UnaryOp::Not => Unary::Not(boxed),
302 UnaryOp::Positive => Unary::Positive(boxed),
303 UnaryOp::Negative => Unary::Negative(boxed),
304 UnaryOp::Cast(typedef) => match typedef {
305 Typedef::Bool => Unary::CastBool(boxed),
306 Typedef::Date => Unary::CastDate(boxed),
307 Typedef::Number => Unary::CastNumber(boxed),
308 Typedef::Task => Unary::CastTask(boxed),
309 Typedef::Text => Unary::CastText(boxed),
310 _ => unreachable!(),
311 }
312 }
313 }
314}
315
316/// Parse a unary expression.
317///
318/// This function parses unary expressions including logical NOT, unary plus/minus,
319/// and type casting operations. It handles the right-to-left associativity of
320/// unary operators and chaining of multiple unary operations.
321///
322/// # Arguments
323///
324/// * `input` - A string slice containing the unary expression to parse
325///
326/// # Returns
327///
328/// * `IResult<&str, Unary>` - A nom result with remaining input and parsed unary expression
329///
330/// # Parsing Algorithm
331///
332/// The parser follows this algorithm:
333/// 1. Attempt to parse one or more unary operators using `many1(unary_operator)`
334/// 2. If successful, parse the postfix expression (operand)
335/// 3. Convert the postfix to a unary expression
336/// 4. Apply unary operators in right-to-left order (reverse of parsing order)
337/// 5. If no unary operators are found, parse the expression as a postfix expression
338///
339/// # Right-to-Left Associativity
340///
341/// Unary operators are right-to-left associative. This means `!+5` parses as `!(+5)`
342/// rather than `(!+)5`. The parser maintains a stack of operators and applies them
343/// in reverse order to achieve this associativity.
344///
345/// # Examples
346///
347/// ```rust
348/// use aimx::expressions::unary::parse_unary;
349///
350/// // Basic unary operations
351/// let (remaining, expr) = parse_unary("!true").unwrap();
352/// assert_eq!(remaining, "");
353///
354/// // Chained operations demonstrate right-to-left associativity
355/// let (remaining, expr) = parse_unary("!(Bool)0").unwrap();
356/// assert_eq!(remaining, "");
357///
358/// // Multiple unary operators
359/// let (remaining, expr) = parse_unary("+-+5").unwrap();
360/// assert_eq!(remaining, "");
361///
362/// // Type casting
363/// let (remaining, expr) = parse_unary("(Number)\"123\"").unwrap();
364/// assert_eq!(remaining, "");
365/// ```
366///
367/// # Error Handling
368///
369/// Returns `IResult::Err` if the input cannot be parsed as a valid unary expression.
370/// Common errors include:
371/// - Invalid unary operator syntax
372/// - Missing operand after unary operator
373/// - Invalid type name in cast operator
374pub fn parse_unary(input: &str) -> IResult<&str, Unary> {
375 // Chain any unary operators
376 if let Ok((input, mut unary_chain)) = many1(unary_operator).parse(input) {
377 // Get the primary operand
378 let (input, postfix) = parse_postfix(input)?;
379 // Convert to unary
380 let mut unary = match postfix {
381 Postfix::Primary(primary) => Unary::Primary(primary),
382 _ => Unary::Primary(Box::new(Primary::Postfix(postfix))),
383 };
384 // Iterate unary chain right to left
385 while let Some(op) = unary_chain.pop() {
386 unary = Unary::new(op, unary);
387 }
388 Ok((input, unary))
389 } else {
390 // Parse the primary
391 let (input, postfix) = parse_postfix(input)?;
392 let unary = match postfix {
393 Postfix::Primary(primary) => Unary::Primary(primary),
394 _ => Unary::Primary(Box::new(Primary::Postfix(postfix))),
395 };
396 Ok((input, unary))
397 }
398
399}
400
401impl ExpressionLike for Unary {
402 /// Evaluate the unary expression within the given context.
403 ///
404 /// This method evaluates the expression recursively, handling each variant
405 /// according to its specific semantics. Evaluation follows the operator
406 /// precedence rules and handles type conversions as needed.
407 ///
408 /// # Arguments
409 ///
410 /// * `context` - The evaluation context providing variable values and function implementations
411 ///
412 /// # Returns
413 ///
414 /// * `Result<Value>` - The evaluated result or an error if evaluation fails
415 ///
416 /// # Evaluation Behavior by Variant
417 ///
418 /// - **`Not`**: Evaluates operand to boolean using `as_bool()`, then applies logical NOT
419 /// - **`Positive`**: Evaluates operand to number using `as_number()`, preserves sign
420 /// - **`Negative`**: Evaluates operand to number using `as_number()`, negates value
421 /// - **Cast variants**: Evaluates operand, then converts to target type using corresponding `as_*()` method
422 /// - **`Primary`**: Delegates evaluation to the contained primary expression
423 ///
424 /// # Error Handling
425 ///
426 /// Returns `Err` if:
427 /// - Operand evaluation fails
428 /// - Type conversion fails (e.g., non-numeric operand for `+`/`-`)
429 /// - Invalid type for cast operation
430 ///
431 /// # Examples
432 ///
433 /// ```rust
434 /// use aimx::{expressions::unary::Unary, ExpressionLike, Context, Literal};
435 ///
436 /// let mut context = Context::new();
437 ///
438 /// // Create a simple unary expression for testing
439 /// let expr = Unary::CastText(Box::new(Unary::Primary(Box::new(aimx::Primary::Literal(Literal::from_number(42.0))))));
440 /// let result = expr.evaluate(&mut context).unwrap();
441 /// assert_eq!(result.to_string(), "42");
442 /// ```
443 fn evaluate(&self, context: &mut dyn ContextLike) -> Result<Value> {
444 match self {
445 Unary::Not(operand) => {
446 let val = operand.evaluate(context)?;
447 let found = val.type_as_string();
448 match val.as_bool() {
449 Value::Literal(Literal::Bool(b)) => Ok(Value::Literal(Literal::Bool(!b))),
450 _ => Err(anyhow!(
451 "Expected Bool, found {}~{}",
452 found,
453 self.to_formula(),
454 )),
455 }
456 },
457 Unary::Positive(operand) => {
458 let val = operand.evaluate(context)?;
459 let found = val.type_as_string();
460 match val.as_number() {
461 Ok(Value::Literal(Literal::Number(n))) => Ok(Value::Literal(Literal::Number(n))),
462 _ => Err(anyhow!(
463 "Expected numeric operand, found {}~{}",
464 found,
465 self.to_formula(),
466 )),
467 }
468 },
469 Unary::Negative(operand) => {
470 let val = operand.evaluate(context)?;
471 let found = val.type_as_string();
472 match val.as_number() {
473 Ok(Value::Literal(Literal::Number(n))) => Ok(Value::Literal(Literal::Number(-n))),
474 _ => Err(anyhow!(
475 "Expected numeric operand, found {}~{}",
476 found,
477 self.to_formula(),
478 )),
479 }
480 },
481 Unary::CastBool(operand) => {
482 let val = operand.evaluate(context)?;
483 Ok(val.as_bool())
484 },
485 Unary::CastDate(operand) => {
486 let val = operand.evaluate(context)?;
487 val.as_date()
488 },
489 Unary::CastNumber(operand) => {
490 let val = operand.evaluate(context)?;
491 val.as_number()
492 },
493 Unary::CastTask(operand) => {
494 let val = operand.evaluate(context)?;
495 val.as_task()
496 },
497 Unary::CastText(operand) => {
498 let val = operand.evaluate(context)?;
499 val.as_text()
500 },
501 Unary::Primary(primary) => primary.evaluate(context),
502 }
503 }
504
505 fn write(&self, writer: &mut Writer) {
506 match self {
507 Unary::Not(operand) => {
508 writer.write_unary_op("!", operand.as_ref());
509 },
510 Unary::Positive(operand) => {
511 writer.write_unary_op("+", operand.as_ref());
512 },
513 Unary::Negative(operand) => {
514 writer.write_unary_op("-", operand.as_ref());
515 },
516 Unary::CastBool(operand) => {
517 writer.write_str("(Bool)");
518 operand.write(writer);
519 },
520 Unary::CastDate(operand) => {
521 writer.write_str("(Date)");
522 operand.write(writer);
523 },
524 Unary::CastNumber(operand) => {
525 writer.write_str("(Number)");
526 operand.write(writer);
527 },
528 Unary::CastTask(operand) => {
529 writer.write_str("(Task)");
530 operand.write(writer);
531 },
532 Unary::CastText(operand) => {
533 writer.write_str("(Text)");
534 operand.write(writer);
535 },
536 Unary::Primary(primary) => primary.write(writer),
537 }
538 }
539
540 /// Convert the expression to a sanitized string representation.
541 ///
542 /// Sanitized strings are suitable for display to users and may omit
543 /// sensitive information or simplify complex expressions.
544 ///
545 /// # Returns
546 ///
547 /// * `String` - A sanitized string representation of the expression
548 fn to_sanitized(&self) -> String {
549 let mut writer = Writer::sanitizer();
550 self.write(&mut writer);
551 writer.finish()
552 }
553
554 /// Convert the expression to a formula string representation.
555 ///
556 /// Formula strings preserve the exact syntax of the original expression
557 /// and are suitable for serialization or debugging.
558 ///
559 /// # Returns
560 ///
561 /// * `String` - A formula string representation of the expression
562 fn to_formula(&self) -> String {
563 let mut writer = Writer::formulizer();
564 self.write(&mut writer);
565 writer.finish()
566 }
567}
568
569impl fmt::Display for Unary {
570 /// Format the unary expression as a string.
571 ///
572 /// This implementation uses the `Writer::stringizer()` to produce
573 /// a human-readable string representation of the expression.
574 ///
575 /// # Arguments
576 ///
577 /// * `f` - The formatter to write to
578 ///
579 /// # Returns
580 ///
581 /// * `fmt::Result` - The result of the formatting operation
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}