Skip to main content

atmos/os_lib/js/
lexer.rs

1//! JavaScript の字句解析器。
2//!
3//! 記号トークンは `Sym(String)` に統一し、最長一致で多文字演算子を切り出す。
4//! コメント (`//`, `/* */`)、エスケープ付き文字列、16進/小数の数値、テンプレート
5//! リテラル(`${expr}` 補間対応。各補間式の生ソースを切り出し、パーサ側で
6//! 独立したサブパーサとして構文解析する)に対応。
7
8use alloc::boxed::Box;
9use alloc::string::String;
10use alloc::vec::Vec;
11
12/// テンプレートリテラルの構成要素。`Quasi` はリテラルテキスト、`Expr` は
13/// `${...}` 内の補間式の生ソース文字列(未パース。パーサ側でサブパーサに渡す)。
14#[derive(Debug, Clone, PartialEq)]
15pub enum TemplatePart {
16    /// (cooked, raw)。`raw` はエスケープ解決前のソース文字そのまま
17    /// (タグ付きテンプレート `` tag`\n` `` の `strings.raw` 用)。
18    Quasi(String, String),
19    Expr(String),
20}
21
22/// 【2026-10-17】メモリ削減: 予約語と記号は固定表の `&'static str`、正規表現リテラルは
23/// 箱詰めにして 1 トークン 56 → 32 バイト。従来は記号(全トークンの約半分)まで 1 個ずつ
24/// `String` を確保しており、YouTube の base.js(2.97MB)のトークン列が約 101MB あった。
25#[derive(Debug, Clone, PartialEq)]
26pub enum Token {
27    Keyword(&'static str),
28    Ident(String),
29    Num(f64),
30    /// BigInt リテラルの数値部分(接尾辞 n を除く。0x/0o/0b 接頭辞は保持)。
31    BigIntLit(String),
32    Str(String),
33    /// テンプレートリテラル: quasi(リテラル部)と補間式の生ソースが交互に並ぶ。
34    /// 常に quasi で始まり quasi で終わる(`Quasi("")` を含む場合もある)。
35    Template(Vec<TemplatePart>),
36    /// 記号(演算子・区切り)。例: "===", "=>", "(", ";"。
37    Sym(&'static str),
38    /// 正規表現リテラル `/pattern/flags`(パターン, フラグ)。
39    Regex(Box<(String, String)>),
40    /// 遅延解析で読み飛ばした大きな関数本体(`{` と `}` の間)。字句解析器は出さず、
41    /// パーサが組み立て時に置く。(開始, 終了)は文字単位の位置、bool は先頭に
42    /// `"use strict"` 指令があったか。
43    LazyBody(u32, u32, bool),
44    Eof,
45}
46
47/// 予約語。
48const KEYWORDS: &[&str] = &[
49    "var",
50    "let",
51    "const",
52    "if",
53    "else",
54    "function",
55    "return",
56    "true",
57    "false",
58    "null",
59    "undefined",
60    "this",
61    "new",
62    "typeof",
63    "void",
64    "delete",
65    "instanceof",
66    "in",
67    "of",
68    "while",
69    "do",
70    "for",
71    "break",
72    "continue",
73    "throw",
74    "try",
75    "catch",
76    "finally",
77    "class",
78    "extends",
79    "super",
80    "switch",
81    "case",
82    "default",
83    "async",
84    "await",
85    "yield",
86    "import",
87    "export",
88];
89
90/// 記号テーブル(最長一致のため長い順)。
91const SYMBOLS: &[&str] = &[
92    ">>>=", "===", "!==", "**=", ">>>", "<<=", ">>=", "&&=", "||=", "??=", "...", "==", "!=", "<=",
93    ">=", "&&", "||", "??", "?.", "**", "++", "--", "+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=",
94    "<<", ">>", "=>", "+", "-", "*", "/", "%", "=", "<", ">", "!", "~", "&", "|", "^", "?", ":",
95    ".", ",", ";", "(", ")", "{", "}", "[", "]",
96];
97
98/// 字句解析で検出した異常(回復して継続した箇所)。
99///
100/// 【2026-07-28】従来この層は異常を一切報告しておらず、
101/// `0xZZ` や壊れた数値は `unwrap_or(0)` で**黙って 0 になり**、
102/// 閉じ忘れの文字列・ブロックコメントは**そこから先を丸ごと飲み込んだ**まま
103/// 正常終了したように見えていた。値が化けているのに誰も気づけない状態を
104/// なくすため、回復はしつつ必ず記録する。
105#[derive(Debug, Clone, PartialEq)]
106pub struct LexError {
107    pub message: String,
108    /// ソース上の文字位置。
109    pub pos: usize,
110}
111
112/// 記録する字句エラーの上限(壊れきった入力でメモリを食わないため)。
113const MAX_LEX_ERRORS: usize = 64;
114
115pub struct Lexer {
116    chars: Vec<char>,
117    pos: usize,
118    /// 直前に返したトークンの開始位置(文字単位)。関数本体の遅延解析で範囲を切り出すのに使う。
119    tok_start: usize,
120    /// 直前に発行したトークン(`/` を正規表現か除算か判定するため)。
121    prev: Option<Token>,
122    /// 回復して継続した字句エラー(上限 `MAX_LEX_ERRORS` 件)。
123    errors: Vec<LexError>,
124    /// 上限で切り捨てた分も含む総数。
125    error_count: usize,
126}
127
128impl Lexer {
129    pub fn new(source: &str) -> Self {
130        Self {
131            chars: {
132                // 大きなスクリプト(10MB 級)で倍々の伸長による余分な確保を避ける。
133                let mut v = Vec::with_capacity(source.chars().count());
134                v.extend(source.chars());
135                v
136            },
137            pos: 0,
138            tok_start: 0,
139            prev: None,
140            errors: Vec::new(),
141            error_count: 0,
142        }
143    }
144
145    /// 記録済みの字句エラー。
146    pub fn errors(&self) -> &[LexError] {
147        &self.errors
148    }
149    /// 検出した字句エラーの総数(記録上限で切り捨てた分も含む)。
150    pub fn error_count(&self) -> usize {
151        self.error_count
152    }
153    /// 記録済みの字句エラーを取り出す。
154    pub fn take_errors(&mut self) -> Vec<LexError> {
155        core::mem::take(&mut self.errors)
156    }
157
158    fn record_error(&mut self, message: String) {
159        self.error_count += 1;
160        if self.errors.len() < MAX_LEX_ERRORS {
161            self.errors.push(LexError {
162                message,
163                pos: self.pos,
164            });
165        }
166    }
167
168    fn peek(&self) -> Option<char> {
169        self.chars.get(self.pos).copied()
170    }
171    fn peek_at(&self, off: usize) -> Option<char> {
172        self.chars.get(self.pos + off).copied()
173    }
174    fn advance(&mut self) -> Option<char> {
175        let ch = self.peek();
176        if ch.is_some() {
177            self.pos += 1;
178        }
179        ch
180    }
181
182    /// 空白とコメントをまとめてスキップ。
183    fn skip_trivia(&mut self) {
184        loop {
185            match self.peek() {
186                Some(c) if c.is_whitespace() => {
187                    self.advance();
188                }
189                Some('/') if self.peek_at(1) == Some('/') => {
190                    while let Some(c) = self.peek() {
191                        if c == '\n' {
192                            break;
193                        }
194                        self.advance();
195                    }
196                }
197                Some('/') if self.peek_at(1) == Some('*') => {
198                    self.advance();
199                    self.advance();
200                    let mut closed = false;
201                    while let Some(c) = self.peek() {
202                        if c == '*' && self.peek_at(1) == Some('/') {
203                            self.advance();
204                            self.advance();
205                            closed = true;
206                            break;
207                        }
208                        self.advance();
209                    }
210                    if !closed {
211                        // 閉じ忘れたブロックコメントは以降のソースを丸ごと
212                        // 飲み込む。黙って空のプログラムにしない。
213                        self.record_error(String::from("unterminated block comment"));
214                    }
215                }
216                _ => break,
217            }
218        }
219    }
220
221    fn read_identifier_or_keyword(&mut self) -> Token {
222        let mut s = String::new();
223        // プライベートクラスフィールド/メソッド名 (#x) の先頭 '#' はここで一度だけ許可する。
224        // '#' を識別子名の一部として含めることで、`this.#x` や `class C { #x = 1 }` が
225        // 既存の識別子ベースのメンバアクセス/フィールド宣言パスをそのまま通る
226        // ('#' を未知文字として読み捨てると `this.x` に化けてしまう危険な挙動だった)。
227        if self.peek() == Some('#') {
228            s.push('#');
229            self.advance();
230        }
231        while let Some(ch) = self.peek() {
232            if ch.is_ascii_alphanumeric() || ch == '_' || ch == '$' {
233                s.push(ch);
234                self.advance();
235            } else {
236                break;
237            }
238        }
239        if let Some(k) = KEYWORDS.iter().find(|k| **k == s.as_str()) {
240            Token::Keyword(k)
241        } else {
242            Token::Ident(s)
243        }
244    }
245
246    fn read_number(&mut self) -> Token {
247        let mut s = String::new();
248        // 16進 / 8進(`0o`) / 2進(`0b`)(ES2015 の `0o`/`0b` は以前は 16進しか対応しておらず、
249        // `0o17`/`0b101` を書くと `0` だけが数値トークンとして打ち切られ、続く `o17`/`b101`
250        // が別の識別子トークンになって構文解析が静かに壊れるバグだった。数値セパレータ
251        // `_`(ES2021)もこの3種すべてで共通対応する)。
252        let radix_prefix = if self.peek() == Some('0') {
253            match self.peek_at(1) {
254                Some('x') | Some('X') => Some((16u32, "0x")),
255                Some('o') | Some('O') => Some((8u32, "0o")),
256                Some('b') | Some('B') => Some((2u32, "0b")),
257                _ => None,
258            }
259        } else {
260            None
261        };
262        if let Some((radix, prefix)) = radix_prefix {
263            self.advance();
264            self.advance();
265            let mut digits = String::new();
266            while let Some(ch) = self.peek() {
267                if ch.is_digit(radix) {
268                    digits.push(ch);
269                    self.advance();
270                } else if ch == '_' {
271                    self.advance();
272                } else {
273                    break;
274                }
275            }
276            // `0x` の後ろに有効な桁が無い/桁あふれ等は、従来 `unwrap_or(0)` で
277            // **黙って 0** になっていた(`0xZZ` が 0 として計算に混ざる)。
278            let val = match u64::from_str_radix(&digits, radix) {
279                Ok(v) => v as f64,
280                // 【2026-09-26】64 ビットを超える値(`0x10000000000000000` 等)は JS では
281                // 浮動小数になるだけで正当。桁を浮動小数で積み上げる。空(`0x` のみ)はエラーのまま。
282                Err(_) if !digits.is_empty() => digits
283                    .chars()
284                    .filter_map(|c| c.to_digit(radix))
285                    .fold(0.0f64, |acc, d| acc * radix as f64 + d as f64),
286                Err(_) => {
287                    let msg = alloc::format!("invalid {} literal '{}{}'", prefix, prefix, digits);
288                    self.record_error(msg);
289                    0.0
290                }
291            };
292            // BigInt 接尾辞 n。
293            if self.peek() == Some('n') {
294                self.advance();
295                let mut lit = String::from(prefix);
296                lit.push_str(&digits);
297                return Token::BigIntLit(lit);
298            }
299            return Token::Num(val);
300        }
301        let mut has_dot = false;
302        let mut has_exp = false;
303        while let Some(ch) = self.peek() {
304            if ch.is_ascii_digit() {
305                s.push(ch);
306                self.advance();
307            } else if ch == '_' {
308                // 数値セパレータ(ES2021。`1_000_000` のような読みやすさ用の区切り)。
309                // 上と同じ理由で以前は未対応だった。値には含めず読み飛ばすだけでよい。
310                self.advance();
311            } else if ch == '.' && !has_dot && !has_exp {
312                s.push(ch);
313                has_dot = true;
314                self.advance();
315            } else if (ch == 'e' || ch == 'E') && !has_exp {
316                s.push(ch);
317                has_exp = true;
318                self.advance();
319                if matches!(self.peek(), Some('+') | Some('-')) {
320                    if let Some(c) = self.advance() {
321                        s.push(c);
322                    }
323                }
324            } else {
325                break;
326            }
327        }
328        // BigInt 接尾辞 n(小数・指数を含まない整数のみ)。
329        if self.peek() == Some('n') && !has_dot && !has_exp {
330            self.advance();
331            return Token::BigIntLit(s);
332        }
333        match s.parse::<f64>() {
334            Ok(v) => Token::Num(v),
335            Err(_) => {
336                // 従来は `unwrap_or(0.0)` で黙って 0 になっていた。
337                self.record_error(alloc::format!("invalid numeric literal '{}'", s));
338                Token::Num(0.0)
339            }
340        }
341    }
342
343    fn read_string(&mut self, quote: char) -> Token {
344        self.advance(); // opening quote
345        let mut s = String::new();
346        let mut closed = false;
347        while let Some(ch) = self.peek() {
348            if ch == quote {
349                self.advance();
350                closed = true;
351                break;
352            }
353            if ch == '\\' {
354                self.advance();
355                if let Some(esc) = self.advance() {
356                    if let Some(c) = unescape(esc, self) {
357                        s.push(c);
358                    }
359                    // `esc` が `None` を返すのは行継続(`\` の直後が改行)の場合のみで、
360                    // 以前は `unescape` が改行文字そのものをそのまま文字列に含めて
361                    // しまっており、`"abc\` + 改行 + `def"` が仕様上の `"abcdef"`
362                    // (行継続は文字列に何も追加しない)ではなく `"abc\ndef"`
363                    // (改行が値に混入)になるバグだった。
364                }
365            } else {
366                s.push(ch);
367                self.advance();
368            }
369        }
370        if !closed {
371            // 閉じ忘れの文字列は、そこから先のソースを文字列として飲み込む。
372            self.record_error(alloc::format!("unterminated string literal ({}...)", quote));
373        }
374        Token::Str(s)
375    }
376
377    fn read_template(&mut self) -> Token {
378        self.advance(); // opening backtick
379        let mut parts = Vec::new();
380        let mut cur = String::new();
381        let mut raw_start = self.pos;
382        while let Some(ch) = self.peek() {
383            if ch == '`' {
384                let raw: String = self.chars[raw_start..self.pos].iter().collect();
385                self.advance();
386                parts.push(TemplatePart::Quasi(core::mem::take(&mut cur), raw));
387                return Token::Template(parts);
388            }
389            if ch == '\\' {
390                self.advance();
391                if let Some(esc) = self.advance() {
392                    if let Some(c) = unescape(esc, self) {
393                        cur.push(c);
394                    }
395                }
396                continue;
397            }
398            if ch == '$' && self.peek_at(1) == Some('{') {
399                let raw: String = self.chars[raw_start..self.pos].iter().collect();
400                self.advance(); // '$'
401                self.advance(); // '{'
402                parts.push(TemplatePart::Quasi(core::mem::take(&mut cur), raw));
403                parts.push(TemplatePart::Expr(self.scan_interpolation_expr()));
404                raw_start = self.pos;
405                continue;
406            }
407            cur.push(ch);
408            self.advance();
409        }
410        // ここへ来たのは閉じバッククォートを見つけずに入力が尽きた場合だけ
411        // (閉じられた場合はループ内で return している)。
412        self.record_error(String::from("unterminated template literal"));
413        let raw: String = self.chars[raw_start..self.pos].iter().collect();
414        parts.push(TemplatePart::Quasi(cur, raw));
415        Token::Template(parts)
416    }
417
418    /// `${` の直後から呼ばれる。対応する `}` の直前までの生ソースを返す(`}` 自体は
419    /// 消費するが戻り値には含めない)。ネストした `{}`(オブジェクトリテラル等)の
420    /// 深さを追跡しつつ、文字列リテラル(`'...'`/`"..."`、バックスラッシュエスケープ
421    /// 考慮)とネストしたテンプレートリテラル(`` `...` ``)の中身はそのまま素通しして
422    /// 深さ計算を乱さないようにする。ネストしたテンプレート内部の `${}` へは再帰せず
423    /// 対応するバッククォートまで丸ごと取り込む簡略実装(`` `${`a${b}c`}` `` のような
424    /// 二重ネストは非対応、この処理系のテンプレート補間の主用途である単純な値の
425    /// 埋め込みでは問題にならない)。
426    fn scan_interpolation_expr(&mut self) -> String {
427        let mut src = String::new();
428        let mut depth = 1i32;
429        while let Some(ch) = self.peek() {
430            match ch {
431                '{' => {
432                    depth += 1;
433                    src.push(ch);
434                    self.advance();
435                }
436                '}' => {
437                    depth -= 1;
438                    self.advance();
439                    if depth == 0 {
440                        break;
441                    }
442                    src.push(ch);
443                }
444                '\'' | '"' => {
445                    let quote = ch;
446                    src.push(ch);
447                    self.advance();
448                    while let Some(c2) = self.peek() {
449                        src.push(c2);
450                        self.advance();
451                        if c2 == '\\' {
452                            if let Some(c3) = self.peek() {
453                                src.push(c3);
454                                self.advance();
455                            }
456                            continue;
457                        }
458                        if c2 == quote {
459                            break;
460                        }
461                    }
462                }
463                '`' => {
464                    src.push(ch);
465                    self.advance();
466                    while let Some(c2) = self.peek() {
467                        src.push(c2);
468                        self.advance();
469                        if c2 == '\\' {
470                            if let Some(c3) = self.peek() {
471                                src.push(c3);
472                                self.advance();
473                            }
474                            continue;
475                        }
476                        if c2 == '`' {
477                            break;
478                        }
479                    }
480                }
481                _ => {
482                    src.push(ch);
483                    self.advance();
484                }
485            }
486        }
487        src
488    }
489
490    pub fn next_token(&mut self) -> Token {
491        let t = self.scan();
492        self.prev = Some(t.clone());
493        t
494    }
495
496    /// 直前に返したトークンの開始位置(文字単位)。
497    pub fn token_start(&self) -> usize {
498        self.tok_start
499    }
500
501    /// 字句解析を終えたソース(文字列の配列)を取り出す。
502    pub fn into_chars(self) -> Vec<char> {
503        self.chars
504    }
505
506    fn scan(&mut self) -> Token {
507        self.skip_trivia();
508        self.tok_start = self.pos;
509        let ch = match self.peek() {
510            Some(c) => c,
511            None => return Token::Eof,
512        };
513        if ch.is_ascii_alphabetic() || ch == '_' || ch == '$' || ch == '#' {
514            return self.read_identifier_or_keyword();
515        }
516        if ch.is_ascii_digit()
517            || (ch == '.' && self.peek_at(1).map(|c| c.is_ascii_digit()).unwrap_or(false))
518        {
519            return self.read_number();
520        }
521        if ch == '"' || ch == '\'' {
522            return self.read_string(ch);
523        }
524        if ch == '`' {
525            return self.read_template();
526        }
527        // 正規表現リテラル(除算と曖昧。直前トークンで判定)。
528        if ch == '/' && self.regex_allowed() {
529            if let Some(rx) = self.try_read_regex() {
530                return rx;
531            }
532        }
533        // 記号は最長一致。
534        for sym in SYMBOLS {
535            if self.match_str(sym) {
536                // 【2026-09-05】`?.` の直後が数字なら、オプショナルチェーンでは
537                // なく**三項演算子の `?` + 小数リテラル**(`cond ? .5 : x`)。
538                // ECMAScript が明示的に定めている例外。
539                //
540                // jQuery 1.8.2 の `...test(...)?.01*parseFloat(RegExp.$1)+"":b?...`
541                // で実際に踏んだ。`?.` として食うと三項の `?` が消え、
542                // 後続の `:` が孤立して構文エラーになり、
543                // **jQuery 本体が丸ごと解析できなくなっていた**。
544                if *sym == "?." && self.peek_at(2).is_some_and(|c| c.is_ascii_digit()) {
545                    continue;
546                }
547                self.pos += sym.chars().count();
548                return Token::Sym(sym);
549            }
550        }
551        // 未知の文字はスキップして次へ。
552        self.advance();
553        self.scan()
554    }
555
556    /// 直前トークンから `/` を正規表現開始とみなしてよいか判定。
557    fn regex_allowed(&self) -> bool {
558        match &self.prev {
559            None | Some(Token::Eof) | Some(Token::LazyBody(..)) => true,
560            Some(Token::Num(_))
561            | Some(Token::BigIntLit(_))
562            | Some(Token::Str(_))
563            | Some(Token::Ident(_))
564            | Some(Token::Template(_))
565            | Some(Token::Regex(_)) => false,
566            Some(Token::Keyword(k)) => !matches!(
567                *k,
568                "this" | "super" | "true" | "false" | "null" | "undefined"
569            ),
570            // 値を終える `)` `]` の後は除算。それ以外(演算子・`(` `,` `{` 等)は正規表現。
571            Some(Token::Sym(s)) => !matches!(*s, ")" | "]"),
572        }
573    }
574
575    /// `/pattern/flags` を読む。閉じ `/` が無い等で正規表現でなければ None(除算扱い)。
576    fn try_read_regex(&mut self) -> Option<Token> {
577        let start = self.pos;
578        self.advance(); // 開始の /
579        let mut pat = String::new();
580        let mut in_class = false;
581        loop {
582            match self.peek() {
583                None | Some('\n') => {
584                    self.pos = start;
585                    return None;
586                }
587                Some('\\') => {
588                    pat.push('\\');
589                    self.advance();
590                    if let Some(c) = self.peek() {
591                        pat.push(c);
592                        self.advance();
593                    }
594                }
595                Some('[') => {
596                    in_class = true;
597                    pat.push('[');
598                    self.advance();
599                }
600                Some(']') => {
601                    in_class = false;
602                    pat.push(']');
603                    self.advance();
604                }
605                Some('/') if !in_class => {
606                    self.advance();
607                    break;
608                }
609                Some(c) => {
610                    pat.push(c);
611                    self.advance();
612                }
613            }
614        }
615        // 空パターンは正規表現とみなさない(`//` はコメントだが skip 済みなので保険)。
616        if pat.is_empty() {
617            self.pos = start;
618            return None;
619        }
620        let mut flags = String::new();
621        while let Some(c) = self.peek() {
622            if c.is_ascii_alphabetic() {
623                flags.push(c);
624                self.advance();
625            } else {
626                break;
627            }
628        }
629        Some(Token::Regex(Box::new((pat, flags))))
630    }
631
632    fn match_str(&self, s: &str) -> bool {
633        for (i, c) in s.chars().enumerate() {
634            if self.peek_at(i) != Some(c) {
635                return false;
636            }
637        }
638        true
639    }
640}
641
642/// エスケープ文字を解決。`\uXXXX` / `\xHH` にも対応。行継続(`\` の直後が改行)は
643/// 仕様上「文字列に何も追加しない」ため `None` を返す(`\r\n` は1つの行終端として
644/// まとめて読み飛ばす)。
645fn unescape(esc: char, lexer: &mut Lexer) -> Option<char> {
646    match esc {
647        '\n' => return None,
648        '\r' => {
649            if lexer.peek() == Some('\n') {
650                lexer.advance();
651            }
652            return None;
653        }
654        _ => {}
655    }
656    Some(match esc {
657        'n' => '\n',
658        't' => '\t',
659        'r' => '\r',
660        'b' => '\u{8}',
661        'f' => '\u{c}',
662        'v' => '\u{b}',
663        '0' => '\0',
664        'u' => {
665            // \uXXXX または \u{XXXXX}
666            let mut hex = String::new();
667            if lexer.peek() == Some('{') {
668                lexer.advance();
669                while let Some(c) = lexer.peek() {
670                    if c == '}' {
671                        lexer.advance();
672                        break;
673                    }
674                    hex.push(c);
675                    lexer.advance();
676                }
677            } else {
678                for _ in 0..4 {
679                    if let Some(c) = lexer.peek() {
680                        if c.is_ascii_hexdigit() {
681                            hex.push(c);
682                            lexer.advance();
683                        } else {
684                            break;
685                        }
686                    }
687                }
688            }
689            u32::from_str_radix(&hex, 16)
690                .ok()
691                .and_then(char::from_u32)
692                .unwrap_or('\u{fffd}')
693        }
694        'x' => {
695            let mut hex = String::new();
696            for _ in 0..2 {
697                if let Some(c) = lexer.peek() {
698                    if c.is_ascii_hexdigit() {
699                        hex.push(c);
700                        lexer.advance();
701                    } else {
702                        break;
703                    }
704                }
705            }
706            u32::from_str_radix(&hex, 16)
707                .ok()
708                .and_then(char::from_u32)
709                .unwrap_or('\u{fffd}')
710        }
711        other => other,
712    })
713}