Tactic mode, basic tactics, rewriting, and induction in Lean
Christiano Braga · Mestrado em Sistemas e Computação · IME
Based on the Hitchhiker's Guide to Logical Verification (LoVe), chapter 3.
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.
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:Prop⊢ a → b → a
a b:Prop⊢ a → b → aa:Propb:Prop⊢ a → b → a
a:Propb:Propha:ahb:b⊢ a
a b:Propha:ahb:b⊢ aa:Propb:Propha:ahb:b⊢ a
All goals completed! 🐙
end Backward
After the first and the second intro
a b:Prop⊢ a → b → aa b:Propha:ahb:b⊢ aBasic 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:b⊢ a
All goals completed! 🐙
theorem fst_of_two_props_exact (a b : Prop)
(ha : a) (hb : b) : a := a:Propb:Propha:ahb:b⊢ a
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:b⊢ a
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 → c⊢ a → c
a:Propb:Propc:Prophab:a → bhbc:b → cha:a⊢ c
a:Propb:Propc:Prophab:a → bhbc:b → cha:a⊢ b
a:Propb:Propc:Prophab:a → bhbc:b → cha:a⊢ a
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.
apply can lose a provable goal
declaration uses `sorry`example (a b : Prop) (hb : b) : a ∨ b := a:Propb:Prophb:b⊢ a ∨ b
a:Propb:Prophb:b⊢ a
a b:Prophb:b⊢ aa:Propb:Prophb:b⊢ a
All goals completed! 🐙
a b:Prophb:b⊢ aexample (a b : Prop) (hb : b) : a ∨ b := a:Propb:Prophb:b⊢ a ∨ b
a:Propb:Prophb:b⊢ b
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 → c⊢ c
b:Propc:Prophb:bhbc:b → c⊢ c
b:Propc:Prophb:bhbc:b → c⊢ b
b:Prophb:b⊢ b
b:Proph:b⊢ b
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.
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:Prop⊢ a ∧ b → b ∧ a
a:Propb:Prophab:a ∧ b⊢ b ∧ a
a:Propb:Prophab:a ∧ b⊢ ba:Propb:Prophab:a ∧ b⊢ a
a:Propb:Prophab:a ∧ b⊢ ?left.a ∧ ba:Propb:Prophab:a ∧ b⊢ Propa:Propb:Prophab:a ∧ b⊢ a
a:Propb:Prophab:a ∧ b⊢ a
a:Propb:Prophab:a ∧ b⊢ a ∧ ?right.ba:Propb:Prophab:a ∧ b⊢ Prop
All goals completed! 🐙
end Backward
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 ∧ b⊢ b ∧ a
a:Propb:Prophab:a ∧ b⊢ ba:Propb:Prophab:a ∧ b⊢ a
a:Propb:Prophab:a ∧ b⊢ b All goals completed! 🐙
a:Propb:Prophab:a ∧ b⊢ a All goals completed! 🐙
end Backward
namespace Backward
opaque f : ℕ → ℕ
theorem f5_if (h : ∀ n : ℕ, f n = n) :
f 5 = 5 := h:∀ (n : ℕ), f n = n⊢ f 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.
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:Prop⊢ a → ¬¬a
a:Propha:ahna:¬a⊢ False
a:Propha:ahna:¬a⊢ a
All goals completed! 🐙
end Backward
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.
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.
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 = b⊢ a = c
α:Typea:αb:αc:αhab:a = bhcb:c = b⊢ a = ?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 = b⊢ a = ?b All goals completed! 🐙
α:Typea:αb:αc:αhab:a = bhcb:c = b⊢ b = c α:Typea:αb:αc:αhab:a = bhcb:c = b⊢ c = b
All goals completed! 🐙
end Backward
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 = b⊢ a = c
α:Typea:αb:αc:αhab:a = bhcb:c = b⊢ b = c
rw [hcb α:Typea:αb:αc:αhab:a = bhcb:c = b⊢ b = b] All goals completed! 🐙
end Backward
namespace Backward
theorem a_proof_of_negation (a : Prop) :
a → ¬¬ a := by a:Prop⊢ a → ¬¬a
rw [Not a:Prop⊢ a → ¬a → False] a:Prop⊢ a → ¬a → False
rw [Not a:Prop⊢ a → (a → False) → False] a:Prop⊢ a → (a → False) → False
intro ha hna a:Propha:ahna:a → False⊢ False
apply hna a:Propha:ahna:a → False⊢ a
exact ha All goals completed! 🐙
end Backward
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 := by α:Typea:αb:αc:αd:αg:α → α → ℕ → αhab:a = bhcd:c = d⊢ g a c (1 + 1) = g b d 2
simp [hab, hcd] 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.
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 := by n:ℕ⊢ add 0 n = n
induction n with
| zero => zero ⊢ add 0 0 = 0 rfl All goals completed! 🐙
| succ n' ih => succ n':ℕih:add 0 n' = n'⊢ add 0 (n' + 1) = n' + 1 simp [add, ih] All goals completed! 🐙
theorem add_succ (m n : ℕ) :
add (Nat.succ m) n = Nat.succ (add m n) := by m:ℕn:ℕ⊢ add m.succ n = (add m n).succ
induction n with
| zero => zero m:ℕ⊢ add m.succ 0 = (add m 0).succ rfl All goals completed! 🐙
| succ n' ih => succ m:ℕn':ℕih:add m.succ n' = (add m n').succ⊢ add m.succ (n' + 1) = (add m (n' + 1)).succ simp [add, ih] All goals completed! 🐙
end Backward
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 := by m:ℕn:ℕ⊢ add m n = add n m
induction n with
| zero => zero m:ℕ⊢ add m 0 = add 0 m simp [add, add_zero] All goals completed! 🐙
| succ n' ih => succ m:ℕn':ℕih:add m n' = add n' m⊢ add m (n' + 1) = add (n' + 1) m simp [add, add_succ, ih] All goals completed! 🐙
theorem add_assoc (l m n : ℕ) :
add (add l m) n = add l (add m n) := by l:ℕm:ℕn:ℕ⊢ add (add l m) n = add l (add m n)
induction n with
| zero => zero l:ℕm:ℕ⊢ add (add l m) 0 = add l (add m 0) rfl All goals completed! 🐙
| succ n' ih => succ 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)) simp [add, ih] 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) := by l:ℕm:ℕn:ℕ⊢ mul l (add m n) = add (mul l m) (mul l n)
induction n with
| zero => zero l:ℕm:ℕ⊢ mul l (add m 0) = add (mul l m) (mul l 0) rfl All goals completed! 🐙
| succ n' ih => succ 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))
simp [add, mul, ih] succ 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'))
ac_rfl All goals completed! 🐙
end Backward
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 := by α:Typeys:List αx:α⊢ appendPretty ys [x] = snoc ys x
induction ys with
| nil => nil α:Typex:α⊢ appendPretty [] [x] = snoc [] x rfl All goals completed! 🐙
| cons y ys' ih => cons α:Typex:αy:αys':List αih:appendPretty ys' [x] = snoc ys' x⊢ appendPretty (y :: ys') [x] = snoc (y :: ys') x
simp [appendPretty, snoc, ih] 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 := by α:Typex:αxs:List α⊢ reverse (x :: xs) = snoc (reverse xs) x
simp [reverse, append_snoc] All goals completed! 🐙
end Backward
A hard case usually signals a missing auxiliary theorem.
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.