mod accept;
pub(crate) use accept::{AcceptCode, derive_accept};
use crate::oniguruma::{
ONIG_OPTION_FIND_NOT_EMPTY, ONIG_OPTION_NOTBOL, ONIG_OPTION_NOTEOL, ONIGENC_CTYPE_WORD,
};
use crate::regcomp::literal_alt_trie_index;
use crate::regenc::OnigEncoding;
use crate::regint::*;
use crate::regparse_types::*;
use regex_automata::hybrid::LazyStateID;
use regex_automata::hybrid::dfa as hybrid;
use regex_automata::hybrid::regex as hybrid_regex;
use regex_automata::nfa::thompson;
use regex_automata::util::pool::Pool;
use regex_automata::{Anchored, Input, MatchKind, PatternID};
use regex_syntax::hir::{
Class, ClassBytes, ClassBytesRange, ClassUnicode, ClassUnicodeRange, Hir, HirKind, Look,
Repetition,
};
use regex_syntax::utf8::Utf8Sequences;
use std::sync::{Arc, OnceLock};
const MIB: usize = 1 << 20;
const SEARCH_CACHE_CAPACITY: usize = 2 * MIB;
const SEARCH_MIN_BYTES_PER_STATE: usize = 10;
const OVERLAPPING_CACHE_CAPACITY: usize = MIB;
const NFA_SIZE_LIMIT: usize = 16 * MIB;
const MIN_CACHE_CLEARS: usize = 3;
const MIN_BYTES_PER_STATE: usize = 20;
const MAX_COUNTED_REPEAT: u32 = 64;
pub(crate) const CANDIDATE_SCAN_BYTES: usize = 256;
pub(crate) const LONG_SEEK_BYTES: usize = 16;
pub(crate) const LONG_WALK_BYTES: usize = 64;
pub(crate) const MAX_NFA_STATES: usize = 65_536;
pub(crate) const TINY_SET_ENTRIES: usize = 12;
pub(crate) const TINY_SET_NFA_STATES: usize = 128;
#[cfg(test)]
thread_local! {
pub(crate) static EARLIEST_CALLS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
pub(crate) static SCAN_STEPS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
pub(crate) static META_BYTES: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
pub(crate) static OVERLAPPING_CACHE_BYTES: std::cell::Cell<Option<usize>> =
const { std::cell::Cell::new(None) };
}
fn overlapping_config() -> hybrid::Config {
let config = hybrid::Config::new()
.match_kind(MatchKind::All)
.skip_cache_capacity_check(true);
#[cfg(test)]
if let Some(bytes) = OVERLAPPING_CACHE_BYTES.with(|bytes| bytes.get()) {
return config
.cache_capacity(bytes)
.minimum_cache_clear_count(None)
.minimum_bytes_per_state(None);
}
config
.cache_capacity(OVERLAPPING_CACHE_CAPACITY)
.minimum_cache_clear_count(Some(MIN_CACHE_CLEARS))
.minimum_bytes_per_state(Some(MIN_BYTES_PER_STATE))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Seek {
Everywhere,
Pattern(SeekCode),
}
pub(crate) fn derive(root: &Node, reg: &RegexType, precise: bool) -> Option<Seek> {
if reg.extp.as_ref().is_some_and(|ext| ext.callout_num != 0) {
return None;
}
let utf8: OnigEncoding = &crate::encodings::utf8::ONIG_ENCODING_UTF8;
if reg.enc.name() != utf8.name() || reg.enc.max_enc_len() != utf8.max_enc_len() {
return None;
}
walk_seek(root, reg, precise)
}
fn walk_seek(root: &Node, reg: &RegexType, precise: bool) -> Option<Seek> {
let mut tails = Vec::new();
if precise {
collect_tails(root, &mut tails);
}
let mut walk = Walk {
reg,
lookaheads_kept: 0,
inexact: false,
tails,
precise,
};
let hir = walk.item(root, true);
if matches_everywhere(&hir) {
return Some(Seek::Everywhere);
}
if precise && nfa_states_estimate(&hir) > MAX_NFA_STATES {
return walk_seek(root, reg, false);
}
let exact = !walk.inexact
&& line_starts_exact(&hir)
&& !reg
.options
.intersects(ONIG_OPTION_NOTBOL | ONIG_OPTION_NOTEOL | ONIG_OPTION_FIND_NOT_EMPTY);
let code = SeekCode::encode(&hir, exact)?;
debug_assert_eq!(code.decode(), hir, "the seek code round-trips");
Some(Seek::Pattern(code))
}
#[derive(Clone, PartialEq, Eq)]
pub(crate) struct SeekCode(Box<[u8]>);
mod tag {
pub(super) const EMPTY: u8 = 0;
pub(super) const LITERAL: u8 = 1;
pub(super) const BYTES: u8 = 2;
pub(super) const UNICODE: u8 = 3;
pub(super) const LOOK: u8 = 4;
pub(super) const REPETITION: u8 = 5;
pub(super) const CONCAT: u8 = 6;
pub(super) const ALTERNATION: u8 = 7;
}
impl SeekCode {
fn encode(hir: &Hir, exact: bool) -> Option<SeekCode> {
let mut out = vec![u8::from(exact)];
Self::write(hir, &mut out)?;
Some(SeekCode(out.into_boxed_slice()))
}
pub(crate) fn exact(&self) -> bool {
self.0[0] != 0
}
fn write(hir: &Hir, out: &mut Vec<u8>) -> Option<()> {
match hir.kind() {
HirKind::Empty => out.push(tag::EMPTY),
HirKind::Literal(literal) => {
out.push(tag::LITERAL);
Self::varint(literal.0.len(), out);
out.extend_from_slice(&literal.0);
}
HirKind::Class(Class::Bytes(class)) => {
out.push(tag::BYTES);
Self::varint(class.ranges().len(), out);
for range in class.ranges() {
out.push(range.start());
out.push(range.end());
}
}
HirKind::Class(Class::Unicode(class)) => {
out.push(tag::UNICODE);
Self::varint(class.ranges().len(), out);
for range in class.ranges() {
Self::varint(range.start() as usize, out);
Self::varint(range.end() as usize, out);
}
}
HirKind::Look(look) => {
out.push(tag::LOOK);
Self::varint(look.as_repr() as usize, out);
}
HirKind::Repetition(rep) => {
out.push(tag::REPETITION);
Self::varint(rep.min as usize, out);
Self::varint(rep.max.map_or(0, |max| max as usize + 1), out);
out.push(u8::from(rep.greedy));
Self::write(&rep.sub, out)?;
}
HirKind::Concat(parts) => {
out.push(tag::CONCAT);
Self::varint(parts.len(), out);
for part in parts {
Self::write(part, out)?;
}
}
HirKind::Alternation(branches) => {
out.push(tag::ALTERNATION);
Self::varint(branches.len(), out);
for branch in branches {
Self::write(branch, out)?;
}
}
HirKind::Capture(_) => return None,
}
Some(())
}
fn varint(mut value: usize, out: &mut Vec<u8>) {
while value >= 0x80 {
out.push((value as u8) | 0x80);
value >>= 7;
}
out.push(value as u8);
}
pub(crate) fn decode(&self) -> Hir {
let mut reader = SeekReader {
code: &self.0,
at: 1,
};
let hir = reader.hir();
debug_assert_eq!(reader.at, self.0.len(), "the seek code is read whole");
hir
}
#[cfg(test)]
pub(crate) fn len(&self) -> usize {
self.0.len()
}
}
struct SeekReader<'a> {
code: &'a [u8],
at: usize,
}
impl SeekReader<'_> {
fn byte(&mut self) -> u8 {
let b = self.code[self.at];
self.at += 1;
b
}
fn varint(&mut self) -> usize {
let mut value = 0usize;
let mut shift = 0;
loop {
let b = self.byte();
value |= usize::from(b & 0x7F) << shift;
if b & 0x80 == 0 {
return value;
}
shift += 7;
}
}
fn hir(&mut self) -> Hir {
match self.byte() {
tag::EMPTY => Hir::empty(),
tag::LITERAL => {
let len = self.varint();
let bytes = &self.code[self.at..self.at + len];
self.at += len;
Hir::literal(bytes)
}
tag::BYTES => {
let count = self.varint();
let ranges: Vec<ClassBytesRange> = (0..count)
.map(|_| {
let lo = self.byte();
let hi = self.byte();
ClassBytesRange::new(lo, hi)
})
.collect();
Hir::class(Class::Bytes(ClassBytes::new(ranges)))
}
tag::UNICODE => {
let count = self.varint();
let ranges: Vec<ClassUnicodeRange> = (0..count)
.map(|_| {
let lo = char::from_u32(self.varint() as u32).expect("a code point");
let hi = char::from_u32(self.varint() as u32).expect("a code point");
ClassUnicodeRange::new(lo, hi)
})
.collect();
Hir::class(Class::Unicode(ClassUnicode::new(ranges)))
}
tag::LOOK => Hir::look(Look::from_repr(self.varint() as u32).expect("a look")),
tag::REPETITION => {
let min = self.varint() as u32;
let max = match self.varint() {
0 => None,
max => Some(max as u32 - 1),
};
let greedy = self.byte() != 0;
let sub = Box::new(self.hir());
Hir::repetition(Repetition {
min,
max,
greedy,
sub,
})
}
tag::CONCAT => {
let count = self.varint();
Hir::concat((0..count).map(|_| self.hir()).collect())
}
tag::ALTERNATION => {
let count = self.varint();
Hir::alternation((0..count).map(|_| self.hir()).collect())
}
_ => unreachable!("a seek code tag"),
}
}
}
impl std::fmt::Debug for SeekCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("SeekCode")
.field(&self.decode().to_string())
.finish()
}
}
fn any_char() -> Hir {
byte_class(u128::MAX, true)
}
fn any_char_except_newline() -> Hir {
byte_class(!(1u128 << b'\n'), true)
}
fn placeholder() -> Hir {
Hir::repetition(Repetition {
min: 0,
max: None,
greedy: true,
sub: Box::new(any_char()),
})
}
fn is_placeholder(hir: &Hir) -> bool {
match hir.kind() {
HirKind::Repetition(rep) => rep.min == 0 && rep.max.is_none() && *rep.sub == any_char(),
_ => false,
}
}
fn non_ascii_char() -> Hir {
Hir::concat(vec![
Hir::class(Class::Bytes(ClassBytes::new([ClassBytesRange::new(
0xC2, 0xF4,
)]))),
Hir::repetition(Repetition {
min: 1,
max: Some(3),
greedy: true,
sub: Box::new(Hir::class(Class::Bytes(ClassBytes::new([
ClassBytesRange::new(0x80, 0xBF),
])))),
}),
])
}
fn matches_everywhere(hir: &Hir) -> bool {
let properties = hir.properties();
if properties.minimum_len() == Some(0) && properties.look_set().is_empty() {
return true;
}
let first = match hir.kind() {
HirKind::Concat(subs) => subs.first(),
_ => Some(hir),
};
first.is_some_and(is_placeholder)
}
fn class_exact(ranges: &[(u32, u32)]) -> bool {
ranges
.iter()
.all(|&(lo, hi)| lo > hi || hi < 0x80 || (lo.max(0x80) == 0x80 && hi >= 0x10FFFF))
}
const EXPLICIT_MEMBERS: usize = 16;
fn explicit_class(ranges: &[(u32, u32)]) -> Option<Hir> {
let mut ascii: u128 = 0;
let mut members = Vec::new();
for &(lo, hi) in ranges {
if lo > hi {
continue;
}
if lo <= 0x7F {
ascii |= ascii_mask(lo as u8, hi.min(0x7F) as u8);
}
let lo = lo.max(0x80);
if hi < lo {
continue;
}
let count = (hi - lo) as usize + 1;
if members.len() + count > EXPLICIT_MEMBERS {
return None;
}
for code in lo..=hi {
if let Some(c) = char::from_u32(code) {
members.push(Hir::literal(c.to_string().into_bytes().into_boxed_slice()));
}
}
}
members.insert(0, byte_class(ascii, false));
Some(alternation(members))
}
fn line_starts_exact(hir: &Hir) -> bool {
if !hir.properties().look_set().contains(Look::StartLF) {
return true;
}
match hir.kind() {
HirKind::Concat(parts) => parts.iter().enumerate().all(|(k, part)| {
if matches!(part.kind(), HirKind::Look(Look::StartLF)) {
parts[k + 1..]
.iter()
.map(|rest| rest.properties().minimum_len().unwrap_or(0))
.sum::<usize>()
>= 1
} else {
line_starts_exact(part)
}
}),
HirKind::Alternation(branches) => branches.iter().all(line_starts_exact),
_ => false,
}
}
fn class_hir(ranges: impl IntoIterator<Item = (u32, u32)>, negate: bool) -> Hir {
let mut ascii: u128 = 0;
let mut non_ascii = false;
let mut all_non_ascii = false;
for (lo, hi) in ranges {
if lo > hi {
continue;
}
if lo <= 0x7F {
ascii |= ascii_mask(lo as u8, hi.min(0x7F) as u8);
}
if hi >= 0x80 {
non_ascii = true;
all_non_ascii |= lo <= 0x80 && hi >= 0x10FFFF;
}
}
if negate {
ascii = !ascii;
non_ascii = !all_non_ascii;
}
byte_class(ascii, non_ascii)
}
fn ascii_mask(lo: u8, hi: u8) -> u128 {
debug_assert!(lo <= hi && hi <= 0x7F);
let width = u32::from(hi - lo) + 1;
let run = if width == 128 {
u128::MAX
} else {
(1u128 << width) - 1
};
run << lo
}
fn byte_class(ascii: u128, non_ascii: bool) -> Hir {
let mut ranges = Vec::new();
let mut byte = 0u32;
while byte <= 0x7F {
if ascii & (1u128 << byte) == 0 {
byte += 1;
continue;
}
let lo = byte;
while byte < 0x7F && ascii & (1u128 << (byte + 1)) != 0 {
byte += 1;
}
ranges.push(ClassBytesRange::new(lo as u8, byte as u8));
byte += 1;
}
match (ranges.is_empty(), non_ascii) {
(false, false) => Hir::class(Class::Bytes(ClassBytes::new(ranges))),
(false, true) => alternation(vec![
Hir::class(Class::Bytes(ClassBytes::new(ranges))),
non_ascii_char(),
]),
(true, true) => non_ascii_char(),
(true, false) => Hir::fail(),
}
}
fn is_fail(hir: &Hir) -> bool {
match hir.kind() {
HirKind::Class(Class::Bytes(class)) => class.ranges().is_empty(),
HirKind::Class(Class::Unicode(class)) => class.ranges().is_empty(),
_ => false,
}
}
fn concat(parts: Vec<Hir>) -> Hir {
if parts.iter().any(is_fail) {
Hir::fail()
} else {
Hir::concat(parts)
}
}
fn repetition(mut min: u32, mut max: Option<u32>, sub: Hir, inexact: &mut bool) -> Hir {
let nested = match sub.kind() {
HirKind::Repetition(inner) => Some((inner.min, inner.max, (*inner.sub).clone())),
_ => None,
};
let sub = match nested {
Some((inner_min, inner_max, inner)) => {
min = min.saturating_mul(inner_min);
max = match (max, inner_max) {
(Some(outer), Some(inner)) => Some(outer.saturating_mul(inner)),
_ => None,
};
inner
}
None => sub,
};
if min > MAX_COUNTED_REPEAT || max.is_some_and(|max| max > MAX_COUNTED_REPEAT) {
*inexact = true;
min = min.min(MAX_COUNTED_REPEAT);
max = None;
}
Hir::repetition(Repetition {
min,
max,
greedy: true,
sub: Box::new(sub),
})
}
fn alternation(mut branches: Vec<Hir>) -> Hir {
branches.retain(|branch| !is_fail(branch));
if branches.is_empty() {
Hir::fail()
} else {
Hir::alternation(branches)
}
}
fn code_ranges(bbuf: &BBuf) -> impl Iterator<Item = (u32, u32)> + '_ {
let words = bbuf.data.as_chunks::<4>().0;
let n = words.first().map_or(0, |chunk| u32::from_ne_bytes(*chunk)) as usize;
words[1..]
.as_chunks::<2>()
.0
.iter()
.take(n)
.map(|pair| (u32::from_ne_bytes(pair[0]), u32::from_ne_bytes(pair[1])))
}
fn class_members(cc: &CClassNode) -> impl Iterator<Item = (u32, u32)> + '_ {
let bits = bitset_members(&cc.bs)
.filter(|&member| member < 0x80)
.map(|member| (member as u32, member as u32));
let ranges = cc
.mbuf
.iter()
.flat_map(code_ranges)
.filter_map(|(lo, hi)| (hi >= 0x80).then_some((lo.max(0x80), hi)));
bits.chain(ranges)
}
fn word_class_look(an: &AnchorNode) -> Option<Look> {
let look = match an.anchor_type {
ANCR_LOOK_BEHIND_NOT => Look::WordStartHalfAscii,
ANCR_PREC_READ_NOT => Look::WordEndHalfAscii,
_ => return None,
};
let body = an.body.as_deref()?;
let word_bytes = (b'0'..=b'9')
.chain(b'A'..=b'Z')
.chain(b'a'..=b'z')
.chain(*b"_");
let holds_every_word_byte = match &body.inner {
NodeInner::CType(ct) => ct.ctype == ONIGENC_CTYPE_WORD as i32 && !ct.not,
NodeInner::CClass(cc) => {
let mut members = word_bytes.map(|byte| bitset_at(&cc.bs, byte as usize));
if cc.is_not() {
members.all(|member| !member)
} else {
members.all(|member| member)
}
}
_ => false,
};
holds_every_word_byte.then_some(look)
}
fn is_capture_check(node: &Node) -> bool {
matches!(node.inner, NodeInner::BackRef(_)) && node.has_status(ND_ST_CHECKER)
}
fn automatic_possessive(node: &Node) -> bool {
node.has_status(ND_ST_STRICT_REAL_REPEAT)
}
fn droppable(node: &Node) -> bool {
match &node.inner {
NodeInner::BackRef(_) => is_capture_check(node),
NodeInner::Anchor(an) => match an.anchor_type {
ANCR_PREC_READ_NOT | ANCR_LOOK_BEHIND_NOT => word_class_look(an).is_none(),
ANCR_PREC_READ
| ANCR_LOOK_BEHIND
| ANCR_BEGIN_POSITION
| ANCR_TEXT_SEGMENT_BOUNDARY
| ANCR_NO_TEXT_SEGMENT_BOUNDARY => true,
_ => false,
},
NodeInner::Gimmick(_) => true,
NodeInner::String(sn) => sn.s.is_empty(),
NodeInner::Quant(qn) => qn.upper == 0 || qn.body.as_deref().is_none_or(droppable),
NodeInner::Bag(bn) => match &bn.bag_data {
BagData::IfElse {
then_node,
else_node,
} => {
bn.body.as_deref().is_none_or(droppable)
&& then_node.as_deref().is_none_or(droppable)
&& else_node.as_deref().is_none_or(droppable)
}
_ => bn.body.as_deref().is_none_or(droppable),
},
NodeInner::List(_) | NodeInner::Alt(_) => cons_items(node).into_iter().all(droppable),
_ => false,
}
}
fn class_union(node: &Node, ranges: &mut Vec<(u32, u32)>) -> Option<(usize, usize)> {
match &node.inner {
NodeInner::CClass(cc) => {
if cc.is_not() {
return None;
}
ranges.extend(class_members(cc));
Some((1, 1))
}
NodeInner::String(sn) => {
if node.has_status(ND_ST_IGNORECASE) && !sn.is_crude() {
return None;
}
let text = std::str::from_utf8(&sn.s).ok()?;
let mut count = 0;
for c in text.chars() {
ranges.push((c as u32, c as u32));
count += 1;
}
Some((count, count))
}
NodeInner::List(_) => {
let mut lens = (0, 0);
for item in cons_items(node) {
let (lo, hi) = class_union(item, ranges)?;
lens = (lens.0 + lo, lens.1 + hi);
}
Some(lens)
}
NodeInner::Alt(_) => {
let mut lens = (usize::MAX, 0);
for item in cons_items(node) {
let (lo, hi) = class_union(item, ranges)?;
lens = (lens.0.min(lo), lens.1.max(hi));
}
Some(lens)
}
NodeInner::Bag(bn) if !matches!(bn.bag_data, BagData::IfElse { .. }) => {
class_union(bn.body.as_deref()?, ranges)
}
_ => None,
}
}
fn cons_items(node: &Node) -> Vec<&Node> {
let mut items = Vec::new();
let mut cur = node;
loop {
match &cur.inner {
NodeInner::List(cons) | NodeInner::Alt(cons) => {
items.push(&*cons.car);
match &cons.cdr {
Some(next) => cur = next,
None => break,
}
}
_ => {
items.push(cur);
break;
}
}
}
items
}
fn collect_tails(node: &Node, out: &mut Vec<*const Node>) {
match &node.inner {
NodeInner::List(_) => {
if let Some(&last) = cons_items(node).last() {
collect_tails(last, out);
}
}
NodeInner::Alt(_) => {
for branch in cons_items(node) {
collect_tails(branch, out);
}
}
NodeInner::Bag(bn) if !matches!(bn.bag_data, BagData::IfElse { .. }) => {
if let Some(body) = bn.body.as_deref() {
collect_tails(body, out);
}
}
NodeInner::Anchor(an) if an.anchor_type == ANCR_SEMI_END_BUF => {
out.push(node as *const Node);
}
_ => {}
}
}
struct Walk<'a> {
reg: &'a RegexType,
lookaheads_kept: u32,
inexact: bool,
tails: Vec<*const Node>,
precise: bool,
}
impl Walk<'_> {
fn item(&mut self, node: &Node, rest_droppable: bool) -> Hir {
if let NodeInner::Anchor(an) = &node.inner {
if an.anchor_type == ANCR_PREC_READ {
return self.lookahead(an, rest_droppable);
}
}
self.node(node, rest_droppable)
}
fn lookahead(&mut self, an: &AnchorNode, keep: bool) -> Hir {
self.inexact = true;
if keep {
self.lookaheads_kept += 1;
an.body
.as_deref()
.map_or_else(Hir::empty, |body| self.item(body, true))
} else {
Hir::empty()
}
}
fn node(&mut self, node: &Node, rest_droppable: bool) -> Hir {
if node.has_status(ND_ST_LITERAL_ALT) {
return self.trie(node);
}
match &node.inner {
NodeInner::String(sn) => {
if node.has_status(ND_ST_IGNORECASE) {
self.inexact = true;
if !sn.is_crude() {
return placeholder();
}
}
if sn.s.is_empty() {
Hir::empty()
} else {
Hir::literal(sn.s.clone().into_boxed_slice())
}
}
NodeInner::CClass(cc) => self.cclass(cc),
NodeInner::CType(ct) => self.ctype(node, ct),
NodeInner::BackRef(_) | NodeInner::Call(_) => {
self.inexact = true;
placeholder()
}
NodeInner::Quant(qn) => {
let Some(body) = qn.body.as_deref() else {
return Hir::empty();
};
let body_rest = rest_droppable && (0..=1).contains(&qn.upper);
let sub = self.item(body, body_rest);
let min = u32::try_from(qn.lower.max(0)).unwrap_or(u32::MAX);
let max = (qn.upper >= 0).then_some(qn.upper as u32);
repetition(min, max, sub, &mut self.inexact)
}
NodeInner::Bag(bn) => match &bn.bag_data {
BagData::IfElse {
then_node,
else_node,
} => {
self.inexact = true;
let condition = bn
.body
.as_deref()
.map_or_else(Hir::empty, |cond| self.condition(cond));
let then = then_node
.as_deref()
.map_or_else(Hir::empty, |n| self.item(n, rest_droppable));
let otherwise = else_node
.as_deref()
.map_or_else(Hir::empty, |n| self.item(n, rest_droppable));
alternation(vec![concat(vec![condition, then]), otherwise])
}
BagData::StopBacktrack | BagData::TextSegment => {
if !(matches!(bn.bag_data, BagData::StopBacktrack)
&& automatic_possessive(node))
{
self.inexact = true;
}
bn.body
.as_deref()
.map_or_else(Hir::empty, |body| self.item(body, rest_droppable))
}
_ => bn
.body
.as_deref()
.map_or_else(Hir::empty, |body| self.item(body, rest_droppable)),
},
NodeInner::Anchor(an) => {
if self.tails.contains(&(node as *const Node)) {
let newline = repetition(0, Some(1), Hir::literal(*b"\n"), &mut self.inexact);
return concat(vec![newline, Hir::look(Look::End)]);
}
self.anchor(an)
}
NodeInner::List(_) => self.list(node, rest_droppable),
NodeInner::Alt(_) => {
let branches = cons_items(node);
if !self.precise
&& branches.len() >= 2
&& matches!(branches[0].inner, NodeInner::CClass(_))
{
let mut ranges = Vec::new();
let mut lens: Option<(usize, usize)> = Some((usize::MAX, 0));
for branch in &branches {
match class_union(branch, &mut ranges) {
Some((lo, hi)) => lens = lens.map(|(a, b)| (a.min(lo), b.max(hi))),
None => {
lens = None;
break;
}
}
}
if let Some((min, max)) = lens {
self.inexact = true;
return Hir::repetition(Repetition {
min: u32::try_from(min).unwrap_or(u32::MAX),
max: Some(u32::try_from(max).unwrap_or(u32::MAX)),
greedy: true,
sub: Box::new(class_hir(ranges, false)),
});
}
}
let branches = branches
.into_iter()
.map(|branch| self.item(branch, rest_droppable))
.collect();
alternation(branches)
}
NodeInner::Gimmick(gn) => {
let keep = match gn.gimmick_type {
GimmickType::Save => gn.detail_type == SaveType::Keep as i32,
GimmickType::UpdateVar => {
gn.detail_type == UpdateVarType::KeepFromStackLast as i32
}
_ => false,
};
if !keep {
self.inexact = true;
}
match gn.gimmick_type {
GimmickType::Fail => Hir::fail(),
_ => Hir::empty(),
}
}
}
}
fn condition(&mut self, cond: &Node) -> Hir {
if is_capture_check(cond) {
Hir::empty()
} else {
self.item(cond, false)
}
}
fn list(&mut self, node: &Node, rest_droppable: bool) -> Hir {
let items = cons_items(node);
let mut parts = Vec::with_capacity(items.len());
let mut kept_before = false;
for (k, item) in items.iter().enumerate() {
let rest =
rest_droppable && !kept_before && items[k + 1..].iter().all(|n| droppable(n));
let kept = self.lookaheads_kept;
parts.push(self.item(item, rest));
if self.lookaheads_kept != kept {
kept_before = true;
}
}
concat(parts)
}
fn anchor(&mut self, an: &AnchorNode) -> Hir {
if !matches!(
an.anchor_type,
ANCR_BEGIN_BUF | ANCR_END_BUF | ANCR_END_LINE | ANCR_BEGIN_LINE
) {
self.inexact = true;
}
match an.anchor_type {
ANCR_PREC_READ | ANCR_LOOK_BEHIND => Hir::empty(),
ANCR_PREC_READ_NOT | ANCR_LOOK_BEHIND_NOT => {
word_class_look(an).map_or_else(Hir::empty, Hir::look)
}
ANCR_BEGIN_BUF => Hir::look(Look::Start),
ANCR_END_BUF => Hir::look(Look::End),
ANCR_SEMI_END_BUF | ANCR_END_LINE => Hir::look(Look::EndLF),
ANCR_BEGIN_LINE => Hir::look(Look::StartLF),
ANCR_BEGIN_POSITION => Hir::empty(),
ANCR_WORD_BOUNDARY | ANCR_NO_WORD_BOUNDARY | ANCR_WORD_BEGIN | ANCR_WORD_END
if an.ascii_mode =>
{
Hir::look(match an.anchor_type {
ANCR_WORD_BOUNDARY => Look::WordAscii,
ANCR_NO_WORD_BOUNDARY => Look::WordAsciiNegate,
ANCR_WORD_BEGIN => Look::WordStartAscii,
_ => Look::WordEndAscii,
})
}
ANCR_WORD_BOUNDARY | ANCR_NO_WORD_BOUNDARY | ANCR_WORD_BEGIN | ANCR_WORD_END => {
Hir::alternation(vec![
Hir::look(match an.anchor_type {
ANCR_WORD_BOUNDARY => Look::WordAscii,
ANCR_NO_WORD_BOUNDARY => Look::WordAsciiNegate,
ANCR_WORD_BEGIN => Look::WordStartAscii,
_ => Look::WordEndAscii,
}),
Hir::look(Look::WordStartHalfAscii),
Hir::look(Look::WordEndHalfAscii),
])
}
_ => Hir::empty(),
}
}
fn cclass(&mut self, cc: &CClassNode) -> Hir {
let ranges: Vec<(u32, u32)> = class_members(cc).collect();
if self.precise && !cc.is_not() {
if let Some(hir) = explicit_class(&ranges) {
return hir;
}
}
if !class_exact(&ranges) {
self.inexact = true;
}
class_hir(ranges, cc.is_not())
}
fn ctype(&mut self, node: &Node, ct: &CtypeNode) -> Hir {
if ct.ctype == CTYPE_ANYCHAR {
return if node.has_status(ND_ST_MULTILINE) {
any_char()
} else {
any_char_except_newline()
};
}
if !(ct.ctype == ONIGENC_CTYPE_WORD as i32 && ct.ascii_mode) {
self.inexact = true;
}
if ct.ctype == ONIGENC_CTYPE_WORD as i32 {
let word = ascii_mask(b'0', b'9')
| ascii_mask(b'A', b'Z')
| ascii_mask(b'_', b'_')
| ascii_mask(b'a', b'z');
return if ct.not {
byte_class(!word, true)
} else {
byte_class(word, !ct.ascii_mode)
};
}
any_char()
}
fn trie(&mut self, node: &Node) -> Hir {
let reg = self.reg;
let Some(trie) = literal_alt_trie_index(node).and_then(|idx| reg.literal_tries.get(idx))
else {
self.inexact = true;
return placeholder();
};
let literals = trie.literals();
if !trie.is_case_insensitive() {
return alternation(
literals
.iter()
.map(|literal| Hir::literal(literal.clone().into_boxed_slice()))
.collect(),
);
}
self.inexact = true;
let Some(folds) = trie.folds() else {
return placeholder();
};
let mut branches: Vec<Hir> = literals
.iter()
.map(|literal| {
concat(
literal
.iter()
.map(|&c| {
if c.is_ascii_alphabetic() {
Hir::class(Class::Bytes(ClassBytes::new([
ClassBytesRange::new(
c.to_ascii_lowercase(),
c.to_ascii_lowercase(),
),
ClassBytesRange::new(
c.to_ascii_uppercase(),
c.to_ascii_uppercase(),
),
])))
} else {
Hir::literal([c])
}
})
.collect(),
)
})
.collect();
let mut non_ascii: Vec<Hir> = folds
.class_members
.iter()
.filter_map(|&(code, _)| char::from_u32(code))
.map(|c| Hir::literal(c.to_string().into_bytes().into_boxed_slice()))
.chain(
folds
.ligatures
.iter()
.map(|(bytes, _)| Hir::literal(bytes.clone().into_boxed_slice())),
)
.collect();
if !non_ascii.is_empty() {
non_ascii.sort_by_key(|a| a.to_string());
non_ascii.dedup_by(|a, b| a.to_string() == b.to_string());
let longest = literals.iter().map(Vec::len).max().unwrap_or(1).max(1);
let rest = u32::try_from(longest - 1).unwrap_or(u32::MAX);
let ascii_prefix = repetition(
0,
Some(rest),
Hir::class(Class::Bytes(ClassBytes::new([ClassBytesRange::new(
0x00, 0x7F,
)]))),
&mut self.inexact,
);
let suffix = repetition(0, Some(rest), any_char(), &mut self.inexact);
branches.push(concat(vec![ascii_prefix, alternation(non_ascii), suffix]));
}
alternation(branches)
}
}
pub(crate) struct Automata {
search: Arc<hybrid_regex::Regex>,
dfa: hybrid::DFA,
entries: Vec<u16>,
own: Vec<u16>,
long: Vec<u16>,
settle: usize,
}
#[derive(Clone, Default)]
pub(crate) struct AutomataCell(Arc<OnceLock<Option<Arc<Automata>>>>);
impl AutomataCell {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn get_or_build(
&self,
build: impl FnOnce() -> Option<Automata>,
) -> Option<Arc<Automata>> {
self.0.get_or_init(|| build().map(Arc::new)).clone()
}
}
pub(crate) struct SetPrefilter {
search: Arc<hybrid_regex::Regex>,
search_cache: hybrid_regex::Cache,
dfa: hybrid::DFA,
dfa_cache: hybrid::Cache,
patset: PatternBits,
entries: Vec<u16>,
own: Vec<u16>,
long: Vec<u16>,
settle: usize,
ask_early: bool,
gave_up: bool,
walked: usize,
dispatch: Vec<[u64; 4]>,
long_dispatch: [u64; 4],
any_dispatch: [u64; 4],
admissible: Vec<u32>,
stamp: u32,
candidates: Vec<u16>,
walked_at: Option<usize>,
search_memo: Option<SearchMemo>,
candidate_memo: CandidateMemo,
}
#[derive(Default)]
struct CandidateMemo {
subject: Option<(crate::regset::FallbackMemoIdentity, usize)>,
active: bool,
last_start: usize,
at: Vec<u32>,
spans: Vec<(u32, u16, bool)>,
store: Vec<u16>,
}
#[inline(never)]
fn record_match(
dfa: &hybrid::DFA,
cache: &hybrid::Cache,
patset: &mut PatternBits,
sid: LazyStateID,
) {
for k in 0..dfa.match_len(cache, sid) {
patset.insert(dfa.match_pattern(cache, sid, k));
}
}
#[inline(never)]
fn record_needed(
dfa: &hybrid::DFA,
cache: &hybrid::Cache,
patset: &mut PatternBits,
sid: LazyStateID,
stamps: &[u32],
stamp: u32,
) -> usize {
let mut found = 0;
for k in 0..dfa.match_len(cache, sid) {
let pid = dfa.match_pattern(cache, sid, k);
if patset.insert(pid) && stamps[pid.as_usize()] == stamp {
found += 1;
}
}
found
}
struct PatternBits {
words: Box<[u64]>,
len: usize,
capacity: usize,
}
impl PatternBits {
fn new(capacity: usize) -> Self {
PatternBits {
words: vec![0; capacity.div_ceil(64)].into_boxed_slice(),
len: 0,
capacity,
}
}
#[inline]
fn clear(&mut self) {
if self.len > 0 {
self.words.fill(0);
self.len = 0;
}
}
#[inline]
fn contains(&self, pid: PatternID) -> bool {
let i = pid.as_usize();
i < self.capacity && (self.words[i / 64] >> (i % 64)) & 1 == 1
}
#[inline]
fn insert(&mut self, pid: PatternID) -> bool {
let i = pid.as_usize();
assert!(i < self.capacity, "pattern ID {i} outside the set");
let word = &mut self.words[i / 64];
let bit = 1u64 << (i % 64);
if *word & bit != 0 {
return false;
}
*word |= bit;
self.len += 1;
true
}
#[inline]
fn is_empty(&self) -> bool {
self.len == 0
}
#[inline]
fn is_full(&self) -> bool {
self.len == self.capacity
}
#[inline]
fn iter(&self) -> impl Iterator<Item = usize> + '_ {
self.words.iter().enumerate().flat_map(|(w, &word)| {
let mut bits = word;
std::iter::from_fn(move || {
(bits != 0).then(|| {
let b = bits.trailing_zeros() as usize;
bits &= bits - 1;
w * 64 + b
})
})
})
}
}
const CANDIDATE_MEMO_POSITIONS: usize = 4096;
impl CandidateMemo {
#[inline]
fn begin(
&mut self,
subject: Option<(crate::regset::FallbackMemoIdentity, usize)>,
start: usize,
) {
if subject.is_some() && subject == self.subject {
if start <= self.last_start {
self.active = true;
}
self.last_start = start;
return;
}
self.subject = subject;
self.active = false;
self.last_start = start;
self.at.clear();
self.spans.clear();
self.store.clear();
}
fn get(&self, at: usize) -> Option<(&[u16], bool)> {
let at = u32::try_from(at).ok()?;
let i = self.at.binary_search(&at).ok()?;
let (start, len, some) = self.spans[i];
Some((
&self.store[start as usize..start as usize + len as usize],
some,
))
}
fn insert(&mut self, at: usize, candidates: &[u16], some: bool) {
if self.at.len() >= CANDIDATE_MEMO_POSITIONS {
return;
}
let (Ok(at), Ok(len)) = (u32::try_from(at), u16::try_from(candidates.len())) else {
return;
};
let start = self.store.len() as u32;
self.store.extend_from_slice(candidates);
let i = match self.at.binary_search(&at) {
Ok(_) => return,
Err(i) => i,
};
self.at.insert(i, at);
self.spans.insert(i, (start, len, some));
}
}
#[derive(Clone, Copy)]
struct SearchMemo {
subject: (crate::regset::FallbackMemoIdentity, usize),
from: usize,
until: usize,
at: Option<usize>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum ScanEnd {
Finished,
Settled,
Bound,
GaveUp,
}
impl std::fmt::Debug for SetPrefilter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SetPrefilter")
.field("entries", &self.entries.len())
.field("own", &self.own.len())
.field("nfa_states", &self.nfa_states())
.finish_non_exhaustive()
}
}
impl Automata {
pub(crate) fn build<'a>(
entries: impl Iterator<Item = (Option<&'a Seek>, &'a RegexType)>,
skip_tiny: bool,
) -> Option<Self> {
let mut hirs: Vec<Hir> = Vec::new();
let mut covered = Vec::new();
let mut own = Vec::new();
for (index, (seek, reg)) in entries.enumerate() {
let index = u16::try_from(index).ok()?;
match seek {
Some(Seek::Pattern(code)) => {
hirs.push(code.decode());
covered.push(index);
}
Some(Seek::Everywhere) => own.push(index),
None if reg.extp.as_ref().is_some_and(|ext| ext.callout_num != 0) => return None,
None => own.push(index),
}
}
if covered.is_empty() {
return None;
}
let entries = covered;
let nfa = thompson::Compiler::new()
.configure(
thompson::Config::new()
.utf8(false)
.which_captures(thompson::WhichCaptures::None)
.nfa_size_limit(Some(NFA_SIZE_LIMIT)),
)
.build_many_from_hir(&hirs)
.ok()?;
if nfa.states().len() > MAX_NFA_STATES {
return None;
}
if skip_tiny
&& entries.len() <= TINY_SET_ENTRIES
&& nfa.states().len() <= TINY_SET_NFA_STATES
{
return None;
}
let nfarev = thompson::Compiler::new()
.configure(
thompson::Config::new()
.utf8(false)
.reverse(true)
.which_captures(thompson::WhichCaptures::None)
.nfa_size_limit(Some(NFA_SIZE_LIMIT)),
)
.build_many_from_hir(&hirs)
.ok()?;
let forward = search_dfa(nfa.clone(), MatchKind::LeftmostFirst)?;
let reverse = search_dfa(nfarev, MatchKind::All)?;
let search = Arc::new(hybrid_regex::Builder::new().build_from_dfas(forward, reverse));
let dfa = hybrid::Builder::new()
.configure(overlapping_config())
.build_from_nfa(nfa)
.ok()?;
let mut long = Vec::new();
let mut settle = 0;
for (pid, hir) in hirs.iter().enumerate() {
match hir.properties().maximum_len() {
Some(len) if len < LONG_SEEK_BYTES => settle = settle.max(len + 1),
_ => long.push(pid as u16),
}
}
Some(Automata {
search,
dfa,
entries,
own,
long,
settle,
})
}
pub(crate) fn nfa_states(&self) -> usize {
self.dfa.get_nfa().states().len()
}
pub(crate) fn memory_usage(&self) -> usize {
automata_memory_usage(&self.dfa, &self.search)
}
}
fn automata_memory_usage(dfa: &hybrid::DFA, search: &hybrid_regex::Regex) -> usize {
dfa.get_nfa().memory_usage()
+ search.reverse().get_nfa().memory_usage()
+ search.forward().memory_usage()
+ search.reverse().memory_usage()
+ dfa.memory_usage()
}
fn search_dfa(nfa: thompson::NFA, match_kind: MatchKind) -> Option<hybrid::DFA> {
hybrid::Builder::new()
.configure(
hybrid::Config::new()
.match_kind(match_kind)
.cache_capacity(SEARCH_CACHE_CAPACITY)
.minimum_cache_clear_count(Some(MIN_CACHE_CLEARS))
.minimum_bytes_per_state(Some(SEARCH_MIN_BYTES_PER_STATE)),
)
.build_from_nfa(nfa)
.ok()
}
impl SetPrefilter {
pub(crate) fn new(automata: &Automata) -> Self {
let search_cache = automata.search.create_cache();
let dfa_cache = automata.dfa.create_cache();
let covered = automata.entries.len();
SetPrefilter {
search: automata.search.clone(),
search_cache,
dfa: automata.dfa.clone(),
dfa_cache,
patset: PatternBits::new(covered),
entries: automata.entries.clone(),
own: automata.own.clone(),
long: automata.long.clone(),
settle: automata.settle,
ask_early: false,
gave_up: false,
walked: 0,
dispatch: vec![[u64::MAX; 4]; covered],
long_dispatch: [0; 4],
any_dispatch: [0; 4],
admissible: vec![0; covered],
stamp: 0,
candidates: Vec::new(),
walked_at: None,
search_memo: None,
candidate_memo: CandidateMemo::default(),
}
}
pub(crate) fn set_dispatch(&mut self, mut bytes: impl FnMut(u16) -> [u64; 4]) {
for (pid, &index) in self.entries.iter().enumerate() {
self.dispatch[pid] = bytes(index);
}
let union = |pids: &mut dyn Iterator<Item = usize>| {
pids.fold([0u64; 4], |mut bits, pid| {
for (word, dispatch) in bits.iter_mut().zip(self.dispatch[pid]) {
*word |= dispatch;
}
bits
})
};
let long_dispatch = union(&mut self.long.iter().map(|&pid| pid as usize));
let any_dispatch = union(&mut (0..self.entries.len()));
self.long_dispatch = long_dispatch;
self.any_dispatch = any_dispatch;
}
#[inline]
pub(crate) fn any_dispatch(&self) -> [u64; 4] {
self.any_dispatch
}
pub(crate) fn own(&self) -> &[u16] {
&self.own
}
pub(crate) fn covered_entries(&self) -> &[u16] {
&self.entries
}
pub(crate) fn covered(&self) -> usize {
self.entries.len()
}
pub(crate) fn candidate_in(
&mut self,
haystack: &[u8],
from: usize,
until: usize,
subject: Option<(crate::regset::FallbackMemoIdentity, usize)>,
admits: &mut dyn FnMut(u16, usize) -> bool,
) -> Option<usize> {
let mut search_from = from;
let mut memo_from = from;
if let (Some(subject), Some(memo)) = (subject, self.search_memo) {
if memo.subject == subject && from >= memo.from {
match memo.at {
Some(at) if from <= at => return (at < until).then_some(at),
None if until <= memo.until => return None,
None if from <= memo.until => {
search_from = memo.until;
memo_from = memo.from;
}
_ => {}
}
}
}
let at = self.search_in(haystack, search_from, until, admits);
if let Some(subject) = subject {
self.search_memo = Some(SearchMemo {
subject,
from: memo_from,
until,
at,
});
}
at
}
#[inline]
pub(crate) fn note_position(
&mut self,
subject: Option<(crate::regset::FallbackMemoIdentity, usize)>,
at: usize,
candidate: bool,
next: usize,
) {
if let (Some(subject), Some(memo)) = (subject, self.search_memo.as_mut()) {
if memo.subject == subject && memo.at.is_none() && memo.until == at {
if candidate {
memo.at = Some(at);
} else {
memo.until = next;
}
}
}
}
fn search_in(
&mut self,
haystack: &[u8],
from: usize,
until: usize,
admits: &mut dyn FnMut(u16, usize) -> bool,
) -> Option<usize> {
if from >= until || from > haystack.len() {
return None;
}
let mut next = from;
if !self.long.is_empty() {
if self
.candidates_at(haystack, from, admits)
.is_some_and(|candidates| !candidates.is_empty())
{
return Some(from);
}
next = from + 1;
while next < haystack.len() && (haystack[next] & 0xC0) == 0x80 {
next += 1;
}
}
let span_end = haystack.len().min(until.saturating_add(self.settle));
let found = if next <= span_end {
#[cfg(test)]
EARLIEST_CALLS.with(|calls| calls.set(calls.get() + 1));
let input = Input::new(haystack).span(next..span_end);
let Ok(found) = self.search.try_search(&mut self.search_cache, &input) else {
self.gave_up = true;
return Some(from);
};
#[cfg(test)]
META_BYTES.with(|bytes| {
let read = found.map_or(span_end - next, |m| {
(m.end() - next) + (m.end() - m.start())
});
bytes.set(bytes.get() + read as u64);
});
found.map(|m| m.start()).filter(|&at| at < until)
} else {
None
};
if self.long.is_empty() {
return found;
}
let dispatch = self.long_dispatch;
let limit = found.unwrap_or(until);
let mut p = next;
while p < limit {
let admitted = haystack
.get(p)
.is_none_or(|&byte| (dispatch[byte as usize / 64] >> (byte % 64)) & 1 == 1);
if admitted
&& self
.candidates_at(haystack, p, admits)
.is_some_and(|candidates| !candidates.is_empty())
{
return Some(p);
}
p += 1;
while p < limit && p < haystack.len() && (haystack[p] & 0xC0) == 0x80 {
p += 1;
}
}
found
}
#[inline]
pub(crate) fn candidates_at(
&mut self,
haystack: &[u8],
at: usize,
admits: &mut dyn FnMut(u16, usize) -> bool,
) -> Option<&[u16]> {
self.candidates.clear();
self.walked_at = Some(at);
if self.candidate_memo.active {
if let Some((memo, some)) = self.candidate_memo.get(at) {
self.candidates.extend_from_slice(memo);
return some.then_some(&self.candidates);
}
}
self.patset.clear();
let (end, asked) = self.scan_candidates(haystack, at, admits);
self.ask_early = end == ScanEnd::Bound || (asked && end == ScanEnd::Settled);
if end == ScanEnd::Finished && self.patset.is_empty() {
if self.candidate_memo.active {
self.candidate_memo.insert(at, &[], false);
}
return None;
}
let entries = &self.entries;
if end == ScanEnd::Bound {
let stamps = &self.admissible;
let stamp = self.stamp;
let patset = &self.patset;
self.candidates.extend(
entries
.iter()
.enumerate()
.filter(|&(pid, _)| {
!asked
|| stamps[pid] == stamp
|| patset.contains(PatternID::new_unchecked(pid))
})
.map(|(_, &index)| index),
);
} else {
self.candidates
.extend(self.patset.iter().map(|pid| entries[pid]));
}
if self.candidate_memo.active {
self.candidate_memo.insert(at, &self.candidates, true);
}
Some(&self.candidates)
}
#[inline]
pub(crate) fn candidates(&self) -> &[u16] {
&self.candidates
}
#[inline]
pub(crate) fn walked_at(&self) -> Option<usize> {
self.walked_at
}
#[inline]
pub(crate) fn begin_search(
&mut self,
subject: Option<(crate::regset::FallbackMemoIdentity, usize)>,
start: usize,
) {
self.walked_at = None;
self.candidate_memo.begin(subject, start);
self.walked = 0;
self.dfa_cache.search_start(0);
}
fn scan_candidates(
&mut self,
haystack: &[u8],
at: usize,
admits: &mut dyn FnMut(u16, usize) -> bool,
) -> (ScanEnd, bool) {
let Self {
dfa,
dfa_cache: cache,
patset,
entries,
long,
settle,
ask_early,
gave_up,
walked,
dispatch,
admissible: stamps,
stamp,
..
} = self;
let mut i = at;
let (end, asked) = Self::walk(
dfa, cache, patset, entries, long, *settle, *ask_early, dispatch, stamps, stamp,
haystack, at, &mut i, admits,
);
*walked += i - at;
cache.search_update(*walked);
if end == ScanEnd::GaveUp {
*gave_up = true;
return (ScanEnd::Bound, asked);
}
(end, asked)
}
#[allow(clippy::too_many_arguments)]
fn walk(
dfa: &hybrid::DFA,
cache: &mut hybrid::Cache,
patset: &mut PatternBits,
entries: &[u16],
long: &[u16],
settle: usize,
ask_early: bool,
dispatch: &[[u64; 4]],
stamps: &mut [u32],
stamp: &mut u32,
haystack: &[u8],
at: usize,
i: &mut usize,
admits: &mut dyn FnMut(u16, usize) -> bool,
) -> (ScanEnd, bool) {
let end = haystack.len();
let input = Input::new(haystack).span(at..end).anchored(Anchored::Yes);
let Ok(mut sid) = dfa.start_state_forward(cache, &input) else {
return (ScanEnd::GaveUp, false);
};
let cut = end.min(at.saturating_add(CANDIDATE_SCAN_BYTES));
let ask_at = if ask_early {
settle
} else {
settle.max(LONG_WALK_BYTES)
};
let ask_i = at.saturating_add(ask_at);
let mut recorded: Option<(LazyStateID, usize)> = None;
macro_rules! at_end {
($asked:expr) => {
if let Ok(eoi) = dfa.next_eoi_state(cache, sid) {
if eoi.is_match() {
record_match(dfa, cache, patset, eoi);
}
return (ScanEnd::Finished, $asked);
}
return (ScanEnd::GaveUp, $asked);
};
}
let plain_end = cut.min(ask_i);
loop {
if sid.is_dead() {
return (ScanEnd::Finished, false);
}
if sid.is_quit() {
return (ScanEnd::Bound, false);
}
if sid.is_match() && recorded != Some((sid, cache.clear_count())) {
recorded = Some((sid, cache.clear_count()));
record_match(dfa, cache, patset, sid);
if patset.is_full() {
return (ScanEnd::Settled, false);
}
}
if *i >= plain_end {
if *i == end {
at_end!(false);
}
if *i == cut {
return (ScanEnd::Bound, false);
}
break;
}
#[cfg(test)]
SCAN_STEPS.with(|steps| steps.set(steps.get() + 1));
sid = match dfa.next_state(cache, sid, haystack[*i]) {
Ok(sid) => sid,
Err(_) => return (ScanEnd::GaveUp, false),
};
*i += 1;
}
*stamp = stamp.wrapping_add(1);
if *stamp == 0 {
stamps.fill(0);
*stamp = 1;
}
let byte = haystack[at];
let mut needed = 0usize;
for &pid in long.iter() {
let bits = dispatch[pid as usize];
if patset.contains(PatternID::new_unchecked(pid as usize))
|| (bits[byte as usize / 64] >> (byte % 64)) & 1 == 0
{
continue;
}
if admits(entries[pid as usize], at) {
stamps[pid as usize] = *stamp;
needed += 1;
}
}
if needed == 0 {
return (ScanEnd::Settled, true);
}
loop {
#[cfg(test)]
SCAN_STEPS.with(|steps| steps.set(steps.get() + 1));
sid = match dfa.next_state(cache, sid, haystack[*i]) {
Ok(sid) => sid,
Err(_) => return (ScanEnd::GaveUp, true),
};
*i += 1;
if sid.is_dead() {
return (ScanEnd::Finished, true);
}
if sid.is_quit() {
return (ScanEnd::Bound, true);
}
if sid.is_match() && recorded != Some((sid, cache.clear_count())) {
recorded = Some((sid, cache.clear_count()));
needed -= record_needed(dfa, cache, patset, sid, stamps, *stamp);
if needed == 0 || patset.is_full() {
return (ScanEnd::Settled, true);
}
}
if *i == cut {
if *i == end {
at_end!(true);
}
return (ScanEnd::Bound, true);
}
}
}
pub(crate) fn gave_up(&self) -> bool {
self.gave_up
}
pub(crate) fn nfa_states(&self) -> usize {
self.dfa.get_nfa().states().len()
}
pub(crate) fn memory_usage(&self) -> usize {
automata_memory_usage(&self.dfa, &self.search) + self.cache_memory_usage()
}
pub(crate) fn cache_memory_usage(&self) -> usize {
self.search_cache.memory_usage() + self.dfa_cache.memory_usage()
}
pub(crate) fn cache_clears(&self) -> usize {
self.dfa_cache.clear_count()
}
}
pub(crate) struct SingleSeek {
dfa: hybrid::DFA,
accept: Option<(hybrid::DFA, accept::AcceptRejects)>,
caches: Pool<SingleCaches, CacheMaker>,
exact: bool,
}
struct SingleCaches {
seek: hybrid::Cache,
accept: Option<hybrid::Cache>,
}
type CacheMaker = Box<dyn Fn() -> SingleCaches + Send + Sync>;
const SINGLE_CACHE_CAPACITY: usize = MIB;
fn nfa_states_estimate(hir: &Hir) -> usize {
states_estimate(hir, false)
}
fn states_estimate(hir: &Hir, utf8_classes: bool) -> usize {
match hir.kind() {
HirKind::Empty | HirKind::Look(_) => 1,
HirKind::Literal(literal) => literal.0.len() + 1,
HirKind::Class(Class::Unicode(class)) if utf8_classes => {
class.ranges().iter().fold(2usize, |sum, range| {
Utf8Sequences::new(range.start(), range.end())
.fold(sum, |sum, sequence| sum.saturating_add(sequence.len()))
})
}
HirKind::Class(_) => 2,
HirKind::Capture(capture) => states_estimate(&capture.sub, utf8_classes) + 2,
HirKind::Repetition(rep) => {
let copies = rep.max.unwrap_or(rep.min).max(rep.min).max(1) as usize;
states_estimate(&rep.sub, utf8_classes)
.saturating_mul(copies)
.saturating_add(2)
}
HirKind::Concat(parts) => parts.iter().fold(0usize, |sum, part| {
sum.saturating_add(states_estimate(part, utf8_classes))
}),
HirKind::Alternation(branches) => branches.iter().fold(2usize, |sum, branch| {
sum.saturating_add(states_estimate(branch, utf8_classes))
}),
}
}
impl SingleSeek {
pub(crate) fn build(seek: &Seek, accept: Option<&AcceptCode>) -> Option<Self> {
let Seek::Pattern(code) = seek else {
return None;
};
let dfa = single_dfa(&code.decode())?;
let accept = accept.and_then(|accept| {
let dfa = single_dfa(&accept.code.decode())?;
Some((dfa, accept.rejects.clone()))
});
let make_seek = dfa.clone();
let make_accept = accept.as_ref().map(|(dfa, _)| dfa.clone());
let caches = Pool::new(Box::new(move || SingleCaches {
seek: make_seek.create_cache(),
accept: make_accept.as_ref().map(hybrid::DFA::create_cache),
}) as CacheMaker);
Some(SingleSeek {
dfa,
accept,
caches,
exact: code.exact(),
})
}
pub(crate) fn search(&self, text: &[u8], start: usize) -> Option<bool> {
let input = Input::new(text).span(start..text.len()).earliest(true);
let mut caches = self.caches.get();
self.dfa
.try_search_fwd(&mut caches.seek, &input)
.ok()
.map(|found| found.is_some())
}
pub(crate) fn exact(&self) -> bool {
self.exact
}
pub(crate) fn has_accept(&self) -> bool {
self.accept.is_some()
}
pub(crate) fn accept_answer(&self, text: &[u8], start: usize) -> Option<bool> {
let (dfa, rejects) = self.accept.as_ref()?;
let input = Input::new(text).span(start..text.len()).earliest(true);
let mut caches = self.caches.get();
let found = dfa
.try_search_fwd(caches.accept.as_mut()?, &input)
.ok()?
.is_some();
if found {
return Some(true);
}
match rejects {
accept::AcceptRejects::Unless(triggers) => {
(!triggers.occur_in(&text[start..])).then_some(false)
}
accept::AcceptRejects::Never => None,
}
}
#[cfg(test)]
pub(crate) fn memory_usage(&self) -> usize {
let caches = self.caches.get();
let accept = self
.accept
.as_ref()
.map_or(0, |(dfa, _)| dfa.memory_usage());
self.dfa.memory_usage()
+ accept
+ caches.seek.memory_usage()
+ caches
.accept
.as_ref()
.map_or(0, hybrid::Cache::memory_usage)
}
}
fn single_dfa(hir: &Hir) -> Option<hybrid::DFA> {
if nfa_states_estimate(hir) > MAX_NFA_STATES {
return None;
}
let nfa = thompson::Compiler::new()
.configure(
thompson::Config::new()
.utf8(false)
.which_captures(thompson::WhichCaptures::None)
.nfa_size_limit(Some(NFA_SIZE_LIMIT)),
)
.build_from_hir(hir)
.ok()?;
if nfa.states().len() > MAX_NFA_STATES {
return None;
}
hybrid::Builder::new()
.configure(
hybrid::Config::new()
.match_kind(MatchKind::LeftmostFirst)
.cache_capacity(SINGLE_CACHE_CAPACITY),
)
.build_from_nfa(nfa)
.ok()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::encodings::utf8::ONIG_ENCODING_UTF8;
use crate::oniguruma::{
ONIG_OPTION_CAPTURE_GROUP, ONIG_OPTION_IGNORECASE, ONIG_OPTION_WORD_IS_ASCII,
OnigOptionType,
};
use crate::regcomp::onig_new_for_scanner;
use crate::regsyntax::OnigSyntaxOniguruma;
use crate::scanner::{Scanner, ScannerConfig, ScannerFindOptions};
fn seek_with(
pattern: &str,
options: OnigOptionType,
) -> Result<Option<Seek>, crate::error::RegexError> {
onig_new_for_scanner(
pattern.as_bytes(),
options,
&ONIG_ENCODING_UTF8,
&OnigSyntaxOniguruma,
false,
true,
)
.map(|(_, seek)| seek)
}
fn seek(pattern: &str) -> Option<Seek> {
seek_with(pattern, ONIG_OPTION_CAPTURE_GROUP).unwrap()
}
fn seek_pattern_with(pattern: &str, options: OnigOptionType) -> String {
match seek_with(pattern, options) {
Ok(Some(Seek::Pattern(seek))) => seek.decode().to_string(),
other => format!("{other:?}"),
}
}
fn seek_pattern(pattern: &str) -> String {
seek_pattern_with(pattern, ONIG_OPTION_CAPTURE_GROUP)
}
fn exact_with(pattern: &str, options: OnigOptionType) -> Option<bool> {
let seek = crate::regcomp::onig_new_for_single_seek(
pattern.as_bytes(),
options,
&ONIG_ENCODING_UTF8,
&OnigSyntaxOniguruma,
false,
true,
)
.unwrap()
.1;
match seek {
Some(Seek::Pattern(code)) => Some(code.exact()),
_ => None,
}
}
#[test]
fn seeks_are_exact_exactly_where_the_matcher_reads_alike() {
let exact = [
"abc",
"[a-c]",
"a|bc",
"ab{2,5}c",
"[^a]",
".",
"\u{3042}+",
"\\x{1F600}",
"x|$",
"a$",
"\\Aab\\z",
"a\\Kb",
"^a",
"^[0-9]{8}$",
"(?:^a|^b)c",
"(?i)[0-9a-f]+",
"(?i)k",
"(?i)s",
"[\u{e9}]",
"(a)(b)",
"^[a-z]+@[a-z]+\\.com$",
"[a-z]*@",
"[^\"]*\"",
"(?:ab)*c",
"([a-z]+)@([a-z]+)",
];
let inexact = [
"(?>abc|ab)c",
"(?>a+)b",
"a++b",
"a*+b",
"[a-z]++@",
"^[a-z]+@(?>[a-z]+)\\.com$",
"(?>[a-z]+)@",
"\\b",
"\\B",
"(?=a)b",
"(?<=a)b",
"a(?!b)",
"a(?<!b)",
"\\Zb",
"^",
"^$",
"(a)\\1",
"\\W",
"\\w",
"\\d+",
"[^\u{e9}]",
"\\G\\w",
"(?:ab|cd){100}",
"(x)?(?(1)a|b)",
];
for pattern in exact {
assert_eq!(
exact_with(pattern, ONIG_OPTION_CAPTURE_GROUP),
Some(true),
"{pattern}"
);
}
for pattern in inexact {
assert_eq!(
exact_with(pattern, ONIG_OPTION_CAPTURE_GROUP),
Some(false),
"{pattern}"
);
}
let ascii_word = ONIG_OPTION_CAPTURE_GROUP | crate::oniguruma::ONIG_OPTION_WORD_IS_ASCII;
assert_eq!(exact_with("\\w+:", ascii_word), Some(true));
assert_eq!(exact_with("\\w+:", ONIG_OPTION_CAPTURE_GROUP), Some(false));
let singleline = ONIG_OPTION_CAPTURE_GROUP | crate::oniguruma::ONIG_OPTION_SINGLELINE;
assert_eq!(exact_with("^a$", singleline), Some(true));
assert_eq!(exact_with("a$|b$", singleline), Some(true));
assert_eq!(exact_with("(?:a|b)\\Z", singleline), Some(true));
assert_eq!(exact_with("a$b", singleline), Some(false));
assert_eq!(exact_with("a$", ONIG_OPTION_CAPTURE_GROUP), Some(true));
assert_eq!(exact_with("(?~abc)x", ONIG_OPTION_CAPTURE_GROUP), None);
assert_eq!(exact_with("(*FAIL)|a", ONIG_OPTION_CAPTURE_GROUP), None);
assert_eq!(exact_with("^a", ONIG_OPTION_NOTBOL), Some(false));
assert_eq!(exact_with("a$", ONIG_OPTION_NOTEOL), Some(false));
assert_eq!(exact_with("a", ONIG_OPTION_FIND_NOT_EMPTY), Some(false));
}
#[test]
fn seeks_approximate_each_construct() {
let cases: &[(&str, &str)] = &[
("abc", "(?:abc)"),
("[a-c]", "(?-u:[a-c])"),
("a|bc", "(?:a|(?:bc))"),
("ab{2,5}c", "(?:ab{2,5}c)"),
("(?:ab)+?", "(?:ab)+"),
("(a)(?:b)(?>c)d?", "(?:(?:abc)d?)"),
("(?:a|bc){70}", "(?:a|(?:bc)){64,}"),
("(?:(?:ab)+)?c", "(?:(?:ab)*c)"),
("(?:(?:ab){2,3}){2}", "(?:ab){4,6}"),
("(?:a*)+b", "(?:a*b)"),
(
"[^x]{2,70}",
"(?:(?-u:[\\x00-wy-\\x7F])|(?:(?-u:[\\xC2-\\xF4])(?-u:[\\x80-\\xBF]){1,3})){2,}",
),
("[é]", "(?:(?-u:[\\xC2-\\xF4])(?-u:[\\x80-\\xBF]){1,3})"),
(
"[^a]",
"(?:(?-u:[\\x00-`b-\\x7F])|(?:(?-u:[\\xC2-\\xF4])(?-u:[\\x80-\\xBF]){1,3}))",
),
(
"\\w",
"(?:(?-u:[0-9A-Z_a-z])|(?:(?-u:[\\xC2-\\xF4])(?-u:[\\x80-\\xBF]){1,3}))",
),
(
".",
"(?:(?-u:[\\x00-\\x09\\x0B-\\x7F])|(?:(?-u:[\\xC2-\\xF4])(?-u:[\\x80-\\xBF]){1,3}))",
),
(
"(?m:.)",
"(?:(?-u:[\\x00-\\x7F])|(?:(?-u:[\\xC2-\\xF4])(?-u:[\\x80-\\xBF]){1,3}))",
),
("\\Aa", "(?:\\Aa)"),
("a\\z", "(?:a\\z)"),
("a\\Z", "(?:a(?m:$))"),
("^a$", "(?:(?m:^)a(?m:$))"),
(
"\\ba",
"(?:(?:(?-u:\\b)|(?-u:\\b{start-half})|(?-u:\\b{end-half}))a)",
),
("(?<=x)a", "a"),
("(?<!x)a", "a"),
("(?<!\\w)a", "(?:(?-u:\\b{start-half})a)"),
(
"(?<![-\\w])a(?![-\\w])",
"(?:(?-u:\\b{start-half})a(?-u:\\b{end-half}))",
),
("(?<!\\W)a", "a"),
("(?<![^\\w])a", "a"),
("(?=ab)(?<!\\w)", "(?-u:\\b{start-half})"),
("(?!x)a", "a"),
("\\Ga", "a"),
("a\\Kb", "(?:ab)"),
("\\ya", "a"),
(
"\\s+(?=use\\b)",
"(?:(?:(?-u:[\\x09-\\x0D\\x20])|(?:(?-u:[\\xC2-\\xF4])(?-u:[\\x80-\\xBF]){1,3}))+(?:use)(?:(?-u:\\b)|(?-u:\\b{start-half})|(?-u:\\b{end-half})))",
),
("(?=a)(?<=b)", "a"),
("(?=a)b", "b"),
("(?=a)(?=b)", "a"),
("y(?:x(?=a))?", "(?:y(?:xa)?)"),
("y(?:x(?=a))+", "(?:yx+)"),
("(?:x(?=a))?y", "(?:x?y)"),
(
"(a)\\1",
"(?:a(?:(?-u:[\\x00-\\x7F])|(?:(?-u:[\\xC2-\\xF4])(?-u:[\\x80-\\xBF]){1,3}))*)",
),
(
"a\\g<1>|(b)",
"(?:(?:a(?:(?-u:[\\x00-\\x7F])|(?:(?-u:[\\xC2-\\xF4])(?-u:[\\x80-\\xBF]){1,3}))*)|b)",
),
("(a)?(?(1)b|c)", "(?:a?[bc])"),
("(?(a)b|c)", "(?:(?:ab)|c)"),
(
"[^[^[^İ]\\S]]",
"(?:(?-u:[\\x00-\\x7F])|(?:(?-u:[\\xC2-\\xF4])(?-u:[\\x80-\\xBF]){1,3}))",
),
("[^[^İ]\\S]", "[a&&b]"),
(
"\\W",
"(?:(?-u:[\\x00-/:-@\\[-\\^`\\{-\\x7F])|(?:(?-u:[\\xC2-\\xF4])(?-u:[\\x80-\\xBF]){1,3}))",
),
("(?i)ab", "(?:(?-u:[Aa])(?-u:[Bb]))"),
(
"(?i)[k]",
"(?:(?-u:[Kk])|(?:(?-u:[\\xC2-\\xF4])(?-u:[\\x80-\\xBF]){1,3}))",
),
(
"(?:error|warn|fatal|panic)",
"(?:(?:error)|(?:warn)|(?:fatal)|(?:panic))",
),
(
"(?i)(?:ab|cd|ef|gh)",
"(?:(?:(?-u:[Aa])(?-u:[Bb]))|(?:(?-u:[Cc])(?-u:[Dd]))|(?:(?-u:[Ee])(?-u:[Ff]))|(?:(?-u:[Gg])(?-u:[Hh]))|(?:(?-u:[\\x00-\\x7F])?[KSks\u{17f}\u{212a}](?:(?-u:[\\x00-\\x7F])|(?:(?-u:[\\xC2-\\xF4])(?-u:[\\x80-\\xBF]){1,3}))?))",
),
(
"(?i)(?:kb|ss|st|xy)",
"(?:(?:(?-u:[Kk])(?-u:[Bb]))|(?:(?-u:[Ss])(?-u:[Ss]))|(?:(?-u:[Ss])(?-u:[Tt]))|(?:(?-u:[Xx])(?-u:[Yy]))|(?:(?-u:[\\x00-\\x7F])?[KSks\u{df}\u{17f}\u{1e9e}\u{212a}\u{fb05}\u{fb06}\u{1df95}](?:(?-u:[\\x00-\\x7F])|(?:(?-u:[\\xC2-\\xF4])(?-u:[\\x80-\\xBF]){1,3}))?))",
),
];
let mismatches: Vec<String> = cases
.iter()
.filter(|(pattern, expected)| seek_pattern(pattern) != *expected)
.map(|(pattern, _)| format!("{pattern:?} => {:?}", seek_pattern(pattern)))
.collect();
assert!(mismatches.is_empty(), "{}", mismatches.join("\n"));
let ascii_word = ONIG_OPTION_CAPTURE_GROUP | ONIG_OPTION_WORD_IS_ASCII;
assert_eq!(seek_pattern_with(r"\w", ascii_word), "(?-u:[0-9A-Z_a-z])");
assert_eq!(seek_pattern_with(r"\ba", ascii_word), "(?:(?-u:\\b)a)");
}
#[test]
fn seeks_that_match_everywhere_leave_the_entry_on_its_own() {
for pattern in [
"",
r"(?<=\))",
r"(?!\s*\[)",
"a*",
r"(a)?\1",
r"\G ?",
r"(?<=}|%>)\s*",
r"(?i)x*",
r"(?~abc)d",
] {
assert_eq!(seek(pattern), Some(Seek::Everywhere), "{pattern}");
}
assert_eq!(seek_pattern("^"), "(?m:^)");
assert_eq!(
seek_pattern(r"\b"),
"(?:(?-u:\\b)|(?-u:\\b{start-half})|(?-u:\\b{end-half}))"
);
}
#[test]
fn seek_codes_round_trip_over_the_captured_grammars() {
let mut patterns = std::collections::BTreeSet::new();
for name in ["cpp", "java", "scss", "c", "php"] {
let path = format!(
"{}/benches/{name}_scanner/trace.json",
env!("CARGO_MANIFEST_DIR")
);
let trace: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
for set in trace["scanners"].as_array().unwrap() {
for pattern in set.as_array().unwrap() {
patterns.insert(pattern.as_str().unwrap().to_owned());
}
}
}
let mut codes = 0usize;
let mut bytes = 0usize;
for pattern in &patterns {
let Some(Seek::Pattern(code)) = seek(pattern) else {
continue;
};
let hir = code.decode();
let again = SeekCode::encode(&hir, code.exact()).expect("the walk's nodes encode");
assert_eq!(again, code, "{pattern}");
assert_eq!(again.decode(), hir, "{pattern}");
codes += 1;
bytes += code.len();
}
assert!(
codes > 600,
"{codes} seeks over {} patterns",
patterns.len()
);
assert!(bytes / codes < 1024, "{bytes} bytes over {codes} seeks");
}
fn flat_grammar_patterns(json: &str) -> Vec<String> {
fn collect(value: &serde_json::Value, out: &mut Vec<String>) {
match value {
serde_json::Value::Object(map) => {
for key in ["match", "begin"] {
if let Some(pattern) = map.get(key).and_then(|v| v.as_str()) {
if !out.iter().any(|p| p == pattern) {
out.push(pattern.to_owned());
}
}
}
for (_, value) in map {
collect(value, out);
}
}
serde_json::Value::Array(items) => {
for item in items {
collect(item, out);
}
}
_ => {}
}
}
let root: serde_json::Value = serde_json::from_str(json).unwrap();
let mut patterns = Vec::new();
collect(&root, &mut patterns);
patterns
.into_iter()
.filter(|pattern| Scanner::new(&[pattern.as_str()]).is_ok())
.collect()
}
fn tokenize(
scanner: &mut Scanner,
line: &crate::scanner::OnigString,
) -> Vec<(usize, usize, usize)> {
let len = line.utf16_len();
let mut pos = 0;
let mut tokens = Vec::new();
while pos < len {
let Some(m) = scanner.find_next_match_utf16(line, pos, ScannerFindOptions::NONE) else {
break;
};
let end = m.captures()[0].end;
tokens.push((m.index, m.captures()[0].start, end));
pos = if end > pos { end } else { pos + 1 };
}
tokens
}
#[test]
fn a_set_whose_cache_thrashes_retires_the_prefilter() {
let _limits = crate::regexec::shared_limits();
let json = std::fs::read_to_string(format!(
"{}/benches/grammars/css.json",
env!("CARGO_MANIFEST_DIR")
))
.unwrap();
let patterns = flat_grammar_patterns(&json);
let refs: Vec<&str> = patterns.iter().map(String::as_str).collect();
let mut filtered =
Scanner::with_config(&refs, &ScannerConfig::default().prefilter_warmup(0)).unwrap();
let mut plain =
Scanner::with_config(&refs, &ScannerConfig::default().prefilter(false)).unwrap();
let stylesheet = ".navbar-primary > .nav-item:first-child {\n\
background-color: oklch(0.65 0.15 250);\n\
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;\n\
transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1);\n\
--custom-property: var(--color-primary, #3b82f6);\n\
@media (min-width: 768px) and (prefers-color-scheme: dark) {\n}\n";
let line = crate::scanner::OnigString::new(stylesheet);
let stats = filtered.prefilter_stats();
assert!(stats.covered > 100 && !stats.retired, "{stats:?}");
let want = tokenize(&mut plain, &line);
let got = tokenize(&mut filtered, &line);
assert_eq!(got, want);
assert!(got.len() > 200);
let stats = filtered.prefilter_stats();
assert!(
stats.retired && !stats.built && stats.own == refs.len(),
"{stats:?}"
);
assert_eq!(tokenize(&mut filtered, &line), want);
assert!(filtered.prefilter_stats().retired);
}
#[test]
fn walks_of_a_subject_searched_again_are_kept() {
let _limits = crate::regexec::shared_limits();
let patterns = [r"[a-z]+:", r"\d+", r"[a-z]+\(", "b", "a"];
let mut filtered =
Scanner::with_config(&patterns, &ScannerConfig::default().prefilter_warmup(0)).unwrap();
let mut plain =
Scanner::with_config(&patterns, &ScannerConfig::default().prefilter(false)).unwrap();
let text = "abc: 12 foo(bar) a b xyz: 7 quux(1) ba";
let line = crate::scanner::OnigString::new(text);
let steps = || SCAN_STEPS.with(|c| c.get());
let want = tokenize(&mut plain, &line);
let before = steps();
assert_eq!(tokenize(&mut filtered, &line), want);
let first = steps() - before;
assert!(first > 0);
let before = steps();
assert_eq!(tokenize(&mut filtered, &line), want);
assert_eq!(steps() - before, first);
let before = steps();
assert_eq!(tokenize(&mut filtered, &line), want);
let third = steps() - before;
assert!(third < first / 4, "{third} steps after {first}");
let before = steps();
assert_eq!(tokenize(&mut filtered, &line), want);
assert_eq!(steps() - before, third);
let other = crate::scanner::OnigString::new(text);
let before = steps();
assert_eq!(tokenize(&mut filtered, &other), want);
assert_eq!(steps() - before, first);
}
#[test]
fn no_seek_without_the_prefilter_or_with_callouts() {
let plain = onig_new_for_scanner(
b"abc",
ONIG_OPTION_CAPTURE_GROUP,
&ONIG_ENCODING_UTF8,
&OnigSyntaxOniguruma,
false,
false,
)
.unwrap();
assert_eq!(plain.1, None);
assert_eq!(seek("(*FAIL)"), None);
assert_eq!(seek("(*MAX{1})a"), None);
assert_eq!(
seek_pattern_with("abc", ONIG_OPTION_IGNORECASE),
"(?:(?-u:[Aa])(?-u:[Bb])(?-u:[Cc]))"
);
let ascii = onig_new_for_scanner(
b"abc",
ONIG_OPTION_CAPTURE_GROUP,
&crate::encodings::ascii::ONIG_ENCODING_ASCII,
&OnigSyntaxOniguruma,
false,
true,
)
.unwrap();
assert_eq!(ascii.1, None);
}
#[test]
fn sets_over_the_state_cutoff_build_no_automata() {
let _limits = crate::regexec::shared_limits();
let branches: Vec<String> = (0..16)
.map(|i| format!("[{}{}]{{64}}{i:x}", (b'a' + i) as char, (b'A' + i) as char))
.collect();
let pattern = format!("(?:{})", branches.join("|"));
let patterns: Vec<&str> = std::iter::repeat_n(pattern.as_str(), 70)
.chain(["2"])
.collect();
let text = format!("1 {}0 2", "a".repeat(64));
let config = ScannerConfig::default().prefilter_warmup(0);
let mut big = Scanner::with_config(&patterns, &config).unwrap();
let mut little = Scanner::with_config(&patterns[69..], &config).unwrap();
for scanner in [&big, &little] {
let stats = scanner.prefilter_stats();
assert!(!stats.built && stats.nfa_states == 0, "{stats:?}");
}
let found = |scanner: &mut Scanner, start: usize| {
scanner
.find_next_match(&text, start, ScannerFindOptions::NONE)
.map(|m| (m.index, m.captures()[0].start, m.captures()[0].end))
};
assert_eq!(found(&mut big, 0), Some((0, 2, 67)));
assert_eq!(found(&mut big, 3), Some((70, 68, 69)));
assert_eq!(found(&mut little, 0), Some((0, 2, 67)));
assert_eq!(found(&mut little, 3), Some((1, 68, 69)));
let stats = big.prefilter_stats();
assert!(
!stats.built && stats.covered == 0 && stats.own == 71,
"{stats:?}"
);
let stats = little.prefilter_stats();
assert!(stats.built && stats.covered == 2 && stats.nfa_states < MAX_NFA_STATES);
}
struct OverlappingCacheBytes;
impl OverlappingCacheBytes {
fn set(bytes: usize) -> Self {
OVERLAPPING_CACHE_BYTES.with(|cell| cell.set(Some(bytes)));
Self
}
}
impl Drop for OverlappingCacheBytes {
fn drop(&mut self) {
OVERLAPPING_CACHE_BYTES.with(|cell| cell.set(None));
}
}
#[test]
fn a_walk_that_clears_the_cache_keeps_the_state_it_reaches() {
let _limits = crate::regexec::shared_limits();
let patterns = ["zz", "z", r"z\w*", "q:"];
let _cache = OverlappingCacheBytes::set(2048);
let mut filtered =
Scanner::with_config(&patterns, &ScannerConfig::default().prefilter_warmup(0)).unwrap();
let mut plain =
Scanner::with_config(&patterns, &ScannerConfig::default().prefilter(false)).unwrap();
let line = crate::scanner::OnigString::new("zqzzzz(");
let want = tokenize(&mut plain, &line);
let got = tokenize(&mut filtered, &line);
let stats = filtered.prefilter_stats();
assert!(stats.built && stats.cache_clears > 0, "{stats:?}");
assert_eq!(want, [(1, 0, 1), (0, 2, 4), (0, 4, 6)]);
assert_eq!(got, want);
}
}