Skip to main content

quickjs_oxide/engine/compiler/
lexer.rs

1//! ECMAScript lexical analysis.
2//!
3//! This module is intentionally independent from the rest of the engine and
4//! from third-party crates. The interface preserves two parser/lexer
5//! boundaries which matter for ECMAScript:
6//!
7//! * A slash is division under LexicalGoal::Div and starts a regular
8//!   expression under LexicalGoal::RegExp. A parser, rather than a lexer
9//!   heuristic, knows which interpretation is valid.
10//! * After the parser consumes the right brace ending a template
11//!   substitution, it asks for LexicalGoal::TemplateContinuation.
12//!
13//! This is the same broad division of responsibility used by QuickJS. It
14//! prevents valid division, regular-expression, and template source from being
15//! silently assigned the wrong meaning before a parser exists.
16//!
17//! Identifier classification uses the checksum-pinned QuickJS Unicode 17
18//! `ID_Start`/`ID_Continue` tables rather than the host or Rust toolchain's
19//! Unicode version. Identifier escape probes are transactional: a failed
20//! ordinary probe falls back to QuickJS's one-byte raw ASCII token, whereas a
21//! private-name failure and a genuinely invalid non-ASCII character remain
22//! lexical errors for the parser's fallible token advance.
23
24use crate::engine::value::{JsString as RuntimeJsString, JsStringError, decode_quickjs_utf8};
25use crate::source::text::{INVALID_BYTE_CARRIER, SourceText};
26use std::fmt;
27use std::iter::FusedIterator;
28use std::ops::Range;
29
30const TEMPLATE_QUOTE: char = '\u{0060}';
31
32/// Lexer positions and spans are the crate's source coordinates: zero-based
33/// byte offsets with one-based lines and Unicode-scalar columns, where CRLF
34/// counts as one line terminator.
35pub use crate::source::{SourceLocation as Position, SourceSpan as Span};
36
37/// Context which changes whether QuickJS treats a word as a keyword.
38#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
39pub struct LexContext {
40    pub strict: bool,
41    pub module: bool,
42    pub generator: bool,
43    pub async_function: bool,
44}
45
46/// Lexer construction options.
47#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
48pub struct LexerOptions {
49    pub context: LexContext,
50    /// Enables Annex B line comments beginning with the HTML open/close forms.
51    pub allow_html_comments: bool,
52}
53
54/// The lexical grammar requested by the parser for the next token.
55#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
56pub enum LexicalGoal {
57    /// A slash is a division punctuator. This is the iterator default.
58    #[default]
59    Div,
60    /// A slash begins a regular-expression literal.
61    RegExp,
62    /// Resume a template immediately after a substitution's right brace.
63    TemplateContinuation,
64}
65
66#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
67pub enum Keyword {
68    Null,
69    False,
70    True,
71    If,
72    Else,
73    Return,
74    Var,
75    This,
76    Delete,
77    Void,
78    Typeof,
79    New,
80    In,
81    Instanceof,
82    Do,
83    While,
84    For,
85    Break,
86    Continue,
87    Switch,
88    Case,
89    Default,
90    Throw,
91    Try,
92    Catch,
93    Finally,
94    Function,
95    Debugger,
96    With,
97    Class,
98    Const,
99    Enum,
100    Export,
101    Extends,
102    Import,
103    Super,
104    Implements,
105    Interface,
106    Let,
107    Package,
108    Private,
109    Protected,
110    Public,
111    Static,
112    Yield,
113    Await,
114}
115
116impl Keyword {
117    pub const fn as_str(self) -> &'static str {
118        match self {
119            Self::Null => "null",
120            Self::False => "false",
121            Self::True => "true",
122            Self::If => "if",
123            Self::Else => "else",
124            Self::Return => "return",
125            Self::Var => "var",
126            Self::This => "this",
127            Self::Delete => "delete",
128            Self::Void => "void",
129            Self::Typeof => "typeof",
130            Self::New => "new",
131            Self::In => "in",
132            Self::Instanceof => "instanceof",
133            Self::Do => "do",
134            Self::While => "while",
135            Self::For => "for",
136            Self::Break => "break",
137            Self::Continue => "continue",
138            Self::Switch => "switch",
139            Self::Case => "case",
140            Self::Default => "default",
141            Self::Throw => "throw",
142            Self::Try => "try",
143            Self::Catch => "catch",
144            Self::Finally => "finally",
145            Self::Function => "function",
146            Self::Debugger => "debugger",
147            Self::With => "with",
148            Self::Class => "class",
149            Self::Const => "const",
150            Self::Enum => "enum",
151            Self::Export => "export",
152            Self::Extends => "extends",
153            Self::Import => "import",
154            Self::Super => "super",
155            Self::Implements => "implements",
156            Self::Interface => "interface",
157            Self::Let => "let",
158            Self::Package => "package",
159            Self::Private => "private",
160            Self::Protected => "protected",
161            Self::Public => "public",
162            Self::Static => "static",
163            Self::Yield => "yield",
164            Self::Await => "await",
165        }
166    }
167}
168
169#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
170pub enum Punctuator {
171    LeftBrace,
172    RightBrace,
173    LeftParen,
174    RightParen,
175    LeftBracket,
176    RightBracket,
177    Dot,
178    Ellipsis,
179    Semicolon,
180    Comma,
181    Less,
182    LessEqual,
183    Greater,
184    GreaterEqual,
185    Equal,
186    EqualEqual,
187    StrictEqual,
188    Not,
189    NotEqual,
190    StrictNotEqual,
191    Plus,
192    Minus,
193    Multiply,
194    Divide,
195    Remainder,
196    Exponent,
197    Increment,
198    Decrement,
199    ShiftLeft,
200    ShiftRight,
201    UnsignedShiftRight,
202    BitAnd,
203    BitOr,
204    BitXor,
205    BitNot,
206    LogicalAnd,
207    LogicalOr,
208    NullishCoalesce,
209    Question,
210    OptionalChain,
211    Colon,
212    Arrow,
213    PlusAssign,
214    MinusAssign,
215    MultiplyAssign,
216    DivideAssign,
217    RemainderAssign,
218    ExponentAssign,
219    ShiftLeftAssign,
220    ShiftRightAssign,
221    UnsignedShiftRightAssign,
222    BitAndAssign,
223    BitOrAssign,
224    BitXorAssign,
225    LogicalAndAssign,
226    LogicalOrAssign,
227    NullishAssign,
228}
229
230impl Punctuator {
231    pub const fn as_str(self) -> &'static str {
232        match self {
233            Self::LeftBrace => "{",
234            Self::RightBrace => "}",
235            Self::LeftParen => "(",
236            Self::RightParen => ")",
237            Self::LeftBracket => "[",
238            Self::RightBracket => "]",
239            Self::Dot => ".",
240            Self::Ellipsis => "...",
241            Self::Semicolon => ";",
242            Self::Comma => ",",
243            Self::Less => "<",
244            Self::LessEqual => "<=",
245            Self::Greater => ">",
246            Self::GreaterEqual => ">=",
247            Self::Equal => "=",
248            Self::EqualEqual => "==",
249            Self::StrictEqual => "===",
250            Self::Not => "!",
251            Self::NotEqual => "!=",
252            Self::StrictNotEqual => "!==",
253            Self::Plus => "+",
254            Self::Minus => "-",
255            Self::Multiply => "*",
256            Self::Divide => "/",
257            Self::Remainder => "%",
258            Self::Exponent => "**",
259            Self::Increment => "++",
260            Self::Decrement => "--",
261            Self::ShiftLeft => "<<",
262            Self::ShiftRight => ">>",
263            Self::UnsignedShiftRight => ">>>",
264            Self::BitAnd => "&",
265            Self::BitOr => "|",
266            Self::BitXor => "^",
267            Self::BitNot => "~",
268            Self::LogicalAnd => "&&",
269            Self::LogicalOr => "||",
270            Self::NullishCoalesce => "??",
271            Self::Question => "?",
272            Self::OptionalChain => "?.",
273            Self::Colon => ":",
274            Self::Arrow => "=>",
275            Self::PlusAssign => "+=",
276            Self::MinusAssign => "-=",
277            Self::MultiplyAssign => "*=",
278            Self::DivideAssign => "/=",
279            Self::RemainderAssign => "%=",
280            Self::ExponentAssign => "**=",
281            Self::ShiftLeftAssign => "<<=",
282            Self::ShiftRightAssign => ">>=",
283            Self::UnsignedShiftRightAssign => ">>>=",
284            Self::BitAndAssign => "&=",
285            Self::BitOrAssign => "|=",
286            Self::BitXorAssign => "^=",
287            Self::LogicalAndAssign => "&&=",
288            Self::LogicalOrAssign => "||=",
289            Self::NullishAssign => "??=",
290        }
291    }
292}
293
294#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
295pub enum NumericRadix {
296    Binary,
297    Octal,
298    Decimal,
299    Hexadecimal,
300}
301
302#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
303pub enum NumberKind {
304    Integer(NumericRadix),
305    Float,
306    BigInt(NumericRadix),
307    /// Annex B leading-zero integer syntax accepted outside strict mode.
308    LegacyOctal,
309    /// Annex B leading-zero syntax containing 8 or 9, evaluated as decimal.
310    LegacyDecimal,
311}
312
313#[derive(Clone, Copy, Debug, PartialEq, Eq)]
314pub struct NumberLiteral<'a> {
315    pub raw: &'a str,
316    pub kind: NumberKind,
317}
318
319/// JavaScript string contents in their native UTF-16 code-unit form.
320///
321/// Rust String cannot represent lone surrogates, but ECMAScript string escape
322/// sequences can. Keeping UTF-16 here avoids losing that language distinction.
323#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
324pub struct JsString {
325    pub utf16: Vec<u16>,
326}
327
328impl JsString {
329    #[cfg(test)]
330    pub fn to_string(&self) -> Result<String, std::string::FromUtf16Error> {
331        String::from_utf16(&self.utf16)
332    }
333}
334
335/// Streaming sink for UTF-16 code units produced while scanning string and
336/// template literals. Scanners push through this trait so the eager value and
337/// the validation-only paths share one code path.
338trait StringSink {
339    fn push_code_unit(&mut self, unit: u16) -> Result<(), JsStringError>;
340
341    /// On overflow, return the number of source bytes through the first unit
342    /// exceeding the limit. The lexer must report that precise cursor, even
343    /// when a later byte would produce another lexical error.
344    fn push_ascii(&mut self, bytes: &[u8]) -> Result<(), usize>;
345
346    fn push_char(&mut self, ch: char) -> Result<(), JsStringError> {
347        self.push_code_point(ch as u32)
348    }
349
350    fn push_code_point(&mut self, value: u32) -> Result<(), JsStringError> {
351        if value <= 0xffff {
352            self.push_code_unit(value as u16)
353        } else {
354            let adjusted = value - 0x1_0000;
355            self.push_code_unit(0xd800 | ((adjusted >> 10) as u16))?;
356            self.push_code_unit(0xdc00 | ((adjusted & 0x3ff) as u16))
357        }
358    }
359}
360
361struct Utf16Sink {
362    units: Vec<u16>,
363    limit: usize,
364}
365
366impl Utf16Sink {
367    fn new(limit: usize) -> Self {
368        Self {
369            units: Vec::new(),
370            limit,
371        }
372    }
373
374    fn into_js_string(self) -> JsString {
375        JsString { utf16: self.units }
376    }
377}
378
379impl StringSink for Utf16Sink {
380    fn push_ascii(&mut self, bytes: &[u8]) -> Result<(), usize> {
381        let remaining = self.limit.saturating_sub(self.units.len());
382        if bytes.len() > remaining {
383            return Err(remaining + 1);
384        }
385        self.units.extend(bytes.iter().copied().map(u16::from));
386        Ok(())
387    }
388
389    fn push_code_unit(&mut self, unit: u16) -> Result<(), JsStringError> {
390        RuntimeJsString::checked_length_with_limit(self.units.len(), 1, self.limit)?;
391        self.units.push(unit);
392        Ok(())
393    }
394}
395
396/// Validation-only sink used by the hot lexing path: it enforces the same
397/// length limit as `Utf16Sink` without materializing the value.
398struct ValidateSink {
399    len: usize,
400    limit: usize,
401}
402
403impl ValidateSink {
404    fn new(limit: usize) -> Self {
405        Self { len: 0, limit }
406    }
407}
408
409impl StringSink for ValidateSink {
410    fn push_ascii(&mut self, bytes: &[u8]) -> Result<(), usize> {
411        let remaining = self.limit.saturating_sub(self.len);
412        if bytes.len() > remaining {
413            return Err(remaining + 1);
414        }
415        self.len += bytes.len();
416        Ok(())
417    }
418
419    fn push_code_unit(&mut self, _unit: u16) -> Result<(), JsStringError> {
420        RuntimeJsString::checked_length_with_limit(self.len, 1, self.limit)?;
421        self.len += 1;
422        Ok(())
423    }
424}
425
426#[derive(Clone, Copy, Debug, PartialEq, Eq)]
427pub struct StringLiteral<'a> {
428    pub raw: &'a str,
429    pub has_escape: bool,
430    pub has_legacy_octal_escape: bool,
431}
432
433#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
434pub enum TemplatePartKind {
435    NoSubstitution,
436    Head,
437    Middle,
438    Tail,
439}
440
441#[derive(Clone, Copy, Debug, PartialEq, Eq)]
442pub struct TemplateEscapeError {
443    pub message: &'static str,
444    pub span: Span,
445}
446
447#[derive(Clone, Copy, Debug, PartialEq, Eq)]
448pub struct TemplatePart<'a> {
449    /// Source between delimiters, before escape processing.
450    pub raw: &'a str,
451    /// Tagged templates observe undefined cooked text. Only an untagged error
452    /// path materializes the full diagnostic using `template_escape_error`;
453    /// carrying its span and message here would widen every ordinary token.
454    pub invalid_escape: bool,
455    pub kind: TemplatePartKind,
456}
457
458#[derive(Clone, Copy, Debug, PartialEq, Eq)]
459pub struct RegExpLiteral<'a> {
460    pub pattern: &'a str,
461    pub flags: &'a str,
462}
463
464#[derive(Clone, Copy, Debug, PartialEq, Eq)]
465pub struct Identifier<'a> {
466    pub raw: &'a str,
467    pub has_escape: bool,
468    /// Set even when parser context means the word remains an identifier.
469    pub keyword_hint: Option<Keyword>,
470    /// QuickJS keeps escaped reserved words as identifiers so the parser can
471    /// issue the context-appropriate reserved-identifier diagnostic.
472    pub escaped_reserved_word: bool,
473}
474
475#[derive(Clone, Copy, Debug, PartialEq, Eq)]
476pub enum TokenKind<'a> {
477    Identifier(Identifier<'a>),
478    PrivateIdentifier(Identifier<'a>),
479    Keyword(Keyword),
480    Number(NumberLiteral<'a>),
481    String(StringLiteral<'a>),
482    Template(TemplatePart<'a>),
483    RegExp(RegExpLiteral<'a>),
484    Punctuator(Punctuator),
485    /// An otherwise unrecognized ASCII byte. QuickJS returns these bytes as
486    /// raw tokens so the parser can choose the context-specific diagnostic;
487    /// non-ASCII invalid input remains a lexical error.
488    RawAscii(u8),
489    Eof,
490}
491
492#[derive(Clone, Copy, Debug, PartialEq, Eq)]
493pub struct Token<'a> {
494    pub kind: TokenKind<'a>,
495    pub span: Span,
496    /// True when trivia before this token contained at least one ECMAScript
497    /// line terminator. Parsers use this for ASI and restricted productions.
498    pub line_terminator_before: bool,
499}
500
501#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
502pub enum LexErrorKind {
503    UnexpectedCharacter,
504    UnterminatedComment,
505    UnterminatedString,
506    UnterminatedTemplate,
507    InvalidEscape,
508    InvalidNumber,
509    InvalidPrivateIdentifier,
510    UnterminatedRegExp,
511    LineTerminatorInRegExp,
512    ExpectedRegExp,
513    ExpectedTemplateContinuation,
514    StringTooLong,
515}
516
517#[derive(Clone, Copy, Debug, PartialEq, Eq)]
518pub struct LexError {
519    pub kind: LexErrorKind,
520    pub span: Span,
521    pub message: &'static str,
522}
523
524impl fmt::Display for LexError {
525    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
526        write!(
527            f,
528            "{}:{} (byte {}): {}",
529            self.span.start.line, self.span.start.column, self.span.start.byte_offset, self.message
530        )
531    }
532}
533
534impl std::error::Error for LexError {}
535
536/// Stateful scanner over one UTF-8 source string.
537#[derive(Clone)]
538pub struct Lexer<'a> {
539    source: &'a str,
540    source_text: Option<&'a SourceText>,
541    offset: usize,
542    line: u32,
543    column: u32,
544    options: LexerOptions,
545    eof_emitted: bool,
546    failed: bool,
547    string_limit: usize,
548}
549
550impl<'a> Lexer<'a> {
551    #[cfg(any(test, feature = "test-support"))]
552    pub fn new(source: &'a str) -> Self {
553        Self::with_options(source, LexerOptions::default())
554    }
555
556    pub fn with_options(source: &'a str, options: LexerOptions) -> Self {
557        Self {
558            source,
559            source_text: None,
560            offset: 0,
561            line: 1,
562            column: 1,
563            options,
564            eof_emitted: false,
565            failed: false,
566            string_limit: RuntimeJsString::MAX_LEN,
567        }
568    }
569
570    /// Scan one reversible dynamic-source carrier.
571    ///
572    /// This remains crate-private so the public lexer continues to accept
573    /// ordinary Rust UTF-8 source. The side table is consulted only when a
574    /// token's semantic value must preserve a lone UTF-16 surrogate.
575    pub fn with_source_text(source: &'a SourceText, options: LexerOptions) -> Self {
576        Self {
577            source: source.carrier(),
578            source_text: Some(source),
579            offset: 0,
580            line: 1,
581            column: 1,
582            options,
583            eof_emitted: false,
584            failed: false,
585            string_limit: RuntimeJsString::MAX_LEN,
586        }
587    }
588
589    #[cfg(test)]
590    fn with_string_limit(mut self, limit: usize) -> Self {
591        self.string_limit = limit.min(RuntimeJsString::MAX_LEN);
592        self
593    }
594
595    pub fn source(&self) -> &'a str {
596        self.source
597    }
598
599    pub fn current_position(&self) -> Position {
600        Position::new(self.offset, self.line, self.column)
601    }
602
603    /// Seeks to a trusted token-start position before rescanning under a new
604    /// lexical context. The caller must provide a position previously produced
605    /// for this source.
606    pub fn seek(&mut self, position: Position) {
607        debug_assert!(position.byte_offset <= self.source.len());
608        debug_assert!(self.source.is_char_boundary(position.byte_offset));
609        self.offset = position.byte_offset;
610        self.line = position.line;
611        self.column = position.column;
612        self.eof_emitted = false;
613        self.failed = false;
614    }
615
616    pub fn context(&self) -> LexContext {
617        self.options.context
618    }
619
620    /// A parser may update context before scanning a contextual keyword.
621    pub fn set_context(&mut self, context: LexContext) {
622        self.options.context = context;
623    }
624
625    /// Scans with the division lexical goal and includes one EOF token.
626    ///
627    /// A parser must use next_token_with_goal for regular expressions and
628    /// template continuations. This convenience method deliberately does not
629    /// guess expression context.
630    #[cfg(test)]
631    pub fn tokenize(mut self) -> Result<Vec<Token<'a>>, LexError> {
632        let mut tokens = Vec::new();
633        loop {
634            let token = self.next_token()?;
635            let is_eof = matches!(token.kind, TokenKind::Eof);
636            tokens.push(token);
637            if is_eof {
638                return Ok(tokens);
639            }
640        }
641    }
642
643    pub fn next_token(&mut self) -> Result<Token<'a>, LexError> {
644        self.next_token_with_goal(LexicalGoal::Div)
645    }
646
647    pub fn next_token_with_goal(&mut self, goal: LexicalGoal) -> Result<Token<'a>, LexError> {
648        let position = self.current_position();
649        let mut token = Token {
650            kind: TokenKind::Eof,
651            span: Span::new(position, position),
652            line_terminator_before: false,
653        };
654        self.next_token_into(goal, &mut token)?;
655        Ok(token)
656    }
657
658    /// Fill the committed parser token directly. Keeping the output separate
659    /// from Result avoids returning and copying the wide token through each
660    /// parser advancement layer. Errors leave the previous token untouched.
661    pub fn next_token_into(
662        &mut self,
663        goal: LexicalGoal,
664        token: &mut Token<'a>,
665    ) -> Result<(), LexError> {
666        if goal == LexicalGoal::TemplateContinuation {
667            *token = self.scan_template(false, false)?;
668            return Ok(());
669        }
670
671        // Most tokens in minified input have no leading trivia. Only enter
672        // the trivia loop for bytes that can actually start it (including
673        // Annex B HTML comments and the offset-zero hashbang).
674        let line_terminator_before = match self.source.as_bytes().get(self.offset) {
675            Some(&byte) if byte >= 0x80 || ASCII_FLAGS[byte as usize] & MAY_START_TRIVIA != 0 => {
676                self.skip_trivia()?
677            }
678            _ => false,
679        };
680        let start = self.current_position();
681
682        if self.offset == self.source.len() {
683            self.eof_emitted = true;
684            *token = Token {
685                kind: TokenKind::Eof,
686                span: Span::new(start, start),
687                line_terminator_before,
688            };
689            return Ok(());
690        }
691
692        if self.invalid_source_byte_at(self.offset) {
693            return Err(self.invalid_utf8_error_here("unexpected character"));
694        }
695
696        if goal == LexicalGoal::RegExp {
697            if self.peek_char() == Some('/') {
698                *token = self.scan_regexp(start, line_terminator_before)?;
699                return Ok(());
700            }
701            return Err(self.error_here(
702                LexErrorKind::ExpectedRegExp,
703                "regular-expression lexical goal requires a slash",
704            ));
705        }
706
707        let byte = self.source.as_bytes()[self.offset];
708        let dispatch = ASCII_DISPATCH
709            .get(byte as usize)
710            .copied()
711            .unwrap_or(ScanKind::Slow);
712        let kind = match dispatch {
713            ScanKind::Single(punctuator) => {
714                self.offset += 1;
715                self.column = self.column.saturating_add(1);
716                TokenKind::Punctuator(punctuator)
717            }
718            ScanKind::Identifier => self.scan_identifier(false)?,
719            ScanKind::Number => self.scan_number(false)?,
720            ScanKind::String => self.scan_string()?,
721            ScanKind::Template => {
722                *token = self.scan_template(true, line_terminator_before)?;
723                return Ok(());
724            }
725            ScanKind::Dot
726                if self
727                    .source
728                    .as_bytes()
729                    .get(self.offset + 1)
730                    .is_some_and(u8::is_ascii_digit) =>
731            {
732                self.scan_number(true)?
733            }
734            ScanKind::Private => self.scan_identifier(true)?,
735            ScanKind::Slow => {
736                let ch = self.peek_char().expect("checked non-empty source");
737                if is_identifier_start(ch) || ch == '\\' {
738                    self.scan_identifier(false)?
739                } else {
740                    self.scan_punctuator()?
741                }
742            }
743            ScanKind::Dot | ScanKind::Punctuator => self.scan_punctuator()?,
744        };
745
746        *token = Token {
747            kind,
748            span: Span::new(start, self.current_position()),
749            line_terminator_before,
750        };
751        Ok(())
752    }
753
754    fn peek_char(&self) -> Option<char> {
755        let byte = *self.source.as_bytes().get(self.offset)?;
756        if byte < 0x80 {
757            return Some(byte as char);
758        }
759        self.source.get(self.offset..)?.chars().next()
760    }
761
762    fn peek_nth_char(&self, n: usize) -> Option<char> {
763        self.source.get(self.offset..)?.chars().nth(n)
764    }
765
766    fn starts_with(&self, text: &str) -> bool {
767        self.source
768            .get(self.offset..)
769            .is_some_and(|rest| rest.starts_with(text))
770    }
771
772    fn surrogate_at(&self, byte_offset: usize) -> Option<u16> {
773        self.source_text
774            .and_then(|source| source.surrogate_at(byte_offset))
775    }
776
777    fn invalid_source_byte_at(&self, byte_offset: usize) -> bool {
778        self.source_text
779            .is_some_and(|source| source.invalid_byte_at(byte_offset))
780    }
781
782    fn invalid_utf8_error_here(&self, message: &'static str) -> LexError {
783        self.error_here(LexErrorKind::UnexpectedCharacter, message)
784    }
785
786    fn semantic_utf16_units(&self, range: Range<usize>) -> Option<Vec<u16>> {
787        if let Some(source) = self.source_text {
788            return source
789                .slice(range)
790                .and_then(|slice| slice.semantic_utf16_units())
791                .map(Iterator::collect);
792        }
793        self.source
794            .get(range)
795            .map(|source| source.encode_utf16().collect())
796    }
797
798    pub fn source_range_to_js_string(
799        &self,
800        range: Range<usize>,
801    ) -> Result<Option<RuntimeJsString>, JsStringError> {
802        if let Some(source) = self.source_text {
803            return source.try_range_to_js_string(range);
804        }
805        self.source
806            .get(range)
807            .map(RuntimeJsString::try_from_utf8)
808            .transpose()
809    }
810
811    /// Advances one scalar value, treating CRLF as one logical character.
812    fn bump_char(&mut self) -> Option<char> {
813        let byte = *self.source.as_bytes().get(self.offset)?;
814        // `INVALID_BYTE_CARRIER` is ASCII in the carrier but can stand for a raw
815        // continuation byte with a zero column delta, so it stays on the slow
816        // path; every other ASCII byte contributes exactly one column.
817        if byte < 0x80 && byte != INVALID_BYTE_CARRIER as u8 {
818            self.offset += 1;
819            return Some(match byte {
820                b'\r' => {
821                    if self.source.as_bytes().get(self.offset) == Some(&b'\n') {
822                        self.offset += 1;
823                    }
824                    self.line = self.line.saturating_add(1);
825                    self.column = 1;
826                    '\r'
827                }
828                b'\n' => {
829                    self.line = self.line.saturating_add(1);
830                    self.column = 1;
831                    '\n'
832                }
833                _ => {
834                    self.column = self.column.saturating_add(1);
835                    byte as char
836                }
837            });
838        }
839
840        let ch = self.source.get(self.offset..)?.chars().next()?;
841        let byte_len = ch.len_utf8();
842        let column_delta = self.quickjs_column_delta(self.offset, byte_len);
843        self.offset += byte_len;
844        if is_line_terminator(ch) {
845            self.line = self.line.saturating_add(1);
846            self.column = 1;
847        } else {
848            self.column = self.column.saturating_add(column_delta);
849        }
850        Some(ch)
851    }
852
853    fn quickjs_column_delta(&self, byte_offset: usize, byte_len: usize) -> u32 {
854        let Some(raw) = self.source_text.and_then(|source| {
855            source
856                .raw_bytes()
857                .get(byte_offset..byte_offset.saturating_add(byte_len))
858        }) else {
859            return 1;
860        };
861        raw.iter()
862            .copied()
863            .filter(|byte| !(0x80..=0xbf).contains(byte))
864            .count() as u32
865    }
866
867    fn error_from(&self, start: Position, kind: LexErrorKind, message: &'static str) -> LexError {
868        LexError {
869            kind,
870            span: Span::new(start, self.current_position()),
871            message,
872        }
873    }
874
875    fn error_here(&self, kind: LexErrorKind, message: &'static str) -> LexError {
876        let start = self.current_position();
877        let mut end = start;
878        if let Some(ch) = self.peek_char() {
879            let byte_len = if ch == '\r' && self.starts_with("\r\n") {
880                2
881            } else {
882                ch.len_utf8()
883            };
884            end.byte_offset += byte_len;
885            if is_line_terminator(ch) {
886                end.line = end.line.saturating_add(1);
887                end.column = 1;
888            } else {
889                end.column = end
890                    .column
891                    .saturating_add(self.quickjs_column_delta(self.offset, byte_len));
892            }
893        }
894        LexError {
895            kind,
896            span: Span::new(start, end),
897            message,
898        }
899    }
900
901    fn string_too_long(&self, start: Position) -> LexError {
902        self.error_from(start, LexErrorKind::StringTooLong, "string too long")
903    }
904
905    fn skip_trivia(&mut self) -> Result<bool, LexError> {
906        let mut saw_line_terminator = false;
907        let token_search_start = self.offset;
908
909        loop {
910            let Some(&byte) = self.source.as_bytes().get(self.offset) else {
911                return Ok(saw_line_terminator);
912            };
913
914            if byte >= 0x80 {
915                let ch = self.peek_char().expect("checked non-empty source");
916                if is_line_terminator(ch) {
917                    self.bump_char();
918                    saw_line_terminator = true;
919                    continue;
920                }
921                if is_js_whitespace(ch) {
922                    self.bump_char();
923                    continue;
924                }
925                return Ok(saw_line_terminator);
926            }
927
928            match byte {
929                b' ' | b'\t' | 0x0b | 0x0c => {
930                    let bytes = self.source.as_bytes();
931                    let mut end = self.offset + 1;
932                    while let Some(&next) = bytes.get(end) {
933                        if matches!(next, b' ' | b'\t' | 0x0b | 0x0c) {
934                            end += 1;
935                        } else {
936                            break;
937                        }
938                    }
939                    self.column = self.column.saturating_add((end - self.offset) as u32);
940                    self.offset = end;
941                }
942                b'\n' => {
943                    self.offset += 1;
944                    self.line = self.line.saturating_add(1);
945                    self.column = 1;
946                    saw_line_terminator = true;
947                }
948                b'\r' => {
949                    self.offset += 1;
950                    if self.source.as_bytes().get(self.offset) == Some(&b'\n') {
951                        self.offset += 1;
952                    }
953                    self.line = self.line.saturating_add(1);
954                    self.column = 1;
955                    saw_line_terminator = true;
956                }
957                b'#' if self.offset == 0 && self.source.as_bytes().get(1) == Some(&b'!') => {
958                    self.offset = 2;
959                    self.column = self.column.saturating_add(2);
960                    self.skip_to_line_end();
961                }
962                b'/' => match self.source.as_bytes().get(self.offset + 1) {
963                    Some(b'/') => {
964                        self.offset += 2;
965                        self.column = self.column.saturating_add(2);
966                        self.skip_to_line_end();
967                    }
968                    Some(b'*') => {
969                        let comment_start = self.current_position();
970                        self.offset += 2;
971                        self.column = self.column.saturating_add(2);
972                        let bytes = self.source.as_bytes();
973                        let mut terminated = false;
974                        while self.offset < bytes.len() {
975                            if bytes[self.offset] == b'*'
976                                && bytes.get(self.offset + 1) == Some(&b'/')
977                            {
978                                self.offset += 2;
979                                self.column = self.column.saturating_add(2);
980                                terminated = true;
981                                break;
982                            }
983                            let next = self.peek_char().expect("not at end");
984                            if is_line_terminator(next) {
985                                saw_line_terminator = true;
986                            }
987                            self.bump_char();
988                        }
989                        if !terminated {
990                            return Err(self.error_from(
991                                comment_start,
992                                LexErrorKind::UnterminatedComment,
993                                "unexpected end of comment",
994                            ));
995                        }
996                    }
997                    _ => return Ok(saw_line_terminator),
998                },
999                b'<' if self.options.allow_html_comments
1000                    && self.source.as_bytes().get(self.offset + 1) == Some(&b'!')
1001                    && self.source.as_bytes().get(self.offset + 2) == Some(&b'-')
1002                    && self.source.as_bytes().get(self.offset + 3) == Some(&b'-') =>
1003                {
1004                    self.offset += 4;
1005                    self.column = self.column.saturating_add(4);
1006                    self.skip_to_line_end();
1007                }
1008                b'-' if self.options.allow_html_comments
1009                    && (saw_line_terminator || token_search_start == 0)
1010                    && self.source.as_bytes().get(self.offset + 1) == Some(&b'-')
1011                    && self.source.as_bytes().get(self.offset + 2) == Some(&b'>') =>
1012                {
1013                    self.offset += 3;
1014                    self.column = self.column.saturating_add(3);
1015                    self.skip_to_line_end();
1016                }
1017                _ => return Ok(saw_line_terminator),
1018            }
1019        }
1020    }
1021
1022    fn skip_to_line_end(&mut self) {
1023        loop {
1024            let Some(&byte) = self.source.as_bytes().get(self.offset) else {
1025                return;
1026            };
1027            if byte < 0x80 && byte != b'\n' && byte != b'\r' && byte != INVALID_BYTE_CARRIER as u8 {
1028                let bytes = self.source.as_bytes();
1029                let mut end = self.offset + 1;
1030                while let Some(&next) = bytes.get(end) {
1031                    if next < 0x80
1032                        && next != b'\n'
1033                        && next != b'\r'
1034                        && next != INVALID_BYTE_CARRIER as u8
1035                    {
1036                        end += 1;
1037                    } else {
1038                        break;
1039                    }
1040                }
1041                self.column = self.column.saturating_add((end - self.offset) as u32);
1042                self.offset = end;
1043                continue;
1044            }
1045            let Some(ch) = self.peek_char() else {
1046                return;
1047            };
1048            if is_line_terminator(ch) {
1049                return;
1050            }
1051            self.bump_char();
1052        }
1053    }
1054
1055    fn scan_identifier(&mut self, private: bool) -> Result<TokenKind<'a>, LexError> {
1056        let raw_start = self.offset;
1057        let value_start = raw_start + usize::from(private);
1058        let bytes = self.source.as_bytes();
1059        if bytes
1060            .get(value_start)
1061            .is_some_and(|byte| *byte < 0x80 && is_ascii_identifier_start_byte(*byte))
1062        {
1063            let mut end = value_start;
1064            let mut flags = 0;
1065            while let Some(&byte) = bytes.get(end) {
1066                if byte >= 0x80 || !is_ascii_identifier_continue_byte(byte) {
1067                    break;
1068                }
1069                flags |= ASCII_FLAGS[byte as usize];
1070                end += 1;
1071            }
1072            // Escapes and non-ASCII boundaries retain the complete scanner,
1073            // including attempted-escape flags and private-name diagnostics.
1074            if !matches!(bytes.get(end), Some(b'\\' | 0x80..=0xff)) {
1075                let length = end - raw_start;
1076                if length > self.string_limit {
1077                    let start = self.current_position();
1078                    self.offset += self.string_limit + 1;
1079                    self.column = self.column.saturating_add((self.string_limit + 1) as u32);
1080                    return Err(self.string_too_long(start));
1081                }
1082                self.offset = end;
1083                self.column = self.column.saturating_add(length as u32);
1084                let raw = &self.source[raw_start..end];
1085                let keyword_hint = if flags & NOT_KEYWORD == 0 {
1086                    keyword_from_str(&self.source[value_start..end])
1087                } else {
1088                    None
1089                };
1090                if !private
1091                    && let Some(keyword) =
1092                        keyword_hint.filter(|keyword| self.keyword_is_active(*keyword))
1093                {
1094                    return Ok(TokenKind::Keyword(keyword));
1095                }
1096                let identifier = Identifier {
1097                    raw,
1098                    has_escape: false,
1099                    keyword_hint,
1100                    escaped_reserved_word: false,
1101                };
1102                return Ok(if private {
1103                    TokenKind::PrivateIdentifier(identifier)
1104                } else {
1105                    TokenKind::Identifier(identifier)
1106                });
1107            }
1108        }
1109        self.scan_identifier_with_value(private)
1110            .map(|(kind, _)| kind)
1111    }
1112
1113    /// Scans an identifier and, only when an escape sequence actually decoded,
1114    /// returns the decoded text. Plain identifiers leave `None` so the common
1115    /// path allocates nothing; callers fall back to the source slice.
1116    fn scan_identifier_with_value(
1117        &mut self,
1118        private: bool,
1119    ) -> Result<(TokenKind<'a>, Option<String>), LexError> {
1120        let start = self.current_position();
1121        let raw_start = self.offset;
1122        if private {
1123            self.bump_char();
1124        }
1125        let value_start = self.offset;
1126
1127        let mut value: Option<String> = None;
1128        // QuickJS interns private names with their leading `#`, so that code
1129        // unit participates in the same 30-bit atom/StringBuffer limit even
1130        // though the public identifier value excludes it.
1131        let mut value_utf16_len = usize::from(private);
1132        let mut has_escape = false;
1133        let mut identifier_flags = 0u8;
1134        let mut first = true;
1135
1136        loop {
1137            // ASCII fast path: consume a whole identifier run with a byte table
1138            // instead of decoding and classifying one scalar per character.
1139            if let Some(&byte) = self.source.as_bytes().get(self.offset)
1140                && byte < 0x80
1141                && byte != b'\\'
1142            {
1143                let valid = if first {
1144                    is_ascii_identifier_start_byte(byte)
1145                } else {
1146                    is_ascii_identifier_continue_byte(byte)
1147                };
1148                if valid {
1149                    identifier_flags |= ASCII_FLAGS[byte as usize];
1150                    let bytes = self.source.as_bytes();
1151                    let mut end = self.offset + 1;
1152                    while let Some(&next) = bytes.get(end) {
1153                        if next < 0x80 && is_ascii_identifier_continue_byte(next) {
1154                            identifier_flags |= ASCII_FLAGS[next as usize];
1155                            end += 1;
1156                        } else {
1157                            break;
1158                        }
1159                    }
1160                    let run_len = end - self.offset;
1161                    let Some(length) = value_utf16_len
1162                        .checked_add(run_len)
1163                        .filter(|length| *length <= self.string_limit)
1164                    else {
1165                        // Mirror the per-character error position: the first
1166                        // character that pushes the length past the limit.
1167                        let consumed = self.string_limit - value_utf16_len + 1;
1168                        self.offset += consumed;
1169                        self.column = self.column.saturating_add(consumed as u32);
1170                        return Err(self.string_too_long(start));
1171                    };
1172                    value_utf16_len = length;
1173                    if let Some(value) = value.as_mut() {
1174                        value.push_str(&self.source[self.offset..end]);
1175                    }
1176                    self.column = self.column.saturating_add(run_len as u32);
1177                    self.offset = end;
1178                    first = false;
1179                    continue;
1180                }
1181            }
1182
1183            let Some(ch) = self.peek_char() else {
1184                if first {
1185                    return Err(self.error_from(
1186                        start,
1187                        LexErrorKind::InvalidPrivateIdentifier,
1188                        "invalid first character of private name",
1189                    ));
1190                }
1191                break;
1192            };
1193
1194            if ch == '\\' {
1195                let checkpoint = (self.offset, self.line, self.column);
1196                let attempted_unicode_escape = self.starts_with("\\u");
1197                if !first && attempted_unicode_escape {
1198                    // QuickJS records an attempted tail `\\u` before it knows
1199                    // whether the escape decodes to IdentifierContinue. The
1200                    // cursor/value still roll back on failure, but this flag
1201                    // keeps an existing reserved spelling on the escaped-word
1202                    // parser path.
1203                    has_escape = true;
1204                }
1205                let decoded = match self.scan_identifier_escape() {
1206                    Ok(decoded) => decoded,
1207                    Err(_) if first && !private => {
1208                        (self.offset, self.line, self.column) = checkpoint;
1209                        self.bump_char();
1210                        return Ok((TokenKind::RawAscii(b'\\'), None));
1211                    }
1212                    Err(_) if first => {
1213                        (self.offset, self.line, self.column) = checkpoint;
1214                        return Err(self.error_from(
1215                            start,
1216                            LexErrorKind::InvalidPrivateIdentifier,
1217                            "invalid first character of private name",
1218                        ));
1219                    }
1220                    Err(_) => {
1221                        (self.offset, self.line, self.column) = checkpoint;
1222                        break;
1223                    }
1224                };
1225                let valid = if first {
1226                    is_identifier_start_code_point(decoded)
1227                } else {
1228                    is_identifier_continue_code_point(decoded)
1229                };
1230                if !valid {
1231                    (self.offset, self.line, self.column) = checkpoint;
1232                    if !first {
1233                        break;
1234                    }
1235                    if private {
1236                        return Err(self.error_from(
1237                            start,
1238                            LexErrorKind::InvalidPrivateIdentifier,
1239                            "invalid first character of private name",
1240                        ));
1241                    }
1242                    self.bump_char();
1243                    return Ok((TokenKind::RawAscii(b'\\'), None));
1244                }
1245                let decoded =
1246                    char::from_u32(decoded).expect("ID_Start/ID_Continue excluded a surrogate");
1247                value_utf16_len = RuntimeJsString::checked_length_with_limit(
1248                    value_utf16_len,
1249                    decoded.len_utf16(),
1250                    self.string_limit,
1251                )
1252                .map_err(|_| self.string_too_long(start))?;
1253                value
1254                    .get_or_insert_with(|| self.source[value_start..checkpoint.0].to_owned())
1255                    .push(decoded);
1256                has_escape = true;
1257                first = false;
1258                continue;
1259            }
1260
1261            let valid = if first {
1262                is_identifier_start(ch)
1263            } else {
1264                is_identifier_continue(ch)
1265            };
1266            if valid {
1267                identifier_flags |= NOT_KEYWORD;
1268                self.bump_char();
1269                value_utf16_len = RuntimeJsString::checked_length_with_limit(
1270                    value_utf16_len,
1271                    ch.len_utf16(),
1272                    self.string_limit,
1273                )
1274                .map_err(|_| self.string_too_long(start))?;
1275                if let Some(value) = value.as_mut() {
1276                    value.push(ch);
1277                }
1278                first = false;
1279                continue;
1280            }
1281
1282            // Non-ASCII whitespace and U+2028/U+2029 terminate an ASCII
1283            // identifier just like their ASCII counterparts. They must be
1284            // left for `skip_trivia`, which records the restricted-production
1285            // LineTerminator flag used by postfix updates.
1286            if !first && (is_line_terminator(ch) || is_js_whitespace(ch)) {
1287                break;
1288            }
1289            if first {
1290                return Err(self.error_from(
1291                    start,
1292                    if private {
1293                        LexErrorKind::InvalidPrivateIdentifier
1294                    } else {
1295                        LexErrorKind::UnexpectedCharacter
1296                    },
1297                    if private {
1298                        "invalid first character of private name"
1299                    } else {
1300                        "invalid first character of identifier"
1301                    },
1302                ));
1303            }
1304            break;
1305        }
1306
1307        let raw = &self.source[raw_start..self.offset];
1308        let keyword_hint = match &value {
1309            Some(value) => keyword_from_str(value),
1310            None if identifier_flags & NOT_KEYWORD == 0 => {
1311                keyword_from_str(&raw[usize::from(private)..])
1312            }
1313            None => None,
1314        };
1315        let active_keyword = keyword_hint.filter(|keyword| self.keyword_is_active(*keyword));
1316        let identifier = Identifier {
1317            raw,
1318            has_escape,
1319            keyword_hint,
1320            escaped_reserved_word: has_escape && active_keyword.is_some(),
1321        };
1322
1323        let kind = if private {
1324            TokenKind::PrivateIdentifier(identifier)
1325        } else if !has_escape {
1326            if let Some(keyword) = active_keyword {
1327                TokenKind::Keyword(keyword)
1328            } else {
1329                TokenKind::Identifier(identifier)
1330            }
1331        } else {
1332            TokenKind::Identifier(identifier)
1333        };
1334        Ok((kind, value))
1335    }
1336
1337    /// Re-decodes a committed identifier token's text. The lexer clone keeps
1338    /// `source_text` and `string_limit`, so the injected test limit and lone
1339    /// surrogate carriers behave exactly like the original scan.
1340    pub fn decode_identifier_text(&self, raw: &str) -> String {
1341        let private = raw.starts_with('#');
1342        let start = raw.as_ptr() as usize - self.source.as_ptr() as usize;
1343        let mut lexer = self.clone();
1344        // This token was already validated; decoding returns only its text,
1345        // never a diagnostic position. Keep the absolute byte offset for the
1346        // SourceText side tables, without replaying the entire source prefix
1347        // to compute an unused line/column for every escaped identifier.
1348        lexer.offset = start;
1349        match lexer.scan_identifier_with_value(private) {
1350            Ok((_, Some(value))) => value,
1351            _ => raw[usize::from(private)..].to_owned(),
1352        }
1353    }
1354
1355    /// Re-derives a committed string literal's cooked value. The lexer clone
1356    /// keeps `source_text` and `string_limit`, so lone surrogate carriers and
1357    /// the injected test limit behave exactly like the original scan.
1358    pub fn decode_string_literal(&self, start: Position) -> Result<JsString, LexError> {
1359        let mut lexer = self.clone();
1360        lexer.seek(start);
1361        let mut value = Utf16Sink::new(lexer.string_limit);
1362        lexer.scan_string_with_sink(&mut value)?;
1363        Ok(value.into_js_string())
1364    }
1365
1366    /// Re-derives a committed template part's raw value. `start` is the token
1367    /// span start, i.e. the backtick for the head or the closing `}` for a
1368    /// continuation.
1369    pub fn decode_template_raw_value(
1370        &self,
1371        start: Position,
1372        initial: bool,
1373    ) -> Result<JsString, LexError> {
1374        let mut lexer = self.clone();
1375        lexer.seek(start);
1376        if !initial {
1377            lexer.bump_char();
1378        }
1379        let mut raw_value = Utf16Sink::new(lexer.string_limit);
1380        let mut no_cooked: Option<ValidateSink> = None;
1381        lexer.scan_template_with_sinks(
1382            initial,
1383            false,
1384            &mut raw_value,
1385            &mut no_cooked,
1386            &mut None,
1387        )?;
1388        Ok(raw_value.into_js_string())
1389    }
1390
1391    /// Re-derives a committed template part's cooked value. Callers must check
1392    /// `invalid_escape` first: a malformed escape has no cooked text.
1393    pub fn decode_template_cooked_value(
1394        &self,
1395        start: Position,
1396        initial: bool,
1397    ) -> Result<JsString, LexError> {
1398        let mut lexer = self.clone();
1399        lexer.seek(start);
1400        if !initial {
1401            lexer.bump_char();
1402        }
1403        let mut raw_value = ValidateSink::new(lexer.string_limit);
1404        let mut cooked = Some(Utf16Sink::new(lexer.string_limit));
1405        let token = lexer.scan_template_with_sinks(
1406            initial,
1407            false,
1408            &mut raw_value,
1409            &mut cooked,
1410            &mut None,
1411        )?;
1412        let TokenKind::Template(part) = token.kind else {
1413            unreachable!("template scan must return a template token");
1414        };
1415        debug_assert!(!part.invalid_escape);
1416        Ok(cooked.expect("cooked sink present").into_js_string())
1417    }
1418
1419    /// Recover the first malformed escape's exact diagnostic on the cold
1420    /// untagged-template error path. Start at the saved token position, keeping
1421    /// source carriers, CRLF rules and the original string limit intact.
1422    pub fn template_escape_error(
1423        &self,
1424        start: Position,
1425        initial: bool,
1426    ) -> Result<Option<TemplateEscapeError>, LexError> {
1427        let mut lexer = self.clone();
1428        lexer.seek(start);
1429        if !initial {
1430            lexer.bump_char();
1431        }
1432        let mut raw = ValidateSink::new(lexer.string_limit);
1433        let mut cooked = Some(ValidateSink::new(lexer.string_limit));
1434        let mut invalid = None;
1435        lexer.scan_template_with_sinks(initial, false, &mut raw, &mut cooked, &mut invalid)?;
1436        Ok(invalid)
1437    }
1438
1439    fn scan_identifier_escape(&mut self) -> Result<u32, LexError> {
1440        let start = self.current_position();
1441        self.bump_char();
1442        if self.invalid_source_byte_at(self.offset) {
1443            return Err(self.invalid_utf8_error_here("invalid UTF-8 sequence"));
1444        }
1445        if self.peek_char() != Some('u') {
1446            return Err(self.error_from(
1447                start,
1448                LexErrorKind::InvalidEscape,
1449                "identifier escapes must use a Unicode escape",
1450            ));
1451        }
1452        self.bump_char();
1453        self.scan_unicode_escape_value(start)
1454    }
1455
1456    fn keyword_is_active(&self, keyword: Keyword) -> bool {
1457        use Keyword::*;
1458        match keyword {
1459            Implements | Interface | Let | Package | Private | Protected | Public | Static => {
1460                self.options.context.strict
1461            }
1462            Yield => self.options.context.strict || self.options.context.generator,
1463            Await => self.options.context.module || self.options.context.async_function,
1464            _ => true,
1465        }
1466    }
1467
1468    fn scan_number(&mut self, initial_dot: bool) -> Result<TokenKind<'a>, LexError> {
1469        let start = self.current_position();
1470        let raw_start = self.offset;
1471
1472        if !initial_dot && self.peek_char() == Some('0') {
1473            let prefix = self.peek_nth_char(1);
1474            let radix = match prefix {
1475                Some('b' | 'B') => Some(NumericRadix::Binary),
1476                Some('o' | 'O') => Some(NumericRadix::Octal),
1477                Some('x' | 'X') => Some(NumericRadix::Hexadecimal),
1478                _ => None,
1479            };
1480            if let Some(radix) = radix {
1481                self.bump_char();
1482                self.bump_char();
1483                let base = radix_value(radix);
1484                let run = self.scan_digit_run(base)?;
1485                if run.digits == 0 {
1486                    return Err(self.error_from(
1487                        start,
1488                        LexErrorKind::InvalidNumber,
1489                        "invalid number literal",
1490                    ));
1491                }
1492
1493                let kind = if self.peek_char() == Some('n') {
1494                    self.bump_char();
1495                    NumberKind::BigInt(radix)
1496                } else {
1497                    NumberKind::Integer(radix)
1498                };
1499                self.reject_number_follow(start)?;
1500                return Ok(TokenKind::Number(NumberLiteral {
1501                    raw: &self.source[raw_start..self.offset],
1502                    kind,
1503                }));
1504            }
1505        }
1506
1507        let mut is_float = initial_dot;
1508        let mut had_separator = false;
1509        let mut integer_digits = 0;
1510        let mut leading_zero = false;
1511        let mut saw_8_or_9 = false;
1512        let mut legacy_octal_integer = false;
1513
1514        if initial_dot {
1515            self.bump_char();
1516            let run = self.scan_digit_run(10)?;
1517            if run.digits == 0 {
1518                return Err(self.error_from(
1519                    start,
1520                    LexErrorKind::InvalidNumber,
1521                    "fraction requires a decimal digit",
1522                ));
1523            }
1524            had_separator |= run.had_separator;
1525        } else {
1526            leading_zero = self.peek_char() == Some('0');
1527            // QuickJS rejects any strict-mode digit after a leading zero
1528            // before it scans the rest of the literal.
1529            if leading_zero
1530                && self.options.context.strict
1531                && self
1532                    .peek_nth_char(1)
1533                    .is_some_and(|next| next.is_ascii_digit())
1534            {
1535                return Err(self.error_from(
1536                    start,
1537                    LexErrorKind::InvalidNumber,
1538                    "octal literals are deprecated in strict mode",
1539                ));
1540            }
1541            if leading_zero && self.peek_nth_char(1) == Some('_') {
1542                self.bump_char();
1543                self.bump_char();
1544                return Err(self.error_from(
1545                    start,
1546                    LexErrorKind::InvalidNumber,
1547                    "numeric separator cannot follow a leading zero",
1548                ));
1549            }
1550            let run = self.scan_digit_run(10)?;
1551            integer_digits = run.digits;
1552            had_separator |= run.had_separator;
1553            saw_8_or_9 |= run.saw_8_or_9;
1554            legacy_octal_integer = leading_zero && integer_digits > 1 && !saw_8_or_9;
1555            if leading_zero && integer_digits > 1 && had_separator {
1556                return Err(self.error_from(
1557                    start,
1558                    LexErrorKind::InvalidNumber,
1559                    "numeric separators are not allowed in legacy leading-zero literals",
1560                ));
1561            }
1562
1563            if self.peek_char() == Some('.') && !legacy_octal_integer {
1564                is_float = true;
1565                self.bump_char();
1566                let run = self.scan_digit_run(10)?;
1567                had_separator |= run.had_separator;
1568            }
1569        }
1570
1571        if matches!(self.peek_char(), Some('e' | 'E')) && !legacy_octal_integer {
1572            is_float = true;
1573            self.bump_char();
1574            if matches!(self.peek_char(), Some('+' | '-')) {
1575                self.bump_char();
1576            }
1577            let run = self.scan_digit_run(10)?;
1578            if run.digits == 0 {
1579                return Err(self.error_from(
1580                    start,
1581                    LexErrorKind::InvalidNumber,
1582                    "exponent requires at least one decimal digit",
1583                ));
1584            }
1585            had_separator |= run.had_separator;
1586        }
1587
1588        let legacy_leading_zero = leading_zero && integer_digits > 1;
1589        if legacy_leading_zero && had_separator {
1590            return Err(self.error_from(
1591                start,
1592                LexErrorKind::InvalidNumber,
1593                "numeric separators are not allowed in legacy leading-zero literals",
1594            ));
1595        }
1596
1597        let kind = if self.peek_char() == Some('n') {
1598            if is_float {
1599                self.bump_char();
1600                return Err(self.error_from(
1601                    start,
1602                    LexErrorKind::InvalidNumber,
1603                    "invalid number literal",
1604                ));
1605            }
1606            if legacy_leading_zero {
1607                self.bump_char();
1608                return Err(self.error_from(
1609                    start,
1610                    LexErrorKind::InvalidNumber,
1611                    "invalid number literal",
1612                ));
1613            }
1614            self.bump_char();
1615            NumberKind::BigInt(NumericRadix::Decimal)
1616        } else if is_float {
1617            NumberKind::Float
1618        } else if legacy_leading_zero && !had_separator {
1619            if saw_8_or_9 {
1620                NumberKind::LegacyDecimal
1621            } else {
1622                NumberKind::LegacyOctal
1623            }
1624        } else {
1625            NumberKind::Integer(NumericRadix::Decimal)
1626        };
1627
1628        self.reject_number_follow(start)?;
1629        Ok(TokenKind::Number(NumberLiteral {
1630            raw: &self.source[raw_start..self.offset],
1631            kind,
1632        }))
1633    }
1634
1635    fn scan_digit_run(&mut self, base: u32) -> Result<DigitRun, LexError> {
1636        let mut result = DigitRun::default();
1637        let mut previous_was_digit = false;
1638
1639        while let Some(ch) = self.peek_char() {
1640            if ch.is_digit(base) {
1641                self.bump_char();
1642                result.digits += 1;
1643                result.saw_8_or_9 |= matches!(ch, '8' | '9');
1644                previous_was_digit = true;
1645                continue;
1646            }
1647
1648            if ch == '_' {
1649                let separator = self.current_position();
1650                let next_is_digit = self
1651                    .peek_nth_char(1)
1652                    .is_some_and(|next| next.is_digit(base));
1653                if !previous_was_digit || !next_is_digit {
1654                    self.bump_char();
1655                    return Err(self.error_from(
1656                        separator,
1657                        LexErrorKind::InvalidNumber,
1658                        "numeric separator must occur between two digits",
1659                    ));
1660                }
1661                self.bump_char();
1662                result.had_separator = true;
1663                previous_was_digit = false;
1664                continue;
1665            }
1666            break;
1667        }
1668        Ok(result)
1669    }
1670
1671    fn reject_number_follow(&mut self, start: Position) -> Result<(), LexError> {
1672        let Some(ch) = self.peek_char() else {
1673            return Ok(());
1674        };
1675        if is_line_terminator(ch) || is_js_whitespace(ch) {
1676            return Ok(());
1677        }
1678        if is_identifier_continue(ch) {
1679            self.bump_char();
1680            return Err(self.error_from(
1681                start,
1682                LexErrorKind::InvalidNumber,
1683                "invalid number literal",
1684            ));
1685        }
1686        Ok(())
1687    }
1688
1689    fn scan_string(&mut self) -> Result<TokenKind<'a>, LexError> {
1690        let mut value = ValidateSink::new(self.string_limit);
1691        let literal = self.scan_string_with_sink(&mut value)?;
1692        Ok(TokenKind::String(literal))
1693    }
1694
1695    fn scan_string_with_sink(
1696        &mut self,
1697        value: &mut impl StringSink,
1698    ) -> Result<StringLiteral<'a>, LexError> {
1699        let start = self.current_position();
1700        let raw_start = self.offset;
1701        let separator = self.bump_char().expect("called at quote");
1702        let mut has_escape = false;
1703        let mut has_legacy_octal_escape = false;
1704
1705        loop {
1706            // Ordinary ASCII cannot be a surrogate marker or malformed-byte
1707            // carrier. Count/copy the whole run without per-scalar dispatch.
1708            // DEL remains on the carrier-aware path along with non-ASCII.
1709            let bytes = self.source.as_bytes();
1710            let mut end = self.offset;
1711            while let Some(&byte) = bytes.get(end) {
1712                if byte >= 0x7f || byte == separator as u8 || matches!(byte, b'\\' | b'\r' | b'\n')
1713                {
1714                    break;
1715                }
1716                end += 1;
1717            }
1718            if end != self.offset {
1719                let result = value.push_ascii(&bytes[self.offset..end]);
1720                let consumed = match result {
1721                    Ok(()) => end - self.offset,
1722                    Err(consumed) => consumed,
1723                };
1724                self.offset += consumed;
1725                self.column = self.column.saturating_add(consumed as u32);
1726                if result.is_err() {
1727                    return Err(self.string_too_long(start));
1728                }
1729            }
1730            if self.invalid_source_byte_at(self.offset) {
1731                return Err(self.error_from(
1732                    start,
1733                    LexErrorKind::UnexpectedCharacter,
1734                    "invalid UTF-8 sequence",
1735                ));
1736            }
1737            let Some(ch) = self.peek_char() else {
1738                return Err(self.error_from(
1739                    start,
1740                    LexErrorKind::UnterminatedString,
1741                    "unexpected end of string",
1742                ));
1743            };
1744            if ch == separator {
1745                self.bump_char();
1746                return Ok(StringLiteral {
1747                    raw: &self.source[raw_start..self.offset],
1748                    has_escape,
1749                    has_legacy_octal_escape,
1750                });
1751            }
1752            if matches!(ch, '\r' | '\n') {
1753                // Pinned QuickJS reaches its `invalid_char` label and reports
1754                // the error through the token pointer, which is still the
1755                // opening quote (a bare CR is not a line continuation either:
1756                // only an escaped CR/LF is consumed by scan_escape_sequence).
1757                return Err(self.error_from(
1758                    start,
1759                    LexErrorKind::UnterminatedString,
1760                    "unexpected end of string",
1761                ));
1762            }
1763            if ch == '\\' {
1764                has_escape = true;
1765                let escape = match self.scan_escape_sequence(false) {
1766                    Err(error)
1767                        if error.kind == LexErrorKind::UnexpectedCharacter
1768                            && error.message == "invalid UTF-8 sequence" =>
1769                    {
1770                        return Err(self.error_from(
1771                            start,
1772                            LexErrorKind::UnexpectedCharacter,
1773                            error.message,
1774                        ));
1775                    }
1776                    // A trailing bare backslash is reported at the opening
1777                    // quote, exactly like any other unterminated string: the
1778                    // pinned parser raises it from the token pointer.
1779                    Err(error)
1780                        if error.kind == LexErrorKind::UnterminatedString
1781                            && error.message == "unexpected end of string" =>
1782                    {
1783                        return Err(self.error_from(
1784                            start,
1785                            LexErrorKind::UnterminatedString,
1786                            error.message,
1787                        ));
1788                    }
1789                    result => result?,
1790                };
1791                has_legacy_octal_escape |= escape.legacy_octal;
1792                if let Some(code_point) = escape.code_point {
1793                    value
1794                        .push_code_point(code_point)
1795                        .map_err(|_| self.string_too_long(start))?;
1796                }
1797                continue;
1798            }
1799
1800            let lone_surrogate = self.surrogate_at(self.offset);
1801            self.bump_char();
1802            if let Some(unit) = lone_surrogate {
1803                value
1804                    .push_code_unit(unit)
1805                    .map_err(|_| self.string_too_long(start))?;
1806            } else {
1807                value
1808                    .push_char(ch)
1809                    .map_err(|_| self.string_too_long(start))?;
1810            }
1811        }
1812    }
1813
1814    fn scan_escape_sequence(&mut self, template: bool) -> Result<EscapeValue, LexError> {
1815        let start = self.current_position();
1816        debug_assert_eq!(self.peek_char(), Some('\\'));
1817        self.bump_char();
1818        if self.invalid_source_byte_at(self.offset) {
1819            return Err(self.invalid_utf8_error_here("invalid UTF-8 sequence"));
1820        }
1821        let Some(ch) = self.peek_char() else {
1822            // Pinned QuickJS's `case '\0': goto invalid_char` turns a bare
1823            // trailing backslash into "unexpected end of string" rather than
1824            // an escape error.  scan_string re-anchors the span at the
1825            // opening quote; the template scanner's own EOF branch wins.
1826            return Err(self.error_from(
1827                start,
1828                LexErrorKind::UnterminatedString,
1829                "unexpected end of string",
1830            ));
1831        };
1832
1833        if is_line_terminator(ch) {
1834            self.bump_char();
1835            return Ok(EscapeValue::line_continuation());
1836        }
1837
1838        let simple = match ch {
1839            '\'' => Some('\'' as u32),
1840            '"' => Some('"' as u32),
1841            '\\' => Some('\\' as u32),
1842            'b' => Some(0x08),
1843            'f' => Some(0x0c),
1844            'n' => Some('\n' as u32),
1845            'r' => Some('\r' as u32),
1846            't' => Some('\t' as u32),
1847            'v' => Some(0x0b),
1848            _ => None,
1849        };
1850        if let Some(code_point) = simple {
1851            self.bump_char();
1852            return Ok(EscapeValue::code_point(code_point));
1853        }
1854
1855        if ch == '0'
1856            && !self
1857                .peek_nth_char(1)
1858                .is_some_and(|next| next.is_ascii_digit())
1859        {
1860            self.bump_char();
1861            return Ok(EscapeValue::code_point(0));
1862        }
1863
1864        match ch {
1865            'x' => {
1866                self.bump_char();
1867                let value = self
1868                    .scan_fixed_hex_digits(2, start)
1869                    .map_err(|error| self.remap_string_escape_utf8(error, start, template))?;
1870                Ok(EscapeValue::code_point(value))
1871            }
1872            'u' => {
1873                self.bump_char();
1874                let value = self
1875                    .scan_unicode_escape_value(start)
1876                    .map_err(|error| self.remap_string_escape_utf8(error, start, template))?;
1877                Ok(EscapeValue::code_point(value))
1878            }
1879            '0'..='7' => {
1880                if template || self.options.context.strict {
1881                    self.bump_char();
1882                    return Err(self.error_from(
1883                        start,
1884                        LexErrorKind::InvalidEscape,
1885                        if self.options.context.strict {
1886                            "octal escape sequences are not allowed in strict mode"
1887                        } else {
1888                            "legacy octal escape is not allowed in this context"
1889                        },
1890                    ));
1891                }
1892                let value = self.scan_legacy_octal_escape();
1893                Ok(EscapeValue::legacy(value))
1894            }
1895            '8' | '9' => {
1896                if template || self.options.context.strict {
1897                    self.bump_char();
1898                    // Pinned QuickJS routes the strict/template `\8` and `\9`
1899                    // rejection through lre_parse_escape's failure label,
1900                    // which uses the generic malformed-escape wording.
1901                    return Err(self.error_from(
1902                        start,
1903                        LexErrorKind::InvalidEscape,
1904                        "malformed escape sequence in string literal",
1905                    ));
1906                }
1907                self.bump_char();
1908                Ok(EscapeValue::legacy(ch as u32))
1909            }
1910            _ => {
1911                let lone_surrogate = self.surrogate_at(self.offset);
1912                self.bump_char();
1913                Ok(EscapeValue::code_point(
1914                    lone_surrogate.map_or(ch as u32, u32::from),
1915                ))
1916            }
1917        }
1918    }
1919
1920    fn scan_legacy_octal_escape(&mut self) -> u32 {
1921        let first = self.peek_char().expect("called at octal digit");
1922        let max_digits = if matches!(first, '0'..='3') { 3 } else { 2 };
1923        let mut value = 0_u32;
1924        let mut count = 0;
1925        while count < max_digits {
1926            let Some(ch @ '0'..='7') = self.peek_char() else {
1927                break;
1928            };
1929            self.bump_char();
1930            value = (value * 8) + ch.to_digit(8).expect("matched octal digit");
1931            count += 1;
1932        }
1933        value
1934    }
1935
1936    fn remap_string_escape_utf8(
1937        &self,
1938        error: LexError,
1939        start: Position,
1940        template: bool,
1941    ) -> LexError {
1942        if !template
1943            && error.kind == LexErrorKind::UnexpectedCharacter
1944            && error.message == "invalid UTF-8 sequence"
1945        {
1946            self.error_from(
1947                start,
1948                LexErrorKind::InvalidEscape,
1949                "malformed escape sequence in string literal",
1950            )
1951        } else {
1952            error
1953        }
1954    }
1955
1956    fn scan_unicode_escape_value(&mut self, start: Position) -> Result<u32, LexError> {
1957        if self.peek_char() == Some('{') {
1958            self.bump_char();
1959            let mut digits = 0;
1960            let mut value = 0_u32;
1961            while let Some(ch) = self.peek_char() {
1962                if self.invalid_source_byte_at(self.offset) {
1963                    return Err(self.invalid_utf8_error_here("invalid UTF-8 sequence"));
1964                }
1965                let Some(digit) = ch.to_digit(16) else {
1966                    break;
1967                };
1968                self.bump_char();
1969                value = value
1970                    .checked_mul(16)
1971                    .and_then(|value| value.checked_add(digit))
1972                    .filter(|value| *value <= 0x10ffff)
1973                    .unwrap_or(0x110000);
1974                digits += 1;
1975            }
1976            if digits == 0 || self.peek_char() != Some('}') {
1977                return Err(self.error_from(
1978                    start,
1979                    LexErrorKind::InvalidEscape,
1980                    "malformed escape sequence in string literal",
1981                ));
1982            }
1983            self.bump_char();
1984            if value > 0x10ffff {
1985                return Err(self.error_from(
1986                    start,
1987                    LexErrorKind::InvalidEscape,
1988                    "malformed escape sequence in string literal",
1989                ));
1990            }
1991            Ok(value)
1992        } else {
1993            self.scan_fixed_hex_digits(4, start)
1994        }
1995    }
1996
1997    fn scan_fixed_hex_digits(&mut self, count: usize, start: Position) -> Result<u32, LexError> {
1998        let mut value = 0_u32;
1999        for _ in 0..count {
2000            if self.invalid_source_byte_at(self.offset) {
2001                return Err(self.invalid_utf8_error_here("invalid UTF-8 sequence"));
2002            }
2003            // Both exhaustion and a non-hex byte take pinned QuickJS's
2004            // lre_parse_escape failure path, whose only wording is the
2005            // generic malformed-escape message.
2006            let Some(ch) = self.peek_char() else {
2007                return Err(self.error_from(
2008                    start,
2009                    LexErrorKind::InvalidEscape,
2010                    "malformed escape sequence in string literal",
2011                ));
2012            };
2013            let Some(digit) = ch.to_digit(16) else {
2014                self.bump_char();
2015                return Err(self.error_from(
2016                    start,
2017                    LexErrorKind::InvalidEscape,
2018                    "malformed escape sequence in string literal",
2019                ));
2020            };
2021            self.bump_char();
2022            value = (value << 4) | digit;
2023        }
2024        Ok(value)
2025    }
2026
2027    fn scan_template(
2028        &mut self,
2029        initial: bool,
2030        line_terminator_before: bool,
2031    ) -> Result<Token<'a>, LexError> {
2032        let mut raw_value = ValidateSink::new(self.string_limit);
2033        let mut cooked = Some(ValidateSink::new(self.string_limit));
2034        self.scan_template_with_sinks(
2035            initial,
2036            line_terminator_before,
2037            &mut raw_value,
2038            &mut cooked,
2039            &mut None,
2040        )
2041    }
2042
2043    fn scan_template_with_sinks(
2044        &mut self,
2045        initial: bool,
2046        line_terminator_before: bool,
2047        raw_value: &mut impl StringSink,
2048        cooked: &mut Option<impl StringSink>,
2049        invalid_escape: &mut Option<TemplateEscapeError>,
2050    ) -> Result<Token<'a>, LexError> {
2051        let start = if initial {
2052            self.current_position()
2053        } else {
2054            let current = self.current_position();
2055            if current.byte_offset == 0
2056                || self.source.as_bytes().get(current.byte_offset - 1) != Some(&b'}')
2057            {
2058                return Err(self.error_from(
2059                    current,
2060                    LexErrorKind::ExpectedTemplateContinuation,
2061                    "template continuation must immediately follow '}'",
2062                ));
2063            }
2064            Position::new(
2065                current.byte_offset - 1,
2066                current.line,
2067                current.column.saturating_sub(1),
2068            )
2069        };
2070        if initial {
2071            debug_assert_eq!(self.peek_char(), Some(TEMPLATE_QUOTE));
2072            self.bump_char();
2073        }
2074        let raw_start = self.offset;
2075
2076        loop {
2077            if self.invalid_source_byte_at(self.offset) {
2078                return Err(self.invalid_utf8_error_here("invalid UTF-8 sequence"));
2079            }
2080            let Some(ch) = self.peek_char() else {
2081                return Err(self.error_from(
2082                    start,
2083                    LexErrorKind::UnterminatedTemplate,
2084                    "unexpected end of string",
2085                ));
2086            };
2087
2088            if ch == TEMPLATE_QUOTE {
2089                let raw_end = self.offset;
2090                self.bump_char();
2091                let kind = if initial {
2092                    TemplatePartKind::NoSubstitution
2093                } else {
2094                    TemplatePartKind::Tail
2095                };
2096                return Ok(Token {
2097                    kind: TokenKind::Template(TemplatePart {
2098                        raw: &self.source[raw_start..raw_end],
2099                        invalid_escape: invalid_escape.is_some(),
2100                        kind,
2101                    }),
2102                    span: Span::new(start, self.current_position()),
2103                    line_terminator_before,
2104                });
2105            }
2106
2107            if ch == '$' && self.peek_nth_char(1) == Some('{') {
2108                let raw_end = self.offset;
2109                self.bump_char();
2110                self.bump_char();
2111                let kind = if initial {
2112                    TemplatePartKind::Head
2113                } else {
2114                    TemplatePartKind::Middle
2115                };
2116                return Ok(Token {
2117                    kind: TokenKind::Template(TemplatePart {
2118                        raw: &self.source[raw_start..raw_end],
2119                        invalid_escape: invalid_escape.is_some(),
2120                        kind,
2121                    }),
2122                    span: Span::new(start, self.current_position()),
2123                    line_terminator_before,
2124                });
2125            }
2126
2127            if ch == '\\' {
2128                let escape_start = self.offset;
2129                // QuickJS first scans the raw template boundary and only then
2130                // cooks the resulting segment.  Keep those two concerns
2131                // transactional here: a malformed `\\x` or `\\u` must not
2132                // consume the closing backtick or the `$` in a following
2133                // `${`, even if the cooked escape scanner inspected it.
2134                let mut cooked_cursor = self.clone();
2135                let escape = cooked_cursor.scan_escape_sequence(true);
2136                if let Err(error) = &escape
2137                    && error.kind == LexErrorKind::UnexpectedCharacter
2138                    && error.message == "invalid UTF-8 sequence"
2139                {
2140                    return Err(*error);
2141                }
2142                if escape.is_ok() {
2143                    *self = cooked_cursor;
2144                } else {
2145                    self.bump_char();
2146                    self.bump_char();
2147                }
2148                self.append_template_raw_source(raw_value, escape_start..self.offset)
2149                    .map_err(|_| self.string_too_long(start))?;
2150                match escape {
2151                    Ok(escape) => {
2152                        if let (Some(value), Some(output)) = (escape.code_point, cooked.as_mut()) {
2153                            output
2154                                .push_code_point(value)
2155                                .map_err(|_| self.string_too_long(start))?;
2156                        }
2157                    }
2158                    Err(error) => {
2159                        if invalid_escape.is_none() {
2160                            *invalid_escape = Some(TemplateEscapeError {
2161                                message: error.message,
2162                                span: error.span,
2163                            });
2164                        }
2165                        *cooked = None;
2166                    }
2167                }
2168                continue;
2169            }
2170
2171            let lone_surrogate = self.surrogate_at(self.offset);
2172            self.bump_char();
2173            if let Some(unit) = lone_surrogate {
2174                raw_value
2175                    .push_code_unit(unit)
2176                    .map_err(|_| self.string_too_long(start))?;
2177                if let Some(output) = cooked.as_mut() {
2178                    output
2179                        .push_code_unit(unit)
2180                        .map_err(|_| self.string_too_long(start))?;
2181                }
2182            } else {
2183                raw_value
2184                    .push_char(if ch == '\r' { '\n' } else { ch })
2185                    .map_err(|_| self.string_too_long(start))?;
2186                if let Some(output) = cooked.as_mut() {
2187                    if ch == '\r' {
2188                        output
2189                            .push_char('\n')
2190                            .map_err(|_| self.string_too_long(start))?;
2191                    } else {
2192                        output
2193                            .push_char(ch)
2194                            .map_err(|_| self.string_too_long(start))?;
2195                    }
2196                }
2197            }
2198        }
2199    }
2200
2201    fn append_template_raw_source(
2202        &self,
2203        value: &mut impl StringSink,
2204        range: Range<usize>,
2205    ) -> Result<(), JsStringError> {
2206        let units = self
2207            .semantic_utf16_units(range)
2208            .expect("template raw range is a trusted source slice");
2209        append_template_raw_units(&mut *value, units)
2210    }
2211
2212    fn scan_regexp(
2213        &mut self,
2214        start: Position,
2215        line_terminator_before: bool,
2216    ) -> Result<Token<'a>, LexError> {
2217        debug_assert_eq!(self.peek_char(), Some('/'));
2218        self.bump_char();
2219        let pattern_start = self.offset;
2220        let mut in_character_class = false;
2221
2222        let pattern_end = loop {
2223            if self.invalid_source_byte_at(self.offset) {
2224                return Err(self.invalid_utf8_error_here("invalid UTF-8 sequence"));
2225            }
2226            let Some(ch) = self.peek_char() else {
2227                return Err(self.error_from(
2228                    start,
2229                    LexErrorKind::UnterminatedRegExp,
2230                    "unexpected end of regexp",
2231                ));
2232            };
2233            if is_line_terminator(ch) {
2234                return Err(self.error_here(
2235                    LexErrorKind::LineTerminatorInRegExp,
2236                    "unexpected line terminator in regexp",
2237                ));
2238            }
2239            if ch == '/' && !in_character_class {
2240                let end = self.offset;
2241                self.bump_char();
2242                break end;
2243            }
2244            if ch == '\\' {
2245                self.bump_char();
2246                if self.invalid_source_byte_at(self.offset) {
2247                    return Err(self.invalid_utf8_error_here("invalid UTF-8 sequence"));
2248                }
2249                let Some(escaped) = self.peek_char() else {
2250                    return Err(self.error_from(
2251                        start,
2252                        LexErrorKind::UnterminatedRegExp,
2253                        "unexpected end of regexp",
2254                    ));
2255                };
2256                if is_line_terminator(escaped) {
2257                    return Err(self.error_here(
2258                        LexErrorKind::LineTerminatorInRegExp,
2259                        "unexpected line terminator in regexp",
2260                    ));
2261                }
2262                self.bump_char();
2263                continue;
2264            }
2265            if ch == '[' {
2266                in_character_class = true;
2267            } else if ch == ']' {
2268                in_character_class = false;
2269            }
2270            self.bump_char();
2271        };
2272
2273        let flags_start = self.offset;
2274        while let Some(ch) = self.peek_char() {
2275            if self.invalid_source_byte_at(self.offset) {
2276                return Err(self.invalid_utf8_error_here("invalid UTF-8 sequence"));
2277            }
2278            if is_identifier_continue(ch) {
2279                self.bump_char();
2280            } else {
2281                break;
2282            }
2283        }
2284
2285        Ok(Token {
2286            kind: TokenKind::RegExp(RegExpLiteral {
2287                pattern: &self.source[pattern_start..pattern_end],
2288                flags: &self.source[flags_start..self.offset],
2289            }),
2290            span: Span::new(start, self.current_position()),
2291            line_terminator_before,
2292        })
2293    }
2294
2295    fn scan_punctuator(&mut self) -> Result<TokenKind<'a>, LexError> {
2296        use Punctuator::*;
2297
2298        let bytes = self.source.as_bytes();
2299        let byte = bytes[self.offset];
2300        let at = |offset: usize| bytes.get(self.offset + offset).copied();
2301
2302        let (len, punctuator) = match byte {
2303            b'>' => match (at(1), at(2), at(3)) {
2304                (Some(b'>'), Some(b'>'), Some(b'=')) => (4, UnsignedShiftRightAssign),
2305                (Some(b'>'), Some(b'>'), _) => (3, UnsignedShiftRight),
2306                (Some(b'>'), Some(b'='), _) => (3, ShiftRightAssign),
2307                (Some(b'>'), _, _) => (2, ShiftRight),
2308                (Some(b'='), _, _) => (2, GreaterEqual),
2309                _ => (1, Greater),
2310            },
2311            b'=' => match (at(1), at(2)) {
2312                (Some(b'='), Some(b'=')) => (3, StrictEqual),
2313                (Some(b'='), _) => (2, EqualEqual),
2314                (Some(b'>'), _) => (2, Arrow),
2315                _ => (1, Equal),
2316            },
2317            b'!' => match (at(1), at(2)) {
2318                (Some(b'='), Some(b'=')) => (3, StrictNotEqual),
2319                (Some(b'='), _) => (2, NotEqual),
2320                _ => (1, Not),
2321            },
2322            b'*' => match (at(1), at(2)) {
2323                (Some(b'*'), Some(b'=')) => (3, ExponentAssign),
2324                (Some(b'*'), _) => (2, Exponent),
2325                (Some(b'='), _) => (2, MultiplyAssign),
2326                _ => (1, Multiply),
2327            },
2328            b'<' => match (at(1), at(2)) {
2329                (Some(b'<'), Some(b'=')) => (3, ShiftLeftAssign),
2330                (Some(b'<'), _) => (2, ShiftLeft),
2331                (Some(b'='), _) => (2, LessEqual),
2332                _ => (1, Less),
2333            },
2334            b'&' => match (at(1), at(2)) {
2335                (Some(b'&'), Some(b'=')) => (3, LogicalAndAssign),
2336                (Some(b'&'), _) => (2, LogicalAnd),
2337                (Some(b'='), _) => (2, BitAndAssign),
2338                _ => (1, BitAnd),
2339            },
2340            b'|' => match (at(1), at(2)) {
2341                (Some(b'|'), Some(b'=')) => (3, LogicalOrAssign),
2342                (Some(b'|'), _) => (2, LogicalOr),
2343                (Some(b'='), _) => (2, BitOrAssign),
2344                _ => (1, BitOr),
2345            },
2346            b'?' => {
2347                if at(1) == Some(b'.') && !at(2).is_some_and(|byte| byte.is_ascii_digit()) {
2348                    (2, OptionalChain)
2349                } else {
2350                    match (at(1), at(2)) {
2351                        (Some(b'?'), Some(b'=')) => (3, NullishAssign),
2352                        (Some(b'?'), _) => (2, NullishCoalesce),
2353                        _ => (1, Question),
2354                    }
2355                }
2356            }
2357            b'.' => match (at(1), at(2)) {
2358                (Some(b'.'), Some(b'.')) => (3, Ellipsis),
2359                _ => (1, Dot),
2360            },
2361            b'-' => match at(1) {
2362                Some(b'-') => (2, Decrement),
2363                Some(b'=') => (2, MinusAssign),
2364                _ => (1, Minus),
2365            },
2366            b'+' => match at(1) {
2367                Some(b'+') => (2, Increment),
2368                Some(b'=') => (2, PlusAssign),
2369                _ => (1, Plus),
2370            },
2371            b'/' => match at(1) {
2372                Some(b'=') => (2, DivideAssign),
2373                _ => (1, Divide),
2374            },
2375            b'%' => match at(1) {
2376                Some(b'=') => (2, RemainderAssign),
2377                _ => (1, Remainder),
2378            },
2379            b'^' => match at(1) {
2380                Some(b'=') => (2, BitXorAssign),
2381                _ => (1, BitXor),
2382            },
2383            b'{' => (1, LeftBrace),
2384            b'}' => (1, RightBrace),
2385            b'(' => (1, LeftParen),
2386            b')' => (1, RightParen),
2387            b'[' => (1, LeftBracket),
2388            b']' => (1, RightBracket),
2389            b';' => (1, Semicolon),
2390            b',' => (1, Comma),
2391            b'~' => (1, BitNot),
2392            b':' => (1, Colon),
2393            _ => {
2394                let ch = self.peek_char().expect("called before end of source");
2395                if ch.is_ascii() {
2396                    self.bump_char();
2397                    return Ok(TokenKind::RawAscii(ch as u8));
2398                }
2399                return Err(
2400                    self.error_here(LexErrorKind::UnexpectedCharacter, "unexpected character")
2401                );
2402            }
2403        };
2404
2405        self.offset += len;
2406        self.column = self.column.saturating_add(len as u32);
2407        Ok(TokenKind::Punctuator(punctuator))
2408    }
2409}
2410
2411impl<'a> Iterator for Lexer<'a> {
2412    type Item = Result<Token<'a>, LexError>;
2413
2414    fn next(&mut self) -> Option<Self::Item> {
2415        if self.eof_emitted || self.failed {
2416            return None;
2417        }
2418        let result = self.next_token();
2419        if result.is_err() {
2420            self.failed = true;
2421        }
2422        Some(result)
2423    }
2424}
2425
2426impl FusedIterator for Lexer<'_> {}
2427
2428#[derive(Clone, Copy, Debug, Default)]
2429struct DigitRun {
2430    digits: usize,
2431    had_separator: bool,
2432    saw_8_or_9: bool,
2433}
2434
2435#[derive(Clone, Copy, Debug)]
2436struct EscapeValue {
2437    code_point: Option<u32>,
2438    legacy_octal: bool,
2439}
2440
2441impl EscapeValue {
2442    const fn code_point(value: u32) -> Self {
2443        Self {
2444            code_point: Some(value),
2445            legacy_octal: false,
2446        }
2447    }
2448
2449    const fn legacy(value: u32) -> Self {
2450        Self {
2451            code_point: Some(value),
2452            legacy_octal: true,
2453        }
2454    }
2455
2456    const fn line_continuation() -> Self {
2457        Self {
2458            code_point: None,
2459            legacy_octal: false,
2460        }
2461    }
2462}
2463
2464fn is_line_terminator(ch: char) -> bool {
2465    matches!(ch, '\n' | '\r' | '\u{2028}' | '\u{2029}')
2466}
2467
2468/// Reproduce the deliberately small scanner behind QuickJS
2469/// `simple_next_token(..., TRUE)`. Its block-comment loop notices only byte
2470/// CR/LF, even though direct U+2028/U+2029 trivia is a LineTerminator. Keep
2471/// this quirk separate from normal token metadata, which remains
2472/// ECMAScript-correct for ASI and restricted productions.
2473pub fn quickjs_simple_lookahead_has_line_terminator(mut source: &str) -> bool {
2474    loop {
2475        let Some(ch) = source.chars().next() else {
2476            return false;
2477        };
2478        match ch {
2479            '\r' | '\n' | '\u{2028}' | '\u{2029}' => return true,
2480            '/' if source.starts_with("//") => {
2481                // QuickJS returns a LineTerminator for this lookahead as soon
2482                // as it sees a line comment, including one ending at EOF.
2483                return true;
2484            }
2485            '/' if source.starts_with("/*") => {
2486                let Some(end) = source[2..].find("*/") else {
2487                    return false;
2488                };
2489                let body = &source[2..2 + end];
2490                if body.bytes().any(|byte| matches!(byte, b'\r' | b'\n')) {
2491                    return true;
2492                }
2493                source = &source[2 + end + 2..];
2494            }
2495            ch if is_js_whitespace(ch) => {
2496                source = &source[ch.len_utf8()..];
2497            }
2498            _ => return false,
2499        }
2500    }
2501}
2502
2503/// Reproduce the `TOK_OF` branch of QuickJS `simple_next_token(..., FALSE)`.
2504/// This is intentionally a raw scanner rather than a normal Lexer pass:
2505/// Annex B HTML comments are not trivia here, and a backslash immediately
2506/// after raw `of` is not decoded as an IdentifierPart by `match_identifier`.
2507pub fn quickjs_simple_lookahead_is_of(mut source: &str) -> bool {
2508    loop {
2509        let Some(ch) = source.chars().next() else {
2510            return false;
2511        };
2512        if source.starts_with("//") {
2513            let Some(end) = source
2514                .as_bytes()
2515                .iter()
2516                .position(|byte| matches!(byte, b'\r' | b'\n'))
2517            else {
2518                return false;
2519            };
2520            source = &source[end..];
2521        } else if source.starts_with("/*") {
2522            let Some(end) = source[2..].find("*/") else {
2523                return false;
2524            };
2525            source = &source[2 + end + 2..];
2526        } else if is_line_terminator(ch) || is_js_whitespace(ch) {
2527            source = &source[ch.len_utf8()..];
2528        } else {
2529            break;
2530        }
2531    }
2532
2533    let Some(after) = source.strip_prefix("of") else {
2534        return false;
2535    };
2536    after
2537        .chars()
2538        .next()
2539        .is_none_or(|ch| !is_identifier_continue(ch))
2540}
2541
2542/// Reproduce the small raw scanner used by QuickJS `JS_DetectModule`.
2543///
2544/// This intentionally does not share the full ECMAScript lexer: upstream's
2545/// heuristic performs literal keyword probes, treats escaped keyword tails as
2546/// matches, and only distinguishes the first token after raw `import`.
2547#[must_use]
2548#[cfg(test)]
2549pub fn quickjs_detect_module(source: &str) -> bool {
2550    quickjs_detect_module_bytes(source.as_bytes())
2551}
2552
2553/// Apply QuickJS's module-detection heuristic to an explicitly sized source
2554/// buffer without decoding or replacing authored bytes.
2555///
2556/// Like upstream `JS_DetectModule`, the shebang scan observes the explicit
2557/// buffer length, while the following simple token scanner treats an embedded
2558/// NUL as end of input. Malformed UTF-8 is skipped inside comments and a
2559/// shebang but remains a non-identifier token everywhere else.
2560#[must_use]
2561pub fn quickjs_detect_module_bytes(mut source: &[u8]) -> bool {
2562    source = quickjs_detect_module_skip_shebang(source);
2563    let Some((token, rest)) = quickjs_detect_module_token(source) else {
2564        return false;
2565    };
2566    match token {
2567        QuickJsDetectModuleToken::Export => true,
2568        QuickJsDetectModuleToken::Import => !matches!(
2569            quickjs_detect_module_token(rest).map(|(token, _)| token),
2570            Some(QuickJsDetectModuleToken::Dot | QuickJsDetectModuleToken::LeftParen)
2571        ),
2572        QuickJsDetectModuleToken::Dot
2573        | QuickJsDetectModuleToken::LeftParen
2574        | QuickJsDetectModuleToken::Other => false,
2575    }
2576}
2577
2578#[derive(Clone, Copy)]
2579enum QuickJsDetectModuleToken {
2580    Import,
2581    Export,
2582    Dot,
2583    LeftParen,
2584    Other,
2585}
2586
2587fn quickjs_detect_module_skip_shebang(source: &[u8]) -> &[u8] {
2588    if !source.starts_with(b"#!") {
2589        return source;
2590    }
2591
2592    let mut offset = 2;
2593    while offset < source.len() {
2594        match source[offset] {
2595            b'\r' | b'\n' => break,
2596            byte if byte >= 0x80 => match decode_quickjs_utf8(&source[offset..]) {
2597                Some((0x2028 | 0x2029, consumed)) => {
2598                    offset += consumed;
2599                    break;
2600                }
2601                Some((_, consumed)) => offset += consumed,
2602                None => offset += 1,
2603            },
2604            _ => offset += 1,
2605        }
2606    }
2607    &source[offset..]
2608}
2609
2610fn quickjs_detect_module_token(mut source: &[u8]) -> Option<(QuickJsDetectModuleToken, &[u8])> {
2611    loop {
2612        let &byte = source.first()?;
2613        if source.starts_with(b"//") {
2614            let (offset, &terminator) = source
2615                .iter()
2616                .enumerate()
2617                .find(|(_, byte)| matches!(byte, b'\0' | b'\r' | b'\n'))?;
2618            if terminator == b'\0' {
2619                return None;
2620            }
2621            source = &source[offset..];
2622        } else if source.starts_with(b"/*") {
2623            let body = &source[2..];
2624            let mut offset = 0;
2625            loop {
2626                let &body_byte = body.get(offset)?;
2627                if body_byte == b'\0' {
2628                    return None;
2629                }
2630                if body_byte == b'*' && body.get(offset + 1) == Some(&b'/') {
2631                    source = &body[offset + 2..];
2632                    break;
2633                }
2634                offset += 1;
2635            }
2636        } else {
2637            let (code_point, consumed) = if byte < 0x80 {
2638                (u32::from(byte), 1)
2639            } else {
2640                let Some(decoded) = decode_quickjs_utf8(source) else {
2641                    break;
2642                };
2643                decoded
2644            };
2645            let is_space = char::from_u32(code_point)
2646                .is_some_and(|ch| is_line_terminator(ch) || is_js_whitespace(ch));
2647            if !is_space {
2648                break;
2649            }
2650            source = &source[consumed..];
2651        }
2652    }
2653
2654    if let Some(rest) = source.strip_prefix(b"import")
2655        && !quickjs_detect_module_starts_identifier_continue(rest)
2656    {
2657        return Some((QuickJsDetectModuleToken::Import, rest));
2658    } else if let Some(rest) = source.strip_prefix(b"export")
2659        && !quickjs_detect_module_starts_identifier_continue(rest)
2660    {
2661        return Some((QuickJsDetectModuleToken::Export, rest));
2662    } else if let Some(rest) = source.strip_prefix(b".") {
2663        return Some((QuickJsDetectModuleToken::Dot, rest));
2664    } else if let Some(rest) = source.strip_prefix(b"(") {
2665        return Some((QuickJsDetectModuleToken::LeftParen, rest));
2666    }
2667    Some((QuickJsDetectModuleToken::Other, source))
2668}
2669
2670fn quickjs_detect_module_starts_identifier_continue(source: &[u8]) -> bool {
2671    decode_quickjs_utf8(source)
2672        .is_some_and(|(code_point, _)| is_identifier_continue_code_point(code_point))
2673}
2674
2675fn is_js_whitespace(ch: char) -> bool {
2676    matches!(
2677        ch,
2678        '\t' | '\u{000b}' | '\u{000c}' | ' ' | '\u{00a0}' | '\u{1680}' | '\u{2000}'
2679            ..='\u{200a}' | '\u{202f}' | '\u{205f}' | '\u{3000}' | '\u{feff}'
2680    )
2681}
2682
2683fn is_ascii_identifier_start(ch: char) -> bool {
2684    ch.is_ascii_alphabetic() || matches!(ch, '_' | '$')
2685}
2686
2687fn is_ascii_identifier_continue(ch: char) -> bool {
2688    is_ascii_identifier_start(ch) || ch.is_ascii_digit()
2689}
2690
2691// Scanner tables keep the common ASCII path independent of Unicode decoding.
2692// V8's scanner uses the same combined classification/keyword-candidate idea.
2693const IDENTIFIER_START: u8 = 1;
2694const IDENTIFIER_CONTINUE: u8 = 2;
2695const NOT_KEYWORD: u8 = 4;
2696const MAY_START_TRIVIA: u8 = 8;
2697const ASCII_FLAGS: [u8; 128] = {
2698    let mut table = [0; 128];
2699    let mut byte = 0u8;
2700    while byte < 128 {
2701        let start = byte.is_ascii_alphabetic() || matches!(byte, b'_' | b'$');
2702        table[byte as usize] = if start {
2703            IDENTIFIER_START | IDENTIFIER_CONTINUE
2704        } else if byte.is_ascii_digit() {
2705            IDENTIFIER_CONTINUE
2706        } else {
2707            0
2708        };
2709        if !byte.is_ascii_lowercase() {
2710            table[byte as usize] |= NOT_KEYWORD;
2711        }
2712        if matches!(
2713            byte,
2714            b' ' | b'\t' | b'\r' | b'\n' | 0x0b | 0x0c | b'/' | b'<' | b'-' | b'#'
2715        ) {
2716            table[byte as usize] |= MAY_START_TRIVIA;
2717        }
2718        byte += 1;
2719    }
2720    table
2721};
2722
2723#[derive(Clone, Copy)]
2724enum ScanKind {
2725    Single(Punctuator),
2726    Identifier,
2727    Number,
2728    String,
2729    Template,
2730    Dot,
2731    Private,
2732    Punctuator,
2733    Slow,
2734}
2735
2736const ASCII_DISPATCH: [ScanKind; 128] = {
2737    let mut table = [ScanKind::Punctuator; 128];
2738    let mut byte = 0u8;
2739    while byte < 128 {
2740        if ASCII_FLAGS[byte as usize] & IDENTIFIER_START != 0 {
2741            table[byte as usize] = ScanKind::Identifier;
2742        }
2743        if byte.is_ascii_digit() {
2744            table[byte as usize] = ScanKind::Number;
2745        }
2746        byte += 1;
2747    }
2748    table[b'\\' as usize] = ScanKind::Slow;
2749    table[b'\'' as usize] = ScanKind::String;
2750    table[b'"' as usize] = ScanKind::String;
2751    table[b'`' as usize] = ScanKind::Template;
2752    table[b'.' as usize] = ScanKind::Dot;
2753    table[b'#' as usize] = ScanKind::Private;
2754    table[b'{' as usize] = ScanKind::Single(Punctuator::LeftBrace);
2755    table[b'}' as usize] = ScanKind::Single(Punctuator::RightBrace);
2756    table[b'(' as usize] = ScanKind::Single(Punctuator::LeftParen);
2757    table[b')' as usize] = ScanKind::Single(Punctuator::RightParen);
2758    table[b'[' as usize] = ScanKind::Single(Punctuator::LeftBracket);
2759    table[b']' as usize] = ScanKind::Single(Punctuator::RightBracket);
2760    table[b';' as usize] = ScanKind::Single(Punctuator::Semicolon);
2761    table[b',' as usize] = ScanKind::Single(Punctuator::Comma);
2762    table[b':' as usize] = ScanKind::Single(Punctuator::Colon);
2763    table[b'~' as usize] = ScanKind::Single(Punctuator::BitNot);
2764    table
2765};
2766
2767fn is_ascii_identifier_start_byte(byte: u8) -> bool {
2768    ASCII_FLAGS[byte as usize] & IDENTIFIER_START != 0
2769}
2770
2771fn is_ascii_identifier_continue_byte(byte: u8) -> bool {
2772    ASCII_FLAGS[byte as usize] & IDENTIFIER_CONTINUE != 0
2773}
2774
2775fn is_identifier_start(ch: char) -> bool {
2776    is_identifier_start_code_point(ch as u32)
2777}
2778
2779fn is_identifier_continue(ch: char) -> bool {
2780    is_identifier_continue_code_point(ch as u32)
2781}
2782
2783fn is_identifier_start_code_point(code_point: u32) -> bool {
2784    if code_point < 0x80 {
2785        char::from_u32(code_point).is_some_and(is_ascii_identifier_start)
2786    } else {
2787        crate::source::unicode::is_id_start(code_point)
2788    }
2789}
2790
2791fn is_identifier_continue_code_point(code_point: u32) -> bool {
2792    if code_point < 0x80 {
2793        char::from_u32(code_point).is_some_and(is_ascii_identifier_continue)
2794    } else {
2795        matches!(code_point, 0x200c | 0x200d) || crate::source::unicode::is_id_continue(code_point)
2796    }
2797}
2798
2799fn radix_value(radix: NumericRadix) -> u32 {
2800    match radix {
2801        NumericRadix::Binary => 2,
2802        NumericRadix::Octal => 8,
2803        NumericRadix::Decimal => 10,
2804        NumericRadix::Hexadecimal => 16,
2805    }
2806}
2807
2808fn append_template_raw_units(
2809    value: &mut impl StringSink,
2810    units: impl IntoIterator<Item = u16>,
2811) -> Result<(), JsStringError> {
2812    let mut units = units.into_iter().peekable();
2813    while let Some(unit) = units.next() {
2814        if unit == u16::from(b'\r') {
2815            if units.peek() == Some(&u16::from(b'\n')) {
2816                units.next();
2817            }
2818            value.push_code_unit(u16::from(b'\n'))?;
2819        } else {
2820            value.push_code_unit(unit)?;
2821        }
2822    }
2823    Ok(())
2824}
2825
2826fn keyword_from_str(value: &str) -> Option<Keyword> {
2827    use Keyword::*;
2828    Some(match value {
2829        "null" => Null,
2830        "false" => False,
2831        "true" => True,
2832        "if" => If,
2833        "else" => Else,
2834        "return" => Return,
2835        "var" => Var,
2836        "this" => This,
2837        "delete" => Delete,
2838        "void" => Void,
2839        "typeof" => Typeof,
2840        "new" => New,
2841        "in" => In,
2842        "instanceof" => Instanceof,
2843        "do" => Do,
2844        "while" => While,
2845        "for" => For,
2846        "break" => Break,
2847        "continue" => Continue,
2848        "switch" => Switch,
2849        "case" => Case,
2850        "default" => Default,
2851        "throw" => Throw,
2852        "try" => Try,
2853        "catch" => Catch,
2854        "finally" => Finally,
2855        "function" => Function,
2856        "debugger" => Debugger,
2857        "with" => With,
2858        "class" => Class,
2859        "const" => Const,
2860        "enum" => Enum,
2861        "export" => Export,
2862        "extends" => Extends,
2863        "import" => Import,
2864        "super" => Super,
2865        "implements" => Implements,
2866        "interface" => Interface,
2867        "let" => Let,
2868        "package" => Package,
2869        "private" => Private,
2870        "protected" => Protected,
2871        "public" => Public,
2872        "static" => Static,
2873        "yield" => Yield,
2874        "await" => Await,
2875        _ => return None,
2876    })
2877}
2878
2879#[cfg(test)]
2880mod tests {
2881    use super::*;
2882
2883    fn kinds(source: &str) -> Vec<TokenKind<'_>> {
2884        Lexer::new(source)
2885            .tokenize()
2886            .expect("source should tokenize")
2887            .into_iter()
2888            .map(|token| token.kind)
2889            .collect()
2890    }
2891
2892    fn number_kinds(source: &str) -> Vec<(&str, NumberKind)> {
2893        Lexer::new(source)
2894            .tokenize()
2895            .expect("numbers should tokenize")
2896            .into_iter()
2897            .filter_map(|token| match token.kind {
2898                TokenKind::Number(number) => Some((number.raw, number.kind)),
2899                _ => None,
2900            })
2901            .collect()
2902    }
2903
2904    #[test]
2905    fn tracks_utf8_spans_lines_and_asi_trivia() {
2906        let source = "\"é\";\r\n/* one\n two */ const x = 1";
2907        let tokens = Lexer::new(source).tokenize().unwrap();
2908
2909        assert_eq!(tokens[0].span.start, Position::new(0, 1, 1));
2910        assert_eq!(tokens[0].span.end, Position::new(4, 1, 4));
2911        assert!(matches!(tokens[2].kind, TokenKind::Keyword(Keyword::Const)));
2912        assert!(tokens[2].line_terminator_before);
2913        assert_eq!(tokens[2].span.start.line, 3);
2914        assert_eq!(tokens[2].span.start.column, 9);
2915        assert!(!tokens[3].line_terminator_before);
2916    }
2917
2918    #[test]
2919    fn classifies_keywords_with_quickjs_context_rules() {
2920        let tokens = Lexer::new("const let yield await").tokenize().unwrap();
2921        assert!(matches!(tokens[0].kind, TokenKind::Keyword(Keyword::Const)));
2922        for token in &tokens[1..4] {
2923            assert!(matches!(
2924                token.kind,
2925                TokenKind::Identifier(Identifier {
2926                    keyword_hint: Some(_),
2927                    ..
2928                })
2929            ));
2930        }
2931
2932        let options = LexerOptions {
2933            context: LexContext {
2934                strict: true,
2935                module: true,
2936                ..LexContext::default()
2937            },
2938            ..LexerOptions::default()
2939        };
2940        let tokens = Lexer::with_options("let yield await", options)
2941            .tokenize()
2942            .unwrap();
2943        assert!(matches!(tokens[0].kind, TokenKind::Keyword(Keyword::Let)));
2944        assert!(matches!(tokens[1].kind, TokenKind::Keyword(Keyword::Yield)));
2945        assert!(matches!(tokens[2].kind, TokenKind::Keyword(Keyword::Await)));
2946    }
2947
2948    #[test]
2949    fn escaped_keyword_remains_identifier_with_reserved_marker() {
2950        let source = r"\u0069f";
2951        let token = Lexer::new(source).next_token().unwrap();
2952        match token.kind {
2953            TokenKind::Identifier(identifier) => {
2954                assert_eq!(
2955                    Lexer::new(source).decode_identifier_text(identifier.raw),
2956                    "if"
2957                );
2958                assert!(identifier.has_escape);
2959                assert_eq!(identifier.keyword_hint, Some(Keyword::If));
2960                assert!(identifier.escaped_reserved_word);
2961            }
2962            other => panic!("expected identifier, got {other:?}"),
2963        }
2964
2965        for source in [r"if\u{}", r"if\u{2d}"] {
2966            let token = Lexer::new(source).next_token().unwrap();
2967            let TokenKind::Identifier(identifier) = token.kind else {
2968                panic!("expected attempted Unicode escape to retain identifier form");
2969            };
2970            assert_eq!(identifier.raw, "if");
2971            assert!(identifier.has_escape);
2972            assert!(identifier.escaped_reserved_word);
2973        }
2974        assert!(matches!(
2975            Lexer::new(r"if\x61").next_token().unwrap().kind,
2976            TokenKind::Keyword(Keyword::If)
2977        ));
2978    }
2979
2980    #[test]
2981    fn escaped_identifier_decode_and_rewind_after_a_long_minified_line() {
2982        let source = format!("{}\\u0061\\u{{62}}é;\r\n\\u{{10400}};", "x;".repeat(4096));
2983        let mut lexer = Lexer::new(&source);
2984        let mut escaped = Vec::new();
2985        loop {
2986            let token = lexer.next_token().unwrap();
2987            if let TokenKind::Identifier(identifier) = token.kind
2988                && identifier.has_escape
2989            {
2990                escaped.push((token, identifier));
2991            }
2992            if matches!(token.kind, TokenKind::Eof) {
2993                break;
2994            }
2995        }
2996        assert_eq!(escaped[0].0.span.start, Position::new(8192, 1, 8193));
2997        assert_eq!(escaped[1].0.span.start.line, 2);
2998        assert_eq!(escaped[1].0.span.start.column, 1);
2999        for ((token, identifier), expected) in escaped.into_iter().zip(["abé", "𐐀"]) {
3000            assert_eq!(lexer.decode_identifier_text(identifier.raw), expected);
3001            lexer.seek(token.span.start);
3002            let replayed = lexer.next_token().unwrap();
3003            assert_eq!(replayed.kind, token.kind);
3004            assert_eq!(replayed.span, token.span);
3005        }
3006    }
3007
3008    #[test]
3009    fn ascii_identifier_path_matches_full_scanner_at_context_and_length_boundaries() {
3010        for context in [
3011            LexContext::default(),
3012            LexContext {
3013                strict: true,
3014                ..LexContext::default()
3015            },
3016            LexContext {
3017                generator: true,
3018                async_function: true,
3019                module: true,
3020                ..LexContext::default()
3021            },
3022        ] {
3023            for source in [
3024                "name;",
3025                "a",
3026                "async()",
3027                "yield ",
3028                "await",
3029                "let",
3030                "Name9_",
3031                "alphaé;",
3032                "éclair;",
3033                r"if\u{2d}",
3034                r"f\u006fo",
3035                "alpha\u{a0}",
3036                "alpha\u{2028}",
3037                "#name",
3038                "#yield",
3039                "#é",
3040                r"#\u0061",
3041                "#9",
3042            ] {
3043                for limit in [1, 2, 5, 31, RuntimeJsString::MAX_LEN] {
3044                    let mut fast = Lexer::with_options(
3045                        source,
3046                        LexerOptions {
3047                            context,
3048                            ..LexerOptions::default()
3049                        },
3050                    )
3051                    .with_string_limit(limit);
3052                    let mut full = fast.clone();
3053                    let private = source.starts_with('#');
3054                    let expected = full
3055                        .scan_identifier_with_value(private)
3056                        .map(|(kind, _)| kind);
3057                    assert_eq!(
3058                        fast.scan_identifier(private),
3059                        expected,
3060                        "{source:?}, {context:?}, limit {limit}"
3061                    );
3062                    assert_eq!(fast.current_position(), full.current_position());
3063                }
3064            }
3065        }
3066    }
3067
3068    #[test]
3069    fn unicode_identifiers_use_pinned_start_continue_and_escape_rules() {
3070        let source = "π变量𐐀a\u{0300}\u{200c}\u{200d}";
3071        let token = Lexer::new(source).next_token().unwrap();
3072        let TokenKind::Identifier(identifier) = token.kind else {
3073            panic!("expected a Unicode identifier");
3074        };
3075        assert_eq!(identifier.raw, source);
3076        assert_eq!(
3077            Lexer::new(source).decode_identifier_text(identifier.raw),
3078            source
3079        );
3080        assert!(!identifier.has_escape);
3081        assert_eq!(
3082            token.span,
3083            Span::new(Position::new(0, 1, 1), Position::new(21, 1, 9))
3084        );
3085
3086        let escaped_source = r"\u03c0\u{10400}a\u0300";
3087        let escaped = Lexer::new(escaped_source).next_token().unwrap();
3088        let TokenKind::Identifier(identifier) = escaped.kind else {
3089            panic!("expected an escaped Unicode identifier");
3090        };
3091        assert_eq!(
3092            Lexer::new(escaped_source).decode_identifier_text(identifier.raw),
3093            "π𐐀a\u{0300}"
3094        );
3095        assert!(identifier.has_escape);
3096
3097        let leading_zero_source = r"\u{0000000000010400}";
3098        let leading_zero_escape = Lexer::new(leading_zero_source).next_token().unwrap();
3099        let TokenKind::Identifier(identifier) = leading_zero_escape.kind else {
3100            panic!("expected a leading-zero Unicode escape identifier");
3101        };
3102        assert_eq!(
3103            Lexer::new(leading_zero_source).decode_identifier_text(identifier.raw),
3104            "𐐀"
3105        );
3106
3107        let escaped_tail_source = r"a\u{2d}";
3108        let mut escaped_invalid_tail = Lexer::new(escaped_tail_source);
3109        let TokenKind::Identifier(identifier) = escaped_invalid_tail.next_token().unwrap().kind
3110        else {
3111            panic!("expected the valid identifier prefix");
3112        };
3113        assert_eq!(
3114            Lexer::new(escaped_tail_source).decode_identifier_text(identifier.raw),
3115            "a"
3116        );
3117        assert!(identifier.has_escape);
3118        assert!(matches!(
3119            escaped_invalid_tail.next_token().unwrap().kind,
3120            TokenKind::RawAscii(b'\\')
3121        ));
3122
3123        assert_eq!(
3124            Lexer::new("😀").next_token().unwrap_err().kind,
3125            LexErrorKind::UnexpectedCharacter
3126        );
3127        for source in [r"\u0300", r"\u{}", r"\u{2d}", r"\x61"] {
3128            let token = Lexer::new(source).next_token().unwrap();
3129            assert!(matches!(token.kind, TokenKind::RawAscii(b'\\')));
3130            assert_eq!(token.span.end.byte_offset, 1);
3131        }
3132        assert!(matches!(
3133            Lexer::new("@").next_token().unwrap().kind,
3134            TokenKind::RawAscii(b'@')
3135        ));
3136        let mut invalid_tail = Lexer::new("ascii😀");
3137        assert!(matches!(
3138            invalid_tail.next_token().unwrap().kind,
3139            TokenKind::Identifier(_)
3140        ));
3141        assert_eq!(
3142            invalid_tail.next_token().unwrap_err().kind,
3143            LexErrorKind::UnexpectedCharacter
3144        );
3145    }
3146
3147    #[test]
3148    fn scans_private_identifiers_and_identifier_escapes() {
3149        let private_source = r"#pr\u0069vate";
3150        let token = Lexer::new(private_source).next_token().unwrap();
3151        match token.kind {
3152            TokenKind::PrivateIdentifier(identifier) => {
3153                assert_eq!(identifier.raw, private_source);
3154                assert_eq!(
3155                    Lexer::new(private_source).decode_identifier_text(identifier.raw),
3156                    "private"
3157                );
3158                assert!(identifier.has_escape);
3159            }
3160            other => panic!("expected private identifier, got {other:?}"),
3161        }
3162        for source in ["#", "#1", "# "] {
3163            let error = Lexer::new(source).next_token().unwrap_err();
3164            assert_eq!(error.kind, LexErrorKind::InvalidPrivateIdentifier);
3165            assert_eq!(error.message, "invalid first character of private name");
3166        }
3167        for source in [r"#\u{}", r"#\u{2d}", r"#\u{d800}"] {
3168            let error = Lexer::new(source).next_token().unwrap_err();
3169            assert_eq!(error.kind, LexErrorKind::InvalidPrivateIdentifier);
3170            assert_eq!(error.span.start, Position::new(0, 1, 1));
3171        }
3172        let unicode_private_source = "#π\u{0300}";
3173        let token = Lexer::new(unicode_private_source).next_token().unwrap();
3174        let TokenKind::PrivateIdentifier(identifier) = token.kind else {
3175            panic!("expected a Unicode private identifier");
3176        };
3177        assert_eq!(
3178            Lexer::new(unicode_private_source).decode_identifier_text(identifier.raw),
3179            "π\u{0300}"
3180        );
3181
3182        for source in [r"#a\u{}", r"#a\u{2d}", r"#a\uD800"] {
3183            let mut lexer = Lexer::new(source);
3184            let TokenKind::PrivateIdentifier(identifier) = lexer.next_token().unwrap().kind else {
3185                panic!("expected the valid private-name prefix for {source}");
3186            };
3187            assert_eq!(
3188                Lexer::new(source).decode_identifier_text(identifier.raw),
3189                "a"
3190            );
3191            assert!(identifier.has_escape);
3192            assert!(matches!(
3193                lexer.next_token().unwrap().kind,
3194                TokenKind::RawAscii(b'\\')
3195            ));
3196        }
3197    }
3198
3199    #[test]
3200    fn scans_numeric_radices_floats_exponents_separators_and_bigints() {
3201        assert_eq!(
3202            number_kinds("0 42 1_000 0b1010 0o755 0xCA_FE .5 1. 2e-3 99n 0xffn"),
3203            vec![
3204                ("0", NumberKind::Integer(NumericRadix::Decimal)),
3205                ("42", NumberKind::Integer(NumericRadix::Decimal)),
3206                ("1_000", NumberKind::Integer(NumericRadix::Decimal)),
3207                ("0b1010", NumberKind::Integer(NumericRadix::Binary)),
3208                ("0o755", NumberKind::Integer(NumericRadix::Octal)),
3209                ("0xCA_FE", NumberKind::Integer(NumericRadix::Hexadecimal)),
3210                (".5", NumberKind::Float),
3211                ("1.", NumberKind::Float),
3212                ("2e-3", NumberKind::Float),
3213                ("99n", NumberKind::BigInt(NumericRadix::Decimal)),
3214                ("0xffn", NumberKind::BigInt(NumericRadix::Hexadecimal)),
3215            ]
3216        );
3217        assert_eq!(
3218            number_kinds("077 089"),
3219            vec![
3220                ("077", NumberKind::LegacyOctal),
3221                ("089", NumberKind::LegacyDecimal),
3222            ]
3223        );
3224    }
3225
3226    #[test]
3227    fn malformed_number_diagnostic_matches_quickjs_wording() {
3228        // Pinned QuickJS 2026-06-04 reports a single `invalid number literal`
3229        // message for every malformed numeric literal (quickjs.c:22928,
3230        // js_parse_get_number), regardless of the specific grammar failure.
3231        // Three failure families, all sharing the same upstream message:
3232        // prefixed literals without digits / out-of-radix digits; a BigInt
3233        // suffix on a fraction or exponent; a decimal BigInt with a legacy
3234        // leading zero.
3235        for source in [
3236            "0x", "0X", "0b", "0B", "0o", "0O", "0b2n", "0xgn", "0o8", "1.0n", ".5n", "0e0n",
3237            "1E2n", "01n", "00n", "012348n", "0008n",
3238        ] {
3239            let error = Lexer::new(source).next_token().unwrap_err();
3240            assert_eq!(error.kind, LexErrorKind::InvalidNumber, "{source}");
3241            assert_eq!(error.message, "invalid number literal", "{source}");
3242            assert_eq!(error.span.start, Position::new(0, 1, 1), "{source}");
3243        }
3244    }
3245
3246    #[test]
3247    fn rejects_malformed_numbers() {
3248        for source in [
3249            "0b2",
3250            "0o8",
3251            "0x",
3252            "1__0",
3253            "1_",
3254            "1e+",
3255            "1.0n",
3256            "01n",
3257            "00_1",
3258            "08.1_2",
3259            "08e1_2",
3260            "10instanceof",
3261            "1π",
3262            "1\u{0300}",
3263        ] {
3264            let error = Lexer::new(source).next_token().unwrap_err();
3265            assert_eq!(
3266                error.kind,
3267                LexErrorKind::InvalidNumber,
3268                "unexpected error for {source}"
3269            );
3270        }
3271
3272        let options = LexerOptions {
3273            context: LexContext {
3274                strict: true,
3275                ..LexContext::default()
3276            },
3277            ..LexerOptions::default()
3278        };
3279        assert_eq!(
3280            Lexer::with_options("077", options)
3281                .next_token()
3282                .unwrap_err()
3283                .kind,
3284            LexErrorKind::InvalidNumber
3285        );
3286
3287        let mut escaped_identifier = Lexer::new(r"1\u0061");
3288        assert!(matches!(
3289            escaped_identifier.next_token().unwrap().kind,
3290            TokenKind::Number(_)
3291        ));
3292        let TokenKind::Identifier(identifier) = escaped_identifier.next_token().unwrap().kind
3293        else {
3294            panic!("expected escaped identifier after the number");
3295        };
3296        assert_eq!(
3297            escaped_identifier.decode_identifier_text(identifier.raw),
3298            "a"
3299        );
3300    }
3301
3302    #[test]
3303    fn decodes_string_escapes_without_losing_lone_surrogates() {
3304        let source = r#""a\n\x42\u{43}\uD800" '\141\8\0'"#;
3305        let lexer = Lexer::new(source);
3306        let tokens = lexer.clone().tokenize().unwrap();
3307        let TokenKind::String(_) = &tokens[0].kind else {
3308            panic!("expected string");
3309        };
3310        let first = lexer.decode_string_literal(tokens[0].span.start).unwrap();
3311        assert_eq!(first.utf16, vec![0x61, 0x0a, 0x42, 0x43, 0xd800]);
3312        assert!(first.to_string().is_err());
3313
3314        let TokenKind::String(second) = &tokens[1].kind else {
3315            panic!("expected string");
3316        };
3317        let second_value = lexer.decode_string_literal(tokens[1].span.start).unwrap();
3318        assert_eq!(second_value.utf16, vec![0x61, 0x38, 0]);
3319        assert!(second.has_legacy_octal_escape);
3320    }
3321
3322    #[test]
3323    fn handles_string_line_continuations_and_strict_escape_errors() {
3324        let lexer = Lexer::new("\"a\\\r\nb\"");
3325        let token = lexer.clone().next_token().unwrap();
3326        let TokenKind::String(_) = token.kind else {
3327            panic!("expected string");
3328        };
3329        assert_eq!(
3330            lexer
3331                .decode_string_literal(token.span.start)
3332                .unwrap()
3333                .to_string()
3334                .unwrap(),
3335            "ab"
3336        );
3337        assert_eq!(token.span.end.line, 2);
3338
3339        let options = LexerOptions {
3340            context: LexContext {
3341                strict: true,
3342                ..LexContext::default()
3343            },
3344            ..LexerOptions::default()
3345        };
3346        assert_eq!(
3347            Lexer::with_options(r"'\1'", options)
3348                .next_token()
3349                .unwrap_err()
3350                .kind,
3351            LexErrorKind::InvalidEscape
3352        );
3353        let zero_lexer = Lexer::with_options(r"'\0'", options);
3354        let zero = zero_lexer.clone().next_token().unwrap();
3355        let TokenKind::String(_) = zero.kind else {
3356            panic!("expected string");
3357        };
3358        assert_eq!(
3359            zero_lexer
3360                .decode_string_literal(zero.span.start)
3361                .unwrap()
3362                .utf16,
3363            vec![0]
3364        );
3365    }
3366
3367    #[test]
3368    fn longest_matches_punctuators() {
3369        let tokens = kinds(">>>= === !== **= &&= ||= ??= ... ?. ?.3 => ++ --");
3370        let punctuators: Vec<Punctuator> = tokens
3371            .into_iter()
3372            .filter_map(|kind| match kind {
3373                TokenKind::Punctuator(punctuator) => Some(punctuator),
3374                _ => None,
3375            })
3376            .collect();
3377        assert_eq!(
3378            punctuators,
3379            vec![
3380                Punctuator::UnsignedShiftRightAssign,
3381                Punctuator::StrictEqual,
3382                Punctuator::StrictNotEqual,
3383                Punctuator::ExponentAssign,
3384                Punctuator::LogicalAndAssign,
3385                Punctuator::LogicalOrAssign,
3386                Punctuator::NullishAssign,
3387                Punctuator::Ellipsis,
3388                Punctuator::OptionalChain,
3389                Punctuator::Question,
3390                Punctuator::Arrow,
3391                Punctuator::Increment,
3392                Punctuator::Decrement,
3393            ]
3394        );
3395    }
3396
3397    #[test]
3398    fn slash_interpretation_is_an_explicit_lexical_goal() {
3399        let mut division = Lexer::new("/= value");
3400        assert!(matches!(
3401            division.next_token().unwrap().kind,
3402            TokenKind::Punctuator(Punctuator::DivideAssign)
3403        ));
3404
3405        let mut regexp = Lexer::new(r"/a[\/]b+/gim");
3406        let token = regexp.next_token_with_goal(LexicalGoal::RegExp).unwrap();
3407        match token.kind {
3408            TokenKind::RegExp(literal) => {
3409                assert_eq!(literal.pattern, r"a[\/]b+");
3410                assert_eq!(literal.flags, "gim");
3411                assert_eq!(
3412                    &regexp.source()[token.span.start.byte_offset..token.span.end.byte_offset],
3413                    r"/a[\/]b+/gim"
3414                );
3415            }
3416            other => panic!("expected regular expression, got {other:?}"),
3417        }
3418
3419        let mut unicode_flags = Lexer::new("/x/π\u{0300}\u{200c}");
3420        let token = unicode_flags
3421            .next_token_with_goal(LexicalGoal::RegExp)
3422            .unwrap();
3423        let TokenKind::RegExp(literal) = token.kind else {
3424            panic!("expected regular expression with Unicode flags");
3425        };
3426        assert_eq!(literal.flags, "π\u{0300}\u{200c}");
3427
3428        assert_eq!(
3429            Lexer::new("value")
3430                .next_token_with_goal(LexicalGoal::RegExp)
3431                .unwrap_err()
3432                .kind,
3433            LexErrorKind::ExpectedRegExp
3434        );
3435    }
3436
3437    #[test]
3438    fn regexp_rejects_line_terminators() {
3439        let mut lexer = Lexer::new("/a\n/");
3440        assert_eq!(
3441            lexer
3442                .next_token_with_goal(LexicalGoal::RegExp)
3443                .unwrap_err()
3444                .kind,
3445            LexErrorKind::LineTerminatorInRegExp
3446        );
3447    }
3448
3449    #[test]
3450    fn reversible_source_text_restores_string_template_and_regexp_values() {
3451        let string_source = SourceText::try_from_utf16([
3452            u16::from(b'\''),
3453            0xd800,
3454            0xe000,
3455            u16::from(b'\\'),
3456            0xdfff,
3457            u16::from(b'\''),
3458        ])
3459        .unwrap();
3460        let string_lexer = Lexer::with_source_text(&string_source, LexerOptions::default());
3461        let string = string_lexer.clone().next_token().unwrap();
3462        let TokenKind::String(_) = string.kind else {
3463            panic!("expected reversible String token");
3464        };
3465        assert_eq!(
3466            string_lexer
3467                .decode_string_literal(string.span.start)
3468                .unwrap()
3469                .utf16,
3470            [0xd800, 0xe000, 0xdfff]
3471        );
3472
3473        let template_source = SourceText::try_from_utf16([
3474            u16::from(b'`'),
3475            0xd800,
3476            u16::from(b'\\'),
3477            0xdfff,
3478            u16::from(b'`'),
3479        ])
3480        .unwrap();
3481        let template_lexer = Lexer::with_source_text(&template_source, LexerOptions::default());
3482        let template = template_lexer.clone().next_token().unwrap();
3483        let TokenKind::Template(_) = template.kind else {
3484            panic!("expected reversible Template token");
3485        };
3486        assert_eq!(
3487            template_lexer
3488                .decode_template_raw_value(template.span.start, true)
3489                .unwrap()
3490                .utf16,
3491            [0xd800, 0x5c, 0xdfff]
3492        );
3493        assert_eq!(
3494            template_lexer
3495                .decode_template_cooked_value(template.span.start, true)
3496                .unwrap()
3497                .utf16,
3498            [0xd800, 0xdfff]
3499        );
3500
3501        let regexp_source = SourceText::try_from_utf16([
3502            u16::from(b'/'),
3503            0xd800,
3504            0xe000,
3505            u16::from(b'\\'),
3506            0xdfff,
3507            u16::from(b'/'),
3508        ])
3509        .unwrap();
3510        let mut lexer = Lexer::with_source_text(&regexp_source, LexerOptions::default());
3511        let regexp = lexer.next_token_with_goal(LexicalGoal::RegExp).unwrap();
3512        let TokenKind::RegExp(regexp) = regexp.kind else {
3513            panic!("expected reversible RegExp token");
3514        };
3515        let pattern = lexer
3516            .source_range_to_js_string(1..1 + regexp.pattern.len())
3517            .unwrap()
3518            .unwrap();
3519        assert_eq!(
3520            pattern.utf16_units().collect::<Vec<_>>(),
3521            [0xd800, 0xe000, 0x5c, 0xdfff]
3522        );
3523    }
3524
3525    #[test]
3526    fn raw_source_invalid_bytes_are_skipped_only_inside_trivia() {
3527        let source = SourceText::try_from_raw_bytes(
3528            b"#! invalid \x80\n// invalid \xc0\x80\n/* invalid \xffX */ 42",
3529        )
3530        .unwrap();
3531        let tokens = Lexer::with_source_text(&source, LexerOptions::default())
3532            .tokenize()
3533            .unwrap();
3534
3535        let TokenKind::Number(number) = &tokens[0].kind else {
3536            panic!("expected number after raw-byte comments");
3537        };
3538        assert_eq!(number.raw, "42");
3539        assert!(matches!(tokens[1].kind, TokenKind::Eof));
3540    }
3541
3542    #[test]
3543    fn raw_source_string_diagnostics_keep_quickjs_token_context() {
3544        for (raw, kind, message, expected_offset) in [
3545            (
3546                b"'\x80'".as_slice(),
3547                LexErrorKind::UnexpectedCharacter,
3548                "invalid UTF-8 sequence",
3549                0,
3550            ),
3551            (
3552                b"'\\\x80'",
3553                LexErrorKind::UnexpectedCharacter,
3554                "invalid UTF-8 sequence",
3555                0,
3556            ),
3557            (
3558                b"'\\x\x80'",
3559                LexErrorKind::InvalidEscape,
3560                "malformed escape sequence in string literal",
3561                1,
3562            ),
3563            (
3564                b"'\\u\x80'",
3565                LexErrorKind::InvalidEscape,
3566                "malformed escape sequence in string literal",
3567                1,
3568            ),
3569            (
3570                b"'\\u{\x80}'",
3571                LexErrorKind::InvalidEscape,
3572                "malformed escape sequence in string literal",
3573                1,
3574            ),
3575        ] {
3576            let source = SourceText::try_from_raw_bytes(raw).unwrap();
3577            let error = Lexer::with_source_text(&source, LexerOptions::default())
3578                .next_token()
3579                .unwrap_err();
3580            assert_eq!(error.kind, kind, "{raw:02x?}");
3581            assert_eq!(error.message, message, "{raw:02x?}");
3582            assert_eq!(error.span.start.byte_offset, expected_offset, "{raw:02x?}");
3583        }
3584    }
3585
3586    #[test]
3587    fn newline_terminated_strings_anchor_diagnostic_at_opening_quote() {
3588        for source in ["'abc\n", "'abc\r\n", "'abc\r", "\"abc\n", "'\\\n", "'"] {
3589            let error = Lexer::new(source).next_token().unwrap_err();
3590            assert_eq!(error.kind, LexErrorKind::UnterminatedString, "{source:?}");
3591            assert_eq!(error.message, "unexpected end of string", "{source:?}");
3592            // Pinned QuickJS raises through the token pointer, which still
3593            // addresses the opening quote at column 1.
3594            assert_eq!(
3595                (error.span.start.line, error.span.start.column),
3596                (1, 1),
3597                "{source:?}"
3598            );
3599        }
3600
3601        let second_line = Lexer::new("var t = 1;\n'abc\n").tokenize().unwrap_err();
3602        assert_eq!(second_line.kind, LexErrorKind::UnterminatedString);
3603        assert_eq!(
3604            (second_line.span.start.line, second_line.span.start.column),
3605            (2, 1)
3606        );
3607    }
3608
3609    #[test]
3610    fn malformed_string_escapes_share_pinned_quickjs_wording_and_span() {
3611        for source in [
3612            r"'\u00'",
3613            r"'\uz'",
3614            r"'\xZZ'",
3615            r"'\x'",
3616            r"'\u'",
3617            r"'\u{}'",
3618            r"'\u{zz}'",
3619            r"'\u{41'",
3620            r"'\u{110000}'",
3621        ] {
3622            let error = Lexer::new(source).next_token().unwrap_err();
3623            assert_eq!(error.kind, LexErrorKind::InvalidEscape, "{source}");
3624            assert_eq!(
3625                error.message, "malformed escape sequence in string literal",
3626                "{source}"
3627            );
3628            assert_eq!(error.span.start.byte_offset, 1, "{source}");
3629            assert_eq!(error.span.start.column, 2, "{source}");
3630        }
3631
3632        let strict = LexerOptions {
3633            context: LexContext {
3634                strict: true,
3635                ..LexContext::default()
3636            },
3637            ..LexerOptions::default()
3638        };
3639        for source in [r"'\8'", r"'\9'"] {
3640            let error = Lexer::with_options(source, strict)
3641                .next_token()
3642                .unwrap_err();
3643            assert_eq!(error.kind, LexErrorKind::InvalidEscape, "{source}");
3644            assert_eq!(
3645                error.message, "malformed escape sequence in string literal",
3646                "{source}"
3647            );
3648        }
3649    }
3650
3651    #[test]
3652    fn raw_source_template_and_regexp_diagnostics_point_at_malformed_byte() {
3653        for (raw, expected_offset) in [
3654            (b"`\x80`".as_slice(), 1),
3655            (b"`\\\x80`", 2),
3656            (b"`\\x\x80`", 3),
3657            (b"`\\u\x80`", 3),
3658            (b"`\\u{\x80}`", 4),
3659        ] {
3660            let source = SourceText::try_from_raw_bytes(raw).unwrap();
3661            let error = Lexer::with_source_text(&source, LexerOptions::default())
3662                .next_token()
3663                .unwrap_err();
3664            assert_eq!(error.kind, LexErrorKind::UnexpectedCharacter, "{raw:02x?}");
3665            assert_eq!(error.message, "invalid UTF-8 sequence", "{raw:02x?}");
3666            assert_eq!(error.span.start.byte_offset, expected_offset, "{raw:02x?}");
3667            assert!(source.invalid_byte_at(expected_offset));
3668        }
3669
3670        for raw in [b"/\x80/".as_slice(), b"/a\\\x80/", b"/a/\x80"] {
3671            let source = SourceText::try_from_raw_bytes(raw).unwrap();
3672            let error = Lexer::with_source_text(&source, LexerOptions::default())
3673                .next_token_with_goal(LexicalGoal::RegExp)
3674                .unwrap_err();
3675            assert_eq!(error.kind, LexErrorKind::UnexpectedCharacter, "{raw:02x?}");
3676            assert_eq!(error.message, "invalid UTF-8 sequence", "{raw:02x?}");
3677            assert!(source.invalid_byte_at(error.span.start.byte_offset));
3678        }
3679    }
3680
3681    #[test]
3682    fn raw_source_identifier_escape_stops_before_the_backslash() {
3683        let identifier = SourceText::try_from_raw_bytes(b"a\\u\x80").unwrap();
3684        let mut lexer = Lexer::with_source_text(&identifier, LexerOptions::default());
3685        let token = lexer.next_token().unwrap();
3686        let TokenKind::Identifier(name) = token.kind else {
3687            panic!("expected identifier before malformed escape");
3688        };
3689        assert_eq!(lexer.decode_identifier_text(name.raw), "a");
3690        assert_eq!(token.span.end.byte_offset, 1);
3691        let backslash = lexer.next_token().unwrap();
3692        assert!(matches!(backslash.kind, TokenKind::RawAscii(b'\\')));
3693        assert_eq!(backslash.span.start.byte_offset, 1);
3694    }
3695
3696    #[test]
3697    fn raw_source_malformed_bytes_never_enter_number_values() {
3698        let number = SourceText::try_from_raw_bytes(b"1\x80").unwrap();
3699        let mut lexer = Lexer::with_source_text(&number, LexerOptions::default());
3700        assert!(matches!(
3701            lexer.next_token().unwrap().kind,
3702            TokenKind::Number(_)
3703        ));
3704        let error = lexer.next_token().unwrap_err();
3705        assert_eq!(error.span.start.byte_offset, 1);
3706        assert_eq!(error.message, "unexpected character");
3707    }
3708
3709    #[test]
3710    fn genuine_del_and_pua_are_not_raw_source_markers() {
3711        let source = SourceText::try_from_raw_bytes("'\u{7f}\u{e000}'".as_bytes()).unwrap();
3712        let lexer = Lexer::with_source_text(&source, LexerOptions::default());
3713        let token = lexer.clone().next_token().unwrap();
3714        let TokenKind::String(_) = token.kind else {
3715            panic!("expected string containing genuine DEL and PUA");
3716        };
3717        assert_eq!(
3718            lexer.decode_string_literal(token.span.start).unwrap().utf16,
3719            [0x7f, 0xe000]
3720        );
3721    }
3722
3723    #[test]
3724    fn template_continuation_is_parser_driven() {
3725        let source = format!(
3726            "{}head{}{{name}}tail{}",
3727            TEMPLATE_QUOTE, '$', TEMPLATE_QUOTE
3728        );
3729        let mut lexer = Lexer::new(&source);
3730        let head_token = lexer.next_token().unwrap();
3731        let TokenKind::Template(head) = head_token.kind else {
3732            panic!("expected template head");
3733        };
3734        assert_eq!(head.kind, TemplatePartKind::Head);
3735        assert_eq!(head.raw, "head");
3736        assert_eq!(
3737            lexer
3738                .decode_template_cooked_value(head_token.span.start, true)
3739                .unwrap()
3740                .to_string()
3741                .unwrap(),
3742            "head"
3743        );
3744
3745        assert!(matches!(
3746            lexer.next_token().unwrap().kind,
3747            TokenKind::Identifier(_)
3748        ));
3749        assert!(matches!(
3750            lexer.next_token().unwrap().kind,
3751            TokenKind::Punctuator(Punctuator::RightBrace)
3752        ));
3753
3754        let tail_token = lexer
3755            .next_token_with_goal(LexicalGoal::TemplateContinuation)
3756            .unwrap();
3757        let TokenKind::Template(tail) = tail_token.kind else {
3758            panic!("expected template tail");
3759        };
3760        assert_eq!(tail.kind, TemplatePartKind::Tail);
3761        assert_eq!(tail.raw, "tail");
3762        assert_eq!(
3763            lexer
3764                .decode_template_cooked_value(tail_token.span.start, false)
3765                .unwrap()
3766                .to_string()
3767                .unwrap(),
3768            "tail"
3769        );
3770
3771        let error = Lexer::new("tail")
3772            .next_token_with_goal(LexicalGoal::TemplateContinuation)
3773            .unwrap_err();
3774        assert_eq!(error.kind, LexErrorKind::ExpectedTemplateContinuation);
3775    }
3776
3777    #[test]
3778    fn template_invalid_escape_is_preserved_for_tagged_semantics() {
3779        let source = format!("{TEMPLATE_QUOTE}bad\\8{TEMPLATE_QUOTE}");
3780        let lexer = Lexer::new(&source);
3781        let token = lexer.clone().next_token().unwrap();
3782        let TokenKind::Template(part) = token.kind else {
3783            panic!("expected template");
3784        };
3785        assert_eq!(part.kind, TemplatePartKind::NoSubstitution);
3786        assert!(part.invalid_escape);
3787        let invalid = lexer
3788            .template_escape_error(token.span.start, true)
3789            .unwrap()
3790            .expect("invalid escape metadata");
3791        assert_eq!(
3792            invalid.message,
3793            "malformed escape sequence in string literal"
3794        );
3795    }
3796
3797    #[test]
3798    fn cold_template_diagnostics_keep_multiline_positions() {
3799        let source = format!("`{}\r\né\\u{{x}}`", "a".repeat(8192));
3800        let lexer = Lexer::new(&source);
3801        let token = lexer.clone().next_token().unwrap();
3802        let TokenKind::Template(part) = token.kind else {
3803            panic!("expected template")
3804        };
3805        assert!(part.invalid_escape);
3806        let error = lexer
3807            .template_escape_error(token.span.start, true)
3808            .unwrap()
3809            .unwrap();
3810        assert_eq!(error.span.start, Position::new(8197, 2, 2));
3811        assert_eq!(error.span.end, Position::new(8200, 2, 5));
3812    }
3813
3814    #[test]
3815    fn cold_template_errors_do_not_widen_ordinary_tokens() {
3816        assert!(std::mem::size_of::<Token<'_>>() <= 80);
3817    }
3818
3819    #[test]
3820    fn malformed_template_escape_cannot_consume_a_structural_delimiter() {
3821        let token = Lexer::new("`\\x`").next_token().unwrap();
3822        let TokenKind::Template(part) = token.kind else {
3823            panic!("expected template");
3824        };
3825        assert_eq!(part.kind, TemplatePartKind::NoSubstitution);
3826        assert_eq!(part.raw, "\\x");
3827        assert!(part.invalid_escape);
3828        assert_eq!(
3829            Lexer::new("`\\x`")
3830                .template_escape_error(token.span.start, true)
3831                .unwrap()
3832                .unwrap()
3833                .span
3834                .start
3835                .column,
3836            2
3837        );
3838
3839        let mut lexer = Lexer::new("`\\x${value}`");
3840        let head_token = lexer.next_token().unwrap();
3841        let TokenKind::Template(head) = head_token.kind else {
3842            panic!("expected template head");
3843        };
3844        assert_eq!(head.kind, TemplatePartKind::Head);
3845        assert_eq!(head.raw, "\\x");
3846        assert_eq!(
3847            lexer
3848                .decode_template_raw_value(head_token.span.start, true)
3849                .unwrap()
3850                .to_string()
3851                .unwrap(),
3852            "\\x"
3853        );
3854        assert!(head.invalid_escape);
3855
3856        assert!(matches!(
3857            lexer.next_token().unwrap().kind,
3858            TokenKind::Identifier(_)
3859        ));
3860        assert!(matches!(
3861            lexer.next_token().unwrap().kind,
3862            TokenKind::Punctuator(Punctuator::RightBrace)
3863        ));
3864        let tail = lexer
3865            .next_token_with_goal(LexicalGoal::TemplateContinuation)
3866            .unwrap();
3867        let TokenKind::Template(tail) = tail.kind else {
3868            panic!("expected template tail");
3869        };
3870        assert_eq!(tail.kind, TemplatePartKind::Tail);
3871        assert_eq!(tail.raw, "");
3872
3873        let mut lexer = Lexer::new("`ok${value}tail\\x`");
3874        assert!(matches!(
3875            lexer.next_token().unwrap().kind,
3876            TokenKind::Template(TemplatePart {
3877                kind: TemplatePartKind::Head,
3878                ..
3879            })
3880        ));
3881        lexer.next_token().unwrap();
3882        lexer.next_token().unwrap();
3883        let tail_token = lexer
3884            .next_token_with_goal(LexicalGoal::TemplateContinuation)
3885            .unwrap();
3886        let TokenKind::Template(tail) = tail_token.kind else {
3887            panic!("expected malformed template tail");
3888        };
3889        assert_eq!(tail.kind, TemplatePartKind::Tail);
3890        assert_eq!(tail.raw, "tail\\x");
3891        assert_eq!(
3892            lexer
3893                .decode_template_raw_value(tail_token.span.start, false)
3894                .unwrap()
3895                .to_string()
3896                .unwrap(),
3897            "tail\\x"
3898        );
3899        assert!(tail.invalid_escape);
3900    }
3901
3902    #[test]
3903    fn template_raw_value_normalizes_only_physical_crlf() {
3904        let source = format!("{TEMPLATE_QUOTE}a\r\n\\nb{TEMPLATE_QUOTE}");
3905        let lexer = Lexer::new(&source);
3906        let token = lexer.clone().next_token().unwrap();
3907        let TokenKind::Template(part) = token.kind else {
3908            panic!("expected template");
3909        };
3910        assert_eq!(part.raw, "a\r\n\\nb");
3911        assert_eq!(
3912            lexer
3913                .decode_template_raw_value(token.span.start, true)
3914                .unwrap()
3915                .to_string()
3916                .unwrap(),
3917            "a\n\\nb"
3918        );
3919        assert_eq!(
3920            lexer
3921                .decode_template_cooked_value(token.span.start, true)
3922                .unwrap()
3923                .to_string()
3924                .unwrap(),
3925            "a\n\nb"
3926        );
3927    }
3928
3929    #[test]
3930    fn string_limit_counts_utf16_and_preserves_lexer_error_order() {
3931        let lexer = Lexer::new("'abc'").with_string_limit(3);
3932        let token = lexer.clone().next_token().unwrap();
3933        let TokenKind::String(_) = token.kind else {
3934            panic!("expected String token");
3935        };
3936        assert_eq!(
3937            lexer.decode_string_literal(token.span.start).unwrap().utf16,
3938            [0x61, 0x62, 0x63]
3939        );
3940
3941        for source in [
3942            "'abc'",
3943            "'😀'",
3944            "'\\u{1F600}'",
3945            "identifier",
3946            "𐐀",
3947            r"\u{10400}",
3948        ] {
3949            let limit = if source == "'abc'" { 2 } else { 1 };
3950            let error = Lexer::new(source)
3951                .with_string_limit(limit)
3952                .next_token()
3953                .unwrap_err();
3954            assert_eq!(error.kind, LexErrorKind::StringTooLong, "{source}");
3955            assert_eq!(error.message, "string too long");
3956        }
3957
3958        assert!(Lexer::new("'😀'").with_string_limit(2).next_token().is_ok());
3959        assert_eq!(
3960            Lexer::new("#a")
3961                .with_string_limit(1)
3962                .next_token()
3963                .unwrap_err()
3964                .kind,
3965            LexErrorKind::StringTooLong
3966        );
3967        assert!(Lexer::new("#a").with_string_limit(2).next_token().is_ok());
3968        assert!(Lexer::new("𐐀").with_string_limit(2).next_token().is_ok());
3969        assert!(
3970            Lexer::new(r"\u{10400}")
3971                .with_string_limit(2)
3972                .next_token()
3973                .is_ok()
3974        );
3975        assert!(
3976            Lexer::new("'\\u{1F600}'")
3977                .with_string_limit(2)
3978                .next_token()
3979                .is_ok()
3980        );
3981        assert!(
3982            Lexer::new("'a\\\nb'")
3983                .with_string_limit(2)
3984                .next_token()
3985                .is_ok()
3986        );
3987
3988        let overflow_first = Lexer::new("'ab\\xZ0'")
3989            .with_string_limit(1)
3990            .next_token()
3991            .unwrap_err();
3992        assert_eq!(overflow_first.kind, LexErrorKind::StringTooLong);
3993        let syntax_first = Lexer::new("'\\xZ0ab'")
3994            .with_string_limit(1)
3995            .next_token()
3996            .unwrap_err();
3997        assert_eq!(syntax_first.kind, LexErrorKind::InvalidEscape);
3998    }
3999
4000    #[test]
4001    fn ascii_string_runs_keep_overflow_and_unicode_cursor_positions() {
4002        for prefix in ["", "abc", "\\u{1f600}", "é", "\\n"] {
4003            let source = format!("'{prefix}abcdefghijklmnopqrstuvwxyz\\xZ'");
4004            let decoded_prefix = match prefix {
4005                "" => 0,
4006                "abc" => 3,
4007                "\\u{1f600}" => 2,
4008                _ => 1,
4009            };
4010            let error = Lexer::new(&source)
4011                .with_string_limit(decoded_prefix + 8)
4012                .next_token()
4013                .unwrap_err();
4014            assert_eq!(error.kind, LexErrorKind::StringTooLong);
4015            assert_eq!(error.span.end.byte_offset, 1 + prefix.len() + 9);
4016        }
4017        let source = "'abcédef\u{2028}ghi' next";
4018        let mut lexer = Lexer::new(source);
4019        let token = lexer.next_token().unwrap();
4020        assert_eq!(token.span.end.line, 2);
4021        assert_eq!(token.span.end.column, 5);
4022        assert_eq!(
4023            lexer.decode_string_literal(token.span.start).unwrap().utf16,
4024            "abcédef\u{2028}ghi".encode_utf16().collect::<Vec<_>>()
4025        );
4026        let next = lexer.next_token().unwrap();
4027        assert_eq!(next.span.start.line, 2);
4028        assert_eq!(next.span.start.column, 6);
4029    }
4030
4031    #[test]
4032    fn template_raw_and_cooked_values_share_the_checked_limit() {
4033        let lexer = Lexer::new("`\\u{1F600}`").with_string_limit(9);
4034        let token = lexer.clone().next_token().unwrap();
4035        let TokenKind::Template(_) = token.kind else {
4036            panic!("expected Template token");
4037        };
4038        assert_eq!(
4039            lexer
4040                .decode_template_raw_value(token.span.start, true)
4041                .unwrap()
4042                .utf16
4043                .len(),
4044            9
4045        );
4046        assert_eq!(
4047            lexer
4048                .decode_template_cooked_value(token.span.start, true)
4049                .unwrap()
4050                .utf16,
4051            [0xd83d, 0xde00]
4052        );
4053
4054        let raw_overflow = Lexer::new("`\\u{1F600}`")
4055            .with_string_limit(8)
4056            .next_token()
4057            .unwrap_err();
4058        assert_eq!(raw_overflow.kind, LexErrorKind::StringTooLong);
4059
4060        let invalid_cooked_still_checks_raw = Lexer::new("`\\8`")
4061            .with_string_limit(1)
4062            .next_token()
4063            .unwrap_err();
4064        assert_eq!(
4065            invalid_cooked_still_checks_raw.kind,
4066            LexErrorKind::StringTooLong
4067        );
4068
4069        assert!(
4070            Lexer::new("`a\r\nb`")
4071                .with_string_limit(3)
4072                .next_token()
4073                .is_ok()
4074        );
4075        assert_eq!(
4076            Lexer::new("`a\r\nb`")
4077                .with_string_limit(2)
4078                .next_token()
4079                .unwrap_err()
4080                .kind,
4081            LexErrorKind::StringTooLong
4082        );
4083    }
4084
4085    #[test]
4086    fn template_raw_overflow_precedes_later_escape_and_eof_errors() {
4087        // Raw preserves the two source units `\\n`, while cooked contains one
4088        // newline. QuickJS therefore trips the raw StringBuffer before it can
4089        // diagnose the missing closing backtick.
4090        let raw_before_eof = Lexer::new("`\\n")
4091            .with_string_limit(1)
4092            .next_token()
4093            .unwrap_err();
4094        assert_eq!(raw_before_eof.kind, LexErrorKind::StringTooLong);
4095        assert_eq!(raw_before_eof.message, "string too long");
4096
4097        let exact_raw_then_eof = Lexer::new("`\\n")
4098            .with_string_limit(2)
4099            .next_token()
4100            .unwrap_err();
4101        assert_eq!(exact_raw_then_eof.kind, LexErrorKind::UnterminatedTemplate);
4102
4103        // Once the first raw escape overflows, a later malformed cooked
4104        // escape is never scanned and cannot change the error category.
4105        let before_later_malformed = Lexer::new("`\\n\\8`")
4106            .with_string_limit(1)
4107            .next_token()
4108            .unwrap_err();
4109        assert_eq!(before_later_malformed.kind, LexErrorKind::StringTooLong);
4110
4111        // Even after cooked becomes undefined, raw accounting must continue.
4112        // The trailing `a` crosses the raw limit before EOF is diagnosed.
4113        let invalid_then_raw_before_eof = Lexer::new("`\\8a")
4114            .with_string_limit(2)
4115            .next_token()
4116            .unwrap_err();
4117        assert_eq!(
4118            invalid_then_raw_before_eof.kind,
4119            LexErrorKind::StringTooLong
4120        );
4121    }
4122
4123    #[test]
4124    fn hashbang_and_comments_feed_line_terminator_metadata() {
4125        let source = "#!/usr/bin/env qjs\nconst a = 1 // comment\n++a";
4126        let tokens = Lexer::new(source).tokenize().unwrap();
4127        assert!(tokens[0].line_terminator_before);
4128        let increment = tokens
4129            .iter()
4130            .find(|token| matches!(token.kind, TokenKind::Punctuator(Punctuator::Increment)))
4131            .unwrap();
4132        assert!(increment.line_terminator_before);
4133    }
4134
4135    #[test]
4136    fn quickjs_module_detection_keeps_the_raw_upstream_heuristic() {
4137        for source in [
4138            "export const answer = 42",
4139            "#!/usr/bin/env qjs\nexport const answer = 42",
4140            "/* leading */ import answer from './answer.js'",
4141            r"export\u0061 = 1",
4142            r"import\u0061 = 1",
4143        ] {
4144            assert!(quickjs_detect_module(source), "{source:?}");
4145        }
4146        for source in [
4147            "import('./answer.js')",
4148            "import . meta",
4149            "import.1",
4150            "// import at EOF",
4151            "// import\u{2028}export const answer = 42",
4152            "//\0\nexport const answer = 42",
4153            "/*\0*/export const answer = 42",
4154            "importπ",
4155            "const importValue = 42",
4156            "/* unterminated import",
4157        ] {
4158            assert!(!quickjs_detect_module(source), "{source:?}");
4159        }
4160        assert!(quickjs_detect_module("import /* unterminated"));
4161        assert!(quickjs_detect_module("import /*\0*/ ("));
4162    }
4163
4164    #[test]
4165    fn quickjs_module_detection_preserves_raw_bytes() {
4166        for source in [
4167            b"/*\x80*/export const answer = 42".as_slice(),
4168            b"//\xff\nimport answer from './answer.js'",
4169            b"#!\x80\xff\0ignored\nexport const answer = 42",
4170            b"\xc2\xa0export const answer = 42",
4171            b"import\x80",
4172            b"/*\xed\xa0\x80*/export const answer = 42",
4173        ] {
4174            assert!(quickjs_detect_module_bytes(source), "{source:?}");
4175        }
4176        for source in [
4177            b"/*\x80\0*/export const answer = 42".as_slice(),
4178            b"//\x80\0\nexport const answer = 42",
4179            b"\x80/* ignored */export const answer = 42",
4180            b"import\xcf\x80",
4181            b"import/*\x80*/('./answer.js')",
4182            b"import/*\xff*/.meta",
4183        ] {
4184            assert!(!quickjs_detect_module_bytes(source), "{source:?}");
4185        }
4186    }
4187
4188    #[test]
4189    fn annex_b_html_comments_are_explicitly_option_gated() {
4190        let disabled = Lexer::new("<!-- ignored").tokenize().unwrap();
4191        assert!(matches!(
4192            disabled[..4],
4193            [
4194                Token {
4195                    kind: TokenKind::Punctuator(Punctuator::Less),
4196                    ..
4197                },
4198                Token {
4199                    kind: TokenKind::Punctuator(Punctuator::Not),
4200                    ..
4201                },
4202                Token {
4203                    kind: TokenKind::Punctuator(Punctuator::Decrement),
4204                    ..
4205                },
4206                Token {
4207                    kind: TokenKind::Identifier(_),
4208                    ..
4209                }
4210            ]
4211        ));
4212
4213        let options = LexerOptions {
4214            allow_html_comments: true,
4215            ..LexerOptions::default()
4216        };
4217        let enabled = Lexer::with_options("value<!-- ignored\nnext", options)
4218            .tokenize()
4219            .unwrap();
4220        let identifiers = enabled
4221            .iter()
4222            .filter_map(|token| match &token.kind {
4223                TokenKind::Identifier(identifier) => Some(identifier.raw),
4224                _ => None,
4225            })
4226            .collect::<Vec<_>>();
4227        assert_eq!(identifiers, ["value", "next"]);
4228        assert!(enabled[1].line_terminator_before);
4229    }
4230
4231    #[test]
4232    fn annex_b_html_close_comment_requires_a_line_boundary() {
4233        let options = LexerOptions {
4234            allow_html_comments: true,
4235            ..LexerOptions::default()
4236        };
4237
4238        for separator in ['\n', '\r', '\u{2028}', '\u{2029}'] {
4239            let source = format!("head{separator}--> ignored{separator}tail");
4240            let tokens = Lexer::with_options(&source, options).tokenize().unwrap();
4241            let identifiers = tokens
4242                .iter()
4243                .filter_map(|token| match &token.kind {
4244                    TokenKind::Identifier(identifier) => Some(identifier.raw),
4245                    _ => None,
4246                })
4247                .collect::<Vec<_>>();
4248            assert_eq!(identifiers, ["head", "tail"], "{separator:?}");
4249            assert!(tokens[1].line_terminator_before, "{separator:?}");
4250        }
4251
4252        for source in [
4253            "--> ignored\nvalue",
4254            "   --> ignored\nvalue",
4255            "/* leading */ --> ignored\nvalue",
4256            "0/*\n*/--> ignored\nvalue",
4257        ] {
4258            let tokens = Lexer::with_options(source, options)
4259                .tokenize()
4260                .unwrap_or_else(|error| {
4261                    panic!("HTML close comment failed for {source:?}: {error}")
4262                });
4263            assert!(
4264                !tokens.iter().any(|token| matches!(
4265                    token.kind,
4266                    TokenKind::Punctuator(Punctuator::Decrement | Punctuator::Greater)
4267                )),
4268                "HTML close comment was tokenized for {source:?}"
4269            );
4270            let identifiers = tokens
4271                .iter()
4272                .filter_map(|token| match &token.kind {
4273                    TokenKind::Identifier(identifier) => Some(identifier.raw),
4274                    _ => None,
4275                })
4276                .collect::<Vec<_>>();
4277            assert_eq!(identifiers, ["value"], "{source:?}");
4278        }
4279
4280        let postfix = Lexer::with_options("count-->0", options)
4281            .tokenize()
4282            .unwrap();
4283        assert!(matches!(
4284            postfix[..4],
4285            [
4286                Token {
4287                    kind: TokenKind::Identifier(_),
4288                    ..
4289                },
4290                Token {
4291                    kind: TokenKind::Punctuator(Punctuator::Decrement),
4292                    ..
4293                },
4294                Token {
4295                    kind: TokenKind::Punctuator(Punctuator::Greater),
4296                    ..
4297                },
4298                Token {
4299                    kind: TokenKind::Number(_),
4300                    ..
4301                }
4302            ]
4303        ));
4304
4305        let no_line_boundary = Lexer::with_options("; --> ignored", options)
4306            .tokenize()
4307            .unwrap();
4308        assert!(matches!(
4309            no_line_boundary[1..3],
4310            [
4311                Token {
4312                    kind: TokenKind::Punctuator(Punctuator::Decrement),
4313                    ..
4314                },
4315                Token {
4316                    kind: TokenKind::Punctuator(Punctuator::Greater),
4317                    ..
4318                }
4319            ]
4320        ));
4321    }
4322
4323    #[test]
4324    fn unicode_line_terminators_after_identifiers_feed_asi_metadata() {
4325        let tokens = Lexer::new("value\u{2028}++next\u{2029}--last")
4326            .tokenize()
4327            .unwrap();
4328        let updates = tokens
4329            .iter()
4330            .filter(|token| {
4331                matches!(
4332                    token.kind,
4333                    TokenKind::Punctuator(Punctuator::Increment | Punctuator::Decrement)
4334                )
4335            })
4336            .collect::<Vec<_>>();
4337        assert_eq!(updates.len(), 2);
4338        assert!(updates.iter().all(|token| token.line_terminator_before));
4339    }
4340
4341    #[test]
4342    fn quickjs_simple_lookahead_preserves_its_block_comment_unicode_quirk() {
4343        for source in [
4344            "\nmethod",
4345            "\rmethod",
4346            "\u{2028}method",
4347            "\u{2029}method",
4348            "/*\n*/method",
4349            "/*\r*/method",
4350            "// comment at eof",
4351        ] {
4352            assert!(
4353                quickjs_simple_lookahead_has_line_terminator(source),
4354                "{source:?}"
4355            );
4356        }
4357        for source in [
4358            " method",
4359            "\u{00a0}method",
4360            "/* no break */method",
4361            "/*\u{2028}*/method",
4362            "/*\u{2029}*/method",
4363            "<!-- comment\nmethod",
4364        ] {
4365            assert!(
4366                !quickjs_simple_lookahead_has_line_terminator(source),
4367                "{source:?}"
4368            );
4369        }
4370    }
4371
4372    #[test]
4373    fn quickjs_simple_of_lookahead_keeps_raw_scanner_boundaries() {
4374        for source in [
4375            "of",
4376            " of",
4377            "\nof",
4378            "// comment\nof",
4379            "/* comment */of",
4380            r"of\u0061",
4381            "of<",
4382        ] {
4383            assert!(quickjs_simple_lookahead_is_of(source), "{source:?}");
4384        }
4385        for source in [
4386            "other",
4387            "ofx",
4388            "of\u{200c}",
4389            "of\u{200d}",
4390            "<!-- comment\nof",
4391            "--> comment\nof",
4392            "// comment\u{2028}of",
4393        ] {
4394            assert!(!quickjs_simple_lookahead_is_of(source), "{source:?}");
4395        }
4396    }
4397
4398    #[test]
4399    fn unterminated_constructs_have_readable_positions() {
4400        let comment = Lexer::new(" \n/* missing").next_token().unwrap_err();
4401        assert_eq!(comment.kind, LexErrorKind::UnterminatedComment);
4402        assert_eq!(comment.span.start.line, 2);
4403        assert_eq!(comment.span.start.column, 1);
4404        assert!(comment.to_string().starts_with("2:1"));
4405
4406        let template_source = format!("{TEMPLATE_QUOTE}missing");
4407        assert_eq!(
4408            Lexer::new(&template_source).next_token().unwrap_err().kind,
4409            LexErrorKind::UnterminatedTemplate
4410        );
4411    }
4412
4413    #[test]
4414    fn iterator_yields_eof_once_and_then_fuses() {
4415        let mut lexer = Lexer::new("");
4416        let eof = lexer.next().unwrap().unwrap();
4417        assert!(matches!(eof.kind, TokenKind::Eof));
4418        assert!(lexer.next().is_none());
4419        assert!(lexer.next().is_none());
4420    }
4421}