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