Skip to main content

formualizer_parse/
parser.rs

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