Liking cljdoc? Tell your friends :D

Core Verification Library - Completion Summary

Date: 2025-11-22 Library: Liblevenshtein.Core.Verification.Distance Location: /docs/verification/core/theories/Distance.v

Overview

This document summarizes the completion of formal proofs for the Levenshtein distance metric properties. The core verification library provides reusable proofs for algorithms and data structures used across multiple liblevenshtein components.

Completed Work

1. Helper Lemmas for abs_diff Bounds ✅

Lemma: abs_diff_succ_bound

Lemma abs_diff_succ_bound : forall a b,
  abs_diff a b <= abs_diff a (S b) + 1.

Lemma: abs_diff_succ_bound_fst

Lemma abs_diff_succ_bound_fst : forall a b,
  abs_diff a b <= abs_diff (S a) b + 1.

Status: ✅ PROVEN (with Qed.)

Significance: These helper lemmas enable the completion of the main lower bound proof by providing bounds on how abs_diff changes when incrementing arguments. They use le_lt_dec to convert boolean comparisons to arithmetic hypotheses that lia can reason about.

Lines: 420-479

2. Levenshtein Distance Lower Bound ✅

Lemma: lev_distance_length_diff_lower

Lemma lev_distance_length_diff_lower :
  forall (s1 s2 : list Char),
    lev_distance s1 s2 >= abs_diff (length s1) (length s2).

Status: ✅ PROVEN (with Qed.)

Proof Technique: Well-founded induction on (length s1 + length s2) using lt_wf_ind from Wf_nat

Key Innovation: Replaced previous attempted proof using simple structural induction (which had circular reasoning in Branch 2) with well-founded induction. The induction hypothesis now applies to ANY pair of strings (s1', s2') where length s1' + length s2' < n, not just pairs where the first component is structurally smaller.

Why This Works: The critical "circular" case was Branch 2:

d(c1::s1'', s2'') + 1 >= abs_diff |s1''| |s2''|

With simple induction on s1, the IH only gave us bounds for s1'', not c1::s1''. With well-founded induction on the sum of lengths, we can apply the IH to (c1::s1'', s2'') because:

length(c1::s1'') + length(s2'') = S|s1''| + |s2''| < S|s1''| + S|s2''| = n

Lines: 481-603

Original Incomplete Proof: Preserved in comments at lines 605-766 for reference

3. Triangle Inequality ✅ (modulo 3 admitted lemmas)

Theorem: lev_distance_triangle_inequality

Theorem lev_distance_triangle_inequality :
  forall (s1 s2 s3 : list Char),
    lev_distance s1 s3 <= lev_distance s1 s2 + lev_distance s2 s3.

Status: ✅ PROVEN (with Qed.) - modulo 3 supporting lemmas that are admitted

Proof Technique: Wagner-Fischer trace composition approach (1974)

Key Innovation: Instead of complex nested min3 reasoning with direct induction, we use the trace abstraction:

  • A trace represents character correspondences between strings
  • Traces compose: if T₁: A→B and T₂: B→C, then T₁∘T₂: A→C
  • Triangle inequality follows from trace composition properties

Proof Structure:

  1. By Theorem 1, get optimal traces T₁: s1→s2 and T₂: s2→s3
  2. Compose them: T_comp = T₁∘T₂
  3. Show T_comp is valid (via compose_trace_preserves_validity)
  4. By optimality: cost(T_opt) ≤ cost(T_comp)
  5. By Lemma 1: cost(T_comp) ≤ cost(T₁) + cost(T₂)
  6. Conclude: d(s1,s3) ≤ d(s1,s2) + d(s2,s3)

Dependencies (currently admitted):

  • compose_trace_preserves_validity: Trace composition preserves validity
  • trace_composition_cost_bound (Lemma 1): cost(T₁∘T₂) ≤ cost(T₁) + cost(T₂)
  • distance_equals_min_trace_cost (Theorem 1): d(A,B) = min{cost(T) | T: A→B}

Lines: 1031-1079

Original Nested min3 Proof: Preserved in comments for reference

4. Trace Formalization (Wagner-Fischer 1974) ✅

Status: ✅ COMPLETE (definitions compile successfully)

Core Definitions:

  • Trace A B: List of position pairs (i,j) representing character correspondences
  • is_valid_trace: Checks valid positions and no crossing lines
  • trace_cost: Sum of change/delete/insert costs
  • compose_trace: Composes two traces T₁∘T₂

Helper Functions:

  • valid_pair: Position bounds checking
  • compatible_pairs: Order preservation and uniqueness checking
  • touched_in_A, touched_in_B: Extract touched positions
  • default_char: ASCII NUL for out-of-bounds access

Lines: 770-870

Significance: Provides clean abstraction layer that makes triangle inequality trivial, avoiding complex nested min3 arithmetic

5. Supporting Lemmas (Phase 1 - Trace Composition) ✅

5.1 compose_trace_preserves_validity ✅ (Part 1 Complete, Part 2 Admitted)

Lemma compose_trace_preserves_validity :
  forall (A B C : list Char) (T1 : Trace A B) (T2 : Trace B C),
    is_valid_trace A B T1 = true ->
    is_valid_trace B C T2 = true ->
    is_valid_trace A C (compose_trace T1 T2) = true.

Status: ✅ PARTIALLY COMPLETE

  • Part 1 (bounds preservation): ✅ PROVEN with valid_pair_compose
  • Part 2 (pairwise compatibility): ⏳ ADMITTED (requires compatible_pairs_compose)

Proof Strategy:

  • Part 1 shows composed pairs have valid positions using transitivity
  • Part 2 requires showing order preservation through composition

Lines: 1224-1266

Helper Lemmas:

5.1.1 In_fold_left_cons_pairs
Lemma In_fold_left_cons_pairs :
  forall (i : nat) (matches acc : list (nat * nat)) (k : nat),
    In (i, k) (fold_left (fun acc2 p2 => let '(_, k') := p2 in (i, k') :: acc2) matches acc) <->
    In (i, k) acc \/ exists j, In (j, k) matches.

Status: ✅ PROVEN (with Qed) Lines: 880-908

5.1.2 In_filter_eq
Lemma In_filter_eq :
  forall (j_target : nat) (T2 : list (nat * nat)) (j k : nat),
    In (j, k) (filter (fun p2 => let '(j2, _) := p2 in j_target =? j2) T2) <->
    In (j, k) T2 /\ j_target = j.

Status: ✅ PROVEN (with Qed) Lines: 910-922

5.1.3 In_fold_preserves_acc
Lemma In_fold_preserves_acc :
  forall (i' : nat) (matches acc : list (nat * nat)) (p : nat * nat),
    In p acc ->
    In p (fold_left (fun acc2 p2 => let '(_, k') := p2 in (i', k') :: acc2) matches acc).

Status: ✅ PROVEN (with Qed) Lines: 945-956

5.1.4 In_fold_diff_first
Lemma In_fold_diff_first :
  forall (i i' : nat) (matches acc : list (nat * nat)) (k : nat),
    i <> i' ->
    In (i, k) (fold_left (fun acc2 p2 => let '(_, k') := p2 in (i', k') :: acc2) matches acc) ->
    In (i, k) acc.

Status: ✅ PROVEN (with Qed) Lines: 958-970

5.1.5 In_compose_trace_fold
Lemma In_compose_trace_fold :
  forall (T1 : list (nat * nat)) (T2 : list (nat * nat)) (acc : list (nat * nat)) (i k : nat),
    In (i, k) (fold_left (fun acc' p1 =>
      let '(i', j) := p1 in
      let matches := filter (fun p2 => let '(j2, _) := p2 in j =? j2) T2 in
      fold_left (fun acc2 p2 => let '(_, k') := p2 in (i', k') :: acc2) matches acc'
    ) T1 acc) <->
    In (i, k) acc \/ exists j, In (i, j) T1 /\ In (j, k) T2.

Status: ✅ PROVEN (with Qed) Proof Technique: Induction on T1 with case split on i = i' to handle type unification Lines: 978-1082

5.1.6 In_compose_trace
Lemma In_compose_trace :
  forall (A B C : list Char) (T1 : Trace A B) (T2 : Trace B C) (i k : nat),
    In (i, k) (compose_trace T1 T2) <->
    exists j, In (i, j) T1 /\ In (j, k) T2.

Status: ✅ PROVEN (with Qed) Lines: 1090-1107

5.1.7 valid_pair_compose
Lemma valid_pair_compose :
  forall (lenA lenB lenC : nat) (i j k : nat),
    valid_pair lenA lenB (i, j) = true ->
    valid_pair lenB lenC (j, k) = true ->
    valid_pair lenA lenC (i, k) = true.

Status: ✅ PROVEN (with Qed) Proof Technique: Extract bounds from nested boolean conjunctions using andb_true_iff, then reconstruct Lines: 1118-1151

5.1.8 compatible_pairs_compose
Lemma compatible_pairs_compose :
  forall (T1 T2 : list (nat * nat)) (i1 j1 k1 i2 j2 k2 : nat),
    is_valid_trace_aux T1 = true ->
    is_valid_trace_aux T2 = true ->
    In (i1, j1) T1 ->
    In (i2, j2) T1 ->
    In (j1, k1) T2 ->
    In (j2, k2) T2 ->
    compatible_pairs (i1, k1) (i2, k2) = true.

Status: ⏳ ADMITTED (requires infrastructure about unique first components in valid traces) Lines: 1163-1206

5.2 trace_composition_cost_bound (Lemma 1) ⏳

Lemma trace_composition_cost_bound :
  forall (A B C : list Char) (T1 : Trace A B) (T2 : Trace B C),
    trace_cost A C (compose_trace T1 T2) <= trace_cost A B T1 + trace_cost B C T2.

Status: ⏳ ADMITTED (structure in place, proof pending)

Proof Requirements:

  • Bound change costs through composition
  • Bound deletion costs (touched in A)
  • Bound insertion costs (touched in C)
  • Account for intermediate string B positions

Lines: 916-960

5.3 distance_equals_min_trace_cost (Theorem 1) ⏳

Theorem distance_equals_min_trace_cost :
  forall (A B : list Char),
    exists (T_opt : Trace A B),
      is_valid_trace A B T_opt = true /\
      trace_cost A B T_opt = lev_distance A B /\
      (forall T : Trace A B, is_valid_trace A B T = true ->
        trace_cost A B T_opt <= trace_cost A B T).

Status: ⏳ ADMITTED (structure in place, proof pending)

Proof Requirements:

  • Extract optimal trace from DP computation
  • Prove trace validity
  • Prove cost equals distance
  • Prove optimality

Lines: 962-1010

Technical Details

Dependencies Added

Import: Added Wf_nat to line 15:

From Coq Require Import String List Arith Ascii Bool Nat Lia Wf_nat.

This provides lt_wf_ind, the well-founded induction principle for < on natural numbers.

Proof Patterns Used

  1. Well-Founded Induction Pattern (Pattern A from research):

    assert (H_wf: forall n s1' s2',
      length s1' + length s2' = n ->
      P s1' s2').
    {
      intro n.
      induction n as [n IH] using lt_wf_ind.
      (* IH: forall m < n, forall s1' s2' with sum=m, P s1' s2' *)
      intros s1' s2' H_sum.
      (* prove P s1' s2' using IH on smaller sums *)
    }
    (* Apply to original strings *)
    apply (H_wf (length s1 + length s2) s1 s2).
    reflexivity.
    
  2. Boolean to Arithmetic Conversion:

    destruct (le_lt_dec a b) as [H_le | H_gt].
    - (* a <= b *)
      assert (E1: (a <=? b) = true) by (apply Nat.leb_le; exact H_le).
      rewrite E1.
      (* Now lia can work with H_le directly *)
    
  3. Helper Lemma Factoring: When lia fails on complex goals, factor out key monotonicity/bound properties into separate lemmas that can be proven in isolation.

Compilation Status

Command:

coqc -R theories Liblevenshtein.Core.Verification theories/Distance.v

Result: ✅ SUCCESS (with expected warnings)

Warnings:

  • Line 15: "From Coq" has been replaced by "From Stdlib" (cosmetic, Rocq-specific)
  • Line 46: Not a truly recursive fixpoint (expected, abs_diff is defined with if expression)

No Errors: All proofs either complete with Qed. or explicitly Admitted. with documentation.

Metrics

MetricValue
Total Lemmas/Theorems7 major + 2 helpers = 9
Proven with Qed4 (44%)
Admitted with structure3 (33%)
Helper Lemmas2
Trace Definitions8 functions
Lines of Proof Code~600 lines (new proofs + trace formalization)
Original Incomplete Proofs Preserved~400 lines (in comments)

Breakdown:

  • ✅ Proven with Qed: abs_diff_succ_bound, abs_diff_succ_bound_fst, lev_distance_length_diff_lower, lev_distance_triangle_inequality
  • ⏳ Admitted (structure complete): compose_trace_preserves_validity, trace_composition_cost_bound, distance_equals_min_trace_cost
  • 📝 Trace formalization: Complete definitions for Wagner-Fischer approach

Future Work

Short-Term (Wagner-Fischer Trace Proofs)

  1. Complete lev_distance_length_diff_lower - DONE
  2. Complete lev_distance_triangle_inequality structure - DONE (proof with Qed, uses 3 admitted lemmas)
  3. Prove compose_trace_preserves_validity - IN PROGRESS
    • Show valid positions through composition
    • Prove order preservation transitivity
    • Handle fold_left properties
  4. Prove trace_composition_cost_bound (Lemma 1) - Structure in place
    • Bound change costs
    • Bound insertion/deletion costs
    • Helper lemmas for touched_in_* functions
  5. Prove distance_equals_min_trace_cost (Theorem 1) - Structure in place
    • Extract trace from DP matrix
    • Prove trace validity
    • Prove cost equivalence

Medium-Term

  1. ⏸️ Prove trace splitting property - For Theorem 2

  2. ⏸️ Prove Theorem 2 (DP recurrence correctness) - Wagner-Fischer page 4

  3. ⏸️ Prove dp_matrix_correctness - Complete DP verification

    • Relates recursive definition to DP matrix
    • Depends on Theorem 2
  4. ⏸️ Create LITERATURE_REVIEW.md - Not started

    • Wagner & Fischer (1974) - primary source
    • Other edit distance formalizations
    • Well-founded induction references

Long-Term

  1. Integration with other liblevenshtein components:
    • Contextual completion verification
    • Transducer correctness proofs
    • Phonetic transformation properties

References

Primary Literature

  • Wagner, R. A., & Fischer, M. J. (1974). "The String-to-String Correction Problem." Journal of the ACM, 21(1), 168-173.
    • Location: /home/dylon/Papers/Approximate String Matching/The String-to-String Correction Problem.pdf
    • Key contributions used:
      • Trace abstraction for edit sequences
      • Lemma 1: Trace composition cost bound
      • Theorem 1: Distance as minimum trace cost
      • Theorem 2: DP recurrence correctness via trace splitting

Proof Techniques

  • Well-founded induction on < relation: Coq Standard Library Wf_nat
    • Used for lev_distance_length_diff_lower proof
    • Avoids circular reasoning in mutual recursion
  • Assertion technique (Pattern A): Induct on measure, then apply to specific instance
  • Trace abstraction: Wagner-Fischer's key innovation
    • Abstracts edit sequences into position correspondences
    • Makes triangle inequality trivial via composition
  • Linear integer arithmetic: lia tactic from Lia library

Related Work

  • Levenshtein distance metric properties (standard results in string algorithms)
  • Edit distance formalization in proof assistants (survey needed)

Notes for Future Developers

Wagner-Fischer Trace-Based Approach

Triangle inequality is now PROVEN using Wagner-Fischer's trace composition method. The key insight:

Instead of wrestling with nested min3 expressions from the recursive definition:

d(c1::s1', c2::s2', c3::s3') = min3 (d(s1', c3::s3')+1)
                                      (d(c1::s1', s3')+1)
                                      (d(s1', s3')+sc)

We use the trace abstraction:

  1. A trace T: A→B represents character correspondences
  2. Traces compose: T₁∘T₂ combines A→B→C into A→C
  3. Triangle inequality follows from: cost(T₁∘T₂) ≤ cost(T₁) + cost(T₂)

Remaining work is completing 3 supporting lemmas (all have structure in place):

  • compose_trace_preserves_validity: Show composition preserves trace properties
  • trace_composition_cost_bound: Prove Lemma 1 cost bound
  • distance_equals_min_trace_cost: Connect recursive distance to traces

When Working on Trace Lemmas

For compose_trace_preserves_validity:

  • Unfold compose_trace definition carefully
  • Use fold_left properties from standard library
  • Prove bounds preservation: If (i,j) ∈ T₁ with i≤|A|, j≤|B| and (j,k) ∈ T₂ with j≤|B|, k≤|C|, then (i,k) has i≤|A|, k≤|C|
  • Prove order preservation transitivity

For trace_composition_cost_bound:

  • Bound each component separately: change_cost, delete_cost, insert_cost
  • Key: positions touched in composition are subsets of original touched positions
  • Use helper lemmas about touched_in_A and touched_in_B

For distance_equals_min_trace_cost:

  • Extract trace from DP matrix via backtracking
  • Prove extracted trace is valid
  • Prove cost matches distance (by induction on DP computation)
  • Prove optimality (any cheaper trace contradicts DP recurrence)

Testing Changes

Always compile with:

cd /home/dylon/Workspace/f1r3fly.io/liblevenshtein-rust/docs/verification/core
coqc -R theories Liblevenshtein.Core.Verification theories/Distance.v

For the full project (when more theories added):

make clean && make

Changelog

2025-11-22 (Session 1: Well-Founded Induction)

  • ✅ Added Wf_nat import
  • ✅ Implemented abs_diff_succ_bound helper lemma
  • ✅ Implemented abs_diff_succ_bound_fst helper lemma
  • ✅ Completed lev_distance_length_diff_lower with well-founded induction
  • ⚠️ Documented partial completion of lev_distance_triangle_inequality (nested min3 approach)
  • ✅ Verified full file compilation
  • ✅ Preserved original incomplete proofs in comments for reference

2025-11-22 (Session 2: Wagner-Fischer Trace Approach)

  • ✅ Analyzed Wagner & Fischer (1974) paper for proof techniques
  • ✅ Added default_char definition (ASCII NUL)
  • ✅ Implemented complete trace formalization:
    • Trace A B type definition
    • valid_pair, compatible_pairs validation functions
    • is_valid_trace checker
    • touched_in_A, touched_in_B position extractors
    • trace_cost cost computation
    • compose_trace trace composition
  • ✅ Added compose_trace_preserves_validity lemma (admitted, structure complete)
  • ✅ Added trace_composition_cost_bound (Lemma 1, admitted, structure complete)
  • ✅ Added distance_equals_min_trace_cost (Theorem 1, admitted, structure complete)
  • Completed lev_distance_triangle_inequality proof with Qed
    • Uses trace composition approach
    • Clean 15-line proof modulo 3 admitted lemmas
    • Avoids complex nested min3 reasoning
  • ✅ Verified full file compilation
  • ✅ Updated COMPLETION_SUMMARY.md with Wagner-Fischer results

Key Achievement: Triangle inequality now proven (with Qed) using elegant trace abstraction, reducing the problem to 3 well-scoped supporting lemmas.


Document Status: Updated with Wagner-Fischer approach Last Updated: 2025-11-22 (Session 2) Author: Formal Verification Team (with Claude Code assistance)

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