//! Parser from regex patterns to IR use crate::{ api, charclasses, codepointset::{CodePointSet, Interval, interval_contains}, ir, types::{ BracketContents, CaptureGroupID, CaptureGroupName, CharacterClassType, MAX_CAPTURE_GROUPS, MAX_LOOPS, }, unicode::{ self, PropertyEscapeKind, unicode_property_from_str, unicode_property_name_from_str, }, unicodetables::{id_continue_ranges, id_start_ranges}, util::to_char_sat, }; use core::{fmt, iter::Peekable}; #[cfg(feature = "std")] use std::collections::HashMap; #[cfg(not(feature = "std"))] use { alloc::{ boxed::Box, string::{String, ToString}, vec::Vec, }, hashbrown::HashMap, }; /// Represents an error encountered during regex compilation. /// /// The text contains a human-readable error message. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Error { pub text: String, } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str(&self.text) } } #[cfg(feature = "std")] impl std::error::Error for Error {} enum ClassAtom { CodePoint(u32), CharacterClass { class_type: CharacterClassType, positive: bool, }, Range { iv: CodePointSet, negate: bool, }, } /// Represents the result of a class set. #[derive(Debug, Clone)] struct ClassSet { codepoints: CodePointSet, alternatives: ClassSetAlternativeStrings, } impl ClassSet { fn new() -> Self { ClassSet { codepoints: CodePointSet::new(), alternatives: ClassSetAlternativeStrings::new(), } } fn node(self, icase: bool, negate_set: bool) -> ir::Node { let codepoints = if icase { unicode::add_icase_code_points(self.codepoints) } else { self.codepoints }; if codepoints.is_empty() && self.alternatives.0.is_empty() { ir::Node::Bracket(BracketContents { invert: negate_set, cps: codepoints, }) } else if codepoints.is_empty() { make_alt(self.alternatives.to_nodes(icase)) } else if self.alternatives.0.is_empty() { ir::Node::Bracket(BracketContents { invert: negate_set, cps: codepoints, }) } else { let mut nodes = self.alternatives.to_nodes(icase); nodes.push(ir::Node::Bracket(BracketContents { invert: negate_set, cps: codepoints, })); make_alt(nodes) } } fn union_operand(&mut self, operand: ClassSetOperand) { match operand { ClassSetOperand::ClassSetCharacter(c) => { self.codepoints.add_one(c); } ClassSetOperand::CharacterClassEscape(cps) => { self.codepoints.add_set(cps); } ClassSetOperand::Class(class) => { self.codepoints.add_set(class.codepoints); self.alternatives.extend(class.alternatives); } ClassSetOperand::ClassStringDisjunction(s) => { self.alternatives.extend(s); } } } fn intersect_operand(&mut self, operand: ClassSetOperand) { match operand { ClassSetOperand::ClassSetCharacter(c) => { if self.codepoints.contains(c) { self.codepoints = CodePointSet::from_sorted_disjoint_intervals(Vec::from([Interval { first: c, last: c, }])); } else { self.codepoints.clear(); } let mut found_alternative = false; for alternative in &mut self.alternatives.0 { if alternative.len() == 1 && alternative[0] == c { found_alternative = true; break; } } self.alternatives.0.clear(); if found_alternative { self.alternatives.0.push(Vec::from([c])); } } ClassSetOperand::CharacterClassEscape(cps) => { self.codepoints.intersect(cps.intervals()); let mut retained = Vec::new(); for alternative in &self.alternatives.0 { if alternative.len() == 1 && cps.contains(alternative[0]) { retained.push(alternative.clone()); } } self.alternatives.0 = retained; } ClassSetOperand::Class(class) => { let mut retained_codepoints = CodePointSet::new(); for alternative in &class.alternatives.0 { if alternative.len() == 1 && self.codepoints.contains(alternative[0]) { retained_codepoints.add_one(alternative[0]); } } let mut retained_alternatives = ClassSetAlternativeStrings::new(); for alternative in &self.alternatives.0 { if alternative.len() == 1 && class.codepoints.contains(alternative[0]) { retained_alternatives.0.push(alternative.clone()); } } self.codepoints.intersect(class.codepoints.intervals()); self.codepoints.add_set(retained_codepoints); self.alternatives.intersect(class.alternatives); self.alternatives.extend(retained_alternatives); } ClassSetOperand::ClassStringDisjunction(s) => { let mut retained = CodePointSet::new(); for alternative in &s.0 { if alternative.len() == 1 && self.codepoints.contains(alternative[0]) { retained.add_one(alternative[0]); } } self.codepoints = retained; self.alternatives.intersect(s); } } } fn subtract_operand(&mut self, operand: ClassSetOperand) { match operand { ClassSetOperand::ClassSetCharacter(c) => { self.codepoints.remove(&[Interval { first: c, last: c }]); self.alternatives .remove(ClassSetAlternativeStrings(Vec::from([Vec::from([c])]))); } ClassSetOperand::CharacterClassEscape(cps) => { self.codepoints.remove(cps.intervals()); let mut to_remove = ClassSetAlternativeStrings::new(); for alternative in &self.alternatives.0 { if alternative.len() == 1 && cps.contains(alternative[0]) { to_remove.0.push(alternative.clone()); } } self.alternatives.remove(to_remove); } ClassSetOperand::Class(class) => { let mut codepoints_removed = CodePointSet::new(); for alternative in &class.alternatives.0 { if alternative.len() == 1 && self.codepoints.contains(alternative[0]) { codepoints_removed.add_one(alternative[0]); } } let mut alternatives_removed = ClassSetAlternativeStrings::new(); for alternative in &self.alternatives.0 { if alternative.len() == 1 && class.codepoints.contains(alternative[0]) { alternatives_removed.0.push(alternative.clone()); } } self.codepoints.remove(codepoints_removed.intervals()); self.codepoints.remove(class.codepoints.intervals()); self.alternatives.remove(alternatives_removed); self.alternatives.remove(class.alternatives); } ClassSetOperand::ClassStringDisjunction(s) => { let mut to_remove = CodePointSet::new(); for alternative in &s.0 { if alternative.len() == 1 && self.codepoints.contains(alternative[0]) { to_remove.add_one(alternative[0]); } } self.codepoints.remove(to_remove.intervals()); self.alternatives.remove(s); } } } } /// Represents all different types of class set operands. #[derive(Debug, Clone)] enum ClassSetOperand { ClassSetCharacter(u32), CharacterClassEscape(CodePointSet), Class(ClassSet), ClassStringDisjunction(ClassSetAlternativeStrings), } #[derive(Debug, Clone)] struct ClassSetAlternativeStrings(Vec>); impl ClassSetAlternativeStrings { fn new() -> Self { ClassSetAlternativeStrings(Vec::new()) } fn extend(&mut self, other: Self) { self.0.extend(other.0); } fn intersect(&mut self, other: Self) { let mut retained = Vec::new(); for string in &self.0 { for other_string in &other.0 { if string == other_string { retained.push(string.clone()); break; } } } self.0 = retained; } fn remove(&mut self, other: Self) { self.0.retain(|string| !other.0.contains(string)); } fn to_nodes(&self, icase: bool) -> Vec { let mut nodes = Vec::new(); for string in &self.0 { nodes.push(ir::Node::Cat( string .iter() .map(|cp| ir::Node::Char { c: *cp, icase }) .collect::>(), )); } nodes } } fn error(text: S) -> Result where S: ToString, { Err(Error { text: text.to_string(), }) } fn make_cat(nodes: ir::NodeList) -> ir::Node { match nodes.len() { 0 => ir::Node::Empty, 1 => nodes.into_iter().next().unwrap(), _ => ir::Node::Cat(nodes), } } fn make_alt(nodes: ir::NodeList) -> ir::Node { let mut mright = None; for node in nodes.into_iter().rev() { match mright { None => mright = Some(node), Some(right) => mright = Some(ir::Node::Alt(Box::new(node), Box::new(right))), } } mright.unwrap_or(ir::Node::Empty) } /// \return a CodePointSet for a given character escape (positive or negative). /// See ES9 21.2.2.12. /// Returns the positive (non-inverted) code point set for a character class. fn codepoints_from_class_positive(ct: CharacterClassType) -> CodePointSet { let mut cps; match ct { CharacterClassType::Digits => { cps = CodePointSet::from_sorted_disjoint_intervals(charclasses::DIGITS.to_vec()) } CharacterClassType::Words => { cps = CodePointSet::from_sorted_disjoint_intervals(charclasses::WORD_CHARS.to_vec()) } CharacterClassType::Spaces => { cps = CodePointSet::from_sorted_disjoint_intervals(charclasses::WHITESPACE.to_vec()); for &iv in charclasses::LINE_TERMINATOR.iter() { cps.add(iv) } } } cps } /// Returns code points for a character class, optionally inverted. fn codepoints_from_class(ct: CharacterClassType, positive: bool) -> CodePointSet { let cps = codepoints_from_class_positive(ct); if positive { cps } else { cps.inverted() } } /// \return a Bracket for a given character escape (positive or negative). /// For icase mode, we expand the positive set first, then invert if needed. fn make_bracket_class(ct: CharacterClassType, positive: bool, icase: bool) -> ir::Node { // Get the positive (non-inverted) set, perform any icase expansion, then maybe invert. let mut cps = codepoints_from_class_positive(ct); if icase { cps = unicode::add_icase_code_points(cps); } if !positive { cps = cps.inverted(); } ir::Node::Bracket(BracketContents { invert: false, cps }) } fn add_class_atom(bc: &mut BracketContents, atom: ClassAtom) { match atom { ClassAtom::CodePoint(c) => bc.cps.add_one(c), ClassAtom::CharacterClass { class_type, positive, } => { bc.cps.add_set(codepoints_from_class(class_type, positive)); } ClassAtom::Range { iv, negate } => { if negate { bc.cps.add_set(iv.inverted()); } else { bc.cps.add_set(iv); } } } } struct LookaroundParams { negate: bool, backwards: bool, } /// Represents an alternative path in a regex pattern. /// For example, in `/(?x)|(?y)/`, the two occurrences of 'a' are in different /// alternative paths (separated by |), so they don't conflict. /// Each element in the vector is (depth, alternative_index) where: /// - depth: parenthesis nesting level (0 = top level) /// - alternative_index: which alternative at that depth (0 = first, 1 = second after |, etc.) #[derive(Debug, Clone, PartialEq, Eq)] struct AlternativePath { /// Vector of (depth, alternative_index) pairs representing the path through alternatives segments: Vec<(usize, usize)>, } impl AlternativePath { /// Check if two alternative paths conflict (i.e., are in the same alternative branch). /// Two paths conflict if they share the same alternative indices at all common depth levels. /// Example: /// - [(0, 0)] and [(0, 0), (1, 0)] conflict (second is nested within first) /// - [(0, 0)] and [(0, 1)] don't conflict (different alternatives at depth 0) fn conflicts_with(&self, other: &AlternativePath) -> bool { let min_len = self.segments.len().min(other.segments.len()); self.segments[..min_len] == other.segments[..min_len] } } /// Represents the state used to parse a regex. struct Parser where I: Iterator, { /// The remaining input. input: Peekable, /// Flags used. flags: api::Flags, /// Number of loops. loop_count: u32, /// Number of capturing groups. group_count: CaptureGroupID, /// Maximum number of capturing groups. group_count_max: u32, /// A map each from capture group name to corresponding group indices in order. /// Note duplicate names may appear in distinct alternations, per the TC39 proposal. /// See named_group_indices: HashMap>, /// Whether a lookbehind was encountered. has_lookbehind: bool, } impl Parser where I: Iterator + Clone, { /// Consume a character, returning it. fn consume>(&mut self, c: C) -> u32 { let nc = self.input.next(); core::debug_assert!(nc == Some(c.into()), "char was not next"); nc.unwrap() } /// If our contents begin with the char c, consume it from our contents /// and return true. Otherwise return false. fn try_consume>(&mut self, c: C) -> bool { self.input.next_if_eq(&c.into()).is_some() } /// If our contents begin with the string \p s, consume it from our contents /// and return true. Otherwise return false. fn try_consume_str(&mut self, s: &str) -> bool { let mut cursor = self.input.clone(); for c1 in s.chars() { if cursor.next() != Some(c1 as u32) { return false; } } self.input = cursor; true } /// Fold a character if icase. fn fold_if_icase(&self, c: u32) -> u32 { if self.flags.icase { unicode::fold_code_point(c, self.flags.unicode) } else { c } } /// Peek at the next character. fn peek(&mut self) -> Option { self.input.peek().copied() } /// \return the next character. fn next(&mut self) -> Option { self.input.next() } fn try_parse(&mut self) -> Result { self.parse_capture_groups()?; // Parse a catenation. If we consume everything, it's success. If there's // something left, it's an error (for example, an excess closing paren). let body = self.consume_disjunction()?; match self.input.peek().copied() { Some(c) if c == ')' as u32 => error("Unbalanced parenthesis"), Some(c) => error(format!( "Unexpected char: {}", char::from_u32(c) .map(String::from) .unwrap_or_else(|| format!("\\u{c:04X}")) )), None => self.finalize(ir::Regex { node: make_cat(vec![body, ir::Node::Goal]), flags: self.flags, }), } } /// ES6 21.2.2.3 Disjunction. fn consume_disjunction(&mut self) -> Result { let mut terms = vec![self.consume_term()?]; while self.try_consume('|') { terms.push(self.consume_term()?) } Ok(make_alt(terms)) } /// ES6 21.2.2.5 Term. fn consume_term(&mut self) -> Result { let mut result: Vec = Vec::new(); loop { let start_group = self.group_count; let mut start_offset = result.len(); let mut quantifier_allowed = true; let nc = self.peek(); if nc.is_none() { return Ok(make_cat(result)); } let c = nc.unwrap(); match to_char_sat(c) { // A concatenation is terminated by closing parens or vertical bar (alternations). ')' | '|' => break, // Term :: Assertion :: ^ '^' => { self.consume('^'); result.push(ir::Node::Anchor { anchor_type: ir::AnchorType::StartOfLine, multiline: self.flags.multiline, }); quantifier_allowed = false; } // Term :: Assertion :: $ '$' => { self.consume('$'); result.push(ir::Node::Anchor { anchor_type: ir::AnchorType::EndOfLine, multiline: self.flags.multiline, }); quantifier_allowed = false; } '\\' => { self.consume('\\'); let Some(c) = self.peek() else { return error("Incomplete escape"); }; match to_char_sat(c) { // Term :: Assertion :: \b 'b' => { self.consume('b'); result.push(ir::Node::WordBoundary { invert: false }); } // Term :: Assertion :: \B 'B' => { self.consume('B'); result.push(ir::Node::WordBoundary { invert: true }); } // Term :: Atom :: \ AtomEscape :: CharacterEscape :: c AsciiLetter // Term :: ExtendedAtom :: \ [lookahead = c] 'c' if !self.flags.unicode => { self.consume('c'); if self .peek() .and_then(char::from_u32) .map(|c| c.is_ascii_alphabetic()) == Some(true) { result.push(ir::Node::Char { c: self.next().expect("char was not next") % 32, icase: self.flags.icase, }); } else { start_offset += 1; result.push(ir::Node::Char { c: u32::from('\\'), icase: self.flags.icase, }); result.push(ir::Node::Char { c: u32::from('c'), icase: self.flags.icase, }); } } // Term :: Atom :: \ AtomEscape _ => { result.push(self.consume_atom_escape()?); } } } // Term :: Atom :: . '.' => { self.consume('.'); result.push(if self.flags.dot_all { ir::Node::MatchAny } else { ir::Node::MatchAnyExceptLineTerminator }); } '(' => { if self.try_consume_str("(?=") { // Positive lookahead. quantifier_allowed = !self.flags.unicode; result.push(self.consume_lookaround_assertion(LookaroundParams { negate: false, backwards: false, })?); } else if self.try_consume_str("(?!") { // Negative lookahead. quantifier_allowed = !self.flags.unicode; result.push(self.consume_lookaround_assertion(LookaroundParams { negate: true, backwards: false, })?); } else if self.try_consume_str("(?<=") { // Positive lookbehind. quantifier_allowed = false; self.has_lookbehind = true; result.push(self.consume_lookaround_assertion(LookaroundParams { negate: false, backwards: true, })?); } else if self.try_consume_str("(?= MAX_CAPTURE_GROUPS { return error("Capture group count limit exceeded"); } self.group_count += 1; // Maybe the capture group has a name! let mut group_name = None; if self.try_consume_str("?") { group_name = self.try_consume_named_capture_group_name(); if group_name.is_none() { return error("Invalid token at named capture group identifier"); }; } let contents = Box::new(self.consume_disjunction()?); result.push(ir::Node::CaptureGroup { id: group, contents, name: group_name, }) } if !self.try_consume(')') { return error("Unbalanced parenthesis"); } } // CharacterClass :: ClassContents :: ClassSetExpression '[' if self.flags.unicode_sets => { self.consume('['); let negate_set = self.try_consume('^'); result.push( self.consume_class_set_expression(negate_set)? .node(self.flags.icase, negate_set), ); } '[' => { result.push(self.consume_bracket()?); } // Term :: ExtendedAtom :: InvalidBracedQuantifier '{' if !self.flags.unicode => { if self.try_consume_braced_quantifier().is_some() { return error("Invalid braced quantifier"); } // Term :: ExtendedAtom :: ExtendedPatternCharacter result.push(ir::Node::Char { c: self.consume(c), icase: self.flags.icase, }) } // Term :: Atom :: PatternCharacter :: SourceCharacter but not ^ $ \ . * + ? ( ) [ ] { } | '*' | '+' | '?' | ']' | '{' | '}' if self.flags.unicode => { return error("Invalid atom character"); } // Term :: ExtendedAtom :: SourceCharacter but not ^ $ \ . * + ? ( ) [ | '*' | '+' | '?' => { return error("Invalid atom character"); } // Term :: Atom :: PatternCharacter // Term :: ExtendedAtom :: ExtendedPatternCharacter _ => { self.consume(c); result.push(ir::Node::Char { c: self.fold_if_icase(c), icase: self.flags.icase, }) } } // We just parsed a term; try parsing a quantifier. if let Some(quant) = self.try_consume_quantifier()? { if !quantifier_allowed { return error("Quantifier not allowed here"); } // Validate the quantifier. // Note we don't want to do this as part of parsing the quantiifer in some cases // an incomplete quantifier is not recognized as a quantifier, e.g. `/{3/` is // valid. if matches!(quant.max, Some(max) if quant.min > max) { return error("Invalid quantifier"); } let quantifee = result.split_off(start_offset); if self.loop_count as usize >= MAX_LOOPS { return error("Loop count limit exceeded"); } self.loop_count += 1; result.push(ir::Node::Loop { loopee: Box::new(make_cat(quantifee)), quant, enclosed_groups: start_group..self.group_count, }); } } Ok(make_cat(result)) } fn try_consume_modifier_group(&mut self) -> Result, Error> { let mut cursor = self.input.clone(); if cursor.next() != Some('(' as u32) { return Ok(None); } if cursor.next() != Some('?' as u32) { return Ok(None); } let Some(mut current) = cursor.next() else { return Ok(None); }; if to_char_sat(current) == '<' { return Ok(None); } let mut seen_hyphen = false; let mut saw_flag = false; let mut icase_override: Option = None; let mut multiline_override: Option = None; let mut dot_all_override: Option = None; loop { let ch = to_char_sat(current); match ch { 'i' | 'm' | 's' => { let value = !seen_hyphen; let target = match ch { 'i' => &mut icase_override, 'm' => &mut multiline_override, 's' => &mut dot_all_override, _ => unreachable!(), }; if target.is_some() { return error("Invalid group modifier"); } *target = Some(value); saw_flag = true; } '-' => { if seen_hyphen { return error("Invalid group modifier"); } seen_hyphen = true; } ':' => { if !saw_flag { return error("Invalid group modifier"); } self.input = cursor; let mut new_flags = self.flags; if let Some(value) = icase_override { new_flags.icase = value; } if let Some(value) = multiline_override { new_flags.multiline = value; } if let Some(value) = dot_all_override { new_flags.dot_all = value; } let saved_flags = self.flags; self.flags = new_flags; let contents = match self.consume_disjunction() { Ok(node) => node, Err(err) => { self.flags = saved_flags; return Err(err); } }; self.flags = saved_flags; return Ok(Some(contents)); } _ => { return error("Invalid group modifier"); } } current = match cursor.next() { Some(next) => next, None => return error("Invalid group modifier"), }; } } /// ES6 21.2.2.13 CharacterClass. fn consume_bracket(&mut self) -> Result { self.consume('['); let invert = self.try_consume('^'); let mut result = BracketContents { invert, cps: CodePointSet::default(), }; loop { match self.peek().map(to_char_sat) { None => { return error("Unbalanced bracket"); } Some(']') => { self.consume(']'); if self.flags.icase { result.cps = unicode::add_icase_code_points(result.cps); } return Ok(ir::Node::Bracket(result)); } _ => {} } // Parse a code point or character class. let Some(first) = self.try_consume_bracket_class_atom()? else { continue; }; // Check for a dash; we may have a range. if !self.try_consume('-') { add_class_atom(&mut result, first); continue; } let Some(second) = self.try_consume_bracket_class_atom()? else { // No second atom. For example: [a-]. add_class_atom(&mut result, first); add_class_atom(&mut result, ClassAtom::CodePoint(u32::from('-'))); continue; }; // Ranges must also be in order: z-a is invalid. // ES6 21.2.2.15.1 "If i > j, throw a SyntaxError exception" if let (ClassAtom::CodePoint(c1), ClassAtom::CodePoint(c2)) = (&first, &second) { if c1 > c2 { return error( "Range values reversed, start char code is greater than end char code.", ); } result.cps.add(Interval { first: *c1, last: *c2, }); continue; } if self.flags.unicode { return error("Invalid character range"); } // If it does not match a range treat as any match single characters. add_class_atom(&mut result, first); add_class_atom(&mut result, ClassAtom::CodePoint(u32::from('-'))); add_class_atom(&mut result, second); } } fn try_consume_bracket_class_atom(&mut self) -> Result, Error> { let c = self.peek(); if c.is_none() { return Ok(None); } let c = c.unwrap(); match to_char_sat(c) { // End of bracket. ']' => Ok(None), // ClassEscape '\\' => { self.consume('\\'); let ec = if let Some(ec) = self.peek() { ec } else { return error("Unterminated escape"); }; match to_char_sat(ec) { // ClassEscape :: b 'b' => { self.consume('b'); Ok(Some(ClassAtom::CodePoint(u32::from('\x08')))) } // ClassEscape :: [+UnicodeMode] - '-' if self.flags.unicode => { self.consume('-'); Ok(Some(ClassAtom::CodePoint(u32::from('-')))) } 'c' if !self.flags.unicode => { let input = self.input.clone(); self.consume('c'); match self.peek().map(to_char_sat) { // ClassEscape :: [~UnicodeMode] c ClassControlLetter Some('0'..='9' | '_') => { let next = self.next().expect("char was not next"); Ok(Some(ClassAtom::CodePoint(next & 0x1F))) } // CharacterEscape :: c AsciiLetter Some('a'..='z' | 'A'..='Z') => { let next = self.next().expect("char was not next"); Ok(Some(ClassAtom::CodePoint(next % 32))) } // ClassAtomNoDash :: \ [lookahead = c] _ => { self.input = input; Ok(Some(ClassAtom::CodePoint(u32::from('\\')))) } } } // ClassEscape :: CharacterClassEscape :: d 'd' => { self.consume('d'); Ok(Some(ClassAtom::CharacterClass { class_type: CharacterClassType::Digits, positive: true, })) } // ClassEscape :: CharacterClassEscape :: D 'D' => { self.consume('D'); Ok(Some(ClassAtom::CharacterClass { class_type: CharacterClassType::Digits, positive: false, })) } // ClassEscape :: CharacterClassEscape :: s 's' => { self.consume('s'); Ok(Some(ClassAtom::CharacterClass { class_type: CharacterClassType::Spaces, positive: true, })) } // ClassEscape :: CharacterClassEscape :: S 'S' => { self.consume('S'); Ok(Some(ClassAtom::CharacterClass { class_type: CharacterClassType::Spaces, positive: false, })) } // ClassEscape :: CharacterClassEscape :: w 'w' => { self.consume('w'); Ok(Some(ClassAtom::CharacterClass { class_type: CharacterClassType::Words, positive: true, })) } // ClassEscape :: CharacterClassEscape :: W 'W' => { self.consume('W'); Ok(Some(ClassAtom::CharacterClass { class_type: CharacterClassType::Words, positive: false, })) } // ClassEscape :: CharacterClassEscape :: [+UnicodeMode] p{ UnicodePropertyValueExpression } // ClassEscape :: CharacterClassEscape :: [+UnicodeMode] P{ UnicodePropertyValueExpression } 'p' | 'P' if self.flags.unicode => { self.consume(ec); let negate = ec == 'P' as u32; match self.try_consume_unicode_property_escape()? { PropertyEscapeKind::CharacterClass(s) => Ok(Some(ClassAtom::Range { iv: CodePointSet::from_sorted_disjoint_intervals(s.to_vec()), negate, })), PropertyEscapeKind::StringSet(_) => error("Invalid property escape"), } } // ClassEscape :: CharacterEscape _ => { let cc = self.consume_character_escape()?; Ok(Some(ClassAtom::CodePoint(cc))) } } } _ => Ok(Some(ClassAtom::CodePoint(self.consume(c)))), } } // CharacterClass :: ClassContents :: ClassSetExpression fn consume_class_set_expression(&mut self, negate_set: bool) -> Result { let mut result = ClassSet::new(); let first = match self.peek() { Some(0x5D /* ] */) => { self.consume(']'); return Ok(result); } Some(_) => self.consume_class_set_operand(negate_set)?, None => { return error("Unbalanced class set bracket"); } }; enum ClassSetOperator { Union, Intersection, Subtraction, } let op = match self.peek() { Some(0x5D /* ] */) => { self.consume(']'); result.union_operand(first); return Ok(result); } Some(0x26 /* & */) => { self.consume('&'); if self.peek() == Some(0x26 /* & */) { self.consume('&'); result.union_operand(first.clone()); ClassSetOperator::Intersection } else { result.codepoints.add_one(0x26 /* & */); ClassSetOperator::Union } } Some(0x2D /* - */) => { self.consume('-'); if self.peek() == Some(0x2D /* - */) { self.consume('-'); result.union_operand(first.clone()); ClassSetOperator::Subtraction } else { match first { ClassSetOperand::ClassSetCharacter(first) => { let ClassSetOperand::ClassSetCharacter(last) = self.consume_class_set_operand(negate_set)? else { return error("Invalid class set range"); }; if first > last { return error("Invalid class set range"); } result.codepoints.add(Interval { first, last }); } _ => { return error("Invalid class set range"); } }; ClassSetOperator::Union } } Some(_) => { result.union_operand(first.clone()); ClassSetOperator::Union } None => { return error("Unbalanced class set bracket"); } }; match op { ClassSetOperator::Union => { loop { let operand = match self.peek() { Some(0x5D /* ] */) => { self.consume(']'); return Ok(result); } Some(_) => self.consume_class_set_operand(negate_set)?, None => return error("Unbalanced class set bracket"), }; if self.peek() == Some(0x2D /* - */) { self.consume('-'); match operand { ClassSetOperand::ClassSetCharacter(first) => { let ClassSetOperand::ClassSetCharacter(last) = self.consume_class_set_operand(negate_set)? else { return error("Invalid class set range"); }; if first > last { return error("Invalid class set range"); } result.codepoints.add(Interval { first, last }); } _ => { return error("Invalid class set range"); } }; } else { result.union_operand(operand); } } } // ClassIntersection :: ClassSetOperand && [lookahead ≠ &] ClassSetOperator::Intersection => { loop { let operand = self.consume_class_set_operand(negate_set)?; result.intersect_operand(operand); match self.next() { Some(0x5D /* ] */) => return Ok(result), Some(0x26 /* & */) => {} Some(_) => return error("Unexpected character in class set intersection"), _ => return error("Unbalanced class set bracket"), } if self.next() != Some(0x26 /* & */) { return error("Unbalanced class set bracket"); } } } // ClassSubtraction :: ClassSubtraction -- ClassSetOperand ClassSetOperator::Subtraction => { loop { let operand = self.consume_class_set_operand(negate_set)?; result.subtract_operand(operand); match self.next() { Some(0x5D /* ] */) => return Ok(result), Some(0x2D /* - */) => {} Some(_) => return error("Unexpected character in class set subtraction"), _ => return error("Unbalanced class set bracket"), } if self.next() != Some(0x2D /* - */) { return error("Unbalanced class set bracket"); } } } } } fn consume_class_set_operand(&mut self, negate_set: bool) -> Result { use ClassSetOperand::*; let Some(cp) = self.peek() else { return error("Empty class set operand"); }; match cp { // ClassSetOperand :: NestedClass :: [ [lookahead ≠ ^] ClassContents[+UnicodeMode, +UnicodeSetsMode] ] // ClassSetOperand :: NestedClass :: [^ ClassContents[+UnicodeMode, +UnicodeSetsMode] ] 0x5B /* [ */ => { self.consume('['); let negate_set = self.try_consume('^'); let result = self.consume_class_set_expression(negate_set)?; if negate_set { result.codepoints.inverted(); } Ok(Class(result)) } // ClassSetOperand :: NestedClass :: \ CharacterClassEscape // ClassSetOperand :: ClassStringDisjunction // ClassSetOperand :: ClassSetCharacter :: \... // ClassSetRange :: ClassSetCharacter :: \... 0x5C /* \ */ => { self.consume('\\'); let Some(cp) = self.peek() else { return error("Incomplete class set escape"); }; match cp { // ClassStringDisjunction \q{ ClassStringDisjunctionContents } 0x71 /* q */ => { self.consume('q'); if !self.try_consume('{') { return error("Invalid class set escape: expected {"); } let mut alternatives = Vec::new(); let mut alternative = Vec::new(); loop { match self.peek() { Some(0x7D /* } */) => { self.consume('}'); if !alternative.is_empty() { alternatives.push(alternative.clone()); alternative.clear(); } break; } Some(0x7C /* | */) => { self.consume('|'); if !alternative.is_empty() { alternatives.push(alternative.clone()); alternative.clear(); } } Some(_) => { alternative.push(self.consume_class_set_character()?); } None => { return error("Unbalanced class set string disjunction"); } } } Ok(ClassStringDisjunction(ClassSetAlternativeStrings(alternatives))) } // CharacterClassEscape :: d 0x64 /* d */ => { self.consume('d'); Ok(CharacterClassEscape(codepoints_from_class(CharacterClassType::Digits, true))) } // CharacterClassEscape :: D 0x44 /* D */ => { self.consume('D'); Ok(CharacterClassEscape(codepoints_from_class(CharacterClassType::Digits, false))) } // CharacterClassEscape :: s 0x73 /* s */ => { self.consume('s'); Ok(CharacterClassEscape(codepoints_from_class(CharacterClassType::Spaces, true))) } // CharacterClassEscape :: S 0x53 /* S */ => { self.consume('S'); Ok(CharacterClassEscape(codepoints_from_class(CharacterClassType::Spaces, false))) } // CharacterClassEscape :: w 0x77 /* w */ => { self.consume('w'); Ok(CharacterClassEscape(codepoints_from_class(CharacterClassType::Words, true))) } // CharacterClassEscape :: W 0x57 /* W */ => { self.consume('W'); Ok(CharacterClassEscape(codepoints_from_class(CharacterClassType::Words, false))) } // CharacterClassEscape :: [+UnicodeMode] p{ UnicodePropertyValueExpression } 0x70 /* p */ => { self.consume('p'); match self.try_consume_unicode_property_escape()? { PropertyEscapeKind::CharacterClass(intervals) => { Ok(CharacterClassEscape(CodePointSet::from_sorted_disjoint_intervals( intervals.to_vec(), ))) } PropertyEscapeKind::StringSet(_) if negate_set => error("Invalid character escape"), PropertyEscapeKind::StringSet(strings) => { Ok(ClassStringDisjunction(ClassSetAlternativeStrings(strings.iter().map(|s| s.to_vec()).collect()))) } } } // CharacterClassEscape :: [+UnicodeMode] P{ UnicodePropertyValueExpression } 0x50 /* P */ => { self.consume('P'); match self.try_consume_unicode_property_escape()? { PropertyEscapeKind::CharacterClass(s) => { Ok(CharacterClassEscape(CodePointSet::from_sorted_disjoint_intervals( s.to_vec(), ).inverted())) } PropertyEscapeKind::StringSet(_) => error("Invalid character escape"), } } // ClassSetCharacter:: \b 0x62 /* b */ => { Ok(ClassSetCharacter(self.consume(cp))) } // ClassSetCharacter:: \ ClassSetReservedPunctuator _ if Self::is_class_set_reserved_punctuator(cp) => Ok(ClassSetCharacter(self.consume(cp))), // ClassSetCharacter:: \ CharacterEscape[+UnicodeMode] _ => Ok(ClassSetCharacter(self.consume_character_escape()?)) } } // ClassSetOperand :: ClassSetCharacter // ClassSetRange :: ClassSetCharacter _ => Ok(ClassSetCharacter(self.consume_class_set_character()?)), } } // ClassSetCharacter fn consume_class_set_character(&mut self) -> Result { let Some(cp) = self.next() else { return error("Incomplete class set character"); }; match cp { 0x5C /* \ */ => { let Some(cp) = self.peek() else { return error("Incomplete class set escape"); }; match cp { // \b 0x62 /* b */ => { Ok(self.consume(cp)) } // \ ClassSetReservedPunctuator _ if Self::is_class_set_reserved_punctuator(cp) => Ok(self.consume(cp)), // \ CharacterEscape[+UnicodeMode] _ => Ok(self.consume_character_escape()?) } } // [lookahead ∉ ClassSetReservedDoublePunctuator] SourceCharacter but not ClassSetSyntaxCharacter 0x28 /* ( */ | 0x29 /* ) */ | 0x7B /* { */ | 0x7D /* } */ | 0x2F /* / */ | 0x2D /* - */ | 0x7C /* | */ => error("Invalid class set character"), _ => { if Self::is_class_set_reserved_double_punctuator(cp) && let Some(cp) = self.peek() && Self::is_class_set_reserved_double_punctuator(cp) { return error("Invalid class set character"); } Ok(cp) } } } // ClassSetReservedPunctuator fn is_class_set_reserved_punctuator(cp: u32) -> bool { match cp { 0x26 /* & */ | 0x2D /* - */ | 0x21 /* ! */ | 0x23 /* # */ | 0x25 /* % */ | 0x2C /* , */ | 0x3A /* : */ | 0x3B /* ; */ | 0x3C /* < */ | 0x3D /* = */ | 0x3E /* > */ | 0x40 /* @ */ | 0x60 /* ` */ | 0x7E /* ~ */ => true, _ => false, } } fn is_class_set_reserved_double_punctuator(cp: u32) -> bool { match cp { 0x26 /* & */ | 0x21 /* ! */ | 0x23 /* # */ | 0x24 /* $ */ | 0x25 /* % */ | 0x2A /* * */ | 0x2B /* + */ | 0x2C /* , */ | 0x2E /* . */ | 0x3A /* : */ | 0x3B /* ; */ | 0x3C /* < */ | 0x3D /* = */ | 0x3E /* > */ | 0x3F /* ? */ | 0x40 /* @ */ | 0x5E /* ^ */ | 0x60 /* ` */ | 0x7E /* ~ */ => true, _ => false, } } fn try_consume_quantifier(&mut self) -> Result, Error> { if let Some(mut quant) = self.try_consume_quantifier_prefix()? { quant.greedy = !self.try_consume('?'); Ok(Some(quant)) } else { Ok(None) } } fn try_consume_quantifier_prefix(&mut self) -> Result, Error> { let nc = self.peek(); if nc.is_none() { return Ok(None); } let c = nc.unwrap(); match char::from_u32(c) { Some('+') => { self.consume('+'); Ok(Some(ir::Quantifier { min: 1, max: None, greedy: true, })) } Some('*') => { self.consume('*'); Ok(Some(ir::Quantifier { min: 0, max: None, greedy: true, })) } Some('?') => { self.consume('?'); Ok(Some(ir::Quantifier { min: 0, max: Some(1), greedy: true, })) } Some('{') => { if let Some(quantifier) = self.try_consume_braced_quantifier() { Ok(Some(quantifier)) } else if self.flags.unicode { // if there was a brace '{' that doesn't parse into a valid quantifier, // it's not valid with the unicode flag error("Invalid quantifier") } else { Ok(None) } } _ => Ok(None), } } fn try_consume_braced_quantifier(&mut self) -> Option { // if parsed input is actually invalid, keep the previous one for rollback let pre_input = self.input.clone(); self.consume('{'); let optmin = self.try_consume_decimal_integer_literal(); let Some(optmin) = optmin else { // not a valid quantifier, rollback consumption self.input = pre_input; return None; }; let mut quant = ir::Quantifier { min: optmin, max: Some(optmin), greedy: true, }; if self.try_consume(',') { let max = self.try_consume_decimal_integer_literal(); // Either like {3,4} in which case we want to set the max; // or like {3,} in which case the max should be None to indicate unbounded. quant.max = max; } else { // Like {3}. } if !self.try_consume('}') { // not a valid quantifier, rollback consumption self.input = pre_input; return None; } Some(quant) } /// ES6 11.8.3 DecimalIntegerLiteral. /// If the value would overflow, usize::MAX is returned. /// All decimal digits are consumed regardless. fn try_consume_decimal_integer_literal(&mut self) -> Option { let mut result: usize = 0; let mut char_count = 0; while let Some(c) = self.peek() { if let Some(digit) = char::from_u32(c).and_then(|c| char::to_digit(c, 10)) { self.consume(c); char_count += 1; result = result.saturating_mul(10); result = result.saturating_add(digit as usize); } else { break; } } if char_count > 0 { Some(result) } else { None } } fn consume_lookaround_assertion( &mut self, params: LookaroundParams, ) -> Result { let start_group = self.group_count; let contents = self.consume_disjunction()?; let end_group = self.group_count; Ok(ir::Node::LookaroundAssertion { negate: params.negate, backwards: params.backwards, start_group, end_group, contents: Box::new(contents), }) } fn consume_character_escape(&mut self) -> Result { let c = self.next().expect("Should have a character"); let ch = to_char_sat(c); match ch { // CharacterEscape :: ControlEscape :: f 'f' => Ok(0xC), // CharacterEscape :: ControlEscape :: n 'n' => Ok(0xA), // CharacterEscape :: ControlEscape :: r 'r' => Ok(0xD), // CharacterEscape :: ControlEscape :: t 't' => Ok(0x9), // CharacterEscape :: ControlEscape :: v 'v' => Ok(0xB), // CharacterEscape :: c AsciiLetter 'c' => { if let Some(nc) = self.next().and_then(char::from_u32) && (nc.is_ascii_lowercase() || nc.is_ascii_uppercase()) { return Ok((nc as u32) % 32); } error("Invalid character escape") } // CharacterEscape :: 0 [lookahead ∉ DecimalDigit] '0' if self .peek() .and_then(char::from_u32) .map(|c: char| c.is_ascii_digit()) != Some(true) => { Ok(0x0) } // CharacterEscape :: HexEscapeSequence :: x HexDigit HexDigit 'x' => { let hex_to_digit = |c: char| c.to_digit(16); let x1 = self.next().and_then(char::from_u32).and_then(hex_to_digit); let x2 = self.next().and_then(char::from_u32).and_then(hex_to_digit); match (x1, x2) { (Some(x1), Some(x2)) => Ok(x1 * 16 + x2), // CharacterEscape :: IdentityEscape :: SourceCharacterIdentityEscape _ if !self.flags.unicode => Ok(c), _ => error("Invalid character escape"), } } // CharacterEscape :: RegExpUnicodeEscapeSequence 'u' => { if let Some(c) = self.try_escape_unicode_sequence() { Ok(c) } else if !self.flags.unicode { // CharacterEscape :: IdentityEscape :: SourceCharacterIdentityEscape Ok(c) } else { error("Invalid unicode escape") } } // CharacterEscape :: [~UnicodeMode] LegacyOctalEscapeSequence '0'..='7' if !self.flags.unicode => { let Some(c1) = self.peek() else { return Ok(c - '0' as u32); }; let ch1 = to_char_sat(c1); match ch { // 0 [lookahead ∈ { 8, 9 }] '0' if ('8'..='9').contains(&ch1) => Ok(0x0), // NonZeroOctalDigit [lookahead ∉ OctalDigit] _ if !('0'..='7').contains(&ch1) => Ok(c - '0' as u32), // FourToSeven OctalDigit '4'..='7' => { self.consume(c1); Ok((c - '0' as u32) * 8 + c1 - '0' as u32) } // ZeroToThree OctalDigit [lookahead ∉ OctalDigit] // ZeroToThree OctalDigit OctalDigit '0'..='3' => { self.consume(c1); if self.peek().map(|c2| ('0'..='7').contains(&to_char_sat(c2))) == Some(true) { let c2 = self.next().expect("char was not next"); Ok((c - '0' as u32) * 64 + (c1 - '0' as u32) * 8 + c2 - '0' as u32) } else { Ok((c - '0' as u32) * 8 + c1 - '0' as u32) } } _ => unreachable!(), } } // CharacterEscape :: IdentityEscape :: [+UnicodeMode] SyntaxCharacter // CharacterEscape :: IdentityEscape :: [+UnicodeMode] / '^' | '$' | '\\' | '.' | '*' | '+' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '/' => Ok(c), // CharacterEscape :: IdentityEscape :: SourceCharacterIdentityEscape _ if !self.flags.unicode => Ok(c), _ => error("Invalid character escape"), } } // AtomEscape fn consume_atom_escape(&mut self) -> Result { let Some(c) = self.peek() else { return error("Incomplete escape"); }; match to_char_sat(c) { 'd' | 'D' => { self.consume(c); Ok(make_bracket_class( CharacterClassType::Digits, c == 'd' as u32, self.flags.icase, )) } 's' | 'S' => { self.consume(c); Ok(make_bracket_class( CharacterClassType::Spaces, c == 's' as u32, self.flags.icase, )) } 'w' | 'W' => { self.consume(c); Ok(make_bracket_class( CharacterClassType::Words, c == 'w' as u32, self.flags.icase, )) } // ClassEscape :: CharacterClassEscape :: [+UnicodeMode] p{ UnicodePropertyValueExpression } // ClassEscape :: CharacterClassEscape :: [+UnicodeMode] P{ UnicodePropertyValueExpression } 'p' | 'P' if self.flags.unicode || self.flags.unicode_sets => { self.consume(c); let negate = c == 'P' as u32; let property_escape = self.try_consume_unicode_property_escape()?; match property_escape { PropertyEscapeKind::CharacterClass(s) => { Ok(ir::Node::Bracket(BracketContents { invert: negate, cps: CodePointSet::from_sorted_disjoint_intervals(s.to_vec()), })) } PropertyEscapeKind::StringSet(_) if negate => error("Invalid character escape"), PropertyEscapeKind::StringSet(strings) => Ok(make_alt( strings .iter() .map(|s| { ir::Node::Cat( s.iter() .map(|c| ir::Node::Char { c: *c, icase: self.flags.icase, }) .collect(), ) }) .collect(), )), } } // [+UnicodeMode] DecimalEscape // Note: This is a backreference. '1'..='9' if self.flags.unicode => { let group = self.try_consume_decimal_integer_literal().unwrap(); if group <= self.group_count_max as usize { Ok(ir::Node::BackRef(group as u32)) } else { error("Invalid character escape") } } // [~UnicodeMode] DecimalEscape but only if the CapturingGroupNumber of DecimalEscape // is ≤ CountLeftCapturingParensWithin(the Pattern containing DecimalEscape) // Note: This could be either a backreference, a legacy octal escape or an identity escape. '1'..='9' => { let input = self.input.clone(); let group = self.try_consume_decimal_integer_literal().unwrap(); if group <= self.group_count_max as usize { Ok(ir::Node::BackRef(group as u32)) } else { self.input = input; let c = self.consume_character_escape()?; Ok(ir::Node::Char { c: self.fold_if_icase(c), icase: self.flags.icase, }) } } // [+NamedCaptureGroups] k GroupName 'k' if self.flags.unicode || !self.named_group_indices.is_empty() => { // The sequence `\k` must be the start of a backreference to a named capture group. // Note multiple capture groups may have the same name; we must map all of them to their indices. self.consume('k'); // Must have a valid group name. let Some(group_name) = self.try_consume_named_capture_group_name() else { return error("Invalid named backreference syntax"); }; // The group name must be the name of a previously defined capture group. let Some(group_indices) = self.named_group_indices.get(&group_name) else { return error(format!( "Backreference to invalid named capture group: {}", &group_name )); }; // Note backreferences are 1-based. let node = match group_indices.len() { 0 => unreachable!("Should not have empty indices for group name"), 1 => { // Common case of a backref matching a single group. ir::Node::BackRef(group_indices[0] + 1) } _ => { // Multiple groups sharing a name: the backref reads the // last occurrence that is defined at match time. ir::Node::BackRefLastDefined(group_indices.iter().map(|i| i + 1).collect()) } }; Ok(node) } // [~NamedCaptureGroups] k GroupName 'k' => { self.consume('k'); Ok(ir::Node::Char { c: self.fold_if_icase(c), icase: self.flags.icase, }) } _ => { let c = self.consume_character_escape()?; Ok(ir::Node::Char { c: self.fold_if_icase(c), icase: self.flags.icase, }) } } } #[allow(clippy::branches_sharing_code)] fn try_escape_unicode_sequence(&mut self) -> Option { let mut orig_input = self.input.clone(); // Support \u{X..X} (Unicode CodePoint) if self.try_consume('{') { let mut s = String::new(); loop { match self.next().and_then(char::from_u32) { Some('}') => break, Some(c) => s.push(c), None => { // Surrogates not supported in code point escapes. self.input = orig_input; return None; } } } match u32::from_str_radix(&s, 16) { Ok(u) => { if u > 0x10_FFFF { self.input = orig_input; None } else { Some(u) } } _ => { self.input = orig_input; None } } } else { // Hex4Digits let mut s = String::new(); for _ in 0..4 { if let Some(c) = self.next().and_then(char::from_u32) { s.push(c); } else { // Surrogates are not hex digits. self.input = orig_input; return None; } } match u16::from_str_radix(&s, 16) { Ok(u) => { if (0xD800..=0xDBFF).contains(&u) { // Found a high surrogate. Try to parse a low surrogate next // to see if we can rebuild the original `char` if !self.try_consume_str("\\u") { return Some(u as u32); } orig_input = self.input.clone(); // A poor man's try block to handle the backtracking // in a single place instead of every time we want to return. // This allows us to use `?` within the inner block without returning // from the entire parent function. let result = (|| { let mut s = String::new(); for _ in 0..4 { let c = self.next().and_then(char::from_u32)?; s.push(c); } let uu = u16::from_str_radix(&s, 16).ok()?; let ch = char::decode_utf16([u, uu]).next()?.ok()?; Some(u32::from(ch)) })(); result.or_else(|| { self.input = orig_input; Some(u as u32) }) } else { // If `u` is not a surrogate or is a low surrogate we can directly return it, // since all paired low surrogates should have been handled above. Some(u as u32) } } _ => { self.input = orig_input; None } } } } fn try_consume_named_capture_group_name(&mut self) -> Option { if !self.try_consume('<') { return None; } let orig_input = self.input.clone(); let mut group_name = String::new(); if let Some(mut c) = self.next().and_then(char::from_u32) { if c == '\\' && self.try_consume('u') { if let Some(escaped) = self.try_escape_unicode_sequence().and_then(char::from_u32) { c = escaped; } else { self.input = orig_input; return None; } } if interval_contains(id_start_ranges(), c.into()) || c == '$' || c == '_' { group_name.push(c); } else { self.input = orig_input; return None; } } else { self.input = orig_input; return None; } loop { if let Some(mut c) = self.next().and_then(char::from_u32) { if c == '\\' && self.try_consume('u') { if let Some(escaped) = self.try_escape_unicode_sequence().and_then(char::from_u32) { c = escaped; } else { self.input = orig_input; return None; } } if c == '>' { break; } if interval_contains(id_continue_ranges(), c.into()) || c == '$' || c == '_' || c == '\u{200C}' /* */ || c == '\u{200D}' /* */ { group_name.push(c); } else { self.input = orig_input; return None; } } else { self.input = orig_input; return None; } } Some(group_name) } // Quickly parse all capture groups. // Per TC39 proposal, duplicate named groups are allowed in different alternatives. fn parse_capture_groups(&mut self) -> Result<(), Error> { let orig_input = self.input.clone(); // Pass 1: Collect all named capture groups with their alternative paths let named_group_locations = self.collect_named_group_locations()?; // Pass 2: Check for conflicts (duplicates in the same alternative path) self.check_duplicate_conflicts(&named_group_locations)?; self.input = orig_input; Ok(()) } /// Pass 1: Collect all named capture groups and record which alternative path each appears in. fn collect_named_group_locations( &mut self, ) -> Result>, Error> { // Track parenthesis depth and alternative index at each depth let mut paren_depth: usize = 0; // Map from depth to current alternative index at that depth let mut alt_indices: HashMap = HashMap::new(); alt_indices.insert(0, 0); // Map from group name to all alternative paths where it appears let mut named_group_locations: HashMap> = HashMap::new(); loop { match self.next().map(to_char_sat) { Some('\\') => { self.next(); continue; } Some('[') => loop { match self.next().map(to_char_sat) { Some('\\') => { self.next(); continue; } Some(']') => break, Some(_) => continue, None => break, } }, Some('(') => { // Determine whether we're a capturing group, and optionally the name. let is_capturing; let group_name; if self.try_consume_str("?") { group_name = self.try_consume_named_capture_group_name(); is_capturing = group_name.is_some(); // (?:, (?=, (?!, etc. are non-capturing. } else { is_capturing = true; group_name = None; // Unnamed capture group } if let Some(name) = group_name { // Build current alternative path from depth 0 to current depth. let mut segments = Vec::new(); for d in 0..=paren_depth { segments.push((d, *alt_indices.get(&d).unwrap_or(&0))); } // Record this location. named_group_locations .entry(name.clone()) .or_default() .push(AlternativePath { segments }); // Store all occurrences in named_group_indices. self.named_group_indices .entry(name) .or_default() .push(self.group_count_max); } if is_capturing { self.group_count_max = if self.group_count_max + 1 > MAX_CAPTURE_GROUPS as u32 { MAX_CAPTURE_GROUPS as u32 } else { self.group_count_max + 1 }; } // Entering a new group. paren_depth += 1; alt_indices.insert(paren_depth, 0); } Some(')') => { // Exiting a group if paren_depth > 0 { alt_indices.remove(&paren_depth); paren_depth -= 1; } } Some('|') => { // Moving to next alternative at current depth *alt_indices.entry(paren_depth).or_insert(0) += 1; } Some(_) => continue, None => break, } } Ok(named_group_locations) } /// Pass 2: Check that named groups with the same name don't conflict. /// Groups conflict if they appear in the same alternative path. /// Per TC39 proposal, `/(?x)|(?y)/` is valid (different alternatives), /// but `/(?x)(?y)/` is invalid (same alternative). fn check_duplicate_conflicts( &self, named_group_locations: &HashMap>, ) -> Result<(), Error> { for paths in named_group_locations.values() { // Check each pair of paths for this group name for i in 0..paths.len() { for j in (i + 1)..paths.len() { if paths[i].conflicts_with(&paths[j]) { return error("Duplicate capture group name"); } } } } Ok(()) } fn try_consume_unicode_property_escape(&mut self) -> Result { if !self.try_consume('{') { return error("Invalid character at property escape start"); } let mut buffer = String::new(); let mut name = None; while let Some(c) = self.peek().and_then(char::from_u32) { match c { '}' => { self.consume(c); let Some(value) = unicode_property_from_str(&buffer, name, self.flags.unicode_sets) else { break; }; return Ok(value); } '=' if name.is_none() => { self.consume(c); let Some(n) = unicode_property_name_from_str(&buffer) else { break; }; name = Some(n); buffer.clear(); } c if c.is_ascii_alphanumeric() || c == '_' => { self.consume(c); buffer.push(c); } _ => break, } } error("Invalid property name") } fn finalize(&self, mut re: ir::Regex) -> Result { debug_assert!(self.loop_count <= MAX_LOOPS as u32); debug_assert!(self.group_count as usize <= MAX_CAPTURE_GROUPS); if self.has_lookbehind { ir::walk_mut( false, re.flags.unicode, &mut re.node, &mut ir::Node::reverse_cats, ); } Ok(re) } } /// Try parsing a given pattern. /// Return the resulting IR regex, or an error. pub fn try_parse(pattern: I, flags: api::Flags) -> Result where I: Iterator + Clone, { let mut p = Parser { input: pattern.peekable(), flags, loop_count: 0, group_count: 0, named_group_indices: HashMap::new(), group_count_max: 0, has_lookbehind: false, }; p.try_parse() }