Zulip Chat Archive
Stream: new members
Topic: simplifying proofs with inductive case analysis
Nada Amin (May 11 2025 at 23:23):
I am wondering how I can simplify a proof in Lean. I got it down to the following: https://github.com/HarvardPL/cs152-lecture-code/blob/main/code01.lean, but the proof of the theorem still feels tedious, while the Dafny version has an automated (trivial & empty!) proof: https://github.com/HarvardPL/cs152-lecture-code/blob/main/code01.dfy
What are the principles/tricks to simplify such proofs in Lean?
Nada Amin (May 11 2025 at 23:26):
Nada Amin said:
I am wondering how I can simplify a proof in Lean. I got it down to the following: https://github.com/HarvardPL/cs152-lecture-code/blob/main/code01.lean, but the proof of the theorem still feels tedious, while the Dafny version has an automated (trivial & empty!) proof: https://github.com/HarvardPL/cs152-lecture-code/blob/main/code01.dfy
What are the principles/tricks to simplify such proofs in Lean?
Nada Amin (May 11 2025 at 23:27):
I forwarded the conversation to new members. Sorry for the noise.
Nada Amin (May 11 2025 at 23:35):
Correction: the dafny proof that the optimizer preserves the semantic is not entirely automatic, it has the inductive sketch, but that's all. The proof of optimality of the optimizer is automatic though, but I didn't do that in Lean yet, because I'd first like to understand how to automate proofs better. Thanks!
Notification Bot (May 11 2025 at 23:44):
2 messages were moved here from #lean4 > simplifying proofs with inductive case analysis by Eric Wieser.
Arthur Adjedj (May 12 2025 at 14:26):
This looks like the sort of proofs that grind should (hopefully) be able to produce automatically in a short future. I had a go at trying to solve it using the tactic, but it still failed on some branches. I'm guessing some lemmas (e.g Int.zero_mul) still need to get tagged with the right attributes in Core for the tactic to work seemlessly on such examples. In any case, here is a shorter proof relying on grind:
set_option grind.warning false
-- Theorem: optimizer preserves semantics
theorem optimize_preserves_semantics (e : Expr) (env : Env) :
evaluate (optimize e) env = evaluate e env := by
induction e <;> simp [optimize, evaluate]
· grind [evaluate,optimize]
· split
· rename_i ih₁ ih₂ _ _ eq
rw [←ih₁,eq]
simp only [evaluate, Int.zero_mul]
· rename_i ih₁ ih₂ _ _ eq _
rw [←ih₁,←ih₂,eq]
simp only [evaluate, Int.mul_zero]
· rename_i ih₁ ih₂ _ _ eq _
rw [←ih₁,←ih₂,eq]
simp only [evaluate, Int.one_mul]
· rename_i ih₁ ih₂ _ _ eq _ _
rw [←ih₁,←ih₂,eq]
simp only [evaluate, Int.mul_one]
· grind [evaluate,optimize]
Nada Amin (May 12 2025 at 14:43):
Thanks, this looks like a great start. I'll be sure to look into grind.
Nada Amin (May 12 2025 at 22:16):
Thanks again for showing me grind! I played with it, and the code below works for all the cases but multiplication. What does multiplication require? Do we have to explicitly use the lemmas Int.one_mul and Int.mul_one?
theorem optimize_preserves_semantics (e : Expr) (env : Env) :
evaluate (optimize e) env = evaluate e env := by
induction e <;> simp [optimize, evaluate]
· grind (config := { trace := true, ring := true, ringSteps := 10, splits := 15, ematch := 15, gen := 15, splitMatch := true }) [evaluate, optimize]
<;> simp_all [evaluate, optimize]
Thanks, again!
Kevin Buzzard (May 12 2025 at 22:18):
(grind is still a WIP tactic, it hasn't even been announced as ready yet)
Nada Amin (May 12 2025 at 22:21):
Ah, ok. Do you recommend I look into other tactics or patterns for tedious case analysis? Thanks!
Kevin Buzzard (May 12 2025 at 22:23):
I don't know anything about how to do case analyses like this, I just thought it was worth clarifying that grind currently has no documentation, is unfinished, and is being actively worked on right now (you can see many Lean 4 PRs recently about this tactic), just so you were clear on its current status.
Nada Amin (May 12 2025 at 22:23):
OK, cool, thanks. It sounds fun to dig more deeply into its internals.
Nada Amin (May 12 2025 at 23:42):
Oh, OK. I was using grind on just the remaining add case, instead of all the remaining cases by using · instead of <;>.
Here is a working proof:
theorem optimize_preserves_semantics (e : Expr) (env : Env) :
evaluate (optimize e) env = evaluate e env := by
induction e <;> simp [optimize, evaluate]
<;> grind (config := { ring := true }) [evaluate, optimize]
Nada Amin (May 13 2025 at 00:11):
I updated the dev from above with the automatic proof. Unfortunately, I can't figure out grind for the next proof, which is slightly less trivial (optimize_optimal). Will continue...
Nada Amin (May 13 2025 at 02:01):
I did a case by case proof of the second lemma:
theorem optimize_optimal (e : Expr) :
optimal (optimize e) := by
induction e
case const n =>
simp [optimize, optimal]
case var x =>
simp [optimize, optimal]
case add e₁ e₂ h₁ h₂ =>
simp [optimal, optimize]
split <;> intros
· simp [optimal]
· rw [h₂]
· rw [h₁]
· simp [optimal, optimize]
rw [h₁, h₂]
apply And.intro
rfl
rfl
case mul e₁ e₂ h₁ h₂ =>
simp [optimal, optimize]
split <;> intros
· simp [optimal]
· simp [optimal]
· simp [optimal]
· rw [h₂]
· rw [h₁]
· simp [optimal, optimize]
rw [h₁, h₂]
apply And.intro
rfl
rfl
Still looking for simplifications on this one. Thanks for any tips!
Arthur Adjedj (May 13 2025 at 09:39):
The following works:
theorem optimize_optimal (e : Expr) :
optimal (optimize e) := by
induction e <;> simp [optimize, optimal] <;> split <;> simp [optimal, *]
Arthur Adjedj (May 13 2025 at 11:00):
BTW, rewording optimal as an inductive predicate makes grind solve it:
-- Datatype for arithmetic expressions
inductive Expr where
| const : Int → Expr -- Constants like 5
| var : String → Expr -- Variables like "x"
| add : Expr → Expr → Expr -- Addition (e₁ + e₂)
| mul : Expr → Expr → Expr -- Multiplication (e₁ * e₂)
deriving Repr
open Expr
-- Environment type (maps variable names to values)
def Env := String → Int
-- Evaluator function
def evaluate : Expr → Env → Int
| const n, _ => n -- A constant evaluates to itself
| var x, env => env x -- A variable evaluates to its value in the environment
| add e₁ e₂, env => evaluate e₁ env + evaluate e₂ env -- Addition evaluates recursively
| mul e₁ e₂, env => evaluate e₁ env * evaluate e₂ env -- Multiplication evaluates recursively too
-- Optimizer to remove additions by 0 & multiplication by 1, and to simplify multiplication by 0
def optimize : Expr → Expr
| const n => const n -- Constants remain unchanged
| var x => var x -- Variables remain unchanged
| add e₁ e₂ =>
let e₁' := optimize e₁
let e₂' := optimize e₂
match e₁', e₂' with
| const n₁, const n₂ => const (n₁ + n₂)
| const 0, e => e -- 0 + e => e
| e, const 0 => e -- e + 0 => e
| _, _ => add e₁' e₂' -- Otherwise keep the optimized addition
| mul e₁ e₂ =>
let e₁' := optimize e₁
let e₂' := optimize e₂
match e₁', e₂' with
| const n₁, const n₂ => const (n₁ * n₂)
| const 0, _ => const 0 -- 0 * e => 0
| _, const 0 => const 0 -- e * 0 => 0
| const 1, e => e -- 1 * e => e
| e, const 1 => e -- e * 1 => e
| _, _ => mul e₁' e₂' -- Otherwise keep the optimized multiplication
-- Theorem: optimizer preserves semantics
-- Thanks to Arthur Adjedj for pointing out the new grind tactic
set_option grind.warning false
theorem optimize_preserves_semantics (e : Expr) (env : Env) :
evaluate (optimize e) env = evaluate e env := by
induction e <;> grind (config := { ring := true }) [evaluate, optimize]
@[grind cases]
inductive NotOptimal : Expr → Prop
| addConst : NotOptimal (add (const c₁) (const c₂))
| addLeftZero : NotOptimal (add (const 0) e)
| addLeftRight : NotOptimal (add e (const 0))
| addNotOptiLeft : NotOptimal e₁ → NotOptimal (add e₁ e₂)
| addNotOptiRight : NotOptimal e₂ → NotOptimal (add e₁ e₂)
| mulConst : NotOptimal (mul (const c₁) (const c₂))
| mulLeftZero : NotOptimal (mul (const 0) e)
| mulLeftRight : NotOptimal (mul e (const 0))
| mulOneZero : NotOptimal (mul (const 1) e)
| mulOneRight : NotOptimal (mul e (const 1))
| mulNotOptiLeft : NotOptimal e₁ → NotOptimal (mul e₁ e₂)
| mulNotOptiRight : NotOptimal e₂ → NotOptimal (mul e₁ e₂)
theorem optimize_optimal (e : Expr) :
¬ NotOptimal (optimize e) := by
induction e <;> grind +ring [optimize,optimal]
Nada Amin (May 13 2025 at 18:57):
Arthur Adjedj said:
The following works:
theorem optimize_optimal (e : Expr) : optimal (optimize e) := by induction e <;> simp [optimize, optimal] <;> split <;> simp [optimal, *]
Thanks! The use of * is neat.
Nada Amin (May 13 2025 at 19:00):
It's interesting that grind works with an inductive definition of NotOptimal. Do you have any intuition why this is easier for grind? I would have thought that the double negation would make it harder.
Thanks so much for exploring the space of solutions with me!
Nada Amin (May 17 2025 at 03:19):
Aesop also works nicely here.
import Aesop
-- Datatype for arithmetic expressions
@[aesop safe [constructors, cases]]
inductive Expr where
| const : Int → Expr -- Constants like 5
| var : String → Expr -- Variables like "x"
| add : Expr → Expr → Expr -- Addition (e₁ + e₂)
| mul : Expr → Expr → Expr -- Multiplication (e₁ * e₂)
deriving Repr
-- Environment type (maps variable names to values)
def Env := String → Int
-- Evaluator function
@[aesop safe]
def evaluate : Expr → Env → Int
| Expr.const n, _ => n -- A constant evaluates to itself
| Expr.var x, env => env x -- A variable evaluates to its value in the environment
| Expr.add e₁ e₂, env => evaluate e₁ env + evaluate e₂ env -- Addition evaluates recursively
| Expr.mul e₁ e₂, env => evaluate e₁ env * evaluate e₂ env -- Multiplication evaluates recursively too
-- Optimizer to remove additions by 0 & multiplication by 1, and to simplify multiplication by 0
@[aesop safe]
def optimize : Expr → Expr
| Expr.const n => Expr.const n -- Constants remain unchanged
| Expr.var x => Expr.var x -- Variables remain unchanged
| Expr.add e₁ e₂ =>
let e₁' := optimize e₁
let e₂' := optimize e₂
match e₁', e₂' with
| Expr.const n₁, Expr.const n₂ => Expr.const (n₁ + n₂)
| Expr.const 0, e => e -- 0 + e => e
| e, Expr.const 0 => e -- e + 0 => e
| _, _ => Expr.add e₁' e₂' -- Otherwise keep the optimized addition
| Expr.mul e₁ e₂ =>
let e₁' := optimize e₁
let e₂' := optimize e₂
match e₁', e₂' with
| Expr.const n₁, Expr.const n₂ => Expr.const (n₁ * n₂)
| Expr.const 0, _ => Expr.const 0 -- 0 * e => 0
| _, Expr.const 0 => Expr.const 0 -- e * 0 => 0
| Expr.const 1, e => e -- 1 * e => e
| e, Expr.const 1 => e -- e * 1 => e
| _, _ => Expr.mul e₁' e₂' -- Otherwise keep the optimized multiplication
theorem optimize_preserves_semantics (e : Expr) (env : Env) :
evaluate (optimize e) env = evaluate e env := by
aesop
-- Predicate defining an optimally optimized expression
def optimal : Expr → Bool
| Expr.const _ => true
| Expr.var _ => true
| Expr.add (Expr.const _) (Expr.const _) => false
| Expr.add (Expr.const 0) _ => false
| Expr.add _ (Expr.const 0) => false
| Expr.add e₁ e₂ => optimal e₁ && optimal e₂
| Expr.mul (Expr.const _) (Expr.const _) => false
| Expr.mul (Expr.const 0) _ => false
| Expr.mul _ (Expr.const 0) => false
| Expr.mul (Expr.const 1) _ => false
| Expr.mul _ (Expr.const 1) => false
| Expr.mul e₁ e₂ => optimal e₁ && optimal e₂
-- Theorem: optimizer produces optimital expressions
theorem optimize_optimal (e : Expr) :
optimal (optimize e) := by
aesop
Nada Amin (May 17 2025 at 03:23):
Oh, nevermind, it looks like the max number of heartbeat was reached for each theorem.
Last updated: Dec 20 2025 at 21:32 UTC