aimx/expressions/
additive.rs

1//! Additive expression parsing and evaluation for AIMX.
2//!
3//! Parses `+` and `-` expressions as left-associative over multiplicative terms and
4//! evaluates them for numbers, text, and booleans.
5//! - Numbers: `+` add, `-` subtract.
6//! - Text: `+` concatenate, `-` remove last occurrence of the right operand (no-op if absent/empty).
7//! - Bool: `+` XOR, `-` XNOR.
8//! Returns errors for incompatible operand types including the reconstructed formula for context.
9//!
10//! Related: [`multiplicative`](super::multiplicative), [`relational`](super::relational),
11//! [`expression`](super::expression), [`primary`](super::primary).
12
13use crate::{
14    aim::{ContextLike, Writer, WriterLike},
15    expressions::{
16        ExpressionLike, Multiplicative, Primary, evaluate_and_promote, parse_multiplicative,
17    },
18    literals::Literal,
19    values::Value,
20};
21use anyhow::{Result, anyhow};
22use nom::{
23    IResult, Parser,
24    branch::alt,
25    character::complete::{char, multispace0},
26    multi::many0,
27};
28use std::{
29    fmt,
30    sync::Arc,
31};
32
33/// Additive expression AST node.
34///
35/// Represents `+` / `-` over multiplicative terms or a flattened primary node.
36/// - `Add`: left + right.
37/// - `Subtract`: left - right.
38/// - `Primary`: wrapped lower-precedence expression.
39#[derive(Debug, Clone, PartialEq)]
40pub enum Additive {
41    Add(Box<Additive>, Multiplicative),
42    Subtract(Box<Additive>, Multiplicative),
43    /// Primary flattened AST optimization
44    Primary(Box<Primary>),
45}
46
47impl Additive {
48    pub fn print(&self, writer: &mut Writer) {
49        match self {
50            Additive::Add(left, right) => {
51                writer.write_binary_op(left.as_ref(), " + ", right);
52            }
53            Additive::Subtract(left, right) => {
54                writer.write_binary_op(left.as_ref(), " - ", right);
55            }
56            Additive::Primary(primary) => primary.print(writer),
57        }
58    }
59}
60
61/// Parse an additive expression.
62///
63/// Consumes a leading multiplicative term followed by zero or more `+` / `-`
64/// multiplicative terms, folded left-associatively into an [`Additive`] AST.
65///
66/// Returns the remaining input and the parsed node.
67pub fn parse_additive(input: &str) -> IResult<&str, Additive> {
68    let (input, first) = parse_multiplicative(input)?;
69    let (input, rest) = many0((
70        multispace0,
71        alt((char('+'), char('-'))),
72        multispace0,
73        parse_multiplicative,
74    ))
75    .parse(input)?;
76
77    // Build the result by folding from left to right
78    let result = rest.into_iter().fold(
79        match first {
80            Multiplicative::Primary(primary) => Additive::Primary(primary),
81            _ => Additive::Primary(Box::new(Primary::Multiplicative(first))),
82        },
83        |acc, (_, op, _, next)| match op {
84            '+' => Additive::Add(Box::new(acc), next),
85            '-' => Additive::Subtract(Box::new(acc), next),
86            _ => unreachable!(),
87        },
88    );
89
90    Ok((input, result))
91}
92
93impl ExpressionLike for Additive {
94    fn evaluate(&self, context: &mut dyn ContextLike) -> Result<Arc<Value>> {
95        match self {
96            Additive::Add(left, right) => {
97                let (left_val, right_val) = evaluate_and_promote(context, left.as_ref(), right)?;
98                if left_val.is_error() {
99                    return Ok(left_val);
100                }
101                if right_val.is_error() {
102                    return Ok(right_val);
103                }
104                // For addition, we can add numbers or concatenate strings
105                match (&*left_val, &*right_val) {
106                    (Value::Literal(Literal::Bool(l)), Value::Literal(Literal::Bool(r))) => {
107                        Ok(Arc::new(Value::Literal(Literal::Bool(l ^ r))))
108                    }
109                    (Value::Literal(Literal::Number(l)), Value::Literal(Literal::Number(r))) => {
110                        Ok(Arc::new(Value::Literal(Literal::Number(l + r))))
111                    }
112                    (Value::Literal(Literal::Text(l)), Value::Literal(Literal::Text(r))) => {
113                        Ok(Arc::new(Value::Literal(Literal::Text(Arc::from(format!("{}{}", l, r))))))
114                    }
115                    _ => Err(anyhow!(
116                        "Expected Bool, Number, or Text, found {} + {}~{}",
117                        left_val.type_as_string(),
118                        right_val.type_as_string(),
119                        self.to_formula(),
120                    )),
121                }
122            }
123            Additive::Subtract(left, right) => {
124                let (left_val, right_val) = evaluate_and_promote(context, left.as_ref(), right)?;
125                if left_val.is_error() {
126                    return Ok(left_val);
127                }
128                if right_val.is_error() {
129                    return Ok(right_val);
130                }
131                // For subtraction, we need numeric operands
132                match (&*left_val, &*right_val) {
133                    (Value::Literal(Literal::Bool(l)), Value::Literal(Literal::Bool(r))) => {
134                        Ok(Arc::new(Value::Literal(Literal::Bool(!(l ^ r)))))
135                    }
136                    (Value::Literal(Literal::Number(l)), Value::Literal(Literal::Number(r))) => {
137                        Ok(Arc::new(Value::Literal(Literal::Number(l - r))))
138                    }
139                    (Value::Literal(Literal::Text(l)), Value::Literal(Literal::Text(r))) => {
140                        if r.is_empty() {
141                            return Ok(Arc::new(Value::Literal(Literal::Text(l.clone()))));
142                        }
143                        if let Some(pos) = l.rfind(r.as_ref()) {
144                            let mut result = String::with_capacity(l.len() - r.len());
145                            result.push_str(&l[..pos]);
146                            result.push_str(&l[pos + r.len()..]);
147                            Ok(Arc::new(Value::Literal(Literal::Text(Arc::from(result)))))
148                        } else {
149                            Ok(Arc::new(Value::Literal(Literal::Text(l.clone()))))
150                        }
151                    }
152                    _ => Err(anyhow!(
153                        "Expected Bool, Number, or Text, found {} - {}~{}",
154                        left_val.type_as_string(),
155                        right_val.type_as_string(),
156                        self.to_formula(),
157                    )),
158                }
159            }
160            Additive::Primary(primary) => primary.evaluate(context),
161        }
162    }
163
164    /// Return the formula-string representation (round-trippable by the parser).
165    fn to_formula(&self) -> String {
166        let mut writer = Writer::formulizer();
167        self.print(&mut writer);
168        writer.finish()
169    }
170}
171
172impl WriterLike for Additive {
173    fn write(&self, writer: &mut Writer) {
174        self.print(writer);
175    }
176}
177
178impl fmt::Display for Additive {
179    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180        write!(f, "{}", self.to_stringized())
181    }
182}