Skip to main content

bicmath_core/
expr.rs

1//! The restricted expression grammar and parser.
2//!
3//! The parser produces an AST. Function resolution and evaluation live in the
4//! engine crate and use exactly the same registry and validation as the typed
5//! `calculate` path. Nothing here evaluates Rust, JavaScript, shell commands,
6//! imports, or remote code.
7//!
8//! Grammar (whitespace-insensitive; `//` line comments and `/* */` block
9//! comments are supported):
10//!
11//! ```text
12//! expr        := or_expr
13//! or_expr     := and_expr (('or' | '||') and_expr)*
14//! and_expr    := not_expr (('and' | '&&') not_expr)*
15//! not_expr    := ('not' | '!') not_expr | comparison
16//! comparison  := additive (comp_op additive)+          // chained, e.g. 0 < x < 1
17//! additive    := multiplicative (('+' | '-') multiplicative)*
18//! multiplicative := unary (('*' | '/' | '%') unary)*
19//! unary       := ('-' | '+') unary | power
20//! power       := primary ('^' unary)?                  // right associative
21//! primary     := NUMBER | STRING | 'true' | 'false' | IDENT
22//!              | IDENT ('.' IDENT)+ '(' arguments? ')'
23//!              | IDENT '(' arguments? ')'             // unqualified calls rejected
24//!              | '(' expr ')' | '[' array ']' | '{' record '}'
25//! arguments   := argument (',' argument)*
26//! argument    := (IDENT '=')? expr
27//! array       := expr (',' expr)* ','?
28//! record      := (IDENT | STRING) ':' expr (',' ...)* ','?
29//! ```
30//!
31//! Precedence (loosest to tightest): `or`, `and`, `not`, comparison,
32//! `+ -`, `* / %`, unary sign, `^`. Exponentiation binds tighter than unary
33//! minus, so `-2^2 == -4`, and is right associative, so `2^3^2 == 2^9`.
34
35use std::collections::BTreeMap;
36
37use serde_json::json;
38
39use crate::error::{EngineError, ErrorCode};
40use crate::limits::Limits;
41use crate::number::Number;
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub enum UnaryOp {
45    Neg,
46    Pos,
47    Not,
48}
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub enum BinaryOp {
52    Add,
53    Sub,
54    Mul,
55    Div,
56    Rem,
57    Pow,
58    And,
59    Or,
60}
61
62#[derive(Clone, Copy, Debug, PartialEq, Eq)]
63pub enum CompareOp {
64    Eq,
65    Ne,
66    Lt,
67    Le,
68    Gt,
69    Ge,
70}
71
72#[derive(Clone, Debug, PartialEq)]
73pub struct CallArg {
74    pub name: Option<String>,
75    pub value: Expr,
76}
77
78#[derive(Clone, Debug, PartialEq)]
79pub enum Expr {
80    Number(Number),
81    Text(String),
82    Bool(bool),
83    Ident(String),
84    Array(Vec<Expr>),
85    Record(Vec<(String, Expr)>),
86    Unary {
87        op: UnaryOp,
88        expr: Box<Expr>,
89    },
90    Binary {
91        op: BinaryOp,
92        left: Box<Expr>,
93        right: Box<Expr>,
94    },
95    Compare {
96        operands: Vec<Expr>,
97        ops: Vec<CompareOp>,
98    },
99    Call {
100        name: String,
101        args: Vec<CallArg>,
102    },
103}
104
105impl Expr {
106    /// Canonical JSON representation used for fingerprints and receipts.
107    pub fn canonical_json(&self) -> serde_json::Value {
108        match self {
109            Expr::Number(number) => json!({
110                "type": "number",
111                "value": serde_json::to_value(number).unwrap_or(serde_json::Value::Null),
112            }),
113            Expr::Text(text) => json!({"type": "text", "value": text}),
114            Expr::Bool(value) => json!({"type": "bool", "value": value}),
115            Expr::Ident(name) => json!({"type": "binding", "name": name}),
116            Expr::Array(items) => json!({
117                "type": "array",
118                "items": items.iter().map(Expr::canonical_json).collect::<Vec<_>>(),
119            }),
120            Expr::Record(fields) => json!({
121                "type": "record",
122                "fields": fields
123                    .iter()
124                    .map(|(k, v)| json!({"name": k, "value": v.canonical_json()}))
125                    .collect::<Vec<_>>(),
126            }),
127            Expr::Unary { op, expr } => json!({
128                "type": "unary",
129                "op": format!("{op:?}").to_lowercase(),
130                "expr": expr.canonical_json(),
131            }),
132            Expr::Binary { op, left, right } => json!({
133                "type": "binary",
134                "op": format!("{op:?}").to_lowercase(),
135                "left": left.canonical_json(),
136                "right": right.canonical_json(),
137            }),
138            Expr::Compare { operands, ops } => json!({
139                "type": "compare",
140                "ops": ops.iter().map(|op| format!("{op:?}").to_lowercase()).collect::<Vec<_>>(),
141                "operands": operands.iter().map(Expr::canonical_json).collect::<Vec<_>>(),
142            }),
143            Expr::Call { name, args } => json!({
144                "type": "call",
145                "name": name,
146                "args": args
147                    .iter()
148                    .map(|arg| json!({
149                        "name": arg.name,
150                        "value": arg.value.canonical_json(),
151                    }))
152                    .collect::<Vec<_>>(),
153            }),
154        }
155    }
156
157    /// Collect all binding identifiers referenced by the expression.
158    pub fn collect_bindings(&self, out: &mut BTreeMap<String, ()>) {
159        match self {
160            Expr::Ident(name) => {
161                out.insert(name.clone(), ());
162            }
163            Expr::Array(items) => {
164                for item in items {
165                    item.collect_bindings(out);
166                }
167            }
168            Expr::Record(fields) => {
169                for (_, value) in fields {
170                    value.collect_bindings(out);
171                }
172            }
173            Expr::Unary { expr, .. } => expr.collect_bindings(out),
174            Expr::Binary { left, right, .. } => {
175                left.collect_bindings(out);
176                right.collect_bindings(out);
177            }
178            Expr::Compare { operands, .. } => {
179                for operand in operands {
180                    operand.collect_bindings(out);
181                }
182            }
183            Expr::Call { args, .. } => {
184                for arg in args {
185                    arg.value.collect_bindings(out);
186                }
187            }
188            Expr::Number(_) | Expr::Text(_) | Expr::Bool(_) => {}
189        }
190    }
191
192    pub fn calls(&self, out: &mut Vec<String>) {
193        match self {
194            Expr::Call { name, args } => {
195                out.push(name.clone());
196                for arg in args {
197                    arg.value.calls(out);
198                }
199            }
200            Expr::Array(items) => {
201                for item in items {
202                    item.calls(out);
203                }
204            }
205            Expr::Record(fields) => {
206                for (_, value) in fields {
207                    value.calls(out);
208                }
209            }
210            Expr::Unary { expr, .. } => expr.calls(out),
211            Expr::Binary { left, right, .. } => {
212                left.calls(out);
213                right.calls(out);
214            }
215            Expr::Compare { operands, .. } => {
216                for operand in operands {
217                    operand.calls(out);
218                }
219            }
220            Expr::Number(_) | Expr::Text(_) | Expr::Bool(_) | Expr::Ident(_) => {}
221        }
222    }
223}
224
225#[derive(Clone, Debug, PartialEq)]
226enum Token {
227    Number(String),
228    Text(String),
229    Ident(String),
230    True,
231    False,
232    And,
233    Or,
234    Not,
235    LParen,
236    RParen,
237    LBracket,
238    RBracket,
239    LBrace,
240    RBrace,
241    Comma,
242    Colon,
243    Assign,
244    Dot,
245    Plus,
246    Minus,
247    Star,
248    Slash,
249    Percent,
250    Caret,
251    Eq,
252    Ne,
253    Lt,
254    Le,
255    Gt,
256    Ge,
257    Bang,
258}
259
260struct Lexer<'a> {
261    source: &'a str,
262    bytes: &'a [u8],
263    pos: usize,
264    tokens: Vec<Token>,
265    limits: &'a Limits,
266}
267
268impl<'a> Lexer<'a> {
269    fn new(source: &'a str, limits: &'a Limits) -> Result<Lexer<'a>, EngineError> {
270        if source.len() > limits.max_expression_length {
271            return Err(EngineError::new(
272                ErrorCode::ResourceLimit,
273                format!(
274                    "expression length {} exceeds the limit of {}",
275                    source.len(),
276                    limits.max_expression_length
277                ),
278            ));
279        }
280        Ok(Lexer {
281            source,
282            bytes: source.as_bytes(),
283            pos: 0,
284            tokens: Vec::new(),
285            limits,
286        })
287    }
288
289    fn peek(&self) -> Option<u8> {
290        self.bytes.get(self.pos).copied()
291    }
292
293    fn peek2(&self) -> Option<u8> {
294        self.bytes.get(self.pos + 1).copied()
295    }
296
297    fn skip_trivia(&mut self) -> Result<(), EngineError> {
298        loop {
299            match self.peek() {
300                Some(b' ' | b'\t' | b'\n' | b'\r') => self.pos += 1,
301                Some(b'/') if self.peek2() == Some(b'/') => {
302                    while let Some(c) = self.peek() {
303                        if c == b'\n' {
304                            break;
305                        }
306                        self.pos += 1;
307                    }
308                }
309                Some(b'/') if self.peek2() == Some(b'*') => {
310                    self.pos += 2;
311                    loop {
312                        match (self.peek(), self.peek2()) {
313                            (Some(b'*'), Some(b'/')) => {
314                                self.pos += 2;
315                                break;
316                            }
317                            (Some(_), _) => self.pos += 1,
318                            (None, _) => {
319                                return Err(EngineError::malformed(
320                                    "unterminated block comment in expression",
321                                ));
322                            }
323                        }
324                    }
325                }
326                _ => return Ok(()),
327            }
328        }
329    }
330
331    fn push(&mut self, token: Token) -> Result<(), EngineError> {
332        self.tokens.push(token);
333        if self.tokens.len() > self.limits.max_expression_tokens {
334            return Err(EngineError::new(
335                ErrorCode::ResourceLimit,
336                format!(
337                    "expression token count exceeds the limit of {}",
338                    self.limits.max_expression_tokens
339                ),
340            ));
341        }
342        Ok(())
343    }
344
345    fn lex_number(&mut self) -> Result<(), EngineError> {
346        let start = self.pos;
347        let mut seen_digit = false;
348        while let Some(c) = self.peek() {
349            match c {
350                b'0'..=b'9' => {
351                    seen_digit = true;
352                    self.pos += 1;
353                }
354                b'.' => {
355                    self.pos += 1;
356                }
357                b'e' | b'E' => {
358                    self.pos += 1;
359                    if matches!(self.peek(), Some(b'+' | b'-')) {
360                        self.pos += 1;
361                    }
362                }
363                _ => break,
364            }
365        }
366        if !seen_digit {
367            return Err(EngineError::malformed(format!(
368                "invalid number literal at byte {start}"
369            )));
370        }
371        let text = &self.source[start..self.pos];
372        self.push(Token::Number(text.to_string()))
373    }
374
375    fn lex_ident(&mut self) -> Result<(), EngineError> {
376        let start = self.pos;
377        while let Some(c) = self.peek() {
378            if c.is_ascii_alphanumeric() || c == b'_' {
379                self.pos += 1;
380            } else {
381                break;
382            }
383        }
384        let text = &self.source[start..self.pos];
385        let token = match text {
386            "true" => Token::True,
387            "false" => Token::False,
388            "and" => Token::And,
389            "or" => Token::Or,
390            "not" => Token::Not,
391            _ => Token::Ident(text.to_string()),
392        };
393        self.push(token)
394    }
395
396    fn lex_string(&mut self) -> Result<(), EngineError> {
397        self.pos += 1; // opening quote
398        let mut out = String::new();
399        loop {
400            match self.peek() {
401                None => {
402                    return Err(EngineError::malformed(
403                        "unterminated string literal in expression",
404                    ));
405                }
406                Some(b'"') => {
407                    self.pos += 1;
408                    break;
409                }
410                Some(b'\\') => {
411                    self.pos += 1;
412                    let escaped = self.peek().ok_or_else(|| {
413                        EngineError::malformed("unterminated escape in string literal")
414                    })?;
415                    self.pos += 1;
416                    match escaped {
417                        b'"' => out.push('"'),
418                        b'\\' => out.push('\\'),
419                        b'/' => out.push('/'),
420                        b'n' => out.push('\n'),
421                        b't' => out.push('\t'),
422                        b'r' => out.push('\r'),
423                        b'b' => out.push('\u{0008}'),
424                        b'f' => out.push('\u{000C}'),
425                        b'u' => {
426                            let hex_start = self.pos;
427                            if hex_start + 4 > self.bytes.len() {
428                                return Err(EngineError::malformed(
429                                    "invalid unicode escape in string literal",
430                                ));
431                            }
432                            let hex = &self.source[hex_start..hex_start + 4];
433                            self.pos += 4;
434                            let code = u32::from_str_radix(hex, 16).map_err(|_| {
435                                EngineError::malformed("invalid unicode escape in string literal")
436                            })?;
437                            let ch = char::from_u32(code).ok_or_else(|| {
438                                EngineError::malformed("invalid unicode scalar in string literal")
439                            })?;
440                            out.push(ch);
441                        }
442                        _ => {
443                            return Err(EngineError::malformed(format!(
444                                "invalid escape sequence \\{} in string literal",
445                                escaped as char
446                            )));
447                        }
448                    }
449                }
450                Some(_) => {
451                    let ch = self.source[self.pos..].chars().next().unwrap();
452                    self.pos += ch.len_utf8();
453                    out.push(ch);
454                }
455            }
456            if out.len() > self.limits.max_string_len {
457                return Err(EngineError::new(
458                    ErrorCode::ResourceLimit,
459                    "string literal exceeds the string length limit",
460                ));
461            }
462        }
463        self.push(Token::Text(out))
464    }
465
466    fn run(mut self) -> Result<Vec<Token>, EngineError> {
467        loop {
468            self.skip_trivia()?;
469            let Some(c) = self.peek() else { break };
470            match c {
471                b'0'..=b'9' | b'.' => {
472                    if c == b'.' && !matches!(self.peek2(), Some(b'0'..=b'9')) {
473                        self.pos += 1;
474                        self.push(Token::Dot)?;
475                    } else {
476                        self.lex_number()?;
477                    }
478                }
479                b'a'..=b'z' | b'A'..=b'Z' | b'_' => self.lex_ident()?,
480                b'"' => self.lex_string()?,
481                b'(' => {
482                    self.pos += 1;
483                    self.push(Token::LParen)?;
484                }
485                b')' => {
486                    self.pos += 1;
487                    self.push(Token::RParen)?;
488                }
489                b'[' => {
490                    self.pos += 1;
491                    self.push(Token::LBracket)?;
492                }
493                b']' => {
494                    self.pos += 1;
495                    self.push(Token::RBracket)?;
496                }
497                b'{' => {
498                    self.pos += 1;
499                    self.push(Token::LBrace)?;
500                }
501                b'}' => {
502                    self.pos += 1;
503                    self.push(Token::RBrace)?;
504                }
505                b',' => {
506                    self.pos += 1;
507                    self.push(Token::Comma)?;
508                }
509                b':' => {
510                    self.pos += 1;
511                    self.push(Token::Colon)?;
512                }
513                b'+' => {
514                    self.pos += 1;
515                    self.push(Token::Plus)?;
516                }
517                b'-' => {
518                    self.pos += 1;
519                    self.push(Token::Minus)?;
520                }
521                b'*' => {
522                    self.pos += 1;
523                    self.push(Token::Star)?;
524                }
525                b'/' => {
526                    self.pos += 1;
527                    self.push(Token::Slash)?;
528                }
529                b'%' => {
530                    self.pos += 1;
531                    self.push(Token::Percent)?;
532                }
533                b'^' => {
534                    self.pos += 1;
535                    self.push(Token::Caret)?;
536                }
537                b'=' => {
538                    if self.peek2() == Some(b'=') {
539                        self.pos += 2;
540                        self.push(Token::Eq)?;
541                    } else {
542                        self.pos += 1;
543                        self.push(Token::Assign)?;
544                    }
545                }
546                b'!' => {
547                    if self.peek2() == Some(b'=') {
548                        self.pos += 2;
549                        self.push(Token::Ne)?;
550                    } else {
551                        self.pos += 1;
552                        self.push(Token::Bang)?;
553                    }
554                }
555                b'<' => {
556                    if self.peek2() == Some(b'=') {
557                        self.pos += 2;
558                        self.push(Token::Le)?;
559                    } else {
560                        self.pos += 1;
561                        self.push(Token::Lt)?;
562                    }
563                }
564                b'>' => {
565                    if self.peek2() == Some(b'=') {
566                        self.pos += 2;
567                        self.push(Token::Ge)?;
568                    } else {
569                        self.pos += 1;
570                        self.push(Token::Gt)?;
571                    }
572                }
573                b'&' if self.peek2() == Some(b'&') => {
574                    self.pos += 2;
575                    self.push(Token::And)?;
576                }
577                b'|' if self.peek2() == Some(b'|') => {
578                    self.pos += 2;
579                    self.push(Token::Or)?;
580                }
581                other => {
582                    return Err(EngineError::malformed(format!(
583                        "unexpected character {:?} in expression at byte {}",
584                        other as char, self.pos
585                    )));
586                }
587            }
588        }
589        Ok(self.tokens)
590    }
591}
592
593struct Parser<'a> {
594    tokens: &'a [Token],
595    pos: usize,
596    limits: &'a Limits,
597    depth: usize,
598}
599
600impl<'a> Parser<'a> {
601    fn new(tokens: &'a [Token], limits: &'a Limits) -> Parser<'a> {
602        Parser {
603            tokens,
604            pos: 0,
605            limits,
606            depth: 0,
607        }
608    }
609
610    fn enter(&mut self) -> Result<(), EngineError> {
611        self.depth += 1;
612        if self.depth > self.limits.max_ast_depth {
613            return Err(EngineError::new(
614                ErrorCode::ResourceLimit,
615                format!(
616                    "expression nesting depth exceeds the limit of {}",
617                    self.limits.max_ast_depth
618                ),
619            ));
620        }
621        Ok(())
622    }
623
624    fn exit(&mut self) {
625        self.depth -= 1;
626    }
627
628    fn peek(&self) -> Option<&Token> {
629        self.tokens.get(self.pos)
630    }
631
632    fn next(&mut self) -> Option<Token> {
633        let token = self.tokens.get(self.pos).cloned();
634        if token.is_some() {
635            self.pos += 1;
636        }
637        token
638    }
639
640    fn expect(&mut self, token: &Token, what: &str) -> Result<(), EngineError> {
641        match self.next() {
642            Some(found) if &found == token => Ok(()),
643            Some(found) => Err(EngineError::malformed(format!(
644                "expected {what}, found {found:?}"
645            ))),
646            None => Err(EngineError::malformed(format!(
647                "expected {what}, found end of expression"
648            ))),
649        }
650    }
651
652    fn parse(&mut self) -> Result<Expr, EngineError> {
653        let expr = self.parse_or()?;
654        if self.pos != self.tokens.len() {
655            return Err(EngineError::malformed(format!(
656                "unexpected token {:?} after expression",
657                self.tokens[self.pos]
658            )));
659        }
660        Ok(expr)
661    }
662
663    fn parse_or(&mut self) -> Result<Expr, EngineError> {
664        let mut left = self.parse_and()?;
665        while matches!(self.peek(), Some(Token::Or)) {
666            self.next();
667            let right = self.parse_and()?;
668            left = Expr::Binary {
669                op: BinaryOp::Or,
670                left: Box::new(left),
671                right: Box::new(right),
672            };
673        }
674        Ok(left)
675    }
676
677    fn parse_and(&mut self) -> Result<Expr, EngineError> {
678        let mut left = self.parse_not()?;
679        while matches!(self.peek(), Some(Token::And)) {
680            self.next();
681            let right = self.parse_not()?;
682            left = Expr::Binary {
683                op: BinaryOp::And,
684                left: Box::new(left),
685                right: Box::new(right),
686            };
687        }
688        Ok(left)
689    }
690
691    fn parse_not(&mut self) -> Result<Expr, EngineError> {
692        if matches!(self.peek(), Some(Token::Not | Token::Bang)) {
693            self.next();
694            self.enter()?;
695            let expr = self.parse_not();
696            self.exit();
697            return Ok(Expr::Unary {
698                op: UnaryOp::Not,
699                expr: Box::new(expr?),
700            });
701        }
702        self.parse_comparison()
703    }
704
705    fn parse_comparison(&mut self) -> Result<Expr, EngineError> {
706        let first = self.parse_additive()?;
707        let mut operands = vec![first];
708        let mut ops = Vec::new();
709        loop {
710            let op = match self.peek() {
711                Some(Token::Eq) => CompareOp::Eq,
712                Some(Token::Ne) => CompareOp::Ne,
713                Some(Token::Lt) => CompareOp::Lt,
714                Some(Token::Le) => CompareOp::Le,
715                Some(Token::Gt) => CompareOp::Gt,
716                Some(Token::Ge) => CompareOp::Ge,
717                _ => break,
718            };
719            self.next();
720            let right = self.parse_additive()?;
721            operands.push(right);
722            ops.push(op);
723        }
724        if ops.is_empty() {
725            Ok(operands.pop().unwrap())
726        } else {
727            Ok(Expr::Compare { operands, ops })
728        }
729    }
730
731    fn parse_additive(&mut self) -> Result<Expr, EngineError> {
732        let mut left = self.parse_multiplicative()?;
733        loop {
734            let op = match self.peek() {
735                Some(Token::Plus) => BinaryOp::Add,
736                Some(Token::Minus) => BinaryOp::Sub,
737                _ => break,
738            };
739            self.next();
740            let right = self.parse_multiplicative()?;
741            left = Expr::Binary {
742                op,
743                left: Box::new(left),
744                right: Box::new(right),
745            };
746        }
747        Ok(left)
748    }
749
750    fn parse_multiplicative(&mut self) -> Result<Expr, EngineError> {
751        let mut left = self.parse_unary()?;
752        loop {
753            let op = match self.peek() {
754                Some(Token::Star) => BinaryOp::Mul,
755                Some(Token::Slash) => BinaryOp::Div,
756                Some(Token::Percent) => BinaryOp::Rem,
757                _ => break,
758            };
759            self.next();
760            let right = self.parse_unary()?;
761            left = Expr::Binary {
762                op,
763                left: Box::new(left),
764                right: Box::new(right),
765            };
766        }
767        Ok(left)
768    }
769
770    fn parse_unary(&mut self) -> Result<Expr, EngineError> {
771        match self.peek() {
772            Some(Token::Minus) => {
773                self.next();
774                self.enter()?;
775                let expr = self.parse_unary();
776                self.exit();
777                Ok(Expr::Unary {
778                    op: UnaryOp::Neg,
779                    expr: Box::new(expr?),
780                })
781            }
782            Some(Token::Plus) => {
783                self.next();
784                self.enter()?;
785                let expr = self.parse_unary();
786                self.exit();
787                Ok(Expr::Unary {
788                    op: UnaryOp::Pos,
789                    expr: Box::new(expr?),
790                })
791            }
792            _ => self.parse_power(),
793        }
794    }
795
796    fn parse_power(&mut self) -> Result<Expr, EngineError> {
797        let base = self.parse_primary()?;
798        if matches!(self.peek(), Some(Token::Caret)) {
799            self.next();
800            self.enter()?;
801            let exponent = self.parse_unary();
802            self.exit();
803            Ok(Expr::Binary {
804                op: BinaryOp::Pow,
805                left: Box::new(base),
806                right: Box::new(exponent?),
807            })
808        } else {
809            Ok(base)
810        }
811    }
812
813    fn parse_primary(&mut self) -> Result<Expr, EngineError> {
814        let token = self
815            .next()
816            .ok_or_else(|| EngineError::malformed("unexpected end of expression"))?;
817        match token {
818            Token::Number(text) => {
819                let number = Number::parse_literal(&text, self.limits)?;
820                Ok(Expr::Number(number))
821            }
822            Token::Text(text) => Ok(Expr::Text(text)),
823            Token::True => Ok(Expr::Bool(true)),
824            Token::False => Ok(Expr::Bool(false)),
825            Token::Ident(name) => {
826                // Qualified call: ident ('.' ident)+ '(' ...
827                let mut qualified = name;
828                let mut saw_dot = false;
829                while matches!(self.peek(), Some(Token::Dot)) {
830                    self.next();
831                    match self.next() {
832                        Some(Token::Ident(part)) => {
833                            qualified.push('.');
834                            qualified.push_str(&part);
835                            saw_dot = true;
836                        }
837                        other => {
838                            return Err(EngineError::malformed(format!(
839                                "expected identifier after '.', found {other:?}"
840                            )));
841                        }
842                    }
843                }
844                if matches!(self.peek(), Some(Token::LParen)) {
845                    self.next();
846                    let args = self.parse_call_args()?;
847                    Ok(Expr::Call {
848                        name: qualified,
849                        args,
850                    })
851                } else if saw_dot {
852                    Err(EngineError::malformed(format!(
853                        "qualified name {qualified:?} must be called as a function"
854                    )))
855                } else {
856                    Ok(Expr::Ident(qualified))
857                }
858            }
859            Token::LParen => {
860                self.enter()?;
861                let expr = self.parse_or();
862                self.exit();
863                let expr = expr?;
864                self.expect(&Token::RParen, "')'")?;
865                Ok(expr)
866            }
867            Token::LBracket => {
868                self.enter()?;
869                let mut items = Vec::new();
870                if !matches!(self.peek(), Some(Token::RBracket)) {
871                    loop {
872                        items.push(self.parse_or()?);
873                        if matches!(self.peek(), Some(Token::Comma)) {
874                            self.next();
875                            if matches!(self.peek(), Some(Token::RBracket)) {
876                                break;
877                            }
878                        } else {
879                            break;
880                        }
881                    }
882                }
883                self.exit();
884                self.expect(&Token::RBracket, "']'")?;
885                if items.len() > self.limits.max_array_len {
886                    return Err(EngineError::new(
887                        ErrorCode::ResourceLimit,
888                        format!(
889                            "array literal length {} exceeds the limit of {}",
890                            items.len(),
891                            self.limits.max_array_len
892                        ),
893                    ));
894                }
895                Ok(Expr::Array(items))
896            }
897            Token::LBrace => {
898                self.enter()?;
899                let mut fields = Vec::new();
900                if !matches!(self.peek(), Some(Token::RBrace)) {
901                    loop {
902                        let key = match self.next() {
903                            Some(Token::Ident(name)) => name,
904                            Some(Token::Text(text)) => text,
905                            other => {
906                                return Err(EngineError::malformed(format!(
907                                    "expected a record key, found {other:?}"
908                                )));
909                            }
910                        };
911                        self.expect(&Token::Colon, "':'")?;
912                        let value = self.parse_or()?;
913                        fields.push((key, value));
914                        if matches!(self.peek(), Some(Token::Comma)) {
915                            self.next();
916                            if matches!(self.peek(), Some(Token::RBrace)) {
917                                break;
918                            }
919                        } else {
920                            break;
921                        }
922                    }
923                }
924                self.exit();
925                self.expect(&Token::RBrace, "'}'")?;
926                Ok(Expr::Record(fields))
927            }
928            other => Err(EngineError::malformed(format!(
929                "unexpected token {other:?} in expression"
930            ))),
931        }
932    }
933
934    fn parse_call_args(&mut self) -> Result<Vec<CallArg>, EngineError> {
935        let mut args = Vec::new();
936        let mut seen_named = false;
937        if matches!(self.peek(), Some(Token::RParen)) {
938            self.next();
939            return Ok(args);
940        }
941        loop {
942            self.enter()?;
943            let parsed = (|| -> Result<CallArg, EngineError> {
944                // Look ahead for `name =`.
945                if let (Some(Token::Ident(name)), Some(Token::Assign)) =
946                    (self.tokens.get(self.pos), self.tokens.get(self.pos + 1))
947                {
948                    let name = name.clone();
949                    self.pos += 2;
950                    seen_named = true;
951                    let value = self.parse_or()?;
952                    Ok(CallArg {
953                        name: Some(name),
954                        value,
955                    })
956                } else {
957                    if seen_named {
958                        return Err(EngineError::malformed(
959                            "positional arguments must precede named arguments",
960                        ));
961                    }
962                    let value = self.parse_or()?;
963                    Ok(CallArg { name: None, value })
964                }
965            })();
966            self.exit();
967            args.push(parsed?);
968            if args.len() > 256 {
969                return Err(EngineError::new(
970                    ErrorCode::ResourceLimit,
971                    "too many call arguments",
972                ));
973            }
974            match self.next() {
975                Some(Token::Comma) => continue,
976                Some(Token::RParen) => break,
977                other => {
978                    return Err(EngineError::malformed(format!(
979                        "expected ',' or ')' in argument list, found {other:?}"
980                    )));
981                }
982            }
983        }
984        Ok(args)
985    }
986}
987
988/// Parse a restricted expression. This performs no function resolution.
989pub fn parse_expression(source: &str, limits: &Limits) -> Result<Expr, EngineError> {
990    let tokens = Lexer::new(source, limits)?.run()?;
991    if tokens.is_empty() {
992        return Err(EngineError::malformed("empty expression"));
993    }
994    Parser::new(&tokens, limits).parse()
995}
996
997#[cfg(test)]
998mod tests {
999    use super::*;
1000
1001    fn parse(source: &str) -> Expr {
1002        parse_expression(source, &Limits::conservative()).unwrap()
1003    }
1004
1005    #[test]
1006    fn precedence_of_unary_minus_and_power() {
1007        let expr = parse("-2^2");
1008        match expr {
1009            Expr::Unary {
1010                op: UnaryOp::Neg,
1011                expr,
1012            } => match *expr {
1013                Expr::Binary {
1014                    op: BinaryOp::Pow, ..
1015                } => {}
1016                other => panic!("expected power, got {other:?}"),
1017            },
1018            other => panic!("expected negation, got {other:?}"),
1019        }
1020    }
1021
1022    #[test]
1023    fn power_is_right_associative() {
1024        let expr = parse("2^3^2");
1025        match expr {
1026            Expr::Binary {
1027                op: BinaryOp::Pow,
1028                right,
1029                ..
1030            } => match *right {
1031                Expr::Binary {
1032                    op: BinaryOp::Pow, ..
1033                } => {}
1034                other => panic!("expected nested power, got {other:?}"),
1035            },
1036            other => panic!("expected power, got {other:?}"),
1037        }
1038    }
1039
1040    #[test]
1041    fn chained_comparison_parses() {
1042        let expr = parse("0 < x < 1");
1043        match expr {
1044            Expr::Compare { operands, ops } => {
1045                assert_eq!(operands.len(), 3);
1046                assert_eq!(ops, vec![CompareOp::Lt, CompareOp::Lt]);
1047            }
1048            other => panic!("expected comparison, got {other:?}"),
1049        }
1050    }
1051
1052    #[test]
1053    fn qualified_call_with_named_arguments() {
1054        let expr = parse("finance.npv(rate = 0.08, cashflows = [-10000, 4000])");
1055        match expr {
1056            Expr::Call { name, args } => {
1057                assert_eq!(name, "finance.npv");
1058                assert_eq!(args.len(), 2);
1059                assert_eq!(args[0].name.as_deref(), Some("rate"));
1060            }
1061            other => panic!("expected call, got {other:?}"),
1062        }
1063    }
1064
1065    #[test]
1066    fn unqualified_call_is_parsed_and_rejected_later() {
1067        let expr = parse("sin(1)");
1068        match expr {
1069            Expr::Call { name, .. } => assert_eq!(name, "sin"),
1070            other => panic!("expected call, got {other:?}"),
1071        }
1072    }
1073
1074    #[test]
1075    fn depth_limit_is_enforced() {
1076        let mut limits = Limits::conservative();
1077        limits.max_ast_depth = 8;
1078        let source = "((((((((((1))))))))))";
1079        assert!(parse_expression(source, &limits).is_err());
1080    }
1081
1082    #[test]
1083    fn record_and_array_literals() {
1084        let expr = parse("{a: 1, b: [2, 3,]}");
1085        match expr {
1086            Expr::Record(fields) => assert_eq!(fields.len(), 2),
1087            other => panic!("expected record, got {other:?}"),
1088        }
1089    }
1090
1091    #[test]
1092    fn canonical_json_is_stable() {
1093        let a = parse("1 + 2 * 3");
1094        let b = parse("1+2*3");
1095        assert_eq!(a.canonical_json(), b.canonical_json());
1096    }
1097}