These are powerful and fully automatic. For their decidable fragments, they're the best tool.
A small example:
def odd (n : Nat) := ∃ k, n = 2 * k + 1
Lean checks this definition: odd maps a natural number to a proposition.
The type of odd 3 is Prop, a statement that can be true or false.
-- We can evaluate:
#eval odd 3 -- This doesn't work! odd returns a Prop, not a Bool.
-- But we can prove it:
example : odd 3 := ⟨1, rfl⟩ -- witness: k = 1, and 3 = 2*1 + 1
Theorem statement:
theorem odd_add_odd (h1 : odd m) (h2 : odd n)
: even (m + n) := by
obtain ⟨k1, _⟩ := h1
obtain ⟨k2, _⟩ := h2
exact ⟨k1 + k2 + 1, by ring⟩
An incorrect proof (Lean rejects it):
theorem odd_add_odd' (h1 : odd m) (h2 : odd n)
: even (m + n) := by
obtain ⟨k1, _⟩ := h1
obtain ⟨k2, _⟩ := h2
exact ⟨k1 + k2, by ring⟩
-- Error: ring failed to prove
-- 2 * k1 + 1 + (2 * k2 + 1)
-- = 2 * (k1 + k2)
Lean catches the mistake immediately: the witness is wrong.
omega, bv_decide, grind) are the calculator for proofs.
The deep connection between logic and programming:
P : Proph : P means h is a proof of P.P → Q = "given a proof of P, produce a proof of Q".(n : Nat) → P n = "for all n, P(n) holds".And P Q, Or P Q, Exists, etc.
-- Types can depend on values
def Vector (α : Type) (n : Nat) := { a : Array α // a.size = n }
-- The type system prevents out-of-bounds access
def Vector.get (v : Vector α n) (i : Fin n) : α :=
v.val[i] -- Lean knows i < n = v.val.size
-- Propositions are types, proofs are terms
example : 2 + 2 = 4 := rfl -- definitional equality
example : ∀ n : Nat, 0 + n = n := fun n => rfl -- a function is a proof of ∀
-- Dependent pairs: the type of the second component depends on the first
example : (n : Nat) × Vector String n :=
⟨3, ⟨#["a", "b", "c"], rfl⟩⟩
theorem add_comm (n m : Nat) : n + m = m + n := by
induction n with
| zero => simp
| succ n ih =>
simp [Nat.succ_add, ih]
▲ cursor hereThe Lean Infoview panel shows goals, types, and diagnostics in real time.
"You have written my favorite computer game" – Kevin Buzzard
The "game board": you see goals and hypotheses, then apply "moves" (tactics).
theorem odd_add_odd
(h1 : odd m) (h2 : odd n)
: even (m + n) := by
done -- ← cursor hereThe goal ⊢ even (m + n) is what we need to prove.
Each tactic transforms the goal until nothing remains.
The obtain tactic destructs an existential hypothesis:
theorem odd_add_odd
(h1 : odd m) (h2 : odd n)
: even (m + n) := by
obtain ⟨k1, _⟩ := h1
obtain ⟨k2, _⟩ := h2
done -- ← cursor hereAfter two obtain moves, the hypotheses are unpacked
and the goal is simplified.
simp MoveThe simplifier, one of the most popular moves in our game. Rewrites the goal using a database of lemmas:
-- simp can solve many routine goals:
example (xs : List α) : (xs ++ []).length = xs.length := by simp
example (n : Nat) : n + 0 = n := by simp
-- simp? tells you which lemmas it used:
example (xs : List α) : (xs ++ []).length = xs.length := by
simp? -- Try this: simp [List.append_nil]
use and exact-- "use" provides a witness for an existential goal
example : ∃ n : Nat, n > 3 := by
use 4 -- now the goal is: 4 > 3
-- "exact" provides the exact proof term
example : ∃ n : Nat, n > 3 := by
use 4
exact Nat.lt_of_sub_eq_succ rfl
-- "decide" is proof by evaluation
example : ∃ n : Nat, n > 3 := by
use 4
decide
-- Or combine them:
example : ∃ n : Nat, n > 3 := ⟨4, by decide⟩
linarith MoveLinear arithmetic. Closes goals about linear inequalities:
-- linarith combines linear hypotheses to derive the goal
example (x y z : Int) (h1 : x + y ≥ 3) (h2 : y + z ≤ 5)
(h3 : x ≥ 0) : x + 2*y + z ≤ x + 8 := by
linarith
-- It works with naturals, integers, and rationals
example (a b : Nat) (h : a ≤ b) : 2 * a ≤ 2 * b := by
linarith
linarith finds a non-negative linear combination of hypotheses
that contradicts the negation of the goal. Same idea as Farkas' lemma.
Just enough syntax to read the code in this talk:
-- Inductive types (algebraic data types)
inductive Weekday where
| monday | tuesday | wednesday
| thursday | friday
| saturday | sunday
-- Pattern matching
def Weekday.isWeekend : Weekday → Bool
| .saturday => true
| .sunday => true
| _ => false
-- Recursive types
inductive MyList (α : Type) where
| nil : MyList α
| cons : α → MyList α → MyList α
-- Functions with type annotations
def double (n : Nat) : Nat := n * 2
-- Anonymous functions
#eval (fun x => x + 1) 5 -- 6
-- Structures (like records)
structure Point where
x : Float
y : Float
-- Dependent types: length-indexed vectors
inductive Vec (α : Type) : Nat → Type where
| nil : Vec α 0
| cons : α → Vec α n → Vec α (n + 1)
-- Prop vs Bool
-- Bool: computed (true/false)
-- Prop: proved (constructive evidence)
-- theorem: a named proof
theorem add_comm (a b : Nat) :
a + b = b + a := by
omega -- decision procedure!
-- example: an unnamed proof
example : 2 + 3 = 5 := by decide
-- by: enter tactic mode
-- Tactics transform proof goals
-- until no goals remain
-- Tactic mode example
theorem and_swap (p q : Prop)
(h : p ∧ q) : q ∧ p := by
-- Goal: q ∧ p
obtain ⟨hp, hq⟩ := h
-- Goals: q, p
exact ⟨hq, hp⟩
-- Term-mode proof (no tactics)
theorem and_swap' (p q : Prop)
(h : p ∧ q) : q ∧ p :=
⟨h.2, h.1⟩
-- Mix both styles freely!
append-- Define List append
def append : (xs ys : List α) → List α
| [], bs => bs
| a :: as, bs => a :: append as bs
theorem append_assoc (as bs cs : List α)
: append (append as bs) cs = append as (append bs cs) := by
induction as with
| nil => rfl
| cons a as ih => simp [ih, append]
Dependent types: more expressive, but harder to automate.
def Array.get {α : Type u} (as : Array α) (i : Nat) (h : i < as.size) : α
2 + i - 1 to i + 1 inside Array.get as (2+i-1) h.2 + i - 1 = i + 1, but naively substituting produces a type-incorrect term:
the proof h has type 2+i-1 < as.size, not i+1 < as.size.This kind of bookkeeping is invisible in SMT (no dependent types), but pervasive in Lean.
class Mul (α : Type u) where mul : α → α → α
instance : Mul Nat where mul := Nat.mul
instance : Mul Int where mul := Int.mul
-- n * n resolves to Nat.mul; i * i resolves to Int.mul
-- An instance can depend on other instances (backward chaining):
instance [Semiring α] [AddRightCancel α] [NoNatZeroDivisors α]
: NoNatZeroDivisors (OfSemiring.Q α) where ...
The Lean community is actively developing proof automation:
Decision procedures from this course appear as Lean tactics
bv_decide: An Efficient Verified Bit-BlasterEntirely implemented in Lean. The LRAT checker is verified, so bugs in CaDiCaL can't produce false proofs.
bv_decide?: The "Try" PatternMany tactics implement this: simp?, aesop?, grind?, bv_decide?
theorem simple (x : BitVec 64) : x + x = 2 * x := by
bv_decide?
-- Quick Fix: Try this: bv_check "Arith.lean-simple-43-2.lrat"
bv_decide? runs the SAT solver, saves the LRAT proof to a file.bv_check replays the saved proof. Fast, no SAT solver needed at build time.
popcount with bv_decideImperative popcount (Hacker's Delight):
def popcount : Stmt := imp {
x := x - ((x >>> 1) &&& 0x55555555);
x := (x &&& 0x33333333)
+ ((x >>> 2) &&& 0x33333333);
x := (x + (x >>> 4)) &&& 0x0F0F0F0F;
x := x + (x >>> 8);
x := x + (x >>> 16);
x := x &&& 0x0000003F;
}
Specification (loop-based):
def pop_spec (x : BitVec 32) : BitVec 32 :=
go x 0 32
where
go (x : BitVec 32) (pop : BitVec 32) (i : Nat) : BitVec 32 :=
match i with
| 0 => pop
| i + 1 =>
let pop := pop + (x &&& 1#32)
go (x >>> 1#32) pop i
theorem popcount_correct : ∀ x, run popcount x = pop_spec x := by
simp [run, popcount, Expr.eval, Expr.BindOp.app]
bv_decide
omega: Cooper's Algorithm as a Lean TacticNat, Int,
Fin, UInt, BitVec. Completely verified.example (a b : Nat) (h : a ≤ b) : a + (b - a) = b := by omega
example (x y : Int) (h1 : x + y ≥ 3) (h2 : x ≤ 1) : y ≥ 2 := by omega
example (n : Nat) (h : n % 2 = 0) : (n + 2) % 2 = 0 := by omega
example (a : Fin 10) : a.val < 10 := by omega
omega is a standalone tactic, but grind subsumes it:
grind's cutsat solver handles linear integer arithmetic as one of its satellite engines.
Same core ideas from Cooper (case splits, bounds propagation), integrated into the E-graph.
grind?
grind Is NOTbv_decide.
grind: Design Principlesgrind? similarly to bv_decide? and aesop?.
grind: ArchitectureThe E-graph is the central data structure. Satellite solvers read from it and propagate back.
grind: Model-Based Theory SolversFor linear arithmetic (linarith) and linear integer arithmetic (cutsat).
linarith is parametrized by a Module over the integers.
Supports preorders, partial orders, and linear orders.cutsat is parametrized by the ToInt type class, which embeds types
like Int32, BitVec 64 into the integers.example {R} [OrderedVectorSpace R] (x y z : R)
: x ≤ 2•y → y < z → x < 2•z := by
grind
grind: cutsat in Actionexample (x y : Int) :
27 ≤ 11*x + 13*y → 11*x + 13*y ≤ 45 →
-10 ≤ 7*x - 9*y → 7*x - 9*y ≤ 4 → False := by
grind
example (a b c : UInt32) :
- a + - c > 1 →
a + 2*b = 0 →
- c + 2*b = 0 → False := by
grind
example (a : Fin 4) : 1 < a → a ≠ 2 → a ≠ 3 → False := by grind
cutsat handles integers, fixed-width integers, and finite types uniformly
via the ToInt type class.
grind: Commutative Rings and FieldsUses Gröbner basis. Parametrized by: CommRing, CommSemiring,
NoNatZeroDivisors, Field, AddRightCancel, IsCharP.
example {α} [CommRing α] (a b c : α)
: a + b + c = 3 →
a^2 + b^2 + c^2 = 5 →
a^3 + b^3 + c^3 = 7 →
a^4 + b^4 + c^4 = 9 := by
grind
example [Field R] (a : R) : (2 * a)⁻¹ = a⁻¹ / 2 := by grind
example [Field R] (a : R) : (2 : R) ≠ 0 →
1 / a + 1 / (2 * a) = 3 / (2 * a) := by grind
example [Field R] [IsCharP R 0] (a : R) :
1 / a + 1 / (2 * a) = 3 / (2 * a) := by grind
-- BitVec 8 is a CommRing of characteristic 256
-- No bitvector-specific theory needed!
example (x : BitVec 8) : (x - 16) * (x + 16) = x^2 := by grind
example (x y : BitVec 16) : x^2*y = 1 → x*y^2 = y → y*x = 1 := by grind
grind: Theory CombinationSatellite solvers share information through the E-graph. When one solver discovers an equality, all solvers see it.
example [CommRing α] [NoNatZeroDivisors α]
(a b c : α) (f : α → Nat) :
a + b + c = 3 → a^2 + b^2 + c^2 = 5 → a^3 + b^3 + c^3 = 7 →
f (a^4 + b^4) + f (9 - c^4) ≠ 1 := by grind
Three solvers cooperate:
a^4 + b^4 = 9 - c^4f (a^4 + b^4) = f (9 - c^4)2 * f (9 - c^4) ≠ 1
grind: E-MatchingE-matching is a heuristic for instantiating universally quantified lemmas. It is used in many SMT solvers. You know this from DPLL(T)!
Matching modulo the equalities in the E-graph:
-- Annotate a lemma for E-matching:
@[grind =] theorem fg {x} : f (g x) = x := by
unfold f g; omega
-- grind finds the right instantiation via E-matching:
example {a b c} : f a = b → a = g c → b = c := by
grind
f (g ?x) in the E-graph,
even when the match requires following equality chains.
grind: Annotations and Forward ReasoningLibrary authors annotate theorems to control how grind uses them:
[grind =] use left-hand side as E-matching pattern[grind _=_] use both sides[grind →] forward reasoning: extract patterns from hypotheses[grind ←] backward reasoning: extract patterns from conclusion[grind] automatic pattern selectiondef R : Nat → Nat → Prop
@[grind →] theorem Rtrans : R x y → R y z → R x z
@[grind →] theorem Rsymm : R x y → R y x
-- grind chains transitivity and symmetry automatically:
example : R a b → R c b → R d c → R a d := by grind
grind: Extensibility[grind] attribute.-- Annotate a lemma for grind:
@[grind =]
theorem getElem?_cons {a l i} : (a :: l)[i]? = if i = 0 then some a else l[i-1]? := by
cases i <;> simp
-- Now grind can use this lemma automatically in any proof about list indexing.
grind: Diagnostics at Your FingertipsWhen grind fails, it tells you why. Example:
example {α} (as bs cs : Array α)
(v₁ v₂ : α) (i₁ i₂ j : Nat)
(h₁ : i₁ < as.size)
(h₂ : bs = as.set i₁ v₁)
(h₃ : i₂ < bs.size)
(h₄ : cs = bs.set i₂ v₂)
(h₅ : i₁ ≠ j)
(h₆ : j < cs.size)
(h₇ : j < as.size)
: cs[j] = as[j] := by
grind[grind] annotation to the missing lemma.
grind: Interactive Modegrind => exposes a DSL for stepping through the solver's execution.
Mix grind steps with ordinary tactics.
example : (cos x + sin x)^2 = 2 * cos x * sin x + 1 := by
grind =>
instantiate only [trig_identity]
ring
grind? outputs a self-contained script with the exact steps used.
It does not depend on environment annotations, improving proof stability.
grind's architecture mirrors the DPLL(T) framework you studied:
grind in Lean
lean-smt: Invoking CVC5 from Leanimport Smt
-- lean-smt translates the goal to SMT-LIB and calls CVC5
example (a b c : Int) (h1 : a + b > c)
(h2 : c > a) : b > 0 := by
smt [h1, h2]
-- It handles quantifiers too
example : ∀ x : Int, x * x ≥ 0 := by
smt
The if-normalization challenge
if true then ...)eval(normalize(e)) = eval(e)
IfExpr Typeinductive IfExpr
| lit : Bool → IfExpr -- constant: true or false
| var : Nat → IfExpr -- variable: x₀, x₁, x₂, ...
| ite : IfExpr → IfExpr → IfExpr → IfExpr -- if i then t else e
deriving DecidableEq
namespace IfExpr
def eval (f : Nat → Bool) : IfExpr → Bool
| lit b => b
| var i => f i
| ite i t e => bif i.eval f then t.eval f else e.eval f
Simple inductive type with three constructors. eval gives the semantics.
hasNestedIf, hasConstantIfdef hasNestedIf : IfExpr → Bool
| lit _ => false
| var _ => false
| ite (ite _ _ _) _ _ => true -- nested if in condition!
| ite _ t e => t.hasNestedIf || e.hasNestedIf
def hasConstantIf : IfExpr → Bool
| lit _ => false
| var _ => false
| ite (lit _) _ _ => true -- constant condition!
| ite i t e => i.hasConstantIf || t.hasConstantIf || e.hasConstantIf
hasRedundantIf, disjointdef hasRedundantIf : IfExpr → Bool
| lit _ => false
| var _ => false
| ite i t e => t == e || i.hasRedundantIf || t.hasRedundantIf || e.hasRedundantIf
def vars : IfExpr → List Nat
| lit _ => []
| var i => [i]
| ite i t e => i.vars ++ t.vars ++ e.vars
/-- Each variable evaluated at most once on any path -/
def disjoint : IfExpr → Bool
| lit _ => true
| var _ => true
| ite i t e =>
i.vars.disjoint t.vars && i.vars.disjoint e.vars
&& i.disjoint && t.disjoint && e.disjoint
normalized Predicate + eval/-- Normalized: no nested, constant, or redundant ifs,
and each variable evaluated at most once. -/
def normalized (e : IfExpr) : Bool :=
!e.hasNestedIf && !e.hasConstantIf && !e.hasRedundantIf && e.disjoint
Four conditions combined into one predicate.
The challenge: construct a function Z such that:
-- The formal statement we need to inhabit:
{ Z : IfExpr → IfExpr // ∀ e, (Z e).normalized ∧ (Z e).eval = e.eval }
Can we do better with grind?
Yes. Dramatically.
normalize Functiondef normalize (assign : Std.HashMap Nat Bool) : IfExpr → IfExpr
| lit b => lit b
| var v =>
match assign[v]? with
| none => var v
| some b => lit b
| ite (lit true) t _ => normalize assign t
| ite (lit false) _ e => normalize assign e
| ite (ite a b c) t e => normalize assign (ite a (ite b t e) (ite c t e))
| ite (var v) t e =>
match assign[v]? with
| none =>
let t' := normalize (assign.insert v true) t
let e' := normalize (assign.insert v false) e
if t' = e' then t' else ite (var v) t' e'
| some b => normalize assign (ite (lit b) t e)
termination_by e => e.normSize
-- This case GROWS the expression:
| ite (ite a b c) t e => normalize assign (ite a (ite b t e) (ite c t e))
-- The condition gets simpler, but t and e are duplicated!
@[simp] def normSize : IfExpr → Nat
| lit _ => 0
| var _ => 1
| .ite i t e => 2 * normSize i + max (normSize t) (normSize e) + 1
-- Add to normalize:
termination_by e => e.normSize
We need to prove three properties simultaneously:
theorem normalize_spec (assign : Std.HashMap Nat Bool) (e : IfExpr) :
(normalize assign e).normalized
∧ (∀ f, (normalize assign e).eval f = e.eval fun w => assign[w]?.getD (f w))
∧ ∀ v, v ∈ vars (normalize assign e) → ¬ v ∈ assign
:= by
???
theorem normalize_spec (assign : Std.HashMap Nat Bool) (e : IfExpr) :
(normalize assign e).normalized
∧ (∀ f, (normalize assign e).eval f = e.eval fun w => assign[w]?.getD (f w))
∧ ∀ v, v ∈ vars (normalize assign e) → ¬ v ∈ assign
:= by
fun_induction normalize with grind +locals
fun_induction normalize: generates one subgoal per recursive case of normalize,
with appropriate inductive hypotheses for each recursive call.grind +locals: unfolds all local definitions and puts all facts together.
Congruence closure + E-matching + case splitting does the rest.
grind +locals Means-- Instead of manually annotating each definition:
attribute [local grind] normalized hasNestedIf hasConstantIf
hasRedundantIf disjoint vars eval List.disjoint
-- The +locals flag tells grind to unfold all definitions
-- in the current file/section:
grind +locals
-- grind can then:
-- 1. Unfold normalized, hasNestedIf, etc. on each case
-- 2. Use congruence closure to reason about equalities
-- 3. Use E-matching to apply inductive hypotheses
-- 4. Case-split on Boolean conditions
-- 5. Use the HashMap lemmas from Std
All the decision procedure machinery from this course (case splitting, equality reasoning, arithmetic) working together on dependent types.
try? Finds It Tootheorem normalize_spec
(assign : Std.HashMap Nat Bool)
(e : IfExpr) :
(normalize assign e).normalized
∧ ... := by
try?try? is an interactive tactic suggestion tool.
-- Change the variable-elimination condition:
-- Version 1: ¬ v ∈ assign
-- Version 2: assign.contains v = false
-- Version 3: assign[v]? = none
-- Same proof works for ALL THREE formulations!
-- grind doesn't care about the surface syntax,
-- it reasons about the underlying semantics.
grind proofs are robust because grind reasons
semantically, not syntactically.
HashMap → TreeMap-- Swap the data structure:
-- Std.HashMap Nat Bool → Std.TreeMap Nat Bool
-- The proof is UNCHANGED.
-- Why? grind uses whatever lemmas are annotated
-- with [grind] in the standard library.
-- Both HashMap and TreeMap have the same interface lemmas:
-- insert_get, get_insert_ne, etc.
-- grind finds and applies them automatically.
[grind].
When does grind not work?
bv_decide instead (SAT-based).grind diagnostics to find what's missing. Add [grind] attributes.x * y = y * x is fine (ring solver),
but x * x ≥ 0 over integers needs special handling.
→ Future work: interval propagation, nonlinear extensions.have to provide the hint, then grind.bv_decide for bit-vectors and combinatorial explosion,
grind for everything else (including linear arithmetic via cutsat),
lean-smt when you want CVC5's power (e.g., lots of case-analysis).
| CS257 Concept | Lean Tactic |
|---|---|
| SAT solving (CDCL) | bv_decide + LRAT checker |
| Cooper's algorithm (QE) | omega / grind (cutsat) |
| DPLL(T) architecture | grind |
| E-matching, congruence closure | grind |
| SMT / CVC5 | lean-smt |
Adoption: grind alone is used ~3,400 times in Mathlib (28% avg LoC reduction),
~500 times each in CSLib, the Prime Number Theorem project, and Terence Tao's Analysis I.
A foundation for computer science in Lean.
Steering committee:
CSLib aims to be a foundation for teaching, research, and new verification efforts, including AI-assisted.
#eval 2 + 3, and hit enter. You're using Lean.