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