#[cfg(all(feature = "jit", target_arch = "aarch64"))]
mod aarch64;
#[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
pub mod jit;
#[cfg(all(feature = "jit", target_arch = "x86_64"))]
mod x86_64;
use crate::nfa::{
at_end_or_before_final_newline, is_word_boundary, ByteRange, Nfa, NfaInstruction, StateId,
};
use std::collections::HashMap;
const MAX_CLOSURES: usize = 512;
const NO_TRANSITION: u8 = u8::MAX;
const MAX_TRANSITIONS: usize = NO_TRANSITION as usize;
#[derive(Debug, Clone, Copy)]
enum Action {
Start(u32),
End(u32),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Guard {
StartOfText,
EndOfText,
StartOfLine,
EndOfLine,
WordBoundary,
NotWordBoundary,
}
impl Guard {
#[inline]
fn holds(self, input: &[u8], pos: usize) -> bool {
match self {
Self::StartOfText => pos == 0,
Self::EndOfText => at_end_or_before_final_newline(input, pos),
Self::StartOfLine => pos == 0 || input.get(pos.wrapping_sub(1)) == Some(&b'\n'),
Self::EndOfLine => pos == input.len() || input.get(pos) == Some(&b'\n'),
Self::WordBoundary => is_word_boundary(input, pos),
Self::NotWordBoundary => !is_word_boundary(input, pos),
}
}
}
fn build_stay_run(
index: usize,
table: &[u8; 256],
transitions: &[Transition],
matches: &[MatchItem],
) -> Option<StayRun> {
if matches.iter().any(|item| !item.guards.is_empty()) {
return None;
}
let index = u32::try_from(index).ok()?;
let limit = matches.first().map_or(u32::MAX, |item| item.order);
let mut stay = Box::new([0u8; 256]);
let mut found = false;
for (byte, &slot) in table.iter().enumerate() {
if slot == NO_TRANSITION {
continue;
}
let Some(transition) = transitions.get(slot as usize) else {
continue;
};
if transition.target != index
|| transition.actions.len != 0
|| !transition.guards.is_empty()
|| transition.order > limit
{
continue;
}
if let Some(entry) = stay.get_mut(byte) {
*entry = 1;
}
found = true;
}
found.then_some(StayRun { table: stay })
}
#[derive(Debug, Clone, Copy)]
struct ActionSpan {
start: u32,
len: u32,
}
impl ActionSpan {
const EMPTY: Self = Self { start: 0, len: 0 };
}
#[derive(Debug, Clone, Copy)]
struct GuardSpan {
start: u32,
len: u32,
}
impl GuardSpan {
const EMPTY: Self = Self { start: 0, len: 0 };
#[inline]
const fn is_empty(self) -> bool {
self.len == 0
}
}
#[derive(Debug, Clone, Copy)]
struct Transition {
target: u32,
actions: ActionSpan,
guards: GuardSpan,
order: u32,
}
#[derive(Debug, Clone, Copy)]
struct MatchItem {
actions: ActionSpan,
guards: GuardSpan,
order: u32,
}
#[derive(Debug)]
struct Closure {
table: [u8; 256],
transitions: Vec<Transition>,
matches: Vec<MatchItem>,
stay: Option<StayRun>,
}
#[derive(Debug)]
struct StayRun {
table: Box<[u8; 256]>,
}
#[derive(Debug)]
pub struct OnePass {
closures: Vec<Closure>,
actions: Vec<Action>,
guards: Vec<Guard>,
slot_count: usize,
#[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
jit: Option<jit::OnePassJit>,
}
impl OnePass {
pub fn compile(nfa: &Nfa) -> Option<Self> {
if nfa.has_backrefs || nfa.has_lookaround {
return None;
}
let mut ids: HashMap<StateId, u32> = HashMap::new();
let mut roots: Vec<StateId> = Vec::new();
ids.insert(nfa.start, 0);
roots.push(nfa.start);
let mut closures: Vec<Closure> = Vec::new();
let mut actions: Vec<Action> = Vec::new();
let mut guards: Vec<Guard> = Vec::new();
let mut next = 0;
while next < roots.len() {
let Some(&root) = roots.get(next) else {
break;
};
next += 1;
let raw = expand_closure(nfa, root)?;
if raw.transitions.len() > MAX_TRANSITIONS {
return None;
}
let mut table = [NO_TRANSITION; 256];
let mut transitions = Vec::with_capacity(raw.transitions.len());
for (index, raw_transition) in raw.transitions.iter().enumerate() {
let range = raw_transition.range;
for byte in range.start..=range.end {
let entry = table.get_mut(byte as usize)?;
if *entry != NO_TRANSITION {
return None;
}
*entry = index as u8;
}
transitions.push(Transition {
target: intern(&mut ids, &mut roots, raw_transition.target)?,
actions: push_actions(&mut actions, &raw_transition.path.actions)?,
guards: push_guards(&mut guards, &raw_transition.path.guards)?,
order: raw_transition.order,
});
}
let mut matches = Vec::with_capacity(raw.matches.len());
for raw_match in &raw.matches {
matches.push(MatchItem {
actions: push_actions(&mut actions, &raw_match.path.actions)?,
guards: push_guards(&mut guards, &raw_match.path.guards)?,
order: raw_match.order,
});
}
let stay = build_stay_run(closures.len(), &table, &transitions, &matches);
closures.push(Closure {
table,
transitions,
matches,
stay,
});
}
let one_pass = Self {
closures,
actions,
guards,
slot_count: nfa.capture_count as usize + 1,
#[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
jit: None,
};
#[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
let one_pass = Self {
jit: jit::OnePassJit::compile(&one_pass),
..one_pass
};
Some(one_pass)
}
pub fn captures_at(&self, input: &[u8], start: usize) -> Option<Vec<Option<(usize, usize)>>> {
let mut slots = vec![None; self.slot_count];
let mut match_slots = vec![None; self.slot_count];
self.captures_at_into(input, start, &mut slots, &mut match_slots)
.then_some(match_slots)
}
pub fn slot_count(&self) -> usize {
self.slot_count
}
pub fn captures_at_into(
&self,
input: &[u8],
start: usize,
slots: &mut [Option<(usize, usize)>],
match_slots: &mut [Option<(usize, usize)>],
) -> bool {
if start > input.len() || slots.len() != self.slot_count {
return false;
}
#[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
if let Some(ref jit) = self.jit {
return jit.captures_at_into(input, start, match_slots);
}
slots.fill(None);
let mut match_end: Option<usize> = None;
let mut pending: Option<(ActionSpan, usize)> = None;
let Some(mut closure) = self.closures.first() else {
return false;
};
let mut pos = start;
loop {
let mut limit = u32::MAX;
for candidate in &closure.matches {
if self.guards_hold(candidate.guards, input, pos) {
pending = Some((candidate.actions, pos));
match_end = Some(pos);
limit = candidate.order;
break;
}
}
let run_end = Self::run_stay(closure, input, pos);
if run_end != pos {
pos = run_end;
continue;
}
let byte = match input.get(pos) {
Some(&byte) => byte,
None => break,
};
let index = match closure.table.get(byte as usize) {
Some(&index) if index != NO_TRANSITION => index as usize,
_ => break,
};
let Some(&transition) = closure.transitions.get(index) else {
break;
};
if transition.order > limit || !self.guards_hold(transition.guards, input, pos) {
break;
}
let Some(target) = self.closures.get(transition.target as usize) else {
break;
};
if transition.actions.len != 0 {
if let Some((actions, at)) = pending.take() {
match_slots.copy_from_slice(slots);
self.apply(match_slots, actions, at);
}
self.apply(slots, transition.actions, pos);
}
closure = target;
pos += 1;
}
if let Some((actions, at)) = pending.take() {
match_slots.copy_from_slice(slots);
self.apply(match_slots, actions, at);
}
let Some(end) = match_end else {
return false;
};
if let Some(slot) = match_slots.first_mut() {
*slot = Some((start, end));
}
true
}
#[inline]
fn run_stay(closure: &Closure, input: &[u8], pos: usize) -> usize {
let Some(ref stay) = closure.stay else {
return pos;
};
let mut end = pos;
while let Some(&byte) = input.get(end) {
if stay
.table
.get(byte as usize)
.is_none_or(|member| *member == 0)
{
break;
}
end += 1;
}
end
}
#[inline]
fn guards_hold(&self, span: GuardSpan, input: &[u8], pos: usize) -> bool {
if span.is_empty() {
return true;
}
let range = span.start as usize..span.start as usize + span.len as usize;
match self.guards.get(range) {
Some(guards) => guards.iter().all(|guard| guard.holds(input, pos)),
None => false,
}
}
#[inline]
fn apply(&self, slots: &mut [Option<(usize, usize)>], span: ActionSpan, pos: usize) {
let range = span.start as usize..span.start as usize + span.len as usize;
let Some(actions) = self.actions.get(range) else {
return;
};
for action in actions {
match *action {
Action::Start(index) => {
if let Some(slot) = slots.get_mut(index as usize) {
*slot = Some((pos, pos));
}
}
Action::End(index) => {
if let Some(slot) = slots.get_mut(index as usize) {
if let Some((slot_start, _)) = *slot {
*slot = Some((slot_start, pos));
}
}
}
}
}
}
}
#[derive(Debug, Clone, Default)]
struct Path {
actions: Vec<Action>,
guards: Vec<Guard>,
}
struct RawTransition {
range: ByteRange,
target: StateId,
path: Path,
order: u32,
}
struct RawMatch {
path: Path,
order: u32,
}
struct RawClosure {
transitions: Vec<RawTransition>,
matches: Vec<RawMatch>,
}
fn expand_closure(nfa: &Nfa, root: StateId) -> Option<RawClosure> {
let mut visited: Vec<Option<Vec<Guard>>> = vec![None; nfa.states.len()];
let mut stack: Vec<(StateId, Path)> = vec![(root, Path::default())];
let mut transitions = Vec::new();
let mut matches = Vec::new();
let mut order = 0u32;
while let Some((state_id, mut path)) = stack.pop() {
let seen = visited.get_mut(state_id as usize)?;
if let Some(previous) = seen {
if *previous != path.guards {
return None;
}
continue;
}
*seen = Some(path.guards.clone());
let state = nfa.get(state_id)?;
match state.instruction {
None | Some(NfaInstruction::NonGreedyExit) => {}
Some(NfaInstruction::CaptureStart(index)) => path.actions.push(Action::Start(index)),
Some(NfaInstruction::CaptureEnd(index)) => path.actions.push(Action::End(index)),
Some(NfaInstruction::StartOfText) => path.guards.push(Guard::StartOfText),
Some(NfaInstruction::EndOfText) => path.guards.push(Guard::EndOfText),
Some(NfaInstruction::StartOfLine) => path.guards.push(Guard::StartOfLine),
Some(NfaInstruction::EndOfLine) => path.guards.push(Guard::EndOfLine),
Some(NfaInstruction::WordBoundary) => path.guards.push(Guard::WordBoundary),
Some(NfaInstruction::NotWordBoundary) => path.guards.push(Guard::NotWordBoundary),
Some(_) => return None,
}
if state.is_match {
let unconditional = path.guards.is_empty();
matches.push(RawMatch { path, order });
order += 1;
if unconditional {
break;
}
continue;
}
for &(range, target) in &state.transitions {
transitions.push(RawTransition {
range,
target,
path: path.clone(),
order,
});
order += 1;
}
for &next in state.epsilon.iter().rev() {
stack.push((next, path.clone()));
}
}
Some(RawClosure {
transitions,
matches,
})
}
fn intern(
ids: &mut HashMap<StateId, u32>,
roots: &mut Vec<StateId>,
state: StateId,
) -> Option<u32> {
if let Some(&index) = ids.get(&state) {
return Some(index);
}
if roots.len() >= MAX_CLOSURES {
return None;
}
let index = u32::try_from(roots.len()).ok()?;
ids.insert(state, index);
roots.push(state);
Some(index)
}
fn push_actions(arena: &mut Vec<Action>, path: &[Action]) -> Option<ActionSpan> {
if path.is_empty() {
return Some(ActionSpan::EMPTY);
}
let start = u32::try_from(arena.len()).ok()?;
let len = u32::try_from(path.len()).ok()?;
arena.extend_from_slice(path);
Some(ActionSpan { start, len })
}
fn push_guards(arena: &mut Vec<Guard>, path: &[Guard]) -> Option<GuardSpan> {
if path.is_empty() {
return Some(GuardSpan::EMPTY);
}
let start = u32::try_from(arena.len()).ok()?;
let len = u32::try_from(path.len()).ok()?;
arena.extend_from_slice(path);
Some(GuardSpan { start, len })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hir::{translate, CodepointClass};
use crate::nfa::{self, NfaState};
use crate::parser::parse;
use crate::vm::PikeVm;
fn build_nfa(pattern: &str) -> Nfa {
let ast = parse(pattern).unwrap();
let hir = translate(&ast).unwrap();
nfa::compile(&hir).unwrap()
}
fn compile(pattern: &str) -> Option<OnePass> {
OnePass::compile(&build_nfa(pattern))
}
#[test]
fn a_guarded_match_kills_lower_priority_transitions() {
for pattern in [r"(a+?)$", r"(a*?)$", r"(\w+?)\b", r"(a+?)\b", r"(?m)(a+?)$"] {
let nfa = build_nfa(pattern);
let one_pass = OnePass::compile(&nfa)
.unwrap_or_else(|| panic!("{pattern} should be one-pass, or this test is vacuous"));
let vm = PikeVm::new(build_nfa(pattern));
for input in ["", "a", "aa", "aaa", "aaa\n", "aab", "ab ab"] {
let bytes = input.as_bytes();
let Some(expected) = vm.captures(bytes) else {
continue;
};
let Some((start, _)) = expected[0] else {
continue;
};
assert_eq!(
one_pass.captures_at(bytes, start),
Some(expected),
"pattern {pattern:?} input {input:?}: the limit did not prune"
);
}
}
}
fn assert_agrees_with_pike(pattern: &str, inputs: &[&str]) {
let nfa = build_nfa(pattern);
let one_pass = OnePass::compile(&nfa).expect("pattern should be one-pass");
let vm = PikeVm::new(nfa);
for input in inputs {
let bytes = input.as_bytes();
let expected = vm.captures(bytes);
let start = match expected.as_ref().and_then(|caps| caps[0]) {
Some((start, _)) => start,
None => {
assert_eq!(
one_pass.captures_at(bytes, 0),
None,
"pattern {pattern:?} input {input:?}: expected no match at 0"
);
continue;
}
};
assert_eq!(
one_pass.captures_at(bytes, start),
expected,
"pattern {pattern:?} input {input:?}"
);
}
}
#[test]
fn test_compiles_deterministic_pattern() {
assert!(compile(r"(\d{4})-(\d{2})").is_some());
assert!(compile(r"(\w+)@(\w+)\.com").is_some());
assert!(compile(r"a(b)*c").is_some());
}
#[test]
fn test_rejects_overlapping_alternation() {
assert!(compile("(a|ab)c").is_none());
assert!(compile("(ab|a)c").is_none());
}
#[test]
fn test_rejects_backreference() {
assert!(compile(r"(a+)\1").is_none());
}
#[test]
fn test_rejects_lookaround() {
assert!(compile(r"(a)(?=b)").is_none());
assert!(compile(r"(?<=a)(b)").is_none());
}
#[test]
fn test_compiles_anchors_and_boundaries() {
assert!(compile(r"^(a)").is_some());
assert!(compile(r"(a)$").is_some());
assert!(compile(r"\b(\w+)\b").is_some());
assert!(compile(r"\B(a)").is_some());
assert!(compile(r"^(\w+): *(\d+)$").is_some());
}
#[test]
fn test_slots_match_pike_vm_with_anchors() {
assert_agrees_with_pike(r"^(\d+)", &["123", "x123", "", "1"]);
assert_agrees_with_pike(r"(\d+)$", &["123", "123x", "1"]);
assert_agrees_with_pike(r"^(\w+):(\d+)$", &["ab:12", "ab:12x", ":1"]);
}
#[test]
fn test_slots_match_pike_vm_with_word_boundaries() {
assert_agrees_with_pike(r"\b(\w+)\b", &["hi there", " hi ", "", "_"]);
assert_agrees_with_pike(r"\B(a)", &["ba", "a", "xa y"]);
}
#[test]
fn test_guarded_match_does_not_end_scan_early() {
assert_agrees_with_pike(r"(a+)$", &["aaa", "aaab", "a"]);
assert_agrees_with_pike(r"(a+?)$", &["aaa", "a", "aab"]);
}
#[test]
fn test_anchor_that_fails_rejects_the_position() {
let one_pass = compile(r"^(\d+)").unwrap();
assert_eq!(one_pass.captures_at(b"x123", 1), None);
let one_pass = compile(r"(\d+)$").unwrap();
assert_eq!(one_pass.captures_at(b"123x", 0), None);
}
#[test]
fn test_rejects_state_reachable_under_differing_guards() {
assert!(compile(r"(?:\b|)(a)").is_none());
}
#[test]
fn test_rejects_codepoint_class() {
let mut nfa = Nfa::new();
let mut start = NfaState::new();
start.instruction = Some(NfaInstruction::CodepointClass(
CodepointClass::new(vec![(0x100, 0x200)], false),
1,
));
nfa.add_state(start);
nfa.add_state(NfaState::match_state());
nfa.start = 0;
nfa.matches = vec![1];
assert!(OnePass::compile(&nfa).is_none());
}
#[test]
fn test_rejects_when_closure_cap_exceeded() {
let pattern = "a".repeat(MAX_CLOSURES + 16);
assert!(compile(&pattern).is_none());
}
#[test]
fn test_slots_match_pike_vm() {
assert_agrees_with_pike(
r"(\d{4})-(\d{2})-(\d{2})",
&["2024-05-17", "x2024-05-17x", "2024-05"],
);
assert_agrees_with_pike(r"(\w+)@(\w+)", &["user@host", "@host", "user@"]);
}
#[test]
fn test_slots_match_pike_vm_nested_groups() {
assert_agrees_with_pike(r"((\d+)-(\d+))", &["12-34", "1-2", "abc"]);
}
#[test]
fn test_slots_match_pike_vm_group_in_repetition() {
assert_agrees_with_pike(r"(?:(\d)x)+", &["1x2x3x", "1x", "x"]);
let one_pass = compile(r"(?:(\d)x)+").unwrap();
let slots = one_pass.captures_at(b"1x2x3x", 0).unwrap();
assert_eq!(slots[0], Some((0, 6)));
assert_eq!(slots[1], Some((4, 5)));
}
#[test]
fn test_slots_match_pike_vm_optional_group() {
assert_agrees_with_pike(r"(a)?b", &["ab", "b"]);
let one_pass = compile(r"(a)?b").unwrap();
let slots = one_pass.captures_at(b"b", 0).unwrap();
assert_eq!(slots.len(), 2);
assert_eq!(slots[0], Some((0, 1)));
assert_eq!(slots[1], None, "unentered group stays None");
}
#[test]
fn test_slots_match_pike_vm_empty_leading_group() {
assert_agrees_with_pike(r"(a*)b", &["aab", "b"]);
let one_pass = compile(r"(a*)b").unwrap();
let slots = one_pass.captures_at(b"b", 0).unwrap();
assert_eq!(slots[1], Some((0, 0)), "group matched empty at 0");
}
#[test]
fn test_no_match_at_position() {
let one_pass = compile(r"(\d+)").unwrap();
assert_eq!(one_pass.captures_at(b"abc", 0), None);
assert_eq!(one_pass.captures_at(b"abc", 9), None);
}
#[test]
fn test_agrees_with_pike_vm_across_assertion_patterns() {
const PATTERNS: &[&str] = &[
r"^(a+)$",
r"^(a*)(b*)$",
r"(a+)$",
r"^(a+)",
r"\b(\w+)\b",
r"\b(\w+)",
r"(\w+)\b",
r"\B(\w)",
r"^(\w+)=(\w*)$",
r"(a+?)$",
r"^(a+?)",
r"^(\d)(\d)?$",
r"\b(\d+)\b",
r"^$",
r"^(x?)$",
r"(?:(a)\b)+",
r"^(a)|^(b)",
r"\b(a+)$",
r"(a)(.+)",
r"(a+)(b*)",
r"(\w)(\w*)",
r"(a)|(ab)",
r"(a)b(c)?",
r"((a)(b*))c*",
r"a((x)|(y))?",
r"(a)((b)|(c))?",
r"(a)(b)?(c)?",
];
const INPUTS: &[&str] = &[
"", "a", "aa", "aaa", "b", "ab", "ba", "a b", " a ", "x=1", "=", "1", "12", "abc",
"abc def", "_", "a\n", "\n", "aab", "x", "xy", "a1", "1a", "abbbb", "abcabc", "aXbXcX",
"azzz", "abbc", "abcc", "ax", "ay", "abc", "ac", "a",
];
let mut compiled = 0usize;
for pattern in PATTERNS {
let nfa = build_nfa(pattern);
let Some(one_pass) = OnePass::compile(&nfa) else {
continue;
};
compiled += 1;
let vm = PikeVm::new(nfa);
let mut ctx = vm.create_context();
for input in INPUTS {
let bytes = input.as_bytes();
for start in 0..=bytes.len() {
let expected = vm.captures_with_context(bytes, &mut ctx, start);
assert_eq!(
one_pass.captures_at(bytes, start),
expected,
"pattern {pattern:?} input {input:?} start {start}"
);
}
}
}
assert!(
compiled >= PATTERNS.len() * 2 / 3,
"only {compiled} of {} patterns compiled",
PATTERNS.len()
);
}
#[test]
fn test_slot_count_matches_capture_count() {
let one_pass = compile(r"(a)(b)(c)").unwrap();
let slots = one_pass.captures_at(b"abc", 0).unwrap();
assert_eq!(slots.len(), 4);
}
}