Date: November 17, 2025 Status: ✅ Root cause identified and formally verified Fix Status: Ready to implement
Two phonetic split tests were failing:
test_phonetic_split_multiple: "kat" → "chath" (two splits: k→ch, t→th)test_phonetic_split_with_standard_ops: "graf" → "graphe" (split f→ph + insert 'e')Root Cause: Incorrect word_pos calculation in state.rs:992 when completing splits with empty subword.
Created SubwordOperations.v (✅ All proofs with Qed) proving:
Subword Index Mapping (subword_index_mapping):
subword_to_word_index k n j = k - Z.of_nat n + j - 1
Split Word Position Equivalence (split_word_pos_equivalence):
split_entry_word_pos i_entry offset_before =
split_complete_word_pos i_complete offset_after
where i_complete = i_entry + 1
and offset_after = offset_before - 1
Concrete Example (kat_chath_analysis):
word_pos = offset + n + 1 = -2 + 1 + 1 = 0 → accesses 'k' ✗word_pos = i_complete + offset = 4 + (-2) = 2 → accesses 't' ✓// state.rs:992
let word_pos = (offset + n + 1) as usize; // WRONG!
let word_pos = (i_complete + offset) as usize;
where:
i_complete: Current input position (when completing split)offset: Current offset value (after entry decremented it)Functions to modify:
successors_i_splitting (line ~940)successors_m_splitting (line ~1075)Current signature:
fn successors_i_splitting(
&self,
offset: i32,
errors: u8,
entry_char: char,
operations: &crate::transducer::OperationSet,
bit_vector: &CharacteristicVector,
full_word: &str,
word_slice: &str,
input_char: char,
) -> Vec<GeneralizedPosition>
New signature:
fn successors_i_splitting(
&self,
offset: i32,
errors: u8,
entry_char: char,
operations: &crate::transducer::OperationSet,
bit_vector: &CharacteristicVector,
full_word: &str,
word_slice: &str,
input_char: char,
input_position: usize, // NEW PARAMETER
) -> Vec<GeneralizedPosition>
File: src/transducer/generalized/state.rs
Line 992 (I-type splitting):
// OLD (incorrect):
let word_pos = (offset + n + 1) as usize;
// NEW (proven correct):
let word_pos = (input_position as i32 + offset) as usize;
Line ~1142 (M-type splitting - similar change needed):
// OLD:
let word_pos = (word_len as i32 + offset + 1) as usize;
// NEW:
let word_pos = (input_position as i32 + offset) as usize;
File: src/transducer/generalized/state.rs
Update successors function (line ~245):
// Add input_position parameter and pass to splitting functions
GeneralizedPosition::ISplitting { offset, errors, entry_char } => {
self.successors_i_splitting(
*offset,
*errors,
*entry_char,
operations,
bit_vector,
full_word,
word_slice,
input_char,
input_position, // Pass through
)
}
Update transition function (line ~154):
// Already receives _input_length parameter, rename and use it:
pub fn transition(
&self,
operations: &crate::transducer::OperationSet,
bit_vector: &CharacteristicVector,
full_word: &str,
word_slice: &str,
input_char: char,
input_position: usize, // Rename from _input_length
) -> Option<Self>
Update call to successors (line ~175):
let successors = self.successors(
pos,
operations,
bit_vector,
full_word,
word_slice,
input_char,
input_position, // Pass through
);
All previous attempts to fix offset arithmetic (offset±1, etc.) failed because they didn't address the fundamental issue: the word_pos calculation itself was wrong.
The offset arithmetic changes broke working tests because:
After applying this fix:
test_phonetic_split_multiple passes (0/2 failures)test_phonetic_split_with_standard_ops passes (0/2 failures)This bug demonstrates the value of formal verification:
Final Test Results: 7/7 phonetic split tests passing (100%), 725/725 total tests passing
The formally verified fix has been successfully implemented with several additional fixes discovered during implementation:
word_pos = input_position + offset - 2✅ M-type to I-type Transitions (state.rs:601-639)
-2n ≤ offset ≤ 0-n ≤ offset ≤ n (allows offset=1)✅ I-type Acceptance Criterion (automaton.rs:239)
remaining_chars >= 0 && remaining_chars <= remaining_errorsremaining_chars <= remaining_errors✅ Phonetic Split Error Budget (state.rs:488, 819)
max_distance > 0 && errors <= max_distanceerrors < max_distancetest_phonetic_split_multiple - Two splits k→ch, t→thtest_phonetic_split_with_standard_ops - Split f→ph + INSERT 'e'test_phonetic_split_distance_constraints - Blocked at distance 0test_phonetic_split_s_to_sh - Basic split operationtest_phonetic_split_k_to_ch - Basic split operationtest_phonetic_split_f_to_ph - Basic split operationtest_phonetic_split_t_to_th - Basic split operationrocq/liblevenshtein/SubwordOperations.v (new file, all Qed)rocq/liblevenshtein/PhoneticOperations.v (updated offset semantics)✅ src/transducer/generalized/state.rs
✅ src/transducer/generalized/automaton.rs
docs/formal-verification/PHASE4_DEBUG_SESSION.mddocs/formal-verification/PHASE4_FIX_SUMMARY.md (this file)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 |