aimx/expressions/
postfix.rs

1//! Postfix expression parsing and evaluation.
2//!
3//! Supports AIMX postfix operators applied after primary expressions with
4//! left-to-right associativity: function calls `()`, indexing `[]`, method
5//! access `.`, inference calls on references, the empty placeholder `_`, and
6//! criticalization `!!`.
7//!
8//! Key entry points:
9//! - [`parse_postfix`] parses a postfix expression into [`Postfix`].
10//! - [`Postfix`] implements [`ExpressionLike`] for evaluation.
11use crate::{
12    aim::{ContextLike, WriterLike, Writer},
13    expressions::{
14        Expression, ExpressionLike, Primary, Reference, parse_argument_list, parse_expression,
15        parse_arc_identifier, parse_primary,
16    },
17    literals::Literal,
18    values::{CriticalError, Value},
19};
20use anyhow::{Result, anyhow};
21use nom::{
22    Err as NomErr, IResult, Parser,
23    branch::alt,
24    bytes::complete::tag,
25    character::complete::{char, multispace0},
26    combinator::{map, opt},
27    error::Error,
28    multi::many0,
29    sequence::delimited,
30};
31use std::{
32    fmt, sync::Arc
33};
34
35/// Postfix expression AST for AIMX.
36///
37/// Captures postfix operations applied after primary expressions, including
38/// function calls, indexing, method calls, inference calls, criticalization,
39/// and flattened primaries for shallow trees.
40///
41/// Used by [`parse_postfix`] and [`ExpressionLike`] evaluation.
42#[derive(Debug, Clone, PartialEq)]
43pub enum Postfix {
44    /// The empty placeholder `_`
45    Empty,
46    /// Function call with function name and argument expression
47    Function(Arc<str>, Box<Expression>),
48    /// Array indexing operation with base and index expressions
49    Index(Box<Postfix>, Box<Expression>),
50    /// Method call on an object with method name and arguments
51    Method(Box<Postfix>, Arc<str>, Box<Expression>),
52    /// Inference call on a reference and arguments
53    Inference(Arc<Reference>, Box<Expression>),
54    /// Criticalization on a postfix expression
55    Critical(Box<Postfix>),
56    /// Primary flattened AST optimization
57    Primary(Box<Primary>),
58}
59
60impl Postfix {
61    /// Returns `true` if this is [`Postfix::Empty`].
62    pub fn is_empty(&self) -> bool {
63        match self {
64            Postfix::Empty => true,
65            _ => false,
66        }
67    }
68
69    pub fn print(&self, writer: &mut Writer) {
70        match self {
71            // Check for Empty '_'
72            Postfix::Empty => {
73                writer.write_char('_');
74            }
75            Postfix::Function(identifier, expression) => {
76                writer.write_str(&identifier);
77                writer.write_char('(');
78                expression.print(writer);
79                writer.write_char(')');
80            }
81            Postfix::Index(postfix, expression) => {
82                postfix.print(writer);
83                writer.write_char('[');
84                expression.print(writer);
85                writer.write_char(']');
86            }
87            Postfix::Method(postfix, identifier, expression) => {
88                postfix.print(writer);
89                writer.write_char('.');
90                writer.write_str(&identifier);
91                writer.write_char('(');
92                expression.print(writer);
93                writer.write_char(')');
94            }
95            Postfix::Inference(reference, expression) => {
96                reference.print(writer);
97                writer.write_char('(');
98                expression.print(writer);
99                writer.write_char(')');
100            }
101            Postfix::Critical(postfix) => {
102                postfix.print(writer);
103                writer.write_str("!!");
104            }
105            Postfix::Primary(primary) => primary.print(writer),
106        }
107    }
108}
109
110/// Internal representation of postfix operations during parsing.
111enum PostfixOp {
112    /// Array indexing operation
113    Index(Expression),
114    /// Method call with method name and arguments
115    Method(Arc<str>, Expression),
116}
117
118/// Parse `.` accessor with surrounding whitespace.
119pub fn parse_accessor(input: &str) -> IResult<&str, &str> {
120    delimited(multispace0, tag("."), multispace0).parse(input)
121}
122
123/// Parse `[expr]` index suffix into [`PostfixOp::Index`].
124fn index_postfix(input: &str) -> IResult<&str, PostfixOp> {
125    let (input, expression) = delimited(char('['), parse_expression, char(']')).parse(input)?;
126    Ok((input, PostfixOp::Index(expression)))
127}
128
129/// Parse `(`argument-list`)` into an [`Expression`] or [`Expression::Empty`].
130fn parse_argument_postfix(input: &str) -> IResult<&str, Expression> {
131    delimited(
132        char('('),
133        alt((
134            map(parse_argument_list, |expr| expr),
135            map(multispace0, |_| Expression::Empty),
136        )),
137        char(')'),
138    )
139    .parse(input)
140}
141
142/// Parse identifier `(`args`)` or `_` into [`Postfix::Function`] or [`Postfix::Empty`].
143fn parse_function(input: &str) -> IResult<&str, Postfix> {
144    let (input, name) = parse_arc_identifier(input)?;
145    let (input, _) = multispace0(input)?;
146    if &*name == "_" {
147        return Ok((input, Postfix::Empty));
148    }
149    let (input, args) = parse_argument_postfix(input)?;
150    let function = Postfix::Function(name, Box::new(args));
151    Ok((input, function))
152}
153
154/// Parse a method call with dot notation.
155///
156/// This function parses method calls using dot notation, handling the
157/// accessor, method name, and argument list.
158///
159/// # Arguments
160///
161/// * `input` - A string slice containing the method call to parse
162///
163/// # Returns
164///
165/// * `IResult<&str, PostfixOp>` - A nom result with remaining input and parsed method operation
166fn parse_method(input: &str) -> IResult<&str, PostfixOp> {
167    map(
168        (
169            parse_accessor,
170            parse_arc_identifier,
171            multispace0,
172            parse_argument_postfix,
173        ),
174        |(_, name, _, args)| PostfixOp::Method(name, args),
175    ).parse(input)
176}
177
178fn parse_critical(input: &str, postfix: Postfix) -> IResult<&str, Postfix> {
179    let (input, _) = multispace0(input)?;
180    let (input, opt_critical) = opt(tag("!!")).parse(input)?;
181    let result = if opt_critical.is_some() {
182        Postfix::Critical(Box::new(postfix))
183    } else {
184        postfix
185    };
186    Ok((input, result))
187}
188
189/// Parse chained postfix forms into [`Postfix`].
190pub fn parse_postfix(input: &str) -> IResult<&str, Postfix> {
191    let (input, first) = if let Ok((input, function)) = parse_function(input) {
192        (input, function)
193    } else {
194        let (input, primary) = parse_primary(input)?;
195        if input.starts_with('(') {
196            let (input, args) = parse_argument_postfix(input)?;
197            match primary {
198                Primary::Reference(reference) => {
199                    let postfix = Postfix::Inference(reference, Box::new(args));
200                    return parse_critical(input, postfix);
201                }
202                _ => {
203                    return Err(NomErr::Failure(Error::new(
204                        input,
205                        nom::error::ErrorKind::Fail,
206                    )));
207                }
208            }
209        }
210        (input, Postfix::Primary(Box::new(primary)))
211    };
212
213    let (input, rest) = many0(delimited(
214        multispace0,
215        alt((index_postfix, parse_method)),
216        multispace0,
217    ))
218    .parse(input)?;
219
220    // Build the result by folding from left to right
221    let result = rest.into_iter().fold(first, |acc, op| match op {
222        PostfixOp::Index(expression) => Postfix::Index(Box::new(acc), Box::new(expression)),
223        PostfixOp::Method(name, args) => Postfix::Method(Box::new(acc), name, Box::new(args)),
224    });
225    return parse_critical(input, result);
226}
227
228impl ExpressionLike for Postfix {
229    /// Evaluate postfix expression in `context`.
230    ///
231    /// Function/method/inference calls are delegated via [`ContextLike`].
232    /// Indexing supports arrays and collections; `Critical` turns [`Value::Errata`]
233    /// into a [`CriticalError`].
234    fn evaluate(&self, context: &mut dyn ContextLike) -> Result<Arc<Value>> {
235        match self {
236            Postfix::Empty => Ok(Value::empty()),
237            Postfix::Function(name, expression) => {
238                // This is a function call like sum(1, 2, 3)
239                let arg = expression.evaluate(context)?;
240                if arg.is_error() {
241                    return Ok(arg);
242                }
243                context.function_call(name.clone(), arg)
244            }
245            Postfix::Index(primary, expression) => {
246                let primary_val = primary.evaluate(context)?;
247                if primary_val.is_error() {
248                    return Ok(primary_val);
249                }
250                let index_val = expression.evaluate(context)?;
251                if index_val.is_error() {
252                    return Ok(index_val);
253                }
254                let found = index_val.type_as_string();
255                // Handle array or collection indexing
256                match (&*primary_val, &*index_val) {
257                    // Array indexing by numeric index
258                    (Value::Array(array), Value::Literal(Literal::Number(index))) => {
259                        let index = *index as usize;
260                        if index < array.len() {
261                            Ok(array[index].clone())
262                        } else {
263                            Err(anyhow!(
264                                "Index out of bounds: {} >= {}~{}",
265                                index,
266                                array.len(),
267                                self.to_formula()
268                            ))
269                        }
270                    }
271                    (Value::Array(_), _) => Err(anyhow!(
272                        "Array index must be a number ~{}",
273                        self.to_formula()
274                    )),
275
276                    // Collection lookup by string key with lazy evaluation
277                    (Value::Collection(pool), Value::Literal(Literal::Text(text))) => {
278                        if let Some(expr) = pool.get(text.as_ref()) {
279                            expr.evaluate(context)
280                        } else {
281                            Ok(Value::empty())
282                        }
283                    }
284
285                    // Allow coercion of index to text when indexing a collection
286                    (Value::Collection(pool), _) => {
287                        let as_text = index_val.as_text()?;
288                        match &*as_text {
289                            Value::Literal(Literal::Text(text)) => {
290                                if let Some(expr) = pool.get(text) {
291                                    expr.evaluate(context)
292                                } else {
293                                    Ok(Value::empty())
294                                }
295                            }
296                            _ => Ok(Value::empty()),
297                        }
298                    }
299
300                    _ => Err(anyhow!(
301                        "Expected Array or Collection for index, found {}~{}",
302                        found,
303                        self.to_formula()
304                    )),
305                }
306            }
307            Postfix::Method(primary, name, expression) => {
308                // This is a method call like array.sum()
309                let value = primary.evaluate(context)?;
310                if value.is_error() {
311                    return Ok(value);
312                }
313                let arg = expression.evaluate(context)?;
314                if arg.is_error() {
315                    return Ok(arg);
316                }
317                context.method_call(name.clone(), value, arg)
318            }
319            Postfix::Inference(reference, expression) => {
320                // This is an inference call like std.extract(document, prompt)
321                let arg = expression.evaluate(context)?;
322                context.inference_call(reference.clone(), arg)
323            }
324            Postfix::Critical(postfix) => match postfix.evaluate(context) {
325                Ok(value) => {
326                    if let Value::Errata(errata) = &*value {
327                        Err(CriticalError::new(errata.reason()).into())
328                    } else {
329                        Ok(value)
330                    }
331                }
332                Err(e) => Err(CriticalError::new(Arc::from(e.to_string())).into()),
333            },
334            Postfix::Primary(primary) => primary.evaluate(context),
335        }
336    }
337
338    /// Return the formula-string representation (round-trippable by the parser).
339    fn to_formula(&self) -> String {
340        let mut writer = Writer::formulizer();
341        self.print(&mut writer);
342        writer.finish()
343    }
344}
345
346impl WriterLike for Postfix {
347    fn write(&self, writer: &mut Writer) {
348        self.print(writer);
349    }
350}
351
352impl fmt::Display for Postfix {
353    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
354        write!(f, "{}", self.to_stringized())
355    }
356}