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