Skip to main content

formualizer_parse/
tokenizer.rs

1use std::convert::TryFrom;
2use std::error::Error;
3use std::fmt::{self, Display};
4use std::sync::Arc;
5
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8
9use crate::types::FormulaDialect;
10
11const TOKEN_ENDERS: &str = ",;}) +-*/^&=><%@";
12
13const fn build_token_enders() -> [bool; 256] {
14    let mut tbl = [false; 256];
15    let bytes = TOKEN_ENDERS.as_bytes();
16    let mut i = 0;
17    while i < bytes.len() {
18        tbl[bytes[i] as usize] = true;
19        i += 1;
20    }
21    tbl
22}
23static TOKEN_ENDERS_TABLE: [bool; 256] = build_token_enders();
24
25#[inline(always)]
26fn is_token_ender(c: u8) -> bool {
27    TOKEN_ENDERS_TABLE[c as usize]
28}
29
30// Recognised Excel error literals. The lookup matches an exact-length slice
31// using `eq_ignore_ascii_case`, so ordering only matters when one entry is a
32// prefix of another (none currently are). We keep entries grouped by era and
33// sorted longest-first defensively so future additions don't introduce
34// prefix-collision ambiguity.
35//
36// Modern (Excel 2018+) literals currently recognised here are limited to the
37// ones whose `ExcelErrorKind` already exists in `formualizer-common`:
38//   - `#SPILL!` (dynamic-array spill blocked)
39//   - `#CALC!`  (generic calc-engine error)
40// Other modern literals (`#FIELD!`, `#BLOCKED!`, `#CONNECT!`, `#UNKNOWN!`,
41// `#EXTERNAL!`, `#BUSY!`, `#PYTHON!`) are intentionally not recognised yet;
42// adding them requires a coordinated change in `ExcelErrorKind` and all
43// downstream exhaustive matches/bindings/serde/display.
44//
45// `#GETTING_DATA` is kept for OOXML/legacy compatibility.
46static ERROR_CODES: &[&str] = &[
47    "#GETTING_DATA",
48    "#DIV/0!",
49    "#VALUE!",
50    "#SPILL!",
51    "#NAME?",
52    "#NULL!",
53    "#CALC!",
54    "#NUM!",
55    "#REF!",
56    "#N/A",
57];
58
59/// Represents operator associativity.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum Associativity {
62    Left,
63    Right,
64}
65
66/// A custom error type for the tokenizer.
67#[derive(Debug)]
68pub struct TokenizerError {
69    pub message: String,
70    pub pos: usize,
71}
72
73/// Recovering action taken for a malformed span.
74#[non_exhaustive]
75#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
77pub enum RecoveryAction {
78    /// Unmatched closer was emitted as a recovery span and scanning continued.
79    SkippedUnmatchedCloser,
80    /// Unterminated string was emitted as one recovery span.
81    UnterminatedString,
82    /// Unmatched opening '[' was emitted as a recovery span.
83    UnmatchedBracket,
84    /// Invalid # literal was emitted as a recovery span.
85    InvalidErrorLiteral,
86    /// Unmatched opener was recorded at end-of-input.
87    UnmatchedOpener,
88}
89
90/// Token-level diagnostic emitted by best-effort tokenization.
91#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct TokenDiagnostic {
94    pub span: TokenSpan,
95    pub message: String,
96    pub recovery: RecoveryAction,
97}
98
99impl TokenDiagnostic {
100    fn new(span: TokenSpan, message: String, recovery: RecoveryAction) -> Self {
101        Self {
102            span,
103            message,
104            recovery,
105        }
106    }
107}
108
109#[derive(Debug, Clone)]
110struct SpanTokenizerError {
111    kind: SpanTokenizerErrorKind,
112    pos: usize,
113    message: String,
114    span_start: Option<usize>,
115    span_end: Option<usize>,
116}
117
118#[derive(Debug, Clone, Copy)]
119enum SpanTokenizerErrorKind {
120    NoMatchingOpener,
121    UnmatchedOpening,
122    UnterminatedString,
123    UnmatchedBracket,
124    MismatchedPair,
125    InvalidErrorLiteral,
126}
127
128impl From<SpanTokenizerError> for TokenizerError {
129    fn from(value: SpanTokenizerError) -> Self {
130        TokenizerError {
131            message: value.message,
132            pos: value.pos,
133        }
134    }
135}
136
137impl fmt::Display for TokenizerError {
138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139        write!(f, "TokenizerError: {}", self.message)
140    }
141}
142
143impl Error for TokenizerError {}
144
145/// The type of a token.
146#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
147#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
148pub enum TokenType {
149    Literal,
150    Operand,
151    Func,
152    Array,
153    Paren,
154    Sep,
155    OpPrefix,
156    OpInfix,
157    OpPostfix,
158    Whitespace,
159}
160impl Display for TokenType {
161    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162        write!(f, "{self:?}")
163    }
164}
165
166/// The subtype of a token.
167#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
168#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
169pub enum TokenSubType {
170    None,
171    Text,
172    Number,
173    Logical,
174    Error,
175    Range,
176    Open,
177    Close,
178    Arg,
179    Row,
180}
181impl Display for TokenSubType {
182    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183        write!(f, "{self:?}")
184    }
185}
186
187/// A token in an Excel formula.
188#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
189#[derive(Debug, Clone, PartialEq, Hash)]
190pub struct Token {
191    pub value: String, // We'll keep this for API compatibility but compute it lazily
192    pub token_type: TokenType,
193    pub subtype: TokenSubType,
194    pub start: usize,
195    pub end: usize,
196}
197
198impl Display for Token {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        write!(
201            f,
202            "<{} subtype: {:?} value: {}>",
203            self.token_type, self.subtype, self.value
204        )
205    }
206}
207
208impl Token {
209    pub fn new(value: String, token_type: TokenType, subtype: TokenSubType) -> Self {
210        Token {
211            value,
212            token_type,
213            subtype,
214            start: 0,
215            end: 0,
216        }
217    }
218
219    pub fn new_with_span(
220        value: String,
221        token_type: TokenType,
222        subtype: TokenSubType,
223        start: usize,
224        end: usize,
225    ) -> Self {
226        Token {
227            value,
228            token_type,
229            subtype,
230            start,
231            end,
232        }
233    }
234
235    fn from_slice(
236        source: &str,
237        token_type: TokenType,
238        subtype: TokenSubType,
239        start: usize,
240        end: usize,
241    ) -> Self {
242        Token {
243            value: source[start..end].to_string(),
244            token_type,
245            subtype,
246            start,
247            end,
248        }
249    }
250
251    pub fn is_operator(&self) -> bool {
252        matches!(
253            self.token_type,
254            TokenType::OpPrefix | TokenType::OpInfix | TokenType::OpPostfix
255        )
256    }
257
258    pub fn get_precedence(&self) -> Option<(u8, Associativity)> {
259        // For a prefix operator, use the 'u' key.
260        let op = if self.token_type == TokenType::OpPrefix {
261            "u"
262        } else {
263            self.value.as_str()
264        };
265
266        // Higher number => tighter binding.
267        // Excel precedence (high to low, simplified):
268        //   reference ops (:
269        //   postfix %
270        //   prefix unary +/- (binds tighter than ^)
271        //   exponent ^ (right-assoc)
272        //   */
273        //   +-
274        //   &
275        //   comparisons
276        match op {
277            "#" => Some((11, Associativity::Left)),
278            ":" => Some((10, Associativity::Left)),
279            " " => Some((9, Associativity::Left)),
280            "," => Some((8, Associativity::Left)),
281            "%" => Some((7, Associativity::Left)),
282            "u" => Some((6, Associativity::Right)),
283            "^" => Some((5, Associativity::Right)),
284            "*" | "/" => Some((4, Associativity::Left)),
285            "+" | "-" => Some((3, Associativity::Left)),
286            "&" => Some((2, Associativity::Left)),
287            "=" | "<" | ">" | "<=" | ">=" | "<>" => Some((1, Associativity::Left)),
288            _ => None,
289        }
290    }
291
292    /// Create an operand token based on the value.
293    pub fn make_operand(value: String) -> Self {
294        let subtype = if value.starts_with('"') {
295            TokenSubType::Text
296        } else if value.starts_with('#') {
297            TokenSubType::Error
298        } else if value.eq_ignore_ascii_case("TRUE") || value.eq_ignore_ascii_case("FALSE") {
299            TokenSubType::Logical
300        } else if value.parse::<f64>().is_ok() {
301            TokenSubType::Number
302        } else {
303            TokenSubType::Range
304        };
305        Token::new(value, TokenType::Operand, subtype)
306    }
307
308    /// Create an operand token with byte position span.
309    pub fn make_operand_with_span(value: String, start: usize, end: usize) -> Self {
310        let subtype = if value.starts_with('"') {
311            TokenSubType::Text
312        } else if value.starts_with('#') {
313            TokenSubType::Error
314        } else if value.eq_ignore_ascii_case("TRUE") || value.eq_ignore_ascii_case("FALSE") {
315            TokenSubType::Logical
316        } else if value.parse::<f64>().is_ok() {
317            TokenSubType::Number
318        } else {
319            TokenSubType::Range
320        };
321        Token::new_with_span(value, TokenType::Operand, subtype, start, end)
322    }
323
324    fn make_operand_from_slice(source: &str, start: usize, end: usize) -> Self {
325        let value_str = &source[start..end];
326        let subtype = if value_str.starts_with('"') {
327            TokenSubType::Text
328        } else if value_str.starts_with('#') {
329            TokenSubType::Error
330        } else if value_str.eq_ignore_ascii_case("TRUE") || value_str.eq_ignore_ascii_case("FALSE")
331        {
332            TokenSubType::Logical
333        } else if value_str.parse::<f64>().is_ok() {
334            TokenSubType::Number
335        } else {
336            TokenSubType::Range
337        };
338        Token::from_slice(source, TokenType::Operand, subtype, start, end)
339    }
340
341    /// Create a subexpression token.
342    ///
343    /// `value` must end with one of '{', '}', '(' or ')'. If `func` is true,
344    /// the token's type is forced to be Func.
345    pub fn make_subexp(value: &str, func: bool) -> Self {
346        let last_char = value.chars().last().expect("Empty token value");
347        assert!(matches!(last_char, '{' | '}' | '(' | ')'));
348        let token_type = if func {
349            TokenType::Func
350        } else if "{}".contains(last_char) {
351            TokenType::Array
352        } else if "()".contains(last_char) {
353            TokenType::Paren
354        } else {
355            TokenType::Func
356        };
357        let subtype = if ")}".contains(last_char) {
358            TokenSubType::Close
359        } else {
360            TokenSubType::Open
361        };
362        Token::new(value.to_string(), token_type, subtype)
363    }
364
365    /// Create a subexpression token with byte position span.
366    pub fn make_subexp_with_span(value: &str, func: bool, start: usize, end: usize) -> Self {
367        let last_char = value.chars().last().expect("Empty token value");
368        assert!(matches!(last_char, '{' | '}' | '(' | ')'));
369        let token_type = if func {
370            TokenType::Func
371        } else if "{}".contains(last_char) {
372            TokenType::Array
373        } else if "()".contains(last_char) {
374            TokenType::Paren
375        } else {
376            TokenType::Func
377        };
378        let subtype = if ")}".contains(last_char) {
379            TokenSubType::Close
380        } else {
381            TokenSubType::Open
382        };
383        Token::new_with_span(value.to_string(), token_type, subtype, start, end)
384    }
385
386    fn make_subexp_from_slice(source: &str, func: bool, start: usize, end: usize) -> Self {
387        let value_str = &source[start..end];
388        let last_char = value_str.chars().last().expect("Empty token value");
389        let token_type = if func {
390            TokenType::Func
391        } else if "{}".contains(last_char) {
392            TokenType::Array
393        } else if "()".contains(last_char) {
394            TokenType::Paren
395        } else {
396            TokenType::Func
397        };
398        let subtype = if ")}".contains(last_char) {
399            TokenSubType::Close
400        } else {
401            TokenSubType::Open
402        };
403        Token::from_slice(source, token_type, subtype, start, end)
404    }
405
406    /// Given an opener token, return its corresponding closer token.
407    pub fn get_closer(&self) -> Result<Token, TokenizerError> {
408        if self.subtype != TokenSubType::Open {
409            return Err(TokenizerError {
410                message: "Token is not an opener".to_string(),
411                pos: 0,
412            });
413        }
414        let closer_value = if self.token_type == TokenType::Array {
415            "}"
416        } else {
417            ")"
418        };
419        Ok(Token::make_subexp(
420            closer_value,
421            self.token_type == TokenType::Func,
422        ))
423    }
424
425    /// Create a separator token.
426    pub fn make_separator(value: &str) -> Self {
427        assert!(value == "," || value == ";");
428        let subtype = if value == "," {
429            TokenSubType::Arg
430        } else {
431            TokenSubType::Row
432        };
433        Token::new(value.to_string(), TokenType::Sep, subtype)
434    }
435
436    /// Create a separator token with byte position span.
437    pub fn make_separator_with_span(value: &str, start: usize, end: usize) -> Self {
438        assert!(value == "," || value == ";");
439        let subtype = if value == "," {
440            TokenSubType::Arg
441        } else {
442            TokenSubType::Row
443        };
444        Token::new_with_span(value.to_string(), TokenType::Sep, subtype, start, end)
445    }
446}
447
448#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
449#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
450pub struct TokenSpan {
451    pub token_type: TokenType,
452    pub subtype: TokenSubType,
453    pub start: usize,
454    pub end: usize,
455}
456
457#[derive(Debug, Clone, Copy, PartialEq, Eq)]
458pub struct TokenView<'a> {
459    pub span: &'a TokenSpan,
460    pub value: &'a str,
461}
462
463/// Source-backed token stream (span-only).
464///
465/// This is intended as a high-performance representation for callers that
466/// want to avoid allocating a `String` per token. It can materialize owned
467/// `Token`s when needed (FFI/debug).
468#[derive(Debug, Clone)]
469pub struct TokenStream {
470    source: Arc<str>,
471    pub spans: Vec<TokenSpan>,
472    dialect: FormulaDialect,
473    diagnostics: Vec<TokenDiagnostic>,
474}
475
476fn is_whitespace_intersection_span(source: &str, span: &TokenSpan) -> bool {
477    if span.token_type != TokenType::OpInfix {
478        return false;
479    }
480    let Some(value) = source.get(span.start..span.end) else {
481        return false;
482    };
483    value.as_bytes().contains(&b' ')
484        && value
485            .as_bytes()
486            .iter()
487            .all(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
488}
489
490impl TokenStream {
491    pub fn new(formula: &str) -> Result<Self, TokenizerError> {
492        Self::new_with_dialect(formula, FormulaDialect::Excel)
493    }
494
495    pub fn new_with_dialect(
496        formula: &str,
497        dialect: FormulaDialect,
498    ) -> Result<Self, TokenizerError> {
499        let source: Arc<str> = Arc::from(formula);
500        let spans = tokenize_spans_with_dialect(source.as_ref(), dialect)?;
501        Ok(TokenStream {
502            source,
503            spans,
504            dialect,
505            diagnostics: Vec::new(),
506        })
507    }
508
509    pub fn new_best_effort(formula: &str) -> Self {
510        Self::new_best_effort_with_dialect(formula, FormulaDialect::Excel)
511    }
512
513    pub fn new_best_effort_with_dialect(formula: &str, dialect: FormulaDialect) -> Self {
514        let source: Arc<str> = Arc::from(formula);
515        let mut tokenizer = SpanTokenizer::new(source.as_ref(), dialect);
516        let spans = tokenizer.parse_best_effort();
517        let diagnostics = tokenizer.diagnostics;
518        TokenStream {
519            source,
520            spans,
521            dialect,
522            diagnostics,
523        }
524    }
525
526    pub fn diagnostics(&self) -> Vec<TokenDiagnostic> {
527        self.diagnostics.clone()
528    }
529
530    pub fn diagnostics_ref(&self) -> &[TokenDiagnostic] {
531        &self.diagnostics
532    }
533
534    pub fn has_errors(&self) -> bool {
535        !self.diagnostics.is_empty()
536    }
537
538    pub fn invalid_spans_iter(&self) -> impl Iterator<Item = &TokenSpan> {
539        self.spans.iter().filter(|span| {
540            self.diagnostics.iter().any(|diag| {
541                diag.span.start == span.start
542                    && diag.span.end == span.end
543                    && diag.span.token_type == span.token_type
544            })
545        })
546    }
547
548    pub fn invalid_spans(&self) -> Vec<&TokenSpan> {
549        self.invalid_spans_iter().collect()
550    }
551
552    pub fn source(&self) -> &str {
553        &self.source
554    }
555
556    pub fn dialect(&self) -> FormulaDialect {
557        self.dialect
558    }
559
560    pub fn len(&self) -> usize {
561        self.spans.len()
562    }
563
564    pub fn is_empty(&self) -> bool {
565        self.spans.is_empty()
566    }
567
568    pub fn get(&self, index: usize) -> Option<TokenView<'_>> {
569        let span = self.spans.get(index)?;
570        let value = self.source.get(span.start..span.end)?;
571        Some(TokenView { span, value })
572    }
573
574    pub fn to_tokens(&self) -> Vec<Token> {
575        self.spans
576            .iter()
577            .map(|s| {
578                let value = if is_whitespace_intersection_span(&self.source, s) {
579                    " ".to_string()
580                } else {
581                    self.source
582                        .get(s.start..s.end)
583                        .unwrap_or_default()
584                        .to_string()
585                };
586                Token::new_with_span(value, s.token_type, s.subtype, s.start, s.end)
587            })
588            .collect()
589    }
590
591    /// Reconstruct the tokenized payload from spans.
592    ///
593    /// For formulas that start with '=', this intentionally omits the leading
594    /// '=' to preserve historical `TokenStream::render()` behavior.
595    pub fn render(&self) -> String {
596        let mut out = String::with_capacity(self.source.len());
597        for span in &self.spans {
598            if let Some(s) = self.source.get(span.start..span.end) {
599                out.push_str(s);
600            }
601        }
602        out
603    }
604
605    /// Reconstruct the full input formula, including a leading '=' when present.
606    pub fn render_formula(&self) -> String {
607        if self.source.as_bytes().first() == Some(&b'=') {
608            format!("={}", self.render())
609        } else {
610            self.render()
611        }
612    }
613}
614
615pub(crate) fn tokenize_spans_with_dialect(
616    formula: &str,
617    dialect: FormulaDialect,
618) -> Result<Vec<TokenSpan>, TokenizerError> {
619    let mut tokenizer = SpanTokenizer::new(formula, dialect);
620    tokenizer.parse()?;
621    Ok(tokenizer.spans)
622}
623
624fn operand_subtype(value_str: &str) -> TokenSubType {
625    if value_str.starts_with('"') {
626        TokenSubType::Text
627    } else if value_str.starts_with('#') {
628        TokenSubType::Error
629    } else if value_str.eq_ignore_ascii_case("TRUE") || value_str.eq_ignore_ascii_case("FALSE") {
630        TokenSubType::Logical
631    } else if value_str.parse::<f64>().is_ok() {
632        TokenSubType::Number
633    } else {
634        TokenSubType::Range
635    }
636}
637
638fn is_cell_reference_like(value: &str) -> bool {
639    let bytes = value.as_bytes();
640    let mut i = 0;
641
642    if i < bytes.len() && bytes[i] == b'$' {
643        i += 1;
644    }
645
646    let col_start = i;
647    while i < bytes.len() && bytes[i].is_ascii_alphabetic() {
648        i += 1;
649    }
650    if i == col_start {
651        return false;
652    }
653
654    if i < bytes.len() && bytes[i] == b'$' {
655        i += 1;
656    }
657
658    let row_start = i;
659    while i < bytes.len() && bytes[i].is_ascii_digit() {
660        i += 1;
661    }
662
663    i == bytes.len() && i > row_start
664}
665
666fn reference_value_contains_range_colon(value: &str) -> bool {
667    let value_part = value
668        .rsplit_once('!')
669        .map_or(value, |(_, value_part)| value_part);
670    value_part.contains(':')
671}
672
673/// Whether a pending operand contains a structured-reference bracket
674/// (`Table1[Col]`, `Sheet1!Table1[Col]`). A bracket that only appears in the
675/// sheet qualifier is an external workbook (`[1]Sheet1!A1`,
676/// `'[Book.xlsx]Sheet 1'!A1`), whose `:` continues the same range reference.
677fn value_has_structured_reference_bracket(value: &str) -> bool {
678    value
679        .rsplit_once('!')
680        .map_or(value, |(_, value_part)| value_part)
681        .contains('[')
682}
683
684fn is_reference_operand_value(value: &str) -> bool {
685    operand_subtype(value) == TokenSubType::Range
686        && (reference_value_contains_range_colon(value)
687            || value.contains('!')
688            || value.contains('[')
689            || is_cell_reference_like(value))
690}
691
692fn next_starts_reference_expression(formula: &str, mut offset: usize) -> bool {
693    let bytes = formula.as_bytes();
694    while offset < bytes.len() && matches!(bytes[offset], b' ' | b'\t' | b'\r' | b'\n') {
695        offset += 1;
696    }
697    if offset >= bytes.len() {
698        return false;
699    }
700
701    matches!(bytes[offset], b'(' | b'[' | b'\'' | b'$') || bytes[offset].is_ascii_alphabetic()
702}
703
704fn next_reference_has_sheet_qualifier(formula: &str, mut offset: usize) -> bool {
705    let bytes = formula.as_bytes();
706    while offset < bytes.len() && matches!(bytes[offset], b' ' | b'\t' | b'\r' | b'\n') {
707        offset += 1;
708    }
709
710    let mut in_quote = false;
711    while offset < bytes.len() {
712        match bytes[offset] {
713            b'\'' => {
714                if in_quote && offset + 1 < bytes.len() && bytes[offset + 1] == b'\'' {
715                    offset += 2;
716                    continue;
717                }
718                in_quote = !in_quote;
719            }
720            b'!' => return true,
721            b':' if !in_quote => return false,
722            b',' | b';' | b'}' | b')' | b' ' | b'\t' | b'\r' | b'\n' | b'+' | b'-' | b'*'
723            | b'/' | b'^' | b'&' | b'=' | b'>' | b'<' | b'%' | b'@'
724                if !in_quote =>
725            {
726                return false;
727            }
728            _ => {}
729        }
730        offset += 1;
731    }
732
733    false
734}
735
736struct SpanTokenizer<'a> {
737    formula: &'a str,
738    spans: Vec<TokenSpan>,
739    token_stack: Vec<TokenSpan>,
740    offset: usize,
741    token_start: usize,
742    token_end: usize,
743    dialect: FormulaDialect,
744    diagnostics: Vec<TokenDiagnostic>,
745}
746
747impl<'a> SpanTokenizer<'a> {
748    fn new(formula: &'a str, dialect: FormulaDialect) -> Self {
749        SpanTokenizer {
750            formula,
751            spans: Vec::with_capacity(formula.len() / 2),
752            token_stack: Vec::with_capacity(16),
753            offset: 0,
754            token_start: 0,
755            token_end: 0,
756            dialect,
757            diagnostics: Vec::new(),
758        }
759    }
760
761    #[inline]
762    fn current_byte(&self) -> Option<u8> {
763        self.formula.as_bytes().get(self.offset).copied()
764    }
765
766    #[inline]
767    fn has_token(&self) -> bool {
768        self.token_end > self.token_start
769    }
770
771    #[inline]
772    fn start_token(&mut self) {
773        self.token_start = self.offset;
774        self.token_end = self.offset;
775    }
776
777    #[inline]
778    fn extend_token(&mut self) {
779        self.token_end = self.offset;
780    }
781
782    fn push_span(
783        &mut self,
784        token_type: TokenType,
785        subtype: TokenSubType,
786        start: usize,
787        end: usize,
788    ) {
789        self.spans.push(TokenSpan {
790            token_type,
791            subtype,
792            start,
793            end,
794        });
795    }
796
797    fn save_token(&mut self) {
798        if self.has_token() {
799            let value_str = &self.formula[self.token_start..self.token_end];
800            let subtype = operand_subtype(value_str);
801            self.push_span(
802                TokenType::Operand,
803                subtype,
804                self.token_start,
805                self.token_end,
806            );
807        }
808    }
809
810    fn check_scientific_notation(&mut self) -> bool {
811        if let Some(curr_byte) = self.current_byte() {
812            if (curr_byte == b'+' || curr_byte == b'-')
813                && self.has_token()
814                && self.is_scientific_notation_base()
815                && self
816                    .formula
817                    .as_bytes()
818                    .get(self.offset + 1)
819                    .is_some_and(|b| b.is_ascii_digit())
820            {
821                self.offset += 1;
822                self.extend_token();
823                return true;
824            }
825        }
826        false
827    }
828
829    fn is_scientific_notation_base(&self) -> bool {
830        if !self.has_token() {
831            return false;
832        }
833
834        let token_slice = &self.formula.as_bytes()[self.token_start..self.token_end];
835        if token_slice.len() < 2 {
836            return false;
837        }
838
839        let last = token_slice[token_slice.len() - 1];
840        if !(last == b'E' || last == b'e') {
841            return false;
842        }
843
844        let first = token_slice[0];
845        if !first.is_ascii_digit() {
846            return false;
847        }
848
849        let mut dot_seen = false;
850        for &ch in &token_slice[1..token_slice.len() - 1] {
851            match ch {
852                b'0'..=b'9' => {}
853                b'.' if !dot_seen => dot_seen = true,
854                _ => return false,
855            }
856        }
857        true
858    }
859
860    fn parse(&mut self) -> Result<(), TokenizerError> {
861        self.parse_with_recovery(false).map_err(Into::into)
862    }
863
864    pub(crate) fn parse_best_effort(&mut self) -> Vec<TokenSpan> {
865        let _ = self.parse_with_recovery(true);
866        self.spans.clone()
867    }
868
869    fn parse_with_recovery(&mut self, best_effort: bool) -> Result<(), SpanTokenizerError> {
870        if self.formula.is_empty() {
871            return Ok(());
872        }
873
874        if self.formula.as_bytes()[0] != b'=' {
875            self.push_span(
876                TokenType::Literal,
877                TokenSubType::None,
878                0,
879                self.formula.len(),
880            );
881            return Ok(());
882        }
883
884        self.offset = 1;
885        self.start_token();
886
887        while self.offset < self.formula.len() {
888            if self.check_scientific_notation() {
889                continue;
890            }
891
892            let curr_byte = self.formula.as_bytes()[self.offset];
893
894            if is_token_ender(curr_byte) && self.has_token() {
895                self.save_token();
896                self.start_token();
897            }
898
899            let parse_result = match curr_byte {
900                b'"' | b'\'' => self.parse_string(),
901                b'[' => self.parse_brackets(),
902                b'#' => {
903                    if self.should_emit_hash_postfix() {
904                        self.emit_hash_postfix();
905                        Ok(())
906                    } else {
907                        self.parse_error()
908                    }
909                }
910                b' ' | b'\t' | b'\r' | b'\n' => self.parse_whitespace(),
911                b':' => {
912                    if self.should_emit_colon_infix() {
913                        self.emit_infix_operator(self.offset, self.offset + 1);
914                        Ok(())
915                    } else {
916                        if !self.has_token() {
917                            self.start_token();
918                        }
919                        self.offset += 1;
920                        self.extend_token();
921                        Ok(())
922                    }
923                }
924                b'+' | b'-' | b'*' | b'/' | b'^' | b'&' | b'=' | b'>' | b'<' | b'%' | b'@' => {
925                    self.parse_operator()
926                }
927                b'{' | b'(' => self.parse_opener(),
928                b')' | b'}' => self.parse_closer(),
929                b';' | b',' => self.parse_separator(),
930                _ => {
931                    if !self.has_token() {
932                        self.start_token();
933                    }
934                    self.offset += 1;
935                    self.extend_token();
936                    Ok(())
937                }
938            };
939
940            if let Err(err) = parse_result {
941                if best_effort {
942                    self.recover_from_error(err);
943                } else {
944                    return Err(err);
945                }
946            }
947        }
948
949        if self.has_token() {
950            self.save_token();
951        }
952
953        if !self.token_stack.is_empty() {
954            if best_effort {
955                while let Some(open_token) = self.token_stack.pop() {
956                    if let Some(span) = self.spans.iter().find(|span| {
957                        span.start == open_token.start
958                            && span.end == open_token.end
959                            && span.token_type == open_token.token_type
960                            && span.subtype == open_token.subtype
961                    }) {
962                        self.diagnostics.push(TokenDiagnostic::new(
963                            *span,
964                            "Unmatched opening parenthesis or bracket".to_string(),
965                            RecoveryAction::UnmatchedOpener,
966                        ));
967                    }
968                }
969            } else {
970                return Err(SpanTokenizerError {
971                    kind: SpanTokenizerErrorKind::UnmatchedOpening,
972                    pos: self.offset,
973                    message: "Unmatched opening parenthesis or bracket".to_string(),
974                    span_start: None,
975                    span_end: None,
976                });
977            }
978        }
979
980        Ok(())
981    }
982
983    fn recover_from_error(&mut self, error: SpanTokenizerError) {
984        match error.kind {
985            SpanTokenizerErrorKind::NoMatchingOpener => {
986                let span = TokenSpan {
987                    token_type: TokenType::Operand,
988                    subtype: TokenSubType::None,
989                    start: error.pos,
990                    end: error.pos + 1,
991                };
992                self.push_span(TokenType::Operand, TokenSubType::None, span.start, span.end);
993                self.offset = span.end;
994                self.start_token();
995                self.diagnostics.push(TokenDiagnostic::new(
996                    span,
997                    format!("No matching opener for closer at position {}", error.pos),
998                    RecoveryAction::SkippedUnmatchedCloser,
999                ));
1000            }
1001            SpanTokenizerErrorKind::UnmatchedOpening => {
1002                debug_assert!(
1003                    false,
1004                    "UnmatchedOpening is handled at end-of-input and should not be routed through recover_from_error"
1005                );
1006            }
1007            SpanTokenizerErrorKind::UnterminatedString => {
1008                let start = error.span_start.unwrap_or(error.pos);
1009                let span = TokenSpan {
1010                    token_type: TokenType::Operand,
1011                    subtype: TokenSubType::None,
1012                    start,
1013                    end: self.formula.len(),
1014                };
1015                self.push_span(TokenType::Operand, TokenSubType::None, span.start, span.end);
1016                self.offset = span.end;
1017                self.start_token();
1018                self.diagnostics.push(TokenDiagnostic::new(
1019                    span,
1020                    "Reached end of formula while parsing string".to_string(),
1021                    RecoveryAction::UnterminatedString,
1022                ));
1023            }
1024            SpanTokenizerErrorKind::UnmatchedBracket => {
1025                let start = error.span_start.unwrap_or(error.pos);
1026                let end = error.span_end.unwrap_or(self.formula.len());
1027                let span = TokenSpan {
1028                    token_type: TokenType::Operand,
1029                    subtype: TokenSubType::None,
1030                    start,
1031                    end,
1032                };
1033                self.push_span(TokenType::Operand, TokenSubType::None, span.start, span.end);
1034                self.offset = span.end;
1035                self.start_token();
1036                self.diagnostics.push(TokenDiagnostic::new(
1037                    span,
1038                    "Encountered unmatched '['".to_string(),
1039                    RecoveryAction::UnmatchedBracket,
1040                ));
1041            }
1042            SpanTokenizerErrorKind::MismatchedPair => {
1043                let span = TokenSpan {
1044                    token_type: TokenType::Operand,
1045                    subtype: TokenSubType::None,
1046                    start: error.pos,
1047                    end: error.pos + 1,
1048                };
1049                self.push_span(TokenType::Operand, TokenSubType::None, span.start, span.end);
1050                self.offset = span.end;
1051                self.start_token();
1052                self.diagnostics.push(TokenDiagnostic::new(
1053                    span,
1054                    "Mismatched ( and { pair".to_string(),
1055                    RecoveryAction::SkippedUnmatchedCloser,
1056                ));
1057            }
1058            SpanTokenizerErrorKind::InvalidErrorLiteral => {
1059                let start = error.span_start.unwrap_or(error.pos);
1060                let end = error.span_end.unwrap_or(error.pos + 1);
1061                let span = TokenSpan {
1062                    token_type: TokenType::Operand,
1063                    subtype: TokenSubType::None,
1064                    start,
1065                    end,
1066                };
1067                self.push_span(TokenType::Operand, TokenSubType::None, span.start, span.end);
1068                self.offset = span.end;
1069                self.start_token();
1070                self.diagnostics.push(TokenDiagnostic::new(
1071                    span,
1072                    "Invalid error code".to_string(),
1073                    RecoveryAction::InvalidErrorLiteral,
1074                ));
1075            }
1076        }
1077    }
1078
1079    fn parse_string(&mut self) -> Result<(), SpanTokenizerError> {
1080        let delim = self.formula.as_bytes()[self.offset];
1081        assert!(delim == b'"' || delim == b'\'');
1082
1083        let is_dollar_ref = delim == b'\''
1084            && self.has_token()
1085            && self.token_end - self.token_start == 1
1086            && self.formula.as_bytes()[self.token_start] == b'$';
1087
1088        // Issue #79: only single-quoted strings (cross-sheet references like
1089        // `A1:'Other Sheet'!B2`) should glue onto a pending `:`-terminated
1090        // token. Double-quoted string literals must always flush so the
1091        // pending `A1:` prefix is not silently discarded.
1092        let glue_to_token = delim == b'\''
1093            && self.has_token()
1094            && self.token_end > 0
1095            && self.formula.as_bytes()[self.token_end - 1] == b':';
1096
1097        if !is_dollar_ref && !glue_to_token && self.has_token() {
1098            self.save_token();
1099            self.start_token();
1100        }
1101
1102        let string_start = if is_dollar_ref {
1103            self.token_start
1104        } else {
1105            self.offset
1106        };
1107        self.offset += 1;
1108
1109        while self.offset < self.formula.len() {
1110            if self.formula.as_bytes()[self.offset] == delim {
1111                self.offset += 1;
1112                if self.offset < self.formula.len() && self.formula.as_bytes()[self.offset] == delim
1113                {
1114                    self.offset += 1;
1115                } else {
1116                    if delim == b'"' {
1117                        let value_str = &self.formula[string_start..self.offset];
1118                        let subtype = operand_subtype(value_str);
1119                        self.push_span(TokenType::Operand, subtype, string_start, self.offset);
1120                        self.start_token();
1121                    } else {
1122                        self.token_end = self.offset;
1123                    }
1124                    return Ok(());
1125                }
1126            } else {
1127                self.offset += 1;
1128            }
1129        }
1130
1131        Err(SpanTokenizerError {
1132            kind: SpanTokenizerErrorKind::UnterminatedString,
1133            pos: self.offset,
1134            message: "Reached end of formula while parsing string".to_string(),
1135            span_start: Some(string_start),
1136            span_end: Some(self.formula.len()),
1137        })
1138    }
1139
1140    fn parse_brackets(&mut self) -> Result<(), SpanTokenizerError> {
1141        assert_eq!(self.formula.as_bytes()[self.offset], b'[');
1142
1143        if !self.has_token() {
1144            self.start_token();
1145        }
1146
1147        let bracket_start = self.offset;
1148        let mut open_count = 1;
1149        self.offset += 1;
1150
1151        while self.offset < self.formula.len() {
1152            match self.formula.as_bytes()[self.offset] {
1153                // OOXML structured-reference escape: a single apostrophe makes
1154                // the next byte literal (used to embed `[`, `]`, `'`, or `#`
1155                // inside a column name). Skip the following byte without
1156                // updating nesting depth.
1157                b'\'' => {
1158                    if self.offset + 1 < self.formula.len() {
1159                        self.offset += 2;
1160                        continue;
1161                    }
1162                    // Trailing apostrophe inside brackets is malformed; fall
1163                    // through so the loop ends with an UnmatchedBracket error.
1164                    self.offset += 1;
1165                    continue;
1166                }
1167                b'[' => open_count += 1,
1168                b']' => {
1169                    open_count -= 1;
1170                    if open_count == 0 {
1171                        self.offset += 1;
1172                        self.extend_token();
1173                        return Ok(());
1174                    }
1175                }
1176                _ => {}
1177            }
1178            self.offset += 1;
1179        }
1180
1181        Err(SpanTokenizerError {
1182            kind: SpanTokenizerErrorKind::UnmatchedBracket,
1183            pos: self.offset,
1184            message: "Encountered unmatched '['".to_string(),
1185            span_start: Some(bracket_start),
1186            span_end: Some(self.formula.len()),
1187        })
1188    }
1189
1190    fn parse_error(&mut self) -> Result<(), SpanTokenizerError> {
1191        // OOXML serializes broken sheet-qualified references as `Sheet1!#REF!`.
1192        // When an accumulated token ends with `!`, treat the prefix as a sheet
1193        // qualifier and discard it: the resulting AST is identical to the bare
1194        // error literal `=#REF!`, preserving the error kind via the matched
1195        // `ERROR_CODES` entry below.
1196        let has_sheet_prefix = self.has_token()
1197            && self.token_end > 0
1198            && self.formula.as_bytes()[self.token_end - 1] == b'!';
1199        if has_sheet_prefix {
1200            if self.token_end - self.token_start <= 1 {
1201                return Err(SpanTokenizerError {
1202                    kind: SpanTokenizerErrorKind::InvalidErrorLiteral,
1203                    pos: self.offset,
1204                    message: format!(
1205                        "Empty sheet qualifier before error literal at position {}",
1206                        self.offset
1207                    ),
1208                    span_start: Some(self.token_start),
1209                    span_end: Some(self.offset),
1210                });
1211            }
1212            // Discard the sheet prefix; the error kind is what matters.
1213            self.start_token();
1214        } else if self.has_token() {
1215            self.save_token();
1216            self.start_token();
1217        }
1218
1219        let error_start = self.offset;
1220
1221        for &err_code in ERROR_CODES {
1222            let err_bytes = err_code.as_bytes();
1223            if self.offset + err_bytes.len() <= self.formula.len() {
1224                let slice = &self.formula.as_bytes()[self.offset..self.offset + err_bytes.len()];
1225                if slice.eq_ignore_ascii_case(err_bytes) {
1226                    self.push_span(
1227                        TokenType::Operand,
1228                        TokenSubType::Error,
1229                        error_start,
1230                        self.offset + err_bytes.len(),
1231                    );
1232                    self.offset += err_bytes.len();
1233                    self.start_token();
1234                    return Ok(());
1235                }
1236            }
1237        }
1238
1239        let mut end = self.offset + 1;
1240        while end < self.formula.len() {
1241            let ch = self.formula.as_bytes()[end];
1242            if is_token_ender(ch)
1243                || ch == b' '
1244                || ch == b'\t'
1245                || ch == b'\r'
1246                || ch == b'\n'
1247                || ch == b'('
1248                || ch == b'{'
1249                || ch == b'['
1250                || ch == b'"'
1251                || ch == b'\''
1252            {
1253                break;
1254            }
1255            end += 1;
1256        }
1257
1258        Err(SpanTokenizerError {
1259            kind: SpanTokenizerErrorKind::InvalidErrorLiteral,
1260            pos: self.offset,
1261            message: format!("Invalid error code at position {}", self.offset),
1262            span_start: Some(error_start),
1263            span_end: Some(end),
1264        })
1265    }
1266
1267    fn parse_whitespace(&mut self) -> Result<(), SpanTokenizerError> {
1268        self.save_token();
1269
1270        let ws_start = self.offset;
1271        let mut contains_intersection_space = false;
1272        while self.offset < self.formula.len() {
1273            match self.formula.as_bytes()[self.offset] {
1274                b' ' => {
1275                    contains_intersection_space = true;
1276                    self.offset += 1;
1277                }
1278                b'\t' | b'\r' | b'\n' => self.offset += 1,
1279                _ => break,
1280            }
1281        }
1282
1283        // Excel accepts TAB/CR/LF as lexical whitespace, but only an ASCII
1284        // space in the run can spell the range-intersection operator.
1285        let token_type = if contains_intersection_space
1286            && self.prev_is_reference_producing()
1287            && next_starts_reference_expression(self.formula, self.offset)
1288        {
1289            TokenType::OpInfix
1290        } else {
1291            TokenType::Whitespace
1292        };
1293        self.push_span(token_type, TokenSubType::None, ws_start, self.offset);
1294        self.start_token();
1295        Ok(())
1296    }
1297
1298    fn prev_is_reference_producing(&self) -> bool {
1299        match self.prev_non_whitespace() {
1300            Some(prev) => match prev.token_type {
1301                TokenType::OpPostfix => true,
1302                TokenType::Paren | TokenType::Func | TokenType::Array
1303                    if prev.subtype == TokenSubType::Close =>
1304                {
1305                    true
1306                }
1307                TokenType::Operand if prev.subtype == TokenSubType::Range => self
1308                    .formula
1309                    .get(prev.start..prev.end)
1310                    .is_some_and(is_reference_operand_value),
1311                _ => false,
1312            },
1313            None => false,
1314        }
1315    }
1316
1317    fn should_emit_colon_infix(&self) -> bool {
1318        if self.has_token() {
1319            let value = &self.formula[self.token_start..self.token_end];
1320            if value.ends_with('!') {
1321                return false;
1322            }
1323            return reference_value_contains_range_colon(value)
1324                || value_has_structured_reference_bracket(value)
1325                || (value.contains('!')
1326                    && next_reference_has_sheet_qualifier(self.formula, self.offset + 1));
1327        }
1328        self.prev_is_reference_producing()
1329    }
1330
1331    fn emit_infix_operator(&mut self, start: usize, end: usize) {
1332        self.save_token();
1333        self.start_token();
1334        self.push_span(TokenType::OpInfix, TokenSubType::None, start, end);
1335        self.offset = end;
1336        self.start_token();
1337    }
1338
1339    fn prev_non_whitespace(&self) -> Option<&TokenSpan> {
1340        self.spans
1341            .iter()
1342            .rev()
1343            .find(|t| t.token_type != TokenType::Whitespace)
1344    }
1345
1346    /// Decide whether a `#` at `self.offset` should be emitted as a spill
1347    /// postfix operator (`OpPostfix`) instead of routed to the error-literal
1348    /// parser. The rule mirrors the space-operator gating: `#` is postfix when
1349    /// it follows a reference-producing token.
1350    fn should_emit_hash_postfix(&self) -> bool {
1351        if self.has_token() {
1352            // An accumulated token whose last byte is `!` is a sheet/file
1353            // qualifier (e.g. `Sheet1!`). Defer to `parse_error` so it can
1354            // merge `Sheet1!` + `#REF!` into a single qualified-error operand.
1355            if self.formula.as_bytes()[self.token_end - 1] == b'!' {
1356                return false;
1357            }
1358            let value = &self.formula[self.token_start..self.token_end];
1359            return operand_subtype(value) == TokenSubType::Range;
1360        }
1361        match self.prev_non_whitespace() {
1362            Some(prev) => match prev.token_type {
1363                TokenType::OpPostfix => true,
1364                TokenType::Paren | TokenType::Func | TokenType::Array
1365                    if prev.subtype == TokenSubType::Close =>
1366                {
1367                    true
1368                }
1369                TokenType::Operand if prev.subtype == TokenSubType::Range => true,
1370                _ => false,
1371            },
1372            None => false,
1373        }
1374    }
1375
1376    fn emit_hash_postfix(&mut self) {
1377        self.save_token();
1378        self.start_token();
1379        self.push_span(
1380            TokenType::OpPostfix,
1381            TokenSubType::None,
1382            self.offset,
1383            self.offset + 1,
1384        );
1385        self.offset += 1;
1386        self.start_token();
1387    }
1388
1389    fn parse_operator(&mut self) -> Result<(), SpanTokenizerError> {
1390        self.save_token();
1391
1392        if self.offset + 1 < self.formula.len() {
1393            let two_char = &self.formula.as_bytes()[self.offset..self.offset + 2];
1394            if two_char == b">=" || two_char == b"<=" || two_char == b"<>" {
1395                self.push_span(
1396                    TokenType::OpInfix,
1397                    TokenSubType::None,
1398                    self.offset,
1399                    self.offset + 2,
1400                );
1401                self.offset += 2;
1402                self.start_token();
1403                return Ok(());
1404            }
1405        }
1406
1407        let curr_byte = self.formula.as_bytes()[self.offset];
1408        let token_type = match curr_byte {
1409            b'@' => TokenType::OpPrefix,
1410            b'%' => TokenType::OpPostfix,
1411            b'+' | b'-' => {
1412                if self.spans.is_empty() {
1413                    TokenType::OpPrefix
1414                } else {
1415                    let prev = self.prev_non_whitespace();
1416                    if let Some(p) = prev {
1417                        if p.subtype == TokenSubType::Close
1418                            || p.token_type == TokenType::OpPostfix
1419                            || p.token_type == TokenType::Operand
1420                        {
1421                            TokenType::OpInfix
1422                        } else {
1423                            TokenType::OpPrefix
1424                        }
1425                    } else {
1426                        TokenType::OpPrefix
1427                    }
1428                }
1429            }
1430            _ => TokenType::OpInfix,
1431        };
1432
1433        self.push_span(token_type, TokenSubType::None, self.offset, self.offset + 1);
1434        self.offset += 1;
1435        self.start_token();
1436        Ok(())
1437    }
1438
1439    fn parse_opener(&mut self) -> Result<(), SpanTokenizerError> {
1440        let curr_byte = self.formula.as_bytes()[self.offset];
1441        assert!(curr_byte == b'(' || curr_byte == b'{');
1442
1443        let token = if curr_byte == b'{' {
1444            self.save_token();
1445            TokenSpan {
1446                token_type: TokenType::Array,
1447                subtype: TokenSubType::Open,
1448                start: self.offset,
1449                end: self.offset + 1,
1450            }
1451        } else if self.has_token() {
1452            let token = TokenSpan {
1453                token_type: TokenType::Func,
1454                subtype: TokenSubType::Open,
1455                start: self.token_start,
1456                end: self.offset + 1,
1457            };
1458            self.token_start = self.offset + 1;
1459            self.token_end = self.offset + 1;
1460            token
1461        } else {
1462            TokenSpan {
1463                token_type: TokenType::Paren,
1464                subtype: TokenSubType::Open,
1465                start: self.offset,
1466                end: self.offset + 1,
1467            }
1468        };
1469
1470        self.spans.push(token);
1471        self.token_stack.push(token);
1472        self.offset += 1;
1473        self.start_token();
1474        Ok(())
1475    }
1476
1477    fn parse_closer(&mut self) -> Result<(), SpanTokenizerError> {
1478        self.save_token();
1479
1480        let curr_byte = self.formula.as_bytes()[self.offset];
1481        assert!(curr_byte == b')' || curr_byte == b'}');
1482
1483        if let Some(open_token) = self.token_stack.last().copied() {
1484            let expected = if open_token.token_type == TokenType::Array {
1485                b'}'
1486            } else {
1487                b')'
1488            };
1489            if curr_byte != expected {
1490                return Err(SpanTokenizerError {
1491                    kind: SpanTokenizerErrorKind::MismatchedPair,
1492                    pos: self.offset,
1493                    message: "Mismatched ( and { pair".to_string(),
1494                    span_start: Some(self.offset),
1495                    span_end: Some(self.offset + 1),
1496                });
1497            }
1498
1499            self.token_stack.pop();
1500            self.push_span(
1501                open_token.token_type,
1502                TokenSubType::Close,
1503                self.offset,
1504                self.offset + 1,
1505            );
1506        } else {
1507            return Err(SpanTokenizerError {
1508                kind: SpanTokenizerErrorKind::NoMatchingOpener,
1509                pos: self.offset,
1510                message: format!("No matching opener for closer at position {}", self.offset),
1511                span_start: Some(self.offset),
1512                span_end: Some(self.offset + 1),
1513            });
1514        }
1515
1516        self.offset += 1;
1517        self.start_token();
1518        Ok(())
1519    }
1520
1521    fn parse_separator(&mut self) -> Result<(), SpanTokenizerError> {
1522        self.save_token();
1523
1524        let curr_byte = self.formula.as_bytes()[self.offset];
1525        assert!(curr_byte == b';' || curr_byte == b',');
1526
1527        let top_token = self.token_stack.last();
1528        let in_function_or_array = matches!(
1529            top_token.map(|t| t.token_type),
1530            Some(TokenType::Func | TokenType::Array)
1531        );
1532        let in_array = matches!(top_token.map(|t| t.token_type), Some(TokenType::Array));
1533
1534        let (token_type, subtype) = match curr_byte {
1535            b',' => {
1536                if in_function_or_array {
1537                    (TokenType::Sep, TokenSubType::Arg)
1538                } else {
1539                    (TokenType::OpInfix, TokenSubType::None)
1540                }
1541            }
1542            b';' => {
1543                if in_array {
1544                    (TokenType::Sep, TokenSubType::Row)
1545                } else if self.dialect == FormulaDialect::OpenFormula && in_function_or_array {
1546                    (TokenType::Sep, TokenSubType::Arg)
1547                } else if self.dialect == FormulaDialect::OpenFormula {
1548                    (TokenType::OpInfix, TokenSubType::None)
1549                } else {
1550                    (TokenType::Sep, TokenSubType::Row)
1551                }
1552            }
1553            _ => (TokenType::OpInfix, TokenSubType::None),
1554        };
1555
1556        self.push_span(token_type, subtype, self.offset, self.offset + 1);
1557        self.offset += 1;
1558        self.start_token();
1559        Ok(())
1560    }
1561}
1562
1563/// A tokenizer for Excel worksheet formulas.
1564pub struct Tokenizer {
1565    formula: String, // The formula string
1566    pub items: Vec<Token>,
1567    token_stack: Vec<Token>,
1568    offset: usize,      // Byte offset in formula
1569    token_start: usize, // Start of current token
1570    token_end: usize,   // End of current token
1571    dialect: FormulaDialect,
1572}
1573
1574impl Tokenizer {
1575    /// Create a new tokenizer and immediately parse the formula.
1576    pub fn new(formula: &str) -> Result<Self, TokenizerError> {
1577        Self::new_with_dialect(formula, FormulaDialect::Excel)
1578    }
1579
1580    /// Create a new tokenizer with best-effort parsing (never fails).
1581    pub fn new_best_effort(formula: &str) -> Self {
1582        Self::new_best_effort_with_dialect(formula, FormulaDialect::Excel)
1583    }
1584
1585    /// Create a new tokenizer with best-effort parsing for the specified dialect.
1586    pub fn new_best_effort_with_dialect(formula: &str, dialect: FormulaDialect) -> Self {
1587        let stream = TokenStream::new_best_effort_with_dialect(formula, dialect);
1588        Self::from_token_stream(&stream)
1589    }
1590
1591    /// Create a new tokenizer for the specified formula dialect.
1592    pub fn new_with_dialect(
1593        formula: &str,
1594        dialect: FormulaDialect,
1595    ) -> Result<Self, TokenizerError> {
1596        let mut tokenizer = Tokenizer {
1597            formula: formula.to_string(),
1598            items: Vec::with_capacity(formula.len() / 2), // Reasonable estimate
1599            token_stack: Vec::with_capacity(16),
1600            offset: 0,
1601            token_start: 0,
1602            token_end: 0,
1603            dialect,
1604        };
1605        tokenizer.parse()?;
1606        Ok(tokenizer)
1607    }
1608
1609    pub fn from_token_stream(stream: &TokenStream) -> Self {
1610        Tokenizer {
1611            formula: stream.source.to_string(),
1612            items: stream.to_tokens(),
1613            token_stack: Vec::with_capacity(16),
1614            offset: 0,
1615            token_start: 0,
1616            token_end: 0,
1617            dialect: stream.dialect,
1618        }
1619    }
1620
1621    /// Get byte at current offset
1622    #[inline]
1623    fn current_byte(&self) -> Option<u8> {
1624        self.formula.as_bytes().get(self.offset).copied()
1625    }
1626
1627    /// Check if we have a token accumulated
1628    #[inline]
1629    fn has_token(&self) -> bool {
1630        self.token_end > self.token_start
1631    }
1632
1633    /// Start a new token at current position
1634    #[inline]
1635    fn start_token(&mut self) {
1636        self.token_start = self.offset;
1637        self.token_end = self.offset;
1638    }
1639
1640    /// Extend current token to current position
1641    #[inline]
1642    fn extend_token(&mut self) {
1643        self.token_end = self.offset;
1644    }
1645
1646    /// Parse the formula into tokens.
1647    fn parse(&mut self) -> Result<(), TokenizerError> {
1648        if self.formula.is_empty() {
1649            return Ok(());
1650        }
1651
1652        // Check for literal formula (doesn't start with '=')
1653        if self.formula.as_bytes()[0] != b'=' {
1654            self.items.push(Token::new_with_span(
1655                self.formula.clone(),
1656                TokenType::Literal,
1657                TokenSubType::None,
1658                0,
1659                self.formula.len(),
1660            ));
1661            return Ok(());
1662        }
1663
1664        // Skip the '=' character
1665        self.offset = 1;
1666        self.start_token();
1667
1668        while self.offset < self.formula.len() {
1669            if self.check_scientific_notation()? {
1670                continue;
1671            }
1672
1673            let curr_byte = self.formula.as_bytes()[self.offset];
1674
1675            // Check if this ends a token
1676            if is_token_ender(curr_byte) && self.has_token() {
1677                self.save_token();
1678                self.start_token();
1679            }
1680
1681            // Dispatch based on the current character
1682            match curr_byte {
1683                b'"' | b'\'' => self.parse_string()?,
1684                b'[' => self.parse_brackets()?,
1685                b'#' => {
1686                    if self.should_emit_hash_postfix() {
1687                        self.emit_hash_postfix();
1688                    } else {
1689                        self.parse_error()?
1690                    }
1691                }
1692                b' ' | b'\t' | b'\r' | b'\n' => self.parse_whitespace()?,
1693                b':' => {
1694                    if self.should_emit_colon_infix() {
1695                        self.emit_infix_operator(self.offset, self.offset + 1);
1696                    } else {
1697                        if !self.has_token() {
1698                            self.start_token();
1699                        }
1700                        self.offset += 1;
1701                        self.extend_token();
1702                    }
1703                }
1704                // operator characters
1705                b'+' | b'-' | b'*' | b'/' | b'^' | b'&' | b'=' | b'>' | b'<' | b'%' | b'@' => {
1706                    self.parse_operator()?
1707                }
1708                b'{' | b'(' => self.parse_opener()?,
1709                b')' | b'}' => self.parse_closer()?,
1710                b';' | b',' => self.parse_separator()?,
1711                _ => {
1712                    // Accumulate into current token
1713                    if !self.has_token() {
1714                        self.start_token();
1715                    }
1716                    self.offset += 1;
1717                    self.extend_token();
1718                }
1719            }
1720        }
1721
1722        // Save any remaining token
1723        if self.has_token() {
1724            self.save_token();
1725        }
1726
1727        // Check for unmatched opening parentheses/brackets
1728        if !self.token_stack.is_empty() {
1729            return Err(TokenizerError {
1730                message: "Unmatched opening parenthesis or bracket".to_string(),
1731                pos: self.offset,
1732            });
1733        }
1734
1735        Ok(())
1736    }
1737
1738    /// If the current token looks like a number in scientific notation,
1739    /// consume the '+' or '-' as part of the number.
1740    ///
1741    /// The `+`/`-` is only consumed when the next byte is an ASCII digit.
1742    /// Without that one-byte lookahead, inputs like `=1e+` and `=1E-A1`
1743    /// would be silently absorbed into a single (invalid) numeric token
1744    /// and surface later as a `NamedRange`. See issue #78.
1745    fn check_scientific_notation(&mut self) -> Result<bool, TokenizerError> {
1746        if let Some(curr_byte) = self.current_byte() {
1747            if (curr_byte == b'+' || curr_byte == b'-')
1748                && self.has_token()
1749                && self.is_scientific_notation_base()
1750                && self
1751                    .formula
1752                    .as_bytes()
1753                    .get(self.offset + 1)
1754                    .is_some_and(|b| b.is_ascii_digit())
1755            {
1756                self.offset += 1;
1757                self.extend_token();
1758                return Ok(true);
1759            }
1760        }
1761        Ok(false)
1762    }
1763
1764    /// Helper: Determine if the current accumulated token is the base of a
1765    /// scientific notation number (e.g., "1.23E" or "9e").
1766    fn is_scientific_notation_base(&self) -> bool {
1767        if !self.has_token() {
1768            return false;
1769        }
1770
1771        let token_slice = &self.formula.as_bytes()[self.token_start..self.token_end];
1772        if token_slice.len() < 2 {
1773            return false;
1774        }
1775
1776        let last = token_slice[token_slice.len() - 1];
1777        if !(last == b'E' || last == b'e') {
1778            return false;
1779        }
1780
1781        let first = token_slice[0];
1782        if !first.is_ascii_digit() {
1783            return false;
1784        }
1785
1786        let mut dot_seen = false;
1787        // Check middle characters
1788        for &ch in &token_slice[1..token_slice.len() - 1] {
1789            match ch {
1790                b'0'..=b'9' => {}
1791                b'.' if !dot_seen => dot_seen = true,
1792                _ => return false,
1793            }
1794        }
1795        true
1796    }
1797
1798    /// If there is an accumulated token, convert it to an operand token and add it to the list.
1799    fn save_token(&mut self) {
1800        if self.has_token() {
1801            let token =
1802                Token::make_operand_from_slice(&self.formula, self.token_start, self.token_end);
1803            self.items.push(token);
1804        }
1805    }
1806
1807    /// Parse a string (or link) literal.
1808    fn parse_string(&mut self) -> Result<(), TokenizerError> {
1809        let delim = self.formula.as_bytes()[self.offset];
1810        assert!(delim == b'"' || delim == b'\'');
1811
1812        // Check for dollar reference special case
1813        let is_dollar_ref = delim == b'\''
1814            && self.has_token()
1815            && self.token_end - self.token_start == 1
1816            && self.formula.as_bytes()[self.token_start] == b'$';
1817
1818        // Issue #79: only the single-quote path should keep accumulating
1819        // onto a `:`-terminated token (e.g. `A1:'Other Sheet'!B2`). For
1820        // double-quoted string literals, always flush the pending token so
1821        // that the prefix (e.g. `A1:`) is not silently discarded.
1822        let glue_to_token = delim == b'\''
1823            && self.has_token()
1824            && self.token_end > 0
1825            && self.formula.as_bytes()[self.token_end - 1] == b':';
1826
1827        if !is_dollar_ref && !glue_to_token && self.has_token() {
1828            self.save_token();
1829            self.start_token();
1830        }
1831
1832        let string_start = if is_dollar_ref {
1833            self.token_start
1834        } else {
1835            self.offset
1836        };
1837        self.offset += 1; // Skip opening delimiter
1838
1839        while self.offset < self.formula.len() {
1840            if self.formula.as_bytes()[self.offset] == delim {
1841                self.offset += 1;
1842                // Check for escaped quote
1843                if self.offset < self.formula.len() && self.formula.as_bytes()[self.offset] == delim
1844                {
1845                    self.offset += 1; // Skip escaped quote
1846                } else {
1847                    // End of string
1848                    if delim == b'"' {
1849                        let token = Token::make_operand_from_slice(
1850                            &self.formula,
1851                            string_start,
1852                            self.offset,
1853                        );
1854                        self.items.push(token);
1855                        self.start_token();
1856                    } else {
1857                        // Single-quoted string becomes part of current token
1858                        self.token_end = self.offset;
1859                    }
1860                    return Ok(());
1861                }
1862            } else {
1863                self.offset += 1;
1864            }
1865        }
1866
1867        Err(TokenizerError {
1868            message: "Reached end of formula while parsing string".to_string(),
1869            pos: self.offset,
1870        })
1871    }
1872
1873    /// Parse the text between matching square brackets.
1874    fn parse_brackets(&mut self) -> Result<(), TokenizerError> {
1875        assert_eq!(self.formula.as_bytes()[self.offset], b'[');
1876
1877        if !self.has_token() {
1878            self.start_token();
1879        }
1880
1881        let mut open_count = 1;
1882        self.offset += 1;
1883
1884        while self.offset < self.formula.len() {
1885            match self.formula.as_bytes()[self.offset] {
1886                // OOXML structured-reference escape: a single apostrophe makes
1887                // the next byte literal (used to embed `[`, `]`, `'`, or `#`
1888                // inside a column name). Skip the following byte without
1889                // updating nesting depth.
1890                b'\'' => {
1891                    if self.offset + 1 < self.formula.len() {
1892                        self.offset += 2;
1893                        continue;
1894                    }
1895                    self.offset += 1;
1896                    continue;
1897                }
1898                b'[' => open_count += 1,
1899                b']' => {
1900                    open_count -= 1;
1901                    if open_count == 0 {
1902                        self.offset += 1;
1903                        self.extend_token();
1904                        return Ok(());
1905                    }
1906                }
1907                _ => {}
1908            }
1909            self.offset += 1;
1910        }
1911
1912        Err(TokenizerError {
1913            message: "Encountered unmatched '['".to_string(),
1914            pos: self.offset,
1915        })
1916    }
1917
1918    /// See `SpanTokenizer::should_emit_hash_postfix` for rationale.
1919    fn should_emit_hash_postfix(&self) -> bool {
1920        if self.has_token() {
1921            if self.formula.as_bytes()[self.token_end - 1] == b'!' {
1922                return false;
1923            }
1924            let value = &self.formula[self.token_start..self.token_end];
1925            // Mirror `make_operand_from_slice` subtype detection: the
1926            // accumulated token forms a Range operand iff it isn't a quoted
1927            // string, error literal, boolean, or number.
1928            let is_range = !value.starts_with('"')
1929                && !value.starts_with('#')
1930                && value != "TRUE"
1931                && value != "FALSE"
1932                && value.parse::<f64>().is_err();
1933            return is_range;
1934        }
1935        let prev = self
1936            .items
1937            .iter()
1938            .rev()
1939            .find(|t| t.token_type != TokenType::Whitespace);
1940        match prev {
1941            Some(p) => match p.token_type {
1942                TokenType::OpPostfix => true,
1943                TokenType::Paren | TokenType::Func | TokenType::Array
1944                    if p.subtype == TokenSubType::Close =>
1945                {
1946                    true
1947                }
1948                TokenType::Operand if p.subtype == TokenSubType::Range => true,
1949                _ => false,
1950            },
1951            None => false,
1952        }
1953    }
1954
1955    fn emit_hash_postfix(&mut self) {
1956        self.save_token();
1957        self.start_token();
1958        self.items.push(Token::from_slice(
1959            &self.formula,
1960            TokenType::OpPostfix,
1961            TokenSubType::None,
1962            self.offset,
1963            self.offset + 1,
1964        ));
1965        self.offset += 1;
1966        self.start_token();
1967    }
1968
1969    /// Parse an error literal that starts with '#'.
1970    fn parse_error(&mut self) -> Result<(), TokenizerError> {
1971        // OOXML serializes broken sheet-qualified references as `Sheet1!#REF!`.
1972        // When an accumulated token ends with `!`, treat the prefix as a sheet
1973        // qualifier and discard it: the resulting AST is identical to the bare
1974        // error literal `=#REF!`, preserving the error kind via `ERROR_CODES`.
1975        let has_sheet_prefix = self.has_token()
1976            && self.token_end > 0
1977            && self.formula.as_bytes()[self.token_end - 1] == b'!';
1978        if has_sheet_prefix {
1979            if self.token_end - self.token_start <= 1 {
1980                return Err(TokenizerError {
1981                    message: format!(
1982                        "Empty sheet qualifier before error literal at position {}",
1983                        self.offset
1984                    ),
1985                    pos: self.offset,
1986                });
1987            }
1988            // Discard the sheet prefix; the error kind is what matters.
1989            self.start_token();
1990        } else if self.has_token() {
1991            self.save_token();
1992            self.start_token();
1993        }
1994
1995        let error_start = self.offset;
1996
1997        // Try to match error codes
1998        for &err_code in ERROR_CODES {
1999            let err_bytes = err_code.as_bytes();
2000            if self.offset + err_bytes.len() <= self.formula.len() {
2001                let slice = &self.formula.as_bytes()[self.offset..self.offset + err_bytes.len()];
2002                if slice.eq_ignore_ascii_case(err_bytes) {
2003                    let token = Token::make_operand_from_slice(
2004                        &self.formula,
2005                        error_start,
2006                        self.offset + err_bytes.len(),
2007                    );
2008                    self.items.push(token);
2009                    self.offset += err_bytes.len();
2010                    self.start_token();
2011                    return Ok(());
2012                }
2013            }
2014        }
2015
2016        Err(TokenizerError {
2017            message: format!("Invalid error code at position {}", self.offset),
2018            pos: self.offset,
2019        })
2020    }
2021
2022    /// Parse a sequence of whitespace characters.
2023    fn parse_whitespace(&mut self) -> Result<(), TokenizerError> {
2024        self.save_token();
2025
2026        let ws_start = self.offset;
2027        let mut contains_intersection_space = false;
2028        while self.offset < self.formula.len() {
2029            match self.formula.as_bytes()[self.offset] {
2030                b' ' => {
2031                    contains_intersection_space = true;
2032                    self.offset += 1;
2033                }
2034                b'\t' | b'\r' | b'\n' => self.offset += 1,
2035                _ => break,
2036            }
2037        }
2038
2039        // Excel accepts TAB/CR/LF as lexical whitespace, but only an ASCII
2040        // space in the run can spell the range-intersection operator.
2041        let token_type = if contains_intersection_space
2042            && self.prev_is_reference_producing()
2043            && next_starts_reference_expression(&self.formula, self.offset)
2044        {
2045            TokenType::OpInfix
2046        } else {
2047            TokenType::Whitespace
2048        };
2049
2050        let token = if token_type == TokenType::OpInfix {
2051            Token::new_with_span(
2052                " ".to_string(),
2053                token_type,
2054                TokenSubType::None,
2055                ws_start,
2056                self.offset,
2057            )
2058        } else {
2059            Token::from_slice(
2060                &self.formula,
2061                token_type,
2062                TokenSubType::None,
2063                ws_start,
2064                self.offset,
2065            )
2066        };
2067        self.items.push(token);
2068        self.start_token();
2069        Ok(())
2070    }
2071
2072    fn prev_non_whitespace(&self) -> Option<&Token> {
2073        self.items
2074            .iter()
2075            .rev()
2076            .find(|t| t.token_type != TokenType::Whitespace)
2077    }
2078
2079    fn prev_is_reference_producing(&self) -> bool {
2080        match self.prev_non_whitespace() {
2081            Some(prev) => match prev.token_type {
2082                TokenType::OpPostfix => true,
2083                TokenType::Paren | TokenType::Func | TokenType::Array
2084                    if prev.subtype == TokenSubType::Close =>
2085                {
2086                    true
2087                }
2088                TokenType::Operand if prev.subtype == TokenSubType::Range => {
2089                    is_reference_operand_value(&prev.value)
2090                }
2091                _ => false,
2092            },
2093            None => false,
2094        }
2095    }
2096
2097    fn should_emit_colon_infix(&self) -> bool {
2098        if self.has_token() {
2099            let value = &self.formula[self.token_start..self.token_end];
2100            if value.ends_with('!') {
2101                return false;
2102            }
2103            return reference_value_contains_range_colon(value)
2104                || value_has_structured_reference_bracket(value)
2105                || (value.contains('!')
2106                    && next_reference_has_sheet_qualifier(&self.formula, self.offset + 1));
2107        }
2108        self.prev_is_reference_producing()
2109    }
2110
2111    fn emit_infix_operator(&mut self, start: usize, end: usize) {
2112        self.save_token();
2113        self.start_token();
2114        self.items.push(Token::from_slice(
2115            &self.formula,
2116            TokenType::OpInfix,
2117            TokenSubType::None,
2118            start,
2119            end,
2120        ));
2121        self.offset = end;
2122        self.start_token();
2123    }
2124
2125    /// Parse an operator token.
2126    fn parse_operator(&mut self) -> Result<(), TokenizerError> {
2127        self.save_token();
2128
2129        // Check for two-character operators
2130        if self.offset + 1 < self.formula.len() {
2131            let two_char = &self.formula.as_bytes()[self.offset..self.offset + 2];
2132            if two_char == b">=" || two_char == b"<=" || two_char == b"<>" {
2133                self.items.push(Token::from_slice(
2134                    &self.formula,
2135                    TokenType::OpInfix,
2136                    TokenSubType::None,
2137                    self.offset,
2138                    self.offset + 2,
2139                ));
2140                self.offset += 2;
2141                self.start_token();
2142                return Ok(());
2143            }
2144        }
2145
2146        let curr_byte = self.formula.as_bytes()[self.offset];
2147        let token_type = match curr_byte {
2148            b'@' => TokenType::OpPrefix,
2149            b'%' => TokenType::OpPostfix,
2150            b'+' | b'-' => {
2151                // Determine if prefix or infix
2152                if self.items.is_empty() {
2153                    TokenType::OpPrefix
2154                } else {
2155                    let prev = self
2156                        .items
2157                        .iter()
2158                        .rev()
2159                        .find(|t| t.token_type != TokenType::Whitespace);
2160                    if let Some(p) = prev {
2161                        if p.subtype == TokenSubType::Close
2162                            || p.token_type == TokenType::OpPostfix
2163                            || p.token_type == TokenType::Operand
2164                        {
2165                            TokenType::OpInfix
2166                        } else {
2167                            TokenType::OpPrefix
2168                        }
2169                    } else {
2170                        TokenType::OpPrefix
2171                    }
2172                }
2173            }
2174            _ => TokenType::OpInfix,
2175        };
2176
2177        self.items.push(Token::from_slice(
2178            &self.formula,
2179            token_type,
2180            TokenSubType::None,
2181            self.offset,
2182            self.offset + 1,
2183        ));
2184        self.offset += 1;
2185        self.start_token();
2186        Ok(())
2187    }
2188
2189    /// Parse an opener token – either '(' or '{'.
2190    fn parse_opener(&mut self) -> Result<(), TokenizerError> {
2191        let curr_byte = self.formula.as_bytes()[self.offset];
2192        assert!(curr_byte == b'(' || curr_byte == b'{');
2193
2194        let token = if curr_byte == b'{' {
2195            self.save_token();
2196            Token::make_subexp_from_slice(&self.formula, false, self.offset, self.offset + 1)
2197        } else if self.has_token() {
2198            // Function call
2199            let token = Token::make_subexp_from_slice(
2200                &self.formula,
2201                true,
2202                self.token_start,
2203                self.offset + 1,
2204            );
2205            self.token_start = self.offset + 1;
2206            self.token_end = self.offset + 1;
2207            token
2208        } else {
2209            Token::make_subexp_from_slice(&self.formula, false, self.offset, self.offset + 1)
2210        };
2211
2212        self.items.push(token.clone());
2213        self.token_stack.push(token);
2214        self.offset += 1;
2215        self.start_token();
2216        Ok(())
2217    }
2218
2219    /// Parse a closer token – either ')' or '}'.
2220    fn parse_closer(&mut self) -> Result<(), TokenizerError> {
2221        self.save_token();
2222
2223        let curr_byte = self.formula.as_bytes()[self.offset];
2224        assert!(curr_byte == b')' || curr_byte == b'}');
2225
2226        if let Some(open_token) = self.token_stack.pop() {
2227            let closer = open_token.get_closer()?;
2228            if (curr_byte == b'}' && closer.value != "}")
2229                || (curr_byte == b')' && closer.value != ")")
2230            {
2231                return Err(TokenizerError {
2232                    message: "Mismatched ( and { pair".to_string(),
2233                    pos: self.offset,
2234                });
2235            }
2236
2237            self.items.push(Token::from_slice(
2238                &self.formula,
2239                closer.token_type,
2240                TokenSubType::Close,
2241                self.offset,
2242                self.offset + 1,
2243            ));
2244        } else {
2245            return Err(TokenizerError {
2246                message: format!("No matching opener for closer at position {}", self.offset),
2247                pos: self.offset,
2248            });
2249        }
2250
2251        self.offset += 1;
2252        self.start_token();
2253        Ok(())
2254    }
2255
2256    /// Parse a separator token – either ',' or ';'.
2257    fn parse_separator(&mut self) -> Result<(), TokenizerError> {
2258        self.save_token();
2259
2260        let curr_byte = self.formula.as_bytes()[self.offset];
2261        assert!(curr_byte == b';' || curr_byte == b',');
2262
2263        let top_token = self.token_stack.last();
2264        let in_function_or_array = matches!(
2265            top_token.map(|t| t.token_type),
2266            Some(TokenType::Func | TokenType::Array)
2267        );
2268        let in_array = matches!(top_token.map(|t| t.token_type), Some(TokenType::Array));
2269
2270        let (token_type, subtype) = match curr_byte {
2271            b',' => {
2272                if in_function_or_array {
2273                    (TokenType::Sep, TokenSubType::Arg)
2274                } else {
2275                    (TokenType::OpInfix, TokenSubType::None)
2276                }
2277            }
2278            b';' => {
2279                if in_array {
2280                    // Array row separator for both dialects
2281                    (TokenType::Sep, TokenSubType::Row)
2282                } else if self.dialect == FormulaDialect::OpenFormula && in_function_or_array {
2283                    // OpenFormula uses ';' for argument separators inside functions
2284                    (TokenType::Sep, TokenSubType::Arg)
2285                } else if self.dialect == FormulaDialect::OpenFormula {
2286                    (TokenType::OpInfix, TokenSubType::None)
2287                } else {
2288                    (TokenType::Sep, TokenSubType::Row)
2289                }
2290            }
2291            _ => (TokenType::OpInfix, TokenSubType::None),
2292        };
2293
2294        self.items.push(Token::from_slice(
2295            &self.formula,
2296            token_type,
2297            subtype,
2298            self.offset,
2299            self.offset + 1,
2300        ));
2301
2302        self.offset += 1;
2303        self.start_token();
2304        Ok(())
2305    }
2306
2307    /// Reconstruct the formula from the parsed tokens.
2308    pub fn render(&self) -> String {
2309        if self.items.is_empty() {
2310            "".to_string()
2311        } else if self.items[0].token_type == TokenType::Literal {
2312            self.items[0].value.clone()
2313        } else {
2314            let concatenated: String = self.items.iter().map(|t| t.value.clone()).collect();
2315            format!("={concatenated}")
2316        }
2317    }
2318
2319    /// Return the dialect used when tokenizing this formula.
2320    pub fn dialect(&self) -> FormulaDialect {
2321        self.dialect
2322    }
2323}
2324
2325impl TryFrom<&str> for Tokenizer {
2326    type Error = TokenizerError;
2327
2328    fn try_from(value: &str) -> Result<Self, Self::Error> {
2329        Tokenizer::new(value)
2330    }
2331}
2332
2333impl TryFrom<String> for Tokenizer {
2334    type Error = TokenizerError;
2335
2336    fn try_from(value: String) -> Result<Self, Self::Error> {
2337        Tokenizer::new(&value)
2338    }
2339}