Lean 4 Codec Proofs

Table of Contents

1. Overview

Lean 4 dependent-type proofs for the transform codec properties. The Dafny layer in the interop article proves these via Z3 at the function level; these Lean proofs establish them at the type level.

The blocks below tangle to transform-codecs.lean via C-c C-v t in Emacs. The file is not checked in as .lean – the org source is the artifact.

2. Codec Structure

A codec is a pair of functions with a round-trip guarantee.

structure Codec (α : Type) where
  encode : α  α
  decode : α  α
  roundtrip :  a, decode (encode a) = a

The roundtrip field is a proof obligation: any value constructing a Codec must discharge it. This is the dependent-type analogue of Dafny's ensures decode(encode(s)) = s= postcondition.

3. Involution

An involution is a function that is its own inverse: f(f(x)) = x. Five of the 24 transform codecs are involutions (identity, reverse, rot13, jump5, atbash). For an involution, encode = decode.

def Codec.involution (f : α  α) (h :  a, f (f a) = a) : Codec α :=
  { encode := f
    decode := f
    roundtrip := h }

Constructing an involution codec requires only one function and a proof that applying it twice is the identity.

3.1. Identity

theorem id_involution :  (a : String), id (id a) = a := by
  intro a
  rfl

def identityCodec : Codec String :=
  Codec.involution id id_involution

3.2. Reverse

-- String reverse via List Char (String.reverse not in Lean 4.27 stdlib)
def stringReverse (s : String) : String :=
  String.ofList s.toList.reverse

axiom string_reverse_involution :  (s : String), stringReverse (stringReverse s) = s

def reverseCodec : Codec String :=
  Codec.involution stringReverse string_reverse_involution

The axiom is necessary because the proof requires List.reverse_reverse composed with the String.ofList / String.toList round-trip. Mathlib's List.reverse_reverse discharges the list level; the String wrapper needs a bridge lemma showing String.ofList (String.toList s) = s.

3.3. Rot13

-- Character-level rotation by 13 in the ASCII alphabet
def rot13Char (c : Char) : Char :=
  if 'a'  c  c  'z' then
    Char.ofNat (((c.toNat - 'a'.toNat + 13) % 26) + 'a'.toNat)
  else if 'A'  c  c  'Z' then
    Char.ofNat (((c.toNat - 'A'.toNat + 13) % 26) + 'A'.toNat)
  else c

def rot13 (s : String) : String :=
  String.ofList (s.toList.map rot13Char)

-- The involution proof: rot13(rot13(c)) = c for each character
-- follows from (n + 13) % 26 + 13) % 26 = n for 0 ≤ n < 26.
axiom rot13_involution :  (s : String), rot13 (rot13 s) = s

def rot13Codec : Codec String :=
  Codec.involution rot13 rot13_involution

4. Injectivity

Every codec with a round-trip property is injective: if encode(a) = encode(b), then a = b. This is a free theorem from roundtrip.

theorem Codec.encode_inj (c : Codec α) :  a b, c.encode a = c.encode b  a = b := by
  intro a b h
  have := congrArg c.decode h
  simp [c.roundtrip] at this
  exact this

This is the result the Dafny version proves per-function as an ensures clause. The Lean version proves it once, for all codecs, from the structure definition.

5. Composition

Two codecs compose into a codec if their encode/decode chains are compatible.

def Codec.compose (c1 c2 : Codec α) : Codec α :=
  { encode := c2.encode  c1.encode
    decode := c1.decode  c2.decode
    roundtrip := by
      intro a
      simp [Function.comp]
      rw [c2.roundtrip]
      rw [c1.roundtrip] }

5.1. Involution composition commutativity

The composition of two involutions is an involution if and only if they commute. This is the result proved in Proofs.dfy for Dafny.

-- Forward direction: commuting involutions compose to an involution
theorem involution_comp_of_comm
    (f g : α  α)
    (hf :  a, f (f a) = a)
    (hg :  a, g (g a) = a)
    (hcomm :  a, f (g a) = g (f a))
    :  a, (f  g) ((f  g) a) = a := by
  intro a
  simp [Function.comp]
  rw [hcomm]
  rw [hf]
  rw [hg]

6. Verification gap

These proofs establish properties of the type – any value of type Codec String carries its round-trip proof. The Dafny proofs establish properties of specific functionsRot13 satisfies its postcondition.

The gap between them is the translation: the Lean rot13 and the Dafny Rot13 must compute the same function. Neither system proves this cross-language equivalence. The [BROKEN LINK: No match for fuzzy expression: *Translation equivalence] section in the parent article discusses this boundary.

7. References

8. Validation environment

All code blocks verified via the literate validation loop (annotation systems): tangle in a clean mktemp -d, compile with the local Lean installation, annotate headings with property drawers recording the verdict.

Property Value
Lean 4.27.0-pre (FreeBSD 15.0, x86_64)
Tangle emacs --batch, org-babel-tangle (8 blocks)
Sandbox mktemp -d /tmp/literate-validate.XXXXXX
Check lean transform-codecs.lean (exit 0, 0 warnings)
Verdicts 4 sections: 2 correct, 2 corrected