Skip to main content

rexact_syntax/
parser.rs

1//! Hand-written recursive-descent parsers for the `ecma` and `python` dialects
2//! (FR-A04..A06, FR-A12).
3//!
4//! Contract: text in, [`Pattern`] or a catalogue error out; never a panic,
5//! never an approximation (RULE-05). Unsupported constructs are recognised and
6//! rejected with their own code. Invariant: nesting is bounded by
7//! [`ParseLimits::max_depth`], so hostile input cannot overflow the stack.
8//! Dialect rules are kept in separate functions (`esc_ecma`, `esc_py`, ...)
9//! and never unified (RULE-10).
10
11use crate::ast::*;
12use crate::dialect::Dialect;
13use crate::error::{ErrorCode, RexactError};
14use crate::flags::Flags;
15use crate::locate::locate;
16use rexact_unicode as uni;
17
18/// Bounds enforced while parsing (RULE-08).
19#[derive(Debug, Clone)]
20pub struct ParseLimits {
21    /// Maximum pattern length in scalar values (`RX-L005`).
22    pub max_length: u64,
23    /// Maximum group nesting depth (`RX-L006`).
24    pub max_depth: u32,
25}
26
27impl Default for ParseLimits {
28    fn default() -> Self {
29        Self {
30            max_length: 1_000_000,
31            max_depth: 100,
32        }
33    }
34}
35
36/// A parsed pattern.
37#[derive(Debug, Clone)]
38pub struct Pattern {
39    /// Original text.
40    pub source: String,
41    /// Dialect it was written for.
42    pub dialect: Dialect,
43    /// Effective global flags (option flags plus leading inline flags).
44    pub flags: Flags,
45    /// Syntax tree.
46    pub ast: Node,
47    /// Number of capturing groups.
48    pub group_count: u32,
49    /// Names of named groups, in order of appearance.
50    pub group_names: Vec<String>,
51    /// Non-fatal notes (for example ignored flags).
52    pub notes: Vec<String>,
53}
54
55impl Pattern {
56    /// Parse with default limits.
57    pub fn parse(source: &str, dialect: Dialect, flags: Flags) -> Result<Pattern, RexactError> {
58        Self::parse_with(source, dialect, flags, &ParseLimits::default())
59    }
60
61    /// Parse with explicit limits.
62    pub fn parse_with(
63        source: &str,
64        dialect: Dialect,
65        flags: Flags,
66        limits: &ParseLimits,
67    ) -> Result<Pattern, RexactError> {
68        let chars: Vec<char> = source.chars().collect();
69        let fail = |e: PErr| {
70            let mut r = RexactError::new(e.code);
71            r.detail = e.detail;
72            r.position = Some(locate(&chars, e.offset));
73            r
74        };
75        if chars.len() as u64 > limits.max_length {
76            return Err(fail(PErr::new(
77                ErrorCode::L005,
78                0,
79                "pattern exceeds the length limit",
80            )));
81        }
82        if matches!(dialect, Dialect::Go | Dialect::Rust) {
83            let out = crate::parser_re2::parse(&chars, dialect, flags, limits).map_err(fail)?;
84            return Ok(Pattern {
85                source: source.to_string(),
86                dialect,
87                flags: out.flags,
88                ast: out.ast,
89                group_count: out.groups,
90                group_names: out.names,
91                notes: out.notes,
92            });
93        }
94        if dialect == Dialect::Java {
95            // Java strings are UTF-16: the pattern is read as characters, whatever the flags.
96            crate::java::check(&chars).map_err(fail)?;
97            let out = crate::parser_pcre::parse(&chars, flags, limits).map_err(fail)?;
98            return Ok(Pattern {
99                source: source.to_string(),
100                dialect,
101                flags: out.flags,
102                ast: out.ast,
103                group_count: out.groups,
104                group_names: out.names,
105                notes: Vec::new(),
106            });
107        }
108        if dialect == Dialect::Pcre2 {
109            // Without `u` PCRE2 works on bytes: parse the UTF-8 bytes of the text.
110            let byte_chars: Vec<char>;
111            let input: &[char] = if flags.u {
112                &chars
113            } else {
114                byte_chars = source.bytes().map(char::from).collect();
115                &byte_chars
116            };
117            let out = crate::parser_pcre::parse(input, flags, limits).map_err(fail)?;
118            return Ok(Pattern {
119                source: source.to_string(),
120                dialect,
121                flags: out.flags,
122                ast: out.ast,
123                group_count: out.groups,
124                group_names: out.names,
125                notes: Vec::new(),
126            });
127        }
128        let (groups, has_named) = scan_groups(&chars, dialect, flags.x);
129        let mut p = Parser {
130            chars: &chars,
131            pos: 0,
132            d: dialect,
133            flags,
134            max_depth: limits.max_depth,
135            depth: 0,
136            total_groups: groups,
137            has_named,
138            next_capture: 1,
139            names: Vec::new(),
140            seen_content: false,
141        };
142        let ast = p.alt().map_err(fail)?;
143        if p.pos < chars.len() {
144            return Err(fail(PErr::new(ErrorCode::P007, p.pos, "unmatched ')'")));
145        }
146        Ok(Pattern {
147            source: source.to_string(),
148            dialect,
149            flags: p.flags,
150            ast,
151            group_count: p.next_capture - 1,
152            group_names: p.names,
153            notes: Vec::new(),
154        })
155    }
156
157    /// Parse a `/body/flags` literal (FR-A03). Flag letters are interpreted
158    /// for `dialect`; letters that do not change the language become notes.
159    pub fn parse_literal(text: &str, dialect: Dialect) -> Result<Pattern, RexactError> {
160        let shape = |why: &str| {
161            let mut e = RexactError::new(ErrorCode::P010);
162            e.detail = Some(format!("{why}; expected /body/flags"));
163            e
164        };
165        let rest = text
166            .strip_prefix('/')
167            .ok_or_else(|| shape("missing opening '/'"))?;
168        let close = rest
169            .rfind('/')
170            .ok_or_else(|| shape("missing closing '/'"))?;
171        let (body, flag_text) = (&rest[..close], &rest[close + 1..]);
172        if body.is_empty() {
173            return Err(shape("empty body (// is a comment, not a regex)"));
174        }
175        if !flag_text.chars().all(|c| c.is_ascii_alphabetic()) {
176            return Err(shape("flags must be letters"));
177        }
178        let (flags, notes) = Flags::from_letters(dialect, flag_text)?;
179        let mut p = Pattern::parse(body, dialect, flags)?;
180        p.notes = notes;
181        Ok(p)
182    }
183}
184
185pub(crate) struct PErr {
186    pub(crate) code: ErrorCode,
187    pub(crate) offset: usize,
188    pub(crate) detail: Option<String>,
189}
190
191impl PErr {
192    pub(crate) fn new(code: ErrorCode, offset: usize, detail: &str) -> Self {
193        Self {
194            code,
195            offset,
196            detail: Some(detail.to_string()),
197        }
198    }
199}
200
201pub(crate) type R<T> = Result<T, PErr>;
202
203pub(crate) fn err<T>(code: ErrorCode, offset: usize, detail: &str) -> R<T> {
204    Err(PErr::new(code, offset, detail))
205}
206
207/// Count capturing groups and detect named groups without building a tree.
208/// Needed because a backreference may precede the group it names.
209fn scan_groups(chars: &[char], d: Dialect, verbose: bool) -> (u32, bool) {
210    let (mut count, mut named) = (0u32, false);
211    let (mut i, mut in_class, mut class_start) = (0usize, false, 0usize);
212    while i < chars.len() {
213        let c = chars[i];
214        if c == '\\' {
215            i += 2;
216            continue;
217        }
218        if in_class {
219            // ECMAScript closes at the first ']'; Python treats a leading ']' as literal.
220            if c == ']' && !(d == Dialect::Python && i == class_start) {
221                in_class = false;
222            }
223        } else if c == '[' {
224            in_class = true;
225            class_start = i + 1;
226            if chars.get(class_start) == Some(&'^') {
227                class_start += 1;
228            }
229        } else if c == '#' && verbose && d == Dialect::Python {
230            while i < chars.len() && chars[i] != '\n' {
231                i += 1;
232            }
233            continue;
234        } else if c == '(' {
235            if chars.get(i + 1) != Some(&'?') {
236                count += 1;
237            } else {
238                let is_named = match d {
239                    Dialect::Python => {
240                        chars.get(i + 2) == Some(&'P') && chars.get(i + 3) == Some(&'<')
241                    }
242                    _ => {
243                        chars.get(i + 2) == Some(&'<')
244                            && !matches!(chars.get(i + 3), Some('=') | Some('!'))
245                    }
246                };
247                if is_named {
248                    count += 1;
249                    named = true;
250                }
251            }
252        }
253        i += 1;
254    }
255    (count, named)
256}
257
258enum Esc {
259    Char(u32),
260    Short(Shorthand),
261    Prop(PropertyRef),
262    Assert(Assertion),
263}
264
265enum ClassAtom {
266    Ch(u32),
267    Sh(Shorthand),
268    Prop(PropertyRef),
269}
270
271impl ClassAtom {
272    fn item(self) -> ClassItem {
273        match self {
274            ClassAtom::Ch(c) => ClassItem::Range(c, c),
275            ClassAtom::Sh(s) => ClassItem::Shorthand(s),
276            ClassAtom::Prop(p) => ClassItem::Property(p),
277        }
278    }
279}
280
281struct Parser<'a> {
282    chars: &'a [char],
283    pos: usize,
284    d: Dialect,
285    flags: Flags,
286    max_depth: u32,
287    depth: u32,
288    total_groups: u32,
289    has_named: bool,
290    next_capture: u32,
291    names: Vec<String>,
292    /// True once any atom or `|` was seen; Python global flags must come first.
293    seen_content: bool,
294}
295
296fn mk(kind: NodeKind, start: usize, end: usize) -> Node {
297    Node {
298        kind,
299        span: Span { start, end },
300    }
301}
302
303fn shorthand(c: char) -> Option<Shorthand> {
304    Some(match c {
305        'd' => Shorthand::Digit,
306        'D' => Shorthand::NotDigit,
307        'w' => Shorthand::Word,
308        'W' => Shorthand::NotWord,
309        's' => Shorthand::Space,
310        'S' => Shorthand::NotSpace,
311        _ => return None,
312    })
313}
314
315impl Parser<'_> {
316    fn peek(&self) -> Option<char> {
317        self.chars.get(self.pos).copied()
318    }
319    fn peek_at(&self, n: usize) -> Option<char> {
320        self.chars.get(self.pos + n).copied()
321    }
322    fn eat(&mut self, c: char) -> bool {
323        if self.peek() == Some(c) {
324            self.pos += 1;
325            true
326        } else {
327            false
328        }
329    }
330    fn is_py(&self) -> bool {
331        self.d == Dialect::Python
332    }
333    /// ECMAScript Unicode mode.
334    fn es_unicode(&self) -> bool {
335        self.d == Dialect::Ecma && self.flags.u
336    }
337
338    /// Verbose mode (Python `x`): skip whitespace and `#` comments.
339    fn skip_x(&mut self) {
340        if !(self.is_py() && self.flags.x) {
341            return;
342        }
343        while let Some(c) = self.peek() {
344            if matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0b' | '\x0c') {
345                self.pos += 1;
346            } else if c == '#' {
347                while let Some(c) = self.peek() {
348                    if c == '\n' {
349                        break;
350                    }
351                    self.pos += 1;
352                }
353            } else {
354                break;
355            }
356        }
357    }
358
359    fn alt(&mut self) -> R<Node> {
360        self.depth += 1;
361        if self.depth > self.max_depth {
362            return err(
363                ErrorCode::L006,
364                self.pos,
365                "group nesting exceeds the depth limit",
366            );
367        }
368        let start = self.pos;
369        let mut branches = vec![self.concat()?];
370        while self.eat('|') {
371            self.seen_content = true;
372            branches.push(self.concat()?);
373        }
374        self.depth -= 1;
375        Ok(match branches.len() {
376            1 => branches.remove(0),
377            _ => mk(NodeKind::Alt(branches), start, self.pos),
378        })
379    }
380
381    fn concat(&mut self) -> R<Node> {
382        let start = self.pos;
383        let mut items = Vec::new();
384        loop {
385            self.skip_x();
386            if matches!(self.peek(), None | Some('|') | Some(')')) {
387                break;
388            }
389            if let Some(n) = self.term()? {
390                self.seen_content = true;
391                items.push(n);
392            }
393        }
394        Ok(match items.len() {
395            0 => mk(NodeKind::Empty, start, self.pos),
396            1 => items.remove(0),
397            _ => mk(NodeKind::Concat(items), start, self.pos),
398        })
399    }
400
401    fn term(&mut self) -> R<Option<Node>> {
402        let start = self.pos;
403        let (atom, quantifiable) = self.atom()?;
404        self.skip_x();
405        if !quantifiable || atom.is_none() {
406            if self.quantifier_ahead()? {
407                return err(ErrorCode::P005, self.pos, "nothing to repeat");
408            }
409            return Ok(atom);
410        }
411        let Some(atom) = atom else { return Ok(None) };
412        let Some((min, max)) = self.take_quantifier()? else {
413            return Ok(Some(atom));
414        };
415        let mut greedy = true;
416        if self.eat('?') {
417            greedy = false;
418        } else if self.is_py() && self.peek() == Some('+') {
419            return err(
420                ErrorCode::U004,
421                self.pos,
422                "possessive quantifier changes matching semantics",
423            );
424        }
425        if self.quantifier_ahead()? {
426            return err(ErrorCode::P005, self.pos, "multiple repeat");
427        }
428        Ok(Some(mk(
429            NodeKind::Repeat {
430                node: Box::new(atom),
431                min,
432                max,
433                greedy,
434            },
435            start,
436            self.pos,
437        )))
438    }
439
440    fn quantifier_ahead(&self) -> R<bool> {
441        Ok(match self.peek() {
442            Some('*') | Some('+') | Some('?') => true,
443            Some('{') => self.braces()?.is_some(),
444            _ => false,
445        })
446    }
447
448    fn digits(&self, mut i: usize) -> (Option<u64>, usize) {
449        let mut v: Option<u64> = None;
450        while let Some(d) = self.chars.get(i).and_then(|c| c.to_digit(10)) {
451            v = Some(
452                v.unwrap_or(0)
453                    .saturating_mul(10)
454                    .saturating_add(u64::from(d)),
455            );
456            i += 1;
457        }
458        (v, i)
459    }
460
461    /// Recognise `{n}`, `{n,}`, `{n,m}` (Python also `{,m}`, `{,}`) at `pos`
462    /// without consuming. Anything else is a literal brace.
463    fn braces(&self) -> R<Option<(u64, Option<u64>, usize)>> {
464        let (lo, i) = self.digits(self.pos + 1);
465        match self.chars.get(i) {
466            Some('}') => Ok(lo.map(|l| (l, Some(l), i + 1))),
467            Some(',') => {
468                let (hi, j) = self.digits(i + 1);
469                if self.chars.get(j) != Some(&'}') {
470                    return Ok(None);
471                }
472                Ok(match (lo, hi) {
473                    (None, _) if !self.is_py() => None,
474                    (l, h) => Some((l.unwrap_or(0), h, j + 1)),
475                })
476            }
477            _ => Ok(None),
478        }
479    }
480
481    fn take_quantifier(&mut self) -> R<Option<(u64, Option<u64>)>> {
482        let start = self.pos;
483        let q = match self.peek() {
484            Some('*') => {
485                self.pos += 1;
486                (0, None)
487            }
488            Some('+') => {
489                self.pos += 1;
490                (1, None)
491            }
492            Some('?') => {
493                self.pos += 1;
494                (0, Some(1))
495            }
496            Some('{') => match self.braces()? {
497                Some((lo, hi, end)) => {
498                    self.pos = end;
499                    if self.is_py()
500                        && (lo >= u64::from(u32::MAX)
501                            || hi.is_some_and(|h| h >= u64::from(u32::MAX)))
502                    {
503                        return err(ErrorCode::P005, start, "repetition count out of range");
504                    }
505                    if hi.is_some_and(|h| lo > h) {
506                        return err(
507                            ErrorCode::P005,
508                            start,
509                            "minimum repeat is greater than maximum",
510                        );
511                    }
512                    (lo, hi)
513                }
514                None => return Ok(None),
515            },
516            _ => return Ok(None),
517        };
518        Ok(Some(q))
519    }
520
521    /// Returns the node (None when the construct vanishes, like a comment or
522    /// global flags) and whether a quantifier may follow it.
523    fn atom(&mut self) -> R<(Option<Node>, bool)> {
524        let start = self.pos;
525        let Some(c) = self.peek() else {
526            return Ok((None, false));
527        };
528        match c {
529            '(' => self.group(),
530            '[' => Ok((Some(self.class()?), true)),
531            '.' => {
532                self.pos += 1;
533                Ok((Some(mk(NodeKind::Any, start, self.pos)), true))
534            }
535            '^' | '$' => {
536                self.pos += 1;
537                let a = if c == '^' {
538                    Assertion::LineStart
539                } else {
540                    Assertion::LineEnd
541                };
542                Ok((Some(mk(NodeKind::Assert(a), start, self.pos)), false))
543            }
544            '\\' => {
545                let esc = self.escape(false)?;
546                let (kind, q) = match esc {
547                    Esc::Char(c) => (NodeKind::Literal(c), true),
548                    Esc::Short(s) => (
549                        NodeKind::Class(Class {
550                            negated: false,
551                            items: vec![ClassItem::Shorthand(s)],
552                        }),
553                        true,
554                    ),
555                    Esc::Prop(p) => (
556                        NodeKind::Class(Class {
557                            negated: false,
558                            items: vec![ClassItem::Property(p)],
559                        }),
560                        true,
561                    ),
562                    Esc::Assert(a) => (NodeKind::Assert(a), false),
563                };
564                Ok((Some(mk(kind, start, self.pos)), q))
565            }
566            '*' | '+' | '?' => err(ErrorCode::P005, start, "nothing to repeat"),
567            '{' => {
568                if self.braces()?.is_some() {
569                    return err(ErrorCode::P005, start, "nothing to repeat");
570                }
571                if self.es_unicode() {
572                    return err(ErrorCode::P001, start, "lone '{' in Unicode mode");
573                }
574                self.pos += 1;
575                Ok((
576                    Some(mk(NodeKind::Literal('{' as u32), start, self.pos)),
577                    true,
578                ))
579            }
580            '}' | ']' if self.es_unicode() => err(
581                ErrorCode::P001,
582                start,
583                "lone bracket in Unicode mode; escape it",
584            ),
585            _ => {
586                self.pos += 1;
587                Ok((Some(mk(NodeKind::Literal(c as u32), start, self.pos)), true))
588            }
589        }
590    }
591
592    // ---------------------------------------------------------------- groups
593
594    fn group_body(&mut self, open: usize) -> R<Node> {
595        let n = self.alt()?;
596        if !self.eat(')') {
597            return err(ErrorCode::P007, open, "unclosed group");
598        }
599        Ok(n)
600    }
601
602    fn group(&mut self) -> R<(Option<Node>, bool)> {
603        let open = self.pos;
604        self.pos += 1;
605        if self.eat('?') {
606            return self.special_group(open);
607        }
608        let idx = self.next_capture;
609        self.next_capture += 1;
610        let body = self.group_body(open)?;
611        let kind = NodeKind::Group {
612            capture: Some(idx),
613            name: None,
614            node: Box::new(body),
615        };
616        Ok((Some(mk(kind, open, self.pos)), true))
617    }
618
619    fn plain_group(
620        &mut self,
621        open: usize,
622        capture: Option<u32>,
623        name: Option<String>,
624    ) -> R<(Option<Node>, bool)> {
625        let body = self.group_body(open)?;
626        let kind = NodeKind::Group {
627            capture,
628            name,
629            node: Box::new(body),
630        };
631        Ok((Some(mk(kind, open, self.pos)), true))
632    }
633
634    fn look(&mut self, open: usize, ahead: bool, negate: bool) -> R<(Option<Node>, bool)> {
635        let body = self.group_body(open)?;
636        let kind = NodeKind::Look {
637            ahead,
638            negate,
639            node: Box::new(body),
640        };
641        // ECMAScript: lookbehind never, lookahead only outside Unicode mode, is quantifiable.
642        let q = !(self.d == Dialect::Ecma && (!ahead || self.flags.u));
643        Ok((Some(mk(kind, open, self.pos)), q))
644    }
645
646    fn named(&mut self, open: usize) -> R<(Option<Node>, bool)> {
647        let name_start = self.pos;
648        let mut name = String::new();
649        loop {
650            match self.peek() {
651                Some('>') => break,
652                Some(c) => {
653                    name.push(c);
654                    self.pos += 1;
655                }
656                None => return err(ErrorCode::P009, name_start, "unterminated group name"),
657            }
658        }
659        let ok_first = |c: char| c.is_alphabetic() || c == '_' || (!self.is_py() && c == '$');
660        let ok_rest = |c: char| c.is_alphanumeric() || c == '_' || (!self.is_py() && c == '$');
661        let valid = name.chars().next().is_some_and(ok_first) && name.chars().all(ok_rest);
662        if !valid {
663            return err(ErrorCode::P009, name_start, "invalid group name");
664        }
665        if self.names.contains(&name) {
666            return err(ErrorCode::P009, name_start, "duplicate group name");
667        }
668        self.pos += 1; // '>'
669        let idx = self.next_capture;
670        self.next_capture += 1;
671        self.names.push(name.clone());
672        self.plain_group(open, Some(idx), Some(name))
673    }
674
675    fn special_group(&mut self, open: usize) -> R<(Option<Node>, bool)> {
676        let Some(c) = self.peek() else {
677            return err(ErrorCode::P007, open, "unclosed group");
678        };
679        self.pos += 1;
680        match (self.d, c) {
681            (_, ':') => self.plain_group(open, None, None),
682            (_, '=') => self.look(open, true, false),
683            (_, '!') => self.look(open, true, true),
684            (_, '<') => match self.peek() {
685                Some('=') => {
686                    self.pos += 1;
687                    self.look(open, false, false)
688                }
689                Some('!') => {
690                    self.pos += 1;
691                    self.look(open, false, true)
692                }
693                _ if self.d == Dialect::Ecma => self.named(open),
694                _ => err(
695                    ErrorCode::P001,
696                    open,
697                    "unknown group extension '(?<'; Python names use (?P<name>...)",
698                ),
699            },
700            (Dialect::Python, 'P') => match self.peek() {
701                Some('<') => {
702                    self.pos += 1;
703                    self.named(open)
704                }
705                Some('=') => err(ErrorCode::U001, open, "named backreference (?P=name)"),
706                _ => err(ErrorCode::P001, open, "unknown group extension '(?P'"),
707            },
708            (Dialect::Python, '#') => {
709                while let Some(c) = self.peek() {
710                    self.pos += 1;
711                    if c == ')' {
712                        return Ok((None, false));
713                    }
714                }
715                err(ErrorCode::P007, open, "unterminated comment")
716            }
717            (Dialect::Python, '>') => err(
718                ErrorCode::U004,
719                open,
720                "atomic group changes matching semantics",
721            ),
722            (Dialect::Python, '(') => err(ErrorCode::U003, open, "conditional group"),
723            (Dialect::Python, _) => {
724                self.pos -= 1;
725                self.py_flags(open)
726            }
727            _ => err(
728                ErrorCode::P001,
729                open,
730                "unrecognised group syntax after '(?'",
731            ),
732        }
733    }
734
735    /// Python `(?aimsux-imsx)` and `(?aimsux-imsx:...)`.
736    fn py_flags(&mut self, open: usize) -> R<(Option<Node>, bool)> {
737        let (mut on, mut off) = (Flags::none(), Flags::none());
738        let mut turning_off = false;
739        let mut any = false;
740        loop {
741            let Some(c) = self.peek() else {
742                return err(ErrorCode::P007, open, "unclosed group");
743            };
744            match c {
745                '-' if !turning_off => {
746                    turning_off = true;
747                    any = false;
748                    self.pos += 1;
749                    continue;
750                }
751                'a' | 'i' | 'm' | 's' | 'u' | 'x' => {
752                    if turning_off && matches!(c, 'a' | 'u') {
753                        return err(
754                            ErrorCode::P003,
755                            self.pos,
756                            "flags 'a' and 'u' cannot be turned off",
757                        );
758                    }
759                    let target = if turning_off { &mut off } else { &mut on };
760                    match c {
761                        'a' => target.a = true,
762                        'i' => target.i = true,
763                        'm' => target.m = true,
764                        's' => target.s = true,
765                        'u' => target.u = true,
766                        _ => target.x = true,
767                    }
768                }
769                'L' => {
770                    return err(
771                        ErrorCode::P003,
772                        self.pos,
773                        "flag 'L' cannot be used with str patterns",
774                    )
775                }
776                ')' | ':' => break,
777                _ => return err(ErrorCode::P001, open, "unknown flag in inline group"),
778            }
779            any = true;
780            self.pos += 1;
781        }
782        if turning_off && !any {
783            return err(ErrorCode::P001, self.pos, "missing flag after '-'");
784        }
785        if !turning_off && !any {
786            return err(ErrorCode::P001, open, "missing inline flags");
787        }
788        if on.a && on.u {
789            return err(ErrorCode::P003, open, "flags 'a' and 'u' are incompatible");
790        }
791        if self.eat(')') {
792            if turning_off {
793                return err(ErrorCode::P001, open, "global flags cannot be turned off");
794            }
795            if self.seen_content || self.depth > 1 {
796                return err(
797                    ErrorCode::P001,
798                    open,
799                    "global flags not at the start of the expression",
800                );
801            }
802            self.flags = self.flags.union(on);
803            return Ok((None, false));
804        }
805        self.pos += 1; // ':'
806        let saved = self.flags;
807        self.flags = saved.union(on).minus(off);
808        let body = self.group_body(open);
809        self.flags = saved;
810        let kind = NodeKind::FlagGroup {
811            delta: FlagDelta { on, off },
812            node: Box::new(body?),
813        };
814        Ok((Some(mk(kind, open, self.pos)), true))
815    }
816
817    // --------------------------------------------------------------- classes
818
819    fn class_atom(&mut self) -> R<ClassAtom> {
820        match self.peek() {
821            Some('\\') => Ok(match self.escape(true)? {
822                Esc::Char(c) => ClassAtom::Ch(c),
823                Esc::Short(s) => ClassAtom::Sh(s),
824                Esc::Prop(p) => ClassAtom::Prop(p),
825                // `escape(true)` never returns assertions; keep the match total anyway.
826                Esc::Assert(_) => return err(ErrorCode::X001, self.pos, "assertion inside class"),
827            }),
828            Some(c) => {
829                self.pos += 1;
830                Ok(ClassAtom::Ch(c as u32))
831            }
832            None => err(ErrorCode::P007, self.pos, "unterminated character class"),
833        }
834    }
835
836    fn class(&mut self) -> R<Node> {
837        let open = self.pos;
838        self.pos += 1;
839        let negated = self.eat('^');
840        let mut items = Vec::new();
841        let mut first = true;
842        loop {
843            let Some(c) = self.peek() else {
844                return err(ErrorCode::P007, open, "unterminated character class");
845            };
846            if c == ']' && !(first && self.is_py()) {
847                self.pos += 1;
848                break;
849            }
850            first = false;
851            let a = self.class_atom()?;
852            if self.peek() == Some('-') && self.peek_at(1).is_some_and(|n| n != ']') {
853                let dash = self.pos;
854                self.pos += 1;
855                let b = self.class_atom()?;
856                match (a, b) {
857                    (ClassAtom::Ch(x), ClassAtom::Ch(y)) => {
858                        if x > y {
859                            return err(ErrorCode::P006, dash, "range end precedes range start");
860                        }
861                        items.push(ClassItem::Range(x, y));
862                    }
863                    (a, b) => {
864                        // Annex B: outside Unicode mode ECMAScript reads `a-\d` as three members.
865                        if self.d != Dialect::Ecma || self.flags.u {
866                            return err(
867                                ErrorCode::P006,
868                                dash,
869                                "a class escape cannot be a range endpoint",
870                            );
871                        }
872                        for x in [a, ClassAtom::Ch('-' as u32), b] {
873                            items.push(x.item());
874                        }
875                    }
876                }
877            } else {
878                items.push(a.item());
879            }
880        }
881        Ok(mk(
882            NodeKind::Class(Class { negated, items }),
883            open,
884            self.pos,
885        ))
886    }
887
888    // --------------------------------------------------------------- escapes
889
890    fn hex(&mut self, n: usize) -> Option<u32> {
891        let mut v = 0u32;
892        for k in 0..n {
893            v = v * 16 + self.peek_at(k)?.to_digit(16)?;
894        }
895        self.pos += n;
896        Some(v)
897    }
898
899    fn octal(&mut self, first: u32, max_extra: usize) -> u32 {
900        let mut v = first;
901        for _ in 0..max_extra {
902            match self.peek().and_then(|c| c.to_digit(8)) {
903                Some(d) => {
904                    v = v * 8 + d;
905                    self.pos += 1;
906                }
907                None => break,
908            }
909        }
910        v
911    }
912
913    fn escape(&mut self, in_class: bool) -> R<Esc> {
914        let bs = self.pos;
915        self.pos += 1;
916        let Some(c) = self.peek() else {
917            return err(ErrorCode::P004, bs, "pattern ends with a backslash");
918        };
919        self.pos += 1;
920        if let Some(s) = shorthand(c) {
921            return Ok(Esc::Short(s));
922        }
923        match self.d {
924            Dialect::Ecma => self.esc_ecma(c, bs, in_class),
925            _ => self.esc_py(c, bs, in_class),
926        }
927    }
928
929    fn esc_ecma(&mut self, c: char, bs: usize, in_class: bool) -> R<Esc> {
930        let u = self.flags.u;
931        Ok(Esc::Char(match c {
932            'b' if in_class => 8,
933            'b' => return Ok(Esc::Assert(Assertion::WordBoundary)),
934            'B' if in_class && u => {
935                return err(ErrorCode::P004, bs, "\\B is not allowed in a class")
936            }
937            'B' if in_class => 'B' as u32,
938            'B' => return Ok(Esc::Assert(Assertion::NotWordBoundary)),
939            'f' => 12,
940            'n' => 10,
941            'r' => 13,
942            't' => 9,
943            'v' => 11,
944            '0' if !self.peek().is_some_and(|n| n.is_ascii_digit()) => 0,
945            '0'..='9' => return self.es_digits(c, bs, in_class),
946            'k' => {
947                if in_class && !u {
948                    'k' as u32
949                } else if u || self.has_named {
950                    if self.peek() == Some('<') && !in_class {
951                        return err(ErrorCode::U001, bs, "named backreference \\k<name>");
952                    }
953                    return err(ErrorCode::P004, bs, "invalid named reference");
954                } else {
955                    'k' as u32
956                }
957            }
958            'c' => match self.peek() {
959                Some(l) if l.is_ascii_alphabetic() => {
960                    self.pos += 1;
961                    l as u32 % 32
962                }
963                _ if u => return err(ErrorCode::P004, bs, "invalid control escape"),
964                _ => {
965                    // Annex B: a lone `\c` is a literal backslash; `c` is read again.
966                    self.pos -= 1;
967                    '\\' as u32
968                }
969            },
970            'x' => match self.hex(2) {
971                Some(v) => v,
972                None if u => return err(ErrorCode::P004, bs, "\\x needs two hex digits"),
973                None => 'x' as u32,
974            },
975            'u' => return self.es_unicode_escape(bs),
976            'p' | 'P' if u => return self.es_property(c == 'P', bs),
977            '-' if in_class && u => '-' as u32,
978            _ if u => {
979                if "^$\\.*+?()[]{}|/".contains(c) {
980                    c as u32
981                } else {
982                    return err(
983                        ErrorCode::P004,
984                        bs,
985                        "invalid identity escape in Unicode mode",
986                    );
987                }
988            }
989            _ => c as u32,
990        }))
991    }
992
993    /// ECMAScript `\p{Name}` / `\p{Name=Value}` (Unicode mode only): exact,
994    /// case-sensitive names from the Unicode property alias files.
995    fn es_property(&mut self, negated: bool, bs: usize) -> R<Esc> {
996        if self.peek() != Some('{') {
997            return err(
998                ErrorCode::P004,
999                bs,
1000                "\\p must be followed by {Name} in Unicode mode",
1001            );
1002        }
1003        let start = self.pos + 1;
1004        let mut i = start;
1005        while self
1006            .chars
1007            .get(i)
1008            .is_some_and(|c| c.is_ascii_alphanumeric() || *c == '_' || *c == '=')
1009        {
1010            i += 1;
1011        }
1012        if self.chars.get(i) != Some(&'}') {
1013            return err(ErrorCode::P004, bs, "malformed \\p{...} escape");
1014        }
1015        let body: String = self.chars[start..i].iter().collect();
1016        self.pos = i + 1;
1017        let unknown = |what: &str| {
1018            let hint = uni::suggest(what);
1019            let tail = if hint.is_empty() {
1020                String::new()
1021            } else {
1022                format!("; did you mean: {}", hint.join(", "))
1023            };
1024            PErr::new(
1025                ErrorCode::P008,
1026                bs,
1027                &format!("unknown Unicode property or value '{what}'{tail}"),
1028            )
1029        };
1030        let (kind, name) = match body.split_once('=') {
1031            None => match uni::gc_canonical(&body) {
1032                Some(n) => (PropKind::GeneralCategory, n),
1033                None => match uni::binary_canonical(&body) {
1034                    Some(n) => (PropKind::Binary, n),
1035                    None => return Err(unknown(&body)),
1036                },
1037            },
1038            Some(("General_Category" | "gc", v)) => (
1039                PropKind::GeneralCategory,
1040                uni::gc_canonical(v).ok_or_else(|| unknown(v))?,
1041            ),
1042            Some(("Script" | "sc", v)) => (
1043                PropKind::Script,
1044                uni::script_canonical(v).ok_or_else(|| unknown(v))?,
1045            ),
1046            Some(("Script_Extensions" | "scx", v)) => (
1047                PropKind::ScriptExtensions,
1048                uni::script_canonical(v).ok_or_else(|| unknown(v))?,
1049            ),
1050            Some((n, _)) => return Err(unknown(n)),
1051        };
1052        Ok(Esc::Prop(PropertyRef {
1053            negated,
1054            kind,
1055            name,
1056            exact: false,
1057        }))
1058    }
1059
1060    fn es_digits(&mut self, c: char, bs: usize, in_class: bool) -> R<Esc> {
1061        let u = self.flags.u;
1062        let first = c.to_digit(10).unwrap_or(0);
1063        if u {
1064            if c == '0' || in_class {
1065                return err(
1066                    ErrorCode::P004,
1067                    bs,
1068                    "invalid decimal escape in Unicode mode",
1069                );
1070            }
1071            let (n, _) = self.digits(self.pos - 1);
1072            return if n.is_some_and(|n| n <= u64::from(self.total_groups)) {
1073                err(ErrorCode::U001, bs, "backreference")
1074            } else {
1075                err(
1076                    ErrorCode::P004,
1077                    bs,
1078                    "reference to a group that does not exist",
1079                )
1080            };
1081        }
1082        if !in_class && c != '0' {
1083            let (n, end) = self.digits(self.pos - 1);
1084            if n.is_some_and(|n| n <= u64::from(self.total_groups)) {
1085                self.pos = end;
1086                return err(ErrorCode::U001, bs, "backreference");
1087            }
1088        }
1089        if first >= 8 {
1090            return Ok(Esc::Char(c as u32));
1091        }
1092        let extra = if first <= 3 { 2 } else { 1 };
1093        Ok(Esc::Char(self.octal(first, extra)))
1094    }
1095
1096    fn es_unicode_escape(&mut self, bs: usize) -> R<Esc> {
1097        let u = self.flags.u;
1098        if u && self.peek() == Some('{') {
1099            let mut i = self.pos + 1;
1100            let mut v: u32 = 0;
1101            let mut n = 0;
1102            while let Some(d) = self.chars.get(i).and_then(|c| c.to_digit(16)) {
1103                v = v.saturating_mul(16).saturating_add(d);
1104                i += 1;
1105                n += 1;
1106            }
1107            if n == 0 || self.chars.get(i) != Some(&'}') || v > 0x10FFFF {
1108                return err(ErrorCode::P004, bs, "invalid \\u{...} escape");
1109            }
1110            self.pos = i + 1;
1111            return Ok(Esc::Char(v));
1112        }
1113        match self.hex(4) {
1114            Some(hi) if u && (0xD800..0xDC00).contains(&hi) => {
1115                // A 😀 pair denotes one code point in Unicode mode.
1116                if self.peek() == Some('\\') && self.peek_at(1) == Some('u') {
1117                    let save = self.pos;
1118                    self.pos += 2;
1119                    if let Some(lo) = self.hex(4) {
1120                        if (0xDC00..0xE000).contains(&lo) {
1121                            return Ok(Esc::Char(0x10000 + ((hi - 0xD800) << 10) + (lo - 0xDC00)));
1122                        }
1123                    }
1124                    self.pos = save;
1125                }
1126                Ok(Esc::Char(hi))
1127            }
1128            Some(v) => Ok(Esc::Char(v)),
1129            None if u => err(ErrorCode::P004, bs, "\\u needs four hex digits"),
1130            None => Ok(Esc::Char('u' as u32)),
1131        }
1132    }
1133
1134    fn esc_py(&mut self, c: char, bs: usize, in_class: bool) -> R<Esc> {
1135        Ok(Esc::Char(match c {
1136            'b' if in_class => 8,
1137            'b' => return Ok(Esc::Assert(Assertion::WordBoundary)),
1138            'B' | 'A' | 'Z' if in_class => {
1139                return err(ErrorCode::P004, bs, "assertion escape inside a class")
1140            }
1141            'B' => return Ok(Esc::Assert(Assertion::NotWordBoundary)),
1142            'A' => return Ok(Esc::Assert(Assertion::TextStart)),
1143            'Z' => return Ok(Esc::Assert(Assertion::TextEnd)),
1144            'a' => 7,
1145            'f' => 12,
1146            'n' => 10,
1147            'r' => 13,
1148            't' => 9,
1149            'v' => 11,
1150            'x' => self
1151                .hex(2)
1152                .ok_or_else(|| PErr::new(ErrorCode::P004, bs, "\\x needs two hex digits"))?,
1153            'u' => self
1154                .hex(4)
1155                .ok_or_else(|| PErr::new(ErrorCode::P004, bs, "\\u needs four hex digits"))?,
1156            'U' => match self.hex(8) {
1157                Some(v) if v <= 0x10FFFF => v,
1158                _ => {
1159                    return err(
1160                        ErrorCode::P004,
1161                        bs,
1162                        "\\U needs eight hex digits up to 10FFFF",
1163                    )
1164                }
1165            },
1166            'N' => {
1167                return err(
1168                    ErrorCode::U007,
1169                    bs,
1170                    "\\N{name} escapes are not yet supported",
1171                )
1172            }
1173            '0' => self.octal(0, 2),
1174            '1'..='9' => return self.py_digits(c, bs, in_class),
1175            _ if c.is_ascii_alphabetic() => {
1176                return err(ErrorCode::P004, bs, "unknown escape of an ASCII letter")
1177            }
1178            _ => c as u32,
1179        }))
1180    }
1181
1182    fn py_digits(&mut self, c: char, bs: usize, in_class: bool) -> R<Esc> {
1183        let first = c.to_digit(10).unwrap_or(0);
1184        let is_octal3 = first < 8
1185            && self.peek().is_some_and(|d| d.is_digit(8))
1186            && self.peek_at(1).is_some_and(|d| d.is_digit(8));
1187        if in_class || is_octal3 {
1188            if first >= 8 {
1189                return err(ErrorCode::P004, bs, "bad escape");
1190            }
1191            let v = self.octal(first, 2);
1192            if v > 0o377 {
1193                return err(ErrorCode::P004, bs, "octal escape value outside 0-0o377");
1194            }
1195            return Ok(Esc::Char(v));
1196        }
1197        // Python reads at most two digits for a group reference.
1198        let second = self.peek().and_then(|d| d.to_digit(10));
1199        let n = u64::from(first) * if second.is_some() { 10 } else { 1 }
1200            + u64::from(second.unwrap_or(0));
1201        let end = self.pos + usize::from(second.is_some());
1202        if n <= u64::from(self.total_groups) {
1203            self.pos = end;
1204            err(ErrorCode::U001, bs, "backreference")
1205        } else {
1206            err(
1207                ErrorCode::P004,
1208                bs,
1209                "reference to a group that does not exist",
1210            )
1211        }
1212    }
1213}