use super::lazy_dfa::{LazyDFA, LazyDFAChar};
use super::product::{ProductAutomaton, ProductAutomatonChar};
use rustc_hash::FxHashMap;
use std::collections::VecDeque;
#[derive(Debug, Clone)]
struct CacheEntryChar {
result: bool,
min_distance: Option<u8>,
}
#[derive(Debug)]
pub struct MemoizedMatcherChar {
product: ProductAutomatonChar,
cache: FxHashMap<String, CacheEntryChar>,
lru_order: VecDeque<String>,
max_cache_size: usize,
hits: usize,
misses: usize,
}
impl MemoizedMatcherChar {
pub fn new(product: ProductAutomatonChar, max_cache_size: usize) -> Self {
Self {
product,
cache: FxHashMap::default(),
lru_order: VecDeque::new(),
max_cache_size,
hits: 0,
misses: 0,
}
}
pub fn accepts(&mut self, input: &str) -> bool {
let key = input.to_string();
if let Some(entry) = self.cache.get(&key).cloned() {
self.hits += 1;
self.update_lru(&key);
return entry.result;
}
self.misses += 1;
let result = self.product.accepts(input);
self.insert_cache(
key,
CacheEntryChar {
result,
min_distance: None,
},
);
result
}
pub fn min_distance(&mut self, input: &str) -> Option<u8> {
let key = input.to_string();
if let Some(entry) = self.cache.get(&key).cloned() {
if entry.min_distance.is_some() {
self.hits += 1;
self.update_lru(&key);
return entry.min_distance;
}
}
self.misses += 1;
let min_dist = self.product.min_distance(input);
let result = min_dist.is_some();
self.insert_cache(
key,
CacheEntryChar {
result,
min_distance: min_dist,
},
);
min_dist
}
fn update_lru(&mut self, key: &str) {
if let Some(pos) = self.lru_order.iter().position(|k| k == key) {
self.lru_order.remove(pos);
}
self.lru_order.push_front(key.to_string());
}
fn insert_cache(&mut self, key: String, entry: CacheEntryChar) {
while self.cache.len() >= self.max_cache_size && !self.lru_order.is_empty() {
if let Some(evict_key) = self.lru_order.pop_back() {
self.cache.remove(&evict_key);
}
}
self.cache.insert(key.clone(), entry);
self.lru_order.push_front(key);
}
pub fn stats(&self) -> MemoizedStats {
MemoizedStats {
size: self.cache.len(),
max_size: self.max_cache_size,
hits: self.hits,
misses: self.misses,
hit_rate: if self.hits + self.misses > 0 {
self.hits as f64 / (self.hits + self.misses) as f64
} else {
0.0
},
}
}
pub fn clear(&mut self) {
self.cache.clear();
self.lru_order.clear();
self.hits = 0;
self.misses = 0;
}
pub fn product(&self) -> &ProductAutomatonChar {
&self.product
}
}
#[derive(Debug, Clone)]
struct CacheEntry {
result: bool,
min_distance: Option<u8>,
}
#[derive(Debug)]
pub struct MemoizedMatcher {
product: ProductAutomaton,
cache: FxHashMap<Vec<u8>, CacheEntry>,
lru_order: VecDeque<Vec<u8>>,
max_cache_size: usize,
hits: usize,
misses: usize,
}
impl MemoizedMatcher {
pub fn new(product: ProductAutomaton, max_cache_size: usize) -> Self {
Self {
product,
cache: FxHashMap::default(),
lru_order: VecDeque::new(),
max_cache_size,
hits: 0,
misses: 0,
}
}
pub fn accepts(&mut self, input: &[u8]) -> bool {
let key = input.to_vec();
if let Some(entry) = self.cache.get(&key).cloned() {
self.hits += 1;
self.update_lru(&key);
return entry.result;
}
self.misses += 1;
let result = self.product.accepts(input);
self.insert_cache(
key,
CacheEntry {
result,
min_distance: None,
},
);
result
}
pub fn min_distance(&mut self, input: &[u8]) -> Option<u8> {
let key = input.to_vec();
if let Some(entry) = self.cache.get(&key).cloned() {
if entry.min_distance.is_some() {
self.hits += 1;
self.update_lru(&key);
return entry.min_distance;
}
}
self.misses += 1;
let min_dist = self.product.min_distance(input);
let result = min_dist.is_some();
self.insert_cache(
key,
CacheEntry {
result,
min_distance: min_dist,
},
);
min_dist
}
fn update_lru(&mut self, key: &[u8]) {
if let Some(pos) = self.lru_order.iter().position(|k| k == key) {
self.lru_order.remove(pos);
}
self.lru_order.push_front(key.to_vec());
}
fn insert_cache(&mut self, key: Vec<u8>, entry: CacheEntry) {
while self.cache.len() >= self.max_cache_size && !self.lru_order.is_empty() {
if let Some(evict_key) = self.lru_order.pop_back() {
self.cache.remove(&evict_key);
}
}
self.cache.insert(key.clone(), entry);
self.lru_order.push_front(key);
}
pub fn stats(&self) -> MemoizedStats {
MemoizedStats {
size: self.cache.len(),
max_size: self.max_cache_size,
hits: self.hits,
misses: self.misses,
hit_rate: if self.hits + self.misses > 0 {
self.hits as f64 / (self.hits + self.misses) as f64
} else {
0.0
},
}
}
pub fn clear(&mut self) {
self.cache.clear();
self.lru_order.clear();
self.hits = 0;
self.misses = 0;
}
pub fn product(&self) -> &ProductAutomaton {
&self.product
}
}
#[derive(Debug)]
pub struct MemoizedLazyDFAChar {
dfa: LazyDFAChar,
result_cache: FxHashMap<String, bool>,
lru_order: VecDeque<String>,
max_cache_size: usize,
hits: usize,
misses: usize,
}
impl MemoizedLazyDFAChar {
pub fn new(dfa: LazyDFAChar, max_cache_size: usize) -> Self {
Self {
dfa,
result_cache: FxHashMap::default(),
lru_order: VecDeque::new(),
max_cache_size,
hits: 0,
misses: 0,
}
}
pub fn accepts(&mut self, input: &str) -> bool {
let key = input.to_string();
if let Some(&result) = self.result_cache.get(&key) {
self.hits += 1;
self.update_lru(&key);
return result;
}
self.misses += 1;
let result = self.dfa.accepts(input);
self.insert_cache(key, result);
result
}
fn update_lru(&mut self, key: &str) {
if let Some(pos) = self.lru_order.iter().position(|k| k == key) {
self.lru_order.remove(pos);
}
self.lru_order.push_front(key.to_string());
}
fn insert_cache(&mut self, key: String, result: bool) {
while self.result_cache.len() >= self.max_cache_size && !self.lru_order.is_empty() {
if let Some(evict_key) = self.lru_order.pop_back() {
self.result_cache.remove(&evict_key);
}
}
self.result_cache.insert(key.clone(), result);
self.lru_order.push_front(key);
}
pub fn stats(&self) -> MemoizedStats {
MemoizedStats {
size: self.result_cache.len(),
max_size: self.max_cache_size,
hits: self.hits,
misses: self.misses,
hit_rate: if self.hits + self.misses > 0 {
self.hits as f64 / (self.hits + self.misses) as f64
} else {
0.0
},
}
}
pub fn clear(&mut self) {
self.result_cache.clear();
self.lru_order.clear();
self.dfa.clear_cache();
self.hits = 0;
self.misses = 0;
}
pub fn dfa(&self) -> &LazyDFAChar {
&self.dfa
}
pub fn dfa_mut(&mut self) -> &mut LazyDFAChar {
&mut self.dfa
}
}
#[derive(Debug)]
pub struct MemoizedLazyDFA {
dfa: LazyDFA,
result_cache: FxHashMap<Vec<u8>, bool>,
lru_order: VecDeque<Vec<u8>>,
max_cache_size: usize,
hits: usize,
misses: usize,
}
impl MemoizedLazyDFA {
pub fn new(dfa: LazyDFA, max_cache_size: usize) -> Self {
Self {
dfa,
result_cache: FxHashMap::default(),
lru_order: VecDeque::new(),
max_cache_size,
hits: 0,
misses: 0,
}
}
pub fn accepts(&mut self, input: &[u8]) -> bool {
let key = input.to_vec();
if let Some(&result) = self.result_cache.get(&key) {
self.hits += 1;
self.update_lru(&key);
return result;
}
self.misses += 1;
let result = self.dfa.accepts(input);
self.insert_cache(key, result);
result
}
fn update_lru(&mut self, key: &[u8]) {
if let Some(pos) = self.lru_order.iter().position(|k| k == key) {
self.lru_order.remove(pos);
}
self.lru_order.push_front(key.to_vec());
}
fn insert_cache(&mut self, key: Vec<u8>, result: bool) {
while self.result_cache.len() >= self.max_cache_size && !self.lru_order.is_empty() {
if let Some(evict_key) = self.lru_order.pop_back() {
self.result_cache.remove(&evict_key);
}
}
self.result_cache.insert(key.clone(), result);
self.lru_order.push_front(key);
}
pub fn stats(&self) -> MemoizedStats {
MemoizedStats {
size: self.result_cache.len(),
max_size: self.max_cache_size,
hits: self.hits,
misses: self.misses,
hit_rate: if self.hits + self.misses > 0 {
self.hits as f64 / (self.hits + self.misses) as f64
} else {
0.0
},
}
}
pub fn clear(&mut self) {
self.result_cache.clear();
self.lru_order.clear();
self.dfa.clear_cache();
self.hits = 0;
self.misses = 0;
}
pub fn dfa(&self) -> &LazyDFA {
&self.dfa
}
pub fn dfa_mut(&mut self) -> &mut LazyDFA {
&mut self.dfa
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MemoizedStats {
pub size: usize,
pub max_size: usize,
pub hits: usize,
pub misses: usize,
pub hit_rate: f64,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::phonetic::nfa::compiler::{compile, compile_bytes};
use crate::phonetic::regex::{parse, parse_bytes};
#[test]
fn test_memoized_matcher_accepts() {
let nfa = compile(&parse("(ph|f)one").expect("test fixture: parse must be Ok"))
.expect("test fixture: compile must be Ok");
let product = ProductAutomatonChar::new(nfa, 1);
let mut cache = MemoizedMatcherChar::new(product, 100);
assert!(cache.accepts("phone"));
let stats = cache.stats();
assert_eq!(stats.misses, 1);
assert_eq!(stats.hits, 0);
assert!(cache.accepts("phone"));
let stats = cache.stats();
assert_eq!(stats.misses, 1);
assert_eq!(stats.hits, 1);
assert!(stats.hit_rate > 0.4); }
#[test]
fn test_memoized_matcher_min_distance() {
let nfa = compile(&parse("phone").expect("test fixture: parse must be Ok"))
.expect("test fixture: compile must be Ok");
let product = ProductAutomatonChar::new(nfa, 2);
let mut cache = MemoizedMatcherChar::new(product, 100);
assert_eq!(cache.min_distance("phone"), Some(0));
assert_eq!(cache.min_distance("phon"), Some(1));
assert_eq!(cache.min_distance("phone"), Some(0));
let stats = cache.stats();
assert!(stats.hits >= 1);
}
#[test]
fn test_memoized_matcher_lru_eviction() {
let nfa = compile(&parse("test").expect("test fixture: parse must be Ok"))
.expect("test fixture: compile must be Ok");
let product = ProductAutomatonChar::new(nfa, 1);
let mut cache = MemoizedMatcherChar::new(product, 3);
cache.accepts("a");
cache.accepts("b");
cache.accepts("c");
assert_eq!(cache.stats().size, 3);
cache.accepts("d");
assert_eq!(cache.stats().size, 3);
let hits_before = cache.stats().hits;
cache.accepts("a");
assert_eq!(cache.stats().hits, hits_before); }
#[test]
fn test_memoized_matcher_clear() {
let nfa = compile(&parse("test").expect("test fixture: parse must be Ok"))
.expect("test fixture: compile must be Ok");
let product = ProductAutomatonChar::new(nfa, 1);
let mut cache = MemoizedMatcherChar::new(product, 100);
cache.accepts("a");
cache.accepts("b");
assert!(cache.stats().size > 0);
cache.clear();
let stats = cache.stats();
assert_eq!(stats.size, 0);
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 0);
}
#[test]
fn test_memoized_lazy_dfa() {
let nfa = compile(&parse("hello").expect("test fixture: parse must be Ok"))
.expect("test fixture: compile must be Ok");
let dfa = LazyDFAChar::new(nfa);
let mut cache = MemoizedLazyDFAChar::new(dfa, 100);
assert!(cache.accepts("hello"));
assert_eq!(cache.stats().misses, 1);
assert!(cache.accepts("hello"));
assert_eq!(cache.stats().hits, 1);
}
#[test]
fn test_memoized_bytes() {
let nfa = compile_bytes(&parse_bytes(b"test").expect("test fixture: parse must be Ok"))
.expect("test fixture: compile must be Ok");
let product = ProductAutomaton::new(nfa, 1);
let mut cache = MemoizedMatcher::new(product, 100);
assert!(cache.accepts(b"test"));
assert!(cache.accepts(b"test")); assert_eq!(cache.stats().hits, 1);
}
#[test]
fn test_memoized_lazy_dfa_bytes() {
let nfa = compile_bytes(&parse_bytes(b"world").expect("test fixture: parse must be Ok"))
.expect("test fixture: compile must be Ok");
let dfa = LazyDFA::new(nfa);
let mut cache = MemoizedLazyDFA::new(dfa, 100);
assert!(cache.accepts(b"world"));
assert!(cache.accepts(b"world")); assert_eq!(cache.stats().hits, 1);
}
#[test]
fn test_hit_rate_calculation() {
let nfa = compile(&parse("x").expect("test fixture: parse must be Ok"))
.expect("test fixture: compile must be Ok");
let product = ProductAutomatonChar::new(nfa, 0);
let mut cache = MemoizedMatcherChar::new(product, 100);
cache.accepts("a");
cache.accepts("a");
cache.accepts("a");
cache.accepts("a");
let stats = cache.stats();
assert_eq!(stats.hits, 3);
assert_eq!(stats.misses, 1);
assert!((stats.hit_rate - 0.75).abs() < 0.001);
}
}