Skip to main content

atmos/os_lib/js/
lexer.rs

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