Zulip Chat Archive
Stream: lean4
Topic: How to properly define the "Syll" tactic?
Andrew Naguib (Aug 02 2024 at 02:19):
I am trying to define a new tactic "Syll" to finish goals that could be deduced from a chain of implications. But, I am not sure how to construct the proof term as to finish the goal by only invoking the tactic. Here is how I implemented it:
open Lean Meta Elab Tactic Term in
elab "Syll " : tactic => Lean.Elab.Tactic.withMainContext do
let lctx ← Lean.MonadLCtx.getLCtx
let goal ← Lean.Elab.Tactic.getMainGoal
let goalType ← Lean.Elab.Tactic.getMainTarget
let hyps ← getLocalHyps
let (hyp1, hyp2) := (hyps.get! 0, hyps.get! 1)
-- defining a list of pairs
let mut chain : List (Expr × Expr) := []
let getImplicationType (e : Expr) : MetaM (Option (Expr × Expr)) := do
match (← inferType e) with
| Expr.forallE _ p q _ => return some (p, q)
| _ => return none
for hyp in ← getLocalHyps do
match (← getImplicationType hyp) with
| some (p, q) => chain := chain ++ [(p, q)]
| none =>
dbg_trace f! /- f-string notation -/
"Expression {hyp} is not of the form `p → q`"
let Expr.forallE _ p r _ :=
goalType | throwError "Goal type is not of the form `p → r`"
if not (← isExprDefEq (chain.get! 0).1 p) then
throwError "The first hypothesis does not match the goal's antecedent"
if not (← isExprDefEq (chain.get! (chain.length - 1)).2 r) then
throwError "The last hypothesis does not match the goal's consequent"
let mut validChain := true
for i in [:chain.length - 1] do
if not (← isExprDefEq chain[i]!.2 chain[i+1]!.1) then
validChain := false
throwError "Syll could not verify the chain of implications"
if validChain then
dbg_trace "Goal can be deduced from the provided chain of hypotheses"
-- How to construct the proofTerm here?
else
throwError "The chain of implications is not valid"
For example, to finish the goal in the following theorem:
theorem test_Syll (p q r: Prop) (H1 : p → q) (H2 : q → r) : p → r := by
Syll
Kyle Miller (Aug 02 2024 at 05:18):
In case you'd be happy using a pre-existing tactic, Lean itself has solve_by_elim
, and Mathlib has tauto
and cc
. The first works using backtracking search, but could fail in some cases, and the second should always work. I think the third does some sort of saturation thing that's more clever (though it's supposed to be used for equations, not implications).
Kyle Miller (Aug 02 2024 at 05:46):
Here's a debugged version of your tactic, with proof construction:
open Lean Meta Elab Tactic Term
structure ImpProof where
(ant cons : Expr)
(proof : Expr)
deriving Inhabited
theorem compose {p q r : Prop} (pq : p → q) (qr : q → r) : p → r :=
qr ∘ pq
/-- Compose two implication proofs using the `compose` theorem. -/
def ImpProof.compose (pq : ImpProof) (qr : ImpProof) : MetaM ImpProof := do
unless ← isDefEq pq.cons qr.ant do
throwError "\
Consequent{indentD pq.cons}\n\
is not definitionally equal to antecedent{indentD qr.ant}"
let proof := mkApp5 (.const ``compose []) pq.ant pq.cons qr.cons pq.proof qr.proof
return { ant := pq.ant, cons := qr.cons, proof := proof }
/-- Create the proof of `p -> p` using the `id` function. -/
def ImpProof.rfl (p : Expr) : ImpProof :=
{ ant := p, cons := p, proof := .app (.const ``id [.zero]) p}
elab "Syll " : tactic => liftMetaTactic1 fun goal => do
let goalType ← goal.getType
-- A list of implications from the local context.
let mut chain : Array ImpProof := #[]
let getImplication? (e : Expr) : MetaM (Option (Expr × Expr)) := do
-- There may be metadata and metavariables, so do some unfolding if necessary:
let ty ← instantiateMVars (← whnfR e)
-- Check if it is a non-dependent forall:
if ty.isArrow then
return (ty.bindingDomain!, ty.bindingBody!)
else
return none
for hyp in ← getLocalHyps do
match ← getImplication? (← inferType hyp) with
| some (p, q) => chain := chain.push {ant := p, cons := q, proof := hyp}
| none => logInfo m!"Expression {hyp} is not of the form `p → q`"
let some (p, q) ← getImplication? goalType
| throwError "Goal type is not of the form `p → q`"
if chain.isEmpty then
throwError "Local context has no implications"
unless ← isExprDefEq chain[0]!.ant p do
throwError "The first hypothesis does not match the goal's antecedent"
unless ← isExprDefEq chain[chain.size - 1]!.cons q do
throwError "The last hypothesis does not match the goal's consequent"
let comp ← chain.foldlM (init := ImpProof.rfl p) (fun pf1 pf2 => pf1.compose pf2)
-- It's good to do one last check that the proof has the correct type before assignment.
unless ← isDefEq (← inferType comp.proof) goalType do
throwError "Invalid proof of goal"
goal.assign comp.proof
return none
theorem test_Syll (p q r : Prop) (H1 : p → q) (H2 : q → r) : p → r := by
Syll
Kyle Miller (Aug 02 2024 at 06:34):
For a more sophisticated version, here's a tactic that allows the implications to come in any order and with any arity.
theorem test1 (p q r : Prop) (H1 : p → q) (H2 : q → r) : p → r := by
Syll
theorem test1' (p q r : Prop) (H2 : q → r) (H1 : p → q) : p → r := by
Syll
theorem test2 (p q r : Prop) (h1 : p → q) (h2 : p → q → r) : p → r := by
Syll
theorem test3 (p q r s : Prop) (h1 : p → q → r) (h2 : s → q) : p → s → r := by
Syll
-- Printing proofs:
#print test1
-- fun p q r H1 H2 a ↦ (fun a ↦ H2 a) ((fun a ↦ H1 a) a)
#print test2
-- fun p q r h1 h2 a ↦ (fun a a_1 ↦ h2 a a_1) a ((fun a ↦ h1 a) a)
#print test3
-- fun p q r s h1 h2 a a_1 ↦ (fun a a_2 ↦ h1 a a_2) a ((fun a ↦ h2 a) a_1)
code
Kyle Miller (Aug 02 2024 at 06:39):
Exercise: get Syll
to work with
theorem test4 (p q r s : Prop) (h1 : p → q → r) (h2 : (q → r) → s) : p → s := by
Syll
Kyle Miller (Aug 02 2024 at 07:08):
Hmm, I guess it's hard to beat the simplicity of simp, though if you want Syll
to intentionally be limited to syllogisms, then this is not the right approach.
import Lean
open Lean Meta Elab Tactic Term
elab "revert_all" : tactic => liftMetaTactic1 fun goal => goal.revertAll
macro "Syll" : tactic => `(tactic| (revert_all; simp (config := { contextual := true })))
theorem test1 (p q r : Prop) (H1 : p → q) (H2 : q → r) : p → r := by
Syll
theorem test1' (p q r : Prop) (H2 : q → r) (H1 : p → q) : p → r := by
Syll
theorem test2 (p q r : Prop) (h1 : p → q) (h2 : p → q → r) : p → r := by
Syll
theorem test3 (p q r s : Prop) (h1 : p → q → r) (h2 : s → q) : p → s → r := by
Syll
theorem test4 (p q r s : Prop) (h1 : p → q → r) (h2 : (q → r) → s) : p → s := by
Syll
Andrew Naguib (Aug 02 2024 at 18:24):
Thank you @Kyle Miller , this is elegant. The debugged version is exactly what I wanted. The version where you handle the syllogism in any arbitrary order was quite interesting.
I am trying to push it a little further so that Syll
also optionally accepts hypotheses as arguments, and only proceeds to using ← getLocalHyps
if none was passed. E.g., Syll [pq, qr, ...]
. Is adding (config)? hyps:rwRuleSeq
(I cheated it from the definition of rw
) after the identifier the correct way to do it?
Kyle Miller (Aug 02 2024 at 20:05):
No, rwRuleSeq
wouldn't be right, since that has supports other features like ← e
rules. Basically, you just need to add ("[" term,* "]")?
to the syntax, which means "optionally a square-braced-enclosed comma-delimited sequence of zero or more term
s". I also split off a separate syntax definition so that it's possible to make use of the more powerful pattern matching in elab_rules
. The syntax there is unfortunately a bit of sorcery, but I copied it from another tactic :-)
The way you process these terms is to use Tactic.elabTerm
on each of them. None of the code beyond that actually cares about whether a particular term is a local hypothesis or not.
import Lean
open Lean Meta Elab Tactic Term
structure ImpProof where
(ant cons : Expr)
(proof : Expr)
deriving Inhabited
theorem compose {p q r : Prop} (pq : p → q) (qr : q → r) : p → r :=
qr ∘ pq
/-- Compose two implication proofs using the `compose` theorem. -/
def ImpProof.compose (pq : ImpProof) (qr : ImpProof) : MetaM ImpProof := do
unless ← isDefEq pq.cons qr.ant do
throwError "\
Consequent{indentD pq.cons}\n\
is not definitionally equal to antecedent{indentD qr.ant}"
let proof := mkApp5 (.const ``compose []) pq.ant pq.cons qr.cons pq.proof qr.proof
return { ant := pq.ant, cons := qr.cons, proof := proof }
/-- Create the proof of `p -> p` using the `id` function. -/
def ImpProof.rfl (p : Expr) : ImpProof :=
{ ant := p, cons := p, proof := .app (.const ``id [.zero]) p}
syntax "Syll" (ppSpace "[" term,* "]")? : tactic
elab_rules : tactic
| `(tactic| Syll $[[$[$terms?],*]]?) => withMainContext do
-- Elaborate all the supplied hypotheses, or use the entire local context if not provided.
let hyps ←
match terms? with
| none => getLocalHyps
| some terms => terms.mapM fun term => Tactic.elabTerm term none
liftMetaTactic1 fun goal => do
let goalType ← goal.getType
-- A list of implications extracted from `hyps`.
let mut chain : Array ImpProof := #[]
let getImplication? (e : Expr) : MetaM (Option (Expr × Expr)) := do
-- There may be metadata and metavariables, so do some unfolding if necessary:
let ty ← instantiateMVars (← whnfR e)
-- Check if it is a non-dependent forall:
if ty.isArrow then
return (ty.bindingDomain!, ty.bindingBody!)
else
return none
for hyp in hyps do
match ← getImplication? (← inferType hyp) with
| some (p, q) => chain := chain.push {ant := p, cons := q, proof := hyp}
| none => logInfo m!"Expression {hyp} is not of the form `p → q`"
let some (p, q) ← getImplication? goalType
| throwError "Goal type is not of the form `p → q`"
if chain.isEmpty then
throwError "Local context has no implications"
unless ← isExprDefEq chain[0]!.ant p do
throwError "The first hypothesis does not match the goal's antecedent"
unless ← isExprDefEq chain[chain.size - 1]!.cons q do
throwError "The last hypothesis does not match the goal's consequent"
let comp ← chain.foldlM (init := ImpProof.rfl p) (fun pf1 pf2 => pf1.compose pf2)
-- It's good to do one last check that the proof has the correct type before assignment.
unless ← isDefEq (← inferType comp.proof) goalType do
throwError "Invalid proof of goal"
goal.assign comp.proof
return none
theorem test1 (p q r : Prop) (H1 : p → q) (H2 : q → r) : p → r := by
Syll [H1] -- fails
theorem test2 (p q r : Prop) (H1 : p → q) (H2 : q → r) : p → r := by
Syll [H2] -- fails
theorem test3 (p q r : Prop) (H1 : p → q) (H2 : q → r) : p → r := by
Syll [H2, H1] -- fails
theorem test4 (p q r : Prop) (H1 : p → q) (H2 : q → r) : p → r := by
Syll [H1, H2] -- succeeds
theorem test5 (p q r : Prop) (H1 : p → q) (H2 : q → r) : p → r := by
Syll -- succeeds
Andrew Naguib (Aug 10 2024 at 11:47):
Thank you @Kyle Miller very much; I didn't know I could specify a regex for the syntax.
Last updated: May 02 2025 at 03:31 UTC