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                // 【2026-09-26】64 ビットを超える値(`0x10000000000000000` 等)は JS では
265                // 浮動小数になるだけで正当。桁を浮動小数で積み上げる。空(`0x` のみ)はエラーのまま。
266                Err(_) if !digits.is_empty() => digits
267                    .chars()
268                    .filter_map(|c| c.to_digit(radix))
269                    .fold(0.0f64, |acc, d| acc * radix as f64 + d as f64),
270                Err(_) => {
271                    let msg = alloc::format!("invalid {} literal '{}{}'", prefix, prefix, digits);
272                    self.record_error(msg);
273                    0.0
274                }
275            };
276            // BigInt 接尾辞 n。
277            if self.peek() == Some('n') {
278                self.advance();
279                let mut lit = String::from(prefix);
280                lit.push_str(&digits);
281                return Token::BigIntLit(lit);
282            }
283            return Token::Num(val);
284        }
285        let mut has_dot = false;
286        let mut has_exp = false;
287        while let Some(ch) = self.peek() {
288            if ch.is_ascii_digit() {
289                s.push(ch);
290                self.advance();
291            } else if ch == '_' {
292                // 数値セパレータ(ES2021。`1_000_000` のような読みやすさ用の区切り)。
293                // 上と同じ理由で以前は未対応だった。値には含めず読み飛ばすだけでよい。
294                self.advance();
295            } else if ch == '.' && !has_dot && !has_exp {
296                s.push(ch);
297                has_dot = true;
298                self.advance();
299            } else if (ch == 'e' || ch == 'E') && !has_exp {
300                s.push(ch);
301                has_exp = true;
302                self.advance();
303                if matches!(self.peek(), Some('+') | Some('-')) {
304                    if let Some(c) = self.advance() {
305                        s.push(c);
306                    }
307                }
308            } else {
309                break;
310            }
311        }
312        // BigInt 接尾辞 n(小数・指数を含まない整数のみ)。
313        if self.peek() == Some('n') && !has_dot && !has_exp {
314            self.advance();
315            return Token::BigIntLit(s);
316        }
317        match s.parse::<f64>() {
318            Ok(v) => Token::Num(v),
319            Err(_) => {
320                // 従来は `unwrap_or(0.0)` で黙って 0 になっていた。
321                self.record_error(alloc::format!("invalid numeric literal '{}'", s));
322                Token::Num(0.0)
323            }
324        }
325    }
326
327    fn read_string(&mut self, quote: char) -> Token {
328        self.advance(); // opening quote
329        let mut s = String::new();
330        let mut closed = false;
331        while let Some(ch) = self.peek() {
332            if ch == quote {
333                self.advance();
334                closed = true;
335                break;
336            }
337            if ch == '\\' {
338                self.advance();
339                if let Some(esc) = self.advance() {
340                    if let Some(c) = unescape(esc, self) {
341                        s.push(c);
342                    }
343                    // `esc` が `None` を返すのは行継続(`\` の直後が改行)の場合のみで、
344                    // 以前は `unescape` が改行文字そのものをそのまま文字列に含めて
345                    // しまっており、`"abc\` + 改行 + `def"` が仕様上の `"abcdef"`
346                    // (行継続は文字列に何も追加しない)ではなく `"abc\ndef"`
347                    // (改行が値に混入)になるバグだった。
348                }
349            } else {
350                s.push(ch);
351                self.advance();
352            }
353        }
354        if !closed {
355            // 閉じ忘れの文字列は、そこから先のソースを文字列として飲み込む。
356            self.record_error(alloc::format!("unterminated string literal ({}...)", quote));
357        }
358        Token::Str(s)
359    }
360
361    fn read_template(&mut self) -> Token {
362        self.advance(); // opening backtick
363        let mut parts = Vec::new();
364        let mut cur = String::new();
365        let mut raw_start = self.pos;
366        while let Some(ch) = self.peek() {
367            if ch == '`' {
368                let raw: String = self.chars[raw_start..self.pos].iter().collect();
369                self.advance();
370                parts.push(TemplatePart::Quasi(core::mem::take(&mut cur), raw));
371                return Token::Template(parts);
372            }
373            if ch == '\\' {
374                self.advance();
375                if let Some(esc) = self.advance() {
376                    if let Some(c) = unescape(esc, self) {
377                        cur.push(c);
378                    }
379                }
380                continue;
381            }
382            if ch == '$' && self.peek_at(1) == Some('{') {
383                let raw: String = self.chars[raw_start..self.pos].iter().collect();
384                self.advance(); // '$'
385                self.advance(); // '{'
386                parts.push(TemplatePart::Quasi(core::mem::take(&mut cur), raw));
387                parts.push(TemplatePart::Expr(self.scan_interpolation_expr()));
388                raw_start = self.pos;
389                continue;
390            }
391            cur.push(ch);
392            self.advance();
393        }
394        // ここへ来たのは閉じバッククォートを見つけずに入力が尽きた場合だけ
395        // (閉じられた場合はループ内で return している)。
396        self.record_error(String::from("unterminated template literal"));
397        let raw: String = self.chars[raw_start..self.pos].iter().collect();
398        parts.push(TemplatePart::Quasi(cur, raw));
399        Token::Template(parts)
400    }
401
402    /// `${` の直後から呼ばれる。対応する `}` の直前までの生ソースを返す(`}` 自体は
403    /// 消費するが戻り値には含めない)。ネストした `{}`(オブジェクトリテラル等)の
404    /// 深さを追跡しつつ、文字列リテラル(`'...'`/`"..."`、バックスラッシュエスケープ
405    /// 考慮)とネストしたテンプレートリテラル(`` `...` ``)の中身はそのまま素通しして
406    /// 深さ計算を乱さないようにする。ネストしたテンプレート内部の `${}` へは再帰せず
407    /// 対応するバッククォートまで丸ごと取り込む簡略実装(`` `${`a${b}c`}` `` のような
408    /// 二重ネストは非対応、この処理系のテンプレート補間の主用途である単純な値の
409    /// 埋め込みでは問題にならない)。
410    fn scan_interpolation_expr(&mut self) -> String {
411        let mut src = String::new();
412        let mut depth = 1i32;
413        while let Some(ch) = self.peek() {
414            match ch {
415                '{' => {
416                    depth += 1;
417                    src.push(ch);
418                    self.advance();
419                }
420                '}' => {
421                    depth -= 1;
422                    self.advance();
423                    if depth == 0 {
424                        break;
425                    }
426                    src.push(ch);
427                }
428                '\'' | '"' => {
429                    let quote = ch;
430                    src.push(ch);
431                    self.advance();
432                    while let Some(c2) = self.peek() {
433                        src.push(c2);
434                        self.advance();
435                        if c2 == '\\' {
436                            if let Some(c3) = self.peek() {
437                                src.push(c3);
438                                self.advance();
439                            }
440                            continue;
441                        }
442                        if c2 == quote {
443                            break;
444                        }
445                    }
446                }
447                '`' => {
448                    src.push(ch);
449                    self.advance();
450                    while let Some(c2) = self.peek() {
451                        src.push(c2);
452                        self.advance();
453                        if c2 == '\\' {
454                            if let Some(c3) = self.peek() {
455                                src.push(c3);
456                                self.advance();
457                            }
458                            continue;
459                        }
460                        if c2 == '`' {
461                            break;
462                        }
463                    }
464                }
465                _ => {
466                    src.push(ch);
467                    self.advance();
468                }
469            }
470        }
471        src
472    }
473
474    pub fn next_token(&mut self) -> Token {
475        let t = self.scan();
476        self.prev = Some(t.clone());
477        t
478    }
479
480    fn scan(&mut self) -> Token {
481        self.skip_trivia();
482        let ch = match self.peek() {
483            Some(c) => c,
484            None => return Token::Eof,
485        };
486        if ch.is_ascii_alphabetic() || ch == '_' || ch == '$' || ch == '#' {
487            return self.read_identifier_or_keyword();
488        }
489        if ch.is_ascii_digit()
490            || (ch == '.' && self.peek_at(1).map(|c| c.is_ascii_digit()).unwrap_or(false))
491        {
492            return self.read_number();
493        }
494        if ch == '"' || ch == '\'' {
495            return self.read_string(ch);
496        }
497        if ch == '`' {
498            return self.read_template();
499        }
500        // 正規表現リテラル(除算と曖昧。直前トークンで判定)。
501        if ch == '/' && self.regex_allowed() {
502            if let Some(rx) = self.try_read_regex() {
503                return rx;
504            }
505        }
506        // 記号は最長一致。
507        for sym in SYMBOLS {
508            if self.match_str(sym) {
509                // 【2026-09-05】`?.` の直後が数字なら、オプショナルチェーンでは
510                // なく**三項演算子の `?` + 小数リテラル**(`cond ? .5 : x`)。
511                // ECMAScript が明示的に定めている例外。
512                //
513                // jQuery 1.8.2 の `...test(...)?.01*parseFloat(RegExp.$1)+"":b?...`
514                // で実際に踏んだ。`?.` として食うと三項の `?` が消え、
515                // 後続の `:` が孤立して構文エラーになり、
516                // **jQuery 本体が丸ごと解析できなくなっていた**。
517                if *sym == "?." && self.peek_at(2).is_some_and(|c| c.is_ascii_digit()) {
518                    continue;
519                }
520                self.pos += sym.chars().count();
521                return Token::Sym(String::from(*sym));
522            }
523        }
524        // 未知の文字はスキップして次へ。
525        self.advance();
526        self.scan()
527    }
528
529    /// 直前トークンから `/` を正規表現開始とみなしてよいか判定。
530    fn regex_allowed(&self) -> bool {
531        match &self.prev {
532            None | Some(Token::Eof) => true,
533            Some(Token::Num(_))
534            | Some(Token::BigIntLit(_))
535            | Some(Token::Str(_))
536            | Some(Token::Ident(_))
537            | Some(Token::Template(_))
538            | Some(Token::Regex(_, _)) => false,
539            Some(Token::Keyword(k)) => !matches!(
540                k.as_str(),
541                "this" | "super" | "true" | "false" | "null" | "undefined"
542            ),
543            // 値を終える `)` `]` の後は除算。それ以外(演算子・`(` `,` `{` 等)は正規表現。
544            Some(Token::Sym(s)) => !matches!(s.as_str(), ")" | "]"),
545        }
546    }
547
548    /// `/pattern/flags` を読む。閉じ `/` が無い等で正規表現でなければ None(除算扱い)。
549    fn try_read_regex(&mut self) -> Option<Token> {
550        let start = self.pos;
551        self.advance(); // 開始の /
552        let mut pat = String::new();
553        let mut in_class = false;
554        loop {
555            match self.peek() {
556                None | Some('\n') => {
557                    self.pos = start;
558                    return None;
559                }
560                Some('\\') => {
561                    pat.push('\\');
562                    self.advance();
563                    if let Some(c) = self.peek() {
564                        pat.push(c);
565                        self.advance();
566                    }
567                }
568                Some('[') => {
569                    in_class = true;
570                    pat.push('[');
571                    self.advance();
572                }
573                Some(']') => {
574                    in_class = false;
575                    pat.push(']');
576                    self.advance();
577                }
578                Some('/') if !in_class => {
579                    self.advance();
580                    break;
581                }
582                Some(c) => {
583                    pat.push(c);
584                    self.advance();
585                }
586            }
587        }
588        // 空パターンは正規表現とみなさない(`//` はコメントだが skip 済みなので保険)。
589        if pat.is_empty() {
590            self.pos = start;
591            return None;
592        }
593        let mut flags = String::new();
594        while let Some(c) = self.peek() {
595            if c.is_ascii_alphabetic() {
596                flags.push(c);
597                self.advance();
598            } else {
599                break;
600            }
601        }
602        Some(Token::Regex(pat, flags))
603    }
604
605    fn match_str(&self, s: &str) -> bool {
606        for (i, c) in s.chars().enumerate() {
607            if self.peek_at(i) != Some(c) {
608                return false;
609            }
610        }
611        true
612    }
613}
614
615/// エスケープ文字を解決。`\uXXXX` / `\xHH` にも対応。行継続(`\` の直後が改行)は
616/// 仕様上「文字列に何も追加しない」ため `None` を返す(`\r\n` は1つの行終端として
617/// まとめて読み飛ばす)。
618fn unescape(esc: char, lexer: &mut Lexer) -> Option<char> {
619    match esc {
620        '\n' => return None,
621        '\r' => {
622            if lexer.peek() == Some('\n') {
623                lexer.advance();
624            }
625            return None;
626        }
627        _ => {}
628    }
629    Some(match esc {
630        'n' => '\n',
631        't' => '\t',
632        'r' => '\r',
633        'b' => '\u{8}',
634        'f' => '\u{c}',
635        'v' => '\u{b}',
636        '0' => '\0',
637        'u' => {
638            // \uXXXX または \u{XXXXX}
639            let mut hex = String::new();
640            if lexer.peek() == Some('{') {
641                lexer.advance();
642                while let Some(c) = lexer.peek() {
643                    if c == '}' {
644                        lexer.advance();
645                        break;
646                    }
647                    hex.push(c);
648                    lexer.advance();
649                }
650            } else {
651                for _ in 0..4 {
652                    if let Some(c) = lexer.peek() {
653                        if c.is_ascii_hexdigit() {
654                            hex.push(c);
655                            lexer.advance();
656                        } else {
657                            break;
658                        }
659                    }
660                }
661            }
662            u32::from_str_radix(&hex, 16)
663                .ok()
664                .and_then(char::from_u32)
665                .unwrap_or('\u{fffd}')
666        }
667        'x' => {
668            let mut hex = String::new();
669            for _ in 0..2 {
670                if let Some(c) = lexer.peek() {
671                    if c.is_ascii_hexdigit() {
672                        hex.push(c);
673                        lexer.advance();
674                    } else {
675                        break;
676                    }
677                }
678            }
679            u32::from_str_radix(&hex, 16)
680                .ok()
681                .and_then(char::from_u32)
682                .unwrap_or('\u{fffd}')
683        }
684        other => other,
685    })
686}