use crate::error::{Error, ErrorKind, Result};
use crate::hir::Hir;
use super::super::interpreter::BacktrackingVm;
use super::super::shared::{BudgetExhausted, CaptureSlots, DEFAULT_BACKTRACK_LIMIT};
use dynasmrt::ExecutableBuffer;
enum Halt {
Retry,
Budget,
}
#[cfg(target_arch = "x86_64")]
use super::x86_64::BacktrackingCompiler;
#[cfg(target_arch = "aarch64")]
use super::aarch64::BacktrackingCompiler;
#[cfg(all(target_arch = "x86_64", target_os = "windows"))]
type MatchFn = unsafe extern "win64" fn(*const u8, usize, *mut i64, u64) -> i64;
#[cfg(all(target_arch = "x86_64", not(target_os = "windows")))]
type MatchFn = unsafe extern "sysv64" fn(*const u8, usize, *mut i64, u64) -> i64;
#[cfg(target_arch = "aarch64")]
type MatchFn = unsafe extern "C" fn(*const u8, usize, *mut i64, u64) -> i64;
pub(super) const STACK_EXHAUSTED: i64 = -2;
pub(super) const BUDGET_EXHAUSTED: i64 = -3;
const INLINE_SLOTS: usize = 8;
fn slot_buffer<'a>(
len: usize,
inline: &'a mut [i64; INLINE_SLOTS],
spilled: &'a mut Vec<i64>,
) -> &'a mut [i64] {
match inline.get_mut(..len) {
Some(slots) => slots,
None => {
*spilled = vec![-1; len];
spilled
}
}
}
fn shift_slots(slots: &mut [i64], offset: usize) {
if offset == 0 {
return;
}
for slot in slots.iter_mut().filter(|slot| **slot >= 0) {
*slot += offset as i64;
}
}
fn write_slots(slots: &mut [i64], caps: Option<&[Option<(usize, usize)>]>, offset: usize) -> bool {
slots.fill(-1);
let Some(caps) = caps else {
return false;
};
for (pair, group) in slots.chunks_exact_mut(2).zip(caps) {
let Some(&(start, end)) = group.as_ref() else {
continue;
};
if let Some(slot) = pair.first_mut() {
*slot = (start + offset) as i64;
}
if let Some(slot) = pair.get_mut(1) {
*slot = (end + offset) as i64;
}
}
true
}
pub struct BacktrackingJit {
#[allow(dead_code)]
pub(super) code: ExecutableBuffer,
pub(super) match_fn: MatchFn,
pub(super) capture_count: u32,
pub(super) vm: BacktrackingVm,
pub(super) needs_left_context: bool,
}
impl BacktrackingJit {
pub fn is_match(&self, input: &[u8]) -> bool {
self.find(input).is_some()
}
fn run(&self, input: &[u8], limit: u64, slots: &mut [i64]) -> std::result::Result<bool, Halt> {
slots.fill(-1);
let result =
unsafe { (self.match_fn)(input.as_ptr(), input.len(), slots.as_mut_ptr(), limit) };
match result {
STACK_EXHAUSTED => Err(Halt::Retry),
BUDGET_EXHAUSTED => Err(Halt::Budget),
negative if negative < 0 => Ok(false),
_ => Ok(true),
}
}
fn search(
&self,
input: &[u8],
from: usize,
limit: u64,
slots: &mut [i64],
) -> std::result::Result<bool, BudgetExhausted> {
if self.needs_left_context && from > 0 {
let caps = self.vm.try_captures_from(input, from, limit)?;
return Ok(write_slots(slots, caps.as_deref(), 0));
}
let (haystack, offset) = if from == 0 {
(input, 0)
} else {
(&input[from..], from)
};
match self.run(haystack, limit, slots) {
Ok(false) => Ok(false),
Ok(true) => {
shift_slots(slots, offset);
Ok(true)
}
Err(Halt::Retry) => {
let caps = self.vm.try_captures_from(haystack, 0, limit)?;
Ok(write_slots(slots, caps.as_deref(), offset))
}
Err(Halt::Budget) => Err(BudgetExhausted),
}
}
fn slot_len(&self) -> usize {
(self.capture_count as usize + 1) * 2
}
pub fn find(&self, input: &[u8]) -> Option<(usize, usize)> {
self.captures(input).and_then(|caps| caps[0])
}
pub fn captures(&self, input: &[u8]) -> Option<Vec<Option<(usize, usize)>>> {
self.try_captures_from(input, 0, DEFAULT_BACKTRACK_LIMIT)
.unwrap_or(None)
}
pub fn try_captures_from(
&self,
input: &[u8],
from: usize,
limit: u64,
) -> std::result::Result<Option<CaptureSlots>, BudgetExhausted> {
if from > input.len() {
return Ok(None);
}
let mut inline = [-1i64; INLINE_SLOTS];
let mut spilled = Vec::new();
let slots = slot_buffer(self.slot_len(), &mut inline, &mut spilled);
if !self.search(input, from, limit, slots)? {
return Ok(None);
}
Ok(Some(
slots
.chunks_exact(2)
.map(|pair| match (pair.first(), pair.get(1)) {
(Some(&start), Some(&end)) if start >= 0 && end >= 0 => {
Some((start as usize, end as usize))
}
_ => None,
})
.collect(),
))
}
pub fn find_at(&self, input: &[u8], start: usize) -> Option<(usize, usize)> {
self.find_from(input, start)
}
pub fn find_from(&self, input: &[u8], from: usize) -> Option<(usize, usize)> {
if from > input.len() {
return None;
}
let mut inline = [-1i64; INLINE_SLOTS];
let mut spilled = Vec::new();
let slots = slot_buffer(self.slot_len(), &mut inline, &mut spilled);
if !self
.search(input, from, DEFAULT_BACKTRACK_LIMIT, slots)
.unwrap_or(false)
{
return None;
}
match (slots.first(), slots.get(1)) {
(Some(&start), Some(&end)) if start >= 0 && end >= 0 => {
Some((start as usize, end as usize))
}
_ => None,
}
}
pub fn captures_from(&self, input: &[u8], from: usize) -> Option<Vec<Option<(usize, usize)>>> {
self.try_captures_from(input, from, DEFAULT_BACKTRACK_LIMIT)
.unwrap_or(None)
}
#[cfg(test)]
pub fn debug_match(&self, input: &[u8]) -> (i64, Vec<i64>) {
let num_slots = (self.capture_count as usize + 1) * 2;
let mut captures: Vec<i64> = vec![-1; num_slots];
let result = unsafe {
(self.match_fn)(
input.as_ptr(),
input.len(),
captures.as_mut_ptr(),
DEFAULT_BACKTRACK_LIMIT,
)
};
(result, captures)
}
}
pub fn compile_backtracking(hir: &Hir) -> Result<BacktrackingJit> {
if crate::hir::has_unbounded_nullable_repeat(&hir.expr) {
return Err(Error::new(
ErrorKind::Jit("unbounded repetition over a nullable body".to_string()),
"",
));
}
let compiler = BacktrackingCompiler::new(hir)?;
let mut jit = compiler.compile()?;
let props = &hir.props;
jit.needs_left_context =
props.has_start_anchor || props.has_multiline_anchors || props.has_word_boundary;
Ok(jit)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hir::translate;
use crate::parser::parse;
fn compile_pattern(pattern: &str) -> Result<BacktrackingJit> {
let ast = parse(pattern)?;
let hir = translate(&ast)?;
compile_backtracking(&hir)
}
#[test]
fn test_literal_debug() {
let jit = compile_pattern("hello").unwrap();
let (result, caps) = jit.debug_match(b"hello");
println!("result: {}, caps: {:?}", result, caps);
assert!(result >= 0, "Expected match, got result={}", result);
}
#[test]
fn test_literal() {
let jit = compile_pattern("hello").unwrap();
assert!(jit.is_match(b"hello"));
assert!(jit.is_match(b"say hello world"));
assert!(!jit.is_match(b"helo"));
}
#[test]
fn test_simple_backref() {
let jit = compile_pattern(r"(a)\1").unwrap();
let (result_aa, caps_aa) = jit.debug_match(b"aa");
println!("(a)\\1 on 'aa': result={}, caps={:?}", result_aa, caps_aa);
let (result_ab, caps_ab) = jit.debug_match(b"ab");
println!("(a)\\1 on 'ab': result={}, caps={:?}", result_ab, caps_ab);
let (result_a, caps_a) = jit.debug_match(b"a");
println!("(a)\\1 on 'a': result={}, caps={:?}", result_a, caps_a);
assert!(jit.is_match(b"aa"), "Should match 'aa'");
assert!(!jit.is_match(b"ab"), "Should NOT match 'ab'");
assert!(!jit.is_match(b"a"), "Should NOT match 'a'");
}
#[test]
fn test_quoted_string() {
let jit = compile_pattern(r#"(['"])[^'"]*\1"#).unwrap();
let (r1, c1) = jit.debug_match(br#""hello""#);
println!(r#"['"][^'"]*\1 on "hello": result={}, caps={:?}"#, r1, c1);
let (r2, c2) = jit.debug_match(b"'world'");
println!(r#"['"][^'"]*\1 on 'world': result={}, caps={:?}"#, r2, c2);
let (r3, c3) = jit.debug_match(br#""mixed'"#);
println!(r#"['"][^'"]*\1 on "mixed': result={}, caps={:?}"#, r3, c3);
let (r4, c4) = jit.debug_match(b"'mixed\"");
println!(r#"['"][^'"]*\1 on 'mixed": result={}, caps={:?}"#, r4, c4);
assert!(jit.is_match(br#""hello""#), "Should match \"hello\"");
assert!(jit.is_match(b"'world'"), "Should match 'world'");
assert!(!jit.is_match(br#""mixed'"#), "Should NOT match \"mixed'");
assert!(!jit.is_match(b"'mixed\""), "Should NOT match 'mixed\"");
}
#[test]
fn test_alternation_backref() {
let jit = compile_pattern(r"(a|b)\1").unwrap();
let (result_aa, caps_aa) = jit.debug_match(b"aa");
println!("(a|b)\\1 on 'aa': result={}, caps={:?}", result_aa, caps_aa);
let (result_bb, caps_bb) = jit.debug_match(b"bb");
println!("(a|b)\\1 on 'bb': result={}, caps={:?}", result_bb, caps_bb);
let (result_ab, caps_ab) = jit.debug_match(b"ab");
println!("(a|b)\\1 on 'ab': result={}, caps={:?}", result_ab, caps_ab);
let (result_ba, caps_ba) = jit.debug_match(b"ba");
println!("(a|b)\\1 on 'ba': result={}, caps={:?}", result_ba, caps_ba);
assert!(jit.is_match(b"aa"), "Should match 'aa'");
assert!(jit.is_match(b"bb"), "Should match 'bb'");
assert!(!jit.is_match(b"ab"), "Should NOT match 'ab'");
assert!(!jit.is_match(b"ba"), "Should NOT match 'ba'");
}
#[test]
fn test_captures() {
let jit = compile_pattern(r"(a)(b)\2\1").unwrap();
let caps = jit.captures(b"abba").unwrap();
assert_eq!(caps[0], Some((0, 4))); assert_eq!(caps[1], Some((0, 1))); assert_eq!(caps[2], Some((1, 2))); }
}