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