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
13#[expect(
14 clippy::indexing_slicing,
15 clippy::arithmetic_side_effects,
16 reason = "const fn evaluated at compile time: a u8 index into a 256-entry table and a cursor bounded by the slice length"
17)]
18const fn build_token_enders() -> [bool; 256] {
19 let mut tbl = [false; 256];
20 let bytes = TOKEN_ENDERS.as_bytes();
21 let mut i = 0;
22 while i < bytes.len() {
23 tbl[bytes[i] as usize] = true;
24 i += 1;
25 }
26 tbl
27}
28static TOKEN_ENDERS_TABLE: [bool; 256] = build_token_enders();
29
30#[inline(always)]
31#[expect(clippy::indexing_slicing, reason = "u8 index into a 256-entry table")]
32fn is_token_ender(c: u8) -> bool {
33 TOKEN_ENDERS_TABLE[c as usize]
34}
35
36static ERROR_CODES: &[&str] = &[
53 "#GETTING_DATA",
54 "#DIV/0!",
55 "#VALUE!",
56 "#SPILL!",
57 "#NAME?",
58 "#NULL!",
59 "#CALC!",
60 "#NUM!",
61 "#REF!",
62 "#N/A",
63];
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum Associativity {
68 Left,
69 Right,
70}
71
72#[derive(Debug)]
74pub struct TokenizerError {
75 pub message: String,
76 pub pos: usize,
77}
78
79#[non_exhaustive]
81#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
83pub enum RecoveryAction {
84 ResourceLimitExceeded,
86 SkippedUnmatchedCloser,
88 UnterminatedString,
90 UnmatchedBracket,
92 InvalidErrorLiteral,
94 UnmatchedOpener,
96}
97
98#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct TokenDiagnostic {
102 pub span: TokenSpan,
103 pub message: String,
104 pub recovery: RecoveryAction,
105}
106
107impl TokenDiagnostic {
108 fn resource_limit(message: String) -> Self {
109 Self::new(
110 TokenSpan {
111 token_type: TokenType::Literal,
112 subtype: TokenSubType::None,
113 start: 0,
114 end: 0,
115 },
116 message,
117 RecoveryAction::ResourceLimitExceeded,
118 )
119 }
120 fn new(span: TokenSpan, message: String, recovery: RecoveryAction) -> Self {
121 Self {
122 span,
123 message,
124 recovery,
125 }
126 }
127}
128
129#[derive(Debug, Clone)]
130struct SpanTokenizerError {
131 kind: SpanTokenizerErrorKind,
132 pos: usize,
133 message: String,
134 span_start: Option<usize>,
135 span_end: Option<usize>,
136}
137
138#[derive(Debug, Clone, Copy)]
139enum SpanTokenizerErrorKind {
140 NoMatchingOpener,
141 UnmatchedOpening,
142 UnterminatedString,
143 UnmatchedBracket,
144 MismatchedPair,
145 InvalidErrorLiteral,
146}
147
148impl From<SpanTokenizerError> for TokenizerError {
149 fn from(value: SpanTokenizerError) -> Self {
150 TokenizerError {
151 message: value.message,
152 pos: value.pos,
153 }
154 }
155}
156
157impl fmt::Display for TokenizerError {
158 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159 write!(f, "TokenizerError: {}", self.message)
160 }
161}
162
163impl Error for TokenizerError {}
164
165#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
168pub enum TokenType {
169 Literal,
170 Operand,
171 Func,
172 Array,
173 Paren,
174 Sep,
175 OpPrefix,
176 OpInfix,
177 OpPostfix,
178 Whitespace,
179}
180impl Display for TokenType {
181 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182 write!(f, "{self:?}")
183 }
184}
185
186#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
189pub enum TokenSubType {
190 None,
191 Text,
192 Number,
193 Logical,
194 Error,
195 Range,
196 Open,
197 Close,
198 Arg,
199 Row,
200}
201impl Display for TokenSubType {
202 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203 write!(f, "{self:?}")
204 }
205}
206
207#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
209#[derive(Debug, Clone, PartialEq, Hash)]
210pub struct Token {
211 pub value: String, pub token_type: TokenType,
213 pub subtype: TokenSubType,
214 pub start: usize,
215 pub end: usize,
216}
217
218impl Display for Token {
219 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220 write!(
221 f,
222 "<{} subtype: {:?} value: {}>",
223 self.token_type, self.subtype, self.value
224 )
225 }
226}
227
228impl Token {
229 pub fn new(value: String, token_type: TokenType, subtype: TokenSubType) -> Self {
230 Token {
231 value,
232 token_type,
233 subtype,
234 start: 0,
235 end: 0,
236 }
237 }
238
239 pub fn new_with_span(
240 value: String,
241 token_type: TokenType,
242 subtype: TokenSubType,
243 start: usize,
244 end: usize,
245 ) -> Self {
246 Token {
247 value,
248 token_type,
249 subtype,
250 start,
251 end,
252 }
253 }
254
255 pub fn is_operator(&self) -> bool {
256 matches!(
257 self.token_type,
258 TokenType::OpPrefix | TokenType::OpInfix | TokenType::OpPostfix
259 )
260 }
261
262 pub fn get_precedence(&self) -> Option<(u8, Associativity)> {
263 let op = if self.token_type == TokenType::OpPrefix {
265 "u"
266 } else {
267 self.value.as_str()
268 };
269
270 match op {
281 "#" => Some((11, Associativity::Left)),
282 ":" => Some((10, Associativity::Left)),
283 " " => Some((9, Associativity::Left)),
284 "," => Some((8, Associativity::Left)),
285 "%" => Some((7, Associativity::Left)),
286 "u" => Some((6, Associativity::Right)),
287 "^" => Some((5, Associativity::Left)),
288 "*" | "/" => Some((4, Associativity::Left)),
289 "+" | "-" => Some((3, Associativity::Left)),
290 "&" => Some((2, Associativity::Left)),
291 "=" | "<" | ">" | "<=" | ">=" | "<>" => Some((1, Associativity::Left)),
292 _ => None,
293 }
294 }
295
296 pub fn make_operand(value: String) -> Self {
298 let subtype = if value.starts_with('"') {
299 TokenSubType::Text
300 } else if value.starts_with('#') {
301 TokenSubType::Error
302 } else if value.eq_ignore_ascii_case("TRUE") || value.eq_ignore_ascii_case("FALSE") {
303 TokenSubType::Logical
304 } else if value.parse::<f64>().is_ok() {
305 TokenSubType::Number
306 } else {
307 TokenSubType::Range
308 };
309 Token::new(value, TokenType::Operand, subtype)
310 }
311
312 pub fn make_operand_with_span(value: String, start: usize, end: usize) -> Self {
314 let subtype = if value.starts_with('"') {
315 TokenSubType::Text
316 } else if value.starts_with('#') {
317 TokenSubType::Error
318 } else if value.eq_ignore_ascii_case("TRUE") || value.eq_ignore_ascii_case("FALSE") {
319 TokenSubType::Logical
320 } else if value.parse::<f64>().is_ok() {
321 TokenSubType::Number
322 } else {
323 TokenSubType::Range
324 };
325 Token::new_with_span(value, TokenType::Operand, subtype, start, end)
326 }
327
328 pub fn get_closer(&self) -> Result<Token, TokenizerError> {
330 if self.subtype != TokenSubType::Open {
331 return Err(TokenizerError {
332 message: "Token is not an opener".to_string(),
333 pos: 0,
334 });
335 }
336 let (value, token_type) = match self.token_type {
337 TokenType::Func => (")", TokenType::Func),
338 TokenType::Array => ("}", TokenType::Array),
339 _ => (")", TokenType::Paren),
340 };
341 Ok(Token::new(
342 value.to_string(),
343 token_type,
344 TokenSubType::Close,
345 ))
346 }
347}
348
349#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
350#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
351pub struct TokenSpan {
352 pub token_type: TokenType,
353 pub subtype: TokenSubType,
354 pub start: usize,
355 pub end: usize,
356}
357
358#[derive(Debug, Clone, Copy, PartialEq, Eq)]
359pub struct TokenView<'a> {
360 pub span: &'a TokenSpan,
361 pub value: &'a str,
362}
363
364#[derive(Debug, Clone)]
370pub struct TokenStream {
371 pub(crate) pending_error: Option<String>,
372 source: Arc<str>,
373 pub spans: Vec<TokenSpan>,
374 dialect: FormulaDialect,
375 diagnostics: Vec<TokenDiagnostic>,
376}
377
378fn is_whitespace_intersection_span(source: &str, span: &TokenSpan) -> bool {
379 if span.token_type != TokenType::OpInfix {
380 return false;
381 }
382 let Some(value) = source.get(span.start..span.end) else {
383 return false;
384 };
385 value.as_bytes().contains(&b' ')
386 && value
387 .as_bytes()
388 .iter()
389 .all(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
390}
391
392impl TokenStream {
393 pub fn new(formula: &str) -> Result<Self, TokenizerError> {
394 Self::new_with_dialect(formula, FormulaDialect::Excel)
395 }
396
397 pub fn new_with_dialect(
398 formula: &str,
399 dialect: FormulaDialect,
400 ) -> Result<Self, TokenizerError> {
401 let spans = tokenize_spans_with_dialect(formula, dialect)?;
402 let source: Arc<str> = Arc::from(formula);
403 Ok(TokenStream {
404 pending_error: None,
405 source,
406 spans,
407 dialect,
408 diagnostics: Vec::new(),
409 })
410 }
411
412 pub fn new_best_effort(formula: &str) -> Self {
413 Self::new_best_effort_with_dialect(formula, FormulaDialect::Excel)
414 }
415
416 pub fn new_best_effort_with_dialect(formula: &str, dialect: FormulaDialect) -> Self {
417 if let Err(error) = crate::ParserLimits::default().check_source(formula) {
418 return TokenStream {
419 source: Arc::from(""),
420 spans: Vec::new(),
421 dialect,
422 diagnostics: vec![TokenDiagnostic::resource_limit(error.message.clone())],
423 pending_error: Some(error.message),
424 };
425 }
426 let source: Arc<str> = Arc::from(formula);
427 let mut tokenizer = SpanTokenizer::new(source.as_ref(), dialect);
428 let spans = tokenizer.parse_best_effort();
429 let pending_error = tokenizer.exceeded.then(|| {
430 format!(
431 "Formula token limit exceeded (max {})",
432 tokenizer.token_limit
433 )
434 });
435 let mut diagnostics = tokenizer.diagnostics;
436 if let Some(message) = &pending_error {
437 diagnostics.push(TokenDiagnostic::resource_limit(message.clone()));
438 }
439 TokenStream {
440 pending_error,
441 source,
442 spans,
443 dialect,
444 diagnostics,
445 }
446 }
447
448 pub(crate) fn admission_error(&self, limits: crate::ParserLimits) -> Option<TokenizerError> {
450 if let Some(message) = &self.pending_error {
451 return Some(TokenizerError {
452 message: message.clone(),
453 pos: 0,
454 });
455 }
456 if let Err(error) = limits.check_source(&self.source) {
457 return Some(error);
458 }
459 if self.spans.len() > limits.tokens() {
460 return Some(TokenizerError {
461 message: format!("Formula token limit exceeded (max {})", limits.tokens()),
462 pos: 0,
463 });
464 }
465 let mut previous_end = 0;
466 for span in &self.spans {
467 if span.start < previous_end || self.source.get(span.start..span.end).is_none() {
470 return Some(TokenizerError {
471 message: "Invalid external token span sequence".into(),
472 pos: span.start,
473 });
474 }
475 previous_end = span.end;
476 }
477 None
478 }
479
480 pub fn diagnostics(&self) -> Vec<TokenDiagnostic> {
481 self.diagnostics.clone()
482 }
483
484 pub fn diagnostics_ref(&self) -> &[TokenDiagnostic] {
485 &self.diagnostics
486 }
487
488 pub fn has_errors(&self) -> bool {
489 !self.diagnostics.is_empty()
490 }
491
492 pub fn invalid_spans_iter(&self) -> impl Iterator<Item = &TokenSpan> {
493 self.spans.iter().filter(|span| {
494 self.diagnostics.iter().any(|diag| {
495 diag.span.start == span.start
496 && diag.span.end == span.end
497 && diag.span.token_type == span.token_type
498 })
499 })
500 }
501
502 pub fn invalid_spans(&self) -> Vec<&TokenSpan> {
503 self.invalid_spans_iter().collect()
504 }
505
506 pub fn source(&self) -> &str {
507 &self.source
508 }
509
510 pub fn dialect(&self) -> FormulaDialect {
511 self.dialect
512 }
513
514 pub fn len(&self) -> usize {
515 self.spans.len()
516 }
517
518 pub fn is_empty(&self) -> bool {
519 self.spans.is_empty()
520 }
521
522 pub fn get(&self, index: usize) -> Option<TokenView<'_>> {
523 let span = self.spans.get(index)?;
524 let value = self.source.get(span.start..span.end)?;
525 Some(TokenView { span, value })
526 }
527
528 pub fn to_tokens(&self) -> Vec<Token> {
529 tokens_from_spans(&self.source, &self.spans)
530 }
531
532 pub fn render(&self) -> String {
537 let mut out = String::with_capacity(self.source.len());
538 for span in &self.spans {
539 if let Some(s) = self.source.get(span.start..span.end) {
540 out.push_str(s);
541 }
542 }
543 out
544 }
545
546 pub fn render_formula(&self) -> String {
548 if self.source.as_bytes().first() == Some(&b'=') {
549 format!("={}", self.render())
550 } else {
551 self.render()
552 }
553 }
554}
555
556fn tokens_from_spans(source: &str, spans: &[TokenSpan]) -> Vec<Token> {
559 spans
560 .iter()
561 .map(|s| {
562 let value = if is_whitespace_intersection_span(source, s) {
563 " ".to_string()
564 } else {
565 source.get(s.start..s.end).unwrap_or_default().to_string()
566 };
567 Token::new_with_span(value, s.token_type, s.subtype, s.start, s.end)
568 })
569 .collect()
570}
571
572pub(crate) fn tokenize_spans_with_dialect(
573 formula: &str,
574 dialect: FormulaDialect,
575) -> Result<Vec<TokenSpan>, TokenizerError> {
576 tokenize_spans_with_limits(formula, dialect, crate::ParserLimits::default())
577}
578pub(crate) fn tokenize_spans_with_limits(
579 formula: &str,
580 dialect: FormulaDialect,
581 limits: crate::ParserLimits,
582) -> Result<Vec<TokenSpan>, TokenizerError> {
583 limits.check_source(formula)?;
584 let mut tokenizer = SpanTokenizer::new_with_limit(formula, dialect, limits.tokens());
585 tokenizer.parse()?;
586 Ok(tokenizer.spans)
587}
588
589fn operand_subtype(value_str: &str) -> TokenSubType {
590 if value_str.starts_with('"') {
591 TokenSubType::Text
592 } else if value_str.starts_with('#') {
593 TokenSubType::Error
594 } else if value_str.eq_ignore_ascii_case("TRUE") || value_str.eq_ignore_ascii_case("FALSE") {
595 TokenSubType::Logical
596 } else if value_str.parse::<f64>().is_ok() {
597 TokenSubType::Number
598 } else {
599 TokenSubType::Range
600 }
601}
602
603fn is_cell_reference_like(value: &str) -> bool {
605 let col = value.strip_prefix('$').unwrap_or(value);
606 let after_letters = col.trim_start_matches(|c: char| c.is_ascii_alphabetic());
607 if after_letters.len() == col.len() {
608 return false;
609 }
610 let row = after_letters.strip_prefix('$').unwrap_or(after_letters);
611 !row.is_empty() && row.bytes().all(|b| b.is_ascii_digit())
612}
613
614fn reference_value_contains_range_colon(value: &str) -> bool {
615 let value_part = value
616 .rsplit_once('!')
617 .map_or(value, |(_, value_part)| value_part);
618 value_part.contains(':')
619}
620
621fn value_has_structured_reference_bracket(value: &str) -> bool {
626 value
627 .rsplit_once('!')
628 .map_or(value, |(_, value_part)| value_part)
629 .contains('[')
630}
631
632#[expect(
637 clippy::arithmetic_side_effects,
638 reason = "byte offset from ASCII delimiter search: bang and colon are indices of bytes inside value"
639)]
640fn range_colon_before_call(value: &str) -> Option<usize> {
641 let name_start = value.rfind('!').map_or(0, |bang| bang + 1);
642 let colon = name_start + value.get(name_start..)?.rfind(':')?;
643 (colon > 0 && colon + 1 < value.len()).then_some(colon)
644}
645
646#[expect(
649 clippy::arithmetic_side_effects,
650 clippy::string_slice,
651 reason = "cursor bounded by slice length; at most 3 letters are accumulated (col <= 18_278), each checked alphabetic before its offset is subtracted; the digit run is sliced at ASCII positions"
652)]
653fn is_a1_range_end(piece: &str) -> bool {
654 let bytes = piece.as_bytes();
655 let mut i = 0;
656 if bytes.get(i) == Some(&b'$') {
657 i += 1;
658 }
659 let letters_start = i;
660 let mut col: u32 = 0;
661 while let Some(&b) = bytes.get(i)
662 && b.is_ascii_alphabetic()
663 && i - letters_start < 3
664 {
665 col = col * 26 + u32::from(b.to_ascii_uppercase() - b'A' + 1);
666 i += 1;
667 }
668 let has_letters = i > letters_start;
669 if has_letters && (col > 16_384 || bytes.get(i).is_some_and(u8::is_ascii_alphabetic)) {
670 return false;
671 }
672 if has_letters && bytes.get(i) == Some(&b'$') {
673 i += 1;
674 }
675 let digits_start = i;
676 while bytes.get(i).is_some_and(u8::is_ascii_digit) {
677 i += 1;
678 }
679 let digits = &piece[digits_start..i];
680 let row_ok = digits.is_empty()
681 || (digits.len() <= 7
682 && digits
683 .parse::<u32>()
684 .is_ok_and(|r| (1..=1_048_576).contains(&r)));
685 i == bytes.len() && (has_letters || !digits.is_empty()) && row_ok
686}
687
688#[expect(
693 clippy::arithmetic_side_effects,
694 reason = "byte offset from ASCII delimiter search: start and the colon are positions inside value"
695)]
696fn range_operator_colon(value: &str) -> Option<usize> {
697 let start = value.rfind('!').map_or(0, |bang| bang + 1);
698 let part = value.get(start..)?;
699 if part.contains(['[', '\'', '"', '#']) || part.matches(':').count() != 1 {
700 return None;
701 }
702 let (left, right) = part.split_once(':')?;
703 if left.is_empty() || right.is_empty() || (is_a1_range_end(left) && is_a1_range_end(right)) {
704 return None;
705 }
706 Some(start + left.len())
707}
708
709fn ref_error_starts_at(formula: &str, offset: usize) -> bool {
711 formula
712 .as_bytes()
713 .get(offset..)
714 .and_then(|rest| rest.get(..5))
715 .is_some_and(|s| s.eq_ignore_ascii_case(b"#REF!"))
716}
717
718fn is_reference_operand_value(value: &str) -> bool {
719 operand_subtype(value) == TokenSubType::Range
720 && (reference_value_contains_range_colon(value)
721 || value.contains('!')
722 || value.contains('[')
723 || is_cell_reference_like(value))
724}
725
726fn next_starts_reference_expression(formula: &str, offset: usize) -> bool {
727 let Some(rest) = formula.as_bytes().get(offset..) else {
728 return false;
729 };
730 rest.iter()
731 .find(|b| !matches!(b, b' ' | b'\t' | b'\r' | b'\n'))
732 .is_some_and(|&b| matches!(b, b'(' | b'[' | b'\'' | b'$') || b.is_ascii_alphabetic())
733}
734
735fn next_reference_has_sheet_qualifier(formula: &str, offset: usize) -> bool {
736 let Some(rest) = formula.as_bytes().get(offset..) else {
737 return false;
738 };
739 let mut bytes = rest
740 .iter()
741 .copied()
742 .skip_while(|b| matches!(b, b' ' | b'\t' | b'\r' | b'\n'))
743 .peekable();
744
745 let mut in_quote = false;
746 while let Some(b) = bytes.next() {
747 match b {
748 b'\'' => {
749 if in_quote && bytes.peek() == Some(&b'\'') {
750 bytes.next();
751 continue;
752 }
753 in_quote = !in_quote;
754 }
755 b'!' => return true,
756 b':' if !in_quote => return false,
757 b',' | b';' | b'}' | b')' | b' ' | b'\t' | b'\r' | b'\n' | b'+' | b'-' | b'*'
758 | b'/' | b'^' | b'&' | b'=' | b'>' | b'<' | b'%' | b'@'
759 if !in_quote =>
760 {
761 return false;
762 }
763 _ => {}
764 }
765 }
766
767 false
768}
769
770struct SpanTokenizer<'a> {
778 formula: &'a str,
779 spans: Vec<TokenSpan>,
780 token_stack: Vec<TokenSpan>,
781 offset: usize,
782 token_start: usize,
783 token_end: usize,
784 dialect: FormulaDialect,
785 diagnostics: Vec<TokenDiagnostic>,
786 token_limit: usize,
787 exceeded: bool,
788}
789
790impl<'a> SpanTokenizer<'a> {
791 fn new(formula: &'a str, dialect: FormulaDialect) -> Self {
792 Self::new_with_limit(formula, dialect, crate::ParserLimits::default().tokens())
793 }
794 fn new_with_limit(formula: &'a str, dialect: FormulaDialect, token_limit: usize) -> Self {
795 SpanTokenizer {
796 formula,
797 spans: Vec::with_capacity((formula.len() / 2).min(token_limit)),
798 token_stack: Vec::with_capacity(16),
799 offset: 0,
800 token_start: 0,
801 token_end: 0,
802 dialect,
803 diagnostics: Vec::new(),
804 token_limit,
805 exceeded: false,
806 }
807 }
808
809 #[inline]
810 fn current_byte(&self) -> Option<u8> {
811 self.formula.as_bytes().get(self.offset).copied()
812 }
813
814 #[inline]
815 fn has_token(&self) -> bool {
816 self.token_end > self.token_start
817 }
818
819 #[inline]
820 fn start_token(&mut self) {
821 self.token_start = self.offset;
822 self.token_end = self.offset;
823 }
824
825 #[inline]
826 fn extend_token(&mut self) {
827 self.token_end = self.offset;
828 }
829
830 #[inline]
832 #[expect(
833 clippy::string_slice,
834 reason = "SpanTokenizer invariant: token_start <= token_end <= formula.len(), and every stored offset is 0, the length, the index of an ASCII byte or one past one, so it is a char boundary"
835 )]
836 fn token_str(&self) -> &'a str {
837 &self.formula[self.token_start..self.token_end]
838 }
839
840 #[inline]
842 #[expect(
843 clippy::string_slice,
844 reason = "SpanTokenizer invariant: token_start <= token_end <= formula.len(), and every stored offset is 0, the length, the index of an ASCII byte or one past one, so it is a char boundary"
845 )]
846 fn source_str(&self, start: usize, end: usize) -> &'a str {
847 &self.formula[start..end]
848 }
849
850 fn push_span(
851 &mut self,
852 token_type: TokenType,
853 subtype: TokenSubType,
854 start: usize,
855 end: usize,
856 ) {
857 if self.spans.len() >= self.token_limit {
858 self.exceeded = true;
859 return;
860 }
861 self.spans.push(TokenSpan {
862 token_type,
863 subtype,
864 start,
865 end,
866 });
867 }
868
869 #[expect(
870 clippy::arithmetic_side_effects,
871 reason = "byte offset from ASCII delimiter search: colon is the index of a `:` inside the token, so colon + 1 <= token_end"
872 )]
873 fn save_token(&mut self) {
874 if self.has_token() {
875 if let Some(colon) = range_operator_colon(self.token_str()) {
876 let colon = self.token_start + colon;
877 let left = operand_subtype(self.source_str(self.token_start, colon));
878 self.push_span(TokenType::Operand, left, self.token_start, colon);
879 self.push_span(TokenType::OpInfix, TokenSubType::None, colon, colon + 1);
880 self.token_start = colon + 1;
881 }
882 let subtype = operand_subtype(self.token_str());
883 self.push_span(
884 TokenType::Operand,
885 subtype,
886 self.token_start,
887 self.token_end,
888 );
889 }
890 }
891
892 #[expect(
893 clippy::arithmetic_side_effects,
894 reason = "cursor bounded by slice length: current_byte() returned Some, so offset < len"
895 )]
896 fn check_scientific_notation(&mut self) -> bool {
897 if let Some(curr_byte) = self.current_byte()
898 && (curr_byte == b'+' || curr_byte == b'-')
899 && self.has_token()
900 && self.is_scientific_notation_base()
901 && self
902 .formula
903 .as_bytes()
904 .get(self.offset + 1)
905 .is_some_and(|b| b.is_ascii_digit())
906 {
907 self.offset += 1;
908 self.extend_token();
909 return true;
910 }
911 false
912 }
913
914 fn is_scientific_notation_base(&self) -> bool {
915 let [first, middle @ .., last] = self.token_str().as_bytes() else {
916 return false;
917 };
918 if !(*last == b'E' || *last == b'e') {
919 return false;
920 }
921 if !first.is_ascii_digit() {
922 return false;
923 }
924
925 let mut dot_seen = false;
926 for &ch in middle {
927 match ch {
928 b'0'..=b'9' => {}
929 b'.' if !dot_seen => dot_seen = true,
930 _ => return false,
931 }
932 }
933 true
934 }
935
936 fn parse(&mut self) -> Result<(), TokenizerError> {
937 let result = self.parse_with_recovery(false).map_err(Into::into);
938 if self.exceeded {
939 return Err(TokenizerError {
940 message: format!("Formula token limit exceeded (max {})", self.token_limit),
941 pos: self.offset,
942 });
943 }
944 result
945 }
946
947 pub(crate) fn parse_best_effort(&mut self) -> Vec<TokenSpan> {
948 let _ = self.parse_with_recovery(true);
949 self.spans.clone()
950 }
951
952 #[expect(
953 clippy::arithmetic_side_effects,
954 reason = "cursor bounded by slice length: current_byte() returned Some, so offset < len"
955 )]
956 fn parse_with_recovery(&mut self, best_effort: bool) -> Result<(), SpanTokenizerError> {
957 match self.formula.as_bytes().first() {
958 None => return Ok(()),
959 Some(b'=') => {}
960 Some(_) => {
961 self.push_span(
962 TokenType::Literal,
963 TokenSubType::None,
964 0,
965 self.formula.len(),
966 );
967 return Ok(());
968 }
969 }
970
971 self.offset = 1;
972 self.start_token();
973
974 while let Some(curr_byte) = self.current_byte() {
977 if self.exceeded {
978 break;
979 }
980 if self.check_scientific_notation() {
981 continue;
982 }
983
984 if is_token_ender(curr_byte) && self.has_token() {
985 self.save_token();
986 self.start_token();
987 }
988
989 let parse_result = match curr_byte {
990 b'"' | b'\'' => self.parse_string(curr_byte),
991 b'[' => self.parse_brackets(),
992 b'#' => {
993 if self.should_emit_hash_postfix() {
994 self.emit_hash_postfix();
995 Ok(())
996 } else {
997 self.parse_error()
998 }
999 }
1000 b' ' | b'\t' | b'\r' | b'\n' => self.parse_whitespace(),
1001 b':' => {
1002 if self.should_emit_colon_infix() {
1003 self.emit_infix_operator(self.offset, self.offset + 1);
1004 Ok(())
1005 } else {
1006 if !self.has_token() {
1007 self.start_token();
1008 }
1009 self.offset += 1;
1010 self.extend_token();
1011 Ok(())
1012 }
1013 }
1014 b'+' | b'-' | b'*' | b'/' | b'^' | b'&' | b'=' | b'>' | b'<' | b'%' | b'@' => {
1015 self.parse_operator(curr_byte)
1016 }
1017 b'{' | b'(' => self.parse_opener(curr_byte),
1018 b')' | b'}' => self.parse_closer(curr_byte),
1019 b';' | b',' => self.parse_separator(curr_byte),
1020 _ => {
1021 if !self.has_token() {
1022 self.start_token();
1023 }
1024 self.offset += 1;
1025 self.extend_token();
1026 Ok(())
1027 }
1028 };
1029
1030 if let Err(err) = parse_result {
1031 if best_effort {
1032 self.recover_from_error(err);
1033 } else {
1034 return Err(err);
1035 }
1036 }
1037 }
1038
1039 if self.exceeded {
1042 return Ok(());
1043 }
1044 if self.has_token() {
1045 self.save_token();
1046 }
1047 if self.exceeded {
1048 return Ok(());
1049 }
1050
1051 if !self.token_stack.is_empty() {
1052 if best_effort {
1053 while let Some(open_token) = self.token_stack.pop() {
1054 if let Some(span) = self.spans.iter().find(|span| {
1055 span.start == open_token.start
1056 && span.end == open_token.end
1057 && span.token_type == open_token.token_type
1058 && span.subtype == open_token.subtype
1059 }) {
1060 self.diagnostics.push(TokenDiagnostic::new(
1061 *span,
1062 "Unmatched opening parenthesis or bracket".to_string(),
1063 RecoveryAction::UnmatchedOpener,
1064 ));
1065 }
1066 }
1067 } else {
1068 return Err(SpanTokenizerError {
1069 kind: SpanTokenizerErrorKind::UnmatchedOpening,
1070 pos: self.offset,
1071 message: "Unmatched opening parenthesis or bracket".to_string(),
1072 span_start: None,
1073 span_end: None,
1074 });
1075 }
1076 }
1077
1078 Ok(())
1079 }
1080
1081 #[expect(
1082 clippy::arithmetic_side_effects,
1083 reason = "error.pos is the offset of the ASCII byte that failed, < len, so pos + 1 <= len"
1084 )]
1085 fn recover_from_error(&mut self, error: SpanTokenizerError) {
1086 if self.spans.len() >= self.token_limit {
1087 self.exceeded = true;
1088 return;
1089 }
1090 match error.kind {
1091 SpanTokenizerErrorKind::NoMatchingOpener => {
1092 let span = TokenSpan {
1093 token_type: TokenType::Operand,
1094 subtype: TokenSubType::None,
1095 start: error.pos,
1096 end: error.pos + 1,
1097 };
1098 self.push_span(TokenType::Operand, TokenSubType::None, span.start, span.end);
1099 self.offset = span.end;
1100 self.start_token();
1101 self.diagnostics.push(TokenDiagnostic::new(
1102 span,
1103 format!("No matching opener for closer at position {}", error.pos),
1104 RecoveryAction::SkippedUnmatchedCloser,
1105 ));
1106 }
1107 SpanTokenizerErrorKind::UnmatchedOpening => {
1108 debug_assert!(
1109 false,
1110 "UnmatchedOpening is handled at end-of-input and should not be routed through recover_from_error"
1111 );
1112 }
1113 SpanTokenizerErrorKind::UnterminatedString => {
1114 let start = error.span_start.unwrap_or(error.pos);
1115 let span = TokenSpan {
1116 token_type: TokenType::Operand,
1117 subtype: TokenSubType::None,
1118 start,
1119 end: self.formula.len(),
1120 };
1121 self.push_span(TokenType::Operand, TokenSubType::None, span.start, span.end);
1122 self.offset = span.end;
1123 self.start_token();
1124 self.diagnostics.push(TokenDiagnostic::new(
1125 span,
1126 "Reached end of formula while parsing string".to_string(),
1127 RecoveryAction::UnterminatedString,
1128 ));
1129 }
1130 SpanTokenizerErrorKind::UnmatchedBracket => {
1131 let start = error.span_start.unwrap_or(error.pos);
1132 let end = error.span_end.unwrap_or(self.formula.len());
1133 let span = TokenSpan {
1134 token_type: TokenType::Operand,
1135 subtype: TokenSubType::None,
1136 start,
1137 end,
1138 };
1139 self.push_span(TokenType::Operand, TokenSubType::None, span.start, span.end);
1140 self.offset = span.end;
1141 self.start_token();
1142 self.diagnostics.push(TokenDiagnostic::new(
1143 span,
1144 "Encountered unmatched '['".to_string(),
1145 RecoveryAction::UnmatchedBracket,
1146 ));
1147 }
1148 SpanTokenizerErrorKind::MismatchedPair => {
1149 let span = TokenSpan {
1150 token_type: TokenType::Operand,
1151 subtype: TokenSubType::None,
1152 start: error.pos,
1153 end: error.pos + 1,
1154 };
1155 self.push_span(TokenType::Operand, TokenSubType::None, span.start, span.end);
1156 self.offset = span.end;
1157 self.start_token();
1158 self.diagnostics.push(TokenDiagnostic::new(
1159 span,
1160 "Mismatched ( and { pair".to_string(),
1161 RecoveryAction::SkippedUnmatchedCloser,
1162 ));
1163 }
1164 SpanTokenizerErrorKind::InvalidErrorLiteral => {
1165 let start = error.span_start.unwrap_or(error.pos);
1166 let end = error.span_end.unwrap_or(error.pos + 1);
1167 let span = TokenSpan {
1168 token_type: TokenType::Operand,
1169 subtype: TokenSubType::None,
1170 start,
1171 end,
1172 };
1173 self.push_span(TokenType::Operand, TokenSubType::None, span.start, span.end);
1174 self.offset = span.end;
1175 self.start_token();
1176 self.diagnostics.push(TokenDiagnostic::new(
1177 span,
1178 "Invalid error code".to_string(),
1179 RecoveryAction::InvalidErrorLiteral,
1180 ));
1181 }
1182 }
1183 }
1184
1185 #[expect(
1187 clippy::arithmetic_side_effects,
1188 reason = "cursor bounded by slice length: offset only advances past bytes read with get"
1189 )]
1190 fn parse_string(&mut self, delim: u8) -> Result<(), SpanTokenizerError> {
1191 debug_assert!(delim == b'"' || delim == b'\'');
1192 debug_assert_eq!(self.current_byte(), Some(delim));
1193
1194 let is_dollar_ref = delim == b'\'' && self.token_str() == "$";
1195
1196 let glue_to_token = delim == b'\'' && self.token_str().ends_with(':');
1201
1202 if !is_dollar_ref && !glue_to_token && self.has_token() {
1203 self.save_token();
1204 self.start_token();
1205 }
1206
1207 let string_start = if is_dollar_ref {
1208 self.token_start
1209 } else {
1210 self.offset
1211 };
1212 self.offset += 1;
1213
1214 while let Some(b) = self.current_byte() {
1215 if b == delim {
1216 self.offset += 1;
1217 if self.current_byte() == Some(delim) {
1218 self.offset += 1;
1219 } else {
1220 if delim == b'"' {
1221 let subtype = operand_subtype(self.source_str(string_start, self.offset));
1222 self.push_span(TokenType::Operand, subtype, string_start, self.offset);
1223 self.start_token();
1224 } else {
1225 self.token_end = self.offset;
1226 }
1227 return Ok(());
1228 }
1229 } else {
1230 self.offset += 1;
1231 }
1232 }
1233
1234 Err(SpanTokenizerError {
1235 kind: SpanTokenizerErrorKind::UnterminatedString,
1236 pos: self.offset,
1237 message: "Reached end of formula while parsing string".to_string(),
1238 span_start: Some(string_start),
1239 span_end: Some(self.formula.len()),
1240 })
1241 }
1242
1243 #[expect(
1244 clippy::arithmetic_side_effects,
1245 reason = "cursor bounded by slice length: offset advances by 2 only when offset + 1 < len; open_count counts unclosed `[` and is decremented only while positive"
1246 )]
1247 fn parse_brackets(&mut self) -> Result<(), SpanTokenizerError> {
1248 debug_assert_eq!(self.current_byte(), Some(b'['));
1249
1250 if !self.has_token() {
1251 self.start_token();
1252 }
1253
1254 let bracket_start = self.offset;
1255 let mut open_count: usize = 1;
1256 self.offset += 1;
1257
1258 while let Some(b) = self.current_byte() {
1259 match b {
1260 b'\'' => {
1265 if self.offset + 1 < self.formula.len() {
1266 self.offset += 2;
1267 continue;
1268 }
1269 self.offset += 1;
1272 continue;
1273 }
1274 b'[' => open_count += 1,
1275 b']' => {
1276 open_count -= 1;
1277 if open_count == 0 {
1278 self.offset += 1;
1279 self.extend_token();
1280 return Ok(());
1281 }
1282 }
1283 _ => {}
1284 }
1285 self.offset += 1;
1286 }
1287
1288 Err(SpanTokenizerError {
1289 kind: SpanTokenizerErrorKind::UnmatchedBracket,
1290 pos: self.offset,
1291 message: "Encountered unmatched '['".to_string(),
1292 span_start: Some(bracket_start),
1293 span_end: Some(self.formula.len()),
1294 })
1295 }
1296
1297 #[expect(
1298 clippy::arithmetic_side_effects,
1299 reason = "cursor bounded by slice length: offset advances past an error code matched inside the formula; end scans under get"
1300 )]
1301 fn parse_error(&mut self) -> Result<(), SpanTokenizerError> {
1302 let has_sheet_prefix = self.token_str().ends_with('!');
1308 if has_sheet_prefix {
1309 if self.token_str().len() <= 1 {
1310 return Err(SpanTokenizerError {
1311 kind: SpanTokenizerErrorKind::InvalidErrorLiteral,
1312 pos: self.offset,
1313 message: format!(
1314 "Empty sheet qualifier before error literal at position {}",
1315 self.offset
1316 ),
1317 span_start: Some(self.token_start),
1318 span_end: Some(self.offset),
1319 });
1320 }
1321 self.start_token();
1323 } else if self.has_token() {
1324 self.save_token();
1325 self.start_token();
1326 }
1327
1328 if ref_error_starts_at(self.formula, self.offset)
1333 && ref_error_starts_at(self.formula, self.offset + 5)
1334 {
1335 self.offset += 5;
1336 }
1337
1338 let error_start = self.offset;
1339
1340 for &err_code in ERROR_CODES {
1341 let err_bytes = err_code.as_bytes();
1342 if self
1343 .formula
1344 .as_bytes()
1345 .get(self.offset..)
1346 .and_then(|rest| rest.get(..err_bytes.len()))
1347 .is_some_and(|slice| slice.eq_ignore_ascii_case(err_bytes))
1348 {
1349 self.push_span(
1350 TokenType::Operand,
1351 TokenSubType::Error,
1352 error_start,
1353 self.offset + err_bytes.len(),
1354 );
1355 self.offset += err_bytes.len();
1356 self.start_token();
1357 return Ok(());
1358 }
1359 }
1360
1361 let mut end = self.offset + 1;
1362 while let Some(&ch) = self.formula.as_bytes().get(end) {
1363 if is_token_ender(ch)
1364 || ch == b' '
1365 || ch == b'\t'
1366 || ch == b'\r'
1367 || ch == b'\n'
1368 || ch == b'('
1369 || ch == b'{'
1370 || ch == b'['
1371 || ch == b'"'
1372 || ch == b'\''
1373 {
1374 break;
1375 }
1376 end += 1;
1377 }
1378
1379 Err(SpanTokenizerError {
1380 kind: SpanTokenizerErrorKind::InvalidErrorLiteral,
1381 pos: self.offset,
1382 message: format!("Invalid error code at position {}", self.offset),
1383 span_start: Some(error_start),
1384 span_end: Some(end),
1385 })
1386 }
1387
1388 #[expect(
1389 clippy::arithmetic_side_effects,
1390 reason = "cursor bounded by slice length"
1391 )]
1392 fn parse_whitespace(&mut self) -> Result<(), SpanTokenizerError> {
1393 self.save_token();
1394
1395 let ws_start = self.offset;
1396 let mut contains_intersection_space = false;
1397 while let Some(b) = self.current_byte() {
1398 match b {
1399 b' ' => {
1400 contains_intersection_space = true;
1401 self.offset += 1;
1402 }
1403 b'\t' | b'\r' | b'\n' => self.offset += 1,
1404 _ => break,
1405 }
1406 }
1407
1408 let token_type = if contains_intersection_space
1411 && self.prev_is_reference_producing()
1412 && next_starts_reference_expression(self.formula, self.offset)
1413 {
1414 TokenType::OpInfix
1415 } else {
1416 TokenType::Whitespace
1417 };
1418 self.push_span(token_type, TokenSubType::None, ws_start, self.offset);
1419 self.start_token();
1420 Ok(())
1421 }
1422
1423 fn prev_is_reference_producing(&self) -> bool {
1424 match self.prev_non_whitespace() {
1425 Some(prev) => match prev.token_type {
1426 TokenType::OpPostfix => true,
1427 TokenType::Paren | TokenType::Func | TokenType::Array
1428 if prev.subtype == TokenSubType::Close =>
1429 {
1430 true
1431 }
1432 TokenType::Operand if prev.subtype == TokenSubType::Range => self
1433 .formula
1434 .get(prev.start..prev.end)
1435 .is_some_and(is_reference_operand_value),
1436 _ => false,
1437 },
1438 None => false,
1439 }
1440 }
1441
1442 #[expect(
1443 clippy::arithmetic_side_effects,
1444 reason = "cursor bounded by slice length: offset is the index of the `:` being dispatched, < len"
1445 )]
1446 fn should_emit_colon_infix(&self) -> bool {
1447 if self.has_token() {
1448 let value = self.token_str();
1449 if value.ends_with('!') {
1450 return false;
1451 }
1452 return reference_value_contains_range_colon(value)
1453 || value_has_structured_reference_bracket(value)
1454 || ref_error_starts_at(self.formula, self.offset + 1)
1457 || (value.contains('!')
1458 && next_reference_has_sheet_qualifier(self.formula, self.offset + 1));
1459 }
1460 self.prev_is_reference_producing()
1461 || self.prev_non_whitespace().is_some_and(|prev| {
1462 prev.subtype == TokenSubType::Error && ref_error_starts_at(self.formula, prev.start)
1463 })
1464 }
1465
1466 fn emit_infix_operator(&mut self, start: usize, end: usize) {
1467 self.save_token();
1468 self.start_token();
1469 self.push_span(TokenType::OpInfix, TokenSubType::None, start, end);
1470 self.offset = end;
1471 self.start_token();
1472 }
1473
1474 fn prev_non_whitespace(&self) -> Option<&TokenSpan> {
1475 self.spans
1476 .iter()
1477 .rev()
1478 .find(|t| t.token_type != TokenType::Whitespace)
1479 }
1480
1481 fn should_emit_hash_postfix(&self) -> bool {
1486 if self.has_token() {
1487 let value = self.token_str();
1488 if value.ends_with('!') {
1492 return false;
1493 }
1494 return operand_subtype(value) == TokenSubType::Range;
1495 }
1496 match self.prev_non_whitespace() {
1497 Some(prev) => match prev.token_type {
1498 TokenType::OpPostfix => true,
1499 TokenType::Paren | TokenType::Func | TokenType::Array
1500 if prev.subtype == TokenSubType::Close =>
1501 {
1502 true
1503 }
1504 TokenType::Operand if prev.subtype == TokenSubType::Range => true,
1505 _ => false,
1506 },
1507 None => false,
1508 }
1509 }
1510
1511 #[expect(
1512 clippy::arithmetic_side_effects,
1513 reason = "cursor bounded by slice length: offset is the index of the `#` being dispatched, < len"
1514 )]
1515 fn emit_hash_postfix(&mut self) {
1516 self.save_token();
1517 self.start_token();
1518 self.push_span(
1519 TokenType::OpPostfix,
1520 TokenSubType::None,
1521 self.offset,
1522 self.offset + 1,
1523 );
1524 self.offset += 1;
1525 self.start_token();
1526 }
1527
1528 #[expect(
1530 clippy::arithmetic_side_effects,
1531 reason = "cursor bounded by slice length: offset < len while dispatching, and the two-byte operators were read with get"
1532 )]
1533 fn parse_operator(&mut self, curr_byte: u8) -> Result<(), SpanTokenizerError> {
1534 debug_assert_eq!(self.current_byte(), Some(curr_byte));
1535 self.save_token();
1536
1537 if let Some(b">=" | b"<=" | b"<>") = self
1538 .formula
1539 .as_bytes()
1540 .get(self.offset..)
1541 .and_then(|rest| rest.get(..2))
1542 {
1543 self.push_span(
1544 TokenType::OpInfix,
1545 TokenSubType::None,
1546 self.offset,
1547 self.offset + 2,
1548 );
1549 self.offset += 2;
1550 self.start_token();
1551 return Ok(());
1552 }
1553
1554 let token_type = match curr_byte {
1555 b'@' => TokenType::OpPrefix,
1556 b'%' => TokenType::OpPostfix,
1557 b'+' | b'-' => {
1558 if self.spans.is_empty() {
1559 TokenType::OpPrefix
1560 } else {
1561 let prev = self.prev_non_whitespace();
1562 if let Some(p) = prev {
1563 if p.subtype == TokenSubType::Close
1564 || p.token_type == TokenType::OpPostfix
1565 || p.token_type == TokenType::Operand
1566 {
1567 TokenType::OpInfix
1568 } else {
1569 TokenType::OpPrefix
1570 }
1571 } else {
1572 TokenType::OpPrefix
1573 }
1574 }
1575 }
1576 _ => TokenType::OpInfix,
1577 };
1578
1579 self.push_span(token_type, TokenSubType::None, self.offset, self.offset + 1);
1580 self.offset += 1;
1581 self.start_token();
1582 Ok(())
1583 }
1584
1585 #[expect(
1587 clippy::arithmetic_side_effects,
1588 reason = "cursor bounded by slice length: offset < len while dispatching; colon is the index of a `:` inside the token"
1589 )]
1590 fn parse_opener(&mut self, curr_byte: u8) -> Result<(), SpanTokenizerError> {
1591 debug_assert!(curr_byte == b'(' || curr_byte == b'{');
1592 debug_assert_eq!(self.current_byte(), Some(curr_byte));
1593
1594 let token = if curr_byte == b'{' {
1595 self.save_token();
1596 TokenSpan {
1597 token_type: TokenType::Array,
1598 subtype: TokenSubType::Open,
1599 start: self.offset,
1600 end: self.offset + 1,
1601 }
1602 } else if self.has_token() {
1603 if let Some(colon) = range_colon_before_call(self.token_str()) {
1604 let colon = self.token_start + colon;
1605 let subtype = operand_subtype(self.source_str(self.token_start, colon));
1606 self.push_span(TokenType::Operand, subtype, self.token_start, colon);
1607 self.push_span(TokenType::OpInfix, TokenSubType::None, colon, colon + 1);
1608 self.token_start = colon + 1;
1609 }
1610 let token = TokenSpan {
1611 token_type: TokenType::Func,
1612 subtype: TokenSubType::Open,
1613 start: self.token_start,
1614 end: self.offset + 1,
1615 };
1616 self.token_start = self.offset + 1;
1617 self.token_end = self.offset + 1;
1618 token
1619 } else {
1620 TokenSpan {
1621 token_type: TokenType::Paren,
1622 subtype: TokenSubType::Open,
1623 start: self.offset,
1624 end: self.offset + 1,
1625 }
1626 };
1627
1628 if self.spans.len() >= self.token_limit {
1629 self.exceeded = true;
1630 return Ok(());
1631 }
1632 self.spans.push(token);
1633 self.token_stack.push(token);
1634 self.offset += 1;
1635 self.start_token();
1636 Ok(())
1637 }
1638
1639 #[expect(
1641 clippy::arithmetic_side_effects,
1642 reason = "cursor bounded by slice length: offset < len while dispatching"
1643 )]
1644 fn parse_closer(&mut self, curr_byte: u8) -> Result<(), SpanTokenizerError> {
1645 debug_assert!(curr_byte == b')' || curr_byte == b'}');
1646 debug_assert_eq!(self.current_byte(), Some(curr_byte));
1647 self.save_token();
1648
1649 if let Some(open_token) = self.token_stack.last().copied() {
1650 let expected = if open_token.token_type == TokenType::Array {
1651 b'}'
1652 } else {
1653 b')'
1654 };
1655 if curr_byte != expected {
1656 return Err(SpanTokenizerError {
1657 kind: SpanTokenizerErrorKind::MismatchedPair,
1658 pos: self.offset,
1659 message: "Mismatched ( and { pair".to_string(),
1660 span_start: Some(self.offset),
1661 span_end: Some(self.offset + 1),
1662 });
1663 }
1664
1665 self.token_stack.pop();
1666 self.push_span(
1667 open_token.token_type,
1668 TokenSubType::Close,
1669 self.offset,
1670 self.offset + 1,
1671 );
1672 } else {
1673 return Err(SpanTokenizerError {
1674 kind: SpanTokenizerErrorKind::NoMatchingOpener,
1675 pos: self.offset,
1676 message: format!("No matching opener for closer at position {}", self.offset),
1677 span_start: Some(self.offset),
1678 span_end: Some(self.offset + 1),
1679 });
1680 }
1681
1682 self.offset += 1;
1683 self.start_token();
1684 Ok(())
1685 }
1686
1687 #[expect(
1689 clippy::arithmetic_side_effects,
1690 reason = "cursor bounded by slice length: offset < len while dispatching"
1691 )]
1692 fn parse_separator(&mut self, curr_byte: u8) -> Result<(), SpanTokenizerError> {
1693 debug_assert!(curr_byte == b';' || curr_byte == b',');
1694 debug_assert_eq!(self.current_byte(), Some(curr_byte));
1695 self.save_token();
1696
1697 let top_token = self.token_stack.last();
1698 let in_function_or_array = matches!(
1699 top_token.map(|t| t.token_type),
1700 Some(TokenType::Func | TokenType::Array)
1701 );
1702 let in_array = matches!(top_token.map(|t| t.token_type), Some(TokenType::Array));
1703
1704 let (token_type, subtype) = match curr_byte {
1705 b',' => {
1706 if in_function_or_array {
1707 (TokenType::Sep, TokenSubType::Arg)
1708 } else {
1709 (TokenType::OpInfix, TokenSubType::None)
1710 }
1711 }
1712 b';' => {
1713 if in_array {
1714 (TokenType::Sep, TokenSubType::Row)
1715 } else if self.dialect == FormulaDialect::OpenFormula && in_function_or_array {
1716 (TokenType::Sep, TokenSubType::Arg)
1717 } else if self.dialect == FormulaDialect::OpenFormula {
1718 (TokenType::OpInfix, TokenSubType::None)
1719 } else {
1720 (TokenType::Sep, TokenSubType::Row)
1721 }
1722 }
1723 _ => (TokenType::OpInfix, TokenSubType::None),
1724 };
1725
1726 self.push_span(token_type, subtype, self.offset, self.offset + 1);
1727 self.offset += 1;
1728 self.start_token();
1729 Ok(())
1730 }
1731}
1732
1733pub struct Tokenizer {
1738 pub items: Vec<Token>,
1739 admission_error: Option<TokenizerError>,
1740 dialect: FormulaDialect,
1741}
1742
1743impl Tokenizer {
1744 pub fn new(formula: &str) -> Result<Self, TokenizerError> {
1746 Self::new_with_dialect(formula, FormulaDialect::Excel)
1747 }
1748
1749 pub fn new_best_effort(formula: &str) -> Self {
1753 Self::new_best_effort_with_dialect(formula, FormulaDialect::Excel)
1754 }
1755
1756 pub fn new_best_effort_with_dialect(formula: &str, dialect: FormulaDialect) -> Self {
1758 let stream = TokenStream::new_best_effort_with_dialect(formula, dialect);
1759 Self::from_token_stream(&stream)
1760 }
1761
1762 pub fn new_with_dialect(
1764 formula: &str,
1765 dialect: FormulaDialect,
1766 ) -> Result<Self, TokenizerError> {
1767 let spans = tokenize_spans_with_dialect(formula, dialect)?;
1768 Ok(Tokenizer {
1769 items: tokens_from_spans(formula, &spans),
1770 admission_error: None,
1771 dialect,
1772 })
1773 }
1774
1775 pub fn from_token_stream(stream: &TokenStream) -> Self {
1776 let admission_error = stream.admission_error(crate::ParserLimits::default());
1777 let items = if admission_error.is_some() {
1778 Vec::new()
1779 } else {
1780 stream.to_tokens()
1781 };
1782 Tokenizer {
1783 items,
1784 admission_error,
1785 dialect: stream.dialect,
1786 }
1787 }
1788
1789 pub fn admission_error(&self) -> Option<&TokenizerError> {
1791 self.admission_error.as_ref()
1792 }
1793
1794 pub fn render(&self) -> String {
1796 match self.items.first() {
1797 None => String::new(),
1798 Some(first) if first.token_type == TokenType::Literal => first.value.clone(),
1799 Some(_) => {
1800 let mut out = String::from("=");
1801 for token in &self.items {
1802 out.push_str(&token.value);
1803 }
1804 out
1805 }
1806 }
1807 }
1808
1809 pub fn dialect(&self) -> FormulaDialect {
1811 self.dialect
1812 }
1813}
1814
1815impl TryFrom<&str> for Tokenizer {
1816 type Error = TokenizerError;
1817
1818 fn try_from(value: &str) -> Result<Self, Self::Error> {
1819 Tokenizer::new(value)
1820 }
1821}
1822
1823impl TryFrom<String> for Tokenizer {
1824 type Error = TokenizerError;
1825
1826 fn try_from(value: String) -> Result<Self, Self::Error> {
1827 Tokenizer::new(&value)
1828 }
1829}