Status: ✅ Formal model and Rust split integration complete; all active phonetic operation proofs verified
Date: November 17, 2025 (refreshed 2026-06-19)
Files: rocq/liblevenshtein/PhoneticOperations.v
Phase 4 establishes the formal semantics of phonetic split operations through rigorous proof-driven design. Unlike Phase 3's bug fixes, Phase 4 takes a top-down approach: define the formal model first, prove invariant preservation, then derive the Rust implementation from the proven specification.
Key Achievement: Discovered critical preconditions through proof attempts that prevent invalid transitions, ensuring phonetic splits maintain the same mathematical rigor as standard Levenshtein operations.
The existing phonetic operation implementation exhibited "buggy and hackish" behavior with multiple regression tests exposing edge-case gaps. Rather than iterative bug fixes, we adopted formal verification to derive correct-by-construction semantics.
User Directive: "I don't want to bounce around between a bunch of one-off attempts to fix these anymore, can we skip straight to the formal verification step to derive how they should be implemented?"
Phonetic splits (e.g., f→ph, k→ch, t→th) are primitive multi-character transformations, similar to skip-to-match. They cannot be decomposed into standard operations:
NOT "delete 'f', insert 'p', insert 'h'"
IS "recognize 'f' and 'ph' as phonetically equivalent"
Phonetic splits are multi-step operations with distinct phases:
1. ENTRY: I-type → ISplitting
- Apply cost (fractional, truncates to 0)
- Decrement offset by 1 (prepare for multi-char match)
- Record entry character for validation
2. PROGRESS: ISplitting → ISplitting
- Consume additional input characters
- Validate pattern match (e.g., 'f' + 'ph')
- No cost, stable position
3. COMPLETION: ISplitting → I-type or M-type
- Increment offset by 1 (net effect: same as entry)
- Errors unchanged (cost applied at entry)
- Transition based on word boundary
Example: "graf" → "graphe" with f→ph split
Start: I+0#0 (processed 'gra')
Step 1 (Entry): word[3]='f', input='p'
Check: Can apply f→ph split
→ ISplitting+(-1)#0_f (cost 0, offset decremented)
Step 2 (Progress): ISplitting+(-1)#0_f, input='h'
Check: 'f' + 'ph' matches pattern
→ ISplitting+(-1)#0_f (consuming 'h')
Step 3 (Completion): Complete split
→ I+0#0 (offset -1+1=0, errors same)
→ M+0#0 (past word end, converts to M-type)
Step 4 (Standard): M+0#0, input='e'
→ INSERT (cost 1)
→ M+1#1
Total cost: 0 (split) + 1 (insert) = 1 error
(* Entry: I-type → ISplitting *)
Inductive i_split_entry : Position -> ascii -> nat -> Position -> Prop
(* Progress: ISplitting → ISplitting *)
Inductive i_split_progress : Position -> ascii -> Position -> Prop
(* Completion: ISplitting → I-type or M-type *)
Inductive i_split_completion : Position -> nat -> Position -> Prop
(* Combined: Entry → Completion *)
Inductive i_phonetic_split : Position -> ascii -> nat -> Position -> Prop
The proof process revealed two essential preconditions not obvious from informal specifications:
offset > -nWhy Needed: Entry decrements offset by 1. Without this bound:
-n \le \text{offset} \le n$Discovery: Proof attempt failed with goal:
offset - 1 >= -Z.of_nat n
When precondition only had offset >= -n, equality case violated the goal.
Fix: Require strict inequality offset > -n in entry precondition.
split_cost = 0 → |offset| < errorsWhy Needed: For fractional costs (0.15 truncates to 0), need reachability:
|\text{offset} - 1| \le \text{errors} + \text{cost}$|-1| = 1 \le 0$ (FAILS)Discovery: Proof attempt with case analysis on cost:
destruct cost as [| cost'].
+ (* cost = 0: exposes the impossible goal |-1| ≤ 0 *)
+ (* cost ≥ 1: proof succeeds with |offset - 1| ≤ |offset| + 1 ≤ errors + cost *)
Fix: For fractional costs (cost=0), require strict inequality |offset| < errors.
Insight: This creates a "budget requirement" for fractional operations. You can only apply fractional-cost operations when you have spare reachability budget. This elegantly captures the intuition that "free" operations still require positional flexibility.
Inductive i_split_entry : Position -> ascii -> nat -> Position -> Prop :=
| ISplitEntry : forall offset errors n entry_char split_cost,
(* Standard I-type preconditions *)
(-Z.of_nat n <= offset <= Z.of_nat n) ->
(Z.abs offset <= Z.of_nat errors) ->
(errors <= n)%nat ->
(errors + split_cost <= n)%nat ->
(* CRITICAL: Discovered through proof attempts *)
(offset > -Z.of_nat n) -> (* Ensures offset - 1 ≥ -n *)
(split_cost = 0%nat -> Z.abs offset < Z.of_nat errors) -> (* Budget for fractional costs *)
i_split_entry
(mkPosition VarINonFinal offset errors n None)
entry_char
split_cost
(mkPosition VarISplitting (offset - 1) (errors + split_cost) n (Some entry_char)).
The completion relation has preconditions that ensure invariants are trivially satisfied:
Inductive i_split_completion : Position -> nat -> Position -> Prop :=
| ISplitComplete_ToI : forall offset errors n entry_char result_offset,
result_offset = offset + 1 ->
(* Precondition: Result must satisfy I-invariant *)
(Z.abs result_offset <= Z.of_nat errors) ->
(-Z.of_nat n <= result_offset <= Z.of_nat n) ->
i_split_completion
(mkPosition VarISplitting offset errors n (Some entry_char))
n
(mkPosition VarINonFinal result_offset errors n None)
| ISplitComplete_ToM : forall offset errors n entry_char result_offset m_offset,
result_offset = offset + 1 ->
m_offset = result_offset - Z.of_nat n ->
(* Precondition: Result must satisfy M-invariant *)
(-Z.of_nat (2 * n) <= m_offset <= 0) ->
(Z.of_nat errors >= -m_offset - Z.of_nat n) ->
i_split_completion
(mkPosition VarISplitting offset errors n (Some entry_char))
n
(mkPosition VarMFinal m_offset errors n None).
Design Pattern: By requiring the result position to satisfy the target invariant as a precondition, the completion proof becomes trivial (just assumption). This pushes complexity to the caller (entry + splitting state invariant).
All three theorems proven with Qed (no Admitted):
Theorem i_split_entry_preserves_invariant : forall p entry_char cost p',
i_invariant p ->
i_split_entry p entry_char cost p' ->
i_splitting_invariant p'.
Proof Strategy: Case analysis on cost (0 vs $\ge 1)$
|offset| < errors
|\text{offset} - 1| \le \text{errors}$|offset| < errors|\text{offset} - 1| \le |\text{offset}| + 1 < \text{errors} + 1$, so $|\text{offset} - 1| \le \text{errors}$ ✓\ge 1$: Standard arithmetic
|\text{offset} - 1| \le |\text{offset}| + 1 \le \text{errors} + 1 \le \text{errors} + \text{cost}$ ✓Line Reference: PhoneticOperations.v:220-248
Theorem i_split_completion_preserves_invariant : forall p n p',
i_splitting_invariant p ->
i_split_completion p n p' ->
(i_invariant p' \/ m_invariant p').
Proof Strategy: Trivial by preconditions
assumption for each goal ✓Line Reference: PhoneticOperations.v:251-290
Theorem i_phonetic_split_preserves_invariant : forall p entry_char cost p',
i_invariant p ->
i_phonetic_split p entry_char cost p' ->
(i_invariant p' \/ m_invariant p').
Proof Strategy: Composition
Line Reference: PhoneticOperations.v:293-305
M-type splits are simpler because M-type is already past the word end:
Inductive m_split_entry : Position -> ascii -> nat -> Position -> Prop :=
| MSplitEntry : forall offset errors n entry_char split_cost,
(* M-type preconditions *)
(-Z.of_nat (2 * n) <= offset <= 0) ->
(Z.of_nat errors >= -offset - Z.of_nat n) ->
(errors <= n)%nat ->
(errors + split_cost <= n)%nat ->
m_split_entry
(mkPosition VarMFinal offset errors n None)
entry_char
split_cost
(mkPosition VarMSplitting (offset - 1) (errors + split_cost) n (Some entry_char)).
Inductive m_split_completion : Position -> Position -> Prop :=
| MSplitComplete : forall offset errors n entry_char result_offset,
result_offset = offset + 1 ->
m_split_completion
(mkPosition VarMSplitting offset errors n (Some entry_char))
(mkPosition VarMFinal result_offset errors n None).
Inductive m_phonetic_split : Position -> ascii -> nat -> Position -> Prop :=
| MPhoneticSplit : forall p1 p2 p3 entry_char split_cost,
m_split_entry p1 entry_char split_cost p2 ->
m_split_completion p2 p3 ->
m_phonetic_split p1 entry_char split_cost p3.
Note: M-type doesn't need the critical preconditions because:
\text{offset} \le 0$, so offset > -2n is always satisfied\text{errors} \ge -\text{offset} - n$ (looser than $|\text{offset}| \le \text{errors}$)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 /\
-Z.of_nat n <= offset <= Z.of_nat n /\
(errors <= n)%nat.
Definition m_splitting_invariant (p : Position) : Prop :=
variant p = VarMSplitting /\
let n := max_distance p in
let offset := offset p in
let errors := errors p in
Z.of_nat errors >= -offset - Z.of_nat n /\
-Z.of_nat (2 * n) <= offset <= 0 /\
(errors <= n)%nat.
Pattern: Splitting invariants match their corresponding final invariants (I-splitting matches I-type, M-splitting matches M-type), differing only in variant tag.
Anti-pattern: Guess preconditions from informal specs, then try to prove.
Best practice: Start with minimal preconditions, let proof attempts reveal necessary constraints.
Example:
(* Initial attempt (WRONG) *)
offset >= -Z.of_nat n (* Allows offset = -n *)
(* Proof fails with goal: offset - 1 >= -n *)
(* When offset = -n: -n - 1 >= -n (FALSE) *)
(* Corrected (RIGHT) *)
offset > -Z.of_nat n (* Strict inequality *)
The $\text{cost}=0$ case has fundamentally different requirements than $\text{cost} \ge 1$:
\ge 1$: Budget naturally covers offset adjustmentThis pattern likely applies to other fractional-cost operations.
By encoding invariant requirements as preconditions in the completion relation:
(Z.abs result_offset <= Z.of_nat errors) -> (* Part of constructor *)
The invariant preservation proof reduces to:
Proof.
intros. inversion H. assumption. (* Trivial! *)
Qed.
Trade-off: Pushes complexity to the caller, but makes compositional reasoning easier.
The Position variant field acts as a state machine:
VarINonFinal: Normal I-type positionVarISplitting: Mid-phonetic-split I-typeVarMFinal: Normal M-type positionVarMSplitting: Mid-phonetic-split M-typeTransitions:
The three-phase lifecycle wasn't clear from Rust code. Formal modeling revealed:
This structure provides hooks for future extensions (multi-step patterns, validation).
The original Phase 4 proof obligation set covered single split lifecycle invariants.
The refreshed PhoneticOperations.v also proves composition and cost accounting
properties needed for consecutive splits and split-plus-standard-operation paths:
Theorem i_phonetic_split_composes_with_i_successor : ...
Theorem m_phonetic_split_composes_with_m_successor : ...
Theorem consecutive_i_phonetic_splits_preserve_invariant : ...
Theorem consecutive_m_phonetic_splits_preserve_invariant : ...
Theorem i_phonetic_split_cost_correct : ...
Theorem m_phonetic_split_cost_correct : ...
Theorem i_phonetic_split_then_i_successor_cost_correct : ...
Theorem m_phonetic_split_then_m_successor_cost_correct : ...
Resolved questions:
Remaining modeling questions:
Current definition is identity:
Inductive i_split_progress : Position -> ascii -> Position -> Prop :=
| ISplitProgress : forall offset errors n entry_char input_char,
i_split_progress
(mkPosition VarISplitting offset errors n (Some entry_char))
input_char
(mkPosition VarISplitting offset errors n (Some entry_char)).
Future: Add character sequence validation:
Current model: entry_char is single character ('f').
Extension: Support multi-character entry patterns ('ch', 'ph', 'th'):
Current: split_cost = 0 (0.15 truncates to 0).
Questions:
The formal relations have corresponding Rust transition paths in
src/transducer/generalized/{position,state}.rs:
Translated Relations
i_split_entry and m_split_entry map to split-entry constructors and
generalized successor generation.i_split_progress and m_split_progress map to splitting-state successor
handling.i_split_completion and m_split_completion map to split-completion
transitions.Precondition Checks
Regression Coverage
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
passed 7/7 focused phonetic split tests on 2026-06-19.Remaining Evaluation
rocq/liblevenshtein/PhoneticOperations.v: 483 lines, all active phonetic operation proofs with Qedrocq/liblevenshtein/_CoqProject: Added PhoneticOperations.v to builddocs/formal-verification/04_phonetic_operations.md: This document$ grep -n "Admitted\|admit" PhoneticOperations.v
✅ No admits found
$ make PhoneticOperations.vo
make: 'PhoneticOperations.vo' is up to date.
All active phonetic operation theorems are proven, no admitted lemmas remain in
PhoneticOperations.v, and compilation succeeds through the Rocq build.
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 |