Skip to main content

formualizer_parse/
parser.rs

1use crate::ParserLimits;
2use crate::structured_ref;
3use crate::tokenizer::{
4    Associativity, Token, TokenSpan, TokenStream, TokenSubType, TokenType, TokenizerError,
5};
6use crate::types::{FormulaDialect, ParsingError};
7use crate::{ExcelError, LiteralValue};
8
9#[cfg(feature = "serde")]
10use serde::{Deserialize, Serialize};
11
12use crate::hasher::FormulaHasher;
13use formualizer_common::coord::{
14    col_index_from_letters_1based, col_letters_from_1based, parse_a1_1based,
15};
16use formualizer_common::{
17    AxisBound, RelativeCoord, SheetCellRef, SheetLocator, SheetRangeRef, SheetRef,
18};
19use once_cell::sync::Lazy;
20use smallvec::SmallVec;
21use std::borrow::Cow;
22use std::error::Error;
23use std::fmt::{self, Display};
24use std::hash::{Hash, Hasher};
25use std::str::FromStr;
26use std::sync::Arc;
27
28type VolatilityFn = dyn Fn(&str) -> bool + Send + Sync + 'static;
29type VolatilityClassifierBox = Box<VolatilityFn>;
30type VolatilityClassifierArc = Arc<VolatilityFn>;
31
32/// A custom error type for the parser.
33#[derive(Debug)]
34pub struct ParserError {
35    pub message: String,
36    /// Byte offset into the formula source where the error was found, when
37    /// known. At the end of the input it is the source length.
38    pub position: Option<usize>,
39}
40
41impl Display for ParserError {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        if let Some(pos) = self.position {
44            write!(f, "ParserError at position {}: {}", pos, self.message)
45        } else {
46            write!(f, "ParserError: {}", self.message)
47        }
48    }
49}
50
51impl Error for ParserError {}
52
53// Column lookup table for common columns (A-ZZ = 702 columns)
54static COLUMN_LOOKUP: Lazy<Vec<String>> = Lazy::new(|| {
55    let mut cols = Vec::with_capacity(702);
56    // Single letters A-Z
57    for c in b'A'..=b'Z' {
58        cols.push(String::from(c as char));
59    }
60    // Double letters AA-ZZ
61    for c1 in b'A'..=b'Z' {
62        for c2 in b'A'..=b'Z' {
63            cols.push(format!("{}{}", c1 as char, c2 as char));
64        }
65    }
66    cols
67});
68
69/// A structured table reference specifier for accessing specific parts of a table
70#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
71#[derive(Debug, Clone, PartialEq, Hash)]
72pub enum TableSpecifier {
73    /// The entire table
74    All,
75    /// The data area of the table (no headers or totals)
76    Data,
77    /// The headers row
78    Headers,
79    /// The totals row
80    Totals,
81    /// A specific row
82    Row(TableRowSpecifier),
83    /// A specific column
84    Column(String),
85    /// A range of columns
86    ColumnRange(String, String),
87    /// Special items like #Headers, #Data, #Totals, etc.
88    SpecialItem(SpecialItem),
89    /// A combination of specifiers, for complex references
90    Combination(Vec<Box<TableSpecifier>>),
91}
92
93/// Specifies which row(s) to use in a table reference
94#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
95#[derive(Debug, Clone, PartialEq, Hash)]
96pub enum TableRowSpecifier {
97    /// The current row (context dependent)
98    Current,
99    /// All rows
100    All,
101    /// Data rows only
102    Data,
103    /// Headers row
104    Headers,
105    /// Totals row
106    Totals,
107    /// Specific row by index (1-based)
108    Index(u32),
109}
110
111/// Special items in structured references
112#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
113#[derive(Debug, Clone, PartialEq, Hash)]
114pub enum SpecialItem {
115    /// The #Headers item
116    Headers,
117    /// The #Data item
118    Data,
119    /// The #Totals item
120    Totals,
121    /// The #All item (the whole table)
122    All,
123    /// The @ item (current row)
124    ThisRow,
125}
126
127/// A reference to a table including specifiers
128#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
129#[derive(Debug, Clone, PartialEq, Hash)]
130pub struct TableReference {
131    /// The name of the table
132    pub name: String,
133    /// Optional specifier for which part of the table to use
134    pub specifier: Option<TableSpecifier>,
135}
136
137#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
138#[derive(Debug, Clone, PartialEq, Hash)]
139pub enum ExternalBookRef {
140    Token(String),
141}
142
143impl ExternalBookRef {
144    pub fn token(&self) -> &str {
145        match self {
146            ExternalBookRef::Token(s) => s,
147        }
148    }
149}
150
151#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
153pub enum ExternalRefKind {
154    Cell {
155        row: u32,
156        col: u32,
157        row_abs: bool,
158        col_abs: bool,
159    },
160    Range {
161        start_row: Option<u32>,
162        start_col: Option<u32>,
163        end_row: Option<u32>,
164        end_col: Option<u32>,
165        start_row_abs: bool,
166        start_col_abs: bool,
167        end_row_abs: bool,
168        end_col_abs: bool,
169    },
170}
171
172impl ExternalRefKind {
173    pub fn cell(row: u32, col: u32) -> Self {
174        Self::Cell {
175            row,
176            col,
177            row_abs: false,
178            col_abs: false,
179        }
180    }
181
182    pub fn cell_with_abs(row: u32, col: u32, row_abs: bool, col_abs: bool) -> Self {
183        Self::Cell {
184            row,
185            col,
186            row_abs,
187            col_abs,
188        }
189    }
190
191    pub fn range(
192        start_row: Option<u32>,
193        start_col: Option<u32>,
194        end_row: Option<u32>,
195        end_col: Option<u32>,
196    ) -> Self {
197        Self::Range {
198            start_row,
199            start_col,
200            end_row,
201            end_col,
202            start_row_abs: false,
203            start_col_abs: false,
204            end_row_abs: false,
205            end_col_abs: false,
206        }
207    }
208
209    #[expect(
210        clippy::too_many_arguments,
211        reason = "constructor mirroring the enum fields; an explicit signature keeps callers readable"
212    )]
213    pub fn range_with_abs(
214        start_row: Option<u32>,
215        start_col: Option<u32>,
216        end_row: Option<u32>,
217        end_col: Option<u32>,
218        start_row_abs: bool,
219        start_col_abs: bool,
220        end_row_abs: bool,
221        end_col_abs: bool,
222    ) -> Self {
223        Self::Range {
224            start_row,
225            start_col,
226            end_row,
227            end_col,
228            start_row_abs,
229            start_col_abs,
230            end_row_abs,
231            end_col_abs,
232        }
233    }
234}
235
236#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
237#[derive(Debug, Clone, PartialEq, Hash)]
238pub struct ExternalReference {
239    pub raw: String,
240    pub book: ExternalBookRef,
241    pub sheet: String,
242    pub kind: ExternalRefKind,
243}
244
245/// A reference to something outside the cell.
246#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
247#[derive(Debug, Clone, PartialEq, Hash)]
248pub enum ReferenceType {
249    Cell {
250        sheet: Option<String>,
251        row: u32,
252        col: u32,
253        row_abs: bool,
254        col_abs: bool,
255    },
256    Range {
257        sheet: Option<String>,
258        start_row: Option<u32>,
259        start_col: Option<u32>,
260        end_row: Option<u32>,
261        end_col: Option<u32>,
262        start_row_abs: bool,
263        start_col_abs: bool,
264        end_row_abs: bool,
265        end_col_abs: bool,
266    },
267    /// 3D cell reference (`Sheet1:Sheet3!A1`).
268    ///
269    /// Excel evaluates aggregating functions across each sheet between
270    /// `sheet_first` and `sheet_last` (inclusive) at the same cell address.
271    Cell3D {
272        sheet_first: String,
273        sheet_last: String,
274        row: u32,
275        col: u32,
276        row_abs: bool,
277        col_abs: bool,
278    },
279    /// 3D range reference (`Sheet1:Sheet3!A1:B2`).
280    Range3D {
281        sheet_first: String,
282        sheet_last: String,
283        start_row: Option<u32>,
284        start_col: Option<u32>,
285        end_row: Option<u32>,
286        end_col: Option<u32>,
287        start_row_abs: bool,
288        start_col_abs: bool,
289        end_row_abs: bool,
290        end_col_abs: bool,
291    },
292    External(ExternalReference),
293    Table(TableReference),
294    NamedRange(String),
295}
296
297impl Display for TableSpecifier {
298    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
299        match self {
300            TableSpecifier::All => write!(f, "#All"),
301            TableSpecifier::Data => write!(f, "#Data"),
302            TableSpecifier::Headers => write!(f, "#Headers"),
303            TableSpecifier::Totals => write!(f, "#Totals"),
304            TableSpecifier::Row(row) => write!(f, "{row}"),
305            TableSpecifier::Column(column) => write!(f, "{column}"),
306            TableSpecifier::ColumnRange(start, end) => write!(f, "{start}:{end}"),
307            TableSpecifier::SpecialItem(item) => write!(f, "{item}"),
308            TableSpecifier::Combination(specs) => {
309                // Emit nested bracketed parts so the surrounding Table formatter prints
310                // canonical structured refs like Table[[#Headers],[Column1]:[Column2]].
311                // ColumnRange children must split their bracket boundary across
312                // both endpoints (`[A]:[B]`) rather than wrapping the whole
313                // range in one bracket pair.
314                let mut first = true;
315                for spec in specs {
316                    if !first {
317                        write!(f, ",")?;
318                    }
319                    first = false;
320                    match spec.as_ref() {
321                        TableSpecifier::ColumnRange(start, end) => {
322                            write!(f, "[{start}]:[{end}]")?;
323                        }
324                        other => write!(f, "[{other}]")?,
325                    }
326                }
327                Ok(())
328            }
329        }
330    }
331}
332
333impl Display for TableRowSpecifier {
334    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
335        match self {
336            TableRowSpecifier::Current => write!(f, "@"),
337            TableRowSpecifier::All => write!(f, "#All"),
338            TableRowSpecifier::Data => write!(f, "#Data"),
339            TableRowSpecifier::Headers => write!(f, "#Headers"),
340            TableRowSpecifier::Totals => write!(f, "#Totals"),
341            TableRowSpecifier::Index(idx) => write!(f, "{idx}"),
342        }
343    }
344}
345
346impl Display for SpecialItem {
347    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348        match self {
349            SpecialItem::Headers => write!(f, "#Headers"),
350            SpecialItem::Data => write!(f, "#Data"),
351            SpecialItem::Totals => write!(f, "#Totals"),
352            SpecialItem::All => write!(f, "#All"),
353            SpecialItem::ThisRow => write!(f, "@"),
354        }
355    }
356}
357
358fn sheet_name_needs_quoting(name: &str) -> bool {
359    formualizer_common::a1_sheet_name_needs_quoting(name)
360}
361
362#[derive(Debug, Clone)]
363struct OpenFormulaRefPart {
364    sheet: Option<String>,
365    coord: String,
366}
367
368type AxisPartWithAbs = Option<(u32, bool)>;
369type RangePartWithAbs = (AxisPartWithAbs, AxisPartWithAbs);
370
371/// Result of extracting the sheet portion of a reference string.
372#[derive(Debug, Clone)]
373enum SheetSpec {
374    /// No sheet segment was present (e.g. plain `A1`).
375    None,
376    /// Standard single-sheet reference (`Sheet1!A1`, `'Sheet 1'!A1`).
377    Single(String),
378    /// Excel 3D sheet range (`Sheet1:Sheet3!A1`, `'Sheet 1':'Sheet 3'!A1`).
379    Range { first: String, last: String },
380}
381
382impl ReferenceType {
383    /// Build a cell reference with relative anchors.
384    pub fn cell(sheet: Option<String>, row: u32, col: u32) -> Self {
385        Self::Cell {
386            sheet,
387            row,
388            col,
389            row_abs: false,
390            col_abs: false,
391        }
392    }
393
394    /// Build a cell reference with explicit anchors.
395    pub fn cell_with_abs(
396        sheet: Option<String>,
397        row: u32,
398        col: u32,
399        row_abs: bool,
400        col_abs: bool,
401    ) -> Self {
402        Self::Cell {
403            sheet,
404            row,
405            col,
406            row_abs,
407            col_abs,
408        }
409    }
410
411    /// Build a range reference with relative anchors.
412    pub fn range(
413        sheet: Option<String>,
414        start_row: Option<u32>,
415        start_col: Option<u32>,
416        end_row: Option<u32>,
417        end_col: Option<u32>,
418    ) -> Self {
419        Self::Range {
420            sheet,
421            start_row,
422            start_col,
423            end_row,
424            end_col,
425            start_row_abs: false,
426            start_col_abs: false,
427            end_row_abs: false,
428            end_col_abs: false,
429        }
430    }
431
432    /// Build a range reference with explicit anchors.
433    #[expect(
434        clippy::too_many_arguments,
435        reason = "constructor mirroring the enum fields; an explicit signature keeps callers readable"
436    )]
437    pub fn range_with_abs(
438        sheet: Option<String>,
439        start_row: Option<u32>,
440        start_col: Option<u32>,
441        end_row: Option<u32>,
442        end_col: Option<u32>,
443        start_row_abs: bool,
444        start_col_abs: bool,
445        end_row_abs: bool,
446        end_col_abs: bool,
447    ) -> Self {
448        Self::Range {
449            sheet,
450            start_row,
451            start_col,
452            end_row,
453            end_col,
454            start_row_abs,
455            start_col_abs,
456            end_row_abs,
457            end_col_abs,
458        }
459    }
460
461    /// Create a reference from a string such as `A1`, `A:A`, `A1:B2`, or `Table1[Column]`.
462    pub fn from_string(reference: &str) -> Result<Self, ParsingError> {
463        Self::parse_excel_reference(reference)
464    }
465
466    /// Create a reference from a string using the specified formula dialect.
467    pub fn from_string_with_dialect(
468        reference: &str,
469        dialect: FormulaDialect,
470    ) -> Result<Self, ParsingError> {
471        match dialect {
472            FormulaDialect::Excel => Self::parse_excel_reference(reference),
473            FormulaDialect::OpenFormula => Self::parse_openformula_reference(reference)
474                .or_else(|_| Self::parse_excel_reference(reference)),
475        }
476    }
477
478    /// Parse a grid reference into a shared SheetRef, preserving $ anchors.
479    ///
480    /// Only cell and range references are supported. Table and named ranges return an error.
481    pub fn parse_sheet_ref(reference: &str) -> Result<SheetRef<'static>, ParsingError> {
482        Self::parse_sheet_ref_with_dialect(reference, FormulaDialect::Excel)
483    }
484
485    /// Parse a grid reference into a shared SheetRef using the specified dialect.
486    pub fn parse_sheet_ref_with_dialect(
487        reference: &str,
488        dialect: FormulaDialect,
489    ) -> Result<SheetRef<'static>, ParsingError> {
490        match dialect {
491            FormulaDialect::Excel => Self::parse_excel_sheet_ref(reference),
492            FormulaDialect::OpenFormula => Self::parse_openformula_sheet_ref(reference)
493                .or_else(|_| Self::parse_excel_sheet_ref(reference)),
494        }
495    }
496
497    /// Lossy conversion from parsed ReferenceType into SheetRef.
498    /// External, table, and named ranges are discarded; anchors are preserved.
499    pub fn to_sheet_ref_lossy(&self) -> Option<SheetRef<'_>> {
500        match self {
501            ReferenceType::Cell {
502                sheet,
503                row,
504                col,
505                row_abs,
506                col_abs,
507            } => {
508                let row0 = row.checked_sub(1)?;
509                let col0 = col.checked_sub(1)?;
510                let sheet_loc = match sheet.as_deref() {
511                    Some(name) => SheetLocator::from_name(name),
512                    None => SheetLocator::Current,
513                };
514                let coord = RelativeCoord::new(row0, col0, *row_abs, *col_abs);
515                Some(SheetRef::Cell(SheetCellRef::new(sheet_loc, coord)))
516            }
517            ReferenceType::Range {
518                sheet,
519                start_row,
520                start_col,
521                end_row,
522                end_col,
523                start_row_abs,
524                start_col_abs,
525                end_row_abs,
526                end_col_abs,
527            } => {
528                let sheet_loc = match sheet.as_deref() {
529                    Some(name) => SheetLocator::from_name(name),
530                    None => SheetLocator::Current,
531                };
532                let sr = start_row
533                    .and_then(|v| v.checked_sub(1).map(|i| AxisBound::new(i, *start_row_abs)));
534                if start_row.is_some() && sr.is_none() {
535                    return None;
536                }
537                let sc = start_col
538                    .and_then(|v| v.checked_sub(1).map(|i| AxisBound::new(i, *start_col_abs)));
539                if start_col.is_some() && sc.is_none() {
540                    return None;
541                }
542                let er =
543                    end_row.and_then(|v| v.checked_sub(1).map(|i| AxisBound::new(i, *end_row_abs)));
544                if end_row.is_some() && er.is_none() {
545                    return None;
546                }
547                let ec =
548                    end_col.and_then(|v| v.checked_sub(1).map(|i| AxisBound::new(i, *end_col_abs)));
549                if end_col.is_some() && ec.is_none() {
550                    return None;
551                }
552                let range = SheetRangeRef::from_parts(sheet_loc, sr, sc, er, ec).ok()?;
553                Some(SheetRef::Range(range))
554            }
555            _ => None,
556        }
557    }
558
559    fn parse_excel_sheet_ref(reference: &str) -> Result<SheetRef<'static>, ParsingError> {
560        let (spec, ref_part) = Self::extract_sheet_spec(reference);
561        let sheet = match spec {
562            SheetSpec::None => None,
563            SheetSpec::Single(name) => Some(name),
564            SheetSpec::Range { .. } => {
565                return Err(ParsingError::InvalidReference(
566                    "3D references are not supported for SheetRef".to_string(),
567                ));
568            }
569        };
570
571        if ref_part.contains('[') {
572            return Err(ParsingError::InvalidReference(
573                "Table references are not supported for SheetRef".to_string(),
574            ));
575        }
576
577        let sheet_loc: SheetLocator<'static> = match sheet {
578            Some(name) => SheetLocator::from_name(name),
579            None => SheetLocator::Current,
580        };
581
582        if let Some((start, end)) = ref_part.split_once(':') {
583            let (start_col, start_row) = Self::parse_range_part_with_abs(start)?;
584            let (end_col, end_row) = Self::parse_range_part_with_abs(end)?;
585
586            let start_col = Self::axis_bound_from_1based(start_col)?;
587            let start_row = Self::axis_bound_from_1based(start_row)?;
588            let end_col = Self::axis_bound_from_1based(end_col)?;
589            let end_row = Self::axis_bound_from_1based(end_row)?;
590
591            let range =
592                SheetRangeRef::from_parts(sheet_loc, start_row, start_col, end_row, end_col)
593                    .map_err(|err| ParsingError::InvalidReference(err.to_string()))?;
594            Ok(SheetRef::Range(range))
595        } else {
596            let (row, col, row_abs, col_abs) = parse_a1_1based(&ref_part)
597                .map_err(|err| ParsingError::InvalidReference(err.to_string()))?;
598            #[expect(
599                clippy::arithmetic_side_effects,
600                reason = "parse_a1_1based returns row >= 1 and col >= 1"
601            )]
602            let coord = RelativeCoord::new(row - 1, col - 1, row_abs, col_abs);
603            Ok(SheetRef::Cell(SheetCellRef::new(sheet_loc, coord)))
604        }
605    }
606
607    fn parse_openformula_sheet_ref(reference: &str) -> Result<SheetRef<'static>, ParsingError> {
608        Self::parse_excel_sheet_ref(reference)
609    }
610
611    fn axis_bound_from_1based(
612        bound: Option<(u32, bool)>,
613    ) -> Result<Option<AxisBound>, ParsingError> {
614        match bound {
615            Some((index, abs)) => AxisBound::from_excel_1based(index, abs)
616                .map(Some)
617                .map_err(|err| ParsingError::InvalidReference(err.to_string())),
618            None => Ok(None),
619        }
620    }
621
622    #[expect(
623        clippy::arithmetic_side_effects,
624        clippy::string_slice,
625        reason = "cursor bounded by slice length; the column and row runs are sliced at ASCII positions found by the scan"
626    )]
627    fn parse_range_part_with_abs(part: &str) -> Result<RangePartWithAbs, ParsingError> {
628        if let Ok((row, col, row_abs, col_abs)) = parse_a1_1based(part) {
629            return Ok((Some((col, col_abs)), Some((row, row_abs))));
630        }
631
632        let bytes = part.as_bytes();
633        let len = bytes.len();
634        let mut i = 0usize;
635
636        let mut col_abs = false;
637        let mut row_abs = false;
638
639        if bytes.get(i) == Some(&b'$') {
640            col_abs = true;
641            i += 1;
642        }
643
644        let col_start = i;
645        while bytes.get(i).is_some_and(u8::is_ascii_alphabetic) {
646            i += 1;
647        }
648
649        if i > col_start {
650            let col_str = &part[col_start..i];
651            let col1 = Self::column_to_number(col_str)?;
652
653            if i == len {
654                return Ok((Some((col1, col_abs)), None));
655            }
656
657            if bytes.get(i) == Some(&b'$') {
658                row_abs = true;
659                i += 1;
660            }
661
662            if i >= len {
663                return Err(ParsingError::InvalidReference(format!(
664                    "Invalid range part: {part}"
665                )));
666            }
667
668            let row_start = i;
669            while bytes.get(i).is_some_and(u8::is_ascii_digit) {
670                i += 1;
671            }
672
673            if row_start == i || i != len {
674                return Err(ParsingError::InvalidReference(format!(
675                    "Invalid range part: {part}"
676                )));
677            }
678
679            let row_str = &part[row_start..i];
680            let row1 = row_str
681                .parse::<u32>()
682                .map_err(|_| ParsingError::InvalidReference(format!("Invalid row: {row_str}")))?;
683            if row1 == 0 {
684                return Err(ParsingError::InvalidReference(format!(
685                    "Invalid range part: {part}"
686                )));
687            }
688
689            return Ok((Some((col1, col_abs)), Some((row1, row_abs))));
690        }
691
692        i = 0;
693        if bytes.get(i) == Some(&b'$') {
694            row_abs = true;
695            i += 1;
696        }
697
698        let row_start = i;
699        while bytes.get(i).is_some_and(u8::is_ascii_digit) {
700            i += 1;
701        }
702
703        if row_start == i || i != len {
704            return Err(ParsingError::InvalidReference(format!(
705                "Invalid range part: {part}"
706            )));
707        }
708
709        let row_str = &part[row_start..i];
710        let row1 = row_str
711            .parse::<u32>()
712            .map_err(|_| ParsingError::InvalidReference(format!("Invalid row: {row_str}")))?;
713        if row1 == 0 {
714            return Err(ParsingError::InvalidReference(format!(
715                "Invalid range part: {part}"
716            )));
717        }
718
719        Ok((None, Some((row1, row_abs))))
720    }
721
722    fn parse_3d_reference(first: &str, last: &str, ref_part: &str) -> Result<Self, ParsingError> {
723        if first.is_empty() || last.is_empty() {
724            return Err(ParsingError::InvalidReference(format!(
725                "3D reference requires two sheet names: {first}:{last}!{ref_part}"
726            )));
727        }
728        if ref_part.is_empty() {
729            return Err(ParsingError::InvalidReference(format!(
730                "3D reference {first}:{last}! is missing a cell or range"
731            )));
732        }
733        // 3D refs cannot point at structured table tokens.
734        if ref_part.contains('[') {
735            return Err(ParsingError::InvalidReference(format!(
736                "3D reference {first}:{last}!{ref_part} cannot target a table"
737            )));
738        }
739
740        if let Some((start, end)) = ref_part.split_once(':') {
741            let (start_col, start_row) = Self::parse_range_part_with_abs(start)?;
742            let (end_col, end_row) = Self::parse_range_part_with_abs(end)?;
743
744            let split = |bound: Option<(u32, bool)>| match bound {
745                Some((index, abs)) => (Some(index), abs),
746                None => (None, false),
747            };
748            let (start_col, start_col_abs) = split(start_col);
749            let (start_row, start_row_abs) = split(start_row);
750            let (end_col, end_col_abs) = split(end_col);
751            let (end_row, end_row_abs) = split(end_row);
752
753            Ok(ReferenceType::Range3D {
754                sheet_first: first.to_string(),
755                sheet_last: last.to_string(),
756                start_row,
757                start_col,
758                end_row,
759                end_col,
760                start_row_abs,
761                start_col_abs,
762                end_row_abs,
763                end_col_abs,
764            })
765        } else {
766            let (col, row, col_abs, row_abs) =
767                Self::parse_cell_reference(ref_part).map_err(|_| {
768                    ParsingError::InvalidReference(format!(
769                        "Invalid 3D reference target: {ref_part}"
770                    ))
771                })?;
772            Ok(ReferenceType::Cell3D {
773                sheet_first: first.to_string(),
774                sheet_last: last.to_string(),
775                row,
776                col,
777                row_abs,
778                col_abs,
779            })
780        }
781    }
782
783    /// Strip every leading workbook-index-0 qualifier.
784    ///
785    /// Workbook index 0 is the workbook the formula lives in (external
786    /// workbooks are numbered from 1), as Excel stores it in OOXML:
787    /// `[0]!Name` is the workbook-scoped `Name`, `[0]Sheet1!A1` is
788    /// `Sheet1!A1` and `'[0]Sheet 1'!A1` is `'Sheet 1'!A1`. A prefix is
789    /// stripped only while a `!` follows it. This is a loop rather than
790    /// recursion so the number of prefixes cannot exhaust the stack, and the
791    /// quoted form allocates its result once.
792    #[inline]
793    fn strip_workbook_zero_prefixes(reference: &str) -> Cow<'_, str> {
794        // Almost no reference starts with a workbook-0 qualifier; test the
795        // prefix before scanning for `!`.
796        if !reference.starts_with("[0]") && !reference.starts_with("'[0]") {
797            return Cow::Borrowed(reference);
798        }
799        Self::strip_workbook_zero_prefixes_slow(reference)
800    }
801
802    #[cold]
803    #[inline(never)]
804    fn strip_workbook_zero_prefixes_slow(reference: &str) -> Cow<'_, str> {
805        // `tail` is always a suffix of `reference`, so it contains a `!`
806        // exactly when it is longer than the text after the last `!`.
807        let Some(after_last_bang) = reference.rsplit_once('!').map(|(_, after)| after.len()) else {
808            return Cow::Borrowed(reference);
809        };
810        let has_bang = |tail: &str| tail.len() > after_last_bang;
811
812        let mut tail = reference;
813        // Whether a leading `'` was taken off `tail` (the quoted form).
814        let mut quoted = false;
815        loop {
816            if !quoted
817                && let Some(rest) = tail.strip_prefix("[0]")
818                && has_bang(rest)
819            {
820                tail = rest.strip_prefix('!').unwrap_or(rest);
821            } else if !quoted
822                && let Some(rest) = tail.strip_prefix("'[0]")
823                && has_bang(rest)
824            {
825                quoted = true;
826                tail = rest;
827            } else if quoted
828                && let Some(rest) = tail.strip_prefix("[0]")
829                && has_bang(rest)
830            {
831                tail = rest;
832            } else {
833                break;
834            }
835        }
836        if quoted {
837            Cow::Owned(format!("'{tail}"))
838        } else {
839            Cow::Borrowed(tail)
840        }
841    }
842
843    fn parse_excel_reference(reference: &str) -> Result<Self, ParsingError> {
844        let reference = Self::strip_workbook_zero_prefixes(reference);
845        let reference = reference.as_ref();
846
847        // Excel structured reference shorthands that appear as a single bracketed token.
848        //
849        // We use these forms to avoid ambiguity with cell refs / named ranges:
850        // - `[TableName]` resolves to the table's data body (equivalent to `TableName[#Data]`).
851        // - `[@Column]` / `[@[Column Name]]` is a "This Row" selector; it requires table-aware
852        //   context during resolution and will be rewritten by the evaluator/graph builder.
853        if reference.starts_with('[') && reference.ends_with(']') && !reference.contains('!') {
854            return Self::parse_bracketed_structured_reference(reference);
855        }
856
857        // Extract sheet specification (none / single / 3D range) if present.
858        let (sheet_spec, ref_part) = Self::extract_sheet_spec(reference);
859
860        let sheet = match sheet_spec {
861            SheetSpec::None => None,
862            SheetSpec::Single(name) => Some(name),
863            // 3D references (`Sheet1:Sheet3!A1` / `Sheet1:Sheet3!A1:B2`) take a
864            // dedicated path because they cannot reuse the 2D Cell/Range carriers.
865            SheetSpec::Range { first, last } => {
866                return Self::parse_3d_reference(&first, &last, &ref_part);
867            }
868        };
869
870        // Table references live in the ref_part (e.g., "Table1[Column]").
871        // Sheet names can contain '[' for external workbook refs (e.g., "[1]Sheet1!A1").
872        if ref_part.contains('[') {
873            // Issue #76: R1C1-shaped operands like `R[1]C[2]`, `R1C[2]`, `RC[1]`
874            // contain `[` but are not table references. Without this gate they
875            // either misclassify as `Table { name: "R1C", specifier: Column("2") }`
876            // or get rejected by the structured-references trailing-garbage check.
877            // We don't add an R1C1 dialect; we just refuse to fabricate a table
878            // and fall back to the same `NamedRange` outcome that bracket-free
879            // R1C1 strings (e.g. `R1C1`, `RC`) already produce.
880            if Self::is_r1c1_shape(&ref_part) {
881                return Ok(ReferenceType::NamedRange(reference.to_string()));
882            }
883            return Self::parse_table_reference(&ref_part);
884        }
885
886        let external_sheet = sheet.as_deref().and_then(|s| {
887            // Excel external workbook refs embed a "[...]" token inside the sheet segment.
888            // Use the last '[' to allow paths/URIs that may contain earlier brackets, then
889            // take the first ']' after it to avoid being confused by ']' in the sheet name.
890            let lb = s.rfind('[')?;
891            let (_, sheet_name) = s.get(lb..)?.split_once(']')?;
892            // `sheet_name` is a suffix of `s`; what precedes it is the book
893            // token through its `]`.
894            let token = s.strip_suffix(sheet_name)?;
895            if sheet_name.is_empty() {
896                None
897            } else {
898                Some((token, sheet_name))
899            }
900        });
901
902        if let Some((start, end)) = ref_part.split_once(':') {
903            // Range reference
904            let (start_col, start_row) = Self::parse_range_part_with_abs(start)?;
905            let (end_col, end_row) = Self::parse_range_part_with_abs(end)?;
906
907            let split = |bound: Option<(u32, bool)>| match bound {
908                Some((index, abs)) => (Some(index), abs),
909                None => (None, false),
910            };
911            let (start_col, start_col_abs) = split(start_col);
912            let (start_row, start_row_abs) = split(start_row);
913            let (end_col, end_col_abs) = split(end_col);
914            let (end_row, end_row_abs) = split(end_row);
915
916            if let Some((book_token, sheet_name)) = external_sheet {
917                Ok(ReferenceType::External(ExternalReference {
918                    raw: reference.to_string(),
919                    book: ExternalBookRef::Token(book_token.to_string()),
920                    sheet: sheet_name.to_string(),
921                    kind: ExternalRefKind::Range {
922                        start_row,
923                        start_col,
924                        end_row,
925                        end_col,
926                        start_row_abs,
927                        start_col_abs,
928                        end_row_abs,
929                        end_col_abs,
930                    },
931                }))
932            } else {
933                Ok(ReferenceType::Range {
934                    sheet,
935                    start_row,
936                    start_col,
937                    end_row,
938                    end_col,
939                    start_row_abs,
940                    start_col_abs,
941                    end_row_abs,
942                    end_col_abs,
943                })
944            }
945        } else {
946            // Try to parse as a single cell reference
947            match Self::parse_cell_reference(&ref_part) {
948                Ok((col, row, col_abs, row_abs)) => {
949                    if let Some((book_token, sheet_name)) = external_sheet {
950                        Ok(ReferenceType::External(ExternalReference {
951                            raw: reference.to_string(),
952                            book: ExternalBookRef::Token(book_token.to_string()),
953                            sheet: sheet_name.to_string(),
954                            kind: ExternalRefKind::Cell {
955                                row,
956                                col,
957                                row_abs,
958                                col_abs,
959                            },
960                        }))
961                    } else {
962                        Ok(ReferenceType::Cell {
963                            sheet,
964                            row,
965                            col,
966                            row_abs,
967                            col_abs,
968                        })
969                    }
970                }
971                Err(_) => {
972                    // Treat it as a named range
973                    Ok(ReferenceType::NamedRange(reference.to_string()))
974                }
975            }
976        }
977    }
978
979    /// Parse a cell reference like "A1" into (column, row) using byte-based parsing.
980    fn parse_cell_reference(reference: &str) -> Result<(u32, u32, bool, bool), ParsingError> {
981        parse_a1_1based(reference)
982            .map(|(row, col, row_abs, col_abs)| (col, row, col_abs, row_abs))
983            .map_err(|_| {
984                ParsingError::InvalidReference(format!("Invalid cell reference: {reference}"))
985            })
986    }
987
988    /// Convert a column letter (e.g., "A", "BC") to a column number (1-based) using byte operations.
989    pub(crate) fn column_to_number(column: &str) -> Result<u32, ParsingError> {
990        col_index_from_letters_1based(column)
991            .map_err(|_| ParsingError::InvalidReference(format!("Invalid column: {column}")))
992    }
993
994    /// Convert a column number to a column letter using lookup table for common values.
995    pub(crate) fn number_to_column(num: u32) -> String {
996        if num == 0 {
997            return String::new();
998        }
999        // Use lookup table for common columns (1-702 covers A-ZZ)
1000        if let Some(letters) = num
1001            .checked_sub(1)
1002            .and_then(|index| COLUMN_LOOKUP.get(index as usize))
1003        {
1004            return letters.clone();
1005        }
1006
1007        col_letters_from_1based(num).unwrap_or_default()
1008    }
1009
1010    fn format_col(col: u32, abs: bool) -> String {
1011        if abs {
1012            format!("${}", Self::number_to_column(col))
1013        } else {
1014            Self::number_to_column(col)
1015        }
1016    }
1017
1018    fn format_row(row: u32, abs: bool) -> String {
1019        if abs {
1020            format!("${row}")
1021        } else {
1022            row.to_string()
1023        }
1024    }
1025}
1026
1027impl Display for ReferenceType {
1028    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1029        write!(
1030            f,
1031            "{}",
1032            match self {
1033                ReferenceType::Cell {
1034                    sheet,
1035                    row,
1036                    col,
1037                    row_abs,
1038                    col_abs,
1039                } => {
1040                    let col_str = Self::format_col(*col, *col_abs);
1041                    let row_str = Self::format_row(*row, *row_abs);
1042
1043                    if let Some(sheet_name) = sheet {
1044                        format!(
1045                            "{}!{col_str}{row_str}",
1046                            formualizer_common::format_a1_sheet_name(sheet_name)
1047                        )
1048                    } else {
1049                        format!("{col_str}{row_str}")
1050                    }
1051                }
1052                ReferenceType::Range {
1053                    sheet,
1054                    start_row,
1055                    start_col,
1056                    end_row,
1057                    end_col,
1058                    start_row_abs,
1059                    start_col_abs,
1060                    end_row_abs,
1061                    end_col_abs,
1062                } => {
1063                    // Format start reference
1064                    let start_ref = match (start_col, start_row) {
1065                        (Some(col), Some(row)) => format!(
1066                            "{}{}",
1067                            Self::format_col(*col, *start_col_abs),
1068                            Self::format_row(*row, *start_row_abs)
1069                        ),
1070                        (Some(col), None) => Self::format_col(*col, *start_col_abs),
1071                        (None, Some(row)) => Self::format_row(*row, *start_row_abs),
1072                        (None, None) => "".to_string(), // Should not happen in normal usage
1073                    };
1074
1075                    // Format end reference
1076                    let end_ref = match (end_col, end_row) {
1077                        (Some(col), Some(row)) => format!(
1078                            "{}{}",
1079                            Self::format_col(*col, *end_col_abs),
1080                            Self::format_row(*row, *end_row_abs)
1081                        ),
1082                        (Some(col), None) => Self::format_col(*col, *end_col_abs),
1083                        (None, Some(row)) => Self::format_row(*row, *end_row_abs),
1084                        (None, None) => "".to_string(), // Should not happen in normal usage
1085                    };
1086
1087                    let range_part = format!("{start_ref}:{end_ref}");
1088
1089                    if let Some(sheet_name) = sheet {
1090                        format!(
1091                            "{}!{range_part}",
1092                            formualizer_common::format_a1_sheet_name(sheet_name)
1093                        )
1094                    } else {
1095                        range_part
1096                    }
1097                }
1098                ReferenceType::Cell3D {
1099                    sheet_first,
1100                    sheet_last,
1101                    row,
1102                    col,
1103                    row_abs,
1104                    col_abs,
1105                } => {
1106                    let col_str = Self::format_col(*col, *col_abs);
1107                    let row_str = Self::format_row(*row, *row_abs);
1108                    let prefix = format_3d_sheet_prefix(sheet_first, sheet_last);
1109                    format!("{prefix}!{col_str}{row_str}")
1110                }
1111                ReferenceType::Range3D {
1112                    sheet_first,
1113                    sheet_last,
1114                    start_row,
1115                    start_col,
1116                    end_row,
1117                    end_col,
1118                    start_row_abs,
1119                    start_col_abs,
1120                    end_row_abs,
1121                    end_col_abs,
1122                } => {
1123                    let start_ref = match (start_col, start_row) {
1124                        (Some(col), Some(row)) => format!(
1125                            "{}{}",
1126                            Self::format_col(*col, *start_col_abs),
1127                            Self::format_row(*row, *start_row_abs)
1128                        ),
1129                        (Some(col), None) => Self::format_col(*col, *start_col_abs),
1130                        (None, Some(row)) => Self::format_row(*row, *start_row_abs),
1131                        (None, None) => "".to_string(),
1132                    };
1133                    let end_ref = match (end_col, end_row) {
1134                        (Some(col), Some(row)) => format!(
1135                            "{}{}",
1136                            Self::format_col(*col, *end_col_abs),
1137                            Self::format_row(*row, *end_row_abs)
1138                        ),
1139                        (Some(col), None) => Self::format_col(*col, *end_col_abs),
1140                        (None, Some(row)) => Self::format_row(*row, *end_row_abs),
1141                        (None, None) => "".to_string(),
1142                    };
1143                    let range_part = format!("{start_ref}:{end_ref}");
1144                    let prefix = format_3d_sheet_prefix(sheet_first, sheet_last);
1145                    format!("{prefix}!{range_part}")
1146                }
1147                ReferenceType::External(ext) => ext.raw.clone(),
1148                ReferenceType::Table(table_ref) => {
1149                    if let Some(specifier) = &table_ref.specifier {
1150                        // For table references, we need to handle column specifiers specially
1151                        // to remove leading/trailing whitespace
1152                        match specifier {
1153                            TableSpecifier::Column(column) => {
1154                                format!("{}[{}]", table_ref.name, column.trim())
1155                            }
1156                            TableSpecifier::ColumnRange(start, end) => {
1157                                format!("{}[{}:{}]", table_ref.name, start.trim(), end.trim())
1158                            }
1159                            _ => {
1160                                // For other specifiers, use the standard formatting
1161                                format!("{}[{}]", table_ref.name, specifier)
1162                            }
1163                        }
1164                    } else {
1165                        table_ref.name.clone()
1166                    }
1167                }
1168                ReferenceType::NamedRange(name) => name.clone(),
1169            }
1170        )
1171    }
1172}
1173
1174/// Render the `Sheet1:SheetN` portion of a 3D reference. When either name
1175/// needs quoting, Excel quotes the whole span as one segment
1176/// (`'Jan 24:Mar 24'!B5`), never each name on its own.
1177fn format_3d_sheet_prefix(first: &str, last: &str) -> String {
1178    if sheet_name_needs_quoting(first) || sheet_name_needs_quoting(last) {
1179        let escaped = format!("{first}:{last}").replace('\'', "''");
1180        format!("'{escaped}'")
1181    } else {
1182        format!("{first}:{last}")
1183    }
1184}
1185
1186impl TryFrom<&str> for ReferenceType {
1187    type Error = ParsingError;
1188
1189    fn try_from(value: &str) -> Result<Self, Self::Error> {
1190        ReferenceType::from_string(value)
1191    }
1192}
1193
1194impl FromStr for ReferenceType {
1195    type Err = ParsingError;
1196
1197    fn from_str(s: &str) -> Result<Self, Self::Err> {
1198        ReferenceType::from_string(s)
1199    }
1200}
1201
1202impl ReferenceType {
1203    /// Normalise the reference string (convert to canonical form)
1204    pub fn normalise(&self) -> String {
1205        format!("{self}")
1206    }
1207
1208    /// Read one sheet-name segment starting at `start`. Returns the parsed
1209    /// (unescaped) name, the byte offset directly after the closing quote
1210    /// (when quoted) or the last alphanumeric byte (when bare), and a flag
1211    /// indicating whether the segment was quoted.
1212    #[expect(
1213        clippy::arithmetic_side_effects,
1214        clippy::string_slice,
1215        reason = "cursor bounded by slice length; the name is sliced between ASCII quotes or up to an ASCII delimiter"
1216    )]
1217    fn read_sheet_segment(reference: &str, start: usize) -> Option<(String, usize, bool)> {
1218        let bytes = reference.as_bytes();
1219        let first = *bytes.get(start)?;
1220
1221        if first == b'\'' {
1222            // Quoted segment. Excel doubles a literal `'` inside the name.
1223            let mut i = start + 1;
1224            let body_start = i;
1225            while let Some(&b) = bytes.get(i) {
1226                if b == b'\'' {
1227                    if bytes.get(i + 1) == Some(&b'\'') {
1228                        i += 2;
1229                        continue;
1230                    }
1231                    let raw = &reference[body_start..i];
1232                    let name = raw.replace("''", "'");
1233                    return Some((name, i + 1, true));
1234                }
1235                i += 1;
1236            }
1237            None
1238        } else {
1239            // Bare segment. Sheet names cannot contain ':', '!', '\'', or any
1240            // ASCII-whitespace/operator characters in unquoted form.
1241            let mut i = start;
1242            while let Some(&b) = bytes.get(i) {
1243                match b {
1244                    b':' | b'!' | b'\'' | b' ' | b'\t' | b'\n' | b'\r' => break,
1245                    _ => i += 1,
1246                }
1247            }
1248            if i == start {
1249                None
1250            } else {
1251                Some((reference[start..i].to_string(), i, false))
1252            }
1253        }
1254    }
1255
1256    /// Extract sheet specification (none, single sheet, or 3D sheet range)
1257    /// from a reference string.
1258    #[expect(
1259        clippy::arithmetic_side_effects,
1260        clippy::string_slice,
1261        reason = "byte offset from ASCII delimiter search: after_first, after_second and bang index ASCII `:` or `!` bytes inside reference"
1262    )]
1263    fn extract_sheet_spec(reference: &str) -> (SheetSpec, String) {
1264        let Some((first_name, after_first, first_quoted)) = Self::read_sheet_segment(reference, 0)
1265        else {
1266            // No sheet segment recognised – fall back to looking for a bare
1267            // `!` separator (e.g. external book tokens such as `[1]Sheet!A1`).
1268            return Self::extract_sheet_spec_fallback(reference);
1269        };
1270        let bytes = reference.as_bytes();
1271
1272        // 3D form: Name1:Name2!...
1273        if bytes.get(after_first) == Some(&b':') {
1274            let second_start = after_first + 1;
1275            if let Some((second_name, after_second, _)) =
1276                Self::read_sheet_segment(reference, second_start)
1277                && bytes.get(after_second) == Some(&b'!')
1278            {
1279                let ref_part = reference[after_second + 1..].to_string();
1280                return (
1281                    SheetSpec::Range {
1282                        first: first_name,
1283                        last: second_name,
1284                    },
1285                    ref_part,
1286                );
1287            }
1288
1289            // The reference looks like the start of a 3D ref but the second
1290            // segment is malformed (e.g. `Sheet1:!A1`). Surface the broken
1291            // form as a 3D range with an empty `last` so the parser layer
1292            // can report a precise error rather than silently treating it as
1293            // a sheet name containing `:`.
1294            if let Some(bang) = reference
1295                .get(second_start..)
1296                .and_then(|rest| rest.find('!'))
1297            {
1298                let ref_part = reference[second_start + bang + 1..].to_string();
1299                return (
1300                    SheetSpec::Range {
1301                        first: first_name,
1302                        last: String::new(),
1303                    },
1304                    ref_part,
1305                );
1306            }
1307        }
1308
1309        // Single-sheet form: Name!...
1310        if bytes.get(after_first) == Some(&b'!') {
1311            let ref_part = reference[after_first + 1..].to_string();
1312            // Excel forbids ':' in sheet names, so one quoted segment holding
1313            // a single ':' is a 3D span written the way Excel writes it:
1314            // `'Jan 24:Mar 24'!B5`. A book path (`'C:\x\[Book.xlsx]S'!A1`)
1315            // is left alone.
1316            if first_quoted
1317                && !first_name.contains('[')
1318                && let Some((first, last)) = first_name.split_once(':')
1319                && !first.is_empty()
1320                && !last.is_empty()
1321                && !last.contains(':')
1322            {
1323                return (
1324                    SheetSpec::Range {
1325                        first: first.to_string(),
1326                        last: last.to_string(),
1327                    },
1328                    ref_part,
1329                );
1330            }
1331            return (SheetSpec::Single(first_name), ref_part);
1332        }
1333
1334        // The leading segment did not terminate in `!`; treat the whole input
1335        // as if no sheet were present and fall through to the legacy logic.
1336        Self::extract_sheet_spec_fallback(reference)
1337    }
1338
1339    fn extract_sheet_spec_fallback(reference: &str) -> (SheetSpec, String) {
1340        // Handle unquoted sheet names containing characters our segment
1341        // reader rejects (such as bracketed external workbook tokens, e.g.
1342        // `[1]Sheet1!A1`). The original implementation scanned for the first
1343        // `!` after byte 0; preserve that behaviour for compatibility.
1344        let mut chars = reference.chars();
1345        if chars.next().is_some()
1346            && let Some((_, ref_part)) = chars.as_str().split_once('!')
1347            && let Some(sheet) = reference
1348                .strip_suffix(ref_part)
1349                .and_then(|head| head.strip_suffix('!'))
1350        {
1351            return (SheetSpec::Single(sheet.to_string()), ref_part.to_string());
1352        }
1353
1354        (SheetSpec::None, reference.to_string())
1355    }
1356
1357    /// Detect R1C1-shaped operands so they aren't routed through the table-
1358    /// reference parser (issue #76).
1359    ///
1360    /// Matches `^R\d*(\[-?\d+\])?C\d*(\[-?\d+\])?$` and additionally requires
1361    /// the operand to contain at least one digit or bracket so that bare `R`,
1362    /// `C`, and `RC` (which already classify cleanly as `NamedRange` via the
1363    /// non-bracket path) are not pulled in here. Plain A1 cells like `R1`,
1364    /// `C5`, and `RC1` never reach this function because they don't contain
1365    /// `[` and are handled by the cell-reference path.
1366    #[expect(
1367        clippy::arithmetic_side_effects,
1368        reason = "cursor bounded by slice length"
1369    )]
1370    fn is_r1c1_shape(s: &str) -> bool {
1371        let bytes = s.as_bytes();
1372        let len = bytes.len();
1373        let mut i = 0usize;
1374        let mut anchored = false;
1375
1376        if bytes.get(i) != Some(&b'R') {
1377            return false;
1378        }
1379        i += 1;
1380
1381        let row_digits_start = i;
1382        while bytes.get(i).is_some_and(u8::is_ascii_digit) {
1383            i += 1;
1384        }
1385        if i > row_digits_start {
1386            anchored = true;
1387        }
1388
1389        if bytes.get(i) == Some(&b'[') {
1390            i += 1;
1391            if bytes.get(i) == Some(&b'-') {
1392                i += 1;
1393            }
1394            let n_start = i;
1395            while bytes.get(i).is_some_and(u8::is_ascii_digit) {
1396                i += 1;
1397            }
1398            if i == n_start || bytes.get(i) != Some(&b']') {
1399                return false;
1400            }
1401            i += 1;
1402            anchored = true;
1403        }
1404
1405        if bytes.get(i) != Some(&b'C') {
1406            return false;
1407        }
1408        i += 1;
1409
1410        let col_digits_start = i;
1411        while bytes.get(i).is_some_and(u8::is_ascii_digit) {
1412            i += 1;
1413        }
1414        if i > col_digits_start {
1415            anchored = true;
1416        }
1417
1418        if bytes.get(i) == Some(&b'[') {
1419            i += 1;
1420            if bytes.get(i) == Some(&b'-') {
1421                i += 1;
1422            }
1423            let n_start = i;
1424            while bytes.get(i).is_some_and(u8::is_ascii_digit) {
1425                i += 1;
1426            }
1427            if i == n_start || bytes.get(i) != Some(&b']') {
1428                return false;
1429            }
1430            i += 1;
1431            anchored = true;
1432        }
1433
1434        i == len && anchored
1435    }
1436
1437    /// Parse a table reference like "Table1[Column1]" or more complex ones
1438    /// like "Table1[[#All],[Column1]:[Column2]]".
1439    ///
1440    /// The specifier syntax is parsed by a real recursive-descent parser
1441    /// (`structured_ref::SpecifierParser`) following MS-XLSX §18.17.6.2.
1442    fn parse_table_reference(reference: &str) -> Result<Self, ParsingError> {
1443        let (table_name, specifier_str) = reference
1444            .find('[')
1445            .and_then(|bracket_pos| reference.split_at_checked(bracket_pos))
1446            .ok_or_else(|| {
1447                ParsingError::InvalidReference(format!(
1448                    "Missing '[' in table reference: {reference}"
1449                ))
1450            })?;
1451        let table_name = table_name.trim();
1452        if table_name.is_empty() {
1453            return Err(ParsingError::InvalidReference(reference.to_string()));
1454        }
1455
1456        let specifier = structured_ref::parse_full_specifier(specifier_str)?;
1457
1458        Ok(ReferenceType::Table(TableReference {
1459            name: table_name.to_string(),
1460            specifier,
1461        }))
1462    }
1463
1464    /// Handle the `[...]` shorthand that appears without a table name. The
1465    /// resolver/evaluator binds the implicit table from cell context.
1466    ///
1467    /// `[TableName]` is the data-body shorthand and is materialised as
1468    /// `Table { name = "TableName", specifier = #Data }`; everything else
1469    /// produces an unnamed `Table` carrying the parsed specifier verbatim.
1470    fn parse_bracketed_structured_reference(reference: &str) -> Result<Self, ParsingError> {
1471        debug_assert!(reference.starts_with('[') && reference.ends_with(']'));
1472        let specifier = structured_ref::parse_full_specifier(reference)?;
1473
1474        match specifier {
1475            Some(TableSpecifier::Column(name)) => Ok(ReferenceType::Table(TableReference {
1476                name,
1477                specifier: Some(TableSpecifier::SpecialItem(SpecialItem::Data)),
1478            })),
1479            other => Ok(ReferenceType::Table(TableReference {
1480                name: String::new(),
1481                specifier: other,
1482            })),
1483        }
1484    }
1485
1486    fn parse_openformula_reference(reference: &str) -> Result<Self, ParsingError> {
1487        if let Some(inner) = reference
1488            .strip_prefix('[')
1489            .and_then(|rest| rest.strip_suffix(']'))
1490        {
1491            if inner.is_empty() {
1492                return Err(ParsingError::InvalidReference(
1493                    "Empty OpenFormula reference".to_string(),
1494                ));
1495            }
1496
1497            let (start_part_str, end_part_str) = match inner.split_once(':') {
1498                Some((start, end)) => (start, Some(end)),
1499                None => (inner, None),
1500            };
1501
1502            let start_part = Self::parse_openformula_part(start_part_str)?;
1503            let end_part = if let Some(part) = end_part_str {
1504                Some(Self::parse_openformula_part(part)?)
1505            } else {
1506                None
1507            };
1508
1509            let sheet = match (&start_part.sheet, &end_part) {
1510                (Some(sheet), Some(end)) => {
1511                    if let Some(end_sheet) = &end.sheet
1512                        && end_sheet != sheet
1513                    {
1514                        return Err(ParsingError::InvalidReference(format!(
1515                            "Mismatched sheets in reference: {sheet} vs {end_sheet}"
1516                        )));
1517                    }
1518                    Some(sheet.clone())
1519                }
1520                (Some(sheet), None) => Some(sheet.clone()),
1521                (None, Some(end)) => end.sheet.clone(),
1522                (None, None) => None,
1523            };
1524
1525            let mut excel_like = String::new();
1526            if let Some(sheet_name) = sheet {
1527                if sheet_name_needs_quoting(&sheet_name) {
1528                    let escaped = sheet_name.replace('\'', "''");
1529                    excel_like.push('\'');
1530                    excel_like.push_str(&escaped);
1531                    excel_like.push('\'');
1532                } else {
1533                    excel_like.push_str(&sheet_name);
1534                }
1535                excel_like.push('!');
1536            }
1537
1538            excel_like.push_str(&start_part.coord);
1539            if let Some(end) = end_part {
1540                excel_like.push(':');
1541                excel_like.push_str(&end.coord);
1542            }
1543
1544            return Self::parse_excel_reference(&excel_like);
1545        }
1546
1547        Err(ParsingError::InvalidReference(format!(
1548            "Unsupported OpenFormula reference: {reference}"
1549        )))
1550    }
1551
1552    fn parse_openformula_part(part: &str) -> Result<OpenFormulaRefPart, ParsingError> {
1553        let trimmed = part.trim();
1554        if trimmed.is_empty() {
1555            return Err(ParsingError::InvalidReference(
1556                "Empty component in OpenFormula reference".to_string(),
1557            ));
1558        }
1559
1560        if trimmed == "." {
1561            return Err(ParsingError::InvalidReference(
1562                "Incomplete OpenFormula reference component".to_string(),
1563            ));
1564        }
1565
1566        if trimmed.starts_with('[') {
1567            // Nested brackets are not expected here
1568            return Err(ParsingError::InvalidReference(format!(
1569                "Unexpected '[' in OpenFormula reference component: {trimmed}"
1570            )));
1571        }
1572
1573        let (sheet, coord_slice) = if let Some(stripped) = trimmed.strip_prefix('.') {
1574            (None, stripped.trim())
1575        } else if let Some(dot_idx) = Self::find_openformula_sheet_separator(trimmed) {
1576            #[expect(
1577                clippy::string_slice,
1578                clippy::arithmetic_side_effects,
1579                reason = "byte offset from ASCII delimiter search: dot_idx is the index of a `.`"
1580            )]
1581            let (sheet_part, coord_part) = (&trimmed[..dot_idx], &trimmed[dot_idx + 1..]);
1582            let (sheet_part, coord_part) = (sheet_part.trim(), coord_part.trim());
1583            if coord_part.is_empty() {
1584                return Err(ParsingError::InvalidReference(format!(
1585                    "Missing coordinate in OpenFormula reference component: {trimmed}"
1586                )));
1587            }
1588            let sheet_name = Self::normalise_openformula_sheet(sheet_part)?;
1589            (Some(sheet_name), coord_part)
1590        } else {
1591            (None, trimmed)
1592        };
1593
1594        let coord = coord_slice.trim_start_matches('.').trim().to_string();
1595
1596        if coord.is_empty() {
1597            return Err(ParsingError::InvalidReference(format!(
1598                "Missing coordinate in OpenFormula reference component: {trimmed}"
1599            )));
1600        }
1601
1602        Ok(OpenFormulaRefPart { sheet, coord })
1603    }
1604
1605    fn normalise_openformula_sheet(sheet: &str) -> Result<String, ParsingError> {
1606        let without_abs = sheet.trim().trim_start_matches('$');
1607
1608        if let Some(quoted) = without_abs.strip_prefix('\'') {
1609            let Some(inner) = quoted.strip_suffix('\'') else {
1610                return Err(ParsingError::InvalidReference(format!(
1611                    "Unterminated sheet name in OpenFormula reference: {sheet}"
1612                )));
1613            };
1614            Ok(inner.replace("''", "'"))
1615        } else {
1616            Ok(without_abs.to_string())
1617        }
1618    }
1619
1620    #[expect(
1621        clippy::arithmetic_side_effects,
1622        reason = "cursor bounded by slice length"
1623    )]
1624    fn find_openformula_sheet_separator(part: &str) -> Option<usize> {
1625        let bytes = part.as_bytes();
1626        let mut i = 0;
1627        let mut in_quotes = false;
1628
1629        while let Some(&b) = bytes.get(i) {
1630            match b {
1631                b'\'' => {
1632                    if bytes.get(i + 1) == Some(&b'\'') {
1633                        i += 2;
1634                        continue;
1635                    }
1636                    in_quotes = !in_quotes;
1637                    i += 1;
1638                }
1639                b'.' if !in_quotes => return Some(i),
1640                _ => i += 1,
1641            }
1642        }
1643
1644        None
1645    }
1646
1647    // The structured-reference grammar lives in the `structured_ref`
1648    // submodule below; legacy `parse_special_item` /
1649    // `parse_complex_table_specifier` helpers were removed when the real
1650    // recursive-descent parser landed for issue #73.
1651
1652    /// Get the Excel-style string representation of this reference
1653    pub fn to_excel_string(&self) -> String {
1654        match self {
1655            ReferenceType::Cell {
1656                sheet,
1657                row,
1658                col,
1659                row_abs,
1660                col_abs,
1661            } => {
1662                let col_str = Self::format_col(*col, *col_abs);
1663                let row_str = Self::format_row(*row, *row_abs);
1664                if let Some(s) = sheet {
1665                    if sheet_name_needs_quoting(s) {
1666                        let escaped_name = s.replace('\'', "''");
1667                        format!("'{}'!{}{}", escaped_name, col_str, row_str)
1668                    } else {
1669                        format!("{}!{}{}", s, col_str, row_str)
1670                    }
1671                } else {
1672                    format!("{}{}", col_str, row_str)
1673                }
1674            }
1675            ReferenceType::Range {
1676                sheet,
1677                start_row,
1678                start_col,
1679                end_row,
1680                end_col,
1681                start_row_abs,
1682                start_col_abs,
1683                end_row_abs,
1684                end_col_abs,
1685            } => {
1686                // Format start reference
1687                let start_ref = match (start_col, start_row) {
1688                    (Some(col), Some(row)) => format!(
1689                        "{}{}",
1690                        Self::format_col(*col, *start_col_abs),
1691                        Self::format_row(*row, *start_row_abs)
1692                    ),
1693                    (Some(col), None) => Self::format_col(*col, *start_col_abs),
1694                    (None, Some(row)) => Self::format_row(*row, *start_row_abs),
1695                    (None, None) => "".to_string(), // Should not happen in normal usage
1696                };
1697
1698                // Format end reference
1699                let end_ref = match (end_col, end_row) {
1700                    (Some(col), Some(row)) => format!(
1701                        "{}{}",
1702                        Self::format_col(*col, *end_col_abs),
1703                        Self::format_row(*row, *end_row_abs)
1704                    ),
1705                    (Some(col), None) => Self::format_col(*col, *end_col_abs),
1706                    (None, Some(row)) => Self::format_row(*row, *end_row_abs),
1707                    (None, None) => "".to_string(), // Should not happen in normal usage
1708                };
1709
1710                let range_part = format!("{start_ref}:{end_ref}");
1711
1712                if let Some(s) = sheet {
1713                    if sheet_name_needs_quoting(s) {
1714                        let escaped_name = s.replace('\'', "''");
1715                        format!("'{escaped_name}'!{range_part}")
1716                    } else {
1717                        format!("{s}!{range_part}")
1718                    }
1719                } else {
1720                    range_part
1721                }
1722            }
1723            ReferenceType::Cell3D { .. } | ReferenceType::Range3D { .. } => format!("{self}"),
1724            ReferenceType::External(ext) => ext.raw.clone(),
1725            ReferenceType::Table(table_ref) => {
1726                if let Some(specifier) = &table_ref.specifier {
1727                    format!("{}[{}]", table_ref.name, specifier)
1728                } else {
1729                    table_ref.name.clone()
1730                }
1731            }
1732            ReferenceType::NamedRange(name) => name.clone(),
1733        }
1734    }
1735}
1736
1737/// A supported unary formula operator.
1738///
1739/// `Debug` prints the quoted spelling to preserve the former string-based
1740/// AST's debug output, also embedded in AST display text.
1741#[non_exhaustive]
1742#[derive(Clone, Copy, PartialEq, Eq)]
1743pub enum UnaryOperator {
1744    Plus,
1745    Minus,
1746    Percent,
1747    Spill,
1748    ImplicitIntersection,
1749}
1750// Serialize as a string even in non-human-readable serde formats. A
1751// renamed unit enum would still use a variant index in binary serializers.
1752#[cfg(feature = "serde")]
1753impl Serialize for UnaryOperator {
1754    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1755        serializer.serialize_str(self.as_str())
1756    }
1757}
1758#[cfg(feature = "serde")]
1759impl<'de> Deserialize<'de> for UnaryOperator {
1760    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1761        let value = String::deserialize(deserializer)?;
1762        Self::try_from(value.as_str()).map_err(serde::de::Error::custom)
1763    }
1764}
1765impl UnaryOperator {
1766    /// The operator spelling used in formulas and serialized ASTs.
1767    pub const fn as_str(self) -> &'static str {
1768        match self {
1769            Self::Plus => "+",
1770            Self::Minus => "-",
1771            Self::Percent => "%",
1772            Self::Spill => "#",
1773            Self::ImplicitIntersection => "@",
1774        }
1775    }
1776}
1777// ASTNodeType's Display embeds Debug for function arguments and arrays.
1778// Preserve the former String representation there as well.
1779impl fmt::Debug for UnaryOperator {
1780    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1781        fmt::Debug::fmt(self.as_str(), f)
1782    }
1783}
1784impl Display for UnaryOperator {
1785    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1786        f.write_str(self.as_str())
1787    }
1788}
1789impl TryFrom<&str> for UnaryOperator {
1790    type Error = &'static str;
1791    fn try_from(value: &str) -> Result<Self, Self::Error> {
1792        match value {
1793            "+" => Ok(Self::Plus),
1794            "-" => Ok(Self::Minus),
1795            "%" => Ok(Self::Percent),
1796            "#" => Ok(Self::Spill),
1797            "@" => Ok(Self::ImplicitIntersection),
1798            _ => Err("unsupported formula operator"),
1799        }
1800    }
1801}
1802impl std::hash::Hash for UnaryOperator {
1803    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1804        self.as_str().hash(state);
1805    }
1806}
1807
1808/// A supported binary formula operator.
1809///
1810/// `Debug` prints the quoted spelling to preserve the former string-based
1811/// AST's debug output, also embedded in AST display text.
1812#[non_exhaustive]
1813#[derive(Clone, Copy, PartialEq, Eq)]
1814pub enum BinaryOperator {
1815    Add,
1816    Subtract,
1817    Multiply,
1818    Divide,
1819    Power,
1820    Concat,
1821    Equal,
1822    NotEqual,
1823    Less,
1824    Greater,
1825    LessEqual,
1826    GreaterEqual,
1827    Range,
1828    Union,
1829    Intersection,
1830}
1831// Serialize as a string even in non-human-readable serde formats. A
1832// renamed unit enum would still use a variant index in binary serializers.
1833#[cfg(feature = "serde")]
1834impl Serialize for BinaryOperator {
1835    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1836        serializer.serialize_str(self.as_str())
1837    }
1838}
1839#[cfg(feature = "serde")]
1840impl<'de> Deserialize<'de> for BinaryOperator {
1841    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1842        let value = String::deserialize(deserializer)?;
1843        Self::try_from(value.as_str()).map_err(serde::de::Error::custom)
1844    }
1845}
1846impl BinaryOperator {
1847    /// The operator spelling used in formulas and serialized ASTs.
1848    pub const fn as_str(self) -> &'static str {
1849        match self {
1850            Self::Add => "+",
1851            Self::Subtract => "-",
1852            Self::Multiply => "*",
1853            Self::Divide => "/",
1854            Self::Power => "^",
1855            Self::Concat => "&",
1856            Self::Equal => "=",
1857            Self::NotEqual => "<>",
1858            Self::Less => "<",
1859            Self::Greater => ">",
1860            Self::LessEqual => "<=",
1861            Self::GreaterEqual => ">=",
1862            Self::Range => ":",
1863            Self::Union => ",",
1864            Self::Intersection => " ",
1865        }
1866    }
1867}
1868// ASTNodeType's Display embeds Debug for function arguments and arrays.
1869// Preserve the former String representation there as well.
1870impl fmt::Debug for BinaryOperator {
1871    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1872        fmt::Debug::fmt(self.as_str(), f)
1873    }
1874}
1875impl Display for BinaryOperator {
1876    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1877        f.write_str(self.as_str())
1878    }
1879}
1880/// Whether a node is a number, text or logical constant: never a reference,
1881/// so never an operand of the union operator (`=1,000` is not a union).
1882/// Error literals are not counted: Excel writes `#REF!` where a reference
1883/// in a union was deleted.
1884fn is_constant_literal(node: &ASTNode) -> bool {
1885    matches!(
1886        &node.node_type,
1887        ASTNodeType::Literal(value) if !matches!(value, LiteralValue::Error(_))
1888    )
1889}
1890
1891impl TryFrom<&str> for BinaryOperator {
1892    type Error = &'static str;
1893    fn try_from(value: &str) -> Result<Self, Self::Error> {
1894        match value {
1895            "+" => Ok(Self::Add),
1896            "-" => Ok(Self::Subtract),
1897            "*" => Ok(Self::Multiply),
1898            "/" => Ok(Self::Divide),
1899            "^" => Ok(Self::Power),
1900            "&" => Ok(Self::Concat),
1901            "=" => Ok(Self::Equal),
1902            "<>" => Ok(Self::NotEqual),
1903            "<" => Ok(Self::Less),
1904            ">" => Ok(Self::Greater),
1905            "<=" => Ok(Self::LessEqual),
1906            ">=" => Ok(Self::GreaterEqual),
1907            ":" => Ok(Self::Range),
1908            "," => Ok(Self::Union),
1909            " " => Ok(Self::Intersection),
1910            _ => Err("unsupported formula operator"),
1911        }
1912    }
1913}
1914impl std::hash::Hash for BinaryOperator {
1915    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1916        self.as_str().hash(state);
1917    }
1918}
1919
1920/// The different types of AST nodes.
1921#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1922#[derive(Debug, Clone, PartialEq, Hash)]
1923pub enum ASTNodeType {
1924    Literal(LiteralValue),
1925    /// An explicitly omitted function argument slot.
1926    ///
1927    /// This node is only valid as a direct argument of a [`Function`](Self::Function)
1928    /// or [`Call`](Self::Call) node.
1929    Omitted,
1930    Reference {
1931        original: String, // Original reference string (preserved for display/debugging)
1932        reference: ReferenceType, // Parsed reference
1933    },
1934    UnaryOp {
1935        op: UnaryOperator,
1936        expr: Box<ASTNode>,
1937    },
1938    BinaryOp {
1939        op: BinaryOperator,
1940        left: Box<ASTNode>,
1941        right: Box<ASTNode>,
1942    },
1943    Function {
1944        name: String,
1945        args: Vec<ASTNode>, // Most functions have <= 4 args
1946    },
1947    /// Generic call where the callee is itself an expression that produces
1948    /// a callable value (e.g. LAMBDA immediate-invocation `LAMBDA(x, x+1)(5)`).
1949    Call {
1950        callee: Box<ASTNode>,
1951        args: Vec<ASTNode>,
1952    },
1953    Array(Vec<Vec<ASTNode>>), // Most arrays are small
1954}
1955
1956impl Display for ASTNodeType {
1957    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1958        match self {
1959            ASTNodeType::Literal(value) => write!(f, "Literal({value})"),
1960            ASTNodeType::Omitted => write!(f, "Omitted"),
1961            ASTNodeType::Reference { reference, .. } => write!(f, "Reference({reference:?})"),
1962            ASTNodeType::UnaryOp { op, expr } => write!(f, "UnaryOp({op}, {expr})"),
1963            ASTNodeType::BinaryOp { op, left, right } => {
1964                write!(f, "BinaryOp({op}, {left}, {right})")
1965            }
1966            ASTNodeType::Function { name, args } => write!(f, "Function({name}, {args:?})"),
1967            ASTNodeType::Call { callee, args } => write!(f, "Call({callee}, {args:?})"),
1968            ASTNodeType::Array(rows) => write!(f, "Array({rows:?})"),
1969        }
1970    }
1971}
1972
1973/// An AST node represents a parsed formula element
1974///
1975/// # Depth
1976///
1977/// Hashing ([`Self::fingerprint`], [`Self::calculate_hash`], `Hash`),
1978/// pretty-printing, `Display` and the derived `Clone`, `PartialEq`, `Debug`,
1979/// serde and drop glue recurse once per tree level. Trees within the default
1980/// [`ParserLimits::ast_height`](crate::ParserLimits::ast_height) limit (256)
1981/// are supported, which includes every tree the parser returns under default
1982/// limits. A taller tree, hand-built or parsed under a raised limit, needs the
1983/// caller to provide the stack (see
1984/// [`ParserLimits::with_ast_height`](crate::ParserLimits::with_ast_height)).
1985/// Reference walks ([`Self::visit_refs`], [`Self::refs`],
1986/// [`Self::get_dependencies`], [`Self::collect_references`],
1987/// [`Self::update_sheet_references`]) use an explicit stack and have no
1988/// depth limit.
1989#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1990#[derive(Debug, Clone, PartialEq)]
1991pub struct ASTNode {
1992    pub node_type: ASTNodeType,
1993    pub source_token: Option<Token>,
1994    /// True if this AST contains any volatile function calls.
1995    ///
1996    /// This is set by the parser when a volatility classifier is provided.
1997    /// For ASTs constructed manually (e.g., in tests), this defaults to false.
1998    pub contains_volatile: bool,
1999}
2000
2001impl ASTNode {
2002    pub fn new(node_type: ASTNodeType, source_token: Option<Token>) -> Self {
2003        ASTNode {
2004            node_type,
2005            source_token,
2006            contains_volatile: false,
2007        }
2008    }
2009
2010    /// Create an ASTNode while explicitly setting contains_volatile.
2011    pub fn new_with_volatile(
2012        node_type: ASTNodeType,
2013        source_token: Option<Token>,
2014        contains_volatile: bool,
2015    ) -> Self {
2016        ASTNode {
2017            node_type,
2018            source_token,
2019            contains_volatile,
2020        }
2021    }
2022
2023    /// Whether this AST contains any volatile functions.
2024    pub fn contains_volatile(&self) -> bool {
2025        self.contains_volatile
2026    }
2027
2028    /// Structural fingerprint of this tree; the same value as
2029    /// [`Self::calculate_hash`].
2030    ///
2031    /// # Depth
2032    ///
2033    /// Recursive in the tree height. Trees within the default
2034    /// [`ParserLimits::ast_height`](crate::ParserLimits::ast_height) limit (256)
2035    /// are supported, which includes every tree the parser returns under default
2036    /// limits. A taller tree, hand-built or parsed under a raised limit, needs the
2037    /// caller to provide the stack (see
2038    /// [`ParserLimits::with_ast_height`](crate::ParserLimits::with_ast_height)).
2039    pub fn fingerprint(&self) -> u64 {
2040        self.calculate_hash()
2041    }
2042
2043    /// Calculate a hash for this ASTNode
2044    ///
2045    /// # Depth
2046    ///
2047    /// Recursive in the tree height. Trees within the default
2048    /// [`ParserLimits::ast_height`](crate::ParserLimits::ast_height) limit (256)
2049    /// are supported, which includes every tree the parser returns under default
2050    /// limits. A taller tree, hand-built or parsed under a raised limit, needs the
2051    /// caller to provide the stack (see
2052    /// [`ParserLimits::with_ast_height`](crate::ParserLimits::with_ast_height)).
2053    pub fn calculate_hash(&self) -> u64 {
2054        let mut hasher = FormulaHasher::new();
2055        self.hash_node(&mut hasher);
2056        hasher.finish()
2057    }
2058
2059    fn hash_node(&self, hasher: &mut FormulaHasher) {
2060        match &self.node_type {
2061            ASTNodeType::Literal(value) => {
2062                hasher.write(&[1]); // Discriminant for Literal
2063                value.hash(hasher);
2064            }
2065            ASTNodeType::Omitted => hasher.write(&[8]),
2066            ASTNodeType::Reference { reference, .. } => {
2067                hasher.write(&[2]); // Discriminant for Reference
2068                reference.hash(hasher);
2069            }
2070            ASTNodeType::UnaryOp { op, expr } => {
2071                hasher.write(&[3]); // Discriminant for UnaryOp
2072                hasher.write(op.as_str().as_bytes());
2073                expr.hash_node(hasher);
2074            }
2075            ASTNodeType::BinaryOp { op, left, right } => {
2076                hasher.write(&[4]); // Discriminant for BinaryOp
2077                hasher.write(op.as_str().as_bytes());
2078                left.hash_node(hasher);
2079                right.hash_node(hasher);
2080            }
2081            ASTNodeType::Function { name, args } => {
2082                hasher.write(&[5]); // Discriminant for Function
2083                // Use lowercase function name to be case-insensitive
2084                let name_lower = name.to_lowercase();
2085                hasher.write(name_lower.as_bytes());
2086                hasher.write_usize(args.len());
2087                for arg in args {
2088                    arg.hash_node(hasher);
2089                }
2090            }
2091            ASTNodeType::Call { callee, args } => {
2092                hasher.write(&[7]); // Discriminant for Call
2093                callee.hash_node(hasher);
2094                hasher.write_usize(args.len());
2095                for arg in args {
2096                    arg.hash_node(hasher);
2097                }
2098            }
2099            ASTNodeType::Array(rows) => {
2100                hasher.write(&[6]); // Discriminant for Array
2101                hasher.write_usize(rows.len());
2102                for row in rows {
2103                    hasher.write_usize(row.len());
2104                    for item in row {
2105                        item.hash_node(hasher);
2106                    }
2107                }
2108            }
2109        }
2110    }
2111
2112    pub fn get_dependencies(&self) -> Vec<&ReferenceType> {
2113        let mut dependencies = Vec::new();
2114        self.collect_dependencies(&mut dependencies);
2115        dependencies
2116    }
2117
2118    pub fn get_dependency_strings(&self) -> Vec<String> {
2119        self.get_dependencies()
2120            .into_iter()
2121            .map(|dep| format!("{dep}"))
2122            .collect()
2123    }
2124
2125    /// Pre-order walk with an explicit stack, so a deep hand-built tree
2126    /// cannot exhaust the call stack. Children are pushed in reverse so they
2127    /// are visited in source order.
2128    fn collect_dependencies<'a>(&'a self, dependencies: &mut Vec<&'a ReferenceType>) {
2129        let mut stack: Vec<&'a ASTNode> = vec![self];
2130        while let Some(node) = stack.pop() {
2131            match &node.node_type {
2132                ASTNodeType::Reference { reference, .. } => dependencies.push(reference),
2133                ASTNodeType::UnaryOp { expr, .. } => stack.push(expr),
2134                ASTNodeType::BinaryOp { left, right, .. } => {
2135                    stack.push(right);
2136                    stack.push(left);
2137                }
2138                ASTNodeType::Function { args, .. } => stack.extend(args.iter().rev()),
2139                ASTNodeType::Call { callee, args } => {
2140                    stack.extend(args.iter().rev());
2141                    stack.push(callee);
2142                }
2143                ASTNodeType::Array(rows) => {
2144                    for row in rows.iter().rev() {
2145                        stack.extend(row.iter().rev());
2146                    }
2147                }
2148                ASTNodeType::Literal(_) | ASTNodeType::Omitted => {}
2149            }
2150        }
2151    }
2152
2153    /// Lightweight borrowed view of a reference encountered during AST traversal.
2154    /// This mirrors ReferenceType variants but borrows sheet/name strings to avoid allocation.
2155    pub fn refs(&self) -> RefIter<'_> {
2156        RefIter {
2157            stack: smallvec::smallvec![self],
2158        }
2159    }
2160
2161    /// Visit all references in this AST without allocating intermediates.
2162    pub fn visit_refs<V: FnMut(RefView<'_>)>(&self, mut visitor: V) {
2163        let mut stack: Vec<&ASTNode> = Vec::with_capacity(8);
2164        stack.push(self);
2165        while let Some(node) = stack.pop() {
2166            match &node.node_type {
2167                ASTNodeType::Reference { reference, .. } => visitor(RefView::from(reference)),
2168                ASTNodeType::UnaryOp { expr, .. } => stack.push(expr),
2169                ASTNodeType::BinaryOp { left, right, .. } => {
2170                    // Push right first so left is visited first (stable-ish order)
2171                    stack.push(right);
2172                    stack.push(left);
2173                }
2174                ASTNodeType::Function { args, .. } => {
2175                    for a in args.iter().rev() {
2176                        stack.push(a);
2177                    }
2178                }
2179                ASTNodeType::Call { callee, args } => {
2180                    for a in args.iter().rev() {
2181                        stack.push(a);
2182                    }
2183                    stack.push(callee);
2184                }
2185                ASTNodeType::Array(rows) => {
2186                    for r in rows.iter().rev() {
2187                        for item in r.iter().rev() {
2188                            stack.push(item);
2189                        }
2190                    }
2191                }
2192                ASTNodeType::Literal(_) | ASTNodeType::Omitted => {}
2193            }
2194        }
2195    }
2196
2197    /// Convenience: collect references into a small, inline vector based on a policy.
2198    pub fn collect_references(&self, policy: &CollectPolicy) -> SmallVec<[ReferenceType; 4]> {
2199        let mut out: SmallVec<[ReferenceType; 4]> = SmallVec::new();
2200        self.visit_refs(|rv| match rv {
2201            RefView::Cell {
2202                sheet,
2203                row,
2204                col,
2205                row_abs,
2206                col_abs,
2207            } => out.push(ReferenceType::Cell {
2208                sheet: sheet.map(|s| s.to_string()),
2209                row,
2210                col,
2211                row_abs,
2212                col_abs,
2213            }),
2214            RefView::Range {
2215                sheet,
2216                start_row,
2217                start_col,
2218                end_row,
2219                end_col,
2220                start_row_abs,
2221                start_col_abs,
2222                end_row_abs,
2223                end_col_abs,
2224            } => {
2225                // Optionally expand very small finite ranges into individual cells
2226                if policy.expand_small_ranges
2227                    && let (Some(sr), Some(sc), Some(er), Some(ec)) =
2228                        (start_row, start_col, end_row, end_col)
2229                {
2230                    // Widened to u64: a full-height or full-width u32
2231                    // span plus one does not fit in u32.
2232                    let rows = u64::from(er.saturating_sub(sr)) + 1;
2233                    let cols = u64::from(ec.saturating_sub(sc)) + 1;
2234                    let area = rows.saturating_mul(cols);
2235                    let limit = u64::try_from(policy.range_expansion_limit).unwrap_or(u64::MAX);
2236                    if area <= limit {
2237                        let row_abs = start_row_abs && end_row_abs;
2238                        let col_abs = start_col_abs && end_col_abs;
2239                        for r in sr..=er {
2240                            for c in sc..=ec {
2241                                out.push(ReferenceType::Cell {
2242                                    sheet: sheet.map(|s| s.to_string()),
2243                                    row: r,
2244                                    col: c,
2245                                    row_abs,
2246                                    col_abs,
2247                                });
2248                            }
2249                        }
2250                        return; // handled
2251                    }
2252                }
2253                out.push(ReferenceType::Range {
2254                    sheet: sheet.map(|s| s.to_string()),
2255                    start_row,
2256                    start_col,
2257                    end_row,
2258                    end_col,
2259                    start_row_abs,
2260                    start_col_abs,
2261                    end_row_abs,
2262                    end_col_abs,
2263                });
2264            }
2265            RefView::Cell3D {
2266                sheet_first,
2267                sheet_last,
2268                row,
2269                col,
2270                row_abs,
2271                col_abs,
2272            } => out.push(ReferenceType::Cell3D {
2273                sheet_first: sheet_first.to_string(),
2274                sheet_last: sheet_last.to_string(),
2275                row,
2276                col,
2277                row_abs,
2278                col_abs,
2279            }),
2280            RefView::Range3D {
2281                sheet_first,
2282                sheet_last,
2283                start_row,
2284                start_col,
2285                end_row,
2286                end_col,
2287                start_row_abs,
2288                start_col_abs,
2289                end_row_abs,
2290                end_col_abs,
2291            } => out.push(ReferenceType::Range3D {
2292                sheet_first: sheet_first.to_string(),
2293                sheet_last: sheet_last.to_string(),
2294                start_row,
2295                start_col,
2296                end_row,
2297                end_col,
2298                start_row_abs,
2299                start_col_abs,
2300                end_row_abs,
2301                end_col_abs,
2302            }),
2303            RefView::External {
2304                raw,
2305                book,
2306                sheet,
2307                kind,
2308            } => out.push(ReferenceType::External(ExternalReference {
2309                raw: raw.to_string(),
2310                book: ExternalBookRef::Token(book.to_string()),
2311                sheet: sheet.to_string(),
2312                kind,
2313            })),
2314            RefView::Table { name, specifier } => out.push(ReferenceType::Table(TableReference {
2315                name: name.to_string(),
2316                specifier: specifier.cloned(),
2317            })),
2318            RefView::NamedRange { name } => {
2319                if policy.include_names {
2320                    out.push(ReferenceType::NamedRange(name.to_string()));
2321                }
2322            }
2323        });
2324        out
2325    }
2326    /// Updates sheet references within the AST.
2327    ///
2328    /// If `target_name` is provided, only references matching that sheet name are updated.
2329    /// This is used for "healing" specific broken references (Tombstone rescue).
2330    /// If `target_name` is None, it acts as a global rename (standard sheet rename).
2331    ///
2332    /// The walk uses an explicit stack, so tree depth is not limited by the
2333    /// call stack.
2334    pub fn update_sheet_references(&mut self, target_name: Option<&str>, new_name: &str) {
2335        let mut stack: Vec<&mut ASTNode> = vec![self];
2336        while let Some(node) = stack.pop() {
2337            match &mut node.node_type {
2338                ASTNodeType::Reference {
2339                    reference:
2340                        ReferenceType::Cell { sheet, .. } | ReferenceType::Range { sheet, .. },
2341                    ..
2342                } => {
2343                    if let Some(current_sheet) = sheet
2344                        && (target_name.is_none() || target_name == Some(current_sheet.as_str()))
2345                    {
2346                        *sheet = Some(new_name.to_string());
2347                    }
2348                }
2349                ASTNodeType::Reference {
2350                    reference:
2351                        ReferenceType::Cell3D {
2352                            sheet_first,
2353                            sheet_last,
2354                            ..
2355                        }
2356                        | ReferenceType::Range3D {
2357                            sheet_first,
2358                            sheet_last,
2359                            ..
2360                        },
2361                    ..
2362                } => {
2363                    if target_name.is_none() || target_name == Some(sheet_first.as_str()) {
2364                        *sheet_first = new_name.to_string();
2365                    }
2366                    if target_name.is_none() || target_name == Some(sheet_last.as_str()) {
2367                        *sheet_last = new_name.to_string();
2368                    }
2369                }
2370                ASTNodeType::UnaryOp { expr, .. } => stack.push(expr),
2371                ASTNodeType::BinaryOp { left, right, .. } => {
2372                    stack.push(right);
2373                    stack.push(left);
2374                }
2375                ASTNodeType::Function { args, .. } => stack.extend(args.iter_mut().rev()),
2376                ASTNodeType::Call { callee, args } => {
2377                    stack.extend(args.iter_mut().rev());
2378                    stack.push(callee);
2379                }
2380                ASTNodeType::Array(rows) => {
2381                    for row in rows.iter_mut().rev() {
2382                        stack.extend(row.iter_mut().rev());
2383                    }
2384                }
2385                ASTNodeType::Reference { .. } | ASTNodeType::Literal(_) | ASTNodeType::Omitted => {}
2386            }
2387        }
2388    }
2389}
2390
2391/// A borrowing view over a ReferenceType. Avoids cloning sheet/names while walking.
2392#[derive(Clone, Copy, Debug)]
2393pub enum RefView<'a> {
2394    Cell {
2395        sheet: Option<&'a str>,
2396        row: u32,
2397        col: u32,
2398        row_abs: bool,
2399        col_abs: bool,
2400    },
2401    Range {
2402        sheet: Option<&'a str>,
2403        start_row: Option<u32>,
2404        start_col: Option<u32>,
2405        end_row: Option<u32>,
2406        end_col: Option<u32>,
2407        start_row_abs: bool,
2408        start_col_abs: bool,
2409        end_row_abs: bool,
2410        end_col_abs: bool,
2411    },
2412    /// 3D cell view (`Sheet1:Sheet3!A1`).
2413    Cell3D {
2414        sheet_first: &'a str,
2415        sheet_last: &'a str,
2416        row: u32,
2417        col: u32,
2418        row_abs: bool,
2419        col_abs: bool,
2420    },
2421    /// 3D range view (`Sheet1:Sheet3!A1:B2`).
2422    Range3D {
2423        sheet_first: &'a str,
2424        sheet_last: &'a str,
2425        start_row: Option<u32>,
2426        start_col: Option<u32>,
2427        end_row: Option<u32>,
2428        end_col: Option<u32>,
2429        start_row_abs: bool,
2430        start_col_abs: bool,
2431        end_row_abs: bool,
2432        end_col_abs: bool,
2433    },
2434    External {
2435        raw: &'a str,
2436        book: &'a str,
2437        sheet: &'a str,
2438        kind: ExternalRefKind,
2439    },
2440    Table {
2441        name: &'a str,
2442        specifier: Option<&'a TableSpecifier>,
2443    },
2444    NamedRange {
2445        name: &'a str,
2446    },
2447}
2448
2449impl<'a> From<&'a ReferenceType> for RefView<'a> {
2450    fn from(r: &'a ReferenceType) -> Self {
2451        match r {
2452            ReferenceType::Cell {
2453                sheet,
2454                row,
2455                col,
2456                row_abs,
2457                col_abs,
2458            } => RefView::Cell {
2459                sheet: sheet.as_deref(),
2460                row: *row,
2461                col: *col,
2462                row_abs: *row_abs,
2463                col_abs: *col_abs,
2464            },
2465            ReferenceType::Range {
2466                sheet,
2467                start_row,
2468                start_col,
2469                end_row,
2470                end_col,
2471                start_row_abs,
2472                start_col_abs,
2473                end_row_abs,
2474                end_col_abs,
2475            } => RefView::Range {
2476                sheet: sheet.as_deref(),
2477                start_row: *start_row,
2478                start_col: *start_col,
2479                end_row: *end_row,
2480                end_col: *end_col,
2481                start_row_abs: *start_row_abs,
2482                start_col_abs: *start_col_abs,
2483                end_row_abs: *end_row_abs,
2484                end_col_abs: *end_col_abs,
2485            },
2486            ReferenceType::Cell3D {
2487                sheet_first,
2488                sheet_last,
2489                row,
2490                col,
2491                row_abs,
2492                col_abs,
2493            } => RefView::Cell3D {
2494                sheet_first: sheet_first.as_str(),
2495                sheet_last: sheet_last.as_str(),
2496                row: *row,
2497                col: *col,
2498                row_abs: *row_abs,
2499                col_abs: *col_abs,
2500            },
2501            ReferenceType::Range3D {
2502                sheet_first,
2503                sheet_last,
2504                start_row,
2505                start_col,
2506                end_row,
2507                end_col,
2508                start_row_abs,
2509                start_col_abs,
2510                end_row_abs,
2511                end_col_abs,
2512            } => RefView::Range3D {
2513                sheet_first: sheet_first.as_str(),
2514                sheet_last: sheet_last.as_str(),
2515                start_row: *start_row,
2516                start_col: *start_col,
2517                end_row: *end_row,
2518                end_col: *end_col,
2519                start_row_abs: *start_row_abs,
2520                start_col_abs: *start_col_abs,
2521                end_row_abs: *end_row_abs,
2522                end_col_abs: *end_col_abs,
2523            },
2524            ReferenceType::External(ext) => RefView::External {
2525                raw: ext.raw.as_str(),
2526                book: ext.book.token(),
2527                sheet: ext.sheet.as_str(),
2528                kind: ext.kind,
2529            },
2530            ReferenceType::Table(tr) => RefView::Table {
2531                name: tr.name.as_str(),
2532                specifier: tr.specifier.as_ref(),
2533            },
2534            ReferenceType::NamedRange(name) => RefView::NamedRange { name },
2535        }
2536    }
2537}
2538
2539/// Iterator over RefView for an AST, implemented via an explicit stack to avoid recursion allocation.
2540pub struct RefIter<'a> {
2541    stack: smallvec::SmallVec<[&'a ASTNode; 8]>,
2542}
2543
2544impl<'a> Iterator for RefIter<'a> {
2545    type Item = RefView<'a>;
2546    fn next(&mut self) -> Option<Self::Item> {
2547        while let Some(node) = self.stack.pop() {
2548            match &node.node_type {
2549                ASTNodeType::Reference { reference, .. } => return Some(RefView::from(reference)),
2550                ASTNodeType::UnaryOp { expr, .. } => self.stack.push(expr),
2551                ASTNodeType::BinaryOp { left, right, .. } => {
2552                    self.stack.push(right);
2553                    self.stack.push(left);
2554                }
2555                ASTNodeType::Function { args, .. } => {
2556                    for a in args.iter().rev() {
2557                        self.stack.push(a);
2558                    }
2559                }
2560                ASTNodeType::Call { callee, args } => {
2561                    for a in args.iter().rev() {
2562                        self.stack.push(a);
2563                    }
2564                    self.stack.push(callee);
2565                }
2566                ASTNodeType::Array(rows) => {
2567                    for r in rows.iter().rev() {
2568                        for item in r.iter().rev() {
2569                            self.stack.push(item);
2570                        }
2571                    }
2572                }
2573                ASTNodeType::Literal(_) | ASTNodeType::Omitted => {}
2574            }
2575        }
2576        None
2577    }
2578}
2579
2580/// Policy controlling how references are collected.
2581#[derive(Debug, Clone)]
2582pub struct CollectPolicy {
2583    pub expand_small_ranges: bool,
2584    pub range_expansion_limit: usize,
2585    pub include_names: bool,
2586}
2587
2588impl Default for CollectPolicy {
2589    fn default() -> Self {
2590        Self {
2591            expand_small_ranges: false,
2592            range_expansion_limit: 0,
2593            include_names: true,
2594        }
2595    }
2596}
2597
2598impl Display for ASTNode {
2599    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2600        write!(f, "{}", self.node_type)
2601    }
2602}
2603
2604impl std::hash::Hash for ASTNode {
2605    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
2606        let hash = self.calculate_hash();
2607        state.write_u64(hash);
2608    }
2609}
2610
2611impl From<TokenizerError> for ParserError {
2612    fn from(err: TokenizerError) -> Self {
2613        ParserError {
2614            message: err.message,
2615            position: Some(err.pos),
2616        }
2617    }
2618}
2619
2620/// Source-span-backed parser for converting formulas into an AST.
2621///
2622/// This is the canonical parser implementation. It owns the formula source and
2623/// span tokens, avoiding per-token string allocation while preserving source
2624/// locations for AST nodes.
2625pub struct Parser {
2626    source: Arc<str>,
2627    tokens: Arc<[TokenSpan]>,
2628    position: usize,
2629    volatility_classifier: Option<VolatilityClassifierBox>,
2630    dialect: FormulaDialect,
2631    /// When > 0, treat a top-level `OpInfix(",")` as a terminator (call-arg
2632    /// separator) instead of the union/list operator. Used by `parse_call_arguments`.
2633    in_call_args_depth: usize,
2634    /// Current recursion depth of `parse_bp`. Bounded by the configured Pratt-frame limit so a
2635    /// deeply nested formula (e.g. `=((((...))))`) returns an error instead of
2636    /// overflowing the stack.
2637    depth: usize,
2638    limits: ParserLimits,
2639    nodes: usize,
2640    pending_error: Option<ParserError>,
2641}
2642
2643/// Ephemeral construction metadata; public AST nodes retain no height field.
2644/// Parent admission uses child heights directly, without traversing subtrees.
2645struct BuiltNode {
2646    node: ASTNode,
2647    height: usize,
2648}
2649#[derive(Default)]
2650struct BuiltArgs {
2651    nodes: Vec<ASTNode>,
2652    height: usize,
2653    volatile: bool,
2654}
2655impl BuiltArgs {
2656    fn push(&mut self, child: BuiltNode) {
2657        self.height = self.height.max(child.height);
2658        self.volatile |= child.node.contains_volatile;
2659        self.nodes.push(child.node);
2660    }
2661}
2662impl std::ops::Deref for BuiltNode {
2663    type Target = ASTNode;
2664    fn deref(&self) -> &ASTNode {
2665        &self.node
2666    }
2667}
2668
2669impl Parser {
2670    /// Tokenize a formula using the default Excel dialect and prepare it for parsing.
2671    pub fn new<T: AsRef<str>>(formula: T) -> Result<Self, TokenizerError> {
2672        Self::new_with_dialect(formula, FormulaDialect::Excel)
2673    }
2674
2675    /// Compatibility alias for `Parser::new`.
2676    pub fn try_from_formula(formula: &str) -> Result<Self, TokenizerError> {
2677        Self::new(formula)
2678    }
2679
2680    /// Tokenize a formula with an explicit dialect and prepare it for parsing.
2681    pub fn new_with_dialect<T: AsRef<str>>(
2682        formula: T,
2683        dialect: FormulaDialect,
2684    ) -> Result<Self, TokenizerError> {
2685        Self::new_with_limits(formula.as_ref(), dialect, ParserLimits::default())
2686    }
2687
2688    fn new_with_limits(
2689        formula: &str,
2690        dialect: FormulaDialect,
2691        limits: ParserLimits,
2692    ) -> Result<Self, TokenizerError> {
2693        limits.check_source(formula)?;
2694        let spans = crate::tokenizer::tokenize_spans_with_limits(formula, dialect, limits)?;
2695        let mut parser = Self::from_source_and_tokens(
2696            Arc::from(formula),
2697            Arc::from(spans.into_boxed_slice()),
2698            dialect,
2699        );
2700        parser.limits = limits;
2701        Ok(parser)
2702    }
2703
2704    /// External streams are mutable: validate admission and all spans before copying.
2705    pub fn from_token_stream(stream: &TokenStream) -> Self {
2706        let limits = ParserLimits::default();
2707        let error = stream.admission_error(limits).map(ParserError::from);
2708        if let Some(error) = error {
2709            let mut parser =
2710                Self::from_source_and_tokens(Arc::from(""), Arc::from([]), stream.dialect());
2711            parser.pending_error = Some(error);
2712            return parser;
2713        }
2714        Self::from_source_and_tokens(
2715            Arc::from(stream.source()),
2716            Arc::from(stream.spans.clone().into_boxed_slice()),
2717            stream.dialect(),
2718        )
2719    }
2720
2721    fn from_source_and_tokens(
2722        source: Arc<str>,
2723        tokens: Arc<[TokenSpan]>,
2724        dialect: FormulaDialect,
2725    ) -> Self {
2726        Parser {
2727            source,
2728            tokens,
2729            position: 0,
2730            volatility_classifier: None,
2731            dialect,
2732            in_call_args_depth: 0,
2733            depth: 0,
2734            limits: ParserLimits::default(),
2735            nodes: 0,
2736            pending_error: None,
2737        }
2738    }
2739
2740    /// Provide a function-volatility classifier for this parser.
2741    /// If set, the parser will annotate ASTs with a contains_volatile bit.
2742    pub fn with_volatility_classifier<F>(mut self, f: F) -> Self
2743    where
2744        F: Fn(&str) -> bool + Send + Sync + 'static,
2745    {
2746        self.volatility_classifier = Some(Box::new(f));
2747        self
2748    }
2749
2750    // Keep rich AST temporaries out of every active Pratt frame in debug builds.
2751    #[expect(
2752        clippy::arithmetic_side_effects,
2753        reason = "child heights are at most the AST height limit and the number of admitted nodes, so 1 + height cannot overflow"
2754    )]
2755    fn unary(&mut self, expr: BuiltNode, span: TokenSpan) -> Result<BuiltNode, ParserError> {
2756        let height = 1 + expr.height;
2757        self.admit(height)?;
2758        let token = self.span_to_token(&span);
2759        let volatile = expr.contains_volatile;
2760        Ok(BuiltNode {
2761            height,
2762            node: ASTNode::new_with_volatile(
2763                ASTNodeType::UnaryOp {
2764                    op: UnaryOperator::try_from(token.value.as_str()).map_err(|message| {
2765                        ParserError {
2766                            message: message.to_string(),
2767                            position: Some(span.start),
2768                        }
2769                    })?,
2770                    expr: Box::new(expr.node),
2771                },
2772                Some(token),
2773                volatile,
2774            ),
2775        })
2776    }
2777    #[expect(
2778        clippy::arithmetic_side_effects,
2779        reason = "child heights are at most the AST height limit and the number of admitted nodes, so 1 + height cannot overflow"
2780    )]
2781    fn binary(
2782        &mut self,
2783        left: BuiltNode,
2784        right: BuiltNode,
2785        span: TokenSpan,
2786    ) -> Result<BuiltNode, ParserError> {
2787        let height = 1 + left.height.max(right.height);
2788        self.admit(height)?;
2789        let token = self.span_to_token(&span);
2790        let op = BinaryOperator::try_from(token.value.as_str()).map_err(|message| ParserError {
2791            message: if token.value == ";" {
2792                "';' is an argument separator, not an operator in this position; OpenFormula union uses '~'".to_string()
2793            } else {
2794                message.to_string()
2795            },
2796            position: Some(span.start),
2797        })?;
2798        if op == BinaryOperator::Union
2799            && (is_constant_literal(&left.node) || is_constant_literal(&right.node))
2800        {
2801            return Err(ParserError {
2802                message: "the union operator joins references; a number, text or logical constant is not a reference".to_string(),
2803                position: Some(span.start),
2804            });
2805        }
2806        let volatile = left.contains_volatile || right.contains_volatile;
2807        Ok(BuiltNode {
2808            height,
2809            node: ASTNode::new_with_volatile(
2810                ASTNodeType::BinaryOp {
2811                    op,
2812                    left: Box::new(left.node),
2813                    right: Box::new(right.node),
2814                },
2815                Some(token),
2816                volatile,
2817            ),
2818        })
2819    }
2820    #[expect(
2821        clippy::arithmetic_side_effects,
2822        reason = "child heights are at most the AST height limit and the number of admitted nodes, so 1 + height cannot overflow"
2823    )]
2824    fn call(&mut self, left: BuiltNode, args: BuiltArgs) -> Result<BuiltNode, ParserError> {
2825        let height = 1 + left.height.max(args.height);
2826        self.admit(height)?;
2827        let volatile = left.contains_volatile || args.volatile;
2828        Ok(BuiltNode {
2829            height,
2830            node: ASTNode::new_with_volatile(
2831                ASTNodeType::Call {
2832                    callee: Box::new(left.node),
2833                    args: args.nodes,
2834                },
2835                None,
2836                volatile,
2837            ),
2838        })
2839    }
2840    /// The byte offset in the source of the token at `index`, or the source
2841    /// length when `index` is past the last token. `ParserError::position`
2842    /// is a byte offset, like the tokenizer's error position.
2843    fn byte_offset(&self, index: usize) -> usize {
2844        self.tokens
2845            .get(index)
2846            .map_or(self.source.len(), |token| token.start)
2847    }
2848
2849    /// The token at the cursor, if any.
2850    #[inline]
2851    fn peek(&self) -> Option<TokenSpan> {
2852        self.tokens.get(self.position).copied()
2853    }
2854
2855    /// Whether the token at the cursor has this type and subtype.
2856    #[inline]
2857    fn peek_is(&self, token_type: TokenType, subtype: TokenSubType) -> bool {
2858        self.peek()
2859            .is_some_and(|t| t.token_type == token_type && t.subtype == subtype)
2860    }
2861
2862    /// Move past the token at the cursor.
2863    #[inline]
2864    #[expect(
2865        clippy::arithmetic_side_effects,
2866        reason = "callers advance only past a token they read with peek, so position < tokens.len()"
2867    )]
2868    fn advance(&mut self) {
2869        self.position += 1;
2870    }
2871
2872    fn skip_whitespace(&mut self) {
2873        while self
2874            .peek()
2875            .is_some_and(|t| t.token_type == TokenType::Whitespace)
2876        {
2877            self.advance();
2878        }
2879    }
2880
2881    #[expect(
2882        clippy::string_slice,
2883        reason = "spans come from the span tokenizer on this source (char-boundary invariant), or from an external TokenStream whose spans admission_error checked with source.get"
2884    )]
2885    fn span_value(&self, span: &TokenSpan) -> &str {
2886        &self.source[span.start..span.end]
2887    }
2888
2889    fn semantic_span_value(&self, span: &TokenSpan) -> &str {
2890        let value = self.span_value(span);
2891        if span.token_type == TokenType::OpInfix
2892            && value.as_bytes().contains(&b' ')
2893            && value
2894                .as_bytes()
2895                .iter()
2896                .all(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
2897        {
2898            " "
2899        } else {
2900            value
2901        }
2902    }
2903
2904    fn span_to_token(&self, span: &TokenSpan) -> Token {
2905        Token::new_with_span(
2906            self.semantic_span_value(span).to_string(),
2907            span.token_type,
2908            span.subtype,
2909            span.start,
2910            span.end,
2911        )
2912    }
2913
2914    fn span_precedence(&self, span: &TokenSpan) -> Option<(u8, Associativity)> {
2915        if !matches!(
2916            span.token_type,
2917            TokenType::OpPrefix | TokenType::OpInfix | TokenType::OpPostfix
2918        ) {
2919            return None;
2920        }
2921
2922        let op = if span.token_type == TokenType::OpPrefix {
2923            "u"
2924        } else {
2925            self.semantic_span_value(span)
2926        };
2927
2928        match op {
2929            "#" => Some((11, Associativity::Left)),
2930            ":" => Some((10, Associativity::Left)),
2931            " " => Some((9, Associativity::Left)),
2932            "," => Some((8, Associativity::Left)),
2933            "%" => Some((7, Associativity::Left)),
2934            "u" => Some((6, Associativity::Right)),
2935            "^" => Some((5, Associativity::Left)),
2936            "*" | "/" => Some((4, Associativity::Left)),
2937            "+" | "-" => Some((3, Associativity::Left)),
2938            "&" => Some((2, Associativity::Left)),
2939            "=" | "<" | ">" | "<=" | ">=" | "<>" => Some((1, Associativity::Left)),
2940            _ => None,
2941        }
2942    }
2943
2944    #[cold]
2945    fn limit_error(&self, budget: &str, limit: usize) -> ParserError {
2946        ParserError {
2947            message: format!("Formula {budget} (max {limit})"),
2948            position: Some(self.byte_offset(self.position)),
2949        }
2950    }
2951    #[expect(
2952        clippy::arithmetic_side_effects,
2953        reason = "nodes < ast_nodes is checked before the increment"
2954    )]
2955    fn admit(&mut self, height: usize) -> Result<(), ParserError> {
2956        if self.nodes >= self.limits.ast_nodes {
2957            return Err(self.limit_error("AST node limit exceeded", self.limits.ast_nodes));
2958        }
2959        if height > self.limits.ast_height {
2960            return Err(self.limit_error("AST height limit exceeded", self.limits.ast_height));
2961        }
2962        self.nodes += 1;
2963        Ok(())
2964    }
2965    fn omitted(&mut self) -> Result<BuiltNode, ParserError> {
2966        self.admit(1)?;
2967        Ok(BuiltNode {
2968            node: ASTNode::new(ASTNodeType::Omitted, None),
2969            height: 1,
2970        })
2971    }
2972    pub fn parse(&mut self) -> Result<ASTNode, ParserError> {
2973        if let Some(error) = &self.pending_error {
2974            return Err(ParserError {
2975                message: error.message.clone(),
2976                position: error.position,
2977            });
2978        }
2979        self.nodes = 0;
2980        if self.tokens.is_empty() {
2981            return Err(ParserError {
2982                message: "No tokens to parse".to_string(),
2983                position: None,
2984            });
2985        }
2986
2987        self.skip_whitespace();
2988        let Some(first) = self.peek() else {
2989            return Err(ParserError {
2990                message: "No tokens to parse".to_string(),
2991                position: None,
2992            });
2993        };
2994
2995        if first.token_type == TokenType::Literal {
2996            let span = first;
2997            self.advance();
2998            self.skip_whitespace();
2999            if let Some(extra) = self.peek() {
3000                return Err(ParserError {
3001                    message: format!("Unexpected token at position {}: {extra:?}", extra.start),
3002                    position: Some(extra.start),
3003                });
3004            }
3005
3006            let token = self.span_to_token(&span);
3007            self.admit(1)?;
3008            return Ok(ASTNode::new(
3009                ASTNodeType::Literal(LiteralValue::Text(token.value.clone())),
3010                Some(token),
3011            ));
3012        }
3013
3014        let ast = self.parse_expression()?;
3015        self.skip_whitespace();
3016        if let Some(extra) = self.peek() {
3017            return Err(ParserError {
3018                message: format!("Unexpected token at position {}: {extra:?}", extra.start),
3019                position: Some(extra.start),
3020            });
3021        }
3022        Ok(ast.node)
3023    }
3024
3025    fn parse_expression(&mut self) -> Result<BuiltNode, ParserError> {
3026        self.parse_bp(0)
3027    }
3028
3029    #[expect(
3030        clippy::arithmetic_side_effects,
3031        reason = "depth counts active parse_bp frames: it is incremented once per frame and decremented on every exit, so it is bounded by the call stack"
3032    )]
3033    fn parse_bp(&mut self, min_precedence: u8) -> Result<BuiltNode, ParserError> {
3034        // Bound recursion so deeply nested input (e.g. `=((((...))))`) returns
3035        // an error instead of overflowing the stack.
3036        self.depth += 1;
3037        if self.depth > self.limits.pratt_frames {
3038            self.depth -= 1;
3039            return Err(self.limit_error("nesting too deep", self.limits.pratt_frames));
3040        }
3041        let result = self.parse_bp_inner(min_precedence);
3042        self.depth -= 1;
3043        result
3044    }
3045
3046    #[expect(
3047        clippy::arithmetic_side_effects,
3048        reason = "precedence comes from the span_precedence table (at most 11), so precedence + 1 fits in u8"
3049    )]
3050    fn parse_bp_inner(&mut self, min_precedence: u8) -> Result<BuiltNode, ParserError> {
3051        let mut left = self.parse_prefix()?;
3052
3053        loop {
3054            self.skip_whitespace();
3055            let Some(token) = self.peek() else {
3056                break;
3057            };
3058
3059            // Postfix call: a `(` directly following a closed expression denotes
3060            // immediate invocation of a callable result (e.g. LAMBDA IIFE).
3061            if token.token_type == TokenType::Paren && token.subtype == TokenSubType::Open {
3062                self.advance();
3063                let args = self.parse_call_arguments()?;
3064                left = self.call(left, args)?;
3065                continue;
3066            }
3067
3068            if token.token_type == TokenType::OpPostfix {
3069                let (precedence, _) = self
3070                    .span_precedence(&token)
3071                    .unwrap_or((0, Associativity::Left));
3072                if precedence < min_precedence {
3073                    break;
3074                }
3075
3076                self.advance();
3077                left = self.unary(left, token)?;
3078                continue;
3079            }
3080
3081            if token.token_type != TokenType::OpInfix {
3082                break;
3083            }
3084
3085            // Inside a postfix call's argument list, treat top-level `,` as
3086            // an argument separator, not as the union operator.
3087            if self.in_call_args_depth > 0 && self.span_value(&token) == "," {
3088                break;
3089            }
3090
3091            let (precedence, associativity) = self
3092                .span_precedence(&token)
3093                .unwrap_or((0, Associativity::Left));
3094            if precedence < min_precedence {
3095                break;
3096            }
3097
3098            let op_span = token;
3099            self.advance();
3100
3101            let next_min_precedence = if associativity == Associativity::Left {
3102                precedence + 1
3103            } else {
3104                precedence
3105            };
3106
3107            let right = self.parse_bp(next_min_precedence)?;
3108            left = self.binary(left, right, op_span)?;
3109        }
3110
3111        Ok(left)
3112    }
3113
3114    fn parse_prefix(&mut self) -> Result<BuiltNode, ParserError> {
3115        self.skip_whitespace();
3116        if let Some(op_span) = self.peek()
3117            && op_span.token_type == TokenType::OpPrefix
3118        {
3119            self.advance();
3120
3121            let (precedence, _) = self
3122                .span_precedence(&op_span)
3123                .unwrap_or((0, Associativity::Right));
3124
3125            let expr = self.parse_bp(precedence)?;
3126            return self.unary(expr, op_span);
3127        }
3128
3129        self.parse_primary()
3130    }
3131
3132    fn parse_primary(&mut self) -> Result<BuiltNode, ParserError> {
3133        self.skip_whitespace();
3134        let Some(token) = self.peek() else {
3135            return Err(ParserError {
3136                message: "Unexpected end of tokens".to_string(),
3137                position: Some(self.byte_offset(self.position)),
3138            });
3139        };
3140
3141        match token.token_type {
3142            TokenType::Operand => {
3143                self.advance();
3144                self.parse_operand(token)
3145            }
3146            TokenType::Func => {
3147                self.advance();
3148                self.parse_function(token)
3149            }
3150            TokenType::Paren if token.subtype == TokenSubType::Open => {
3151                self.advance();
3152                let expr = self.parse_expression()?;
3153                self.skip_whitespace();
3154                if !self.peek_is(TokenType::Paren, TokenSubType::Close) {
3155                    return Err(ParserError {
3156                        message: "Expected closing parenthesis".to_string(),
3157                        position: Some(self.byte_offset(self.position)),
3158                    });
3159                }
3160                self.advance();
3161                Ok(expr)
3162            }
3163            TokenType::Array if token.subtype == TokenSubType::Open => {
3164                self.advance();
3165                self.parse_array()
3166            }
3167            _ => Err(ParserError {
3168                message: format!("Unexpected token: {token:?}"),
3169                position: Some(self.byte_offset(self.position)),
3170            }),
3171        }
3172    }
3173
3174    fn parse_operand(&mut self, span: TokenSpan) -> Result<BuiltNode, ParserError> {
3175        self.admit(1)?;
3176        let value = self.span_value(&span);
3177        let token = self.span_to_token(&span);
3178
3179        match span.subtype {
3180            TokenSubType::Number => {
3181                let value = value.parse::<f64>().map_err(|_| ParserError {
3182                    message: format!("Invalid number: {value}"),
3183                    position: Some(self.byte_offset(self.position)),
3184                })?;
3185                Ok(BuiltNode {
3186                    height: 1,
3187                    node: ASTNode::new(
3188                        ASTNodeType::Literal(LiteralValue::Number(value)),
3189                        Some(token),
3190                    ),
3191                })
3192            }
3193            TokenSubType::Text => {
3194                let text = match value.strip_prefix('"').and_then(|v| v.strip_suffix('"')) {
3195                    Some(inner) => inner.replace("\"\"", "\""),
3196                    None => value.to_string(),
3197                };
3198                Ok(BuiltNode {
3199                    height: 1,
3200                    node: ASTNode::new(ASTNodeType::Literal(LiteralValue::Text(text)), Some(token)),
3201                })
3202            }
3203            TokenSubType::Logical => {
3204                let v = value.eq_ignore_ascii_case("TRUE");
3205                Ok(BuiltNode {
3206                    height: 1,
3207                    node: ASTNode::new(ASTNodeType::Literal(LiteralValue::Boolean(v)), Some(token)),
3208                })
3209            }
3210            TokenSubType::Error => {
3211                let error = ExcelError::from_error_string(value);
3212                Ok(BuiltNode {
3213                    height: 1,
3214                    node: ASTNode::new(
3215                        ASTNodeType::Literal(LiteralValue::Error(error)),
3216                        Some(token),
3217                    ),
3218                })
3219            }
3220            TokenSubType::Range => {
3221                let reference = ReferenceType::from_string_with_dialect(value, self.dialect)
3222                    .map_err(|e| ParserError {
3223                        message: format!("Invalid reference '{value}': {e}"),
3224                        position: Some(self.byte_offset(self.position)),
3225                    })?;
3226                Ok(BuiltNode {
3227                    height: 1,
3228                    node: ASTNode::new(
3229                        ASTNodeType::Reference {
3230                            original: value.to_string(),
3231                            reference,
3232                        },
3233                        Some(token),
3234                    ),
3235                })
3236            }
3237            _ => Err(ParserError {
3238                message: format!("Unexpected operand subtype: {:?}", span.subtype),
3239                position: Some(self.byte_offset(self.position)),
3240            }),
3241        }
3242    }
3243
3244    #[expect(
3245        clippy::arithmetic_side_effects,
3246        reason = "child heights are at most the AST height limit and the number of admitted nodes, so 1 + height cannot overflow"
3247    )]
3248    fn parse_function(&mut self, func_span: TokenSpan) -> Result<BuiltNode, ParserError> {
3249        let Some(name) = self.span_value(&func_span).strip_suffix('(') else {
3250            return Err(ParserError {
3251                message: "Invalid function token".to_string(),
3252                position: Some(self.byte_offset(self.position)),
3253            });
3254        };
3255        let name = name.to_string();
3256        let args = self.parse_function_arguments()?;
3257
3258        let this_is_volatile = self
3259            .volatility_classifier
3260            .as_ref()
3261            .map(|f| f(name.as_str()))
3262            .unwrap_or(false);
3263        let args_volatile = args.volatile;
3264
3265        let height = 1 + args.height;
3266        self.admit(height)?;
3267        let func_token = self.span_to_token(&func_span);
3268        Ok(BuiltNode {
3269            height,
3270            node: ASTNode::new_with_volatile(
3271                ASTNodeType::Function {
3272                    name,
3273                    args: args.nodes,
3274                },
3275                Some(func_token),
3276                this_is_volatile || args_volatile,
3277            ),
3278        })
3279    }
3280
3281    /// Parse arguments for a postfix call (immediate invocation), where the
3282    /// opening `(` is a `Paren:Open` and the matching `)` is a `Paren:Close`.
3283    /// Caller has already consumed the opening paren. See the classic parser
3284    /// version for details on how top-level `,` is handled.
3285    #[expect(
3286        clippy::arithmetic_side_effects,
3287        reason = "in_call_args_depth is incremented and decremented once per call, so it counts active calls and is bounded by the Pratt-frame limit"
3288    )]
3289    fn parse_call_arguments(&mut self) -> Result<BuiltArgs, ParserError> {
3290        let mut args = BuiltArgs::default();
3291
3292        self.skip_whitespace();
3293        if self.peek_is(TokenType::Paren, TokenSubType::Close) {
3294            self.advance();
3295            return Ok(args);
3296        }
3297
3298        self.in_call_args_depth += 1;
3299        let result = (|| -> Result<BuiltArgs, ParserError> {
3300            let mut expecting_argument = true;
3301            let mut saw_argument = false;
3302            loop {
3303                self.skip_whitespace();
3304                let Some(token) = self.peek() else {
3305                    return Err(ParserError {
3306                        message: "Unterminated call argument list".to_string(),
3307                        position: Some(self.byte_offset(self.position)),
3308                    });
3309                };
3310
3311                let is_separator = (token.token_type == TokenType::Sep
3312                    && token.subtype == TokenSubType::Arg)
3313                    || (token.token_type == TokenType::OpInfix && self.span_value(&token) == ",");
3314                let is_close =
3315                    token.token_type == TokenType::Paren && token.subtype == TokenSubType::Close;
3316
3317                if expecting_argument {
3318                    if is_close {
3319                        if saw_argument {
3320                            args.push(self.omitted()?);
3321                        }
3322                        self.advance();
3323                        return Ok(std::mem::take(&mut args));
3324                    }
3325                    if is_separator {
3326                        args.push(self.omitted()?);
3327                        saw_argument = true;
3328                        self.advance();
3329                    } else {
3330                        args.push(self.parse_expression()?);
3331                        saw_argument = true;
3332                        expecting_argument = false;
3333                    }
3334                } else if is_separator {
3335                    self.advance();
3336                    expecting_argument = true;
3337                } else if is_close {
3338                    self.advance();
3339                    return Ok(std::mem::take(&mut args));
3340                } else {
3341                    return Err(ParserError {
3342                        message: format!("Expected ',' or ')' in call arguments, got {token:?}"),
3343                        position: Some(self.byte_offset(self.position)),
3344                    });
3345                }
3346            }
3347        })();
3348        self.in_call_args_depth -= 1;
3349        result
3350    }
3351
3352    fn parse_function_arguments(&mut self) -> Result<BuiltArgs, ParserError> {
3353        let mut args = BuiltArgs::default();
3354
3355        self.skip_whitespace();
3356        if self.peek_is(TokenType::Func, TokenSubType::Close) {
3357            self.advance();
3358            return Ok(args);
3359        }
3360
3361        let mut expecting_argument = true;
3362        let mut saw_argument = false;
3363        loop {
3364            self.skip_whitespace();
3365            let Some(token) = self.peek() else {
3366                return Err(ParserError {
3367                    message: "Unterminated function argument list".to_string(),
3368                    position: Some(self.byte_offset(self.position)),
3369                });
3370            };
3371
3372            let is_separator =
3373                token.token_type == TokenType::Sep && token.subtype == TokenSubType::Arg;
3374            let is_close =
3375                token.token_type == TokenType::Func && token.subtype == TokenSubType::Close;
3376
3377            if expecting_argument {
3378                if is_close {
3379                    if saw_argument {
3380                        args.push(self.omitted()?);
3381                    }
3382                    self.advance();
3383                    return Ok(args);
3384                }
3385                if is_separator {
3386                    args.push(self.omitted()?);
3387                    saw_argument = true;
3388                    self.advance();
3389                } else {
3390                    args.push(self.parse_expression()?);
3391                    saw_argument = true;
3392                    expecting_argument = false;
3393                }
3394            } else if is_separator {
3395                self.advance();
3396                expecting_argument = true;
3397            } else if is_close {
3398                self.advance();
3399                return Ok(args);
3400            } else {
3401                return Err(ParserError {
3402                    message: format!("Expected ',' or ')' in function arguments, got {token:?}"),
3403                    position: Some(self.byte_offset(self.position)),
3404                });
3405            }
3406        }
3407    }
3408
3409    #[expect(
3410        clippy::arithmetic_side_effects,
3411        reason = "child heights are at most the AST height limit and the number of admitted nodes, so 1 + height cannot overflow"
3412    )]
3413    fn append_array_item(
3414        &mut self,
3415        row: &mut Vec<ASTNode>,
3416        height: &mut usize,
3417        volatile: &mut bool,
3418    ) -> Result<(), ParserError> {
3419        let child = self.parse_expression()?;
3420        *height = (*height).max(1 + child.height);
3421        *volatile |= child.contains_volatile;
3422        row.push(child.node);
3423        Ok(())
3424    }
3425    fn finish_array(
3426        &mut self,
3427        rows: Vec<Vec<ASTNode>>,
3428        height: usize,
3429        volatile: bool,
3430    ) -> Result<BuiltNode, ParserError> {
3431        self.admit(height)?;
3432        Ok(BuiltNode {
3433            height,
3434            node: ASTNode::new_with_volatile(ASTNodeType::Array(rows), None, volatile),
3435        })
3436    }
3437    fn parse_array(&mut self) -> Result<BuiltNode, ParserError> {
3438        let mut rows = Vec::new();
3439        let mut current_row = Vec::new();
3440        let mut height = 1;
3441        let mut contains_volatile = false;
3442
3443        self.skip_whitespace();
3444        if self.peek_is(TokenType::Array, TokenSubType::Close) {
3445            self.advance();
3446            return self.finish_array(rows, 1, false);
3447        }
3448
3449        self.append_array_item(&mut current_row, &mut height, &mut contains_volatile)?;
3450
3451        while self.position < self.tokens.len() {
3452            self.skip_whitespace();
3453            let Some(token) = self.peek() else {
3454                break;
3455            };
3456
3457            // Every arm advances `position` or leaves the loop. A separator
3458            // with another subtype (possible in an external TokenStream) is an
3459            // unexpected token, not a no-op that would loop forever.
3460            match (token.token_type, token.subtype) {
3461                (TokenType::Sep, TokenSubType::Arg) => {
3462                    self.advance();
3463                    self.append_array_item(&mut current_row, &mut height, &mut contains_volatile)?;
3464                }
3465                (TokenType::Sep, TokenSubType::Row) => {
3466                    self.advance();
3467                    rows.push(current_row);
3468                    current_row = Vec::new();
3469                    self.append_array_item(&mut current_row, &mut height, &mut contains_volatile)?;
3470                }
3471                (TokenType::Array, TokenSubType::Close) => {
3472                    self.advance();
3473                    rows.push(current_row);
3474                    break;
3475                }
3476                _ => {
3477                    return Err(ParserError {
3478                        message: format!("Unexpected token in array: {token:?}"),
3479                        position: Some(self.byte_offset(self.position)),
3480                    });
3481                }
3482            }
3483        }
3484
3485        // Array evaluation requires a rectangular shape. Reject malformed
3486        // literals before returning an AST to assignment or ingestion; never
3487        // silently pad missing elements.
3488        if let Some(first) = rows.first()
3489            && rows.iter().any(|row| row.len() != first.len())
3490        {
3491            return Err(ParserError {
3492                message: "Array rows must have equal length".to_string(),
3493                position: Some(self.byte_offset(self.position.saturating_sub(1))),
3494            });
3495        }
3496
3497        self.finish_array(rows, height, contains_volatile)
3498    }
3499}
3500
3501impl TryFrom<&str> for Parser {
3502    type Error = TokenizerError;
3503
3504    fn try_from(formula: &str) -> Result<Self, Self::Error> {
3505        Self::new(formula)
3506    }
3507}
3508
3509impl TryFrom<String> for Parser {
3510    type Error = TokenizerError;
3511
3512    fn try_from(formula: String) -> Result<Self, Self::Error> {
3513        Self::new(formula)
3514    }
3515}
3516
3517impl From<&TokenStream> for Parser {
3518    fn from(stream: &TokenStream) -> Self {
3519        Self::from_token_stream(stream)
3520    }
3521}
3522
3523impl FromStr for ASTNode {
3524    type Err = ParserError;
3525
3526    fn from_str(formula: &str) -> Result<Self, Self::Err> {
3527        parse(formula)
3528    }
3529}
3530
3531impl TryFrom<&str> for ASTNode {
3532    type Error = ParserError;
3533
3534    fn try_from(formula: &str) -> Result<Self, Self::Error> {
3535        parse(formula)
3536    }
3537}
3538
3539impl TryFrom<String> for ASTNode {
3540    type Error = ParserError;
3541
3542    fn try_from(formula: String) -> Result<Self, Self::Error> {
3543        parse(formula)
3544    }
3545}
3546
3547impl Parser {
3548    pub fn builder() -> ParserBuilder {
3549        ParserBuilder::default()
3550    }
3551}
3552
3553#[derive(Default)]
3554pub struct ParserBuilder {
3555    dialect: FormulaDialect,
3556    volatility_classifier: Option<VolatilityClassifierArc>,
3557    limits: ParserLimits,
3558}
3559
3560impl ParserBuilder {
3561    pub fn limits(mut self, limits: ParserLimits) -> Self {
3562        self.limits = limits;
3563        self
3564    }
3565    pub fn dialect(mut self, dialect: FormulaDialect) -> Self {
3566        self.dialect = dialect;
3567        self
3568    }
3569
3570    pub fn with_volatility_classifier<F>(mut self, f: F) -> Self
3571    where
3572        F: Fn(&str) -> bool + Send + Sync + 'static,
3573    {
3574        self.volatility_classifier = Some(Arc::new(f));
3575        self
3576    }
3577
3578    pub fn build<T: AsRef<str>>(self, formula: T) -> Result<Parser, TokenizerError> {
3579        let mut parser = Parser::new_with_limits(formula.as_ref(), self.dialect, self.limits)?;
3580        if let Some(classifier) = self.volatility_classifier {
3581            parser = parser.with_volatility_classifier(move |name| classifier(name));
3582        }
3583        Ok(parser)
3584    }
3585
3586    pub fn parse<T: AsRef<str>>(self, formula: T) -> Result<ASTNode, ParserError> {
3587        let mut parser = self.build(formula)?;
3588        parser.parse()
3589    }
3590}
3591
3592/// Normalise a reference string to its canonical form
3593pub fn normalise_reference(reference: &str) -> Result<String, ParsingError> {
3594    let ref_type = ReferenceType::from_string(reference)?;
3595    Ok(ref_type.to_string())
3596}
3597
3598pub fn parse<T: AsRef<str>>(formula: T) -> Result<ASTNode, ParserError> {
3599    parse_with_dialect(formula, FormulaDialect::Excel)
3600}
3601
3602pub fn parse_with_dialect<T: AsRef<str>>(
3603    formula: T,
3604    dialect: FormulaDialect,
3605) -> Result<ASTNode, ParserError> {
3606    let mut parser = Parser::new_with_dialect(formula, dialect)?;
3607    parser.parse()
3608}
3609
3610/// Parse a single formula and annotate volatility using the provided classifier.
3611/// This is a convenience wrapper around `Parser::with_volatility_classifier`.
3612pub fn parse_with_volatility_classifier<T, F>(
3613    formula: T,
3614    classifier: F,
3615) -> Result<ASTNode, ParserError>
3616where
3617    T: AsRef<str>,
3618    F: Fn(&str) -> bool + Send + Sync + 'static,
3619{
3620    parse_with_dialect_and_volatility_classifier(formula, FormulaDialect::Excel, classifier)
3621}
3622
3623pub fn parse_with_dialect_and_volatility_classifier<T, F>(
3624    formula: T,
3625    dialect: FormulaDialect,
3626    classifier: F,
3627) -> Result<ASTNode, ParserError>
3628where
3629    T: AsRef<str>,
3630    F: Fn(&str) -> bool + Send + Sync + 'static,
3631{
3632    let mut parser =
3633        Parser::new_with_dialect(formula, dialect)?.with_volatility_classifier(classifier);
3634    parser.parse()
3635}
3636
3637/// Efficient batch parser with an internal token cache and optional volatility classifier.
3638///
3639/// The cache is keyed by the original formula string; repeated formulas across a batch
3640/// (very common in spreadsheets) will avoid re-tokenization and whitespace filtering.
3641pub struct BatchParser {
3642    include_whitespace: bool,
3643    volatility_classifier: Option<VolatilityClassifierArc>,
3644    token_cache: crate::token_cache::TokenCache,
3645    limits: ParserLimits,
3646    dialect: FormulaDialect,
3647}
3648
3649impl BatchParser {
3650    pub fn builder() -> BatchParserBuilder {
3651        BatchParserBuilder::default()
3652    }
3653
3654    /// Parse a formula using the internal cache and configured classifier.
3655    pub fn parse(&mut self, formula: &str) -> Result<ASTNode, ParserError> {
3656        self.limits.check_source(formula)?;
3657        let (source, spans) = if let Some(pair) = self.token_cache.get(formula) {
3658            pair
3659        } else {
3660            let mut spans =
3661                crate::tokenizer::tokenize_spans_with_limits(formula, self.dialect, self.limits)?;
3662            let source: Arc<str> = Arc::from(formula);
3663            if !self.include_whitespace {
3664                spans.retain(|t| t.token_type != TokenType::Whitespace);
3665            }
3666
3667            let spans: Arc<[TokenSpan]> = Arc::from(spans.into_boxed_slice());
3668            self.token_cache
3669                .insert(Arc::clone(&source), Arc::clone(&spans));
3670            (source, spans)
3671        };
3672
3673        let mut parser = Parser::from_source_and_tokens(source, spans, self.dialect);
3674        parser.limits = self.limits;
3675        if let Some(classifier) = self.volatility_classifier.clone() {
3676            parser = parser.with_volatility_classifier(move |name| classifier(name));
3677        }
3678        parser.parse()
3679    }
3680}
3681
3682pub struct BatchParserBuilder {
3683    limits: ParserLimits,
3684    cache_entries: usize,
3685    cache_bytes: usize,
3686    include_whitespace: bool,
3687    volatility_classifier: Option<VolatilityClassifierArc>,
3688    dialect: FormulaDialect,
3689}
3690
3691impl Default for BatchParserBuilder {
3692    fn default() -> Self {
3693        Self {
3694            include_whitespace: false,
3695            volatility_classifier: None,
3696            dialect: FormulaDialect::default(),
3697            limits: ParserLimits::default(),
3698            cache_entries: 16_384,
3699            cache_bytes: 8 * 1024 * 1024,
3700        }
3701    }
3702}
3703impl BatchParserBuilder {
3704    pub fn limits(mut self, limits: ParserLimits) -> Self {
3705        self.limits = limits;
3706        self
3707    }
3708    /// Set FIFO cache payload bounds. Zero entries disables retention.
3709    pub fn cache_capacity(mut self, entries: usize, bytes: usize) -> Self {
3710        self.cache_entries = entries;
3711        self.cache_bytes = bytes;
3712        self
3713    }
3714    pub fn include_whitespace(mut self, include: bool) -> Self {
3715        self.include_whitespace = include;
3716        self
3717    }
3718
3719    pub fn with_volatility_classifier<F>(mut self, f: F) -> Self
3720    where
3721        F: Fn(&str) -> bool + Send + Sync + 'static,
3722    {
3723        self.volatility_classifier = Some(Arc::new(f));
3724        self
3725    }
3726
3727    pub fn dialect(mut self, dialect: FormulaDialect) -> Self {
3728        self.dialect = dialect;
3729        self
3730    }
3731
3732    pub fn build(self) -> BatchParser {
3733        BatchParser {
3734            include_whitespace: self.include_whitespace,
3735            volatility_classifier: self.volatility_classifier,
3736            token_cache: crate::token_cache::TokenCache::new(self.cache_entries, self.cache_bytes),
3737            limits: self.limits,
3738            dialect: self.dialect,
3739        }
3740    }
3741}