use std::marker::PhantomData;
use lling_llang::prelude::{
LazyState, LazyWfst, StateId, StateSource, TropicalWeight, WeightedTransition, Wfst,
};
use rustc_hash::FxHashMap;
use smallvec::SmallVec;
use liblevenshtein::transducer::Algorithm;
use liblevenshtein::wallbreaker::{WallBreaker, WallBreakerResult};
use libdictenstein::substring::{BidirectionalDictionaryNode, SubstringDictionary};
use libdictenstein::{Dictionary, DictionaryNode};
#[derive(Clone)]
struct CachedWallBreakerState {
is_final: bool,
final_weight: TropicalWeight,
transitions: SmallVec<[WeightedTransition<char, TropicalWeight>; 4]>,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
struct WallBreakerStateKey {
result_index: u32,
char_position: u32,
}
#[derive(Clone)]
pub struct WallBreakerWfst<'a, D>
where
D: Dictionary + SubstringDictionary + Clone + Send + Sync,
D::Node: BidirectionalDictionaryNode,
<D::Node as DictionaryNode>::Unit: Into<u32>,
{
_dictionary: PhantomData<&'a D>,
query: String,
max_distance: usize,
algorithm: Algorithm,
results: Vec<WallBreakerResult>,
state_map: FxHashMap<StateId, WallBreakerStateKey>,
reverse_map: FxHashMap<WallBreakerStateKey, StateId>,
cache: FxHashMap<StateId, CachedWallBreakerState>,
next_state_id: StateId,
cache_policy: lling_llang::wfst::CachePolicy,
}
impl<'a, D> WallBreakerWfst<'a, D>
where
D: Dictionary + SubstringDictionary + Clone + Send + Sync,
D::Node: BidirectionalDictionaryNode,
<D::Node as DictionaryNode>::Unit: Into<u32>,
{
pub fn new(dictionary: &'a D, query: &str, max_distance: usize) -> Self {
Self::with_algorithm(dictionary, query, max_distance, Algorithm::Standard)
}
pub fn with_algorithm(
dictionary: &'a D,
query: &str,
max_distance: usize,
algorithm: Algorithm,
) -> Self {
let wb = WallBreaker::with_algorithm(dictionary, max_distance, algorithm);
let results: Vec<_> = wb.query(query).collect();
let mut wfst = Self {
_dictionary: PhantomData,
query: query.to_string(),
max_distance,
algorithm,
results,
state_map: FxHashMap::default(),
reverse_map: FxHashMap::default(),
cache: FxHashMap::default(),
next_state_id: 0,
cache_policy: lling_llang::wfst::CachePolicy::CacheAll,
};
let start_key = WallBreakerStateKey {
result_index: u32::MAX, char_position: 0,
};
wfst.register_state(start_key);
wfst
}
fn register_state(&mut self, key: WallBreakerStateKey) -> StateId {
if let Some(&id) = self.reverse_map.get(&key) {
return id;
}
let id = self.next_state_id;
self.next_state_id += 1;
self.state_map.insert(id, key);
self.reverse_map.insert(key, id);
id
}
#[inline]
pub fn query(&self) -> &str {
&self.query
}
#[inline]
pub fn max_distance(&self) -> usize {
self.max_distance
}
#[inline]
pub fn algorithm(&self) -> Algorithm {
self.algorithm
}
#[inline]
pub fn num_results(&self) -> usize {
self.results.len()
}
fn ensure_state(&mut self, state_id: StateId) {
if self.cache.contains_key(&state_id) {
return;
}
let key = match self.state_map.get(&state_id) {
Some(k) => *k,
None => return,
};
let (is_final, final_weight, transitions) = if key.result_index == u32::MAX {
self.compute_super_start_transitions()
} else {
self.compute_result_state(&key)
};
self.cache.insert(
state_id,
CachedWallBreakerState {
is_final,
final_weight,
transitions,
},
);
}
fn compute_super_start_transitions(
&mut self,
) -> (
bool,
TropicalWeight,
SmallVec<[WeightedTransition<char, TropicalWeight>; 4]>,
) {
let mut transitions = SmallVec::new();
let result_info: Vec<_> = self
.results
.iter()
.enumerate()
.filter(|(_, r)| !r.term.is_empty())
.map(|(idx, r)| {
let first_char = r
.term
.chars()
.next()
.expect("filtered out empty terms above");
let term_len = r.term.len();
let distance = r.distance;
(idx, first_char, term_len, distance)
})
.collect();
for (result_idx, first_char, term_len, distance) in result_info {
let new_key = WallBreakerStateKey {
result_index: result_idx as u32,
char_position: 1,
};
let new_id = self.register_state(new_key);
let weight = if term_len == 1 {
distance as f64
} else {
0.0 };
transitions.push(WeightedTransition::new(
0,
Some(first_char),
Some(first_char),
new_id,
TropicalWeight::new(weight),
));
}
let has_empty_result = self
.results
.iter()
.any(|r| r.term.is_empty() && r.distance <= self.max_distance);
let final_weight = if has_empty_result {
self.results
.iter()
.filter(|r| r.term.is_empty())
.map(|r| r.distance as f64)
.fold(f64::INFINITY, f64::min)
} else {
f64::INFINITY
};
(
has_empty_result,
TropicalWeight::new(final_weight),
transitions,
)
}
fn compute_result_state(
&mut self,
key: &WallBreakerStateKey,
) -> (
bool,
TropicalWeight,
SmallVec<[WeightedTransition<char, TropicalWeight>; 4]>,
) {
let mut transitions = SmallVec::new();
let result_info = match self.results.get(key.result_index as usize) {
Some(r) => {
let term_chars: Vec<char> = r.term.chars().collect();
let distance = r.distance;
Some((term_chars, distance))
}
None => None,
};
let (term_chars, distance) = match result_info {
Some(info) => info,
None => return (false, TropicalWeight::infinity(), transitions),
};
let pos = key.char_position as usize;
let is_final = pos >= term_chars.len();
let final_weight = if is_final {
TropicalWeight::new(distance as f64)
} else {
TropicalWeight::infinity()
};
if pos < term_chars.len() {
let next_char = term_chars[pos];
let new_key = WallBreakerStateKey {
result_index: key.result_index,
char_position: (pos + 1) as u32,
};
let new_id = self.register_state(new_key);
let weight = if pos + 1 >= term_chars.len() {
distance as f64
} else {
0.0
};
transitions.push(WeightedTransition::new(
0,
Some(next_char),
Some(next_char),
new_id,
TropicalWeight::new(weight),
));
}
(is_final, final_weight, transitions)
}
}
impl<'a, D> Wfst<char, TropicalWeight> for WallBreakerWfst<'a, D>
where
D: Dictionary + SubstringDictionary + Clone + Send + Sync,
D::Node: BidirectionalDictionaryNode,
<D::Node as DictionaryNode>::Unit: Into<u32>,
{
fn start(&self) -> StateId {
0
}
fn is_final(&self, state: StateId) -> bool {
self.cache.get(&state).map(|s| s.is_final).unwrap_or(false)
}
fn final_weight(&self, state: StateId) -> TropicalWeight {
self.cache
.get(&state)
.map(|s| s.final_weight)
.unwrap_or_else(TropicalWeight::infinity)
}
fn transitions(&self, state: StateId) -> &[WeightedTransition<char, TropicalWeight>] {
static EMPTY: &[WeightedTransition<char, TropicalWeight>] = &[];
self.cache
.get(&state)
.map(|s| s.transitions.as_slice())
.unwrap_or(EMPTY)
}
fn num_states(&self) -> usize {
self.state_map.len()
}
#[inline]
fn is_empty(&self) -> bool {
self.results.is_empty()
}
#[inline]
fn is_valid_state(&self, state: StateId) -> bool {
self.state_map.contains_key(&state)
}
}
impl<'a, D> LazyWfst<char, TropicalWeight> for WallBreakerWfst<'a, D>
where
D: Dictionary + SubstringDictionary + Clone + Send + Sync,
D::Node: BidirectionalDictionaryNode,
<D::Node as DictionaryNode>::Unit: Into<u32>,
{
fn is_expanded(&self, state: StateId) -> bool {
self.cache.contains_key(&state)
}
fn expand(&mut self, state: StateId) {
self.ensure_state(state);
}
fn transitions_lazy(&mut self, state: StateId) -> &[WeightedTransition<char, TropicalWeight>] {
self.ensure_state(state);
self.transitions(state)
}
fn cache_policy(&self) -> lling_llang::wfst::CachePolicy {
self.cache_policy
}
fn set_cache_policy(&mut self, policy: lling_llang::wfst::CachePolicy) {
self.cache_policy = policy;
}
fn computed_states(&self) -> usize {
self.cache.len()
}
fn clear_cache(&mut self) {
self.cache.clear();
}
}
impl<'a, D> StateSource<char, TropicalWeight> for WallBreakerWfst<'a, D>
where
D: Dictionary + SubstringDictionary + Clone + Send + Sync,
D::Node: BidirectionalDictionaryNode,
<D::Node as DictionaryNode>::Unit: Into<u32>,
{
fn compute_state(&self, _state: StateId) -> LazyState<char, TropicalWeight> {
LazyState::Pending
}
fn start(&self) -> StateId {
0
}
fn num_states_hint(&self) -> Option<usize> {
let total_chars: usize = self.results.iter().map(|r| r.term.len()).sum();
Some(1 + total_chars + self.results.len())
}
}
pub struct WallBreakerWfstBuilder<'a, D>
where
D: Dictionary + SubstringDictionary + Clone + Send + Sync,
D::Node: BidirectionalDictionaryNode,
<D::Node as DictionaryNode>::Unit: Into<u32>,
{
dictionary: &'a D,
query: Option<String>,
max_distance: usize,
algorithm: Algorithm,
}
impl<'a, D> WallBreakerWfstBuilder<'a, D>
where
D: Dictionary + SubstringDictionary + Clone + Send + Sync,
D::Node: BidirectionalDictionaryNode,
<D::Node as DictionaryNode>::Unit: Into<u32>,
{
pub fn new(dictionary: &'a D) -> Self {
Self {
dictionary,
query: None,
max_distance: 2,
algorithm: Algorithm::Standard,
}
}
pub fn query(mut self, query: &str) -> Self {
self.query = Some(query.to_string());
self
}
pub fn max_distance(mut self, distance: usize) -> Self {
self.max_distance = distance;
self
}
pub fn algorithm(mut self, algorithm: Algorithm) -> Self {
self.algorithm = algorithm;
self
}
pub fn standard(mut self) -> Self {
self.algorithm = Algorithm::Standard;
self
}
pub fn transposition(mut self) -> Self {
self.algorithm = Algorithm::Transposition;
self
}
pub fn merge_and_split(mut self) -> Self {
self.algorithm = Algorithm::MergeAndSplit;
self
}
pub fn build(self) -> Result<WallBreakerWfst<'a, D>, String> {
let query = self.query.ok_or_else(|| "Query not set".to_string())?;
Ok(WallBreakerWfst::with_algorithm(
self.dictionary,
&query,
self.max_distance,
self.algorithm,
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use libdictenstein::scdawg::Scdawg;
#[test]
fn test_wallbreaker_wfst_creation() {
let dict = Scdawg::<()>::from_terms(vec!["hello", "help", "world"]);
let wfst = WallBreakerWfst::new(&dict, "helo", 2);
assert!(!wfst.is_empty());
assert_eq!(wfst.query(), "helo");
assert_eq!(wfst.max_distance(), 2);
}
#[test]
fn test_wallbreaker_wfst_results() {
let dict = Scdawg::<()>::from_terms(vec!["hello", "help", "world"]);
let wfst = WallBreakerWfst::new(&dict, "helo", 2);
assert!(wfst.num_results() > 0);
}
#[test]
fn test_wallbreaker_wfst_start_state() {
let dict = Scdawg::<()>::from_terms(vec!["test"]);
let wfst = WallBreakerWfst::new(&dict, "tset", 2);
let start = Wfst::start(&wfst);
assert!(wfst.is_valid_state(start));
}
#[test]
fn test_wallbreaker_wfst_lazy_expansion() {
let dict = Scdawg::<()>::from_terms(vec!["hello", "help"]);
let mut wfst = WallBreakerWfst::new(&dict, "helo", 2);
let start = Wfst::start(&wfst);
assert!(!wfst.is_expanded(start));
wfst.expand(start);
assert!(wfst.is_expanded(start));
}
#[test]
fn test_wallbreaker_wfst_transitions() {
let dict = Scdawg::<()>::from_terms(vec!["hello", "help"]);
let mut wfst = WallBreakerWfst::new(&dict, "helo", 2);
let start = Wfst::start(&wfst);
wfst.expand(start);
let transitions = wfst.transitions(start);
assert!(!transitions.is_empty() || wfst.num_results() == 0);
}
#[test]
fn test_wallbreaker_wfst_with_algorithm() {
let dict = Scdawg::<()>::from_terms(vec!["test", "tset"]);
let wfst_std = WallBreakerWfst::new(&dict, "tset", 1);
let wfst_trans =
WallBreakerWfst::with_algorithm(&dict, "tset", 1, Algorithm::Transposition);
assert!(matches!(wfst_std.algorithm(), Algorithm::Standard));
assert!(matches!(wfst_trans.algorithm(), Algorithm::Transposition));
}
#[test]
fn test_builder_creation() {
let dict = Scdawg::<()>::from_terms(vec!["test"]);
let result = WallBreakerWfstBuilder::new(&dict)
.query("tset")
.max_distance(2)
.build();
assert!(result.is_ok());
}
#[test]
fn test_builder_no_query() {
let dict = Scdawg::<()>::from_terms(vec!["test"]);
let result = WallBreakerWfstBuilder::new(&dict).build();
assert!(result.is_err());
}
#[test]
fn test_builder_with_transposition() {
let dict = Scdawg::<()>::from_terms(vec!["test"]);
let result = WallBreakerWfstBuilder::new(&dict)
.query("tset")
.transposition()
.build();
assert!(result.is_ok());
assert!(matches!(
result.expect("test fixture: build must be Ok").algorithm(),
Algorithm::Transposition
));
}
#[test]
fn test_builder_with_merge_and_split() {
let dict = Scdawg::<()>::from_terms(vec!["test"]);
let result = WallBreakerWfstBuilder::new(&dict)
.query("test")
.merge_and_split()
.build();
assert!(result.is_ok());
assert!(matches!(
result.expect("test fixture: build must be Ok").algorithm(),
Algorithm::MergeAndSplit
));
}
#[test]
fn test_wfst_cache_operations() {
let dict = Scdawg::<()>::from_terms(vec!["test"]);
let mut wfst = WallBreakerWfst::new(&dict, "test", 1);
wfst.expand(0);
let before = wfst.computed_states();
wfst.clear_cache();
assert_eq!(wfst.computed_states(), 0);
assert!(before > 0);
}
#[test]
fn test_wfst_num_states_hint() {
let dict = Scdawg::<()>::from_terms(vec!["hello", "world"]);
let wfst = WallBreakerWfst::new(&dict, "helo", 2);
let hint = StateSource::<char, TropicalWeight>::num_states_hint(&wfst);
assert!(hint.is_some());
assert!(hint.expect("expected Some hint in test") > 0);
}
#[test]
fn test_wallbreaker_wfst_empty_results() {
let dict = Scdawg::<()>::from_terms(vec!["hello", "world"]);
let wfst = WallBreakerWfst::new(&dict, "zzzzz", 1);
assert!(wfst.is_empty() || wfst.num_results() == 0);
}
#[test]
fn test_wallbreaker_wfst_exact_match() {
let dict = Scdawg::<()>::from_terms(vec!["hello", "world"]);
let wfst = WallBreakerWfst::new(&dict, "hello", 0);
assert_eq!(wfst.num_results(), 1);
}
#[test]
fn test_wallbreaker_wfst_trace_path() {
let dict = Scdawg::<()>::from_terms(vec!["cat"]);
let mut wfst = WallBreakerWfst::new(&dict, "cat", 0);
let start = Wfst::start(&wfst);
wfst.expand(start);
let trans = wfst.transitions(start);
if !trans.is_empty() {
let next = trans[0].to;
wfst.expand(next);
let mut current = next;
for _ in 0..10 {
let t = wfst.transitions(current);
if t.is_empty() || wfst.is_final(current) {
break;
}
current = t[0].to;
wfst.expand(current);
}
}
}
}