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