Skip to main content

m2_syn/lex/
lexer.rs

1//! Native source-to-token conversion.
2//!
3//! This module owns byte classification, maximal munch, delimiter balancing,
4//! and top-level cell splitting. Its output uses only the shared raw atoms from
5//! `token_stream`; it never constructs a second lexer-specific token model.
6//! Every lexical atom slices one shared, validated source allocation through
7//! its span; token nodes never retain copied spellings.
8
9use std::error::Error;
10use std::fmt::{Display, Formatter, Result as FmtResult};
11use std::sync::Arc;
12
13use crate::{
14    Delimiter, DelimiterKind, DoubleSpan, EocToken, GENERATED_OPERATOR_SPELLINGS,
15    GENERATED_POSTFIX_OPERATOR_SPELLINGS, GENERATED_PUNCTUATION_SPELLINGS,
16    GENERATED_WORD_TOKEN_SPELLINGS, IdentToken, LiteralKind, LiteralToken, Offset, Punct,
17    Recovered, Span, Spanned, TokenGroup, TokenStream, TokenTree, Trivia, TriviaKind,
18};
19
20const MAX_GROUP_DEPTH: usize = 256;
21
22/// Collapses an explicitly `Core$`-qualified identifier back to its bare
23/// spelling when that spelling is one of the hardcoded word-token aliases.
24pub(crate) fn canonical_word_spelling(spelling: &str) -> &str {
25    let Some(word) = spelling.strip_prefix("Core$") else {
26        return spelling;
27    };
28    if GENERATED_WORD_TOKEN_SPELLINGS.contains(&word) {
29        word
30    } else {
31        spelling
32    }
33}
34
35#[doc(hidden)]
36pub fn __qualified_identifier_spans(spelling: &str, span: &Span) -> (Option<Span>, Option<Span>) {
37    let components = spelling.match_indices('$').collect::<Vec<_>>();
38    let subspan = |start: usize, end: usize| span.subspan(start, end);
39    match components.as_slice() {
40        [(separator, _)] => {
41            let left = &spelling[..*separator];
42            let right_start = separator + 1;
43            let right = &spelling[right_start..];
44            if is_bare_symbol_name(left) && is_bare_symbol_name(right) {
45                (Some(subspan(0, *separator)), None)
46            } else if is_bare_symbol_name(left) && is_decimal_index(right) {
47                (None, Some(subspan(right_start, spelling.len())))
48            } else {
49                (None, None)
50            }
51        }
52        [(package_separator, _), (index_separator, _)] => {
53            let package = &spelling[..*package_separator];
54            let name_start = package_separator + 1;
55            let name = &spelling[name_start..*index_separator];
56            let index_start = index_separator + 1;
57            let index = &spelling[index_start..];
58            if is_bare_symbol_name(package) && is_bare_symbol_name(name) && is_decimal_index(index)
59            {
60                (
61                    Some(subspan(0, *package_separator)),
62                    Some(subspan(index_start, spelling.len())),
63                )
64            } else {
65                (None, None)
66            }
67        }
68        _ => (None, None),
69    }
70}
71
72fn is_bare_symbol_name(spelling: &str) -> bool {
73    let Some(first) = spelling.as_bytes().first().copied() else {
74        return false;
75    };
76    is_identifier_start(first)
77        && spelling
78            .as_bytes()
79            .iter()
80            .copied()
81            .all(|byte| byte != b'$' && is_identifier_continue(byte))
82}
83
84fn is_decimal_index(spelling: &str) -> bool {
85    !spelling.is_empty() && spelling.bytes().all(|byte| byte.is_ascii_digit())
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
89/// The class of a tokenization failure.
90pub enum LexErrorKind {
91    /// A byte cannot begin any M2 token.
92    InvalidCharacter,
93    /// A string contains an invalid escape sequence.
94    InvalidEscape,
95    /// A numeric spelling is malformed.
96    InvalidNumber,
97    /// The byte input is not valid UTF-8.
98    InvalidUtf8,
99    /// Delimiter nesting exceeded the defensive parser limit.
100    NestingLimitExceeded,
101    /// A closing delimiter has no matching opener.
102    UnexpectedClosingDelimiter,
103    /// Input ended before a delimited group closed.
104    UnterminatedGroup,
105    /// Input ended inside a string literal.
106    UnterminatedString,
107    /// Input ended inside a raw string literal.
108    UnterminatedRawString,
109    /// Input ended inside a block comment.
110    UnterminatedBlockComment,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Hash, Spanned)]
114/// A tokenization failure and the source span that caused it.
115pub struct LexError {
116    kind: LexErrorKind,
117    delimiter: Option<DelimiterKind>,
118    span: Span,
119}
120
121impl LexError {
122    /// Returns the error class.
123    pub fn kind(&self) -> LexErrorKind {
124        self.kind
125    }
126
127    /// Returns the unclosed expression family for a missing group closer.
128    pub fn unclosed_expression(&self) -> Option<&'static str> {
129        self.delimiter.map(DelimiterKind::expression_name)
130    }
131
132    /// Returns the token spelling required to complete this error.
133    pub fn expected(&self) -> Option<&'static str> {
134        self.delimiter.map(DelimiterKind::closing)
135    }
136
137    /// Returns a readable description of what terminated this error.
138    pub fn found(&self) -> Option<&'static str> {
139        self.delimiter.map(|_| "end of input")
140    }
141}
142
143impl Display for LexError {
144    fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
145        let message = match self.kind {
146            LexErrorKind::InvalidCharacter => "invalid character",
147            LexErrorKind::InvalidEscape => "invalid string escape",
148            LexErrorKind::InvalidNumber => "invalid number literal",
149            LexErrorKind::InvalidUtf8 => "source is not valid UTF-8",
150            LexErrorKind::NestingLimitExceeded => "delimiter nesting limit exceeded",
151            LexErrorKind::UnexpectedClosingDelimiter => "unexpected closing delimiter",
152            LexErrorKind::UnterminatedGroup => {
153                if let Some(delimiter) = self.delimiter {
154                    return write!(
155                        formatter,
156                        "unclosed {}: expected `{}`, found end of input",
157                        delimiter.expression_name(),
158                        delimiter.closing(),
159                    );
160                }
161                "unterminated delimited group"
162            }
163            LexErrorKind::UnterminatedString => "unterminated string literal",
164            LexErrorKind::UnterminatedRawString => "unterminated raw string literal",
165            LexErrorKind::UnterminatedBlockComment => "unterminated block comment",
166        };
167        formatter.write_str(message)
168    }
169}
170
171impl Error for LexError {}
172
173/// Lexes bytes into one source-spanned stream of token trees.
174///
175/// Malformed source is always reported as [`LexError`]; byte contents do not
176/// cause this function to panic. A user-provided iterator may still panic from
177/// its own [`Iterator::next`] implementation.
178///
179/// # Errors
180///
181/// Returns [`LexError`] for malformed UTF-8, literals, comments, delimiters,
182/// or characters that Macaulay2 cannot tokenize.
183pub fn lex<I>(bytes: I) -> Result<TokenStream, LexError>
184where
185    I: IntoIterator<Item = u8>,
186{
187    lexer_from_bytes(bytes)?.lex(true)
188}
189
190/// Lexes bytes while retaining groups whose closing delimiter is missing.
191///
192/// Every missing closer becomes a zero-width closing boundary at end of input.
193/// The corresponding [`LexErrorKind::UnterminatedGroup`] diagnostic spans from
194/// that group's opening delimiter through end of input. Other lexical failures
195/// remain fatal.
196///
197/// # Errors
198///
199/// Returns [`LexError`] for malformed UTF-8, literals, comments, unmatched
200/// closing delimiters, invalid characters, or excessive delimiter nesting.
201pub fn lex_recovering<I>(bytes: I) -> Result<Recovered<TokenStream, LexError>, LexError>
202where
203    I: IntoIterator<Item = u8>,
204{
205    lexer_from_bytes(bytes)?.lex_recovering(true)
206}
207
208/// Lexes M2 source text into its source-spanned token tree.
209///
210/// # Errors
211///
212/// Returns [`LexError`] when the source contains malformed lexical syntax.
213pub fn lex_str(source: &str) -> Result<TokenStream, LexError> {
214    NativeLexer::new(Arc::<str>::from(source)).lex(true)
215}
216
217/// Lexes source text while retaining the interior of unclosed groups.
218///
219/// # Errors
220///
221/// Returns [`LexError`] for malformed lexical syntax other than missing group
222/// closers, which are returned in [`Recovered::diagnostics`].
223pub fn lex_str_recovering(source: &str) -> Result<Recovered<TokenStream, LexError>, LexError> {
224    NativeLexer::new(Arc::<str>::from(source)).lex_recovering(true)
225}
226
227/// Lexes one statically spelled fragment emitted by [`crate::quote_m2!`].
228///
229/// Procedural-macro input arrives as Rust token trees, so the macro first joins
230/// each uninterrupted static run into M2 source text. Sending that text through
231/// the real lexer keeps quoted literals, identifier rules, and maximal
232/// punctuation exactly aligned with ordinary source parsing.
233///
234/// # Panics
235///
236/// Panics when a static fragment accepted by Rust's tokenization is not valid
237/// M2 lexical syntax. Interpolate a pre-built [`TokenStream`] when its source
238/// spelling cannot be represented directly in Rust macro input.
239#[doc(hidden)]
240pub fn __quote_fragment(source: &str) -> TokenStream {
241    NativeLexer::new(Arc::<str>::from(source))
242        .lex(false)
243        .unwrap_or_else(|error| panic!("invalid static M2 quote fragment: {error}"))
244}
245
246fn lexer_from_bytes<I>(bytes: I) -> Result<NativeLexer, LexError>
247where
248    I: IntoIterator<Item = u8>,
249{
250    let bytes = bytes.into_iter().collect::<Vec<_>>();
251    let len = bytes.len();
252    let source = String::from_utf8(bytes).map_err(|_| LexError {
253        kind: LexErrorKind::InvalidUtf8,
254        delimiter: None,
255        span: Span::new(0, len),
256    })?;
257    Ok(NativeLexer::new(Arc::<str>::from(source)))
258}
259
260struct NativeLexer {
261    source: Arc<str>,
262    source_span: Span,
263    offset: Offset,
264}
265
266impl NativeLexer {
267    fn new(source: Arc<str>) -> Self {
268        let source_span = Span::source(source.clone());
269        Self {
270            source,
271            source_span,
272            offset: 0,
273        }
274    }
275
276    fn lex(self, terminate_final_cell: bool) -> Result<TokenStream, LexError> {
277        let recovered = self.lex_recovering(terminate_final_cell)?;
278        match recovered.diagnostics.into_iter().next() {
279            Some(error) => Err(error),
280            None => Ok(recovered.value),
281        }
282    }
283
284    fn lex_recovering(
285        mut self,
286        terminate_final_cell: bool,
287    ) -> Result<Recovered<TokenStream, LexError>, LexError> {
288        let mut output = TokenStream::new();
289        let mut groups = Vec::<OpenGroup>::new();
290        // A quote specifier takes the following token literally, so that token
291        // is not interpreted as structure. This is the one grammar fact the
292        // lexer needs: without it `symbol (` opens a group and `symbol ;` ends
293        // a cell, neither of which M2 does.
294        let mut quote_next = false;
295        loop {
296            let Some(first) = self.peek(0) else {
297                let mut diagnostics = Vec::with_capacity(groups.len());
298                while let Some(group) = groups.pop() {
299                    diagnostics.push(LexError {
300                        kind: LexErrorKind::UnterminatedGroup,
301                        delimiter: Some(group.kind),
302                        span: self.span_from(group.opening_start),
303                    });
304                    let contents = std::mem::replace(&mut output, group.parent);
305                    output.extend([TokenGroup::new(
306                        Delimiter::new(
307                            group.kind,
308                            DoubleSpan::unclosed(group.opening, self.offset),
309                        ),
310                        contents,
311                    )]);
312                }
313                if terminate_final_cell {
314                    terminate_cell(&mut output, self.offset, false);
315                }
316                return Ok(Recovered::new(output, diagnostics));
317            };
318
319            if first == b'\n' || (first == b'\r' && self.byte_after_current() == Some(b'\n')) {
320                self.lex_line_break(&mut output)?;
321                if groups.is_empty()
322                    && current_cell_has_syntax(&output)
323                    && newline_ends_cell(&output)
324                {
325                    terminate_cell(&mut output, self.offset, false);
326                }
327            } else if first == b'\r' {
328                self.lex_carriage_return(&mut output)?;
329            } else if matches!(first, b' ' | b'\t') {
330                self.lex_whitespace(&mut output)?;
331            } else if let Some(width) = self.math_operator_width() {
332                self.lex_math_operator(width, &mut output)?;
333                quote_next = false;
334            } else if is_identifier_start(first) {
335                let quoted = std::mem::take(&mut quote_next);
336                self.lex_identifier(&mut output)?;
337                quote_next = !quoted && last_token_quotes_next(&output);
338            } else if first.is_ascii_digit()
339                || (first == b'.'
340                    && self
341                        .byte_after_current()
342                        .is_some_and(|byte| byte.is_ascii_digit()))
343            {
344                self.lex_number(&mut output)?;
345                quote_next = false;
346            } else if first == b'"' {
347                self.lex_string(&mut output)?;
348                quote_next = false;
349            } else if let Some(fixed) = self.match_fixed() {
350                let quoted = std::mem::take(&mut quote_next);
351                match fixed.kind {
352                    // A quoted bracket is an ordinary token, not a group
353                    // boundary; the parser reads it as the quoted symbol.
354                    FixedKind::Open(_) | FixedKind::Close(_) if quoted => {
355                        self.lex_punctuation(fixed.len, &mut output)?;
356                    }
357                    FixedKind::Punctuation => {
358                        self.lex_punctuation(fixed.len, &mut output)?;
359                        if !quoted && groups.is_empty() && first == b';' {
360                            terminate_cell(&mut output, self.offset, true);
361                        }
362                    }
363                    // A comment does not consume the pending quote: M2 reads
364                    // `symbol -- note⏎x` as the quoted `x`.
365                    FixedKind::LineComment => {
366                        self.lex_line_comment(fixed.len, &mut output)?;
367                        quote_next = quoted;
368                    }
369                    FixedKind::BlockComment => {
370                        self.lex_block_comment(fixed.len, &mut output)?;
371                        quote_next = quoted;
372                        if groups.is_empty()
373                            && output.last().is_some_and(
374                                |token| matches!(token, TokenTree::Trivia(trivia) if trivia.contains_line_break()),
375                            )
376                            && current_cell_has_syntax(&output)
377                            && newline_ends_cell(&output)
378                        {
379                            terminate_cell(&mut output, self.offset, false);
380                        }
381                    }
382                    FixedKind::RawString => {
383                        self.lex_raw_string(fixed.len, &mut output)?;
384                    }
385                    FixedKind::Open(kind) => {
386                        if groups.len() == MAX_GROUP_DEPTH {
387                            return Err(self.error(LexErrorKind::NestingLimitExceeded, self.offset));
388                        }
389                        let opening_start = self.offset;
390                        self.take_exact(fixed.len, opening_start)?;
391                        let opening = self.span_from(opening_start);
392                        groups.push(OpenGroup {
393                            kind,
394                            opening_start,
395                            opening,
396                            parent: std::mem::take(&mut output),
397                        });
398                    }
399                    FixedKind::Close(kind) => {
400                        let closing_start = self.offset;
401                        self.take_exact(fixed.len, closing_start)?;
402                        let closing = self.span_from(closing_start);
403                        let Some(group) = groups.pop() else {
404                            return Err(
405                                self.error(LexErrorKind::UnexpectedClosingDelimiter, closing_start)
406                            );
407                        };
408                        if group.kind != kind {
409                            return Err(
410                                self.error(LexErrorKind::UnexpectedClosingDelimiter, closing_start)
411                            );
412                        }
413                        let contents = std::mem::replace(&mut output, group.parent);
414                        output.extend([TokenGroup::new(
415                            Delimiter::new(kind, DoubleSpan::new(group.opening, closing)),
416                            contents,
417                        )]);
418                    }
419                }
420            } else {
421                let start = self.offset;
422                self.next();
423                return Err(self.error(LexErrorKind::InvalidCharacter, start));
424            }
425        }
426    }
427
428    fn lex_line_break(&mut self, output: &mut TokenStream) -> Result<(), LexError> {
429        let start = self.offset;
430        let first = self.next_or_error(LexErrorKind::InvalidCharacter, start)?;
431        let mut bytes = vec![first];
432        if first == b'\r' && self.peek(0) == Some(b'\n') {
433            bytes.push(self.next_or_error(LexErrorKind::InvalidCharacter, start)?);
434        }
435        let _ = bytes;
436        let span = self.span_from(start);
437        output.extend([TokenTree::Trivia(Trivia::from_span(
438            TriviaKind::Whitespace,
439            span,
440        ))]);
441        Ok(())
442    }
443
444    fn lex_carriage_return(&mut self, output: &mut TokenStream) -> Result<(), LexError> {
445        let start = self.offset;
446        let byte = self.next_or_error(LexErrorKind::InvalidCharacter, start)?;
447        let _ = byte;
448        output.extend([TokenTree::Trivia(Trivia::from_span(
449            TriviaKind::Whitespace,
450            self.span_from(start),
451        ))]);
452        Ok(())
453    }
454
455    fn lex_whitespace(&mut self, output: &mut TokenStream) -> Result<(), LexError> {
456        let start = self.offset;
457        let bytes = self.take_while(|byte| matches!(byte, b' ' | b'\t'));
458        let _ = bytes;
459        output.extend([TokenTree::Trivia(Trivia::from_span(
460            TriviaKind::Whitespace,
461            self.span_from(start),
462        ))]);
463        Ok(())
464    }
465
466    fn lex_identifier(&mut self, output: &mut TokenStream) -> Result<(), LexError> {
467        let start = self.offset;
468        let mut bytes = vec![self.next_or_error(LexErrorKind::InvalidUtf8, start)?];
469        loop {
470            let Some(byte) = self.peek(0) else {
471                break;
472            };
473            if !is_identifier_continue(byte) || self.math_operator_width().is_some() {
474                break;
475            }
476            bytes.push(self.next_or_error(LexErrorKind::InvalidUtf8, start)?);
477        }
478        let _ = bytes;
479        output.extend([TokenTree::Ident(IdentToken::from_span(
480            self.span_from(start),
481        ))]);
482        Ok(())
483    }
484
485    fn lex_number(&mut self, output: &mut TokenStream) -> Result<(), LexError> {
486        let start = self.offset;
487        let mut bytes = Vec::new();
488        let mut decimal = true;
489
490        if self.peek(0) == Some(b'0') {
491            bytes.push(self.next_or_error(LexErrorKind::InvalidNumber, start)?);
492            if let Some(radix) = self.number_radix() {
493                decimal = false;
494                bytes.push(self.next_or_error(LexErrorKind::InvalidNumber, start)?);
495                bytes.extend(self.take_while(|byte| radix.is_digit(byte)));
496            } else {
497                bytes.extend(self.take_while(|byte| byte.is_ascii_digit()));
498            }
499        } else {
500            bytes.extend(self.take_while(|byte| byte.is_ascii_digit()));
501        }
502
503        let next = self.peek(0);
504        let begins_float = decimal
505            && (next == Some(b'.') && self.byte_after_current() != Some(b'.')
506                || matches!(next, Some(b'p' | b'e' | b'E')));
507        let kind = if begins_float {
508            if self.peek(0) == Some(b'.') {
509                bytes.push(self.next_or_error(LexErrorKind::InvalidNumber, start)?);
510                bytes.extend(self.take_while(|byte| byte.is_ascii_digit()));
511            }
512            if self.peek(0) == Some(b'p') {
513                bytes.push(self.next_or_error(LexErrorKind::InvalidNumber, start)?);
514                if !self.peek(0).is_some_and(|byte| byte.is_ascii_digit()) {
515                    return Err(self.error(LexErrorKind::InvalidNumber, start));
516                }
517                bytes.extend(self.take_while(|byte| byte.is_ascii_digit()));
518            }
519            if matches!(self.peek(0), Some(b'e' | b'E')) {
520                bytes.push(self.next_or_error(LexErrorKind::InvalidNumber, start)?);
521                if matches!(self.peek(0), Some(b'+' | b'-')) {
522                    bytes.push(self.next_or_error(LexErrorKind::InvalidNumber, start)?);
523                }
524                if !self.peek(0).is_some_and(|byte| byte.is_ascii_digit()) {
525                    return Err(self.error(LexErrorKind::InvalidNumber, start));
526                }
527                bytes.extend(self.take_while(|byte| byte.is_ascii_digit()));
528            }
529            LiteralKind::Float
530        } else {
531            LiteralKind::Integer
532        };
533
534        let _ = bytes;
535        output.extend([TokenTree::Literal(LiteralToken::from_span(
536            kind,
537            self.span_from(start),
538        ))]);
539        Ok(())
540    }
541
542    fn number_radix(&mut self) -> Option<NumberRadix> {
543        let marker = self.peek(0)?;
544        let digit = self.peek(1);
545        match marker {
546            b'b' | b'B' if digit.is_some_and(|byte| NumberRadix::Binary.is_digit(byte)) => {
547                Some(NumberRadix::Binary)
548            }
549            b'o' | b'O' if digit.is_some_and(|byte| NumberRadix::Octal.is_digit(byte)) => {
550                Some(NumberRadix::Octal)
551            }
552            b'x' | b'X' if digit.is_some_and(|byte| NumberRadix::Hexadecimal.is_digit(byte)) => {
553                Some(NumberRadix::Hexadecimal)
554            }
555            _ => None,
556        }
557    }
558
559    fn lex_string(&mut self, output: &mut TokenStream) -> Result<(), LexError> {
560        let start = self.offset;
561        let mut bytes = vec![self.next_or_error(LexErrorKind::UnterminatedString, start)?];
562
563        loop {
564            match self.peek(0) {
565                None => return Err(self.error(LexErrorKind::UnterminatedString, start)),
566                Some(b'"') => {
567                    bytes.push(self.next_or_error(LexErrorKind::UnterminatedString, start)?);
568                    let _ = bytes;
569                    output.extend([TokenTree::Literal(LiteralToken::from_span(
570                        LiteralKind::String,
571                        self.span_from(start),
572                    ))]);
573                    return Ok(());
574                }
575                Some(b'\\') => {
576                    let escape_start = self.offset;
577                    bytes.push(self.next_or_error(LexErrorKind::UnterminatedString, start)?);
578                    let Some(escaped) = self.peek(0) else {
579                        return Err(self.error(LexErrorKind::UnterminatedString, start));
580                    };
581                    bytes.push(self.next_or_error(LexErrorKind::UnterminatedString, start)?);
582                    match escaped {
583                        b'a' | b'b' | b'e' | b'E' | b'f' | b'r' | b't' | b'v' | b'n' | b'"'
584                        | b'\\' => {}
585                        b'0'..=b'7' => {
586                            for _ in 0..2 {
587                                if !matches!(self.peek(0), Some(b'0'..=b'7')) {
588                                    break;
589                                }
590                                bytes.push(
591                                    self.next_or_error(LexErrorKind::InvalidEscape, escape_start)?,
592                                );
593                            }
594                        }
595                        b'x' => self.take_hex_escape(&mut bytes, 2, escape_start)?,
596                        b'u' => self.take_hex_escape(&mut bytes, 4, escape_start)?,
597                        _ => return Err(self.error(LexErrorKind::InvalidEscape, escape_start)),
598                    }
599                }
600                Some(_) => bytes.push(self.next_or_error(LexErrorKind::UnterminatedString, start)?),
601            }
602        }
603    }
604
605    fn take_hex_escape(
606        &mut self,
607        bytes: &mut Vec<u8>,
608        digits: usize,
609        start: Offset,
610    ) -> Result<(), LexError> {
611        for _ in 0..digits {
612            if !self.peek(0).is_some_and(|byte| byte.is_ascii_hexdigit()) {
613                return Err(self.error(LexErrorKind::InvalidEscape, start));
614            }
615            bytes.push(self.next_or_error(LexErrorKind::InvalidEscape, start)?);
616        }
617        Ok(())
618    }
619
620    fn lex_raw_string(
621        &mut self,
622        opener_len: usize,
623        output: &mut TokenStream,
624    ) -> Result<(), LexError> {
625        let start = self.offset;
626        let mut bytes = self.take_exact(opener_len, start)?;
627
628        loop {
629            if self.peek(0).is_none() {
630                return Err(self.error(LexErrorKind::UnterminatedRawString, start));
631            }
632            if self.peek(0) == Some(b'/') {
633                match self.slash_run_len() {
634                    3 => {
635                        bytes.extend(self.take_exact(3, start)?);
636                        let _ = bytes;
637                        output.extend([TokenTree::Literal(LiteralToken::from_span(
638                            LiteralKind::RawString,
639                            self.span_from(start),
640                        ))]);
641                        return Ok(());
642                    }
643                    0 => return Err(self.error(LexErrorKind::UnterminatedRawString, start)),
644                    1 => {
645                        bytes.push(self.next_or_error(LexErrorKind::UnterminatedRawString, start)?);
646                    }
647                    _ => bytes.extend(self.take_exact(2, start)?),
648                }
649            } else {
650                bytes.push(self.next_or_error(LexErrorKind::UnterminatedRawString, start)?);
651            }
652        }
653    }
654
655    fn lex_line_comment(
656        &mut self,
657        opener_len: usize,
658        output: &mut TokenStream,
659    ) -> Result<(), LexError> {
660        let start = self.offset;
661        let mut bytes = self.take_exact(opener_len, start)?;
662        loop {
663            match self.peek(0) {
664                None | Some(b'\n') => break,
665                Some(b'\r') if self.byte_after_current() == Some(b'\n') => break,
666                Some(_) => bytes.push(self.next_or_error(LexErrorKind::InvalidCharacter, start)?),
667            }
668        }
669        let _ = bytes;
670        output.extend([TokenTree::Trivia(Trivia::from_span(
671            TriviaKind::LineComment,
672            self.span_from(start),
673        ))]);
674        Ok(())
675    }
676
677    fn lex_block_comment(
678        &mut self,
679        opener_len: usize,
680        output: &mut TokenStream,
681    ) -> Result<(), LexError> {
682        let start = self.offset;
683        let mut bytes = self.take_exact(opener_len, start)?;
684        loop {
685            if self.peek(0).is_none() {
686                return Err(self.error(LexErrorKind::UnterminatedBlockComment, start));
687            }
688            if self.peek(0) == Some(b'*') && self.byte_after_current() == Some(b'-') {
689                bytes.extend(self.take_exact(2, start)?);
690                let _ = bytes;
691                output.extend([TokenTree::Trivia(Trivia::from_span(
692                    TriviaKind::BlockComment,
693                    self.span_from(start),
694                ))]);
695                return Ok(());
696            }
697            bytes.push(self.next_or_error(LexErrorKind::UnterminatedBlockComment, start)?);
698        }
699    }
700
701    fn match_fixed(&mut self) -> Option<FixedMatch> {
702        fixed_spellings()
703            .filter(|(spelling, _)| {
704                spelling
705                    .bytes()
706                    .enumerate()
707                    .all(|(distance, expected)| self.peek(distance) == Some(expected))
708            })
709            .max_by_key(|(spelling, _)| spelling.len())
710            .map(|(spelling, kind)| FixedMatch {
711                len: spelling.len(),
712                kind,
713            })
714    }
715
716    fn math_operator_width(&mut self) -> Option<usize> {
717        let first = self.peek(0)?;
718        if !matches!(first, 0xc2 | 0xc3 | 0xe2) {
719            return None;
720        }
721        self.peek(1)
722            .filter(|second| is_math_operator_pair(first, *second))
723            .and_then(|_| utf8_char_width(first))
724    }
725
726    fn lex_math_operator(
727        &mut self,
728        width: usize,
729        output: &mut TokenStream,
730    ) -> Result<(), LexError> {
731        let start = self.offset;
732        let mut bytes = self.take_exact(width, start)?;
733        if self.peek(0) == Some(b'=') {
734            bytes.push(self.next_or_error(LexErrorKind::InvalidUtf8, start)?);
735        }
736        self.emit_punctuation(bytes, start, output)
737    }
738
739    fn lex_punctuation(&mut self, len: usize, output: &mut TokenStream) -> Result<(), LexError> {
740        let start = self.offset;
741        let bytes = self.take_exact(len, start)?;
742        self.emit_punctuation(bytes, start, output)
743    }
744
745    fn emit_punctuation(
746        &self,
747        bytes: Vec<u8>,
748        start: Offset,
749        output: &mut TokenStream,
750    ) -> Result<(), LexError> {
751        let _ = bytes;
752        output.extend([TokenTree::Punct(Punct::from_span(self.span_from(start)))]);
753        Ok(())
754    }
755
756    fn byte_after_current(&mut self) -> Option<u8> {
757        self.peek(1)
758    }
759
760    fn slash_run_len(&mut self) -> usize {
761        let mut len = 0;
762        while len < 4 && self.peek(len) == Some(b'/') {
763            len += 1;
764        }
765        len
766    }
767
768    fn take_while(&mut self, predicate: impl Fn(u8) -> bool) -> Vec<u8> {
769        let mut bytes = Vec::new();
770        while self.peek(0).is_some_and(&predicate) {
771            let Some(byte) = self.next() else {
772                break;
773            };
774            bytes.push(byte);
775        }
776        bytes
777    }
778
779    fn take_exact(&mut self, len: usize, start: Offset) -> Result<Vec<u8>, LexError> {
780        let mut bytes = Vec::with_capacity(len);
781        for _ in 0..len {
782            let Some(byte) = self.next() else {
783                return Err(self.error(LexErrorKind::InvalidUtf8, start));
784            };
785            bytes.push(byte);
786        }
787        Ok(bytes)
788    }
789
790    fn next(&mut self) -> Option<u8> {
791        let byte = self.peek(0)?;
792        self.offset += 1;
793        Some(byte)
794    }
795
796    fn peek(&self, distance: usize) -> Option<u8> {
797        self.offset
798            .checked_add(distance)
799            .and_then(|offset| self.source.as_bytes().get(offset))
800            .copied()
801    }
802
803    fn next_or_error(&mut self, kind: LexErrorKind, start: Offset) -> Result<u8, LexError> {
804        match self.next() {
805            Some(byte) => Ok(byte),
806            None => Err(self.error(kind, start)),
807        }
808    }
809
810    fn error(&self, kind: LexErrorKind, start: Offset) -> LexError {
811        LexError {
812            kind,
813            delimiter: None,
814            span: self.span_from(start),
815        }
816    }
817
818    /// The span running from `start` to wherever the cursor has reached.
819    fn span_from(&self, start: Offset) -> Span {
820        self.source_span.subspan(start, self.offset)
821    }
822}
823
824#[derive(Debug, Clone, Copy, PartialEq, Eq)]
825enum RequiredClause {
826    Then,
827    LoopBody,
828    ExceptBody,
829}
830
831pub(crate) fn newline_ends_cell(stream: &TokenStream) -> bool {
832    let mut required_clauses = Vec::new();
833    let mut last_text = None;
834    let mut quote_next = false;
835
836    for tree in stream.iter() {
837        let text = match tree {
838            TokenTree::Eoc(_) => {
839                required_clauses.clear();
840                last_text = None;
841                quote_next = false;
842                None
843            }
844            TokenTree::Ident(token) => Some(token.text()),
845            TokenTree::Punct(token) => Some(token.text()),
846            TokenTree::Literal(_) | TokenTree::Group(_) => {
847                quote_next = false;
848                last_text = None;
849                None
850            }
851            TokenTree::Trivia(_) => None,
852        };
853
854        let Some(text) = text else {
855            continue;
856        };
857        if quote_next {
858            quote_next = false;
859            last_text = None;
860            continue;
861        }
862        let text = canonical_word_spelling(text);
863        last_text = Some(text);
864        if is_quote_specifier(text) {
865            quote_next = true;
866            continue;
867        }
868        match text {
869            "if" => required_clauses.push(RequiredClause::Then),
870            "for" | "while" => required_clauses.push(RequiredClause::LoopBody),
871            "except" => required_clauses.push(RequiredClause::ExceptBody),
872            "then" => {
873                remove_last_clause(&mut required_clauses, RequiredClause::Then);
874            }
875            "list" => {
876                remove_last_clause(&mut required_clauses, RequiredClause::LoopBody);
877            }
878            "do" => {
879                if !remove_last_clause(&mut required_clauses, RequiredClause::ExceptBody) {
880                    remove_last_clause(&mut required_clauses, RequiredClause::LoopBody);
881                }
882            }
883            _ => {}
884        }
885    }
886
887    if !required_clauses.is_empty() {
888        return false;
889    }
890
891    let Some(last) = last_text else {
892        return true;
893    };
894    if keyword_requires_value(last) {
895        return false;
896    }
897    !GENERATED_OPERATOR_SPELLINGS.contains(&last)
898        || GENERATED_POSTFIX_OPERATOR_SPELLINGS.contains(&last)
899}
900
901fn current_cell_has_syntax(stream: &TokenStream) -> bool {
902    stream
903        .iter()
904        .rev()
905        .take_while(|token| !matches!(token, TokenTree::Eoc(_)))
906        .any(|token| !matches!(token, TokenTree::Trivia(_)))
907}
908
909fn terminate_cell(stream: &mut TokenStream, offset: Offset, after_semicolon: bool) {
910    if current_cell_has_syntax(stream) {
911        let span = Span::from_offset(offset);
912        let marker = if after_semicolon {
913            EocToken::after_semicolon(span)
914        } else {
915            EocToken::new(span)
916        };
917        stream.extend([TokenTree::Eoc(marker)]);
918    }
919}
920
921/// One lexer-delimited top-level cell.
922pub(crate) struct RawCell {
923    pub(crate) stream: TokenStream,
924    pub(crate) semicolon: Option<Span>,
925    pub(crate) eoc: Span,
926}
927
928/// Partitions a token stream at the EOC markers inserted during lexing.
929pub(crate) fn split_cells(tokens: TokenStream) -> Vec<RawCell> {
930    let mut cells = Vec::new();
931    let mut current = TokenStream::new();
932
933    for token in tokens {
934        let TokenTree::Eoc(marker) = token else {
935            current.extend([token]);
936            continue;
937        };
938        let semicolon = marker.follows_semicolon().then(|| {
939            current
940                .pop()
941                .expect("the semicolon was just observed")
942                .span()
943        });
944        if current_cell_has_syntax(&current) || semicolon.is_some() {
945            cells.push(RawCell {
946                stream: std::mem::take(&mut current),
947                semicolon,
948                eoc: marker.span(),
949            });
950        }
951    }
952
953    if current_cell_has_syntax(&current) {
954        let eoc = current
955            .last()
956            .and_then(|token| token.span().end())
957            .map_or_else(Span::detached, Span::from_offset);
958        cells.push(RawCell {
959            stream: current,
960            semicolon: None,
961            eoc,
962        });
963    }
964    cells
965}
966
967fn remove_last_clause(clauses: &mut Vec<RequiredClause>, clause: RequiredClause) -> bool {
968    let Some(position) = clauses.iter().rposition(|candidate| *candidate == clause) else {
969        return false;
970    };
971    clauses.remove(position);
972    true
973}
974
975fn keyword_requires_value(keyword: &str) -> bool {
976    matches!(
977        keyword,
978        "if" | "then"
979            | "else"
980            | "for"
981            | "while"
982            | "in"
983            | "from"
984            | "to"
985            | "when"
986            | "list"
987            | "do"
988            | "new"
989            | "of"
990            | "try"
991            | "except"
992            | "catch"
993            | "throw"
994            | "trap"
995            | "shield"
996            | "TEST"
997            | "time"
998            | "timing"
999            | "breakpoint"
1000            | "elapsedTime"
1001            | "elapsedTiming"
1002            | "profile"
1003            | "symbol"
1004            | "local"
1005            | "global"
1006            | "threadVariable"
1007            | "threadLocal"
1008    )
1009}
1010
1011/// Whether the token just lexed quotes the one after it.
1012fn last_token_quotes_next(stream: &TokenStream) -> bool {
1013    matches!(stream.last(), Some(TokenTree::Ident(token))
1014        if is_quote_specifier(canonical_word_spelling(token.text())))
1015}
1016
1017pub(crate) fn is_quote_specifier(keyword: &str) -> bool {
1018    matches!(
1019        keyword,
1020        "symbol" | "local" | "global" | "threadVariable" | "threadLocal"
1021    )
1022}
1023
1024pub(crate) fn punctuation_would_merge(left: &str, right: &str) -> bool {
1025    let mut combined = String::with_capacity(left.len() + right.len());
1026    combined.push_str(left);
1027    combined.push_str(right);
1028    fixed_spellings()
1029        .any(|(spelling, _)| spelling.len() > left.len() && combined.starts_with(spelling))
1030        || (right.starts_with('=') && is_math_operator(left))
1031}
1032
1033#[derive(Debug)]
1034struct OpenGroup {
1035    kind: DelimiterKind,
1036    opening_start: Offset,
1037    opening: Span,
1038    parent: TokenStream,
1039}
1040
1041#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1042enum FixedKind {
1043    Punctuation,
1044    LineComment,
1045    BlockComment,
1046    RawString,
1047    Open(DelimiterKind),
1048    Close(DelimiterKind),
1049}
1050
1051const FIXED_SPELLINGS: &[(&str, FixedKind)] = &[
1052    ("///", FixedKind::RawString),
1053    ("--", FixedKind::LineComment),
1054    ("-*", FixedKind::BlockComment),
1055    ("(", FixedKind::Open(DelimiterKind::Parenthesis)),
1056    (")", FixedKind::Close(DelimiterKind::Parenthesis)),
1057    ("[", FixedKind::Open(DelimiterKind::Bracket)),
1058    ("]", FixedKind::Close(DelimiterKind::Bracket)),
1059    ("{", FixedKind::Open(DelimiterKind::Brace)),
1060    ("}", FixedKind::Close(DelimiterKind::Brace)),
1061    ("<|", FixedKind::Open(DelimiterKind::AngleBar)),
1062    ("|>", FixedKind::Close(DelimiterKind::AngleBar)),
1063];
1064
1065fn fixed_spellings() -> impl Iterator<Item = (&'static str, FixedKind)> {
1066    FIXED_SPELLINGS.iter().copied().chain(
1067        GENERATED_PUNCTUATION_SPELLINGS
1068            .iter()
1069            .map(|spelling| (*spelling, FixedKind::Punctuation)),
1070    )
1071}
1072
1073#[derive(Debug, Clone, PartialEq, Eq)]
1074struct FixedMatch {
1075    len: usize,
1076    kind: FixedKind,
1077}
1078
1079#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1080enum NumberRadix {
1081    Binary,
1082    Octal,
1083    Hexadecimal,
1084}
1085
1086impl NumberRadix {
1087    fn is_digit(self, byte: u8) -> bool {
1088        match self {
1089            Self::Binary => matches!(byte, b'0' | b'1'),
1090            Self::Octal => matches!(byte, b'0'..=b'7'),
1091            Self::Hexadecimal => byte.is_ascii_hexdigit(),
1092        }
1093    }
1094}
1095
1096fn is_identifier_start(byte: u8) -> bool {
1097    byte.is_ascii_alphabetic() || byte >= 128
1098}
1099
1100fn is_identifier_continue(byte: u8) -> bool {
1101    byte.is_ascii_alphanumeric() || matches!(byte, b'$' | b'\'') || byte >= 128
1102}
1103
1104fn is_math_operator_pair(first: u8, second: u8) -> bool {
1105    matches!(
1106        (first, second),
1107        (0xc2, 0xa0..=0xbf)
1108            | (0xc3, 0x97 | 0xb7)
1109            | (0xe2, 0x86..=0x8f | 0x9f | 0xa4..=0xaf)
1110    )
1111}
1112
1113fn is_math_operator(spelling: &str) -> bool {
1114    let bytes = spelling.as_bytes();
1115    bytes
1116        .first()
1117        .zip(bytes.get(1))
1118        .is_some_and(|(first, second)| {
1119            is_math_operator_pair(*first, *second) && utf8_char_width(*first) == Some(bytes.len())
1120        })
1121}
1122
1123fn utf8_char_width(first: u8) -> Option<usize> {
1124    match first {
1125        0x00..=0x7f => Some(1),
1126        0xc2..=0xdf => Some(2),
1127        0xe0..=0xef => Some(3),
1128        0xf0..=0xf4 => Some(4),
1129        _ => None,
1130    }
1131}
1132
1133#[cfg(test)]
1134mod tests {
1135    use super::*;
1136
1137    fn tokens(source: &str) -> TokenStream {
1138        let mut stream = lex_str(source).unwrap();
1139        assert_eq!(format!("{stream}"), source);
1140        if matches!(stream.last(), Some(TokenTree::Eoc(_))) {
1141            stream.pop();
1142        }
1143        stream
1144    }
1145
1146    #[test]
1147    fn lexer_accepts_an_arbitrary_non_peekable_byte_iterator() {
1148        let bytes = vec![b'a', b'+', b'1'].into_iter();
1149        let stream = lex(bytes).unwrap();
1150        assert_eq!(format!("{stream}"), "a+1");
1151    }
1152
1153    #[test]
1154    fn every_short_byte_sequence_returns_without_panicking() {
1155        fn check(bytes: &[u8]) {
1156            let outcome = std::panic::catch_unwind(|| lex(bytes.iter().copied()));
1157            assert!(outcome.is_ok(), "lexer panicked for bytes {bytes:?}");
1158        }
1159
1160        check(&[]);
1161        for first in u8::MIN..=u8::MAX {
1162            check(&[first]);
1163            for second in u8::MIN..=u8::MAX {
1164                check(&[first, second]);
1165            }
1166        }
1167    }
1168
1169    #[test]
1170    fn invalid_utf8_is_a_lex_error() {
1171        assert_eq!(
1172            lex([0xf0, 0x80, 0x80]).unwrap_err().kind(),
1173            LexErrorKind::InvalidUtf8
1174        );
1175    }
1176
1177    #[test]
1178    fn punctuation_is_returned_as_maximal_tokens() {
1179        let stream = tokens("<<|-1||>2");
1180        let punctuation = stream
1181            .iter()
1182            .filter_map(|tree| match tree {
1183                TokenTree::Punct(token) => Some(token.text()),
1184                _ => None,
1185            })
1186            .collect::<Vec<_>>();
1187        let integers = stream
1188            .iter()
1189            .filter_map(|tree| match tree {
1190                TokenTree::Literal(token) if token.kind == LiteralKind::Integer => {
1191                    Some(token.text())
1192                }
1193                _ => None,
1194            })
1195            .collect::<Vec<_>>();
1196        assert_eq!(punctuation, ["<<", "|-", "||", ">"]);
1197        assert_eq!(integers, ["1", "2"]);
1198    }
1199
1200    #[test]
1201    fn comment_openers_only_win_at_the_current_position() {
1202        let ordinary = tokens("|--1");
1203        assert!(
1204            ordinary
1205                .iter()
1206                .all(|tree| !matches!(tree, TokenTree::Trivia(_)))
1207        );
1208
1209        let comments = tokens("-- comment\n-* block *-");
1210        let trees = comments.iter().collect::<Vec<_>>();
1211        assert!(matches!(
1212            trees.as_slice(),
1213            [
1214                TokenTree::Trivia(line),
1215                TokenTree::Trivia(newline),
1216                TokenTree::Trivia(block),
1217            ] if line.kind() == TriviaKind::LineComment
1218                && newline.kind() == TriviaKind::Whitespace
1219                && newline.contains_line_break()
1220                && block.kind() == TriviaKind::BlockComment
1221        ));
1222    }
1223
1224    #[test]
1225    fn leading_operator_runs_split_at_maximal_known_spellings() {
1226        let stream = tokens("***1");
1227        let trees = stream.iter().collect::<Vec<_>>();
1228        assert!(matches!(
1229            trees.as_slice(),
1230            [
1231                first,
1232                second,
1233                TokenTree::Literal(integer),
1234            ] if first.spelling() == Some("**")
1235                && second.spelling() == Some("*")
1236                && integer.kind == LiteralKind::Integer
1237                && integer.text() == "1"
1238        ));
1239    }
1240
1241    #[test]
1242    fn identifiers_follow_the_m2_character_classes() {
1243        let qualified = tokens("Core$foo'12 1not not1");
1244        let trees = qualified.iter().collect::<Vec<_>>();
1245        assert!(matches!(
1246            trees.as_slice(),
1247            [
1248                TokenTree::Ident(qualified),
1249                TokenTree::Trivia(first_space),
1250                TokenTree::Literal(one),
1251                TokenTree::Ident(not),
1252                TokenTree::Trivia(second_space),
1253                TokenTree::Ident(not_one),
1254            ] if qualified.text() == "Core$foo'12"
1255                && first_space.kind() == TriviaKind::Whitespace
1256                && one.text() == "1"
1257                && not.text() == "not"
1258                && second_space.kind() == TriviaKind::Whitespace
1259                && not_one.text() == "not1"
1260        ));
1261
1262        let underscored = tokens("_name");
1263        let trees = underscored.iter().collect::<Vec<_>>();
1264        assert!(matches!(
1265            trees.as_slice(),
1266            [underscore, TokenTree::Ident(name)]
1267                if underscore.spelling() == Some("_") && name.text() == "name"
1268        ));
1269
1270        let unicode = tokens("éλ🐻");
1271        assert!(matches!(
1272            unicode.iter().next(),
1273            Some(TokenTree::Ident(identifier)) if identifier.text() == "éλ🐻"
1274        ));
1275
1276        let operated = tokens("α⊠β");
1277        let trees = operated.iter().collect::<Vec<_>>();
1278        assert!(matches!(
1279            trees.as_slice(),
1280            [
1281                TokenTree::Ident(alpha),
1282                operator,
1283                TokenTree::Ident(beta),
1284            ] if alpha.text() == "α"
1285                && operator.spelling() == Some("⊠")
1286                && beta.text() == "β"
1287        ));
1288    }
1289
1290    #[test]
1291    fn qualified_identifier_metadata_uses_subspans_without_splitting_the_atom() {
1292        let spelling = "Package$name$12";
1293        let stream = tokens(spelling);
1294        assert!(matches!(
1295            stream.iter().collect::<Vec<_>>().as_slice(),
1296            [TokenTree::Ident(_)]
1297        ));
1298
1299        let symbol = crate::Symbol::new(spelling, Span::new(100, 100 + spelling.len()));
1300        assert_eq!(
1301            symbol.package_qualifier.as_ref().and_then(Span::bounds),
1302            Some((100, 107))
1303        );
1304        assert_eq!(
1305            symbol.index_qualifier.as_ref().and_then(Span::bounds),
1306            Some((113, 115))
1307        );
1308        assert!(matches!(
1309            crate::Lex::to_token_stream(&symbol)
1310                .iter()
1311                .collect::<Vec<_>>()
1312                .as_slice(),
1313            [TokenTree::Ident(identifier)] if identifier.text() == spelling
1314        ));
1315
1316        let package = crate::Symbol::new("Package$name", Span::new(20, 32));
1317        assert_eq!(
1318            package.package_qualifier.as_ref().and_then(Span::bounds),
1319            Some((20, 27))
1320        );
1321        assert_eq!(package.index_qualifier, None);
1322
1323        let indexed = crate::Symbol::new("name$12", Span::new(30, 37));
1324        assert_eq!(indexed.package_qualifier, None);
1325        assert_eq!(
1326            indexed.index_qualifier.as_ref().and_then(Span::bounds),
1327            Some((35, 37))
1328        );
1329
1330        for unqualified in ["name", "A$B$C", "A$$B", "A$", "A$1B", "Core$Core$not"] {
1331            let symbol = crate::Symbol::new(unqualified, Span::detached());
1332            assert_eq!(symbol.package_qualifier, None, "`{unqualified}`");
1333            assert_eq!(symbol.index_qualifier, None, "`{unqualified}`");
1334        }
1335    }
1336
1337    #[test]
1338    fn radix_literals_stop_before_the_first_invalid_digit() {
1339        let binary = tokens("0b010201");
1340        let trees = binary.iter().collect::<Vec<_>>();
1341        assert!(matches!(
1342            trees.as_slice(),
1343            [
1344                TokenTree::Literal(binary),
1345                TokenTree::Literal(decimal),
1346            ]
1347                if binary.text() == "0b010" && decimal.text() == "201"
1348        ));
1349
1350        let octal = tokens("0o70789");
1351        let trees = octal.iter().collect::<Vec<_>>();
1352        assert!(matches!(
1353            trees.as_slice(),
1354            [
1355                TokenTree::Literal(octal),
1356                TokenTree::Literal(decimal),
1357            ]
1358                if octal.text() == "0o707" && decimal.text() == "89"
1359        ));
1360
1361        let binary_then_identifier = tokens("0b1E2");
1362        let trees = binary_then_identifier.iter().collect::<Vec<_>>();
1363        assert!(matches!(
1364            trees.as_slice(),
1365            [
1366                TokenTree::Literal(binary),
1367                TokenTree::Ident(identifier),
1368            ]
1369                if binary.text() == "0b1" && identifier.text() == "E2"
1370        ));
1371    }
1372
1373    #[test]
1374    fn trivia_preserves_physical_separation_between_punctuation_tokens() {
1375        let stream = tokens("++x +\n+");
1376        let trees = stream.iter().collect::<Vec<_>>();
1377        assert!(matches!(
1378            trees.as_slice(),
1379            [
1380                increment,
1381                TokenTree::Ident(identifier),
1382                TokenTree::Trivia(space),
1383                first_plus,
1384                TokenTree::Trivia(newline),
1385                second_plus,
1386            ] if increment.spelling() == Some("++")
1387                && identifier.text() == "x"
1388                && space.kind() == TriviaKind::Whitespace
1389                && first_plus.spelling() == Some("+")
1390                && newline.kind() == TriviaKind::Whitespace
1391                && newline.contains_line_break()
1392                && second_plus.spelling() == Some("+")
1393        ));
1394    }
1395
1396    #[test]
1397    fn strings_and_raw_strings_are_literal_token_trees() {
1398        for (source, kind) in [
1399            (r#""a\n\u0101""#, LiteralKind::String),
1400            ("///left////right///", LiteralKind::RawString),
1401            ("///left//right/single///", LiteralKind::RawString),
1402        ] {
1403            let stream = tokens(source);
1404            assert!(matches!(
1405                stream.iter().next(),
1406                Some(TokenTree::Literal(literal))
1407                    if literal.kind == kind && literal.text() == source
1408            ));
1409        }
1410    }
1411
1412    #[test]
1413    fn structural_delimiters_form_nested_groups_with_exact_spans() {
1414        let source = "(1, [2, <|3|>])";
1415        let stream = tokens(source);
1416        let mut trees = stream.into_iter();
1417        let Some(TokenTree::Group(parenthesized)) = trees.next() else {
1418            panic!("the source should be one parenthesized group");
1419        };
1420        assert!(trees.next().is_none());
1421        assert_eq!(parenthesized.delim_kind(), DelimiterKind::Parenthesis);
1422
1423        let span = parenthesized.span();
1424        assert_eq!(span.start().unwrap(), 0);
1425        assert_eq!(span.end().unwrap(), 15);
1426        assert_eq!(parenthesized.delimiter().opening_span().end().unwrap(), 1);
1427        assert_eq!(
1428            parenthesized.delimiter().closing_span().start().unwrap(),
1429            14
1430        );
1431
1432        let bracketed = parenthesized
1433            .stream()
1434            .iter()
1435            .find_map(|tree| match tree {
1436                TokenTree::Group(group) => Some(group),
1437                _ => None,
1438            })
1439            .unwrap();
1440        assert_eq!(bracketed.delim_kind(), DelimiterKind::Bracket);
1441        assert_eq!(bracketed.delimiter().opening_span().start().unwrap(), 4);
1442        assert_eq!(bracketed.delimiter().closing_span().start().unwrap(), 13);
1443
1444        let angle_bar = bracketed
1445            .stream()
1446            .iter()
1447            .find_map(|tree| match tree {
1448                TokenTree::Group(group) => Some(group),
1449                _ => None,
1450            })
1451            .unwrap();
1452        assert_eq!(angle_bar.delim_kind(), DelimiterKind::AngleBar);
1453        let opening = angle_bar.delimiter().opening_span();
1454        let closing = angle_bar.delimiter().closing_span();
1455        assert_eq!(opening.start().unwrap(), 8);
1456        assert_eq!(opening.end().unwrap(), 10);
1457        assert_eq!(closing.start().unwrap(), 11);
1458        assert_eq!(closing.end().unwrap(), 13);
1459    }
1460
1461    #[test]
1462    fn graded_operator_wins_before_parenthesis_grouping() {
1463        let graded = tokens("(*)");
1464        let trees = graded.iter().collect::<Vec<_>>();
1465        assert!(matches!(
1466            trees.as_slice(),
1467            [graded] if graded.spelling() == Some("(*)")
1468        ));
1469
1470        let grouped = tokens("(*x)");
1471        assert!(matches!(grouped.iter().next(), Some(TokenTree::Group(_))));
1472    }
1473
1474    #[test]
1475    fn malformed_source_returns_lex_errors() {
1476        for (source, kind) in [
1477            ("\"\\q\"", LexErrorKind::InvalidEscape),
1478            ("(1", LexErrorKind::UnterminatedGroup),
1479            (")", LexErrorKind::UnexpectedClosingDelimiter),
1480            ("(]", LexErrorKind::UnexpectedClosingDelimiter),
1481            ("///", LexErrorKind::UnterminatedRawString),
1482            ("-* unterminated", LexErrorKind::UnterminatedBlockComment),
1483        ] {
1484            assert_eq!(lex_str(source).unwrap_err().kind(), kind);
1485        }
1486
1487        let nested = "(".repeat(MAX_GROUP_DEPTH + 1);
1488        assert_eq!(
1489            lex_str(&nested).unwrap_err().kind(),
1490            LexErrorKind::NestingLimitExceeded
1491        );
1492    }
1493
1494    #[test]
1495    fn a_crlf_line_break_counts_as_the_two_bytes_it_is() {
1496        let stream = tokens("x\r\ny");
1497        let range = stream
1498            .iter()
1499            .find_map(|tree| match tree {
1500                TokenTree::Ident(identifier) if identifier.text() == "y" => Some(identifier.span()),
1501                _ => None,
1502            })
1503            .unwrap();
1504        // `x` `\r` `\n` `y`, so `y` occupies byte 3 alone.
1505        assert_eq!(range.bounds(), Some((3, 4)));
1506
1507        let error = lex_str("-* x\r\n").unwrap_err();
1508        assert_eq!(error.span().end(), Some("-* x\r\n".len()));
1509    }
1510
1511    #[test]
1512    fn identifier_classes_match_m2_byte_ctype() {
1513        for byte in [b'a', b'Z', 0x80, 0xff] {
1514            assert!(is_identifier_start(byte));
1515            assert!(is_identifier_continue(byte));
1516        }
1517        for byte in [b'0', b'9', b'$', b'\''] {
1518            assert!(!is_identifier_start(byte));
1519            assert!(is_identifier_continue(byte));
1520        }
1521        for byte in [b'_', b' ', b'+'] {
1522            assert!(!is_identifier_start(byte));
1523            assert!(!is_identifier_continue(byte));
1524        }
1525    }
1526
1527    #[test]
1528    fn math_operator_prefixes_match_m2_ranges() {
1529        for pair in [
1530            (0xc2, 0xa0),
1531            (0xc2, 0xbf),
1532            (0xc3, 0x97),
1533            (0xc3, 0xb7),
1534            (0xe2, 0x86),
1535            (0xe2, 0x87),
1536            (0xe2, 0x88),
1537            (0xe2, 0x8f),
1538            (0xe2, 0x9f),
1539            (0xe2, 0xa4),
1540            (0xe2, 0xa7),
1541            (0xe2, 0xa8),
1542            (0xe2, 0xaf),
1543        ] {
1544            assert!(is_math_operator_pair(pair.0, pair.1));
1545        }
1546        for pair in [
1547            (0xc2, 0x9f),
1548            (0xc3, 0x96),
1549            (0xc3, 0x98),
1550            (0xe2, 0x85),
1551            (0xe2, 0x90),
1552            (0xe2, 0x9e),
1553            (0xe2, 0xa3),
1554            (0xe2, 0xb0),
1555        ] {
1556            assert!(!is_math_operator_pair(pair.0, pair.1));
1557        }
1558    }
1559
1560    #[test]
1561    fn utf8_width_rejects_continuations_and_invalid_leads() {
1562        assert_eq!(utf8_char_width(b'a'), Some(1));
1563        assert_eq!(utf8_char_width(0xc2), Some(2));
1564        assert_eq!(utf8_char_width(0xe2), Some(3));
1565        assert_eq!(utf8_char_width(0xf0), Some(4));
1566        assert_eq!(utf8_char_width(0x80), None);
1567        assert_eq!(utf8_char_width(0xc0), None);
1568        assert_eq!(utf8_char_width(0xf5), None);
1569    }
1570
1571    #[test]
1572    fn ordinary_strings_accept_exact_m2_escape_widths() {
1573        for source in [r#""\u0101""#, r#""\x4f""#, r#""\1\12\123""#, r#""\n\"\\""#] {
1574            let stream = tokens(source);
1575            assert!(matches!(
1576                stream.iter().next(),
1577                Some(TokenTree::Literal(literal))
1578                    if literal.kind == LiteralKind::String && literal.text() == source
1579            ));
1580        }
1581        for source in [r#""\u101""#, r#""\u010g""#, r#""\x4""#] {
1582            assert_eq!(
1583                lex_str(source).unwrap_err().kind(),
1584                LexErrorKind::InvalidEscape
1585            );
1586        }
1587    }
1588}