Date: 2025-10-30
Test File: tests/proptest_automaton_distance_cross_validation.rs
Purpose: Verify that Levenshtein automaton results exactly match distance function results
Cross-validation testing between the Levenshtein automaton and direct distance functions revealed 2 critical bugs in the automaton implementation:
The Levenshtein automaton does not return empty strings from the dictionary, even when:
let dict_words = vec!["".to_string()];
let dat = DoubleArrayTrie::from_terms(dict_words);
let transducer = Transducer::new(dat, Algorithm::Standard);
// Query: "" with max_distance = 0
let results: Vec<_> = transducer.query("", 0).collect();
// Expected: [""]
// Actual: []
All three algorithms fail to return empty strings.
The distance functions correctly compute distance 0 for empty-to-empty:
standard_distance("", "") == 0 // ✓ Correct
UPDATE (2025-10-30): Root cause confirmed through testing:
The bug is in dictionary construction, NOT the query algorithm.
Test results:
let dict = DoubleArrayTrie::from_terms(vec!["".to_string()]);
println!("Root is_final: {}", dict.root().is_final());
// Output: Root is_final: false
// ^^^^^^^^ SHOULD BE TRUE!
The DoubleArrayTrie builder does not mark the root node as final when empty strings are inserted.
Fix Status:
Original suspected issues (investigation complete):
// Discovered by proptest with minimal shrinking
dict_words = [""]
query = ""
max_dist = 0
// Expected: {""}
// Actual: {}
The Transposition algorithm automaton fails to return words that are reachable via transposition operations, even though the distance function correctly identifies them as within range.
let dict_words = vec!["ab", "ba", "abc"];
let dat = DoubleArrayTrie::from_terms(dict_words);
let transducer = Transducer::new(dat, Algorithm::Transposition);
// Query: "ab" with max_distance = 1
let results: Vec<_> = transducer.query("ab", 1).collect();
// Expected: ["ab", "ba", "abc"]
// Actual: ["ab", "abc"] // Missing "ba"
The distance function correctly computes transposition distances:
transposition_distance("ab", "ab") == 0 // ✓ Correct
transposition_distance("ab", "ba") == 1 // ✓ Correct (one transposition)
transposition_distance("ab", "abc") == 1 // ✓ Correct (one insertion)
The Transposition automaton is not correctly exploring all transposition paths. Specifically:
Possible causes:
src/transducer/algorithm.rs - Algorithm::Transposition implementationsrc/transducer/parametric.rs - Transposition transition generationsrc/transducer/mod.rs - Core query logicsrc/transducer/state.rs - State management and subsumptionFor each algorithm, we compare:
// Linear scan: O(n) where n = dictionary size
let linear_results: HashSet<String> = dict_words.iter()
.filter(|word| distance_function(query, word) <= max_distance)
.cloned()
.collect();
// Automaton: O(m * k) where m = query length, k = max_distance
let automaton_results: HashSet<String> = transducer
.query(query, max_distance)
.collect();
// These MUST be equal
assert_eq!(automaton_results, linear_results);
running 16 tests
test prop_standard_automaton_distance_matches_function ... ok [2/16 passed]
test prop_transposition_automaton_distance_matches_function ... ok
test prop_empty_dictionary_all_algorithms ... ok [3/16 passed]
test regression_tests::test_deletion_bug_cross_validation ... ok [4/16 passed]
test regression_tests::test_merge_split_specific_case ... ok [5/16 passed]
test prop_empty_query_all_algorithms ... FAILED (empty string bug)
test prop_duplicate_words_all_algorithms ... FAILED (empty string bug)
test prop_exact_match_only_all_algorithms ... FAILED (empty string bug)
test prop_standard_automaton_matches_linear_scan ... FAILED (empty string bug)
test prop_standard_large_dict_matches ... FAILED (empty string bug)
test prop_standard_unicode_matches ... FAILED (empty string bug)
test prop_transposition_automaton_matches_linear_scan ... FAILED (both bugs)
test prop_transposition_handles_swaps_correctly ... FAILED (both bugs)
test prop_merge_split_automaton_matches_linear_scan ... FAILED (empty string bug)
test prop_merge_split_automaton_distance_matches_function ... FAILED (empty string bug)
test regression_tests::test_transposition_specific_case ... FAILED (transposition bug)
Result: 5 passed, 11 failed
Once bugs are fixed:
benches/automaton_vs_linear_scan.rs)This testing approach proved its worth by:
Conclusion: The Levenshtein automaton implementation has subtle bugs that are caught by comparing against the reference implementation (distance functions). Cross-validation testing should be standard practice for all approximate matching implementations.
tests/proptest_automaton_distance_cross_validation.rs (481 lines)
benches/automaton_vs_linear_scan.rs (522 lines)
docs/CROSS_VALIDATION_BUG_REPORT.md (this document)
Note: These bugs do NOT affect the SIMD-optimized distance functions themselves - those are working correctly as evidenced by the cross-validation tests. The bugs are in the automaton query logic, not the distance computation.
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 |