Lecture 4 · Formal Software Verification

Backward Proofs

Tactic mode, basic tactics, rewriting, and induction in Lean

Based on the Hitchhiker's Guide to Logical Verification (LoVe), chapter 3.

§4.1 Backward and forward

  • A tactic operates on a goal and either proves it or creates subgoals. A goal is the sequent C ⊢ Q, with antecedent C, the local context, and consequent Q, the conclusion.

Backward, from the goal

to prove c,
  by hbc it suffices to prove b;
to prove b,
  by hab it suffices to prove a;
and ha proves a.

Forward, from the hypotheses

from ha and hab, we have b;
from b and hbc, we have c.
  • The characteristic phrase of a backward proof is "it suffices to prove". In a natural deduction derivation, the premises of each rule sit above the inference line and the conclusion below it. The forward reading goes from the assumptions, at the top, to the conclusion, and the backward reading goes from the conclusion to the assumptions.

§4.1 Tactic mode

  • The keyword by enters tactic mode, and each line after it is one tactic. trace_state prints the goal between the steps.

namespace Backward theorem fst_of_two_props : a b : Prop, a b a := (a b : Prop), a b a a:Propb:Propa b a a b:Propa b aa:Propb:Propa b a a:Propb:Propha:ahb:ba a b:Propha:ahb:baa:Propb:Propha:ahb:ba All goals completed! 🐙 end Backward

After the first and the second intro

a b:Propa  b  a
a b:Propha:ahb:ba

§4.2 The four basic tactics

  • Basic tactics perform one elementary transformation of the proof state each, and none depends on a particular connective or theory.

  • intro moves variables and assumptions into the context; apply matches the conclusion of the goal with that of a theorem and leaves its unresolved arguments and premises as goals; exact closes the goal with a term; assumption searches the context.

namespace Backward theorem fst_of_two_props_params (a b : Prop) (ha : a) (hb : b) : a := a:Propb:Propha:ahb:ba All goals completed! 🐙 theorem fst_of_two_props_exact (a b : Prop) (ha : a) (hb : b) : a := a:Propb:Propha:ahb:ba All goals completed! 🐙 end Backward
namespace Backward theorem fst_of_two_props_assumption (a b : Prop) (ha : a) (hb : b) : a := a:Propb:Propha:ahb:ba All goals completed! 🐙 theorem prop_comp (a b c : Prop) (hab : a b) (hbc : b c) : a c := a:Propb:Propc:Prophab:a bhbc:b ca c a:Propb:Propc:Prophab:a bhbc:b cha:ac a:Propb:Propc:Prophab:a bhbc:b cha:ab a:Propb:Propc:Prophab:a bhbc:b cha:aa All goals completed! 🐙 end Backward
  • Lean inserts the parameters left of the colon into the local context of the initial goal, so these proofs need no intro.

§4.2 Losing provability

apply can lose a provable goal

declaration uses `sorry`example (a b : Prop) (hb : b) : a b := a:Propb:Prophb:ba b a:Propb:Prophb:ba a b:Prophb:baa:Propb:Prophb:ba All goals completed! 🐙
a b:Prophb:ba
example (a b : Prop) (hb : b) : a b := a:Propb:Prophb:ba b a:Propb:Prophb:bb All goals completed! 🐙

sorry, clear, rename

namespace Backward theorem cleanup_example (a b c : Prop) (ha : a) (hb : b) (hab : a b) (hbc : b c) : c := a:Propb:Propc:Propha:ahb:bhab:a bhbc:b cc b:Propc:Prophb:bhbc:b cc b:Propc:Prophb:bhbc:b cb b:Prophb:bb b:Proph:bb All goals completed! 🐙 end Backward
  • A provable goal stays provable after intro. apply and clear can turn a provable goal into an unprovable one. sorry closes anything and #print axioms reports it as sorryAx.

§4.3 Rules as theorems

  • Every inference figure of Lecture 1 is an ordinary theorem, applied backwards by apply.

Introduction and elimination

And.intro : ?a → ?b → ?a ∧ ?b
And.left  : ?a ∧ ?b → ?a
And.right : ?a ∧ ?b → ?b
Or.inl    : ?a → ?a ∨ ?b
Or.inr    : ?b → ?a ∨ ?b
Or.elim   : ?a ∨ ?b →
  (?a → ?c) → (?b → ?c) → ?c
Iff.intro : (?a → ?b) →
  (?b → ?a) → (?a ↔ ?b)
Iff.mp    : (?a ↔ ?b) → ?a → ?b
Iff.mpr   : (?a ↔ ?b) → ?b → ?a
namespace Backward theorem And_swap (a b : Prop) : a b b a := a:Propb:Propa b b a a:Propb:Prophab:a bb a a:Propb:Prophab:a bba:Propb:Prophab:a ba a:Propb:Prophab:a b?left.a ba:Propb:Prophab:a bPropa:Propb:Prophab:a ba a:Propb:Prophab:a ba a:Propb:Prophab:a ba ?right.ba:Propb:Prophab:a bProp All goals completed! 🐙 end Backward

§4.3 Metavariables and bullets

  • A metavariable ?a stands for a term still to be determined, and unification determines it. The · bullet focuses one subgoal; juxtaposition instantiates a rule forwards.

namespace Backward theorem And_swap_braces : a b : Prop, a b b a := (a b : Prop), a b b a a:Propb:Prophab:a bb a a:Propb:Prophab:a bba:Propb:Prophab:a ba a:Propb:Prophab:a bb All goals completed! 🐙 a:Propb:Prophab:a ba All goals completed! 🐙 end Backward
namespace Backward opaque f : theorem f5_if (h : n : , f n = n) : f 5 = 5 := h: (n : ), f n = nf 5 = 5 All goals completed! 🐙 end Backward
  • Passing hab directly to And.right is a small forward step inside a backward proof, and it avoids the metavariables that apply And.right would leave.

§4.3 Quantifiers, truth, falsehood, negation

Exists.intro : ∀ (w : ?α), ?p w → ∃ x, ?p x
Exists.elim  : (∃ x, ?p x) → (∀ (w : ?α), ?p w → ?b) → ?b
True.intro   : True
False.elim   : False → ?c
Classical.em : ∀ (p : Prop), p ∨ ¬p
Classical.byContradiction : (¬?a → False) → ?a
  • Negation needs no rules: ¬a is defined as a → False, so intro applies to a negated conclusion.

  • True.intro is the only rule for truth; falsehood has no introduction rule, and False.elim closes any goal from a proof of False.

namespace Backward theorem Not_Not_intro (a : Prop) : a ¬¬ a := a:Propa ¬¬a a:Propha:ahna:¬aFalse a:Propha:ahna:¬aa All goals completed! 🐙 end Backward

§4.3 Strategies

The guide's strategies for propositional proofs.

  • Look at the conclusion. An implication or a negation calls for intro.

  • Look at the hypotheses. A conjunction offers And.left and And.right, a disjunction offers Or.elim, an equivalence offers Iff.mp and Iff.mpr.

  • Match the conclusion of the goal with an introduction rule and apply it.

  • Prefer tactics that preserve provability while they make progress, and record the choice points where a tactic commits to a side.

  • When a subgoal repeats a hypothesis, exact or assumption closes it.

  • When nothing constructive applies, consider a case analysis on Classical.em.

  • If the proof makes no progress, backtrack to the last choice point and try the other option.

§4.4 rfl and the conversions

  • rfl proves l = r when the sides agree up to computation, and each computation step is a named conversion.

α  renames a bound variable
β  applies an anonymous function
δ  unfolds a definition
ζ  substitutes a let
η  fun x => f x equals f
ι  projects a constructor
namespace Backward def double (n : ) : := n + n end Backward
namespace Backward theorem β_example {α β : Type} (f : α β) (a : α) : (fun x => f x) a = f a := α:Typeβ:Typef:α βa:α(fun x => f x) a = f a All goals completed! 🐙 theorem δ_example : double 5 = 5 + 5 := double 5 = 5 + 5 All goals completed! 🐙 theorem ι_example {α β : Type} (a : α) (b : β) : Prod.fst (a, b) = a := α:Typeβ:Typea:αb:β(a, b).fst = a All goals completed! 🐙 end Backward
  • ac_rfl adds associativity and commutativity of the registered operators, as in a + b + c = c + b + a.

§4.4 Equality as rules

Eq.refl  : ∀ (a : ?α), a = a
Eq.symm  : ?a = ?b → ?b = ?a
Eq.trans : ?a = ?b → ?b = ?c →
           ?a = ?c
Eq.subst : ?a = ?b → ?P ?a → ?P ?b
  • = binds more tightly than the connectives, so a = b ∧ c = d reads (a = b) ∧ (c = d).

namespace Backward theorem Eq_trans_symm {α : Type} (a b c : α) (hab : a = b) (hcb : c = b) : a = c := α:Typea:αb:αc:αhab:a = bhcb:c = ba = c α:Typea:αb:αc:αhab:a = bhcb:c = ba = ?bα:Typea:αb:αc:αhab:a = bhcb:c = b?b = cα:Typea:αb:αc:αhab:a = bhcb:c = bα α:Typea:αb:αc:αhab:a = bhcb:c = ba = ?b All goals completed! 🐙 α:Typea:αb:αc:αhab:a = bhcb:c = bb = c α:Typea:αb:αc:αhab:a = bhcb:c = bc = b All goals completed! 🐙 end Backward

§4.5 rw

  • rw applies an equation left to right, once: it finds the first matching subterm, replaces every occurrence of it, and then tries rfl. reverses the equation, at h rewrites a hypothesis, and a constant name uses its defining equations.

namespace Backward theorem Eq_trans_symm_rw {α : Type} (a b c : α) (hab : a = b) (hcb : c = b) : a = c := α:Typea:αb:αc:αhab:a = bhcb:c = ba = c α:Typea:αb:αc:αhab:a = bhcb:c = bb = c All goals completed! 🐙 end Backward
namespace Backward theorem a_proof_of_negation (a : Prop) : a ¬¬ a := a:Propa ¬¬a a:Propa ¬a False a:Propa (a False) False a:Propha:ahna:a FalseFalse a:Propha:ahna:a Falsea All goals completed! 🐙 end Backward

§4.5 simp

  • simp rewrites with the simp set exhaustively. simp [t] adds a theorem or constant for one call, simp [-t] removes one, simp [*] at * uses everything everywhere, and @[simp] registers a theorem permanently.

namespace Backward theorem cong_two_args_1p1 {α : Type} (a b c d : α) (g : α α α) (hab : a = b) (hcd : c = d) : g a c (1 + 1) = g b d 2 := α:Typea:αb:αc:αd:αg:α α αhab:a = bhcd:c = dg a c (1 + 1) = g b d 2 All goals completed! 🐙 end Backward
  • Rewriting is where proofs stop being predictable. Try a tactic, study the subgoals that emerge, and adjust. In the guide's words, DON'T PANIC.

§4.6 Induction

  • induction produces one named subgoal per constructor, and the branch names bind the constructor arguments and the induction hypothesis.

induction n with
| zero       => (base case)
| succ n' ih => (step case)
  • Induct on the argument the recursion consumes.

  • A difficult base case signals the wrong variable or a missing auxiliary theorem.

namespace Backward theorem add_zero (n : ) : add 0 n = n := n:add 0 n = n induction n with add 0 0 = 0 All goals completed! 🐙 n':ih:add 0 n' = n'add 0 (n' + 1) = n' + 1 All goals completed! 🐙 theorem add_succ (m n : ) : add (Nat.succ m) n = Nat.succ (add m n) := m:n:add m.succ n = (add m n).succ induction n with m:add m.succ 0 = (add m 0).succ All goals completed! 🐙 m:n':ih:add m.succ n' = (add m n').succadd m.succ (n' + 1) = (add m (n' + 1)).succ All goals completed! 🐙 end Backward

§4.6 Commutativity and ac_rfl

  • These re-prove laws Lecture 3 stated with sorry, now as theorems of their own, and the instances let ac_rfl treat add like +.

namespace Backward theorem add_comm (m n : ) : add m n = add n m := m:n:add m n = add n m induction n with m:add m 0 = add 0 m All goals completed! 🐙 m:n':ih:add m n' = add n' madd m (n' + 1) = add (n' + 1) m All goals completed! 🐙 theorem add_assoc (l m n : ) : add (add l m) n = add l (add m n) := l:m:n:add (add l m) n = add l (add m n) induction n with l:m:add (add l m) 0 = add l (add m 0) All goals completed! 🐙 l:m:n':ih:add (add l m) n' = add l (add m n')add (add l m) (n' + 1) = add l (add m (n' + 1)) All goals completed! 🐙 end Backward
namespace Backward instance Associative_add : Std.Associative add := { assoc := add_assoc } instance Commutative_add : Std.Commutative add := { comm := add_comm } theorem mul_add (l m n : ) : mul l (add m n) = add (mul l m) (mul l n) := l:m:n:mul l (add m n) = add (mul l m) (mul l n) induction n with l:m:mul l (add m 0) = add (mul l m) (mul l 0) All goals completed! 🐙 l:m:n':ih:mul l (add m n') = add (mul l m) (mul l n')mul l (add m (n' + 1)) = add (mul l m) (mul l (n' + 1)) l:m:n':ih:mul l (add m n') = add (mul l m) (mul l n')add l (add (mul l m) (mul l n')) = add (mul l m) (add l (mul l n')) All goals completed! 🐙 end Backward

§4.7 Worked example: discharging reverse_cons

  • Lecture 3 stated reverse (x :: xs) = snoc (reverse xs) x with sorry. The statement mixes appendPretty and snoc, so the missing piece is the theorem that relates them.

The auxiliary theorem

namespace Backward theorem append_snoc {α : Type} (ys : List α) (x : α) : appendPretty ys [x] = snoc ys x := α:Typeys:List αx:αappendPretty ys [x] = snoc ys x induction ys with α:Typex:αappendPretty [] [x] = snoc [] x All goals completed! 🐙 α:Typex:αy:αys':List αih:appendPretty ys' [x] = snoc ys' xappendPretty (y :: ys') [x] = snoc (y :: ys') x All goals completed! 🐙 end Backward

The theorem, one simp away

namespace Backward theorem reverse_cons {α : Type} (x : α) (xs : List α) : reverse (x :: xs) = snoc (reverse xs) x := α:Typex:αxs:List αreverse (x :: xs) = snoc (reverse xs) x All goals completed! 🐙 end Backward
  • A hard case usually signals a missing auxiliary theorem.

Summary

  • A tactic transforms the goal, and a backward proof reads as a chain of "it suffices to".

  • intro, apply, exact and assumption prove the propositional theorems of this lecture, and among them only intro never loses a provable goal.

  • Every rule of Lecture 1 is a theorem that apply consumes backwards and juxtaposition instantiates forwards.

  • rfl decides equality up to computation, one named conversion at a time, and ac_rfl adds associativity and commutativity.

  • rw rewrites once at the first match and then tries rfl; simp rewrites exhaustively with the simp set.

  • induction … with proves the general laws that computation cannot reach, and it re-proves several of Lecture 3's statements as theorems of its own.

  • Lecture 5 turns the same proofs around, into forward and structured proofs.

Exercises: see the lecture notes.