1use std::sync::Arc;
14
15use lling_llang::prelude::{
16 LazyState, Semiring, StateId, StateSource, TropicalWeight, WeightedTransition,
17};
18use rustc_hash::FxHashMap;
19use smallvec::SmallVec;
20
21use liblevenshtein::transducer::universal::{
22 CharacteristicVector, PositionVariant, UniversalAutomaton, UniversalState,
23};
24use libdictenstein::{Dictionary, DictionaryNode};
25
26use crate::state_encoding;
27
28pub struct UniversalStateRegistry<V: PositionVariant> {
33 state_to_id: FxHashMap<UniversalStateKey, u32>,
35 id_to_state: Vec<UniversalState<V>>,
37}
38
39#[derive(Clone, PartialEq, Eq, Hash)]
41struct UniversalStateKey(Vec<u8>);
42
43impl<V: PositionVariant> UniversalStateRegistry<V> {
44 pub fn new(max_distance: u8) -> Self {
46 let mut registry = Self {
47 state_to_id: FxHashMap::default(),
48 id_to_state: Vec::new(),
49 };
50
51 let initial = UniversalState::initial(max_distance);
53 registry.register_state(initial);
54
55 registry
56 }
57
58 pub fn register_state(&mut self, state: UniversalState<V>) -> u32 {
60 let key = self.state_to_key(&state);
61 if let Some(&id) = self.state_to_id.get(&key) {
62 return id;
63 }
64
65 let id = self.id_to_state.len() as u32;
66 self.state_to_id.insert(key, id);
67 self.id_to_state.push(state);
68 id
69 }
70
71 pub fn get_state(&self, id: u32) -> Option<&UniversalState<V>> {
73 self.id_to_state.get(id as usize)
74 }
75
76 fn state_to_key(&self, state: &UniversalState<V>) -> UniversalStateKey {
78 let mut bytes = Vec::new();
80 for pos in state.positions() {
81 let (pos_type, offset, errors) = if pos.is_i_type() {
83 (0u8, pos.offset(), pos.errors())
84 } else {
85 (1u8, pos.offset(), pos.errors())
86 };
87 bytes.push(pos_type);
88 bytes.extend_from_slice(&(offset as i16).to_le_bytes());
89 bytes.push(errors);
90 }
91 UniversalStateKey(bytes)
92 }
93
94 pub fn len(&self) -> usize {
96 self.id_to_state.len()
97 }
98
99 pub fn is_empty(&self) -> bool {
101 self.id_to_state.is_empty()
102 }
103}
104
105#[derive(Clone)]
116pub struct UniversalLevenshteinStateSource<V, D>
117where
118 V: PositionVariant + Clone + Send + Sync,
119 V::State: Send + Sync,
120 D: Dictionary + Clone + Send + Sync,
121 D::Node: Send + Sync,
122 <D::Node as DictionaryNode>::Unit: Into<char> + TryFrom<char> + Copy + Send + Sync,
123{
124 dictionary: D,
126 query_chars: Arc<Vec<char>>,
128 automaton: UniversalAutomaton<V>,
130 state_registry: Arc<std::sync::RwLock<UniversalStateRegistry<V>>>,
132 node_registry: Arc<std::sync::RwLock<NodeRegistry<D::Node>>>,
134 max_automaton_states: u32,
136}
137
138struct NodeRegistry<N: DictionaryNode> {
140 node_to_id: FxHashMap<u64, u32>,
142 id_to_node: Vec<N>,
144}
145
146impl<N: DictionaryNode> NodeRegistry<N> {
147 fn new(root: N) -> Self {
148 let mut registry = Self {
149 node_to_id: FxHashMap::default(),
150 id_to_node: Vec::new(),
151 };
152 registry.register_node(root, 0);
154 registry
155 }
156
157 fn register_node(&mut self, node: N, path_hash: u64) -> u32 {
158 if let Some(&id) = self.node_to_id.get(&path_hash) {
159 return id;
160 }
161
162 let id = self.id_to_node.len() as u32;
163 self.node_to_id.insert(path_hash, id);
164 self.id_to_node.push(node);
165 id
166 }
167
168 fn get_node(&self, id: u32) -> Option<&N> {
169 self.id_to_node.get(id as usize)
170 }
171}
172
173impl<V, D> UniversalLevenshteinStateSource<V, D>
174where
175 V: PositionVariant + Clone + Send + Sync,
176 V::State: Send + Sync,
177 D: Dictionary + Clone + Send + Sync,
178 D::Node: Send + Sync,
179 <D::Node as DictionaryNode>::Unit: Into<char> + TryFrom<char> + Copy + Send + Sync,
180{
181 pub fn new(dictionary: &D, query: &str, max_distance: u8) -> Self {
189 let query_chars: Vec<char> = query.chars().collect();
190 let max_automaton_states =
191 state_encoding::estimate_automaton_states(query_chars.len(), max_distance as usize);
192
193 let root = dictionary.root();
194 let node_registry = NodeRegistry::new(root);
195 let state_registry = UniversalStateRegistry::new(max_distance);
196
197 Self {
198 dictionary: dictionary.clone(),
199 query_chars: Arc::new(query_chars),
200 automaton: UniversalAutomaton::new(max_distance),
201 state_registry: Arc::new(std::sync::RwLock::new(state_registry)),
202 node_registry: Arc::new(std::sync::RwLock::new(node_registry)),
203 max_automaton_states,
204 }
205 }
206
207 pub fn query(&self) -> String {
209 self.query_chars.iter().collect()
210 }
211
212 fn relevant_subword(&self, word: &[char], position: usize) -> String {
216 let n = self.automaton.max_distance() as i32;
217 let i = position as i32;
218
219 let start = i - n;
220 let v = std::cmp::min(word.len() as i32, i + n + 1);
221
222 let mut result = String::new();
223 for pos in start..=v {
224 if pos < 1 {
225 result.push('$');
226 } else if pos <= word.len() as i32 {
227 let idx = (pos - 1) as usize;
228 result.push(word[idx]);
229 }
230 }
231 result
232 }
233
234 fn compute_transitions(
236 &self,
237 dict_node_id: u32,
238 automaton_state_id: u32,
239 ) -> (
240 bool,
241 TropicalWeight,
242 SmallVec<[WeightedTransition<char, TropicalWeight>; 4]>,
243 ) {
244 let node_registry = self.node_registry.read().expect("Lock poisoned");
245 let state_registry = self.state_registry.read().expect("Lock poisoned");
246
247 let dict_node = match node_registry.get_node(dict_node_id) {
248 Some(node) => node.clone(),
249 None => {
250 return (false, TropicalWeight::zero(), SmallVec::new());
251 }
252 };
253
254 let automaton_state = match state_registry.get_state(automaton_state_id) {
255 Some(state) => state.clone(),
256 None => {
257 return (false, TropicalWeight::zero(), SmallVec::new());
258 }
259 };
260
261 drop(node_registry);
262 drop(state_registry);
263
264 let mut transitions = SmallVec::new();
265 let query_len = self.query_chars.len();
266
267 for (unit, child_node) in dict_node.edges() {
269 let dict_char: char = unit.into();
270
271 let child_node_id = {
273 let mut registry = self.node_registry.write().expect("Lock poisoned");
274 let path_hash = compute_path_hash(dict_node_id, dict_char);
275 registry.register_node(child_node.clone(), path_hash)
276 };
277
278 let current_pos = self.estimate_query_position(&automaton_state);
285
286 if current_pos <= query_len {
287 let subword = self.relevant_subword(&self.query_chars, current_pos);
289 let bit_vector = CharacteristicVector::new(dict_char, &subword);
290
291 if let Some(next_auto_state) = automaton_state.transition(&bit_vector, current_pos)
293 {
294 let next_auto_id = {
296 let mut registry = self.state_registry.write().expect("Lock poisoned");
297 registry.register_state(next_auto_state.clone())
298 };
299
300 let cost = if current_pos > 0
302 && current_pos <= query_len
303 && self.query_chars[current_pos - 1] == dict_char
304 {
305 0.0 } else {
307 1.0 };
309
310 let from_state = state_encoding::encode(
311 dict_node_id,
312 automaton_state_id,
313 self.max_automaton_states,
314 );
315
316 let target_state = state_encoding::encode(
317 child_node_id,
318 next_auto_id,
319 self.max_automaton_states,
320 );
321
322 transitions.push(WeightedTransition::new(
323 from_state,
324 Some(dict_char),
325 Some(dict_char),
326 target_state,
327 TropicalWeight::new(cost),
328 ));
329 }
330 }
331 }
332
333 let is_final = dict_node.is_final() && automaton_state.is_final();
335 let final_weight = if is_final {
336 let min_errors = automaton_state
338 .positions()
339 .filter(|p| p.is_m_type() && p.offset() <= 0)
340 .map(|p| p.errors())
341 .min()
342 .unwrap_or(self.automaton.max_distance() + 1);
343 TropicalWeight::new(min_errors as f64)
344 } else {
345 TropicalWeight::zero()
346 };
347
348 (is_final, final_weight, transitions)
349 }
350
351 fn estimate_query_position(&self, state: &UniversalState<V>) -> usize {
355 state
357 .positions()
358 .filter(|p| p.is_i_type())
359 .map(|p| p.offset())
360 .min()
361 .map(|offset| {
362 if offset >= 0 {
365 offset as usize
366 } else {
367 0
368 }
369 })
370 .unwrap_or(0)
371 }
372}
373
374impl<V, D> StateSource<char, TropicalWeight> for UniversalLevenshteinStateSource<V, D>
375where
376 V: PositionVariant + Clone + Send + Sync,
377 V::State: Send + Sync,
378 D: Dictionary + Clone + Send + Sync,
379 D::Node: Send + Sync,
380 <D::Node as DictionaryNode>::Unit: Into<char> + TryFrom<char> + Copy + Send + Sync,
381{
382 fn compute_state(&self, state: StateId) -> LazyState<char, TropicalWeight> {
383 let (dict_node_id, automaton_state_id) =
384 state_encoding::decode(state, self.max_automaton_states);
385
386 let (is_final, final_weight, transitions) =
387 self.compute_transitions(dict_node_id, automaton_state_id);
388
389 if is_final {
390 LazyState::final_state(final_weight, transitions)
391 } else {
392 LazyState::non_final(transitions)
393 }
394 }
395
396 fn start(&self) -> StateId {
397 state_encoding::encode(0, 0, self.max_automaton_states)
399 }
400
401 fn num_states_hint(&self) -> Option<usize> {
402 let dict_size = self.dictionary.len().unwrap_or(1000);
403 let state_registry = self.state_registry.read().expect("Lock poisoned");
404 let automaton_states = state_registry.len();
405 Some((dict_size * automaton_states).min(1_000_000))
406 }
407}
408
409#[inline]
411fn compute_path_hash(parent_id: u32, edge_label: char) -> u64 {
412 use std::hash::{Hash, Hasher};
413 let mut hasher = rustc_hash::FxHasher::default();
414 parent_id.hash(&mut hasher);
415 edge_label.hash(&mut hasher);
416 hasher.finish()
417}
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422 use liblevenshtein::transducer::universal::Standard;
423 use libdictenstein::dynamic_dawg::char::DynamicDawgChar;
424
425 #[test]
426 fn test_universal_state_registry_creation() {
427 let registry = UniversalStateRegistry::<Standard>::new(2);
428 assert_eq!(registry.len(), 1); }
430
431 #[test]
432 fn test_universal_state_registry_register() {
433 let mut registry = UniversalStateRegistry::<Standard>::new(2);
434 let state = UniversalState::initial(2);
435 let id = registry.register_state(state.clone());
436 assert_eq!(id, 0); let id2 = registry.register_state(state);
440 assert_eq!(id, id2);
441 }
442
443 #[test]
444 fn test_universal_state_source_creation() {
445 let dict = DynamicDawgChar::<()>::from_terms(vec!["hello", "help", "world"]);
446 let source = UniversalLevenshteinStateSource::<Standard, _>::new(&dict, "helo", 2);
447
448 assert_eq!(source.start(), 0);
449 assert!(source.num_states_hint().is_some());
450 }
451
452 #[test]
453 fn test_universal_state_source_query() {
454 let dict = DynamicDawgChar::<()>::from_terms(vec!["hello"]);
455 let source = UniversalLevenshteinStateSource::<Standard, _>::new(&dict, "helo", 2);
456
457 assert_eq!(source.query(), "helo");
458 }
459}