Zulip Chat Archive

Stream: Program verification

Topic: Help with a basic proof about Vectors


Aiken Cairncross (Nov 25 2023 at 18:00):

I'm writing a data structure for representing n-dimensional (rectangular) arrays using a single flat array to contain the elements and this is needing me to do some of my first non-trivial proofs.

My problem is with defining the GetElem instance for my type. Given a proof that a multi-dimensional index is in bounds of an arrays shape (e.g. the index [5, 50, 15] is in bounds of the shape [10, 100, 20] because each index value is smaller than its corresponding dimension in the shape), I need to prove that the flattening of the multi-dimensional index into a single integer is in the bounds of the size of the flat array. I think I should be able to do it with induction but I can't figure out how/it's not giving me the hypotheses I thought it would.

Thanks in advance!

import Mathlib.Data.Vector

namespace Nat
def product (l : List Nat) : Nat := l.foldr (· * ·) 1
end Nat

namespace RankNArray

structure RankNArray (α : Type) (n : Nat) where
  data : Array α
  shape : Vector Nat n
  property : data.size = Nat.product shape.val

-- flattenIndex [s₀, .. , sₙ] [i₀, .. , iₙ] = (s₁ * .. * sₙ) * i₀ + .. + sₙ * i_(n-1) + iₙ
def flattenIndex (shape : Vector Nat n) (idx : Vector Nat n) : Nat :=
  let strides := (List.scanr (· * ·) 1 shape.val).tail
  let flatIndex := List.foldr (fun (s, i) acc =>
    acc + (s * i)) 0 (strides.zip idx.val)
  flatIndex

-- A multi-dimensional index is in the bounds of a shape if each of the indices
-- is less than its respective dimension.
def inBounds (xs : RankNArray α n) (idx : Vector Nat n) : Prop :=
  (xs.shape.toList.zip idx.toList).all fun (x, i) => i < x

def flattenIndex_inBounds (xs : RankNArray α n) (idx : Vector Nat n) : Prop :=
  flattenIndex xs.shape idx < Nat.product xs.shape.val

instance : GetElem (RankNArray α n) (Vector Nat n) α inBounds where
  getElem (xs : RankNArray α n) (idx : Vector Nat n) ok :=
    let flatIdx := List.foldr (fun a b => b) 0 idx.val
    -- Can't do this without a proof of (flattenIndex_inBounds xs idx):
    -- xs.data[flatIdx]
    sorry
    -- Try and establish (flattenIndex_inBounds xs idx) by proving a theorem
    -- below

-- If a multi-dimensional index is in the bounds of a shape, the flattening of
-- the index is in the bounds of the flattened shape
theorem inBounds_implies_flattenIndex_inBounds (xs : RankNArray α n) (idx : Vector Nat n) :
  inBounds xs idx -> flattenIndex_inBounds xs idx := by
    induction xs.shape.val
    -- where are the hypotheses, e.g. xs.shape.val = nil?
    sorry

end RankNArray

Thomas Murrills (Nov 25 2023 at 19:56):

Hmm, yeah…where are the hypotheses? I think we might be a bit too eager to clear them.

Mario Carneiro (Nov 26 2023 at 06:17):

-- If a multi-dimensional index is in the bounds of a shape, the flattening of
-- the index is in the bounds of the flattened shape
theorem inBounds_implies_flattenIndex_inBounds :
   (shape : Vector  n) (idx : Vector Nat n),
    (h : (shape.toList.zip idx.toList).all fun (x, i) => i < x) 
    flattenIndex shape idx < Nat.product shape.val
  | shape, hshape⟩, idx, hidx => by
    have H1 shape :
        (List.foldr (fun a x  (a * x.1, x.1 :: x.2)) (1, []) shape).1 = Nat.product shape := by
      unfold Nat.product; induction shape <;> simp [*]
    simp only [Vector.toList_mk, List.all_eq_true, decide_eq_true_eq, Prod.forall, flattenIndex,
      List.scanr, List.tail_cons, Nat.product]
    subst n
    induction shape generalizing idx with simp | cons a shape IH => ?_
    cases idx with injection hidx | cons b idx => ?_
    simp
    intro h
    specialize IH idx _ fun a b h' => h a b (.inr h')
    refine (add_lt_add_right IH _).trans_le ?_
    rw [H1, mul_comm a, add_comm, Nat.product,  Nat.mul_succ]
    have := h _ _ (.inl rfl, rfl⟩)
    exact Nat.mul_le_mul_left _ (Nat.succ_le_of_lt this)

instance : GetElem (RankNArray α n) (Vector Nat n) α inBounds where
    getElem (xs : RankNArray α n) (idx : Vector Nat n) ok :=
  xs.data[flattenIndex xs.shape idx]'
    (xs.property  inBounds_implies_flattenIndex_inBounds xs.shape idx ok)

Aiken Cairncross (Nov 26 2023 at 09:45):

Wow, I'll have a good study of that!

Aiken Cairncross (Nov 26 2023 at 18:08):

Hmm, yeah…where are the hypotheses? I think we might be a bit too eager to clear them.

I'm still interested in the answer to this though!

Mario Carneiro (Nov 26 2023 at 18:19):

doing induction xs.shape.val will only generalize xs.shape.val where it appears in the goal state, so all other facts about xs are lost (the variable xs is still around but it has no relation to the newly introduced variable being inducted over)

Mario Carneiro (Nov 26 2023 at 18:20):

That's why my proof works by pattern matching xs instead (or rather, calling a lemma with xs.shape, which itself pattern matches as ⟨shape, hshape⟩). Note that the induction shape line does manage to change the type of hshape to match

Mario Carneiro (Nov 26 2023 at 18:26):

However, regarding:

    -- where are the hypotheses, e.g. xs.shape.val = nil?

You normally don't want to have something like xs.shape.val = nil in the context after induction, because the only way to get that in the context is to put xs.shape.val = x as part of the induction hypothesis, and this will make the induction hypothesis useless (you will have IH : xs.shape.val = a -> P a and h : xs.shape.val = a :: b in the context, so the hypothesis is not applicable because it has a false assumption), which is why only cases does this (with the syntax cases e : xs.shape.val to introduce e : xs.shape.val = nil in the subgoal), not induction.

Thomas Murrills (Nov 26 2023 at 20:29):

Shouldn’t these equations be there if we’re generalizing xs, at least, though?


Last updated: Dec 20 2023 at 11:08 UTC