use crate::error::{Error, ErrorKind, Result};
use crate::nfa::utf8_automata::{
compile_utf8_complement, compile_utf8_range, optimize_sequences, Utf8Sequence,
};
use crate::parser::{
Anchor, Ast, Class, ClassRange, Expr, Flags, Group, GroupKind, Lookaround, LookaroundKind,
PerlClassKind, Repeat,
};
pub const HORIZONTAL_WHITESPACE: &[(u32, u32)] = &[
(0x0009, 0x0009),
(0x0020, 0x0020),
(0x00A0, 0x00A0),
(0x1680, 0x1680),
(0x2000, 0x200A),
(0x202F, 0x202F),
(0x205F, 0x205F),
(0x3000, 0x3000),
];
const LINE_BREAK_SINGLE: &[(u32, u32)] = &[(0x000A, 0x000D), (0x0085, 0x0085), (0x2028, 0x2029)];
use super::unicode_data;
use super::{
CodepointClass, Hir, HirAnchor, HirCapture, HirClass, HirExpr, HirLookaround,
HirLookaroundKind, HirProps, HirRepeat,
};
fn strict_end_of_text_lookahead() -> HirExpr {
let any_byte = HirExpr::Class(HirClass::new(vec![(0, 255)], false));
HirExpr::Lookaround(Box::new(HirLookaround {
expr: any_byte,
kind: HirLookaroundKind::NegativeLookahead,
}))
}
pub struct HirTranslator {
props: HirProps,
flags: Flags,
max_backref: u32,
current_class_codepoints: Option<(Vec<(u32, u32)>, bool)>,
engine_already_pinned: bool,
}
impl HirTranslator {
pub fn new() -> Self {
Self {
props: HirProps::default(),
flags: Flags::default(),
max_backref: 0,
current_class_codepoints: None,
engine_already_pinned: false,
}
}
pub fn translate(&mut self, ast: &Ast) -> Result<Hir> {
self.translate_with_limit(ast, DEFAULT_EXPANDED_SIZE)
}
pub fn translate_with_limit(&mut self, ast: &Ast, limit: u32) -> Result<Hir> {
self.flags = ast.flags;
self.engine_already_pinned = pins_codepoint_engine(&ast.expr);
let expr = self.translate_expr(&ast.expr)?;
if self.max_backref > self.props.capture_count {
return Err(Error::new(
ErrorKind::BackrefNotFound(self.max_backref as usize),
format!(
"backreference \\{} references non-existent capture group (only {} groups defined)",
self.max_backref, self.props.capture_count
),
));
}
if let Some((ranges, negated)) = self.current_class_codepoints.take() {
let is_simple_class = Self::is_simple_unicode_class(&expr);
if is_simple_class {
self.props.codepoint_class = Some(CodepointClass::new(ranges, negated));
}
}
let size = expanded_size(&expr);
if size > limit {
return Err(Error::new(
ErrorKind::ExpansionTooLarge { size, limit },
format!(
"the pattern expands to {size} elements, past the limit of {limit}; \
raise it with RegexBuilder::size_limit if the cost is acceptable"
),
));
}
Ok(Hir {
expr,
props: self.props.clone(),
})
}
fn translate_expr(&mut self, expr: &Expr) -> Result<HirExpr> {
match expr {
Expr::Empty => Ok(HirExpr::Empty),
Expr::Literal(c) => self.translate_literal(*c),
Expr::GraphemeCluster => Ok(super::grapheme::grapheme_cluster()),
Expr::Dot => {
let dot_all = self.flags.dot_all;
Ok(self.build_dot_expr(dot_all))
}
Expr::Concat(exprs) => {
let mut hir_exprs = Vec::with_capacity(exprs.len());
for e in exprs {
hir_exprs.push(self.translate_expr(e)?);
}
Ok(HirExpr::Concat(hir_exprs))
}
Expr::Alt(exprs) => {
let mut hir_exprs = Vec::with_capacity(exprs.len());
for e in exprs {
hir_exprs.push(self.translate_expr(e)?);
}
Ok(HirExpr::Alt(hir_exprs))
}
Expr::Repeat(rep) => self.translate_repeat(rep),
Expr::Group(group) => self.translate_group(group),
Expr::Class(class) => self.translate_class(class),
Expr::Anchor(anchor) => {
let hir_anchor = match anchor {
Anchor::StartOfString | Anchor::StartOfInput => {
self.props.has_anchors = true;
self.props.has_start_anchor = true;
HirAnchor::Start
}
Anchor::EndOfString | Anchor::EndOfInputBeforeNewline => {
self.props.has_anchors = true;
self.props.has_end_anchor = true;
HirAnchor::End
}
Anchor::EndOfInput => {
self.props.has_anchors = true;
self.props.has_end_anchor = true;
self.props.has_lookaround = true;
return Ok(strict_end_of_text_lookahead());
}
Anchor::StartOfLine => {
self.props.has_anchors = true;
self.props.has_start_anchor = true;
self.props.has_multiline_anchors = true;
HirAnchor::StartLine
}
Anchor::EndOfLine => {
self.props.has_anchors = true;
self.props.has_end_anchor = true;
self.props.has_multiline_anchors = true;
HirAnchor::EndLine
}
Anchor::WordBoundary => {
self.props.has_word_boundary = true;
HirAnchor::WordBoundary
}
Anchor::NotWordBoundary => {
self.props.has_word_boundary = true;
HirAnchor::NotWordBoundary
}
};
Ok(HirExpr::Anchor(hir_anchor))
}
Expr::Lookaround(la) => self.translate_lookaround(la),
Expr::Backref(n) => {
self.props.has_backrefs = true;
self.max_backref = self.max_backref.max(*n);
Ok(HirExpr::Backref(*n))
}
Expr::UnicodeProperty { name, negated } => {
self.translate_unicode_property(name, *negated)
}
Expr::PerlClass(kind) => self.translate_perl_class(*kind),
Expr::LineBreak => self.build_line_break_expr(),
Expr::AnyExceptNewline => Ok(self.build_dot_expr(false)),
}
}
fn translate_literal(&mut self, c: char) -> Result<HirExpr> {
if self.flags.case_insensitive {
let equivalents = unicode_data::case_fold_equivalents(c as u32);
if equivalents.len() > 1 {
let ranges: Vec<(u32, u32)> = equivalents.iter().map(|&cp| (cp, cp)).collect();
return self.translate_ranges_to_hir(&ranges, false);
}
}
let mut bytes = [0u8; 4];
let len = c.encode_utf8(&mut bytes).len();
Ok(HirExpr::Literal(bytes[..len].to_vec()))
}
fn translate_perl_class(&mut self, kind: PerlClassKind) -> Result<HirExpr> {
let unicode = self.flags.unicode
|| matches!(
kind,
PerlClassKind::Whitespace
| PerlClassKind::NotWhitespace
| PerlClassKind::HorizontalWhitespace
| PerlClassKind::NotHorizontalWhitespace
);
if unicode {
self.translate_perl_class_unicode(kind)
} else {
self.translate_perl_class_ascii(kind)
}
}
fn translate_perl_class_ascii(&self, kind: PerlClassKind) -> Result<HirExpr> {
let (ranges, negated) = match kind {
PerlClassKind::Digit => (vec![(b'0', b'9')], false),
PerlClassKind::NotDigit => (vec![(b'0', b'9')], true),
PerlClassKind::Word => (
vec![(b'a', b'z'), (b'A', b'Z'), (b'0', b'9'), (b'_', b'_')],
false,
),
PerlClassKind::NotWord => (
vec![(b'a', b'z'), (b'A', b'Z'), (b'0', b'9'), (b'_', b'_')],
true,
),
PerlClassKind::Whitespace => (
vec![
(b' ', b' '),
(b'\t', b'\t'),
(b'\n', b'\n'),
(b'\r', b'\r'),
(0x0C, 0x0C),
(0x0B, 0x0B),
],
false,
),
PerlClassKind::NotWhitespace => (
vec![
(b' ', b' '),
(b'\t', b'\t'),
(b'\n', b'\n'),
(b'\r', b'\r'),
(0x0C, 0x0C),
(0x0B, 0x0B),
],
true,
),
PerlClassKind::HorizontalWhitespace => (vec![(b'\t', b'\t'), (b' ', b' ')], false),
PerlClassKind::NotHorizontalWhitespace => (vec![(b'\t', b'\t'), (b' ', b' ')], true),
};
if negated {
let surviving_ascii = merge_byte_ranges(complement_within_ascii(&ranges));
return Ok(self.build_ascii_or_non_ascii(surviving_ascii));
}
Ok(HirExpr::Class(HirClass::new(ranges, negated)))
}
fn translate_perl_class_unicode(&mut self, kind: PerlClassKind) -> Result<HirExpr> {
let (ranges, negated): (&[(u32, u32)], bool) = match kind {
PerlClassKind::Digit => (unicode_data::PERL_DECIMAL, false),
PerlClassKind::NotDigit => (unicode_data::PERL_DECIMAL, true),
PerlClassKind::Word => (unicode_data::PERL_WORD, false),
PerlClassKind::NotWord => (unicode_data::PERL_WORD, true),
PerlClassKind::Whitespace => (unicode_data::PERL_SPACE, false),
PerlClassKind::NotWhitespace => (unicode_data::PERL_SPACE, true),
PerlClassKind::HorizontalWhitespace => (HORIZONTAL_WHITESPACE, false),
PerlClassKind::NotHorizontalWhitespace => (HORIZONTAL_WHITESPACE, true),
};
self.translate_ranges_to_hir(ranges, negated)
}
fn translate_ranges_to_hir(&mut self, ranges: &[(u32, u32)], negated: bool) -> Result<HirExpr> {
let mut byte_ranges: Vec<(u8, u8)> = Vec::new();
let mut utf8_sequences: Vec<Utf8Sequence> = Vec::new();
for &(start, end) in ranges {
push_codepoint_range(start, end, &mut byte_ranges, &mut utf8_sequences);
}
byte_ranges.sort_by_key(|r| r.0);
let merged_bytes = merge_byte_ranges(byte_ranges);
let optimized_seqs = optimize_sequences(utf8_sequences);
Ok(self.build_class_expr(merged_bytes, optimized_seqs, negated))
}
fn translate_unicode_property(&mut self, name: &str, negated: bool) -> Result<HirExpr> {
let ranges = unicode_data::get_property(name)
.ok_or_else(|| Error::new(ErrorKind::UnknownUnicodeProperty(name.to_string()), name))?;
let total_codepoints: u32 = ranges.iter().map(|(s, e)| e - s + 1).sum();
let has_large_range = ranges.iter().any(|(s, e)| e - s > 500);
let has_many_ranges = ranges.len() > 50;
let is_large = negated || total_codepoints > 500 || has_large_range || has_many_ranges;
if is_large {
self.props.has_large_unicode_class = true;
let cp_ranges: Vec<(u32, u32)> = ranges.to_vec();
return Ok(HirExpr::UnicodeCpClass(CodepointClass::new(
cp_ranges, negated,
)));
}
let mut byte_ranges: Vec<(u8, u8)> = Vec::new();
let mut utf8_sequences: Vec<Utf8Sequence> = Vec::new();
for &(start, end) in ranges {
push_codepoint_range(start, end, &mut byte_ranges, &mut utf8_sequences);
}
byte_ranges.sort_by_key(|r| r.0);
let merged_bytes = merge_byte_ranges(byte_ranges);
let optimized_seqs = optimize_sequences(utf8_sequences);
Ok(self.build_class_expr(merged_bytes, optimized_seqs, false))
}
#[allow(dead_code)]
fn build_negated_unicode_from_ranges(&mut self, ranges: &[(u32, u32)]) -> HirExpr {
let complement_sequences = compile_utf8_complement(ranges);
let mut complement_bytes: Vec<(u8, u8)> = Vec::new();
let mut complement_multibyte: Vec<Utf8Sequence> = Vec::new();
for seq in complement_sequences {
if seq.len() == 1 {
complement_bytes.push(seq.ranges[0]);
} else {
complement_multibyte.push(seq);
}
}
complement_bytes.sort_by_key(|r| r.0);
let merged_bytes = merge_byte_ranges(complement_bytes);
let mut alternatives: Vec<HirExpr> = Vec::new();
if !merged_bytes.is_empty() {
alternatives.push(HirExpr::Class(HirClass::new(merged_bytes, false)));
}
if !complement_multibyte.is_empty() {
let trie_expr = self.build_utf8_trie(&complement_multibyte);
alternatives.push(trie_expr);
}
match alternatives.len() {
0 => HirExpr::Class(HirClass::new(vec![], false)), 1 => alternatives.pop().unwrap(),
_ => HirExpr::Alt(alternatives),
}
}
fn translate_repeat(&mut self, rep: &Repeat) -> Result<HirExpr> {
let expr = self.translate_expr(&rep.expr)?;
if !rep.greedy {
self.props.has_non_greedy = true;
}
Ok(HirExpr::Repeat(Box::new(HirRepeat {
expr,
min: rep.min,
max: rep.max,
greedy: rep.greedy,
})))
}
fn translate_group(&mut self, group: &Group) -> Result<HirExpr> {
if let GroupKind::Flagged(flags) = &group.kind {
let saved = self.flags;
self.flags = *flags;
let expr = self.translate_expr(&group.expr)?;
self.flags = saved;
return Ok(expr);
}
let expr = self.translate_expr(&group.expr)?;
match &group.kind {
GroupKind::Capturing(index) => {
self.props.capture_count = self.props.capture_count.max(*index);
Ok(HirExpr::Capture(Box::new(HirCapture {
index: *index,
name: None,
expr,
})))
}
GroupKind::NamedCapturing { name, index } => {
self.props.capture_count = self.props.capture_count.max(*index);
self.props.named_groups.insert(name.clone(), *index);
Ok(HirExpr::Capture(Box::new(HirCapture {
index: *index,
name: Some(name.clone()),
expr,
})))
}
GroupKind::NonCapturing => Ok(expr),
GroupKind::Flagged(_) => Ok(expr),
}
}
fn translate_class(&mut self, class: &Class) -> Result<HirExpr> {
let mut byte_ranges: Vec<(u8, u8)> = Vec::new();
let mut utf8_sequences: Vec<Utf8Sequence> = Vec::new();
let folded: Vec<ClassRange>;
let ranges: &[ClassRange] = if self.flags.case_insensitive {
let mut out = class.ranges.clone();
for r in &class.ranges {
let (s, e) = (r.start as u32, r.end as u32);
if e.saturating_sub(s) >= 0x1000 {
continue;
}
for cp in s..=e {
for &fc in unicode_data::case_fold_equivalents(cp).iter() {
if fc != cp {
if let Some(fch) = char::from_u32(fc) {
out.push(ClassRange::new(fch, fch));
}
}
}
}
}
folded = out;
&folded
} else {
&class.ranges
};
let mut codepoint_ranges: Vec<(u32, u32)> = Vec::new();
for range in ranges {
codepoint_ranges.push((range.start as u32, range.end as u32));
self.collect_class_ranges(range, &mut byte_ranges, &mut utf8_sequences);
}
codepoint_ranges.sort_by_key(|r| r.0);
let merged_codepoints = merge_codepoint_ranges(codepoint_ranges);
self.current_class_codepoints = Some((merged_codepoints, class.negated));
byte_ranges.sort_by_key(|r| r.0);
let merged_bytes = merge_byte_ranges(byte_ranges);
let optimized_seqs = optimize_sequences(utf8_sequences);
let expr = self.build_class_expr(merged_bytes, optimized_seqs, class.negated);
Ok(expr)
}
fn collect_class_ranges(
&self,
range: &ClassRange,
byte_ranges: &mut Vec<(u8, u8)>,
utf8_sequences: &mut Vec<Utf8Sequence>,
) {
push_codepoint_range(
range.start as u32,
range.end as u32,
byte_ranges,
utf8_sequences,
);
}
fn build_class_expr(
&mut self,
byte_ranges: Vec<(u8, u8)>,
utf8_sequences: Vec<Utf8Sequence>,
negated: bool,
) -> HirExpr {
if negated && !utf8_sequences.is_empty() {
let excluded = class_codepoint_ranges(&byte_ranges, &utf8_sequences, self);
if let Some(expr) = self.lower_complement_to_bytes(&excluded) {
return expr;
}
return self.build_negated_unicode_class(byte_ranges, utf8_sequences);
}
if negated && byte_ranges.iter().all(|&(_, hi)| hi <= 0x7f) {
let surviving_ascii = merge_byte_ranges(complement_within_ascii(&byte_ranges));
return self.build_ascii_or_non_ascii(surviving_ascii);
}
if utf8_sequences.len() > MAX_TRIE_SEQUENCES
|| (self.engine_already_pinned && !utf8_sequences.is_empty())
{
return self.build_unicode_codepoint_class(byte_ranges, utf8_sequences, negated);
}
let mut alternatives: Vec<HirExpr> = Vec::new();
if !byte_ranges.is_empty() {
debug_assert!(
!negated,
"a negated class must be expanded before reaching the byte-class branch"
);
alternatives.push(HirExpr::Class(HirClass::new(byte_ranges, negated)));
}
if !utf8_sequences.is_empty() {
let trie_expr = self.build_utf8_trie(&utf8_sequences);
alternatives.push(trie_expr);
}
match alternatives.len() {
0 => {
HirExpr::Class(HirClass::new(vec![], false))
}
1 => alternatives.pop().unwrap(),
_ => HirExpr::Alt(alternatives),
}
}
fn lower_complement_to_bytes(&self, excluded: &[(u32, u32)]) -> Option<HirExpr> {
if self.engine_already_pinned {
return None;
}
let complement = compile_utf8_complement(excluded);
if complement.len() > MAX_TRIE_SEQUENCES {
return None;
}
let mut byte_ranges = Vec::new();
let mut sequences = Vec::new();
for seq in complement {
match seq.ranges.as_slice() {
[single] => byte_ranges.push(*single),
_ => sequences.push(seq),
}
}
let mut alternatives: Vec<HirExpr> = Vec::new();
if !byte_ranges.is_empty() {
alternatives.push(HirExpr::Class(HirClass::new(
merge_byte_ranges(byte_ranges),
false,
)));
}
if !sequences.is_empty() {
alternatives.push(self.build_utf8_trie(&sequences));
}
match alternatives.len() {
0 => Some(HirExpr::Class(HirClass::new(vec![], false))),
1 => alternatives.pop(),
_ => Some(HirExpr::Alt(alternatives)),
}
}
fn build_dot_expr(&mut self, dot_all: bool) -> HirExpr {
let ascii: Vec<(u8, u8)> = if dot_all {
vec![(0x00, 0x7f)]
} else {
vec![(0x00, 0x09), (0x0b, 0x7f)]
};
self.build_ascii_or_non_ascii(ascii)
}
fn build_line_break_expr(&mut self) -> Result<HirExpr> {
let crlf = HirExpr::Literal(vec![b'\r', b'\n']);
let single = self.translate_ranges_to_hir(LINE_BREAK_SINGLE, false)?;
Ok(HirExpr::Alt(vec![crlf, single]))
}
fn build_ascii_or_non_ascii(&self, ascii_ranges: Vec<(u8, u8)>) -> HirExpr {
let non_ascii = any_non_ascii_character();
let mut alternatives = Vec::new();
if !ascii_ranges.is_empty() {
alternatives.push(HirExpr::Class(HirClass::new(ascii_ranges, false)));
}
if !non_ascii.is_empty() {
alternatives.push(self.build_utf8_trie(&non_ascii));
}
match alternatives.len() {
0 => HirExpr::Class(HirClass::new(vec![], false)),
1 => alternatives.pop().unwrap(),
_ => HirExpr::Alt(alternatives),
}
}
#[allow(clippy::only_used_in_recursion)]
fn build_utf8_trie(&self, sequences: &[Utf8Sequence]) -> HirExpr {
if sequences.is_empty() {
return HirExpr::Empty;
}
let atoms = leading_range_atoms(sequences);
let mut groups: std::collections::BTreeMap<(u8, u8), Vec<Utf8Sequence>> =
std::collections::BTreeMap::new();
for seq in sequences {
let Some(&(lo, hi)) = seq.ranges.first() else {
continue;
};
for &atom in atoms.iter().filter(|(a, b)| *a >= lo && *b <= hi) {
groups
.entry(atom)
.or_default()
.push(Utf8Sequence::new(seq.ranges[1..].to_vec()));
}
}
let mut alternatives: Vec<HirExpr> = Vec::new();
for ((lo, hi), suffixes) in groups {
let first_class = HirExpr::Class(HirClass::new(vec![(lo, hi)], false));
if suffixes.is_empty() || suffixes.iter().all(|s| s.ranges.is_empty()) {
alternatives.push(first_class);
} else {
let non_empty: Vec<_> = suffixes
.into_iter()
.filter(|s| !s.ranges.is_empty())
.collect();
if non_empty.is_empty() {
alternatives.push(first_class);
} else {
let suffix_expr = self.build_utf8_trie(&non_empty);
alternatives.push(HirExpr::Concat(vec![first_class, suffix_expr]));
}
}
}
match alternatives.len() {
0 => HirExpr::Empty,
1 => alternatives.pop().unwrap(),
_ => HirExpr::Alt(alternatives),
}
}
fn build_negated_unicode_class(
&mut self,
byte_ranges: Vec<(u8, u8)>,
utf8_sequences: Vec<Utf8Sequence>,
) -> HirExpr {
let mut codepoint_ranges: Vec<(u32, u32)> = Vec::new();
for (start, end) in &byte_ranges {
codepoint_ranges.push((*start as u32, *end as u32));
}
for seq in &utf8_sequences {
if let Some(range) = self.utf8_sequence_to_code_point_range(seq) {
codepoint_ranges.push(range);
}
}
codepoint_ranges.sort_by_key(|r| r.0);
let merged = merge_codepoint_ranges(codepoint_ranges);
self.props.has_large_unicode_class = true;
HirExpr::UnicodeCpClass(CodepointClass::new(merged, true))
}
fn build_unicode_codepoint_class(
&mut self,
byte_ranges: Vec<(u8, u8)>,
utf8_sequences: Vec<Utf8Sequence>,
negated: bool,
) -> HirExpr {
self.props.has_large_unicode_class = true;
let mut code_point_ranges = Vec::new();
for (start, end) in byte_ranges {
code_point_ranges.push((start as u32, end as u32));
}
for seq in utf8_sequences {
if let Some(range) = self.utf8_sequence_to_code_point_range(&seq) {
code_point_ranges.push(range);
}
}
code_point_ranges.sort_by_key(|r| r.0);
let merged = merge_codepoint_ranges(code_point_ranges);
HirExpr::UnicodeCpClass(CodepointClass::new(merged, negated))
}
fn utf8_sequence_to_code_point_range(&self, seq: &Utf8Sequence) -> Option<(u32, u32)> {
match seq.len() {
1 => {
let (start, end) = seq.ranges[0];
Some((start as u32, end as u32))
}
2 => {
let (b1_start, b1_end) = seq.ranges[0];
let (b2_start, b2_end) = seq.ranges[1];
let start = (((b1_start & 0x1F) as u32) << 6) | ((b2_start & 0x3F) as u32);
let end = (((b1_end & 0x1F) as u32) << 6) | ((b2_end & 0x3F) as u32);
Some((start, end))
}
3 => {
let (b1_start, b1_end) = seq.ranges[0];
let (b2_start, b2_end) = seq.ranges[1];
let (b3_start, b3_end) = seq.ranges[2];
let start = (((b1_start & 0x0F) as u32) << 12)
| (((b2_start & 0x3F) as u32) << 6)
| ((b3_start & 0x3F) as u32);
let end = (((b1_end & 0x0F) as u32) << 12)
| (((b2_end & 0x3F) as u32) << 6)
| ((b3_end & 0x3F) as u32);
Some((start, end))
}
4 => {
let (b1_start, b1_end) = seq.ranges[0];
let (b2_start, b2_end) = seq.ranges[1];
let (b3_start, b3_end) = seq.ranges[2];
let (b4_start, b4_end) = seq.ranges[3];
let start = (((b1_start & 0x07) as u32) << 18)
| (((b2_start & 0x3F) as u32) << 12)
| (((b3_start & 0x3F) as u32) << 6)
| ((b4_start & 0x3F) as u32);
let end = (((b1_end & 0x07) as u32) << 18)
| (((b2_end & 0x3F) as u32) << 12)
| (((b3_end & 0x3F) as u32) << 6)
| ((b4_end & 0x3F) as u32);
Some((start, end))
}
_ => None,
}
}
fn translate_lookaround(&mut self, la: &Lookaround) -> Result<HirExpr> {
self.props.has_lookaround = true;
let expr = self.translate_expr(&la.expr)?;
let kind = match la.kind {
LookaroundKind::PositiveLookahead => HirLookaroundKind::PositiveLookahead,
LookaroundKind::NegativeLookahead => HirLookaroundKind::NegativeLookahead,
LookaroundKind::PositiveLookbehind => HirLookaroundKind::PositiveLookbehind,
LookaroundKind::NegativeLookbehind => HirLookaroundKind::NegativeLookbehind,
};
Ok(HirExpr::Lookaround(Box::new(HirLookaround { expr, kind })))
}
fn is_simple_unicode_class(expr: &HirExpr) -> bool {
match expr {
HirExpr::Class(_) => true,
HirExpr::Alt(alts) => {
alts.iter().all(|alt| {
match alt {
HirExpr::Class(_) => true,
HirExpr::Concat(parts) => {
parts
.iter()
.all(|p| matches!(p, HirExpr::Class(_) | HirExpr::Literal(_)))
}
_ => false,
}
})
}
_ => false,
}
}
}
impl Default for HirTranslator {
fn default() -> Self {
Self::new()
}
}
fn merge_byte_ranges(mut ranges: Vec<(u8, u8)>) -> Vec<(u8, u8)> {
if ranges.is_empty() {
return ranges;
}
ranges.sort_by_key(|r| r.0);
let mut merged = vec![ranges[0]];
for range in ranges.into_iter().skip(1) {
let last = merged.last_mut().unwrap();
if range.0 <= last.1.saturating_add(1) {
last.1 = last.1.max(range.1);
} else {
merged.push(range);
}
}
merged
}
fn merge_codepoint_ranges(mut ranges: Vec<(u32, u32)>) -> Vec<(u32, u32)> {
if ranges.is_empty() {
return ranges;
}
ranges.sort_by_key(|r| r.0);
let mut merged = vec![ranges[0]];
for range in ranges.into_iter().skip(1) {
let last = merged.last_mut().unwrap();
if range.0 <= last.1.saturating_add(1) {
last.1 = last.1.max(range.1);
} else {
merged.push(range);
}
}
merged
}
fn leading_range_atoms(sequences: &[Utf8Sequence]) -> Vec<(u8, u8)> {
let mut cuts: Vec<u16> = Vec::with_capacity(sequences.len() * 2);
for seq in sequences {
if let Some(&(lo, hi)) = seq.ranges.first() {
cuts.push(lo as u16);
cuts.push(hi as u16 + 1);
}
}
cuts.sort_unstable();
cuts.dedup();
let mut atoms = Vec::with_capacity(cuts.len());
for pair in cuts.windows(2) {
let (Some(&start), Some(&end)) = (pair.first(), pair.get(1)) else {
continue;
};
if let (Ok(lo), Ok(hi)) = (u8::try_from(start), u8::try_from(end - 1)) {
atoms.push((lo, hi));
}
}
atoms
}
const MAX_TRIE_SEQUENCES: usize = 64;
fn pins_codepoint_engine(expr: &Expr) -> bool {
!contains_backref(expr) && pins_without_backref(expr)
}
fn pins_without_backref(expr: &Expr) -> bool {
match expr {
Expr::Alt(branches) => branches.iter().any(pins_without_backref),
Expr::Lookaround(_) => true,
Expr::Repeat(repeat) => !repeat.greedy || pins_without_backref(&repeat.expr),
Expr::Concat(exprs) => exprs.iter().any(pins_without_backref),
Expr::Group(group) => pins_without_backref(&group.expr),
Expr::Backref(_)
| Expr::Empty
| Expr::Literal(_)
| Expr::Class(_)
| Expr::Anchor(_)
| Expr::Dot
| Expr::GraphemeCluster
| Expr::UnicodeProperty { .. }
| Expr::PerlClass(_)
| Expr::LineBreak
| Expr::AnyExceptNewline => false,
}
}
fn contains_backref(expr: &Expr) -> bool {
match expr {
Expr::Backref(_) => true,
Expr::Alt(branches) => branches.iter().any(contains_backref),
Expr::Concat(exprs) => exprs.iter().any(contains_backref),
Expr::Repeat(repeat) => contains_backref(&repeat.expr),
Expr::Group(group) => contains_backref(&group.expr),
Expr::Lookaround(lookaround) => contains_backref(&lookaround.expr),
Expr::Empty
| Expr::Literal(_)
| Expr::Class(_)
| Expr::Anchor(_)
| Expr::Dot
| Expr::GraphemeCluster
| Expr::UnicodeProperty { .. }
| Expr::PerlClass(_)
| Expr::LineBreak
| Expr::AnyExceptNewline => false,
}
}
fn class_codepoint_ranges(
byte_ranges: &[(u8, u8)],
utf8_sequences: &[Utf8Sequence],
translator: &HirTranslator,
) -> Vec<(u32, u32)> {
let mut ranges: Vec<(u32, u32)> = byte_ranges
.iter()
.map(|&(start, end)| (start as u32, end as u32))
.collect();
for seq in utf8_sequences {
if let Some(range) = translator.utf8_sequence_to_code_point_range(seq) {
ranges.push(range);
}
}
ranges.sort_by_key(|range| range.0);
merge_codepoint_ranges(ranges)
}
fn push_codepoint_range(
start_cp: u32,
end_cp: u32,
byte_ranges: &mut Vec<(u8, u8)>,
utf8_sequences: &mut Vec<Utf8Sequence>,
) {
if start_cp <= 127 && end_cp <= 127 {
byte_ranges.push((start_cp as u8, end_cp as u8));
return;
}
let utf8_start = if start_cp <= 127 {
byte_ranges.push((start_cp as u8, 127));
128
} else {
start_cp
};
for seq in compile_utf8_range(utf8_start, end_cp) {
if seq.len() == 1 {
byte_ranges.push(seq.ranges[0]);
} else {
utf8_sequences.push(seq);
}
}
}
fn any_non_ascii_character() -> Vec<Utf8Sequence> {
vec![
Utf8Sequence::new(vec![(0xc2, 0xdf), (0x80, 0xbf)]),
Utf8Sequence::new(vec![(0xe0, 0xef), (0x80, 0xbf), (0x80, 0xbf)]),
Utf8Sequence::new(vec![(0xf0, 0xf4), (0x80, 0xbf), (0x80, 0xbf), (0x80, 0xbf)]),
]
}
fn complement_within_ascii(excluded: &[(u8, u8)]) -> Vec<(u8, u8)> {
let mut sorted = excluded.to_vec();
sorted.sort_unstable();
let mut out = Vec::new();
let mut next = 0u16;
for (lo, hi) in sorted {
if lo as u16 > next {
out.push((next as u8, lo - 1));
}
next = next.max(hi as u16 + 1);
}
if next <= 0x7f {
out.push((next as u8, 0x7f));
}
out
}
pub const DEFAULT_EXPANDED_SIZE: u32 = 10_000;
fn expanded_size(expr: &HirExpr) -> u32 {
match expr {
HirExpr::Empty | HirExpr::Anchor(_) | HirExpr::Backref(_) => 0,
HirExpr::Class(_) | HirExpr::UnicodeCpClass(_) => 1,
HirExpr::Literal(bytes) => u32::try_from(bytes.len()).unwrap_or(u32::MAX),
HirExpr::Concat(exprs) | HirExpr::Alt(exprs) => exprs
.iter()
.fold(0u32, |total, e| total.saturating_add(expanded_size(e))),
HirExpr::Capture(capture) => expanded_size(&capture.expr),
HirExpr::Lookaround(look) => expanded_size(&look.expr),
HirExpr::Repeat(repeat) => {
let copies = repeat.max.unwrap_or(repeat.min).max(1);
expanded_size(&repeat.expr).saturating_mul(copies)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse;
#[test]
fn test_translate_literal() {
let ast = parse("abc").unwrap();
let hir = HirTranslator::new().translate(&ast).unwrap();
assert!(matches!(hir.expr, HirExpr::Concat(_)));
}
#[test]
fn test_translate_class() {
let ast = parse("[a-z]").unwrap();
let hir = HirTranslator::new().translate(&ast).unwrap();
if let HirExpr::Class(cls) = hir.expr {
assert_eq!(cls.ranges, vec![(b'a', b'z')]);
} else {
panic!("Expected Class");
}
}
#[test]
fn test_merge_ranges() {
let ranges = vec![(1, 3), (2, 5), (7, 9)];
let merged = merge_byte_ranges(ranges);
assert_eq!(merged, vec![(1, 5), (7, 9)]);
}
fn contains_codepoint_class(expr: &HirExpr) -> bool {
match expr {
HirExpr::UnicodeCpClass(_) => true,
HirExpr::Concat(exprs) | HirExpr::Alt(exprs) => {
exprs.iter().any(contains_codepoint_class)
}
HirExpr::Repeat(repeat) => contains_codepoint_class(&repeat.expr),
HirExpr::Capture(capture) => contains_codepoint_class(&capture.expr),
HirExpr::Lookaround(look) => contains_codepoint_class(&look.expr),
HirExpr::Empty
| HirExpr::Literal(_)
| HirExpr::Class(_)
| HirExpr::Anchor(_)
| HirExpr::Backref(_) => false,
}
}
#[test]
fn test_translate_full_codepoint_range() {
let ast = parse("[\\x00-\\xff]").unwrap();
let hir = HirTranslator::new().translate(&ast).unwrap();
assert!(
!contains_codepoint_class(&hir.expr),
"small class should lower to bytes, got {:?}",
hir.expr
);
let re = crate::Regex::new("^[\\x00-\\xff]$").unwrap();
assert!(re.is_match("\u{0}"));
assert!(re.is_match("é"));
assert!(!re.is_match("Ā"), "U+0100 is outside the class");
}
#[test]
fn test_translate_high_codepoint_range() {
let ast = parse("[\\x80-\\xff]").unwrap();
let hir = HirTranslator::new().translate(&ast).unwrap();
assert!(
!contains_codepoint_class(&hir.expr),
"small class should lower to bytes, got {:?}",
hir.expr
);
let re = crate::Regex::new("^[\\x80-\\xff]$").unwrap();
assert!(re.is_match("\u{80}"));
assert!(re.is_match("ÿ"));
assert!(!re.is_match("a"));
assert!(!re.is_match("Ā"));
}
#[test]
fn test_translate_unicode_class_greek() {
let ast = parse("[α-ω]").unwrap();
let hir = HirTranslator::new().translate(&ast).unwrap();
assert!(
!contains_codepoint_class(&hir.expr),
"small Unicode range should lower to bytes, got {:?}",
hir.expr
);
let re = crate::Regex::new("^[α-ω]$").unwrap();
assert!(re.is_match("α"));
assert!(re.is_match("ω"));
assert!(!re.is_match("Α"), "uppercase alpha is outside the range");
assert!(!re.is_match("a"));
}
#[test]
fn test_translate_unicode_single_char() {
let ast = parse("[α]").unwrap();
let hir = HirTranslator::new().translate(&ast).unwrap();
assert!(
!contains_codepoint_class(&hir.expr),
"single multi-byte char should lower to bytes, got {:?}",
hir.expr
);
let re = crate::Regex::new("^[α]$").unwrap();
assert!(re.is_match("α"));
assert!(!re.is_match("β"));
}
#[test]
fn test_translate_mixed_ascii_unicode() {
let re = crate::Regex::new("[a-zα-ω]").unwrap();
assert!(re.is_match("a"));
assert!(re.is_match("z"));
assert!(re.is_match("α"));
assert!(re.is_match("ω"));
assert!(!re.is_match("A"));
assert!(!re.is_match("1"));
}
#[test]
fn test_translate_emoji_class() {
let re = crate::Regex::new("[😀-😂]").unwrap();
assert!(re.is_match("😀"));
assert!(re.is_match("😁"));
assert!(re.is_match("😂"));
assert!(!re.is_match("a"));
}
#[test]
fn test_backref_validation() {
let ast = parse(r"(a)\1").unwrap();
let result = HirTranslator::new().translate(&ast);
assert!(result.is_ok(), "Valid backref \\1 with 1 group should work");
let ast = parse(r"(a)(b)\1\2").unwrap();
let result = HirTranslator::new().translate(&ast);
assert!(
result.is_ok(),
"Valid backrefs \\1\\2 with 2 groups should work"
);
let ast = parse(r"\1").unwrap();
let result = HirTranslator::new().translate(&ast);
assert!(result.is_err(), "Backref \\1 with no groups should fail");
let ast = parse(r"(a)\2").unwrap();
let result = HirTranslator::new().translate(&ast);
assert!(result.is_err(), "Backref \\2 with only 1 group should fail");
}
#[test]
fn test_named_groups_tracking() {
let ast = parse(r"(?<word>\w+)").unwrap();
println!("AST: {:?}", ast);
let hir = HirTranslator::new().translate(&ast).unwrap();
println!("HIR props: {:?}", hir.props);
println!("Named groups: {:?}", hir.props.named_groups);
assert_eq!(hir.props.named_groups.len(), 1);
assert_eq!(hir.props.named_groups.get("word"), Some(&1));
let ast = parse(r"(?P<foo>\d+)").unwrap();
let hir = HirTranslator::new().translate(&ast).unwrap();
assert_eq!(hir.props.named_groups.len(), 1);
assert_eq!(hir.props.named_groups.get("foo"), Some(&1));
let ast = parse(r"(?<a>\w)(?<b>\d)").unwrap();
let hir = HirTranslator::new().translate(&ast).unwrap();
assert_eq!(hir.props.named_groups.len(), 2);
assert_eq!(hir.props.named_groups.get("a"), Some(&1));
assert_eq!(hir.props.named_groups.get("b"), Some(&2));
}
#[test]
fn test_small_perl_classes_are_not_large_unicode() {
for pattern in [r"\s", r"\S", r"\s+", r"\S+", r"\h", r"\H"] {
let ast = parse(pattern).unwrap();
let hir = HirTranslator::new().translate(&ast).unwrap();
assert!(
!hir.props.has_large_unicode_class,
"{pattern} lowers to a byte trie and must not be flagged large"
);
assert!(
!contains_codepoint_class(&hir.expr),
"{pattern} should contain no code-point node"
);
}
}
#[test]
fn test_large_unicode_class_detection() {
let ast = parse(r"\p{Han}").unwrap();
let hir = HirTranslator::new().translate(&ast).unwrap();
assert!(
hir.props.has_large_unicode_class,
"\\p{{Han}} should be detected as large unicode class"
);
let ast = parse(r"[a-z]").unwrap();
let hir = HirTranslator::new().translate(&ast).unwrap();
assert!(
!hir.props.has_large_unicode_class,
"[a-z] should not be large"
);
let ast = parse(r"[α-ω]").unwrap();
let hir = HirTranslator::new().translate(&ast).unwrap();
assert!(
!hir.props.has_large_unicode_class,
"[α-ω] lowers to a small trie and should not be flagged large"
);
}
}