use alloc::boxed::Box;
use alloc::vec::Vec;
use crate::errors::{DaachorseError, Result};
use crate::serializer::{Serializable, SerializableVec};
#[derive(Clone, Eq, Hash, PartialEq)]
pub struct Prefilter {
table: Box<[u8; Self::TABLE_LEN]>,
window_len: u8,
hit_bit: u8,
}
impl Prefilter {
pub const MAX_WINDOW_LEN: usize = 9;
pub const MIN_WINDOW_LEN: usize = 2;
const TABLE_LEN: usize = 65536;
const MAX_EXPECTED_CANDIDATE_RATE: f64 = 1. / 16.;
#[inline(always)]
pub fn next_position(&self, haystack: &[u8], pos: usize) -> usize {
let Some(&(mut prev)) = haystack.get(pos) else {
return haystack.len();
};
let mut e = u8::MAX;
for (i, &c) in haystack.iter().enumerate().skip(pos + 1) {
e = (e << 1) | self.table[usize::from(prev) << 8 | usize::from(c)];
if e & self.hit_bit == 0 {
return i + 1 - usize::from(self.window_len);
}
prev = c;
}
haystack.len()
}
#[inline(always)]
pub fn next_position_at_char_boundary(&self, haystack: &str, mut pos: usize) -> usize {
loop {
let candidate_pos = self.next_position(haystack.as_bytes(), pos);
if haystack.is_char_boundary(candidate_pos) {
return candidate_pos;
}
pos = candidate_pos + 1;
}
}
#[allow(clippy::unused_self)]
pub const fn heap_bytes(&self) -> usize {
Self::TABLE_LEN
}
#[allow(clippy::as_conversions)]
fn expected_candidate_rate(&self) -> f64 {
let mut zeros = [0u32; 8];
for &bits in self.table.iter() {
for (i, n) in zeros.iter_mut().enumerate() {
*n += u32::from((bits >> i) & 1 == 0);
}
}
let mut rate = 1.;
for &n in &zeros[..usize::from(self.window_len - 1)] {
rate *= f64::from(n) / Self::TABLE_LEN as f64;
}
rate
}
}
pub struct PrefilterBuilder {
table: Vec<u8>,
min_len: usize,
}
impl PrefilterBuilder {
pub fn new() -> Self {
Self {
table: vec![u8::MAX; Prefilter::TABLE_LEN],
min_len: usize::MAX,
}
}
pub fn add(&mut self, pattern: &[u8]) {
self.min_len = self.min_len.min(pattern.len());
let window = &pattern[..pattern.len().min(Prefilter::MAX_WINDOW_LEN)];
for (i, gram) in window.windows(2).enumerate() {
self.table[usize::from(gram[0]) << 8 | usize::from(gram[1])] &= !(1 << i);
}
}
pub fn build(self) -> Option<Prefilter> {
if self.min_len < Prefilter::MIN_WINDOW_LEN || self.min_len == usize::MAX {
return None;
}
let window_len = self.min_len.min(Prefilter::MAX_WINDOW_LEN);
let prefilter = Prefilter {
table: self.table.into_boxed_slice().try_into().unwrap(),
window_len: window_len.try_into().unwrap(),
hit_bit: 1 << (window_len - 2),
};
(prefilter.expected_candidate_rate() <= Prefilter::MAX_EXPECTED_CANDIDATE_RATE)
.then_some(prefilter)
}
}
impl Serializable for Prefilter {
fn serialize_to_vec(&self, dst: &mut Vec<u8>) {
self.window_len.serialize_to_vec(dst);
self.table.serialize_to_vec(dst);
}
fn deserialize_from_slice(src: &[u8]) -> Result<(Self, &[u8])> {
let (window_len, src) = u8::deserialize_from_slice(src)?;
if !(Self::MIN_WINDOW_LEN..=Self::MAX_WINDOW_LEN).contains(&usize::from(window_len)) {
return Err(DaachorseError::invalid_automaton());
}
let (table, rest) = Box::<[u8; Self::TABLE_LEN]>::deserialize_from_slice(src)?;
Ok((
Self {
table,
window_len,
hit_bit: 1 << (window_len - 2),
},
rest,
))
}
fn serialized_bytes() -> usize {
u8::serialized_bytes() + Self::TABLE_LEN
}
}
#[derive(Clone)]
pub struct PrefilterGate {
calls_in_window: u32,
gain_in_window: usize,
enabled: bool,
}
impl PrefilterGate {
const GATE_WINDOW_CALLS: u32 = 64;
const GATE_MIN_WINDOW_GAIN: usize = 512;
pub(crate) const fn new() -> Self {
Self {
calls_in_window: 0,
gain_in_window: 0,
enabled: true,
}
}
#[inline(always)]
pub(crate) const fn is_enabled(&self) -> bool {
self.enabled
}
#[inline(always)]
pub(crate) fn record(&mut self, gain: usize) {
self.calls_in_window += 1;
self.gain_in_window += gain;
if self.calls_in_window == Self::GATE_WINDOW_CALLS {
if self.gain_in_window < Self::GATE_MIN_WINDOW_GAIN {
self.enabled = false;
}
self.calls_in_window = 0;
self.gain_in_window = 0;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn build(patterns: &[&[u8]]) -> Option<Prefilter> {
let mut builder = PrefilterBuilder::new();
patterns.iter().for_each(|pattern| builder.add(pattern));
builder.build()
}
#[test]
fn test_build_none_for_short_min_pattern() {
assert!(build(&[b"aqua", b"a"]).is_none());
assert!(build(&[b"aqua", b""]).is_none());
}
#[test]
fn test_build_none_for_empty_pattern_set() {
assert!(build(&[]).is_none());
}
#[test]
fn test_build_none_when_table_saturated() {
let mut builder = PrefilterBuilder::new();
(0..=u16::MAX).for_each(|gram| builder.add(&gram.to_be_bytes()));
assert!(builder.build().is_none());
}
#[test]
fn test_long_pattern_windows_are_capped() {
let long = b"undine".repeat(20);
let pf = build(&[&long, b"neovenezia"]).unwrap();
let haystack = [&[b'x'; 50][..], &long].concat();
assert_eq!(pf.next_position(&haystack, 0), 50);
}
#[test]
fn test_next_position_reports_occurrence() {
let pf = build(&[b"aria", b"iris"]).unwrap();
assert_eq!(pf.next_position(b"xxxxariaxx", 0), 4);
assert_eq!(pf.next_position(b"xxxxariaxx", 5), 10);
assert_eq!(pf.next_position(b"xxxxariaxx", 10), 10);
}
#[test]
fn test_next_position_without_occurrence() {
let pf = build(&[b"aria", b"iris"]).unwrap();
assert_eq!(pf.next_position(b"xxxxxxxxxx", 0), 10);
assert_eq!(pf.next_position(b"xxarxrixia", 0), 10);
}
#[test]
fn test_next_position_may_report_false_positive() {
let pf = build(&[b"aria", b"iris"]).unwrap();
assert_eq!(pf.next_position(b"xxarisxxxx", 0), 2);
}
#[test]
fn test_min_window_pattern_builds() {
let pf = build(&[b"ai", b"aika"]).unwrap();
assert_eq!(pf.next_position(b"xxxxaixxxx", 0), 4);
assert_eq!(pf.next_position(b"xxxxxxxxxx", 0), 10);
}
#[test]
fn test_window_is_capped_at_exactly_nine_bytes() {
let pf = build(&[b"neovenezia"]).unwrap();
assert_eq!(pf.next_position(b"xxneovenezix", 0), 2);
assert_eq!(pf.next_position(b"xxneovenezxx", 0), 12);
}
#[test]
fn test_char_boundary_reports_occurrence() {
let pf = build(&["火星猫".as_bytes(), b"undine"]).unwrap();
let haystack = "アリア社長は火星猫です";
assert_eq!(pf.next_position_at_char_boundary(haystack, 0), 18);
assert_eq!(
pf.next_position_at_char_boundary(haystack, 19),
haystack.len()
);
}
#[test]
fn test_char_boundary_skips_mid_char_candidates() {
let pf = build(&[b"\x98\x9f\xe7\x8c\xab"]).unwrap();
let haystack = "星猫";
assert_eq!(pf.next_position(haystack.as_bytes(), 0), 1);
assert_eq!(
pf.next_position_at_char_boundary(haystack, 0),
haystack.len()
);
}
#[test]
fn test_char_boundary_candidate_right_after_mid_char_candidate() {
let pf = build(&[b"\x9f\xe7\x8c", "猫".as_bytes()]).unwrap();
let haystack = "星猫"; assert_eq!(pf.next_position(haystack.as_bytes(), 0), 2);
assert_eq!(pf.next_position_at_char_boundary(haystack, 0), 3);
}
#[test]
fn test_next_position_never_goes_backward() {
let pf = build(&[b"\x9f\xe7\x8c", "猫".as_bytes()]).unwrap();
let haystack = "星猫".as_bytes();
for pos in 0..=haystack.len() {
assert!(pf.next_position(haystack, pos) >= pos, "pos {pos}");
}
}
#[test]
fn test_gate_closes_after_low_gain_window() {
let mut gate = PrefilterGate::new();
for _ in 0..63 {
gate.record(7);
assert!(gate.is_enabled());
}
gate.record(7);
assert!(!gate.is_enabled());
}
#[test]
fn test_gate_stays_open_at_exact_threshold() {
let mut gate = PrefilterGate::new();
for _ in 0..64 {
gate.record(8);
}
assert!(gate.is_enabled());
}
#[test]
fn test_gate_evaluates_each_window_independently() {
let mut gate = PrefilterGate::new();
gate.record(100_000);
for _ in 0..63 {
gate.record(0);
}
assert!(gate.is_enabled());
for _ in 0..64 {
gate.record(0);
}
assert!(!gate.is_enabled());
}
#[test]
fn test_serialize_roundtrip() {
let pf = build(&[b"aqua", b"aria"]).unwrap();
let mut data = vec![];
pf.serialize_to_vec(&mut data);
assert_eq!(data.len(), Prefilter::serialized_bytes());
data.push(42);
let (other, rest) = Prefilter::deserialize_from_slice(&data).unwrap();
assert_eq!(&[42], rest);
assert!(pf == other);
}
#[test]
fn test_deserialize_rejects_invalid_data() {
let pf = build(&[b"gondola"]).unwrap();
let mut data = vec![];
pf.serialize_to_vec(&mut data);
let with_window_len = |window_len: u8| [&[window_len], &data[1..]].concat();
assert!(Prefilter::deserialize_from_slice(&with_window_len(0)).is_err());
assert!(Prefilter::deserialize_from_slice(&with_window_len(1)).is_err());
assert!(Prefilter::deserialize_from_slice(&with_window_len(10)).is_err());
assert!(Prefilter::deserialize_from_slice(&with_window_len(u8::MAX)).is_err());
assert!(Prefilter::deserialize_from_slice(&data[..data.len() - 1]).is_err());
}
}