use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EmptyPattern;
impl fmt::Display for EmptyPattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("an empty pattern matches everywhere")
}
}
impl std::error::Error for EmptyPattern {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Literal {
needle: Vec<u8>,
}
impl Literal {
pub fn new(pattern: &[u8]) -> Result<Self, EmptyPattern> {
if pattern.is_empty() {
return Err(EmptyPattern);
}
Ok(Self {
needle: pattern.to_vec(),
})
}
#[must_use]
pub fn pattern(&self) -> &[u8] {
&self.needle
}
pub fn matches<'t>(&'t self, text: &'t [u8]) -> Matches<'t> {
Matches {
needle: &self.needle,
text,
base: 0,
word: 0,
next_block: 0,
min: 0,
}
}
}
fn match_bits(text: &[u8], needle: &[u8], base: usize) -> u64 {
let last = text.len().saturating_sub(needle.len() - 1); let room = last.saturating_sub(base).min(64);
if room == 0 {
return 0;
}
let mut m = if room == 64 {
!0u64
} else {
(1u64 << room) - 1
};
for (k, &b) in needle.iter().enumerate() {
m &= eq_bits(text, base + k, b);
if m == 0 {
break;
}
}
m
}
#[inline]
fn any_bits(text: &[u8], base: usize, b: u8) -> bool {
if base + 64 <= text.len() {
#[cfg(target_arch = "aarch64")]
return unsafe { crate::simd::neon::any64(text, base, b) };
#[cfg(target_arch = "x86_64")]
if crate::has_ssse3() {
return unsafe { crate::simd::x86::any64(text, base, b) };
}
}
true
}
#[inline]
fn eq_bits(text: &[u8], base: usize, b: u8) -> u64 {
if base + 64 <= text.len() {
#[cfg(target_arch = "aarch64")]
return unsafe { crate::simd::neon::eq64(text, base, b) };
#[cfg(target_arch = "x86_64")]
if crate::has_ssse3() {
return unsafe { crate::simd::x86::eq64(text, base, b) };
}
}
let mut m = 0u64;
for i in 0..(text.len() - base).min(64) {
if text[base + i] == b {
m |= 1u64 << i;
}
}
m
}
pub struct Matches<'t> {
needle: &'t [u8],
text: &'t [u8],
base: usize,
word: u64,
next_block: usize,
min: usize, }
impl Iterator for Matches<'_> {
type Item = usize;
fn next(&mut self) -> Option<usize> {
loop {
while self.word != 0 {
let i = self.word.trailing_zeros() as usize;
self.word &= self.word - 1;
let at = self.base + i;
if at >= self.min {
self.min = at + self.needle.len();
return Some(at);
}
}
if self.next_block >= self.text.len() {
return None;
}
self.base = self.next_block;
self.next_block += 64;
if any_bits(self.text, self.base, self.needle[0]) {
self.word = match_bits(self.text, self.needle, self.base);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn find(pat: &str, hay: &str) -> Vec<usize> {
Literal::new(pat.as_bytes())
.unwrap()
.matches(hay.as_bytes())
.collect()
}
#[test]
fn matches_a_naive_scan() {
let cases: &[(&str, String)] = &[
("a", "aaaa".into()),
("aa", "aaaaa".into()),
("ab", "abab".into()),
("▁", "▁a▁bb▁".into()),
("▁▁", "▁▁▁▁".into()),
("xyz", "xyxyz".into()),
("a", "b".repeat(200)),
("▁w", format!("{}▁w{}", "q".repeat(63), "z".repeat(70))),
("abc", format!("{}abc", "a".repeat(62))),
];
for (pat, hay) in cases {
let (p, h) = (pat.as_bytes(), hay.as_bytes());
let mut want = Vec::new();
let mut i = 0;
while i + p.len() <= h.len() {
if &h[i..i + p.len()] == p {
want.push(i);
i += p.len(); } else {
i += 1;
}
}
assert_eq!(find(pat, hay), want, "{pat:?} in {hay:?}");
}
}
#[test]
fn finds_the_needle_at_every_offset() {
let needle = "▁w";
for off in 0..180usize {
let hay = format!("{}{needle}{}", "q".repeat(off), "q".repeat(180 - off));
assert_eq!(find(needle, &hay), vec![off], "offset {off}");
}
}
#[test]
fn corpora_match_a_naive_scan() {
let dir = std::path::Path::new(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../../tokbench/data/fixtures"
));
let Ok(entries) = std::fs::read_dir(dir) else {
eprintln!("skip: no fixtures at {}", dir.display());
return;
};
let mut files: Vec<_> = entries
.flatten()
.map(|e| e.path())
.filter(|p| p.extension().is_some_and(|x| x == "txt"))
.collect();
files.sort();
let needles = [" ", "\n", "\u{2581}", "</s>", "the", "\u{2591}\u{2592}"];
let mut cells = 0;
for path in &files {
let Ok(text) = std::fs::read_to_string(path) else {
continue;
};
let end = (0..=(1 << 20).min(text.len()))
.rev()
.find(|&i| text.is_char_boundary(i))
.unwrap();
let h = &text.as_bytes()[..end];
for pat in needles {
let p = pat.as_bytes();
let mut want = Vec::new();
let mut i = 0;
while i + p.len() <= h.len() {
if &h[i..i + p.len()] == p {
want.push(i);
i += p.len();
} else {
i += 1;
}
}
let got: Vec<usize> = Literal::new(p).unwrap().matches(h).collect();
assert_eq!(
got,
want,
"{pat:?} in {} ({} bytes)",
path.file_name().unwrap().to_string_lossy(),
h.len()
);
cells += 1;
}
}
assert!(cells > 0, "fixture dir exists but nothing was compared");
eprintln!("{cells} corpus x needle pairs match a naive scan");
}
#[test]
fn an_empty_pattern_is_rejected() {
assert_eq!(Literal::new(b""), Err(EmptyPattern));
}
}