Skip to main content

truecalc_core/parser/
mod.rs

1pub mod ast;
2pub mod refs;
3pub mod tokens;
4
5pub use ast::Expr;
6pub use refs::{CellAddr, Ref};
7use ast::{BinaryOp, Span, UnaryOp};
8use crate::types::ParseError;
9use nom::{IResult, character::complete::multispace0};
10use tokens::{bool_literal, dollar_cell_ref, 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            let (rest, expr) = self.parse_comparison(inner)?;
70            let rest = multispace0(rest)?.0;
71            if let Some(after) = rest.strip_prefix(')') {
72                return Ok((after, expr));
73            }
74            return Err(nom::Err::Error(nom::error::Error::new(
75                rest,
76                nom::error::ErrorKind::Char,
77            )));
78        }
79
80        // Boolean (before identifier — uses word-boundary check in bool_literal)
81        if let Ok((rest, b)) = bool_literal(i) {
82            return Ok((rest, Expr::Bool(b, self.span(i, rest))));
83        }
84
85        // Quoted-sheet reference: 'Sheet Name'!A1 / 'Sheet Name'!A1:B2
86        if i.starts_with('\'') {
87            return self.parse_quoted_sheet_ref(i);
88        }
89
90        // $-anchored cell reference (bare, no sheet): $A$1, $A1, A$1. A
91        // '$'-bearing token can only ever be a cell/range reference (never a
92        // sheet name, function call, or plain variable — none of those can
93        // contain '$'), so it short-circuits straight to Expr::Variable,
94        // mirroring how plain `A1` becomes Expr::Variable("A1", ..) below.
95        if let Ok((rest, span)) = dollar_cell_ref(i) {
96            let rest_ws = multispace0(rest)?.0;
97            if let Some(after_colon) = rest_ws.strip_prefix(':') {
98                if let Ok((rest2, end_span)) = cell_ref_text(after_colon) {
99                    if CellAddr::parse(end_span).is_some() {
100                        let range_name = format!("{}:{}", span, end_span);
101                        return Ok((rest2, Expr::Variable(range_name, self.span(i, rest2))));
102                    }
103                }
104            }
105            return Ok((rest, Expr::Variable(span.to_string(), self.span(i, rest))));
106        }
107
108        // Identifier: sheet-qualified reference, variable, or function call
109        if let Ok((rest, name)) = identifier(i) {
110            // Sheet-qualified reference: Sheet1!A1 / Sheet1!A1:B2 — `!` binds
111            // tightly to the sheet name (no whitespace on either side).
112            if let Some(after_bang) = rest.strip_prefix('!') {
113                return self.parse_ref_body(i, name.to_string(), after_bang);
114            }
115            let rest_ws = multispace0(rest)?.0;
116            if let Some(args_input) = rest_ws.strip_prefix('(') {
117                // Function call
118                let (rest2, args) = self.parse_arg_list(args_input)?;
119                let rest2 = multispace0(rest2)?.0;
120                if let Some(after_close) = rest2.strip_prefix(')') {
121                    let func_expr = Expr::FunctionCall {
122                        name: name.to_uppercase(),
123                        args,
124                        span: self.span(i, after_close),
125                    };
126                    // Detect immediately-invoked call: FUNC(lambda_args)(call_args)
127                    let after_ws = multispace0(after_close)?.0;
128                    if let Some(call_input) = after_ws.strip_prefix('(') {
129                        let (rest3, call_args) = self.parse_arg_list(call_input)?;
130                        let rest3 = multispace0(rest3)?.0;
131                        if let Some(after) = rest3.strip_prefix(')') {
132                            return Ok((after, Expr::Apply {
133                                func: Box::new(func_expr),
134                                call_args,
135                                span: self.span(i, after),
136                            }));
137                        }
138                        return Err(nom::Err::Error(nom::error::Error::new(
139                            rest3,
140                            nom::error::ErrorKind::Char,
141                        )));
142                    }
143                    return Ok((after_close, func_expr));
144                }
145                return Err(nom::Err::Error(nom::error::Error::new(
146                    rest2,
147                    nom::error::ErrorKind::Char,
148                )));
149            }
150            // Range reference: A1:D4 (end corner may itself be $-anchored,
151            // e.g. A1:$D$4 — validated via the same CellAddr::parse used
152            // for the dollar-led branch above, so both paths agree on shape).
153            if CellAddr::parse(name).is_some() {
154                if let Some(after_colon) = rest_ws.strip_prefix(':') {
155                    if let Ok((rest2, name2)) = cell_ref_text(after_colon) {
156                        if CellAddr::parse(name2).is_some() {
157                            let range_name = format!("{}:{}", name, name2);
158                            return Ok((rest2, Expr::Variable(range_name, self.span(i, rest2))));
159                        }
160                    }
161                }
162            }
163            return Ok((rest, Expr::Variable(name.to_string(), self.span(i, rest))));
164        }
165
166        Err(nom::Err::Error(nom::error::Error::new(i, nom::error::ErrorKind::Alt)))
167    }
168
169    // ── sheet-qualified references ──────────────────────────────────────
170
171    /// Parse the part after `!`: a cell address, optionally `:cell` for a
172    /// range. `start` is where the whole reference began (for spans); `sheet`
173    /// is the unescaped sheet name.
174    fn parse_ref_body(&self, start: &'a str, sheet: String, i: &'a str) -> IResult<&'a str, Expr> {
175        let err = || nom::Err::Error(nom::error::Error::new(i, nom::error::ErrorKind::Tag));
176        let (rest, cell_text) = cell_ref_text(i).map_err(|_| err())?;
177        let addr = CellAddr::parse(cell_text).ok_or_else(err)?;
178        // Optional range tail, mirroring the bare `A1:D4` grammar below.
179        let rest_ws = multispace0(rest)?.0;
180        if let Some(after_colon) = rest_ws.strip_prefix(':') {
181            if let Ok((rest2, end_text)) = cell_ref_text(after_colon) {
182                if let Some(end) = CellAddr::parse(end_text) {
183                    let r = Ref::Range { sheet: Some(sheet), start: addr, end };
184                    return Ok((rest2, Expr::Reference(r, self.span(start, rest2))));
185                }
186            }
187        }
188        let r = Ref::Cell { sheet: Some(sheet), addr };
189        Ok((rest, Expr::Reference(r, self.span(start, rest))))
190    }
191
192    /// Parse `'Sheet Name'!A1` / `'Sheet Name'!A1:B2`. `i` starts at the
193    /// opening quote. `''` inside the quotes is an escaped single quote.
194    fn parse_quoted_sheet_ref(&self, i: &'a str) -> IResult<&'a str, Expr> {
195        let inner = &i[1..];
196        let mut sheet = String::new();
197        let mut idx = 0;
198        loop {
199            match inner[idx..].find('\'') {
200                // Unterminated quoted sheet name
201                None => {
202                    return Err(nom::Err::Error(nom::error::Error::new(
203                        i,
204                        nom::error::ErrorKind::Char,
205                    )));
206                }
207                Some(q) => {
208                    sheet.push_str(&inner[idx..idx + q]);
209                    let after = idx + q + 1;
210                    if inner[after..].starts_with('\'') {
211                        sheet.push('\'');
212                        idx = after + 1;
213                    } else {
214                        idx = after;
215                        break;
216                    }
217                }
218            }
219        }
220        let rest = &inner[idx..];
221        if sheet.is_empty() {
222            return Err(nom::Err::Error(nom::error::Error::new(
223                i,
224                nom::error::ErrorKind::Char,
225            )));
226        }
227        match rest.strip_prefix('!') {
228            Some(after_bang) => self.parse_ref_body(i, sheet, after_bang),
229            None => Err(nom::Err::Error(nom::error::Error::new(
230                rest,
231                nom::error::ErrorKind::Char,
232            ))),
233        }
234    }
235
236    fn parse_arg_list(&self, i: &'a str) -> IResult<&'a str, Vec<Expr>> {
237        let mut args = Vec::new();
238        let mut rest = multispace0(i)?.0;
239
240        if rest.starts_with(')') {
241            return Ok((rest, args));
242        }
243
244        // Parse first argument (may be empty if it starts with comma or close paren)
245        let ws = multispace0(rest)?.0;
246        if ws.starts_with(',') || ws.starts_with(')') {
247            // Empty first argument
248            args.push(Expr::Variable(String::new(), Span::new(0, 0)));
249        } else {
250            let (r, first) = self.parse_comparison(rest)?;
251            args.push(first);
252            rest = r;
253        }
254
255        loop {
256            rest = multispace0(rest)?.0;
257            if let Some(after_comma) = rest.strip_prefix(',') {
258                let after_ws = multispace0(after_comma)?.0;
259                if after_ws.starts_with(',') || after_ws.starts_with(')') {
260                    // Empty argument
261                    args.push(Expr::Variable(String::new(), Span::new(0, 0)));
262                    rest = after_comma;
263                } else {
264                    let (r, arg) = self.parse_comparison(after_comma)?;
265                    args.push(arg);
266                    rest = r;
267                }
268            } else {
269                break;
270            }
271        }
272
273        Ok((rest, args))
274    }
275
276    fn parse_array_elements(&self, i: &'a str) -> IResult<&'a str, Vec<Expr>> {
277        let mut rows: Vec<Vec<Expr>> = Vec::new();
278        let mut current_row: Vec<Expr> = Vec::new();
279        let mut rest = multispace0(i)?.0;
280        if rest.starts_with('}') {
281            return Ok((rest, Vec::new())); // empty array {}
282        }
283        let (r, first) = self.parse_comparison(rest)?;
284        current_row.push(first);
285        rest = r;
286        loop {
287            rest = multispace0(rest)?.0;
288            if let Some(after_comma) = rest.strip_prefix(',') {
289                let (r, elem) = self.parse_comparison(after_comma)?;
290                current_row.push(elem);
291                rest = r;
292            } else if let Some(after_semi) = rest.strip_prefix(';') {
293                rows.push(std::mem::take(&mut current_row));
294                let (r, elem) = self.parse_comparison(after_semi)?;
295                current_row.push(elem);
296                rest = r;
297            } else {
298                break;
299            }
300        }
301        rows.push(current_row);
302        // If only one row (no semicolons), return flat vec
303        if rows.len() == 1 {
304            return Ok((rest, rows.into_iter().next().unwrap()));
305        }
306        // Multiple rows → wrap each row in an Array node
307        let span_start = i;
308        let row_exprs: Vec<Expr> = rows
309            .into_iter()
310            .map(|row_elems| {
311                let s = self.span(span_start, rest);
312                Expr::Array(row_elems, s)
313            })
314            .collect();
315        Ok((rest, row_exprs))
316    }
317
318    // ── postfix % ─────────────────────────────────────────────────────────
319
320    fn parse_postfix(&self, i: &'a str) -> IResult<&'a str, Expr> {
321        let (rest, expr) = self.parse_primary(i)?;
322        let rest_ws = multispace0(rest)?.0;
323        if let Some(after) = rest_ws.strip_prefix('%') {
324            return Ok((after, Expr::UnaryOp {
325                op: UnaryOp::Percent,
326                operand: Box::new(expr),
327                span: self.span(i, after),
328            }));
329        }
330        Ok((rest, expr))
331    }
332
333    // ── unary minus ───────────────────────────────────────────────────────
334
335    fn parse_unary(&self, i: &'a str) -> IResult<&'a str, Expr> {
336        let i_ws = multispace0(i)?.0;
337        if let Some(after_minus) = i_ws.strip_prefix('-') {
338            let (rest, operand) = self.parse_unary(after_minus)?;
339            return Ok((rest, Expr::UnaryOp {
340                op: UnaryOp::Neg,
341                operand: Box::new(operand),
342                span: self.span(i_ws, rest),
343            }));
344        }
345        self.parse_postfix(i)
346    }
347
348    // ── power ^ (right-associative) ───────────────────────────────────────
349
350    fn parse_power(&self, i: &'a str) -> IResult<&'a str, Expr> {
351        let (rest, left) = self.parse_unary(i)?;
352        let rest_ws = multispace0(rest)?.0;
353        if let Some(after_op) = rest_ws.strip_prefix('^') {
354            let (rest2, right) = self.parse_power(after_op)?;
355            return Ok((rest2, Expr::BinaryOp {
356                op: BinaryOp::Pow,
357                left: Box::new(left),
358                right: Box::new(right),
359                span: self.span(i, rest2),
360            }));
361        }
362        Ok((rest, left))
363    }
364
365    // ── multiplicative * / ────────────────────────────────────────────────
366
367    fn parse_multiplicative(&self, i: &'a str) -> IResult<&'a str, Expr> {
368        let (mut rest, mut left) = self.parse_power(i)?;
369        loop {
370            let ws = multispace0(rest)?.0;
371            let op = ws.strip_prefix('*').map(|after| (BinaryOp::Mul, after))
372                .or_else(|| ws.strip_prefix('/').map(|after| (BinaryOp::Div, after)));
373            match op {
374                None => break,
375                Some((op, after)) => {
376                    let (r, right) = self.parse_power(after)?;
377                    left = Expr::BinaryOp {
378                        op,
379                        span: self.span(i, r),
380                        left: Box::new(left),
381                        right: Box::new(right),
382                    };
383                    rest = r;
384                }
385            }
386        }
387        Ok((rest, left))
388    }
389
390    // ── additive + - ──────────────────────────────────────────────────────
391
392    fn parse_additive(&self, i: &'a str) -> IResult<&'a str, Expr> {
393        let (mut rest, mut left) = self.parse_multiplicative(i)?;
394        loop {
395            let ws = multispace0(rest)?.0;
396            let op = ws.strip_prefix('+').map(|after| (BinaryOp::Add, after))
397                .or_else(|| ws.strip_prefix('-').map(|after| (BinaryOp::Sub, after)));
398            match op {
399                None => break,
400                Some((op, after)) => {
401                    let (r, right) = self.parse_multiplicative(after)?;
402                    left = Expr::BinaryOp {
403                        op,
404                        span: self.span(i, r),
405                        left: Box::new(left),
406                        right: Box::new(right),
407                    };
408                    rest = r;
409                }
410            }
411        }
412        Ok((rest, left))
413    }
414
415    // ── concat & ─────────────────────────────────────────────────────────
416
417    fn parse_concat(&self, i: &'a str) -> IResult<&'a str, Expr> {
418        let (mut rest, mut left) = self.parse_additive(i)?;
419        loop {
420            let ws = multispace0(rest)?.0;
421            if let Some(after) = ws.strip_prefix('&') {
422                let (r, right) = self.parse_additive(after)?;
423                left = Expr::BinaryOp {
424                    op: BinaryOp::Concat,
425                    span: self.span(i, r),
426                    left: Box::new(left),
427                    right: Box::new(right),
428                };
429                rest = r;
430            } else {
431                break;
432            }
433        }
434        Ok((rest, left))
435    }
436
437    // ── comparison = <> < > <= >= ─────────────────────────────────────────
438
439    fn parse_comparison(&self, i: &'a str) -> IResult<&'a str, Expr> {
440        let (rest, left) = self.parse_concat(i)?;
441        let ws = multispace0(rest)?.0;
442
443        // Longest match first
444        let op_result: Option<(BinaryOp, &'a str)> = if let Some(after) = ws.strip_prefix("<>") {
445            Some((BinaryOp::Ne, after))
446        } else if let Some(after) = ws.strip_prefix("<=") {
447            Some((BinaryOp::Le, after))
448        } else if let Some(after) = ws.strip_prefix(">=") {
449            Some((BinaryOp::Ge, after))
450        } else if let Some(after) = ws.strip_prefix('<') {
451            Some((BinaryOp::Lt, after))
452        } else if let Some(after) = ws.strip_prefix('>') {
453            Some((BinaryOp::Gt, after))
454        } else if let Some(after) = ws.strip_prefix('=') {
455            Some((BinaryOp::Eq, after))
456        } else {
457            None
458        };
459
460        if let Some((op, after)) = op_result {
461            let (r, right) = self.parse_concat(after)?;
462            return Ok((r, Expr::BinaryOp {
463                op,
464                span: self.span(i, r),
465                left: Box::new(left),
466                right: Box::new(right),
467            }));
468        }
469
470        Ok((rest, left))
471    }
472}
473
474// ── public API ──────────────────────────────────────────────────────────────
475
476/// Parse a formula string into an expression tree.
477///
478/// The formula must start with `=`. Returns a [`ParseError`] if the input
479/// is not a valid formula.
480#[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")]
481pub fn parse(formula: &str) -> Result<Expr, ParseError> {
482    parse_formula(formula)
483}
484
485/// Crate-internal parser entry point used by [`crate::Engine`].
486pub(crate) fn parse_formula(formula: &str) -> Result<Expr, ParseError> {
487    let input = formula.strip_prefix('=').unwrap_or(formula).trim();
488    let p = Parser::new(formula);
489    match p.parse_comparison(input) {
490        Ok((rest, expr)) => {
491            let rest = rest.trim();
492            if rest.is_empty() {
493                Ok(expr)
494            } else {
495                Err(ParseError {
496                    message: format!("Unexpected input '{}'", rest),
497                    position: offset(formula, rest),
498                })
499            }
500        }
501        Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => Err(ParseError {
502            message: "Parse error".into(),
503            position: offset(formula, e.input),
504        }),
505        Err(nom::Err::Incomplete(_)) => Err(ParseError {
506            message: "Incomplete input".into(),
507            position: formula.len(),
508        }),
509    }
510}
511
512/// Validate that a formula string is syntactically correct without returning the AST.
513#[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")]
514pub fn validate(formula: &str) -> Result<(), ParseError> {
515    parse_formula(formula).map(|_| ())
516}
517
518#[cfg(test)]
519mod tests {
520    use super::*;
521    use super::parse_formula as parse;
522    use crate::parser::ast::{BinaryOp, Expr, UnaryOp};
523
524    #[test]
525    fn parse_number_literal() {
526        let expr = parse("=42").unwrap();
527        assert!(matches!(expr, Expr::Number(n, _) if n == 42.0));
528    }
529
530    #[test]
531    fn parse_binary_add() {
532        let expr = parse("=1+2").unwrap();
533        assert!(matches!(expr, Expr::BinaryOp { op: BinaryOp::Add, .. }));
534    }
535
536    #[test]
537    fn parse_precedence() {
538        // 2+3*4 should parse as 2+(3*4)
539        let expr = parse("=2+3*4").unwrap();
540        match expr {
541            Expr::BinaryOp { op: BinaryOp::Add, right, .. } => {
542                assert!(matches!(*right, Expr::BinaryOp { op: BinaryOp::Mul, .. }));
543            }
544            _ => panic!("Expected Add at top"),
545        }
546    }
547
548    #[test]
549    fn parse_function_call() {
550        let expr = parse("=SUM(1,2,3)").unwrap();
551        match expr {
552            Expr::FunctionCall { name, args, .. } => {
553                assert_eq!(name, "SUM");
554                assert_eq!(args.len(), 3);
555            }
556            _ => panic!("Expected FunctionCall"),
557        }
558    }
559
560    #[test]
561    fn parse_percent() {
562        let expr = parse("=50%").unwrap();
563        assert!(matches!(expr, Expr::UnaryOp { op: UnaryOp::Percent, .. }));
564    }
565
566    #[test]
567    fn parse_string_literal() {
568        let expr = parse("=\"hello\"").unwrap();
569        assert!(matches!(expr, Expr::Text(ref s, _) if s == "hello"));
570    }
571
572    #[test]
573    fn parse_concat_op() {
574        let expr = parse("=\"a\"&\"b\"").unwrap();
575        assert!(matches!(expr, Expr::BinaryOp { op: BinaryOp::Concat, .. }));
576    }
577
578    #[test]
579    #[allow(deprecated)]
580    fn validate_incomplete_fails() {
581        let err = validate("=SUM(1,").unwrap_err();
582        assert!(!err.message.is_empty());
583    }
584
585    #[test]
586    fn parse_nested() {
587        assert!(parse("=ROUND(SUM(1,2)*1.1, 1)").is_ok());
588    }
589
590    #[test]
591    fn parse_boolean() {
592        let expr = parse("=TRUE").unwrap();
593        assert!(matches!(expr, Expr::Bool(true, _)));
594    }
595
596    #[test]
597    fn parse_variable() {
598        let expr = parse("=myVar").unwrap();
599        assert!(matches!(expr, Expr::Variable(ref n, _) if n == "myVar"));
600    }
601
602    #[test]
603    fn parse_array_literal_numbers() {
604        let expr = parse("={1,2,3}").unwrap();
605        match expr {
606            Expr::Array(elems, _) => assert_eq!(elems.len(), 3),
607            _ => panic!("Expected Array"),
608        }
609    }
610
611    #[test]
612    fn parse_array_literal_mixed() {
613        let expr = parse("={1,\"hello\",TRUE}").unwrap();
614        assert!(matches!(expr, Expr::Array(_, _)));
615    }
616
617    #[test]
618    fn parse_array_literal_empty() {
619        let expr = parse("={}").unwrap();
620        assert!(matches!(expr, Expr::Array(ref e, _) if e.is_empty()));
621    }
622
623    #[test]
624    fn parse_array_in_function_call() {
625        let expr = parse("=SUM({1,2,3})").unwrap();
626        match expr {
627            Expr::FunctionCall { name, args, .. } => {
628                assert_eq!(name, "SUM");
629                assert_eq!(args.len(), 1);
630                assert!(matches!(args[0], Expr::Array(_, _)));
631            }
632            _ => panic!("Expected FunctionCall"),
633        }
634    }
635
636    #[test]
637    fn parse_power_right_assoc() {
638        // 2^3^2 = 2^(3^2) = 2^9 = 512 (right-associative)
639        let expr = parse("=2^3^2").unwrap();
640        match expr {
641            Expr::BinaryOp { op: BinaryOp::Pow, right, .. } => {
642                assert!(matches!(*right, Expr::BinaryOp { op: BinaryOp::Pow, .. }));
643            }
644            _ => panic!("Expected Pow at top"),
645        }
646    }
647}