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, 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        // Quoted-sheet reference: 'Sheet Name'!A1 / 'Sheet Name'!A1:B2
91        if i.starts_with('\'') {
92            return self.parse_quoted_sheet_ref(i);
93        }
94
95        // $-anchored cell reference (bare, no sheet): $A$1, $A1, A$1. A
96        // '$'-bearing token can only ever be a cell/range reference (never a
97        // sheet name, function call, or plain variable — none of those can
98        // contain '$'), so it short-circuits straight to Expr::Variable,
99        // mirroring how plain `A1` becomes Expr::Variable("A1", ..) below.
100        if let Ok((rest, span)) = dollar_cell_ref(i) {
101            let rest_ws = multispace0(rest)?.0;
102            if let Some(after_colon) = rest_ws.strip_prefix(':') {
103                if let Ok((rest2, end_span)) = cell_ref_text(after_colon) {
104                    if CellAddr::parse(end_span).is_some() {
105                        let range_name = format!("{}:{}", span, end_span);
106                        return Ok((rest2, Expr::Variable(range_name, self.span(i, rest2))));
107                    }
108                }
109            }
110            return Ok((rest, Expr::Variable(span.to_string(), self.span(i, rest))));
111        }
112
113        // Identifier: sheet-qualified reference, variable, or function call
114        if let Ok((rest, name)) = identifier(i) {
115            // Sheet-qualified reference: Sheet1!A1 / Sheet1!A1:B2 — `!` binds
116            // tightly to the sheet name (no whitespace on either side).
117            if let Some(after_bang) = rest.strip_prefix('!') {
118                return self.parse_ref_body(i, name.to_string(), after_bang);
119            }
120            let rest_ws = multispace0(rest)?.0;
121            if let Some(args_input) = rest_ws.strip_prefix('(') {
122                // Function call
123                let (rest2, args) = self.parse_arg_list(args_input)?;
124                let rest2 = multispace0(rest2)?.0;
125                if let Some(after_close) = rest2.strip_prefix(')') {
126                    let func_expr = Expr::FunctionCall {
127                        name: name.to_uppercase(),
128                        args,
129                        span: self.span(i, after_close),
130                    };
131                    // Detect immediately-invoked call: FUNC(lambda_args)(call_args)
132                    let after_ws = multispace0(after_close)?.0;
133                    if let Some(call_input) = after_ws.strip_prefix('(') {
134                        let (rest3, call_args) = self.parse_arg_list(call_input)?;
135                        let rest3 = multispace0(rest3)?.0;
136                        if let Some(after) = rest3.strip_prefix(')') {
137                            return Ok((after, Expr::Apply {
138                                func: Box::new(func_expr),
139                                call_args,
140                                span: self.span(i, after),
141                            }));
142                        }
143                        return Err(nom::Err::Error(nom::error::Error::new(
144                            rest3,
145                            nom::error::ErrorKind::Char,
146                        )));
147                    }
148                    return Ok((after_close, func_expr));
149                }
150                return Err(nom::Err::Error(nom::error::Error::new(
151                    rest2,
152                    nom::error::ErrorKind::Char,
153                )));
154            }
155            // Range reference: A1:D4 (end corner may itself be $-anchored,
156            // e.g. A1:$D$4 — validated via the same CellAddr::parse used
157            // for the dollar-led branch above, so both paths agree on shape).
158            if CellAddr::parse(name).is_some() {
159                if let Some(after_colon) = rest_ws.strip_prefix(':') {
160                    if let Ok((rest2, name2)) = cell_ref_text(after_colon) {
161                        if CellAddr::parse(name2).is_some() {
162                            let range_name = format!("{}:{}", name, name2);
163                            return Ok((rest2, Expr::Variable(range_name, self.span(i, rest2))));
164                        }
165                    }
166                }
167            }
168            return Ok((rest, Expr::Variable(name.to_string(), self.span(i, rest))));
169        }
170
171        Err(nom::Err::Error(nom::error::Error::new(i, nom::error::ErrorKind::Alt)))
172    }
173
174    // ── sheet-qualified references ──────────────────────────────────────
175
176    /// Parse the part after `!`: a cell address, optionally `:cell` for a
177    /// range. `start` is where the whole reference began (for spans); `sheet`
178    /// is the unescaped sheet name.
179    fn parse_ref_body(&self, start: &'a str, sheet: String, i: &'a str) -> IResult<&'a str, Expr> {
180        let err = || nom::Err::Error(nom::error::Error::new(i, nom::error::ErrorKind::Tag));
181        let (rest, cell_text) = cell_ref_text(i).map_err(|_| err())?;
182        let addr = CellAddr::parse(cell_text).ok_or_else(err)?;
183        // Optional range tail, mirroring the bare `A1:D4` grammar below.
184        let rest_ws = multispace0(rest)?.0;
185        if let Some(after_colon) = rest_ws.strip_prefix(':') {
186            if let Ok((rest2, end_text)) = cell_ref_text(after_colon) {
187                if let Some(end) = CellAddr::parse(end_text) {
188                    let r = Ref::Range { sheet: Some(sheet), start: addr, end };
189                    return Ok((rest2, Expr::Reference(r, self.span(start, rest2))));
190                }
191            }
192        }
193        let r = Ref::Cell { sheet: Some(sheet), addr };
194        Ok((rest, Expr::Reference(r, self.span(start, rest))))
195    }
196
197    /// Parse `'Sheet Name'!A1` / `'Sheet Name'!A1:B2`. `i` starts at the
198    /// opening quote. `''` inside the quotes is an escaped single quote.
199    fn parse_quoted_sheet_ref(&self, i: &'a str) -> IResult<&'a str, Expr> {
200        let inner = &i[1..];
201        let mut sheet = String::new();
202        let mut idx = 0;
203        loop {
204            match inner[idx..].find('\'') {
205                // Unterminated quoted sheet name
206                None => {
207                    return Err(nom::Err::Error(nom::error::Error::new(
208                        i,
209                        nom::error::ErrorKind::Char,
210                    )));
211                }
212                Some(q) => {
213                    sheet.push_str(&inner[idx..idx + q]);
214                    let after = idx + q + 1;
215                    if inner[after..].starts_with('\'') {
216                        sheet.push('\'');
217                        idx = after + 1;
218                    } else {
219                        idx = after;
220                        break;
221                    }
222                }
223            }
224        }
225        let rest = &inner[idx..];
226        if sheet.is_empty() {
227            return Err(nom::Err::Error(nom::error::Error::new(
228                i,
229                nom::error::ErrorKind::Char,
230            )));
231        }
232        match rest.strip_prefix('!') {
233            Some(after_bang) => self.parse_ref_body(i, sheet, after_bang),
234            None => Err(nom::Err::Error(nom::error::Error::new(
235                rest,
236                nom::error::ErrorKind::Char,
237            ))),
238        }
239    }
240
241    fn parse_arg_list(&self, i: &'a str) -> IResult<&'a str, Vec<Expr>> {
242        let mut args = Vec::new();
243        let mut rest = multispace0(i)?.0;
244
245        if rest.starts_with(')') {
246            return Ok((rest, args));
247        }
248
249        // Parse first argument (may be empty if it starts with comma or close paren)
250        let ws = multispace0(rest)?.0;
251        if ws.starts_with(',') || ws.starts_with(')') {
252            // Empty first argument
253            args.push(Expr::Variable(String::new(), Span::new(0, 0)));
254        } else {
255            let (r, first) = self.parse_comparison(rest)?;
256            args.push(first);
257            rest = r;
258        }
259
260        loop {
261            rest = multispace0(rest)?.0;
262            if let Some(after_comma) = rest.strip_prefix(',') {
263                let after_ws = multispace0(after_comma)?.0;
264                if after_ws.starts_with(',') || after_ws.starts_with(')') {
265                    // Empty argument
266                    args.push(Expr::Variable(String::new(), Span::new(0, 0)));
267                    rest = after_comma;
268                } else {
269                    // Parse from the first non-whitespace token, not from
270                    // just after the comma — a compound (BinaryOp) argument's
271                    // span is measured from its entry point here, so passing
272                    // the untrimmed `after_comma` would make the span start
273                    // at the separating whitespace instead of the argument's
274                    // own first token.
275                    let (r, arg) = self.parse_comparison(after_ws)?;
276                    args.push(arg);
277                    rest = r;
278                }
279            } else {
280                break;
281            }
282        }
283
284        Ok((rest, args))
285    }
286
287    fn parse_array_elements(&self, i: &'a str) -> IResult<&'a str, Vec<Expr>> {
288        let mut rows: Vec<Vec<Expr>> = Vec::new();
289        let mut current_row: Vec<Expr> = Vec::new();
290        let mut rest = multispace0(i)?.0;
291        if rest.starts_with('}') {
292            return Ok((rest, Vec::new())); // empty array {}
293        }
294        let (r, first) = self.parse_comparison(rest)?;
295        current_row.push(first);
296        rest = r;
297        loop {
298            rest = multispace0(rest)?.0;
299            if let Some(after_comma) = rest.strip_prefix(',') {
300                // Parse from the first non-whitespace token, not from just
301                // after the comma — same leading-whitespace bug as #746's
302                // function-argument fix, but here in the array-element
303                // separator: passing the untrimmed `after_comma` would let a
304                // compound (BinaryOp) element's span start at the separating
305                // whitespace instead of the element's own first token.
306                let after_ws = multispace0(after_comma)?.0;
307                let (r, elem) = self.parse_comparison(after_ws)?;
308                current_row.push(elem);
309                rest = r;
310            } else if let Some(after_semi) = rest.strip_prefix(';') {
311                rows.push(std::mem::take(&mut current_row));
312                // Same trim as the comma branch above, for the first element
313                // of the new row.
314                let after_ws = multispace0(after_semi)?.0;
315                let (r, elem) = self.parse_comparison(after_ws)?;
316                current_row.push(elem);
317                rest = r;
318            } else {
319                break;
320            }
321        }
322        rows.push(current_row);
323        // If only one row (no semicolons), return flat vec
324        if rows.len() == 1 {
325            return Ok((rest, rows.into_iter().next().unwrap()));
326        }
327        // Multiple rows → wrap each row in an Array node. Each row's span
328        // must cover only that row's own elements (its first element's start
329        // to its last element's end) — not the whole `{…}` body, which is
330        // what every row got when this span was computed once outside the
331        // loop below.
332        let row_exprs: Vec<Expr> = rows
333            .into_iter()
334            .map(|row_elems| {
335                let s = match (row_elems.first(), row_elems.last()) {
336                    (Some(first), Some(last)) => {
337                        let start = first.span().offset;
338                        let end = last.span().offset + last.span().length;
339                        Span::new(start, end - start)
340                    }
341                    // A row is never empty in practice (each row starts with
342                    // an element pushed either before the loop or right
343                    // after a `;`), but fall back to the old whole-body span
344                    // rather than panic if that ever changes.
345                    _ => self.span(i, rest),
346                };
347                Expr::Array(row_elems, s)
348            })
349            .collect();
350        Ok((rest, row_exprs))
351    }
352
353    // ── postfix % ─────────────────────────────────────────────────────────
354
355    fn parse_postfix(&self, i: &'a str) -> IResult<&'a str, Expr> {
356        let (rest, expr) = self.parse_primary(i)?;
357        let rest_ws = multispace0(rest)?.0;
358        if let Some(after) = rest_ws.strip_prefix('%') {
359            return Ok((after, Expr::UnaryOp {
360                op: UnaryOp::Percent,
361                operand: Box::new(expr),
362                span: self.span(i, after),
363            }));
364        }
365        Ok((rest, expr))
366    }
367
368    // ── unary minus ───────────────────────────────────────────────────────
369
370    fn parse_unary(&self, i: &'a str) -> IResult<&'a str, Expr> {
371        let i_ws = multispace0(i)?.0;
372        if let Some(after_minus) = i_ws.strip_prefix('-') {
373            let (rest, operand) = self.parse_unary(after_minus)?;
374            return Ok((rest, Expr::UnaryOp {
375                op: UnaryOp::Neg,
376                operand: Box::new(operand),
377                span: self.span(i_ws, rest),
378            }));
379        }
380        self.parse_postfix(i)
381    }
382
383    // ── power ^ (right-associative) ───────────────────────────────────────
384
385    fn parse_power(&self, i: &'a str) -> IResult<&'a str, Expr> {
386        let (rest, left) = self.parse_unary(i)?;
387        let rest_ws = multispace0(rest)?.0;
388        if let Some(after_op) = rest_ws.strip_prefix('^') {
389            let (rest2, right) = self.parse_power(after_op)?;
390            return Ok((rest2, Expr::BinaryOp {
391                op: BinaryOp::Pow,
392                left: Box::new(left),
393                right: Box::new(right),
394                span: self.span(i, rest2),
395            }));
396        }
397        Ok((rest, left))
398    }
399
400    // ── multiplicative * / ────────────────────────────────────────────────
401
402    fn parse_multiplicative(&self, i: &'a str) -> IResult<&'a str, Expr> {
403        let (mut rest, mut left) = self.parse_power(i)?;
404        loop {
405            let ws = multispace0(rest)?.0;
406            let op = ws.strip_prefix('*').map(|after| (BinaryOp::Mul, after))
407                .or_else(|| ws.strip_prefix('/').map(|after| (BinaryOp::Div, after)));
408            match op {
409                None => break,
410                Some((op, after)) => {
411                    let (r, right) = self.parse_power(after)?;
412                    left = Expr::BinaryOp {
413                        op,
414                        span: self.span(i, r),
415                        left: Box::new(left),
416                        right: Box::new(right),
417                    };
418                    rest = r;
419                }
420            }
421        }
422        Ok((rest, left))
423    }
424
425    // ── additive + - ──────────────────────────────────────────────────────
426
427    fn parse_additive(&self, i: &'a str) -> IResult<&'a str, Expr> {
428        let (mut rest, mut left) = self.parse_multiplicative(i)?;
429        loop {
430            let ws = multispace0(rest)?.0;
431            let op = ws.strip_prefix('+').map(|after| (BinaryOp::Add, after))
432                .or_else(|| ws.strip_prefix('-').map(|after| (BinaryOp::Sub, after)));
433            match op {
434                None => break,
435                Some((op, after)) => {
436                    let (r, right) = self.parse_multiplicative(after)?;
437                    left = Expr::BinaryOp {
438                        op,
439                        span: self.span(i, r),
440                        left: Box::new(left),
441                        right: Box::new(right),
442                    };
443                    rest = r;
444                }
445            }
446        }
447        Ok((rest, left))
448    }
449
450    // ── concat & ─────────────────────────────────────────────────────────
451
452    fn parse_concat(&self, i: &'a str) -> IResult<&'a str, Expr> {
453        let (mut rest, mut left) = self.parse_additive(i)?;
454        loop {
455            let ws = multispace0(rest)?.0;
456            if let Some(after) = ws.strip_prefix('&') {
457                let (r, right) = self.parse_additive(after)?;
458                left = Expr::BinaryOp {
459                    op: BinaryOp::Concat,
460                    span: self.span(i, r),
461                    left: Box::new(left),
462                    right: Box::new(right),
463                };
464                rest = r;
465            } else {
466                break;
467            }
468        }
469        Ok((rest, left))
470    }
471
472    // ── comparison = <> < > <= >= ─────────────────────────────────────────
473
474    fn parse_comparison(&self, i: &'a str) -> IResult<&'a str, Expr> {
475        let (rest, left) = self.parse_concat(i)?;
476        let ws = multispace0(rest)?.0;
477
478        // Longest match first
479        let op_result: Option<(BinaryOp, &'a str)> = if let Some(after) = ws.strip_prefix("<>") {
480            Some((BinaryOp::Ne, after))
481        } else if let Some(after) = ws.strip_prefix("<=") {
482            Some((BinaryOp::Le, after))
483        } else if let Some(after) = ws.strip_prefix(">=") {
484            Some((BinaryOp::Ge, after))
485        } else if let Some(after) = ws.strip_prefix('<') {
486            Some((BinaryOp::Lt, after))
487        } else if let Some(after) = ws.strip_prefix('>') {
488            Some((BinaryOp::Gt, after))
489        } else if let Some(after) = ws.strip_prefix('=') {
490            Some((BinaryOp::Eq, after))
491        } else {
492            None
493        };
494
495        if let Some((op, after)) = op_result {
496            let (r, right) = self.parse_concat(after)?;
497            return Ok((r, Expr::BinaryOp {
498                op,
499                span: self.span(i, r),
500                left: Box::new(left),
501                right: Box::new(right),
502            }));
503        }
504
505        Ok((rest, left))
506    }
507}
508
509// ── public API ──────────────────────────────────────────────────────────────
510
511/// Parse a formula string into an expression tree.
512///
513/// The formula must start with `=`. Returns a [`ParseError`] if the input
514/// is not a valid formula.
515#[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")]
516pub fn parse(formula: &str) -> Result<Expr, ParseError> {
517    parse_formula(formula)
518}
519
520/// Crate-internal parser entry point used by [`crate::Engine`].
521pub(crate) fn parse_formula(formula: &str) -> Result<Expr, ParseError> {
522    let input = formula.strip_prefix('=').unwrap_or(formula).trim();
523    let p = Parser::new(formula);
524    match p.parse_comparison(input) {
525        Ok((rest, expr)) => {
526            let rest = rest.trim();
527            if rest.is_empty() {
528                Ok(expr)
529            } else {
530                Err(ParseError {
531                    message: format!("Unexpected input '{}'", rest),
532                    position: offset(formula, rest),
533                })
534            }
535        }
536        Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => Err(ParseError {
537            message: "Parse error".into(),
538            position: offset(formula, e.input),
539        }),
540        Err(nom::Err::Incomplete(_)) => Err(ParseError {
541            message: "Incomplete input".into(),
542            position: formula.len(),
543        }),
544    }
545}
546
547/// Validate that a formula string is syntactically correct without returning the AST.
548#[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")]
549pub fn validate(formula: &str) -> Result<(), ParseError> {
550    parse_formula(formula).map(|_| ())
551}
552
553#[cfg(test)]
554mod tests {
555    use super::*;
556    use super::parse_formula as parse;
557    use crate::parser::ast::{BinaryOp, Expr, UnaryOp};
558
559    #[test]
560    fn parse_number_literal() {
561        let expr = parse("=42").unwrap();
562        assert!(matches!(expr, Expr::Number(n, _) if n == 42.0));
563    }
564
565    #[test]
566    fn parse_binary_add() {
567        let expr = parse("=1+2").unwrap();
568        assert!(matches!(expr, Expr::BinaryOp { op: BinaryOp::Add, .. }));
569    }
570
571    #[test]
572    fn parse_precedence() {
573        // 2+3*4 should parse as 2+(3*4)
574        let expr = parse("=2+3*4").unwrap();
575        match expr {
576            Expr::BinaryOp { op: BinaryOp::Add, right, .. } => {
577                assert!(matches!(*right, Expr::BinaryOp { op: BinaryOp::Mul, .. }));
578            }
579            _ => panic!("Expected Add at top"),
580        }
581    }
582
583    #[test]
584    fn parse_function_call() {
585        let expr = parse("=SUM(1,2,3)").unwrap();
586        match expr {
587            Expr::FunctionCall { name, args, .. } => {
588                assert_eq!(name, "SUM");
589                assert_eq!(args.len(), 3);
590            }
591            _ => panic!("Expected FunctionCall"),
592        }
593    }
594
595    #[test]
596    fn parse_function_call_arg_spans_no_leading_space() {
597        // A BinaryOp argument in 2nd+ position must span exactly its own
598        // tokens — not the whitespace after the preceding comma (issue #746).
599        let src = "=SUM(X1*2, X2*3, X3*4)";
600        let expr = parse(src).unwrap();
601        let slice = |s: &Span| &src[s.offset..s.offset + s.length];
602        match expr {
603            Expr::FunctionCall { name, args, .. } => {
604                assert_eq!(name, "SUM");
605                assert_eq!(args.len(), 3);
606                assert_eq!(slice(args[0].span()), "X1*2");
607                assert_eq!(slice(args[1].span()), "X2*3");
608                assert_eq!(slice(args[2].span()), "X3*4");
609            }
610            _ => panic!("Expected FunctionCall"),
611        }
612    }
613
614    #[test]
615    fn parse_function_call_arg_spans_no_space_after_comma_unaffected() {
616        // Gap noted in review: when there's no whitespace after the comma at
617        // all (`after_ws == after_comma`), the fix must be a no-op — args
618        // still slice to exactly their own tokens.
619        let src = "=SUM(X1*2,X2*3)";
620        let expr = parse(src).unwrap();
621        let slice = |s: &Span| &src[s.offset..s.offset + s.length];
622        match expr {
623            Expr::FunctionCall { name, args, .. } => {
624                assert_eq!(name, "SUM");
625                assert_eq!(args.len(), 2);
626                assert_eq!(slice(args[0].span()), "X1*2");
627                assert_eq!(slice(args[1].span()), "X2*3");
628            }
629            _ => panic!("Expected FunctionCall"),
630        }
631    }
632
633    #[test]
634    fn parse_parenthesised_expr_span_no_leading_space() {
635        // A padded grouping `( A1 + B1 )` must not leak the space after '(' into
636        // the inner expression's span (issue #751 — same class as #746/#748/#749).
637        let src = "=( A1 + B1 )";
638        let expr = parse(src).unwrap();
639        assert!(matches!(expr, Expr::BinaryOp { .. }));
640        let sp = expr.span();
641        assert_eq!(&src[sp.offset..sp.offset + sp.length], "A1 + B1");
642        // Extra whitespace + a different precedence level, still clean.
643        let src2 = "=(  A1*B1  )";
644        let expr2 = parse(src2).unwrap();
645        let sp2 = expr2.span();
646        assert_eq!(&src2[sp2.offset..sp2.offset + sp2.length], "A1*B1");
647    }
648
649    #[test]
650    fn parse_function_call_nested_and_unary_arg_spans_unaffected() {
651        // Nested-call and unary args were already correct — confirm the
652        // leading-whitespace fix doesn't disturb them.
653        let src = "=SUM(1, MAX(2,3), 4)";
654        let expr = parse(src).unwrap();
655        let slice = |s: &Span| &src[s.offset..s.offset + s.length];
656        match expr {
657            Expr::FunctionCall { name, args, .. } => {
658                assert_eq!(name, "SUM");
659                assert_eq!(args.len(), 3);
660                assert_eq!(slice(args[0].span()), "1");
661                assert_eq!(slice(args[1].span()), "MAX(2,3)");
662                assert_eq!(slice(args[2].span()), "4");
663            }
664            _ => panic!("Expected FunctionCall"),
665        }
666
667        let src2 = "=SUM(1, -2)";
668        let expr2 = parse(src2).unwrap();
669        let slice2 = |s: &Span| &src2[s.offset..s.offset + s.length];
670        match expr2 {
671            Expr::FunctionCall { name, args, .. } => {
672                assert_eq!(name, "SUM");
673                assert_eq!(args.len(), 2);
674                assert_eq!(slice2(args[0].span()), "1");
675                assert_eq!(slice2(args[1].span()), "-2");
676            }
677            _ => panic!("Expected FunctionCall"),
678        }
679    }
680
681    #[test]
682    fn parse_percent() {
683        let expr = parse("=50%").unwrap();
684        assert!(matches!(expr, Expr::UnaryOp { op: UnaryOp::Percent, .. }));
685    }
686
687    #[test]
688    fn parse_string_literal() {
689        let expr = parse("=\"hello\"").unwrap();
690        assert!(matches!(expr, Expr::Text(ref s, _) if s == "hello"));
691    }
692
693    #[test]
694    fn parse_concat_op() {
695        let expr = parse("=\"a\"&\"b\"").unwrap();
696        assert!(matches!(expr, Expr::BinaryOp { op: BinaryOp::Concat, .. }));
697    }
698
699    #[test]
700    #[allow(deprecated)]
701    fn validate_incomplete_fails() {
702        let err = validate("=SUM(1,").unwrap_err();
703        assert!(!err.message.is_empty());
704    }
705
706    #[test]
707    fn parse_nested() {
708        assert!(parse("=ROUND(SUM(1,2)*1.1, 1)").is_ok());
709    }
710
711    #[test]
712    fn parse_boolean() {
713        let expr = parse("=TRUE").unwrap();
714        assert!(matches!(expr, Expr::Bool(true, _)));
715    }
716
717    #[test]
718    fn parse_variable() {
719        let expr = parse("=myVar").unwrap();
720        assert!(matches!(expr, Expr::Variable(ref n, _) if n == "myVar"));
721    }
722
723    #[test]
724    fn parse_array_literal_numbers() {
725        let expr = parse("={1,2,3}").unwrap();
726        match expr {
727            Expr::Array(elems, _) => assert_eq!(elems.len(), 3),
728            _ => panic!("Expected Array"),
729        }
730    }
731
732    #[test]
733    fn parse_array_literal_mixed() {
734        let expr = parse("={1,\"hello\",TRUE}").unwrap();
735        assert!(matches!(expr, Expr::Array(_, _)));
736    }
737
738    #[test]
739    fn parse_array_literal_empty() {
740        let expr = parse("={}").unwrap();
741        assert!(matches!(expr, Expr::Array(ref e, _) if e.is_empty()));
742    }
743
744    #[test]
745    fn parse_array_literal_row_spans() {
746        // Each row's span must cover only that row's own elements, not the
747        // whole `{...}` body (issue #745).
748        let src = "={1,2;3,4}";
749        let expr = parse(src).unwrap();
750        let slice = |s: &Span| &src[s.offset..s.offset + s.length];
751
752        let (rows, outer_span) = match &expr {
753            Expr::Array(rows, span) => (rows, span),
754            _ => panic!("Expected outer Array"),
755        };
756        assert_eq!(slice(outer_span), "{1,2;3,4}");
757        assert_eq!(rows.len(), 2);
758
759        let (row0_elems, row0_span) = match &rows[0] {
760            Expr::Array(elems, span) => (elems, span),
761            _ => panic!("Expected row 0 to be an Array"),
762        };
763        assert_eq!(slice(row0_span), "1,2");
764        assert_eq!(row0_elems.len(), 2);
765        assert_eq!(slice(row0_elems[0].span()), "1");
766        assert_eq!(slice(row0_elems[1].span()), "2");
767
768        let (row1_elems, row1_span) = match &rows[1] {
769            Expr::Array(elems, span) => (elems, span),
770            _ => panic!("Expected row 1 to be an Array"),
771        };
772        assert_eq!(slice(row1_span), "3,4");
773        assert_eq!(row1_elems.len(), 2);
774        assert_eq!(slice(row1_elems[0].span()), "3");
775        assert_eq!(slice(row1_elems[1].span()), "4");
776    }
777
778    #[test]
779    fn parse_array_literal_single_row_unaffected() {
780        // A single-row array (no semicolons) returns a flat Vec<Expr> — no
781        // row-wrapper Array nodes — and must be unaffected by the row-span
782        // fix (issue #745).
783        let src = "={1,2,3}";
784        let expr = parse(src).unwrap();
785        let slice = |s: &Span| &src[s.offset..s.offset + s.length];
786        match &expr {
787            Expr::Array(elems, span) => {
788                assert_eq!(slice(span), "{1,2,3}");
789                assert_eq!(elems.len(), 3);
790                assert_eq!(slice(elems[0].span()), "1");
791                assert_eq!(slice(elems[1].span()), "2");
792                assert_eq!(slice(elems[2].span()), "3");
793                for e in elems {
794                    assert!(!matches!(e, Expr::Array(_, _)), "single-row elements must not be row-wrapped");
795                }
796            }
797            _ => panic!("Expected Array"),
798        }
799    }
800
801    #[test]
802    fn parse_array_literal_element_no_leading_space() {
803        // A compound (BinaryOp) element after a `;` (or `,`) separator must
804        // span exactly its own tokens — not the whitespace after the
805        // separator. Same root cause as issue #746's function-argument bug,
806        // here in the sibling `parse_array_elements`.
807        let src = "={1; 2+3}";
808        let expr = parse(src).unwrap();
809        let slice = |s: &Span| &src[s.offset..s.offset + s.length];
810
811        let rows = match &expr {
812            Expr::Array(rows, _) => rows,
813            _ => panic!("Expected outer Array"),
814        };
815        assert_eq!(rows.len(), 2);
816        let row1_elems = match &rows[1] {
817            Expr::Array(elems, _) => elems,
818            _ => panic!("Expected row 1 to be an Array"),
819        };
820        assert_eq!(row1_elems.len(), 1);
821        assert_eq!(slice(row1_elems[0].span()), "2+3");
822    }
823
824    #[test]
825    fn parse_array_literal_multi_row_element_no_leading_space() {
826        // Same as above, but with a compound element after a `,` in each
827        // row of a multi-row array — every compound element must slice
828        // minimally, with no leading whitespace from its separator.
829        let src = "={1, 2+3; 4, 5*6}";
830        let expr = parse(src).unwrap();
831        let slice = |s: &Span| &src[s.offset..s.offset + s.length];
832
833        let rows = match &expr {
834            Expr::Array(rows, _) => rows,
835            _ => panic!("Expected outer Array"),
836        };
837        assert_eq!(rows.len(), 2);
838
839        let row0_elems = match &rows[0] {
840            Expr::Array(elems, _) => elems,
841            _ => panic!("Expected row 0 to be an Array"),
842        };
843        assert_eq!(slice(row0_elems[0].span()), "1");
844        assert_eq!(slice(row0_elems[1].span()), "2+3");
845
846        let row1_elems = match &rows[1] {
847            Expr::Array(elems, _) => elems,
848            _ => panic!("Expected row 1 to be an Array"),
849        };
850        assert_eq!(slice(row1_elems[0].span()), "4");
851        assert_eq!(slice(row1_elems[1].span()), "5*6");
852    }
853
854    #[test]
855    fn parse_array_literal_single_element_rows() {
856        // Gap noted in review: single-element-per-row arrays must produce
857        // flat one-element rows with correct spans.
858        let src = "={1;2}";
859        let expr = parse(src).unwrap();
860        let slice = |s: &Span| &src[s.offset..s.offset + s.length];
861
862        let rows = match &expr {
863            Expr::Array(rows, span) => {
864                assert_eq!(slice(span), "{1;2}");
865                rows
866            }
867            _ => panic!("Expected outer Array"),
868        };
869        assert_eq!(rows.len(), 2);
870
871        let (row0_elems, row0_span) = match &rows[0] {
872            Expr::Array(elems, span) => (elems, span),
873            _ => panic!("Expected row 0 to be an Array"),
874        };
875        assert_eq!(slice(row0_span), "1");
876        assert_eq!(row0_elems.len(), 1);
877        assert_eq!(slice(row0_elems[0].span()), "1");
878
879        let (row1_elems, row1_span) = match &rows[1] {
880            Expr::Array(elems, span) => (elems, span),
881            _ => panic!("Expected row 1 to be an Array"),
882        };
883        assert_eq!(slice(row1_span), "2");
884        assert_eq!(row1_elems.len(), 1);
885        assert_eq!(slice(row1_elems[0].span()), "2");
886    }
887
888    #[test]
889    fn parse_array_in_function_call() {
890        let expr = parse("=SUM({1,2,3})").unwrap();
891        match expr {
892            Expr::FunctionCall { name, args, .. } => {
893                assert_eq!(name, "SUM");
894                assert_eq!(args.len(), 1);
895                assert!(matches!(args[0], Expr::Array(_, _)));
896            }
897            _ => panic!("Expected FunctionCall"),
898        }
899    }
900
901    #[test]
902    fn parse_power_right_assoc() {
903        // 2^3^2 = 2^(3^2) = 2^9 = 512 (right-associative)
904        let expr = parse("=2^3^2").unwrap();
905        match expr {
906            Expr::BinaryOp { op: BinaryOp::Pow, right, .. } => {
907                assert!(matches!(*right, Expr::BinaryOp { op: BinaryOp::Pow, .. }));
908            }
909            _ => panic!("Expected Pow at top"),
910        }
911    }
912}