Date: November 17, 2025 Status: ✅ Core implementation complete; focused phonetic split tests passing
Phase 4 successfully implemented phonetic split operations using a formal-verification-first approach. The active Rocq model proves lifecycle invariants, split-plus-standard composition, consecutive split preservation, and additive cost accounting without admits. The Rust implementation was derived from the proven formal model and the focused phonetic split regression suite now passes 7 out of 7 tests, including consecutive splits and split plus standard operations.
File: rocq/liblevenshtein/PhoneticOperations.v (483 lines)
Theorems Proven:
i_split_entry_preserves_invariant - Entry maintains I-splitting invarianti_split_completion_preserves_invariant - Completion restores I-type or M-type invarianti_phonetic_split_preserves_invariant - Full split operation preserves invariantsm_split_entry_preserves_invariant - M-type entry maintains M-splitting invariantm_split_completion_preserves_invariant - M-type completion restores M-type invariantm_phonetic_split_preserves_invariant - Full M-type split operation preserves invariantsi_phonetic_split_composes_with_i_successor - I split composes with standard I successorm_phonetic_split_composes_with_m_successor - M split composes with standard M successorconsecutive_i_phonetic_splits_preserve_invariant - Consecutive I splits preserve invariantsconsecutive_m_phonetic_splits_preserve_invariant - Consecutive M splits preserve invariantsi_phonetic_split_cost_correct / m_phonetic_split_cost_correct - Split cost accounting is exacti_phonetic_split_then_i_successor_cost_correct / m_phonetic_split_then_m_successor_cost_correct - Split-plus-standard cost accounting is additiveCritical Preconditions Discovered:
Offset Lower Bound: offset > -n
offset - 1 ≥ -n after entry decrementoffset - 1 >= -n when precondition was only offset >= -noffset > -nFractional Cost Budget: split_cost = 0 → |offset| < errors
|offset - 1| ≤ errors|offset - 1| ≤ |offset| + 1 < errors + 1, so |offset - 1| ≤ errors ✓Relaxed Splitting Invariant (discovered during Rust implementation):
|offset| ≤ errors (same as I-type)|offset| ≤ errors + 1 (allows intermediate states)|-1| ≤ 0 + 1 ✓Files Modified:
src/transducer/generalized/state.rs - Entry logic with preconditionssrc/transducer/generalized/position.rs - Relaxed splitting invariantsKey Changes:
Before (too restrictive):
// I-splitting invariant: same as I-type
let invariant_satisfied = offset.abs() <= errors as i32
&& offset >= -n
&& offset <= n
&& errors <= max_distance;
After (allows phonetic splits from I+0#0):
// Phase 4: Relaxed invariant for splitting states
// |offset| ≤ errors + 1 (one extra buffer for offset decrement)
let invariant_satisfied = offset.abs() <= (errors as i32 + 1)
&& offset >= -n
&& offset <= n
&& errors <= max_distance;
Rationale: The split is a two-step operation:
offset - 1 (may temporarily exceed standard reachability)offset + 1 (restores reachability)I-type split entry:
// CRITICAL PRECONDITION 1: offset > -n
// Without this, offset - 1 could violate I-splitting invariant: -n ≤ offset ≤ n
let offset_allows_entry = offset > -n;
if offset_allows_entry {
// ... phonetic split logic ...
if can_phonetic_split {
// Phonetic split: enter with errors+0 (fractional weight truncates to 0)
// The constructor validates the relaxed splitting invariant: |offset| ≤ errors + 1
if let Ok(split) = GeneralizedPosition::new_i_splitting(
offset - 1, // Decrement offset (will increment at completion, net effect: same)
errors, // Errors unchanged (cost=0)
self.max_distance,
input_char // Store entry character for pattern validation at completion
) {
successors.push(split);
}
}
}
M-type split entry:
// M-type splits simpler because M-type is already past word end
// M-type bounds: -2n ≤ offset ≤ 0
if can_phonetic_split {
// The constructor validates the relaxed M-splitting invariant
if let Ok(split) = GeneralizedPosition::new_m_splitting(
offset - 1, // Decrement offset
errors, // Errors unchanged (cost=0)
self.max_distance,
input_char
) {
successors.push(split);
}
}
Focused Phonetic Split Tests: 7/7 passing
Passing Tests ✅:
test_phonetic_split_f_to_ph - "graf" → "graph" ✅test_phonetic_split_k_to_ch - "kan" → "chan" ✅test_phonetic_split_s_to_sh - "sip" → "ship" ✅test_phonetic_split_t_to_th - "tank" → "thank" ✅test_phonetic_split_multiple - "kat" → "chath" ✅test_phonetic_split_with_standard_ops - "graf" → "graphe" ✅test_phonetic_split_distance_constraints - Distance limits enforced ✅Verification command:
systemd-run --user --scope -p MemoryMax=4G -p MemorySwapMax=0 \
env CARGO_BUILD_JOBS=1 cargo test -j1 --lib test_phonetic_split -- --test-threads=1
This command passed on 2026-06-19 with all seven focused split tests passing.
All Other Phonetic Tests Passing ✅:
The Rust implementation revealed that the initial formal model needed a relaxed splitting invariant for intermediate states. The active Rocq model has been updated and re-proven against that semantics.
Initial model:
Definition i_splitting_invariant (p : Position) : Prop :=
variant p = VarISplitting /\
let n := max_distance p in
let offset := offset p in
let errors := errors p in
Z.abs offset <= Z.of_nat errors /\ (* Too restrictive *)
-Z.of_nat n <= offset <= Z.of_nat n /\
(errors <= n)%nat.
Current model:
Definition i_splitting_invariant (p : Position) : Prop :=
variant p = VarISplitting /\
let n := max_distance p in
let offset := offset p in
let errors := errors p in
Z.abs offset <= Z.of_nat errors + 1 /\ (* Relaxed: +1 buffer *)
-Z.of_nat n <= offset <= Z.of_nat n /\
(errors <= n)%nat.
Justification: Splitting states are temporary intermediate states. The +1 buffer allows offset - 1 at entry, with completion doing offset + 1 to restore the standard invariant.
The entry relation already has the correct preconditions discovered through proofs:
offset > -Z.of_nat n (prevents out-of-bounds after decrement)split_cost = 0 → Z.abs offset < Z.of_nat errors (fractional budget)These remain correct and were validated by the Rust implementation.
After updating splitting invariants, the active Rocq model re-proves the I-type and M-type entry/completion/full-split theorems and extends them with composition and cost-accounting theorems.
The initial formal model (splitting invariant = I-type invariant) was too restrictive. Discovering this through implementation is part of the formal verification process. The formal model now documents the CORRECT invariant that allows valid operations.
The critical preconditions (offset > -n, fractional budget check) were discovered by attempting proofs and letting them fail. This is more reliable than guessing preconditions from informal specs.
Multi-step operations (entry → progress → completion) often need relaxed invariants for intermediate states, as long as the final state satisfies standard invariants. This pattern likely applies to other multi-step operations.
Encoding invariant checks in constructors (new_i_splitting, new_m_splitting) ensures invariants can't be violated. The relaxed invariant in constructors prevented invalid states during testing.
The regression tests revealed that the formal model's splitting invariant was too restrictive. Without tests expecting splits from I+0#0, we wouldn't have discovered the need for the +1 buffer.
Both previous edge cases now pass:
test_phonetic_split_multiple covers "kat" → "chath" using two splits.test_phonetic_split_with_standard_ops covers "graf" → "graphe" using a split plus insertion.+1 intermediate-state buffer.Create proptest suite for phonetic operations:
#[test]
fn phonetic_split_preserves_invariants() {
// Property: Any valid phonetic split creates valid positions
// Validates i_split_entry_preserves_invariant theorem
}
#[test]
fn phonetic_split_completion_restores_invariant() {
// Property: Completing any split produces I-type or M-type position
// Validates i_split_completion_preserves_invariant theorem
}
#[test]
fn phonetic_split_net_effect_is_identity() {
// Property: entry(offset) → completion = offset (net offset unchanged)
}
rocq/liblevenshtein/PhoneticOperations.v - Complete active phonetic operation model (483 lines)rocq/liblevenshtein/_CoqProject - Added PhoneticOperations.v to buildsrc/transducer/generalized/state.rs - Entry logic with preconditionssrc/transducer/generalized/position.rs - Relaxed splitting invariantsdocs/formal-verification/04_phonetic_operations.md - Insights and designdocs/formal-verification/PHASE4_SUMMARY.md - This documentPhase 4 demonstrates the power of formal-verification-first development:
Results:
Next Steps:
Phase 4 is functionally complete for the focused phonetic split paths currently covered by Rust tests and Rocq invariants.
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 |