Copyright (c) 2025 Heather Macbeth. All rights reserved. Released under Apache 2.0 license as described in the file LICENSE. Authors: Robert Y. Lewis, Heather Macbeth
import Mathlib.Tactic.IntervalCases
In this demo, we'll build up our own version of the interval_cases tactic
bit by bit. We'll touch on some of the key ideas involved in tactic writing.
A tactic is an example of a metaprogram, that is, a program that manipulates other programs (in this case, proofs). Metaprogramming is a powerful technique that can be used to automate repetitive tasks. Tactics are not the only kind of metaprograms in Lean; there are also elaborators, commands, and more.
In Lean, metaprograms are written in Lean itself, using a monad hierarchy
that provides access to stateful components of the system (as well as the
possibility of failing when appropriate). These metaprograms are executed
as native code, similar to #eval (and not #reduce). There is a rich API
surrounding metaprogramming, a very small part of which we will explore in
these demos, and another small part of which is covered in the Hitchhiker's Guide.
Given the complexity of the Lean system, this API is necessarily large and
complicated, and we will only be able to scratch the surface here.
Phase 0: What should the interval_cases tactic do?
The first step in writing a tactic is to write by hand some examples of what the tactic should do.
Here is a typical use case for the interval_cases tactic.
example (k : Nat) (h1 : 1 ≤ k) (h2 : k ≤ 4) : k ∣ 12 := by
interval_cases k <;>
decide
Exercise: Write code which does the same thing without using the interval_cases tactic.
theorem eq_or_succ_le_of_le {k l : Nat} (h : k ≤ l) : k = l ∨ k + 1 ≤ l :=
Nat.eq_or_lt_of_le h
example (k : Nat) (h1 : 1 ≤ k) (h2 : k ≤ 4) : k ∣ 12 := by
have h2o : 1 = k ∨ 2 ≤ k := eq_or_succ_le_of_le h1
apply Or.elim h2o
. intro hk
subst hk
decide
. intro h3
have h3o : 2 = k ∨ 3 ≤ k := eq_or_succ_le_of_le h3
apply Or.elim h3o
. intro hk
subst hk
decide
. intro h4
have h4o : 3 = k ∨ 4 ≤ k := eq_or_succ_le_of_le h4
apply Or.elim h4o
. intro hk
subst hk
decide
. intro h5
have h5o : 4 = k ∨ 5 ≤ k := eq_or_succ_le_of_le h5
apply Or.elim h5o
. intro hk
subst hk
decide
. intro h6
have hnot : ¬ 5 ≤ 4 := by decide
have hpos : 5 ≤ 4 := Nat.le_trans h6 h2
exact absurd hpos hnot
We're going to implement our own version of the interval_cases tactic step by step.
We'll call this new tactic my_interval_cases.
Here's a stub for how we set up this tactic.
open Lean Meta Qq
partial def intervalCases (n1 n2 : Nat) (x : Q(Nat)) (h_min : Q($n1 ≤ $x)) (h_max : Q($x ≤ $n2))
(g : MVarId) :
MetaM (List MVarId) :=
withErasedFVars #[h_min.fvarId!, h_max.fvarId!] do
let t : Q(Prop) ← g.getType
if n2 < n1 then
let hnot ← mkDecideProofQ q(¬ $n1 ≤ $n2)
let hpos : Q($n1 ≤ $n2) := q(Nat.le_trans $h_min $h_max)
let pf : Q($t) := q(absurd $hpos $hnot)
g.assign pf
return []
else
let n1' : Nat := n1 + 1
let h_or : Q($n1 = $x ∨ $n1' ≤ $x) := q(eq_or_succ_le_of_le $h_min)
let g1 ← mkFreshExprMVarQ q($n1 = $x → $t)
let g2 ← mkFreshExprMVarQ q($n1' ≤ $x → $t)
let (h_eq, g1') ← g1.mvarId!.intro1
let g1'' ← subst g1' h_eq
let (h_min', g2') ← g2.mvarId!.intro1
let gs ← intervalCases n1' n2 x (Expr.fvar h_min') h_max g2'
let pf : Q($t) := q(Or.elim $h_or $g1 $g2)
g.assign pf
return g1'' :: gs
/-- Interpret the syntax `my_interval_cases k with h1 h2 between n1 n2`, and run the `intervalCases`
function on what gets parsed. -/
elab "my_interval_cases " x:term " with" h1:(ppSpace colGt ident)
h2:(ppSpace colGt ident)
" between" n1:(ppSpace colGt num) n2:(ppSpace colGt num) :
tactic => do
let x : Expr ← Elab.Tactic.elabTerm x none
let h1 : Expr ← Elab.Tactic.elabTerm h1 none
let h2 : Expr ← Elab.Tactic.elabTerm h2 none
let n1 : Nat := Lean.TSyntax.getNat n1
let n2 : Nat := Lean.TSyntax.getNat n2
Elab.Tactic.liftMetaTactic <| intervalCases n1 n2 x h1 h2
set_option trace.debug true in
example (k : Nat) (h1 : 1 ≤ k) (h2 : k ≤ 4) : k ∣ 12 := show_term by
my_interval_cases k with h1 h2 between 1 4
<;> decide
We've seen the Expr type before: this represents Lean's internal syntax for terms.
The MVarId type represents a metavariable, which is how Lean represents goals internally.
(We can convert between these and Exprs.)
The MetaM monad is the monad in which most metaprogramming happens.
In this monad, we can access the global environment, the local context corresponding to
a particular goal,
#check Expr
#check MVarId
#check Expr.mvarId!
#check MetaM
#check MVarId.getType
#check MVarId.assign
#check mkFreshExprMVar -- make a new goal of specified type, `Option Expr → MetaM Expr`
#check MVarId.intro1 -- run `intro` on a specified goal, `MVarId → MetaM (FVarId × MVarId)`
#check mkDecideProof -- run `decide` to prove a specified statement, `Expr → MetaM Expr`
Finally: what are the Q and q notations doing in our implementation of intervalCases?
The Q and q notations are part of Lean's quotation mechanism.
A term of type Q(α) is an Expr that, if we computed its type, would have type α.
Notice that we're mixing object and meta levels here: α is an object-level type,
but a term of type Q(α) lives at the meta level.
Q(α) is definitionally equal to Expr, but the extra type information is useful for
a "soft" type check on our metaprogram.
#check Q(Nat)
example : Q(Nat) = Expr := rfl
example : Q(Nat) := q(1 + 2)
-- example : Q(Nat) := q(true) -- fails
The q(...) notation lets us define terms of type Q(α) using object-level syntax.
For example: q(1 + 2) is a term of type Q(Nat), representing the expression 1 + 2.