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