Skip to main content

nodejs/
lexer.rs

1//! JavaScript tokenizer.
2//!
3//! Produces a flat token stream ending in `Eof`. Unlike Python, JS is not
4//! indentation-sensitive: blocks are brace-delimited and statements are
5//! semicolon-terminated, with Automatic Semicolon Insertion (ASI) filling in
6//! for newline-terminated statements. Each token records whether a line break
7//! preceded it (`newline_before`) so the parser can apply ASI. `//` and `/* */`
8//! comments are stripped here. Template literals are emitted as a single
9//! `Template` token carrying the cooked quasis plus the raw source of each
10//! `${...}` field; the parser recursively parses those fields.
11
12/// A lexical token.
13#[derive(Debug, Clone, PartialEq)]
14pub enum Tok {
15    Num(f64),
16    /// A `BigInt` literal (`10n`, `0xffn`, …) carried as its canonical decimal
17    /// digit string; the compiler lowers it to a heap `JsObj::BigInt`.
18    BigInt(String),
19    /// A regular-expression literal (`/pat/flags`): `(pattern, flags)`. The lexer
20    /// only recognizes it in expression-start position (see `regex_allowed`).
21    Regex(String, String),
22    Str(String),
23    /// A template literal: `quasis.len() == exprs.len() + 1`. `quasis` are the
24    /// cooked (escape-decoded) strings, `raws` the corresponding raw source
25    /// (undecoded, for tagged templates / `String.raw`), and each `exprs` entry is
26    /// the raw source text between `${` and its matching `}`.
27    Template {
28        quasis: Vec<String>,
29        raws: Vec<String>,
30        exprs: Vec<String>,
31        /// Byte offset of each `exprs` entry in the text handed to [`lex`].
32        expr_at: Vec<u32>,
33    },
34    Ident(String),
35    /// An operator or delimiter, e.g. `+`, `===`, `=>`, `(`, `{`, `.`, `?.`.
36    Punct(String),
37    Eof,
38}
39
40/// A token plus its 1-based source line and whether a newline preceded it.
41#[derive(Debug, Clone, PartialEq)]
42pub struct Token {
43    pub tok: Tok,
44    pub line: u32,
45    pub newline_before: bool,
46    /// UTF-8 byte offsets of the token's first character and one past its
47    /// last, in the text handed to [`lex`].
48    pub start: u32,
49    pub end: u32,
50}
51
52struct Lexer {
53    src: Vec<char>,
54    pos: usize,
55    /// Byte offset of each char index (`src.len() + 1` entries).
56    byte_at: Vec<u32>,
57    /// Char index where the token being scanned began.
58    tok_start: usize,
59    line: u32,
60    out: Vec<Token>,
61    pending_newline: bool,
62}
63
64/// Multi-char operators, longest first so the scanner is greedy.
65const OPS4: &[&str] = &[">>>="];
66const OPS3: &[&str] = &[
67    "===", "!==", "**=", "...", ">>>", "<<=", ">>=", "&&=", "||=", "??=",
68];
69const OPS2: &[&str] = &[
70    "==", "!=", "<=", ">=", "&&", "||", "??", "?.", "=>", "++", "--", "+=", "-=", "*=", "/=", "%=",
71    "&=", "|=", "^=", "<<", ">>", "**",
72];
73
74/// Tokenize `src` into a token stream ending in `Eof`.
75pub fn lex(src: &str) -> Result<Vec<Token>, String> {
76    let mut byte_at: Vec<u32> = src.char_indices().map(|(b, _)| b as u32).collect();
77    byte_at.push(src.len() as u32);
78    let mut lx = Lexer {
79        src: src.chars().collect(),
80        pos: 0,
81        byte_at,
82        tok_start: 0,
83        line: 1,
84        out: Vec::new(),
85        pending_newline: false,
86    };
87    lx.run()?;
88    Ok(lx.out)
89}
90
91impl Lexer {
92    fn peek(&self) -> Option<char> {
93        self.src.get(self.pos).copied()
94    }
95    fn peek_at(&self, n: usize) -> Option<char> {
96        self.src.get(self.pos + n).copied()
97    }
98    fn bump(&mut self) -> Option<char> {
99        let c = self.src.get(self.pos).copied();
100        if let Some(ch) = c {
101            self.pos += 1;
102            if ch == '\n' {
103                self.line += 1;
104            }
105        }
106        c
107    }
108    fn push(&mut self, tok: Tok) {
109        self.out.push(Token {
110            tok,
111            line: self.line,
112            newline_before: self.pending_newline,
113            start: self.byte_at[self.tok_start.min(self.pos)],
114            end: self.byte_at[self.pos],
115        });
116        self.pending_newline = false;
117    }
118
119    fn run(&mut self) -> Result<(), String> {
120        // A Hashbang comment (12.5) is legal only as the very first thing in
121        // the source: `#!/usr/bin/env node` runs to the end of its line.
122        if self.peek() == Some('#') && self.peek_at(1) == Some('!') {
123            while !matches!(self.peek(), None | Some('\n')) {
124                self.bump();
125            }
126        }
127        loop {
128            match self.peek() {
129                None => break,
130                Some('\n') => {
131                    self.bump();
132                    self.pending_newline = true;
133                }
134                // LS and PS are line terminators (12.3) just as LF is.
135                Some('\u{2028}' | '\u{2029}') => {
136                    self.bump();
137                    self.pending_newline = true;
138                }
139                // WhiteSpace (12.2): TAB, VT, FF, the BOM and every Zs space.
140                Some(
141                    ' '
142                    | '\t'
143                    | '\r'
144                    | '\u{0B}'
145                    | '\u{0C}'
146                    | '\u{FEFF}'
147                    | '\u{A0}'
148                    | '\u{1680}'
149                    | '\u{2000}'..='\u{200A}'
150                    | '\u{202F}'
151                    | '\u{205F}'
152                    | '\u{3000}',
153                ) => {
154                    self.bump();
155                }
156                Some('/') if self.peek_at(1) == Some('/') => {
157                    while let Some(c) = self.peek() {
158                        if c == '\n' {
159                            break;
160                        }
161                        self.bump();
162                    }
163                }
164                Some('/') if self.peek_at(1) == Some('*') => {
165                    self.bump();
166                    self.bump();
167                    while let Some(c) = self.peek() {
168                        if c == '*' && self.peek_at(1) == Some('/') {
169                            self.bump();
170                            self.bump();
171                            break;
172                        }
173                        if c == '\n' {
174                            self.pending_newline = true;
175                        }
176                        self.bump();
177                    }
178                }
179                // A `/` in expression-start position is a regex literal, not the
180                // division operator (comments were already ruled out above).
181                Some('/') if self.regex_allowed() => {
182                    self.tok_start = self.pos;
183                    self.scan_regex()?
184                }
185                Some(_) => {
186                    self.tok_start = self.pos;
187                    self.scan_token()?
188                }
189            }
190        }
191        self.tok_start = self.pos;
192        self.push(Tok::Eof);
193        Ok(())
194    }
195
196    /// Whether a `/` here begins a regex literal (expression-start position)
197    /// rather than the division operator. Decided by the previous significant
198    /// token: after a value (identifier/number/string/`)`/`]`) `/` is division;
199    /// after an operator, `(`, `,`, `{`, `[`, `;`, `:`, `return`, etc. it opens a
200    /// regex. This is the standard "regex-or-divide" ASI-adjacent heuristic.
201    fn regex_allowed(&self) -> bool {
202        match self.out.last().map(|t| &t.tok) {
203            None => true, // program start
204            Some(Tok::Num(_))
205            | Some(Tok::BigInt(_))
206            | Some(Tok::Str(_))
207            | Some(Tok::Template { .. })
208            | Some(Tok::Regex(..)) => false,
209            Some(Tok::Ident(s)) => matches!(
210                s.as_str(),
211                // Keywords that precede an expression → regex; a plain variable
212                // name (or a value keyword like `this`/`true`) → division.
213                "return"
214                    | "typeof"
215                    | "instanceof"
216                    | "in"
217                    | "of"
218                    | "new"
219                    | "delete"
220                    | "void"
221                    | "do"
222                    | "else"
223                    | "case"
224                    | "throw"
225                    | "yield"
226                    | "await"
227            ),
228            Some(Tok::Punct(p)) => !matches!(p.as_str(), ")" | "]" | "}" | "++" | "--"),
229            Some(Tok::Eof) => true,
230        }
231    }
232
233    /// Scan a `/pat/flags` regex literal. The opening `/` is current. The body
234    /// runs to the next unescaped `/` that is not inside a `[...]` character
235    /// class; trailing ASCII-letter flags follow.
236    fn scan_regex(&mut self) -> Result<(), String> {
237        self.bump(); // opening slash
238        let mut pat = String::new();
239        let mut in_class = false;
240        loop {
241            match self.peek() {
242                None | Some('\n') => {
243                    return Err(format!(
244                        "SyntaxError: unterminated regular expression (line {})",
245                        self.line
246                    ))
247                }
248                Some('\\') => {
249                    // Keep the escape verbatim (the translator interprets it).
250                    pat.push('\\');
251                    self.bump();
252                    if let Some(c) = self.bump() {
253                        pat.push(c);
254                    }
255                }
256                Some('[') => {
257                    in_class = true;
258                    pat.push('[');
259                    self.bump();
260                }
261                Some(']') => {
262                    in_class = false;
263                    pat.push(']');
264                    self.bump();
265                }
266                Some('/') if !in_class => {
267                    self.bump();
268                    break;
269                }
270                Some(c) => {
271                    pat.push(c);
272                    self.bump();
273                }
274            }
275        }
276        let mut flags = String::new();
277        while let Some(c) = self.peek() {
278            if c.is_ascii_alphabetic() {
279                flags.push(c);
280                self.bump();
281            } else {
282                break;
283            }
284        }
285        self.push(Tok::Regex(pat, flags));
286        Ok(())
287    }
288
289    fn scan_token(&mut self) -> Result<(), String> {
290        let c = self.peek().unwrap();
291        if c == '"' || c == '\'' {
292            return self.scan_string(c);
293        }
294        if c == '`' {
295            return self.scan_template();
296        }
297        // IdentifierStart (12.7) is any Unicode letter, not only ASCII:
298        // `const é = 1` and `let Δx` are ordinary names. `scan_name` already
299        // continues on any alphanumeric.
300        if c.is_alphabetic() || c == '_' || c == '$' {
301            return self.scan_name();
302        }
303        // Private class member (`#name`): scanned as an identifier keeping the `#`.
304        if c == '#'
305            && self
306                .peek_at(1)
307                .map(|d| d.is_alphabetic() || d == '_' || d == '$')
308                .unwrap_or(false)
309        {
310            return self.scan_name();
311        }
312        if c.is_ascii_digit()
313            || (c == '.' && self.peek_at(1).map(|d| d.is_ascii_digit()).unwrap_or(false))
314        {
315            return self.scan_number();
316        }
317        self.scan_op()
318    }
319
320    fn scan_name(&mut self) -> Result<(), String> {
321        let mut s = String::new();
322        // A leading `#` (private class member name) is kept as part of the ident.
323        if self.peek() == Some('#') {
324            s.push('#');
325            self.pos += 1;
326        }
327        while let Some(c) = self.peek() {
328            if c.is_alphanumeric() || c == '_' || c == '$' {
329                s.push(c);
330                self.pos += 1;
331            } else {
332                break;
333            }
334        }
335        self.push(Tok::Ident(s));
336        Ok(())
337    }
338
339    fn scan_string(&mut self, quote: char) -> Result<(), String> {
340        self.bump(); // opening quote
341        let mut raw = String::new();
342        loop {
343            match self.peek() {
344                None => {
345                    return Err(format!(
346                        "SyntaxError: unterminated string (line {})",
347                        self.line
348                    ))
349                }
350                Some(c) if c == quote => {
351                    self.bump();
352                    break;
353                }
354                Some('\\') => {
355                    self.bump();
356                    if let Some(e) = self.bump() {
357                        push_escape(&mut raw, e, self);
358                    }
359                }
360                Some('\n') => {
361                    return Err(format!(
362                        "SyntaxError: unterminated string literal (line {})",
363                        self.line
364                    ))
365                }
366                Some(c) => {
367                    raw.push(c);
368                    self.bump();
369                }
370            }
371        }
372        self.push(Tok::Str(raw));
373        Ok(())
374    }
375
376    /// Scan a `` `...${expr}...` `` template. Cooked quasis are decoded; each
377    /// `${...}` field's raw source (with balanced braces) is captured for the
378    /// parser to re-parse.
379    fn scan_template(&mut self) -> Result<(), String> {
380        self.bump(); // opening backtick
381        let mut quasis = Vec::new();
382        let mut raws = Vec::new();
383        let mut exprs = Vec::new();
384        let mut expr_at = Vec::new();
385        let mut cur = String::new();
386        let mut cur_raw = String::new();
387        loop {
388            match self.peek() {
389                None => {
390                    return Err(format!(
391                        "SyntaxError: unterminated template (line {})",
392                        self.line
393                    ))
394                }
395                Some('`') => {
396                    self.bump();
397                    break;
398                }
399                Some('\\') => {
400                    // Cooked decodes the escape; raw keeps the exact source span it
401                    // spans (including any hex/unicode digits push_escape consumes).
402                    let start = self.pos;
403                    self.bump();
404                    if let Some(e) = self.bump() {
405                        push_escape(&mut cur, e, self);
406                    }
407                    for c in &self.src[start..self.pos] {
408                        cur_raw.push(*c);
409                    }
410                }
411                Some('$') if self.peek_at(1) == Some('{') => {
412                    self.bump();
413                    self.bump();
414                    quasis.push(std::mem::take(&mut cur));
415                    raws.push(std::mem::take(&mut cur_raw));
416                    // Capture raw source until the matching `}` (brace-balanced,
417                    // skipping strings).
418                    expr_at.push(self.byte_at[self.pos]);
419                    let mut depth = 1;
420                    let mut src = String::new();
421                    loop {
422                        match self.peek() {
423                            None => {
424                                return Err(format!(
425                                    "SyntaxError: unterminated template expression (line {})",
426                                    self.line
427                                ))
428                            }
429                            Some('{') => {
430                                depth += 1;
431                                src.push('{');
432                                self.bump();
433                            }
434                            Some('}') => {
435                                depth -= 1;
436                                self.bump();
437                                if depth == 0 {
438                                    break;
439                                }
440                                src.push('}');
441                            }
442                            Some(q) if q == '"' || q == '\'' || q == '`' => {
443                                src.push(q);
444                                self.bump();
445                                while let Some(cc) = self.peek() {
446                                    src.push(cc);
447                                    self.bump();
448                                    if cc == '\\' {
449                                        if let Some(n) = self.peek() {
450                                            src.push(n);
451                                            self.bump();
452                                        }
453                                    } else if cc == q {
454                                        break;
455                                    }
456                                }
457                            }
458                            Some(cc) => {
459                                src.push(cc);
460                                self.bump();
461                            }
462                        }
463                    }
464                    exprs.push(src);
465                }
466                Some(c) => {
467                    cur.push(c);
468                    cur_raw.push(c);
469                    self.bump();
470                }
471            }
472        }
473        quasis.push(cur);
474        raws.push(cur_raw);
475        self.push(Tok::Template {
476            quasis,
477            raws,
478            exprs,
479            expr_at,
480        });
481        Ok(())
482    }
483
484    fn scan_number(&mut self) -> Result<(), String> {
485        // Radix prefixes: 0x / 0o / 0b.
486        if self.peek() == Some('0') {
487            if let Some(r) = self.peek_at(1) {
488                if matches!(r, 'x' | 'X' | 'o' | 'O' | 'b' | 'B') {
489                    self.bump();
490                    self.bump();
491                    let radix = match r.to_ascii_lowercase() {
492                        'x' => 16,
493                        'o' => 8,
494                        _ => 2,
495                    };
496                    let mut digits = String::new();
497                    while let Some(c) = self.peek() {
498                        if c == '_' {
499                            self.pos += 1;
500                        } else if c.is_digit(radix) {
501                            digits.push(c);
502                            self.pos += 1;
503                        } else {
504                            break;
505                        }
506                    }
507                    // `0x..n` / `0o..n` / `0b..n` BigInt literal: the digits carry
508                    // arbitrary precision, so parse them as a bignum (radix-aware)
509                    // rather than through `i64`.
510                    if self.peek() == Some('n') {
511                        self.pos += 1;
512                        let big = num_bigint::BigInt::parse_bytes(digits.as_bytes(), radix)
513                            .ok_or_else(|| {
514                                format!("SyntaxError: bad bigint (line {})", self.line)
515                            })?;
516                        self.push(Tok::BigInt(big.to_string()));
517                        return Ok(());
518                    }
519                    let n = i64::from_str_radix(&digits, radix)
520                        .map_err(|_| format!("SyntaxError: bad number (line {})", self.line))?;
521                    self.push(Tok::Num(n as f64));
522                    return Ok(());
523                }
524            }
525        }
526        // A LEGACY OCTAL literal: `0` followed by digit-run that is all 0-7 is
527        // base 8 (`012` is 10, not 12). A run containing an 8 or a 9 is the
528        // legacy DECIMAL form and stays base 10 (`08` is 8). Both are
529        // SyntaxErrors in strict code, which this lexer cannot see — recorded in
530        // BUGS.md. The value was simply read as decimal, so `012` was 12.
531        if self.peek() == Some('0') {
532            let mut n = 1;
533            while self.peek_at(n).is_some_and(|c| c.is_ascii_digit()) {
534                n += 1;
535            }
536            let run: String = (0..n).filter_map(|i| self.peek_at(i)).collect();
537            let terminated = !self
538                .peek_at(n)
539                .is_some_and(|c| matches!(c, '.' | 'e' | 'E' | 'n'));
540            if n > 1 && terminated && run.chars().all(|c| ('0'..='7').contains(&c)) {
541                self.pos += n;
542                let v = u64::from_str_radix(&run, 8).unwrap_or(0);
543                self.push(Tok::Num(v as f64));
544                return Ok(());
545            }
546        }
547        let mut s = String::new();
548        while let Some(c) = self.peek() {
549            match c {
550                '0'..='9' => {
551                    s.push(c);
552                    self.pos += 1;
553                }
554                '_' => {
555                    self.pos += 1;
556                }
557                '.' => {
558                    s.push(c);
559                    self.pos += 1;
560                }
561                'e' | 'E' => {
562                    s.push('e');
563                    self.pos += 1;
564                    if matches!(self.peek(), Some('+') | Some('-')) {
565                        s.push(self.peek().unwrap());
566                        self.pos += 1;
567                    }
568                }
569                _ => break,
570            }
571        }
572        // Decimal `BigInt` literal (`123n`): only integer digit runs may carry the
573        // `n` suffix (a `.`/`e` makes it an ordinary number, and `1.5n` is a
574        // SyntaxError in JS — we leave the `n` as a stray identifier so it fails).
575        if self.peek() == Some('n') && !s.is_empty() && s.chars().all(|c| c.is_ascii_digit()) {
576            self.pos += 1;
577            self.push(Tok::BigInt(s));
578            return Ok(());
579        }
580        let v: f64 = s
581            .parse()
582            .map_err(|_| format!("SyntaxError: bad number '{s}' (line {})", self.line))?;
583        self.push(Tok::Num(v));
584        Ok(())
585    }
586
587    fn scan_op(&mut self) -> Result<(), String> {
588        let slice: String = self.src[self.pos..(self.pos + 4).min(self.src.len())]
589            .iter()
590            .collect();
591        for op in OPS4 {
592            if slice.starts_with(op) {
593                self.pos += 4;
594                self.push(Tok::Punct((*op).to_string()));
595                return Ok(());
596            }
597        }
598        for op in OPS3 {
599            if slice.starts_with(op) {
600                self.pos += 3;
601                self.push(Tok::Punct((*op).to_string()));
602                return Ok(());
603            }
604        }
605        for op in OPS2 {
606            if slice.starts_with(op) {
607                self.pos += 2;
608                self.push(Tok::Punct((*op).to_string()));
609                return Ok(());
610            }
611        }
612        let c = self.bump().unwrap();
613        if "+-*/%<>=!&|^~?:;,.(){}[]".contains(c) {
614            self.push(Tok::Punct(c.to_string()));
615            Ok(())
616        } else {
617            Err(format!(
618                "SyntaxError: unexpected character {c:?} (line {})",
619                self.line
620            ))
621        }
622    }
623}
624
625/// Append one escape sequence's decoded character(s) to `out`. `\xNN` and
626/// The value of a `\uDC00..\uDFFF` escape sitting at the lexer's cursor, without
627/// consuming it. Used to rejoin a surrogate PAIR written as two escapes.
628fn peek_low_surrogate(lx: &Lexer) -> Option<u32> {
629    if lx.peek() != Some('\\') || lx.peek_at(1) != Some('u') {
630        return None;
631    }
632    let mut n = 0u32;
633    for i in 0..4 {
634        let c = lx.peek_at(2 + i)?;
635        n = n * 16 + c.to_digit(16)?;
636    }
637    (0xDC00..=0xDFFF).contains(&n).then_some(n)
638}
639
640/// Append the code point `n` to a string literal's value.
641///
642/// `char::from_u32` rejects `U+D800..=U+DFFF`, and the old code simply dropped
643/// what it rejected — so `"\ud800".length` was 0 where every engine says 1, and
644/// `"a\ud800b".length` was 2 instead of 3. This runtime's documented policy for
645/// an unpaired surrogate is to substitute `U+FFFD` (see `utf16`), which is ONE
646/// code unit and therefore keeps the length arithmetic exact; dropping the unit
647/// broke that invariant rather than implementing it.
648fn push_code_point(out: &mut String, n: u32) {
649    match char::from_u32(n) {
650        Some(ch) => out.push(ch),
651        None => out.push('\u{FFFD}'),
652    }
653}
654
655/// `\uNNNN` / `\u{...}` are decoded; unknown escapes keep the literal char.
656fn push_escape(out: &mut String, e: char, lx: &mut Lexer) {
657    match e {
658        'n' => out.push('\n'),
659        't' => out.push('\t'),
660        'r' => out.push('\r'),
661        'b' => out.push('\u{08}'),
662        'f' => out.push('\u{0C}'),
663        'v' => out.push('\u{0B}'),
664        '0' => out.push('\0'),
665        '\\' => out.push('\\'),
666        '\'' => out.push('\''),
667        '"' => out.push('"'),
668        '`' => out.push('`'),
669        '\n' => {} // line continuation
670        'x' => {
671            let mut h = String::new();
672            for _ in 0..2 {
673                if let Some(c) = lx.peek() {
674                    if c.is_ascii_hexdigit() {
675                        h.push(c);
676                        lx.bump();
677                    }
678                }
679            }
680            if let Ok(n) = u32::from_str_radix(&h, 16) {
681                if let Some(ch) = char::from_u32(n) {
682                    out.push(ch);
683                }
684            }
685        }
686        'u' => {
687            if lx.peek() == Some('{') {
688                lx.bump();
689                let mut h = String::new();
690                while let Some(c) = lx.peek() {
691                    if c == '}' {
692                        lx.bump();
693                        break;
694                    }
695                    h.push(c);
696                    lx.bump();
697                }
698                if let Ok(n) = u32::from_str_radix(&h, 16) {
699                    push_code_point(out, n);
700                }
701            } else {
702                let mut h = String::new();
703                for _ in 0..4 {
704                    if let Some(c) = lx.peek() {
705                        if c.is_ascii_hexdigit() {
706                            h.push(c);
707                            lx.bump();
708                        }
709                    }
710                }
711                if let Ok(n) = u32::from_str_radix(&h, 16) {
712                    // A HIGH surrogate followed by a `\uXXXX` LOW surrogate is one
713                    // astral character, and `"\ud83d\ude00"` is the ordinary
714                    // ASCII-safe way to write one. Decoding each half on its own
715                    // turned every such literal into two `U+FFFD`s.
716                    if (0xD800..=0xDBFF).contains(&n) {
717                        if let Some(lo) = peek_low_surrogate(lx) {
718                            for _ in 0..6 {
719                                lx.bump();
720                            }
721                            let cp = 0x10000 + ((n - 0xD800) << 10) + (lo - 0xDC00);
722                            push_code_point(out, cp);
723                            return;
724                        }
725                    }
726                    push_code_point(out, n);
727                }
728            }
729        }
730        other => out.push(other),
731    }
732}