Skip to main content

submilli_engine/
lexer.rs

1use crate::compiler_error::{CompileError, CompilerFailure, CompilerStage};
2use crate::literal_units::{push_literal_char, push_lone_surrogate};
3use num_bigint::BigUint;
4use num_traits::Num;
5use unicode_ident::{is_xid_continue, is_xid_start};
6
7use crate::{Diagnostic, FileId, RawDoc, Severity, Span, Token, TokenKind};
8
9const VALID_ESCAPES: &str =
10    "valid escapes: \\\", \\', \\\\, \\/, \\n, \\t, \\r, \\b, \\f, \\v, \\0, \\u{HHHH}";
11
12/// UTF-8 encoding of U+FEFF, which many editors write at the start of a file.
13const BOM: &[u8] = "\u{feff}".as_bytes();
14/// Match the parser's diagnostic cap so malformed source cannot allocate one
15/// owned diagnostic per byte before the parser gets control.
16const MAX_LEXER_DIAGNOSTICS: usize = 20;
17
18pub struct Lexer<'a> {
19    source: &'a str,
20    bytes: &'a [u8],
21    file: FileId,
22    pos: u32,
23    diagnostics: Vec<Diagnostic>,
24    fatal: Option<CompilerFailure>,
25    /// Only the last doc comment before a declaration attaches (JSDoc convention); earlier ones are dropped.
26    pending_docs: Vec<RawDoc>,
27    /// Brace depth for each active `${ … }` interpolation. A `}` with the top
28    /// frame at `0` closes the interpolation and resumes template scanning;
29    /// nested templates push additional frames.
30    template_frames: Vec<u32>,
31    /// Previous non-newline token, used to disambiguate `/` as regex literal vs. division.
32    last_significant_token: Option<TokenKind>,
33    last_bang_is_postfix: bool,
34}
35
36impl<'a> Lexer<'a> {
37    /// Lex `source`, attributing every span to `file`. The caller owns the
38    /// [`FileId`] (it indexes into the [`Sources`](crate::Sources) registry):
39    /// single-file scripts pass the script's id, multi-file packages pass each
40    /// module's.
41    pub fn new(source: &'a str, file: FileId) -> Self {
42        Self {
43            source,
44            bytes: source.as_bytes(),
45            file,
46            // A leading U+FEFF is an encoding marker, not source text. It is skipped by
47            // advancing past it rather than trimming `source`, so every span stays a byte
48            // offset into the file as it exists on disk and carets remain accurate.
49            // Offset 0 only: anywhere else U+FEFF is a real character and still an error.
50            pos: if source.as_bytes().starts_with(BOM) {
51                BOM.len() as u32
52            } else {
53                0
54            },
55            diagnostics: Vec::new(),
56            fatal: (source.len() > (u32::MAX - 4) as usize).then(|| CompilerFailure::Limit {
57                stage: CompilerStage::Parse,
58                span: None,
59                message: "source exceeds the 32-bit lexer offset limit".into(),
60                help: vec!["split the source into smaller modules".into()],
61            }),
62            pending_docs: Vec::new(),
63            template_frames: Vec::new(),
64            last_significant_token: None,
65            last_bang_is_postfix: false,
66        }
67    }
68
69    fn span(&self, start: u32, end: u32) -> Span {
70        Span {
71            file: self.file,
72            start,
73            end,
74        }
75    }
76
77    pub fn next_token(&mut self) -> Token {
78        let token = self.next_token_inner();
79        if self.fatal.is_none()
80            && let Err(error) = token.span.text(self.source, self.file)
81        {
82            self.fatal = Some(error.into_compiler_failure(CompilerStage::Parse));
83            return self.eof_token();
84        }
85        token
86    }
87
88    fn next_token_inner(&mut self) -> Token {
89        if self.fatal.is_some() {
90            return self.eof_token();
91        }
92        loop {
93            self.skip_trivia();
94            let Some(b) = self.peek() else {
95                let tok = self.eof_token();
96                return self.finalize(tok);
97            };
98            let tok = match b {
99                // Returns early to bypass finalize so pending_docs survive intervening newlines.
100                b'\n' | b'\r' => return self.lex_newline(),
101                b'0'..=b'9' => self.lex_number(),
102                b'.' if self.digit_at(1) => self.lex_number(),
103                b'"' | b'\'' => self.lex_string(b),
104                b'`' => {
105                    let start = self.pos;
106                    self.pos += 1;
107                    self.lex_template_part(start, true)
108                }
109                b'=' | b'!' | b'<' | b'>' | b'+' | b'-' | b'*' | b'/' | b'%' | b'.' => {
110                    self.lex_operator()
111                }
112                b'&' | b'^' | b'~' => self.lex_operator(),
113                b'|' => self.lex_operator(),
114                b'(' | b')' | b'{' | b'}' | b'[' | b']' | b',' | b':' | b';' | b'?' => {
115                    self.lex_delimiter()
116                }
117                _ => {
118                    if b.is_ascii_alphabetic() || b == b'_' || b == b'$' {
119                        self.lex_ident()
120                    } else if b >= 0x80
121                        && let Some(c) = self.peek_char()
122                    {
123                        if is_xid_start(c) {
124                            self.lex_ident()
125                        } else {
126                            self.diagnose_unexpected_char(c);
127                            self.pos += c.len_utf8() as u32;
128                            continue;
129                        }
130                    } else {
131                        self.diagnose_unexpected_byte(b);
132                        self.pos += 1;
133                        continue;
134                    }
135                }
136            };
137            return self.finalize(tok);
138        }
139    }
140
141    fn finalize(&mut self, tok: Token) -> Token {
142        let tok = self.attach_doc(tok);
143        match &tok.kind {
144            TokenKind::Newline => {}
145            other => {
146                self.last_bang_is_postfix =
147                    matches!(other, TokenKind::Bang) && !self.regex_context();
148                self.last_significant_token = Some(other.clone());
149            }
150        }
151        tok
152    }
153
154    /// ASI knows statement boundaries and keyword property names that the raw
155    /// token table cannot distinguish. Apply its correction before lexing `/`.
156    pub(crate) fn set_bang_is_postfix(&mut self, postfix: bool) {
157        self.last_bang_is_postfix = postfix;
158    }
159
160    fn regex_context(&self) -> bool {
161        !self.last_bang_is_postfix && is_regex_context(&self.last_significant_token)
162    }
163
164    pub fn into_diagnostics(self) -> Vec<Diagnostic> {
165        let file = self.file;
166        self.finish()
167            .unwrap_or_else(|error| error.into_diagnostics(file))
168    }
169
170    pub fn finish(self) -> Result<Vec<Diagnostic>, CompileError> {
171        match self.fatal {
172            Some(fatal) => Err(CompileError {
173                diagnostics: self.diagnostics,
174                fatal: Some(fatal),
175            }),
176            None => Ok(self.diagnostics),
177        }
178    }
179
180    fn fail(&mut self, message: &str) -> Token {
181        self.fatal.get_or_insert_with(|| CompilerFailure::Internal {
182            stage: CompilerStage::Parse,
183            span: None,
184            message: message.into(),
185        });
186        self.eof_token()
187    }
188
189    fn peek(&self) -> Option<u8> {
190        if self.fatal.is_some() {
191            return None;
192        }
193        self.bytes.get(self.pos as usize).copied()
194    }
195
196    fn digit_at(&self, offset: usize) -> bool {
197        self.peek_at(offset).is_some_and(|b| b.is_ascii_digit())
198    }
199
200    fn peek_at(&self, offset: usize) -> Option<u8> {
201        (self.pos as usize)
202            .checked_add(offset)
203            .and_then(|index| self.bytes.get(index))
204            .copied()
205    }
206
207    fn peek_char(&self) -> Option<char> {
208        self.source.get(self.pos as usize..)?.chars().next()
209    }
210
211    fn eof_token(&self) -> Token {
212        Token::new(TokenKind::Eof, self.span(self.pos, self.pos))
213    }
214
215    fn error(&mut self, span: Span, message: impl Into<String>) {
216        if self.diagnostics.len() >= MAX_LEXER_DIAGNOSTICS {
217            return;
218        }
219        self.diagnostics.push(Diagnostic {
220            severity: Severity::Error,
221            span,
222            message: message.into(),
223            help: vec![],
224            notes: vec![],
225        });
226    }
227
228    fn error_with_help(&mut self, span: Span, message: impl Into<String>, help: Vec<String>) {
229        if self.diagnostics.len() >= MAX_LEXER_DIAGNOSTICS {
230            return;
231        }
232        self.diagnostics.push(Diagnostic {
233            severity: Severity::Error,
234            span,
235            message: message.into(),
236            help,
237            notes: vec![],
238        });
239    }
240
241    fn diagnose_unexpected_byte(&mut self, b: u8) {
242        if self.diagnostics.len() >= MAX_LEXER_DIAGNOSTICS {
243            return;
244        }
245        let span = self.span(self.pos, self.pos + 1);
246        let message = if b.is_ascii() && !b.is_ascii_control() {
247            format!("unexpected character `{}`", b as char)
248        } else {
249            format!("unexpected byte 0x{b:02X}")
250        };
251        self.error(span, message);
252    }
253
254    fn diagnose_unexpected_char(&mut self, c: char) {
255        if self.diagnostics.len() >= MAX_LEXER_DIAGNOSTICS {
256            return;
257        }
258        let len = c.len_utf8() as u32;
259        self.error(
260            self.span(self.pos, self.pos + len),
261            format!("unexpected character `{c}`"),
262        );
263    }
264
265    fn skip_trivia(&mut self) {
266        loop {
267            match self.peek() {
268                // Space, tab, vertical tab, form feed.
269                Some(b' ' | b'\t' | 0x0B | 0x0C) => self.pos += 1,
270                Some(0x80..) if self.skip_space_separator() => {}
271                Some(b'/') => match self.peek_at(1) {
272                    Some(b'/') => self.skip_line_comment(),
273                    Some(b'*') => {
274                        // `/**/` is a regular empty block comment; doc requires `/**` + non-`/` next.
275                        if self.peek_at(2) == Some(b'*') && self.peek_at(3) != Some(b'/') {
276                            self.capture_doc_comment();
277                        } else {
278                            self.skip_block_comment();
279                        }
280                    }
281                    _ => return,
282                },
283                _ => return,
284            }
285        }
286    }
287
288    /// Skips one non-ASCII space separator; false when the next character is not one.
289    fn skip_space_separator(&mut self) -> bool {
290        let Some(c) = self.peek_char().filter(|&c| is_space_separator(c)) else {
291            return false;
292        };
293        self.pos += c.len_utf8() as u32;
294        true
295    }
296
297    fn skip_line_comment(&mut self) {
298        self.pos += 2;
299        while let Some(b) = self.peek() {
300            if matches!(b, b'\n' | b'\r') {
301                break;
302            }
303            self.pos += 1;
304        }
305    }
306
307    fn skip_block_comment(&mut self) {
308        let start = self.pos;
309        self.pos += 2;
310        loop {
311            match self.peek() {
312                None => {
313                    self.error(self.span(start, start + 2), "unterminated block comment");
314                    return;
315                }
316                Some(b'*') if self.peek_at(1) == Some(b'/') => {
317                    self.pos += 2;
318                    return;
319                }
320                Some(_) => self.pos += 1,
321            }
322        }
323    }
324
325    /// Captures a `/** ... */` comment into `pending_docs`. Stored text includes delimiters.
326    fn capture_doc_comment(&mut self) {
327        let start = self.pos;
328        self.pos += 3; // past `/**`
329        loop {
330            match self.peek() {
331                None => {
332                    self.error(self.span(start, start + 3), "unterminated block comment");
333                    return;
334                }
335                Some(b'*') if self.peek_at(1) == Some(b'/') => {
336                    self.pos += 2;
337                    let span = self.span(start, self.pos);
338                    let Some(text) = self.source.get(start as usize..self.pos as usize) else {
339                        self.fail("invalid documentation span");
340                        return;
341                    };
342                    let text = text.to_string();
343                    self.pending_docs.push(RawDoc { text, span });
344                    return;
345                }
346                Some(_) => self.pos += 1,
347            }
348        }
349    }
350
351    fn attach_doc(&mut self, mut tok: Token) -> Token {
352        if !matches!(tok.kind, TokenKind::Newline) {
353            tok.leading_doc = self.pending_docs.pop();
354            self.pending_docs.clear();
355        }
356        tok
357    }
358
359    // next_token_inner matches the current byte immediately before dispatch.
360    fn lex_newline(&mut self) -> Token {
361        let start = self.pos;
362        match self.peek() {
363            Some(b'\r') => {
364                self.pos += 1;
365                if self.peek() == Some(b'\n') {
366                    self.pos += 1;
367                }
368            }
369            Some(b'\n') => self.pos += 1,
370            _ => unreachable!("newline dispatch requires a newline byte"),
371        }
372        Token::new(TokenKind::Newline, self.span(start, self.pos))
373    }
374
375    /// Entered at a digit, or at a `.` followed by a digit (`.5`), whose integer
376    /// part is then empty.
377    fn lex_number(&mut self) -> Token {
378        let start = self.pos;
379
380        if self.peek() == Some(b'0')
381            && let Some(radix) = self.peek_at(1).and_then(Radix::from_prefix)
382        {
383            return self.lex_radix_number(start, radix);
384        }
385
386        let token = self.lex_decimal_number(start);
387        if matches!(
388            token.kind,
389            TokenKind::NumberLiteral(_) | TokenKind::BigIntLiteral(_)
390        ) {
391            self.reject_leading_zero(token.span);
392        }
393        token
394    }
395
396    fn lex_decimal_number(&mut self, start: u32) -> Token {
397        let mut has_fraction_or_exponent = false;
398
399        self.scan_digits(|b| b.is_ascii_digit());
400
401        let Some(int_part) = self.source.get(start as usize..self.pos as usize) else {
402            return self.fail("invalid numeric literal span");
403        };
404        if self.peek() == Some(b'.') {
405            match self.decimal_point_role(int_part) {
406                DecimalPoint::Literal => {
407                    has_fraction_or_exponent = true;
408                    self.pos += 1;
409                    self.scan_digits(|b| b.is_ascii_digit());
410                }
411                DecimalPoint::MemberAccess => {}
412                DecimalPoint::NameAfter => self.report_name_after_decimal_point(start, int_part),
413            }
414        }
415
416        if matches!(self.peek(), Some(b'e' | b'E')) {
417            has_fraction_or_exponent = true;
418            let exp_start = self.pos;
419            self.pos += 1;
420            if matches!(self.peek(), Some(b'+' | b'-')) {
421                self.pos += 1;
422            }
423            let digits_start = self.pos;
424            self.scan_digits(|b| b.is_ascii_digit());
425            if digits_start == self.pos {
426                self.error(self.span(exp_start, self.pos), "missing digits in exponent");
427                return Token::new(
428                    TokenKind::NumberLiteral(f64::NAN),
429                    self.span(start, self.pos),
430                );
431            }
432        }
433
434        if self.peek() == Some(b'n') {
435            self.pos += 1;
436            let span = self.span(start, self.pos);
437            if has_fraction_or_exponent {
438                self.error(
439                    span,
440                    "bigint literal cannot have a fractional or exponent part; \
441                     remove the `.` / exponent or drop the `n` suffix",
442                );
443            }
444            // Without a fraction or exponent the digits are `int_part`; with one,
445            // keeping the integer prefix is the recovery.
446            return Token::new(TokenKind::BigIntLiteral(without_separators(int_part)), span);
447        }
448
449        let span = self.span(start, self.pos);
450        let Some(lexeme) = self.source.get(start as usize..self.pos as usize) else {
451            return self.fail("invalid token source span");
452        };
453        let lexeme = without_separators(lexeme);
454        let value = if let Ok(v) = lexeme.parse::<f64>() {
455            v
456        } else {
457            self.error(span, format!("invalid number literal `{lexeme}`"));
458            f64::NAN
459        };
460
461        Token::new(TokenKind::NumberLiteral(value), span)
462    }
463
464    /// What the `.` at the current position, after the integer part `int_part`, is.
465    fn decimal_point_role(&self, int_part: &str) -> DecimalPoint {
466        if self.digit_at(1) {
467            return DecimalPoint::Literal;
468        }
469        if is_legacy_octal_digits(int_part) {
470            return DecimalPoint::MemberAccess;
471        }
472        if !self.identifier_starts_at(1) || self.literal_continues_after_point() {
473            return DecimalPoint::Literal;
474        }
475        DecimalPoint::NameAfter
476    }
477
478    /// After `1.`, an exponent (`1.e5`), a bigint suffix (`1.n`) and a separator
479    /// (`1._5`) belong to the literal and are checked as part of it. An `n` or `_`
480    /// that starts a longer name (`1.name`, `1._x`) does not.
481    fn literal_continues_after_point(&self) -> bool {
482        match self.peek_at(1) {
483            Some(b'e' | b'E') => true,
484            Some(b'n') => !self.identifier_continues_at(2),
485            Some(b'_') => {
486                let mut offset = 1;
487                while self.peek_at(offset) == Some(b'_') {
488                    offset += 1;
489                }
490                self.digit_at(offset) || !self.identifier_continues_at(offset)
491            }
492            _ => false,
493        }
494    }
495
496    /// Reports `1.toString()`. The `.` is then left as a member access, so the rest
497    /// parses as written instead of cascading into further errors.
498    fn report_name_after_decimal_point(&mut self, start: u32, int_part: &str) {
499        let help = match self.identifier_text_at(1) {
500            Some(name) if is_plain_decimal_integer(int_part) => format!(
501                "wrap the number in parentheses, `({int_part}).{name}`, or write `{int_part}..{name}`"
502            ),
503            _ => "wrap the number in parentheses, or write a second `.`".to_string(),
504        };
505        self.error_with_help(
506            self.span(start, self.pos + 1),
507            format!("`{int_part}.` is a complete number, so a name cannot follow it directly"),
508            vec![help],
509        );
510    }
511
512    /// Strict-mode JavaScript and TypeScript reject an integer part that starts with
513    /// `0` and has more digits: legacy octal (`010`, which sloppy JavaScript reads as
514    /// 8), decimals with a leading zero (`09`, `08.5`), and a separator after the
515    /// zero (`0_1`).
516    fn reject_leading_zero(&mut self, span: Span) {
517        // An invalid span is reported as a fatal error by `next_token`.
518        let Ok(literal) = span.text(self.source, self.file) else {
519            return;
520        };
521        let int_len = literal
522            .bytes()
523            .take_while(|b| b.is_ascii_digit() || *b == b'_')
524            .count();
525        let (int_part, rest) = literal.split_at(int_len);
526        let digits = without_separators(int_part);
527        if digits.len() < 2 || !digits.starts_with('0') {
528            return;
529        }
530        let significant = int_part.trim_start_matches(['0', '_']);
531        let suggested_literal = if significant.is_empty() {
532            format!("0{rest}")
533        } else {
534            format!("{significant}{rest}")
535        };
536        if int_part.as_bytes().get(1) == Some(&b'_') {
537            self.error_with_help(
538                span,
539                format!("a numeric separator cannot follow a leading `0` in `{literal}`"),
540                vec![format!("write `{suggested_literal}`")],
541            );
542            return;
543        }
544        if matches!(rest, "" | "n") && is_legacy_octal_digits(&digits) {
545            let help = if significant.is_empty() {
546                format!("write `{suggested_literal}`")
547            } else {
548                format!(
549                    "write `0o{significant}{rest}` for octal, or `{suggested_literal}` for decimal"
550                )
551            };
552            self.error_with_help(
553                span,
554                format!("legacy octal literal `{literal}` is not allowed"),
555                vec![help],
556            );
557            return;
558        }
559        self.error_with_help(
560            span,
561            format!("decimal literal `{literal}` cannot have a leading zero"),
562            vec![format!("write `{suggested_literal}`")],
563        );
564    }
565
566    /// Lex a radix-prefixed integer literal (`0x`/`0b`/`0o`). `start` points at the
567    /// leading `0`; the prefix letter has not been consumed yet. Trailing junk (e.g.
568    /// `0xfg`) is left for the next token, matching how `123abc` lexes as `123` + `abc`.
569    fn lex_radix_number(&mut self, start: u32, radix: Radix) -> Token {
570        self.pos += 2; // `0` + prefix letter
571        let digits_start = self.pos;
572        self.scan_digits(|b| radix.accepts(b));
573        let Some(digits) = self.source.get(digits_start as usize..self.pos as usize) else {
574            return self.fail("invalid numeric literal span");
575        };
576        let digits = without_separators(digits);
577        if digits.is_empty() {
578            let span = self.span(start, self.pos);
579            self.error(span, format!("missing digits after `{}`", radix.prefix()));
580            return Token::new(TokenKind::NumberLiteral(f64::NAN), span);
581        }
582
583        if self.peek() == Some(b'n') {
584            self.pos += 1;
585            return Token::new(
586                TokenKind::BigIntLiteral(radix.to_decimal(&digits)),
587                self.span(start, self.pos),
588            );
589        }
590
591        Token::new(
592            TokenKind::NumberLiteral(radix.to_f64(&digits)),
593            self.span(start, self.pos),
594        )
595    }
596
597    /// Consumes a run of digits that may contain numeric separators (`1_000`). A
598    /// separator is valid only as a single `_` between two digits of the run; a
599    /// misplaced one is reported and consumed, and a run that doesn't continue with
600    /// a digit after it ends there.
601    fn scan_digits(&mut self, is_digit: impl Fn(u8) -> bool) {
602        let mut follows_digit = false;
603        loop {
604            match self.peek() {
605                Some(b) if is_digit(b) => {
606                    self.pos += 1;
607                    follows_digit = true;
608                }
609                Some(b'_') => {
610                    let separator_start = self.pos;
611                    while self.peek() == Some(b'_') {
612                        self.pos += 1;
613                    }
614                    let precedes_digit = self.peek().is_some_and(&is_digit);
615                    self.check_separator(separator_start, follows_digit && precedes_digit);
616                    if !precedes_digit {
617                        return;
618                    }
619                    follows_digit = false;
620                }
621                _ => return,
622            }
623        }
624    }
625
626    fn check_separator(&mut self, start: u32, is_between_digits: bool) {
627        let span = self.span(start, self.pos);
628        if !is_between_digits {
629            self.error_with_help(
630                span,
631                "numeric separators are only allowed between digits",
632                vec!["remove the `_`".to_string()],
633            );
634        } else if self.pos - start > 1 {
635            self.error_with_help(
636                span,
637                "only one numeric separator is allowed between digits",
638                vec!["remove the extra `_`".to_string()],
639            );
640        }
641    }
642
643    fn identifier_starts_at(&self, offset: usize) -> bool {
644        match self.peek_at(offset) {
645            Some(b) if b.is_ascii_alphabetic() || b == b'$' || b == b'_' => true,
646            Some(0x80..) => self.char_at(offset).is_some_and(is_xid_start),
647            _ => false,
648        }
649    }
650
651    fn identifier_continues_at(&self, offset: usize) -> bool {
652        self.char_at(offset).is_some_and(is_identifier_continue)
653    }
654
655    /// The identifier starting `offset` bytes ahead, for a help message.
656    fn identifier_text_at(&self, offset: usize) -> Option<&'a str> {
657        let start = (self.pos as usize).checked_add(offset)?;
658        let rest = self.source.get(start..)?;
659        let len: usize = rest
660            .chars()
661            .take_while(|&c| is_identifier_continue(c))
662            .map(char::len_utf8)
663            .sum();
664        rest.get(..len).filter(|name| !name.is_empty())
665    }
666
667    fn char_at(&self, offset: usize) -> Option<char> {
668        let start = (self.pos as usize).checked_add(offset)?;
669        self.source.get(start..)?.chars().next()
670    }
671
672    fn lex_string(&mut self, quote: u8) -> Token {
673        let start = self.pos;
674        self.pos += 1; // consume opening quote
675        let mut value = String::new();
676
677        loop {
678            match self.peek() {
679                None => {
680                    self.error(self.span(start, self.pos), "unterminated string literal");
681                    return Token::new(TokenKind::StringLiteral(value), self.span(start, self.pos));
682                }
683                Some(b) if b == quote => {
684                    self.pos += 1;
685                    return Token::new(TokenKind::StringLiteral(value), self.span(start, self.pos));
686                }
687                Some(b'\\') => self.read_escape(&mut value),
688                // Raw newlines are accepted as `\n` (forgiveness principle): LLMs
689                // routinely emit them via tool-input escaping slips, and the intent
690                // is unambiguous. CR/CRLF normalise to LF as in templates.
691                Some(b'\r') => {
692                    self.pos += 1;
693                    if self.peek() == Some(b'\n') {
694                        self.pos += 1;
695                    }
696                    value.push('\n');
697                }
698                Some(_) => {
699                    let Some(c) = self.peek_char() else {
700                        return self.fail("lexer cursor is not at a source character");
701                    };
702                    push_literal_char(&mut value, c);
703                    self.pos += c.len_utf8() as u32;
704                }
705            }
706        }
707    }
708
709    /// Lex a regex literal. Escape sequences are preserved verbatim (the regex engine
710    /// interprets them). `/` inside `[...]` does not close the literal.
711    fn lex_regex_literal(&mut self) -> Token {
712        let start = self.pos;
713        self.pos += 1; // consume opening `/`
714        let mut source = String::new();
715        let mut in_class = false;
716
717        loop {
718            match self.peek() {
719                None | Some(b'\n' | b'\r') => {
720                    self.error(self.span(start, self.pos), "unterminated regex literal");
721                    return Token::new(
722                        TokenKind::RegexLiteral {
723                            source,
724                            flags: String::new(),
725                        },
726                        self.span(start, self.pos),
727                    );
728                }
729                Some(b'\\') => {
730                    source.push('\\');
731                    self.pos += 1;
732                    if let Some(c) = self.peek_char() {
733                        // JS disallows newline-continued escapes in regex literals.
734                        if matches!(c, '\n' | '\r') {
735                            continue;
736                        }
737                        source.push(c);
738                        self.pos += c.len_utf8() as u32;
739                    }
740                }
741                Some(b'[') if !in_class => {
742                    in_class = true;
743                    source.push('[');
744                    self.pos += 1;
745                }
746                Some(b']') if in_class => {
747                    in_class = false;
748                    source.push(']');
749                    self.pos += 1;
750                }
751                Some(b'/') if !in_class => {
752                    self.pos += 1;
753                    let flags = self.lex_regex_flags();
754                    return Token::new(
755                        TokenKind::RegexLiteral { source, flags },
756                        self.span(start, self.pos),
757                    );
758                }
759                Some(_) => {
760                    let Some(c) = self.peek_char() else {
761                        return self.fail("lexer cursor is not at a source character");
762                    };
763                    source.push(c);
764                    self.pos += c.len_utf8() as u32;
765                }
766            }
767        }
768    }
769
770    /// Accepts any ASCII alphabetic letter; flag validation (`gimsuy` only) is the translator's job.
771    fn lex_regex_flags(&mut self) -> String {
772        let mut flags = String::new();
773        while let Some(b) = self.peek() {
774            if b.is_ascii_alphabetic() {
775                flags.push(b as char);
776                self.pos += 1;
777            } else {
778                break;
779            }
780        }
781        flags
782    }
783
784    fn read_escape(&mut self, out: &mut String) {
785        let esc_start = self.pos;
786        self.pos += 1;
787        match self.peek() {
788            None => {
789                self.error(
790                    self.span(esc_start, self.pos),
791                    "unterminated string literal",
792                );
793            }
794            Some(b'"') => {
795                out.push('"');
796                self.pos += 1;
797            }
798            Some(b'\'') => {
799                out.push('\'');
800                self.pos += 1;
801            }
802            Some(b'\\') => {
803                out.push('\\');
804                self.pos += 1;
805            }
806            // JSON's escape set includes `\/`, so strings pasted from JSON carry it.
807            Some(b'/') => {
808                out.push('/');
809                self.pos += 1;
810            }
811            Some(b'n') => {
812                out.push('\n');
813                self.pos += 1;
814            }
815            Some(b't') => {
816                out.push('\t');
817                self.pos += 1;
818            }
819            Some(b'r') => {
820                out.push('\r');
821                self.pos += 1;
822            }
823            Some(b'b') => {
824                out.push('\u{08}');
825                self.pos += 1;
826            }
827            Some(b'f') => {
828                out.push('\u{0C}');
829                self.pos += 1;
830            }
831            Some(b'v') => {
832                out.push('\u{0B}');
833                self.pos += 1;
834            }
835            Some(b'0') => {
836                out.push('\0');
837                self.pos += 1;
838            }
839            Some(b'u') => {
840                self.pos += 1;
841                self.read_unicode_escape(esc_start, out);
842            }
843            Some(_) => {
844                let Some(c) = self.peek_char() else {
845                    self.fail("escape cursor is not at a source character");
846                    return;
847                };
848                let end = self.pos + c.len_utf8() as u32;
849                self.error_with_help(
850                    self.span(esc_start, end),
851                    format!("unknown escape sequence `\\{c}`"),
852                    vec![VALID_ESCAPES.to_string()],
853                );
854                push_literal_char(out, c);
855                self.pos = end;
856            }
857        }
858    }
859
860    fn read_unicode_escape(&mut self, esc_start: u32, out: &mut String) {
861        if self.peek() == Some(b'{') {
862            self.pos += 1;
863            let hex_start = self.pos;
864            while matches!(self.peek(), Some(b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F')) {
865                self.pos += 1;
866            }
867            let hex_end = self.pos;
868            if hex_end == hex_start {
869                self.error(
870                    self.span(esc_start, self.pos),
871                    "invalid unicode escape: expected hex digits",
872                );
873                return;
874            }
875            if self.peek() != Some(b'}') {
876                self.error(
877                    self.span(esc_start, self.pos),
878                    "invalid unicode escape: expected `}`",
879                );
880                return;
881            }
882            self.pos += 1;
883            let Some(hex) = self.source.get(hex_start as usize..hex_end as usize) else {
884                self.fail("invalid Unicode escape span");
885                return;
886            };
887            if hex.len() > 6 {
888                self.error(
889                    self.span(esc_start, self.pos),
890                    "invalid code point in `\\u{…}`: too many digits",
891                );
892                return;
893            }
894            let value = u32::from_str_radix(hex, 16).unwrap_or(0);
895            if value > 0x10FFFF {
896                self.error(
897                    self.span(esc_start, self.pos),
898                    "invalid code point in `\\u{…}`: exceeds U+10FFFF",
899                );
900                return;
901            }
902            if push_lone_surrogate(out, value) {
903                return;
904            }
905            if let Some(c) = char::from_u32(value) {
906                push_literal_char(out, c);
907            }
908        } else {
909            let Some(value) = self.read_four_hex(esc_start) else {
910                return;
911            };
912            if !push_lone_surrogate(out, value)
913                && let Some(c) = char::from_u32(value)
914            {
915                push_literal_char(out, c);
916            }
917        }
918    }
919
920    fn read_four_hex(&mut self, esc_start: u32) -> Option<u32> {
921        let mut value = 0u32;
922        for _ in 0..4 {
923            if let Some(d) = self.peek().and_then(|byte| char::from(byte).to_digit(16)) {
924                value = value * 16 + d;
925                self.pos += 1;
926            } else {
927                self.error(
928                    self.span(esc_start, self.pos),
929                    "invalid unicode escape: expected 4 hex digits",
930                );
931                return None;
932            }
933        }
934        Some(value)
935    }
936
937    fn lex_ident(&mut self) -> Token {
938        let start = self.pos;
939        let Some(first) = self.peek_char() else {
940            return self.fail("identifier lexer has no source character");
941        };
942        self.pos += first.len_utf8() as u32;
943        while let Some(c) = self.peek_char() {
944            if is_identifier_continue(c) {
945                self.pos += c.len_utf8() as u32;
946            } else {
947                break;
948            }
949        }
950        let span = self.span(start, self.pos);
951        let Some(lexeme) = self.source.get(start as usize..self.pos as usize) else {
952            return self.fail("invalid token source span");
953        };
954        let kind = match lexeme {
955            "true" => TokenKind::BooleanLiteral(true),
956            "false" => TokenKind::BooleanLiteral(false),
957            "null" => TokenKind::NullLiteral,
958            "let" => TokenKind::Let,
959            "const" => TokenKind::Const,
960            "function" => TokenKind::Function,
961            "if" => TokenKind::If,
962            "else" => TokenKind::Else,
963            "while" => TokenKind::While,
964            "do" => TokenKind::Do,
965            "for" => TokenKind::For,
966            "break" => TokenKind::Break,
967            "continue" => TokenKind::Continue,
968            "return" => TokenKind::Return,
969            "switch" => TokenKind::Switch,
970            "case" => TokenKind::Case,
971            "default" => TokenKind::Default,
972            "export" => TokenKind::Export,
973            "void" => TokenKind::Void,
974            "interface" => TokenKind::Interface,
975            "enum" => TokenKind::Enum,
976            "in" => TokenKind::In,
977            "typeof" => TokenKind::Typeof,
978            "import" => TokenKind::Import,
979            "instanceof" => TokenKind::Instanceof,
980            "new" => TokenKind::New,
981            "try" => TokenKind::Try,
982            "catch" => TokenKind::Catch,
983            "finally" => TokenKind::Finally,
984            "throw" => TokenKind::Throw,
985            "class" => TokenKind::Class,
986            "extends" => TokenKind::Extends,
987            "implements" => TokenKind::Implements,
988            "super" => TokenKind::Super,
989            "this" => TokenKind::This,
990            _ => TokenKind::Identifier,
991        };
992        Token::new(kind, span)
993    }
994
995    // next_token_inner matches the current byte immediately before dispatch.
996    fn lex_operator(&mut self) -> Token {
997        let start = self.pos;
998        let b = self.peek().expect("dispatch matched a source byte");
999        let kind = match b {
1000            b'=' => {
1001                self.pos += 1;
1002                if self.peek() == Some(b'=') {
1003                    self.pos += 1;
1004                    if self.peek() == Some(b'=') {
1005                        self.pos += 1;
1006                        TokenKind::EqEqEq
1007                    } else {
1008                        TokenKind::EqEq
1009                    }
1010                } else if self.peek() == Some(b'>') {
1011                    self.pos += 1;
1012                    TokenKind::Arrow
1013                } else {
1014                    TokenKind::Equals
1015                }
1016            }
1017            b'!' => {
1018                self.pos += 1;
1019                if self.peek() == Some(b'=') {
1020                    self.pos += 1;
1021                    if self.peek() == Some(b'=') {
1022                        self.pos += 1;
1023                        TokenKind::BangEqEq
1024                    } else {
1025                        TokenKind::BangEq
1026                    }
1027                } else {
1028                    TokenKind::Bang
1029                }
1030            }
1031            b'<' => {
1032                self.pos += 1;
1033                if self.peek() == Some(b'=') {
1034                    self.pos += 1;
1035                    TokenKind::LessEquals
1036                } else {
1037                    TokenKind::LessThan
1038                }
1039            }
1040            b'>' => {
1041                self.pos += 1;
1042                if self.peek() == Some(b'=') {
1043                    self.pos += 1;
1044                    TokenKind::GreaterEquals
1045                } else {
1046                    TokenKind::GreaterThan
1047                }
1048            }
1049            b'+' => {
1050                self.pos += 1;
1051                match self.peek() {
1052                    Some(b'+') => {
1053                        self.pos += 1;
1054                        TokenKind::PlusPlus
1055                    }
1056                    Some(b'=') => {
1057                        self.pos += 1;
1058                        TokenKind::PlusEquals
1059                    }
1060                    _ => TokenKind::Plus,
1061                }
1062            }
1063            b'-' => {
1064                self.pos += 1;
1065                match self.peek() {
1066                    Some(b'-') => {
1067                        self.pos += 1;
1068                        TokenKind::MinusMinus
1069                    }
1070                    Some(b'=') => {
1071                        self.pos += 1;
1072                        TokenKind::MinusEquals
1073                    }
1074                    _ => TokenKind::Minus,
1075                }
1076            }
1077            b'*' => {
1078                self.pos += 1;
1079                match self.peek() {
1080                    Some(b'*') => {
1081                        self.pos += 1;
1082                        // `**=` checked before `**` so `x **= 2` doesn't lex as `**` + `=`.
1083                        if self.peek() == Some(b'=') {
1084                            self.pos += 1;
1085                            TokenKind::StarStarEquals
1086                        } else {
1087                            TokenKind::StarStar
1088                        }
1089                    }
1090                    Some(b'=') => {
1091                        self.pos += 1;
1092                        TokenKind::StarEquals
1093                    }
1094                    _ => TokenKind::Star,
1095                }
1096            }
1097            b'/' => {
1098                // `//` and `/*` already consumed as trivia; remaining `/` is regex or division.
1099                if self.regex_context() {
1100                    return self.lex_regex_literal();
1101                }
1102                self.pos += 1;
1103                if self.peek() == Some(b'=') {
1104                    self.pos += 1;
1105                    TokenKind::SlashEquals
1106                } else {
1107                    TokenKind::Slash
1108                }
1109            }
1110            b'%' => {
1111                self.pos += 1;
1112                if self.peek() == Some(b'=') {
1113                    self.pos += 1;
1114                    TokenKind::PercentEquals
1115                } else {
1116                    TokenKind::Percent
1117                }
1118            }
1119            b'&' => {
1120                self.pos += 1;
1121                match self.peek() {
1122                    Some(b'&') => {
1123                        self.pos += 1;
1124                        TokenKind::AmpAmp
1125                    }
1126                    Some(b'=') => {
1127                        self.pos += 1;
1128                        TokenKind::AmpEquals
1129                    }
1130                    _ => TokenKind::Amp,
1131                }
1132            }
1133            b'^' => {
1134                self.pos += 1;
1135                if self.peek() == Some(b'=') {
1136                    self.pos += 1;
1137                    TokenKind::CaretEquals
1138                } else {
1139                    TokenKind::Caret
1140                }
1141            }
1142            b'~' => {
1143                self.pos += 1;
1144                TokenKind::Tilde
1145            }
1146            b'|' => {
1147                self.pos += 1;
1148                if self.peek() == Some(b'|') {
1149                    self.pos += 1;
1150                    TokenKind::PipePipe
1151                } else if self.peek() == Some(b'=') {
1152                    self.pos += 1;
1153                    TokenKind::PipeEquals
1154                } else {
1155                    TokenKind::Pipe
1156                }
1157            }
1158            b'.' => {
1159                self.pos += 1;
1160                // Single token to avoid 3-token lookahead in the parser.
1161                if self.peek() == Some(b'.') && self.peek_at(1) == Some(b'.') {
1162                    self.pos += 2;
1163                    TokenKind::DotDotDot
1164                } else {
1165                    TokenKind::Dot
1166                }
1167            }
1168            _ => unreachable!("operator dispatch requires an operator byte"),
1169        };
1170        Token::new(kind, self.span(start, self.pos))
1171    }
1172
1173    // next_token_inner matches the current byte immediately before dispatch.
1174    fn lex_delimiter(&mut self) -> Token {
1175        let start = self.pos;
1176        let b = self.peek().expect("dispatch matched a source byte");
1177        if let Some(depth) = self.template_frames.last_mut() {
1178            if b == b'{' {
1179                let Some(next) = depth.checked_add(1) else {
1180                    return self.fail("template brace depth overflow");
1181                };
1182                *depth = next;
1183            } else if b == b'}' {
1184                if *depth == 0 {
1185                    self.template_frames.pop();
1186                    self.pos += 1;
1187                    return self.lex_template_part(start, false);
1188                }
1189                *depth -= 1;
1190            }
1191        }
1192        self.pos += 1;
1193        let kind = match b {
1194            b'(' => TokenKind::LeftParen,
1195            b')' => TokenKind::RightParen,
1196            b'{' => TokenKind::LeftBrace,
1197            b'}' => TokenKind::RightBrace,
1198            b'[' => TokenKind::LeftBracket,
1199            b']' => TokenKind::RightBracket,
1200            b',' => TokenKind::Comma,
1201            b':' => TokenKind::Colon,
1202            b';' => TokenKind::Semicolon,
1203            b'?' => match self.peek() {
1204                // `?.5` is a ternary on `.5`, as in JavaScript, not optional chaining.
1205                Some(b'.') if !self.digit_at(1) => {
1206                    self.pos += 1;
1207                    TokenKind::QuestionDot
1208                }
1209                Some(b'?') => {
1210                    self.pos += 1;
1211                    TokenKind::QuestionQuestion
1212                }
1213                _ => TokenKind::Question,
1214            },
1215            _ => unreachable!("delimiter dispatch requires a delimiter byte"),
1216        };
1217        Token::new(kind, self.span(start, self.pos))
1218    }
1219
1220    /// Scan one cooked segment of a template literal.
1221    /// `start` is the offset of the leading delimiter; `is_head` selects
1222    /// Head/NoSubstitution (true) vs. Middle/Tail (false).
1223    fn lex_template_part(&mut self, start: u32, is_head: bool) -> Token {
1224        let mut value = String::new();
1225        loop {
1226            match self.peek() {
1227                None => {
1228                    self.error(self.span(start, self.pos), "unterminated template literal");
1229                    let kind = if is_head {
1230                        TokenKind::TemplateNoSubstitution(value)
1231                    } else {
1232                        TokenKind::TemplateTail(value)
1233                    };
1234                    return Token::new(kind, self.span(start, self.pos));
1235                }
1236                Some(b'`') => {
1237                    self.pos += 1;
1238                    let kind = if is_head {
1239                        TokenKind::TemplateNoSubstitution(value)
1240                    } else {
1241                        TokenKind::TemplateTail(value)
1242                    };
1243                    return Token::new(kind, self.span(start, self.pos));
1244                }
1245                Some(b'$') if self.peek_at(1) == Some(b'{') => {
1246                    self.pos += 2; // consume `${`
1247                    self.template_frames.push(0);
1248                    let kind = if is_head {
1249                        TokenKind::TemplateHead(value)
1250                    } else {
1251                        TokenKind::TemplateMiddle(value)
1252                    };
1253                    return Token::new(kind, self.span(start, self.pos));
1254                }
1255                Some(b'\\') => self.read_template_escape(&mut value),
1256                Some(b'\r') => {
1257                    // Normalise CR/CRLF to LF so platform line endings don't affect the cooked value.
1258                    self.pos += 1;
1259                    if self.peek() == Some(b'\n') {
1260                        self.pos += 1;
1261                    }
1262                    value.push('\n');
1263                }
1264                Some(_) => {
1265                    let Some(c) = self.peek_char() else {
1266                        return self.fail("lexer cursor is not at a source character");
1267                    };
1268                    push_literal_char(&mut value, c);
1269                    self.pos += c.len_utf8() as u32;
1270                }
1271            }
1272        }
1273    }
1274
1275    /// Like `read_escape` but also handles `` \` `` and `\$` (template-only escapes).
1276    fn read_template_escape(&mut self, out: &mut String) {
1277        match self.peek_at(1) {
1278            Some(b'`') => {
1279                out.push('`');
1280                self.pos += 2;
1281            }
1282            Some(b'$') => {
1283                out.push('$');
1284                self.pos += 2;
1285            }
1286            Some(_) => self.read_escape(out),
1287            None => self.pos += 1,
1288        }
1289    }
1290}
1291
1292fn is_identifier_continue(c: char) -> bool {
1293    c == '_' || c == '$' || is_xid_continue(c)
1294}
1295
1296/// What the `.` right after a decimal integer part is.
1297enum DecimalPoint {
1298    /// Part of the literal: `1.`, `1.5`, `1.e5`, and the first `.` of `1..toString()`.
1299    Literal,
1300    /// A member access after a legacy octal integer (`010.toString()`), which has no
1301    /// fraction in JavaScript.
1302    MemberAccess,
1303    /// A name directly after the `.` (`1.toString()`), which is an error.
1304    NameAfter,
1305}
1306
1307/// Whether `int_part` is a well-formed decimal integer, so a help message can repeat it.
1308fn is_plain_decimal_integer(int_part: &str) -> bool {
1309    let has_leading_zero = int_part.len() > 1 && int_part.starts_with('0');
1310    !has_leading_zero && !int_part.ends_with('_') && !int_part.contains("__")
1311}
1312
1313/// An integer part sloppy JavaScript reads as octal: a `0` followed by octal digits.
1314fn is_legacy_octal_digits(int_part: &str) -> bool {
1315    int_part.len() >= 2
1316        && int_part.starts_with('0')
1317        && int_part.bytes().all(|digit| matches!(digit, b'0'..=b'7'))
1318}
1319
1320fn without_separators(digits: &str) -> String {
1321    digits.replace('_', "")
1322}
1323
1324/// A radix-prefixed integer literal base (`0x`, `0b`, `0o`).
1325#[derive(Clone, Copy)]
1326enum Radix {
1327    Hex,
1328    Binary,
1329    Octal,
1330}
1331
1332impl Radix {
1333    fn from_prefix(b: u8) -> Option<Self> {
1334        match b {
1335            b'x' | b'X' => Some(Self::Hex),
1336            b'b' | b'B' => Some(Self::Binary),
1337            b'o' | b'O' => Some(Self::Octal),
1338            _ => None,
1339        }
1340    }
1341
1342    fn accepts(self, b: u8) -> bool {
1343        match self {
1344            Self::Hex => b.is_ascii_hexdigit(),
1345            Self::Binary => matches!(b, b'0' | b'1'),
1346            Self::Octal => matches!(b, b'0'..=b'7'),
1347        }
1348    }
1349
1350    fn base(self) -> u32 {
1351        match self {
1352            Self::Hex => 16,
1353            Self::Binary => 2,
1354            Self::Octal => 8,
1355        }
1356    }
1357
1358    fn prefix(self) -> &'static str {
1359        match self {
1360            Self::Hex => "0x",
1361            Self::Binary => "0b",
1362            Self::Octal => "0o",
1363        }
1364    }
1365
1366    /// Accumulates in `f64` so an over-wide literal saturates to infinity rather than
1367    /// panicking; `accepts` already guaranteed every byte is a valid digit.
1368    fn to_f64(self, digits: &str) -> f64 {
1369        let base = f64::from(self.base());
1370        digits
1371            .bytes()
1372            .map(|b| f64::from(hex_digit_value(b)))
1373            .fold(0.0, |acc, d| acc * base + d)
1374    }
1375
1376    fn to_decimal(self, digits: &str) -> String {
1377        BigUint::from_str_radix(digits, self.base())
1378            .map_or_else(|_| "0".to_string(), |v| v.to_str_radix(10))
1379    }
1380}
1381
1382/// Unicode space separators (category Zs) beyond ASCII space, which JavaScript treats
1383/// as whitespace. A no-break space pasted from a web page or word processor is the
1384/// usual source.
1385fn is_space_separator(c: char) -> bool {
1386    matches!(
1387        c,
1388        '\u{a0}' | '\u{1680}' | '\u{2000}'..='\u{200a}' | '\u{202f}' | '\u{205f}' | '\u{3000}'
1389    )
1390}
1391
1392/// Numeric value of an ASCII hex digit; non-digits (never passed by `Radix::accepts`)
1393/// map to `0`.
1394fn hex_digit_value(b: u8) -> u32 {
1395    match b {
1396        b'0'..=b'9' => u32::from(b - b'0'),
1397        b'a'..=b'f' => u32::from(b - b'a' + 10),
1398        b'A'..=b'F' => u32::from(b - b'A' + 10),
1399        _ => 0,
1400    }
1401}
1402
1403/// Returns `true` when `/` should start a regex literal rather than act as division.
1404/// `None` (start of input) is treated as regex-context so `/foo/` at program start works.
1405#[allow(clippy::match_like_matches_macro)] // grouped arms read clearer than a flat `matches!`
1406pub fn is_regex_context(prev: &Option<TokenKind>) -> bool {
1407    let Some(kind) = prev else {
1408        return true;
1409    };
1410    match kind {
1411        TokenKind::Plus
1412        | TokenKind::Minus
1413        | TokenKind::Star
1414        | TokenKind::Slash
1415        | TokenKind::Percent
1416        | TokenKind::Equals
1417        | TokenKind::PlusEquals
1418        | TokenKind::MinusEquals
1419        | TokenKind::StarEquals
1420        | TokenKind::SlashEquals
1421        | TokenKind::PercentEquals
1422        | TokenKind::StarStar
1423        | TokenKind::StarStarEquals
1424        | TokenKind::EqEqEq
1425        | TokenKind::EqEq
1426        | TokenKind::BangEqEq
1427        | TokenKind::BangEq
1428        | TokenKind::LessThan
1429        | TokenKind::GreaterThan
1430        | TokenKind::LessEquals
1431        | TokenKind::GreaterEquals
1432        | TokenKind::Bang
1433        | TokenKind::AmpAmp
1434        | TokenKind::Amp
1435        | TokenKind::AmpEquals
1436        | TokenKind::PipeEquals
1437        | TokenKind::Caret
1438        | TokenKind::CaretEquals
1439        | TokenKind::Tilde
1440        | TokenKind::Pipe
1441        | TokenKind::PipePipe
1442        | TokenKind::Question
1443        | TokenKind::QuestionDot
1444        | TokenKind::QuestionQuestion
1445        | TokenKind::Arrow
1446        | TokenKind::DotDotDot => true,
1447
1448        TokenKind::LeftParen
1449        | TokenKind::LeftBrace
1450        | TokenKind::LeftBracket
1451        | TokenKind::Comma
1452        | TokenKind::Colon
1453        | TokenKind::Semicolon => true,
1454
1455        TokenKind::Return
1456        | TokenKind::Throw
1457        | TokenKind::Typeof
1458        | TokenKind::In
1459        | TokenKind::New
1460        | TokenKind::Case
1461        | TokenKind::Default
1462        | TokenKind::Else
1463        | TokenKind::Do
1464        | TokenKind::Void => true,
1465
1466        _ => false,
1467    }
1468}
1469
1470#[cfg(test)]
1471mod tests {
1472    use super::Lexer;
1473    use crate::source::Sources;
1474    use crate::{Diagnostic, FileId, Span, Token, TokenKind, diagnostics};
1475
1476    const F: FileId = FileId(0);
1477
1478    #[test]
1479    fn invalid_unicode_cursor_and_template_depth_return_fatal_errors() {
1480        let mut lexer = Lexer::new("é", F);
1481        lexer.pos = 1;
1482        assert!(matches!(lexer.lex_ident().kind, TokenKind::Eof));
1483        assert!(
1484            lexer
1485                .finish()
1486                .expect_err("invalid UTF-8 cursor")
1487                .fatal
1488                .is_some()
1489        );
1490
1491        let mut lexer = Lexer::new("{", F);
1492        lexer.template_frames.push(u32::MAX);
1493        assert!(matches!(lexer.lex_delimiter().kind, TokenKind::Eof));
1494        assert!(
1495            lexer
1496                .finish()
1497                .expect_err("template depth overflow")
1498                .fatal
1499                .is_some()
1500        );
1501    }
1502
1503    fn sources(text: &str) -> Sources {
1504        let (sources, _) = Sources::single("script.subm", text).unwrap();
1505        sources
1506    }
1507
1508    fn tokenize_all(source: &str) -> (Vec<Token>, Vec<Diagnostic>) {
1509        let mut lx = Lexer::new(source, crate::FileId(0));
1510        let mut tokens = Vec::new();
1511        loop {
1512            let tok = lx.next_token();
1513            let is_eof = tok.kind == TokenKind::Eof;
1514            tokens.push(tok);
1515            if is_eof {
1516                break;
1517            }
1518        }
1519        (tokens, lx.into_diagnostics())
1520    }
1521
1522    fn tokenize_one(source: &str) -> (Token, Token, Vec<Diagnostic>) {
1523        let mut lx = Lexer::new(source, crate::FileId(0));
1524        let first = lx.next_token();
1525        let second = lx.next_token();
1526        (first, second, lx.into_diagnostics())
1527    }
1528
1529    fn expect_number(source: &str, value: f64, span: Span) {
1530        let (tok, eof, diags) = tokenize_one(source);
1531        assert_eq!(tok.span, span, "span mismatch for {source:?}");
1532        match tok.kind {
1533            TokenKind::NumberLiteral(v) => assert_eq!(v, value, "value mismatch for {source:?}"),
1534            other => panic!("expected NumberLiteral for {source:?}, got {other:?}"),
1535        }
1536        assert_eq!(eof.kind, TokenKind::Eof);
1537        assert!(
1538            diags.is_empty(),
1539            "unexpected diagnostics for {source:?}: {diags:?}"
1540        );
1541    }
1542
1543    fn expect_string(source: &str, value: &str) {
1544        let (tok, eof, diags) = tokenize_one(source);
1545        match tok.kind {
1546            TokenKind::StringLiteral(ref s) => {
1547                assert_eq!(s, value, "string mismatch for {source:?}");
1548            }
1549            other => panic!("expected StringLiteral for {source:?}, got {other:?}"),
1550        }
1551        assert_eq!(eof.kind, TokenKind::Eof);
1552        assert!(
1553            diags.is_empty(),
1554            "unexpected diagnostics for {source:?}: {diags:?}"
1555        );
1556    }
1557
1558    fn expect_single_token(source: &str, expected: TokenKind, span: Span) {
1559        let (tok, eof, diags) = tokenize_one(source);
1560        assert_eq!(tok.kind, expected, "kind mismatch for {source:?}");
1561        assert_eq!(tok.span, span, "span mismatch for {source:?}");
1562        assert_eq!(eof.kind, TokenKind::Eof);
1563        assert!(
1564            diags.is_empty(),
1565            "unexpected diagnostics for {source:?}: {diags:?}"
1566        );
1567    }
1568
1569    #[test]
1570    fn slash_after_non_null_assertion_is_division() {
1571        for prefix in ["a!", "f()!", "a[0]!", "o?.b!.count!", "a!!"] {
1572            for (operator, expected) in [("/", TokenKind::Slash), ("/=", TokenKind::SlashEquals)] {
1573                let (tokens, diagnostics) = tokenize_all(&format!("{prefix} {operator} 10"));
1574                assert!(
1575                    diagnostics.is_empty(),
1576                    "{prefix} {operator}: {diagnostics:?}"
1577                );
1578                assert!(tokens.iter().any(|token| token.kind == expected));
1579            }
1580        }
1581        for source in ["!/x/.test(s)", "!!/x/.test(s)", "a! / /x/.test(s)"] {
1582            let (tokens, diagnostics) = tokenize_all(source);
1583            assert!(diagnostics.is_empty(), "{source}: {diagnostics:?}");
1584            assert!(
1585                tokens
1586                    .iter()
1587                    .any(|token| matches!(token.kind, TokenKind::RegexLiteral { .. }))
1588            );
1589        }
1590    }
1591
1592    #[test]
1593    fn lex_integer() {
1594        expect_number("42", 42.0, Span::new(F, 0, 2).unwrap());
1595    }
1596
1597    #[test]
1598    fn lex_zero() {
1599        expect_number("0", 0.0, Span::new(F, 0, 1).unwrap());
1600    }
1601
1602    #[test]
1603    #[allow(clippy::approx_constant)]
1604    fn lex_decimal() {
1605        expect_number("3.14", 3.14, Span::new(F, 0, 4).unwrap());
1606    }
1607
1608    #[test]
1609    fn lex_leading_zero_decimal() {
1610        expect_number("0.5", 0.5, Span::new(F, 0, 3).unwrap());
1611    }
1612
1613    #[test]
1614    fn lex_exponent() {
1615        expect_number("1e10", 1e10, Span::new(F, 0, 4).unwrap());
1616    }
1617
1618    #[test]
1619    fn lex_decimal_with_exponent() {
1620        expect_number("1.5e10", 1.5e10, Span::new(F, 0, 6).unwrap());
1621    }
1622
1623    #[test]
1624    fn lex_uppercase_exponent_with_negative_sign() {
1625        expect_number("2E-3", 2e-3, Span::new(F, 0, 4).unwrap());
1626    }
1627
1628    #[test]
1629    fn lex_positive_exponent() {
1630        expect_number("1e+2", 1e2, Span::new(F, 0, 4).unwrap());
1631    }
1632
1633    #[test]
1634    fn lex_number_with_trailing_whitespace() {
1635        let mut lx = Lexer::new("42 ", crate::FileId(0));
1636        let tok = lx.next_token();
1637        assert_eq!(tok.span, Span::new(F, 0, 2).unwrap());
1638        assert_eq!(tok.kind, TokenKind::NumberLiteral(42.0));
1639        assert_eq!(lx.next_token().kind, TokenKind::Eof);
1640        assert!(lx.into_diagnostics().is_empty());
1641    }
1642
1643    #[test]
1644    fn unicode_space_separators_are_whitespace() {
1645        let source = "a\u{a0}b\u{1680}c\u{2000}d\u{200a}e\u{202f}f\u{205f}g\u{3000}h\u{0b}i\u{0c}j";
1646        let (tokens, diags) = tokenize_all(source);
1647        let names: Vec<_> = tokens
1648            .iter()
1649            .filter(|t| t.kind == TokenKind::Identifier)
1650            .map(|t| &source[t.span.start as usize..t.span.end as usize])
1651            .collect();
1652        assert_eq!(names, ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]);
1653        assert!(diags.is_empty());
1654    }
1655
1656    #[test]
1657    fn zero_width_space_is_not_whitespace() {
1658        // U+200B is a format character (Cf), not a space separator, in JavaScript too.
1659        let (_tokens, diags) = tokenize_all("a\u{200b}b");
1660        assert_eq!(diags.len(), 1);
1661        assert!(diags[0].message.starts_with("unexpected character"));
1662    }
1663
1664    #[test]
1665    fn empty_input_is_eof() {
1666        let mut lx = Lexer::new("", crate::FileId(0));
1667        let tok = lx.next_token();
1668        assert_eq!(tok.kind, TokenKind::Eof);
1669        assert_eq!(tok.span, Span::new(F, 0, 0).unwrap());
1670        assert!(lx.into_diagnostics().is_empty());
1671    }
1672
1673    #[test]
1674    fn bare_exponent_is_nan_with_diagnostic() {
1675        let mut lx = Lexer::new("1e", crate::FileId(0));
1676        let tok = lx.next_token();
1677        assert_eq!(tok.span, Span::new(F, 0, 2).unwrap());
1678        match tok.kind {
1679            TokenKind::NumberLiteral(v) => assert!(v.is_nan()),
1680            other => panic!("expected NumberLiteral(NaN), got {other:?}"),
1681        }
1682        let diags = lx.into_diagnostics();
1683        assert_eq!(diags.len(), 1);
1684        assert_eq!(diags[0].span, Span::new(F, 1, 2).unwrap());
1685        assert_eq!(diags[0].message, "missing digits in exponent");
1686    }
1687
1688    #[test]
1689    fn signed_exponent_with_no_digits_is_nan_with_diagnostic() {
1690        let mut lx = Lexer::new("3.14e+", crate::FileId(0));
1691        let tok = lx.next_token();
1692        assert_eq!(tok.span, Span::new(F, 0, 6).unwrap());
1693        match tok.kind {
1694            TokenKind::NumberLiteral(v) => assert!(v.is_nan()),
1695            other => panic!("expected NumberLiteral(NaN), got {other:?}"),
1696        }
1697        let diags = lx.into_diagnostics();
1698        assert_eq!(diags.len(), 1);
1699        assert_eq!(diags[0].span, Span::new(F, 4, 6).unwrap());
1700    }
1701
1702    #[test]
1703    fn renders_bare_exponent_diagnostic() {
1704        let source = "1e";
1705        let mut lx = Lexer::new(source, crate::FileId(0));
1706        let _ = lx.next_token();
1707        let diags = lx.into_diagnostics();
1708        let rendered = diagnostics::render(&diags[0], &sources(source));
1709        insta::assert_snapshot!(rendered);
1710    }
1711
1712    fn expect_bigint(source: &str, digits: &str, span: Span) {
1713        let (tok, eof, diags) = tokenize_one(source);
1714        assert_eq!(tok.span, span, "span mismatch for {source:?}");
1715        match tok.kind {
1716            TokenKind::BigIntLiteral(ref s) => {
1717                assert_eq!(s, digits, "digits mismatch for {source:?}");
1718            }
1719            other => panic!("expected BigIntLiteral for {source:?}, got {other:?}"),
1720        }
1721        assert_eq!(eof.kind, TokenKind::Eof);
1722        assert!(
1723            diags.is_empty(),
1724            "unexpected diagnostics for {source:?}: {diags:?}"
1725        );
1726    }
1727
1728    #[test]
1729    fn lex_bigint_simple() {
1730        expect_bigint("42n", "42", Span::new(F, 0, 3).unwrap());
1731    }
1732
1733    #[test]
1734    fn lex_bigint_zero() {
1735        expect_bigint("0n", "0", Span::new(F, 0, 2).unwrap());
1736    }
1737
1738    #[test]
1739    fn lex_bigint_large_beyond_u64() {
1740        // Beyond u64::MAX — preserved as raw string for codegen.
1741        expect_bigint(
1742            "1267650600228229401496703205376n",
1743            "1267650600228229401496703205376",
1744            Span::new(F, 0, 32).unwrap(),
1745        );
1746    }
1747
1748    #[test]
1749    fn lex_bigint_fraction_rejected() {
1750        let mut lx = Lexer::new("3.14n", crate::FileId(0));
1751        let tok = lx.next_token();
1752        assert_eq!(tok.span, Span::new(F, 0, 5).unwrap());
1753        match tok.kind {
1754            TokenKind::BigIntLiteral(ref s) => assert_eq!(s, "3"),
1755            other => panic!("expected BigIntLiteral (recovery), got {other:?}"),
1756        }
1757        let diags = lx.into_diagnostics();
1758        assert_eq!(diags.len(), 1);
1759        assert_eq!(diags[0].span, Span::new(F, 0, 5).unwrap());
1760        assert!(
1761            diags[0]
1762                .message
1763                .contains("bigint literal cannot have a fractional or exponent part"),
1764            "unexpected message: {:?}",
1765            diags[0].message
1766        );
1767    }
1768
1769    #[test]
1770    fn lex_bigint_exponent_rejected() {
1771        let mut lx = Lexer::new("1e10n", crate::FileId(0));
1772        let tok = lx.next_token();
1773        assert_eq!(tok.span, Span::new(F, 0, 5).unwrap());
1774        match tok.kind {
1775            TokenKind::BigIntLiteral(ref s) => assert_eq!(s, "1"),
1776            other => panic!("expected BigIntLiteral (recovery), got {other:?}"),
1777        }
1778        let diags = lx.into_diagnostics();
1779        assert_eq!(diags.len(), 1);
1780        assert!(
1781            diags[0]
1782                .message
1783                .contains("bigint literal cannot have a fractional or exponent part"),
1784            "unexpected message: {:?}",
1785            diags[0].message
1786        );
1787    }
1788
1789    #[test]
1790    fn lex_hex_literal() {
1791        expect_number("0xff", 255.0, Span::new(F, 0, 4).unwrap());
1792        expect_number("0XFF", 255.0, Span::new(F, 0, 4).unwrap());
1793        expect_number("0x0", 0.0, Span::new(F, 0, 3).unwrap());
1794        expect_number("0x10", 16.0, Span::new(F, 0, 4).unwrap());
1795    }
1796
1797    #[test]
1798    fn lex_binary_literal() {
1799        expect_number("0b1010", 10.0, Span::new(F, 0, 6).unwrap());
1800        expect_number("0B1", 1.0, Span::new(F, 0, 3).unwrap());
1801        expect_number("0b0", 0.0, Span::new(F, 0, 3).unwrap());
1802    }
1803
1804    #[test]
1805    fn lex_octal_literal() {
1806        expect_number("0o17", 15.0, Span::new(F, 0, 4).unwrap());
1807        expect_number("0O7", 7.0, Span::new(F, 0, 3).unwrap());
1808        expect_number("0o0", 0.0, Span::new(F, 0, 3).unwrap());
1809    }
1810
1811    #[test]
1812    fn lex_radix_bigint() {
1813        expect_bigint("0xffn", "255", Span::new(F, 0, 5).unwrap());
1814        expect_bigint("0b101n", "5", Span::new(F, 0, 6).unwrap());
1815        expect_bigint("0o17n", "15", Span::new(F, 0, 5).unwrap());
1816    }
1817
1818    #[test]
1819    fn lex_hex_bigint_beyond_u64() {
1820        expect_bigint(
1821            "0xffffffffffffffffn",
1822            "18446744073709551615",
1823            Span::new(F, 0, 19).unwrap(),
1824        );
1825    }
1826
1827    #[test]
1828    fn lex_radix_missing_digits() {
1829        for src in ["0x", "0b", "0o"] {
1830            let mut lx = Lexer::new(src, crate::FileId(0));
1831            let tok = lx.next_token();
1832            assert_eq!(tok.span, Span::new(F, 0, 2).unwrap(), "span for {src:?}");
1833            assert!(
1834                matches!(tok.kind, TokenKind::NumberLiteral(v) if v.is_nan()),
1835                "expected NaN recovery for {src:?}, got {:?}",
1836                tok.kind
1837            );
1838            let diags = lx.into_diagnostics();
1839            assert_eq!(diags.len(), 1, "diags for {src:?}");
1840            assert!(
1841                diags[0].message.contains("missing digits after"),
1842                "unexpected message for {src:?}: {:?}",
1843                diags[0].message
1844            );
1845        }
1846    }
1847
1848    fn expect_single_leading_zero_error(source: &str, message: &str, help: &str) {
1849        let (_, eof, diags) = tokenize_one(source);
1850        assert_eq!(
1851            eof.kind,
1852            TokenKind::Eof,
1853            "{source:?} should lex as one token"
1854        );
1855        assert_eq!(diags.len(), 1, "diags for {source:?}: {diags:?}");
1856        assert_eq!(diags[0].message, message, "message for {source:?}");
1857        assert_eq!(diags[0].help, vec![help.to_string()], "help for {source:?}");
1858        let source_len = u32::try_from(source.len()).unwrap();
1859        assert_eq!(diags[0].span, Span::new(F, 0, source_len).unwrap());
1860    }
1861
1862    #[test]
1863    fn lex_legacy_octal_rejected() {
1864        expect_single_leading_zero_error(
1865            "010",
1866            "legacy octal literal `010` is not allowed",
1867            "write `0o10` for octal, or `10` for decimal",
1868        );
1869        expect_single_leading_zero_error(
1870            "010n",
1871            "legacy octal literal `010n` is not allowed",
1872            "write `0o10n` for octal, or `10n` for decimal",
1873        );
1874        expect_single_leading_zero_error(
1875            "000",
1876            "legacy octal literal `000` is not allowed",
1877            "write `0`",
1878        );
1879    }
1880
1881    #[test]
1882    fn lex_leading_zero_decimal_rejected() {
1883        expect_single_leading_zero_error(
1884            "09",
1885            "decimal literal `09` cannot have a leading zero",
1886            "write `9`",
1887        );
1888        expect_single_leading_zero_error(
1889            "08.5",
1890            "decimal literal `08.5` cannot have a leading zero",
1891            "write `8.5`",
1892        );
1893        expect_single_leading_zero_error(
1894            "00.5",
1895            "decimal literal `00.5` cannot have a leading zero",
1896            "write `0.5`",
1897        );
1898        expect_single_leading_zero_error(
1899            "07e1",
1900            "decimal literal `07e1` cannot have a leading zero",
1901            "write `7e1`",
1902        );
1903        expect_single_leading_zero_error(
1904            "08n",
1905            "decimal literal `08n` cannot have a leading zero",
1906            "write `8n`",
1907        );
1908    }
1909
1910    #[test]
1911    fn lex_single_leading_zero_accepted() {
1912        expect_number("0", 0.0, Span::new(F, 0, 1).unwrap());
1913        expect_number("0.5", 0.5, Span::new(F, 0, 3).unwrap());
1914        expect_number("0e1", 0.0, Span::new(F, 0, 3).unwrap());
1915        expect_number("0.010", 0.01, Span::new(F, 0, 5).unwrap());
1916        expect_number("1e010", 1e10, Span::new(F, 0, 5).unwrap());
1917        expect_number("0x010", 16.0, Span::new(F, 0, 5).unwrap());
1918        expect_bigint("0n", "0", Span::new(F, 0, 2).unwrap());
1919    }
1920
1921    #[test]
1922    fn lex_numeric_separators() {
1923        expect_number("1_000", 1000.0, Span::new(F, 0, 5).unwrap());
1924        expect_number("1_000.5_5", 1000.55, Span::new(F, 0, 9).unwrap());
1925        expect_number("1e1_0", 1e10, Span::new(F, 0, 5).unwrap());
1926        expect_number("0.0_1", 0.01, Span::new(F, 0, 5).unwrap());
1927        expect_number("0xF_F", 255.0, Span::new(F, 0, 5).unwrap());
1928        expect_number("0b1_0", 2.0, Span::new(F, 0, 5).unwrap());
1929        expect_number("0o1_7", 15.0, Span::new(F, 0, 5).unwrap());
1930        expect_bigint("1_000n", "1000", Span::new(F, 0, 6).unwrap());
1931        expect_bigint("0xF_Fn", "255", Span::new(F, 0, 6).unwrap());
1932        expect_number("0XF_F", 255.0, Span::new(F, 0, 5).unwrap());
1933        expect_number("0B1_0", 2.0, Span::new(F, 0, 5).unwrap());
1934        expect_number("0O1_7", 15.0, Span::new(F, 0, 5).unwrap());
1935        expect_number("1E1_0", 1e10, Span::new(F, 0, 5).unwrap());
1936        expect_number("1e+1_0", 1e10, Span::new(F, 0, 6).unwrap());
1937        expect_number("1e-1_0", 1e-10, Span::new(F, 0, 6).unwrap());
1938    }
1939
1940    #[test]
1941    fn lex_misplaced_numeric_separator_rejected() {
1942        for source in [
1943            "1_", "1_.5", "1._5", "1_e5", "1e_5", "1e+_5", "1_n", "0x_1", "0x1_",
1944        ] {
1945            let (_, eof, diags) = tokenize_one(source);
1946            assert_eq!(
1947                eof.kind,
1948                TokenKind::Eof,
1949                "{source:?} should lex as one token"
1950            );
1951            assert_eq!(diags.len(), 1, "diags for {source:?}: {diags:?}");
1952            assert_eq!(
1953                diags[0].message, "numeric separators are only allowed between digits",
1954                "message for {source:?}"
1955            );
1956        }
1957        let (_, _, diags) = tokenize_one("1__0");
1958        assert_eq!(diags.len(), 1, "diags: {diags:?}");
1959        assert_eq!(
1960            diags[0].message,
1961            "only one numeric separator is allowed between digits"
1962        );
1963        assert_eq!(diags[0].span, Span::new(F, 1, 3).unwrap());
1964        expect_single_leading_zero_error(
1965            "0_1",
1966            "a numeric separator cannot follow a leading `0` in `0_1`",
1967            "write `1`",
1968        );
1969    }
1970
1971    #[test]
1972    fn lex_number_ending_in_decimal_point() {
1973        expect_number("1.", 1.0, Span::new(F, 0, 2).unwrap());
1974        expect_number("1.e5", 1e5, Span::new(F, 0, 4).unwrap());
1975        expect_number("0.", 0.0, Span::new(F, 0, 2).unwrap());
1976        let (tokens, diags) = tokenize_all("1..toString");
1977        assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
1978        assert!(matches!(tokens[0].kind, TokenKind::NumberLiteral(v) if v == 1.0));
1979        assert_eq!(tokens[0].span, Span::new(F, 0, 2).unwrap());
1980        assert_eq!(tokens[1].kind, TokenKind::Dot);
1981    }
1982
1983    #[test]
1984    fn lex_number_starting_with_decimal_point() {
1985        expect_number(".5", 0.5, Span::new(F, 0, 2).unwrap());
1986        expect_number(".5e1", 5.0, Span::new(F, 0, 4).unwrap());
1987        expect_number(".5_5", 0.55, Span::new(F, 0, 4).unwrap());
1988        // `?.` before a digit is a ternary `?` and a number, as in JavaScript.
1989        let (tokens, diags) = tokenize_all("c?.5:1");
1990        assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
1991        assert_eq!(tokens[1].kind, TokenKind::Question);
1992        assert!(matches!(tokens[2].kind, TokenKind::NumberLiteral(v) if v == 0.5));
1993        let (tokens, _) = tokenize_all("c?.x");
1994        assert_eq!(tokens[1].kind, TokenKind::QuestionDot);
1995        // Errors in a leading-dot literal are checked as in any other.
1996        let (_, _, diags) = tokenize_one(".5n");
1997        assert_eq!(diags.len(), 1, "diags: {diags:?}");
1998        assert!(
1999            diags[0]
2000                .message
2001                .contains("bigint literal cannot have a fractional")
2002        );
2003        let (_, _, diags) = tokenize_one(".5_");
2004        assert_eq!(diags.len(), 1, "diags: {diags:?}");
2005        assert_eq!(
2006            diags[0].message,
2007            "numeric separators are only allowed between digits"
2008        );
2009    }
2010
2011    #[test]
2012    fn lex_name_after_decimal_point_rejected() {
2013        // A name starting with `n` or `_` is a name, not a bigint suffix or separator.
2014        for (source, literal_end) in [
2015            ("1.toString", 2),
2016            ("1_0.x", 4),
2017            ("1.$", 2),
2018            ("1.name", 2),
2019            ("1._x", 2),
2020            ("1.\u{e4}", 2),
2021        ] {
2022            let (tokens, diags) = tokenize_all(source);
2023            assert_eq!(diags.len(), 1, "diags for {source:?}: {diags:?}");
2024            assert!(
2025                diags[0].message.contains("is a complete number"),
2026                "message for {source:?}: {diags:?}"
2027            );
2028            assert_eq!(diags[0].span, Span::new(F, 0, literal_end).unwrap());
2029            // The `.` is left as a member access, so the rest parses as written.
2030            assert_eq!(tokens[1].kind, TokenKind::Dot, "tokens for {source:?}");
2031        }
2032        let (_, diags) = tokenize_all("1.toString");
2033        assert_eq!(
2034            diags[0].help,
2035            vec!["wrap the number in parentheses, `(1).toString`, or write `1..toString`"]
2036        );
2037        let (_, diags) = tokenize_all("1.\u{e4}");
2038        assert_eq!(
2039            diags[0].help,
2040            vec!["wrap the number in parentheses, `(1).\u{e4}`, or write `1..\u{e4}`"]
2041        );
2042        // Repeating a literal that is itself an error would suggest broken code.
2043        let (_, diags) = tokenize_all("1_.x");
2044        assert_eq!(diags.len(), 2, "diags: {diags:?}");
2045        assert_eq!(
2046            diags[1].help,
2047            vec!["wrap the number in parentheses, or write a second `.`"]
2048        );
2049    }
2050
2051    #[test]
2052    fn lex_legacy_octal_leaves_decimal_point() {
2053        let (tokens, diags) = tokenize_all("010.toString");
2054        assert_eq!(diags.len(), 1, "diags: {diags:?}");
2055        assert!(diags[0].message.contains("legacy octal literal `010`"));
2056        assert_eq!(tokens[1].kind, TokenKind::Dot);
2057    }
2058
2059    #[test]
2060    fn lex_hex_trailing_junk_splits() {
2061        // `0xfg` lexes as `0xf` (15) then identifier `g`, mirroring `123abc`.
2062        let (tokens, diags) = tokenize_all("0xfg");
2063        assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
2064        assert!(matches!(tokens[0].kind, TokenKind::NumberLiteral(v) if v == 15.0));
2065        assert_eq!(tokens[0].span, Span::new(F, 0, 3).unwrap());
2066        assert_eq!(tokens[1].kind, TokenKind::Identifier);
2067        assert_eq!(tokens[1].span, Span::new(F, 3, 4).unwrap());
2068    }
2069
2070    #[test]
2071    fn lex_bigint_then_dot_method() {
2072        let (tokens, diags) = tokenize_all("42n.toString()");
2073        assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
2074        let kinds: Vec<_> = tokens.iter().map(|t| &t.kind).collect();
2075        assert!(matches!(kinds[0], TokenKind::BigIntLiteral(s) if s == "42"));
2076        assert_eq!(tokens[0].span, Span::new(F, 0, 3).unwrap());
2077        assert_eq!(tokens[1].kind, TokenKind::Dot);
2078    }
2079
2080    #[test]
2081    fn string_double_quoted() {
2082        expect_string("\"hello\"", "hello");
2083    }
2084
2085    #[test]
2086    fn string_single_quoted() {
2087        expect_string("'world'", "world");
2088    }
2089
2090    #[test]
2091    fn string_escaped_double_quote() {
2092        expect_string("\"with \\\"escape\\\"\"", "with \"escape\"");
2093    }
2094
2095    #[test]
2096    fn string_escaped_single_quote() {
2097        expect_string("'it\\'s'", "it's");
2098    }
2099
2100    #[test]
2101    fn string_newline_and_tab_escapes() {
2102        expect_string("\"\\n\\t\"", "\n\t");
2103    }
2104
2105    #[test]
2106    fn string_carriage_return_escape() {
2107        expect_string("\"a\\rb\"", "a\rb");
2108    }
2109
2110    #[test]
2111    fn string_null_and_other_escapes() {
2112        expect_string("\"\\0\\b\\f\\v\"", "\0\u{08}\u{0C}\u{0B}");
2113    }
2114
2115    #[test]
2116    fn string_backslash_escape() {
2117        expect_string("\"a\\\\b\"", "a\\b");
2118    }
2119
2120    #[test]
2121    fn string_unicode_4hex_escape() {
2122        expect_string("\"\\u00e9\"", "é");
2123    }
2124
2125    #[test]
2126    fn string_unicode_brace_escape() {
2127        expect_string("\"\\u{1F600}\"", "\u{1F600}");
2128    }
2129
2130    #[test]
2131    fn string_surrogate_pair_joins() {
2132        // 😀 = U+1F600. Surrogate pair: D83D DE00.
2133        expect_string("\"\\uD83D\\uDE00\"", "\u{1F600}");
2134    }
2135
2136    #[test]
2137    fn string_contains_multibyte_literal() {
2138        expect_string("\"café\"", "café");
2139    }
2140
2141    #[test]
2142    fn string_unterminated_at_eof() {
2143        let (tok, _eof, diags) = tokenize_one("\"hello");
2144        assert!(matches!(tok.kind, TokenKind::StringLiteral(ref s) if s == "hello"));
2145        assert_eq!(diags.len(), 1);
2146        assert_eq!(diags[0].message, "unterminated string literal");
2147    }
2148
2149    #[test]
2150    fn string_raw_newline_is_part_of_the_value() {
2151        let (tok, _eof, diags) = tokenize_one("\"a\nb\"");
2152        assert!(matches!(tok.kind, TokenKind::StringLiteral(ref s) if s == "a\nb"));
2153        assert!(diags.is_empty());
2154    }
2155
2156    #[test]
2157    fn string_raw_crlf_normalised_to_lf() {
2158        let (tok, _eof, diags) = tokenize_one("\"a\r\nb\"");
2159        assert!(matches!(tok.kind, TokenKind::StringLiteral(ref s) if s == "a\nb"));
2160        assert!(diags.is_empty());
2161    }
2162
2163    #[test]
2164    fn string_still_unterminated_when_newline_reaches_eof() {
2165        let (tok, _eof, diags) = tokenize_one("\"abc\n");
2166        assert!(matches!(tok.kind, TokenKind::StringLiteral(ref s) if s == "abc\n"));
2167        assert_eq!(diags.len(), 1);
2168        assert_eq!(diags[0].message, "unterminated string literal");
2169    }
2170
2171    #[test]
2172    fn string_unknown_escape_diagnoses_but_keeps_character() {
2173        let (tok, _eof, diags) = tokenize_one("\"\\q\"");
2174        assert!(matches!(tok.kind, TokenKind::StringLiteral(ref s) if s == "q"));
2175        assert_eq!(diags.len(), 1);
2176        assert_eq!(diags[0].message, "unknown escape sequence `\\q`");
2177    }
2178
2179    #[test]
2180    fn string_escaped_slash_is_a_slash() {
2181        let (tok, _eof, diags) = tokenize_one("\"a\\/b\"");
2182        assert!(matches!(tok.kind, TokenKind::StringLiteral(ref s) if s == "a/b"));
2183        assert!(diags.is_empty());
2184    }
2185
2186    #[test]
2187    fn string_lone_surrogates_are_kept() {
2188        for (source, unit) in [
2189            ("\"\\uD83D\"", 0xD83D),
2190            ("\"\\uDE00\"", 0xDE00),
2191            ("\"\\u{D800}\"", 0xD800),
2192        ] {
2193            let (tok, _, diags) = tokenize_one(source);
2194            assert!(diags.is_empty(), "{source}: {diags:?}");
2195            let TokenKind::StringLiteral(text) = tok.kind else {
2196                panic!("{source}: expected a string literal");
2197            };
2198            assert_eq!(crate::literal_units::literal_units(&text), vec![unit]);
2199        }
2200    }
2201
2202    #[test]
2203    fn string_marker_code_point_is_kept() {
2204        let (tok, _, diags) = tokenize_one("\"\\u{10FFFE}\u{10FFFE}\"");
2205        assert!(diags.is_empty());
2206        let TokenKind::StringLiteral(text) = tok.kind else {
2207            panic!("expected a string literal");
2208        };
2209        assert_eq!(
2210            crate::literal_units::literal_units(&text),
2211            "\u{10FFFE}\u{10FFFE}".encode_utf16().collect::<Vec<_>>()
2212        );
2213    }
2214
2215    #[test]
2216    fn string_brace_escape_empty_diagnosed() {
2217        let (_, _, diags) = tokenize_one("\"\\u{}\"");
2218        assert_eq!(diags.len(), 1);
2219        assert!(diags[0].message.starts_with("invalid unicode escape"));
2220    }
2221
2222    #[test]
2223    fn string_brace_escape_too_big_diagnosed() {
2224        let (_, _, diags) = tokenize_one("\"\\u{110000}\"");
2225        assert_eq!(diags.len(), 1);
2226        assert!(diags[0].message.contains("exceeds U+10FFFF"));
2227    }
2228
2229    #[test]
2230    fn string_brace_escape_unclosed_diagnosed() {
2231        let (_, _, diags) = tokenize_one("\"\\u{1F600\"");
2232        // At minimum the first diagnostic is about the unicode escape; unterminated string may follow.
2233        assert!(!diags.is_empty());
2234        assert!(diags[0].message.contains("invalid unicode escape"));
2235    }
2236
2237    #[test]
2238    fn renders_unterminated_string_diagnostic() {
2239        let source = "let s = \"hello";
2240        let (_, diags) = tokenize_all(source);
2241        let unterminated = diags
2242            .iter()
2243            .find(|d| d.message == "unterminated string literal")
2244            .expect("expected an unterminated-string diagnostic");
2245        let rendered = diagnostics::render(unterminated, &sources(source));
2246        insta::assert_snapshot!(rendered);
2247    }
2248
2249    fn expect_template(source: &str, expected: &[TokenKind]) {
2250        let (tokens, diags) = tokenize_all(source);
2251        assert!(
2252            diags.is_empty(),
2253            "unexpected diagnostics for {source:?}: {diags:?}"
2254        );
2255        let kinds: Vec<TokenKind> = tokens.iter().map(|t| t.kind.clone()).collect();
2256        let mut want = expected.to_vec();
2257        want.push(TokenKind::Eof);
2258        assert_eq!(kinds, want, "token stream mismatch for {source:?}");
2259    }
2260
2261    #[test]
2262    fn template_no_substitution_plain() {
2263        expect_template(
2264            "`hello`",
2265            &[TokenKind::TemplateNoSubstitution("hello".to_string())],
2266        );
2267    }
2268
2269    #[test]
2270    fn template_no_substitution_empty() {
2271        expect_template("``", &[TokenKind::TemplateNoSubstitution(String::new())]);
2272    }
2273
2274    #[test]
2275    fn template_no_substitution_with_dollar_not_followed_by_brace() {
2276        expect_template(
2277            "`a$b`",
2278            &[TokenKind::TemplateNoSubstitution("a$b".to_string())],
2279        );
2280    }
2281
2282    #[test]
2283    fn template_single_interpolation() {
2284        expect_template(
2285            "`a${x}b`",
2286            &[
2287                TokenKind::TemplateHead("a".to_string()),
2288                TokenKind::Identifier,
2289                TokenKind::TemplateTail("b".to_string()),
2290            ],
2291        );
2292    }
2293
2294    #[test]
2295    fn template_two_interpolations() {
2296        expect_template(
2297            "`a${x}b${y}c`",
2298            &[
2299                TokenKind::TemplateHead("a".to_string()),
2300                TokenKind::Identifier,
2301                TokenKind::TemplateMiddle("b".to_string()),
2302                TokenKind::Identifier,
2303                TokenKind::TemplateTail("c".to_string()),
2304            ],
2305        );
2306    }
2307
2308    #[test]
2309    fn template_empty_head_and_tail() {
2310        expect_template(
2311            "`${x}`",
2312            &[
2313                TokenKind::TemplateHead(String::new()),
2314                TokenKind::Identifier,
2315                TokenKind::TemplateTail(String::new()),
2316            ],
2317        );
2318    }
2319
2320    #[test]
2321    fn template_object_literal_inside_interpolation_does_not_close() {
2322        // `{` and `}` inside `${ }` adjust the frame counter, not the interpolation boundary.
2323        let (tokens, diags) = tokenize_all("`x=${ {a:1} }`");
2324        assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
2325        let kinds: Vec<TokenKind> = tokens.iter().map(|t| t.kind.clone()).collect();
2326        assert_eq!(
2327            kinds,
2328            vec![
2329                TokenKind::TemplateHead("x=".to_string()),
2330                TokenKind::LeftBrace,
2331                TokenKind::Identifier,
2332                TokenKind::Colon,
2333                TokenKind::NumberLiteral(1.0),
2334                TokenKind::RightBrace,
2335                TokenKind::TemplateTail(String::new()),
2336                TokenKind::Eof,
2337            ]
2338        );
2339    }
2340
2341    #[test]
2342    fn template_nested_in_interpolation() {
2343        // Nested template: outer pushes frame 0, inner backtick pushes frame 1;
2344        // each `}` pops back to its level. Tests multi-frame correctness.
2345        expect_template(
2346            "`out${`in${x}`}end`",
2347            &[
2348                TokenKind::TemplateHead("out".to_string()),
2349                TokenKind::TemplateHead("in".to_string()),
2350                TokenKind::Identifier,
2351                TokenKind::TemplateTail(String::new()),
2352                TokenKind::TemplateTail("end".to_string()),
2353            ],
2354        );
2355    }
2356
2357    #[test]
2358    fn template_escapes_inside_part() {
2359        // \` and \$ are template-only; others delegate to read_escape.
2360        expect_template(
2361            "`a\\nb\\`c\\${d}\\\\e\\u{1F600}`",
2362            &[TokenKind::TemplateNoSubstitution(
2363                "a\nb`c${d}\\e\u{1F600}".to_string(),
2364            )],
2365        );
2366    }
2367
2368    #[test]
2369    fn template_multiline_lf_preserved() {
2370        expect_template(
2371            "`line1\nline2`",
2372            &[TokenKind::TemplateNoSubstitution(
2373                "line1\nline2".to_string(),
2374            )],
2375        );
2376    }
2377
2378    #[test]
2379    fn template_multiline_crlf_normalised_to_lf() {
2380        expect_template(
2381            "`a\r\nb`",
2382            &[TokenKind::TemplateNoSubstitution("a\nb".to_string())],
2383        );
2384    }
2385
2386    #[test]
2387    fn template_unterminated_no_substitution_diagnosed() {
2388        let (tokens, diags) = tokenize_all("`hello");
2389        assert_eq!(diags.len(), 1, "diags: {diags:?}");
2390        assert_eq!(diags[0].message, "unterminated template literal");
2391        assert!(matches!(
2392            &tokens[0].kind,
2393            TokenKind::TemplateNoSubstitution(s) if s == "hello"
2394        ));
2395    }
2396
2397    #[test]
2398    fn template_unterminated_after_head_diagnosed() {
2399        let (_, diags) = tokenize_all("`a${x");
2400        // The interpolation is still open at EOF — lexer is not in template-scanning mode,
2401        // so no unterminated-template diagnostic; the parser catches the unclosed `${`.
2402        assert!(diags.is_empty(), "unexpected lexer diags: {diags:?}");
2403    }
2404
2405    #[test]
2406    fn template_unterminated_after_interpolation_diagnosed() {
2407        let (_, diags) = tokenize_all("`a${x}b");
2408        assert_eq!(diags.len(), 1);
2409        assert_eq!(diags[0].message, "unterminated template literal");
2410    }
2411
2412    #[test]
2413    fn renders_unterminated_template_diagnostic() {
2414        let source = "let s = `hello";
2415        let (_, diags) = tokenize_all(source);
2416        let unterminated = diags
2417            .iter()
2418            .find(|d| d.message == "unterminated template literal")
2419            .expect("expected an unterminated-template diagnostic");
2420        let rendered = diagnostics::render(unterminated, &sources(source));
2421        insta::assert_snapshot!(rendered);
2422    }
2423
2424    #[test]
2425    fn lex_true_false_null() {
2426        let (tokens, diags) = tokenize_all("true false null");
2427        assert!(diags.is_empty());
2428        assert_eq!(tokens[0].kind, TokenKind::BooleanLiteral(true));
2429        assert_eq!(tokens[1].kind, TokenKind::BooleanLiteral(false));
2430        assert_eq!(tokens[2].kind, TokenKind::NullLiteral);
2431        assert_eq!(tokens[3].kind, TokenKind::Eof);
2432    }
2433
2434    #[test]
2435    fn literal_prefix_words_are_identifiers() {
2436        for src in ["trueValue", "falseness", "nullable"] {
2437            let (tok, _eof, diags) = tokenize_one(src);
2438            assert!(diags.is_empty(), "diagnostics for {src:?}");
2439            assert_eq!(
2440                tok.kind,
2441                TokenKind::Identifier,
2442                "expected Identifier for {src:?}"
2443            );
2444        }
2445    }
2446
2447    #[test]
2448    fn lex_all_mvp_keywords() {
2449        let keywords = [
2450            ("let", TokenKind::Let),
2451            ("const", TokenKind::Const),
2452            ("function", TokenKind::Function),
2453            ("if", TokenKind::If),
2454            ("else", TokenKind::Else),
2455            ("while", TokenKind::While),
2456            ("return", TokenKind::Return),
2457            ("void", TokenKind::Void),
2458            ("interface", TokenKind::Interface),
2459            ("export", TokenKind::Export),
2460            ("typeof", TokenKind::Typeof),
2461            ("import", TokenKind::Import),
2462            ("new", TokenKind::New),
2463        ];
2464        for (src, expected) in keywords {
2465            let (tok, _eof, diags) = tokenize_one(src);
2466            assert!(diags.is_empty(), "diagnostics for {src:?}");
2467            assert_eq!(tok.kind, expected, "kind mismatch for {src:?}");
2468            assert_eq!(tok.span, Span::new(F, 0, src.len() as u32).unwrap());
2469        }
2470    }
2471
2472    #[test]
2473    fn contextual_keywords_lex_as_identifiers() {
2474        for src in ["type", "is", "from", "as", "of"] {
2475            let (tok, _eof, diags) = tokenize_one(src);
2476            assert!(diags.is_empty(), "diagnostics for {src:?}");
2477            assert_eq!(tok.kind, TokenKind::Identifier, "kind mismatch for {src:?}");
2478        }
2479    }
2480
2481    #[test]
2482    fn lex_ascii_identifiers() {
2483        for src in ["foo", "_bar", "$baz", "x1", "_123", "camelCase"] {
2484            let (tok, _eof, diags) = tokenize_one(src);
2485            assert!(diags.is_empty(), "diagnostics for {src:?}");
2486            assert_eq!(tok.kind, TokenKind::Identifier, "kind mismatch for {src:?}");
2487            assert_eq!(tok.span, Span::new(F, 0, src.len() as u32).unwrap());
2488        }
2489    }
2490
2491    #[test]
2492    fn lex_unicode_identifier() {
2493        let (tok, _eof, diags) = tokenize_one("café");
2494        assert!(diags.is_empty());
2495        assert_eq!(tok.kind, TokenKind::Identifier);
2496        assert_eq!(tok.span, Span::new(F, 0, "café".len() as u32).unwrap());
2497    }
2498
2499    #[test]
2500    fn keyword_prefix_is_identifier() {
2501        let (tok, _eof, diags) = tokenize_one("letx");
2502        assert!(diags.is_empty());
2503        assert_eq!(tok.kind, TokenKind::Identifier);
2504    }
2505
2506    #[test]
2507    fn lex_arithmetic_operators() {
2508        expect_single_token("+", TokenKind::Plus, Span::new(F, 0, 1).unwrap());
2509        expect_single_token("-", TokenKind::Minus, Span::new(F, 0, 1).unwrap());
2510        expect_single_token("*", TokenKind::Star, Span::new(F, 0, 1).unwrap());
2511        // `/` after an identifier is division; standalone it would start a regex literal.
2512        let (toks, diags) = tokenize_all("a / b");
2513        assert!(diags.is_empty(), "diags: {diags:?}");
2514        assert_eq!(toks[1].kind, TokenKind::Slash);
2515        expect_single_token("%", TokenKind::Percent, Span::new(F, 0, 1).unwrap());
2516    }
2517
2518    #[test]
2519    fn lex_compound_assignment_operators() {
2520        expect_single_token("+=", TokenKind::PlusEquals, Span::new(F, 0, 2).unwrap());
2521        expect_single_token("-=", TokenKind::MinusEquals, Span::new(F, 0, 2).unwrap());
2522        expect_single_token("*=", TokenKind::StarEquals, Span::new(F, 0, 2).unwrap());
2523        // `/=` after an identifier is compound-assign; standalone it would start a regex literal.
2524        let (toks, diags) = tokenize_all("a /= 2");
2525        assert!(diags.is_empty(), "diags: {diags:?}");
2526        assert_eq!(toks[1].kind, TokenKind::SlashEquals);
2527        expect_single_token("%=", TokenKind::PercentEquals, Span::new(F, 0, 2).unwrap());
2528        expect_single_token("**", TokenKind::StarStar, Span::new(F, 0, 2).unwrap());
2529        expect_single_token(
2530            "**=",
2531            TokenKind::StarStarEquals,
2532            Span::new(F, 0, 3).unwrap(),
2533        );
2534        // `**=` must beat `**` + `=` in greedy dispatch.
2535        let (toks, diags) = tokenize_all("a **= 2");
2536        assert!(diags.is_empty(), "diags: {diags:?}");
2537        assert_eq!(toks[1].kind, TokenKind::StarStarEquals);
2538        let (toks, diags) = tokenize_all("a ** b");
2539        assert!(diags.is_empty(), "diags: {diags:?}");
2540        assert_eq!(toks[1].kind, TokenKind::StarStar);
2541    }
2542
2543    #[test]
2544    fn lex_bitwise_operators() {
2545        for (source, kind) in [
2546            ("&", TokenKind::Amp),
2547            ("&=", TokenKind::AmpEquals),
2548            ("|", TokenKind::Pipe),
2549            ("|=", TokenKind::PipeEquals),
2550            ("^", TokenKind::Caret),
2551            ("^=", TokenKind::CaretEquals),
2552            ("~", TokenKind::Tilde),
2553        ] {
2554            expect_single_token(source, kind, Span::new(F, 0, source.len() as u32).unwrap());
2555        }
2556        let (tokens, diags) = tokenize_all("~ /x/.test('x')");
2557        assert!(diags.is_empty(), "{diags:?}");
2558        assert!(matches!(tokens[1].kind, TokenKind::RegexLiteral { .. }));
2559    }
2560
2561    #[test]
2562    fn lex_regex_literal_basic() {
2563        // At start of input `last_significant_token` is `None`; `is_regex_context` returns true.
2564        expect_single_token(
2565            "/abc/",
2566            TokenKind::RegexLiteral {
2567                source: "abc".to_string(),
2568                flags: String::new(),
2569            },
2570            Span::new(F, 0, 5).unwrap(),
2571        );
2572    }
2573
2574    #[test]
2575    fn lex_regex_literal_with_flags() {
2576        let (toks, diags) = tokenize_all("/foo/gi");
2577        assert!(diags.is_empty(), "diags: {diags:?}");
2578        assert_eq!(
2579            toks[0].kind,
2580            TokenKind::RegexLiteral {
2581                source: "foo".to_string(),
2582                flags: "gi".to_string(),
2583            }
2584        );
2585    }
2586
2587    #[test]
2588    fn lex_regex_after_open_paren_is_literal() {
2589        let (toks, diags) = tokenize_all("f(/x/)");
2590        assert!(diags.is_empty(), "diags: {diags:?}");
2591        // [f, `(`, /x/, `)`, Eof]
2592        assert_eq!(toks[0].kind, TokenKind::Identifier);
2593        assert_eq!(toks[1].kind, TokenKind::LeftParen);
2594        assert!(matches!(toks[2].kind, TokenKind::RegexLiteral { .. }));
2595        assert_eq!(toks[3].kind, TokenKind::RightParen);
2596    }
2597
2598    #[test]
2599    fn lex_regex_after_equals_is_literal() {
2600        let (toks, diags) = tokenize_all("let r = /a/g;");
2601        assert!(diags.is_empty(), "diags: {diags:?}");
2602        assert!(toks.iter().any(|t| matches!(
2603            &t.kind,
2604            TokenKind::RegexLiteral { source, flags } if source == "a" && flags == "g"
2605        )));
2606    }
2607
2608    #[test]
2609    fn lex_regex_after_return_is_literal() {
2610        let (toks, diags) = tokenize_all("return /x/;");
2611        assert!(diags.is_empty(), "diags: {diags:?}");
2612        assert_eq!(toks[0].kind, TokenKind::Return);
2613        assert!(matches!(toks[1].kind, TokenKind::RegexLiteral { .. }));
2614    }
2615
2616    #[test]
2617    fn lex_regex_after_typeof_is_literal() {
2618        let (toks, diags) = tokenize_all("typeof /x/");
2619        assert!(diags.is_empty(), "diags: {diags:?}");
2620        assert_eq!(toks[0].kind, TokenKind::Typeof);
2621        assert!(matches!(toks[1].kind, TokenKind::RegexLiteral { .. }));
2622    }
2623
2624    #[test]
2625    fn lex_division_after_identifier_stays_division() {
2626        let (toks, diags) = tokenize_all("a / b");
2627        assert!(diags.is_empty(), "diags: {diags:?}");
2628        assert_eq!(toks[0].kind, TokenKind::Identifier);
2629        assert_eq!(toks[1].kind, TokenKind::Slash);
2630        assert_eq!(toks[2].kind, TokenKind::Identifier);
2631    }
2632
2633    #[test]
2634    fn lex_division_after_close_paren_stays_division() {
2635        let (toks, diags) = tokenize_all("(x) / 2");
2636        assert!(diags.is_empty(), "diags: {diags:?}");
2637        assert!(toks.iter().any(|t| t.kind == TokenKind::Slash));
2638        assert!(
2639            !toks
2640                .iter()
2641                .any(|t| matches!(t.kind, TokenKind::RegexLiteral { .. }))
2642        );
2643    }
2644
2645    #[test]
2646    fn lex_division_chain_a_div_b_div_c() {
2647        let (toks, diags) = tokenize_all("a / b / c");
2648        assert!(diags.is_empty(), "diags: {diags:?}");
2649        let kinds: Vec<&TokenKind> = toks.iter().map(|t| &t.kind).collect();
2650        assert_eq!(
2651            kinds,
2652            vec![
2653                &TokenKind::Identifier,
2654                &TokenKind::Slash,
2655                &TokenKind::Identifier,
2656                &TokenKind::Slash,
2657                &TokenKind::Identifier,
2658                &TokenKind::Eof,
2659            ]
2660        );
2661    }
2662
2663    #[test]
2664    fn lex_regex_escaped_slash_does_not_close() {
2665        let (toks, diags) = tokenize_all("/a\\/b/");
2666        assert!(diags.is_empty(), "diags: {diags:?}");
2667        assert_eq!(
2668            toks[0].kind,
2669            TokenKind::RegexLiteral {
2670                source: "a\\/b".to_string(),
2671                flags: String::new(),
2672            }
2673        );
2674    }
2675
2676    #[test]
2677    fn lex_regex_slash_inside_char_class_does_not_close() {
2678        let (toks, diags) = tokenize_all("/[/]/");
2679        assert!(diags.is_empty(), "diags: {diags:?}");
2680        assert_eq!(
2681            toks[0].kind,
2682            TokenKind::RegexLiteral {
2683                source: "[/]".to_string(),
2684                flags: String::new(),
2685            }
2686        );
2687    }
2688
2689    #[test]
2690    fn lex_regex_unterminated_at_newline_is_diagnosed() {
2691        let (toks, diags) = tokenize_all("/abc\n");
2692        assert!(
2693            !diags.is_empty()
2694                && diags
2695                    .iter()
2696                    .any(|d| d.message.contains("unterminated regex literal")),
2697            "expected unterminated diagnostic, got {diags:?}"
2698        );
2699        assert!(matches!(toks[0].kind, TokenKind::RegexLiteral { .. }));
2700    }
2701
2702    #[test]
2703    fn lex_regex_unterminated_at_eof_is_diagnosed() {
2704        let (_toks, diags) = tokenize_all("/abc");
2705        assert!(
2706            diags
2707                .iter()
2708                .any(|d| d.message.contains("unterminated regex literal")),
2709            "expected unterminated diagnostic, got {diags:?}"
2710        );
2711    }
2712
2713    #[test]
2714    fn lex_regex_after_regex_is_division() {
2715        // A regex literal is an operand; the `/` that follows is division.
2716        let (toks, diags) = tokenize_all("/x/ / 2");
2717        assert!(diags.is_empty(), "diags: {diags:?}");
2718        assert!(matches!(toks[0].kind, TokenKind::RegexLiteral { .. }));
2719        assert_eq!(toks[1].kind, TokenKind::Slash);
2720    }
2721
2722    #[test]
2723    fn lex_block_comment_remains_unaffected() {
2724        let (toks, diags) = tokenize_all("/* a / b */ x");
2725        assert!(diags.is_empty(), "diags: {diags:?}");
2726        assert_eq!(toks[0].kind, TokenKind::Identifier);
2727    }
2728
2729    #[test]
2730    fn lex_line_comment_remains_unaffected() {
2731        let (toks, diags) = tokenize_all("// /x/\n");
2732        assert!(diags.is_empty(), "diags: {diags:?}");
2733        // Line comment + newline are trivia; only Newline and Eof tokens remain.
2734        assert!(
2735            toks.iter()
2736                .all(|t| matches!(t.kind, TokenKind::Newline | TokenKind::Eof))
2737        );
2738        assert!(
2739            !toks
2740                .iter()
2741                .any(|t| matches!(t.kind, TokenKind::RegexLiteral { .. }))
2742        );
2743    }
2744
2745    #[test]
2746    fn lex_postfix_increment_decrement() {
2747        // `++` must beat `+=` in greedy dispatch; `--` must beat `-=`.
2748        expect_single_token("++", TokenKind::PlusPlus, Span::new(F, 0, 2).unwrap());
2749        expect_single_token("--", TokenKind::MinusMinus, Span::new(F, 0, 2).unwrap());
2750        let (toks, diags) = tokenize_all("+=");
2751        assert!(diags.is_empty());
2752        assert_eq!(toks[0].kind, TokenKind::PlusEquals);
2753        let (toks, diags) = tokenize_all("-=");
2754        assert!(diags.is_empty());
2755        assert_eq!(toks[0].kind, TokenKind::MinusEquals);
2756        // Three pluses lex as `++ +` (greedy longest-match grabs the first two).
2757        let (toks, diags) = tokenize_all("+++");
2758        assert!(diags.is_empty());
2759        assert_eq!(toks[0].kind, TokenKind::PlusPlus);
2760        assert_eq!(toks[1].kind, TokenKind::Plus);
2761    }
2762
2763    #[test]
2764    fn lex_equality_operators_longest_match() {
2765        expect_single_token("=", TokenKind::Equals, Span::new(F, 0, 1).unwrap());
2766        expect_single_token("==", TokenKind::EqEq, Span::new(F, 0, 2).unwrap());
2767        expect_single_token("===", TokenKind::EqEqEq, Span::new(F, 0, 3).unwrap());
2768        expect_single_token("!", TokenKind::Bang, Span::new(F, 0, 1).unwrap());
2769        expect_single_token("!=", TokenKind::BangEq, Span::new(F, 0, 2).unwrap());
2770        expect_single_token("!==", TokenKind::BangEqEq, Span::new(F, 0, 3).unwrap());
2771    }
2772
2773    #[test]
2774    fn lex_arrow_token() {
2775        expect_single_token("=>", TokenKind::Arrow, Span::new(F, 0, 2).unwrap());
2776    }
2777
2778    #[test]
2779    fn lex_comparison_operators() {
2780        expect_single_token("<", TokenKind::LessThan, Span::new(F, 0, 1).unwrap());
2781        expect_single_token(">", TokenKind::GreaterThan, Span::new(F, 0, 1).unwrap());
2782        expect_single_token("<=", TokenKind::LessEquals, Span::new(F, 0, 2).unwrap());
2783        expect_single_token(">=", TokenKind::GreaterEquals, Span::new(F, 0, 2).unwrap());
2784    }
2785
2786    #[test]
2787    fn lex_logical_operators() {
2788        expect_single_token("&&", TokenKind::AmpAmp, Span::new(F, 0, 2).unwrap());
2789        expect_single_token("||", TokenKind::PipePipe, Span::new(F, 0, 2).unwrap());
2790    }
2791
2792    #[test]
2793    fn lex_pipe_longest_match() {
2794        expect_single_token("|", TokenKind::Pipe, Span::new(F, 0, 1).unwrap());
2795        expect_single_token("||", TokenKind::PipePipe, Span::new(F, 0, 2).unwrap());
2796    }
2797
2798    #[test]
2799    fn lex_dot_standalone_and_after_identifier() {
2800        expect_single_token(".", TokenKind::Dot, Span::new(F, 0, 1).unwrap());
2801        let (tokens, diags) = tokenize_all("foo.bar");
2802        assert!(diags.is_empty());
2803        assert_eq!(tokens[0].kind, TokenKind::Identifier);
2804        assert_eq!(tokens[1].kind, TokenKind::Dot);
2805        assert_eq!(tokens[2].kind, TokenKind::Identifier);
2806    }
2807
2808    #[test]
2809    fn single_amp_is_bitwise_and() {
2810        expect_single_token("&", TokenKind::Amp, Span::new(F, 0, 1).unwrap());
2811    }
2812
2813    #[test]
2814    fn lex_all_delimiters() {
2815        expect_single_token("(", TokenKind::LeftParen, Span::new(F, 0, 1).unwrap());
2816        expect_single_token(")", TokenKind::RightParen, Span::new(F, 0, 1).unwrap());
2817        expect_single_token("{", TokenKind::LeftBrace, Span::new(F, 0, 1).unwrap());
2818        expect_single_token("}", TokenKind::RightBrace, Span::new(F, 0, 1).unwrap());
2819        expect_single_token("[", TokenKind::LeftBracket, Span::new(F, 0, 1).unwrap());
2820        expect_single_token("]", TokenKind::RightBracket, Span::new(F, 0, 1).unwrap());
2821        expect_single_token(",", TokenKind::Comma, Span::new(F, 0, 1).unwrap());
2822        expect_single_token(":", TokenKind::Colon, Span::new(F, 0, 1).unwrap());
2823        expect_single_token(";", TokenKind::Semicolon, Span::new(F, 0, 1).unwrap());
2824        expect_single_token("?", TokenKind::Question, Span::new(F, 0, 1).unwrap());
2825    }
2826
2827    #[test]
2828    fn lex_brackets_combined() {
2829        let (tokens, diags) = tokenize_all("({[]})");
2830        assert!(diags.is_empty());
2831        let kinds: Vec<_> = tokens.iter().map(|t| t.kind.clone()).collect();
2832        assert_eq!(
2833            kinds,
2834            vec![
2835                TokenKind::LeftParen,
2836                TokenKind::LeftBrace,
2837                TokenKind::LeftBracket,
2838                TokenKind::RightBracket,
2839                TokenKind::RightBrace,
2840                TokenKind::RightParen,
2841                TokenKind::Eof,
2842            ]
2843        );
2844    }
2845
2846    #[test]
2847    fn lex_newline_lf() {
2848        expect_single_token("\n", TokenKind::Newline, Span::new(F, 0, 1).unwrap());
2849    }
2850
2851    #[test]
2852    fn lex_newline_crlf_is_one_token() {
2853        expect_single_token("\r\n", TokenKind::Newline, Span::new(F, 0, 2).unwrap());
2854    }
2855
2856    #[test]
2857    fn lex_newline_cr() {
2858        expect_single_token("\r", TokenKind::Newline, Span::new(F, 0, 1).unwrap());
2859    }
2860
2861    #[test]
2862    fn lex_mixed_newlines() {
2863        let (tokens, diags) = tokenize_all("a\nb\r\nc\rd");
2864        assert!(diags.is_empty());
2865        let kinds: Vec<_> = tokens.iter().map(|t| t.kind.clone()).collect();
2866        assert_eq!(
2867            kinds,
2868            vec![
2869                TokenKind::Identifier,
2870                TokenKind::Newline,
2871                TokenKind::Identifier,
2872                TokenKind::Newline,
2873                TokenKind::Identifier,
2874                TokenKind::Newline,
2875                TokenKind::Identifier,
2876                TokenKind::Eof,
2877            ]
2878        );
2879    }
2880
2881    #[test]
2882    fn line_comment_skipped_but_newline_preserved() {
2883        let (tokens, diags) = tokenize_all("a // comment\nb");
2884        assert!(diags.is_empty());
2885        let kinds: Vec<_> = tokens.iter().map(|t| t.kind.clone()).collect();
2886        assert_eq!(
2887            kinds,
2888            vec![
2889                TokenKind::Identifier,
2890                TokenKind::Newline,
2891                TokenKind::Identifier,
2892                TokenKind::Eof,
2893            ]
2894        );
2895    }
2896
2897    #[test]
2898    fn block_comment_skipped() {
2899        let (tokens, diags) = tokenize_all("a /* block */ b");
2900        assert!(diags.is_empty());
2901        let kinds: Vec<_> = tokens.iter().map(|t| t.kind.clone()).collect();
2902        assert_eq!(
2903            kinds,
2904            vec![TokenKind::Identifier, TokenKind::Identifier, TokenKind::Eof]
2905        );
2906    }
2907
2908    #[test]
2909    fn empty_block_comment_is_fine() {
2910        let (tokens, diags) = tokenize_all("/**/");
2911        assert!(diags.is_empty());
2912        assert_eq!(tokens.len(), 1);
2913        assert_eq!(tokens[0].kind, TokenKind::Eof);
2914    }
2915
2916    #[test]
2917    fn unterminated_block_comment_diagnosed() {
2918        let (_, diags) = tokenize_all("/* unterminated");
2919        assert_eq!(diags.len(), 1);
2920        assert_eq!(diags[0].message, "unterminated block comment");
2921        assert_eq!(diags[0].span, Span::new(F, 0, 2).unwrap());
2922    }
2923
2924    #[test]
2925    fn single_slash_star_not_block_comment_start() {
2926        // `/*/` is parsed as the start of a block comment with no terminator.
2927        let (_, diags) = tokenize_all("/*/");
2928        assert_eq!(diags.len(), 1);
2929        assert_eq!(diags[0].message, "unterminated block comment");
2930    }
2931
2932    #[test]
2933    fn doc_comment_attaches_to_next_token() {
2934        let (tok, eof, diags) = tokenize_one("/** Summary. */ foo");
2935        let doc = tok.leading_doc.as_ref().expect("doc attached");
2936        assert_eq!(doc.text, "/** Summary. */");
2937        assert_eq!(doc.span, Span::new(F, 0, 15).unwrap());
2938        assert_eq!(tok.kind, TokenKind::Identifier);
2939        assert!(eof.leading_doc.is_none());
2940        assert!(diags.is_empty());
2941    }
2942
2943    #[test]
2944    fn doc_comment_survives_intervening_newlines() {
2945        let (tokens, diags) = tokenize_all("/** doc */\n\nfoo");
2946        assert!(diags.is_empty());
2947        let ident = tokens
2948            .iter()
2949            .find(|t| t.kind == TokenKind::Identifier)
2950            .expect("identifier present");
2951        assert!(ident.leading_doc.is_some());
2952        for nl in tokens.iter().filter(|t| t.kind == TokenKind::Newline) {
2953            assert!(nl.leading_doc.is_none(), "newline shouldn't claim the doc");
2954        }
2955    }
2956
2957    #[test]
2958    fn empty_doc_comment_captured() {
2959        let (tok, _eof, _diags) = tokenize_one("/** */ x");
2960        let doc = tok.leading_doc.as_ref().expect("doc attached");
2961        assert_eq!(doc.text, "/** */");
2962    }
2963
2964    #[test]
2965    fn regular_block_comment_not_captured() {
2966        let (tok, _eof, _diags) = tokenize_one("/* not a doc */ foo");
2967        assert!(tok.leading_doc.is_none());
2968    }
2969
2970    #[test]
2971    fn empty_block_comment_not_a_doc() {
2972        let (tok, _eof, _diags) = tokenize_one("/**/ foo");
2973        assert!(tok.leading_doc.is_none());
2974    }
2975
2976    #[test]
2977    fn line_comment_not_captured() {
2978        let (tok, _eof, _diags) = tokenize_one("// line\nfoo");
2979        assert!(tok.leading_doc.is_none());
2980    }
2981
2982    #[test]
2983    fn last_doc_wins_when_multiple_in_a_row() {
2984        let (tok, _eof, _diags) = tokenize_one("/** first */ /** second */ foo");
2985        let doc = tok.leading_doc.as_ref().expect("doc attached");
2986        assert_eq!(doc.text, "/** second */");
2987    }
2988
2989    #[test]
2990    fn unterminated_doc_emits_diagnostic() {
2991        let (tok, _eof, diags) = tokenize_one("/** unterminated");
2992        assert_eq!(tok.kind, TokenKind::Eof);
2993        assert_eq!(diags.len(), 1);
2994        assert_eq!(diags[0].message, "unterminated block comment");
2995    }
2996
2997    /// Editors commonly prepend U+FEFF; TypeScript ignores it, and so do we.
2998    #[test]
2999    fn leading_bom_is_skipped() {
3000        let (tokens, diags) = tokenize_all("\u{feff}const x = 1;");
3001        assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
3002        assert_eq!(tokens[0].kind, TokenKind::Const);
3003    }
3004
3005    /// The BOM is stepped over rather than stripped, so spans stay byte offsets into
3006    /// the file on disk — otherwise every caret after it would be three bytes off.
3007    #[test]
3008    fn leading_bom_keeps_spans_aligned_to_the_file() {
3009        let source = "\u{feff}const x = 1;";
3010        let (tokens, _diags) = tokenize_all(source);
3011        let start = tokens[0].span.start as usize;
3012        assert_eq!(&source[start..start + 5], "const");
3013    }
3014
3015    /// Only offset 0 is an encoding marker. Elsewhere it is an ordinary invalid character.
3016    #[test]
3017    fn bom_after_the_first_byte_is_still_an_error() {
3018        let (_tokens, diags) = tokenize_all("const \u{feff}x = 1;");
3019        assert_eq!(diags.len(), 1);
3020        assert!(
3021            diags[0].message.contains("unexpected character"),
3022            "got: {}",
3023            diags[0].message
3024        );
3025    }
3026
3027    #[test]
3028    fn malformed_bytes_stop_accumulating_diagnostics_at_the_cap() {
3029        let source = "@".repeat(10_000);
3030        let mut lexer = Lexer::new(&source, F);
3031        assert!(matches!(lexer.next_token().kind, TokenKind::Eof));
3032        let diagnostics = lexer.finish().unwrap();
3033        assert_eq!(diagnostics.len(), super::MAX_LEXER_DIAGNOSTICS);
3034        assert!(
3035            diagnostics
3036                .iter()
3037                .all(|diagnostic| diagnostic.message.contains("unexpected character"))
3038        );
3039    }
3040
3041    #[test]
3042    fn tokenize_mvp_fixture() {
3043        let source = "function greet(name: string): string {\n  return \"Hello, \" + name;\n}\n\nfunction main(): string {\n  const msg = greet(\"world\");\n  console.log(msg);\n  assert(msg === \"Hello, world\", \"greeting should match\");\n  return msg;\n}\n";
3044        let (tokens, diags) = tokenize_all(source);
3045        assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
3046        let kinds: Vec<String> = tokens.iter().map(|t| format!("{:?}", t.kind)).collect();
3047        insta::assert_debug_snapshot!(kinds);
3048    }
3049}