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}