Zulip Chat Archive
Stream: Program verification
Topic: Destructing fixed-length list
Hanno Becker (Apr 23 2024 at 09:33):
Hi :wave: Another example of 'how would an expert do this'... I have a very simple lemma the type of which a serious program verification effort would likely encounter hundreds or thousands. Therefore, I am interested in a short proof which requires as little human intervention as possible. Also, I think the lemma is so trivial that no specialized tactics should be required -- ideally, I'd like to stay in the core lib.
The lemma is x.length = 4 --> x = [x[0], x[1], x[2], x[3]]
and here are my clumsy attempts. You will find numerous QUESTION
s where I'm not sure what's going on, and I'd be grateful about clarification on any of them.
I am particularly interested in knowing how the core tactics would allow me to repeatedly and anonymously (without naming hypotheses explicitly) split _any_ disjunction in the set of hypotheses, which is one stumbling block below.
import Lean
import Std.Data.List.Lemmas
open Lean Meta Elab.Tactic
theorem lt_succ_iff_lt_or_eq {n m : Nat} (h : 0 < m) : (n < m) <-> (n = m-1 ∨ n < m-1) := by
sorry
theorem or_forall {a b : Prop} {P : a ∨ b → Prop} : (∀(h : a ∨ b), P h) <-> ((∀h: a, P (Or.inl h)) ∧ (∀h: b, P (Or.inr h))) := by
sorry
theorem destruct_length4_list_0 {x : List α} (h : x.length = 4) : x = [x[0], x[1], x[2], x[3] ] := by
-- Not idiomatic -- why not call `ext`? See below...
apply List.ext_get <;> simp_all [h, lt_succ_iff_lt_or_eq, Nat.not_lt_zero, or_forall]
theorem destruct_length4_list' {x : List α} (h : x.length = 4) : x = [x[0], x[1], x[2], x[3] ] := by
-- `ext` leads to a formulation with `get?` which does not seem easy to handle automatically?
-- If `List.get_ext` was the `ext`-rule for lists... then we would end up with this (?)
apply List.ext_get
· intros; assumption
· intros i hi hi'
-- Get `i < 4` by simplifying `h: List.length x = 4`
-- QUESTION: Why do neither `simp_all` nor `simp [h] at *` do this?
-- Doesn't work: simp [h] at *
-- Doesn't work: simp_all
simp [h] at hi
-- QUESTION: Now we need to split `i < 4` into cases. `interval_cases` from mathlib does this, but how
-- can this be done on the basis of core tactics? It seems like a common thing to do.
-- QUESTION: Why _does_ `simp ... at *` work here, i.e. hi does not have to be named explicitly?
simp [lt_succ_iff_lt_or_eq, Nat.not_lt_zero] at *
-- QUESTION: `cases_type* Or` does the case splitting here, but this should be something
-- the core lib should be able to do, ideally without having to name hypotheses explicitly?
--
-- Can be done with general-purpose `elim`, see below, but that does not exist.
sorry
----------------------------- custom elim -----------------------------------------------------
/-- Try to close goal by assumption. Upon succes, return fvar id of
matching assumption. Otherwise, return none. --/
def assumptionCore' (mvarId : MVarId) : MetaM (Option FVarId) :=
mvarId.withContext do
mvarId.checkNotAssigned `assumption
match (← findLocalDeclWithType? (← mvarId.getType)) with
| none => return none
| some fvarId => mvarId.assign (mkFVar fvarId); return (some fvarId)
/-- Close goal `mvarId` using an assumption. Throw error message if failed. -/
def assumption' (mvarId : MVarId) : MetaM FVarId := do
let res ← assumptionCore' mvarId
match res with
| none => throwTacticEx `assumption mvarId "assumption' tactic failed"
| some fvarid => return fvarid
def erule (e : Expr) (mvid : MVarId) (with_intro : Bool := true) : MetaM (List MVarId) := do
let s ← saveState
try
let mvids ← mvid.apply e
match mvids with
| [] => throwError "ill-formed elimination rule {e}"
| main :: other =>
-- Try to solve main goal by assumption, remember fvar of hypothesis
let fvid ← assumption' main
-- Remove hypothesis from all other goals
let other' ← other.mapM (fun mvid => do
if (← mvid.isAssignedOrDelayedAssigned) then
return mvid
let mvid ← mvid.clear fvid
if with_intro then
let (_, mvid) ← mvid.intros
return mvid
else
return mvid
)
return other'
catch _ =>
restoreState s
throwError "erule_tac failed"
-- Run erule repeatedly
def elim (e : Expr) (mvid : MVarId) : MetaM (List MVarId) := do
Meta.repeat1' (erule e) [mvid]
elab "erule" e:term : tactic => do
let e ← elabTermForApply e
Elab.Tactic.liftMetaTactic (erule e)
elab "elim" e:term : tactic => do
let e ← elabTermForApply e
Elab.Tactic.liftMetaTactic (elim e)
--------------------------------- end custom elim -------------------------------------------------------
theorem destruct_length4_list'' {x : List α} (h : x.length = 4) : x = [x[0], x[1], x[2], x[3] ] := by
-- `ext` leads to a formulation with get? which does not seem easy to handle automatically.
-- Let's pretend `List.get_ext` was the `ext`-rule for lists... then we would end up with this:
apply List.ext_get
· intros; assumption
· intros i hi hi'
-- Get `i < 4` by simplifying `h: List.length x = 4`
-- QUESTION: Why do neither `simp_all` nor `simp [h] at *` do this?
-- Doesn't work: simp [h] at *
-- Doesn't work: simp_all
simp [h] at hi
-- QUESTION: Now we need to split `i < 4` into cases. `interval_cases` from mathlib does this, but how
-- can this be done on the basis of core tactics? It seems like a common thing to do.
-- QUESTION: Why _does_ `simp ... at *` work here, i.e. hi does not have to be named explicitly?
simp [lt_succ_iff_lt_or_eq, Nat.not_lt_zero] at *
-- QUESTION: `cases_type* Or` does the case splitting here, but this should be something
-- the core lib should be able to do, ideally without having to name hypotheses explicitly?
--
-- Can be done with general-purpose `elim`:
elim Or.elim <;> simp_all
done
Markus Himmel (Apr 23 2024 at 09:40):
This doesn't answer any of your questions, and you might already be aware of this, but just for completeness I should mention that the specific lemma you're trying to prove can be proved by pattern matching:
theorem destruct_length4_list'' : ∀ {x : List α} (h : x.length = 4), x = [x[0], x[1], x[2], x[3]]
| [_, _, _, _], _ => rfl
Hanno Becker (Apr 23 2024 at 09:46):
@Markus Himmel No, I wasn't aware -- thank you very much, that's useful to know. Can one reformulate this so it is generic in the length, though?
Markus Himmel (Apr 23 2024 at 09:50):
I'm not sure, but since we're relying on the equation compiler to perform the case analysis here my guess would be that making that approach generic in the length will require metaprogramming of some kind.
Eric Wieser (Apr 23 2024 at 10:52):
I would guess you can copy the approach of docs#FinVec.etaExpand_eq
Mario Carneiro (Apr 23 2024 at 15:16):
It depends on how you want to state the variable length version, but one version of this theorem is docs#List.ofFn_get
Last updated: May 02 2025 at 03:31 UTC