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