Liking cljdoc? Tell your friends :D

Cross-Validation Bug Report

Date: 2025-10-30 Test File: tests/proptest_automaton_distance_cross_validation.rs Purpose: Verify that Levenshtein automaton results exactly match distance function results

Executive Summary

Cross-validation testing between the Levenshtein automaton and direct distance functions revealed 2 critical bugs in the automaton implementation:

  1. Empty String Bug: Automaton fails to return empty strings even when they exist in the dictionary
  2. Transposition Bug: Transposition automaton misses valid transposition matches

Bug #1: Empty String Handling

Description

The Levenshtein automaton does not return empty strings from the dictionary, even when:

  • The empty string exists in the dictionary
  • The query is an empty string
  • The max distance allows the match (distance = 0)

Test Case

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:   []

Affected Algorithms

  • ✗ Standard
  • ✗ Transposition
  • ✗ MergeAndSplit

All three algorithms fail to return empty strings.

Impact

  • Severity: Medium
  • Scope: Any application that includes empty strings in dictionaries
  • Consequence: False negatives (missed matches)

Distance Function Verification

The distance functions correctly compute distance 0 for empty-to-empty:

standard_distance("", "") == 0  // ✓ Correct

Root Cause Analysis

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:

  • ✅ Query logic fix added (root finality check in QueryIterator/OrderedQueryIterator)
  • ❌ Dictionary construction fix needed (DoubleArrayTrieBuilder must mark root as final)

Original suspected issues (investigation complete):

  1. Dictionary construction: CONFIRMED - DoubleArrayTrie does not mark root as final for empty strings
  2. Query initialization: Not the issue - query logic would work if dict were correct
  3. Acceptance condition: Not the issue - root finality check added but dict returns false

Minimal Failing Case

// Discovered by proptest with minimal shrinking
dict_words = [""]
query = ""
max_dist = 0
// Expected: {""}
// Actual: {}

Bug #2: Transposition Automaton Missing Matches

Description

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.

Test Case

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"

Distance Function Verification

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)

Affected Algorithms

  • ✓ Standard (passes)
  • ✗ Transposition (fails)
  • ? MergeAndSplit (not tested for this specific case)

Impact

  • Severity: High
  • Scope: All users of Transposition algorithm
  • Consequence: False negatives (missed valid matches)
  • Data Loss: Users relying on transposition matching are missing legitimate results

Root Cause Analysis

The Transposition automaton is not correctly exploring all transposition paths. Specifically:

  • It correctly handles insertions ("ab" → "abc")
  • It correctly handles exact matches ("ab" → "ab")
  • It incorrectly fails to explore transposition transitions ("ab" → "ba")

Possible causes:

  1. State transition bug: Transposition transitions not properly generated
  2. State subsumption bug: Valid transposition states being incorrectly subsumed
  3. Characteristic vector bug: Transposition characteristic vectors not correctly accounting for swaps

Files to Investigate

  • src/transducer/algorithm.rs - Algorithm::Transposition implementation
  • src/transducer/parametric.rs - Transposition transition generation
  • src/transducer/mod.rs - Core query logic
  • src/transducer/state.rs - State management and subsumption

Testing Methodology

Cross-Validation Approach

For 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);

Test Coverage

  • 500 test cases per algorithm (1500 total)
  • Property-based testing with proptest
  • Automatic shrinking to minimal failing cases
  • Multiple dictionary sizes: 1-100 words
  • Multiple query lengths: 0-15 characters
  • Multiple distance values: 0-3

Bugs Found

  • Empty string bug: Found in 3 test cases (all 3 algorithms)
  • Transposition bug: Found in 6 test cases
  • Total failures: 12 out of 16 tests failed initially

Test Results Summary

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

Recommendations

Priority 1: Fix Empty String Bug

  1. Investigate DoubleArrayTrie construction with empty string inputs
  2. Add explicit empty string handling in query initialization
  3. Ensure acceptance state recognizes empty paths
  4. Add unit tests specifically for empty string scenarios

Priority 2: Fix Transposition Bug

  1. Review transposition transition generation in parametric automaton
  2. Verify characteristic vector computation for transposition states
  3. Check state subsumption logic - may be incorrectly pruning valid transposition states
  4. Add debugging output to trace transposition path exploration

Priority 3: Expand Test Coverage

  1. Keep cross-validation tests as regression tests
  2. Run tests in CI/CD before every release
  3. Add more edge cases: single-character words, repeated characters, etc.
  4. Test with real-world dictionaries (system word lists)

Priority 4: Performance Validation

Once bugs are fixed:

  1. Run automaton vs linear scan benchmark (benches/automaton_vs_linear_scan.rs)
  2. Verify performance advantage of automaton over linear scan
  3. Document performance characteristics in README

Cross-Validation Test Value

This testing approach proved its worth by:

  1. Found 2 critical bugs that unit tests missed
  2. Provided minimal failing cases via proptest shrinking
  3. Verified distance function correctness (all distance functions pass)
  4. Established ground truth for automaton correctness
  5. Demonstrated scientific validation approach

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.

Files Created

  1. tests/proptest_automaton_distance_cross_validation.rs (481 lines)

    • Cross-validation property tests for all three algorithms
    • Edge case testing (empty strings, duplicates, Unicode)
    • Regression tests for known bugs
  2. benches/automaton_vs_linear_scan.rs (522 lines)

    • Performance comparison: automaton vs linear scan
    • Tests across different dictionary sizes (25, 100, 1000 words)
    • Tests varying query lengths and distance values
    • Best/worst case analysis
  3. docs/CROSS_VALIDATION_BUG_REPORT.md (this document)

    • Comprehensive bug analysis
    • Minimal failing test cases
    • Recommendations for fixes

Next Steps

  1. File GitHub Issues for both bugs with minimal test cases
  2. Fix Empty String Bug (affects all algorithms)
  3. Fix Transposition Bug (affects Transposition algorithm only)
  4. Re-run cross-validation tests to verify fixes
  5. Run performance benchmarks to demonstrate automaton advantage
  6. Update documentation with any caveats or limitations discovered

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

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close