Use when proving encode/decode roundtrip theorems, suffix invariance (_append lemmas), goR (decode-with-remaining) patterns, padding extraction, or composing per-level roundtrip proofs into a unified theorem.
Suffix invariance proves that appending extra bits to decoder input doesn't affect the decoded result — the decoder processes only its content and leaves trailing bits untouched.
Every _append lemma follows this template:
theorem f_append (bits suffix : List Bool) (result : T) (rest : List Bool)
(h : f bits = some (result, rest)) :
f (bits ++ suffix) = some (result, rest ++ suffix)
Hypothesis: f succeeds on bits, consuming them down to rest.
Goal: f succeeds on bits ++ suffix, leaving rest ++ suffix.
Suffix invariance composes bottom-up. Each layer's proof uses the layer below:
readBitsLSB_append — induction on bit count, case-split on listdecodeStored_append, decodeLitLen_append, decodeSymbols_append
— use readBitsLSB_append + Huffman decode invariancedecodeDynamicTables_append — chains multiple decoder-level appendsdecode_go_suffix — case-splits on block type, applies per-type appendtheorem f_append (bits suffix : List Bool) ... :
f (bits ++ suffix) = some (result, rest ++ suffix) := by
-- Induction on fuel or structure
induction n generalizing bits result rest with
| zero => simp_all [f]
| succ k ih =>
-- Unfold one layer
unfold f at h ⊢
-- Case-split on sub-operations
cases hx : subOp bits with
| none => simp [hx] at h
| some p =>
obtain ⟨val, rem⟩ := p
-- Apply lower-level _append to transform goal
rw [subOp_append bits suffix val rem hx]
-- Reduce monadic bind
dsimp only [bind, Except.bind] -- or Option.bind
simp only [hx] at h
-- Recurse or close
exact ih rem suffix ...
dsimp only [bind, Except.bind] after rewriting with _append — reduces
match .ok x with ... without looping (see lean-monad-proofs skill)by_cases hcond : condition for if branches — apply rw [if_pos/if_neg]
at both h and ⊢simp [hx] at h ⊢ simultaneously in suffix proofs — h has
f bits while ⊢ has f (bits ++ suffix), so the same simp won't match bothgoR is a variant of the decoder's recursive go function that returns both
the decoded result AND the remaining (unconsumed) bits.
Use goR when you need to prove properties about what the decoder leaves behind
after processing — especially for:
def decode.goR (bits : List Bool) (acc : List UInt8) :
Nat → Option (List UInt8 × List Bool)
| 0 => none
| fuel + 1 => do
let (bfinal, bits) ← readBitsLSB 1 bits
let (btype, bits) ← readBitsLSB 2 bits
match btype with
| 0 =>
let (bytes, bits) ← decodeStored bits
let acc := acc ++ bytes
if bfinal == 1 then return (acc, bits) else decode.goR bits acc fuel
| 1 => ... -- same structure, returns (acc, remaining_bits)
| _ => none
Two key theorems connect goR to the original go:
decode_goR_fst — (goR bits acc fuel).map Prod.fst = go bits acc fueldecode_goR_exists — if go succeeds, goR returns some remaining bitstheorem decode_goR_fst ... :
(goR bits acc fuel).map Prod.fst = go bits acc fuel := by
induction fuel generalizing bits acc with
| zero => simp [goR, go]
| succ n ih =>
unfold goR go
-- Case-split on each monadic operation
cases h1 : readBitsLSB 1 bits with
| none => simp [h1]
| some p =>
simp only [h1, bind, Option.bind]
-- ... continue matching structure of go ...
-- Recursive call: exact ih ...
After encoding, the bitstream must be byte-aligned. The padding proof shows
that the encoder's output decomposes as contentBits ++ padding where
padding.length < 8.
theorem deflateRaw_pad (data : ByteArray) (level : UInt8) :
∃ (contentBits padding : List Bool),
bytesToBits (deflateRaw data level) = contentBits ++ padding ∧
padding.length < 8 := by
unfold deflateRaw
split
· -- Level 0 (stored): byte-aligned, padding = []
exact ⟨..., [], ..., by simp⟩
· split
· -- Level 1 (fixed Huffman): extract from deflateFixed_spec
obtain ⟨bits, _, hbytes⟩ := deflateFixed_spec data
exact ⟨bits, List.replicate ((8 - bits.length % 8) % 8) false,
hbytes, by simp [List.length_replicate]; omega⟩
· ... -- similar for other levels
deflateFixed_spec) provide the
contentBits and prove bytesToBits output = contentBits ++ paddingList.replicate ((8 - bits.length % 8) % 8) falsesimp [List.length_replicate]; omega closes the < 8 goal[] (empty) since stored blocks are byte-alignedThe top-level roundtrip theorem composes per-level proofs:
theorem inflate_deflateRaw (data : ByteArray) (level : UInt8)
(hsize : data.size < maxSize) :
inflate (deflateRaw data level) = .ok data := by
unfold deflateRaw
split
· exact inflate_deflateStoredPure data (by omega)
· split
· exact inflate_deflateFixedIter data (by omega)
· split
· exact inflate_deflateLazy data hsize
· exact inflate_deflateDynamic data (by omega)
by omegaEach per-level roundtrip follows the same pyramid:
encode_spec: encoder produces specific bit structure
↓
decode_go_suffix: decoder preserves trailing bits (suffix invariance)
↓
goR connection: decoder leaves exactly the padding bits
↓
inflate_complete: bit-to-byte alignment → original data recovered
When an optimized iterative version uses an accumulator, prove equivalence with the recursive version.
theorem iterFunc_eq_recFunc (acc : Array T) (args...) :
iterFunc args acc = acc ++ (recFunc args).toArray
induction h : measure using Nat.strongRecOn generalizing pos acc with
| _ n ih =>
unfold iterFunc recFunc
-- Normalize shared helpers (non-recursive = definitionally equal)
simp only [show @iterHelper = @recHelper from rfl]
split
· -- Recursive case: rewrite with IH, then array manipulation
rw [ih _ (by omega) _ _ rfl]
rw [List.toArray_cons, ← Array.append_assoc, Array.push_eq_append]
· -- Base case
simp
show @iterHelper = @recHelper from rfl — when helper functions are
definitionally equal (non-recursive), use this as a simp lemma to normalizeArray.push_eq_append + Array.append_assoc — for accumulator manipulationList.toArray_cons — converts (x :: xs).toArray to #[x] ++ xs.toArraydata.size - pos)push_eq_append + toArray_conssimp only [Array.push_eq_append, List.toArray_cons, Array.append_assoc] loops.
These two lemmas create a rewriting cycle. Use explicit rw in this order instead:
-- Single push: acc.push x ++ rest.toArray = acc ++ (x :: rest).toArray
rw [Array.push_eq_append, Array.append_assoc, ← List.toArray_cons]
-- Double push: (acc.push x).push y ++ rest.toArray = acc ++ (x :: y :: rest).toArray
rw [Array.push_eq_append, Array.push_eq_append,
Array.append_assoc, Array.append_assoc,
← List.toArray_cons, ← List.toArray_cons]
Order matters: normalize pushes to appends first, reassociate, then fold back.
Build bottom-up: helper equivalences first, then compose in the main theorem:
theorem lz77GreedyIter_eq_lz77Greedy (data : ByteArray) (ws : Nat) :
lz77GreedyIter data ws = lz77Greedy data ws := by
unfold lz77GreedyIter lz77Greedy
split
· rw [trailing_eq]; simp
· rw [mainLoop_eq]; simp