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 // A LEGACY OCTAL literal: `0` followed by digit-run that is all 0-7 is
472 // base 8 (`012` is 10, not 12). A run containing an 8 or a 9 is the
473 // legacy DECIMAL form and stays base 10 (`08` is 8). Both are
474 // SyntaxErrors in strict code, which this lexer cannot see — recorded in
475 // BUGS.md. The value was simply read as decimal, so `012` was 12.
476 if self.peek() == Some('0') {
477 let mut n = 1;
478 while self.peek_at(n).is_some_and(|c| c.is_ascii_digit()) {
479 n += 1;
480 }
481 let run: String = (0..n).filter_map(|i| self.peek_at(i)).collect();
482 let terminated = !self
483 .peek_at(n)
484 .is_some_and(|c| matches!(c, '.' | 'e' | 'E' | 'n'));
485 if n > 1 && terminated && run.chars().all(|c| ('0'..='7').contains(&c)) {
486 self.pos += n;
487 let v = u64::from_str_radix(&run, 8).unwrap_or(0);
488 self.push(Tok::Num(v as f64));
489 return Ok(());
490 }
491 }
492 let mut s = String::new();
493 while let Some(c) = self.peek() {
494 match c {
495 '0'..='9' => {
496 s.push(c);
497 self.pos += 1;
498 }
499 '_' => {
500 self.pos += 1;
501 }
502 '.' => {
503 s.push(c);
504 self.pos += 1;
505 }
506 'e' | 'E' => {
507 s.push('e');
508 self.pos += 1;
509 if matches!(self.peek(), Some('+') | Some('-')) {
510 s.push(self.peek().unwrap());
511 self.pos += 1;
512 }
513 }
514 _ => break,
515 }
516 }
517 // Decimal `BigInt` literal (`123n`): only integer digit runs may carry the
518 // `n` suffix (a `.`/`e` makes it an ordinary number, and `1.5n` is a
519 // SyntaxError in JS — we leave the `n` as a stray identifier so it fails).
520 if self.peek() == Some('n') && !s.is_empty() && s.chars().all(|c| c.is_ascii_digit()) {
521 self.pos += 1;
522 self.push(Tok::BigInt(s));
523 return Ok(());
524 }
525 let v: f64 = s
526 .parse()
527 .map_err(|_| format!("SyntaxError: bad number '{s}' (line {})", self.line))?;
528 self.push(Tok::Num(v));
529 Ok(())
530 }
531
532 fn scan_op(&mut self) -> Result<(), String> {
533 let slice: String = self.src[self.pos..(self.pos + 4).min(self.src.len())]
534 .iter()
535 .collect();
536 for op in OPS4 {
537 if slice.starts_with(op) {
538 self.pos += 4;
539 self.push(Tok::Punct((*op).to_string()));
540 return Ok(());
541 }
542 }
543 for op in OPS3 {
544 if slice.starts_with(op) {
545 self.pos += 3;
546 self.push(Tok::Punct((*op).to_string()));
547 return Ok(());
548 }
549 }
550 for op in OPS2 {
551 if slice.starts_with(op) {
552 self.pos += 2;
553 self.push(Tok::Punct((*op).to_string()));
554 return Ok(());
555 }
556 }
557 let c = self.bump().unwrap();
558 if "+-*/%<>=!&|^~?:;,.(){}[]".contains(c) {
559 self.push(Tok::Punct(c.to_string()));
560 Ok(())
561 } else {
562 Err(format!(
563 "SyntaxError: unexpected character {c:?} (line {})",
564 self.line
565 ))
566 }
567 }
568}
569
570/// Append one escape sequence's decoded character(s) to `out`. `\xNN` and
571/// The value of a `\uDC00..\uDFFF` escape sitting at the lexer's cursor, without
572/// consuming it. Used to rejoin a surrogate PAIR written as two escapes.
573fn peek_low_surrogate(lx: &Lexer) -> Option<u32> {
574 if lx.peek() != Some('\\') || lx.peek_at(1) != Some('u') {
575 return None;
576 }
577 let mut n = 0u32;
578 for i in 0..4 {
579 let c = lx.peek_at(2 + i)?;
580 n = n * 16 + c.to_digit(16)?;
581 }
582 (0xDC00..=0xDFFF).contains(&n).then_some(n)
583}
584
585/// Append the code point `n` to a string literal's value.
586///
587/// `char::from_u32` rejects `U+D800..=U+DFFF`, and the old code simply dropped
588/// what it rejected — so `"\ud800".length` was 0 where every engine says 1, and
589/// `"a\ud800b".length` was 2 instead of 3. This runtime's documented policy for
590/// an unpaired surrogate is to substitute `U+FFFD` (see `utf16`), which is ONE
591/// code unit and therefore keeps the length arithmetic exact; dropping the unit
592/// broke that invariant rather than implementing it.
593fn push_code_point(out: &mut String, n: u32) {
594 match char::from_u32(n) {
595 Some(ch) => out.push(ch),
596 None => out.push('\u{FFFD}'),
597 }
598}
599
600/// `\uNNNN` / `\u{...}` are decoded; unknown escapes keep the literal char.
601fn push_escape(out: &mut String, e: char, lx: &mut Lexer) {
602 match e {
603 'n' => out.push('\n'),
604 't' => out.push('\t'),
605 'r' => out.push('\r'),
606 'b' => out.push('\u{08}'),
607 'f' => out.push('\u{0C}'),
608 'v' => out.push('\u{0B}'),
609 '0' => out.push('\0'),
610 '\\' => out.push('\\'),
611 '\'' => out.push('\''),
612 '"' => out.push('"'),
613 '`' => out.push('`'),
614 '\n' => {} // line continuation
615 'x' => {
616 let mut h = String::new();
617 for _ in 0..2 {
618 if let Some(c) = lx.peek() {
619 if c.is_ascii_hexdigit() {
620 h.push(c);
621 lx.bump();
622 }
623 }
624 }
625 if let Ok(n) = u32::from_str_radix(&h, 16) {
626 if let Some(ch) = char::from_u32(n) {
627 out.push(ch);
628 }
629 }
630 }
631 'u' => {
632 if lx.peek() == Some('{') {
633 lx.bump();
634 let mut h = String::new();
635 while let Some(c) = lx.peek() {
636 if c == '}' {
637 lx.bump();
638 break;
639 }
640 h.push(c);
641 lx.bump();
642 }
643 if let Ok(n) = u32::from_str_radix(&h, 16) {
644 push_code_point(out, n);
645 }
646 } else {
647 let mut h = String::new();
648 for _ in 0..4 {
649 if let Some(c) = lx.peek() {
650 if c.is_ascii_hexdigit() {
651 h.push(c);
652 lx.bump();
653 }
654 }
655 }
656 if let Ok(n) = u32::from_str_radix(&h, 16) {
657 // A HIGH surrogate followed by a `\uXXXX` LOW surrogate is one
658 // astral character, and `"\ud83d\ude00"` is the ordinary
659 // ASCII-safe way to write one. Decoding each half on its own
660 // turned every such literal into two `U+FFFD`s.
661 if (0xD800..=0xDBFF).contains(&n) {
662 if let Some(lo) = peek_low_surrogate(lx) {
663 for _ in 0..6 {
664 lx.bump();
665 }
666 let cp = 0x10000 + ((n - 0xD800) << 10) + (lo - 0xDC00);
667 push_code_point(out, cp);
668 return;
669 }
670 }
671 push_code_point(out, n);
672 }
673 }
674 }
675 other => out.push(other),
676 }
677}