Skip to main content

truecalc_core/parser/
mod.rs

1pub mod ast;
2pub mod refs;
3pub mod tokens;
4
5pub use ast::{Expr, Span};
6pub use refs::{CellAddr, Ref};
7use ast::{BinaryOp, UnaryOp};
8use crate::types::ParseError;
9use nom::{IResult, character::complete::multispace0};
10use tokens::{bool_literal, dollar_cell_ref, error_literal, identifier, number_literal, offset, string_literal};
11
12/// A cell-address token: `dollar_cell_ref()`'s `$`-bearing shape (`$A1`,
13/// `A$1`, `$A$1`), or — when no literal `$` is present — `identifier()`'s
14/// plain shape (`A1`). `dollar_cell_ref` must be tried first: `identifier`
15/// does not fail on input like `"A$1"`, it just stops early at `A` and
16/// succeeds, so trying it first would never give `dollar_cell_ref` a chance
17/// to claim the `$1` suffix. Used wherever a range endpoint is expected, so
18/// either corner of a range may independently carry `$` anchors (e.g.
19/// `A1:$D$4`).
20fn cell_ref_text(i: &str) -> IResult<&str, &str> {
21    dollar_cell_ref(i).or_else(|_| identifier(i))
22}
23
24struct Parser<'a> {
25    full: &'a str,
26}
27
28impl<'a> Parser<'a> {
29    fn new(full: &'a str) -> Self {
30        Self { full }
31    }
32
33    fn span(&self, before: &str, after: &str) -> Span {
34        let start = offset(self.full, before);
35        let end = offset(self.full, after);
36        Span::new(start, end - start)
37    }
38
39    // ── primary ────────────────────────────────────────────────────────────
40
41    fn parse_primary(&self, i: &'a str) -> IResult<&'a str, Expr> {
42        let i = multispace0(i)?.0;
43
44        // Number literal (must come before identifier to catch e.g. "1e3")
45        if let Ok((rest, n)) = number_literal(i) {
46            return Ok((rest, Expr::Number(n, self.span(i, rest))));
47        }
48
49        // String literal
50        if let Ok((rest, text)) = string_literal(i) {
51            return Ok((rest, Expr::Text(text, self.span(i, rest))));
52        }
53
54        // Array literal: {expr, expr, ...}
55        if let Some(inner) = i.strip_prefix('{') {
56            let (rest, elems) = self.parse_array_elements(inner)?;
57            let rest = multispace0(rest)?.0;
58            if let Some(after) = rest.strip_prefix('}') {
59                return Ok((after, Expr::Array(elems, self.span(i, after))));
60            }
61            return Err(nom::Err::Error(nom::error::Error::new(
62                rest,
63                nom::error::ErrorKind::Char,
64            )));
65        }
66
67        // Parenthesised expression
68        if let Some(inner) = i.strip_prefix('(') {
69            // Trim whitespace after '(' so a padded grouping like `( A1 + B1 )`
70            // does not leak the leading space into the inner expression's span
71            // (issue #751 — same class as #746/#748/#749, which also trim before
72            // parse_comparison; the trailing side is already trimmed below).
73            let inner = multispace0(inner)?.0;
74            let (rest, expr) = self.parse_comparison(inner)?;
75            let rest = multispace0(rest)?.0;
76            if let Some(after) = rest.strip_prefix(')') {
77                return Ok((after, expr));
78            }
79            return Err(nom::Err::Error(nom::error::Error::new(
80                rest,
81                nom::error::ErrorKind::Char,
82            )));
83        }
84
85        // Boolean (before identifier — uses word-boundary check in bool_literal)
86        if let Ok((rest, b)) = bool_literal(i) {
87            return Ok((rest, Expr::Bool(b, self.span(i, rest))));
88        }
89
90        // Error literal: #REF!, #DIV/0!, #NAME?, #VALUE!, #NUM!, #N/A, #NULL!
91        // (issue #716) — parses straight to its error value, same as a
92        // number/string/boolean literal parses straight to theirs. No other
93        // primary form starts with '#', so this can be tried unconditionally.
94        if let Ok((rest, kind)) = error_literal(i) {
95            return Ok((rest, Expr::Error(kind, self.span(i, rest))));
96        }
97
98        // Quoted-sheet reference: 'Sheet Name'!A1 / 'Sheet Name'!A1:B2
99        if i.starts_with('\'') {
100            return self.parse_quoted_sheet_ref(i);
101        }
102
103        // $-anchored cell reference (bare, no sheet): $A$1, $A1, A$1. A
104        // '$'-bearing token can only ever be a cell/range reference (never a
105        // sheet name, function call, or plain variable — none of those can
106        // contain '$'), so it short-circuits straight to Expr::Variable,
107        // mirroring how plain `A1` becomes Expr::Variable("A1", ..) below.
108        if let Ok((rest, span)) = dollar_cell_ref(i) {
109            let rest_ws = multispace0(rest)?.0;
110            if let Some(after_colon) = rest_ws.strip_prefix(':') {
111                if let Ok((rest2, end_span)) = cell_ref_text(after_colon) {
112                    if CellAddr::parse(end_span).is_some() {
113                        let range_name = format!("{}:{}", span, end_span);
114                        return Ok((rest2, Expr::Variable(range_name, self.span(i, rest2))));
115                    }
116                }
117            }
118            return Ok((rest, Expr::Variable(span.to_string(), self.span(i, rest))));
119        }
120
121        // Identifier: sheet-qualified reference, variable, or function call
122        if let Ok((rest, name)) = identifier(i) {
123            // Sheet-qualified reference: Sheet1!A1 / Sheet1!A1:B2 — `!` binds
124            // tightly to the sheet name (no whitespace on either side).
125            if let Some(after_bang) = rest.strip_prefix('!') {
126                return self.parse_ref_body(i, name.to_string(), after_bang);
127            }
128            let rest_ws = multispace0(rest)?.0;
129            if let Some(args_input) = rest_ws.strip_prefix('(') {
130                // Function call
131                let (rest2, args) = self.parse_arg_list(args_input)?;
132                let rest2 = multispace0(rest2)?.0;
133                if let Some(after_close) = rest2.strip_prefix(')') {
134                    let func_expr = Expr::FunctionCall {
135                        name: name.to_uppercase(),
136                        args,
137                        span: self.span(i, after_close),
138                    };
139                    // Detect immediately-invoked call: FUNC(lambda_args)(call_args)
140                    let after_ws = multispace0(after_close)?.0;
141                    if let Some(call_input) = after_ws.strip_prefix('(') {
142                        let (rest3, call_args) = self.parse_arg_list(call_input)?;
143                        let rest3 = multispace0(rest3)?.0;
144                        if let Some(after) = rest3.strip_prefix(')') {
145                            return Ok((after, Expr::Apply {
146                                func: Box::new(func_expr),
147                                call_args,
148                                span: self.span(i, after),
149                            }));
150                        }
151                        return Err(nom::Err::Error(nom::error::Error::new(
152                            rest3,
153                            nom::error::ErrorKind::Char,
154                        )));
155                    }
156                    return Ok((after_close, func_expr));
157                }
158                return Err(nom::Err::Error(nom::error::Error::new(
159                    rest2,
160                    nom::error::ErrorKind::Char,
161                )));
162            }
163            // Range reference: A1:D4 (end corner may itself be $-anchored,
164            // e.g. A1:$D$4 — validated via the same CellAddr::parse used
165            // for the dollar-led branch above, so both paths agree on shape).
166            if CellAddr::parse(name).is_some() {
167                if let Some(after_colon) = rest_ws.strip_prefix(':') {
168                    if let Ok((rest2, name2)) = cell_ref_text(after_colon) {
169                        if CellAddr::parse(name2).is_some() {
170                            let range_name = format!("{}:{}", name, name2);
171                            return Ok((rest2, Expr::Variable(range_name, self.span(i, rest2))));
172                        }
173                    }
174                }
175            }
176            return Ok((rest, Expr::Variable(name.to_string(), self.span(i, rest))));
177        }
178
179        Err(nom::Err::Error(nom::error::Error::new(i, nom::error::ErrorKind::Alt)))
180    }
181
182    // ── sheet-qualified references ──────────────────────────────────────
183
184    /// Parse the part after `!`: a cell address, optionally `:cell` for a
185    /// range. `start` is where the whole reference began (for spans); `sheet`
186    /// is the unescaped sheet name.
187    fn parse_ref_body(&self, start: &'a str, sheet: String, i: &'a str) -> IResult<&'a str, Expr> {
188        let err = || nom::Err::Error(nom::error::Error::new(i, nom::error::ErrorKind::Tag));
189        let (rest, cell_text) = cell_ref_text(i).map_err(|_| err())?;
190        let addr = CellAddr::parse(cell_text).ok_or_else(err)?;
191        // Optional range tail, mirroring the bare `A1:D4` grammar below.
192        let rest_ws = multispace0(rest)?.0;
193        if let Some(after_colon) = rest_ws.strip_prefix(':') {
194            if let Ok((rest2, end_text)) = cell_ref_text(after_colon) {
195                if let Some(end) = CellAddr::parse(end_text) {
196                    let r = Ref::Range { sheet: Some(sheet), start: addr, end };
197                    return Ok((rest2, Expr::Reference(r, self.span(start, rest2))));
198                }
199            }
200        }
201        let r = Ref::Cell { sheet: Some(sheet), addr };
202        Ok((rest, Expr::Reference(r, self.span(start, rest))))
203    }
204
205    /// Parse `'Sheet Name'!A1` / `'Sheet Name'!A1:B2`. `i` starts at the
206    /// opening quote. `''` inside the quotes is an escaped single quote.
207    fn parse_quoted_sheet_ref(&self, i: &'a str) -> IResult<&'a str, Expr> {
208        let inner = &i[1..];
209        let mut sheet = String::new();
210        let mut idx = 0;
211        loop {
212            match inner[idx..].find('\'') {
213                // Unterminated quoted sheet name
214                None => {
215                    return Err(nom::Err::Error(nom::error::Error::new(
216                        i,
217                        nom::error::ErrorKind::Char,
218                    )));
219                }
220                Some(q) => {
221                    sheet.push_str(&inner[idx..idx + q]);
222                    let after = idx + q + 1;
223                    if inner[after..].starts_with('\'') {
224                        sheet.push('\'');
225                        idx = after + 1;
226                    } else {
227                        idx = after;
228                        break;
229                    }
230                }
231            }
232        }
233        let rest = &inner[idx..];
234        if sheet.is_empty() {
235            return Err(nom::Err::Error(nom::error::Error::new(
236                i,
237                nom::error::ErrorKind::Char,
238            )));
239        }
240        match rest.strip_prefix('!') {
241            Some(after_bang) => self.parse_ref_body(i, sheet, after_bang),
242            None => Err(nom::Err::Error(nom::error::Error::new(
243                rest,
244                nom::error::ErrorKind::Char,
245            ))),
246        }
247    }
248
249    fn parse_arg_list(&self, i: &'a str) -> IResult<&'a str, Vec<Expr>> {
250        let mut args = Vec::new();
251        let mut rest = multispace0(i)?.0;
252
253        if rest.starts_with(')') {
254            return Ok((rest, args));
255        }
256
257        // Parse first argument (may be empty if it starts with comma or close paren)
258        let ws = multispace0(rest)?.0;
259        if ws.starts_with(',') || ws.starts_with(')') {
260            // Empty first argument
261            args.push(Expr::Variable(String::new(), Span::new(0, 0)));
262        } else {
263            let (r, first) = self.parse_comparison(rest)?;
264            args.push(first);
265            rest = r;
266        }
267
268        loop {
269            rest = multispace0(rest)?.0;
270            if let Some(after_comma) = rest.strip_prefix(',') {
271                let after_ws = multispace0(after_comma)?.0;
272                if after_ws.starts_with(',') || after_ws.starts_with(')') {
273                    // Empty argument
274                    args.push(Expr::Variable(String::new(), Span::new(0, 0)));
275                    rest = after_comma;
276                } else {
277                    // Parse from the first non-whitespace token, not from
278                    // just after the comma — a compound (BinaryOp) argument's
279                    // span is measured from its entry point here, so passing
280                    // the untrimmed `after_comma` would make the span start
281                    // at the separating whitespace instead of the argument's
282                    // own first token.
283                    let (r, arg) = self.parse_comparison(after_ws)?;
284                    args.push(arg);
285                    rest = r;
286                }
287            } else {
288                break;
289            }
290        }
291
292        Ok((rest, args))
293    }
294
295    fn parse_array_elements(&self, i: &'a str) -> IResult<&'a str, Vec<Expr>> {
296        let mut rows: Vec<Vec<Expr>> = Vec::new();
297        let mut current_row: Vec<Expr> = Vec::new();
298        let mut rest = multispace0(i)?.0;
299        if rest.starts_with('}') {
300            return Ok((rest, Vec::new())); // empty array {}
301        }
302        let (r, first) = self.parse_comparison(rest)?;
303        current_row.push(first);
304        rest = r;
305        loop {
306            rest = multispace0(rest)?.0;
307            if let Some(after_comma) = rest.strip_prefix(',') {
308                // Parse from the first non-whitespace token, not from just
309                // after the comma — same leading-whitespace bug as #746's
310                // function-argument fix, but here in the array-element
311                // separator: passing the untrimmed `after_comma` would let a
312                // compound (BinaryOp) element's span start at the separating
313                // whitespace instead of the element's own first token.
314                let after_ws = multispace0(after_comma)?.0;
315                let (r, elem) = self.parse_comparison(after_ws)?;
316                current_row.push(elem);
317                rest = r;
318            } else if let Some(after_semi) = rest.strip_prefix(';') {
319                rows.push(std::mem::take(&mut current_row));
320                // Same trim as the comma branch above, for the first element
321                // of the new row.
322                let after_ws = multispace0(after_semi)?.0;
323                let (r, elem) = self.parse_comparison(after_ws)?;
324                current_row.push(elem);
325                rest = r;
326            } else {
327                break;
328            }
329        }
330        rows.push(current_row);
331        // If only one row (no semicolons), return flat vec
332        if rows.len() == 1 {
333            return Ok((rest, rows.into_iter().next().unwrap()));
334        }
335        // Multiple rows → wrap each row in an Array node. Each row's span
336        // must cover only that row's own elements (its first element's start
337        // to its last element's end) — not the whole `{…}` body, which is
338        // what every row got when this span was computed once outside the
339        // loop below.
340        let row_exprs: Vec<Expr> = rows
341            .into_iter()
342            .map(|row_elems| {
343                let s = match (row_elems.first(), row_elems.last()) {
344                    (Some(first), Some(last)) => {
345                        let start = first.span().offset;
346                        let end = last.span().offset + last.span().length;
347                        Span::new(start, end - start)
348                    }
349                    // A row is never empty in practice (each row starts with
350                    // an element pushed either before the loop or right
351                    // after a `;`), but fall back to the old whole-body span
352                    // rather than panic if that ever changes.
353                    _ => self.span(i, rest),
354                };
355                Expr::Array(row_elems, s)
356            })
357            .collect();
358        Ok((rest, row_exprs))
359    }
360
361    // ── postfix % ─────────────────────────────────────────────────────────
362
363    fn parse_postfix(&self, i: &'a str) -> IResult<&'a str, Expr> {
364        let (rest, expr) = self.parse_primary(i)?;
365        let rest_ws = multispace0(rest)?.0;
366        if let Some(after) = rest_ws.strip_prefix('%') {
367            return Ok((after, Expr::UnaryOp {
368                op: UnaryOp::Percent,
369                operand: Box::new(expr),
370                span: self.span(i, after),
371            }));
372        }
373        Ok((rest, expr))
374    }
375
376    // ── unary minus ───────────────────────────────────────────────────────
377
378    fn parse_unary(&self, i: &'a str) -> IResult<&'a str, Expr> {
379        let i_ws = multispace0(i)?.0;
380        if let Some(after_minus) = i_ws.strip_prefix('-') {
381            let (rest, operand) = self.parse_unary(after_minus)?;
382            return Ok((rest, Expr::UnaryOp {
383                op: UnaryOp::Neg,
384                operand: Box::new(operand),
385                span: self.span(i_ws, rest),
386            }));
387        }
388        self.parse_postfix(i)
389    }
390
391    // ── power ^ (right-associative) ───────────────────────────────────────
392
393    fn parse_power(&self, i: &'a str) -> IResult<&'a str, Expr> {
394        let (rest, left) = self.parse_unary(i)?;
395        let rest_ws = multispace0(rest)?.0;
396        if let Some(after_op) = rest_ws.strip_prefix('^') {
397            let (rest2, right) = self.parse_power(after_op)?;
398            return Ok((rest2, Expr::BinaryOp {
399                op: BinaryOp::Pow,
400                left: Box::new(left),
401                right: Box::new(right),
402                span: self.span(i, rest2),
403            }));
404        }
405        Ok((rest, left))
406    }
407
408    // ── multiplicative * / ────────────────────────────────────────────────
409
410    fn parse_multiplicative(&self, i: &'a str) -> IResult<&'a str, Expr> {
411        let (mut rest, mut left) = self.parse_power(i)?;
412        loop {
413            let ws = multispace0(rest)?.0;
414            let op = ws.strip_prefix('*').map(|after| (BinaryOp::Mul, after))
415                .or_else(|| ws.strip_prefix('/').map(|after| (BinaryOp::Div, after)));
416            match op {
417                None => break,
418                Some((op, after)) => {
419                    let (r, right) = self.parse_power(after)?;
420                    left = Expr::BinaryOp {
421                        op,
422                        span: self.span(i, r),
423                        left: Box::new(left),
424                        right: Box::new(right),
425                    };
426                    rest = r;
427                }
428            }
429        }
430        Ok((rest, left))
431    }
432
433    // ── additive + - ──────────────────────────────────────────────────────
434
435    fn parse_additive(&self, i: &'a str) -> IResult<&'a str, Expr> {
436        let (mut rest, mut left) = self.parse_multiplicative(i)?;
437        loop {
438            let ws = multispace0(rest)?.0;
439            let op = ws.strip_prefix('+').map(|after| (BinaryOp::Add, after))
440                .or_else(|| ws.strip_prefix('-').map(|after| (BinaryOp::Sub, after)));
441            match op {
442                None => break,
443                Some((op, after)) => {
444                    let (r, right) = self.parse_multiplicative(after)?;
445                    left = Expr::BinaryOp {
446                        op,
447                        span: self.span(i, r),
448                        left: Box::new(left),
449                        right: Box::new(right),
450                    };
451                    rest = r;
452                }
453            }
454        }
455        Ok((rest, left))
456    }
457
458    // ── concat & ─────────────────────────────────────────────────────────
459
460    fn parse_concat(&self, i: &'a str) -> IResult<&'a str, Expr> {
461        let (mut rest, mut left) = self.parse_additive(i)?;
462        loop {
463            let ws = multispace0(rest)?.0;
464            if let Some(after) = ws.strip_prefix('&') {
465                let (r, right) = self.parse_additive(after)?;
466                left = Expr::BinaryOp {
467                    op: BinaryOp::Concat,
468                    span: self.span(i, r),
469                    left: Box::new(left),
470                    right: Box::new(right),
471                };
472                rest = r;
473            } else {
474                break;
475            }
476        }
477        Ok((rest, left))
478    }
479
480    // ── comparison = <> < > <= >= ─────────────────────────────────────────
481
482    fn parse_comparison(&self, i: &'a str) -> IResult<&'a str, Expr> {
483        let (rest, left) = self.parse_concat(i)?;
484        let ws = multispace0(rest)?.0;
485
486        // Longest match first
487        let op_result: Option<(BinaryOp, &'a str)> = if let Some(after) = ws.strip_prefix("<>") {
488            Some((BinaryOp::Ne, after))
489        } else if let Some(after) = ws.strip_prefix("<=") {
490            Some((BinaryOp::Le, after))
491        } else if let Some(after) = ws.strip_prefix(">=") {
492            Some((BinaryOp::Ge, after))
493        } else if let Some(after) = ws.strip_prefix('<') {
494            Some((BinaryOp::Lt, after))
495        } else if let Some(after) = ws.strip_prefix('>') {
496            Some((BinaryOp::Gt, after))
497        } else if let Some(after) = ws.strip_prefix('=') {
498            Some((BinaryOp::Eq, after))
499        } else {
500            None
501        };
502
503        if let Some((op, after)) = op_result {
504            let (r, right) = self.parse_concat(after)?;
505            return Ok((r, Expr::BinaryOp {
506                op,
507                span: self.span(i, r),
508                left: Box::new(left),
509                right: Box::new(right),
510            }));
511        }
512
513        Ok((rest, left))
514    }
515}
516
517// ── public API ──────────────────────────────────────────────────────────────
518
519/// Parse a formula string into an expression tree.
520///
521/// The formula must start with `=`. Returns a [`ParseError`] if the input
522/// is not a valid formula.
523#[deprecated(since = "0.7.0", note = "use Engine::sheets()/Engine::excel() and engine.parse() — engine flavor is required; see ADR 2026-04-27; removal target: 0.7.0 coordinated release")]
524pub fn parse(formula: &str) -> Result<Expr, ParseError> {
525    parse_formula(formula)
526}
527
528/// Crate-internal parser entry point used by [`crate::Engine`].
529pub(crate) fn parse_formula(formula: &str) -> Result<Expr, ParseError> {
530    let input = formula.strip_prefix('=').unwrap_or(formula).trim();
531    let p = Parser::new(formula);
532    match p.parse_comparison(input) {
533        Ok((rest, expr)) => {
534            let rest = rest.trim();
535            if rest.is_empty() {
536                Ok(expr)
537            } else {
538                Err(ParseError {
539                    message: format!("Unexpected input '{}'", rest),
540                    position: offset(formula, rest),
541                })
542            }
543        }
544        Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => Err(ParseError {
545            message: "Parse error".into(),
546            position: offset(formula, e.input),
547        }),
548        Err(nom::Err::Incomplete(_)) => Err(ParseError {
549            message: "Incomplete input".into(),
550            position: formula.len(),
551        }),
552    }
553}
554
555/// Validate that a formula string is syntactically correct without returning the AST.
556#[deprecated(since = "0.7.0", note = "use Engine::sheets()/Engine::excel() and engine.validate() — engine flavor is required; see ADR 2026-04-27; removal target: 0.7.0 coordinated release")]
557pub fn validate(formula: &str) -> Result<(), ParseError> {
558    parse_formula(formula).map(|_| ())
559}
560
561#[cfg(test)]
562mod tests;