Skip to main content

atmos/os_lib/js/
regex.rs

1//! 小さなバックトラッキング正規表現エンジン。
2//!
3//! パターンを命令列(バックトラッキング VM のバイトコード)へコンパイルし、再帰で実行する。
4//! 暴走(catastrophic backtracking)からカーネルを守るため **実行予算**で打ち切る。
5//!
6//! 対応: リテラル・`.`・文字クラス `[...]`/`[^...]`(範囲・`\d\w\s` 等)・アンカー `^ $`・
7//! 量指定子 `* + ? {n} {n,} {n,m}`(貪欲/非貪欲)・グループ `(...)`/`(?:...)`・選択 `|`・
8//! 後方参照 `\1`・単語境界 `\b \B`。フラグ: `i`(無視) `m`(複数行) `s`(dotall) `g`(グローバル)。
9
10use alloc::boxed::Box;
11use alloc::string::String;
12use alloc::vec;
13use alloc::vec::Vec;
14
15/// 文字クラスの 1 要素。
16#[derive(Clone)]
17enum ClassItem {
18    Ch(char),
19    Range(char, char),
20    Digit,
21    NotDigit,
22    Word,
23    NotWord,
24    Space,
25    NotSpace,
26    UnicodeProp(String, bool),
27}
28
29#[derive(Clone)]
30struct ClassData {
31    negate: bool,
32    items: Vec<ClassItem>,
33}
34
35impl ClassData {
36    fn matches(&self, c: char, ignorecase: bool) -> bool {
37        let hit = self.items.iter().any(|it| item_match(it, c, ignorecase));
38        hit ^ self.negate
39    }
40}
41
42fn item_match(it: &ClassItem, c: char, ignorecase: bool) -> bool {
43    match it {
44        ClassItem::Ch(x) => eqc(*x, c, ignorecase),
45        ClassItem::Range(a, b) => {
46            if *a <= c && c <= *b {
47                return true;
48            }
49            if ignorecase {
50                let lc = c.to_ascii_lowercase();
51                let uc = c.to_ascii_uppercase();
52                (*a <= lc && lc <= *b) || (*a <= uc && uc <= *b)
53            } else {
54                false
55            }
56        }
57        ClassItem::Digit => c.is_ascii_digit(),
58        ClassItem::NotDigit => !c.is_ascii_digit(),
59        ClassItem::Word => c == '_' || c.is_ascii_alphanumeric(),
60        ClassItem::NotWord => !(c == '_' || c.is_ascii_alphanumeric()),
61        ClassItem::Space => c.is_whitespace(),
62        ClassItem::NotSpace => !c.is_whitespace(),
63        ClassItem::UnicodeProp(prop, negate) => {
64            let hit = match prop.as_str() {
65                "ASCII" => c.is_ascii(),
66                "Letter" | "L" => c.is_alphabetic(),
67                "Number" | "N" => c.is_numeric(),
68                "Punctuation" | "P" => c.is_ascii_punctuation(),
69                "Symbol" | "S" => c.is_ascii_punctuation() || (!c.is_alphanumeric() && !c.is_whitespace()),
70                "Separator" | "Z" | "Space" => c.is_whitespace(),
71                _ => c.is_alphanumeric(),
72            };
73            hit ^ negate
74        }
75    }
76}
77
78fn eqc(a: char, b: char, ignorecase: bool) -> bool {
79    if a == b {
80        return true;
81    }
82    ignorecase && a.eq_ignore_ascii_case(&b)
83}
84
85fn is_word(c: char) -> bool {
86    c == '_' || c.is_ascii_alphanumeric()
87}
88
89// ---- パース木 ----
90
91enum Node {
92    Empty,
93    Char(char),
94    Any,
95    Class(ClassData),
96    Start,
97    End,
98    WordBoundary(bool), // true=\b, false=\B
99    Group {
100        idx: Option<usize>,
101        node: Box<Node>,
102    },
103    Concat(Vec<Node>),
104    Alt(Vec<Node>),
105    Repeat {
106        node: Box<Node>,
107        min: usize,
108        max: Option<usize>,
109        greedy: bool,
110    },
111    Backref(usize),
112}
113
114// ---- VM 命令 ----
115
116enum Inst {
117    Char(char),
118    Any,
119    Class(ClassData),
120    Match,
121    Jmp(usize),
122    Split(usize, usize),
123    Save(usize),
124    AssertStart,
125    AssertEnd,
126    WordBoundary(bool),
127    Backref(usize),
128}
129
130pub struct Regex {
131    prog: Vec<Inst>,
132    pub ngroups: usize,
133    pub ignorecase: bool,
134    pub multiline: bool,
135    pub dotall: bool,
136    pub global: bool,
137    pub sticky: bool,
138    /// `d`(hasIndices)フラグ。true なら `exec()` の結果に各キャプチャの
139    /// [start,end) 文字インデックスを持つ `indices` 配列を追加する。
140    pub has_indices: bool,
141    pub unicode: bool,
142    pub unicode_sets: bool,
143    pub source: String,
144    pub flags: String,
145    /// 名前付きキャプチャグループ `(?<name>...)`(ES2018)の (名前, グループ番号) 一覧。
146    /// 一つも無ければ空(`exec()` の `groups` プロパティは `undefined` になる)。
147    pub group_names: Vec<(String, usize)>,
148}
149
150/// 1 マッチ結果。captures[0] が全体、以降が各グループ(None は不参加)。
151pub struct Match {
152    pub start: usize,
153    pub end: usize,
154    pub captures: Vec<Option<(usize, usize)>>,
155}
156
157/// JS `RegExp` オブジェクトの内部状態(コンパイル済み + lastIndex)。
158pub struct RegExpData {
159    pub re: Regex,
160    pub last_index: usize,
161}
162
163struct Parser {
164    chars: Vec<char>,
165    pos: usize,
166    ngroups: usize,
167    /// 名前付きキャプチャグループ `(?<name>...)`(ES2018)の (名前, グループ番号) 一覧。
168    group_names: Vec<(String, usize)>,
169}
170
171impl Regex {
172    /// パターンとフラグからコンパイル。失敗時はリテラル一致にフォールバック。
173    pub fn new(pattern: &str, flags: &str) -> Regex {
174        let mut p = Parser {
175            chars: pattern.chars().collect(),
176            pos: 0,
177            ngroups: 0,
178            group_names: Vec::new(),
179        };
180        let node = p.parse_alt();
181        let ngroups = p.ngroups;
182        let group_names = p.group_names;
183        // コンパイル: Save(0) <body> Save(1) Match
184        let mut prog = Vec::new();
185        prog.push(Inst::Save(0));
186        compile(&node, &mut prog);
187        prog.push(Inst::Save(1));
188        prog.push(Inst::Match);
189        Regex {
190            prog,
191            ngroups,
192            ignorecase: flags.contains('i'),
193            multiline: flags.contains('m'),
194            dotall: flags.contains('s'),
195            global: flags.contains('g'),
196            sticky: flags.contains('y'),
197            has_indices: flags.contains('d'),
198            unicode: flags.contains('u'),
199            unicode_sets: flags.contains('v'),
200            source: String::from(pattern),
201            flags: String::from(flags),
202            group_names,
203        }
204    }
205
206    /// `start` 以降で最初のマッチを探す。
207    pub fn find_at(&self, subject: &[char], start: usize) -> Option<Match> {
208        for sp in start..=subject.len() {
209            if let Some(m) = self.try_match_at(subject, sp) {
210                return Some(m);
211            }
212        }
213        None
214    }
215
216    /// `y`(sticky)フラグ用: `start` の位置でのみマッチを試みる(前方探索しない)。
217    /// 通常の `find_at` と違い、その場所で一致しなければ探索を打ち切って `None` を返す。
218    pub fn find_exact_at(&self, subject: &[char], start: usize) -> Option<Match> {
219        if start > subject.len() {
220            return None;
221        }
222        self.try_match_at(subject, start)
223    }
224
225    fn try_match_at(&self, subject: &[char], sp: usize) -> Option<Match> {
226        let nslots = 2 * (self.ngroups + 1);
227        let mut caps: Vec<isize> = vec![-1; nslots];
228        let mut budget: u32 = 600_000;
229        if self.run(0, subject, sp, &mut caps, &mut budget) {
230            let s = caps[0].max(0) as usize;
231            let e = caps[1].max(0) as usize;
232            let mut captures = Vec::with_capacity(self.ngroups + 1);
233            for g in 0..=self.ngroups {
234                let a = caps[2 * g];
235                let b = caps[2 * g + 1];
236                if a >= 0 && b >= 0 {
237                    captures.push(Some((a as usize, b as usize)));
238                } else {
239                    captures.push(None);
240                }
241            }
242            Some(Match {
243                start: s,
244                end: e,
245                captures,
246            })
247        } else {
248            None
249        }
250    }
251
252    fn run(
253        &self,
254        mut pc: usize,
255        s: &[char],
256        mut sp: usize,
257        caps: &mut Vec<isize>,
258        budget: &mut u32,
259    ) -> bool {
260        loop {
261            if *budget == 0 {
262                return false;
263            }
264            *budget -= 1;
265            match &self.prog[pc] {
266                Inst::Match => return true,
267                Inst::Char(c) => {
268                    if sp < s.len() && eqc(*c, s[sp], self.ignorecase) {
269                        pc += 1;
270                        sp += 1;
271                    } else {
272                        return false;
273                    }
274                }
275                Inst::Any => {
276                    if sp < s.len() && (self.dotall || s[sp] != '\n') {
277                        pc += 1;
278                        sp += 1;
279                    } else {
280                        return false;
281                    }
282                }
283                Inst::Class(cd) => {
284                    if sp < s.len() && cd.matches(s[sp], self.ignorecase) {
285                        pc += 1;
286                        sp += 1;
287                    } else {
288                        return false;
289                    }
290                }
291                Inst::Jmp(x) => pc = *x,
292                Inst::Split(a, b) => {
293                    let (a, b) = (*a, *b);
294                    let saved = caps.clone();
295                    if self.run(a, s, sp, caps, budget) {
296                        return true;
297                    }
298                    *caps = saved;
299                    pc = b;
300                }
301                Inst::Save(n) => {
302                    let n = *n;
303                    let old = caps[n];
304                    caps[n] = sp as isize;
305                    if self.run(pc + 1, s, sp, caps, budget) {
306                        return true;
307                    }
308                    caps[n] = old;
309                    return false;
310                }
311                Inst::AssertStart => {
312                    let ok = sp == 0 || (self.multiline && sp > 0 && s[sp - 1] == '\n');
313                    if ok {
314                        pc += 1;
315                    } else {
316                        return false;
317                    }
318                }
319                Inst::AssertEnd => {
320                    let ok = sp == s.len() || (self.multiline && s[sp] == '\n');
321                    if ok {
322                        pc += 1;
323                    } else {
324                        return false;
325                    }
326                }
327                Inst::WordBoundary(want) => {
328                    let before = sp > 0 && is_word(s[sp - 1]);
329                    let after = sp < s.len() && is_word(s[sp]);
330                    let boundary = before != after;
331                    if boundary == *want {
332                        pc += 1;
333                    } else {
334                        return false;
335                    }
336                }
337                Inst::Backref(idx) => {
338                    let (a, b) = (caps[2 * idx], caps[2 * idx + 1]);
339                    if a < 0 || b < 0 {
340                        pc += 1;
341                        continue;
342                    } // 未参加グループは空一致。
343                    let (a, b) = (a as usize, b as usize);
344                    let len = b - a;
345                    if sp + len <= s.len()
346                        && (0..len).all(|k| eqc(s[a + k], s[sp + k], self.ignorecase))
347                    {
348                        sp += len;
349                        pc += 1;
350                    } else {
351                        return false;
352                    }
353                }
354            }
355        }
356    }
357}
358
359fn compile(node: &Node, prog: &mut Vec<Inst>) {
360    match node {
361        Node::Empty => {}
362        Node::Char(c) => prog.push(Inst::Char(*c)),
363        Node::Any => prog.push(Inst::Any),
364        Node::Class(cd) => prog.push(Inst::Class(cd.clone())),
365        Node::Start => prog.push(Inst::AssertStart),
366        Node::End => prog.push(Inst::AssertEnd),
367        Node::WordBoundary(b) => prog.push(Inst::WordBoundary(*b)),
368        Node::Backref(i) => prog.push(Inst::Backref(*i)),
369        Node::Concat(items) => {
370            for it in items {
371                compile(it, prog);
372            }
373        }
374        Node::Group { idx, node } => {
375            if let Some(i) = idx {
376                prog.push(Inst::Save(2 * i));
377            }
378            compile(node, prog);
379            if let Some(i) = idx {
380                prog.push(Inst::Save(2 * i + 1));
381            }
382        }
383        Node::Alt(branches) => {
384            // Split で分岐し、各枝の末尾を最後へ Jmp。
385            let mut jmp_fixups = Vec::new();
386            for (k, br) in branches.iter().enumerate() {
387                if k + 1 < branches.len() {
388                    let split_pc = prog.len();
389                    prog.push(Inst::Split(0, 0)); // 後で埋める
390                    let l1 = prog.len();
391                    compile(br, prog);
392                    let jmp_pc = prog.len();
393                    prog.push(Inst::Jmp(0));
394                    jmp_fixups.push(jmp_pc);
395                    let l2 = prog.len();
396                    if let Inst::Split(a, b) = &mut prog[split_pc] {
397                        *a = l1;
398                        *b = l2;
399                    }
400                } else {
401                    compile(br, prog);
402                }
403            }
404            let end = prog.len();
405            for j in jmp_fixups {
406                if let Inst::Jmp(x) = &mut prog[j] {
407                    *x = end;
408                }
409            }
410        }
411        Node::Repeat {
412            node,
413            min,
414            max,
415            greedy,
416        } => {
417            // 下限分を必須展開。
418            for _ in 0..*min {
419                compile(node, prog);
420            }
421            match max {
422                None => {
423                    // {min,}: 残りは * 相当のループ。
424                    let l1 = prog.len();
425                    let split_pc = prog.len();
426                    prog.push(Inst::Split(0, 0));
427                    let body = prog.len();
428                    compile(node, prog);
429                    prog.push(Inst::Jmp(l1));
430                    let after = prog.len();
431                    if let Inst::Split(a, b) = &mut prog[split_pc] {
432                        if *greedy {
433                            *a = body;
434                            *b = after;
435                        } else {
436                            *a = after;
437                            *b = body;
438                        }
439                    }
440                }
441                Some(mx) => {
442                    // {min,max}: 追加 (max-min) 回をオプション展開。
443                    let opt = mx.saturating_sub(*min);
444                    let mut split_fixups = Vec::new();
445                    for _ in 0..opt {
446                        let split_pc = prog.len();
447                        prog.push(Inst::Split(0, 0));
448                        let body = prog.len();
449                        compile(node, prog);
450                        split_fixups.push((split_pc, body));
451                    }
452                    let after = prog.len();
453                    for (sp_pc, body) in split_fixups {
454                        if let Inst::Split(a, b) = &mut prog[sp_pc] {
455                            if *greedy {
456                                *a = body;
457                                *b = after;
458                            } else {
459                                *a = after;
460                                *b = body;
461                            }
462                        }
463                    }
464                }
465            }
466        }
467    }
468}
469
470impl Parser {
471    fn peek(&self) -> Option<char> {
472        self.chars.get(self.pos).copied()
473    }
474    fn peek_at(&self, offset: usize) -> Option<char> {
475        self.chars.get(self.pos + offset).copied()
476    }
477    fn bump(&mut self) -> Option<char> {
478        let c = self.peek();
479        if c.is_some() {
480            self.pos += 1;
481        }
482        c
483    }
484    fn eat(&mut self, c: char) -> bool {
485        if self.peek() == Some(c) {
486            self.pos += 1;
487            true
488        } else {
489            false
490        }
491    }
492
493    fn parse_alt(&mut self) -> Node {
494        let mut branches = vec![self.parse_concat()];
495        while self.eat('|') {
496            branches.push(self.parse_concat());
497        }
498        if branches.len() == 1 {
499            branches.pop().unwrap_or(Node::Empty)
500        } else {
501            Node::Alt(branches)
502        }
503    }
504
505    fn parse_concat(&mut self) -> Node {
506        let mut items = Vec::new();
507        while let Some(c) = self.peek() {
508            if c == '|' || c == ')' {
509                break;
510            }
511            let atom = self.parse_atom();
512            let quantified = self.parse_quantifier(atom);
513            items.push(quantified);
514        }
515        if items.is_empty() {
516            Node::Empty
517        } else if items.len() == 1 {
518            items.pop().unwrap_or(Node::Empty)
519        } else {
520            Node::Concat(items)
521        }
522    }
523
524    fn parse_quantifier(&mut self, atom: Node) -> Node {
525        let (min, max) = match self.peek() {
526            Some('*') => {
527                self.bump();
528                (0, None)
529            }
530            Some('+') => {
531                self.bump();
532                (1, None)
533            }
534            Some('?') => {
535                self.bump();
536                (0, Some(1))
537            }
538            Some('{') => {
539                if let Some((mn, mx)) = self.try_parse_brace() {
540                    (mn, mx)
541                } else {
542                    return atom;
543                }
544            }
545            _ => return atom,
546        };
547        let greedy = !self.eat('?'); // 後置 ? で非貪欲。
548        Node::Repeat {
549            node: Box::new(atom),
550            min,
551            max,
552            greedy,
553        }
554    }
555
556    fn try_parse_brace(&mut self) -> Option<(usize, Option<usize>)> {
557        let save = self.pos;
558        self.bump(); // {
559        let mut min_s = String::new();
560        while let Some(c) = self.peek() {
561            if c.is_ascii_digit() {
562                min_s.push(c);
563                self.bump();
564            } else {
565                break;
566            }
567        }
568        if min_s.is_empty() {
569            self.pos = save;
570            return None;
571        }
572        let min: usize = min_s.parse().unwrap_or(0);
573        let max = if self.eat(',') {
574            let mut max_s = String::new();
575            while let Some(c) = self.peek() {
576                if c.is_ascii_digit() {
577                    max_s.push(c);
578                    self.bump();
579                } else {
580                    break;
581                }
582            }
583            if max_s.is_empty() {
584                None
585            } else {
586                Some(max_s.parse().unwrap_or(min))
587            }
588        } else {
589            Some(min)
590        };
591        if !self.eat('}') {
592            self.pos = save;
593            return None;
594        }
595        Some((min, max))
596    }
597
598    fn parse_atom(&mut self) -> Node {
599        match self.peek() {
600            Some('(') => {
601                self.bump();
602                let mut group_name: Option<String> = None;
603                let idx = if self.peek() == Some('?') {
604                    self.bump(); // ?
605                    // 名前付きキャプチャグループ `(?<name>...)`(ES2018)。lookbehind の
606                    // `(?<=...)`/`(?<!...)` と先頭が同じなので、`<` の次が `=`/`!` なら
607                    // 除外する(lookbehind 自体はこのエンジン未対応で、他の `(?...)` と同じ
608                    // 非キャプチャへのフォールバックのまま)。
609                    if self.peek() == Some('<') && !matches!(self.peek_at(1), Some('=') | Some('!'))
610                    {
611                        self.bump(); // <
612                        let mut name = String::new();
613                        while let Some(c) = self.peek() {
614                            if c == '>' {
615                                break;
616                            }
617                            name.push(c);
618                            self.bump();
619                        }
620                        self.bump(); // >
621                        self.ngroups += 1;
622                        group_name = Some(name);
623                        Some(self.ngroups)
624                    } else {
625                        // (?: 非キャプチャ。他の (?...)(lookahead/lookbehind 等)は
626                        // 非キャプチャ扱いにフォールバック(既存の簡略実装のまま)。
627                        self.bump();
628                        None
629                    }
630                } else {
631                    self.ngroups += 1;
632                    Some(self.ngroups)
633                };
634                let inner = self.parse_alt();
635                self.eat(')');
636                if let (Some(idx), Some(name)) = (idx, group_name) {
637                    self.group_names.push((name, idx));
638                }
639                Node::Group {
640                    idx,
641                    node: Box::new(inner),
642                }
643            }
644            Some('[') => self.parse_class(),
645            Some('.') => {
646                self.bump();
647                Node::Any
648            }
649            Some('^') => {
650                self.bump();
651                Node::Start
652            }
653            Some('$') => {
654                self.bump();
655                Node::End
656            }
657            Some('\\') => {
658                self.bump();
659                self.parse_escape()
660            }
661            Some(c) => {
662                self.bump();
663                Node::Char(c)
664            }
665            None => Node::Empty,
666        }
667    }
668
669    fn parse_escape(&mut self) -> Node {
670        match self.bump() {
671            Some('d') => Node::Class(ClassData {
672                negate: false,
673                items: vec![ClassItem::Digit],
674            }),
675            Some('D') => Node::Class(ClassData {
676                negate: false,
677                items: vec![ClassItem::NotDigit],
678            }),
679            Some('w') => Node::Class(ClassData {
680                negate: false,
681                items: vec![ClassItem::Word],
682            }),
683            Some('W') => Node::Class(ClassData {
684                negate: false,
685                items: vec![ClassItem::NotWord],
686            }),
687            Some('s') => Node::Class(ClassData {
688                negate: false,
689                items: vec![ClassItem::Space],
690            }),
691            Some('S') => Node::Class(ClassData {
692                negate: false,
693                items: vec![ClassItem::NotSpace],
694            }),
695            Some('b') => Node::WordBoundary(true),
696            Some('B') => Node::WordBoundary(false),
697            Some('n') => Node::Char('\n'),
698            Some('t') => Node::Char('\t'),
699            Some('r') => Node::Char('\r'),
700            Some('f') => Node::Char('\u{0C}'),
701            Some('v') => Node::Char('\u{0B}'),
702            Some('0') => Node::Char('\0'),
703            // `\uXXXX`/`\u{X...}`(Unicode エスケープ)・`\xXX`(16進エスケープ)が
704            // 丸ごと未対応で、`A` が文字コード指定ではなくリテラルの `u0041`
705            // (4文字分の文字クラス)として解釈されてしまうバグだった。
706            Some('u') => Node::Char(self.parse_u_escape()),
707            Some('x') => Node::Char(self.parse_x_escape()),
708            Some('p') => self.parse_p_escape(false),
709            Some('P') => self.parse_p_escape(true),
710            Some(c) if c.is_ascii_digit() => {
711                let mut num = String::new();
712                num.push(c);
713                while let Some(d) = self.peek() {
714                    if d.is_ascii_digit() {
715                        num.push(d);
716                        self.bump();
717                    } else {
718                        break;
719                    }
720                }
721                Node::Backref(num.parse().unwrap_or(0))
722            }
723            Some(c) => Node::Char(c), // エスケープされたメタ文字。
724            None => Node::Empty,
725        }
726    }
727
728    fn parse_p_escape(&mut self, negate: bool) -> Node {
729        if self.peek() == Some('{') {
730            self.bump();
731            let mut prop_name = String::new();
732            while let Some(c) = self.bump() {
733                if c == '}' {
734                    break;
735                }
736                prop_name.push(c);
737            }
738            Node::Class(ClassData {
739                negate: false,
740                items: vec![ClassItem::UnicodeProp(prop_name, negate)],
741            })
742        } else {
743            Node::Char(if negate { 'P' } else { 'p' })
744        }
745    }
746
747    /// `\uXXXX`/`\u{X...}` の本体を解析する。呼出し時点で `\u` の `u` まで読み終えている
748    /// 前提(`parse_escape()`/`parse_class()` の両方から共用)。
749    fn parse_u_escape(&mut self) -> char {
750        if self.peek() == Some('{') {
751            self.bump(); // {
752            let mut hex = String::new();
753            while let Some(c) = self.peek() {
754                if c == '}' {
755                    break;
756                }
757                hex.push(c);
758                self.bump();
759            }
760            self.bump(); // }
761            u32::from_str_radix(&hex, 16).ok().and_then(char::from_u32).unwrap_or('u')
762        } else {
763            let save = self.pos;
764            let mut hex = String::new();
765            for _ in 0..4 {
766                match self.peek() {
767                    Some(c) if c.is_ascii_hexdigit() => {
768                        hex.push(c);
769                        self.bump();
770                    }
771                    _ => break,
772                }
773            }
774            match u32::from_str_radix(&hex, 16).ok().and_then(char::from_u32) {
775                Some(ch) if hex.len() == 4 => ch,
776                _ => {
777                    // 4桁揃わなければ通常の JS 正規表現と同様、`u` のリテラルに
778                    // フォールバックし、読み進めた分は巻き戻す。
779                    self.pos = save;
780                    'u'
781                }
782            }
783        }
784    }
785    /// `\xXX` の本体を解析する(呼出し時点で `x` まで読み終えている前提)。
786    fn parse_x_escape(&mut self) -> char {
787        let save = self.pos;
788        let mut hex = String::new();
789        for _ in 0..2 {
790            match self.peek() {
791                Some(c) if c.is_ascii_hexdigit() => {
792                    hex.push(c);
793                    self.bump();
794                }
795                _ => break,
796            }
797        }
798        match u32::from_str_radix(&hex, 16).ok().and_then(char::from_u32) {
799            Some(ch) if hex.len() == 2 => ch,
800            _ => {
801                self.pos = save;
802                'x'
803            }
804        }
805    }
806
807    fn parse_class(&mut self) -> Node {
808        self.bump(); // [
809        let negate = self.eat('^');
810        let mut items = Vec::new();
811        while let Some(c) = self.peek() {
812            if c == ']' {
813                break;
814            }
815            let lo = if c == '\\' {
816                self.bump();
817                match self.bump() {
818                    Some('d') => {
819                        items.push(ClassItem::Digit);
820                        continue;
821                    }
822                    Some('D') => {
823                        items.push(ClassItem::NotDigit);
824                        continue;
825                    }
826                    Some('w') => {
827                        items.push(ClassItem::Word);
828                        continue;
829                    }
830                    Some('W') => {
831                        items.push(ClassItem::NotWord);
832                        continue;
833                    }
834                    Some('s') => {
835                        items.push(ClassItem::Space);
836                        continue;
837                    }
838                    Some('S') => {
839                        items.push(ClassItem::NotSpace);
840                        continue;
841                    }
842                    Some('n') => '\n',
843                    Some('t') => '\t',
844                    Some('r') => '\r',
845                    // `[A-Z]` のような文字クラス内エスケープも `parse_escape()`
846                    // と同じく丸ごと未対応で、`u`/`x` の1文字+続く16進桁がそのまま
847                    // リテラル文字として範囲に混入する静かなバグだった。
848                    Some('u') => self.parse_u_escape(),
849                    Some('x') => self.parse_x_escape(),
850                    Some(e) => e,
851                    None => break,
852                }
853            } else {
854                self.bump();
855                c
856            };
857            // 範囲 a-z
858            if self.peek() == Some('-') && self.chars.get(self.pos + 1).is_some_and(|&x| x != ']') {
859                self.bump(); // -
860                let hi = if self.peek() == Some('\\') {
861                    self.bump();
862                    match self.bump() {
863                        Some('u') => self.parse_u_escape(),
864                        Some('x') => self.parse_x_escape(),
865                        Some(e) => e,
866                        None => lo,
867                    }
868                } else {
869                    self.bump().unwrap_or(lo)
870                };
871                items.push(ClassItem::Range(lo, hi));
872            } else {
873                items.push(ClassItem::Ch(lo));
874            }
875        }
876        self.eat(']');
877        Node::Class(ClassData { negate, items })
878    }
879}