Zulip Chat Archive

Stream: new members

Topic: Difficulty proving byte/bitvec conversion theorems


Paula Neeley (Sep 19 2025 at 16:30):

I'm working on some code where I sometimes have to reason about bit vectors, and other times about lists of bytes. I've defined some conversion functions between the two, but I'm having more difficulty than I expected proving that the conversion functions are inverses of each other.

Specifically, I can prove the inverse theorems (which I'm calling list_bytes_to_bitvec_eq and bitvec_to_list_bytes_eq) if I rely on some helper lemmas, but I'm having trouble proving those. If I unfold definitions, things just get really messy.

What proof approaches should I be using on the helper functions? Or should I redefine my conversion functions away from zipIdx, foldl, and reverse to somehow make the reasoning easier to do? Thanks in advance.

import Mathlib

@[reducible] def ByteSize :  := 8

@[reducible] def Byte := BitVec ByteSize
  deriving DecidableEq

inductive Endian
  | l
  | b
  deriving DecidableEq

def BytesToBitvec {n : Nat} (bytes : List Byte) (e : Endian) (_ : bytes.length = n) : BitVec (ByteSize * n) :=
  let ordered := match e with
    | Endian.l => bytes
    | Endian.b => bytes.reverse
  let natVal := ordered.zipIdx.foldl (init := 0) fun acc b, i => acc + (b.toNat <<< (ByteSize * i))
  BitVec.ofNat (ByteSize * n) natVal

def BitvecToBytes {n : Nat} (bv : BitVec (ByteSize * n)) (e : Endian) : List Byte :=
  let ordered := match e with
    | Endian.l => List.range n
    | Endian.b => (List.range n).reverse
  ordered.map (λ i =>
    let shift := i * ByteSize
    BitVec.ofNat ByteSize ((bv >>> shift).toNat &&& 0xFF))

lemma BitvecToBytes_length {n : } (bv : BitVec (ByteSize * n)) (e : Endian) :
  (BitvecToBytes bv e).length = n :=
  by
  unfold BitvecToBytes
  dsimp
  simp_all only [BitVec.ofNat_and, List.length_map]
  split
  next e => simp_all only [List.length_range]
  next e => simp_all only [List.length_reverse, List.length_range]


lemma BitvecToBytes_cons_LE {n : } (b : Byte) (bs : List Byte) (hbs : bs.length = n) :
  BitvecToBytes (BytesToBitvec (b :: bs) Endian.l (by rfl)) Endian.l =
  b :: BitvecToBytes (BytesToBitvec bs Endian.l hbs) Endian.l :=
  by
  ext m b'
  constructor
  intro H
  subst hbs
  have H1 := @List.getElem?_cons _ b (BitvecToBytes (BytesToBitvec bs Endian.l (by rfl)) Endian.l) m
  rw [H1]
  sorry
  sorry

lemma BitvecToBytes_append_BE {n : } (b : Byte) (bs : List Byte) (hbs : bs.length = n) :
  BitvecToBytes (BytesToBitvec (b :: bs) Endian.b (by rfl)) Endian.b =
  BitvecToBytes (BytesToBitvec bs Endian.b hbs) Endian.b ++ [b] :=
  by
  ext m b'
  constructor
  intro H
  subst hbs
  sorry
  sorry


theorem list_bytes_to_bitvec_eq (bs : List Byte) (e : Endian) :
  BitvecToBytes (BytesToBitvec bs e (by rfl)) e =
    match e with
    | Endian.l => bs
    | Endian.b => bs.reverse :=
  by
  cases e with
  | l =>
    induction bs with
    | nil =>
      unfold BitvecToBytes
      unfold BytesToBitvec
      simp_all only [List.length_nil, Nat.mul_zero, List.zipIdx_nil, List.foldl_nil, BitVec.zero_ushiftRight,
        BitVec.toNat_ofNat, pow_zero, Nat.zero_mod, Nat.zero_and, List.range_zero, List.map_nil]
    | cons b bs ih =>
      rw [@BitvecToBytes_cons_LE bs.length b bs]
      rw [ih]
  | b =>
    induction bs with
    | nil =>
      unfold BitvecToBytes
      unfold BytesToBitvec
      simp_all only [List.length_nil, Nat.mul_zero, List.reverse_nil, List.zipIdx_nil, List.foldl_nil,
        BitVec.zero_ushiftRight, BitVec.toNat_ofNat, pow_zero, Nat.zero_mod, Nat.zero_and, List.range_zero,
        List.map_nil]
    | cons b bs ih =>
      have H := @BitvecToBytes_append_BE bs.length b bs
      rw [H]
      rw [ih]
      simp_all only [List.length_cons, forall_const, List.reverse_cons]


lemma BitvecToBytes_reconst {x : BitVec (ByteSize * n)} (e : Endian) :
  let bs := BitvecToBytes x e
  let ordered := match e with
    | Endian.l => bs
    | Endian.b => bs.reverse
  ordered.zipIdx.foldl (init := 0) (fun acc b, i => acc + (b.toNat <<< (ByteSize * i))) = x.toNat :=
  by
  unfold BitvecToBytes at *
  unfold ByteSize at *
  unfold Byte at *
  intro bs ordered
  simp_all only [Lean.Elab.WF.paramLet, BitVec.toNat_ushiftRight, BitVec.ofNat_and, ordered, bs]
  cases e with
  | l =>
    simp_all only
    sorry
  | b =>
    simp_all only [List.map_reverse, List.reverse_reverse]
    sorry


theorem bitvec_to_list_bytes_eq (x : BitVec (ByteSize * n)) (e : Endian) :
  BytesToBitvec (BitvecToBytes x e) e (by rw [BitvecToBytes_length]) = x :=
  by
  have H : (BytesToBitvec (BitvecToBytes x e) e (by rw [BitvecToBytes_length])) = BitVec.ofNat (ByteSize * n) x.toNat :=
    by
    have H1 := @BitvecToBytes_reconst n x e
    unfold BytesToBitvec at *
    unfold BitvecToBytes at *
    simp only
    simp_all only [BitVec.toNat_ushiftRight, BitVec.ofNat_and, BitVec.ofNat_toNat, BitVec.setWidth_eq]
  rw [H]
  simp_all only [BitVec.ofNat_toNat, BitVec.setWidth_eq]

Robin Arnez (Sep 19 2025 at 18:12):

What about

@[reducible] def byteSize : Nat := 8

@[reducible] def Byte := BitVec byteSize
  deriving DecidableEq

inductive Endian
  | l
  | b
  deriving DecidableEq

def bytesToBitvec {n : Nat} (bytes : List Byte) (e : Endian) (h : bytes.length = n) : BitVec (byteSize * n) :=
  match bytes with
  | [] => 0#_
  | a :: l =>
    match e with
    | .l => (bytesToBitvec l e rfl ++ a).cast (by grind)
    | .b => (a ++ bytesToBitvec l e rfl).cast (by grind)

def bitvecToBytes {n : Nat} (bv : BitVec (byteSize * n)) (e : Endian) : List Byte :=
  match e with
  | .l => List.ofFn (n := n) fun i => bv.extractLsb' (byteSize * i) _
  | .b => List.ofFn (n := n) fun i => bv.extractLsb' (byteSize * (n - i - 1)) _

@[simp]
theorem length_bitvecToBytes (bv : BitVec (byteSize * n)) (e : Endian) : (bitvecToBytes bv e).length = n := by
  cases e <;> simp [bitvecToBytes]

theorem bitvecToBytes_bytesToBitvec (l : List Byte) (e : Endian) : bitvecToBytes (bytesToBitvec l e rfl) e = l := by
  induction l with
  | nil => cases e <;> simp [bytesToBitvec, bitvecToBytes]
  | cons a l ih => cases e <;> simp_all [bitvecToBytes, bytesToBitvec] <;> grind

theorem bytesToBitvec_bitvecToBytes (bv : BitVec (byteSize * n)) (e : Endian) :
    bytesToBitvec (bitvecToBytes bv e) e (by simp) = bv := by
  induction n with
  | zero => apply BitVec.eq_of_zero_length; simp
  | succ k ih =>
    cases e
    · specialize ih (bv.extractLsb' byteSize (byteSize * k))
      simp_all [bitvecToBytes, List.ofFn_succ, bytesToBitvec]
      grind
    · specialize ih (bv.extractLsb' 0 (byteSize * k))
      simp_all [bitvecToBytes, List.ofFn_succ, bytesToBitvec]
      grind

Paula Neeley (Sep 19 2025 at 18:38):

Thanks, this code certainly looks much cleaner. Unfortunately, the grind actually doesn't work for me on bytesToBitvec_bitvecToBytes and bitvecToBytes_bytesToBitvec. Does it depend on what version of Lean I'm running? I'm using v4.22.0.


Last updated: Dec 20 2025 at 21:32 UTC