use std::cell::Cell;
use std::collections::{BTreeMap, HashSet};
use crate::ast::{Atom, Greed, Pattern};
use crate::engine::{Match, Span, byte_class_matches, register_eq_matches};
use crate::lexer::Significant;
use crate::token::{Token, TokenKind};
pub trait SigStream {
fn len(&self) -> usize;
fn is_empty(&self) -> bool {
self.len() == 0
}
fn kind(&self, k: usize) -> TokenKind;
fn span(&self, k: usize) -> (usize, usize);
fn longest_run(&self, kind: TokenKind, ceiling: usize) -> Option<usize> {
let (mut best, mut run) = (0usize, 0usize);
for k in 0..self.len() {
if self.kind(k) == kind {
run += 1;
if run > best {
best = run;
if best > ceiling {
return None;
}
}
} else {
run = 0;
}
}
Some(best)
}
fn first_at_or_after(&self, at: usize) -> usize {
let (mut lo, mut hi) = (0usize, self.len());
while lo < hi {
let mid = lo + (hi - lo) / 2;
if self.span(mid).0 < at {
lo = mid + 1;
} else {
hi = mid;
}
}
lo
}
}
impl<S: SigStream + ?Sized> SigStream for &S {
#[inline]
fn len(&self) -> usize {
(**self).len()
}
#[inline]
fn kind(&self, k: usize) -> TokenKind {
(**self).kind(k)
}
#[inline]
fn span(&self, k: usize) -> (usize, usize) {
(**self).span(k)
}
#[inline]
fn longest_run(&self, kind: TokenKind, ceiling: usize) -> Option<usize> {
(**self).longest_run(kind, ceiling)
}
#[inline]
fn first_at_or_after(&self, at: usize) -> usize {
(**self).first_at_or_after(at)
}
}
fn first_at_or_after_over_parts(parts: &[Significant], bases: &[usize], at: usize) -> usize {
let (mut lo, mut hi) = (0usize, parts.len());
while lo < hi {
let mid = lo + (hi - lo) / 2;
let mut q = mid;
while q < parts.len() && parts[q].spans.is_empty() {
q += 1;
}
if q == parts.len() || (parts[q].spans[0].0 as usize) >= at {
hi = mid;
} else {
lo = q + 1;
}
}
if lo > 0 {
let prev = &parts[lo - 1];
let i = prev.spans.partition_point(|&(s, _)| (s as usize) < at);
if i < prev.spans.len() {
return bases[lo - 1] + i;
}
}
let mut q = lo;
while q < parts.len() && parts[q].spans.is_empty() {
q += 1;
}
bases[q]
}
#[inline]
fn longest_run_in(kinds: &[u32], code: u32, ceiling: usize, run: &mut usize, best: &mut usize) -> bool {
let (mut r, mut b) = (*run, *best);
for &k in kinds {
r = if k == code { r + 1 } else { 0 };
b = b.max(r);
if b > ceiling {
return false;
}
}
*run = r;
*best = b;
true
}
fn longest_run_over_parts(parts: &[Significant], kind: TokenKind, ceiling: usize) -> Option<usize> {
let code = kind.code();
let (mut run, mut best) = (0usize, 0usize);
for part in parts {
if !longest_run_in(&part.kinds, code, ceiling, &mut run, &mut best) {
return None;
}
}
Some(best)
}
pub(crate) trait LeafRead: SigStream + Sync {
type Leaf<'a>: SigStream
where
Self: 'a;
fn leaf(&self) -> Self::Leaf<'_>;
}
pub(crate) struct Stitched<'a> {
toks: &'a [Token],
sig: &'a [usize],
}
impl<'a> Stitched<'a> {
pub(crate) fn new(toks: &'a [Token], sig: &'a [usize]) -> Self {
Stitched { toks, sig }
}
pub(crate) fn significant_of(toks: &[Token]) -> Vec<usize> {
(0..toks.len()).filter(|&i| toks[i].kind != TokenKind::Whitespace).collect()
}
}
impl SigStream for Stitched<'_> {
#[inline]
fn len(&self) -> usize {
self.sig.len()
}
#[inline]
fn kind(&self, k: usize) -> TokenKind {
self.toks[self.sig[k]].kind
}
#[inline]
fn span(&self, k: usize) -> (usize, usize) {
let t = &self.toks[self.sig[k]];
(t.start(), t.end())
}
}
impl<'a> LeafRead for Stitched<'a> {
type Leaf<'l>
= &'l Stitched<'a>
where
Self: 'l;
#[inline]
fn leaf(&self) -> &Stitched<'a> {
self
}
}
pub(crate) struct Parts<'a> {
parts: &'a [Significant],
bases: &'a [usize],
}
impl<'a> Parts<'a> {
pub(crate) fn bases_of(parts: &[Significant]) -> Vec<usize> {
let mut bases = Vec::with_capacity(parts.len() + 1);
let mut total = 0;
for part in parts {
bases.push(total);
total += part.kinds.len();
}
bases.push(total);
bases
}
pub(crate) fn new(parts: &'a [Significant], bases: &'a [usize]) -> Self {
debug_assert_eq!(bases.len(), parts.len() + 1, "one base a part, then the total");
Parts { parts, bases }
}
#[inline]
fn locate(&self, k: usize) -> (usize, usize) {
let p = self.bases.partition_point(|&b| b <= k) - 1;
(p, k - self.bases[p])
}
#[inline]
fn holds(&self, p: usize, k: usize) -> bool {
p + 1 < self.bases.len() && self.bases[p] <= k && k < self.bases[p + 1]
}
fn cursor(&self) -> PartsCursor<'_> {
PartsCursor { parts: self, at: Cell::new(0) }
}
}
impl<'a> LeafRead for Parts<'a> {
type Leaf<'l>
= PartsCursor<'l>
where
Self: 'l;
#[inline]
fn leaf(&self) -> PartsCursor<'_> {
self.cursor()
}
}
impl SigStream for Parts<'_> {
#[inline]
fn len(&self) -> usize {
self.bases[self.bases.len() - 1]
}
#[inline]
fn kind(&self, k: usize) -> TokenKind {
let (p, i) = self.locate(k);
TokenKind::from_code(self.parts[p].kinds[i])
}
#[inline]
fn span(&self, k: usize) -> (usize, usize) {
let (p, i) = self.locate(k);
let (a, b) = self.parts[p].spans[i];
(a as usize, b as usize)
}
fn longest_run(&self, kind: TokenKind, ceiling: usize) -> Option<usize> {
longest_run_over_parts(self.parts, kind, ceiling)
}
fn first_at_or_after(&self, at: usize) -> usize {
first_at_or_after_over_parts(self.parts, self.bases, at)
}
}
pub(crate) struct PartsCursor<'a> {
parts: &'a Parts<'a>,
at: Cell<usize>,
}
impl PartsCursor<'_> {
#[inline]
fn locate(&self, k: usize) -> (usize, usize) {
let mut p = self.at.get();
if !self.parts.holds(p, k) {
p = self.parts.locate(k).0;
self.at.set(p);
}
(p, k - self.parts.bases[p])
}
}
impl SigStream for PartsCursor<'_> {
#[inline]
fn len(&self) -> usize {
self.parts.len()
}
#[inline]
fn kind(&self, k: usize) -> TokenKind {
let (p, i) = self.locate(k);
TokenKind::from_code(self.parts.parts[p].kinds[i])
}
#[inline]
fn span(&self, k: usize) -> (usize, usize) {
let (p, i) = self.locate(k);
let (a, b) = self.parts.parts[p].spans[i];
(a as usize, b as usize)
}
fn longest_run(&self, kind: TokenKind, ceiling: usize) -> Option<usize> {
self.parts.longest_run(kind, ceiling)
}
fn first_at_or_after(&self, at: usize) -> usize {
self.parts.first_at_or_after(at)
}
}
pub(crate) struct OwnedStream {
parts: crate::parallel_lex::OwnedParts,
bases: Vec<usize>,
at: Cell<usize>,
}
impl OwnedStream {
pub(crate) fn over(input: &[u8]) -> Self {
let parts = crate::parallel_lex::lex_significant_parts_owned(input);
let bases = Parts::bases_of(parts.parts());
OwnedStream { parts, bases, at: Cell::new(0) }
}
pub(crate) fn parts_view(&self) -> Parts<'_> {
Parts::new(self.parts.parts(), &self.bases)
}
#[inline]
fn locate(&self, k: usize) -> (usize, usize) {
let mut p = self.at.get();
if !(p + 1 < self.bases.len() && self.bases[p] <= k && k < self.bases[p + 1]) {
p = self.bases.partition_point(|&b| b <= k) - 1;
self.at.set(p);
}
(p, k - self.bases[p])
}
}
impl SigStream for OwnedStream {
#[inline]
fn len(&self) -> usize {
self.bases[self.bases.len() - 1]
}
#[inline]
fn kind(&self, k: usize) -> TokenKind {
let (p, i) = self.locate(k);
TokenKind::from_code(self.parts.parts()[p].kinds[i])
}
#[inline]
fn span(&self, k: usize) -> (usize, usize) {
let (p, i) = self.locate(k);
let (a, b) = self.parts.parts()[p].spans[i];
(a as usize, b as usize)
}
fn longest_run(&self, kind: TokenKind, ceiling: usize) -> Option<usize> {
longest_run_over_parts(self.parts.parts(), kind, ceiling)
}
fn first_at_or_after(&self, at: usize) -> usize {
first_at_or_after_over_parts(self.parts.parts(), &self.bases, at)
}
}
pub(crate) trait Shardable: SigStream {
fn shard(&self) -> Option<Parts<'_>> {
None
}
}
impl Shardable for Stitched<'_> {}
impl Shardable for OwnedStream {
fn shard(&self) -> Option<Parts<'_>> {
Some(self.parts_view())
}
}
#[inline]
fn text_of<'i, S: SigStream + ?Sized>(input: &'i [u8], s: &S, k: usize) -> &'i [u8] {
let (a, b) = s.span(k);
&input[a..b]
}
#[inline]
fn bytes_between<'i, S: SigStream + ?Sized>(input: &'i [u8], s: &S, ks: usize, ke: usize) -> &'i [u8] {
&input[s.span(ks).0..s.span(ke - 1).1]
}
#[inline]
fn span_between<S: SigStream + ?Sized>(s: &S, ks: usize, ke: usize) -> Span {
Span { start: s.span(ks).0 as u32, end: s.span(ke - 1).1 as u32 }
}
#[derive(Clone, Debug, PartialEq)]
enum Inst {
Atom(Atom),
Split(usize, usize),
Jmp(usize),
Save(usize),
Guard(Vec<u8>, bool),
Anchor(crate::ast::AnchorKind),
Match,
}
struct Program {
insts: Vec<Inst>,
nslots: usize,
slots: BTreeMap<String, usize>,
names: std::sync::Arc<[String]>,
reads_registers: bool,
clock: crate::typed::ClockCell,
}
pub(crate) fn needs_set_engine(pat: &Pattern) -> bool {
needs_set_engine_unless(pat, false)
}
fn needs_set_engine_unless(pat: &Pattern, spectral_ok: bool) -> bool {
let needs = |p: &Pattern| needs_set_engine_unless(p, spectral_ok);
match pat {
Pattern::Balanced(..) | Pattern::Field(..) | Pattern::Within(..) => true,
Pattern::Anchor(k) => !matches!(
k,
crate::ast::AnchorKind::LineStart
| crate::ast::AnchorKind::LineEnd
| crate::ast::AnchorKind::InputStart
| crate::ast::AnchorKind::InputEnd
| crate::ast::AnchorKind::Resume
| crate::ast::AnchorKind::ResetStart
),
Pattern::Atom(a) => atom_needs_set_engine(a, spectral_ok),
Pattern::Assert(..) => true,
Pattern::Atomic(..) => true,
Pattern::Empty | Pattern::Guard(..) => false,
Pattern::Star(p, _) | Pattern::Plus(p, _) | Pattern::Opt(p, _) | Pattern::Bind(_, _, p) => needs(p),
Pattern::Repeat(p, _, _, _) => needs(p),
Pattern::Alt(v, mode) => *mode != crate::ast::AltMode::First || v.iter().any(needs),
Pattern::Concat(v) => v.iter().any(needs),
}
}
fn atom_needs_set_engine(a: &Atom, spectral_ok: bool) -> bool {
match a {
Atom::Spectral(_) => !spectral_ok,
Atom::Kind(TokenKind::Whitespace) | Atom::Byte(crate::ast::ByteClass::Space) => true,
Atom::Magnitude(p) | Atom::KindMag(_, p) => p.scope().is_some(),
Atom::Since(..) => true,
Atom::RegisterKin(..) => true,
Atom::Class(c) => c.members().any(|m| atom_needs_set_engine(m, spectral_ok)),
_ => false,
}
}
pub(crate) fn empty_loop_needs_set_engine(pat: &Pattern) -> bool {
match pat {
Pattern::Star(p, _) | Pattern::Plus(p, _) => {
nullable(p) || empty_loop_needs_set_engine(p)
}
Pattern::Repeat(p, _, hi, _) => {
(hi.is_none() && nullable(p)) || empty_loop_needs_set_engine(p)
}
Pattern::Opt(p, _)
| Pattern::Bind(_, _, p)
| Pattern::Atomic(p)
| Pattern::Field(_, p)
| Pattern::Balanced(_, p)
| Pattern::Assert(p, _, _) => empty_loop_needs_set_engine(p),
Pattern::Concat(v) | Pattern::Alt(v, _) => v.iter().any(empty_loop_needs_set_engine),
Pattern::Empty
| Pattern::Atom(_)
| Pattern::Within(..)
| Pattern::Guard(..)
| Pattern::Anchor(_) => false,
}
}
fn lean(g: Greed, body: usize, exit: usize) -> Inst {
match g {
Greed::Greedy => Inst::Split(body, exit),
Greed::Lazy => Inst::Split(exit, body),
}
}
fn nullable(pat: &Pattern) -> bool {
match pat {
Pattern::Empty | Pattern::Guard(..) | Pattern::Anchor(_) | Pattern::Assert(..) => true,
Pattern::Atom(_) | Pattern::Balanced(..) | Pattern::Within(..) => false,
Pattern::Bind(_, _, p) | Pattern::Atomic(p) | Pattern::Field(_, p) | Pattern::Plus(p, _) => {
nullable(p)
}
Pattern::Concat(v) => v.iter().all(nullable),
Pattern::Alt(v, _) => v.iter().any(nullable),
Pattern::Star(..) | Pattern::Opt(..) => true,
Pattern::Repeat(p, m, _, _) => *m == 0 || nullable(p),
}
}
struct Compiler {
insts: Vec<Inst>,
slots: BTreeMap<String, usize>,
}
impl Compiler {
fn here(&self) -> usize {
self.insts.len()
}
fn push(&mut self, inst: Inst) -> usize {
let at = self.here();
self.insts.push(inst);
at
}
fn slot_of(&mut self, name: &str) -> usize {
if let Some(&base) = self.slots.get(name) {
return base;
}
let base = 2 + self.slots.len() * 2;
self.slots.insert(name.to_string(), base);
base
}
fn emit(&mut self, pat: &Pattern) {
match pat {
Pattern::Empty => {}
Pattern::Atom(a) => {
self.push(Inst::Atom(a.clone()));
}
Pattern::Within(..) => unreachable!("an edit-distance group routes to the set engine"),
Pattern::Guard(lit, neg) => {
self.push(Inst::Guard(lit.as_bytes().to_vec(), *neg));
}
Pattern::Concat(parts) => {
for p in parts {
self.emit(p);
}
}
Pattern::Alt(parts, _) => self.emit_alt(parts),
Pattern::Star(p, g) => self.emit_star(p, *g),
Pattern::Plus(p, g) => self.emit_plus(p, *g),
Pattern::Opt(p, g) => {
let split = self.push(Inst::Split(0, 0));
let body = self.here();
self.emit(p);
let exit = self.here();
self.insts[split] = lean(*g, body, exit);
}
Pattern::Repeat(p, m, n, g) => self.emit_repeat(p, *m, *n, *g),
Pattern::Bind(name, _, p) => {
let base = self.slot_of(name);
self.push(Inst::Save(base));
self.emit(p);
self.push(Inst::Save(base + 1));
}
Pattern::Anchor(crate::ast::AnchorKind::ResetStart) => {
self.push(Inst::Save(0));
}
Pattern::Anchor(k) => {
self.push(Inst::Anchor(k.clone()));
}
Pattern::Balanced(..)
| Pattern::Field(..)
| Pattern::Assert(..)
| Pattern::Atomic(..) => unreachable!(),
}
}
fn emit_alt(&mut self, alts: &[Pattern]) {
if alts.len() == 1 {
self.emit(&alts[0]);
return;
}
let split = self.push(Inst::Split(0, 0));
let first = self.here();
self.emit(&alts[0]);
let jmp = self.push(Inst::Jmp(0));
let rest = self.here();
self.emit_alt(&alts[1..]);
let end = self.here();
self.insts[split] = Inst::Split(first, rest);
self.insts[jmp] = Inst::Jmp(end);
}
fn emit_star(&mut self, p: &Pattern, g: Greed) {
let head = self.push(Inst::Split(0, 0));
let body = self.here();
self.emit(p);
if nullable(p) {
let back = self.push(Inst::Split(0, 0));
let exit = self.here();
self.insts[head] = lean(g, body, exit);
self.insts[back] = lean(g, body, exit);
} else {
self.push(Inst::Jmp(head));
let exit = self.here();
self.insts[head] = lean(g, body, exit);
}
}
fn emit_plus(&mut self, p: &Pattern, g: Greed) {
let body = self.here();
self.emit(p);
let back = self.push(Inst::Split(0, 0));
let exit = self.here();
self.insts[back] = lean(g, body, exit);
}
fn emit_repeat(&mut self, p: &Pattern, m: usize, n: Option<usize>, g: Greed) {
match n {
None => {
if m == 0 {
self.emit_star(p, g);
} else {
for _ in 1..m {
self.emit(p);
}
self.emit_plus(p, g);
}
}
Some(nn) => {
for _ in 0..m {
self.emit(p);
}
let mut splits = Vec::new();
for _ in m..nn {
let split = self.push(Inst::Split(0, 0));
splits.push(split);
let body = self.here();
self.emit(p);
self.insts[split] = lean(g, body, 0);
}
let end = self.here();
for split in splits {
if let Inst::Split(a, b) = self.insts[split] {
self.insts[split] = match g {
Greed::Greedy => Inst::Split(a, end),
Greed::Lazy => Inst::Split(end, b),
};
}
}
}
}
}
}
fn compile(pat: &Pattern) -> Option<Program> {
compile_unless(pat, false)
}
fn compile_unless(pat: &Pattern, spectral_ok: bool) -> Option<Program> {
if needs_set_engine_unless(pat, spectral_ok) {
return None;
}
let mut c = Compiler { insts: Vec::new(), slots: BTreeMap::new() };
c.push(Inst::Save(0));
c.emit(pat);
c.push(Inst::Save(1));
c.push(Inst::Match);
let nslots = 2 + c.slots.len() * 2;
let reads_registers = pat.reads_registers();
let names: std::sync::Arc<[String]> = c.slots.keys().cloned().collect();
Some(Program {
insts: c.insts,
nslots,
slots: c.slots,
names,
reads_registers,
clock: crate::typed::ClockCell::now(),
})
}
const MAX_SAVE_SLOTS: usize = 8;
#[derive(Clone, Copy, PartialEq, Eq)]
struct Saves {
slots: [usize; MAX_SAVE_SLOTS],
len: usize,
}
impl std::hash::Hash for Saves {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.len.hash(state);
self.slots[..self.len].hash(state);
}
}
impl Saves {
#[inline]
fn unset(len: usize) -> Self {
Saves { slots: [usize::MAX; MAX_SAVE_SLOTS], len }
}
#[inline]
fn get(&self, i: usize) -> usize {
self.slots[i]
}
#[inline]
fn set(&mut self, i: usize, v: usize) {
self.slots[i] = v;
}
#[inline]
fn as_slice(&self) -> &[usize] {
&self.slots[..self.len]
}
#[inline]
fn register_key(&self) -> Saves {
let mut k = *self;
k.slots[0] = 0;
k.slots[1] = 0;
k
}
}
#[derive(Clone, Copy)]
struct Thread {
pc: usize,
saves: Saves,
}
#[derive(Clone)]
struct RegisterSlot {
stamp: u32,
pc: u32,
saves: Saves,
}
struct RegisterSeen {
slots: Vec<RegisterSlot>,
live: usize,
stamp: u32,
build: crate::fxhash::FxBuild,
}
impl RegisterSeen {
const INITIAL: usize = 64;
fn new() -> Self {
Self {
slots: vec![RegisterSlot { stamp: 0, pc: 0, saves: Saves::unset(0) }; Self::INITIAL],
live: 0,
stamp: 1,
build: crate::fxhash::FxBuild::process(),
}
}
fn clear(&mut self) {
self.live = 0;
self.stamp = self.stamp.wrapping_add(1);
if self.stamp == 0 {
for slot in &mut self.slots {
slot.stamp = 0;
}
self.stamp = 1;
}
}
#[inline]
fn insert(&mut self, pc: usize, saves: Saves) -> bool {
if (self.live + 1) * 4 > self.slots.len() * 3 {
self.grow();
}
let mask = self.slots.len() - 1;
let mut i = self.hash(pc, &saves) as usize & mask;
loop {
if self.slots[i].stamp != self.stamp {
self.slots[i] = RegisterSlot { stamp: self.stamp, pc: pc as u32, saves };
self.live += 1;
return true;
}
if self.slots[i].pc as usize == pc && self.slots[i].saves == saves {
return false;
}
i = (i + 1) & mask;
}
}
#[inline(always)]
fn hash(&self, pc: usize, saves: &Saves) -> u64 {
use std::hash::BuildHasher;
self.build.hash_one((pc, saves))
}
fn grow(&mut self) {
let capacity = self.slots.len() * 2;
let free = RegisterSlot { stamp: 0, pc: 0, saves: Saves::unset(0) };
let old = std::mem::replace(&mut self.slots, vec![free; capacity]);
let stamp = self.stamp;
self.live = 0;
for slot in old {
if slot.stamp == stamp {
self.insert(slot.pc as usize, slot.saves);
}
}
}
}
#[cfg(test)]
mod register_seen_tests {
use super::{RegisterSeen, Saves};
fn key(a: usize, b: usize) -> Saves {
let mut s = Saves::unset(4);
s.set(2, a);
s.set(3, b);
s
}
#[test]
fn a_key_is_new_once_and_held_after() {
let mut seen = RegisterSeen::new();
assert!(seen.insert(7, key(1, 2)));
assert!(!seen.insert(7, key(1, 2)));
assert!(seen.insert(8, key(1, 2)), "another counter is another key");
assert!(seen.insert(7, key(1, 3)), "another binding is another key");
}
#[test]
fn clearing_frees_every_key_without_walking() {
let mut seen = RegisterSeen::new();
assert!(seen.insert(3, key(4, 5)));
seen.clear();
assert!(seen.insert(3, key(4, 5)), "the stamp moved, so the slot reads free");
assert_eq!(seen.live, 1);
}
#[test]
fn it_grows_past_its_first_slots_and_keeps_every_key() {
let mut seen = RegisterSeen::new();
for i in 0..500usize {
assert!(seen.insert(i, key(i, i + 1)), "key {i} is new");
}
assert!(seen.slots.len() >= 512, "the table doubled to hold them");
for i in 0..500usize {
assert!(!seen.insert(i, key(i, i + 1)), "key {i} is still held");
}
}
#[test]
fn a_wrapped_stamp_frees_the_table() {
let mut seen = RegisterSeen::new();
seen.stamp = u32::MAX;
assert!(seen.insert(1, key(9, 9)));
seen.clear();
assert_eq!(seen.stamp, 1, "the stamp restarts past a wrap");
assert!(seen.insert(1, key(9, 9)), "and every slot reads free");
}
}
struct ThreadList {
dense: Vec<Thread>,
seen: Vec<u32>,
stamp: u32,
seen_regs: RegisterSeen,
keyed_by_registers: bool,
closure_steps: u64,
steps_already_seen: u64,
threads_queued: u64,
}
impl ThreadList {
fn new(np: usize, keyed_by_registers: bool) -> Self {
Self {
dense: Vec::new(),
seen: vec![0; np],
stamp: 1,
seen_regs: RegisterSeen::new(),
keyed_by_registers,
closure_steps: 0,
steps_already_seen: 0,
threads_queued: 0,
}
}
fn clear(&mut self) {
self.dense.clear();
self.stamp = self.stamp.wrapping_add(1);
if self.stamp == 0 {
self.seen.fill(0);
self.stamp = 1;
}
if self.keyed_by_registers {
self.seen_regs.clear();
}
}
fn mark(&mut self, pc: usize, saves: &Saves) -> bool {
if self.keyed_by_registers {
!self.seen_regs.insert(pc, saves.register_key())
} else {
let was = self.seen[pc] == self.stamp;
self.seen[pc] = self.stamp;
was
}
}
#[allow(clippy::too_many_arguments)]
fn add<S: SigStream + ?Sized>(
&mut self,
prog: &Program,
pc: usize,
saves: &Saves,
pos: usize,
input: &[u8],
stream: &S,
absent: &HashSet<Vec<u8>>,
) {
self.closure_steps += 1;
if self.mark(pc, saves) {
self.steps_already_seen += 1;
return;
}
match &prog.insts[pc] {
Inst::Jmp(t) => self.add(prog, *t, saves, pos, input, stream, absent),
Inst::Split(a, b) => {
self.add(prog, *a, saves, pos, input, stream, absent);
self.add(prog, *b, saves, pos, input, stream, absent);
}
Inst::Save(slot) => {
let mut s = *saves;
s.set(*slot, pos);
self.add(prog, pc + 1, &s, pos, input, stream, absent);
}
Inst::Guard(lit, neg) => {
if guard_present(input, stream, pos, lit, absent) != *neg {
self.add(prog, pc + 1, saves, pos, input, stream, absent);
}
}
Inst::Anchor(kind) => {
let m = stream.len();
let holds = pos < m && {
let (a, b) = stream.span(pos);
crate::engine::anchor_holds_at(kind, input, a, b, pos == 0, pos + 1 == m)
};
if holds {
self.add(prog, pc + 1, saves, pos, input, stream, absent);
}
}
Inst::Atom(_) | Inst::Match => {
self.threads_queued += 1;
self.dense.push(Thread { pc, saves: *saves });
}
}
}
}
impl Drop for ThreadList {
fn drop(&mut self) {
if std::thread::panicking() {
return;
}
crate::trace::counted("the walk: closure steps", self.closure_steps);
crate::trace::counted("the walk: closure steps already seen", self.steps_already_seen);
crate::trace::counted("the walk: threads queued", self.threads_queued);
}
}
fn guard_present<S: SigStream + ?Sized>(
input: &[u8],
stream: &S,
pos: usize,
lit: &[u8],
absent: &HashSet<Vec<u8>>,
) -> bool {
if absent.contains(lit) {
return false;
}
let from = if pos < stream.len() { stream.span(pos).0 } else { input.len() };
crate::byte_simd::contains(&input[from..], lit)
}
fn atom_matches<S: SigStream + ?Sized>(
a: &Atom,
prog: &Program,
input: &[u8],
stream: &S,
k: usize,
saves: &[usize],
) -> bool {
let kind = stream.kind(k);
let txt = || text_of(input, stream, k);
match a {
Atom::Kind(want) => kind == *want,
Atom::Any => true,
Atom::Literal(lit, group) => {
crate::engine::register_eq_matches(lit.as_bytes(), txt(), *group)
}
Atom::Byte(bc) => byte_class_matches(*bc, txt()),
Atom::BytePattern(bp) => bp.matches_whole(txt()),
Atom::Spectral(_) | Atom::Since(..) | Atom::RegisterKin(..) => false,
Atom::Magnitude(pred) => pred.matches(crate::magnitude::token_magnitude(kind, txt()), None),
Atom::KindMag(want, pred) => {
kind == *want && pred.matches(crate::magnitude::token_magnitude(kind, txt()), None)
}
Atom::KindPred(want, pred) => kind == *want && pred.matches_as(kind, txt(), || prog.clock.get()),
Atom::Class(c) => {
let hit = c.any.iter().any(|m| atom_matches(m, prog, input, stream, k, saves))
&& (c.all.is_empty() || c.all.iter().any(|m| atom_matches(m, prog, input, stream, k, saves)))
&& !c.none.iter().any(|m| atom_matches(m, prog, input, stream, k, saves));
hit != c.negated
}
Atom::RegisterEq(name, group) => match prog.slots.get(name) {
Some(&base) => {
let (start, end) = (saves[base], saves[base + 1]);
if start == usize::MAX || end == usize::MAX || end <= start {
return false;
}
let bound = bytes_between(input, stream, start, end);
register_eq_matches(bound, txt(), *group)
}
None => false,
},
Atom::RegisterRelated(name, relation) => match prog.slots.get(name) {
Some(&base) => {
let (start, end) = (saves[base], saves[base + 1]);
if start == usize::MAX || end == usize::MAX || end <= start {
return false;
}
relation.related(bytes_between(input, stream, start, end), txt())
}
None => false,
},
Atom::LiteralWithin(lit, k, group) => {
crate::engine::within_edits(lit.as_bytes(), txt(), *k, *group)
}
Atom::RegisterWithin(name, k, group) => match prog.slots.get(name) {
Some(&base) => {
let (start, end) = (saves[base], saves[base + 1]);
if start == usize::MAX || end == usize::MAX || end <= start {
return false;
}
crate::engine::within_edits(bytes_between(input, stream, start, end), txt(), *k, *group)
}
None => false,
},
}
}
fn opening_atoms(prog: &Program) -> Option<Vec<&Atom>> {
let pcs = opening_counters(prog)?;
let mut opens = Vec::with_capacity(pcs.len());
for pc in pcs {
if let Inst::Atom(a) = &prog.insts[pc] {
opens.push(a);
}
}
Some(opens)
}
fn opening_literals<'a>(opens: &[&'a Atom]) -> Option<Vec<&'a [u8]>> {
let mut lits = Vec::with_capacity(opens.len());
for a in opens {
match a {
Atom::Literal(lit, crate::orbit::OrbitGroup::Identity) if !lit.is_empty() => {
lits.push(lit.as_bytes());
}
_ => return None,
}
}
(!lits.is_empty()).then_some(lits)
}
fn opening_counters(prog: &Program) -> Option<Vec<usize>> {
if prog.reads_registers {
return None;
}
let mut seen = vec![false; prog.insts.len()];
let mut stack = vec![0usize];
let mut opens = Vec::new();
while let Some(pc) = stack.pop() {
if std::mem::replace(&mut seen[pc], true) {
continue;
}
match &prog.insts[pc] {
Inst::Atom(_) => opens.push(pc),
Inst::Match => return None,
Inst::Split(a, b) => {
stack.push(*a);
stack.push(*b);
}
Inst::Jmp(t) => stack.push(*t),
Inst::Save(_) | Inst::Guard(..) | Inst::Anchor(_) => stack.push(pc + 1),
}
}
(!opens.is_empty()).then_some(opens)
}
pub(crate) struct Union {
prog: Program,
members: usize,
owner: Vec<Option<u32>>,
entries: Vec<usize>,
opens: Vec<Vec<usize>>,
always: Vec<u32>,
by_kind: std::collections::HashMap<TokenKind, Vec<u32>>,
by_literal: std::collections::HashMap<Vec<u8>, Vec<u32>>,
}
struct Trie {
inst: Inst,
children: Vec<Trie>,
ends: Vec<u32>,
}
fn linear_prefix_len(insts: &[Inst]) -> usize {
let mut targeted = vec![false; insts.len()];
for inst in insts {
match inst {
Inst::Split(a, b) => {
targeted[*a] = true;
targeted[*b] = true;
}
Inst::Jmp(t) => targeted[*t] = true,
Inst::Atom(_) | Inst::Save(_) | Inst::Guard(..) | Inst::Anchor(_) | Inst::Match => {}
}
}
let end = insts
.iter()
.enumerate()
.skip(1)
.find(|(pc, inst)| {
targeted[*pc] || matches!(inst, Inst::Split(..) | Inst::Jmp(_) | Inst::Match)
})
.map_or(insts.len(), |(pc, _)| pc);
end.max(1)
}
fn trie_insert(nodes: &mut Vec<Trie>, prefix: &[Inst], m: u32) {
let Some((first, rest)) = prefix.split_first() else {
return;
};
let at = match nodes.iter().position(|n| n.inst == *first) {
Some(at) => at,
None => {
nodes.push(Trie { inst: first.clone(), children: Vec::new(), ends: Vec::new() });
nodes.len() - 1
}
};
if rest.is_empty() {
nodes[at].ends.push(m);
} else {
trie_insert(&mut nodes[at].children, rest, m);
}
}
fn opening_from(insts: &[Inst], entry: usize) -> Vec<usize> {
let mut seen = vec![false; insts.len()];
let mut stack = vec![entry];
let mut opens = Vec::new();
while let Some(pc) = stack.pop() {
if std::mem::replace(&mut seen[pc], true) {
continue;
}
match &insts[pc] {
Inst::Atom(_) => opens.push(pc),
Inst::Match => {}
Inst::Split(a, b) => {
stack.push(*a);
stack.push(*b);
}
Inst::Jmp(t) => stack.push(*t),
Inst::Save(_) | Inst::Guard(..) | Inst::Anchor(_) => stack.push(pc + 1),
}
}
opens
}
#[must_use]
pub(crate) fn union_eligible(pattern: &Pattern) -> bool {
if pattern.starts_with_resume()
|| pattern.mentions_reset_start()
|| !pattern.library_kinds().is_empty()
{
return false;
}
match compile(pattern) {
Some(prog) => {
prog.nslots <= MAX_SAVE_SLOTS
&& !prog.reads_registers
&& opening_counters(&prog).is_some()
}
None => false,
}
}
impl Union {
pub(crate) fn of(patterns: &[&Pattern]) -> Union {
let mut rests: Vec<(usize, Vec<Inst>)> = Vec::with_capacity(patterns.len());
let mut root: Vec<Trie> = Vec::new();
let mut unshared: Vec<u32> = Vec::new();
let mut nslots = 0;
for (i, p) in patterns.iter().enumerate() {
let m = u32::try_from(i).expect("a union holds fewer than four billion members");
let prog = compile(p).expect("every member passed union_eligible");
nslots = nslots.max(prog.nslots);
let split = linear_prefix_len(&prog.insts);
if split > 1 {
trie_insert(&mut root, &prog.insts[1..split], m);
} else {
unshared.push(m);
}
rests.push((split, prog.insts[split..].to_vec()));
}
let mut insts: Vec<Inst> = Vec::new();
let mut owner: Vec<Option<u32>> = Vec::new();
let mut entries = Vec::with_capacity(root.len() + unshared.len());
for node in &root {
entries.push(insts.len());
insts.push(Inst::Save(0));
owner.push(None);
emit_trie(node, &mut insts, &mut owner, &rests);
}
for &m in &unshared {
entries.push(insts.len());
insts.push(Inst::Save(0));
owner.push(None);
emit_rest(m, &mut insts, &mut owner, &rests);
}
let mut opens = Vec::with_capacity(entries.len());
let mut always = Vec::new();
let mut by_kind: std::collections::HashMap<TokenKind, Vec<u32>> =
std::collections::HashMap::new();
let mut by_literal: std::collections::HashMap<Vec<u8>, Vec<u32>> =
std::collections::HashMap::new();
for (e, &entry) in entries.iter().enumerate() {
let e = u32::try_from(e).expect("fewer entries than members");
let opening = opening_from(&insts, entry);
let mut in_always = false;
for &pc in &opening {
match &insts[pc] {
Inst::Atom(Atom::Kind(k) | Atom::KindMag(k, _) | Atom::KindPred(k, _)) => {
let members = by_kind.entry(*k).or_default();
if members.last() != Some(&e) {
members.push(e);
}
}
Inst::Atom(Atom::Literal(lit, crate::orbit::OrbitGroup::Identity)) => {
let members = by_literal.entry(lit.as_bytes().to_vec()).or_default();
if members.last() != Some(&e) {
members.push(e);
}
}
Inst::Atom(_) => {
if !in_always {
always.push(e);
in_always = true;
}
}
Inst::Split(..) | Inst::Jmp(_) | Inst::Save(_) | Inst::Guard(..) | Inst::Anchor(_) | Inst::Match => {}
}
}
opens.push(opening);
}
Union {
prog: Program {
insts,
nslots,
slots: BTreeMap::new(),
names: std::sync::Arc::from(Vec::<String>::new()),
reads_registers: false,
clock: crate::typed::ClockCell::now(),
},
members: patterns.len(),
owner,
entries,
opens,
always,
by_kind,
by_literal,
}
}
pub(crate) fn len(&self) -> usize {
self.members
}
fn candidates<S: SigStream + ?Sized>(&self, input: &[u8], stream: &S, k: usize, out: &mut Vec<u32>) {
out.clear();
out.extend_from_slice(&self.always);
if let Some(entries) = self.by_kind.get(&stream.kind(k)) {
out.extend_from_slice(entries);
}
if !self.by_literal.is_empty()
&& let Some(entries) = self.by_literal.get(text_of(input, stream, k))
{
out.extend_from_slice(entries);
}
}
fn admits<S: SigStream + ?Sized>(&self, e: usize, input: &[u8], stream: &S, k: usize) -> bool {
self.opens[e].iter().any(|&pc| match &self.prog.insts[pc] {
Inst::Atom(a) => atom_matches(a, &self.prog, input, stream, k, &[]),
_ => false,
})
}
}
fn emit_trie(node: &Trie, insts: &mut Vec<Inst>, owner: &mut Vec<Option<u32>>, rests: &[(usize, Vec<Inst>)]) {
insts.push(node.inst.clone());
owner.push(None);
let branches = node.children.len() + node.ends.len();
if branches == 1 {
match node.children.first() {
Some(child) => emit_trie(child, insts, owner, rests),
None => emit_rest(node.ends[0], insts, owner, rests),
}
return;
}
if branches == 0 {
return;
}
let first_split = insts.len();
for _ in 0..branches - 1 {
insts.push(Inst::Split(0, 0));
owner.push(None);
}
let mut starts = Vec::with_capacity(branches);
for child in &node.children {
starts.push(insts.len());
emit_trie(child, insts, owner, rests);
}
for &m in &node.ends {
starts.push(insts.len());
emit_rest(m, insts, owner, rests);
}
for i in 0..branches - 1 {
let next = if i + 1 < branches - 1 { first_split + i + 1 } else { starts[branches - 1] };
insts[first_split + i] = Inst::Split(starts[i], next);
}
}
fn emit_rest(m: u32, insts: &mut Vec<Inst>, owner: &mut Vec<Option<u32>>, rests: &[(usize, Vec<Inst>)]) {
let (split, rest) = &rests[m as usize];
let base = insts.len();
let rebase = |t: usize| base + t.checked_sub(*split).expect("a jump never lands in the shared run");
for inst in rest {
insts.push(match inst {
Inst::Split(a, b) => Inst::Split(rebase(*a), rebase(*b)),
Inst::Jmp(t) => Inst::Jmp(rebase(*t)),
other => other.clone(),
});
owner.push(Some(m));
}
}
pub(crate) fn first_spans_union(
u: &Union,
active: &[bool],
input: &[u8],
toks: &[Token],
from: usize,
absent: &HashSet<Vec<u8>>,
) -> Vec<Option<Span>> {
u.prog.clock.refresh();
let sig: Vec<usize> = (from.min(toks.len())..toks.len())
.filter(|&i| toks[i].kind != TokenKind::Whitespace)
.collect();
let stream = Stitched { toks, sig: &sig };
let m = stream.len();
let n = u.len();
let mut first: Vec<Option<Span>> = vec![None; n];
let mut pending: Vec<Option<Saves>> = vec![None; n];
let mut done: Vec<bool> = active.iter().map(|a| !*a).collect();
let mut open = done.iter().filter(|d| !**d).count();
let mut cut: Vec<usize> = vec![0; n];
let np = u.prog.insts.len();
let mut clist = ThreadList::new(np, false);
let mut nlist = ThreadList::new(np, false);
let mut seeds: Vec<u32> = Vec::new();
let mut k = 0usize;
let mut step = 0usize;
while open > 0 {
step += 1;
if k < m {
u.candidates(input, &stream, k, &mut seeds);
for &e in &seeds {
let e = e as usize;
if u.admits(e, input, &stream, k) {
clist.add(&u.prog, u.entries[e], &Saves::unset(u.prog.nslots), k, input, &stream, absent);
}
}
}
nlist.clear();
for &t in &clist.dense {
let start = t.saves.get(0);
if let Some(j) = u.owner[t.pc] {
let j = j as usize;
if done[j] || cut[j] == step {
continue;
}
if let Some(p) = pending[j]
&& start > p.get(0)
{
continue;
}
}
match &u.prog.insts[t.pc] {
Inst::Match => {
if let Some(j) = u.owner[t.pc] {
let j = j as usize;
pending[j] = Some(t.saves);
cut[j] = step;
}
}
Inst::Atom(a) if k < m && atom_matches(a, &u.prog, input, &stream, k, t.saves.as_slice()) => {
nlist.add(&u.prog, t.pc + 1, &t.saves, k + 1, input, &stream, absent);
}
_ => {}
}
}
let oldest = nlist.dense.iter().map(|t| t.saves.get(0)).min();
for j in 0..n {
if done[j] {
continue;
}
if let Some(p) = pending[j]
&& oldest.is_none_or(|o| o > p.get(0))
{
let (ks, ke) = (p.get(0), p.get(1));
if ke > ks {
first[j] = Some(span_between(&stream, ks, ke));
}
done[j] = true;
open -= 1;
}
}
std::mem::swap(&mut clist, &mut nlist);
if k >= m {
break;
}
k += 1;
}
first
}
#[inline]
fn opening_admits<S: SigStream + ?Sized>(
prog: &Program,
opens: &[&Atom],
input: &[u8],
stream: &S,
k: usize,
) -> bool {
k < stream.len() && opens.iter().any(|a| atom_matches(a, prog, input, stream, k, &[]))
}
#[allow(clippy::too_many_arguments)]
fn find_leftmost<'l, S: SigStream + ?Sized>(
prog: &Program,
input: &[u8],
stream: &S,
from: usize,
absent: &HashSet<Vec<u8>>,
inject_until: usize,
clist: &'l mut ThreadList,
nlist: &'l mut ThreadList,
) -> Option<Saves> {
find_leftmost_ending(prog, input, stream, from, absent, inject_until, clist, nlist, false)
}
#[allow(clippy::too_many_arguments)]
fn find_leftmost_ending<'l, S: SigStream + ?Sized>(
prog: &Program,
input: &[u8],
stream: &S,
from: usize,
absent: &HashSet<Vec<u8>>,
inject_until: usize,
mut clist: &'l mut ThreadList,
mut nlist: &'l mut ThreadList,
earliest_end: bool,
) -> Option<Saves> {
let m = stream.len();
clist.clear();
nlist.clear();
let mut matched: Option<Saves> = None;
let mut k = from;
loop {
let may_start = k < inject_until;
if matched.is_none() && may_start {
clist.add(prog, 0, &Saves::unset(prog.nslots), k, input, stream, absent);
}
if clist.dense.is_empty() && (matched.is_some() || !may_start) {
break;
}
nlist.clear();
let mut ti = 0;
while ti < clist.dense.len() {
let pc = clist.dense[ti].pc;
match &prog.insts[pc] {
Inst::Match => {
matched = Some(clist.dense[ti].saves);
if earliest_end {
return matched;
}
break;
}
Inst::Atom(a)
if k < m
&& atom_matches(a, prog, input, stream, k, clist.dense[ti].saves.as_slice()) =>
{
nlist.add(prog, pc + 1, &clist.dense[ti].saves, k + 1, input, stream, absent);
}
_ => {}
}
ti += 1;
}
std::mem::swap(&mut clist, &mut nlist);
if k >= m {
break;
}
k += 1;
}
matched
}
fn resolve_captures<S: SigStream + ?Sized>(
prog: &Program,
saves: &[usize],
stream: &S,
) -> crate::engine::Regs {
let bound = |base: usize| {
let (start, end) = (saves[base], saves[base + 1]);
if start != usize::MAX && end != usize::MAX && end > start {
span_between(stream, start, end)
} else {
Span { start: 0, end: 0 }
}
};
let n = prog.slots.len();
if n <= crate::engine::INLINE_REGS {
let mut held = [Span { start: 0, end: 0 }; crate::engine::INLINE_REGS];
for (slot, &base) in held.iter_mut().zip(prog.slots.values()) {
*slot = bound(base);
}
let count = u8::try_from(n).expect("at most INLINE_REGS");
return crate::engine::Regs::Inline(count, held);
}
let caps: Vec<Span> = prog.slots.values().map(|&base| bound(base)).collect();
crate::engine::Regs::Shared(caps.into())
}
fn register_names(prog: &Program) -> std::sync::Arc<[String]> {
prog.names.clone()
}
fn resolve_captures_into<S: SigStream + ?Sized>(
prog: &Program,
saves: &[usize],
stream: &S,
order: &[String],
spans: &mut [Option<Span>],
extents: &mut [Option<(usize, usize)>],
) {
for s in spans.iter_mut() {
*s = None;
}
for e in extents.iter_mut() {
*e = None;
}
for (name, &base) in &prog.slots {
let Some(i) = order.iter().position(|n| n == name) else { continue };
let (start, end) = (saves[base], saves[base + 1]);
if start != usize::MAX && end != usize::MAX && end > start {
spans[i] = Some(span_between(stream, start, end));
extents[i] = Some((start, end));
}
}
}
pub(crate) fn bounded_max_len(pat: &Pattern) -> Option<usize> {
match pat {
Pattern::Assert(..) => Some(0),
Pattern::Empty | Pattern::Guard(..) => Some(0),
Pattern::Atom(_) => Some(1),
Pattern::Bind(_, _, p) | Pattern::Opt(p, _) | Pattern::Atomic(p) => bounded_max_len(p),
Pattern::Concat(v) => {
let mut total = 0usize;
for p in v {
total = total.checked_add(bounded_max_len(p)?)?;
}
Some(total)
}
Pattern::Alt(v, _) => {
let mut mx = 0usize;
for p in v {
mx = mx.max(bounded_max_len(p)?);
}
Some(mx)
}
Pattern::Repeat(p, _, Some(hi), _) => bounded_max_len(p)?.checked_mul(*hi),
Pattern::Star(..) | Pattern::Plus(..) | Pattern::Repeat(_, _, None, _) => None,
Pattern::Balanced(..) | Pattern::Field(..) => None,
Pattern::Within(v, k) => v.len().checked_add(usize::from(*k)),
Pattern::Anchor(_) => Some(0),
}
}
const PARALLEL_SCAN_MIN_ANCHORS: usize = 8192;
const PARALLEL_SCAN_MAX_MATCH_LEN: usize = 64;
#[must_use]
pub fn scan_nfa(pattern: &Pattern, input: &[u8]) -> Option<Vec<Span>> {
let prog = compile(pattern)?;
if prog.nslots > MAX_SAVE_SLOTS {
return None;
}
let lexing = crate::trace::phase("the single pass: its lex, in parts");
crate::parallel_lex::lex_significant_parts_held(input, |parts| {
drop(lexing);
scan_nfa_parts(pattern, input, parts)
})
}
#[must_use]
pub fn scan_nfa_over(pattern: &Pattern, input: &[u8], toks: &[Token]) -> Option<Vec<Span>> {
let prog = compile(pattern)?;
if prog.nslots > MAX_SAVE_SLOTS {
return None;
}
let sig: Vec<usize> =
(0..toks.len()).filter(|&i| toks[i].kind != TokenKind::Whitespace).collect();
let stream = Stitched { toks, sig: &sig };
Some(scan_stream(&prog, pattern, input, &stream, &stream))
}
#[must_use]
pub fn earliest_unsettled(pattern: &Pattern, input: &[u8], toks: &[Token]) -> Option<Option<usize>> {
let prog = compile(pattern)?;
if prog.nslots > MAX_SAVE_SLOTS {
return None;
}
let sig: Vec<usize> =
(0..toks.len()).filter(|&i| toks[i].kind != TokenKind::Whitespace).collect();
let stream = Stitched { toks, sig: &sig };
let absent = crate::prefilter::absent_guard_literals(pattern, input);
let m = stream.len();
let np = prog.insts.len();
let keyed = prog.reads_registers;
let mut clist = ThreadList::new(np, keyed);
let mut nlist = ThreadList::new(np, keyed);
let mut k = 0;
loop {
clist.add(&prog, 0, &Saves::unset(prog.nslots), k, input, &stream, &absent);
if k >= m {
break;
}
nlist.clear();
let mut ti = 0;
while ti < clist.dense.len() {
let pc = clist.dense[ti].pc;
if let Inst::Atom(a) = &prog.insts[pc]
&& atom_matches(a, &prog, input, &stream, k, clist.dense[ti].saves.as_slice())
{
nlist.add(&prog, pc + 1, &clist.dense[ti].saves, k + 1, input, &stream, &absent);
}
ti += 1;
}
std::mem::swap(&mut clist, &mut nlist);
k += 1;
}
let earliest = clist.dense.iter().map(|t| t.saves.get(0)).min();
Some(earliest.map(|start| if start == usize::MAX { m } else { start }).map(|start| {
if start < m { stream.span(start).0 } else { input.len() }
}))
}
#[must_use]
pub fn scan_nfa_parts(pattern: &Pattern, input: &[u8], parts: &[Significant]) -> Option<Vec<Span>> {
let prog = compile(pattern)?;
if prog.nslots > MAX_SAVE_SLOTS {
return None;
}
let bases = Parts::bases_of(parts);
let stream = Parts::new(parts, &bases);
Some(scan_stream(&prog, pattern, input, &stream, &stream.cursor()))
}
#[doc(hidden)]
#[must_use]
pub fn any_nfa(pattern: &Pattern, input: &[u8]) -> Option<bool> {
let prog = compile(pattern)?;
if prog.nslots > MAX_SAVE_SLOTS {
return None;
}
crate::parallel_lex::lex_significant_parts_held(input, |parts| {
let bases = Parts::bases_of(parts);
let stream = Parts::new(parts, &bases);
let absent = crate::prefilter::absent_guard_literals(pattern, input);
Some(any_serial(&prog, input, &stream.cursor(), &absent, pattern.starts_with_resume()))
})
}
pub struct Compiled(Program);
#[must_use]
pub fn compile_pattern(pattern: &Pattern) -> Option<Compiled> {
let prog = compile(pattern)?;
(prog.nslots <= MAX_SAVE_SLOTS).then_some(Compiled(prog))
}
#[must_use]
pub fn scan_nfa_over_compiled(
compiled: &Compiled,
pattern: &Pattern,
input: &[u8],
toks: &[Token],
) -> Vec<Span> {
compiled.0.clock.refresh();
let sig: Vec<usize> =
(0..toks.len()).filter(|&i| toks[i].kind != TokenKind::Whitespace).collect();
let stream = Stitched { toks, sig: &sig };
scan_stream(&compiled.0, pattern, input, &stream, &stream)
}
pub struct KindSpans<'a> {
kinds: &'a [u32],
spans: &'a [(u32, u32)],
}
impl<'a> KindSpans<'a> {
#[must_use]
pub fn new(kinds: &'a [u32], spans: &'a [(u32, u32)]) -> Self {
assert_eq!(kinds.len(), spans.len(), "one kind and one span a token");
KindSpans { kinds, spans }
}
}
impl SigStream for KindSpans<'_> {
#[inline]
fn len(&self) -> usize {
self.kinds.len()
}
#[inline]
fn kind(&self, k: usize) -> TokenKind {
TokenKind::from_code(self.kinds[k])
}
#[inline]
fn span(&self, k: usize) -> (usize, usize) {
let (a, b) = self.spans[k];
(a as usize, b as usize)
}
fn longest_run(&self, kind: TokenKind, ceiling: usize) -> Option<usize> {
let (mut run, mut best) = (0usize, 0usize);
longest_run_in(self.kinds, kind.code(), ceiling, &mut run, &mut best).then_some(best)
}
fn first_at_or_after(&self, at: usize) -> usize {
self.spans.partition_point(|&(s, _)| (s as usize) < at)
}
}
pub fn anchor_ends_into(
prog: &Compiled,
input: &[u8],
stream: &(impl SigStream + ?Sized),
lo: usize,
out: &mut [i32],
) {
let prog = &prog.0;
let np = prog.insts.len();
let keyed = prog.reads_registers;
let mut clist = ThreadList::new(np, keyed);
let mut nlist = ThreadList::new(np, keyed);
let absent = HashSet::new();
for (i, slot) in out.iter_mut().enumerate() {
let a = lo + i;
*slot = match find_leftmost(prog, input, stream, a, &absent, a + 1, &mut clist, &mut nlist) {
Some(s) if s.get(1) > s.get(0) => s.get(1) as u32 as i32,
_ => 0,
};
}
}
pub fn walk_prefix_into(
prog: &Compiled,
input: &[u8],
stream: &(impl SigStream + ?Sized),
until: usize,
out: &mut [i32],
) -> usize {
let prog = &prog.0;
let np = prog.insts.len();
let keyed = prog.reads_registers;
let mut clist = ThreadList::new(np, keyed);
let mut nlist = ThreadList::new(np, keyed);
let absent = HashSet::new();
let mut from = 0usize;
while from < until {
let Some(saves) = find_leftmost(prog, input, stream, from, &absent, until, &mut clist, &mut nlist)
else {
return from;
};
let (ks, ke) = (saves.get(0), saves.get(1));
if ke <= ks {
from = ks + 1;
continue;
}
out[ks] = ke as u32 as i32;
from = ke;
}
from
}
fn open_repeat_kind(pat: &Pattern) -> Option<(TokenKind, usize)> {
let over_one_kind = |body: &Pattern| match body {
Pattern::Atom(Atom::Kind(k)) => Some((*k, 0usize)),
_ => None,
};
match pat {
Pattern::Star(body, _) | Pattern::Plus(body, _) | Pattern::Repeat(body, _, None, _) => {
over_one_kind(body)
}
Pattern::Bind(_, _, p) | Pattern::Atomic(p) => open_repeat_kind(p),
Pattern::Concat(v) => {
let (mut open, mut fixed) = (None, 0usize);
for part in v {
if let Some(n) = bounded_max_len(part) {
fixed += n;
continue;
}
if open.is_some() {
return None;
}
open = Some(open_repeat_kind(part)?);
}
open.map(|(k, inner)| (k, fixed + inner))
}
_ => None,
}
}
fn stream_bounded_max_len<S: SigStream + ?Sized>(
pat: &Pattern,
stream: &S,
ceiling: usize,
) -> Option<usize> {
let (kind, fixed) = open_repeat_kind(pat)?;
if fixed > ceiling {
return None;
}
let best = stream.longest_run(kind, ceiling - fixed)?;
Some(fixed + best)
}
pub struct AttemptScratch {
clist: ThreadList,
nlist: ThreadList,
sig: Vec<usize>,
}
impl AttemptScratch {
#[must_use]
pub fn for_program(compiled: &Compiled) -> Self {
let np = compiled.0.insts.len();
let keyed = compiled.0.reads_registers;
AttemptScratch {
clist: ThreadList::new(np, keyed),
nlist: ThreadList::new(np, keyed),
sig: Vec::new(),
}
}
}
pub struct FlatShape {
atoms: Vec<Atom>,
slot: Vec<Option<usize>>,
reg_index: Vec<Option<usize>>,
nslots: usize,
}
impl FlatShape {
#[must_use]
pub fn len(&self) -> usize {
self.atoms.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.atoms.is_empty()
}
#[must_use]
pub fn walks_from_bytes(&self) -> bool {
self.atoms.iter().all(|a| {
matches!(
a,
Atom::Literal(_, crate::orbit::OrbitGroup::Identity)
| Atom::Kind(TokenKind::Word)
)
})
}
}
#[must_use]
pub fn flat_shape(compiled: &Compiled) -> Option<FlatShape> {
let prog = &compiled.0;
if prog.reads_registers || prog.nslots > MAX_SAVE_SLOTS {
return None;
}
let insts = &prog.insts;
if !matches!(insts.first(), Some(Inst::Save(0))) {
return None;
}
let (mut atoms, mut slot) = (Vec::new(), Vec::new());
let mut i = 1usize;
loop {
match insts.get(i)? {
Inst::Save(1) => break,
Inst::Save(base) => {
let base = *base;
let Some(Inst::Atom(a)) = insts.get(i + 1) else { return None };
if !matches!(insts.get(i + 2), Some(Inst::Save(c)) if *c == base + 1) {
return None;
}
atoms.push(a.clone());
slot.push(Some(base));
i += 3;
}
Inst::Atom(a) => {
atoms.push(a.clone());
slot.push(None);
i += 1;
}
_ => return None,
}
}
if !matches!(insts.get(i + 1), Some(Inst::Match)) {
return None;
}
if atoms.is_empty() {
return None;
}
let order: Vec<usize> = prog.slots.values().copied().collect();
let reg_index: Vec<Option<usize>> =
slot.iter().map(|s| s.and_then(|b| order.iter().position(|&o| o == b))).collect();
Some(FlatShape { atoms, slot, reg_index, nslots: prog.nslots })
}
fn flat_accepts(
compiled: &Compiled,
shape: &FlatShape,
input: &[u8],
toks: &[Token],
sig: &mut Vec<usize>,
) -> bool {
sig.clear();
sig.extend((0..toks.len()).filter(|&i| toks[i].kind != TokenKind::Whitespace));
if sig.len() < shape.atoms.len() {
return false;
}
let stream = Stitched { toks, sig };
shape
.atoms
.iter()
.enumerate()
.all(|(k, a)| atom_matches(a, &compiled.0, input, &stream, k, &[]))
}
#[must_use]
pub fn flat_match_at_first_token(
compiled: &Compiled,
shape: &FlatShape,
input: &[u8],
toks: &[Token],
scratch: &mut AttemptScratch,
) -> Option<Span> {
let AttemptScratch { sig, .. } = scratch;
if !flat_accepts(compiled, shape, input, toks, sig) {
return None;
}
Some(span_between(&Stitched { toks, sig }, 0, shape.atoms.len()))
}
#[must_use]
pub fn names_of(compiled: &Compiled) -> std::sync::Arc<[String]> {
compiled.0.names.clone()
}
pub const MAX_FLAT_REGISTERS: usize = (MAX_SAVE_SLOTS - 2) / 2;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FlatMatch {
pub span: Span,
pub regs: [Span; MAX_FLAT_REGISTERS],
}
impl FlatShape {
#[must_use]
pub fn regs_from_bytes(&self, input: &[u8], span: Span) -> Option<FlatMatch> {
let (lo, hi) = (span.start(), span.end());
if hi > input.len() {
return None;
}
let mut out =
FlatMatch { span, regs: [Span { start: 0, end: 0 }; MAX_FLAT_REGISTERS] };
let mut p = lo;
let skip = |p: &mut usize| {
while *p < hi && input[*p].is_ascii_whitespace() {
*p += 1;
}
};
for (k, atom) in self.atoms.iter().enumerate() {
skip(&mut p);
let start = p;
match atom {
Atom::Literal(lit, crate::orbit::OrbitGroup::Identity) => {
let lit = lit.as_bytes();
if !input[p..hi].starts_with(lit) {
return None;
}
p += lit.len();
}
Atom::Kind(TokenKind::Word) => {
while p < hi && (input[p].is_ascii_alphanumeric() || input[p] == b'_') {
p += 1;
}
if p == start {
return None;
}
}
_ => return None,
}
if let Some(i) = self.reg_index[k] {
out.regs[i] = Span { start: start as u32, end: p as u32 };
}
}
skip(&mut p);
if p != hi {
return None;
}
Some(out)
}
}
#[must_use]
pub fn flat_regs_at_first_token(
compiled: &Compiled,
shape: &FlatShape,
input: &[u8],
toks: &[Token],
scratch: &mut AttemptScratch,
) -> Option<FlatMatch> {
let AttemptScratch { sig, .. } = scratch;
if !flat_accepts(compiled, shape, input, toks, sig) {
return None;
}
let n = shape.atoms.len();
let stream = Stitched { toks, sig };
let mut out =
FlatMatch { span: span_between(&stream, 0, n), regs: [Span { start: 0, end: 0 }; MAX_FLAT_REGISTERS] };
for (k, idx) in shape.reg_index.iter().enumerate() {
if let Some(i) = *idx {
out.regs[i] = span_between(&stream, k, k + 1);
}
}
Some(out)
}
#[must_use]
pub fn flat_captures_at_first_token(
compiled: &Compiled,
shape: &FlatShape,
input: &[u8],
toks: &[Token],
scratch: &mut AttemptScratch,
) -> Option<Match> {
let AttemptScratch { sig, .. } = scratch;
if !flat_accepts(compiled, shape, input, toks, sig) {
return None;
}
let n = shape.atoms.len();
let mut saves = [usize::MAX; MAX_SAVE_SLOTS];
saves[0] = 0;
saves[1] = n;
for (k, base) in shape.slot.iter().enumerate() {
if let Some(base) = *base {
saves[base] = k;
saves[base + 1] = k + 1;
}
}
let stream = Stitched { toks, sig };
let span = span_between(&stream, 0, n);
Some(Match::bound(
span.start(),
span.end(),
resolve_captures(&compiled.0, &saves[..shape.nslots], &stream),
register_names(&compiled.0),
))
}
#[cfg(test)]
mod flat_shape_tests {
use super::{
AttemptScratch, Stitched, captures_at_first_token, compile_pattern, flat_captures_at_first_token,
flat_match_at_first_token, flat_shape, match_at_first_token,
};
#[test]
fn the_flat_attempt_answers_what_the_simulation_answers() {
let mut text = String::new();
for i in 0..120u32 {
text.push_str(&format!("let value_{i} = {} ; call_{i}(alpha, beta) ;\n", i * 37));
}
let input = text.as_bytes();
let toks = crate::lexer::lex(input);
let absent = std::collections::HashSet::new();
for (src, flat) in [
("\"let\" \\W \"=\"", true),
("\"let\" \\W:v \"=\"", true),
("\\W:name \"=\"", true),
("\"alpha\"", true),
("\\W \\W", true),
("(\"let\" | \"call\")", false),
("\"let\" \\W*", false),
("\\W:v \"=\" =v", false),
] {
let p = crate::parser::parse(src).expect("parses");
let compiled = compile_pattern(&p).expect("compiles");
let shape = flat_shape(&compiled);
assert_eq!(shape.is_some(), flat, "{src}");
let Some(shape) = shape else { continue };
let mut sim = AttemptScratch::for_program(&compiled);
let mut fast = AttemptScratch::for_program(&compiled);
let mut answered = 0usize;
for i in 0..toks.len() {
let cut = &toks[i..];
let want = match_at_first_token(&compiled, input, cut, &mut sim, &absent);
let got = flat_match_at_first_token(&compiled, &shape, input, cut, &mut fast);
assert_eq!(got, want, "{src} anchored at token {i}");
let want = captures_at_first_token(&compiled, input, cut, &mut sim, &absent);
let got = flat_captures_at_first_token(&compiled, &shape, input, cut, &mut fast);
let seen = |m: &Option<super::Match>| {
m.as_ref().map(|m| (m.start, m.end, m.captures().to_vec(), m.names().to_vec()))
};
assert_eq!(seen(&got), seen(&want), "{src} anchored at token {i}");
answered += usize::from(want.is_some());
}
assert!(answered > 50, "{src} matched at {answered} of {} anchors", toks.len());
}
}
#[test]
fn a_pattern_bounded_by_the_stream_answers_what_the_sweep_answers() {
use super::{bounded_max_len, open_repeat_kind, scan_nfa, scan_nfa_over_serial};
let mut short = String::new();
for i in 0..2_000u32 {
short.push_str(&format!("let value_{i} = {i} ; call_{i}(alpha beta) ;\n"));
}
let mut long = short.clone();
long.push_str("let ");
for i in 0..4_000u32 {
long.push_str(&format!("w{i} "));
}
long.push_str("= 1 ;\n");
for src in ["\"let\" \\W* \"=\"", "\"let\" \\W+ \"=\"", "\\W* \"=\"", "\"let\" \\W{1,}"] {
let p = crate::parser::parse(src).expect("parses");
assert!(bounded_max_len(&p).is_none(), "{src} carries no bound of its own");
assert!(open_repeat_kind(&p).is_some(), "{src} is unbounded only by a repeat of one kind");
for text in [&short, &long] {
let input = text.as_bytes();
let toks = crate::lexer::lex(input);
assert_eq!(
scan_nfa(&p, input).expect("the single-pass engine takes this"),
scan_nfa_over_serial(&p, input, &toks).expect("and so does the sweep"),
"{src} over {} bytes",
input.len()
);
}
}
for src in ["\\W* \"=\" \\N*", "(\\W | \\N)* \"=\"", "\"let\" .* \"=\""] {
let p = crate::parser::parse(src).expect("parses");
assert!(open_repeat_kind(&p).is_none(), "{src} has no one kind to bound");
}
}
#[test]
fn a_window_too_short_for_the_shape_matches_nothing() {
let input = b"let a = 1 ;";
let toks = crate::lexer::lex(input);
let absent = std::collections::HashSet::new();
let p = crate::parser::parse("\"let\" \\W:v \"=\"").expect("parses");
let compiled = compile_pattern(&p).expect("compiles");
let shape = flat_shape(&compiled).expect("the program is flat");
let sig = Stitched::significant_of(&toks);
let cut = &toks[..sig[2]];
let mut sim = AttemptScratch::for_program(&compiled);
let mut fast = AttemptScratch::for_program(&compiled);
assert_eq!(match_at_first_token(&compiled, input, cut, &mut sim, &absent), None);
assert_eq!(flat_match_at_first_token(&compiled, &shape, input, cut, &mut fast), None);
}
}
#[must_use]
pub fn match_at_first_token(
compiled: &Compiled,
input: &[u8],
toks: &[Token],
scratch: &mut AttemptScratch,
absent: &HashSet<Vec<u8>>,
) -> Option<Span> {
let AttemptScratch { clist, nlist, sig } = scratch;
let saves = attempt_at_first_token(compiled, input, toks, clist, nlist, sig, absent)?;
let stream = Stitched { toks, sig };
Some(span_between(&stream, saves.get(0), saves.get(1)))
}
#[must_use]
pub fn captures_at_first_token(
compiled: &Compiled,
input: &[u8],
toks: &[Token],
scratch: &mut AttemptScratch,
absent: &HashSet<Vec<u8>>,
) -> Option<Match> {
let AttemptScratch { clist, nlist, sig } = scratch;
let saves = attempt_at_first_token(compiled, input, toks, clist, nlist, sig, absent)?;
let stream = Stitched { toks, sig };
let span = span_between(&stream, saves.get(0), saves.get(1));
Some(Match::bound(
span.start(),
span.end(),
resolve_captures(&compiled.0, saves.as_slice(), &stream),
register_names(&compiled.0),
))
}
fn attempt_at_first_token(
compiled: &Compiled,
input: &[u8],
toks: &[Token],
clist: &mut ThreadList,
nlist: &mut ThreadList,
sig: &mut Vec<usize>,
absent: &HashSet<Vec<u8>>,
) -> Option<Saves> {
sig.clear();
sig.extend((0..toks.len()).filter(|&i| toks[i].kind != TokenKind::Whitespace));
if sig.is_empty() {
return None;
}
let stream = Stitched { toks, sig };
let saves = find_leftmost(&compiled.0, input, &stream, 0, absent, 1, clist, nlist)?;
(saves.get(1) > saves.get(0)).then_some(saves)
}
fn scan_stream<P: LeafRead + ?Sized, W: SigStream + ?Sized>(
prog: &Program,
pattern: &Pattern,
input: &[u8],
shared: &P,
walked: &W,
) -> Vec<Span> {
let m = shared.len();
let guarding = crate::trace::phase("the single pass: the absent guards");
let absent = crate::prefilter::absent_guard_literals(pattern, input);
drop(guarding);
let contiguous = pattern.starts_with_resume();
let bounding = crate::trace::phase("the single pass: the bound on a match");
let bound = bounded_max_len(pattern)
.or_else(|| stream_bounded_max_len(pattern, walked, PARALLEL_SCAN_MAX_MATCH_LEN));
drop(bounding);
let parallel = !contiguous
&& !pattern.mentions_reset_start()
&& m >= PARALLEL_SCAN_MIN_ANCHORS
&& matches!(bound, Some(1..=PARALLEL_SCAN_MAX_MATCH_LEN));
if parallel {
crate::trace::rung("scan", "an attempt an anchor, across the cores", input.len());
scan_nfa_parallel(prog, input, shared, &absent)
} else {
crate::trace::rung("scan", "one serial sweep of the stream", input.len());
let _sweeping = crate::trace::phase("the single pass: one serial sweep");
scan_nfa_serial(prog, input, walked, &absent, contiguous)
}
}
#[must_use]
pub fn scan_nfa_over_serial(pattern: &Pattern, input: &[u8], toks: &[Token]) -> Option<Vec<Span>> {
scan_nfa_over_serial_from(pattern, input, toks, 0)
}
#[must_use]
pub fn shortest_end(pattern: &Pattern, input: &[u8], toks: &[Token], from: usize) -> Option<usize> {
let prog = compile(pattern)?;
if prog.nslots > MAX_SAVE_SLOTS {
return None;
}
let sig: Vec<usize> = (from.min(toks.len())..toks.len())
.filter(|&i| toks[i].kind != TokenKind::Whitespace)
.collect();
let stream = Stitched { toks, sig: &sig };
shortest_end_over(&prog, pattern, input, &stream, 0)
}
#[must_use]
pub fn shortest_end_from_byte(pattern: &Pattern, input: &[u8], at: usize) -> Option<usize> {
let prog = compile(pattern)?;
if prog.nslots > MAX_SAVE_SLOTS {
return None;
}
let stream = OwnedStream::over(input);
let k = first_at_or_after(&stream, at);
shortest_end_over(&prog, pattern, input, &stream, k)
}
#[must_use]
pub fn shortest_end_over_compiled(
compiled: &Compiled,
pattern: &Pattern,
input: &[u8],
toks: &[Token],
from: usize,
) -> Option<usize> {
let sig: Vec<usize> = (from.min(toks.len())..toks.len())
.filter(|&i| toks[i].kind != TokenKind::Whitespace)
.collect();
let stream = Stitched { toks, sig: &sig };
shortest_end_over(&compiled.0, pattern, input, &stream, 0)
}
fn shortest_end_over<S: SigStream + ?Sized>(
prog: &Program,
pattern: &Pattern,
input: &[u8],
stream: &S,
k: usize,
) -> Option<usize> {
let absent = crate::prefilter::absent_guard_literals(pattern, input);
let m = stream.len();
let keyed = prog.reads_registers;
let mut clist = ThreadList::new(prog.insts.len(), keyed);
let mut nlist = ThreadList::new(prog.insts.len(), keyed);
let saves = find_leftmost_ending(
prog, input, stream, k, &absent, m + 1, &mut clist, &mut nlist, true,
)?;
let (ks, ke) = (saves.get(0), saves.get(1));
if ke > ks && ke <= m {
return Some(stream.span(ke - 1).1);
}
Some(if ks < m { stream.span(ks).0 } else { input.len() })
}
fn first_at_or_after<S: SigStream + ?Sized>(stream: &S, at: usize) -> usize {
stream.first_at_or_after(at)
}
#[must_use]
pub fn scan_nfa_over_serial_from(
pattern: &Pattern,
input: &[u8],
toks: &[Token],
from: usize,
) -> Option<Vec<Span>> {
let compiling = crate::trace::phase("the resumed walk: compiling the program");
let prog = compile(pattern)?;
drop(compiling);
if prog.nslots > MAX_SAVE_SLOTS {
return None;
}
let gathering = crate::trace::phase("the resumed walk: its significant indices");
let sig: Vec<usize> = (from.min(toks.len())..toks.len())
.filter(|&i| toks[i].kind != TokenKind::Whitespace)
.collect();
drop(gathering);
let guarding = crate::trace::phase("the resumed walk: its absent guards");
let absent = crate::prefilter::absent_guard_literals(pattern, input);
drop(guarding);
let simulating = crate::trace::phase("the resumed walk: the simulation");
let stream = Stitched { toks, sig: &sig };
let found = scan_nfa_serial(&prog, input, &stream, &absent, pattern.starts_with_resume());
drop(simulating);
Some(found)
}
#[must_use]
pub fn scan_nfa_over_serial_from_compiled(
compiled: &Compiled,
pattern: &Pattern,
input: &[u8],
toks: &[Token],
from: usize,
) -> Vec<Span> {
compiled.0.clock.refresh();
let sig: Vec<usize> = (from.min(toks.len())..toks.len())
.filter(|&i| toks[i].kind != TokenKind::Whitespace)
.collect();
let absent = crate::prefilter::absent_guard_literals(pattern, input);
let stream = Stitched { toks, sig: &sig };
scan_nfa_serial(&compiled.0, input, &stream, &absent, pattern.starts_with_resume())
}
fn walk_serial<S: SigStream + ?Sized>(
prog: &Program,
input: &[u8],
stream: &S,
absent: &HashSet<Vec<u8>>,
contiguous: bool,
mut emit: impl FnMut(usize, usize, Saves) -> bool,
) {
let np = prog.insts.len();
let keyed = prog.reads_registers;
let mut clist = ThreadList::new(np, keyed);
let mut nlist = ThreadList::new(np, keyed);
let mut from = 0;
while let Some((ks, ke, saves)) =
step_serial(prog, input, stream, absent, contiguous, &mut from, &mut clist, &mut nlist)
{
if !emit(ks, ke, saves) {
break;
}
}
}
#[allow(clippy::too_many_arguments)]
fn step_serial<S: SigStream + ?Sized>(
prog: &Program,
input: &[u8],
stream: &S,
absent: &HashSet<Vec<u8>>,
contiguous: bool,
from: &mut usize,
clist: &mut ThreadList,
nlist: &mut ThreadList,
) -> Option<(usize, usize, Saves)> {
let m = stream.len();
while *from <= m {
let Some(saves) = find_leftmost(prog, input, stream, *from, absent, m + 1, clist, nlist)
else {
*from = m + 1;
return None;
};
let ks = saves.get(0);
if contiguous && ks != *from {
return None;
}
let ke = saves.get(1);
if ke > ks {
*from = ke;
return Some((ks, ke, saves));
}
*from = ks + 1;
}
None
}
pub(crate) struct SerialWalk<S: Shardable> {
prog: Program,
stream: S,
absent: HashSet<Vec<u8>>,
contiguous: bool,
from: usize,
clist: ThreadList,
nlist: ThreadList,
sharded: bool,
window: usize,
batch: Vec<(usize, usize)>,
taken: usize,
handed: usize,
ends: Vec<u32>,
saves: Vec<Saves>,
}
impl SerialWalk<OwnedStream> {
pub(crate) fn over(pattern: &Pattern, input: &[u8]) -> Option<Self> {
let prog = compile(pattern)?;
if prog.nslots > MAX_SAVE_SLOTS {
return None;
}
Some(Self::with_stream(prog, pattern, input, OwnedStream::over(input)))
}
pub(crate) fn any_match(pattern: &Pattern, input: &[u8]) -> Option<bool> {
let mut walk = Self::over(pattern, input)?;
walk.handed = 1;
Some(walk.next_span(input).is_some())
}
}
impl<S: Shardable> SerialWalk<S> {
pub(crate) fn over_stream(pattern: &Pattern, input: &[u8], stream: S) -> Option<Self> {
let prog = compile(pattern)?;
if prog.nslots > MAX_SAVE_SLOTS {
return None;
}
Some(Self::with_stream(prog, pattern, input, stream))
}
fn with_stream(prog: Program, pattern: &Pattern, input: &[u8], stream: S) -> Self {
let np = prog.insts.len();
let keyed = prog.reads_registers;
SerialWalk {
absent: crate::prefilter::absent_guard_literals(pattern, input),
contiguous: pattern.starts_with_resume(),
sharded: !pattern.starts_with_resume()
&& !pattern.mentions_reset_start()
&& matches!(bounded_max_len(pattern), Some(1..=PARALLEL_SCAN_MAX_MATCH_LEN)),
prog,
stream,
from: 0,
clist: ThreadList::new(np, keyed),
nlist: ThreadList::new(np, keyed),
window: PARALLEL_SCAN_MIN_ANCHORS,
batch: Vec::new(),
taken: 0,
handed: 0,
ends: Vec::new(),
saves: Vec::new(),
}
}
fn next_batched(&mut self, input: &[u8]) -> Option<(usize, usize)> {
if self.handed == 0 {
return None;
}
while self.taken == self.batch.len() {
if !self.fill_window(input) {
return None;
}
}
let out = self.batch[self.taken];
self.taken += 1;
self.handed += 1;
Some(out)
}
fn fill_window(&mut self, input: &[u8]) -> bool {
if !self.sharded {
return false;
}
let Self { prog, stream, absent, from, window, batch, taken, ends, saves, .. } = self;
let Some(shard) = stream.shard() else {
return false;
};
let m = shard.len();
if *from >= m || m - *from < PARALLEL_SCAN_MIN_ANCHORS {
return false;
}
let win = (*window).min(m - *from);
ends.clear();
ends.resize(win, 0);
anchor_ends_across(&*prog, input, &shard, &*absent, *from, ends);
batch.clear();
saves.clear();
*taken = 0;
let mut at = *from;
while at < *from + win {
let ke = ends[at - *from] as usize;
if ke > at {
batch.push((at, ke));
at = ke;
} else {
at += 1;
}
}
*from = at;
*window = window.saturating_mul(2);
true
}
fn next_bounds(&mut self, input: &[u8]) -> Option<(usize, usize)> {
if let Some(bounds) = self.next_batched(input) {
return Some(bounds);
}
let Self { prog, stream, absent, contiguous, from, clist, nlist, handed, .. } = self;
let (ks, ke, _) =
step_serial(&*prog, input, &*stream, &*absent, *contiguous, from, clist, nlist)?;
*handed += 1;
Some((ks, ke))
}
fn fill_batch_saves(&mut self, input: &[u8]) {
if self.saves.len() == self.batch.len() {
return;
}
let Self { prog, stream, absent, batch, saves, .. } = self;
let shard = stream.shard().expect("a window is filled only from a stream that shards");
saves.resize(batch.len(), Saves::unset(prog.nslots));
anchor_saves_across(
&*prog,
input,
&shard,
&*absent,
batch.as_slice(),
saves.as_mut_slice(),
);
}
fn next_saves(&mut self, input: &[u8]) -> Option<(usize, usize, Saves)> {
if let Some((ks, ke)) = self.next_batched(input) {
self.fill_batch_saves(input);
return Some((ks, ke, self.saves[self.taken - 1]));
}
let Self { prog, stream, absent, contiguous, from, clist, nlist, handed, .. } = self;
let found =
step_serial(&*prog, input, &*stream, &*absent, *contiguous, from, clist, nlist)?;
*handed += 1;
Some(found)
}
pub(crate) fn next_span(&mut self, input: &[u8]) -> Option<Span> {
let (ks, ke) = self.next_bounds(input)?;
Some(span_between(&self.stream, ks, ke))
}
pub(crate) fn next_match(&mut self, input: &[u8]) -> Option<Match> {
let (ks, ke, saves) = self.next_saves(input)?;
let span = span_between(&self.stream, ks, ke);
Some(Match::bound(
span.start(),
span.end(),
resolve_captures(&self.prog, saves.as_slice(), &self.stream),
register_names(&self.prog),
))
}
pub(crate) fn next_into(
&mut self,
input: &[u8],
order: &[String],
spans: &mut [Option<Span>],
extents: &mut [Option<(usize, usize)>],
) -> Option<(Span, usize, usize)> {
let (ks, ke, saves) = self.next_saves(input)?;
resolve_captures_into(&self.prog, saves.as_slice(), &self.stream, order, spans, extents);
Some((span_between(&self.stream, ks, ke), ks, ke))
}
pub(crate) fn next_span_and_extent(&mut self, input: &[u8]) -> Option<(Span, usize)> {
let (ks, ke) = self.next_bounds(input)?;
Some((span_between(&self.stream, ks, ke), ke - ks))
}
pub(crate) fn seek(&mut self, at: usize) {
self.from = first_at_or_after(&self.stream, at);
self.batch.clear();
self.saves.clear();
self.taken = 0;
self.handed = 0;
self.window = PARALLEL_SCAN_MIN_ANCHORS;
}
}
fn scan_nfa_serial<S: SigStream + ?Sized>(
prog: &Program,
input: &[u8],
stream: &S,
absent: &HashSet<Vec<u8>>,
contiguous: bool,
) -> Vec<Span> {
let mut matches = Vec::new();
walk_serial(prog, input, stream, absent, contiguous, |ks, ke, _| {
matches.push(span_between(stream, ks, ke));
true
});
matches
}
fn any_serial<S: SigStream + ?Sized>(
prog: &Program,
input: &[u8],
stream: &S,
absent: &HashSet<Vec<u8>>,
contiguous: bool,
) -> bool {
let mut found = false;
walk_serial(prog, input, stream, absent, contiguous, |_, _, _| {
found = true;
false
});
found
}
fn scan_nfa_parallel<S: LeafRead + ?Sized>(
prog: &Program,
input: &[u8],
stream: &S,
absent: &HashSet<Vec<u8>>,
) -> Vec<Span> {
let mut ends = HELD_ENDS.take();
ends.clear();
ends.resize(stream.len(), 0);
let attempting = crate::trace::phase("the single pass: an attempt an anchor");
anchor_ends_across(prog, input, stream, absent, 0, &mut ends);
drop(attempting);
let selecting = crate::trace::phase("the single pass: the leftmost selection");
let matches = select_ends(&stream.leaf(), &ends);
drop(selecting);
HELD_ENDS.set(ends);
matches
}
thread_local! {
static HELD_ENDS: std::cell::Cell<Vec<u32>> = const { std::cell::Cell::new(Vec::new()) };
}
fn anchor_ends_across<S: LeafRead + ?Sized>(
prog: &Program,
input: &[u8],
stream: &S,
absent: &HashSet<Vec<u8>>,
base: usize,
ends: &mut [u32],
) {
use flynnel::JobPlan;
use flynnel::sched::par_iter::for_each_chunk_indexed_min_leaf;
let opens = opening_atoms(prog);
let opens = opens.as_deref();
if let (Some(opens), Some(lits)) = (opens, opens.and_then(opening_literals)) {
anchor_ends_at_literal_hits(prog, input, stream, absent, base, ends, opens, &lits);
return;
}
let cores = std::thread::available_parallelism().map_or(1, std::num::NonZero::get);
let min_leaf = ends.len().div_ceil(cores * 4).max(64);
let plan = JobPlan::new(0, ends.len() as u32)
.with_leaf_shape(flynnel::LeafShape::PortCompute);
for_each_chunk_indexed_min_leaf(&plan, ends, min_leaf, |start, slots| {
let np = prog.insts.len();
let keyed = prog.reads_registers;
let mut clist = ThreadList::new(np, keyed);
let mut nlist = ThreadList::new(np, keyed);
let read = stream.leaf();
for (i, slot) in slots.iter_mut().enumerate() {
let a = base + start + i;
if opens.is_some_and(|o| !opening_admits(prog, o, input, &read, a)) {
*slot = 0;
continue;
}
*slot = match find_leftmost(prog, input, &read, a, absent, a + 1, &mut clist, &mut nlist) {
Some(s) if s.get(1) > s.get(0) => s.get(1) as u32,
_ => 0,
};
}
});
}
#[allow(clippy::too_many_arguments)]
fn anchor_ends_at_literal_hits<S: LeafRead + ?Sized>(
prog: &Program,
input: &[u8],
stream: &S,
absent: &HashSet<Vec<u8>>,
base: usize,
ends: &mut [u32],
opens: &[&Atom],
lits: &[&[u8]],
) {
use flynnel::JobPlan;
use flynnel::sched::par_iter::for_each_chunk_indexed_min_leaf;
let last = (base + ends.len()).min(stream.len());
if base >= last {
return;
}
let searching = crate::trace::phase("the single pass: the search for the opening literals");
let read = stream.leaf();
let lo = read.span(base).0;
let hi = read.span(last - 1).1;
let mut hits: Vec<usize> = Vec::new();
for lit in lits {
hits.extend(crate::byte_simd::find_all(&input[lo..hi], lit).into_iter().map(|h| h + lo));
}
hits.sort_unstable();
hits.dedup();
drop(searching);
crate::trace::counted("the single pass: hits of the opening literals", hits.len() as u64);
let attempting = crate::trace::phase("the single pass: the attempts at the hits");
let mut found: Vec<Option<(u32, u32)>> = vec![None; hits.len()];
let cores = std::thread::available_parallelism().map_or(1, std::num::NonZero::get);
let min_leaf = found.len().div_ceil(cores * 4).max(64);
let plan = JobPlan::new(0, found.len() as u32)
.with_leaf_shape(flynnel::LeafShape::PortCompute);
for_each_chunk_indexed_min_leaf(&plan, &mut found, min_leaf, |start, slots| {
let np = prog.insts.len();
let keyed = prog.reads_registers;
let mut clist = ThreadList::new(np, keyed);
let mut nlist = ThreadList::new(np, keyed);
let read = stream.leaf();
for (i, slot) in slots.iter_mut().enumerate() {
let h = hits[start + i];
let a = first_at_or_after(&read, h);
if a < base || a >= last || read.span(a).0 != h || !opening_admits(prog, opens, input, &read, a) {
continue;
}
let end = match find_leftmost(prog, input, &read, a, absent, a + 1, &mut clist, &mut nlist) {
Some(s) if s.get(1) > s.get(0) => s.get(1) as u32,
_ => 0,
};
*slot = Some((a as u32, end));
}
});
drop(attempting);
let mut admitted = 0u64;
for slot in found.iter().flatten() {
let (a, end) = *slot;
admitted += 1;
if end != 0 {
ends[a as usize - base] = end;
}
}
crate::trace::counted("the single pass: anchors the hits admit", admitted);
}
fn anchor_saves_across<S: LeafRead + ?Sized>(
prog: &Program,
input: &[u8],
stream: &S,
absent: &HashSet<Vec<u8>>,
matches: &[(usize, usize)],
saves: &mut [Saves],
) {
use flynnel::JobPlan;
use flynnel::sched::par_iter::for_each_chunk_indexed_min_leaf;
let cores = std::thread::available_parallelism().map_or(1, std::num::NonZero::get);
let min_leaf = saves.len().div_ceil(cores * 4).max(64);
let plan = JobPlan::new(0, saves.len() as u32)
.with_leaf_shape(flynnel::LeafShape::PortCompute);
for_each_chunk_indexed_min_leaf(&plan, saves, min_leaf, |start, slots| {
let np = prog.insts.len();
let keyed = prog.reads_registers;
let mut clist = ThreadList::new(np, keyed);
let mut nlist = ThreadList::new(np, keyed);
let read = stream.leaf();
for (i, slot) in slots.iter_mut().enumerate() {
let a = matches[start + i].0;
*slot = find_leftmost(prog, input, &read, a, absent, a + 1, &mut clist, &mut nlist)
.expect("the window's table was filled by this attempt at this anchor");
}
});
}
fn select_ends<S: SigStream + ?Sized>(stream: &S, ends: &[u32]) -> Vec<Span> {
let m = stream.len();
let mut count = 0usize;
selected_anchors(ends, m, |_, _| count += 1);
let mut matches = Vec::with_capacity(count);
selected_anchors(ends, m, |a, ke| matches.push(span_between(stream, a, ke)));
matches
}
fn selected_anchors(ends: &[u32], m: usize, mut each: impl FnMut(usize, usize)) {
let m = m.min(ends.len());
let mut from = 0usize;
while from < m {
let Some(step) = ends[from..m].iter().position(|&e| e != 0) else {
return;
};
let at = from + step;
let ke = ends[at] as usize;
debug_assert!(ke > at, "a non-empty match at {at} ends past it, not at {ke}");
each(at, ke);
from = ke;
}
}
pub(crate) fn captures_over(
pattern: &Pattern,
input: &[u8],
toks: &[Token],
spans: &[Span],
) -> Option<Vec<Match>> {
let prog = compile(pattern)?;
if prog.nslots > MAX_SAVE_SLOTS {
return None;
}
let sig: Vec<usize> =
(0..toks.len()).filter(|&i| toks[i].kind != TokenKind::Whitespace).collect();
let stream = Stitched { toks, sig: &sig };
Some(captures_over_stream(&prog, pattern, input, &stream, &stream, spans))
}
pub(crate) fn captures_over_parts(
pattern: &Pattern,
input: &[u8],
spans: &[Span],
) -> Option<Vec<Match>> {
let prog = compile(pattern)?;
if prog.nslots > MAX_SAVE_SLOTS {
return None;
}
let owned = OwnedStream::over(input);
Some(captures_over_stream(&prog, pattern, input, &owned.parts_view(), &owned, spans))
}
fn captures_over_stream<P: LeafRead + ?Sized, W: SigStream + ?Sized>(
prog: &Program,
pattern: &Pattern,
input: &[u8],
shared: &P,
walked: &W,
spans: &[Span],
) -> Vec<Match> {
let absent = crate::prefilter::absent_guard_literals(pattern, input);
if pattern.mentions_reset_start() {
let mut out = Vec::with_capacity(spans.len());
walk_serial(prog, input, walked, &absent, pattern.starts_with_resume(), |ks, ke, saves| {
let span = span_between(walked, ks, ke);
if spans.get(out.len()) == Some(&span) {
out.push(Match::bound(
span.start(),
span.end(),
resolve_captures(prog, saves.as_slice(), walked),
register_names(prog),
));
}
true
});
assert_eq!(out.len(), spans.len(), "every span is a match of this pattern over this input");
return out;
}
use flynnel::JobPlan;
use flynnel::sched::par_iter::for_each_chunk_indexed_min_leaf;
let mut out: Vec<Match> = spans
.iter()
.map(|s| Match::plain(s.start(), s.end()))
.collect();
let cores = std::thread::available_parallelism().map_or(1, std::num::NonZero::get);
let min_leaf = out.len().div_ceil(cores * 4).max(16);
let plan = JobPlan::new(0, out.len() as u32)
.with_leaf_shape(flynnel::LeafShape::PortCompute);
for_each_chunk_indexed_min_leaf(&plan, &mut out, min_leaf, |start, slots| {
let np = prog.insts.len();
let keyed = prog.reads_registers;
let mut clist = ThreadList::new(np, keyed);
let mut nlist = ThreadList::new(np, keyed);
let read = shared.leaf();
for (i, slot) in slots.iter_mut().enumerate() {
let span = spans[start + i];
let a = first_at_or_after(&read, span.start());
let saves = (a < read.len())
.then(|| find_leftmost(prog, input, &read, a, &absent, a + 1, &mut clist, &mut nlist))
.flatten()
.filter(|s| s.get(1) > a && read.span(s.get(1) - 1).1 == span.end())
.expect("a span a scan of this pattern returned is reproduced by the attempt at its start");
*slot = Match::bound(
slot.start,
slot.end,
resolve_captures(prog, saves.as_slice(), &read),
register_names(prog),
);
}
});
out
}
#[cfg(feature = "gpu")]
pub(crate) struct GpuNfa {
pub atom_kind: Vec<u32>,
pub next_closure: Vec<u64>,
pub is_atom: Vec<u32>,
pub mag_lo: Vec<f32>,
pub mag_hi: Vec<f32>,
pub reads_magnitude: bool,
pub ref_back: Vec<i32>,
pub class_group: Option<crate::orbit::OrbitGroup>,
pub binds_registers: bool,
pub need_mask: Vec<u32>,
pub reads_bytes: bool,
pub literals: Vec<Option<Vec<u8>>>,
pub ent_lo: Vec<f32>,
pub ent_hi: Vec<f32>,
pub period_need: Vec<u32>,
pub period_val: Vec<u32>,
pub texture_need: Vec<u32>,
pub onset_need: Vec<u32>,
pub reads_spectral: bool,
pub start_closure: u64,
pub match_mask: u64,
pub nstates: usize,
}
pub(crate) fn texture_id(t: crate::spectral::Texture) -> u32 {
use crate::spectral::Texture;
match t {
Texture::Prose => 0,
Texture::Code => 1,
Texture::Math => 2,
Texture::Data => 3,
Texture::Mixed => 4,
}
}
#[cfg(feature = "gpu")]
fn spec_texture_id(t: crate::ast::SpecTexture) -> u32 {
use crate::ast::SpecTexture;
use crate::spectral::Texture;
texture_id(match t {
SpecTexture::Prose => Texture::Prose,
SpecTexture::Code => Texture::Code,
SpecTexture::Math => Texture::Math,
SpecTexture::Data => Texture::Data,
})
}
#[cfg(feature = "gpu")]
fn magnitude_bounds(pred: &crate::ast::MagPred) -> Option<(f32, f32)> {
use crate::ast::MagPred;
match pred {
MagPred::Ge(centi) => Some((*centi as f32, f32::INFINITY)),
MagPred::Le(centi) => Some((f32::NEG_INFINITY, *centi as f32)),
MagPred::Above(..) | MagPred::Below(..) => None,
}
}
pub(crate) fn byte_class_bit(bc: crate::ast::ByteClass) -> Option<u32> {
use crate::ast::ByteClass;
match bc {
ByteClass::Digit => Some(1),
ByteClass::Word => Some(2),
ByteClass::Hex => Some(4),
ByteClass::Alpha => Some(8),
ByteClass::Upper => Some(16),
ByteClass::Lower => Some(32),
ByteClass::Space => None,
}
}
fn fixed_bind_offsets(prog: &Program) -> Option<(Vec<i32>, Option<crate::orbit::OrbitGroup>)> {
use std::collections::BTreeSet;
const FAR: u8 = 65;
let n = prog.insts.len();
let mut offsets = vec![0i32; n];
let mut group = None;
let mut refs: Vec<(usize, usize)> = Vec::new();
for (pc, inst) in prog.insts.iter().enumerate() {
let g = match inst {
Inst::Atom(Atom::RegisterEq(name, g)) => {
refs.push((pc, *prog.slots.get(name)?));
g
}
Inst::Atom(Atom::Literal(_, g)) => g,
_ => continue,
};
match group {
None => group = Some(*g),
Some(seen) if seen == *g => {}
Some(_) => return None,
}
}
let bases: BTreeSet<usize> = refs.iter().map(|&(_, base)| base).collect();
for base in bases {
let mut reached: Vec<BTreeSet<(u8, u8)>> = vec![BTreeSet::new(); n];
let mut work = vec![(0usize, (FAR, FAR))];
while let Some((pc, (since, width))) = work.pop() {
if pc >= n || !reached[pc].insert((since, width)) {
continue;
}
match &prog.insts[pc] {
Inst::Save(s) if *s == base => work.push((pc + 1, (0, FAR))),
Inst::Save(s) if *s == base + 1 => work.push((pc + 1, (since, since))),
Inst::Save(_) | Inst::Guard(..) | Inst::Anchor(_) => work.push((pc + 1, (since, width))),
Inst::Jmp(t) => work.push((*t, (since, width))),
Inst::Split(a, b) => {
work.push((*a, (since, width)));
work.push((*b, (since, width)));
}
Inst::Atom(_) => work.push((pc + 1, (since.saturating_add(1).min(FAR), width))),
Inst::Match => {}
}
}
for &(pc, _) in refs.iter().filter(|&&(_, b)| b == base) {
let mut states = reached[pc].iter();
let &(since, width) = states.next()?;
if width != 1 || since == 0 || since >= FAR || states.any(|&s| s != (since, width)) {
return None;
}
offsets[pc] = i32::from(since);
}
}
Some((offsets, group))
}
pub(crate) fn device_binds_fit(pattern: &Pattern) -> bool {
compile(pattern).is_some_and(|prog| prog.nslots <= MAX_SAVE_SLOTS && fixed_bind_offsets(&prog).is_some())
}
#[cfg(feature = "gpu")]
pub(crate) fn compile_for_gpu(pattern: &Pattern) -> Option<GpuNfa> {
let prog = compile_unless(pattern, true)?;
let n = prog.insts.len();
if n > 64 || prog.nslots > MAX_SAVE_SLOTS {
return None;
}
let (ref_back, class_group) = fixed_bind_offsets(&prog)?;
let mut atom_kind = vec![0u32; n];
let mut next_closure = vec![0u64; n];
let mut is_atom = vec![0u32; n];
let mut mag_lo = vec![f32::NEG_INFINITY; n];
let mut mag_hi = vec![f32::INFINITY; n];
let mut reads_magnitude = false;
let mut need_mask = vec![0u32; n];
let mut reads_bytes = false;
let mut literals: Vec<Option<Vec<u8>>> = vec![None; n];
let mut ent_lo = vec![f32::NEG_INFINITY; n];
let mut ent_hi = vec![f32::INFINITY; n];
let mut period_need = vec![0u32; n];
let mut period_val = vec![0u32; n];
let mut texture_need = vec![u32::MAX; n];
let mut onset_need = vec![0u32; n];
let mut reads_spectral = false;
let mut match_mask = 0u64;
for pc in 0..n {
match &prog.insts[pc] {
Inst::Atom(a) => {
is_atom[pc] = 1;
atom_kind[pc] = match a {
Atom::Kind(k) => k.code(),
Atom::Any => u32::MAX,
Atom::Magnitude(pred) => {
(mag_lo[pc], mag_hi[pc]) = magnitude_bounds(pred)?;
reads_magnitude = true;
u32::MAX
}
Atom::KindMag(k, pred) => {
(mag_lo[pc], mag_hi[pc]) = magnitude_bounds(pred)?;
reads_magnitude = true;
k.code()
}
Atom::RegisterEq(..) => u32::MAX,
Atom::Literal(text, _) => {
literals[pc] = Some(text.as_bytes().to_vec());
u32::MAX
}
Atom::Byte(bc) => {
need_mask[pc] = byte_class_bit(*bc)?;
reads_bytes = true;
u32::MAX
}
Atom::Spectral(pred) => {
use crate::ast::SpectralPred;
match pred {
SpectralPred::EntropyGe(p) => ent_lo[pc] = f32::from(*p),
SpectralPred::EntropyLe(p) => ent_hi[pc] = f32::from(*p),
SpectralPred::PeriodAny => period_need[pc] = 1,
SpectralPred::PeriodEq(v) => {
period_need[pc] = 2;
period_val[pc] = u32::from(*v);
}
SpectralPred::Texture(t) => texture_need[pc] = spec_texture_id(*t),
SpectralPred::Onset => onset_need[pc] = 1,
}
reads_spectral = true;
u32::MAX
}
Atom::Class(_) => return None,
_ => return None,
};
next_closure[pc] = epsilon_closure(&prog.insts, pc + 1);
}
Inst::Match => match_mask |= 1u64 << pc,
Inst::Guard(..) | Inst::Anchor(_) => return None,
Inst::Split(..) | Inst::Jmp(_) | Inst::Save(_) => {}
}
}
let start_closure = epsilon_closure(&prog.insts, 0);
Some(GpuNfa {
atom_kind,
next_closure,
is_atom,
mag_lo,
mag_hi,
reads_magnitude,
ref_back,
class_group,
binds_registers: !prog.slots.is_empty(),
need_mask,
reads_bytes,
literals,
ent_lo,
ent_hi,
period_need,
period_val,
texture_need,
onset_need,
reads_spectral,
start_closure,
match_mask,
nstates: n,
})
}
#[cfg(feature = "gpu")]
fn epsilon_closure(insts: &[Inst], start: usize) -> u64 {
let mut seen = 0u64;
let mut result = 0u64;
let mut stack = vec![start];
while let Some(pc) = stack.pop() {
if pc >= insts.len() || (seen >> pc) & 1 == 1 {
continue;
}
seen |= 1u64 << pc;
match &insts[pc] {
Inst::Atom(_) | Inst::Match => result |= 1u64 << pc,
Inst::Jmp(t) => stack.push(*t),
Inst::Split(a, b) => {
stack.push(*a);
stack.push(*b);
}
Inst::Save(_) => stack.push(pc + 1),
Inst::Guard(..) | Inst::Anchor(_) => {}
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse;
fn bind_offsets(src: &str) -> Option<(Vec<i32>, Option<crate::orbit::OrbitGroup>)> {
fixed_bind_offsets(&compile(&parse(src).unwrap()).expect("compiles for the linear engine"))
}
fn reference_offsets(src: &str) -> Option<Vec<i32>> {
bind_offsets(src).map(|(o, _)| o.into_iter().filter(|&d| d > 0).collect())
}
#[test]
fn a_one_token_bind_at_a_fixed_distance_gives_its_offset() {
assert_eq!(reference_offsets("\\W:x =x"), Some(vec![1]));
assert_eq!(reference_offsets("\\W:x \\N =x"), Some(vec![2]));
assert_eq!(reference_offsets("(\\W:x =x)+"), Some(vec![1]));
assert_eq!(reference_offsets("\\W:x \\N{2} =x \\W =x"), Some(vec![3, 5]));
assert_eq!(reference_offsets("\\W:t \\N"), Some(vec![]));
assert_eq!(
bind_offsets("\\W:x =case x").and_then(|(_, g)| g),
Some(crate::orbit::OrbitGroup::Case)
);
}
#[test]
fn a_reference_with_no_single_offset_is_refused() {
for src in ["\\W+:x =x", "\\W:x \\N* =x", "\\W:x \\N? =x", "\\W:x =x \\W:y =case y", "(\\W:x)? =x", "\\W:x =case x \"the\""] {
assert_eq!(bind_offsets(src), None, "{src} has no single fixed offset");
}
}
fn run(pattern: &str, input: &str) -> Vec<Match> {
let p = parse(pattern).unwrap();
let spans = scan_nfa(&p, input.as_bytes()).expect("nfa handles this pattern");
crate::engine::captures(&p, input.as_bytes(), &spans)
}
#[test]
fn number_then_word() {
let m = run("\\N \\W", "weight 12 items here");
assert_eq!(m.len(), 1);
assert_eq!(&"weight 12 items here"[m[0].start..m[0].end], "12 items");
assert!(run("\\N \\W", "weight 12 kg here").is_empty(), "`12 kg` is one quantity token");
}
#[test]
fn matched_tag_binds_and_rejects() {
let hay = "<div>hi</div>";
let ok = run("<\\W:t>.*</=t>", hay);
assert_eq!(ok.len(), 1);
assert_eq!(ok[0].group("t", hay.as_bytes()), Some(b"div" as &[u8]));
let bad = run("<\\W:t>.*</=t>", "<div>hi</span>");
assert!(bad.is_empty());
}
#[test]
fn repeated_token() {
let ok = run("\\W:x =x", "the the cat");
assert_eq!(ok.len(), 1);
assert_eq!(&"the the cat"[ok[0].start..ok[0].end], "the the");
assert!(run("\\W:x =x", "the cat sat").is_empty());
}
#[test]
fn leftmost_first_prefers_the_earlier_branch() {
let m = run("\\W | \\W \\W", "a b");
assert_eq!(m.len(), 2);
assert_eq!(&"a b"[m[0].start..m[0].end], "a");
}
#[test]
fn leftmost_first_falls_back_when_the_continuation_fails() {
let m = run("(\"a\" | \"a\" \"b\") \"c\"", "a b c");
assert_eq!(m.len(), 1);
assert_eq!(&"a b c"[m[0].start..m[0].end], "a b c");
}
#[test]
fn an_iteration_that_matched_empty_ranks_as_a_regular_expression_ranks_it() {
let input = "foo 969 8 foo foo qux";
let m = run("((.*? | \\N | .))+ (\\W{1,} | . | \\N \\N) \\W", input);
assert_eq!(m.len(), 1, "{m:?}");
assert_eq!(&input[m[0].start..m[0].end], input);
let m = run("\\W (\\N*?)*", "qux 769");
assert_eq!(m.len(), 1, "{m:?}");
assert_eq!(&"qux 769"[m[0].start..m[0].end], "qux");
}
#[test]
fn an_open_repeat_is_laid_out_as_the_plus_it_means() {
let pairs = [
("(\\N*? | \\W){1,} \\N", "(\\N*? | \\W)+ \\N"),
("(. | \\N \\N){2,} \\W", "(. | \\N \\N) (. | \\N \\N)+ \\W"),
("(\\W*?){0,} \\N", "(\\W*?)* \\N"),
];
for (a, b) in pairs {
for input in ["1 2 a 3", "a 1 2 3 b", "1 1 1 1 a", "a b c 1"] {
assert_eq!(run(a, input), run(b, input), "{a} against {b} over {input:?}");
}
}
}
#[test]
fn balanced_routes_to_other_engine() {
let p = parse("\\W\\B(.*)").unwrap();
assert!(scan_nfa(&p, b"f(x)").is_none());
}
#[test]
fn differential_against_set_reachability() {
let patterns = [
"\\N",
"\\W",
"\\Q",
".",
"\\N \\W",
"\\W \\W",
"<\\W:t>.*</=t>",
"\\W:x =x",
"\\N | \\W",
". ~\"END\"",
"\\W*",
"\\W+",
"\\W?",
"\\N{1,2}",
"\"foo\"",
"(\\N | \\W)+",
"\\W:a \\W:b =a",
"\\N \\N",
".*",
"\\W \\N \\W",
"`[A-Z][a-z]*`",
"`\\d+`",
"`[a-z]+`:t =t",
"`[A-Z]+` \\N",
"<`[a-z]+`:t>.*</=t>",
"\\M{>1}",
"\\M{<3}",
"\\N \\M{>1}",
"\\N{>1}",
"\\N{<3}",
"\\W:x =shape x",
"\\W:x =case x",
"\\V",
"\\G",
"\\{mac}",
"^ \\W",
"$ \\W",
"\\A \\W",
"\\z \\W",
"^ $ \\W",
"^ \\W \\N",
"(^ | \"|\") \\W",
"\\W*? \\N",
"\\W+? \\N",
"\\W?? \\N",
"\\W{1,3}? \\N",
".*? \"q\"",
"(\\N | \\W)*? \\N",
"(\\W | \\W \\W)+? \\N",
"\\W*? \\N \\W*?",
"(\\W*?)* \\N",
"\\H",
"\\C",
"\\%",
"\\Z",
"\\$",
"\\D",
"\\R",
"\\L",
"\\W \\V",
". !~\"END\"",
"\\W !~\"cat\"",
"\\N !~\"the\"",
"#\"W(W,W)\"",
"#\"N.N\"",
"(\"a\" | \"a\" \"b\") \"c\"",
"(\"a\" \"b\" | \"a\") \"c\"",
"(\\W | \\W \\W) \\W",
"(\\N | \\N \\N) \\W",
"(\\W \\W | \\W) =x",
"(\\W:x | \\W \\W) =x",
"(\\W \\W | \\W:x) =x",
"(\\W:x \\W | \\W \\W:x) =x",
"[\\N \\W]",
"[^\\N]",
"[^\\W]",
"[\\W && \\h]",
"[\\W -- \\u]",
"[\\N \\W] [\\N \\W]",
"[\"cat\" \"dog\"]",
"[`[a-z]+` \\N]",
"[^\\N]:x =x",
];
let inputs = [
"",
"a",
"12",
"a b c",
"the the cat dog dog",
"<div>hi</div>",
"<a>x</a> y <b>z</b>",
"12 kg 30 m s",
"foo foo bar foo",
"a a a a a",
"x",
"END here END now",
"begin END end",
"12 34 56",
"word",
"1 a 2 b 3 c",
"the the the",
" spaced out ",
"a1 b2 c3",
"\"q\" \"q\"",
"Hello World foo Bar",
"abc DEF 99 ghi",
"deadbeef cafe deadbeef",
"<div>hi</div> <Span>x</Span>",
"rel v1.2.3 id 550e8400-e29b-41d4-a716-446655440000 nic 01:23:45:67:89:ab",
"bg #ff8800 net 192.168.0.0/24 host 10.0.0.1 tag 2.0.0",
"f(a,b) g(x,y) h(1) k(p,q,r) 3.4",
"up 42% cache 512KB cost $1,234.56 bare 10M plain 50",
"took 1500ms wait 3h20m open /usr/bin/x rel ./a/b drive C:\\d\\e a/b",
];
for pat in patterns {
let p = parse(pat).expect("pattern parses");
for inp in inputs {
let nfa = scan_nfa(&p, inp.as_bytes()).expect("nfa handles pattern");
let set = crate::engine::scan_set_reachability(&p, inp.as_bytes());
assert_eq!(nfa, set, "disagreement on pattern {pat:?} input {inp:?}");
}
}
}
fn scan_serial_ref(pattern: &Pattern, input: &[u8]) -> Vec<Span> {
let prog = compile(pattern).expect("compiles");
let toks = crate::lexer::lex(input);
let sig: Vec<usize> =
(0..toks.len()).filter(|&i| toks[i].kind != TokenKind::Whitespace).collect();
let absent = crate::prefilter::absent_guard_literals(pattern, input);
let stream = Stitched { toks: &toks, sig: &sig };
scan_nfa_serial(&prog, input, &stream, &absent, pattern.starts_with_resume())
}
fn parts_of(input: &[u8], counts: &[usize]) -> Vec<Significant> {
let toks = crate::lexer::lex(input);
let sig: Vec<&Token> = toks.iter().filter(|t| t.kind != TokenKind::Whitespace).collect();
let mut parts = Vec::new();
let mut at = 0;
for &n in counts {
let mut part = Significant::with_base(0, n);
for t in &sig[at..at + n] {
part.kinds.push(t.kind.code());
part.spans.push((t.start, t.end));
}
parts.push(part);
at += n;
}
assert_eq!(at, sig.len(), "the counts cover the stream");
parts
}
#[test]
fn the_longest_run_is_the_same_whichever_form_holds_the_stream() {
let input = b"a b c 1 d e f g 2 h i 3 4 5 j k l m n o 6";
let toks = crate::lexer::lex(input);
let sig = Stitched::significant_of(&toks);
assert_eq!(sig.len(), 21, "one token a letter or digit");
let stitched = Stitched { toks: &toks, sig: &sig };
let kinds: Vec<u32> = sig.iter().map(|&i| toks[i].kind.code()).collect();
let spans: Vec<(u32, u32)> = sig.iter().map(|&i| (toks[i].start, toks[i].end)).collect();
let split = KindSpans::new(&kinds, &spans);
for cuts in [
vec![21],
vec![17, 4],
vec![12, 9],
vec![15, 3, 3],
vec![17, 0, 4],
vec![1, 1, 1, 18],
] {
let parts = parts_of(input, &cuts);
let bases = Parts::bases_of(&parts);
let shared = Parts::new(&parts, &bases);
for (kind, longest) in [(TokenKind::Word, 6usize), (TokenKind::Number, 3usize)] {
for ceiling in 0..8usize {
let want = (longest <= ceiling).then_some(longest);
assert_eq!(stitched.longest_run(kind, ceiling), want, "stitched, {kind:?} under {ceiling}");
assert_eq!(split.longest_run(kind, ceiling), want, "split, {kind:?} under {ceiling}");
assert_eq!(
shared.longest_run(kind, ceiling),
want,
"parts cut {cuts:?}, {kind:?} under {ceiling}"
);
assert_eq!(
shared.cursor().longest_run(kind, ceiling),
want,
"a cursor over parts cut {cuts:?}, {kind:?} under {ceiling}"
);
}
}
for at in 0..input.len() + 3 {
let want = stitched.first_at_or_after(at);
assert_eq!(shared.first_at_or_after(at), want, "parts cut {cuts:?}, at byte {at}");
assert_eq!(
shared.cursor().first_at_or_after(at),
want,
"a cursor over parts cut {cuts:?}, at byte {at}"
);
assert_eq!(split.first_at_or_after(at), want, "split, at byte {at}");
}
}
}
#[test]
fn every_kind_survives_its_code() {
use crate::token::BracketKind::{Brace, Paren, Square};
let kinds = [
TokenKind::Number, TokenKind::Word, TokenKind::Quoted, TokenKind::Ip, TokenKind::Url,
TokenKind::Email, TokenKind::Timestamp, TokenKind::Whitespace, TokenKind::Punct,
TokenKind::Other, TokenKind::Open(Paren), TokenKind::Open(Square), TokenKind::Open(Brace),
TokenKind::Close(Paren), TokenKind::Close(Square), TokenKind::Close(Brace),
TokenKind::Version, TokenKind::Uuid, TokenKind::Mac, TokenKind::HexColor, TokenKind::Cidr,
TokenKind::Percent, TokenKind::ByteSize, TokenKind::Money, TokenKind::HashDigest,
TokenKind::Duration, TokenKind::Path, TokenKind::Jwt, TokenKind::CreditCard,
TokenKind::Base64, TokenKind::Geo, TokenKind::Phone, TokenKind::Quantity,
TokenKind::Custom(0), TokenKind::Custom(7), TokenKind::Custom(255),
];
for k in kinds {
assert_eq!(TokenKind::from_code(k.code()), k, "{k:?}");
}
}
#[test]
fn the_parts_stream_reads_as_the_stitched_one() {
let input = b"alpha = 1; beta = 2\nif (x) { y_2 = \"q\" } 3.5 z";
let toks = crate::lexer::lex(input);
let sig: Vec<usize> = (0..toks.len()).filter(|&i| toks[i].kind != TokenKind::Whitespace).collect();
let stitched = Stitched { toks: &toks, sig: &sig };
let m = sig.len();
assert!(m >= 12, "the input must span several parts ({m} tokens)");
let parts = parts_of(input, &[3, 0, 5, m - 8]);
let owned = OwnedStream::over(input);
assert_eq!(owned.len(), sig.len(), "the owned stream spans the same tokens");
for k in (0..sig.len()).chain((0..sig.len()).rev()) {
assert_eq!(owned.kind(k), stitched.kind(k), "owned kind at {k}");
assert_eq!(owned.span(k), stitched.span(k), "owned span at {k}");
}
let bases = Parts::bases_of(&parts);
let shared = Parts::new(&parts, &bases);
let cursor = shared.cursor();
assert_eq!(shared.len(), m);
assert_eq!(cursor.len(), m);
for k in (0..m).chain((0..m).rev()).chain([m - 1, 0, 3, 2, 8, 7]) {
assert_eq!(shared.kind(k), stitched.kind(k), "kind at {k}");
assert_eq!(shared.span(k), stitched.span(k), "span at {k}");
assert_eq!(cursor.kind(k), stitched.kind(k), "cursor kind at {k}");
assert_eq!(cursor.span(k), stitched.span(k), "cursor span at {k}");
}
}
#[test]
fn a_prefix_walk_and_a_per_anchor_fill_select_the_same_spans() {
let mut text = String::new();
for i in 0..4000u32 {
text.push_str(&format!("tag {} word {} {}\n", i % 7, i % 13, i));
}
let input = text.as_bytes();
let (kinds, spans) = crate::parallel_lex::lex_significant_parallel(input);
let stream = KindSpans::new(&kinds, &spans);
let n = kinds.len();
for src in ["\\W \\N", "\\N \\W", "\\W \\W", "\\N{1,2}", "\\W \\N \\W", "\\Q \\N"] {
let p = parse(src).expect("parses");
let prog = compile_pattern(&p).expect("compiles");
let want = crate::engine::scan(&p, input);
for per_mille in [0u32, 1, 250, 700, 999, 1000] {
let mid = (n * per_mille as usize / 1000).min(n);
let mut filled = vec![0i32; n];
anchor_ends_into(&prog, &[], &stream, 0, &mut filled);
let mut walked = vec![0i32; n];
walk_prefix_into(&prog, &[], &stream, mid, &mut walked[..]);
anchor_ends_into(&prog, &[], &stream, mid, &mut walked[mid..]);
let as_spans = |ends: &[i32]| {
let mut out = Vec::new();
let mut a = 0usize;
while a < n {
let e = ends[a] as usize;
if e > a {
out.push(crate::engine::Span { start: spans[a].0, end: spans[e - 1].1 });
a = e;
} else {
a += 1;
}
}
out
};
assert_eq!(as_spans(&filled), want, "{src} filled");
assert_eq!(as_spans(&walked), want, "{src} walked at {per_mille} per mille");
}
}
}
#[test]
fn the_scan_over_parts_is_the_scan_over_the_stitched_stream() {
let mut text = String::new();
for i in 0..2500u32 {
text.push_str(&format!("name{i}: alice{i} = alice{i} ; ^tag {i} end\n"));
}
let input = text.as_bytes();
let toks = crate::lexer::lex(input);
let m = toks.iter().filter(|t| t.kind != TokenKind::Whitespace).count();
assert!(m >= PARALLEL_SCAN_MIN_ANCHORS, "the dispatch must be exercised ({m} tokens)");
let cuts = [m / 3, 0, 1, m / 2 - m / 3 - 1, m - m / 2];
let parts = parts_of(input, &cuts);
for src in [
"\\W \":\" \\W", "\\W:x \"=\" =x", "^ \\W", "\\W $", "\\A \\W", "\\W \\z", "\\W ~\"end\"",
"\\N{1,2}", "\\W | \\N", "\"alice7\" \"=\"", "\\G \\W", "\\W \\K \":\"",
] {
let p = parse(src).expect("parses");
let over = scan_nfa_over(&p, input, &toks).expect("the engine takes the pattern");
let parts_scan = scan_nfa_parts(&p, input, &parts).expect("the engine takes the pattern");
assert_eq!(parts_scan, over, "{src}");
}
}
#[test]
fn the_anchors_a_literal_opening_finds_are_the_anchors_the_walk_admits() {
let mut text = String::new();
for i in 0..2500u32 {
text.push_str(&format!(
"let x{i} = {i} ; letter outlet let({i}) k{i}={i} var y{i} = {i} ;\nlet\n"
));
}
let input = text.as_bytes();
let toks = crate::lexer::lex(input);
let m = toks.iter().filter(|t| t.kind != TokenKind::Whitespace).count();
assert!(m >= PARALLEL_SCAN_MIN_ANCHORS, "the dispatch must be exercised ({m} tokens)");
for src in [
"\"let\" \\W \"=\"",
"(\"let\" | \"var\") \\W \"=\"",
"^ \"let\" \\W",
"\"let\" \"(\" \\N",
"\"=\" \\N",
"\"zzzqqq\" \\W",
"\"letter\" \\W",
"\"let\" $",
] {
let p = parse(src).expect("parses");
let prog = compile(&p).expect("the engine takes it");
let opens = opening_atoms(&prog).expect("an opening to test");
assert!(opening_literals(&opens).is_some(), "{src} opens with plain literals");
assert_eq!(
scan_nfa(&p, input).expect("the engine takes the pattern"),
scan_serial_ref(&p, input),
"{src}"
);
}
for src in ["\\W \"=\"", "(\"let\" | \\W) \"=\"", "(?orbit:case \"let\") \\W"] {
let p = parse(src).expect("parses");
let prog = compile(&p).expect("the engine takes it");
let opens = opening_atoms(&prog).expect("an opening to test");
assert!(opening_literals(&opens).is_none(), "{src} does not open with plain literals alone");
}
}
#[test]
fn the_windowed_walk_hands_out_the_scan_s_matches() {
let mut text = String::new();
for i in 0..3000u32 {
text.push_str(&format!("tag {} word_{} = {} ;\n", i % 7, i % 13, i));
}
let input = text.as_bytes();
let toks = crate::lexer::lex(input);
let m = toks.iter().filter(|t| t.kind != TokenKind::Whitespace).count();
assert!(m >= PARALLEL_SCAN_MIN_ANCHORS, "the windows must be exercised ({m} tokens)");
for src in [
"\\W \"=\"", "\\N", "\\W:x \"=\" =x", "\"tag\" \\N", "\"zzzqqq\"", "\\N{1,2}",
"\\G \\W", "\\W \\K \"=\"",
] {
let p = parse(src).expect("parses");
let want = scan_nfa(&p, input).expect("the engine takes the pattern");
let mut w = SerialWalk::over(&p, input).expect("the engine takes the pattern");
let mut spans = Vec::new();
while let Some(s) = w.next_span(input) {
spans.push(s);
}
assert_eq!(spans, want, "{src} by span");
assert_eq!(w.next_span(input), None, "{src} past the end");
assert_eq!(w.next_span(input), None, "{src} past the end twice");
let mut w = SerialWalk::over(&p, input).expect("the engine takes the pattern");
let mut matched = Vec::new();
while let Some(found) = w.next_match(input) {
matched.push(found);
}
let spans: Vec<Span> = matched
.iter()
.map(|f| Span { start: f.start as u32, end: f.end as u32 })
.collect();
assert_eq!(spans, want, "{src} by match");
let bound = captures_over(&p, input, &toks, &want).expect("the engine takes it");
for (got, expect) in matched.iter().zip(&bound) {
assert_eq!(got.captures(), expect.captures(), "{src}: registers at {}", got.start);
}
assert_eq!(
SerialWalk::any_match(&p, input),
Some(!want.is_empty()),
"{src} by any_match"
);
if p.mentions_reset_start() {
continue;
}
let at = want.get(want.len() / 2).map_or(0, |s| s.start());
let mut w = SerialWalk::over(&p, input).expect("the engine takes the pattern");
assert!(w.next_span(input).is_some() || want.is_empty(), "{src} has a first match");
w.seek(at);
let mut sought = Vec::new();
while let Some(s) = w.next_span(input) {
sought.push(s);
}
let from_at: Vec<Span> = want.iter().copied().filter(|s| s.start() >= at).collect();
assert_eq!(sought, from_at, "{src} after a seek to {at}");
}
}
#[test]
fn resolving_over_the_parts_binds_what_resolving_over_the_stitched_stream_binds() {
let mut text = String::new();
for i in 0..3000u32 {
text.push_str(&format!("name{}: alice{} = alice{} ; tag {}\n", i % 11, i % 7, i % 7, i));
}
let input = text.as_bytes();
let toks = crate::lexer::lex(input);
let m = toks.iter().filter(|t| t.kind != TokenKind::Whitespace).count();
assert!(m >= PARALLEL_SCAN_MIN_ANCHORS, "the dispatch must be exercised ({m} tokens)");
for src in [
"\\W:x \"=\" =x", "\\W:name \":\"", "\\N:n", "\\W:a \\W:b", "\\W \\K \":\"",
"\\W:v \"=\" \\W",
] {
let p = parse(src).expect("parses");
let spans = scan_nfa(&p, input).expect("the engine takes the pattern");
let stitched = captures_over(&p, input, &toks, &spans).expect("the engine takes it");
let parts = captures_over_parts(&p, input, &spans).expect("the engine takes it");
assert_eq!(parts.len(), stitched.len(), "{src}: a different number of matches");
for (a, b) in parts.iter().zip(&stitched) {
assert_eq!((a.start, a.end), (b.start, b.end), "{src}: a different span");
assert_eq!(a.captures(), b.captures(), "{src}: different registers at {}", a.start);
}
}
}
#[test]
fn the_opening_atoms_refuse_what_cannot_be_tested_before_an_attempt() {
for src in ["\\W{0,1}", "\\W:x \"=\" =x"] {
let p = parse(src).expect("parses");
let prog = compile(&p).expect("the engine takes it");
assert!(opening_atoms(&prog).is_none(), "{src} must attempt every anchor");
}
for src in ["\"let\" \\W \"=\"", "(\\W | \\N)", "^ \"let\"", "\\W{2}"] {
let p = parse(src).expect("parses");
let prog = compile(&p).expect("the engine takes it");
assert!(opening_atoms(&prog).is_some(), "{src} opens with an atom to test");
}
}
#[test]
fn an_opening_atom_accepts_every_anchor_a_match_begins_at() {
let mut text = String::new();
for i in 0..3000u32 {
text.push_str(&format!("let value_{} = {} ; tag {}\n", i % 11, i * 7, i));
}
let input = text.as_bytes();
let stream = OwnedStream::over(input);
for src in ["\"let\" \\W \"=\"", "\\W \"=\"", "\\N", "(\\W | \\N)", "^ \"let\"", "\\W{2}"] {
let p = parse(src).expect("parses");
let prog = compile(&p).expect("the engine takes it");
let Some(opens) = opening_atoms(&prog) else { continue };
let want = scan_nfa(&p, input).expect("the engine takes the pattern");
assert!(!want.is_empty(), "{src} must match this corpus for the check to mean anything");
for s in &want {
let a = first_at_or_after(&stream, s.start());
assert!(
opening_admits(&prog, &opens, input, &stream, a),
"{src}: the anchor of the match at {} was rejected",
s.start()
);
}
}
}
#[test]
fn parallel_scan_matches_serial_on_a_large_stream() {
let mut input = String::new();
for i in 0..1500u32 {
input.push_str(&format!("tag {} the the {} end ", i % 7, i % 13));
}
let bytes = input.as_bytes();
let sig = crate::lexer::lex(bytes).iter().filter(|t| t.kind != TokenKind::Whitespace).count();
assert!(
sig >= PARALLEL_SCAN_MIN_ANCHORS,
"test must exercise the parallel path ({sig} tokens)"
);
for pat in ["\\W \\N", "\\N \\W", "\\W:x =x", "\\N{1,2}", "\\W | \\N", "\\W \\N \\W"] {
let p = parse(pat).expect("parses");
let par = scan_nfa(&p, bytes).expect("nfa handles pattern");
let ser = scan_serial_ref(&p, bytes);
assert_eq!(par, ser, "parallel != serial on {pat:?}");
}
for pat in ["\\W \\N", "\\W:x =x"] {
let p = parse(pat).expect("parses");
let par = scan_nfa(&p, bytes).expect("nfa handles pattern");
let oracle = crate::engine::scan_set_reachability(&p, bytes);
assert_eq!(par, oracle, "parallel != set-reachability on {pat:?}");
}
let p = parse("\\W:x =x").expect("parses");
let spans = scan_nfa(&p, bytes).expect("nfa handles pattern");
let resolved = crate::engine::captures(&p, bytes, &spans);
assert_eq!(resolved.len(), spans.len());
for m in &resolved {
let text = &input[m.start..m.end];
let word = text.split(' ').next().expect("a match holds a word");
assert_eq!(m.group("x", bytes), Some(word.as_bytes()), "{text:?}");
}
}
#[test]
fn reset_start_on_a_large_stream_reports_what_follows_it() {
let mut input = String::new();
for i in 0..3000u32 {
input.push_str(&format!("name{i}: alice{i} "));
}
let bytes = input.as_bytes();
let sig = crate::lexer::lex(bytes).iter().filter(|t| t.kind != TokenKind::Whitespace).count();
assert!(sig >= PARALLEL_SCAN_MIN_ANCHORS, "test must reach the parallel floor ({sig} tokens)");
let p = parse("\\W \":\" \\K \\W").expect("parses");
let got = scan_nfa(&p, bytes).expect("nfa handles pattern");
assert_eq!(got, scan_serial_ref(&p, bytes));
assert_eq!(got.len(), 3000);
assert_eq!(&input[got[0].range()], "alice0");
let resolved = crate::engine::captures(&p, bytes, &got);
assert_eq!(resolved.len(), 3000);
assert_eq!(&input[resolved[7].start..resolved[7].end], "alice7");
}
#[test]
fn high_capture_pattern_routes_to_set_engine() {
let p = parse("\\W:a \\W:b \\W:c \\W:d").unwrap();
assert!(scan_nfa(&p, b"one two three four").is_none());
let spans = crate::engine::scan(&p, b"one two three four");
let got = crate::engine::captures(&p, b"one two three four", &spans);
assert_eq!(got.len(), 1);
assert_eq!(&"one two three four"[got[0].start..got[0].end], "one two three four");
let hay = b"one two three four" as &[u8];
assert_eq!(got[0].group("a", hay), Some(b"one" as &[u8]));
assert_eq!(got[0].group("d", hay), Some(b"four" as &[u8]));
}
}