Liking cljdoc? Tell your friends :D

Phase 4: Phonetic Operations - Implementation Summary

Date: November 17, 2025 Status: ✅ Core implementation complete; focused phonetic split tests passing

Executive Summary

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.

Key Achievements

1. Formal Model Complete (✅ All Proofs with Qed)

File: rocq/liblevenshtein/PhoneticOperations.v (483 lines)

Theorems Proven:

  • i_split_entry_preserves_invariant - Entry maintains I-splitting invariant
  • i_split_completion_preserves_invariant - Completion restores I-type or M-type invariant
  • i_phonetic_split_preserves_invariant - Full split operation preserves invariants
  • m_split_entry_preserves_invariant - M-type entry maintains M-splitting invariant
  • m_split_completion_preserves_invariant - M-type completion restores M-type invariant
  • m_phonetic_split_preserves_invariant - Full M-type split operation preserves invariants
  • i_phonetic_split_composes_with_i_successor - I split composes with standard I successor
  • m_phonetic_split_composes_with_m_successor - M split composes with standard M successor
  • consecutive_i_phonetic_splits_preserve_invariant - Consecutive I splits preserve invariants
  • consecutive_m_phonetic_splits_preserve_invariant - Consecutive M splits preserve invariants
  • i_phonetic_split_cost_correct / m_phonetic_split_cost_correct - Split cost accounting is exact
  • i_phonetic_split_then_i_successor_cost_correct / m_phonetic_split_then_m_successor_cost_correct - Split-plus-standard cost accounting is additive

Critical Preconditions Discovered:

  1. Offset Lower Bound: offset > -n

    • Why: Ensures offset - 1 ≥ -n after entry decrement
    • Discovery: Proof exposed offset - 1 >= -n when precondition was only offset >= -n
    • Fix: Changed to strict inequality offset > -n
  2. Fractional Cost Budget: split_cost = 0 → |offset| < errors

    • Why: For cost=0, need reachability after decrement: |offset - 1| ≤ errors
    • Discovery: Case analysis on cost revealed cost=0 case failed without strict inequality
    • Proof: |offset - 1| ≤ |offset| + 1 < errors + 1, so |offset - 1| ≤ errors
  3. Relaxed Splitting Invariant (discovered during Rust implementation):

    • Original: |offset| ≤ errors (same as I-type)
    • Relaxed: |offset| ≤ errors + 1 (allows intermediate states)
    • Why: Phonetic splits from I+0#0 create ISplitting+(-1)#0, requiring |-1| ≤ 0 + 1
    • Rationale: Splitting state is temporary; completion restores standard invariant

2. Rust Implementation Derived from Formal Model

Files Modified:

  • src/transducer/generalized/state.rs - Entry logic with preconditions
  • src/transducer/generalized/position.rs - Relaxed splitting invariants

Key Changes:

Splitting Invariant Relaxation

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:

  • Entry: offset - 1 (may temporarily exceed standard reachability)
  • Completion: offset + 1 (restores reachability)
  • Net effect: offset unchanged, so final position satisfies standard invariant

Entry Precondition Enforcement

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

3. Test Results

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 ✅:

  • ✅ All 2-to-1 digraph tests (ph→f, ch→k, th→t, sh→s)
  • ✅ All transpose tests (qu↔kw)
  • ✅ Mixed operations (merge, split, transpose)
  • ✅ Distance constraint tests
  • ✅ Standard operations integration

Improvements to Formal Model

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.

1. Update Splitting Invariant Definitions

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.

2. Update Entry Preconditions

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.

3. Re-proved Theorems with Relaxed Invariant

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.

Lessons Learned

1. Formal Model Iteration is Normal

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.

2. Proof-Driven Preconditions Work

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.

3. Intermediate States Need Relaxed Invariants

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.

4. Constructor Validation is Crucial

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.

5. Test-Driven Invariant Discovery

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.

Completed Follow-ups And Remaining Evaluation

1. Previously Failing Tests

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.

2. Formal Model Follow-up

  1. ✅ Changed splitting invariants to use the +1 intermediate-state buffer.
  2. ✅ Re-proved theorems with relaxed invariants.
  3. ✅ Added composition theorems for split plus standard successors.
  4. ✅ Proved cost accounting for split plus standard operations.

3. Property-Based Tests

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)
}

4. Performance Validation

  • Benchmark phonetic operations vs standard operations
  • Ensure fractional costs don't add overhead
  • Profile split entry/completion paths

Files Changed

Coq/Rocq

  • rocq/liblevenshtein/PhoneticOperations.v - Complete active phonetic operation model (483 lines)
  • rocq/liblevenshtein/_CoqProject - Added PhoneticOperations.v to build

Rust Implementation

  • src/transducer/generalized/state.rs - Entry logic with preconditions
  • src/transducer/generalized/position.rs - Relaxed splitting invariants

Documentation

  • docs/formal-verification/04_phonetic_operations.md - Insights and design
  • docs/formal-verification/PHASE4_SUMMARY.md - This document

Tests

  • ✅ Focused phonetic split tests passing: 7/7

Conclusion

Phase 4 demonstrates the power of formal-verification-first development:

  1. Started with formal model - Defined semantics in Coq before implementation
  2. Discovered preconditions through proofs - Critical constraints revealed by proof attempts
  3. Implemented from proven spec - Rust code derived from verified formal model
  4. Tests validated model - Failing tests revealed formal model needed refinement (splitting invariant)
  5. Iterated on formal model - Updated invariants based on implementation insights

Results:

  • Formal model: Active phonetic operation theorems proven without admits
  • Implementation: Focused split regression coverage passes for single split, consecutive split, split plus standard operation, and distance constraints
  • Documentation: Design rationale, proof obligations, and refreshed evidence captured

Next Steps:

  1. Add property-based tests to validate theorems empirically.
  2. Benchmark split-entry and split-completion paths against standard-operation hot paths.
  3. Consider whether arbitrary overlapping split chains need additional formal bounds.

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

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