use crate::hir::{Hir, HirExpr};
use crate::nfa::{
compile_glushkov, compile_glushkov_wide, BitSet256, GlushkovNfa, GlushkovWideNfa,
MAX_POSITIONS, MAX_POSITIONS_WIDE,
};
#[derive(Debug)]
pub struct ShiftOr {
pub(crate) masks: [u64; 256],
pub(crate) accept: u64,
pub(crate) first: u64,
pub(crate) follow: Vec<u64>,
pub(crate) nullable: bool,
pub(crate) position_count: usize,
pub(crate) has_leading_word_boundary: bool,
pub(crate) has_trailing_word_boundary: bool,
pub(crate) has_start_anchor: bool,
pub(crate) has_end_anchor: bool,
pub(crate) class_run: Option<ClassRun>,
}
#[derive(Debug, Clone)]
pub(crate) struct ClassRun {
table: Box<[u8; 256]>,
min: usize,
max: Option<usize>,
}
pub fn is_class_run_shape(hir: &Hir) -> bool {
ClassRun::from_hir(hir).is_some()
}
impl ClassRun {
fn from_hir(hir: &Hir) -> Option<Self> {
if hir.props.capture_count > 0 {
return None;
}
let HirExpr::Repeat(repeat) = &hir.expr else {
return None;
};
let HirExpr::Class(class) = &repeat.expr else {
return None;
};
if !repeat.greedy || repeat.min == 0 {
return None;
}
Some(Self {
table: Box::new(crate::literal::byte_class_set(&class.ranges, class.negated)),
min: repeat.min as usize,
max: repeat.max.map(|max| max as usize),
})
}
#[inline]
fn contains(&self, byte: u8) -> bool {
self.table.get(byte as usize).is_some_and(|m| *m != 0)
}
fn find_from(&self, input: &[u8], from: usize) -> Option<(usize, usize)> {
let mut pos = from;
loop {
while !self.contains(*input.get(pos)?) {
pos += 1;
}
let start = pos;
let limit = self.max.map_or(input.len(), |max| {
input.len().min(start.saturating_add(max))
});
while pos < limit && input.get(pos).is_some_and(|&b| self.contains(b)) {
pos += 1;
}
if pos - start >= self.min {
return Some((start, pos));
}
while input.get(pos).is_some_and(|&b| self.contains(b)) {
pos += 1;
}
}
}
}
const SCAN_BUDGET_FACTOR: usize = 4;
impl ShiftOr {
pub fn from_hir(hir: &Hir) -> Option<Self> {
if hir.props.has_backrefs
|| hir.props.has_lookaround
|| hir.props.has_anchors
|| hir.props.has_word_boundary
|| hir.props.has_non_greedy
{
return None;
}
let glushkov = compile_glushkov(hir)?;
let mut matcher = Self::from_glushkov_with_boundaries(&glushkov, false, false)?;
matcher.class_run = ClassRun::from_hir(hir);
Some(matcher)
}
pub fn from_hir_with_anchors(hir: &Hir) -> Option<Self> {
if hir.props.has_backrefs
|| hir.props.has_lookaround
|| hir.props.has_word_boundary
|| hir.props.has_non_greedy
{
return None;
}
let glushkov = compile_glushkov(hir)?;
let has_start_anchor = hir.props.has_start_anchor;
let has_end_anchor = hir.props.has_end_anchor;
Self::from_glushkov_with_options(&glushkov, false, false, has_start_anchor, has_end_anchor)
}
pub fn from_glushkov(nfa: &GlushkovNfa) -> Option<Self> {
Self::from_glushkov_with_options(nfa, false, false, false, false)
}
fn from_glushkov_with_boundaries(
nfa: &GlushkovNfa,
has_leading_word_boundary: bool,
has_trailing_word_boundary: bool,
) -> Option<Self> {
Self::from_glushkov_with_options(
nfa,
has_leading_word_boundary,
has_trailing_word_boundary,
false,
false,
)
}
fn from_glushkov_with_options(
nfa: &GlushkovNfa,
has_leading_word_boundary: bool,
has_trailing_word_boundary: bool,
has_start_anchor: bool,
has_end_anchor: bool,
) -> Option<Self> {
if nfa.position_count > MAX_POSITIONS || nfa.position_count == 0 {
return None;
}
let masks = nfa.build_shift_or_masks();
let accept = nfa.build_accept_mask();
Some(Self {
masks,
accept,
first: nfa.first,
follow: nfa.follow.clone(),
nullable: nfa.nullable,
position_count: nfa.position_count,
has_leading_word_boundary,
has_trailing_word_boundary,
has_start_anchor,
has_end_anchor,
class_run: None,
})
}
#[inline]
pub fn has_class_run(&self) -> bool {
self.class_run.is_some()
}
#[inline]
pub fn has_word_boundary(&self) -> bool {
self.has_leading_word_boundary || self.has_trailing_word_boundary
}
pub fn state_count(&self) -> usize {
self.position_count
}
pub fn masks(&self) -> &[u64; 256] {
&self.masks
}
pub fn accept(&self) -> u64 {
self.accept
}
pub fn first(&self) -> u64 {
self.first
}
pub fn follow(&self) -> &[u64] {
&self.follow
}
pub fn is_nullable(&self) -> bool {
self.nullable
}
pub fn has_leading_word_boundary(&self) -> bool {
self.has_leading_word_boundary
}
pub fn has_trailing_word_boundary(&self) -> bool {
self.has_trailing_word_boundary
}
pub fn is_match(&self, input: &[u8]) -> bool {
self.find(input).is_some()
}
pub fn find(&self, input: &[u8]) -> Option<(usize, usize)> {
if self.has_start_anchor {
if let Some(end) = self.match_at(input, 0) {
if self.has_end_anchor && !crate::nfa::at_end_or_before_final_newline(input, end) {
return None;
}
return Some((0, end));
}
if self.nullable && (!self.has_end_anchor || input.is_empty()) {
return Some((0, 0));
}
return None;
}
let scan_end = self.scan_limit(input, 0)?;
if self.has_end_anchor {
if let Some(found) = self.find_end_anchored(input, 0) {
return Some(found);
}
} else {
for start in 0..=scan_end {
if let Some(end) = self.match_at(input, start) {
return Some((start, end));
}
}
}
if self.nullable && !self.has_end_anchor {
return Some((0, 0));
}
None
}
pub fn find_at(&self, input: &[u8], pos: usize) -> Option<(usize, usize)> {
if pos > input.len() {
return None;
}
if let Some(ref run) = self.class_run {
return run.find_from(input, pos);
}
if self.has_start_anchor && pos > 0 {
return None;
}
let search_start = if self.has_start_anchor { 0 } else { pos };
let scan_end = self.scan_limit(input, search_start)?;
if self.has_end_anchor && !self.has_start_anchor {
return self.find_end_anchored(input, search_start);
}
for start in search_start..=scan_end {
if let Some(end) = self.match_at(input, start) {
if self.has_end_anchor && !crate::nfa::at_end_or_before_final_newline(input, end) {
if self.has_start_anchor {
return None;
}
continue;
}
return Some((start, end));
}
if self.has_start_anchor {
break;
}
}
None
}
pub(crate) fn earliest_match_end(&self, input: &[u8], from: usize) -> Option<usize> {
let mut state = !0u64;
for (i, &byte) in input[from..].iter().enumerate() {
let mut reachable = self.first;
let mut active = !state;
while active != 0 {
let pos = active.trailing_zeros() as usize;
reachable |= self.follow[pos];
active &= active - 1;
}
state = (!reachable) | self.masks[byte as usize];
if (state | self.accept) != !0u64 {
return Some(from + i + 1);
}
}
None
}
pub(crate) fn scan_limit(&self, input: &[u8], search_start: usize) -> Option<usize> {
if self.has_start_anchor || self.nullable {
return Some(input.len());
}
match self.earliest_match_end(input, search_start) {
None => None,
Some(_) if self.has_end_anchor => Some(input.len()),
Some(end) => Some(end),
}
}
fn matches_end_anchored_starting_by(
&self,
input: &[u8],
from: usize,
seed_until: usize,
) -> bool {
let mut state = !0u64;
for pos in from..=input.len() {
if self.nullable
&& pos <= seed_until
&& crate::nfa::at_end_or_before_final_newline(input, pos)
{
return true;
}
if pos == input.len() {
break;
}
let mut reachable = if pos <= seed_until { self.first } else { 0 };
let mut active = !state;
while active != 0 {
let p = active.trailing_zeros() as usize;
reachable |= self.follow[p];
active &= active - 1;
}
state = (!reachable) | self.masks[input[pos] as usize];
if (state | self.accept) != !0u64
&& crate::nfa::at_end_or_before_final_newline(input, pos + 1)
{
return true;
}
if state == !0u64 && pos >= seed_until {
return false;
}
}
false
}
fn leftmost_end_anchored_start(&self, input: &[u8], from: usize) -> Option<usize> {
if !self.matches_end_anchored_starting_by(input, from, input.len()) {
return None;
}
let (mut lo, mut hi) = (from, input.len());
while lo < hi {
let mid = lo + (hi - lo) / 2;
if self.matches_end_anchored_starting_by(input, from, mid) {
hi = mid;
} else {
lo = mid + 1;
}
}
Some(lo)
}
fn find_end_anchored(&self, input: &[u8], from: usize) -> Option<(usize, usize)> {
let budget = input.len().saturating_mul(SCAN_BUDGET_FACTOR);
let mut walked = 0usize;
for start in from..=input.len() {
let (end, reach) = self.match_at_reach(input, start);
if let Some(end) = end {
if crate::nfa::at_end_or_before_final_newline(input, end) {
return Some((start, end));
}
}
walked += reach.saturating_sub(start);
if walked > budget {
let s = self.leftmost_end_anchored_start(input, start + 1)?;
let end = self.match_at(input, s)?;
if !crate::nfa::at_end_or_before_final_newline(input, end) {
return None;
}
return Some((s, end));
}
}
None
}
pub fn try_match_at(&self, input: &[u8], pos: usize) -> Option<(usize, usize)> {
if self.has_start_anchor && pos != 0 {
return None;
}
match self.match_at(input, pos) {
Some(end) => {
if self.has_end_anchor && !crate::nfa::at_end_or_before_final_newline(input, end) {
return None;
}
Some((pos, end))
}
None => None,
}
}
fn match_at(&self, input: &[u8], start: usize) -> Option<usize> {
self.match_at_reach(input, start).0
}
fn match_at_reach(&self, input: &[u8], start: usize) -> (Option<usize>, usize) {
if start > input.len() {
return (None, start);
}
let mut last_match = None;
if self.nullable {
last_match = Some(start);
}
let mut state = !0u64;
for (i, &byte) in input[start..].iter().enumerate() {
let byte_mask = self.masks[byte as usize];
if i == 0 {
state = (!self.first) | byte_mask;
} else {
let mut active = !state;
let mut reachable = 0u64;
while active != 0 {
let pos = active.trailing_zeros() as usize;
reachable |= self.follow[pos];
active &= active - 1; }
state = (!reachable) | byte_mask;
}
if (state | self.accept) != !0u64 {
last_match = Some(start + i + 1);
}
if state == !0u64 {
return (last_match, start + i + 1);
}
}
(last_match, input.len())
}
}
use crate::hir::matches_empty as hir_is_nullable;
pub fn is_shift_or_compatible(hir: &Hir) -> bool {
if hir.props.has_backrefs
|| hir.props.has_lookaround
|| hir.props.has_multiline_anchors
|| hir.props.has_word_boundary
|| hir.props.has_non_greedy
{
return false;
}
if hir_is_nullable(&hir.expr) {
return false;
}
compile_glushkov(hir)
.map(|nfa| nfa.position_count <= MAX_POSITIONS && nfa.position_count > 0)
.unwrap_or(false)
}
#[derive(Debug)]
pub struct ShiftOrWide {
pub(crate) masks: Box<[BitSet256; 256]>,
pub(crate) accept: BitSet256,
pub(crate) first: BitSet256,
pub(crate) follow: Vec<BitSet256>,
pub(crate) nullable: bool,
pub(crate) position_count: usize,
}
impl ShiftOrWide {
pub fn from_hir(hir: &Hir) -> Option<Self> {
if hir.props.has_backrefs
|| hir.props.has_lookaround
|| hir.props.has_anchors
|| hir.props.has_word_boundary
|| hir.props.has_non_greedy
{
return None;
}
let glushkov = compile_glushkov_wide(hir)?;
Self::from_glushkov(&glushkov)
}
pub fn from_glushkov(nfa: &GlushkovWideNfa) -> Option<Self> {
if nfa.position_count > MAX_POSITIONS_WIDE || nfa.position_count == 0 {
return None;
}
let masks = Box::new(nfa.build_shift_or_masks());
let accept = nfa.build_accept_mask();
Some(Self {
masks,
accept,
first: nfa.first,
follow: nfa.follow.clone(),
nullable: nfa.nullable,
position_count: nfa.position_count,
})
}
pub fn state_count(&self) -> usize {
self.position_count
}
pub fn is_nullable(&self) -> bool {
self.nullable
}
pub fn is_match(&self, input: &[u8]) -> bool {
self.find(input).is_some()
}
pub fn find(&self, input: &[u8]) -> Option<(usize, usize)> {
let scan_end = self.scan_limit(input, 0)?;
for start in 0..=scan_end {
if let Some(end) = self.match_at(input, start) {
return Some((start, end));
}
}
if self.nullable {
return Some((0, 0));
}
None
}
pub fn find_at(&self, input: &[u8], pos: usize) -> Option<(usize, usize)> {
if pos > input.len() {
return None;
}
let scan_end = self.scan_limit(input, pos)?;
for start in pos..=scan_end {
if let Some(end) = self.match_at(input, start) {
return Some((start, end));
}
}
None
}
fn earliest_match_end(&self, input: &[u8], from: usize) -> Option<usize> {
let mut state = BitSet256::all_ones();
for (i, &byte) in input[from..].iter().enumerate() {
let mut reachable = self.first;
let active = state.complement();
for word_idx in 0..4 {
let mut word = active.parts[word_idx];
while word != 0 {
let pos = word_idx * 64 + word.trailing_zeros() as usize;
if pos < self.follow.len() {
reachable.union_assign(self.follow[pos]);
}
word &= word - 1;
}
}
state = reachable.complement().union(self.masks[byte as usize]);
if !state.union(self.accept).is_all_ones() {
return Some(from + i + 1);
}
}
None
}
fn scan_limit(&self, input: &[u8], search_start: usize) -> Option<usize> {
if self.nullable {
return Some(input.len());
}
self.earliest_match_end(input, search_start)
}
pub fn try_match_at(&self, input: &[u8], pos: usize) -> Option<(usize, usize)> {
self.match_at(input, pos).map(|end| (pos, end))
}
fn match_at(&self, input: &[u8], start: usize) -> Option<usize> {
if start > input.len() {
return None;
}
let mut last_match = None;
if self.nullable {
last_match = Some(start);
}
let mut state = BitSet256::all_ones();
for (i, &byte) in input[start..].iter().enumerate() {
let byte_mask = self.masks[byte as usize];
if i == 0 {
state = self.first.complement().union(byte_mask);
} else {
let active = state.complement();
let mut reachable = BitSet256::empty();
for word_idx in 0..4 {
let mut word = active.parts[word_idx];
while word != 0 {
let bit_idx = word.trailing_zeros() as usize;
let pos = word_idx * 64 + bit_idx;
if pos < self.follow.len() {
reachable.union_assign(self.follow[pos]);
}
word &= word - 1; }
}
state = reachable.complement().union(byte_mask);
}
if !state.union(self.accept).is_all_ones() {
last_match = Some(start + i + 1);
}
if state.is_all_ones() {
break;
}
}
last_match
}
}
pub fn is_shift_or_wide_compatible(hir: &Hir) -> bool {
if hir.props.has_backrefs
|| hir.props.has_lookaround
|| hir.props.has_anchors
|| hir.props.has_word_boundary
|| hir.props.has_non_greedy
{
return false;
}
if hir_is_nullable(&hir.expr) {
return false;
}
compile_glushkov_wide(hir)
.map(|nfa| {
nfa.position_count > MAX_POSITIONS
&& nfa.position_count <= MAX_POSITIONS_WIDE
&& nfa.position_count > 0
})
.unwrap_or(false)
}
#[cfg(test)]
mod scan_bound_tests {
use super::*;
use crate::hir::translate;
use crate::parser::parse;
fn compile(pattern: &str) -> Option<ShiftOr> {
let hir = parse(pattern).and_then(|ast| translate(&ast)).ok()?;
if hir.props.has_anchors {
ShiftOr::from_hir_with_anchors(&hir)
} else {
ShiftOr::from_hir(&hir)
}
}
fn brute_force(so: &ShiftOr, input: &[u8], from: usize) -> Option<(usize, usize)> {
if so.has_start_anchor && from > 0 {
return None;
}
let last = if so.has_start_anchor { 0 } else { input.len() };
for start in from..=last {
if let Some(end) = so.match_at(input, start) {
if so.has_end_anchor && !crate::nfa::at_end_or_before_final_newline(input, end) {
continue;
}
return Some((start, end));
}
}
None
}
const PATTERNS: &[&str] = &[
"a", "ab", "abc", "[ab]", "a+", "a*", "a?", "a{2}", "a{1,3}", "\\w", "\\w+", "\\w*", "\\d",
"\\w*\\d", "[a-z]*9", "a*b", "a.*b", "a.c", "(?:ab)+", "[^a]", "[^a]+", ".", ".*", ".+",
"\\s*", "\\s+", "ab*c", "a[bc]d", "^a", "a$", "^ab$", "^a*", "a*$",
];
const TEXTS: &[&str] = &[
"",
"a",
"aa",
"ab",
"ba",
"abc",
"abab",
"aaab",
"aaa9",
"9aaa",
"aaa",
"a b c",
" ",
"xyz",
"aaaaaaaaab",
"baaaaaaaaa",
"abcabcabc",
"a\nb",
"aaa\n",
"9",
"z",
];
#[test]
fn bounded_scan_matches_brute_force() {
let mut failures = Vec::new();
for pattern in PATTERNS {
let Some(so) = compile(pattern) else {
continue;
};
for text in TEXTS {
let bytes = text.as_bytes();
for from in 0..=bytes.len() {
let expected = brute_force(&so, bytes, from);
let got = so.find_at(bytes, from);
if got != expected {
failures.push(format!(
"{pattern:?} on {text:?} from {from}: brute={expected:?} got={got:?}"
));
}
}
let got = so.find(bytes);
let expected = brute_force(&so, bytes, 0);
if got != expected && !(so.nullable && expected.is_none()) {
failures.push(format!(
"find {pattern:?} on {text:?}: brute={expected:?} got={got:?}"
));
}
}
}
assert!(
failures.is_empty(),
"{} divergences:\n{}",
failures.len(),
failures.join("\n")
);
}
#[test]
fn earliest_match_end_agrees_with_scan() {
for pattern in PATTERNS {
let Some(so) = compile(pattern) else {
continue;
};
if so.nullable || so.has_start_anchor {
continue;
}
for text in TEXTS {
let bytes = text.as_bytes();
for from in 0..=bytes.len() {
let any_match = (from..=bytes.len())
.filter_map(|s| so.match_at(bytes, s).map(|e| (s, e)))
.find(|(s, e)| e > s);
let end = so.earliest_match_end(bytes, from);
assert_eq!(
end.is_some(),
any_match.is_some(),
"{pattern:?} on {text:?} from {from}: pass={end:?} scan={any_match:?}"
);
if let (Some(e), Some((s, _))) = (end, any_match) {
assert!(
s <= e,
"{pattern:?} on {text:?} from {from}: leftmost start {s} > bound {e}"
);
}
}
}
}
}
}