use crate::hir::{Hir, HirExpr};
use crate::nfa::Nfa;
use crate::vm::{is_shift_or_compatible, is_shift_or_wide_compatible};
fn hir_uses_codepoint_class(expr: &HirExpr) -> bool {
match expr {
HirExpr::UnicodeCpClass(_) => true,
HirExpr::Concat(exprs) | HirExpr::Alt(exprs) => exprs.iter().any(hir_uses_codepoint_class),
HirExpr::Repeat(r) => hir_uses_codepoint_class(&r.expr),
HirExpr::Capture(c) => hir_uses_codepoint_class(&c.expr),
HirExpr::Lookaround(l) => hir_uses_codepoint_class(&l.expr),
HirExpr::Empty
| HirExpr::Literal(_)
| HirExpr::Class(_)
| HirExpr::Anchor(_)
| HirExpr::Backref(_) => false,
}
}
fn branches_start_disjointly(branches: &[HirExpr]) -> bool {
let mut claimed = [false; 256];
for branch in branches {
if contains_assertion(branch) {
return false;
}
let Some(first) = first_bytes(branch) else {
return false;
};
for (byte, &possible) in first.iter().enumerate() {
if possible {
if claimed[byte] {
return false;
}
claimed[byte] = true;
}
}
}
true
}
fn contains_assertion(expr: &HirExpr) -> bool {
match expr {
HirExpr::Anchor(_) | HirExpr::Lookaround(_) | HirExpr::Backref(_) => true,
HirExpr::Concat(exprs) | HirExpr::Alt(exprs) => exprs.iter().any(contains_assertion),
HirExpr::Repeat(r) => contains_assertion(&r.expr),
HirExpr::Capture(c) => contains_assertion(&c.expr),
HirExpr::Empty | HirExpr::Literal(_) | HirExpr::Class(_) | HirExpr::UnicodeCpClass(_) => {
false
}
}
}
fn first_bytes(expr: &HirExpr) -> Option<[bool; 256]> {
let mut set = [false; 256];
match expr {
HirExpr::Literal(bytes) => {
set[*bytes.first()? as usize] = true;
}
HirExpr::Class(class) => {
for byte in 0..=255u8 {
let in_ranges = class
.ranges
.iter()
.any(|&(lo, hi)| lo <= byte && byte <= hi);
set[byte as usize] = in_ranges != class.negated;
}
}
HirExpr::Concat(exprs) => return first_bytes(exprs.first()?),
HirExpr::Alt(branches) => {
for branch in branches {
let branch_set = first_bytes(branch)?;
for (byte, possible) in branch_set.iter().enumerate() {
set[byte] |= possible;
}
}
}
HirExpr::Capture(c) => return first_bytes(&c.expr),
HirExpr::Repeat(r) if r.min >= 1 => return first_bytes(&r.expr),
_ => return None,
}
Some(set)
}
pub fn needs_boundary_aware_empty_match(hir: &Hir) -> bool {
hir.props.has_word_boundary && crate::hir::matches_empty(&hir.expr)
}
pub fn hir_contains_alternation(expr: &HirExpr) -> bool {
match expr {
HirExpr::Alt(branches) => {
branches.len() >= 2 || branches.iter().any(hir_contains_alternation)
}
HirExpr::Concat(exprs) => exprs.iter().any(hir_contains_alternation),
HirExpr::Repeat(repeat) => hir_contains_alternation(&repeat.expr),
HirExpr::Capture(capture) => hir_contains_alternation(&capture.expr),
_ => false,
}
}
pub fn hir_has_alternation(expr: &HirExpr) -> bool {
match expr {
HirExpr::Alt(branches) => {
(branches.len() >= 2 && !branches_start_disjointly(branches))
|| branches.iter().any(hir_has_alternation)
}
HirExpr::Concat(exprs) => exprs.iter().any(hir_has_alternation),
HirExpr::Repeat(r) => hir_has_alternation(&r.expr),
HirExpr::Capture(c) => hir_has_alternation(&c.expr),
HirExpr::Lookaround(_)
| HirExpr::Empty
| HirExpr::Literal(_)
| HirExpr::Class(_)
| HirExpr::UnicodeCpClass(_)
| HirExpr::Anchor(_)
| HirExpr::Backref(_) => false,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EngineType {
PikeVm,
BacktrackingVm,
ShiftOr,
ShiftOrWide,
LazyDfa,
#[cfg(feature = "jit")]
Jit,
}
pub fn select_engine(nfa: &Nfa) -> EngineType {
if nfa.has_backrefs {
return EngineType::BacktrackingVm;
}
if nfa.has_lookaround {
return EngineType::PikeVm;
}
EngineType::LazyDfa
}
pub fn select_engine_from_hir(hir: &Hir) -> EngineType {
if hir.props.has_backrefs {
return EngineType::BacktrackingVm;
}
if hir.props.has_lookaround || hir.props.has_non_greedy {
return EngineType::PikeVm;
}
if hir_uses_codepoint_class(&hir.expr) {
return EngineType::PikeVm;
}
if needs_boundary_aware_empty_match(hir) {
return EngineType::PikeVm;
}
if hir_has_alternation(&hir.expr) {
return EngineType::PikeVm;
}
if hir_contains_alternation(&hir.expr) {
return EngineType::LazyDfa;
}
if is_shift_or_compatible(hir) {
return EngineType::ShiftOr;
}
if hir.props.has_multiline_anchors {
return EngineType::LazyDfa;
}
if is_shift_or_wide_compatible(hir) {
return EngineType::ShiftOrWide;
}
if hir.props.has_word_boundary {
return EngineType::LazyDfa;
}
EngineType::LazyDfa
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Capabilities {
#[cfg(feature = "simd")]
pub has_avx2: bool,
#[cfg(feature = "jit")]
pub has_jit: bool,
}
impl Capabilities {
pub fn detect() -> Self {
Self {
#[cfg(feature = "simd")]
has_avx2: Self::detect_avx2(),
#[cfg(feature = "jit")]
has_jit: Self::detect_jit(),
}
}
#[cfg(feature = "simd")]
fn detect_avx2() -> bool {
#[cfg(target_arch = "x86_64")]
{
is_x86_feature_detected!("avx2")
}
#[cfg(not(target_arch = "x86_64"))]
{
false
}
}
#[cfg(feature = "jit")]
fn detect_jit() -> bool {
cfg!(any(target_arch = "x86_64", target_arch = "aarch64"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hir::translate;
use crate::nfa::compile;
use crate::parser::parse;
fn get_engine_from_hir(pattern: &str) -> EngineType {
let ast = parse(pattern).unwrap();
let hir = translate(&ast).unwrap();
select_engine_from_hir(&hir)
}
fn get_engine_from_nfa(pattern: &str) -> EngineType {
let ast = parse(pattern).unwrap();
let hir = translate(&ast).unwrap();
let nfa = compile(&hir).unwrap();
select_engine(&nfa)
}
#[test]
fn test_simple_pattern_uses_shift_or() {
let engine = get_engine_from_hir("abc");
assert_eq!(engine, EngineType::ShiftOr);
}
#[test]
fn test_medium_pattern_uses_shift_or_wide() {
let medium_pattern = "a".repeat(100);
let engine = get_engine_from_hir(&medium_pattern);
assert_eq!(engine, EngineType::ShiftOrWide);
}
#[test]
fn test_very_long_pattern_uses_lazy_dfa() {
let long_pattern = "a".repeat(300);
let engine = get_engine_from_hir(&long_pattern);
assert_eq!(engine, EngineType::LazyDfa);
}
#[test]
fn test_backref_uses_backtracking() {
let ast = parse(r"(a)\1").unwrap();
let hir = translate(&ast).unwrap();
let nfa = compile(&hir).unwrap();
if nfa.has_backrefs {
assert_eq!(select_engine(&nfa), EngineType::BacktrackingVm);
}
if hir.props.has_backrefs {
assert_eq!(select_engine_from_hir(&hir), EngineType::BacktrackingVm);
}
}
#[test]
fn test_nfa_api_defaults_to_lazy_dfa() {
let engine = get_engine_from_nfa("abc");
assert_eq!(engine, EngineType::LazyDfa);
}
#[test]
fn test_word_boundary_uses_lazy_dfa() {
assert_eq!(get_engine_from_hir(r"\bthe\b"), EngineType::LazyDfa);
assert_eq!(get_engine_from_hir(r"\bword\b"), EngineType::LazyDfa);
assert_eq!(get_engine_from_hir(r"\b\d+\b"), EngineType::LazyDfa);
assert_eq!(get_engine_from_hir(r"a\Bb"), EngineType::LazyDfa);
let long_pattern = format!(r"\b{}\b", "a".repeat(100));
assert_eq!(get_engine_from_hir(&long_pattern), EngineType::LazyDfa);
}
#[test]
fn test_anchors_engine_selection() {
assert_eq!(get_engine_from_hir(r"^hello"), EngineType::ShiftOr);
assert_eq!(get_engine_from_hir(r"world$"), EngineType::ShiftOr);
assert_eq!(get_engine_from_hir(r"^hello$"), EngineType::ShiftOr);
assert_eq!(get_engine_from_hir(r"(?m)^line"), EngineType::LazyDfa);
assert_eq!(get_engine_from_hir(r"(?m)line$"), EngineType::LazyDfa);
}
}