use super::shared::PatternStep;
use crate::nfa::{ByteClass, ByteRange, Nfa, NfaInstruction, NfaState, StateId};
pub fn combine_greedy_with_lookahead(steps: Vec<PatternStep>) -> Vec<PatternStep> {
let mut result = Vec::with_capacity(steps.len());
let mut i = 0;
while i < steps.len() {
match &steps[i] {
PatternStep::GreedyPlus(ranges) if i + 2 == steps.len() => match &steps[i + 1] {
PatternStep::PositiveLookahead(inner) => {
result.push(PatternStep::GreedyPlusLookahead(
ranges.clone(),
inner.clone(),
true,
));
i += 2;
continue;
}
PatternStep::NegativeLookahead(inner) => {
result.push(PatternStep::GreedyPlusLookahead(
ranges.clone(),
inner.clone(),
false,
));
i += 2;
continue;
}
_ => {}
},
PatternStep::GreedyStar(ranges) if i + 2 == steps.len() => match &steps[i + 1] {
PatternStep::PositiveLookahead(inner) => {
result.push(PatternStep::GreedyStarLookahead(
ranges.clone(),
inner.clone(),
true,
));
i += 2;
continue;
}
PatternStep::NegativeLookahead(inner) => {
result.push(PatternStep::GreedyStarLookahead(
ranges.clone(),
inner.clone(),
false,
));
i += 2;
continue;
}
_ => {}
},
_ => {}
}
result.push(steps[i].clone());
i += 1;
}
result
}
#[derive(Default, PartialEq, Eq)]
pub(crate) struct AssertionTally {
lookahead: usize,
lookbehind: usize,
anchor: usize,
word_boundary: usize,
backref: usize,
}
impl AssertionTally {
fn add(&mut self, other: Self) {
self.lookahead += other.lookahead;
self.lookbehind += other.lookbehind;
self.anchor += other.anchor;
self.word_boundary += other.word_boundary;
self.backref += other.backref;
}
}
pub(crate) const MAX_LOOKBEHIND_WIDTHS: usize = 16;
pub(crate) fn byte_len_set(steps: &[PatternStep]) -> Option<Vec<usize>> {
let mut totals = vec![0usize];
for step in steps {
let step_widths: Vec<usize> = match step {
PatternStep::Byte(_) | PatternStep::ByteClass(_) => vec![1],
PatternStep::CodepointClass(cpclass, _) => utf8_width_set(cpclass)?,
PatternStep::WordBoundary
| PatternStep::NotWordBoundary
| PatternStep::StartOfText
| PatternStep::EndOfText
| PatternStep::StartOfLine
| PatternStep::EndOfLine
| PatternStep::CaptureStart(_)
| PatternStep::CaptureEnd(_)
| PatternStep::PositiveLookahead(_)
| PatternStep::NegativeLookahead(_)
| PatternStep::PositiveLookbehind(_, _)
| PatternStep::NegativeLookbehind(_, _) => vec![0],
PatternStep::GreedyPlus(_)
| PatternStep::GreedyStar(_)
| PatternStep::GreedyPlusLookahead(_, _, _)
| PatternStep::GreedyStarLookahead(_, _, _)
| PatternStep::NonGreedyPlus(_, _)
| PatternStep::NonGreedyStar(_, _)
| PatternStep::GreedyCodepointPlus(_)
| PatternStep::Alt(_)
| PatternStep::Backref(_) => return None,
};
let mut next = Vec::with_capacity(totals.len() * step_widths.len());
for &total in &totals {
for &width in &step_widths {
next.push(total + width);
}
}
next.sort_unstable();
next.dedup();
if next.len() > MAX_LOOKBEHIND_WIDTHS {
return None;
}
totals = next;
}
Some(totals)
}
#[cfg_attr(
not(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64"))),
allow(dead_code)
)]
pub(crate) fn fixed_byte_len(steps: &[PatternStep]) -> Option<usize> {
match byte_len_set(steps)?.as_slice() {
[width] => Some(*width),
_ => None,
}
}
#[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
pub(crate) fn min_byte_len(steps: &[PatternStep]) -> usize {
steps
.iter()
.map(|s| match s {
PatternStep::Byte(_) | PatternStep::ByteClass(_) => 1,
PatternStep::CodepointClass(_, _) | PatternStep::GreedyCodepointPlus(_) => 1,
PatternStep::GreedyPlus(_) | PatternStep::GreedyPlusLookahead(_, _, _) => 1,
PatternStep::GreedyStar(_) | PatternStep::GreedyStarLookahead(_, _, _) => 0,
PatternStep::NonGreedyPlus(_, suffix) => 1 + min_byte_len(std::slice::from_ref(suffix)),
PatternStep::NonGreedyStar(_, suffix) => min_byte_len(std::slice::from_ref(suffix)),
PatternStep::Alt(branches) => {
branches.iter().map(|b| min_byte_len(b)).min().unwrap_or(0)
}
PatternStep::CaptureStart(_)
| PatternStep::CaptureEnd(_)
| PatternStep::WordBoundary
| PatternStep::NotWordBoundary
| PatternStep::StartOfText
| PatternStep::EndOfText
| PatternStep::StartOfLine
| PatternStep::EndOfLine
| PatternStep::PositiveLookahead(_)
| PatternStep::NegativeLookahead(_)
| PatternStep::PositiveLookbehind(_, _)
| PatternStep::NegativeLookbehind(_, _)
| PatternStep::Backref(_) => 0,
})
.sum()
}
fn utf8_width_set(cpclass: &crate::hir::CodepointClass) -> Option<Vec<usize>> {
if cpclass.negated || cpclass.ranges.is_empty() {
return None;
}
let bounds = crate::nfa::utf8_automata::UTF8_WIDTH_BOUNDARIES;
let mut widths = Vec::new();
for &(start, end) in &cpclass.ranges {
for band in 0..4 {
if start < bounds[band + 1] && end >= bounds[band] && !widths.contains(&(band + 1)) {
widths.push(band + 1);
}
}
}
if widths.is_empty() {
return None;
}
widths.sort_unstable();
Some(widths)
}
pub(crate) enum TerminalAssertion {
Nothing,
Step(PatternStep),
Unsupported,
}
pub(crate) fn terminal_assertion(state: &NfaState) -> TerminalAssertion {
match state.instruction {
None => TerminalAssertion::Nothing,
Some(NfaInstruction::CaptureStart(_) | NfaInstruction::CaptureEnd(_)) => {
TerminalAssertion::Nothing
}
Some(NfaInstruction::WordBoundary) => TerminalAssertion::Step(PatternStep::WordBoundary),
Some(NfaInstruction::NotWordBoundary) => {
TerminalAssertion::Step(PatternStep::NotWordBoundary)
}
Some(NfaInstruction::StartOfText) => TerminalAssertion::Step(PatternStep::StartOfText),
Some(NfaInstruction::EndOfText) => TerminalAssertion::Step(PatternStep::EndOfText),
Some(NfaInstruction::StartOfLine) => TerminalAssertion::Step(PatternStep::StartOfLine),
Some(NfaInstruction::EndOfLine) => TerminalAssertion::Step(PatternStep::EndOfLine),
Some(_) => TerminalAssertion::Unsupported,
}
}
fn nfa_has_lookaround(nfa: &Nfa) -> bool {
nfa.states.iter().any(|s| {
matches!(
s.instruction,
Some(
NfaInstruction::PositiveLookahead(_)
| NfaInstruction::NegativeLookahead(_)
| NfaInstruction::PositiveLookbehind(_)
| NfaInstruction::NegativeLookbehind(_)
)
)
})
}
pub(crate) fn count_assertions_in_nfa(nfa: &Nfa) -> AssertionTally {
let mut t = AssertionTally::default();
for s in &nfa.states {
match s.instruction {
Some(
NfaInstruction::PositiveLookahead(ref inner)
| NfaInstruction::NegativeLookahead(ref inner),
) => {
t.lookahead += 1;
t.add(count_assertions_in_nfa(inner));
}
Some(
NfaInstruction::PositiveLookbehind(ref inner)
| NfaInstruction::NegativeLookbehind(ref inner),
) => {
t.lookbehind += 1;
t.add(count_assertions_in_nfa(inner));
}
Some(
NfaInstruction::StartOfText
| NfaInstruction::EndOfText
| NfaInstruction::StartOfLine
| NfaInstruction::EndOfLine,
) => t.anchor += 1,
Some(NfaInstruction::WordBoundary | NfaInstruction::NotWordBoundary) => {
t.word_boundary += 1
}
Some(NfaInstruction::Backref(_)) => t.backref += 1,
_ => {}
}
}
t
}
pub(crate) fn count_assertions_in_steps(steps: &[PatternStep]) -> AssertionTally {
let mut t = AssertionTally::default();
for step in steps {
match step {
PatternStep::PositiveLookahead(inner) | PatternStep::NegativeLookahead(inner) => {
t.lookahead += 1;
t.add(count_assertions_in_steps(inner));
}
PatternStep::GreedyPlusLookahead(_, inner, _)
| PatternStep::GreedyStarLookahead(_, inner, _) => {
t.lookahead += 1;
t.add(count_assertions_in_steps(inner));
}
PatternStep::PositiveLookbehind(inner, _)
| PatternStep::NegativeLookbehind(inner, _) => {
t.lookbehind += 1;
t.add(count_assertions_in_steps(inner));
}
PatternStep::StartOfText
| PatternStep::EndOfText
| PatternStep::StartOfLine
| PatternStep::EndOfLine => t.anchor += 1,
PatternStep::WordBoundary | PatternStep::NotWordBoundary => t.word_boundary += 1,
PatternStep::Backref(_) => t.backref += 1,
PatternStep::Alt(branches) => {
for b in branches {
t.add(count_assertions_in_steps(b));
}
}
_ => {}
}
}
t
}
#[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
pub(crate) fn jit_must_defer(steps: &[PatternStep]) -> bool {
let counts = count_step_kinds(steps);
let quantifiers = counts.greedy + counts.combined;
if quantifiers >= 2 {
return true;
}
quantifiers >= 1 && counts.lookaround >= 1
}
#[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
#[derive(Default, Clone, Copy)]
struct StepKinds {
greedy: usize,
combined: usize,
lookaround: usize,
}
#[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
fn count_step_kinds(steps: &[PatternStep]) -> StepKinds {
let mut counts = StepKinds::default();
for step in steps {
match step {
PatternStep::GreedyPlus(_)
| PatternStep::GreedyStar(_)
| PatternStep::GreedyCodepointPlus(_)
| PatternStep::NonGreedyPlus(_, _)
| PatternStep::NonGreedyStar(_, _) => counts.greedy += 1,
PatternStep::GreedyPlusLookahead(_, _, _)
| PatternStep::GreedyStarLookahead(_, _, _) => counts.combined += 1,
PatternStep::PositiveLookahead(_)
| PatternStep::NegativeLookahead(_)
| PatternStep::PositiveLookbehind(_, _)
| PatternStep::NegativeLookbehind(_, _) => counts.lookaround += 1,
PatternStep::Alt(branches) => {
let mut worst = StepKinds::default();
for branch in branches {
let branch_counts = count_step_kinds(branch);
worst.greedy = worst.greedy.max(branch_counts.greedy);
worst.combined = worst.combined.max(branch_counts.combined);
worst.lookaround = worst.lookaround.max(branch_counts.lookaround);
}
counts.greedy += worst.greedy;
counts.combined += worst.combined;
counts.lookaround += worst.lookaround;
}
_ => {}
}
}
counts
}
pub(crate) fn greedy_star_body(
nfa: &Nfa,
enter: StateId,
exit: StateId,
) -> Option<(Vec<ByteRange>, StateId)> {
let body = nfa.get(enter)?;
if !body.epsilon.is_empty() || body.instruction.is_some() || body.is_match {
return None;
}
let &(_, loop_id) = body.transitions.first()?;
if !body.transitions.iter().all(|(_, t)| *t == loop_id) {
return None;
}
let loop_state = nfa.get(loop_id)?;
if !loop_state.transitions.is_empty() || loop_state.instruction.is_some() || loop_state.is_match
{
return None;
}
let [loop_back, loop_exit] = loop_state.epsilon.as_slice() else {
return None;
};
if *loop_back != enter || *loop_exit != exit {
return None;
}
let ranges = body.transitions.iter().map(|(r, _)| *r).collect();
Some((ranges, loop_id))
}
const MAX_EXTRACTED_STEPS: usize = 200_000;
pub struct StepExtractor<'a> {
nfa: &'a Nfa,
budget: std::cell::Cell<usize>,
}
impl<'a> StepExtractor<'a> {
pub fn new(nfa: &'a Nfa) -> Self {
Self {
nfa,
budget: std::cell::Cell::new(MAX_EXTRACTED_STEPS),
}
}
fn charge(&self) -> bool {
let remaining = self.budget.get();
if remaining == 0 {
false
} else {
self.budget.set(remaining - 1);
true
}
}
pub fn extract(&self) -> Option<Vec<PatternStep>> {
let mut visited = vec![false; self.nfa.states.len()];
let steps = self.extract_from_state(self.nfa.start, &mut visited);
if steps.is_empty() {
return None;
}
if count_assertions_in_steps(&steps) != count_assertions_in_nfa(self.nfa) {
return None;
}
Some(combine_greedy_with_lookahead(steps))
}
fn alternation_loops_back(&self, state: StateId) -> bool {
let mut seen = vec![false; self.nfa.states.len()];
let mut stack: Vec<StateId> = Vec::new();
let Some(root) = self.nfa.get(state) else {
return true;
};
stack.extend(root.epsilon.iter().copied());
stack.extend(root.transitions.iter().map(|&(_, target)| target));
while let Some(id) = stack.pop() {
if id == state {
return true;
}
match seen.get_mut(id as usize) {
Some(flag) if !*flag => *flag = true,
Some(_) => continue,
None => return true,
}
let Some(next) = self.nfa.get(id) else {
return true;
};
stack.extend(next.epsilon.iter().copied());
stack.extend(next.transitions.iter().map(|&(_, target)| target));
}
false
}
fn extract_from_state(&self, start: StateId, visited: &mut [bool]) -> Vec<PatternStep> {
let mut steps = Vec::new();
let mut current = start;
let mut iteration = 0;
loop {
{
iteration += 1;
if iteration > 1000 {
return Vec::new();
}
}
if !self.charge() {
return Vec::new();
}
if current as usize >= self.nfa.states.len() {
return Vec::new();
}
let state = &self.nfa.states[current as usize];
if let Some(ref instr) = state.instruction {
match instr {
NfaInstruction::CaptureStart(_) | NfaInstruction::CaptureEnd(_) => {
}
NfaInstruction::WordBoundary => {
steps.push(PatternStep::WordBoundary);
}
NfaInstruction::NotWordBoundary => {
steps.push(PatternStep::NotWordBoundary);
}
NfaInstruction::StartOfText => {
steps.push(PatternStep::StartOfText);
}
NfaInstruction::EndOfText => {
steps.push(PatternStep::EndOfText);
}
NfaInstruction::PositiveLookahead(inner_nfa) => {
let inner_steps = self.extract_lookaround_steps(inner_nfa);
if inner_steps.is_empty() {
return Vec::new();
}
steps.push(PatternStep::PositiveLookahead(inner_steps));
}
NfaInstruction::NegativeLookahead(inner_nfa) => {
let inner_steps = self.extract_lookaround_steps(inner_nfa);
if inner_steps.is_empty() {
return Vec::new();
}
steps.push(PatternStep::NegativeLookahead(inner_steps));
}
NfaInstruction::PositiveLookbehind(inner_nfa) => {
let inner_steps = self.extract_lookbehind_steps(inner_nfa);
if inner_steps.is_empty() {
return Vec::new();
}
let Some(widths) = byte_len_set(&inner_steps) else {
return Vec::new();
};
steps.push(PatternStep::PositiveLookbehind(inner_steps, widths));
}
NfaInstruction::NegativeLookbehind(inner_nfa) => {
let inner_steps = self.extract_lookbehind_steps(inner_nfa);
if inner_steps.is_empty() {
return Vec::new();
}
let Some(widths) = byte_len_set(&inner_steps) else {
return Vec::new();
};
steps.push(PatternStep::NegativeLookbehind(inner_steps, widths));
}
NfaInstruction::CodepointClass(cpclass, target) => {
if (*target as usize) < self.nfa.states.len() {
let target_state = &self.nfa.states[*target as usize];
if target_state.epsilon.len() == 2
&& target_state.transitions.is_empty()
{
let eps0 = target_state.epsilon[0];
let eps1 = target_state.epsilon[1];
if eps0 == current {
steps.push(PatternStep::GreedyCodepointPlus(cpclass.clone()));
visited[current as usize] = true;
visited[*target as usize] = true;
current = eps1;
continue;
} else if eps1 == current {
steps.push(PatternStep::GreedyCodepointPlus(cpclass.clone()));
visited[current as usize] = true;
visited[*target as usize] = true;
current = eps0;
continue;
}
}
}
steps.push(PatternStep::CodepointClass(cpclass.clone(), *target));
if visited[current as usize] {
return Vec::new();
}
visited[current as usize] = true;
current = *target;
continue;
}
_ => {
return Vec::new();
}
}
}
if !state.transitions.is_empty() {
let target = state.transitions[0].1;
if !state.transitions.iter().all(|(_, t)| *t == target) {
return Vec::new();
}
let ranges: Vec<ByteRange> = state.transitions.iter().map(|(r, _)| *r).collect();
let target_state = &self.nfa.states[target as usize];
if target_state.epsilon.len() == 2 && target_state.transitions.is_empty() {
let eps0 = target_state.epsilon[0];
let eps1 = target_state.epsilon[1];
if eps0 == current {
steps.push(PatternStep::GreedyPlus(ByteClass::new(ranges)));
if visited[target as usize] {
return Vec::new();
}
visited[target as usize] = true;
current = eps1;
continue;
}
}
if visited[current as usize] {
return Vec::new();
}
visited[current as usize] = true;
if ranges.len() == 1 && ranges[0].start == ranges[0].end {
steps.push(PatternStep::Byte(ranges[0].start));
} else {
steps.push(PatternStep::ByteClass(ByteClass::new(ranges)));
}
current = target;
continue;
}
if state.epsilon.len() == 1 {
if visited[current as usize] {
return Vec::new();
}
visited[current as usize] = true;
current = state.epsilon[0];
continue;
}
if let [enter, exit] = state.epsilon.as_slice() {
let (enter, exit) = (*enter, *exit);
if let Some((ranges, loop_state)) = greedy_star_body(self.nfa, enter, exit) {
let fresh = [current, enter, loop_state]
.iter()
.all(|&id| visited.get(id as usize).is_some_and(|seen| !*seen));
if fresh {
steps.push(PatternStep::GreedyStar(ByteClass::new(ranges)));
for id in [current, enter, loop_state] {
if let Some(seen) = visited.get_mut(id as usize) {
*seen = true;
}
}
current = exit;
continue;
}
}
}
if state.epsilon.len() >= 2 {
let mut alternatives: Vec<Vec<PatternStep>> = Vec::new();
for &target in state.epsilon.iter() {
let mut branch_visited = visited.to_vec();
branch_visited[current as usize] = true;
let Some(branch_steps) = self.extract_branch(target, &mut branch_visited)
else {
return Vec::new();
};
alternatives.push(branch_steps);
}
steps.push(PatternStep::Alt(alternatives));
break; }
break;
}
steps
}
fn extract_branch(&self, start: StateId, visited: &mut [bool]) -> Option<Vec<PatternStep>> {
let mut steps = Vec::new();
let mut current = start;
let mut iteration = 0;
loop {
{
iteration += 1;
if iteration > 10000 {
return None;
}
}
if !self.charge() {
return None;
}
if current as usize >= self.nfa.states.len() {
return None;
}
if visited[current as usize] {
return None;
}
let state = &self.nfa.states[current as usize];
if state.is_match {
match terminal_assertion(state) {
TerminalAssertion::Nothing => {}
TerminalAssertion::Step(step) => steps.push(step),
TerminalAssertion::Unsupported => return None,
}
break;
}
if let Some(ref instr) = state.instruction {
match instr {
NfaInstruction::CaptureStart(_) | NfaInstruction::CaptureEnd(_) => {
}
NfaInstruction::WordBoundary => {
steps.push(PatternStep::WordBoundary);
}
NfaInstruction::NotWordBoundary => {
steps.push(PatternStep::NotWordBoundary);
}
NfaInstruction::StartOfText => {
steps.push(PatternStep::StartOfText);
}
NfaInstruction::EndOfText => {
steps.push(PatternStep::EndOfText);
}
NfaInstruction::PositiveLookahead(inner_nfa) => {
let inner_steps = self.extract_lookaround_steps(inner_nfa);
if inner_steps.is_empty() {
return None;
}
steps.push(PatternStep::PositiveLookahead(inner_steps));
}
NfaInstruction::NegativeLookahead(inner_nfa) => {
let inner_steps = self.extract_lookaround_steps(inner_nfa);
if inner_steps.is_empty() {
return None;
}
steps.push(PatternStep::NegativeLookahead(inner_steps));
}
NfaInstruction::CodepointClass(cpclass, target) => {
if (*target as usize) >= self.nfa.states.len() {
return None;
}
let target_state = &self.nfa.states[*target as usize];
if target_state.epsilon.len() == 2 && target_state.transitions.is_empty() {
let eps0 = target_state.epsilon[0];
let eps1 = target_state.epsilon[1];
if eps0 == current {
steps.push(PatternStep::GreedyCodepointPlus(cpclass.clone()));
visited[current as usize] = true;
visited[*target as usize] = true;
current = eps1;
continue;
} else if eps1 == current {
steps.push(PatternStep::GreedyCodepointPlus(cpclass.clone()));
visited[current as usize] = true;
visited[*target as usize] = true;
current = eps0;
continue;
}
}
steps.push(PatternStep::CodepointClass(cpclass.clone(), *target));
visited[current as usize] = true;
current = *target;
continue;
}
_ => {
return None;
}
}
}
if !state.transitions.is_empty() {
let target = state.transitions[0].1;
if !state.transitions.iter().all(|(_, t)| *t == target) {
return None;
}
let ranges: Vec<ByteRange> = state.transitions.iter().map(|(r, _)| *r).collect();
let target_state = &self.nfa.states[target as usize];
if target_state.epsilon.len() == 2 && target_state.transitions.is_empty() {
let eps0 = target_state.epsilon[0];
let eps1 = target_state.epsilon[1];
if eps0 == current {
steps.push(PatternStep::GreedyPlus(ByteClass::new(ranges)));
visited[current as usize] = true;
visited[target as usize] = true;
current = eps1;
continue;
} else if eps1 == current {
steps.push(PatternStep::GreedyPlus(ByteClass::new(ranges)));
visited[current as usize] = true;
visited[target as usize] = true;
current = eps0;
continue;
}
}
steps.push(PatternStep::ByteClass(ByteClass::new(ranges)));
visited[current as usize] = true;
current = target;
continue;
}
if state.epsilon.len() == 1 {
visited[current as usize] = true;
current = state.epsilon[0];
continue;
}
if state.epsilon.len() == 2 {
let eps0 = state.epsilon[0];
let eps1 = state.epsilon[1];
let eps0_visited = visited[eps0 as usize];
let eps1_visited = visited[eps1 as usize];
if eps0_visited && !eps1_visited {
visited[current as usize] = true;
current = eps1;
continue;
}
if eps1_visited && !eps0_visited {
visited[current as usize] = true;
current = eps0;
continue;
}
if self.alternation_loops_back(current) {
return None;
}
let mut alternatives: Vec<Vec<PatternStep>> = Vec::new();
let mut any_valid = false;
for &target in state.epsilon.iter() {
let mut branch_visited = visited.to_vec();
branch_visited[current as usize] = true;
let target_state = &self.nfa.states[target as usize];
if target_state.is_match {
alternatives.push(Vec::new());
any_valid = true;
continue;
}
let branch_steps = self.extract_branch(target, &mut branch_visited)?;
alternatives.push(branch_steps);
any_valid = true;
}
if !any_valid {
return None;
}
steps.push(PatternStep::Alt(alternatives));
break;
}
if state.epsilon.len() > 2 {
if self.alternation_loops_back(current) {
return None;
}
let mut alternatives: Vec<Vec<PatternStep>> = Vec::new();
let mut any_valid = false;
for &target in state.epsilon.iter() {
let mut branch_visited = visited.to_vec();
branch_visited[current as usize] = true;
let target_state = &self.nfa.states[target as usize];
if target_state.is_match {
alternatives.push(Vec::new());
any_valid = true;
continue;
}
let branch_steps = self.extract_branch(target, &mut branch_visited)?;
alternatives.push(branch_steps);
any_valid = true;
}
if !any_valid {
return None;
}
steps.push(PatternStep::Alt(alternatives));
break;
}
return None;
}
Some(steps)
}
fn extract_lookaround_steps(&self, inner_nfa: &Nfa) -> Vec<PatternStep> {
if nfa_has_lookaround(inner_nfa) {
return Vec::new();
}
let mut visited = vec![false; inner_nfa.states.len()];
let mut steps = Vec::new();
let mut current = inner_nfa.start;
let mut iteration = 0;
loop {
{
iteration += 1;
if iteration > 10000 {
return Vec::new();
}
}
if !self.charge() {
return Vec::new();
}
if current as usize >= inner_nfa.states.len() {
return Vec::new();
}
let state = &inner_nfa.states[current as usize];
if state.is_match {
match terminal_assertion(state) {
TerminalAssertion::Nothing => {}
TerminalAssertion::Step(step) => steps.push(step),
TerminalAssertion::Unsupported => return Vec::new(),
}
break;
}
if let Some(ref instr) = state.instruction {
match instr {
NfaInstruction::WordBoundary => {
steps.push(PatternStep::WordBoundary);
}
NfaInstruction::EndOfText => {
steps.push(PatternStep::EndOfText);
}
NfaInstruction::StartOfText => {
steps.push(PatternStep::StartOfText);
}
NfaInstruction::CaptureStart(_) | NfaInstruction::CaptureEnd(_) => {
}
NfaInstruction::CodepointClass(cpclass, target) => {
steps.push(PatternStep::CodepointClass(cpclass.clone(), *target));
if visited[current as usize] {
return Vec::new();
}
visited[current as usize] = true;
current = *target;
continue;
}
_ => {
return Vec::new();
}
}
}
if !state.transitions.is_empty() {
let target = state.transitions[0].1;
if !state.transitions.iter().all(|(_, t)| *t == target) {
return Vec::new();
}
let ranges: Vec<ByteRange> = state.transitions.iter().map(|(r, _)| *r).collect();
let target_state = &inner_nfa.states[target as usize];
if target_state.transitions.is_empty() && target_state.epsilon.len() == 2 {
let eps0 = target_state.epsilon[0];
let eps1 = target_state.epsilon[1];
if eps0 == current {
steps.push(PatternStep::GreedyPlus(ByteClass::new(ranges)));
if visited[target as usize] {
return Vec::new();
}
visited[target as usize] = true;
current = eps1; continue;
} else if eps1 == current {
steps.push(PatternStep::GreedyPlus(ByteClass::new(ranges)));
if visited[target as usize] {
return Vec::new();
}
visited[target as usize] = true;
current = eps0; continue;
}
}
if visited[current as usize] {
return Vec::new();
}
visited[current as usize] = true;
if ranges.len() == 1 && ranges[0].start == ranges[0].end {
steps.push(PatternStep::Byte(ranges[0].start));
} else {
steps.push(PatternStep::ByteClass(ByteClass::new(ranges)));
}
current = target;
continue;
}
if state.epsilon.len() == 1 && state.transitions.is_empty() {
if visited[current as usize] {
return Vec::new();
}
visited[current as usize] = true;
current = state.epsilon[0];
continue;
}
if state.epsilon.len() == 2 && state.transitions.is_empty() {
let eps0 = state.epsilon[0];
let eps1 = state.epsilon[1];
if let Some((ranges, exit_state)) =
self.detect_greedy_star_in_lookaround(inner_nfa, current, eps0, eps1, &visited)
{
steps.push(PatternStep::GreedyStar(ByteClass::new(ranges)));
visited[current as usize] = true;
current = exit_state;
continue;
}
if let Some((ranges, exit_state)) =
self.detect_greedy_star_in_lookaround(inner_nfa, current, eps1, eps0, &visited)
{
steps.push(PatternStep::GreedyStar(ByteClass::new(ranges)));
visited[current as usize] = true;
current = exit_state;
continue;
}
return Vec::new();
}
if !state.epsilon.is_empty() {
return Vec::new();
}
break;
}
steps
}
fn extract_lookbehind_steps(&self, inner_nfa: &Nfa) -> Vec<PatternStep> {
if nfa_has_lookaround(inner_nfa) {
return Vec::new();
}
let mut visited = vec![false; inner_nfa.states.len()];
let mut steps = Vec::new();
let mut current = inner_nfa.start;
loop {
if current as usize >= inner_nfa.states.len() {
return Vec::new();
}
let state = &inner_nfa.states[current as usize];
if state.is_match {
match terminal_assertion(state) {
TerminalAssertion::Nothing => {}
TerminalAssertion::Step(step) => steps.push(step),
TerminalAssertion::Unsupported => return Vec::new(),
}
break;
}
if let Some(NfaInstruction::CodepointClass(cpclass, target)) = &state.instruction {
steps.push(PatternStep::CodepointClass(cpclass.clone(), *target));
if visited[current as usize] {
return Vec::new();
}
visited[current as usize] = true;
current = *target;
continue;
}
match terminal_assertion(state) {
TerminalAssertion::Nothing => {}
TerminalAssertion::Step(step) => steps.push(step),
TerminalAssertion::Unsupported => return Vec::new(),
}
if !state.transitions.is_empty() {
let target = state.transitions[0].1;
if !state.transitions.iter().all(|(_, t)| *t == target) {
return Vec::new();
}
let ranges: Vec<ByteRange> = state.transitions.iter().map(|(r, _)| *r).collect();
let target_state = &inner_nfa.states[target as usize];
if target_state.transitions.is_empty() && target_state.epsilon.len() == 2 {
let eps0 = target_state.epsilon[0];
let eps1 = target_state.epsilon[1];
if eps0 == current || eps1 == current {
return Vec::new();
}
}
if visited[current as usize] {
return Vec::new();
}
visited[current as usize] = true;
if ranges.len() == 1 && ranges[0].start == ranges[0].end {
steps.push(PatternStep::Byte(ranges[0].start));
} else {
steps.push(PatternStep::ByteClass(ByteClass::new(ranges)));
}
current = target;
continue;
}
if state.epsilon.len() == 1 && state.transitions.is_empty() {
if visited[current as usize] {
return Vec::new();
}
visited[current as usize] = true;
current = state.epsilon[0];
continue;
}
if !state.epsilon.is_empty() {
return Vec::new();
}
break;
}
steps
}
fn detect_greedy_star_in_lookaround(
&self,
inner_nfa: &Nfa,
_branch_state: StateId,
loop_start: StateId,
exit_state: StateId,
visited: &[bool],
) -> Option<(Vec<ByteRange>, StateId)> {
if loop_start as usize >= inner_nfa.states.len() {
return None;
}
let loop_state = &inner_nfa.states[loop_start as usize];
if loop_state.transitions.is_empty() {
return None;
}
let target = loop_state.transitions[0].1;
if !loop_state.transitions.iter().all(|(_, t)| *t == target) {
return None;
}
let ranges: Vec<ByteRange> = loop_state.transitions.iter().map(|(r, _)| *r).collect();
let target_state = &inner_nfa.states[target as usize];
if target_state.epsilon.len() == 2 {
let eps0 = target_state.epsilon[0];
let eps1 = target_state.epsilon[1];
if eps0 == loop_start {
if !visited[loop_start as usize] {
return Some((ranges, exit_state));
}
} else if eps1 == loop_start {
if !visited[loop_start as usize] {
return Some((ranges, exit_state));
}
}
}
if target_state.epsilon.len() == 1
&& target_state.epsilon[0] == loop_start
&& !visited[loop_start as usize]
{
return Some((ranges, exit_state));
}
None
}
}
#[cfg(test)]
mod fixed_byte_len_tests {
use super::*;
use crate::hir::CodepointClass;
fn class(ranges: &[(u32, u32)], negated: bool) -> PatternStep {
PatternStep::CodepointClass(CodepointClass::new(ranges.to_vec(), negated), 0)
}
#[test]
fn consuming_steps_add_their_width() {
assert_eq!(fixed_byte_len(&[]), Some(0));
assert_eq!(
fixed_byte_len(&[PatternStep::Byte(b'a'), PatternStep::Byte(b'b')]),
Some(2)
);
}
#[test]
fn assertions_and_capture_markers_are_zero_width() {
assert_eq!(
fixed_byte_len(&[
PatternStep::StartOfText,
PatternStep::Byte(b'a'),
PatternStep::WordBoundary,
PatternStep::NotWordBoundary,
PatternStep::EndOfText,
PatternStep::StartOfLine,
PatternStep::EndOfLine,
PatternStep::CaptureStart(1),
PatternStep::CaptureEnd(1),
]),
Some(1)
);
}
#[test]
fn codepoint_class_counts_its_utf8_width() {
assert_eq!(fixed_byte_len(&[class(&[(0x3B1, 0x3C9)], false)]), Some(2));
assert_eq!(
fixed_byte_len(&[class(&[(0x4E00, 0x9FFF)], false)]),
Some(3)
);
assert_eq!(
fixed_byte_len(&[class(&[(b'a' as u32, b'z' as u32)], false)]),
Some(1)
);
assert_eq!(
fixed_byte_len(&[class(&[(0x1F600, 0x1F64F)], false)]),
Some(4)
);
}
#[test]
fn variable_width_codepoint_classes_are_refused() {
assert_eq!(fixed_byte_len(&[class(&[(0x7F, 0x80)], false)]), None);
assert_eq!(
fixed_byte_len(&[class(&[(b'a' as u32, b'z' as u32), (0x3B1, 0x3C9)], false)]),
None
);
assert_eq!(fixed_byte_len(&[class(&[(0x3B1, 0x3C9)], true)]), None);
assert_eq!(fixed_byte_len(&[class(&[], false)]), None);
}
#[test]
fn variable_width_steps_are_refused() {
let bytes = ByteClass::new(vec![ByteRange::new(b'a', b'z')]);
assert_eq!(
fixed_byte_len(&[PatternStep::GreedyPlus(bytes.clone())]),
None
);
assert_eq!(
fixed_byte_len(&[PatternStep::GreedyStar(bytes.clone())]),
None
);
assert_eq!(fixed_byte_len(&[PatternStep::Backref(1)]), None);
assert_eq!(
fixed_byte_len(&[PatternStep::Alt(vec![
vec![PatternStep::Byte(b'a')],
vec![PatternStep::Byte(b'a'), PatternStep::Byte(b'b')],
])]),
None
);
}
#[test]
fn one_refusal_refuses_the_whole_program() {
assert_eq!(
fixed_byte_len(&[PatternStep::Byte(b'a'), PatternStep::Backref(1)]),
None
);
}
#[test]
fn width_set_of_a_fixed_program_is_a_single_total() {
assert_eq!(byte_len_set(&[]), Some(vec![0]));
assert_eq!(
byte_len_set(&[PatternStep::Byte(b'a'), PatternStep::Byte(b'b')]),
Some(vec![2])
);
assert_eq!(
byte_len_set(&[PatternStep::StartOfText, class(&[(0x3B1, 0x3C9)], false)]),
Some(vec![2])
);
}
#[test]
fn a_range_straddling_an_encoding_boundary_spans_both_widths() {
assert_eq!(
byte_len_set(&[class(&[(0x7F, 0x80)], false)]),
Some(vec![1, 2])
);
assert_eq!(
byte_len_set(&[class(&[(0, 0x10FFFF)], false)]),
Some(vec![1, 2, 3, 4])
);
}
#[test]
fn separate_ranges_contribute_their_own_widths() {
assert_eq!(
byte_len_set(&[class(
&[(0x20, 0x20), (0xA0, 0xA0), (0x2003, 0x2003)],
false
)]),
Some(vec![1, 2, 3])
);
}
#[test]
fn widths_of_successive_steps_form_a_sumset() {
let two_spaces = [
class(&[(0x20, 0x20), (0xA0, 0xA0)], false),
class(&[(0x20, 0x20), (0xA0, 0xA0)], false),
];
assert_eq!(byte_len_set(&two_spaces), Some(vec![2, 3, 4]));
let space_then_x = [
class(&[(0x20, 0x20), (0xA0, 0xA0)], false),
PatternStep::Byte(b'x'),
];
assert_eq!(byte_len_set(&space_then_x), Some(vec![2, 3]));
}
#[test]
fn width_sets_that_fan_out_past_the_cap_are_declined() {
let all_widths = class(&[(0, 0x10FFFF)], false);
let five = vec![all_widths.clone(); 5];
let six = vec![all_widths; 6];
assert_eq!(MAX_LOOKBEHIND_WIDTHS, 16);
assert_eq!(byte_len_set(&five).map(|w| w.len()), Some(16));
assert_eq!(byte_len_set(&six), None);
}
#[test]
fn variable_width_steps_have_no_width_set() {
let bytes = ByteClass::new(vec![ByteRange::new(b'a', b'z')]);
assert_eq!(byte_len_set(&[PatternStep::GreedyPlus(bytes)]), None);
assert_eq!(byte_len_set(&[PatternStep::Backref(1)]), None);
assert_eq!(byte_len_set(&[class(&[(0x3B1, 0x3C9)], true)]), None);
assert_eq!(byte_len_set(&[class(&[], false)]), None);
}
#[test]
fn fixed_byte_len_is_the_single_candidate_case() {
let mixed = [class(&[(0x20, 0x20), (0xA0, 0xA0)], false)];
assert_eq!(byte_len_set(&mixed), Some(vec![1, 2]));
assert_eq!(fixed_byte_len(&mixed), None);
}
}
#[cfg(test)]
mod extraction_budget_tests {
use super::*;
use crate::hir::translate;
use crate::parser::parse;
fn build_nfa(pattern: &str) -> Nfa {
let ast = parse(pattern).unwrap();
let hir = translate(&ast).unwrap();
crate::nfa::compile(&hir).unwrap()
}
const CL100K: &str = r"'(?i:[sdmt]|ll|ve|re)|[^\r\n\p{L}\p{N}]?\p{L}+|\p{N}{1,3}| ?[^\s\p{L}\p{N}]+[\r\n]*|\s+$|\s*[\r\n]|\s+(?!\S)|\s";
const O200K: &str = r"[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}]*[\p{Ll}\p{Lm}\p{Lo}\p{M}]+(?i:'s|'t|'re|'ve|'m|'ll|'d)?|[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}]+[\p{Ll}\p{Lm}\p{Lo}\p{M}]*(?i:'s|'t|'re|'ve|'m|'ll|'d)?|\p{N}{1,3}| ?[^\s\p{L}\p{N}]+[\r\n/]*|\s*[\r\n]+|\s+(?!\S)|\s+";
#[test]
fn tokenizer_patterns_stay_under_the_budget() {
for pattern in [CL100K, O200K] {
let nfa = build_nfa(pattern);
assert!(
StepExtractor::new(&nfa).extract().is_some(),
"extraction budget pushed a real tokenizer pattern to the PikeVm fallback: {pattern}"
);
}
}
#[test]
fn pathological_sequential_alternations_are_declined_not_built() {
let groups = ["(?:ab|cd)", "(?:ef|gh)", "(?:ij|kl)", "(?:mn|op)"];
let mut pattern = String::from("(?=a)");
for i in 0..24 {
pattern.push_str(groups[i % groups.len()]);
}
let nfa = build_nfa(&pattern);
assert!(StepExtractor::new(&nfa).extract().is_none());
}
}
#[cfg(test)]
mod nullable_run_with_assertion_tests {
use super::*;
use crate::hir::translate;
use crate::parser::parse;
fn extract(pattern: &str) -> Option<Vec<PatternStep>> {
let ast = parse(pattern).unwrap();
let hir = translate(&ast).unwrap();
let nfa = crate::nfa::compile(&hir).unwrap();
StepExtractor::new(&nfa).extract()
}
fn has_star_lookahead(steps: &[PatternStep]) -> bool {
steps
.iter()
.any(|s| matches!(s, PatternStep::GreedyStarLookahead(_, _, _)))
}
#[test]
fn a_greedy_byte_class_run_beside_an_assertion_now_extracts() {
for pattern in [
r"\w*(?=ing)",
r"a\w*(?=ing)",
r"[a-z]*(?=xy)",
r"\d[a-z]*(?=xy)",
r"[a-z]*(?=x)",
r"\w*(?!ing)",
] {
let steps =
extract(pattern).unwrap_or_else(|| panic!("{pattern:?} no longer extracts at all"));
assert!(
has_star_lookahead(&steps),
"{pattern:?} extracted no combined greedy-star+lookahead step: {steps:?}"
);
}
}
#[test]
fn a_nullable_run_that_is_not_a_greedy_byte_class_is_still_declined() {
for (pattern, why) in [
(r"\w*?(?=ing)", "non-greedy star"),
(r"(?:ab)*(?=x)", "multi-state body"),
(r"\p{L}*(?=x)", "codepoint class body"),
(r"\w?(?=ing)", "optional, not a repeat"),
(r"(?:x|\w*(?=ing))", "nullable run inside an Alt branch"),
(r"(?:\w*(?=ing)|q)", "nullable run inside an Alt branch"),
] {
assert!(
extract(pattern).is_none(),
"{pattern:?} ({why}) now extracts; the structural star check may \
be too loose"
);
}
}
#[test]
fn a_genuine_alternation_is_not_recognised_as_a_star() {
for pattern in [r"(?:a+|b)", r"(?:a+|b)c"] {
let steps =
extract(pattern).unwrap_or_else(|| panic!("{pattern:?} no longer extracts"));
assert!(
!steps
.iter()
.any(|s| matches!(s, PatternStep::GreedyStar(_)))
&& !has_star_lookahead(&steps),
"{pattern:?} was miscompiled as `a*b`: {steps:?}"
);
}
assert!(
extract(r"(?:\w+|x)(?=ing)").is_none(),
"an alternation was recognised as a star: the exit-convergence check \
is too loose"
);
}
#[test]
fn the_non_nullable_sibling_still_takes_the_fast_path() {
let steps = extract(r"\w+(?=ing)").expect("plus form extracts");
assert!(
steps
.iter()
.any(|s| matches!(s, PatternStep::GreedyPlusLookahead(_, _, _))),
"expected a combined greedy+lookahead step, got {steps:?}"
);
}
}