Bug: Query "ab" fails to find dictionary term "ba" at distance 1 using the Transposition algorithm.
Root Cause: Position (1,1,false) incorrectly subsumes position (0,1,true) during state construction, preventing the transposition path from being explored.
For query "ab" and dictionary "ba" with transposition algorithm:
The automaton returns no matches.
Starting from root with query "ab":
When match is at index 1 (lines 226-231 in transition.rs), we generate:
Positions are inserted into the state via State::insert():
Insert (0,1,false):
State: [(0,1,false)]
Insert (0,1,true):
State: [(0,1,false), (0,1,true)] ← Both coexist ✓
Insert (1,1,false):
State: [(0,1,false), (1,1,false)] ← (0,1,true) DISAPPEARED! ❌
BUG LOCATION: When inserting (1,1,false), it removes (0,1,true) via the subsumption check at line 91 of state.rs:
self.positions.retain(|p| !position.subsumes(p, algorithm, query_length));
This means (1,1,false) subsumes (0,1,true)!
Testing: Does (1,1,false) subsume (0,1,true)?
Inputs:
Subsumption logic (position.rs lines 116-124):
if t { // rhs is special
let adjusted_diff = if j < i {
i.saturating_sub(j).saturating_sub(1) // 1 - 0 - 1 = 0
} else {
j.saturating_sub(i) + 1
};
return adjusted_diff <= (f - e); // 0 <= (1 - 1) = 0 <= 0 → TRUE ✓
}
Result: Returns true - (1,1,false) subsumes (0,1,true)
The special position (0,1,true) represents a transposition-in-progress state. It's fundamentally different from a normal position at (1,1,false):
These represent DIFFERENT computational paths in the automaton. The special position needs to:
If we remove (0,1,true), we lose the ability to explore the transposition path!
Java code (SubsumesFunction.java lines 74-93):
if (t) {
return (j < i ? i - j - 1 : j - i + 1) <= (f - e);
}
The Rust implementation is IDENTICAL to Java. So either:
The C++ code (subsumes.cpp line 24) has a typo:
bool t = lhs->is_special(); // BUG: should be rhs->is_special()
This is clearly wrong - it checks lhs twice instead of checking rhs. This bug would prevent the problematic subsumption from occurring, potentially masking the issue!
The transposition subsumption formula may be mathematically correct for general cases, but it fails to account for the semantic difference between:
Proposed Fix: Special positions should NEVER be subsumed by normal positions when using the Transposition algorithm, regardless of the distance formula. They represent fundamentally different states.
use liblevenshtein::prelude::*;
let dict = DoubleArrayTrie::from_terms(vec!["ba".to_string()]);
let transducer = Transducer::new(dict, Algorithm::Transposition);
let results: Vec<_> = transducer.query("ab", 1).collect();
// Expected: ["ba"]
// Actual: []
assert_eq!(results, vec!["ba"]); // FAILS
In position.rs, modify the transposition subsumption logic:
Algorithm::Transposition => {
// ... existing s checks ...
if t {
// CRITICAL: Special positions (transposition-in-progress) represent
// fundamentally different computational paths than normal positions.
// A normal position should NEVER subsume a special position, as this
// would prematurely terminate exploration of valid transposition paths.
//
// Example: Query "ab", dict "ba"
// (1,1,false) should NOT subsume (0,1,true)
// The special position is needed to complete the transposition!
if !s {
return false; // Normal position cannot subsume special position
}
// rhs is special: adjusted formula (only applies when lhs is also special)
let adjusted_diff = if j < i {
i.saturating_sub(j).saturating_sub(1)
} else {
j.saturating_sub(i) + 1
};
return adjusted_diff <= (f - e);
}
// Neither special: standard formula
let index_diff = i.abs_diff(j);
let error_diff = f - e;
index_diff <= error_diff
}
This ensures that special transposition positions can only be subsumed by other special positions, preserving the transposition exploration path.
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 |