The Universal Levenshtein Automaton was rejecting valid words that required substitution operations.
Example: "test" → "text" (1 substitution: s→x) was incorrectly rejected with max_distance=2.
Test Results Before Fix:
Location: src/transducer/universal/state.rs, line 155 in add_position() method
Bug: The subsumption logic was backwards. When adding a new position to the state:
// WRONG (before fix):
if !self.positions.iter().any(|p| subsumes(&pos, p, self.max_distance)) {
self.positions.insert(pos);
}
This rejected a new position pos if it subsumed any existing position p. But subsumption means "pos is BETTER than p", so pos should be ADDED (and p should be removed).
From thesis Definition 11 (page 22):
i#e subsumes j#f if f > e AND |j - i| ≤ f - ei#e is better (fewer errors) and can cover j#f's matchesAt step 4 of processing "test" → "text":
[I-2#2, I-1#2] (both with 2 errors)I+0#1 (only 1 error - BETTER!)I+0#1 subsume I-1#2?
I+0#1 subsumes I-1#2, so it was rejected ❌This left the state with only error positions, causing acceptance to fail.
Fixed Code:
pub fn add_position(&mut self, pos: UniversalPosition<V>) {
// Step 1: Remove any existing positions that are subsumed by the new position
// If pos subsumes p, then p is worse and should be removed
self.positions.retain(|p| !subsumes(&pos, p, self.max_distance));
// Step 2: Add the new position only if it's not subsumed by any remaining position
// If p subsumes pos for some p, then pos is worse and should be rejected
if !self.positions.iter().any(|p| subsumes(p, &pos, self.max_distance)) {
self.positions.insert(pos);
}
}
Key Changes:
pos subsumes something (WRONG)pos (CORRECT)Created tests/universal_vs_parameterized.rs to compare Universal vs Parameterized automata:
// Test that both implementations agree
let universal = UniversalAutomaton::<UniversalStandard>::new(max_distance);
let universal_result = universal.accepts(word, input);
let transducer = Transducer::standard(dict);
let parameterized_results: Vec<_> = transducer.query(input, max_distance).collect();
let parameterized_result = parameterized_results.iter().any(|w| w == word);
assert_eq!(universal_result, parameterized_result);
Result: All 3 cross-validation tests pass ✓
Before Fix: 6/10 passing After Fix: 10/10 passing ✓
All test cases now work:
Cross-validation: Compared Universal vs Parameterized implementations
Detailed tracing: Added debug output to track:
Trace analysis: Discovered:
I+0#1[I-2#2, I-1#2]Subsumption analysis: Calculated:
I+0#1 subsumes I-1#2 (better position)I+0#1 for subsuming somethingFix applied: Flipped the subsumption logic
Validation: All tests pass
src/transducer/universal/state.rs (line 148-158)
add_position() subsumption logicsrc/transducer/universal/automaton.rs (line 526-534)
tests/universal_vs_parameterized.rs (NEW)
PHASE4_TRACE_ANALYSIS.md - Detailed trace of "test"→"text" executionPHASE4_BUG_FIX_SUMMARY.md - This documentDIAGONAL_CROSSING_DEBUG_SUMMARY.md - Previous debugging session notesThis fix resolves the substitution handling bug and brings the Universal Levenshtein Automaton to full correctness for Standard (insert/delete/substitute) operations with max_distance ≤ 2.
Status: Phase 4 implementation complete ✓
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |