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
673fn is_reference_operand_value(value: &str) -> bool {
674    operand_subtype(value) == TokenSubType::Range
675        && (reference_value_contains_range_colon(value)
676            || value.contains('!')
677            || value.contains('[')
678            || is_cell_reference_like(value))
679}
680
681fn next_starts_reference_expression(formula: &str, mut offset: usize) -> bool {
682    let bytes = formula.as_bytes();
683    while offset < bytes.len() && matches!(bytes[offset], b' ' | b'\t' | b'\r' | b'\n') {
684        offset += 1;
685    }
686    if offset >= bytes.len() {
687        return false;
688    }
689
690    matches!(bytes[offset], b'(' | b'[' | b'\'' | b'$') || bytes[offset].is_ascii_alphabetic()
691}
692
693fn next_reference_has_sheet_qualifier(formula: &str, mut offset: usize) -> bool {
694    let bytes = formula.as_bytes();
695    while offset < bytes.len() && matches!(bytes[offset], b' ' | b'\t' | b'\r' | b'\n') {
696        offset += 1;
697    }
698
699    let mut in_quote = false;
700    while offset < bytes.len() {
701        match bytes[offset] {
702            b'\'' => {
703                if in_quote && offset + 1 < bytes.len() && bytes[offset + 1] == b'\'' {
704                    offset += 2;
705                    continue;
706                }
707                in_quote = !in_quote;
708            }
709            b'!' => return true,
710            b':' if !in_quote => return false,
711            b',' | b';' | b'}' | b')' | b' ' | b'\t' | b'\r' | b'\n' | b'+' | b'-' | b'*'
712            | b'/' | b'^' | b'&' | b'=' | b'>' | b'<' | b'%' | b'@'
713                if !in_quote =>
714            {
715                return false;
716            }
717            _ => {}
718        }
719        offset += 1;
720    }
721
722    false
723}
724
725struct SpanTokenizer<'a> {
726    formula: &'a str,
727    spans: Vec<TokenSpan>,
728    token_stack: Vec<TokenSpan>,
729    offset: usize,
730    token_start: usize,
731    token_end: usize,
732    dialect: FormulaDialect,
733    diagnostics: Vec<TokenDiagnostic>,
734}
735
736impl<'a> SpanTokenizer<'a> {
737    fn new(formula: &'a str, dialect: FormulaDialect) -> Self {
738        SpanTokenizer {
739            formula,
740            spans: Vec::with_capacity(formula.len() / 2),
741            token_stack: Vec::with_capacity(16),
742            offset: 0,
743            token_start: 0,
744            token_end: 0,
745            dialect,
746            diagnostics: Vec::new(),
747        }
748    }
749
750    #[inline]
751    fn current_byte(&self) -> Option<u8> {
752        self.formula.as_bytes().get(self.offset).copied()
753    }
754
755    #[inline]
756    fn has_token(&self) -> bool {
757        self.token_end > self.token_start
758    }
759
760    #[inline]
761    fn start_token(&mut self) {
762        self.token_start = self.offset;
763        self.token_end = self.offset;
764    }
765
766    #[inline]
767    fn extend_token(&mut self) {
768        self.token_end = self.offset;
769    }
770
771    fn push_span(
772        &mut self,
773        token_type: TokenType,
774        subtype: TokenSubType,
775        start: usize,
776        end: usize,
777    ) {
778        self.spans.push(TokenSpan {
779            token_type,
780            subtype,
781            start,
782            end,
783        });
784    }
785
786    fn save_token(&mut self) {
787        if self.has_token() {
788            let value_str = &self.formula[self.token_start..self.token_end];
789            let subtype = operand_subtype(value_str);
790            self.push_span(
791                TokenType::Operand,
792                subtype,
793                self.token_start,
794                self.token_end,
795            );
796        }
797    }
798
799    fn check_scientific_notation(&mut self) -> bool {
800        if let Some(curr_byte) = self.current_byte() {
801            if (curr_byte == b'+' || curr_byte == b'-')
802                && self.has_token()
803                && self.is_scientific_notation_base()
804                && self
805                    .formula
806                    .as_bytes()
807                    .get(self.offset + 1)
808                    .is_some_and(|b| b.is_ascii_digit())
809            {
810                self.offset += 1;
811                self.extend_token();
812                return true;
813            }
814        }
815        false
816    }
817
818    fn is_scientific_notation_base(&self) -> bool {
819        if !self.has_token() {
820            return false;
821        }
822
823        let token_slice = &self.formula.as_bytes()[self.token_start..self.token_end];
824        if token_slice.len() < 2 {
825            return false;
826        }
827
828        let last = token_slice[token_slice.len() - 1];
829        if !(last == b'E' || last == b'e') {
830            return false;
831        }
832
833        let first = token_slice[0];
834        if !first.is_ascii_digit() {
835            return false;
836        }
837
838        let mut dot_seen = false;
839        for &ch in &token_slice[1..token_slice.len() - 1] {
840            match ch {
841                b'0'..=b'9' => {}
842                b'.' if !dot_seen => dot_seen = true,
843                _ => return false,
844            }
845        }
846        true
847    }
848
849    fn parse(&mut self) -> Result<(), TokenizerError> {
850        self.parse_with_recovery(false).map_err(Into::into)
851    }
852
853    pub(crate) fn parse_best_effort(&mut self) -> Vec<TokenSpan> {
854        let _ = self.parse_with_recovery(true);
855        self.spans.clone()
856    }
857
858    fn parse_with_recovery(&mut self, best_effort: bool) -> Result<(), SpanTokenizerError> {
859        if self.formula.is_empty() {
860            return Ok(());
861        }
862
863        if self.formula.as_bytes()[0] != b'=' {
864            self.push_span(
865                TokenType::Literal,
866                TokenSubType::None,
867                0,
868                self.formula.len(),
869            );
870            return Ok(());
871        }
872
873        self.offset = 1;
874        self.start_token();
875
876        while self.offset < self.formula.len() {
877            if self.check_scientific_notation() {
878                continue;
879            }
880
881            let curr_byte = self.formula.as_bytes()[self.offset];
882
883            if is_token_ender(curr_byte) && self.has_token() {
884                self.save_token();
885                self.start_token();
886            }
887
888            let parse_result = match curr_byte {
889                b'"' | b'\'' => self.parse_string(),
890                b'[' => self.parse_brackets(),
891                b'#' => {
892                    if self.should_emit_hash_postfix() {
893                        self.emit_hash_postfix();
894                        Ok(())
895                    } else {
896                        self.parse_error()
897                    }
898                }
899                b' ' | b'\t' | b'\r' | b'\n' => self.parse_whitespace(),
900                b':' => {
901                    if self.should_emit_colon_infix() {
902                        self.emit_infix_operator(self.offset, self.offset + 1);
903                        Ok(())
904                    } else {
905                        if !self.has_token() {
906                            self.start_token();
907                        }
908                        self.offset += 1;
909                        self.extend_token();
910                        Ok(())
911                    }
912                }
913                b'+' | b'-' | b'*' | b'/' | b'^' | b'&' | b'=' | b'>' | b'<' | b'%' | b'@' => {
914                    self.parse_operator()
915                }
916                b'{' | b'(' => self.parse_opener(),
917                b')' | b'}' => self.parse_closer(),
918                b';' | b',' => self.parse_separator(),
919                _ => {
920                    if !self.has_token() {
921                        self.start_token();
922                    }
923                    self.offset += 1;
924                    self.extend_token();
925                    Ok(())
926                }
927            };
928
929            if let Err(err) = parse_result {
930                if best_effort {
931                    self.recover_from_error(err);
932                } else {
933                    return Err(err);
934                }
935            }
936        }
937
938        if self.has_token() {
939            self.save_token();
940        }
941
942        if !self.token_stack.is_empty() {
943            if best_effort {
944                while let Some(open_token) = self.token_stack.pop() {
945                    if let Some(span) = self.spans.iter().find(|span| {
946                        span.start == open_token.start
947                            && span.end == open_token.end
948                            && span.token_type == open_token.token_type
949                            && span.subtype == open_token.subtype
950                    }) {
951                        self.diagnostics.push(TokenDiagnostic::new(
952                            *span,
953                            "Unmatched opening parenthesis or bracket".to_string(),
954                            RecoveryAction::UnmatchedOpener,
955                        ));
956                    }
957                }
958            } else {
959                return Err(SpanTokenizerError {
960                    kind: SpanTokenizerErrorKind::UnmatchedOpening,
961                    pos: self.offset,
962                    message: "Unmatched opening parenthesis or bracket".to_string(),
963                    span_start: None,
964                    span_end: None,
965                });
966            }
967        }
968
969        Ok(())
970    }
971
972    fn recover_from_error(&mut self, error: SpanTokenizerError) {
973        match error.kind {
974            SpanTokenizerErrorKind::NoMatchingOpener => {
975                let span = TokenSpan {
976                    token_type: TokenType::Operand,
977                    subtype: TokenSubType::None,
978                    start: error.pos,
979                    end: error.pos + 1,
980                };
981                self.push_span(TokenType::Operand, TokenSubType::None, span.start, span.end);
982                self.offset = span.end;
983                self.start_token();
984                self.diagnostics.push(TokenDiagnostic::new(
985                    span,
986                    format!("No matching opener for closer at position {}", error.pos),
987                    RecoveryAction::SkippedUnmatchedCloser,
988                ));
989            }
990            SpanTokenizerErrorKind::UnmatchedOpening => {
991                debug_assert!(
992                    false,
993                    "UnmatchedOpening is handled at end-of-input and should not be routed through recover_from_error"
994                );
995            }
996            SpanTokenizerErrorKind::UnterminatedString => {
997                let start = error.span_start.unwrap_or(error.pos);
998                let span = TokenSpan {
999                    token_type: TokenType::Operand,
1000                    subtype: TokenSubType::None,
1001                    start,
1002                    end: self.formula.len(),
1003                };
1004                self.push_span(TokenType::Operand, TokenSubType::None, span.start, span.end);
1005                self.offset = span.end;
1006                self.start_token();
1007                self.diagnostics.push(TokenDiagnostic::new(
1008                    span,
1009                    "Reached end of formula while parsing string".to_string(),
1010                    RecoveryAction::UnterminatedString,
1011                ));
1012            }
1013            SpanTokenizerErrorKind::UnmatchedBracket => {
1014                let start = error.span_start.unwrap_or(error.pos);
1015                let end = error.span_end.unwrap_or(self.formula.len());
1016                let span = TokenSpan {
1017                    token_type: TokenType::Operand,
1018                    subtype: TokenSubType::None,
1019                    start,
1020                    end,
1021                };
1022                self.push_span(TokenType::Operand, TokenSubType::None, span.start, span.end);
1023                self.offset = span.end;
1024                self.start_token();
1025                self.diagnostics.push(TokenDiagnostic::new(
1026                    span,
1027                    "Encountered unmatched '['".to_string(),
1028                    RecoveryAction::UnmatchedBracket,
1029                ));
1030            }
1031            SpanTokenizerErrorKind::MismatchedPair => {
1032                let span = TokenSpan {
1033                    token_type: TokenType::Operand,
1034                    subtype: TokenSubType::None,
1035                    start: error.pos,
1036                    end: error.pos + 1,
1037                };
1038                self.push_span(TokenType::Operand, TokenSubType::None, span.start, span.end);
1039                self.offset = span.end;
1040                self.start_token();
1041                self.diagnostics.push(TokenDiagnostic::new(
1042                    span,
1043                    "Mismatched ( and { pair".to_string(),
1044                    RecoveryAction::SkippedUnmatchedCloser,
1045                ));
1046            }
1047            SpanTokenizerErrorKind::InvalidErrorLiteral => {
1048                let start = error.span_start.unwrap_or(error.pos);
1049                let end = error.span_end.unwrap_or(error.pos + 1);
1050                let span = TokenSpan {
1051                    token_type: TokenType::Operand,
1052                    subtype: TokenSubType::None,
1053                    start,
1054                    end,
1055                };
1056                self.push_span(TokenType::Operand, TokenSubType::None, span.start, span.end);
1057                self.offset = span.end;
1058                self.start_token();
1059                self.diagnostics.push(TokenDiagnostic::new(
1060                    span,
1061                    "Invalid error code".to_string(),
1062                    RecoveryAction::InvalidErrorLiteral,
1063                ));
1064            }
1065        }
1066    }
1067
1068    fn parse_string(&mut self) -> Result<(), SpanTokenizerError> {
1069        let delim = self.formula.as_bytes()[self.offset];
1070        assert!(delim == b'"' || delim == b'\'');
1071
1072        let is_dollar_ref = delim == b'\''
1073            && self.has_token()
1074            && self.token_end - self.token_start == 1
1075            && self.formula.as_bytes()[self.token_start] == b'$';
1076
1077        // Issue #79: only single-quoted strings (cross-sheet references like
1078        // `A1:'Other Sheet'!B2`) should glue onto a pending `:`-terminated
1079        // token. Double-quoted string literals must always flush so the
1080        // pending `A1:` prefix is not silently discarded.
1081        let glue_to_token = delim == b'\''
1082            && self.has_token()
1083            && self.token_end > 0
1084            && self.formula.as_bytes()[self.token_end - 1] == b':';
1085
1086        if !is_dollar_ref && !glue_to_token && self.has_token() {
1087            self.save_token();
1088            self.start_token();
1089        }
1090
1091        let string_start = if is_dollar_ref {
1092            self.token_start
1093        } else {
1094            self.offset
1095        };
1096        self.offset += 1;
1097
1098        while self.offset < self.formula.len() {
1099            if self.formula.as_bytes()[self.offset] == delim {
1100                self.offset += 1;
1101                if self.offset < self.formula.len() && self.formula.as_bytes()[self.offset] == delim
1102                {
1103                    self.offset += 1;
1104                } else {
1105                    if delim == b'"' {
1106                        let value_str = &self.formula[string_start..self.offset];
1107                        let subtype = operand_subtype(value_str);
1108                        self.push_span(TokenType::Operand, subtype, string_start, self.offset);
1109                        self.start_token();
1110                    } else {
1111                        self.token_end = self.offset;
1112                    }
1113                    return Ok(());
1114                }
1115            } else {
1116                self.offset += 1;
1117            }
1118        }
1119
1120        Err(SpanTokenizerError {
1121            kind: SpanTokenizerErrorKind::UnterminatedString,
1122            pos: self.offset,
1123            message: "Reached end of formula while parsing string".to_string(),
1124            span_start: Some(string_start),
1125            span_end: Some(self.formula.len()),
1126        })
1127    }
1128
1129    fn parse_brackets(&mut self) -> Result<(), SpanTokenizerError> {
1130        assert_eq!(self.formula.as_bytes()[self.offset], b'[');
1131
1132        if !self.has_token() {
1133            self.start_token();
1134        }
1135
1136        let bracket_start = self.offset;
1137        let mut open_count = 1;
1138        self.offset += 1;
1139
1140        while self.offset < self.formula.len() {
1141            match self.formula.as_bytes()[self.offset] {
1142                // OOXML structured-reference escape: a single apostrophe makes
1143                // the next byte literal (used to embed `[`, `]`, `'`, or `#`
1144                // inside a column name). Skip the following byte without
1145                // updating nesting depth.
1146                b'\'' => {
1147                    if self.offset + 1 < self.formula.len() {
1148                        self.offset += 2;
1149                        continue;
1150                    }
1151                    // Trailing apostrophe inside brackets is malformed; fall
1152                    // through so the loop ends with an UnmatchedBracket error.
1153                    self.offset += 1;
1154                    continue;
1155                }
1156                b'[' => open_count += 1,
1157                b']' => {
1158                    open_count -= 1;
1159                    if open_count == 0 {
1160                        self.offset += 1;
1161                        self.extend_token();
1162                        return Ok(());
1163                    }
1164                }
1165                _ => {}
1166            }
1167            self.offset += 1;
1168        }
1169
1170        Err(SpanTokenizerError {
1171            kind: SpanTokenizerErrorKind::UnmatchedBracket,
1172            pos: self.offset,
1173            message: "Encountered unmatched '['".to_string(),
1174            span_start: Some(bracket_start),
1175            span_end: Some(self.formula.len()),
1176        })
1177    }
1178
1179    fn parse_error(&mut self) -> Result<(), SpanTokenizerError> {
1180        // OOXML serializes broken sheet-qualified references as `Sheet1!#REF!`.
1181        // When an accumulated token ends with `!`, treat the prefix as a sheet
1182        // qualifier and discard it: the resulting AST is identical to the bare
1183        // error literal `=#REF!`, preserving the error kind via the matched
1184        // `ERROR_CODES` entry below.
1185        let has_sheet_prefix = self.has_token()
1186            && self.token_end > 0
1187            && self.formula.as_bytes()[self.token_end - 1] == b'!';
1188        if has_sheet_prefix {
1189            if self.token_end - self.token_start <= 1 {
1190                return Err(SpanTokenizerError {
1191                    kind: SpanTokenizerErrorKind::InvalidErrorLiteral,
1192                    pos: self.offset,
1193                    message: format!(
1194                        "Empty sheet qualifier before error literal at position {}",
1195                        self.offset
1196                    ),
1197                    span_start: Some(self.token_start),
1198                    span_end: Some(self.offset),
1199                });
1200            }
1201            // Discard the sheet prefix; the error kind is what matters.
1202            self.start_token();
1203        } else if self.has_token() {
1204            self.save_token();
1205            self.start_token();
1206        }
1207
1208        let error_start = self.offset;
1209
1210        for &err_code in ERROR_CODES {
1211            let err_bytes = err_code.as_bytes();
1212            if self.offset + err_bytes.len() <= self.formula.len() {
1213                let slice = &self.formula.as_bytes()[self.offset..self.offset + err_bytes.len()];
1214                if slice.eq_ignore_ascii_case(err_bytes) {
1215                    self.push_span(
1216                        TokenType::Operand,
1217                        TokenSubType::Error,
1218                        error_start,
1219                        self.offset + err_bytes.len(),
1220                    );
1221                    self.offset += err_bytes.len();
1222                    self.start_token();
1223                    return Ok(());
1224                }
1225            }
1226        }
1227
1228        let mut end = self.offset + 1;
1229        while end < self.formula.len() {
1230            let ch = self.formula.as_bytes()[end];
1231            if is_token_ender(ch)
1232                || ch == b' '
1233                || ch == b'\t'
1234                || ch == b'\r'
1235                || ch == b'\n'
1236                || ch == b'('
1237                || ch == b'{'
1238                || ch == b'['
1239                || ch == b'"'
1240                || ch == b'\''
1241            {
1242                break;
1243            }
1244            end += 1;
1245        }
1246
1247        Err(SpanTokenizerError {
1248            kind: SpanTokenizerErrorKind::InvalidErrorLiteral,
1249            pos: self.offset,
1250            message: format!("Invalid error code at position {}", self.offset),
1251            span_start: Some(error_start),
1252            span_end: Some(end),
1253        })
1254    }
1255
1256    fn parse_whitespace(&mut self) -> Result<(), SpanTokenizerError> {
1257        self.save_token();
1258
1259        let ws_start = self.offset;
1260        let mut contains_intersection_space = false;
1261        while self.offset < self.formula.len() {
1262            match self.formula.as_bytes()[self.offset] {
1263                b' ' => {
1264                    contains_intersection_space = true;
1265                    self.offset += 1;
1266                }
1267                b'\t' | b'\r' | b'\n' => self.offset += 1,
1268                _ => break,
1269            }
1270        }
1271
1272        // Excel accepts TAB/CR/LF as lexical whitespace, but only an ASCII
1273        // space in the run can spell the range-intersection operator.
1274        let token_type = if contains_intersection_space
1275            && self.prev_is_reference_producing()
1276            && next_starts_reference_expression(self.formula, self.offset)
1277        {
1278            TokenType::OpInfix
1279        } else {
1280            TokenType::Whitespace
1281        };
1282        self.push_span(token_type, TokenSubType::None, ws_start, self.offset);
1283        self.start_token();
1284        Ok(())
1285    }
1286
1287    fn prev_is_reference_producing(&self) -> bool {
1288        match self.prev_non_whitespace() {
1289            Some(prev) => match prev.token_type {
1290                TokenType::OpPostfix => true,
1291                TokenType::Paren | TokenType::Func | TokenType::Array
1292                    if prev.subtype == TokenSubType::Close =>
1293                {
1294                    true
1295                }
1296                TokenType::Operand if prev.subtype == TokenSubType::Range => self
1297                    .formula
1298                    .get(prev.start..prev.end)
1299                    .is_some_and(is_reference_operand_value),
1300                _ => false,
1301            },
1302            None => false,
1303        }
1304    }
1305
1306    fn should_emit_colon_infix(&self) -> bool {
1307        if self.has_token() {
1308            let value = &self.formula[self.token_start..self.token_end];
1309            if value.ends_with('!') {
1310                return false;
1311            }
1312            return reference_value_contains_range_colon(value)
1313                || value.contains('[')
1314                || (value.contains('!')
1315                    && next_reference_has_sheet_qualifier(self.formula, self.offset + 1));
1316        }
1317        self.prev_is_reference_producing()
1318    }
1319
1320    fn emit_infix_operator(&mut self, start: usize, end: usize) {
1321        self.save_token();
1322        self.start_token();
1323        self.push_span(TokenType::OpInfix, TokenSubType::None, start, end);
1324        self.offset = end;
1325        self.start_token();
1326    }
1327
1328    fn prev_non_whitespace(&self) -> Option<&TokenSpan> {
1329        self.spans
1330            .iter()
1331            .rev()
1332            .find(|t| t.token_type != TokenType::Whitespace)
1333    }
1334
1335    /// Decide whether a `#` at `self.offset` should be emitted as a spill
1336    /// postfix operator (`OpPostfix`) instead of routed to the error-literal
1337    /// parser. The rule mirrors the space-operator gating: `#` is postfix when
1338    /// it follows a reference-producing token.
1339    fn should_emit_hash_postfix(&self) -> bool {
1340        if self.has_token() {
1341            // An accumulated token whose last byte is `!` is a sheet/file
1342            // qualifier (e.g. `Sheet1!`). Defer to `parse_error` so it can
1343            // merge `Sheet1!` + `#REF!` into a single qualified-error operand.
1344            if self.formula.as_bytes()[self.token_end - 1] == b'!' {
1345                return false;
1346            }
1347            let value = &self.formula[self.token_start..self.token_end];
1348            return operand_subtype(value) == TokenSubType::Range;
1349        }
1350        match self.prev_non_whitespace() {
1351            Some(prev) => match prev.token_type {
1352                TokenType::OpPostfix => true,
1353                TokenType::Paren | TokenType::Func | TokenType::Array
1354                    if prev.subtype == TokenSubType::Close =>
1355                {
1356                    true
1357                }
1358                TokenType::Operand if prev.subtype == TokenSubType::Range => true,
1359                _ => false,
1360            },
1361            None => false,
1362        }
1363    }
1364
1365    fn emit_hash_postfix(&mut self) {
1366        self.save_token();
1367        self.start_token();
1368        self.push_span(
1369            TokenType::OpPostfix,
1370            TokenSubType::None,
1371            self.offset,
1372            self.offset + 1,
1373        );
1374        self.offset += 1;
1375        self.start_token();
1376    }
1377
1378    fn parse_operator(&mut self) -> Result<(), SpanTokenizerError> {
1379        self.save_token();
1380
1381        if self.offset + 1 < self.formula.len() {
1382            let two_char = &self.formula.as_bytes()[self.offset..self.offset + 2];
1383            if two_char == b">=" || two_char == b"<=" || two_char == b"<>" {
1384                self.push_span(
1385                    TokenType::OpInfix,
1386                    TokenSubType::None,
1387                    self.offset,
1388                    self.offset + 2,
1389                );
1390                self.offset += 2;
1391                self.start_token();
1392                return Ok(());
1393            }
1394        }
1395
1396        let curr_byte = self.formula.as_bytes()[self.offset];
1397        let token_type = match curr_byte {
1398            b'@' => TokenType::OpPrefix,
1399            b'%' => TokenType::OpPostfix,
1400            b'+' | b'-' => {
1401                if self.spans.is_empty() {
1402                    TokenType::OpPrefix
1403                } else {
1404                    let prev = self.prev_non_whitespace();
1405                    if let Some(p) = prev {
1406                        if p.subtype == TokenSubType::Close
1407                            || p.token_type == TokenType::OpPostfix
1408                            || p.token_type == TokenType::Operand
1409                        {
1410                            TokenType::OpInfix
1411                        } else {
1412                            TokenType::OpPrefix
1413                        }
1414                    } else {
1415                        TokenType::OpPrefix
1416                    }
1417                }
1418            }
1419            _ => TokenType::OpInfix,
1420        };
1421
1422        self.push_span(token_type, TokenSubType::None, self.offset, self.offset + 1);
1423        self.offset += 1;
1424        self.start_token();
1425        Ok(())
1426    }
1427
1428    fn parse_opener(&mut self) -> Result<(), SpanTokenizerError> {
1429        let curr_byte = self.formula.as_bytes()[self.offset];
1430        assert!(curr_byte == b'(' || curr_byte == b'{');
1431
1432        let token = if curr_byte == b'{' {
1433            self.save_token();
1434            TokenSpan {
1435                token_type: TokenType::Array,
1436                subtype: TokenSubType::Open,
1437                start: self.offset,
1438                end: self.offset + 1,
1439            }
1440        } else if self.has_token() {
1441            let token = TokenSpan {
1442                token_type: TokenType::Func,
1443                subtype: TokenSubType::Open,
1444                start: self.token_start,
1445                end: self.offset + 1,
1446            };
1447            self.token_start = self.offset + 1;
1448            self.token_end = self.offset + 1;
1449            token
1450        } else {
1451            TokenSpan {
1452                token_type: TokenType::Paren,
1453                subtype: TokenSubType::Open,
1454                start: self.offset,
1455                end: self.offset + 1,
1456            }
1457        };
1458
1459        self.spans.push(token);
1460        self.token_stack.push(token);
1461        self.offset += 1;
1462        self.start_token();
1463        Ok(())
1464    }
1465
1466    fn parse_closer(&mut self) -> Result<(), SpanTokenizerError> {
1467        self.save_token();
1468
1469        let curr_byte = self.formula.as_bytes()[self.offset];
1470        assert!(curr_byte == b')' || curr_byte == b'}');
1471
1472        if let Some(open_token) = self.token_stack.last().copied() {
1473            let expected = if open_token.token_type == TokenType::Array {
1474                b'}'
1475            } else {
1476                b')'
1477            };
1478            if curr_byte != expected {
1479                return Err(SpanTokenizerError {
1480                    kind: SpanTokenizerErrorKind::MismatchedPair,
1481                    pos: self.offset,
1482                    message: "Mismatched ( and { pair".to_string(),
1483                    span_start: Some(self.offset),
1484                    span_end: Some(self.offset + 1),
1485                });
1486            }
1487
1488            self.token_stack.pop();
1489            self.push_span(
1490                open_token.token_type,
1491                TokenSubType::Close,
1492                self.offset,
1493                self.offset + 1,
1494            );
1495        } else {
1496            return Err(SpanTokenizerError {
1497                kind: SpanTokenizerErrorKind::NoMatchingOpener,
1498                pos: self.offset,
1499                message: format!("No matching opener for closer at position {}", self.offset),
1500                span_start: Some(self.offset),
1501                span_end: Some(self.offset + 1),
1502            });
1503        }
1504
1505        self.offset += 1;
1506        self.start_token();
1507        Ok(())
1508    }
1509
1510    fn parse_separator(&mut self) -> Result<(), SpanTokenizerError> {
1511        self.save_token();
1512
1513        let curr_byte = self.formula.as_bytes()[self.offset];
1514        assert!(curr_byte == b';' || curr_byte == b',');
1515
1516        let top_token = self.token_stack.last();
1517        let in_function_or_array = matches!(
1518            top_token.map(|t| t.token_type),
1519            Some(TokenType::Func | TokenType::Array)
1520        );
1521        let in_array = matches!(top_token.map(|t| t.token_type), Some(TokenType::Array));
1522
1523        let (token_type, subtype) = match curr_byte {
1524            b',' => {
1525                if in_function_or_array {
1526                    (TokenType::Sep, TokenSubType::Arg)
1527                } else {
1528                    (TokenType::OpInfix, TokenSubType::None)
1529                }
1530            }
1531            b';' => {
1532                if in_array {
1533                    (TokenType::Sep, TokenSubType::Row)
1534                } else if self.dialect == FormulaDialect::OpenFormula && in_function_or_array {
1535                    (TokenType::Sep, TokenSubType::Arg)
1536                } else if self.dialect == FormulaDialect::OpenFormula {
1537                    (TokenType::OpInfix, TokenSubType::None)
1538                } else {
1539                    (TokenType::Sep, TokenSubType::Row)
1540                }
1541            }
1542            _ => (TokenType::OpInfix, TokenSubType::None),
1543        };
1544
1545        self.push_span(token_type, subtype, self.offset, self.offset + 1);
1546        self.offset += 1;
1547        self.start_token();
1548        Ok(())
1549    }
1550}
1551
1552/// A tokenizer for Excel worksheet formulas.
1553pub struct Tokenizer {
1554    formula: String, // The formula string
1555    pub items: Vec<Token>,
1556    token_stack: Vec<Token>,
1557    offset: usize,      // Byte offset in formula
1558    token_start: usize, // Start of current token
1559    token_end: usize,   // End of current token
1560    dialect: FormulaDialect,
1561}
1562
1563impl Tokenizer {
1564    /// Create a new tokenizer and immediately parse the formula.
1565    pub fn new(formula: &str) -> Result<Self, TokenizerError> {
1566        Self::new_with_dialect(formula, FormulaDialect::Excel)
1567    }
1568
1569    /// Create a new tokenizer with best-effort parsing (never fails).
1570    pub fn new_best_effort(formula: &str) -> Self {
1571        Self::new_best_effort_with_dialect(formula, FormulaDialect::Excel)
1572    }
1573
1574    /// Create a new tokenizer with best-effort parsing for the specified dialect.
1575    pub fn new_best_effort_with_dialect(formula: &str, dialect: FormulaDialect) -> Self {
1576        let stream = TokenStream::new_best_effort_with_dialect(formula, dialect);
1577        Self::from_token_stream(&stream)
1578    }
1579
1580    /// Create a new tokenizer for the specified formula dialect.
1581    pub fn new_with_dialect(
1582        formula: &str,
1583        dialect: FormulaDialect,
1584    ) -> Result<Self, TokenizerError> {
1585        let mut tokenizer = Tokenizer {
1586            formula: formula.to_string(),
1587            items: Vec::with_capacity(formula.len() / 2), // Reasonable estimate
1588            token_stack: Vec::with_capacity(16),
1589            offset: 0,
1590            token_start: 0,
1591            token_end: 0,
1592            dialect,
1593        };
1594        tokenizer.parse()?;
1595        Ok(tokenizer)
1596    }
1597
1598    pub fn from_token_stream(stream: &TokenStream) -> Self {
1599        Tokenizer {
1600            formula: stream.source.to_string(),
1601            items: stream.to_tokens(),
1602            token_stack: Vec::with_capacity(16),
1603            offset: 0,
1604            token_start: 0,
1605            token_end: 0,
1606            dialect: stream.dialect,
1607        }
1608    }
1609
1610    /// Get byte at current offset
1611    #[inline]
1612    fn current_byte(&self) -> Option<u8> {
1613        self.formula.as_bytes().get(self.offset).copied()
1614    }
1615
1616    /// Check if we have a token accumulated
1617    #[inline]
1618    fn has_token(&self) -> bool {
1619        self.token_end > self.token_start
1620    }
1621
1622    /// Start a new token at current position
1623    #[inline]
1624    fn start_token(&mut self) {
1625        self.token_start = self.offset;
1626        self.token_end = self.offset;
1627    }
1628
1629    /// Extend current token to current position
1630    #[inline]
1631    fn extend_token(&mut self) {
1632        self.token_end = self.offset;
1633    }
1634
1635    /// Parse the formula into tokens.
1636    fn parse(&mut self) -> Result<(), TokenizerError> {
1637        if self.formula.is_empty() {
1638            return Ok(());
1639        }
1640
1641        // Check for literal formula (doesn't start with '=')
1642        if self.formula.as_bytes()[0] != b'=' {
1643            self.items.push(Token::new_with_span(
1644                self.formula.clone(),
1645                TokenType::Literal,
1646                TokenSubType::None,
1647                0,
1648                self.formula.len(),
1649            ));
1650            return Ok(());
1651        }
1652
1653        // Skip the '=' character
1654        self.offset = 1;
1655        self.start_token();
1656
1657        while self.offset < self.formula.len() {
1658            if self.check_scientific_notation()? {
1659                continue;
1660            }
1661
1662            let curr_byte = self.formula.as_bytes()[self.offset];
1663
1664            // Check if this ends a token
1665            if is_token_ender(curr_byte) && self.has_token() {
1666                self.save_token();
1667                self.start_token();
1668            }
1669
1670            // Dispatch based on the current character
1671            match curr_byte {
1672                b'"' | b'\'' => self.parse_string()?,
1673                b'[' => self.parse_brackets()?,
1674                b'#' => {
1675                    if self.should_emit_hash_postfix() {
1676                        self.emit_hash_postfix();
1677                    } else {
1678                        self.parse_error()?
1679                    }
1680                }
1681                b' ' | b'\t' | b'\r' | b'\n' => self.parse_whitespace()?,
1682                b':' => {
1683                    if self.should_emit_colon_infix() {
1684                        self.emit_infix_operator(self.offset, self.offset + 1);
1685                    } else {
1686                        if !self.has_token() {
1687                            self.start_token();
1688                        }
1689                        self.offset += 1;
1690                        self.extend_token();
1691                    }
1692                }
1693                // operator characters
1694                b'+' | b'-' | b'*' | b'/' | b'^' | b'&' | b'=' | b'>' | b'<' | b'%' | b'@' => {
1695                    self.parse_operator()?
1696                }
1697                b'{' | b'(' => self.parse_opener()?,
1698                b')' | b'}' => self.parse_closer()?,
1699                b';' | b',' => self.parse_separator()?,
1700                _ => {
1701                    // Accumulate into current token
1702                    if !self.has_token() {
1703                        self.start_token();
1704                    }
1705                    self.offset += 1;
1706                    self.extend_token();
1707                }
1708            }
1709        }
1710
1711        // Save any remaining token
1712        if self.has_token() {
1713            self.save_token();
1714        }
1715
1716        // Check for unmatched opening parentheses/brackets
1717        if !self.token_stack.is_empty() {
1718            return Err(TokenizerError {
1719                message: "Unmatched opening parenthesis or bracket".to_string(),
1720                pos: self.offset,
1721            });
1722        }
1723
1724        Ok(())
1725    }
1726
1727    /// If the current token looks like a number in scientific notation,
1728    /// consume the '+' or '-' as part of the number.
1729    ///
1730    /// The `+`/`-` is only consumed when the next byte is an ASCII digit.
1731    /// Without that one-byte lookahead, inputs like `=1e+` and `=1E-A1`
1732    /// would be silently absorbed into a single (invalid) numeric token
1733    /// and surface later as a `NamedRange`. See issue #78.
1734    fn check_scientific_notation(&mut self) -> Result<bool, TokenizerError> {
1735        if let Some(curr_byte) = self.current_byte() {
1736            if (curr_byte == b'+' || curr_byte == b'-')
1737                && self.has_token()
1738                && self.is_scientific_notation_base()
1739                && self
1740                    .formula
1741                    .as_bytes()
1742                    .get(self.offset + 1)
1743                    .is_some_and(|b| b.is_ascii_digit())
1744            {
1745                self.offset += 1;
1746                self.extend_token();
1747                return Ok(true);
1748            }
1749        }
1750        Ok(false)
1751    }
1752
1753    /// Helper: Determine if the current accumulated token is the base of a
1754    /// scientific notation number (e.g., "1.23E" or "9e").
1755    fn is_scientific_notation_base(&self) -> bool {
1756        if !self.has_token() {
1757            return false;
1758        }
1759
1760        let token_slice = &self.formula.as_bytes()[self.token_start..self.token_end];
1761        if token_slice.len() < 2 {
1762            return false;
1763        }
1764
1765        let last = token_slice[token_slice.len() - 1];
1766        if !(last == b'E' || last == b'e') {
1767            return false;
1768        }
1769
1770        let first = token_slice[0];
1771        if !first.is_ascii_digit() {
1772            return false;
1773        }
1774
1775        let mut dot_seen = false;
1776        // Check middle characters
1777        for &ch in &token_slice[1..token_slice.len() - 1] {
1778            match ch {
1779                b'0'..=b'9' => {}
1780                b'.' if !dot_seen => dot_seen = true,
1781                _ => return false,
1782            }
1783        }
1784        true
1785    }
1786
1787    /// If there is an accumulated token, convert it to an operand token and add it to the list.
1788    fn save_token(&mut self) {
1789        if self.has_token() {
1790            let token =
1791                Token::make_operand_from_slice(&self.formula, self.token_start, self.token_end);
1792            self.items.push(token);
1793        }
1794    }
1795
1796    /// Parse a string (or link) literal.
1797    fn parse_string(&mut self) -> Result<(), TokenizerError> {
1798        let delim = self.formula.as_bytes()[self.offset];
1799        assert!(delim == b'"' || delim == b'\'');
1800
1801        // Check for dollar reference special case
1802        let is_dollar_ref = delim == b'\''
1803            && self.has_token()
1804            && self.token_end - self.token_start == 1
1805            && self.formula.as_bytes()[self.token_start] == b'$';
1806
1807        // Issue #79: only the single-quote path should keep accumulating
1808        // onto a `:`-terminated token (e.g. `A1:'Other Sheet'!B2`). For
1809        // double-quoted string literals, always flush the pending token so
1810        // that the prefix (e.g. `A1:`) is not silently discarded.
1811        let glue_to_token = delim == b'\''
1812            && self.has_token()
1813            && self.token_end > 0
1814            && self.formula.as_bytes()[self.token_end - 1] == b':';
1815
1816        if !is_dollar_ref && !glue_to_token && self.has_token() {
1817            self.save_token();
1818            self.start_token();
1819        }
1820
1821        let string_start = if is_dollar_ref {
1822            self.token_start
1823        } else {
1824            self.offset
1825        };
1826        self.offset += 1; // Skip opening delimiter
1827
1828        while self.offset < self.formula.len() {
1829            if self.formula.as_bytes()[self.offset] == delim {
1830                self.offset += 1;
1831                // Check for escaped quote
1832                if self.offset < self.formula.len() && self.formula.as_bytes()[self.offset] == delim
1833                {
1834                    self.offset += 1; // Skip escaped quote
1835                } else {
1836                    // End of string
1837                    if delim == b'"' {
1838                        let token = Token::make_operand_from_slice(
1839                            &self.formula,
1840                            string_start,
1841                            self.offset,
1842                        );
1843                        self.items.push(token);
1844                        self.start_token();
1845                    } else {
1846                        // Single-quoted string becomes part of current token
1847                        self.token_end = self.offset;
1848                    }
1849                    return Ok(());
1850                }
1851            } else {
1852                self.offset += 1;
1853            }
1854        }
1855
1856        Err(TokenizerError {
1857            message: "Reached end of formula while parsing string".to_string(),
1858            pos: self.offset,
1859        })
1860    }
1861
1862    /// Parse the text between matching square brackets.
1863    fn parse_brackets(&mut self) -> Result<(), TokenizerError> {
1864        assert_eq!(self.formula.as_bytes()[self.offset], b'[');
1865
1866        if !self.has_token() {
1867            self.start_token();
1868        }
1869
1870        let mut open_count = 1;
1871        self.offset += 1;
1872
1873        while self.offset < self.formula.len() {
1874            match self.formula.as_bytes()[self.offset] {
1875                // OOXML structured-reference escape: a single apostrophe makes
1876                // the next byte literal (used to embed `[`, `]`, `'`, or `#`
1877                // inside a column name). Skip the following byte without
1878                // updating nesting depth.
1879                b'\'' => {
1880                    if self.offset + 1 < self.formula.len() {
1881                        self.offset += 2;
1882                        continue;
1883                    }
1884                    self.offset += 1;
1885                    continue;
1886                }
1887                b'[' => open_count += 1,
1888                b']' => {
1889                    open_count -= 1;
1890                    if open_count == 0 {
1891                        self.offset += 1;
1892                        self.extend_token();
1893                        return Ok(());
1894                    }
1895                }
1896                _ => {}
1897            }
1898            self.offset += 1;
1899        }
1900
1901        Err(TokenizerError {
1902            message: "Encountered unmatched '['".to_string(),
1903            pos: self.offset,
1904        })
1905    }
1906
1907    /// See `SpanTokenizer::should_emit_hash_postfix` for rationale.
1908    fn should_emit_hash_postfix(&self) -> bool {
1909        if self.has_token() {
1910            if self.formula.as_bytes()[self.token_end - 1] == b'!' {
1911                return false;
1912            }
1913            let value = &self.formula[self.token_start..self.token_end];
1914            // Mirror `make_operand_from_slice` subtype detection: the
1915            // accumulated token forms a Range operand iff it isn't a quoted
1916            // string, error literal, boolean, or number.
1917            let is_range = !value.starts_with('"')
1918                && !value.starts_with('#')
1919                && value != "TRUE"
1920                && value != "FALSE"
1921                && value.parse::<f64>().is_err();
1922            return is_range;
1923        }
1924        let prev = self
1925            .items
1926            .iter()
1927            .rev()
1928            .find(|t| t.token_type != TokenType::Whitespace);
1929        match prev {
1930            Some(p) => match p.token_type {
1931                TokenType::OpPostfix => true,
1932                TokenType::Paren | TokenType::Func | TokenType::Array
1933                    if p.subtype == TokenSubType::Close =>
1934                {
1935                    true
1936                }
1937                TokenType::Operand if p.subtype == TokenSubType::Range => true,
1938                _ => false,
1939            },
1940            None => false,
1941        }
1942    }
1943
1944    fn emit_hash_postfix(&mut self) {
1945        self.save_token();
1946        self.start_token();
1947        self.items.push(Token::from_slice(
1948            &self.formula,
1949            TokenType::OpPostfix,
1950            TokenSubType::None,
1951            self.offset,
1952            self.offset + 1,
1953        ));
1954        self.offset += 1;
1955        self.start_token();
1956    }
1957
1958    /// Parse an error literal that starts with '#'.
1959    fn parse_error(&mut self) -> Result<(), TokenizerError> {
1960        // OOXML serializes broken sheet-qualified references as `Sheet1!#REF!`.
1961        // When an accumulated token ends with `!`, treat the prefix as a sheet
1962        // qualifier and discard it: the resulting AST is identical to the bare
1963        // error literal `=#REF!`, preserving the error kind via `ERROR_CODES`.
1964        let has_sheet_prefix = self.has_token()
1965            && self.token_end > 0
1966            && self.formula.as_bytes()[self.token_end - 1] == b'!';
1967        if has_sheet_prefix {
1968            if self.token_end - self.token_start <= 1 {
1969                return Err(TokenizerError {
1970                    message: format!(
1971                        "Empty sheet qualifier before error literal at position {}",
1972                        self.offset
1973                    ),
1974                    pos: self.offset,
1975                });
1976            }
1977            // Discard the sheet prefix; the error kind is what matters.
1978            self.start_token();
1979        } else if self.has_token() {
1980            self.save_token();
1981            self.start_token();
1982        }
1983
1984        let error_start = self.offset;
1985
1986        // Try to match error codes
1987        for &err_code in ERROR_CODES {
1988            let err_bytes = err_code.as_bytes();
1989            if self.offset + err_bytes.len() <= self.formula.len() {
1990                let slice = &self.formula.as_bytes()[self.offset..self.offset + err_bytes.len()];
1991                if slice.eq_ignore_ascii_case(err_bytes) {
1992                    let token = Token::make_operand_from_slice(
1993                        &self.formula,
1994                        error_start,
1995                        self.offset + err_bytes.len(),
1996                    );
1997                    self.items.push(token);
1998                    self.offset += err_bytes.len();
1999                    self.start_token();
2000                    return Ok(());
2001                }
2002            }
2003        }
2004
2005        Err(TokenizerError {
2006            message: format!("Invalid error code at position {}", self.offset),
2007            pos: self.offset,
2008        })
2009    }
2010
2011    /// Parse a sequence of whitespace characters.
2012    fn parse_whitespace(&mut self) -> Result<(), TokenizerError> {
2013        self.save_token();
2014
2015        let ws_start = self.offset;
2016        let mut contains_intersection_space = false;
2017        while self.offset < self.formula.len() {
2018            match self.formula.as_bytes()[self.offset] {
2019                b' ' => {
2020                    contains_intersection_space = true;
2021                    self.offset += 1;
2022                }
2023                b'\t' | b'\r' | b'\n' => self.offset += 1,
2024                _ => break,
2025            }
2026        }
2027
2028        // Excel accepts TAB/CR/LF as lexical whitespace, but only an ASCII
2029        // space in the run can spell the range-intersection operator.
2030        let token_type = if contains_intersection_space
2031            && self.prev_is_reference_producing()
2032            && next_starts_reference_expression(&self.formula, self.offset)
2033        {
2034            TokenType::OpInfix
2035        } else {
2036            TokenType::Whitespace
2037        };
2038
2039        let token = if token_type == TokenType::OpInfix {
2040            Token::new_with_span(
2041                " ".to_string(),
2042                token_type,
2043                TokenSubType::None,
2044                ws_start,
2045                self.offset,
2046            )
2047        } else {
2048            Token::from_slice(
2049                &self.formula,
2050                token_type,
2051                TokenSubType::None,
2052                ws_start,
2053                self.offset,
2054            )
2055        };
2056        self.items.push(token);
2057        self.start_token();
2058        Ok(())
2059    }
2060
2061    fn prev_non_whitespace(&self) -> Option<&Token> {
2062        self.items
2063            .iter()
2064            .rev()
2065            .find(|t| t.token_type != TokenType::Whitespace)
2066    }
2067
2068    fn prev_is_reference_producing(&self) -> bool {
2069        match self.prev_non_whitespace() {
2070            Some(prev) => match prev.token_type {
2071                TokenType::OpPostfix => true,
2072                TokenType::Paren | TokenType::Func | TokenType::Array
2073                    if prev.subtype == TokenSubType::Close =>
2074                {
2075                    true
2076                }
2077                TokenType::Operand if prev.subtype == TokenSubType::Range => {
2078                    is_reference_operand_value(&prev.value)
2079                }
2080                _ => false,
2081            },
2082            None => false,
2083        }
2084    }
2085
2086    fn should_emit_colon_infix(&self) -> bool {
2087        if self.has_token() {
2088            let value = &self.formula[self.token_start..self.token_end];
2089            if value.ends_with('!') {
2090                return false;
2091            }
2092            return reference_value_contains_range_colon(value)
2093                || value.contains('[')
2094                || (value.contains('!')
2095                    && next_reference_has_sheet_qualifier(&self.formula, self.offset + 1));
2096        }
2097        self.prev_is_reference_producing()
2098    }
2099
2100    fn emit_infix_operator(&mut self, start: usize, end: usize) {
2101        self.save_token();
2102        self.start_token();
2103        self.items.push(Token::from_slice(
2104            &self.formula,
2105            TokenType::OpInfix,
2106            TokenSubType::None,
2107            start,
2108            end,
2109        ));
2110        self.offset = end;
2111        self.start_token();
2112    }
2113
2114    /// Parse an operator token.
2115    fn parse_operator(&mut self) -> Result<(), TokenizerError> {
2116        self.save_token();
2117
2118        // Check for two-character operators
2119        if self.offset + 1 < self.formula.len() {
2120            let two_char = &self.formula.as_bytes()[self.offset..self.offset + 2];
2121            if two_char == b">=" || two_char == b"<=" || two_char == b"<>" {
2122                self.items.push(Token::from_slice(
2123                    &self.formula,
2124                    TokenType::OpInfix,
2125                    TokenSubType::None,
2126                    self.offset,
2127                    self.offset + 2,
2128                ));
2129                self.offset += 2;
2130                self.start_token();
2131                return Ok(());
2132            }
2133        }
2134
2135        let curr_byte = self.formula.as_bytes()[self.offset];
2136        let token_type = match curr_byte {
2137            b'@' => TokenType::OpPrefix,
2138            b'%' => TokenType::OpPostfix,
2139            b'+' | b'-' => {
2140                // Determine if prefix or infix
2141                if self.items.is_empty() {
2142                    TokenType::OpPrefix
2143                } else {
2144                    let prev = self
2145                        .items
2146                        .iter()
2147                        .rev()
2148                        .find(|t| t.token_type != TokenType::Whitespace);
2149                    if let Some(p) = prev {
2150                        if p.subtype == TokenSubType::Close
2151                            || p.token_type == TokenType::OpPostfix
2152                            || p.token_type == TokenType::Operand
2153                        {
2154                            TokenType::OpInfix
2155                        } else {
2156                            TokenType::OpPrefix
2157                        }
2158                    } else {
2159                        TokenType::OpPrefix
2160                    }
2161                }
2162            }
2163            _ => TokenType::OpInfix,
2164        };
2165
2166        self.items.push(Token::from_slice(
2167            &self.formula,
2168            token_type,
2169            TokenSubType::None,
2170            self.offset,
2171            self.offset + 1,
2172        ));
2173        self.offset += 1;
2174        self.start_token();
2175        Ok(())
2176    }
2177
2178    /// Parse an opener token – either '(' or '{'.
2179    fn parse_opener(&mut self) -> Result<(), TokenizerError> {
2180        let curr_byte = self.formula.as_bytes()[self.offset];
2181        assert!(curr_byte == b'(' || curr_byte == b'{');
2182
2183        let token = if curr_byte == b'{' {
2184            self.save_token();
2185            Token::make_subexp_from_slice(&self.formula, false, self.offset, self.offset + 1)
2186        } else if self.has_token() {
2187            // Function call
2188            let token = Token::make_subexp_from_slice(
2189                &self.formula,
2190                true,
2191                self.token_start,
2192                self.offset + 1,
2193            );
2194            self.token_start = self.offset + 1;
2195            self.token_end = self.offset + 1;
2196            token
2197        } else {
2198            Token::make_subexp_from_slice(&self.formula, false, self.offset, self.offset + 1)
2199        };
2200
2201        self.items.push(token.clone());
2202        self.token_stack.push(token);
2203        self.offset += 1;
2204        self.start_token();
2205        Ok(())
2206    }
2207
2208    /// Parse a closer token – either ')' or '}'.
2209    fn parse_closer(&mut self) -> Result<(), TokenizerError> {
2210        self.save_token();
2211
2212        let curr_byte = self.formula.as_bytes()[self.offset];
2213        assert!(curr_byte == b')' || curr_byte == b'}');
2214
2215        if let Some(open_token) = self.token_stack.pop() {
2216            let closer = open_token.get_closer()?;
2217            if (curr_byte == b'}' && closer.value != "}")
2218                || (curr_byte == b')' && closer.value != ")")
2219            {
2220                return Err(TokenizerError {
2221                    message: "Mismatched ( and { pair".to_string(),
2222                    pos: self.offset,
2223                });
2224            }
2225
2226            self.items.push(Token::from_slice(
2227                &self.formula,
2228                closer.token_type,
2229                TokenSubType::Close,
2230                self.offset,
2231                self.offset + 1,
2232            ));
2233        } else {
2234            return Err(TokenizerError {
2235                message: format!("No matching opener for closer at position {}", self.offset),
2236                pos: self.offset,
2237            });
2238        }
2239
2240        self.offset += 1;
2241        self.start_token();
2242        Ok(())
2243    }
2244
2245    /// Parse a separator token – either ',' or ';'.
2246    fn parse_separator(&mut self) -> Result<(), TokenizerError> {
2247        self.save_token();
2248
2249        let curr_byte = self.formula.as_bytes()[self.offset];
2250        assert!(curr_byte == b';' || curr_byte == b',');
2251
2252        let top_token = self.token_stack.last();
2253        let in_function_or_array = matches!(
2254            top_token.map(|t| t.token_type),
2255            Some(TokenType::Func | TokenType::Array)
2256        );
2257        let in_array = matches!(top_token.map(|t| t.token_type), Some(TokenType::Array));
2258
2259        let (token_type, subtype) = match curr_byte {
2260            b',' => {
2261                if in_function_or_array {
2262                    (TokenType::Sep, TokenSubType::Arg)
2263                } else {
2264                    (TokenType::OpInfix, TokenSubType::None)
2265                }
2266            }
2267            b';' => {
2268                if in_array {
2269                    // Array row separator for both dialects
2270                    (TokenType::Sep, TokenSubType::Row)
2271                } else if self.dialect == FormulaDialect::OpenFormula && in_function_or_array {
2272                    // OpenFormula uses ';' for argument separators inside functions
2273                    (TokenType::Sep, TokenSubType::Arg)
2274                } else if self.dialect == FormulaDialect::OpenFormula {
2275                    (TokenType::OpInfix, TokenSubType::None)
2276                } else {
2277                    (TokenType::Sep, TokenSubType::Row)
2278                }
2279            }
2280            _ => (TokenType::OpInfix, TokenSubType::None),
2281        };
2282
2283        self.items.push(Token::from_slice(
2284            &self.formula,
2285            token_type,
2286            subtype,
2287            self.offset,
2288            self.offset + 1,
2289        ));
2290
2291        self.offset += 1;
2292        self.start_token();
2293        Ok(())
2294    }
2295
2296    /// Reconstruct the formula from the parsed tokens.
2297    pub fn render(&self) -> String {
2298        if self.items.is_empty() {
2299            "".to_string()
2300        } else if self.items[0].token_type == TokenType::Literal {
2301            self.items[0].value.clone()
2302        } else {
2303            let concatenated: String = self.items.iter().map(|t| t.value.clone()).collect();
2304            format!("={concatenated}")
2305        }
2306    }
2307
2308    /// Return the dialect used when tokenizing this formula.
2309    pub fn dialect(&self) -> FormulaDialect {
2310        self.dialect
2311    }
2312}
2313
2314impl TryFrom<&str> for Tokenizer {
2315    type Error = TokenizerError;
2316
2317    fn try_from(value: &str) -> Result<Self, Self::Error> {
2318        Tokenizer::new(value)
2319    }
2320}
2321
2322impl TryFrom<String> for Tokenizer {
2323    type Error = TokenizerError;
2324
2325    fn try_from(value: String) -> Result<Self, Self::Error> {
2326        Tokenizer::new(&value)
2327    }
2328}