Skip to main content

improv_core_model/
parser.rs

1//! Textual formula parser: `"Revenue = Price * Quantity"` -> [`Formula`].
2//!
3//! Hand-rolled tokenizer + recursive-descent parser (same shape as
4//! `improv_nl_formula`), for the *symbolic* v1 grammar in
5//! `AGENT_FORMULA_LANGUAGE.md` §4. Measure/category names resolve against a
6//! [`Model`] (like `NlContext`); unknown names are a clean [`ParseError`],
7//! never a panic.
8//!
9//! # Grammar (`AGENT_FORMULA_LANGUAGE.md` §4)
10//!
11//! ```ebnf
12//! Formula      = Identifier "=" Expression ;
13//! Expression   = OrExpr ;
14//! OrExpr       = AndExpr { "OR" AndExpr } ;
15//! AndExpr      = NotExpr { "AND" NotExpr } ;
16//! NotExpr      = "NOT" NotExpr | Comparison ;        (* NOT a > b == NOT (a > b) *)
17//! Comparison   = Concat [ CmpOp Concat ] ;          (* == <> != < <= > >= *)
18//! Concat       = Additive { "&" Additive } ;
19//! Additive     = Term { ("+" | "-") Term } ;
20//! Term         = Unary { ("*" | "/") Unary } ;
21//! Unary        = "-" Unary | Power ;
22//! Power        = Postfix [ "^" Unary ] ;            (* right-assoc; -2^2 = -4 *)
23//! Postfix      = Primary { "%" } ;                  (* x% = x / 100 *)
24//! Primary      = Literal | Call | Special | MeasureRef | Local
25//!              | "(" Expression ")" ;
26//! Literal      = Number | Text | "TRUE" | "FALSE" | DateLiteral ;  (* #2025-01-31# *)
27//! MeasureRef   = Name [ "[" Name { "," Name } "]" ] ;
28//! Local        = Identifier ;                       (* bound by LET / LAMBDA *)
29//!
30//! Call         = FuncName "(" [ Arg { Sep Arg } ] ")" ;  (* Sep: "," or ";" (de) *)
31//! Arg          = Expression [ OverClause ] [ IntoClause ] ;
32//! OverClause   = "OVER" ( Cats [ Window ] | "ALL" [ "EXCEPT" Cats ] )
33//!                [ "WHERE" Expression ] ;
34//! Window       = "[" Bound ".." Bound "]" ;          (* FIRST | LAST | THIS[+-n] *)
35//! IntoClause   = "INTO" Cats ;                      (* a list result, row-major *)
36//! Cats         = Name { "," Name } ;
37//!
38//! Special      = "LET" "(" { Identifier "," Expression "," } Expression ")"
39//!              | Lambda "(" Expression { "," Expression } ")"   (* called on the spot *)
40//!              | "MAP" "(" Expression { "," Expression } "," Lambda ")"
41//!              | ("SCAN" | "REDUCE") "(" Expression "," MeasureRef "OVER" Name "," Lambda ")"
42//!              | ("BYROW" | "BYCOL") "(" MeasureRef "OVER" Name "," Lambda ")"
43//!              | "FIXPOINT" "(" Expression "," Lambda [ "," Expression [ "," Expression ] ] ")"
44//!              | "MAKEARRAY" "(" Name "," Name "," Lambda ")"
45//!              | "SEQUENCE" "(" Name [ "," Expression [ "," Expression ] ] ")"
46//!              | NamedLambda "(" [ Expression { "," Expression } ] ")"
47//!                (* an argument for a category parameter is a category Name *) ;
48//! Lambda       = "LAMBDA" "(" Identifier { "," Identifier } "," Expression ")" ;
49//!
50//! Definition   = Formula
51//!              | Identifier "=" "SQL" "(" Text ")"          (* SQL-sourced input *)
52//!              | Identifier "=" "CALL" "(" Name { "," Name } ")"  (* external fn *)
53//!              | Identifier "=" Lambda ;                   (* a named LAMBDA *)
54//! ```
55//!
56//! ## Quoted identifiers (`'Unit Price'`)
57//!
58//! Anywhere a measure or category name may appear, it may instead be written
59//! inside **single** quotes — the Quantrix spelling. This is the only way to
60//! name a measure a CSV header produced (`import_csv` takes the header
61//! verbatim, so `Unit Price`, `Cost/Unit` and `2024 Total` are all real
62//! measure names), and the only way to name one that collides with a grammar
63//! keyword (`'Over'`, `'SUM'`, `'NOT'`).
64//!
65//! * Double quotes are untouched: `"..."` is still a *text literal*, `'...'` is
66//!   a *name*. They never overlap.
67//! * A literal `'` inside a quoted name is written **doubled**: `'Bob''s Rate'`
68//!   names the measure `Bob's Rate`. (Quantrix/SQL convention; chosen over
69//!   backslashes so the quoted form needs no escape character at all.)
70//! * A quoted name is never a keyword, a function, `TRUE`/`FALSE` or `OVER`: it
71//!   resolves against the model as a measure/category name, full stop.
72//! * `''` (empty) and an unterminated `'` are clean parse errors.
73//!
74//! ## Dotted qualification (`Matrix.Measure`) — parsed, not yet resolvable
75//!
76//! Quantrix qualifies a name by its owning matrix
77//! (`'Defined Input & Outputs'.'Income tax rate (Corporate)'`). Improv has no
78//! matrix concept yet: a measure belongs to the *model*, not to a matrix, so
79//! there is nothing a qualifier could name. The tokenizer and parser therefore
80//! accept the form and reject it with an error that names the unknown
81//! qualifier, instead of the old `unexpected character: '.'`. When multiple
82//! matrices per view land (GUI plan Step 3), `Parser::parse_name` is the one
83//! place resolution has to learn about.
84//!
85//! ## The `=` ambiguity (assignment vs. equality)
86//!
87//! `=` is *only* the top-level assignment separator: [`parse_formula`] splits
88//! `Identifier "=" ...` first, then parses the RHS as an [`Expr`]. Inside an
89//! expression, equality comparison is spelled `==` and not-equal is `<>`
90//! (or `!=`). This keeps `Revenue = Price * Quantity` unambiguous while still
91//! allowing `flag = Price == 10`.
92//!
93//! ## Aggregation `DimensionSpec` convention
94//!
95//! `SUM(Revenue OVER Time)` -> `Call(FuncId(1), [Ref(revenue, DimensionSpec {
96//! over: [Time], by: [], except: [] })])`. The compiler (`engine::compiler`)
97//! reads the arg ref's `over` to pick the collapsed category, so only `over`
98//! is set. Function ids come from `crate::functions` (SUM is 1).
99//!
100//! ## Not implemented (deferred, per §4/§11.3)
101//!
102//! * (Date literals `#2025-01-01#` and `#...T...Z#` ARE supported — see
103//!   `parse_date_literal`.)
104//! * Nothing else material: named scalar calls (`ABS`, `SQRT`, `MIN2`, …) and
105//!   the whole-RHS source forms `CALL(...)` / `SQL("...")` (via
106//!   `parse_definition`) are supported.
107
108use crate::formula::{BinaryOp, DimensionSpec, Expr, Formula, FuncId, UnaryOp};
109use crate::functions;
110use crate::ids::{CategoryId, MeasureId, Name};
111use crate::value::Value;
112use crate::Model;
113
114pub const FUNC_SUM: FuncId = FuncId(1);
115pub const FUNC_AVG: FuncId = FuncId(2);
116pub const FUNC_MIN: FuncId = FuncId(3);
117pub const FUNC_MAX: FuncId = FuncId(4);
118
119/// The built-in a name calls as `NAME(args...)` (aggregations excluded):
120/// its id and parameter count, from [`crate::functions::all`]
121/// (case-insensitive, aliases included).
122pub fn scalar_func(name: &str) -> Option<(FuncId, usize)> {
123    functions::callable(name).map(|d| (d.id, d.params.len()))
124}
125
126/// A formula parse failure. `position` is a byte offset into the source token
127/// stream's originating text when known.
128#[derive(Debug, Clone, PartialEq)]
129pub struct ParseError {
130    pub message: String,
131    pub position: Option<usize>,
132}
133
134impl ParseError {
135    fn new(message: impl Into<String>, position: Option<usize>) -> Self {
136        ParseError {
137            message: message.into(),
138            position,
139        }
140    }
141}
142
143impl std::fmt::Display for ParseError {
144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145        match self.position {
146            Some(p) => write!(f, "parse error at {p}: {}", self.message),
147            None => write!(f, "parse error: {}", self.message),
148        }
149    }
150}
151
152impl std::error::Error for ParseError {}
153
154/// A parsed formula: the target measure name (LHS of `=`) plus its expression.
155#[derive(Debug, Clone, PartialEq)]
156pub struct FormulaText {
157    pub target: Name,
158    pub formula: Formula,
159}
160
161/// A parsed measure *definition* — either an ordinary formula or a source form
162/// (`SQL("...")` / `CALL(fn, m1, m2, ...)`) that describes a host-side source
163/// rather than a differential-dataflow expression. Source forms carry the
164/// metadata a refresh consumes; they never enter the engine's expression graph,
165/// preserving the deterministic core.
166#[derive(Debug, Clone, PartialEq)]
167pub enum Definition {
168    /// `Target = <expr>` — an ordinary engine formula.
169    Formula(FormulaText),
170    /// `Target = SQL("<query>")` — a SQL-sourced input measure. The query is the
171    /// raw string; column→dimension mapping is the caller's concern.
172    Sql { target: Name, query: String },
173    /// `Target = CALL(func, arg_measure, ...)` — an external-function measure.
174    /// `func` is the registered function name; `args` are the argument measure
175    /// names (resolved to ids by the caller against the model).
176    Call {
177        target: Name,
178        func: String,
179        args: Vec<Name>,
180    },
181    /// `Name = LAMBDA(p, …, body)` — a named LAMBDA (Excel's Name Manager),
182    /// callable as `Name(args)` in formulas once added to the model.
183    Lambda(LambdaText),
184}
185
186/// A parsed named-LAMBDA definition.
187#[derive(Debug, Clone, PartialEq)]
188pub struct LambdaText {
189    pub name: Name,
190    pub lambda: crate::NamedLambda,
191}
192
193// ---------------------------------------------------------------------------
194// Tokenizer
195// ---------------------------------------------------------------------------
196
197#[derive(Debug, Clone, PartialEq)]
198enum Tok {
199    Ident(String),
200    /// A single-quoted name (`'Unit Price'`), with `''` already un-doubled to a
201    /// single `'`. Distinct from [`Tok::Ident`] because a quoted name is *never*
202    /// a keyword or function: it can only be a measure/category name.
203    QIdent(String),
204    Number(f64),
205    Str(String),
206    /// A date/time literal `#YYYY-MM-DD#` or `#YYYY-MM-DDTHH:MM:SSZ#`, stored as
207    /// a UTC timestamp.
208    Date(chrono::DateTime<chrono::Utc>),
209    /// A punctuation/operator lexeme (`+`, `<=`, `==`, `[`, ...).
210    Op(String),
211}
212
213/// A token plus the byte offset where it began (for error positions).
214#[derive(Debug, Clone)]
215struct Spanned {
216    tok: Tok,
217    pos: usize,
218}
219
220/// Parse the inside of a `#...#` date literal. Accepts a bare date
221/// `YYYY-MM-DD` (midnight UTC) or an RFC3339 timestamp `YYYY-MM-DDTHH:MM:SSZ`.
222fn parse_date_literal(raw: &str) -> Option<chrono::DateTime<chrono::Utc>> {
223    let raw = raw.trim();
224    if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(raw) {
225        return Some(dt.with_timezone(&chrono::Utc));
226    }
227    if let Ok(d) = chrono::NaiveDate::parse_from_str(raw, "%Y-%m-%d") {
228        return d.and_hms_opt(0, 0, 0).map(|ndt| ndt.and_utc());
229    }
230    None
231}
232
233fn tokenize(text: &str) -> Result<Vec<Spanned>, ParseError> {
234    let bytes = text.as_bytes();
235    let mut out = Vec::new();
236    let mut i = 0;
237    while i < bytes.len() {
238        let start = i;
239        // Decode the real character at this byte offset. `bytes[i] as char`
240        // (the previous approach) casts a single BYTE, which is wrong for any
241        // multi-byte UTF-8 sequence and can walk `i` to a non-boundary offset,
242        // panicking on the eventual `text[..i]` slice. `text` is valid UTF-8
243        // (it's a `&str`), so decoding at a boundary we maintain never fails.
244        let c = text[i..]
245            .chars()
246            .next()
247            .expect("i < len implies a char here");
248        match c {
249            c if c.is_whitespace() => i += c.len_utf8(),
250            '"' => {
251                // String literal: consume to the closing quote (no escapes in v1).
252                i += 1;
253                let s0 = i;
254                while i < bytes.len() && bytes[i] != b'"' {
255                    i += 1;
256                }
257                if i >= bytes.len() {
258                    return Err(ParseError::new("unterminated string literal", Some(start)));
259                }
260                let s = text[s0..i].to_string();
261                i += 1; // closing quote
262                out.push(Spanned {
263                    tok: Tok::Str(s),
264                    pos: start,
265                });
266            }
267            '#' => {
268                // Date literal: #YYYY-MM-DD# or #YYYY-MM-DDTHH:MM:SSZ#.
269                i += 1;
270                let s0 = i;
271                while i < bytes.len() && bytes[i] != b'#' {
272                    i += 1;
273                }
274                if i >= bytes.len() {
275                    return Err(ParseError::new("unterminated date literal", Some(start)));
276                }
277                let raw = &text[s0..i];
278                i += 1; // closing '#'
279                let dt = parse_date_literal(raw).ok_or_else(|| {
280                    ParseError::new(format!("invalid date: {raw:?}"), Some(start))
281                })?;
282                out.push(Spanned {
283                    tok: Tok::Date(dt),
284                    pos: start,
285                });
286            }
287            c if c.is_ascii_digit() => {
288                // Digits, then `.digits` (a `.` not followed by a digit ends
289                // the number: the `..` of a window, `THIS-1..THIS`). A second
290                // `.` stays in the number so `1.2.3` is an invalid number.
291                while i < bytes.len()
292                    && (bytes[i].is_ascii_digit()
293                        || (bytes[i] == b'.' && bytes.get(i + 1).is_some_and(u8::is_ascii_digit)))
294                {
295                    i += 1;
296                }
297                let raw = &text[start..i];
298                let n = raw
299                    .parse::<f64>()
300                    .map_err(|_| ParseError::new(format!("invalid number: {raw}"), Some(start)))?;
301                out.push(Spanned {
302                    tok: Tok::Number(n),
303                    pos: start,
304                });
305            }
306            '\'' => {
307                // Quoted identifier `'Unit Price'`; `''` is one literal `'`.
308                // Advance by `len_utf8()` only (never by raw bytes) so a
309                // multi-byte name cannot walk `i` off a char boundary.
310                i += 1; // opening quote
311                let mut name = String::new();
312                let mut closed = false;
313                while i < bytes.len() {
314                    let c = text[i..].chars().next().expect("i < len");
315                    i += c.len_utf8();
316                    if c == '\'' {
317                        // `i` is on a char boundary here, so this is safe.
318                        if text[i..].starts_with('\'') {
319                            name.push('\'');
320                            i += 1;
321                            continue;
322                        }
323                        closed = true;
324                        break;
325                    }
326                    name.push(c);
327                }
328                if !closed {
329                    return Err(ParseError::new("unterminated quoted name", Some(start)));
330                }
331                if name.is_empty() {
332                    return Err(ParseError::new(
333                        "empty quoted name: '' names nothing",
334                        Some(start),
335                    ));
336                }
337                out.push(Spanned {
338                    tok: Tok::QIdent(name),
339                    pos: start,
340                });
341            }
342            c if c.is_alphabetic()
343                || c == '_'
344                || (c == '@' && text[i + 1..].starts_with(char::is_alphabetic)) =>
345            {
346                i += c.len_utf8();
347                let word_end = |mut i: usize| {
348                    while i < bytes.len() {
349                        let b = text[i..].chars().next().expect("i < len");
350                        if b.is_alphanumeric() || b == '_' {
351                            i += b.len_utf8();
352                        } else {
353                            break;
354                        }
355                    }
356                    i
357                };
358                i = word_end(i);
359                // A dotted function name (`FLOOR.MATH(`, `NORM.S.DIST(`,
360                // `T.DIST.2T(`): the longest run of `.part`s that names a
361                // function and is followed by `(` is one identifier.
362                let mut j = i;
363                let mut best = None;
364                // No function name has more than three dots (`T.DIST.2T`),
365                // so a long `a.a.a…` costs O(n), not O(n²).
366                let mut parts = 0;
367                while parts < 3
368                    && text[j..].starts_with('.')
369                    && text[j + 1..].starts_with(|c: char| c.is_alphanumeric() || c == '_')
370                {
371                    parts += 1;
372                    j = word_end(j + 1);
373                    let rest = text[j..].trim_start();
374                    if rest.starts_with('(') && functions::by_name(&text[start..j]).next().is_some()
375                    {
376                        best = Some(j);
377                    }
378                }
379                if let Some(end) = best {
380                    i = end;
381                }
382                out.push(Spanned {
383                    tok: Tok::Ident(text[start..i].to_string()),
384                    pos: start,
385                });
386            }
387            // Two-char operators first, then single-char.
388            _ => {
389                let two = text.get(i..i + 2);
390                let op = match two {
391                    Some("<=") | Some(">=") | Some("<>") | Some("==") | Some("!=") => {
392                        i += 2;
393                        two.unwrap().to_string()
394                    }
395                    _ => {
396                        let single = "+-*/^=<>()[],.&%".find(c);
397                        if single.is_none() {
398                            return Err(ParseError::new(
399                                format!("unexpected character: {c:?}"),
400                                Some(start),
401                            ));
402                        }
403                        i += c.len_utf8();
404                        c.to_string()
405                    }
406                };
407                out.push(Spanned {
408                    tok: Tok::Op(op),
409                    pos: start,
410                });
411            }
412        }
413    }
414    Ok(out)
415}
416
417// ---------------------------------------------------------------------------
418// Parser
419// ---------------------------------------------------------------------------
420
421/// An aggregation's `OVER …` clause as parsed.
422#[derive(Default)]
423struct OverClause {
424    cats: Vec<CategoryId>,
425    /// `OVER ALL [EXCEPT …]`.
426    all: bool,
427    except: Vec<CategoryId>,
428    /// `OVER T[from..to]`: bound literals (see `functions::WINDOW`).
429    window: Option<(Expr, Expr)>,
430    /// `INTO K`: where an array function's results land.
431    into: Vec<CategoryId>,
432    pred: Option<Expr>,
433}
434
435struct Parser<'a> {
436    toks: &'a [Spanned],
437    pos: usize,
438    model: &'a Model,
439    /// Current recursive-descent nesting depth (parens / unary chains).
440    /// Bounded so adversarial input (e.g. thousands of nested `(`) errors
441    /// cleanly instead of overflowing the stack.
442    depth: usize,
443    /// Names bound by `LET` and `LAMBDA`, innermost last; they shadow
444    /// measure names and are replaced by what they are bound to.
445    scope: Vec<(String, Expr)>,
446    /// How many `REDUCE`/`SCAN` lambdas this formula has (each gets its own
447    /// placeholder ids).
448    lambdas: u32,
449}
450
451/// Maximum recursive-descent nesting depth for a single formula (Excel allows
452/// 64 nested levels). Each level costs one frame per grammar level, about
453/// 10 KiB in a debug build, so 100 levels fit a 2 MiB test-thread stack with
454/// room to spare; lower it again if the grammar grows many more levels.
455const MAX_PARSE_DEPTH: usize = 100;
456
457impl<'a> Parser<'a> {
458    fn peek(&self) -> Option<&Tok> {
459        self.toks.get(self.pos).map(|s| &s.tok)
460    }
461
462    fn peek_pos(&self) -> Option<usize> {
463        self.toks.get(self.pos).map(|s| s.pos).or_else(|| {
464            // Point past the last token at EOF.
465            self.toks.last().map(|s| s.pos)
466        })
467    }
468
469    fn bump(&mut self) -> Option<&Tok> {
470        let t = self.toks.get(self.pos).map(|s| &s.tok);
471        if t.is_some() {
472            self.pos += 1;
473        }
474        t
475    }
476
477    /// Consume a specific operator lexeme if present.
478    fn eat_op(&mut self, op: &str) -> bool {
479        if matches!(self.peek(), Some(Tok::Op(o)) if o == op) {
480            self.pos += 1;
481            true
482        } else {
483            false
484        }
485    }
486
487    /// Case-insensitive keyword match on a *bare* identifier token (without
488    /// consuming). A quoted name ([`Tok::QIdent`]) is never a keyword, so
489    /// `'Over'` stays a measure name.
490    fn peek_kw(&self, kw: &str) -> bool {
491        matches!(self.peek(), Some(Tok::Ident(w)) if w.eq_ignore_ascii_case(kw))
492    }
493
494    /// Consume one identifier — bare (`Price`) or quoted (`'Unit Price'`) — and
495    /// return its text. `what` names the expectation for the error message.
496    fn take_ident(&mut self, what: &str) -> Result<String, ParseError> {
497        match self.peek() {
498            Some(Tok::Ident(w) | Tok::QIdent(w)) => {
499                let w = w.clone();
500                self.pos += 1;
501                Ok(w)
502            }
503            _ => Err(self.err(format!("expected {what}"))),
504        }
505    }
506
507    /// `Name = [ Identifier "." ] Identifier` — a measure/category name, bare or
508    /// quoted, optionally qualified (`Matrix.Measure`). Returns the name plus
509    /// the byte offset it started at, so a failed lookup points at the *name*
510    /// rather than at whatever follows it.
511    ///
512    /// Qualification *parses* but never resolves: Improv has no namespace a
513    /// qualifier could name (a measure belongs to the model, not to a matrix),
514    /// so a qualified name is a clear error naming the unknown qualifier rather
515    /// than a stray-character complaint. Exactly one dot is consumed, so no
516    /// input can make this allocate or recurse without bound.
517    fn parse_name(&mut self, what: &str) -> Result<(String, Option<usize>), ParseError> {
518        let pos = self.peek_pos();
519        let first = self.take_ident(what)?;
520        if !self.eat_op(".") {
521            return Ok((first, pos));
522        }
523        let leaf = self.take_ident(what)?;
524        Err(ParseError::new(
525            format!(
526                "unknown qualifier: {first} (no such matrix; `{first}.{leaf}` cannot be \
527                 resolved — write the name alone)"
528            ),
529            pos,
530        ))
531    }
532
533    fn err(&self, msg: impl Into<String>) -> ParseError {
534        ParseError::new(msg, self.peek_pos())
535    }
536
537    // --- resolution ---
538
539    /// Resolve a measure name consumed by [`Self::parse_name`], reporting the
540    /// failure at the name's own offset (not at whatever follows it).
541    fn measure_id(&self, name: &str, pos: Option<usize>) -> Result<MeasureId, ParseError> {
542        self.model
543            .measure_by_name(name)
544            .map(|m| m.id)
545            .ok_or_else(|| ParseError::new(format!("unknown measure: {name}"), pos))
546    }
547
548    fn category_id(&self, name: &str, pos: Option<usize>) -> Result<CategoryId, ParseError> {
549        // A named LAMBDA's parameter where a category goes (`OVER c`).
550        if let Some(Expr::Ref(id, _)) = self.local(name) {
551            if let Some(i) = functions::lambda_param_of(id) {
552                return Ok(functions::lambda_category(i));
553            }
554        }
555        self.model
556            .category_by_name(name)
557            .map(|c| c.id)
558            .ok_or_else(|| ParseError::new(format!("unknown category: {name}"), pos))
559    }
560
561    // --- grammar ---
562
563    /// Expression = OrExpr
564    fn parse_expr(&mut self) -> Result<Expr, ParseError> {
565        self.parse_or()
566    }
567
568    /// OrExpr = AndExpr { "OR" AndExpr }
569    fn parse_or(&mut self) -> Result<Expr, ParseError> {
570        let mut lhs = self.parse_and()?;
571        while self.peek_kw("or") {
572            self.bump();
573            let rhs = self.parse_and()?;
574            lhs = Expr::BinaryOp(BinaryOp::Or, Box::new(lhs), Box::new(rhs));
575        }
576        Ok(lhs)
577    }
578
579    /// AndExpr = NotExpr { "AND" NotExpr }
580    fn parse_and(&mut self) -> Result<Expr, ParseError> {
581        let mut lhs = self.parse_not()?;
582        while self.peek_kw("and") {
583            self.bump();
584            let rhs = self.parse_not()?;
585            lhs = Expr::BinaryOp(BinaryOp::And, Box::new(lhs), Box::new(rhs));
586        }
587        Ok(lhs)
588    }
589
590    /// NotExpr = { "NOT" } Comparison
591    ///
592    /// NOT binds looser than a comparison (as in SQL and spreadsheets), so
593    /// `NOT Price > 15` is `NOT (Price > 15)`. A chain of NOTs is read in a
594    /// loop (no parser stack), but each NOT still counts toward
595    /// `MAX_PARSE_DEPTH`: the compiler and printers walk the tree recursively.
596    fn parse_not(&mut self) -> Result<Expr, ParseError> {
597        let mut nots = 0usize;
598        while self.peek_kw("not") {
599            if self.depth + nots >= MAX_PARSE_DEPTH {
600                return Err(self.err("expression nested too deeply"));
601            }
602            self.bump();
603            nots += 1;
604        }
605        self.depth += nots;
606        let e = self.parse_comparison();
607        self.depth -= nots;
608        let mut e = e?;
609        for _ in 0..nots {
610            e = Expr::UnaryOp(UnaryOp::Not, Box::new(e));
611        }
612        Ok(e)
613    }
614
615    /// Comparison = Additive [ cmp-op Additive ]  (non-associative)
616    fn parse_comparison(&mut self) -> Result<Expr, ParseError> {
617        let lhs = self.parse_concat()?;
618        let op = match self.peek() {
619            Some(Tok::Op(o)) => match o.as_str() {
620                "==" => Some(BinaryOp::Eq),
621                "<>" | "!=" => Some(BinaryOp::Ne),
622                "<" => Some(BinaryOp::Lt),
623                "<=" => Some(BinaryOp::Le),
624                ">" => Some(BinaryOp::Gt),
625                ">=" => Some(BinaryOp::Ge),
626                _ => None,
627            },
628            _ => None,
629        };
630        match op {
631            Some(op) => {
632                self.bump();
633                let rhs = self.parse_concat()?;
634                Ok(Expr::BinaryOp(op, Box::new(lhs), Box::new(rhs)))
635            }
636            None => Ok(lhs),
637        }
638    }
639
640    /// Concat = Additive { "&" Additive }
641    fn parse_concat(&mut self) -> Result<Expr, ParseError> {
642        let mut lhs = self.parse_additive()?;
643        while self.eat_op("&") {
644            let rhs = self.parse_additive()?;
645            lhs = Expr::BinaryOp(BinaryOp::Concat, Box::new(lhs), Box::new(rhs));
646        }
647        Ok(lhs)
648    }
649
650    /// Additive = Term { ("+"|"-") Term }
651    fn parse_additive(&mut self) -> Result<Expr, ParseError> {
652        let mut lhs = self.parse_term()?;
653        loop {
654            let op = match self.peek() {
655                Some(Tok::Op(o)) if o == "+" => BinaryOp::Add,
656                Some(Tok::Op(o)) if o == "-" => BinaryOp::Sub,
657                _ => break,
658            };
659            self.bump();
660            let rhs = self.parse_term()?;
661            lhs = Expr::BinaryOp(op, Box::new(lhs), Box::new(rhs));
662        }
663        Ok(lhs)
664    }
665
666    /// Term = Unary { ("*"|"/") Unary }
667    fn parse_term(&mut self) -> Result<Expr, ParseError> {
668        let mut lhs = self.parse_unary()?;
669        loop {
670            let op = match self.peek() {
671                Some(Tok::Op(o)) if o == "*" => BinaryOp::Mul,
672                Some(Tok::Op(o)) if o == "/" => BinaryOp::Div,
673                _ => break,
674            };
675            self.bump();
676            let rhs = self.parse_unary()?;
677            lhs = Expr::BinaryOp(op, Box::new(lhs), Box::new(rhs));
678        }
679        Ok(lhs)
680    }
681
682    /// Unary = "-" Unary | Power. A chain of minuses is read in a loop (each
683    /// counts toward `MAX_PARSE_DEPTH`, as the compiler recurses over it).
684    /// Unary minus binds looser than `^`: `-2^2` is `-(2^2)`.
685    fn parse_unary(&mut self) -> Result<Expr, ParseError> {
686        let mut negs = 0usize;
687        while matches!(self.peek(), Some(Tok::Op(o)) if o == "-") {
688            if self.depth + negs >= MAX_PARSE_DEPTH {
689                return Err(self.err("expression nested too deeply"));
690            }
691            self.bump();
692            negs += 1;
693        }
694        self.depth += negs;
695        let e = self.parse_power();
696        self.depth -= negs;
697        let mut e = e?;
698        for _ in 0..negs {
699            e = Expr::UnaryOp(UnaryOp::Neg, Box::new(e));
700        }
701        Ok(e)
702    }
703
704    /// Power = Postfix [ "^" Unary ]: right-associative (`2^3^2` is `2^9`),
705    /// and the exponent may be negative (`2^-1`).
706    fn parse_power(&mut self) -> Result<Expr, ParseError> {
707        let base = self.parse_postfix()?;
708        if !self.eat_op("^") {
709            return Ok(base);
710        }
711        self.depth += 1;
712        if self.depth > MAX_PARSE_DEPTH {
713            self.depth -= 1;
714            return Err(self.err("expression nested too deeply"));
715        }
716        let exp = self.parse_unary();
717        self.depth -= 1;
718        Ok(Expr::BinaryOp(
719            BinaryOp::Pow,
720            Box::new(base),
721            Box::new(exp?),
722        ))
723    }
724
725    /// Postfix = Primary { "%" }: `x%` is `x / 100` (`50%` folds to `0.5`).
726    fn parse_postfix(&mut self) -> Result<Expr, ParseError> {
727        let mut e = self.parse_primary()?;
728        while self.eat_op("%") {
729            e = match e {
730                Expr::Literal(Value::Number(n)) => Expr::Literal(Value::Number(n / 100.0)),
731                other => Expr::BinaryOp(
732                    BinaryOp::Div,
733                    Box::new(other),
734                    Box::new(Expr::Literal(Value::Number(100.0))),
735                ),
736            };
737        }
738        Ok(e)
739    }
740
741    /// Primary = Literal | Call | MeasureRef | "(" Expression ")"
742    fn parse_primary(&mut self) -> Result<Expr, ParseError> {
743        self.depth += 1;
744        if self.depth > MAX_PARSE_DEPTH {
745            self.depth -= 1;
746            return Err(self.err("expression nested too deeply"));
747        }
748        let result = self.parse_primary_inner();
749        self.depth -= 1;
750        result
751    }
752
753    fn parse_primary_inner(&mut self) -> Result<Expr, ParseError> {
754        match self.peek() {
755            None => Err(self.err("unexpected end of input")),
756            Some(Tok::Op(o)) if o == "(" => {
757                self.bump();
758                let e = self.parse_expr()?;
759                if !self.eat_op(")") {
760                    return Err(self.err("expected ')'"));
761                }
762                Ok(e)
763            }
764            Some(Tok::Number(n)) => {
765                let n = *n;
766                self.bump();
767                Ok(Expr::Literal(Value::Number(n)))
768            }
769            Some(Tok::Str(s)) => {
770                let s = s.clone();
771                self.bump();
772                Ok(Expr::Literal(Value::Text(s)))
773            }
774            Some(Tok::Date(dt)) => {
775                let dt = *dt;
776                self.bump();
777                Ok(Expr::Literal(Value::DateTime(dt)))
778            }
779            Some(Tok::Ident(_)) => self.parse_ident_primary(),
780            // A quoted name is only ever a measure reference.
781            Some(Tok::QIdent(_)) => self.parse_measure_ref(),
782            Some(Tok::Op(o)) => Err(self.err(format!("unexpected operator: {o}"))),
783        }
784    }
785
786    /// Identifier-headed primary: TRUE/FALSE, aggregation, or a measure ref.
787    fn parse_ident_primary(&mut self) -> Result<Expr, ParseError> {
788        // Peek the identifier text without holding the borrow.
789        let word = match self.peek() {
790            Some(Tok::Ident(w)) => w.clone(),
791            _ => return Err(self.err("expected identifier")),
792        };
793        // `TRUE()` / `FALSE()` are the functions; bare `TRUE` the literal.
794        let is_call =
795            matches!(self.toks.get(self.pos + 1).map(|s| &s.tok), Some(Tok::Op(o)) if o == "(");
796        // A name bound by LET or LAMBDA.
797        if !is_call {
798            if let Some(e) = self.local(&word) {
799                self.bump();
800                return Ok(e);
801            }
802        }
803        if is_call && functions::callable(&word).is_none() {
804            if let Some((id, l)) = self.model.lambda_by_name(&word) {
805                let kinds = l.param_kinds().map_err(|e| self.err(e))?;
806                return self.parse_named_lambda_call(id, &kinds, &word);
807            }
808        }
809        if is_call {
810            match word.to_ascii_uppercase().as_str() {
811                "LET" => return self.parse_let(),
812                "LAMBDA" => return self.parse_lambda_call(),
813                "MAP" => return self.parse_map(),
814                "SEQUENCE" => return self.parse_sequence(),
815                "MAKEARRAY" => return self.parse_makearray(),
816                "BYROW" | "BYCOL" => return self.parse_byrow(&word),
817                "REDUCE" | "SCAN" => return self.parse_fold(&word),
818                "FIXPOINT" => return self.parse_fixpoint(),
819                _ => {}
820            }
821        }
822        if is_call && (word.eq_ignore_ascii_case("true") || word.eq_ignore_ascii_case("false")) {
823            return self.parse_call(&word);
824        }
825
826        if word.eq_ignore_ascii_case("true") {
827            self.bump();
828            return Ok(Expr::Literal(Value::Boolean(true)));
829        }
830        if word.eq_ignore_ascii_case("false") {
831            self.bump();
832            return Ok(Expr::Literal(Value::Boolean(false)));
833        }
834        // A function name only calls when directly followed by "(";
835        // otherwise the word is an ordinary measure name.
836        if is_call
837            && (functions::aggregation(&word).is_some() || functions::callable(&word).is_some())
838        {
839            return self.parse_call(&word);
840        }
841        // `NAME(` that is neither a function nor a measure: say so, and name
842        // the closest functions (a typo like PMTT).
843        if is_call && self.model.measure_by_name(&word).is_none() {
844            let pos = self.toks.get(self.pos).map(|s| s.pos);
845            let near = functions::closest(&word, 3);
846            let hint = if near.is_empty() {
847                String::new()
848            } else {
849                format!("; did you mean {}?", near.join(", "))
850            };
851            return Err(ParseError::new(
852                format!("unknown function: {word}{hint}"),
853                pos,
854            ));
855        }
856        self.parse_measure_ref()
857    }
858
859    /// `NAME(args…)`: an aggregation `NAME(MeasureRef OVER Category)` when
860    /// the name has one and `OVER` follows the first argument, otherwise a
861    /// call of the non-aggregating function the name means. Arity is checked
862    /// against the registry so a bad call is a clear parse error.
863    fn parse_call(&mut self, name: &str) -> Result<Expr, ParseError> {
864        self.bump(); // function name
865        if !self.eat_op("(") {
866            return Err(self.err("expected '(' after function name"));
867        }
868        // The callable's parameter kinds, to read category arguments
869        // (`POSITION(Time)`) as categories rather than measures.
870        let callable = functions::callable(name);
871        let mut args = Vec::new();
872        let mut over: Option<OverClause> = None;
873        let mut into: Vec<CategoryId> = Vec::new();
874        if !matches!(self.toks.get(self.pos).map(|s| &s.tok), Some(Tok::Op(o)) if o == ")") {
875            loop {
876                let i = args.len();
877                let is_cat = over.is_none()
878                    && callable.is_some_and(|d| d.param(i) == functions::Arg::Category)
879                    && functions::aggregation(name).is_none();
880                if is_cat {
881                    args.push(Expr::Category(self.parse_category_name()?));
882                } else {
883                    args.push(self.parse_expr()?);
884                }
885                if self.peek_kw("over") {
886                    if over.is_some() {
887                        return Err(self.err("only one OVER clause per aggregation"));
888                    }
889                    over = Some(self.parse_over_clause()?);
890                }
891                if self.peek_kw("into") {
892                    self.bump();
893                    if !into.is_empty() {
894                        return Err(self.err("only one INTO clause per call"));
895                    }
896                    // `INTO R, C`: a list laid over several categories.
897                    into.push(self.parse_category_name()?);
898                    while self.comma_then_category() {
899                        self.bump(); // ,
900                        into.push(self.parse_category_name()?);
901                    }
902                }
903                if self.eat_op(",") {
904                    continue;
905                }
906                break;
907            }
908        }
909        if !self.eat_op(")") {
910            return Err(self.err("expected ')' to close function call"));
911        }
912        // A BYROW parameter (`SUM(row)`) carries its row's category: the
913        // call reduces over it.
914        if over.is_none() && functions::aggregation(name).is_some() {
915            over = args.iter().find_map(|a| match a {
916                Expr::Ref(_, s) if !s.over.is_empty() => Some(OverClause {
917                    cats: s.over.clone(),
918                    ..OverClause::default()
919                }),
920                _ => None,
921            });
922        }
923        if over.is_some() || !into.is_empty() {
924            let mut clause = over.unwrap_or_default();
925            clause.into = into;
926            return self.finish_aggregation(name, args, clause);
927        }
928        let def = callable.ok_or_else(|| {
929            self.err(format!(
930                "{name} aggregates: write {name}(Measure OVER Category)"
931            ))
932        })?;
933        if !def.accepts(args.len()) {
934            return Err(self.err(format!(
935                "{} takes {}, got {} argument(s)",
936                def.name,
937                def.hint(),
938                args.len()
939            )));
940        }
941        Ok(Expr::Call(def.id, args))
942    }
943
944    /// `OVER Cat {, Cat} [ "[" Bound ".." Bound "]" ] [WHERE pred]` or
945    /// `OVER ALL [EXCEPT Cat {, Cat}] [WHERE pred]`. A `,` continues the
946    /// category list only when a category name follows and is not itself
947    /// followed by `(`; otherwise it starts the next argument.
948    fn parse_over_clause(&mut self) -> Result<OverClause, ParseError> {
949        self.bump(); // OVER
950        let mut clause = OverClause::default();
951        if self.peek_kw("all") && !self.next_is_open_paren() {
952            self.bump();
953            clause.all = true;
954            if self.peek_kw("except") {
955                self.bump();
956                clause.except.push(self.parse_category_name()?);
957                while self.comma_then_category() {
958                    self.bump(); // ,
959                    clause.except.push(self.parse_category_name()?);
960                }
961            }
962        } else {
963            clause.cats.push(self.parse_category_name()?);
964            if self.eat_op("[") {
965                let from = self.parse_bound()?;
966                if !self.eat_op(".") || !self.eat_op(".") {
967                    return Err(self.err("expected '..' in a window: OVER T[FIRST..THIS]"));
968                }
969                let to = self.parse_bound()?;
970                if !self.eat_op("]") {
971                    return Err(self.err("expected ']' to close the window"));
972                }
973                clause.window = Some((from, to));
974            } else {
975                while self.comma_then_category() {
976                    self.bump(); // ,
977                    clause.cats.push(self.parse_category_name()?);
978                }
979            }
980        }
981        if self.peek_kw("where") {
982            self.bump();
983            clause.pred = Some(self.parse_expr()?);
984        }
985        Ok(clause)
986    }
987
988    /// A window bound: `FIRST`, `LAST`, `THIS`, `THIS-n`, `THIS+n`, as the
989    /// literal the `WINDOW` marker stores (text, or an offset number).
990    fn parse_bound(&mut self) -> Result<Expr, ParseError> {
991        let word = match self.peek() {
992            Some(Tok::Ident(w)) => w.to_ascii_uppercase(),
993            _ => return Err(self.err("expected FIRST, LAST or THIS in a window")),
994        };
995        self.bump();
996        let lit = |v| Ok(Expr::Literal(v));
997        match word.as_str() {
998            "FIRST" | "LAST" => lit(Value::Text(word)),
999            "THIS" => {
1000                let sign = match self.peek() {
1001                    Some(Tok::Op(o)) if o == "+" => 1.0,
1002                    Some(Tok::Op(o)) if o == "-" => -1.0,
1003                    _ => return lit(Value::Number(0.0)),
1004                };
1005                self.bump();
1006                match self.peek() {
1007                    Some(Tok::Number(n)) if n.fract() == 0.0 => {
1008                        let n = *n;
1009                        self.bump();
1010                        lit(Value::Number(sign * n))
1011                    }
1012                    _ => Err(self.err("expected a whole number after THIS+ / THIS-")),
1013                }
1014            }
1015            _ => Err(self.err("expected FIRST, LAST or THIS in a window")),
1016        }
1017    }
1018
1019    fn next_is_open_paren(&self) -> bool {
1020        matches!(self.toks.get(self.pos + 1).map(|s| &s.tok), Some(Tok::Op(o)) if o == "(")
1021    }
1022
1023    /// Whether the next tokens are `, Category` (and not `, Name(` or a
1024    /// measure): the OVER category list continues.
1025    fn comma_then_category(&self) -> bool {
1026        let at = |k: usize| self.toks.get(self.pos + k).map(|s| &s.tok);
1027        let name = match (at(0), at(1)) {
1028            (Some(Tok::Op(c)), Some(Tok::Ident(n) | Tok::QIdent(n))) if c == "," => n,
1029            _ => return false,
1030        };
1031        let followed_ok = !matches!(at(2), Some(Tok::Op(o)) if o == "(" || o == ".");
1032        followed_ok
1033            && self.model.category_by_name(name).is_some()
1034            && self.model.measure_by_name(name).is_none()
1035    }
1036
1037    /// Build `AGG(args…)` once its OVER clause is read: every argument the
1038    /// registry marks as reduced must be a measure reference and gets the
1039    /// OVER categories; a `WHERE` and a window become trailing markers.
1040    fn finish_aggregation(
1041        &mut self,
1042        name: &str,
1043        mut args: Vec<Expr>,
1044        clause: OverClause,
1045    ) -> Result<Expr, ParseError> {
1046        let def = functions::aggregation(name)
1047            .ok_or_else(|| self.err(format!("{name} does not aggregate OVER a category")))?;
1048        if !def.accepts(args.len()) {
1049            return Err(self.err(format!(
1050                "{} takes {}, got {} argument(s)",
1051                def.name,
1052                def.hint(),
1053                args.len()
1054            )));
1055        }
1056        if clause.window.is_some() && def.kind == functions::Kind::PerItem {
1057            return Err(self.err(format!("{} cannot take a window", def.name)));
1058        }
1059        // Over::Leading: every leading measure reference is reduced.
1060        let leading = match def.over {
1061            functions::Over::Leading(min) => {
1062                let n = args
1063                    .iter()
1064                    .take_while(|a| matches!(a, Expr::Ref(..)))
1065                    .count();
1066                if n < min {
1067                    return Err(self.err(format!(
1068                        "{} needs at least {min} measure(s) before its options",
1069                        def.name
1070                    )));
1071                }
1072                Some(n)
1073            }
1074            _ => None,
1075        };
1076        for (i, a) in args.iter_mut().enumerate() {
1077            if !leading.map_or_else(|| def.over.contains(i), |n| i < n) {
1078                continue;
1079            }
1080            let Expr::Ref(id, spec) = a else {
1081                return Err(self.err(format!(
1082                    "{} argument {} must be a measure reference (it is reduced OVER the \
1083                     categories)",
1084                    def.name,
1085                    i + 1
1086                )));
1087            };
1088            let mut s = spec.clone();
1089            if clause.all {
1090                // OVER ALL [EXCEPT …]: every category of the measure but the
1091                // excepted ones (resolved now, from the measure).
1092                let m = self.model.measures.get(id);
1093                s.over = m
1094                    .map(|m| {
1095                        m.categories
1096                            .iter()
1097                            .copied()
1098                            .filter(|c| !clause.except.contains(c))
1099                            .collect()
1100                    })
1101                    .unwrap_or_default();
1102            } else {
1103                s.over = clause.cats.clone();
1104            }
1105            *a = Expr::Ref(*id, s);
1106        }
1107        if let Some(p) = clause.pred {
1108            args.push(Expr::Call(functions::WHERE, vec![p]));
1109        }
1110        if let Some((from, to)) = clause.window {
1111            args.push(Expr::Call(functions::WINDOW, vec![from, to]));
1112        }
1113        if !clause.into.is_empty() {
1114            args.push(Expr::Call(
1115                functions::INTO,
1116                clause.into.into_iter().map(Expr::Category).collect(),
1117            ));
1118        }
1119        Ok(Expr::Call(def.id, args))
1120    }
1121
1122    /// What a `LET`/`LAMBDA` name is bound to, innermost binding first.
1123    fn local(&self, name: &str) -> Option<Expr> {
1124        self.scope
1125            .iter()
1126            .rev()
1127            .find(|(n, _)| n.eq_ignore_ascii_case(name))
1128            .map(|(_, e)| e.clone())
1129    }
1130
1131    /// A bare name for LET/LAMBDA.
1132    fn binding_name(&mut self) -> Result<String, ParseError> {
1133        match self.peek() {
1134            Some(Tok::Ident(w)) => {
1135                let w = w.clone();
1136                self.bump();
1137                Ok(w)
1138            }
1139            _ => Err(self.err("expected a name to bind")),
1140        }
1141    }
1142
1143    fn expect(&mut self, op: &str, what: &str) -> Result<(), ParseError> {
1144        if self.eat_op(op) {
1145            Ok(())
1146        } else {
1147            Err(self.err(format!("expected {what}")))
1148        }
1149    }
1150
1151    /// `LET(name, value, [name, value, …], body)`: the body with each name
1152    /// replaced by its value (a later value may use an earlier name). LET
1153    /// is expanded here; the stored formula is the expanded expression.
1154    fn parse_let(&mut self) -> Result<Expr, ParseError> {
1155        self.bump(); // LET
1156        self.expect("(", "'(' after LET")?;
1157        let base = self.scope.len();
1158        loop {
1159            // `name ,` starts another binding; anything else is the body.
1160            let is_binding = matches!(self.peek(), Some(Tok::Ident(_)))
1161                && matches!(self.toks.get(self.pos + 1).map(|s| &s.tok), Some(Tok::Op(o)) if o == ",")
1162                && self.scope.len() - base < 126;
1163            if !is_binding && self.scope.len() > base {
1164                break;
1165            }
1166            let name = self.binding_name()?;
1167            self.expect(",", "',' after a LET name")?;
1168            let value = self.parse_expr()?;
1169            self.scope.push((name, value));
1170            self.expect(",", "',' then the LET body")?;
1171        }
1172        let body = self.parse_expr();
1173        self.scope.truncate(base);
1174        let body = body?;
1175        self.expect(")", "')' to close LET")?;
1176        Ok(body)
1177    }
1178
1179    /// `LAMBDA(p1, …, body)` as written: its parameter names and the
1180    /// tokens of its body (parsed when the arguments are known).
1181    fn parse_lambda_params(&mut self) -> Result<(Vec<String>, usize, usize), ParseError> {
1182        self.bump(); // LAMBDA
1183        self.expect("(", "'(' after LAMBDA")?;
1184        let mut params = Vec::new();
1185        // Parameters are the names followed by ','; the last part is the body.
1186        while matches!(self.peek(), Some(Tok::Ident(_)))
1187            && matches!(self.toks.get(self.pos + 1).map(|s| &s.tok), Some(Tok::Op(o)) if o == ",")
1188        {
1189            params.push(self.binding_name()?);
1190            self.bump(); // ,
1191        }
1192        let start = self.pos;
1193        // Skip the body to its closing ')' (balanced).
1194        let mut level = 0usize;
1195        while let Some(t) = self.peek() {
1196            match t {
1197                Tok::Op(o) if o == "(" => level += 1,
1198                Tok::Op(o) if o == ")" && level == 0 => break,
1199                Tok::Op(o) if o == ")" => level -= 1,
1200                _ => {}
1201            }
1202            self.bump();
1203        }
1204        let end = self.pos;
1205        self.expect(")", "')' to close LAMBDA")?;
1206        if params.is_empty() {
1207            return Err(self.err("LAMBDA needs at least one parameter"));
1208        }
1209        Ok((params, start, end))
1210    }
1211
1212    /// Parse the body tokens `start..end` with `params` bound to `args`.
1213    fn lambda_body(
1214        &mut self,
1215        params: &[String],
1216        args: Vec<Expr>,
1217        start: usize,
1218        end: usize,
1219    ) -> Result<Expr, ParseError> {
1220        if params.len() != args.len() {
1221            return Err(self.err(format!(
1222                "this LAMBDA takes {} argument(s), got {}",
1223                params.len(),
1224                args.len()
1225            )));
1226        }
1227        let saved = self.pos;
1228        let base = self.scope.len();
1229        self.scope.extend(params.iter().cloned().zip(args));
1230        self.pos = start;
1231        let body = self.parse_expr();
1232        let stopped = self.pos;
1233        self.scope.truncate(base);
1234        self.pos = saved;
1235        let body = body?;
1236        if stopped != end {
1237            return Err(ParseError::new(
1238                "unexpected trailing input in the LAMBDA body",
1239                self.toks.get(stopped).map(|s| s.pos),
1240            ));
1241        }
1242        Ok(body)
1243    }
1244
1245    /// `Name(a, …)` of a named LAMBDA whose parameters take `kinds`: stored
1246    /// as a call of [`functions::named_lambda`], expanded when compiled. A
1247    /// category parameter's argument is a category name, a reduced one's a
1248    /// measure reference.
1249    fn parse_named_lambda_call(
1250        &mut self,
1251        id: u32,
1252        kinds: &[crate::LambdaParam],
1253        name: &str,
1254    ) -> Result<Expr, ParseError> {
1255        self.bump(); // name
1256        self.expect("(", "'('")?;
1257        let n = kinds.len();
1258        let mut args = Vec::new();
1259        if !self.eat_op(")") {
1260            loop {
1261                let arg = match kinds.get(args.len()) {
1262                    Some(crate::LambdaParam::Category) => {
1263                        Expr::Category(self.parse_category_name()?)
1264                    }
1265                    kind => {
1266                        let e = self.parse_expr()?;
1267                        if kind == Some(&crate::LambdaParam::Reduced) && !matches!(e, Expr::Ref(..))
1268                        {
1269                            return Err(self.err(format!(
1270                                "{name} reduces argument {} over a category: pass a measure, \
1271                                 not an expression",
1272                                args.len() + 1
1273                            )));
1274                        }
1275                        e
1276                    }
1277                };
1278                args.push(arg);
1279                if !self.eat_op(",") {
1280                    break;
1281                }
1282            }
1283            self.expect(")", "')' to close the call")?;
1284        }
1285        if args.len() != n {
1286            return Err(self.err(format!("{name} takes {n} argument(s), got {}", args.len())));
1287        }
1288        Ok(Expr::Call(functions::named_lambda(id), args))
1289    }
1290
1291    /// `LAMBDA(p, …, body)(a, …)`: called on the spot.
1292    fn parse_lambda_call(&mut self) -> Result<Expr, ParseError> {
1293        let (params, start, end) = self.parse_lambda_params()?;
1294        if !self.eat_op("(") {
1295            return Err(self.err(
1296                "a LAMBDA must be called on the spot (LAMBDA(x, x + 1)(Price)) or passed \
1297                 to MAP, REDUCE or SCAN",
1298            ));
1299        }
1300        let mut args = Vec::new();
1301        loop {
1302            args.push(self.parse_expr()?);
1303            if !self.eat_op(",") {
1304                break;
1305            }
1306        }
1307        self.expect(")", "')' to close the LAMBDA call")?;
1308        self.lambda_body(&params, args, start, end)
1309    }
1310
1311    /// `MAP(a, [b, …], LAMBDA(p, [q, …], body))`: the body with each
1312    /// parameter replaced by its argument, element by element.
1313    fn parse_map(&mut self) -> Result<Expr, ParseError> {
1314        self.bump(); // MAP
1315        self.expect("(", "'(' after MAP")?;
1316        let mut args = Vec::new();
1317        while !self.peek_kw("lambda") {
1318            args.push(self.parse_expr()?);
1319            self.expect(",", "',' then a LAMBDA in MAP")?;
1320        }
1321        let (params, start, end) = self.parse_lambda_params()?;
1322        self.expect(")", "')' to close MAP")?;
1323        self.lambda_body(&params, args, start, end)
1324    }
1325
1326    /// `POSITION(cat)`, the item's 1-based position along `cat`.
1327    fn position(cat: CategoryId) -> Expr {
1328        Expr::Call(functions::POSITION, vec![Expr::Category(cat)])
1329    }
1330
1331    /// `SEQUENCE(T, [start = 1], [step = 1])`: `start`, `start + step`, …
1332    /// along T's items, i.e. `(POSITION(T) - 1) * step + start`. Excel's
1333    /// `SEQUENCE(rows, cols, …)` sizes a spill; here the category is the size.
1334    fn parse_sequence(&mut self) -> Result<Expr, ParseError> {
1335        self.bump(); // SEQUENCE
1336        self.expect("(", "'(' after SEQUENCE")?;
1337        let cat = self.parse_category_name()?;
1338        let mut rest = Vec::new();
1339        while self.eat_op(",") {
1340            rest.push(self.parse_expr()?);
1341        }
1342        self.expect(")", "')' to close SEQUENCE")?;
1343        if rest.len() > 2 {
1344            return Err(self.err("SEQUENCE takes (Category, [start], [step])"));
1345        }
1346        let mut it = rest.into_iter();
1347        let one = || Expr::Literal(Value::Number(1.0));
1348        let (start, step) = (it.next().unwrap_or_else(one), it.next().unwrap_or_else(one));
1349        let index = Expr::BinaryOp(
1350            BinaryOp::Sub,
1351            Box::new(Self::position(cat)),
1352            Box::new(one()),
1353        );
1354        Ok(Expr::BinaryOp(
1355            BinaryOp::Add,
1356            Box::new(Expr::BinaryOp(
1357                BinaryOp::Mul,
1358                Box::new(index),
1359                Box::new(step),
1360            )),
1361            Box::new(start),
1362        ))
1363    }
1364
1365    /// `MAKEARRAY(R, C, LAMBDA(r, c, body))`: the body over R × C with `r`
1366    /// and `c` the item positions along R and C.
1367    fn parse_makearray(&mut self) -> Result<Expr, ParseError> {
1368        self.bump(); // MAKEARRAY
1369        self.expect("(", "'(' after MAKEARRAY")?;
1370        let r = self.parse_category_name()?;
1371        self.expect(",", "',' after the row category")?;
1372        let c = self.parse_category_name()?;
1373        self.expect(",", "',' then LAMBDA(r, c, body)")?;
1374        if !self.peek_kw("lambda") {
1375            return Err(self.err("MAKEARRAY needs LAMBDA(r, c, body)"));
1376        }
1377        let (params, start, end) = self.parse_lambda_params()?;
1378        self.expect(")", "')' to close MAKEARRAY")?;
1379        self.lambda_body(
1380            &params,
1381            vec![Self::position(r), Self::position(c)],
1382            start,
1383            end,
1384        )
1385    }
1386
1387    /// `BYROW(x OVER C, LAMBDA(row, body))` (and `BYCOL`): the body once
1388    /// per item of x's other categories, with `row` the values along C,
1389    /// which the body must reduce (`SUM(row)`, `MAX(row) - MIN(row)`, …).
1390    /// In a dimensional model a row and a column differ only in which
1391    /// category is named, so BYCOL is the same form.
1392    fn parse_byrow(&mut self, name: &str) -> Result<Expr, ParseError> {
1393        let name = name.to_ascii_uppercase();
1394        self.bump(); // BYROW / BYCOL
1395        self.expect("(", "'(' after the function name")?;
1396        let x = self.parse_measure_ref()?;
1397        if !self.peek_kw("over") {
1398            return Err(self.err(format!("{name} needs x OVER Category (the row's category)")));
1399        }
1400        self.bump();
1401        let cat = self.parse_category_name()?;
1402        let row = match x {
1403            Expr::Ref(id, mut spec) => {
1404                spec.over = vec![cat];
1405                Expr::Ref(id, spec)
1406            }
1407            _ => return Err(self.err(format!("{name} takes a measure reference"))),
1408        };
1409        self.expect(",", "',' then LAMBDA(row, body)")?;
1410        if !self.peek_kw("lambda") {
1411            return Err(self.err(format!("{name} needs LAMBDA(row, body)")));
1412        }
1413        let (params, start, end) = self.parse_lambda_params()?;
1414        self.expect(")", "')' to close the call")?;
1415        let body = self.lambda_body(&params, vec![row], start, end)?;
1416        fn bare(e: &Expr) -> bool {
1417            match e {
1418                Expr::Ref(_, s) => !s.over.is_empty(),
1419                Expr::Call(f, _) if functions::by_id(*f).is_some_and(|d| d.kind.reduces()) => false,
1420                Expr::Call(_, a) => a.iter().any(bare),
1421                Expr::UnaryOp(_, a) => bare(a),
1422                Expr::BinaryOp(_, a, b) => bare(a) || bare(b),
1423                _ => false,
1424            }
1425        }
1426        if bare(&body) {
1427            return Err(self.err(format!(
1428                "{name}'s LAMBDA must reduce its row to one value (SUM(row), MAX(row), …)"
1429            )));
1430        }
1431        Ok(body)
1432    }
1433
1434    /// `REDUCE(init, x OVER T, LAMBDA(acc, v, body))` and `SCAN(…)`: a fold
1435    /// along T in item order. Stored as `Call(id, [init, x OVER T, body])`
1436    /// with `acc` and `v` as placeholder references (see
1437    /// `functions::lambda_slot`).
1438    fn parse_fold(&mut self, name: &str) -> Result<Expr, ParseError> {
1439        let def = functions::callable(name).expect("REDUCE/SCAN are registered");
1440        self.bump(); // name
1441        self.expect("(", "'(' after the function name")?;
1442        let init = self.parse_expr()?;
1443        self.expect(",", "',' after the initial value")?;
1444        let x = self.parse_measure_ref()?;
1445        if !self.peek_kw("over") {
1446            return Err(self.err(format!(
1447                "{} folds a measure: write x OVER Category",
1448                def.name
1449            )));
1450        }
1451        self.bump();
1452        let cat = self.parse_category_name()?;
1453        let x = match x {
1454            Expr::Ref(id, mut spec) => {
1455                spec.over = vec![cat];
1456                Expr::Ref(id, spec)
1457            }
1458            _ => return Err(self.err(format!("{} folds a measure reference", def.name))),
1459        };
1460        self.expect(",", "',' then a LAMBDA(accumulator, value, body)")?;
1461        if !self.peek_kw("lambda") {
1462            return Err(self.err(format!(
1463                "{} needs LAMBDA(accumulator, value, body)",
1464                def.name
1465            )));
1466        }
1467        let (params, start, end) = self.parse_lambda_params()?;
1468        self.expect(")", "')' to close the fold")?;
1469        if params.len() != 2 {
1470            return Err(self.err(format!(
1471                "{}'s LAMBDA takes (accumulator, value), got {} parameter(s)",
1472                def.name,
1473                params.len()
1474            )));
1475        }
1476        let k = self.lambdas;
1477        self.lambdas += 1;
1478        let slot = |i| Expr::Ref(functions::lambda_slot(k, i), DimensionSpec::default());
1479        let body = self.lambda_body(&params, vec![slot(0), slot(1)], start, end)?;
1480        Ok(Expr::Call(def.id, vec![init, x, body]))
1481    }
1482
1483    /// `FIXPOINT(init, LAMBDA(r, body), [max_iter], [tolerance])`: stored as
1484    /// `Call(id, [init, body, max_iter?, tolerance?])` with `r` the
1485    /// placeholder `functions::lambda_slot(k, 0)`. `k` is taken before
1486    /// `init` is read, so a FIXPOINT that is the whole formula has k = 0.
1487    fn parse_fixpoint(&mut self) -> Result<Expr, ParseError> {
1488        let def = functions::callable("FIXPOINT").expect("FIXPOINT is registered");
1489        self.bump(); // FIXPOINT
1490        self.expect("(", "'(' after FIXPOINT")?;
1491        let k = self.lambdas;
1492        self.lambdas += 1;
1493        let init = self.parse_expr()?;
1494        self.expect(",", "',' then LAMBDA(r, body)")?;
1495        if !self.peek_kw("lambda") {
1496            return Err(self.err("FIXPOINT needs LAMBDA(r, body)"));
1497        }
1498        let (params, start, end) = self.parse_lambda_params()?;
1499        if params.len() != 1 {
1500            return Err(self.err(format!(
1501                "FIXPOINT's LAMBDA takes one parameter (the iterate), got {}",
1502                params.len()
1503            )));
1504        }
1505        let r = Expr::Ref(functions::lambda_slot(k, 0), DimensionSpec::default());
1506        let body = self.lambda_body(&params, vec![r], start, end)?;
1507        let mut args = vec![init, body];
1508        while args.len() < 4 && self.eat_op(",") {
1509            args.push(self.parse_expr()?);
1510        }
1511        self.expect(")", "')' to close FIXPOINT")?;
1512        Ok(Expr::Call(def.id, args))
1513    }
1514
1515    /// MeasureRef = Name [ "[" DimList "]" ]
1516    fn parse_measure_ref(&mut self) -> Result<Expr, ParseError> {
1517        let (name, pos) = self.parse_name("a measure name")?;
1518        let id = self.measure_id(&name, pos)?;
1519        let mut spec = DimensionSpec::default();
1520        if self.eat_op("[") {
1521            spec.by.push(self.parse_category_name()?);
1522            while self.eat_op(",") {
1523                spec.by.push(self.parse_category_name()?);
1524            }
1525            if !self.eat_op("]") {
1526                return Err(self.err("expected ']' to close dimension list"));
1527            }
1528        }
1529        Ok(Expr::Ref(id, spec))
1530    }
1531
1532    fn parse_category_name(&mut self) -> Result<CategoryId, ParseError> {
1533        let (name, pos) = self.parse_name("a category name")?;
1534        self.category_id(&name, pos)
1535    }
1536}
1537
1538// ---------------------------------------------------------------------------
1539// Public API
1540// ---------------------------------------------------------------------------
1541
1542/// Parse a full `Target = Expression` formula. The LHS identifier is the target
1543/// measure name (returned as-is; it need not exist yet). The RHS is resolved
1544/// against `model`.
1545pub fn parse_formula(model: &Model, text: &str) -> Result<FormulaText, ParseError> {
1546    let toks = tokenize(text)?;
1547    let (target, rhs_start) = parse_lhs(&toks)?;
1548    let mut p = Parser {
1549        toks: &toks[rhs_start..],
1550        pos: 0,
1551        model,
1552        depth: 0,
1553        scope: Vec::new(),
1554        lambdas: 0,
1555    };
1556    let expr = p.parse_expr()?;
1557    if p.peek().is_some() {
1558        return Err(p.err("unexpected trailing input"));
1559    }
1560    Ok(FormulaText {
1561        target,
1562        formula: Formula::new(expr),
1563    })
1564}
1565
1566/// Locate the target name (LHS) and the index of the first RHS token (past
1567/// `=`), skipping an optional bracketed target dimension list. Shared by
1568/// [`parse_formula`] and [`parse_definition`].
1569fn parse_lhs(toks: &[Spanned]) -> Result<(Name, usize), ParseError> {
1570    if toks.is_empty() {
1571        return Err(ParseError::new("empty formula", None));
1572    }
1573    let target = match &toks[0].tok {
1574        // Bare or quoted: `'Unit Price' = ...` names a CSV-derived measure.
1575        Tok::Ident(w) | Tok::QIdent(w) => Name(w.clone()),
1576        _ => {
1577            return Err(ParseError::new(
1578                "formula must start with a target measure name",
1579                Some(toks[0].pos),
1580            ))
1581        }
1582    };
1583    let mut idx = 1;
1584    if matches!(toks.get(idx).map(|s| &s.tok), Some(Tok::Op(o)) if o == "[") {
1585        idx += 1;
1586        while !matches!(toks.get(idx).map(|s| &s.tok), Some(Tok::Op(o)) if o == "]") {
1587            if toks.get(idx).is_none() {
1588                return Err(ParseError::new(
1589                    "unterminated '[' in target dimension list",
1590                    Some(toks[0].pos),
1591                ));
1592            }
1593            idx += 1;
1594        }
1595        idx += 1; // past ']'
1596    }
1597    if !matches!(toks.get(idx).map(|s| &s.tok), Some(Tok::Op(o)) if o == "=") {
1598        return Err(ParseError::new(
1599            "expected '=' after target measure name",
1600            toks.get(idx).map(|s| s.pos),
1601        ));
1602    }
1603    Ok((target, idx + 1))
1604}
1605
1606/// Parse a measure definition: an ordinary `Target = <expr>` formula, or a
1607/// source form `Target = SQL("...")` / `Target = CALL(fn, m1, m2, ...)`. The
1608/// source forms are recognized only as the *entire* RHS (they are measure
1609/// sources, not sub-expressions), keeping them out of the engine's expression
1610/// grammar and thus off the deterministic hot path.
1611pub fn parse_definition(model: &Model, text: &str) -> Result<Definition, ParseError> {
1612    let toks = tokenize(text)?;
1613    let (target, rhs) = parse_lhs(&toks)?;
1614    // Is the RHS exactly `IDENT ( ... )` where IDENT is SQL/CALL?
1615    if let Some(Tok::Ident(head)) = toks.get(rhs).map(|s| &s.tok) {
1616        let is_open = matches!(toks.get(rhs + 1).map(|s| &s.tok), Some(Tok::Op(o)) if o == "(");
1617        if is_open && head.eq_ignore_ascii_case("sql") {
1618            return parse_sql_form(&toks, rhs, target);
1619        }
1620        if is_open && head.eq_ignore_ascii_case("call") {
1621            return parse_call_form(&toks, rhs, target);
1622        }
1623        if is_open && head.eq_ignore_ascii_case("lambda") {
1624            return parse_lambda_form(model, &toks, rhs, target);
1625        }
1626    }
1627    // Fall through to an ordinary formula expression.
1628    let mut p = Parser {
1629        toks: &toks[rhs..],
1630        pos: 0,
1631        model,
1632        depth: 0,
1633        scope: Vec::new(),
1634        lambdas: 0,
1635    };
1636    let expr = p.parse_expr()?;
1637    if p.peek().is_some() {
1638        return Err(p.err("unexpected trailing input"));
1639    }
1640    Ok(Definition::Formula(FormulaText {
1641        target,
1642        formula: Formula::new(expr),
1643    }))
1644}
1645
1646/// `Name = LAMBDA(p, …, body)`: a named LAMBDA. The body may read only its
1647/// parameters (and literals, functions and earlier named LAMBDAs), so a
1648/// call depends on nothing but its arguments.
1649fn parse_lambda_form(
1650    model: &Model,
1651    toks: &[Spanned],
1652    rhs: usize,
1653    target: Name,
1654) -> Result<Definition, ParseError> {
1655    if functions::callable(&target.0).is_some() {
1656        return Err(ParseError::new(
1657            format!(
1658                "{} is a built-in function; name the LAMBDA differently",
1659                target.0
1660            ),
1661            None,
1662        ));
1663    }
1664    // The lambda being (re)defined is not visible in its own body.
1665    let mut m = model.clone();
1666    let own = m.lambda_by_name(&target.0).map(|(id, _)| id);
1667    if let Some(id) = own {
1668        m.lambdas.remove(&id);
1669    }
1670    let mut p = Parser {
1671        toks: &toks[rhs..],
1672        pos: 0,
1673        model: &m,
1674        depth: 0,
1675        scope: Vec::new(),
1676        lambdas: 0,
1677    };
1678    let (params, start, end) = p.parse_lambda_params()?;
1679    if p.peek().is_some() {
1680        return Err(p.err("unexpected input after the LAMBDA (a named LAMBDA is not called here)"));
1681    }
1682    let args = (0..params.len())
1683        .map(|i| Expr::Ref(functions::lambda_param(i), DimensionSpec::default()))
1684        .collect();
1685    let body = p.lambda_body(&params, args, start, end)?;
1686    let refs = body.referenced_measures();
1687    if refs
1688        .iter()
1689        .any(|r| functions::lambda_param_of(*r).is_none())
1690    {
1691        return Err(ParseError::new(
1692            "a named LAMBDA reads only its parameters: pass measures as arguments",
1693            None,
1694        ));
1695    }
1696    if refs.iter().any(|r| functions::lambda_slot_of(*r).is_some()) {
1697        return Err(ParseError::new(
1698            "a named LAMBDA cannot hold SCAN or REDUCE",
1699            None,
1700        ));
1701    }
1702    let lambda = crate::NamedLambda {
1703        name: target.clone(),
1704        params,
1705        body,
1706    };
1707    lambda.param_kinds().map_err(|e| ParseError::new(e, None))?;
1708    Ok(Definition::Lambda(LambdaText {
1709        name: target,
1710        lambda,
1711    }))
1712}
1713
1714/// `SQL("<query>")` — a single string literal argument.
1715fn parse_sql_form(toks: &[Spanned], rhs: usize, target: Name) -> Result<Definition, ParseError> {
1716    // toks[rhs] = SQL, [rhs+1] = '(', [rhs+2] = Str, [rhs+3] = ')'
1717    let query = match toks.get(rhs + 2).map(|s| &s.tok) {
1718        Some(Tok::Str(s)) => s.clone(),
1719        _ => {
1720            return Err(ParseError::new(
1721                "SQL(...) takes a single quoted query string",
1722                toks.get(rhs + 2).map(|s| s.pos),
1723            ))
1724        }
1725    };
1726    match toks.get(rhs + 3).map(|s| &s.tok) {
1727        Some(Tok::Op(o)) if o == ")" => {}
1728        _ => {
1729            return Err(ParseError::new(
1730                "expected ')' to close SQL(...)",
1731                toks.get(rhs + 3).map(|s| s.pos),
1732            ))
1733        }
1734    }
1735    if toks.get(rhs + 4).is_some() {
1736        return Err(ParseError::new(
1737            "unexpected trailing input after SQL(...)",
1738            toks.get(rhs + 4).map(|s| s.pos),
1739        ));
1740    }
1741    Ok(Definition::Sql { target, query })
1742}
1743
1744/// `CALL(func, arg_measure, ...)` — a function name then zero or more measure
1745/// name arguments.
1746fn parse_call_form(toks: &[Spanned], rhs: usize, target: Name) -> Result<Definition, ParseError> {
1747    let func = match toks.get(rhs + 2).map(|s| &s.tok) {
1748        Some(Tok::Ident(w)) => w.clone(),
1749        _ => {
1750            return Err(ParseError::new(
1751                "CALL(...) takes a function name then argument measures",
1752                toks.get(rhs + 2).map(|s| s.pos),
1753            ))
1754        }
1755    };
1756    // After the function name: optional `, arg, arg, ...` then `)`.
1757    let mut i = rhs + 3;
1758    let mut args = Vec::new();
1759    loop {
1760        match toks.get(i).map(|s| &s.tok) {
1761            Some(Tok::Op(o)) if o == ")" => {
1762                i += 1;
1763                break;
1764            }
1765            Some(Tok::Op(o)) if o == "," => {
1766                i += 1;
1767                match toks.get(i).map(|s| &s.tok) {
1768                    Some(Tok::Ident(w) | Tok::QIdent(w)) => {
1769                        args.push(Name(w.clone()));
1770                        i += 1;
1771                    }
1772                    _ => {
1773                        return Err(ParseError::new(
1774                            "expected an argument measure name after ',' in CALL(...)",
1775                            toks.get(i).map(|s| s.pos),
1776                        ))
1777                    }
1778                }
1779            }
1780            other => {
1781                return Err(ParseError::new(
1782                    if other.is_none() {
1783                        "unterminated CALL(...)"
1784                    } else {
1785                        "expected ',' or ')' in CALL(...)"
1786                    },
1787                    toks.get(i).map(|s| s.pos),
1788                ))
1789            }
1790        }
1791    }
1792    if toks.get(i).is_some() {
1793        return Err(ParseError::new(
1794            "unexpected trailing input after CALL(...)",
1795            toks.get(i).map(|s| s.pos),
1796        ));
1797    }
1798    Ok(Definition::Call { target, func, args })
1799}
1800
1801/// Parse just an expression (the RHS), with no target/assignment.
1802pub fn parse_expr(model: &Model, text: &str) -> Result<Formula, ParseError> {
1803    let toks = tokenize(text)?;
1804    if toks.is_empty() {
1805        return Err(ParseError::new("empty expression", None));
1806    }
1807    let mut p = Parser {
1808        toks: &toks,
1809        pos: 0,
1810        model,
1811        depth: 0,
1812        scope: Vec::new(),
1813        lambdas: 0,
1814    };
1815    let expr = p.parse_expr()?;
1816    if p.peek().is_some() {
1817        return Err(p.err("unexpected trailing input"));
1818    }
1819    Ok(Formula::new(expr))
1820}
1821
1822// ---------------------------------------------------------------------------
1823// Tests
1824// ---------------------------------------------------------------------------
1825
1826#[cfg(test)]
1827#[cfg_attr(coverage_nightly, coverage(off))]
1828mod tests {
1829    use super::*;
1830    use crate::{Measure, MeasureKind, Name, ValueType};
1831
1832    const TIME: CategoryId = CategoryId(1);
1833    const PRODUCT: CategoryId = CategoryId(2);
1834    const PRICE: MeasureId = MeasureId(100);
1835    const QUANTITY: MeasureId = MeasureId(101);
1836    const REVENUE: MeasureId = MeasureId(102);
1837    const COST: MeasureId = MeasureId(103);
1838
1839    fn fixture() -> Model {
1840        let mut m = Model::new();
1841        m.add_category(TIME, "Time");
1842        m.add_category(PRODUCT, "Product");
1843        for (id, name, cats, vt) in [
1844            (PRICE, "Price", vec![PRODUCT], ValueType::Number),
1845            (QUANTITY, "Quantity", vec![TIME, PRODUCT], ValueType::Number),
1846            (REVENUE, "Revenue", vec![TIME, PRODUCT], ValueType::Number),
1847            (COST, "Cost", vec![TIME, PRODUCT], ValueType::Number),
1848        ] {
1849            m.add_measure(Measure {
1850                id,
1851                name: Name(name.into()),
1852                value_type: vt,
1853                categories: cats,
1854                kind: MeasureKind::Input,
1855                description: None,
1856            });
1857        }
1858        m
1859    }
1860
1861    fn refr(id: MeasureId) -> Expr {
1862        Expr::Ref(id, DimensionSpec::default())
1863    }
1864
1865    /// Every form of the OVER clause, category arguments, `@` and dotted
1866    /// names, and the errors each can raise.
1867    #[test]
1868    fn over_clauses_category_args_and_their_errors() {
1869        let m = fixture();
1870        let p = |s: &str| parse_expr(&m, s).map(|f| f.expr);
1871        let call = |f: u32, a: Vec<Expr>| Expr::Call(FuncId(f), a);
1872        let over = |id, cats: Vec<CategoryId>| {
1873            Expr::Ref(
1874                id,
1875                DimensionSpec {
1876                    over: cats,
1877                    ..Default::default()
1878                },
1879            )
1880        };
1881        // Several categories; a comma then a non-category is the next argument.
1882        assert_eq!(
1883            p("SUM(Quantity OVER Time, Product)"),
1884            Ok(call(1, vec![over(QUANTITY, vec![TIME, PRODUCT])]))
1885        );
1886        assert_eq!(
1887            p("TEXTJOIN(\"-\", TRUE, Quantity OVER Time)"),
1888            Ok(call(
1889                909,
1890                vec![
1891                    Expr::Literal(Value::Text("-".into())),
1892                    Expr::Literal(Value::Boolean(true)),
1893                    over(QUANTITY, vec![TIME])
1894                ]
1895            ))
1896        );
1897        // ALL, ALL EXCEPT (resolved from the measure's categories).
1898        assert_eq!(
1899            p("SUM(Quantity OVER ALL)"),
1900            Ok(call(1, vec![over(QUANTITY, vec![TIME, PRODUCT])]))
1901        );
1902        assert_eq!(
1903            p("SUM(Quantity OVER ALL EXCEPT Product)"),
1904            Ok(call(1, vec![over(QUANTITY, vec![TIME])]))
1905        );
1906        assert_eq!(
1907            p("SUM(Quantity OVER ALL EXCEPT Product, Time)"),
1908            Ok(call(1, vec![over(QUANTITY, vec![])]))
1909        );
1910        // WHERE and windows become trailing markers.
1911        let w = p("SUM(Quantity OVER Time WHERE Quantity > 1)").unwrap();
1912        assert!(
1913            matches!(&w, Expr::Call(_, a) if matches!(&a[1], Expr::Call(id, _) if *id == functions::WHERE))
1914        );
1915        for (src, from, to) in [
1916            (
1917                "SUM(Quantity OVER Time[FIRST..THIS])",
1918                Value::Text("FIRST".into()),
1919                Value::Number(0.0),
1920            ),
1921            (
1922                "SUM(Quantity OVER Time[THIS-2..THIS+1])",
1923                Value::Number(-2.0),
1924                Value::Number(1.0),
1925            ),
1926            (
1927                "SUM(Quantity OVER Time[THIS..LAST])",
1928                Value::Number(0.0),
1929                Value::Text("LAST".into()),
1930            ),
1931        ] {
1932            let e = p(src).unwrap();
1933            let Expr::Call(_, a) = e else { panic!() };
1934            assert_eq!(
1935                a[1],
1936                Expr::Call(
1937                    functions::WINDOW,
1938                    vec![Expr::Literal(from), Expr::Literal(to)]
1939                ),
1940                "{src}"
1941            );
1942        }
1943        // OVER after the second reduced argument applies to both (CORREL-like
1944        // shapes are exercised by the engine; TEXTJOIN reduces arg 2 only).
1945        // Category arguments, `@` names, dotted names.
1946        assert_eq!(
1947            p("POSITION(Time)"),
1948            Ok(call(900, vec![Expr::Category(TIME)]))
1949        );
1950        assert_eq!(
1951            p("@ITEM(Product)"),
1952            Ok(call(901, vec![Expr::Category(PRODUCT)]))
1953        );
1954        assert_eq!(
1955            p("@itemcount(Time)"),
1956            Ok(call(903, vec![Expr::Category(TIME)]))
1957        );
1958        assert_eq!(
1959            p("PREV(Quantity, Time, 2, 0)"),
1960            Ok(call(
1961                904,
1962                vec![
1963                    refr(QUANTITY),
1964                    Expr::Category(TIME),
1965                    Expr::Literal(Value::Number(2.0)),
1966                    Expr::Literal(Value::Number(0.0))
1967                ]
1968            ))
1969        );
1970        assert_eq!(p("TRUE()"), Ok(call(39, vec![])));
1971        assert_eq!(p("ISO.CEILING(Price)"), Ok(call(210, vec![refr(PRICE)])));
1972        // Dotted names whose prefixes are not functions, and digit parts.
1973        let joined = |src: &str| tokenize(src).unwrap().first().map(|t| t.tok.clone());
1974        assert_eq!(
1975            joined("NORM.S.DIST(1)"),
1976            Some(Tok::Ident("NORM.S.DIST".into()))
1977        );
1978        assert_eq!(
1979            joined("T.DIST.2T(1, 2)"),
1980            Some(Tok::Ident("T.DIST.2T".into()))
1981        );
1982        assert_eq!(
1983            joined("FLOOR.MATH (1)"),
1984            Some(Tok::Ident("FLOOR.MATH".into()))
1985        );
1986        assert_eq!(joined("Sheet.Price"), Some(Tok::Ident("Sheet".into())));
1987        // Errors.
1988        for (src, want) in [
1989            ("SUM(Quantity OVER Time OVER Product)", "expected ')'"),
1990            (
1991                "SUM(Quantity OVER Time, Quantity OVER Product)",
1992                "only one OVER",
1993            ),
1994            ("SUM(Quantity OVER Time[FIRST.THIS])", "'..'"),
1995            ("SUM(Quantity OVER Time[FIRST..THIS)", "']'"),
1996            ("SUM(Quantity OVER Time[NOW..THIS])", "FIRST, LAST or THIS"),
1997            ("SUM(Quantity OVER Time[1..THIS])", "FIRST, LAST or THIS"),
1998            ("SUM(Quantity OVER Time[THIS+x..THIS])", "whole number"),
1999            ("SUM(Quantity OVER Time[THIS-1.5..THIS])", "whole number"),
2000            ("ABS(Price OVER Time)", "does not aggregate"),
2001            ("SUM(Price * 2 OVER Time)", "measure reference"),
2002            ("POSITION(Price)", "unknown category"),
2003            ("SUM(Quantity OVER Nope)", "unknown category"),
2004            ("SUM(Quantity OVER ALL EXCEPT Nope)", "unknown category"),
2005            ("TEXTJOIN(\"-\", Quantity OVER Time)", "takes"),
2006        ] {
2007            let e = p(src).unwrap_err();
2008            assert!(e.message.contains(want), "{src}: {}", e.message);
2009        }
2010        // `@` alone, or before a non-letter, is not a name.
2011        assert!(p("@ + 1").is_err());
2012        assert!(p("@1").is_err());
2013        // A dotted measure-like name that is not a function stays an error.
2014        assert!(p("FLOOR.NOPE(Price)").is_err());
2015        // `ALL(` is a function-looking word, not OVER ALL.
2016        assert!(p("SUM(Quantity OVER ALL(Time))").is_err());
2017    }
2018
2019    /// LET and LAMBDA expand in place; their errors name the problem.
2020    /// A named LAMBDA's parameter used as a category (`OVER c`,
2021    /// `POSITION(c)`) takes a category name at the call; on expansion the
2022    /// reduced argument carries the `OVER` and the category maps through,
2023    /// also through another lambda.
2024    #[test]
2025    fn named_lambda_category_parameters() {
2026        use crate::LambdaParam;
2027        let mut m = fixture();
2028        let def = |m: &Model, t: &str| match parse_definition(m, t) {
2029            Ok(Definition::Lambda(l)) => l.lambda,
2030            other => panic!("{t}: {other:?}"),
2031        };
2032        let p = |m: &Model, t: &str| parse_expr(m, t).unwrap().expr;
2033        let l = def(&m, "LOGNORM = LAMBDA(x, c, LN1P(x) / LN1P(MAX(x OVER c)))");
2034        assert_eq!(
2035            l.param_kinds(),
2036            Ok(vec![LambdaParam::Reduced, LambdaParam::Category])
2037        );
2038        m.lambdas.insert(1, l);
2039        let call = p(&m, "LOGNORM(Quantity, Time)");
2040        assert_eq!(
2041            call,
2042            Expr::Call(
2043                functions::named_lambda(1),
2044                vec![refr(QUANTITY), Expr::Category(TIME)]
2045            )
2046        );
2047        assert_eq!(
2048            m.expand_lambdas(&call),
2049            Ok(p(&m, "LN1P(Quantity) / LN1P(MAX(Quantity OVER Time))"))
2050        );
2051        let at = def(&m, "AT = LAMBDA(v, k, LOGNORM(v, k) * POSITION(k))");
2052        assert_eq!(
2053            at.param_kinds(),
2054            Ok(vec![LambdaParam::Value, LambdaParam::Category])
2055        );
2056        m.lambdas.insert(2, at);
2057        assert_eq!(
2058            m.expand_lambdas(&p(&m, "AT(Quantity, Product)")),
2059            Ok(p(
2060                &m,
2061                "LN1P(Quantity) / LN1P(MAX(Quantity OVER Product)) * POSITION(Product)"
2062            ))
2063        );
2064        for (text, why) in [
2065            ("LOGNORM(Quantity * 2, Time)", "pass a measure"),
2066            ("LOGNORM(Quantity, Nope)", "unknown category: Nope"),
2067            ("LOGNORM(Quantity)", "takes 2"),
2068        ] {
2069            let e = parse_expr(&m, text).unwrap_err();
2070            assert!(e.message.contains(why), "{text}: {}", e.message);
2071        }
2072        let e = parse_definition(&m, "Bad = LAMBDA(x, c, c + SUM(x OVER c))").unwrap_err();
2073        assert!(e.message.contains("both as a value and as a category"));
2074        // Hand-built calls with the wrong kind of argument fail to expand.
2075        let call = |args| Expr::Call(functions::named_lambda(1), args);
2076        let e = m.expand_lambdas(&call(vec![refr(QUANTITY), refr(PRICE)]));
2077        assert!(e
2078            .unwrap_err()
2079            .contains("LOGNORM: argument 2 must be a category"));
2080        let one = Expr::Literal(Value::Number(1.0));
2081        let e = m.expand_lambdas(&call(vec![one, Expr::Category(TIME)]));
2082        assert!(e.unwrap_err().contains("must be a measure"));
2083    }
2084
2085    #[test]
2086    fn let_and_lambda() {
2087        let m = fixture();
2088        let p = |s: &str| parse_expr(&m, s).map(|f| f.expr);
2089        assert_eq!(p("LET(x, Price, x + x)"), p("Price + Price"));
2090        assert_eq!(p("LET(x, Price, y, x * 2, y - x)"), p("Price * 2 - Price"));
2091        // A local shadows a measure; it ends with its LET.
2092        assert_eq!(p("LET(Price, 3, Price) + Price"), p("3 + Price"));
2093        assert_eq!(
2094            p("LAMBDA(a, a * a)(Price + 1)"),
2095            p("(Price + 1) * (Price + 1)")
2096        );
2097        assert_eq!(
2098            p("MAP(Price, Cost, LAMBDA(a, b, a - b))"),
2099            p("Price - Cost")
2100        );
2101        let fold = p("REDUCE(0, Quantity OVER Time, LAMBDA(acc, v, acc + v))").unwrap();
2102        let Expr::Call(_, args) = fold else { panic!() };
2103        assert_eq!(
2104            args[2],
2105            Expr::BinaryOp(
2106                BinaryOp::Add,
2107                Box::new(Expr::Ref(
2108                    functions::lambda_slot(0, 0),
2109                    DimensionSpec::default()
2110                )),
2111                Box::new(Expr::Ref(
2112                    functions::lambda_slot(0, 1),
2113                    DimensionSpec::default()
2114                )),
2115            )
2116        );
2117        assert_eq!(
2118            functions::lambda_slot_of(functions::lambda_slot(3, 1)),
2119            Some((3, 1))
2120        );
2121        assert_eq!(functions::lambda_slot_of(MeasureId(5)), None);
2122        // FIXPOINT: r is the first placeholder, the options stay as written.
2123        let r = Expr::Ref(functions::lambda_slot(0, 0), DimensionSpec::default());
2124        let fix = functions::callable("FIXPOINT").unwrap().id;
2125        assert_eq!(
2126            p("FIXPOINT(Price, LAMBDA(r, r / 2 + 1), 50)"),
2127            Ok(Expr::Call(
2128                fix,
2129                vec![
2130                    refr(PRICE),
2131                    Expr::BinaryOp(
2132                        BinaryOp::Add,
2133                        Box::new(Expr::BinaryOp(
2134                            BinaryOp::Div,
2135                            Box::new(r.clone()),
2136                            Box::new(Expr::Literal(Value::Number(2.0)))
2137                        )),
2138                        Box::new(Expr::Literal(Value::Number(1.0)))
2139                    ),
2140                    Expr::Literal(Value::Number(50.0)),
2141                ]
2142            ))
2143        );
2144        assert_eq!(
2145            p("FIXPOINT(1, LAMBDA(x, x))"),
2146            Ok(Expr::Call(fix, vec![Expr::Literal(Value::Number(1.0)), r]))
2147        );
2148        for (src, want) in [
2149            ("LET(x, Price)", "expected"),
2150            ("LET(1, Price, 2)", "a name to bind"),
2151            ("LET(x Price, x)", "',' after a LET name"),
2152            ("LET(x, Price, x", "')' to close LET"),
2153            ("LAMBDA(x, x)", "called on the spot"),
2154            ("LAMBDA(x)(1)", "at least one parameter"),
2155            ("LAMBDA(x, x + 1)(1, 2)", "takes 1 argument"),
2156            ("LAMBDA(x, x +)(1)", ""),
2157            ("LAMBDA(x, x", "')' to close LAMBDA"),
2158            ("MAP(Price LAMBDA(v, v))", "',' then a LAMBDA"),
2159            ("MAP(Price, LAMBDA(v, v)", "')' to close MAP"),
2160            ("SCAN(0, Quantity, LAMBDA(a, v, a))", "OVER"),
2161            ("SCAN(0, Quantity OVER Time, 1)", "needs LAMBDA"),
2162            (
2163                "SCAN(0, Quantity OVER Time LAMBDA(a, v, a))",
2164                "',' then a LAMBDA",
2165            ),
2166            (
2167                "SCAN(0, Quantity OVER Time, LAMBDA(v, v))",
2168                "takes (accumulator, value)",
2169            ),
2170            (
2171                "SCAN(0 Quantity OVER Time, LAMBDA(a, v, a))",
2172                "',' after the initial",
2173            ),
2174            (
2175                "SCAN(0, Quantity OVER Time, LAMBDA(a, v, a)",
2176                "')' to close the fold",
2177            ),
2178            ("SCAN 1", "unknown measure"),
2179            ("FIXPOINT(0, 1)", "needs LAMBDA"),
2180            ("FIXPOINT(0 LAMBDA(r, r))", "',' then LAMBDA"),
2181            ("FIXPOINT(0, LAMBDA(a, b, a))", "one parameter"),
2182            (
2183                "FIXPOINT(0, LAMBDA(r, r), 5, 0, 1)",
2184                "')' to close FIXPOINT",
2185            ),
2186            ("LET 1", "unknown measure"),
2187        ] {
2188            let e = p(src).unwrap_err();
2189            assert!(e.message.contains(want), "{src}: {}", e.message);
2190        }
2191    }
2192
2193    /// `NAME(` that is no function or measure: an "unknown function" with
2194    /// the nearest names, at the name.
2195    #[test]
2196    fn unknown_function_suggests() {
2197        let m = fixture();
2198        let e = parse_expr(&m, "1 + PMTT(1, 2, 3)").unwrap_err();
2199        assert!(
2200            e.message
2201                .starts_with("unknown function: PMTT; did you mean PMT"),
2202            "{}",
2203            e.message
2204        );
2205        assert_eq!(e.position, Some(4));
2206        let e = parse_expr(&m, "Qqqqqqqqqq(1)").unwrap_err();
2207        assert_eq!(e.message, "unknown function: Qqqqqqqqqq");
2208    }
2209
2210    #[test]
2211    fn parses_assignment_and_multiplication() {
2212        let m = fixture();
2213        let f = parse_formula(&m, "Revenue = Price * Quantity").unwrap();
2214        assert_eq!(f.target, Name("Revenue".into()));
2215        assert_eq!(
2216            f.formula.expr,
2217            Expr::BinaryOp(
2218                BinaryOp::Mul,
2219                Box::new(refr(PRICE)),
2220                Box::new(refr(QUANTITY))
2221            )
2222        );
2223    }
2224
2225    #[test]
2226    fn precedence_mul_over_add() {
2227        // a + b * c == a + (b*c)
2228        let m = fixture();
2229        let f = parse_expr(&m, "Price + Quantity * Revenue").unwrap();
2230        assert_eq!(
2231            f.expr,
2232            Expr::BinaryOp(
2233                BinaryOp::Add,
2234                Box::new(refr(PRICE)),
2235                Box::new(Expr::BinaryOp(
2236                    BinaryOp::Mul,
2237                    Box::new(refr(QUANTITY)),
2238                    Box::new(refr(REVENUE)),
2239                )),
2240            )
2241        );
2242    }
2243
2244    #[test]
2245    fn parens_group() {
2246        // (a + b) * c
2247        let m = fixture();
2248        let f = parse_expr(&m, "(Price + Quantity) * Revenue").unwrap();
2249        assert_eq!(
2250            f.expr,
2251            Expr::BinaryOp(
2252                BinaryOp::Mul,
2253                Box::new(Expr::BinaryOp(
2254                    BinaryOp::Add,
2255                    Box::new(refr(PRICE)),
2256                    Box::new(refr(QUANTITY)),
2257                )),
2258                Box::new(refr(REVENUE)),
2259            )
2260        );
2261    }
2262
2263    #[test]
2264    fn subtraction() {
2265        let m = fixture();
2266        let f = parse_formula(&m, "Profit = Revenue - Cost").unwrap();
2267        assert_eq!(f.target, Name("Profit".into()));
2268        assert_eq!(
2269            f.formula.expr,
2270            Expr::BinaryOp(BinaryOp::Sub, Box::new(refr(REVENUE)), Box::new(refr(COST)))
2271        );
2272    }
2273
2274    #[test]
2275    fn aggregation_sum_over_time() {
2276        // TotalRevenue[Product] = SUM(Revenue OVER Time)
2277        let m = fixture();
2278        let f = parse_formula(&m, "TotalRevenue[Product] = SUM(Revenue OVER Time)").unwrap();
2279        assert_eq!(f.target, Name("TotalRevenue".into()));
2280        let expected = Expr::Call(
2281            FUNC_SUM,
2282            vec![Expr::Ref(
2283                REVENUE,
2284                DimensionSpec {
2285                    over: vec![TIME],
2286                    by: vec![],
2287                    except: vec![],
2288                },
2289            )],
2290        );
2291        assert_eq!(f.formula.expr, expected);
2292    }
2293
2294    #[test]
2295    fn dim_list_sets_by() {
2296        let m = fixture();
2297        let f = parse_expr(&m, "Quantity[Time, Product]").unwrap();
2298        assert_eq!(
2299            f.expr,
2300            Expr::Ref(
2301                QUANTITY,
2302                DimensionSpec {
2303                    by: vec![TIME, PRODUCT],
2304                    over: vec![],
2305                    except: vec![],
2306                }
2307            )
2308        );
2309    }
2310
2311    /// The mathematical convention for `^`: right-associative and tighter
2312    /// than unary minus. `%` binds tightest; `&` sits between `+ -` and the
2313    /// comparisons.
2314    #[test]
2315    fn power_concat_and_percent_precedence() {
2316        let m = fixture();
2317        let p = |s: &str| parse_expr(&m, s).unwrap().expr;
2318        let num = |n: f64| Expr::Literal(Value::Number(n));
2319        let pow = |a, b| Expr::BinaryOp(BinaryOp::Pow, Box::new(a), Box::new(b));
2320        let neg = |a| Expr::UnaryOp(UnaryOp::Neg, Box::new(a));
2321        assert_eq!(p("-2^2"), neg(pow(num(2.0), num(2.0))));
2322        assert_eq!(p("2^3^2"), pow(num(2.0), pow(num(3.0), num(2.0))));
2323        assert_eq!(p("2^-1"), pow(num(2.0), neg(num(1.0))));
2324        assert_eq!(p("(-2)^2"), pow(neg(num(2.0)), num(2.0)));
2325        assert_eq!(p("2*3^2"), parse_expr(&m, "2*(3^2)").unwrap().expr);
2326        assert_eq!(p("50%"), num(0.5));
2327        assert_eq!(p("Price%"), parse_expr(&m, "Price / 100").unwrap().expr);
2328        assert_eq!(p("2^50%"), pow(num(2.0), num(0.5)));
2329        assert_eq!(
2330            p("Price + 1 & \"x\""),
2331            parse_expr(&m, "(Price + 1) & \"x\"").unwrap().expr
2332        );
2333        assert_eq!(
2334            p("\"a\" & \"b\" == \"ab\""),
2335            parse_expr(&m, "(\"a\" & \"b\") == \"ab\"").unwrap().expr
2336        );
2337        assert_eq!(p("--Price"), neg(neg(refr(PRICE))));
2338        assert!(parse_expr(&m, &format!("{}1", "-".repeat(10_000))).is_err());
2339        assert!(parse_expr(&m, "2^").is_err());
2340    }
2341
2342    /// NOT binds looser than a comparison and tighter than AND, as in SQL:
2343    /// `NOT Price > 15` is `NOT (Price > 15)`, and `NOT a AND b` is
2344    /// `(NOT a) AND b`.
2345    #[test]
2346    fn not_binds_looser_than_comparison() {
2347        let m = fixture();
2348        assert_eq!(
2349            parse_expr(&m, "NOT Price > 15").unwrap(),
2350            parse_expr(&m, "NOT (Price > 15)").unwrap()
2351        );
2352        assert_eq!(
2353            parse_expr(&m, "NOT Price > 15 AND Price < 20").unwrap(),
2354            parse_expr(&m, "(NOT (Price > 15)) AND Price < 20").unwrap()
2355        );
2356        assert_eq!(
2357            parse_expr(&m, "NOT NOT Price > 15").unwrap(),
2358            parse_expr(&m, "NOT (NOT (Price > 15))").unwrap()
2359        );
2360        // NOT chains count toward the depth limit (the compiler recurses).
2361        let deep = format!("{}Price > 1", "NOT ".repeat(10_000));
2362        assert!(parse_expr(&m, &deep).is_err());
2363        let ok = format!("{}Price > 1", "NOT ".repeat(50));
2364        assert!(parse_expr(&m, &ok).is_ok());
2365    }
2366
2367    #[test]
2368    fn comparison_and_logical() {
2369        let m = fixture();
2370        // Price > 10
2371        let f = parse_expr(&m, "Price > 10").unwrap();
2372        assert_eq!(
2373            f.expr,
2374            Expr::BinaryOp(
2375                BinaryOp::Gt,
2376                Box::new(refr(PRICE)),
2377                Box::new(Expr::Literal(Value::Number(10.0))),
2378            )
2379        );
2380
2381        // NOT (Price > 10)
2382        let f = parse_expr(&m, "NOT (Price > 10)").unwrap();
2383        assert!(matches!(f.expr, Expr::UnaryOp(UnaryOp::Not, _)));
2384
2385        // Price > 10 AND Quantity < 5  -> And(Gt, Lt)
2386        let f = parse_expr(&m, "Price > 10 AND Quantity < 5").unwrap();
2387        assert!(matches!(f.expr, Expr::BinaryOp(BinaryOp::And, _, _)));
2388
2389        // Equality uses '==' inside expressions (not top-level '=').
2390        let f = parse_expr(&m, "Price == 10").unwrap();
2391        assert!(matches!(f.expr, Expr::BinaryOp(BinaryOp::Eq, _, _)));
2392        // '<>' and '!=' both map to Ne.
2393        assert!(matches!(
2394            parse_expr(&m, "Price <> 10").unwrap().expr,
2395            Expr::BinaryOp(BinaryOp::Ne, _, _)
2396        ));
2397        assert!(matches!(
2398            parse_expr(&m, "Price != 10").unwrap().expr,
2399            Expr::BinaryOp(BinaryOp::Ne, _, _)
2400        ));
2401    }
2402
2403    #[test]
2404    fn literals() {
2405        let m = fixture();
2406        assert_eq!(
2407            parse_expr(&m, "TRUE").unwrap().expr,
2408            Expr::Literal(Value::Boolean(true))
2409        );
2410        assert_eq!(
2411            parse_expr(&m, "false").unwrap().expr,
2412            Expr::Literal(Value::Boolean(false))
2413        );
2414        assert_eq!(
2415            parse_expr(&m, "2.5").unwrap().expr,
2416            Expr::Literal(Value::Number(2.5))
2417        );
2418        assert_eq!(
2419            parse_expr(&m, "\"hello\"").unwrap().expr,
2420            Expr::Literal(Value::Text("hello".into()))
2421        );
2422    }
2423
2424    #[test]
2425    fn unary_neg() {
2426        let m = fixture();
2427        let f = parse_expr(&m, "-Price").unwrap();
2428        assert_eq!(f.expr, Expr::UnaryOp(UnaryOp::Neg, Box::new(refr(PRICE))));
2429    }
2430
2431    #[test]
2432    fn errors_never_panic() {
2433        let m = fixture();
2434        // Unknown measure.
2435        assert!(parse_expr(&m, "Widgets * Price").is_err());
2436        // Unknown category in dim list.
2437        assert!(parse_expr(&m, "Price[Region]").is_err());
2438        // Trailing garbage.
2439        assert!(parse_expr(&m, "Price Quantity").is_err());
2440        // Unbalanced paren.
2441        assert!(parse_expr(&m, "(Price + Quantity").is_err());
2442        // Missing '=' at top level.
2443        assert!(parse_formula(&m, "Revenue Price").is_err());
2444        // Empty.
2445        assert!(parse_expr(&m, "").is_err());
2446        // Unterminated string.
2447        assert!(parse_expr(&m, "\"oops").is_err());
2448        // A dangling operator.
2449        assert!(parse_expr(&m, "Price ^").is_err());
2450        assert!(parse_expr(&m, "Price &").is_err());
2451    }
2452
2453    #[test]
2454    fn error_has_position_and_display() {
2455        let m = fixture();
2456        let e = parse_expr(&m, "Price + Widgets").unwrap_err();
2457        assert!(e.position.is_some());
2458        assert!(e.to_string().contains("unknown measure"));
2459    }
2460
2461    #[test]
2462    fn parses_named_scalar_call() {
2463        let m = fixture();
2464        // ABS(Price) -> Call(FuncId(10), [Ref(Price)]).
2465        let f = parse_expr(&m, "ABS(Price)").unwrap();
2466        assert_eq!(f.expr, Expr::Call(FuncId(10), vec![refr(PRICE)]));
2467
2468        // Two-arg MIN2, case-insensitive, args are full expressions.
2469        let f = parse_expr(&m, "min2(Price, Cost - Revenue)").unwrap();
2470        match f.expr {
2471            Expr::Call(FuncId(20), args) => {
2472                assert_eq!(args.len(), 2);
2473                assert_eq!(args[0], refr(PRICE));
2474            }
2475            other => panic!("expected MIN2 call, got {other:?}"),
2476        }
2477    }
2478
2479    #[test]
2480    fn scalar_call_wrong_arity_errors() {
2481        let m = fixture();
2482        assert!(parse_expr(&m, "ABS(Price, Cost)").is_err()); // ABS is arity 1
2483        assert!(parse_expr(&m, "MID(Price)").is_err()); // MID takes 3
2484        assert!(parse_expr(&m, "ROUND()").is_err()); // ROUND needs at least 1
2485        let e = parse_expr(&m, "ROUND(Price, 1, 2)").unwrap_err();
2486        assert!(
2487            e.message.contains("ROUND(number, [num_digits=0])"),
2488            "{}",
2489            e.message
2490        );
2491        // MIN2 is MIN's old name: variadic now, so one argument is fine.
2492        assert_eq!(
2493            parse_expr(&m, "MIN2(Price)").unwrap().expr,
2494            Expr::Call(FuncId(20), vec![refr(PRICE)])
2495        );
2496        // A scalar-only name with OVER, and an aggregation name without it.
2497        assert!(parse_expr(&m, "ABS(Price OVER Time)").is_err());
2498        assert_eq!(
2499            parse_expr(&m, "SUM(Price, Cost)").unwrap().expr,
2500            Expr::Call(FuncId(22), vec![refr(PRICE), refr(COST)])
2501        );
2502        // Optional arguments, aliases, zero-arg calls.
2503        assert_eq!(
2504            parse_expr(&m, "round(Price, 2)").unwrap().expr,
2505            Expr::Call(
2506                FuncId(11),
2507                vec![refr(PRICE), Expr::Literal(Value::Number(2.0))]
2508            )
2509        );
2510        assert_eq!(
2511            parse_expr(&m, "PI()").unwrap().expr,
2512            Expr::Call(FuncId(112), vec![])
2513        );
2514        assert_eq!(
2515            parse_expr(&m, "AVG(Price OVER Time)").unwrap().expr,
2516            parse_expr(&m, "AVERAGE(Price OVER Time)").unwrap().expr
2517        );
2518    }
2519
2520    #[test]
2521    fn scalar_name_without_paren_is_a_measure_ref() {
2522        // A bare name matching a scalar func but not followed by "(" is parsed
2523        // as a measure ref (and errors if unknown) — no false function parse.
2524        let m = fixture();
2525        assert!(parse_expr(&m, "ABS").is_err()); // unknown measure "ABS"
2526    }
2527
2528    #[test]
2529    fn definition_falls_through_to_ordinary_formula() {
2530        let m = fixture();
2531        match parse_definition(&m, "Revenue = Price * Quantity").unwrap() {
2532            Definition::Formula(ft) => {
2533                assert_eq!(ft.target, Name("Revenue".into()));
2534            }
2535            other => panic!("expected Formula, got {other:?}"),
2536        }
2537    }
2538
2539    #[test]
2540    fn parses_date_literal() {
2541        let m = fixture();
2542        // Bare date -> midnight UTC.
2543        let f = parse_expr(&m, "#2025-01-15#").unwrap();
2544        match f.expr {
2545            Expr::Literal(Value::DateTime(dt)) => {
2546                assert_eq!(dt.to_rfc3339(), "2025-01-15T00:00:00+00:00");
2547            }
2548            other => panic!("expected date literal, got {other:?}"),
2549        }
2550        // Full RFC3339.
2551        let f = parse_expr(&m, "#2025-01-15T09:30:00Z#").unwrap();
2552        assert!(matches!(f.expr, Expr::Literal(Value::DateTime(_))));
2553        // Bad date errors, not panics.
2554        assert!(parse_expr(&m, "#not-a-date#").is_err());
2555        assert!(parse_expr(&m, "#2025-01-15").is_err()); // unterminated
2556    }
2557
2558    #[test]
2559    fn definition_parses_sql_form() {
2560        let m = fixture();
2561        match parse_definition(&m, r#"Sales = SQL("select region, amount from sales")"#).unwrap() {
2562            Definition::Sql { target, query } => {
2563                assert_eq!(target, Name("Sales".into()));
2564                assert_eq!(query, "select region, amount from sales");
2565            }
2566            other => panic!("expected Sql, got {other:?}"),
2567        }
2568        // SQL(...) needs exactly one string literal.
2569        assert!(parse_definition(&m, "X = SQL(Price)").is_err());
2570        assert!(parse_definition(&m, r#"X = SQL("a", "b")"#).is_err());
2571    }
2572
2573    #[test]
2574    fn definition_parses_call_form() {
2575        let m = fixture();
2576        // Zero-arg call.
2577        match parse_definition(&m, "Now = CALL(now)").unwrap() {
2578            Definition::Call { target, func, args } => {
2579                assert_eq!(target, Name("Now".into()));
2580                assert_eq!(func, "now");
2581                assert!(args.is_empty());
2582            }
2583            other => panic!("expected Call, got {other:?}"),
2584        }
2585        // Multi-arg call, argument measures by name (resolved by the caller).
2586        match parse_definition(&m, "H = CALL(hypot, Price, Quantity)").unwrap() {
2587            Definition::Call { func, args, .. } => {
2588                assert_eq!(func, "hypot");
2589                assert_eq!(args, vec![Name("Price".into()), Name("Quantity".into())]);
2590            }
2591            other => panic!("expected Call, got {other:?}"),
2592        }
2593        // Malformed arg lists error, not panic.
2594        assert!(parse_definition(&m, "X = CALL(f,)").is_err());
2595        assert!(parse_definition(&m, "X = CALL(f, Price").is_err()); // unterminated
2596        assert!(parse_definition(&m, "X = CALL()").is_err()); // no function name
2597    }
2598
2599    #[test]
2600    fn call_and_sql_are_only_recognized_as_the_whole_rhs() {
2601        // `CALL`/`SQL` as a sub-expression is NOT a source form; it falls into
2602        // the expression parser, where an unknown measure named CALL/SQL errors
2603        // (they are not builtins). This keeps source forms off the engine path.
2604        let m = fixture();
2605        assert!(parse_definition(&m, "X = Price + SQL(\"q\")").is_err());
2606    }
2607
2608    #[test]
2609    fn adversarial_inputs_never_panic() {
2610        // Manual fallback for fuzz/fuzz_targets/fuzz_formula_parser.rs (which
2611        // needs a nightly toolchain + cargo-fuzz to actually run under
2612        // libFuzzer): feed a batch of hand-picked adversarial strings through
2613        // all three parser entry points and assert none of them panics. An
2614        // Ok or an Err are both fine outcomes; a panic is the only failure.
2615        let m = fixture();
2616        let mut inputs: Vec<String> = [
2617            "",
2618            "   \t\n  ",
2619            "((((((((((((((((((((((((((((((((",
2620            "\"unterminated",
2621            "'unterminated", // unterminated quoted name
2622            "''",            // empty quoted name
2623            "'a''",          // doubled quote then EOF
2624            "Sheet1.Price",  // dotted qualification (unresolvable today)
2625            ".",
2626            "#2025-01-01", // unterminated date literal
2627            "#not-a-date#",
2628            "999999999999999999999999999999999999999999999999",
2629            "1e999999999999999999999999999999",
2630            "+++++---***///",
2631            "CALL(",
2632            "CALL(f,)",
2633            "CALL(f, ,)",
2634            "SQL(",
2635            "SQL(\"\")",
2636            "SQL(1)",
2637            "\u{0}\u{0}\u{0}",
2638            "\u{1F4A9}\u{1F4A9}\u{1F4A9}", // multi-byte UTF-8 (emoji)
2639            "Price[Product",
2640            "Price[,,,]",
2641            "SUM(Price OVER)",
2642            "SUM(Price OVER Product OVER Time)",
2643            "=",
2644            "X = = =",
2645            "X=",
2646            "X = CALL(CALL(CALL(CALL(f))))",
2647        ]
2648        .iter()
2649        .map(|s| s.to_string())
2650        .collect();
2651        inputs.push("a".repeat(10_000));
2652        inputs.push("(".repeat(5_000));
2653        for s in &inputs {
2654            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2655                let _ = parse_expr(&m, s);
2656                let _ = parse_formula(&m, s);
2657                let _ = parse_definition(&m, s);
2658            }))
2659            .unwrap_or_else(|e| panic!("parser panicked on input {s:?}: {e:?}"));
2660        }
2661    }
2662
2663    #[test]
2664    fn multi_byte_utf8_does_not_panic_the_tokenizer() {
2665        // Regression: the tokenizer used to cast a raw BYTE to `char` and step
2666        // one byte at a time, slicing off a UTF-8 character boundary on any
2667        // multi-byte input (found via the adversarial-input sweep above with
2668        // an emoji). Multi-byte identifiers/whitespace/unknown chars must all
2669        // either tokenize or error cleanly — never panic.
2670        let m = fixture();
2671        assert!(parse_expr(&m, "\u{1F4A9}\u{1F4A9}\u{1F4A9}").is_err()); // unknown char, not a crash
2672                                                                         // A multi-byte char inside what would otherwise be an identifier.
2673        let _ = parse_expr(&m, "Price\u{00e9}"); // must not panic either way
2674    }
2675
2676    // --- quoted identifiers (`'Unit Price'`) ---
2677
2678    /// A model whose measure/category names are the kind `import_csv` produces
2679    /// verbatim from a CSV header: spaces, punctuation, leading digits,
2680    /// non-ASCII, and names that collide with grammar keywords.
2681    fn odd_names() -> Model {
2682        let mut m = fixture();
2683        m.add_category(CategoryId(3), "Fiscal Year");
2684        for (id, name) in [
2685            (MeasureId(200), "Unit Price"),
2686            (MeasureId(201), "Cost-Per-Unit"),
2687            (MeasureId(202), "Revenue/Unit"),
2688            (MeasureId(203), "Margin (net)"),
2689            (MeasureId(204), "2024 Total"),
2690            (MeasureId(205), "Bob's Rate"),
2691            (MeasureId(206), "Umsatz \u{20AC} \u{5E74}\u{5EA6}"),
2692            (MeasureId(207), "Over"),
2693            (MeasureId(208), "SUM"),
2694            (MeasureId(209), "NOT"),
2695        ] {
2696            m.add_measure(Measure {
2697                id,
2698                name: Name(name.into()),
2699                value_type: ValueType::Number,
2700                categories: vec![TIME, PRODUCT],
2701                kind: MeasureKind::Input,
2702                description: None,
2703            });
2704        }
2705        m
2706    }
2707
2708    #[test]
2709    fn quoted_name_resolves_a_measure_with_spaces() {
2710        // The defect this closes: a CSV header `Unit Price` had no spelling.
2711        let m = odd_names();
2712        let f = parse_expr(&m, "'Unit Price' * Quantity").unwrap();
2713        assert_eq!(
2714            f.expr,
2715            Expr::BinaryOp(
2716                BinaryOp::Mul,
2717                Box::new(refr(MeasureId(200))),
2718                Box::new(refr(QUANTITY)),
2719            )
2720        );
2721    }
2722
2723    #[test]
2724    fn quoted_names_cover_punctuation_digits_utf8_and_keywords() {
2725        let m = odd_names();
2726        for (src, id) in [
2727            ("'Unit Price'", 200),
2728            ("'Cost-Per-Unit'", 201),
2729            ("'Revenue/Unit'", 202),
2730            ("'Margin (net)'", 203),
2731            ("'2024 Total'", 204),
2732            ("'Umsatz \u{20AC} \u{5E74}\u{5EA6}'", 206),
2733            // Keyword collisions: quoting makes them names, not syntax.
2734            ("'Over'", 207),
2735            ("'SUM'", 208),
2736            ("'NOT'", 209),
2737        ] {
2738            assert_eq!(
2739                parse_expr(&m, src).unwrap().expr,
2740                refr(MeasureId(id)),
2741                "{src}"
2742            );
2743        }
2744        // And they compose: a quoted keyword-named measure inside an expression
2745        // is not read as an operator.
2746        assert!(matches!(
2747            parse_expr(&m, "'NOT' + 'SUM'").unwrap().expr,
2748            Expr::BinaryOp(BinaryOp::Add, _, _)
2749        ));
2750        // `SUM` quoted is a ref even directly before '(' — quoting wins.
2751        assert!(parse_expr(&m, "'SUM'(Price)").is_err()); // trailing '(', not a call
2752    }
2753
2754    #[test]
2755    fn doubled_quote_escapes_an_embedded_quote() {
2756        // Chosen escape: `''` inside a quoted name is one literal `'`
2757        // (Quantrix/SQL convention).
2758        let m = odd_names();
2759        let f = parse_expr(&m, "'Bob''s Rate' + Price").unwrap();
2760        assert_eq!(
2761            f.expr,
2762            Expr::BinaryOp(
2763                BinaryOp::Add,
2764                Box::new(refr(MeasureId(205))),
2765                Box::new(refr(PRICE)),
2766            )
2767        );
2768        // Without the doubling it is a *different* (unknown) name, and the
2769        // trailing `s Rate'` opens an unterminated quote.
2770        assert!(parse_expr(&m, "'Bob's Rate'").is_err());
2771    }
2772
2773    #[test]
2774    fn quoted_names_work_everywhere_a_name_does() {
2775        let m = odd_names();
2776        // Target of an assignment.
2777        let f = parse_formula(&m, "'Gross Margin' = 'Unit Price' - 'Cost-Per-Unit'").unwrap();
2778        assert_eq!(f.target, Name("Gross Margin".into()));
2779        // Dimension list and aggregation category.
2780        let f = parse_expr(&m, "'Unit Price'[Time, Product]").unwrap();
2781        assert_eq!(
2782            f.expr,
2783            Expr::Ref(
2784                MeasureId(200),
2785                DimensionSpec {
2786                    by: vec![TIME, PRODUCT],
2787                    over: vec![],
2788                    except: vec![],
2789                }
2790            )
2791        );
2792        let f = parse_expr(&m, "SUM('Unit Price' OVER 'Fiscal Year')").unwrap();
2793        assert_eq!(
2794            f.expr,
2795            Expr::Call(
2796                FUNC_SUM,
2797                vec![Expr::Ref(
2798                    MeasureId(200),
2799                    DimensionSpec {
2800                        over: vec![CategoryId(3)],
2801                        by: vec![],
2802                        except: vec![],
2803                    }
2804                )]
2805            )
2806        );
2807        // Scalar call argument, and CALL(...) argument measures.
2808        assert!(parse_expr(&m, "ABS('Unit Price')").is_ok());
2809        match parse_definition(&m, "X = CALL(f, 'Unit Price')").unwrap() {
2810            Definition::Call { args, .. } => {
2811                assert_eq!(args, vec![Name("Unit Price".into())]);
2812            }
2813            other => panic!("expected Call, got {other:?}"),
2814        }
2815    }
2816
2817    #[test]
2818    fn quoted_name_errors_are_clear_not_panics() {
2819        let m = odd_names();
2820        // Unknown quoted measure / category name.
2821        let e = parse_expr(&m, "'No Such Measure' * Price").unwrap_err();
2822        assert!(
2823            e.to_string().contains("unknown measure: No Such Measure"),
2824            "{e}"
2825        );
2826        assert_eq!(e.position, Some(0));
2827        let e = parse_expr(&m, "SUM(Price OVER 'No Such Category')").unwrap_err();
2828        assert!(
2829            e.to_string().contains("unknown category: No Such Category"),
2830            "{e}"
2831        );
2832        // Unterminated and empty quoted names.
2833        let e = parse_expr(&m, "'Unit Price").unwrap_err();
2834        assert!(e.to_string().contains("unterminated quoted name"), "{e}");
2835        let e = parse_expr(&m, "'' + Price").unwrap_err();
2836        assert!(e.to_string().contains("empty quoted name"), "{e}");
2837    }
2838
2839    #[test]
2840    fn double_quotes_are_still_text_literals() {
2841        // `'` introduces a NAME, `"` a text literal. Adding one must not have
2842        // disturbed the other.
2843        let m = odd_names();
2844        assert_eq!(
2845            parse_expr(&m, "\"Unit Price\"").unwrap().expr,
2846            Expr::Literal(Value::Text("Unit Price".into()))
2847        );
2848        // A single quote inside a text literal is just a character.
2849        assert_eq!(
2850            parse_expr(&m, "\"it's\"").unwrap().expr,
2851            Expr::Literal(Value::Text("it's".into()))
2852        );
2853    }
2854
2855    #[test]
2856    fn dotted_qualification_parses_but_names_the_unknown_qualifier() {
2857        // Improv has no matrix namespace yet (GUI plan Step 3), so the only
2858        // honest resolution is a clear error naming the qualifier — not the old
2859        // `unexpected character: '.'`.
2860        let m = odd_names();
2861        for src in [
2862            "Sheet1.Price",
2863            "'Defined Input & Outputs'.'Income tax rate'",
2864            "Price + Sheet1.Quantity",
2865            "SUM(Price OVER Sheet1.Time)",
2866        ] {
2867            let e = parse_expr(&m, src).unwrap_err();
2868            assert!(e.to_string().contains("unknown qualifier"), "{src}: {e}");
2869        }
2870        // The qualifier is named verbatim, quoted or not.
2871        let e = parse_expr(&m, "'Defined Input & Outputs'.'Income tax rate'").unwrap_err();
2872        assert!(e.to_string().contains("Defined Input & Outputs"), "{e}");
2873        // A dot with no name after it is also an error, never a panic.
2874        assert!(parse_expr(&m, "Price.").is_err());
2875        assert!(parse_expr(&m, ".Price").is_err());
2876        // Chains do not recurse: exactly one dot is consumed, then it errors.
2877        assert!(parse_expr(&m, &"a.".repeat(5_000)).is_err());
2878    }
2879
2880    #[test]
2881    fn quoted_and_dotted_adversarial_inputs_never_panic() {
2882        // Companion to `adversarial_inputs_never_panic`, for the quoting and
2883        // qualification additions. Ok or Err are both fine; a panic is not.
2884        let m = odd_names();
2885        let mut inputs: Vec<String> = [
2886            "'",
2887            "''",
2888            "'''",
2889            "''''",
2890            "'''''",
2891            "'unterminated",
2892            "'a\u{20AC}",  // unterminated, multi-byte
2893            "'\u{1F4A9}'", // quoted emoji name (unknown measure)
2894            "'\u{1F4A9}",  // unterminated after multi-byte
2895            "'a''",        // doubling then EOF
2896            "'\u{0}'",
2897            "'Unit Price'[",
2898            "'Unit Price'[']",
2899            ".",
2900            "..",
2901            "a..b",
2902            "'a'.'b'.'c'",
2903            "X = 'a'.'b'",
2904            "X = ''",
2905            "SUM('a' OVER 'b')",
2906            "CALL(f, 'a'",
2907            "SQL('q')",
2908        ]
2909        .iter()
2910        .map(|s| s.to_string())
2911        .collect();
2912        inputs.push(format!("'{}", "a".repeat(10_000))); // unterminated, long
2913        inputs.push("'a''".repeat(5_000)); // doubling storm
2914        inputs.push(".".repeat(5_000));
2915        for s in &inputs {
2916            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2917                let _ = parse_expr(&m, s);
2918                let _ = parse_formula(&m, s);
2919                let _ = parse_definition(&m, s);
2920            }))
2921            .unwrap_or_else(|e| panic!("parser panicked on input {s:?}: {e:?}"));
2922        }
2923    }
2924
2925    #[test]
2926    fn deeply_nested_parens_error_instead_of_overflowing_the_stack() {
2927        // Regression: recursive-descent `parse_primary` recursed once per '(',
2928        // so thousands of nested opens overflowed the stack (found via the
2929        // adversarial-input sweep). A bounded depth now errors cleanly.
2930        let m = fixture();
2931        let deep = "(".repeat(5_000);
2932        assert!(parse_expr(&m, &deep).is_err());
2933        // A reasonable nesting depth still works fine.
2934        let reasonable = format!("{}Price{}", "(".repeat(50), ")".repeat(50));
2935        assert!(parse_expr(&m, &reasonable).is_ok());
2936    }
2937}