From 46a078995c0e9e91e566225953f60a722ab14484 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Tue, 26 May 2026 14:43:44 +0200 Subject: [PATCH 001/181] Start implementing iinduction: move IntoIH from Induction.lean to Classes.lean and its type class instances to Instances.lean --- Iris/Iris/ProofMode/Classes.lean | 15 +++- Iris/Iris/ProofMode/Instances.lean | 30 ++++++++ Iris/Iris/ProofMode/Tactics.lean | 1 + Iris/Iris/ProofMode/Tactics/Induction.lean | 88 +++++++++++----------- 4 files changed, 85 insertions(+), 49 deletions(-) diff --git a/Iris/Iris/ProofMode/Classes.lean b/Iris/Iris/ProofMode/Classes.lean index fdbe326cb..18868a05a 100644 --- a/Iris/Iris/ProofMode/Classes.lean +++ b/Iris/Iris/ProofMode/Classes.lean @@ -1,7 +1,7 @@ /- Copyright (c) 2022 Lars König. All rights reserved. Released under Apache 2.0 license as described in the file LICENSE. -Authors: Lars König, Michael Sammler, Alvin Tang +Authors: Lars König, Michael Sammler, Yunsong Yang, Alvin Tang -/ module @@ -208,18 +208,27 @@ class IntoLaterN {PROP} [BI PROP] (only_head : Bool) (n : Nat) (P : PROP) (Q : o export IntoLaterN (into_laterN) /-- `CombineSepAs` combines two propositions `P` and `Q` into `R` -/ -@[ipm_class] +@[ipm_class, rocq_alias CombineSepAs] class CombineSepAs [BI PROP] (P Q : PROP) (R : outParam PROP) where combine_sep_as : P ∗ Q ⊢ R export CombineSepAs (combine_sep_as) /-- `CombineSepGives` combines two propositions `P` and `Q` for a proposition with the `` modality -/ -@[ipm_class] +@[ipm_class, rocq_alias CombineSepGives] class CombineSepGives [BI PROP] (P Q : PROP) (R : outParam PROP) where combine_sep_gives : P ∗ Q ⊢ R export CombineSepGives (combine_sep_gives) +/- + `IntoIH φ P Q` describes how to turn a pure induction hypothesis `φ` into a proofmode + hypothesis `Q` under an intuitionistic BI context `□ P`. +-/ +@[ipm_class, rocq_alias IntoIH] +class IntoIH [BI PROP] (φ : Prop) (P : PROP) (Q : outParam PROP) where + into_ih : φ → □ P ⊢ Q +export IntoIH (into_ih) + #rocq_ignore elim_inv_tc_opaque "No tc_opaque in Lean" #rocq_ignore elim_modal_tc_opaque "No tc_opaque in Lean" #rocq_ignore from_and_tc_opaque "No tc_opaque in Lean" diff --git a/Iris/Iris/ProofMode/Instances.lean b/Iris/Iris/ProofMode/Instances.lean index 53fecb860..ee702a834 100644 --- a/Iris/Iris/ProofMode/Instances.lean +++ b/Iris/Iris/ProofMode/Instances.lean @@ -7,6 +7,7 @@ module public import Iris.BI public import Iris.ProofMode.Classes +public import Iris.ProofMode.ClassesMake public import Iris.ProofMode.ModalityInstances public import Iris.Std.TC public import Iris.Std.RocqPorting @@ -917,3 +918,32 @@ instance combineSepGives_persistently [BI PROP] (Q1 Q2 P : PROP) [h : CombineSepGives Q1 Q2 P] : CombineSepGives iprop( Q1) iprop( Q2) iprop( P) where combine_sep_gives := persistently_sep_2.trans (persistently_mono h.combine_sep_gives) + +-- TODO: two more instances [into_ih_Forall] and [into_ih_Forall2] +-- have not been implemented + +@[rocq_alias into_ih_entails] +instance intoIH_entails [BI PROP] (P Q : PROP) : IntoIH (P ⊢ Q) P Q where + into_ih := λ hpq => intuitionistically_elim.trans hpq + +@[rocq_alias into_ih_forall] +instance intoIH_forall [BI PROP] (φ : α → Prop) (P : PROP) (Φ : α → PROP) + [h : ∀ x, IntoIH (φ x) P (Φ x)] : + IntoIH (∀ x, φ x) P (BI.forall Φ) where + into_ih := by + intro hφ + apply forall_intro + intro x + exact (h x).into_ih (hφ x) + +@[rocq_alias into_ih_impl] +instance intoIH_imp [BI PROP] (φ ψ : Prop) (Δ P Q : PROP) + [h1 : MakeAffinely iprop(⌜φ⌝) P] + [h2 : IntoIH ψ Δ Q] : + IntoIH (φ → ψ) Δ iprop(P -∗ Q) where + into_ih := by + intro hImp + apply wand_intro + refine (sep_mono_r h1.make_affinely.mpr).trans ?_ + refine persistent_and_affinely_sep_r.2.trans ?_ + exact pure_elim_r (fun hφ => h2.into_ih (hImp hφ)) diff --git a/Iris/Iris/ProofMode/Tactics.lean b/Iris/Iris/ProofMode/Tactics.lean index 361255881..f74191c1c 100644 --- a/Iris/Iris/ProofMode/Tactics.lean +++ b/Iris/Iris/ProofMode/Tactics.lean @@ -12,6 +12,7 @@ public meta import Iris.ProofMode.Tactics.ExFalso public meta import Iris.ProofMode.Tactics.Exists public meta import Iris.ProofMode.Tactics.Frame public meta import Iris.ProofMode.Tactics.Have +public meta import Iris.ProofMode.Tactics.Induction public meta import Iris.ProofMode.Tactics.Intro public meta import Iris.ProofMode.Tactics.LeftRight public meta import Iris.ProofMode.Tactics.Loeb diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 533818f9f..177106c22 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -1,59 +1,55 @@ /- Copyright (c) 2026 Yunsong Yang. All rights reserved. Released under Apache 2.0 license as described in the file LICENSE. -Authors: Yunsong Yang +Authors: Yunsong Yang, Alvin Tang -/ module -public import Iris.ProofMode.ClassesMake public meta import Iris.ProofMode.Tactics.Basic +public meta import Iris.ProofMode.Tactics.Assumption +public meta import Iris.ProofMode.Tactics.Cases +public meta import Iris.ProofMode.Patterns.CasesPattern +public meta import Iris.ProofMode.ClassesMake +public meta import Iris.ProofMode.Tactics.RevertIntro namespace Iris.ProofMode -public section -open BI Std +public meta section +open BI Std Lean Elab Tactic Meta Qq -/- - `IntoIH φ P Q` describes how to turn a pure induction hypothesis `φ` into a proofmode - hypothesis `Q` under an intuitionistic BI context `□ P`. +private def iInductionCore {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} + (hyps : Hyps bi e) (goal : Q($prop)) : + ProofModeM (Q($e ⊢ $goal)) := sorry + +/-- + Given a collection of hypotheses (`hyps`) and a free variable `fvar`, return + the subset of hypotheses in `hyps` that contains the `fvar`. -/ -@[ipm_class] -class IntoIH [BI PROP] (φ : Prop) (P : PROP) (Q : outParam PROP) where - into_ih : φ → □ P ⊢ Q - --- TODO: two more instances [into_ih_Forall] and [into_ih_Forall2] --- have not been implemented -instance intoIH_entails [BI PROP] (P Q : PROP) : IntoIH (P ⊢ Q) P Q where - into_ih := λ hpq => intuitionistically_elim.trans hpq -instance intoIH_forall [BI PROP] (φ : α → Prop) (P : PROP) (Φ : α → PROP) - [h : ∀ x, IntoIH (φ x) P (Φ x)] : - IntoIH (∀ x, φ x) P (BI.forall Φ) where - into_ih := by - intro hφ - apply forall_intro - intro x - exact (h x).into_ih (hφ x) -instance intoIH_imp [BI PROP] (φ ψ : Prop) (Δ P Q : PROP) - [h1 : MakeAffinely iprop(⌜φ⌝) P] - [h2 : IntoIH ψ Δ Q] : - IntoIH (φ → ψ) Δ iprop(P -∗ Q) where - into_ih := by - intro hImp - apply wand_intro - refine (sep_mono_r h1.make_affinely.mpr).trans ?_ - refine persistent_and_affinely_sep_r.2.trans ?_ - exact pure_elim_r (fun hφ => h2.into_ih (hImp hφ)) - -theorem ih_revert [BI PROP] {Δ P Q : PROP} {φ : Prop} (hφ : φ) - [hP : IntoIH φ Δ P] - (hΔ : Δ ⊢ □ Δ) - (hPQ : Δ ⊢ iprop( P → Q)) : - Δ ⊢ Q := by - have hP' : □ Δ ⊢ P := - (intuitionistically_intro' (hP.into_ih hφ)).trans persistently_of_intuitionistically - have hAnd : □ Δ ⊢ iprop( P ∧ ( P → Q)) := - and_intro hP' <| intuitionistically_elim.trans hPQ - exact hΔ.trans <| hAnd.trans (imp_elim_r (P := iprop( P)) (Q := Q)) +private def iHypsContaining {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} + (hyps : Hyps bi e) (fvar : FVarId) : List SelTarget := + let ivars := hyps.spatialIVarIds ++ hyps.intuitionisticIVarIds -public meta section -open Lean Elab Tactic Meta Qq + let containing := ivars.filter fun ivar => + match hyps.getDecl? ivar with + | some (_, _, _, ty) => ty.containsFVar fvar + | none => false + + containing.map (fun ivar => { kind := .ipm ivar, explicit := false }) + +elab "iinduction" colGt x:ident : tactic => do + -- Get the ID of the variable on which induction is being performed + let fvar ← getFVarId x + + ProofModeM.runTactic λ mvar { hyps, goal, .. } => do + -- Find all hypotheses that contain the variable being performed induction on + let targets := iHypsContaining hyps fvar + + -- Revert all hypotheses in the list + let pf ← iRevertIntro hyps goal targets ( + fun hyps' goal' k => do + -- Use built-in induction in Lean + evalTactic (← `(tactic| induction x:id)) + k hyps' goal' + ) + + mvar.assign pf From 5421a78bf21beeb1fc0f11572d12b2b02cf36542 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Thu, 28 May 2026 14:13:30 +0200 Subject: [PATCH 002/181] Simple iinduction test --- Iris/Iris/Tests/Tactics.lean | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index edfb8d86c..538bb0b55 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2715,3 +2715,12 @@ example (P Q : PROP) : iloeb as IH end iloeb + +section iinduction + +example [BI PROP] {P Q : PROP} {n : Nat} : + ⊢ P -∗ ⌜n = 0⌝ -∗ Q -∗ ⌜n ≠ 1⌝ -∗ P ∗ Q ∗ ⌜n = n⌝ := by + iintro H1 H2 H3 #H4 + iinduction n + +end iinduction From 562fd6478468200912e696cdcd47a021aeca550c Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Thu, 28 May 2026 14:27:57 +0200 Subject: [PATCH 003/181] Use evalTacticAt for iinduction --- Iris/Iris/ProofMode/Tactics/Induction.lean | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 177106c22..060967509 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -47,8 +47,10 @@ elab "iinduction" colGt x:ident : tactic => do -- Revert all hypotheses in the list let pf ← iRevertIntro hyps goal targets ( fun hyps' goal' k => do - -- Use built-in induction in Lean - evalTactic (← `(tactic| induction x:id)) + -- Create a new metavariable for the proof goal upon reverting hypotheses + let m ← mkBIGoal hyps' goal' + -- Use built-in induction in Lean to generate the subgoals for induction + let subgoals ← evalTacticAt (← `(tactic| induction $x:ident)) m.mvarId! k hyps' goal' ) From 9af4f6f7e8a7868579575bcb6f093c21838c9f23 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Thu, 28 May 2026 14:54:13 +0200 Subject: [PATCH 004/181] Try using iRevertCore and iIntroCore separately --- Iris/Iris/ProofMode/Tactics/Induction.lean | 32 ++++++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 060967509..80bc6b6d6 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -45,13 +45,39 @@ elab "iinduction" colGt x:ident : tactic => do let targets := iHypsContaining hyps fvar -- Revert all hypotheses in the list - let pf ← iRevertIntro hyps goal targets ( - fun hyps' goal' k => do + let pf ← iRevertCore targets hyps goal ( + + let names : List (Syntax × IntroPat) := ← targets.mapM (fun + | {kind := .pure id, ..} => do + let name ← Lean.mkIdent <$> id.getUserName + let ident ← `(binderIdent| $name:ident) + return (name, IntroPat.intro <| .pure ident) + | {kind := .ipm ivar, ..} => do + let name ← Lean.mkIdent <$> (hyps.getUserName? ivar).getM + let ident ← `(binderIdent| $name:ident) + return (name, IntroPat.intro <| (if ivar.persistent? then iCasesPat.intuitionistic else id) <| .one ident) + ) + + -- The function takes the hypotheses and goal after reverting + fun hyps' goal' => do -- Create a new metavariable for the proof goal upon reverting hypotheses let m ← mkBIGoal hyps' goal' + -- Use built-in induction in Lean to generate the subgoals for induction let subgoals ← evalTacticAt (← `(tactic| induction $x:ident)) m.mvarId! - k hyps' goal' + + for s in subgoals do + s.withContext do + let sType ← instantiateMVars (← s.getType) + + let some irisGoal := parseIrisGoal? sType + -- This should not happen + | throwError "iinduction: fail to parse induction subgoal" + + let casePf ← iIntroCore irisGoal.hyps irisGoal.goal names + s.assign casePf + + return m ) mvar.assign pf From 72fd7a0be2be9f4c72565c160227b115d541dc2d Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Thu, 28 May 2026 14:55:52 +0200 Subject: [PATCH 005/181] Finish the proof for iinduction --- Iris/Iris/Tests/Tactics.lean | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 538bb0b55..5681c1bd3 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2722,5 +2722,8 @@ example [BI PROP] {P Q : PROP} {n : Nat} : ⊢ P -∗ ⌜n = 0⌝ -∗ Q -∗ ⌜n ≠ 1⌝ -∗ P ∗ Q ∗ ⌜n = n⌝ := by iintro H1 H2 H3 #H4 iinduction n + · iframe + · iframe + itrivial end iinduction From a7aeae1b5b676cef69769c79bd8c33838be538b1 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Thu, 28 May 2026 16:08:03 +0200 Subject: [PATCH 006/181] Use iRevertIntro instead of iRevertCore and iIntroCore separately This also requires generalisation of ProofModeContinuation --- Iris/Iris/ProofMode/Tactics/Induction.lean | 20 ++++---------------- Iris/Iris/ProofMode/Tactics/RevertIntro.lean | 8 ++++---- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 80bc6b6d6..e3f640d21 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -45,27 +45,16 @@ elab "iinduction" colGt x:ident : tactic => do let targets := iHypsContaining hyps fvar -- Revert all hypotheses in the list - let pf ← iRevertCore targets hyps goal ( - - let names : List (Syntax × IntroPat) := ← targets.mapM (fun - | {kind := .pure id, ..} => do - let name ← Lean.mkIdent <$> id.getUserName - let ident ← `(binderIdent| $name:ident) - return (name, IntroPat.intro <| .pure ident) - | {kind := .ipm ivar, ..} => do - let name ← Lean.mkIdent <$> (hyps.getUserName? ivar).getM - let ident ← `(binderIdent| $name:ident) - return (name, IntroPat.intro <| (if ivar.persistent? then iCasesPat.intuitionistic else id) <| .one ident) - ) - + let pf ← iRevertIntro hyps goal targets ( -- The function takes the hypotheses and goal after reverting - fun hyps' goal' => do + fun hyps' goal' k => do -- Create a new metavariable for the proof goal upon reverting hypotheses let m ← mkBIGoal hyps' goal' -- Use built-in induction in Lean to generate the subgoals for induction let subgoals ← evalTacticAt (← `(tactic| induction $x:ident)) m.mvarId! + -- Handle each subgoal for s in subgoals do s.withContext do let sType ← instantiateMVars (← s.getType) @@ -74,8 +63,7 @@ elab "iinduction" colGt x:ident : tactic => do -- This should not happen | throwError "iinduction: fail to parse induction subgoal" - let casePf ← iIntroCore irisGoal.hyps irisGoal.goal names - s.assign casePf + s.assign <| ← k irisGoal.hyps irisGoal.goal return m ) diff --git a/Iris/Iris/ProofMode/Tactics/RevertIntro.lean b/Iris/Iris/ProofMode/Tactics/RevertIntro.lean index 380de3c6d..70d0e2e52 100644 --- a/Iris/Iris/ProofMode/Tactics/RevertIntro.lean +++ b/Iris/Iris/ProofMode/Tactics/RevertIntro.lean @@ -13,16 +13,16 @@ open Lean Meta Elab.Tactic Qq public meta section -abbrev ProofModeContinuation (u : Level) := - ∀ {prop : Q(Type u)} {bi : Q(BI $prop)} {e : Q($prop)} - (_hyps : Hyps bi e)(goal: Q($prop)), +abbrev ProofModeContinuation := + ∀ {u : Level} {prop : Q(Type u)} {bi : Q(BI $prop)} {e : Q($prop)} + (_hyps : Hyps bi e) (goal: Q($prop)), ProofModeM Q($e ⊢ $goal) def iRevertIntro {prop: Q(Type u)} {bi : Q(BI $prop)} {e : Q($prop)} (hyps : Hyps bi e) (goal: Q($prop)) (hs : List SelTarget) (k : ∀ {prop : Q(Type u)} {bi : Q(BI $prop)} {e : Q($prop)} - (_hyps : Hyps bi e) (goal: Q($prop)), ProofModeContinuation u → + (_hyps : Hyps bi e) (goal: Q($prop)), ProofModeContinuation → ProofModeM Q($e ⊢ $goal)) : ProofModeM Q($e ⊢ $goal) := do let names : List (Syntax × IntroPat) ← hs.mapM fun From 6203c5f3189c895c46e27cc0efa185968afcd53f Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Thu, 28 May 2026 16:27:17 +0200 Subject: [PATCH 007/181] Remove redundant Iris entailment in Lean context --- Iris/Iris/ProofMode/Tactics/Induction.lean | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index e3f640d21..12e1fc113 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -36,6 +36,16 @@ private def iHypsContaining {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} containing.map (fun ivar => { kind := .ipm ivar, explicit := false }) +/-- Clear all local hypotheses whose type parses as an IrisGoal. -/ +private def clearIrisGoalHyps (s : MVarId) : MetaM MVarId := + s.withContext do + (← getLCtx).foldlM (fun s' ldecl => do + if ldecl.isAuxDecl then return s' + if (parseIrisGoal? (← instantiateMVars ldecl.type)).isSome then + try s'.clear ldecl.fvarId + catch _ => pure s' + else pure s') s + elab "iinduction" colGt x:ident : tactic => do -- Get the ID of the variable on which induction is being performed let fvar ← getFVarId x @@ -56,6 +66,7 @@ elab "iinduction" colGt x:ident : tactic => do -- Handle each subgoal for s in subgoals do + let s ← clearIrisGoalHyps s s.withContext do let sType ← instantiateMVars (← s.getType) From 7035695bdcd3ce14e6b9a055c87f5e383b1d03d4 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Thu, 28 May 2026 16:50:00 +0200 Subject: [PATCH 008/181] Avoid unnamed variable in generated induction subgoals --- Iris/Iris/ProofMode/Tactics/Induction.lean | 7 +++++++ Iris/Iris/Tests/Tactics.lean | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 12e1fc113..1e347890e 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -67,6 +67,13 @@ elab "iinduction" colGt x:ident : tactic => do -- Handle each subgoal for s in subgoals do let s ← clearIrisGoalHyps s + + let s ← ( + do + let [s'] ← evalTacticAt (← `(tactic| rename_i $x:ident)) s | pure s + pure s' + ) <|> pure s + s.withContext do let sType ← instantiateMVars (← s.getType) diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 5681c1bd3..b9b86b40f 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2726,4 +2726,12 @@ example [BI PROP] {P Q : PROP} {n : Nat} : · iframe itrivial +example [BI PROP] {P Q : PROP} {n : Nat} : + ⊢ P -∗ □ Q -∗ ⌜n + 0 = n⌝ := by + iintro H1 #H2 + iinduction n + · iframe + · iframe + itrivial + end iinduction From e963f76210f35ad3f1358edb4230a55a189ddbe8 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Thu, 28 May 2026 17:50:30 +0200 Subject: [PATCH 009/181] Revert all hypotheses in the intuitionistic context --- Iris/Iris/ProofMode/Tactics/Induction.lean | 30 +++------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 1e347890e..fb8d6dd74 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -17,34 +17,20 @@ namespace Iris.ProofMode public meta section open BI Std Lean Elab Tactic Meta Qq -private def iInductionCore {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} - (hyps : Hyps bi e) (goal : Q($prop)) : - ProofModeM (Q($e ⊢ $goal)) := sorry - /-- Given a collection of hypotheses (`hyps`) and a free variable `fvar`, return the subset of hypotheses in `hyps` that contains the `fvar`. -/ private def iHypsContaining {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} (hyps : Hyps bi e) (fvar : FVarId) : List SelTarget := - let ivars := hyps.spatialIVarIds ++ hyps.intuitionisticIVarIds - - let containing := ivars.filter fun ivar => + let intuitionisticIVarIds := hyps.intuitionisticIVarIds.filter fun ivar => match hyps.getDecl? ivar with | some (_, _, _, ty) => ty.containsFVar fvar | none => false - containing.map (fun ivar => { kind := .ipm ivar, explicit := false }) - -/-- Clear all local hypotheses whose type parses as an IrisGoal. -/ -private def clearIrisGoalHyps (s : MVarId) : MetaM MVarId := - s.withContext do - (← getLCtx).foldlM (fun s' ldecl => do - if ldecl.isAuxDecl then return s' - if (parseIrisGoal? (← instantiateMVars ldecl.type)).isSome then - try s'.clear ldecl.fvarId - catch _ => pure s' - else pure s') s + (hyps.spatialIVarIds ++ intuitionisticIVarIds).map ( + fun ivar => { kind := .ipm ivar, explicit := false } + ) elab "iinduction" colGt x:ident : tactic => do -- Get the ID of the variable on which induction is being performed @@ -66,14 +52,6 @@ elab "iinduction" colGt x:ident : tactic => do -- Handle each subgoal for s in subgoals do - let s ← clearIrisGoalHyps s - - let s ← ( - do - let [s'] ← evalTacticAt (← `(tactic| rename_i $x:ident)) s | pure s - pure s' - ) <|> pure s - s.withContext do let sType ← instantiateMVars (← s.getType) From d9fa30a0dae158a2b19db9fea3b68c0c0169c3ca Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Thu, 28 May 2026 18:27:49 +0200 Subject: [PATCH 010/181] Towards using IntoIH for moving induction hypothesis from Lean's context into Iris proof state --- Iris/Iris/ProofMode/Expr.lean | 6 ++++ Iris/Iris/ProofMode/Tactics/Induction.lean | 34 +++++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/Iris/Iris/ProofMode/Expr.lean b/Iris/Iris/ProofMode/Expr.lean index 481b3bcc5..870a0c1bc 100644 --- a/Iris/Iris/ProofMode/Expr.lean +++ b/Iris/Iris/ProofMode/Expr.lean @@ -189,6 +189,12 @@ partial def Hyps.intuitionisticIVarIds {u prop bi} : | _, .hyp _ _ ivar p _ _ => if isTrue p then [ivar] else [] | _, .sep _ _ _ _ lhs rhs => lhs.intuitionisticIVarIds ++ rhs.intuitionisticIVarIds +partial def Hyps.intuitionisticProps {u prop bi} : + ∀ {s}, @Hyps u prop bi s → List Q($prop) + | _, .emp _ => [] + | _, .hyp _ _ _ p ty _ => if isTrue p then [ty] else [] + | _, .sep _ _ _ _ lhs rhs => lhs.intuitionisticProps ++ rhs.intuitionisticProps + variable (oldIVar : IVarId) (new : Name) {prop : Q(Type u)} {bi : Q(BI $prop)} in def Hyps.rename : ∀ {e}, Hyps bi e → Option (Hyps bi e) | _, .emp _ => none diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index fb8d6dd74..b964e1884 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -32,6 +32,32 @@ private def iHypsContaining {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} fun ivar => { kind := .ipm ivar, explicit := false } ) +private def intuitionisticHyps {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} + (h : Hyps bi e) : ProofModeM Q($prop) := do + let props : List Q($prop) := h.intuitionisticProps + if props.isEmpty then + return q(emp) + else + return props.foldl (fun acc ty => q(iprop($acc ∗ $ty))) props[0]! + +private def addIntoIH {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} + (hyps : Hyps bi e) (φ : Q(Prop)) (hFVar : FVarId) : + ProofModeM (Hyps bi e) := do + let P : Q($prop) ← intuitionisticHyps hyps + + let Q ← mkFreshExprMVarQ q($prop) + let some inst ← ProofModeM.trySynthInstanceQ q(IntoIH $φ $P $Q) + | throwError "iinduction: type class synthesis failed" + + let dummyIdent ← `(binderIdent| _) + let ⟨newName, newIdent⟩ ← getFreshName dummyIdent + + let ⟨ivar, newHyps⟩ ← Hyps.addWithInfo bi dummyIdent q(true) Q hyps + + return sorry + +private def findIH : ProofModeM FVarId := sorry + elab "iinduction" colGt x:ident : tactic => do -- Get the ID of the variable on which induction is being performed let fvar ← getFVarId x @@ -59,7 +85,13 @@ elab "iinduction" colGt x:ident : tactic => do -- This should not happen | throwError "iinduction: fail to parse induction subgoal" - s.assign <| ← k irisGoal.hyps irisGoal.goal + -- Find the induction hypothesis generated by Lean's `induction` tactic + let ihFVar ← findIH + + -- Introduce the induction hypothesis back into the Iris proof state + let newHyps ← addIntoIH irisGoal.hyps (← inferType <| mkFVar ihFVar) ihFVar + + s.assign <| ← k newHyps irisGoal.goal return m ) From ffcdbb38c32b1c027623cc3b33bc4efdb5fd0b10 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Fri, 29 May 2026 10:16:48 +0200 Subject: [PATCH 011/181] Revert only spatial and intuitionistic hypotheses that contain x --- Iris/Iris/ProofMode/Tactics/Induction.lean | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index b964e1884..39c7a65dc 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -23,12 +23,12 @@ open BI Std Lean Elab Tactic Meta Qq -/ private def iHypsContaining {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} (hyps : Hyps bi e) (fvar : FVarId) : List SelTarget := - let intuitionisticIVarIds := hyps.intuitionisticIVarIds.filter fun ivar => + let ivars := (hyps.spatialIVarIds ++ hyps.intuitionisticIVarIds).filter fun ivar => match hyps.getDecl? ivar with | some (_, _, _, ty) => ty.containsFVar fvar | none => false - (hyps.spatialIVarIds ++ intuitionisticIVarIds).map ( + ivars.map ( fun ivar => { kind := .ipm ivar, explicit := false } ) From e94141bbe37e5230a8a888cba535b7933f2a2cc2 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Fri, 29 May 2026 10:30:08 +0200 Subject: [PATCH 012/181] Use the result of IntoIH synthesis in addIntoIH --- Iris/Iris/ProofMode/Tactics/Induction.lean | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 39c7a65dc..026a6cf32 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -40,21 +40,25 @@ private def intuitionisticHyps {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} else return props.foldl (fun acc ty => q(iprop($acc ∗ $ty))) props[0]! +/-- + Used for every induction hypothesis generated by Lean upon using the built-in + `induction` tactic. The type class `IntoIH` is used for obtaining the + corresponding proposition in the Iris proof mode. +-/ private def addIntoIH {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} (hyps : Hyps bi e) (φ : Q(Prop)) (hFVar : FVarId) : - ProofModeM (Hyps bi e) := do + ProofModeM ((e' : Q($prop)) × Hyps bi e') := do let P : Q($prop) ← intuitionisticHyps hyps let Q ← mkFreshExprMVarQ q($prop) - let some inst ← ProofModeM.trySynthInstanceQ q(IntoIH $φ $P $Q) - | throwError "iinduction: type class synthesis failed" - - let dummyIdent ← `(binderIdent| _) - let ⟨newName, newIdent⟩ ← getFreshName dummyIdent + let some _ ← ProofModeM.trySynthInstanceQ q(IntoIH $φ $P $Q) + | throwError "iinduction: type class synthesis with IntoIH failed" - let ⟨ivar, newHyps⟩ ← Hyps.addWithInfo bi dummyIdent q(true) Q hyps + let nameIdent := mkIdent <| ← hFVar.getUserName + let binderIdent ← `(binderIdent| $nameIdent:ident) + let ⟨_, newHyps⟩ ← Hyps.addWithInfo bi binderIdent q(true) Q hyps - return sorry + return ⟨_, newHyps⟩ private def findIH : ProofModeM FVarId := sorry @@ -91,7 +95,7 @@ elab "iinduction" colGt x:ident : tactic => do -- Introduce the induction hypothesis back into the Iris proof state let newHyps ← addIntoIH irisGoal.hyps (← inferType <| mkFVar ihFVar) ihFVar - s.assign <| ← k newHyps irisGoal.goal + -- s.assign <| ← k newHyps irisGoal.goal return m ) From 8ba460a52638d46179b981a5bdf5c716de7bf2a7 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Fri, 29 May 2026 10:44:12 +0200 Subject: [PATCH 013/181] Iteratively add induction hypotheses into the Iris proof state --- Iris/Iris/ProofMode/Tactics/Induction.lean | 28 ++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 026a6cf32..59bf3be1f 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -60,7 +60,27 @@ private def addIntoIH {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} return ⟨_, newHyps⟩ -private def findIH : ProofModeM FVarId := sorry +/-- + Search for all induction hypotheses generated by Lean's built-in `induction` + tactic and return their `FVarId` values as a list. +-/ +private def findIHs : ProofModeM (List FVarId) := sorry + +/-- + Introduce a list of induction hypotheses into Iris proof state. + The list of `FVarId` values (`ihFVars`) should be the list returned by + `findIHs`. +-/ +private def addAllIHs {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} + (hyps : Hyps bi e) (ihFVars : List FVarId) : + ProofModeM ((e' : Q($prop)) × Hyps bi e') := do + let mut current : (e' : Q($prop)) × Hyps bi e' := ⟨_, hyps⟩ + + for i in ihFVars do + let φ ← inferType (mkFVar i) + current ← addIntoIH current.snd φ i + + return current elab "iinduction" colGt x:ident : tactic => do -- Get the ID of the variable on which induction is being performed @@ -90,12 +110,12 @@ elab "iinduction" colGt x:ident : tactic => do | throwError "iinduction: fail to parse induction subgoal" -- Find the induction hypothesis generated by Lean's `induction` tactic - let ihFVar ← findIH + let ihFVars ← findIHs -- Introduce the induction hypothesis back into the Iris proof state - let newHyps ← addIntoIH irisGoal.hyps (← inferType <| mkFVar ihFVar) ihFVar + let ⟨_, newHyps⟩ ← addAllIHs irisGoal.hyps ihFVars - -- s.assign <| ← k newHyps irisGoal.goal + s.assign <| ← k newHyps irisGoal.goal return m ) From 8524a82be7686c01af8c7e73ebc04d160982580f Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Fri, 29 May 2026 12:02:13 +0200 Subject: [PATCH 014/181] Towards implementing findIHs --- Iris/Iris/ProofMode/Tactics/Induction.lean | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 59bf3be1f..b5563b04a 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -60,11 +60,26 @@ private def addIntoIH {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} return ⟨_, newHyps⟩ +private def exprContainsConst (e : Expr) (n : Name) : Bool := + (e.find? fun + | .const c _ => c == n + | _ => false).isSome + /-- Search for all induction hypotheses generated by Lean's built-in `induction` tactic and return their `FVarId` values as a list. -/ -private def findIHs : ProofModeM (List FVarId) := sorry +private def findIHs : ProofModeM (List FVarId) := do + let lctx ← getLCtx + return ← lctx.decls.toList.filterMapM fun + | none => return none + | some ldecl => + if ldecl.isAuxDecl || ldecl.isImplementationDetail + then return none + else + if exprContainsConst ldecl.type ``Entails' + then return some ldecl.fvarId + else return none /-- Introduce a list of induction hypotheses into Iris proof state. From 998ffb8b07b4d2c6cab8a74d320c39280ee4f42a Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Fri, 29 May 2026 16:02:48 +0200 Subject: [PATCH 015/181] Implement findIHs --- Iris/Iris/ProofMode/Tactics/Induction.lean | 35 ++++++++-------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index b5563b04a..86f89379b 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -60,26 +60,17 @@ private def addIntoIH {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} return ⟨_, newHyps⟩ -private def exprContainsConst (e : Expr) (n : Name) : Bool := - (e.find? fun - | .const c _ => c == n - | _ => false).isSome - -/-- - Search for all induction hypotheses generated by Lean's built-in `induction` - tactic and return their `FVarId` values as a list. --/ -private def findIHs : ProofModeM (List FVarId) := do - let lctx ← getLCtx - return ← lctx.decls.toList.filterMapM fun - | none => return none - | some ldecl => - if ldecl.isAuxDecl || ldecl.isImplementationDetail - then return none - else - if exprContainsConst ldecl.type ``Entails' - then return some ldecl.fvarId - else return none +/-- Search for hypotheses in the regular Lean context that represent induction + hypotheses and return their IDs as a list. -/ +private def findIHs (m : MVarId) : MetaM (List FVarId) := + m.withContext do + let lctx ← getLCtx + let mut ihs := [] + for decl in lctx do + let type ← instantiateMVars decl.type + if isIrisGoal type.consumeMData then + ihs := (decl.fvarId) :: ihs + return ihs.reverse /-- Introduce a list of induction hypotheses into Iris proof state. @@ -124,8 +115,8 @@ elab "iinduction" colGt x:ident : tactic => do -- This should not happen | throwError "iinduction: fail to parse induction subgoal" - -- Find the induction hypothesis generated by Lean's `induction` tactic - let ihFVars ← findIHs + -- Find the induction hypotheses generated by Lean's `induction` tactic + let ihFVars ← findIHs s -- Introduce the induction hypothesis back into the Iris proof state let ⟨_, newHyps⟩ ← addAllIHs irisGoal.hyps ihFVars From 4bc644cd142b2534489d7891fa4e8ebabf6a6fb6 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Fri, 29 May 2026 16:04:48 +0200 Subject: [PATCH 016/181] Remove induction hypotheses from the regular Lean context --- Iris/Iris/ProofMode/Tactics/Induction.lean | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 86f89379b..10fbe0e3a 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -121,7 +121,7 @@ elab "iinduction" colGt x:ident : tactic => do -- Introduce the induction hypothesis back into the Iris proof state let ⟨_, newHyps⟩ ← addAllIHs irisGoal.hyps ihFVars - s.assign <| ← k newHyps irisGoal.goal + s.assign <| ← withoutFVars (u := 0) ihFVars.toArray <| k newHyps irisGoal.goal return m ) From 4f6817a6026c0b2f9b2fa260254c278c3ad8cecd Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Fri, 29 May 2026 16:41:38 +0200 Subject: [PATCH 017/181] Adjust iHypsContaining --- Iris/Iris/ProofMode/Expr.lean | 6 ++++++ Iris/Iris/ProofMode/Tactics/Induction.lean | 10 ++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Iris/Iris/ProofMode/Expr.lean b/Iris/Iris/ProofMode/Expr.lean index 870a0c1bc..50c9cfa4a 100644 --- a/Iris/Iris/ProofMode/Expr.lean +++ b/Iris/Iris/ProofMode/Expr.lean @@ -195,6 +195,12 @@ partial def Hyps.intuitionisticProps {u prop bi} : | _, .hyp _ _ _ p ty _ => if isTrue p then [ty] else [] | _, .sep _ _ _ _ lhs rhs => lhs.intuitionisticProps ++ rhs.intuitionisticProps +partial def Hyps.spatialProps {u prop bi} : + ∀ {s}, @Hyps u prop bi s → List Q($prop) + | _, .emp _ => [] + | _, .hyp _ _ _ p ty _ => if isTrue p then [] else [ty] + | _, .sep _ _ _ _ lhs rhs => lhs.spatialProps ++ rhs.spatialProps + variable (oldIVar : IVarId) (new : Name) {prop : Q(Type u)} {bi : Q(BI $prop)} in def Hyps.rename : ∀ {e}, Hyps bi e → Option (Hyps bi e) | _, .emp _ => none diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 10fbe0e3a..338a563b5 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -23,12 +23,12 @@ open BI Std Lean Elab Tactic Meta Qq -/ private def iHypsContaining {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} (hyps : Hyps bi e) (fvar : FVarId) : List SelTarget := - let ivars := (hyps.spatialIVarIds ++ hyps.intuitionisticIVarIds).filter fun ivar => + let ivars := hyps.intuitionisticIVarIds.filter fun ivar => match hyps.getDecl? ivar with | some (_, _, _, ty) => ty.containsFVar fvar | none => false - ivars.map ( + (ivars ++ hyps.spatialIVarIds).map ( fun ivar => { kind := .ipm ivar, explicit := false } ) @@ -51,6 +51,8 @@ private def addIntoIH {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} let P : Q($prop) ← intuitionisticHyps hyps let Q ← mkFreshExprMVarQ q($prop) + logInfo m!"[DEBUG] φ: {φ}, P: {P}, Q: {Q}" + let some _ ← ProofModeM.trySynthInstanceQ q(IntoIH $φ $P $Q) | throwError "iinduction: type class synthesis with IntoIH failed" @@ -68,8 +70,8 @@ private def findIHs (m : MVarId) : MetaM (List FVarId) := let mut ihs := [] for decl in lctx do let type ← instantiateMVars decl.type - if isIrisGoal type.consumeMData then - ihs := (decl.fvarId) :: ihs + if isIrisGoal type then + ihs := decl.fvarId :: ihs return ihs.reverse /-- From ff5ea5c357844341d6c9a49da664bab32c50bcc2 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Fri, 29 May 2026 17:33:13 +0200 Subject: [PATCH 018/181] Introducing IHs into the Iris intuitionstic context To-do: 1. Bug: Some kernel mismatch error when the tactic is used 2. Bug: The variable being performed induction on becomes unnamed 3. Bug: The induction hypotheses are unnamed 4. Feature: Introduction patterns --- Iris/Iris/ProofMode/Expr.lean | 8 +------- Iris/Iris/ProofMode/Tactics/Induction.lean | 13 +++++++++---- Iris/Iris/Tests/Tactics.lean | 8 ++++---- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/Iris/Iris/ProofMode/Expr.lean b/Iris/Iris/ProofMode/Expr.lean index 50c9cfa4a..a45f3e023 100644 --- a/Iris/Iris/ProofMode/Expr.lean +++ b/Iris/Iris/ProofMode/Expr.lean @@ -192,15 +192,9 @@ partial def Hyps.intuitionisticIVarIds {u prop bi} : partial def Hyps.intuitionisticProps {u prop bi} : ∀ {s}, @Hyps u prop bi s → List Q($prop) | _, .emp _ => [] - | _, .hyp _ _ _ p ty _ => if isTrue p then [ty] else [] + | _, .hyp tm _ _ p _ _ => if isTrue p then [tm] else [] | _, .sep _ _ _ _ lhs rhs => lhs.intuitionisticProps ++ rhs.intuitionisticProps -partial def Hyps.spatialProps {u prop bi} : - ∀ {s}, @Hyps u prop bi s → List Q($prop) - | _, .emp _ => [] - | _, .hyp _ _ _ p ty _ => if isTrue p then [] else [ty] - | _, .sep _ _ _ _ lhs rhs => lhs.spatialProps ++ rhs.spatialProps - variable (oldIVar : IVarId) (new : Name) {prop : Q(Type u)} {bi : Q(BI $prop)} in def Hyps.rename : ∀ {e}, Hyps bi e → Option (Hyps bi e) | _, .emp _ => none diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 338a563b5..321cfdf04 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -19,7 +19,8 @@ open BI Std Lean Elab Tactic Meta Qq /-- Given a collection of hypotheses (`hyps`) and a free variable `fvar`, return - the subset of hypotheses in `hyps` that contains the `fvar`. + the subset of intuitionistic hypotheses in `hyps` that contains the `fvar` + and all spatial hypotheses in the proof state. -/ private def iHypsContaining {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} (hyps : Hyps bi e) (fvar : FVarId) : List SelTarget := @@ -32,13 +33,18 @@ private def iHypsContaining {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} fun ivar => { kind := .ipm ivar, explicit := false } ) +/-- + Given `h` of type `Hyps bi e`, search for all hypotheses in the intuitionistic + context, concatenate them using the separation conjunction and return + it as a single proposition. +-/ private def intuitionisticHyps {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} (h : Hyps bi e) : ProofModeM Q($prop) := do let props : List Q($prop) := h.intuitionisticProps if props.isEmpty then return q(emp) else - return props.foldl (fun acc ty => q(iprop($acc ∗ $ty))) props[0]! + return props.tail.reverse.foldr (fun acc tm => q(iprop($tm ∗ $acc))) props[0]! /-- Used for every induction hypothesis generated by Lean upon using the built-in @@ -51,8 +57,6 @@ private def addIntoIH {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} let P : Q($prop) ← intuitionisticHyps hyps let Q ← mkFreshExprMVarQ q($prop) - logInfo m!"[DEBUG] φ: {φ}, P: {P}, Q: {Q}" - let some _ ← ProofModeM.trySynthInstanceQ q(IntoIH $φ $P $Q) | throwError "iinduction: type class synthesis with IntoIH failed" @@ -123,6 +127,7 @@ elab "iinduction" colGt x:ident : tactic => do -- Introduce the induction hypothesis back into the Iris proof state let ⟨_, newHyps⟩ ← addAllIHs irisGoal.hyps ihFVars + -- Remove the IHs from the regular context s.assign <| ← withoutFVars (u := 0) ihFVars.toArray <| k newHyps irisGoal.goal return m diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index b9b86b40f..4d26742e9 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2726,11 +2726,11 @@ example [BI PROP] {P Q : PROP} {n : Nat} : · iframe itrivial -example [BI PROP] {P Q : PROP} {n : Nat} : - ⊢ P -∗ □ Q -∗ ⌜n + 0 = n⌝ := by - iintro H1 #H2 +example [BI PROP] {P Q R S T : PROP} {n : Nat} : + ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by + iintro HP #HQ #HR HS #HT iinduction n - · iframe + · itrivial · iframe itrivial From 2f47ceb6edf1c8cf3db284b9d869d5e9539a8860 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 30 May 2026 16:38:46 +0200 Subject: [PATCH 019/181] Use Lean.Meta.Tactic.induction instead of evalTacticAt for more flexibility regarding IH naming --- Iris/Iris/ProofMode/Tactics/Induction.lean | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 321cfdf04..e0c817231 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -110,25 +110,26 @@ elab "iinduction" colGt x:ident : tactic => do let m ← mkBIGoal hyps' goal' -- Use built-in induction in Lean to generate the subgoals for induction - let subgoals ← evalTacticAt (← `(tactic| induction $x:ident)) m.mvarId! + let subgoals ← m.mvarId!.withContext do + m.mvarId!.induction fvar `Nat.recOn #[] -- Handle each subgoal for s in subgoals do - s.withContext do - let sType ← instantiateMVars (← s.getType) + s.mvarId.withContext do + let sType ← instantiateMVars (← s.mvarId.getType) let some irisGoal := parseIrisGoal? sType -- This should not happen | throwError "iinduction: fail to parse induction subgoal" -- Find the induction hypotheses generated by Lean's `induction` tactic - let ihFVars ← findIHs s + let ihFVars ← findIHs s.mvarId -- Introduce the induction hypothesis back into the Iris proof state let ⟨_, newHyps⟩ ← addAllIHs irisGoal.hyps ihFVars -- Remove the IHs from the regular context - s.assign <| ← withoutFVars (u := 0) ihFVars.toArray <| k newHyps irisGoal.goal + s.mvarId.assign <| ← withoutFVars (u := 0) ihFVars.toArray <| k newHyps irisGoal.goal return m ) From a9be948bedaf5ba85bfdf2507a9bcda8afb7288c Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 30 May 2026 17:11:56 +0200 Subject: [PATCH 020/181] Generalisation to find recursor name for induction --- Iris/Iris/ProofMode/Tactics/Induction.lean | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index e0c817231..c8546486b 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -102,6 +102,15 @@ elab "iinduction" colGt x:ident : tactic => do -- Find all hypotheses that contain the variable being performed induction on let targets := iHypsContaining hyps fvar + -- Find the recursor name for induction + let fvarType ← whnf <| ← inferType <| mkFVar fvar + let recName : Name ← match fvarType.getAppFn with + | Expr.const indName _ => + match (← getEnv).find? indName with + | some (ConstantInfo.inductInfo val) => pure (Name.mkStr val.name "recOn") + | _ => throwError "iinduction: {indName} is not inductive" + | _ => throwError "iinduction: unable to determine inductive type" + -- Revert all hypotheses in the list let pf ← iRevertIntro hyps goal targets ( -- The function takes the hypotheses and goal after reverting @@ -111,7 +120,7 @@ elab "iinduction" colGt x:ident : tactic => do -- Use built-in induction in Lean to generate the subgoals for induction let subgoals ← m.mvarId!.withContext do - m.mvarId!.induction fvar `Nat.recOn #[] + m.mvarId!.induction fvar recName #[] -- Handle each subgoal for s in subgoals do From c627c24dc93ece749ce084b746fddeb8a0afc777 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 30 May 2026 17:26:19 +0200 Subject: [PATCH 021/181] Towards having explicit variable names for induction hypotheses --- Iris/Iris/ProofMode/Tactics/Induction.lean | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index c8546486b..927a1d7bb 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -105,12 +105,17 @@ elab "iinduction" colGt x:ident : tactic => do -- Find the recursor name for induction let fvarType ← whnf <| ← inferType <| mkFVar fvar let recName : Name ← match fvarType.getAppFn with - | Expr.const indName _ => + | .const indName _ => match (← getEnv).find? indName with - | some (ConstantInfo.inductInfo val) => pure (Name.mkStr val.name "recOn") + | some (.inductInfo val) => pure <| Name.mkStr val.name "recOn" | _ => throwError "iinduction: {indName} is not inductive" | _ => throwError "iinduction: unable to determine inductive type" + let varNames : Array AltVarNames := #[ + { explicit := true, varNames := [] }, + { explicit := true, varNames := [x.getId, `IH] } + ] + -- Revert all hypotheses in the list let pf ← iRevertIntro hyps goal targets ( -- The function takes the hypotheses and goal after reverting @@ -120,7 +125,7 @@ elab "iinduction" colGt x:ident : tactic => do -- Use built-in induction in Lean to generate the subgoals for induction let subgoals ← m.mvarId!.withContext do - m.mvarId!.induction fvar recName #[] + m.mvarId!.induction fvar recName varNames -- Handle each subgoal for s in subgoals do From a11f96700cbdcae75b6177329aea883ab3f914e7 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 30 May 2026 18:03:21 +0200 Subject: [PATCH 022/181] Solve kernel mismatch, currently using a placeholder theorem --- Iris/Iris/ProofMode/Tactics/Induction.lean | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 927a1d7bb..60ba83c1b 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -94,6 +94,16 @@ private def addAllIHs {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} return current +/-- + The `iinduction` tactic user provides the proof for the induction subgoal, + which has hypotheses introduced into the Iris proof state. + + Given this proof, obtain the proof where the hypotheses are not yet + introduced into the Iris proof state. +-/ +@[rocq_alias tac_revert_ih] +theorem revert_IH [BI PROP] {a b c d : PROP} (h : a ⊢ b) : c ⊢ d := sorry + elab "iinduction" colGt x:ident : tactic => do -- Get the ID of the variable on which induction is being performed let fvar ← getFVarId x @@ -142,8 +152,10 @@ elab "iinduction" colGt x:ident : tactic => do -- Introduce the induction hypothesis back into the Iris proof state let ⟨_, newHyps⟩ ← addAllIHs irisGoal.hyps ihFVars + let pf ← withoutFVars (u := 0) ihFVars.toArray <| k newHyps irisGoal.goal + -- Remove the IHs from the regular context - s.mvarId.assign <| ← withoutFVars (u := 0) ihFVars.toArray <| k newHyps irisGoal.goal + s.mvarId.assign <| (q(revert_IH $pf) : Q($irisGoal.e ⊢ $irisGoal.goal)) return m ) From b304c88316b159f8992b96ff083cd0e5f991c706 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 30 May 2026 18:36:31 +0200 Subject: [PATCH 023/181] Implement InductionState, towards finishing the placeholder theorem --- Iris/Iris/ProofMode/Tactics/Induction.lean | 92 ++++++++++++++-------- 1 file changed, 57 insertions(+), 35 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 60ba83c1b..bea34528f 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -46,63 +46,85 @@ private def intuitionisticHyps {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} else return props.tail.reverse.foldr (fun acc tm => q(iprop($tm ∗ $acc))) props[0]! +/-- Search for hypotheses in the regular Lean context that represent induction + hypotheses and return their IDs as a list. -/ +private def findIHs (m : MVarId) : MetaM (List FVarId) := + m.withContext do + let lctx ← getLCtx + let mut ihs := [] + for decl in lctx do + let type ← instantiateMVars decl.type + if isIrisGoal type then + ihs := decl.fvarId :: ihs + return ihs.reverse + +/-- + The `iinduction` tactic user provides the proof for the induction subgoal, + which has hypotheses introduced into the Iris proof state. + + Given this proof, obtain the proof where the hypotheses are not yet + introduced into the Iris proof state. +-/ +@[rocq_alias tac_revert_ih] +theorem revert_IH [BI PROP] {P Q R goal : PROP} + {φ : Prop} + (inst : IntoIH φ P Q) + (h : R ∗ □ Q ⊢ goal) : R ⊢ goal := sorry + +/-- + Designed to be a mutable state such that `newHyps` contains induction + hypotheses generated by Lean's built-in induction. +-/ +private structure InductionState {u} {prop : Q(Type u)} {bi} (origE goal : Q($prop)) where + {newE : Q($prop)} + (newHyps : Hyps bi newE) + (pf : Q(($newE ⊢ $goal) → $origE ⊢ $goal)) + /-- Used for every induction hypothesis generated by Lean upon using the built-in `induction` tactic. The type class `IntoIH` is used for obtaining the corresponding proposition in the Iris proof mode. -/ -private def addIntoIH {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} - (hyps : Hyps bi e) (φ : Q(Prop)) (hFVar : FVarId) : - ProofModeM ((e' : Q($prop)) × Hyps bi e') := do +private def addIntoIH {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e goal} + (st : @InductionState u prop bi e goal) + (φ : Q(Prop)) (hFVar : FVarId) : + ProofModeM (@InductionState u prop bi e goal) := do + + let hyps := st.newHyps + let P : Q($prop) ← intuitionisticHyps hyps let Q ← mkFreshExprMVarQ q($prop) - let some _ ← ProofModeM.trySynthInstanceQ q(IntoIH $φ $P $Q) + let some inst ← ProofModeM.trySynthInstanceQ q(IntoIH $φ $P $Q) | throwError "iinduction: type class synthesis with IntoIH failed" let nameIdent := mkIdent <| ← hFVar.getUserName let binderIdent ← `(binderIdent| $nameIdent:ident) let ⟨_, newHyps⟩ ← Hyps.addWithInfo bi binderIdent q(true) Q hyps - return ⟨_, newHyps⟩ - -/-- Search for hypotheses in the regular Lean context that represent induction - hypotheses and return their IDs as a list. -/ -private def findIHs (m : MVarId) : MetaM (List FVarId) := - m.withContext do - let lctx ← getLCtx - let mut ihs := [] - for decl in lctx do - let type ← instantiateMVars decl.type - if isIrisGoal type then - ihs := decl.fvarId :: ihs - return ihs.reverse + return { + newHyps, + pf := q(fun pf => $st.pf <| revert_IH $inst pf) + } /-- Introduce a list of induction hypotheses into Iris proof state. The list of `FVarId` values (`ihFVars`) should be the list returned by `findIHs`. -/ -private def addAllIHs {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} +private def addAllIHs {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e goal : Q($prop)} (hyps : Hyps bi e) (ihFVars : List FVarId) : - ProofModeM ((e' : Q($prop)) × Hyps bi e') := do - let mut current : (e' : Q($prop)) × Hyps bi e' := ⟨_, hyps⟩ + ProofModeM (@InductionState u prop bi e goal) := do + + let mut st : InductionState e goal := { + newHyps := hyps, pf := q(id) + } for i in ihFVars do let φ ← inferType (mkFVar i) - current ← addIntoIH current.snd φ i - - return current - -/-- - The `iinduction` tactic user provides the proof for the induction subgoal, - which has hypotheses introduced into the Iris proof state. + st ← addIntoIH st φ i - Given this proof, obtain the proof where the hypotheses are not yet - introduced into the Iris proof state. --/ -@[rocq_alias tac_revert_ih] -theorem revert_IH [BI PROP] {a b c d : PROP} (h : a ⊢ b) : c ⊢ d := sorry + return st elab "iinduction" colGt x:ident : tactic => do -- Get the ID of the variable on which induction is being performed @@ -150,12 +172,12 @@ elab "iinduction" colGt x:ident : tactic => do let ihFVars ← findIHs s.mvarId -- Introduce the induction hypothesis back into the Iris proof state - let ⟨_, newHyps⟩ ← addAllIHs irisGoal.hyps ihFVars + let st ← addAllIHs irisGoal.hyps ihFVars (goal := irisGoal.goal) - let pf ← withoutFVars (u := 0) ihFVars.toArray <| k newHyps irisGoal.goal + let pf' ← withoutFVars (u := 0) ihFVars.toArray <| k st.newHyps irisGoal.goal -- Remove the IHs from the regular context - s.mvarId.assign <| (q(revert_IH $pf) : Q($irisGoal.e ⊢ $irisGoal.goal)) + s.mvarId.assign <| q($st.pf $pf') return m ) From 73951390a1a30bee46b5c5e396e767a026a9aa31 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 30 May 2026 18:57:13 +0200 Subject: [PATCH 024/181] Finish revert_IH proof, more assumptions required --- Iris/Iris/ProofMode/Tactics/Induction.lean | 27 ++++++++++++++-------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index bea34528f..bbd0025cf 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -66,10 +66,17 @@ private def findIHs (m : MVarId) : MetaM (List FVarId) := introduced into the Iris proof state. -/ @[rocq_alias tac_revert_ih] -theorem revert_IH [BI PROP] {P Q R goal : PROP} - {φ : Prop} - (inst : IntoIH φ P Q) - (h : R ∗ □ Q ⊢ goal) : R ⊢ goal := sorry +theorem revert_IH [BI PROP] {P Q R goal : PROP} {φ} + (ih : φ) + (inst : IntoIH φ P Q) + (h1 : □ P ⊣⊢ R) + (h2 : R ∗ □ Q ⊢ goal) : R ⊢ goal := calc + R ⊢ □ P := h1.mpr + _ ⊢ □ P ∗ □ P := intuitionistically_sep_dup.mp + _ ⊢ □ P ∗ □ □ P := sep_mono_r intuitionistically_idem.mpr + _ ⊢ □ P ∗ □ Q := sep_mono_r <| intuitionistically_mono <| inst.into_ih ih + _ ⊢ R ∗ □ Q := sep_mono_l h1.mp + _ ⊢ goal := h2 /-- Designed to be a mutable state such that `newHyps` contains induction @@ -89,10 +96,10 @@ private def addIntoIH {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e goal} (st : @InductionState u prop bi e goal) (φ : Q(Prop)) (hFVar : FVarId) : ProofModeM (@InductionState u prop bi e goal) := do + let oldE := st.newE + let oldHyps : Hyps bi oldE := st.newHyps - let hyps := st.newHyps - - let P : Q($prop) ← intuitionisticHyps hyps + let P : Q($prop) ← intuitionisticHyps oldHyps let Q ← mkFreshExprMVarQ q($prop) let some inst ← ProofModeM.trySynthInstanceQ q(IntoIH $φ $P $Q) @@ -100,11 +107,13 @@ private def addIntoIH {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e goal} let nameIdent := mkIdent <| ← hFVar.getUserName let binderIdent ← `(binderIdent| $nameIdent:ident) - let ⟨_, newHyps⟩ ← Hyps.addWithInfo bi binderIdent q(true) Q hyps + let ⟨_, newHyps⟩ ← Hyps.addWithInfo bi binderIdent q(true) Q oldHyps + + let a := q(revert_IH sorry $inst (goal := $goal) (R := $oldE) sorry) return { newHyps, - pf := q(fun pf => $st.pf <| revert_IH $inst pf) + pf := q(fun pf => $st.pf <| $a pf) } /-- From cbdadb66503092964718ba350fd8c9f59838f233 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 30 May 2026 19:24:01 +0200 Subject: [PATCH 025/181] Towards completing the use of InductionState To-do: 1. Prove that the spatial context is empty after reverting hypotheses 2. Remaining tactic syntax 3. Remaining two IntoIH instance --- Iris/Iris/ProofMode/Tactics/Induction.lean | 42 +++++++++++++++------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index bbd0025cf..6e0bcfd4d 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -66,25 +66,35 @@ private def findIHs (m : MVarId) : MetaM (List FVarId) := introduced into the Iris proof state. -/ @[rocq_alias tac_revert_ih] -theorem revert_IH [BI PROP] {P Q R goal : PROP} {φ} +theorem revert_IH [BI PROP] {P Q goal : PROP} {φ} (ih : φ) (inst : IntoIH φ P Q) - (h1 : □ P ⊣⊢ R) - (h2 : R ∗ □ Q ⊢ goal) : R ⊢ goal := calc - R ⊢ □ P := h1.mpr + (h1 : P ⊢ □ P) + (h2 : P ∗ □ Q ⊢ goal) : P ⊢ goal := calc + P ⊢ □ P := h1 _ ⊢ □ P ∗ □ P := intuitionistically_sep_dup.mp _ ⊢ □ P ∗ □ □ P := sep_mono_r intuitionistically_idem.mpr _ ⊢ □ P ∗ □ Q := sep_mono_r <| intuitionistically_mono <| inst.into_ih ih - _ ⊢ R ∗ □ Q := sep_mono_l h1.mp + _ ⊢ P ∗ □ Q := sep_mono_l intuitionistically_elim _ ⊢ goal := h2 +theorem stay_intuitionistic [BI PROP] {P Q : PROP} + (h1 : P ⊢ □ P) : + P ∗ □ Q ⊢ □ (P ∗ □ Q) := calc + _ ⊢ □ P ∗ □ Q := sep_mono_l h1 + _ ⊢ □ P ∗ □ □ Q := sep_mono_r intuitionistically_idem.mpr + _ ⊢ □ (P ∗ □ Q) := intuitionistically_sep_2 + /-- Designed to be a mutable state such that `newHyps` contains induction hypotheses generated by Lean's built-in induction. -/ private structure InductionState {u} {prop : Q(Type u)} {bi} (origE goal : Q($prop)) where + -- Hypotheses in the latest Iris proof goal {newE : Q($prop)} (newHyps : Hyps bi newE) + -- Proof that the hypotheses is intuitionsitic + (pfAux : Q($newE ⊢ □ $newE)) (pf : Q(($newE ⊢ $goal) → $origE ⊢ $goal)) /-- @@ -94,28 +104,33 @@ private structure InductionState {u} {prop : Q(Type u)} {bi} (origE goal : Q($pr -/ private def addIntoIH {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e goal} (st : @InductionState u prop bi e goal) - (φ : Q(Prop)) (hFVar : FVarId) : + (φ : Q(Prop)) (p : Q($φ)) (hFVar : FVarId) : ProofModeM (@InductionState u prop bi e goal) := do let oldE := st.newE let oldHyps : Hyps bi oldE := st.newHyps - let P : Q($prop) ← intuitionisticHyps oldHyps - let Q ← mkFreshExprMVarQ q($prop) - let some inst ← ProofModeM.trySynthInstanceQ q(IntoIH $φ $P $Q) + let some inst ← ProofModeM.trySynthInstanceQ q(IntoIH $φ $oldE $Q) | throwError "iinduction: type class synthesis with IntoIH failed" let nameIdent := mkIdent <| ← hFVar.getUserName let binderIdent ← `(binderIdent| $nameIdent:ident) let ⟨_, newHyps⟩ ← Hyps.addWithInfo bi binderIdent q(true) Q oldHyps - let a := q(revert_IH sorry $inst (goal := $goal) (R := $oldE) sorry) + let a := q(revert_IH $p $inst (goal := $goal) $st.pfAux) return { newHyps, - pf := q(fun pf => $st.pf <| $a pf) + pf := q(fun pf => $st.pf <| $a pf), + pfAux := q(stay_intuitionistic $st.pfAux) } +/-- + This should be true in `addAllIHs` because all spatial hypotheses have + been reverted, so the Iris proof state contains no hypothesis in the + spatial context. -/ +theorem dummy [BI PROP] {e : PROP} : e ⊢ □ e := sorry + /-- Introduce a list of induction hypotheses into Iris proof state. The list of `FVarId` values (`ihFVars`) should be the list returned by @@ -126,12 +141,13 @@ private def addAllIHs {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e goal : Q($pro ProofModeM (@InductionState u prop bi e goal) := do let mut st : InductionState e goal := { - newHyps := hyps, pf := q(id) + newHyps := hyps, pf := q(id), pfAux := q(dummy) } for i in ihFVars do + let p := mkFVar i let φ ← inferType (mkFVar i) - st ← addIntoIH st φ i + st ← addIntoIH st φ p i return st From 5a5beaa254c06366981c93768370ded40852c6a6 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 30 May 2026 20:39:20 +0200 Subject: [PATCH 026/181] Port type class instances into_ih_Forall and into_ih_Forall2 --- Iris/Iris/ProofMode/Instances.lean | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/Iris/Iris/ProofMode/Instances.lean b/Iris/Iris/ProofMode/Instances.lean index ee702a834..b6a8e63a7 100644 --- a/Iris/Iris/ProofMode/Instances.lean +++ b/Iris/Iris/ProofMode/Instances.lean @@ -919,9 +919,6 @@ instance combineSepGives_persistently [BI PROP] (Q1 Q2 P : PROP) CombineSepGives iprop( Q1) iprop( Q2) iprop( P) where combine_sep_gives := persistently_sep_2.trans (persistently_mono h.combine_sep_gives) --- TODO: two more instances [into_ih_Forall] and [into_ih_Forall2] --- have not been implemented - @[rocq_alias into_ih_entails] instance intoIH_entails [BI PROP] (P Q : PROP) : IntoIH (P ⊢ Q) P Q where into_ih := λ hpq => intuitionistically_elim.trans hpq @@ -947,3 +944,21 @@ instance intoIH_imp [BI PROP] (φ ψ : Prop) (Δ P Q : PROP) refine (sep_mono_r h1.make_affinely.mpr).trans ?_ refine persistent_and_affinely_sep_r.2.trans ?_ exact pure_elim_r (fun hφ => h2.into_ih (hImp hφ)) + +/-- Support for induction principles whose IH is guarded by `List.Forall`, e.g. + `∀ l, Forall P l → P (Tree l)` arising from nested inductive types like + `inductive ntree := Tree : List ntree → ntree`. -/ +@[rocq_alias into_ih_Forall] +instance intoIH_listForall [BI PROP] (φ : α → Bool) (l : List α) (P : PROP) (Φ : α → PROP) + [h : ∀ x, IntoIH (φ x) P (Φ x)] : + IntoIH (l.all φ) P (bigSepL (fun _ a => iprop(□ Φ a)) l) where + into_ih := sorry + +/-- Support for induction principles whose IH is guarded by `List.Forall₂`, e.g. + arising from mutual inductive types relating two lists element-wise. -/ +@[rocq_alias into_ih_Forall2] +instance intoIH_listForall₂ [BI PROP] (φ : α → β → Prop) (l1 : List α) (l2 : List β) + (P : PROP) (Φ : α → β → PROP) + [h : ∀ x1 x2, IntoIH (φ x1 x2) P (Φ x1 x2)] : + IntoIH (List.Forall₂ φ l1 l2) P (bigSepL2 (fun _ x1 x2 => iprop(□ Φ x1 x2)) l1 l2) where + into_ih := sorry From aa5f430f0706e30bc8fb14b46039ffc7ed4bc475 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 30 May 2026 21:42:38 +0200 Subject: [PATCH 027/181] Finish proof for intoIH_listForall --- Iris/Iris/ProofMode/Instances.lean | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Iris/Iris/ProofMode/Instances.lean b/Iris/Iris/ProofMode/Instances.lean index b6a8e63a7..e6090b814 100644 --- a/Iris/Iris/ProofMode/Instances.lean +++ b/Iris/Iris/ProofMode/Instances.lean @@ -952,7 +952,20 @@ instance intoIH_imp [BI PROP] (φ ψ : Prop) (Δ P Q : PROP) instance intoIH_listForall [BI PROP] (φ : α → Bool) (l : List α) (P : PROP) (Φ : α → PROP) [h : ∀ x, IntoIH (φ x) P (Φ x)] : IntoIH (l.all φ) P (bigSepL (fun _ a => iprop(□ Φ a)) l) where - into_ih := sorry + into_ih := by + intro h1 + induction l generalizing Φ with + | nil => + simp [affine] + | cons x xs ih => + simp [List.all, bigSepL] at h1 ⊢ + rcases h1 with ⟨hx, hxs⟩ + apply intuitionistically_sep_idem.mpr.trans + refine sep_mono ?_ ?_ + · exact intuitionistically_intro' ((h x).into_ih hx) + · apply ih + apply (List.all_eq_true.mpr) + apply hxs /-- Support for induction principles whose IH is guarded by `List.Forall₂`, e.g. arising from mutual inductive types relating two lists element-wise. -/ From eab9ccf4d4870008a06e415385dd9d99cce96f36 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 30 May 2026 21:46:29 +0200 Subject: [PATCH 028/181] Finish proof for intoIH_listForall2 --- Iris/Iris/ProofMode/Instances.lean | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Iris/Iris/ProofMode/Instances.lean b/Iris/Iris/ProofMode/Instances.lean index e6090b814..5e309e3d8 100644 --- a/Iris/Iris/ProofMode/Instances.lean +++ b/Iris/Iris/ProofMode/Instances.lean @@ -974,4 +974,13 @@ instance intoIH_listForall₂ [BI PROP] (φ : α → β → Prop) (l1 : List α) (P : PROP) (Φ : α → β → PROP) [h : ∀ x1 x2, IntoIH (φ x1 x2) P (Φ x1 x2)] : IntoIH (List.Forall₂ φ l1 l2) P (bigSepL2 (fun _ x1 x2 => iprop(□ Φ x1 x2)) l1 l2) where - into_ih := sorry + into_ih := by + intro h + induction h with + | nil => simp [bigSepL2, affine] + | cons x xs ih => + simp [bigSepL2] at ⊢ + apply intuitionistically_sep_idem.mpr.trans + refine sep_mono ?_ ?_ + · exact intuitionistically_intro' ((h _ _).into_ih x) + · exact ih From b6b03a10f4c03803caffd5572ef977bf887c19da Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 30 May 2026 22:36:06 +0200 Subject: [PATCH 029/181] Code refactoring and comments --- Iris/Iris/ProofMode/Tactics/Induction.lean | 108 +++++++++++++-------- 1 file changed, 66 insertions(+), 42 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 6e0bcfd4d..f57c02677 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -17,6 +17,37 @@ namespace Iris.ProofMode public meta section open BI Std Lean Elab Tactic Meta Qq +/-- + The `iinduction` tactic user provides the proof for the induction subgoal, + which has hypotheses introduced into the Iris proof state. + + Given this proof, obtain the proof where the hypotheses are not yet + introduced into the Iris proof state. +-/ +@[rocq_alias tac_revert_ih] +theorem revert_IH [BI PROP] {P Q goal : PROP} {φ} + (ih : φ) + (inst : IntoIH φ P Q) + (h1 : P ⊢ □ P) + (h2 : P ∗ □ Q ⊢ goal) : P ⊢ goal := calc + P ⊢ □ P := h1 + _ ⊢ □ P ∗ □ P := intuitionistically_sep_dup.mp + _ ⊢ □ P ∗ □ □ P := sep_mono_r intuitionistically_idem.mpr + _ ⊢ □ P ∗ □ Q := sep_mono_r <| intuitionistically_mono <| inst.into_ih ih + _ ⊢ P ∗ □ Q := sep_mono_l intuitionistically_elim + _ ⊢ goal := h2 + +/-- + Given two intuitionistic propositions, their combined proposition + remains intuitionistic. +-/ +theorem combine_intuitionistic_props [BI PROP] {P Q : PROP} + (h1 : P ⊢ □ P) : + P ∗ □ Q ⊢ □ (P ∗ □ Q) := calc + _ ⊢ □ P ∗ □ Q := sep_mono_l h1 + _ ⊢ □ P ∗ □ □ Q := sep_mono_r intuitionistically_idem.mpr + _ ⊢ □ (P ∗ □ Q) := intuitionistically_sep_2 + /-- Given a collection of hypotheses (`hyps`) and a free variable `fvar`, return the subset of intuitionistic hypotheses in `hyps` that contains the `fvar` @@ -58,33 +89,6 @@ private def findIHs (m : MVarId) : MetaM (List FVarId) := ihs := decl.fvarId :: ihs return ihs.reverse -/-- - The `iinduction` tactic user provides the proof for the induction subgoal, - which has hypotheses introduced into the Iris proof state. - - Given this proof, obtain the proof where the hypotheses are not yet - introduced into the Iris proof state. --/ -@[rocq_alias tac_revert_ih] -theorem revert_IH [BI PROP] {P Q goal : PROP} {φ} - (ih : φ) - (inst : IntoIH φ P Q) - (h1 : P ⊢ □ P) - (h2 : P ∗ □ Q ⊢ goal) : P ⊢ goal := calc - P ⊢ □ P := h1 - _ ⊢ □ P ∗ □ P := intuitionistically_sep_dup.mp - _ ⊢ □ P ∗ □ □ P := sep_mono_r intuitionistically_idem.mpr - _ ⊢ □ P ∗ □ Q := sep_mono_r <| intuitionistically_mono <| inst.into_ih ih - _ ⊢ P ∗ □ Q := sep_mono_l intuitionistically_elim - _ ⊢ goal := h2 - -theorem stay_intuitionistic [BI PROP] {P Q : PROP} - (h1 : P ⊢ □ P) : - P ∗ □ Q ⊢ □ (P ∗ □ Q) := calc - _ ⊢ □ P ∗ □ Q := sep_mono_l h1 - _ ⊢ □ P ∗ □ □ Q := sep_mono_r intuitionistically_idem.mpr - _ ⊢ □ (P ∗ □ Q) := intuitionistically_sep_2 - /-- Designed to be a mutable state such that `newHyps` contains induction hypotheses generated by Lean's built-in induction. @@ -94,7 +98,7 @@ private structure InductionState {u} {prop : Q(Type u)} {bi} (origE goal : Q($pr {newE : Q($prop)} (newHyps : Hyps bi newE) -- Proof that the hypotheses is intuitionsitic - (pfAux : Q($newE ⊢ □ $newE)) + (pfIntHyps : Q($newE ⊢ □ $newE)) (pf : Q(($newE ⊢ $goal) → $origE ⊢ $goal)) /-- @@ -113,37 +117,34 @@ private def addIntoIH {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e goal} let some inst ← ProofModeM.trySynthInstanceQ q(IntoIH $φ $oldE $Q) | throwError "iinduction: type class synthesis with IntoIH failed" + -- Introduce the induction hypothesis into the intuitionistic context let nameIdent := mkIdent <| ← hFVar.getUserName let binderIdent ← `(binderIdent| $nameIdent:ident) let ⟨_, newHyps⟩ ← Hyps.addWithInfo bi binderIdent q(true) Q oldHyps - let a := q(revert_IH $p $inst (goal := $goal) $st.pfAux) + let a := q(revert_IH $p $inst (goal := $goal) $st.pfIntHyps) return { newHyps, pf := q(fun pf => $st.pf <| $a pf), - pfAux := q(stay_intuitionistic $st.pfAux) + pfIntHyps := q(combine_intuitionistic_props $st.pfIntHyps) } -/-- - This should be true in `addAllIHs` because all spatial hypotheses have - been reverted, so the Iris proof state contains no hypothesis in the - spatial context. -/ -theorem dummy [BI PROP] {e : PROP} : e ⊢ □ e := sorry - /-- Introduce a list of induction hypotheses into Iris proof state. The list of `FVarId` values (`ihFVars`) should be the list returned by `findIHs`. -/ private def addAllIHs {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e goal : Q($prop)} - (hyps : Hyps bi e) (ihFVars : List FVarId) : + (pfIntHyps : Q($e ⊢ □ $e)) (hyps : Hyps bi e) (ihFVars : List FVarId) : ProofModeM (@InductionState u prop bi e goal) := do + -- Initialise the mutable instance of `InductionState` let mut st : InductionState e goal := { - newHyps := hyps, pf := q(id), pfAux := q(dummy) + newHyps := hyps, pf := q(id), pfIntHyps } + -- Iteratively move the induction hypotheses into the intuitionistic context for i in ihFVars do let p := mkFVar i let φ ← inferType (mkFVar i) @@ -151,6 +152,26 @@ private def addAllIHs {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e goal : Q($pro return st +/-- + Given hypothesis `hyps` representing `e` where every hypothesis exist in the + intuitionistic context, `e ⊢ □ e` must hold. +-/ +private def buildPfIntHyps {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} + (hyps : Hyps bi e) : ProofModeM Q($e ⊢ □ $e) := + match hyps with + | .emp _ => + pure q(intuitionistically_emp.mpr) + | .hyp _ _ _ p _ _ => + match matchBool p with + | .inl _ => + pure q(intuitionistically_idem.mpr) + | .inr _ => + throwError "iinduction: spatial context is not empty after reverting hypotheses" + | .sep _ _ _ _ lhs rhs => do + let pfL ← buildPfIntHyps lhs + let pfR ← buildPfIntHyps rhs + pure q((sep_mono $pfL $pfR).trans intuitionistically_sep_2) + elab "iinduction" colGt x:ident : tactic => do -- Get the ID of the variable on which induction is being performed let fvar ← getFVarId x @@ -168,6 +189,7 @@ elab "iinduction" colGt x:ident : tactic => do | _ => throwError "iinduction: {indName} is not inductive" | _ => throwError "iinduction: unable to determine inductive type" + -- TODO: generalisation for other inductive data structures let varNames : Array AltVarNames := #[ { explicit := true, varNames := [] }, { explicit := true, varNames := [x.getId, `IH] } @@ -184,7 +206,7 @@ elab "iinduction" colGt x:ident : tactic => do let subgoals ← m.mvarId!.withContext do m.mvarId!.induction fvar recName varNames - -- Handle each subgoal + -- Handle each subgoal generated by Lean's induction for s in subgoals do s.mvarId.withContext do let sType ← instantiateMVars (← s.mvarId.getType) @@ -196,12 +218,14 @@ elab "iinduction" colGt x:ident : tactic => do -- Find the induction hypotheses generated by Lean's `induction` tactic let ihFVars ← findIHs s.mvarId + -- A proof that all hypotheses in the Iris goal exist in the intuitionistic context + let pfIntHyps ← buildPfIntHyps irisGoal.hyps + -- Introduce the induction hypothesis back into the Iris proof state - let st ← addAllIHs irisGoal.hyps ihFVars (goal := irisGoal.goal) + let st ← addAllIHs pfIntHyps irisGoal.hyps ihFVars (goal := irisGoal.goal) + -- Remove the induction hypotheses from the regular Lean context let pf' ← withoutFVars (u := 0) ihFVars.toArray <| k st.newHyps irisGoal.goal - - -- Remove the IHs from the regular context s.mvarId.assign <| q($st.pf $pf') return m From 783ad437e2d42012ee0179d18cb707b7ed18659b Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 30 May 2026 22:42:47 +0200 Subject: [PATCH 030/181] Remove goal from InductionState, simplifications --- Iris/Iris/ProofMode/Tactics/Induction.lean | 33 ++++++++++------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index f57c02677..0104acb5a 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -25,17 +25,16 @@ open BI Std Lean Elab Tactic Meta Qq introduced into the Iris proof state. -/ @[rocq_alias tac_revert_ih] -theorem revert_IH [BI PROP] {P Q goal : PROP} {φ} +theorem revert_IH [BI PROP] {P Q : PROP} {φ} (ih : φ) (inst : IntoIH φ P Q) - (h1 : P ⊢ □ P) - (h2 : P ∗ □ Q ⊢ goal) : P ⊢ goal := calc + (h1 : P ⊢ □ P) : + P ⊢ P ∗ □ Q := calc P ⊢ □ P := h1 _ ⊢ □ P ∗ □ P := intuitionistically_sep_dup.mp _ ⊢ □ P ∗ □ □ P := sep_mono_r intuitionistically_idem.mpr _ ⊢ □ P ∗ □ Q := sep_mono_r <| intuitionistically_mono <| inst.into_ih ih _ ⊢ P ∗ □ Q := sep_mono_l intuitionistically_elim - _ ⊢ goal := h2 /-- Given two intuitionistic propositions, their combined proposition @@ -93,23 +92,23 @@ private def findIHs (m : MVarId) : MetaM (List FVarId) := Designed to be a mutable state such that `newHyps` contains induction hypotheses generated by Lean's built-in induction. -/ -private structure InductionState {u} {prop : Q(Type u)} {bi} (origE goal : Q($prop)) where +private structure InductionState {u} {prop : Q(Type u)} {bi} (origE : Q($prop)) where -- Hypotheses in the latest Iris proof goal {newE : Q($prop)} (newHyps : Hyps bi newE) -- Proof that the hypotheses is intuitionsitic (pfIntHyps : Q($newE ⊢ □ $newE)) - (pf : Q(($newE ⊢ $goal) → $origE ⊢ $goal)) + (pf : Q($origE ⊢ $newE)) /-- Used for every induction hypothesis generated by Lean upon using the built-in `induction` tactic. The type class `IntoIH` is used for obtaining the corresponding proposition in the Iris proof mode. -/ -private def addIntoIH {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e goal} - (st : @InductionState u prop bi e goal) +private def addIntoIH {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} + (st : @InductionState u prop bi e) (φ : Q(Prop)) (p : Q($φ)) (hFVar : FVarId) : - ProofModeM (@InductionState u prop bi e goal) := do + ProofModeM (@InductionState u prop bi e) := do let oldE := st.newE let oldHyps : Hyps bi oldE := st.newHyps @@ -122,11 +121,9 @@ private def addIntoIH {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e goal} let binderIdent ← `(binderIdent| $nameIdent:ident) let ⟨_, newHyps⟩ ← Hyps.addWithInfo bi binderIdent q(true) Q oldHyps - let a := q(revert_IH $p $inst (goal := $goal) $st.pfIntHyps) - return { newHyps, - pf := q(fun pf => $st.pf <| $a pf), + pf := q($(st.pf).trans <| revert_IH $p $inst $st.pfIntHyps), pfIntHyps := q(combine_intuitionistic_props $st.pfIntHyps) } @@ -135,13 +132,13 @@ private def addIntoIH {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e goal} The list of `FVarId` values (`ihFVars`) should be the list returned by `findIHs`. -/ -private def addAllIHs {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e goal : Q($prop)} +private def addAllIHs {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e : Q($prop)} (pfIntHyps : Q($e ⊢ □ $e)) (hyps : Hyps bi e) (ihFVars : List FVarId) : - ProofModeM (@InductionState u prop bi e goal) := do + ProofModeM (@InductionState u prop bi e) := do -- Initialise the mutable instance of `InductionState` - let mut st : InductionState e goal := { - newHyps := hyps, pf := q(id), pfIntHyps + let mut st : InductionState e := { + newHyps := hyps, pf := q(.rfl), pfIntHyps } -- Iteratively move the induction hypotheses into the intuitionistic context @@ -222,11 +219,11 @@ elab "iinduction" colGt x:ident : tactic => do let pfIntHyps ← buildPfIntHyps irisGoal.hyps -- Introduce the induction hypothesis back into the Iris proof state - let st ← addAllIHs pfIntHyps irisGoal.hyps ihFVars (goal := irisGoal.goal) + let st ← addAllIHs pfIntHyps irisGoal.hyps ihFVars -- Remove the induction hypotheses from the regular Lean context let pf' ← withoutFVars (u := 0) ihFVars.toArray <| k st.newHyps irisGoal.goal - s.mvarId.assign <| q($st.pf $pf') + s.mvarId.assign <| q($(st.pf).trans $pf') return m ) From 31e5633ac1d768f7ede237ad6ef0802596ed6707 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sun, 31 May 2026 14:31:27 +0200 Subject: [PATCH 031/181] Add test for iinduction --- Iris/Iris/Tests/Tactics.lean | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 4d26742e9..f721334e1 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2718,14 +2718,8 @@ end iloeb section iinduction -example [BI PROP] {P Q : PROP} {n : Nat} : - ⊢ P -∗ ⌜n = 0⌝ -∗ Q -∗ ⌜n ≠ 1⌝ -∗ P ∗ Q ∗ ⌜n = n⌝ := by - iintro H1 H2 H3 #H4 - iinduction n - · iframe - · iframe - itrivial - +/-- Tests `iinduction` with induction on natural numbers. Hypotheses in the + spatial context become premises of the induction hypothesis. -/ example [BI PROP] {P Q R S T : PROP} {n : Nat} : ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by iintro HP #HQ #HR HS #HT @@ -2734,4 +2728,10 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : · iframe itrivial +/- Tests `iinduction` with a non-inductive datatype -/ +/-- error: iinduction: unable to determine inductive type -/ +#guard_msgs in +example [BI PROP] {P : PROP} : ⊢ P := by + iinduction P + end iinduction From bfd3e59b90655828f5fd486e79ac62d9ce6f7b2c Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sun, 31 May 2026 15:00:14 +0200 Subject: [PATCH 032/181] Obtain constructor names from inductInfo: towards allowing user-given variable and IH names --- Iris/Iris/ProofMode/Tactics/Induction.lean | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 0104acb5a..4878cb04b 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -179,10 +179,10 @@ elab "iinduction" colGt x:ident : tactic => do -- Find the recursor name for induction let fvarType ← whnf <| ← inferType <| mkFVar fvar - let recName : Name ← match fvarType.getAppFn with + let ⟨recName, ctors⟩ ← match fvarType.getAppFn with | .const indName _ => match (← getEnv).find? indName with - | some (.inductInfo val) => pure <| Name.mkStr val.name "recOn" + | some (.inductInfo val) => pure <| (Name.mkStr val.name "recOn", val.ctors) | _ => throwError "iinduction: {indName} is not inductive" | _ => throwError "iinduction: unable to determine inductive type" From e5c7a22823df5f23e4a0dac5b56838ce5edbde31 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sun, 31 May 2026 15:33:26 +0200 Subject: [PATCH 033/181] Towards implementing the `with` syntax --- Iris/Iris/ProofMode/Tactics/Induction.lean | 60 +++++++++++++++++++--- Iris/Iris/Tests/Tactics.lean | 19 +++++++ 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 4878cb04b..b44df8796 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -169,10 +169,33 @@ private def buildPfIntHyps {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} let pfR ← buildPfIntHyps rhs pure q((sep_mono $pfL $pfR).trans intuitionistically_sep_2) -elab "iinduction" colGt x:ident : tactic => do +-- One case upon applying the `iinduction` tactic +syntax iinductionAlt := "| " ident ident* " => " tacticSeq + +private def parseIInductionAlt (alt : TSyntax `Iris.ProofMode.iinductionAlt) : + TacticM (Name × Array Name × TSyntax `Lean.Parser.Tactic.tacticSeq) := do + let `(iinductionAlt| | $ctor:ident $vars:ident* => $tac:tacticSeq) := alt + | throwError "iinduction: invalid syntax" + + return (ctor.getId, vars.map (·.getId), tac) + +/-- Check whether a fully-qualified constructor name (e.g. `Nat.succ`) matches a + user-written short name (e.g. `succ`) or an already-qualified name. -/ +private def matchesCtorName (fullName : Name) (userShort : Name) : Bool := + fullName == userShort || fullName.getString! == userShort.getString! + + +private def throwMissingAlt {α} (ctor : Name) : ProofModeM α := + throwError "iinduction: case '{ctor.getString!}' is not handled" + + +elab "iinduction" colGt x:ident alts:(colGe iinductionAlt)* : tactic => do -- Get the ID of the variable on which induction is being performed let fvar ← getFVarId x + -- Parse the list of cases supplied by the user + let parsedAlts ← alts.mapM parseIInductionAlt + ProofModeM.runTactic λ mvar { hyps, goal, .. } => do -- Find all hypotheses that contain the variable being performed induction on let targets := iHypsContaining hyps fvar @@ -187,10 +210,14 @@ elab "iinduction" colGt x:ident : tactic => do | _ => throwError "iinduction: unable to determine inductive type" -- TODO: generalisation for other inductive data structures - let varNames : Array AltVarNames := #[ - { explicit := true, varNames := [] }, - { explicit := true, varNames := [x.getId, `IH] } - ] + let varNames : Array AltVarNames ← if parsedAlts.isEmpty then + pure <| ctors.toArray.map (fun _ => {explicit := true, varNames := []}) + else do + ctors.toArray.mapM fun ctor => + match parsedAlts.find? (fun (altCtor, _, _) => matchesCtorName ctor altCtor) with + | some (_, varNms, _) => + pure ({ explicit := true, varNames := varNms.toList } : AltVarNames) + | none => throwMissingAlt ctor -- Revert all hypotheses in the list let pf ← iRevertIntro hyps goal targets ( @@ -204,7 +231,10 @@ elab "iinduction" colGt x:ident : tactic => do m.mvarId!.induction fvar recName varNames -- Handle each subgoal generated by Lean's induction - for s in subgoals do + for (s, ctor) in subgoals.toList.zip ctors do + if subgoals.size != ctors.length then + throwError "iinduction: expected {ctors.length} subgoals for {ctors.length} constructors, got {subgoals.size}" + s.mvarId.withContext do let sType ← instantiateMVars (← s.mvarId.getType) @@ -223,8 +253,26 @@ elab "iinduction" colGt x:ident : tactic => do -- Remove the induction hypotheses from the regular Lean context let pf' ← withoutFVars (u := 0) ihFVars.toArray <| k st.newHyps irisGoal.goal + s.mvarId.assign <| q($(st.pf).trans $pf') + -- Case applicable for `with` syntax only + if !parsedAlts.isEmpty then + let biGoalMVar := (← getThe ProofModeM.State).goals.back! + modify (fun s => { s with goals := s.goals.pop }) + + let some (_, _, userTac) := + parsedAlts.find? (fun (altCtor, _, _) => matchesCtorName ctor altCtor) + | throwMissingAlt ctor + + -- Run the user tactic on the BI goal + let savedGoals ← getGoals + setGoals [biGoalMVar] + evalTactic userTac + let remaining ← getGoals + setGoals savedGoals + for r in remaining do addMVarGoal r + return m ) diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index f721334e1..aebe1462a 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2728,10 +2728,29 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : · iframe itrivial +/-- Tests `iinduction` with induction on natural numbers with user-supplied + names for variables and induction hypotheses. -/ +example [BI PROP] {P Q R S T : PROP} {n : Nat} : + ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by + iintro HP #HQ #HR HS #HT + iinduction n + | zero => itrivial + | succ n IH => iframe; itrivial + | aa => sorry + /- Tests `iinduction` with a non-inductive datatype -/ /-- error: iinduction: unable to determine inductive type -/ #guard_msgs in example [BI PROP] {P : PROP} : ⊢ P := by iinduction P +example [BI PROP] {P Q R S T : PROP} {n : Nat} : + ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by + iintro HP #HQ #HR HS #HT + induction n with + | zero => sorry + | succ aa bb => sorry + + + end iinduction From 7d1cb7c168f3c950bc4cfe1ed2b310119ba66d02 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sun, 31 May 2026 16:11:25 +0200 Subject: [PATCH 034/181] Pretty variables for cases in the `with` syntax --- Iris/Iris/ProofMode/Tactics/Induction.lean | 26 +++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index b44df8796..59561174a 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -173,11 +173,11 @@ private def buildPfIntHyps {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} syntax iinductionAlt := "| " ident ident* " => " tacticSeq private def parseIInductionAlt (alt : TSyntax `Iris.ProofMode.iinductionAlt) : - TacticM (Name × Array Name × TSyntax `Lean.Parser.Tactic.tacticSeq) := do + TacticM (Name × Array (TSyntax `ident) × TSyntax `Lean.Parser.Tactic.tacticSeq) := do let `(iinductionAlt| | $ctor:ident $vars:ident* => $tac:tacticSeq) := alt | throwError "iinduction: invalid syntax" - return (ctor.getId, vars.map (·.getId), tac) + return (ctor.getId, vars, tac) /-- Check whether a fully-qualified constructor name (e.g. `Nat.succ`) matches a user-written short name (e.g. `succ`) or an already-qualified name. -/ @@ -210,13 +210,14 @@ elab "iinduction" colGt x:ident alts:(colGe iinductionAlt)* : tactic => do | _ => throwError "iinduction: unable to determine inductive type" -- TODO: generalisation for other inductive data structures - let varNames : Array AltVarNames ← if parsedAlts.isEmpty then + let varNames : Array AltVarNames ← + if parsedAlts.isEmpty then pure <| ctors.toArray.map (fun _ => {explicit := true, varNames := []}) else do ctors.toArray.mapM fun ctor => match parsedAlts.find? (fun (altCtor, _, _) => matchesCtorName ctor altCtor) with - | some (_, varNms, _) => - pure ({ explicit := true, varNames := varNms.toList } : AltVarNames) + | some (_, vars, _) => + pure ({ explicit := true, varNames := vars.toList.map (·.getId) } : AltVarNames) | none => throwMissingAlt ctor -- Revert all hypotheses in the list @@ -236,6 +237,21 @@ elab "iinduction" colGt x:ident alts:(colGe iinductionAlt)* : tactic => do throwError "iinduction: expected {ctors.length} subgoals for {ctors.length} constructors, got {subgoals.size}" s.mvarId.withContext do + + if !parsedAlts.isEmpty then + + + match parsedAlts.find? (fun (altCtor, _, _) => matchesCtorName ctor altCtor) with + | some (_, vars, _) => + for (fieldFVar, varStx) in s.fields.toList.zip vars.toList do + let lctx ← getLCtx + let fieldType ← inferType fieldFVar + addLocalVarInfo varStx lctx fieldFVar (some fieldType) (isBinder := true) + + | none => throwMissingAlt ctor + + + let sType ← instantiateMVars (← s.mvarId.getType) let some irisGoal := parseIrisGoal? sType From b826e7f21a37e46871a1d52ac977b407d0f57044 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sun, 31 May 2026 17:20:01 +0200 Subject: [PATCH 035/181] Check for invalid alternative names and throw pretty error --- Iris/Iris/ProofMode/Tactics/Induction.lean | 25 +++++++++++---------- Iris/Iris/Tests/Tactics.lean | 26 ++++++++++++++++------ 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 59561174a..60bb0e710 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -184,10 +184,17 @@ private def parseIInductionAlt (alt : TSyntax `Iris.ProofMode.iinductionAlt) : private def matchesCtorName (fullName : Name) (userShort : Name) : Bool := fullName == userShort || fullName.getString! == userShort.getString! - private def throwMissingAlt {α} (ctor : Name) : ProofModeM α := - throwError "iinduction: case '{ctor.getString!}' is not handled" + throwError "iinduction: alternative `{ctor.getString!}` has not been provided" + +private def checkCtors (ctors altCtors : List Name) : MetaM Unit := do + let invalidAltCtors := + altCtors.filter fun alt => + not <| ctors.any fun ctor => matchesCtorName ctor alt + if !invalidAltCtors.isEmpty then + let invalids := String.intercalate ", " <| invalidAltCtors.map (s!"`{·}`") + throwError m!"iinduction: invalid alternative names {invalids}" elab "iinduction" colGt x:ident alts:(colGe iinductionAlt)* : tactic => do -- Get the ID of the variable on which induction is being performed @@ -209,7 +216,7 @@ elab "iinduction" colGt x:ident alts:(colGe iinductionAlt)* : tactic => do | _ => throwError "iinduction: {indName} is not inductive" | _ => throwError "iinduction: unable to determine inductive type" - -- TODO: generalisation for other inductive data structures + -- Define the names for variables and induction hypotheses if supplied by user let varNames : Array AltVarNames ← if parsedAlts.isEmpty then pure <| ctors.toArray.map (fun _ => {explicit := true, varNames := []}) @@ -231,27 +238,21 @@ elab "iinduction" colGt x:ident alts:(colGe iinductionAlt)* : tactic => do let subgoals ← m.mvarId!.withContext do m.mvarId!.induction fvar recName varNames + if !parsedAlts.isEmpty then + checkCtors ctors <| parsedAlts.toList.map (fun a => match a with | (aa, _, _) => aa) + -- Handle each subgoal generated by Lean's induction for (s, ctor) in subgoals.toList.zip ctors do - if subgoals.size != ctors.length then - throwError "iinduction: expected {ctors.length} subgoals for {ctors.length} constructors, got {subgoals.size}" - s.mvarId.withContext do - if !parsedAlts.isEmpty then - - match parsedAlts.find? (fun (altCtor, _, _) => matchesCtorName ctor altCtor) with | some (_, vars, _) => for (fieldFVar, varStx) in s.fields.toList.zip vars.toList do let lctx ← getLCtx let fieldType ← inferType fieldFVar addLocalVarInfo varStx lctx fieldFVar (some fieldType) (isBinder := true) - | none => throwMissingAlt ctor - - let sType ← instantiateMVars (← s.mvarId.getType) let some irisGoal := parseIrisGoal? sType diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index aebe1462a..b92cd4b1d 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2734,9 +2734,8 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by iintro HP #HQ #HR HS #HT iinduction n - | zero => itrivial - | succ n IH => iframe; itrivial - | aa => sorry + | Nat.zero => itrivial -- Using the full name of the constructor + | succ n IH => iframe; itrivial -- Using the short name of the constructor /- Tests `iinduction` with a non-inductive datatype -/ /-- error: iinduction: unable to determine inductive type -/ @@ -2744,13 +2743,26 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : example [BI PROP] {P : PROP} : ⊢ P := by iinduction P +/- Tests `iinduction` with induction on natural numbers with user-supplied + names, missing `succ` case -/ +/-- error: iinduction: alternative `succ` has not been provided -/ +#guard_msgs in example [BI PROP] {P Q R S T : PROP} {n : Nat} : ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by iintro HP #HQ #HR HS #HT - induction n with - | zero => sorry - | succ aa bb => sorry - + iinduction n + | zero => itrivial +/- Tests `iinduction` with induction on natural numbers with user-supplied + names, missing `succ` case -/ +/-- error: iinduction: invalid alternative names `a` -/ +#guard_msgs in +example [BI PROP] {P Q R S T : PROP} {n : Nat} : + ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by + iintro HP #HQ #HR HS #HT + iinduction n + | zero => itrivial + | a => itrivial + | succ n IH => iframe; itrivial end iinduction From 43b82159c57b221c53ae3b26047b1bd0fac42dc2 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sun, 31 May 2026 17:24:46 +0200 Subject: [PATCH 036/181] Move most of elaboration code into iInductionCore for reusability --- Iris/Iris/ProofMode/Tactics/Induction.lean | 185 +++++++++++---------- 1 file changed, 96 insertions(+), 89 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 60bb0e710..d86603358 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -196,6 +196,101 @@ private def checkCtors (ctors altCtors : List Name) : MetaM Unit := do let invalids := String.intercalate ", " <| invalidAltCtors.map (s!"`{·}`") throwError m!"iinduction: invalid alternative names {invalids}" +private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} + (hyps : Hyps bi e) (goal : Q($prop)) (fvar : FVarId) + (parsedAlts : Array (Name × Array (TSyntax `ident) × TSyntax `Lean.Parser.Tactic.tacticSeq)) : + ProofModeM Q($e ⊢ $goal) := do + -- Find all hypotheses that contain the variable being performed induction on + let targets := iHypsContaining hyps fvar + + -- Find the recursor name for induction + let fvarType ← whnf <| ← inferType <| mkFVar fvar + let ⟨recName, ctors⟩ ← match fvarType.getAppFn with + | .const indName _ => + match (← getEnv).find? indName with + | some (.inductInfo val) => pure <| (Name.mkStr val.name "recOn", val.ctors) + | _ => throwError "iinduction: {indName} is not inductive" + | _ => throwError "iinduction: unable to determine inductive type" + + -- Define the names for variables and induction hypotheses if supplied by user + let varNames : Array AltVarNames ← + if parsedAlts.isEmpty then + pure <| ctors.toArray.map (fun _ => {explicit := true, varNames := []}) + else do + ctors.toArray.mapM fun ctor => + match parsedAlts.find? (fun (altCtor, _, _) => matchesCtorName ctor altCtor) with + | some (_, vars, _) => + pure ({ explicit := true, varNames := vars.toList.map (·.getId) } : AltVarNames) + | none => throwMissingAlt ctor + + -- Revert all hypotheses in the list + let pf ← iRevertIntro hyps goal targets ( + -- The function takes the hypotheses and goal after reverting + fun hyps' goal' k => do + -- Create a new metavariable for the proof goal upon reverting hypotheses + let m ← mkBIGoal hyps' goal' + + -- Use built-in induction in Lean to generate the subgoals for induction + let subgoals ← m.mvarId!.withContext do + m.mvarId!.induction fvar recName varNames + + if !parsedAlts.isEmpty then + checkCtors ctors <| parsedAlts.toList.map (fun a => match a with | (aa, _, _) => aa) + + -- Handle each subgoal generated by Lean's induction + for (s, ctor) in subgoals.toList.zip ctors do + s.mvarId.withContext do + if !parsedAlts.isEmpty then + match parsedAlts.find? (fun (altCtor, _, _) => matchesCtorName ctor altCtor) with + | some (_, vars, _) => + for (fieldFVar, varStx) in s.fields.toList.zip vars.toList do + let lctx ← getLCtx + let fieldType ← inferType fieldFVar + addLocalVarInfo varStx lctx fieldFVar (some fieldType) (isBinder := true) + | none => throwMissingAlt ctor + + let sType ← instantiateMVars (← s.mvarId.getType) + + let some irisGoal := parseIrisGoal? sType + -- This should not happen + | throwError "iinduction: fail to parse induction subgoal" + + -- Find the induction hypotheses generated by Lean's `induction` tactic + let ihFVars ← findIHs s.mvarId + + -- A proof that all hypotheses in the Iris goal exist in the intuitionistic context + let pfIntHyps ← buildPfIntHyps irisGoal.hyps + + -- Introduce the induction hypothesis back into the Iris proof state + let st ← addAllIHs pfIntHyps irisGoal.hyps ihFVars + + -- Remove the induction hypotheses from the regular Lean context + let pf' ← withoutFVars (u := 0) ihFVars.toArray <| k st.newHyps irisGoal.goal + + s.mvarId.assign <| q($(st.pf).trans $pf') + + -- Case applicable for `with` syntax only + if !parsedAlts.isEmpty then + let biGoalMVar := (← getThe ProofModeM.State).goals.back! + modify (fun s => { s with goals := s.goals.pop }) + + let some (_, _, userTac) := + parsedAlts.find? (fun (altCtor, _, _) => matchesCtorName ctor altCtor) + | throwMissingAlt ctor + + -- Run the user tactic on the BI goal + let savedGoals ← getGoals + setGoals [biGoalMVar] + evalTactic userTac + let remaining ← getGoals + setGoals savedGoals + for r in remaining do addMVarGoal r + + return m + ) + + return pf + elab "iinduction" colGt x:ident alts:(colGe iinductionAlt)* : tactic => do -- Get the ID of the variable on which induction is being performed let fvar ← getFVarId x @@ -204,93 +299,5 @@ elab "iinduction" colGt x:ident alts:(colGe iinductionAlt)* : tactic => do let parsedAlts ← alts.mapM parseIInductionAlt ProofModeM.runTactic λ mvar { hyps, goal, .. } => do - -- Find all hypotheses that contain the variable being performed induction on - let targets := iHypsContaining hyps fvar - - -- Find the recursor name for induction - let fvarType ← whnf <| ← inferType <| mkFVar fvar - let ⟨recName, ctors⟩ ← match fvarType.getAppFn with - | .const indName _ => - match (← getEnv).find? indName with - | some (.inductInfo val) => pure <| (Name.mkStr val.name "recOn", val.ctors) - | _ => throwError "iinduction: {indName} is not inductive" - | _ => throwError "iinduction: unable to determine inductive type" - - -- Define the names for variables and induction hypotheses if supplied by user - let varNames : Array AltVarNames ← - if parsedAlts.isEmpty then - pure <| ctors.toArray.map (fun _ => {explicit := true, varNames := []}) - else do - ctors.toArray.mapM fun ctor => - match parsedAlts.find? (fun (altCtor, _, _) => matchesCtorName ctor altCtor) with - | some (_, vars, _) => - pure ({ explicit := true, varNames := vars.toList.map (·.getId) } : AltVarNames) - | none => throwMissingAlt ctor - - -- Revert all hypotheses in the list - let pf ← iRevertIntro hyps goal targets ( - -- The function takes the hypotheses and goal after reverting - fun hyps' goal' k => do - -- Create a new metavariable for the proof goal upon reverting hypotheses - let m ← mkBIGoal hyps' goal' - - -- Use built-in induction in Lean to generate the subgoals for induction - let subgoals ← m.mvarId!.withContext do - m.mvarId!.induction fvar recName varNames - - if !parsedAlts.isEmpty then - checkCtors ctors <| parsedAlts.toList.map (fun a => match a with | (aa, _, _) => aa) - - -- Handle each subgoal generated by Lean's induction - for (s, ctor) in subgoals.toList.zip ctors do - s.mvarId.withContext do - if !parsedAlts.isEmpty then - match parsedAlts.find? (fun (altCtor, _, _) => matchesCtorName ctor altCtor) with - | some (_, vars, _) => - for (fieldFVar, varStx) in s.fields.toList.zip vars.toList do - let lctx ← getLCtx - let fieldType ← inferType fieldFVar - addLocalVarInfo varStx lctx fieldFVar (some fieldType) (isBinder := true) - | none => throwMissingAlt ctor - - let sType ← instantiateMVars (← s.mvarId.getType) - - let some irisGoal := parseIrisGoal? sType - -- This should not happen - | throwError "iinduction: fail to parse induction subgoal" - - -- Find the induction hypotheses generated by Lean's `induction` tactic - let ihFVars ← findIHs s.mvarId - - -- A proof that all hypotheses in the Iris goal exist in the intuitionistic context - let pfIntHyps ← buildPfIntHyps irisGoal.hyps - - -- Introduce the induction hypothesis back into the Iris proof state - let st ← addAllIHs pfIntHyps irisGoal.hyps ihFVars - - -- Remove the induction hypotheses from the regular Lean context - let pf' ← withoutFVars (u := 0) ihFVars.toArray <| k st.newHyps irisGoal.goal - - s.mvarId.assign <| q($(st.pf).trans $pf') - - -- Case applicable for `with` syntax only - if !parsedAlts.isEmpty then - let biGoalMVar := (← getThe ProofModeM.State).goals.back! - modify (fun s => { s with goals := s.goals.pop }) - - let some (_, _, userTac) := - parsedAlts.find? (fun (altCtor, _, _) => matchesCtorName ctor altCtor) - | throwMissingAlt ctor - - -- Run the user tactic on the BI goal - let savedGoals ← getGoals - setGoals [biGoalMVar] - evalTactic userTac - let remaining ← getGoals - setGoals savedGoals - for r in remaining do addMVarGoal r - - return m - ) - + let pf ← iInductionCore hyps goal fvar parsedAlts mvar.assign pf From 48945d99755fa07bb4d1bb2f4bdba443f53b1b40 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sun, 31 May 2026 17:32:39 +0200 Subject: [PATCH 037/181] iInductionCore: alternative names being optional, use pattern matching --- Iris/Iris/ProofMode/Tactics/Induction.lean | 44 ++++++++++++++-------- Iris/Iris/Tests/Tactics.lean | 6 +-- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index d86603358..726894aea 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -198,7 +198,7 @@ private def checkCtors (ctors altCtors : List Name) : MetaM Unit := do private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} (hyps : Hyps bi e) (goal : Q($prop)) (fvar : FVarId) - (parsedAlts : Array (Name × Array (TSyntax `ident) × TSyntax `Lean.Parser.Tactic.tacticSeq)) : + (parsedAlts : Option <| Array <| Name × Array (TSyntax `ident) × TSyntax `Lean.Parser.Tactic.tacticSeq) : ProofModeM Q($e ⊢ $goal) := do -- Find all hypotheses that contain the variable being performed induction on let targets := iHypsContaining hyps fvar @@ -214,14 +214,15 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- Define the names for variables and induction hypotheses if supplied by user let varNames : Array AltVarNames ← - if parsedAlts.isEmpty then - pure <| ctors.toArray.map (fun _ => {explicit := true, varNames := []}) - else do - ctors.toArray.mapM fun ctor => - match parsedAlts.find? (fun (altCtor, _, _) => matchesCtorName ctor altCtor) with - | some (_, vars, _) => - pure ({ explicit := true, varNames := vars.toList.map (·.getId) } : AltVarNames) - | none => throwMissingAlt ctor + match parsedAlts with + | none => + pure <| ctors.toArray.map (fun _ => {explicit := true, varNames := []}) + | some parsedAlts => do + ctors.toArray.mapM fun ctor => + match parsedAlts.find? (fun (altCtor, _, _) => matchesCtorName ctor altCtor) with + | some (_, vars, _) => + pure ({ explicit := true, varNames := vars.toList.map (·.getId) } : AltVarNames) + | none => throwMissingAlt ctor -- Revert all hypotheses in the list let pf ← iRevertIntro hyps goal targets ( @@ -234,13 +235,16 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} let subgoals ← m.mvarId!.withContext do m.mvarId!.induction fvar recName varNames - if !parsedAlts.isEmpty then - checkCtors ctors <| parsedAlts.toList.map (fun a => match a with | (aa, _, _) => aa) + match parsedAlts with + | none => pure () + | some parsedAlts => checkCtors ctors <| parsedAlts.toList.map Prod.fst -- Handle each subgoal generated by Lean's induction for (s, ctor) in subgoals.toList.zip ctors do s.mvarId.withContext do - if !parsedAlts.isEmpty then + match parsedAlts with + | none => pure () + | some parsedAlts => match parsedAlts.find? (fun (altCtor, _, _) => matchesCtorName ctor altCtor) with | some (_, vars, _) => for (fieldFVar, varStx) in s.fields.toList.zip vars.toList do @@ -270,7 +274,9 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} s.mvarId.assign <| q($(st.pf).trans $pf') -- Case applicable for `with` syntax only - if !parsedAlts.isEmpty then + match parsedAlts with + | none => pure () + | some parsedAlts => let biGoalMVar := (← getThe ProofModeM.State).goals.back! modify (fun s => { s with goals := s.goals.pop }) @@ -291,7 +297,15 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} return pf -elab "iinduction" colGt x:ident alts:(colGe iinductionAlt)* : tactic => do +elab "iinduction" colGt x:ident : tactic => do + -- Get the ID of the variable on which induction is being performed + let fvar ← getFVarId x + + ProofModeM.runTactic λ mvar { hyps, goal, .. } => do + let pf ← iInductionCore hyps goal fvar none + mvar.assign pf + +elab "iinduction" colGt x:ident "with" alts:(colGe iinductionAlt)* : tactic => do -- Get the ID of the variable on which induction is being performed let fvar ← getFVarId x @@ -299,5 +313,5 @@ elab "iinduction" colGt x:ident alts:(colGe iinductionAlt)* : tactic => do let parsedAlts ← alts.mapM parseIInductionAlt ProofModeM.runTactic λ mvar { hyps, goal, .. } => do - let pf ← iInductionCore hyps goal fvar parsedAlts + let pf ← iInductionCore hyps goal fvar (some parsedAlts) mvar.assign pf diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index b92cd4b1d..d812dd79e 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2733,7 +2733,7 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : example [BI PROP] {P Q R S T : PROP} {n : Nat} : ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by iintro HP #HQ #HR HS #HT - iinduction n + iinduction n with | Nat.zero => itrivial -- Using the full name of the constructor | succ n IH => iframe; itrivial -- Using the short name of the constructor @@ -2750,7 +2750,7 @@ example [BI PROP] {P : PROP} : ⊢ P := by example [BI PROP] {P Q R S T : PROP} {n : Nat} : ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by iintro HP #HQ #HR HS #HT - iinduction n + iinduction n with | zero => itrivial /- Tests `iinduction` with induction on natural numbers with user-supplied @@ -2760,7 +2760,7 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : example [BI PROP] {P Q R S T : PROP} {n : Nat} : ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by iintro HP #HQ #HR HS #HT - iinduction n + iinduction n with | zero => itrivial | a => itrivial | succ n IH => iframe; itrivial From 7378f8d249746628890900634c8d3b66fbbfd1ee Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sun, 31 May 2026 17:42:21 +0200 Subject: [PATCH 038/181] Code refactoring --- Iris/Iris/ProofMode/Tactics/Induction.lean | 47 ++++++++++++++-------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 726894aea..33cb85475 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -18,11 +18,17 @@ public meta section open BI Std Lean Elab Tactic Meta Qq /-- - The `iinduction` tactic user provides the proof for the induction subgoal, - which has hypotheses introduced into the Iris proof state. + The user of the `iinduction` tactic provides the proof for the induction + subgoal. Induction hypotheses are available for the user in the intuitionistic + context of the Iris proof state. - Given this proof, obtain the proof where the hypotheses are not yet - introduced into the Iris proof state. + Meanwhile, the actual induction subgoal generated by Lean with the `induction` + tactic has induction hypotheses in the regular context. + + Given all hypotheses in the Iris proof state (represented by `P`) exist in + the intuitionistic context (that is, the spatial context is empty), we can + introduce another induction hypothesis generated by Lean into the + intuitionistic context. -/ @[rocq_alias tac_revert_ih] theorem revert_IH [BI PROP] {P Q : PROP} {φ} @@ -47,6 +53,18 @@ theorem combine_intuitionistic_props [BI PROP] {P Q : PROP} _ ⊢ □ P ∗ □ □ Q := sep_mono_r intuitionistically_idem.mpr _ ⊢ □ (P ∗ □ Q) := intuitionistically_sep_2 +/-- + Designed to be a mutable state such that `newHyps` contains induction + hypotheses generated by Lean's built-in induction. +-/ +private structure InductionState {u} {prop : Q(Type u)} {bi} (origE : Q($prop)) where + -- Hypotheses in the Iris proof goal + {newE : Q($prop)} + (newHyps : Hyps bi newE) + -- Proof that the hypotheses is intuitionsitic + (pfIntHyps : Q($newE ⊢ □ $newE)) + (pf : Q($origE ⊢ $newE)) + /-- Given a collection of hypotheses (`hyps`) and a free variable `fvar`, return the subset of intuitionistic hypotheses in `hyps` that contains the `fvar` @@ -88,18 +106,6 @@ private def findIHs (m : MVarId) : MetaM (List FVarId) := ihs := decl.fvarId :: ihs return ihs.reverse -/-- - Designed to be a mutable state such that `newHyps` contains induction - hypotheses generated by Lean's built-in induction. --/ -private structure InductionState {u} {prop : Q(Type u)} {bi} (origE : Q($prop)) where - -- Hypotheses in the latest Iris proof goal - {newE : Q($prop)} - (newHyps : Hyps bi newE) - -- Proof that the hypotheses is intuitionsitic - (pfIntHyps : Q($newE ⊢ □ $newE)) - (pf : Q($origE ⊢ $newE)) - /-- Used for every induction hypothesis generated by Lean upon using the built-in `induction` tactic. The type class `IntoIH` is used for obtaining the @@ -297,6 +303,10 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} return pf +/-- + Simple tactic with inaccessible names of variables and induction hypotheses + generated by Lean automatically. +-/ elab "iinduction" colGt x:ident : tactic => do -- Get the ID of the variable on which induction is being performed let fvar ← getFVarId x @@ -305,11 +315,14 @@ elab "iinduction" colGt x:ident : tactic => do let pf ← iInductionCore hyps goal fvar none mvar.assign pf +/-- + Tactic with names of variables and induction hypotheses supplied by the user. +-/ elab "iinduction" colGt x:ident "with" alts:(colGe iinductionAlt)* : tactic => do -- Get the ID of the variable on which induction is being performed let fvar ← getFVarId x - -- Parse the list of cases supplied by the user + -- Parse the list of alternative names supplied by the user let parsedAlts ← alts.mapM parseIInductionAlt ProofModeM.runTactic λ mvar { hyps, goal, .. } => do From 3632223103f76a88f7e9c82cbf4bff3af5bc5251 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sun, 31 May 2026 17:55:43 +0200 Subject: [PATCH 039/181] More code refactoring --- Iris/Iris/ProofMode/Tactics/Induction.lean | 57 ++++++---------------- 1 file changed, 15 insertions(+), 42 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 33cb85475..bde7de888 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -33,23 +33,13 @@ open BI Std Lean Elab Tactic Meta Qq @[rocq_alias tac_revert_ih] theorem revert_IH [BI PROP] {P Q : PROP} {φ} (ih : φ) - (inst : IntoIH φ P Q) - (h1 : P ⊢ □ P) : - P ⊢ P ∗ □ Q := calc - P ⊢ □ P := h1 + (h : R ⊢ □ P) + (inst : IntoIH φ P Q) : + R ⊢ □ (P ∗ □ Q) := calc + _ ⊢ □ P := h _ ⊢ □ P ∗ □ P := intuitionistically_sep_dup.mp _ ⊢ □ P ∗ □ □ P := sep_mono_r intuitionistically_idem.mpr _ ⊢ □ P ∗ □ Q := sep_mono_r <| intuitionistically_mono <| inst.into_ih ih - _ ⊢ P ∗ □ Q := sep_mono_l intuitionistically_elim - -/-- - Given two intuitionistic propositions, their combined proposition - remains intuitionistic. --/ -theorem combine_intuitionistic_props [BI PROP] {P Q : PROP} - (h1 : P ⊢ □ P) : - P ∗ □ Q ⊢ □ (P ∗ □ Q) := calc - _ ⊢ □ P ∗ □ Q := sep_mono_l h1 _ ⊢ □ P ∗ □ □ Q := sep_mono_r intuitionistically_idem.mpr _ ⊢ □ (P ∗ □ Q) := intuitionistically_sep_2 @@ -58,44 +48,31 @@ theorem combine_intuitionistic_props [BI PROP] {P Q : PROP} hypotheses generated by Lean's built-in induction. -/ private structure InductionState {u} {prop : Q(Type u)} {bi} (origE : Q($prop)) where - -- Hypotheses in the Iris proof goal {newE : Q($prop)} (newHyps : Hyps bi newE) - -- Proof that the hypotheses is intuitionsitic - (pfIntHyps : Q($newE ⊢ □ $newE)) - (pf : Q($origE ⊢ $newE)) + (pf : Q($origE ⊢ □ $newE)) /-- Given a collection of hypotheses (`hyps`) and a free variable `fvar`, return the subset of intuitionistic hypotheses in `hyps` that contains the `fvar` and all spatial hypotheses in the proof state. + + This function is used for identifying the hypotheses in the Iris proof state + that must be reverted before applying Lean's built-in `induction` tactic. -/ private def iHypsContaining {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} (hyps : Hyps bi e) (fvar : FVarId) : List SelTarget := - let ivars := hyps.intuitionisticIVarIds.filter fun ivar => + let ivars := hyps.intuitionisticIVarIds.filter <| fun ivar => match hyps.getDecl? ivar with | some (_, _, _, ty) => ty.containsFVar fvar | none => false - (ivars ++ hyps.spatialIVarIds).map ( - fun ivar => { kind := .ipm ivar, explicit := false } - ) + (ivars ++ hyps.spatialIVarIds).map ({ kind := .ipm ·, explicit := false }) /-- - Given `h` of type `Hyps bi e`, search for all hypotheses in the intuitionistic - context, concatenate them using the separation conjunction and return - it as a single proposition. + Search for hypotheses in the regular Lean context that represent induction + hypotheses and return their IDs as a list. -/ -private def intuitionisticHyps {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} - (h : Hyps bi e) : ProofModeM Q($prop) := do - let props : List Q($prop) := h.intuitionisticProps - if props.isEmpty then - return q(emp) - else - return props.tail.reverse.foldr (fun acc tm => q(iprop($tm ∗ $acc))) props[0]! - -/-- Search for hypotheses in the regular Lean context that represent induction - hypotheses and return their IDs as a list. -/ private def findIHs (m : MVarId) : MetaM (List FVarId) := m.withContext do let lctx ← getLCtx @@ -127,11 +104,7 @@ private def addIntoIH {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} let binderIdent ← `(binderIdent| $nameIdent:ident) let ⟨_, newHyps⟩ ← Hyps.addWithInfo bi binderIdent q(true) Q oldHyps - return { - newHyps, - pf := q($(st.pf).trans <| revert_IH $p $inst $st.pfIntHyps), - pfIntHyps := q(combine_intuitionistic_props $st.pfIntHyps) - } + return { newHyps, pf := q(revert_IH $p $st.pf $inst) } /-- Introduce a list of induction hypotheses into Iris proof state. @@ -144,7 +117,7 @@ private def addAllIHs {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e : Q($prop)} -- Initialise the mutable instance of `InductionState` let mut st : InductionState e := { - newHyps := hyps, pf := q(.rfl), pfIntHyps + newHyps := hyps, pf := q($pfIntHyps) } -- Iteratively move the induction hypotheses into the intuitionistic context @@ -277,7 +250,7 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- Remove the induction hypotheses from the regular Lean context let pf' ← withoutFVars (u := 0) ihFVars.toArray <| k st.newHyps irisGoal.goal - s.mvarId.assign <| q($(st.pf).trans $pf') + s.mvarId.assign <| q($(st.pf).trans <| intuitionistically_elim.trans $pf') -- Case applicable for `with` syntax only match parsedAlts with From 1dbd7ac3b1716eef95d9410cdcf204603b9e1e56 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sun, 31 May 2026 18:25:45 +0200 Subject: [PATCH 040/181] More code refactoring 2 and fix tests --- Iris/Iris/ProofMode/Tactics/Induction.lean | 88 +++++++++++++++------- Iris/Iris/Tests/Tactics.lean | 10 ++- 2 files changed, 66 insertions(+), 32 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index bde7de888..e21d91d3d 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -85,16 +85,17 @@ private def findIHs (m : MVarId) : MetaM (List FVarId) := /-- Used for every induction hypothesis generated by Lean upon using the built-in - `induction` tactic. The type class `IntoIH` is used for obtaining the - corresponding proposition in the Iris proof mode. + `induction` tactic. This function is called by `addIHs` and returns + an instance of `InductionState` after handling one induction hypothesis. -/ -private def addIntoIH {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} +private def addIH {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} (st : @InductionState u prop bi e) (φ : Q(Prop)) (p : Q($φ)) (hFVar : FVarId) : ProofModeM (@InductionState u prop bi e) := do let oldE := st.newE let oldHyps : Hyps bi oldE := st.newHyps + -- Obtain the proposition to be introduced into the intuitionistic context let Q ← mkFreshExprMVarQ q($prop) let some inst ← ProofModeM.trySynthInstanceQ q(IntoIH $φ $oldE $Q) | throwError "iinduction: type class synthesis with IntoIH failed" @@ -107,40 +108,41 @@ private def addIntoIH {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} return { newHyps, pf := q(revert_IH $p $st.pf $inst) } /-- - Introduce a list of induction hypotheses into Iris proof state. - The list of `FVarId` values (`ihFVars`) should be the list returned by - `findIHs`. + Introduce a list of induction hypotheses into the intuitionistic context + of the Iris proof state. The list of `FVarId` values (`ihFVars`) should be + the list returned by `findIHs`. The function returns the final + `InductionState` with all induction hypotheses handled. + + The argument `pfIntHyps` holds when the spatial context is empty, which + is indeed the case when all hypotheses therein have been reverted. -/ -private def addAllIHs {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e : Q($prop)} +private def addIHs {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e : Q($prop)} (pfIntHyps : Q($e ⊢ □ $e)) (hyps : Hyps bi e) (ihFVars : List FVarId) : ProofModeM (@InductionState u prop bi e) := do -- Initialise the mutable instance of `InductionState` - let mut st : InductionState e := { - newHyps := hyps, pf := q($pfIntHyps) - } + let mut st : InductionState e := { newHyps := hyps, pf := q($pfIntHyps) } -- Iteratively move the induction hypotheses into the intuitionistic context for i in ihFVars do let p := mkFVar i let φ ← inferType (mkFVar i) - st ← addIntoIH st φ p i + st ← addIH st φ p i return st /-- Given hypothesis `hyps` representing `e` where every hypothesis exist in the - intuitionistic context, `e ⊢ □ e` must hold. + intuitionistic context, return the proof of `e ⊢ □ e`. Throw an error if + `hyps` contains hypotheses in the spatial context. -/ private def buildPfIntHyps {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} (hyps : Hyps bi e) : ProofModeM Q($e ⊢ □ $e) := match hyps with - | .emp _ => - pure q(intuitionistically_emp.mpr) + | .emp _ => pure q(intuitionistically_emp.mpr) | .hyp _ _ _ p _ _ => match matchBool p with - | .inl _ => - pure q(intuitionistically_idem.mpr) + | .inl _ => pure q(intuitionistically_idem.mpr) | .inr _ => throwError "iinduction: spatial context is not empty after reverting hypotheses" | .sep _ _ _ _ lhs rhs => do @@ -148,24 +150,42 @@ private def buildPfIntHyps {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} let pfR ← buildPfIntHyps rhs pure q((sep_mono $pfL $pfR).trans intuitionistically_sep_2) --- One case upon applying the `iinduction` tactic -syntax iinductionAlt := "| " ident ident* " => " tacticSeq +/-- + Tactic syntax for user-supplied alternative names. +-/ +syntax inductionAlts := "| " ident+ " => " tacticSeq + +/-- + Parse the tactic syntax for user-supplied alternative names. -private def parseIInductionAlt (alt : TSyntax `Iris.ProofMode.iinductionAlt) : + This function returns a triple, which includes: + 1. the name of the constructor supplied by the user (`ctor.getId`), + 2. the alternative names for that constructor (`vars`), and + 3. the tactics for this induction subgoal. +-/ +private def parseInductionAlts (alt : TSyntax `Iris.ProofMode.inductionAlts) : TacticM (Name × Array (TSyntax `ident) × TSyntax `Lean.Parser.Tactic.tacticSeq) := do - let `(iinductionAlt| | $ctor:ident $vars:ident* => $tac:tacticSeq) := alt + let `(inductionAlts| | $ctor:ident $vars:ident* => $tac:tacticSeq) := alt | throwError "iinduction: invalid syntax" + return ⟨ctor.getId, vars, tac⟩ - return (ctor.getId, vars, tac) - -/-- Check whether a fully-qualified constructor name (e.g. `Nat.succ`) matches a - user-written short name (e.g. `succ`) or an already-qualified name. -/ +/-- + Check whether a fully-qualified constructor name (e.g. `Nat.succ`) matches a + user-written short name (e.g. `succ`) or an already-qualified name. +-/ private def matchesCtorName (fullName : Name) (userShort : Name) : Bool := fullName == userShort || fullName.getString! == userShort.getString! +/-- + Throw an error if the user has supplied some but not all alternative names. +-/ private def throwMissingAlt {α} (ctor : Name) : ProofModeM α := throwError "iinduction: alternative `{ctor.getString!}` has not been provided" +/-- + Checks whether an invalid constructor names have been supplied by the user. + Throw an error if this is the case. +-/ private def checkCtors (ctors altCtors : List Name) : MetaM Unit := do let invalidAltCtors := altCtors.filter fun alt => @@ -173,8 +193,20 @@ private def checkCtors (ctors altCtors : List Name) : MetaM Unit := do if !invalidAltCtors.isEmpty then let invalids := String.intercalate ", " <| invalidAltCtors.map (s!"`{·}`") - throwError m!"iinduction: invalid alternative names {invalids}" + throwError m!"iinduction: invalid alternative name(s): {invalids}" +/-- + The main function handling the steps for the `iinduction` tactic. + 1. Revert intutionistic hypotheses that include `fvar` on which induction is performed. + 2. Revert all spatial hypotheses in the Iris proof state. + 3. Prepare the arguments for `Lean.Meta.Tactic.induction`. + 4. Apply `Lean.Meta.Tactic.induction` to obtain the induction subgoals. + 5. For each subgoal, move the induction hypotheses from the regular Lean + context into the Iris intuitionistic context. + 6. Introduce reverted hypotheses back into the Iris proof state. + + Step 1, 2 and 6 are handled by `iRevertIntro`, which is called by this function. +-/ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} (hyps : Hyps bi e) (goal : Q($prop)) (fvar : FVarId) (parsedAlts : Option <| Array <| Name × Array (TSyntax `ident) × TSyntax `Lean.Parser.Tactic.tacticSeq) : @@ -245,7 +277,7 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} let pfIntHyps ← buildPfIntHyps irisGoal.hyps -- Introduce the induction hypothesis back into the Iris proof state - let st ← addAllIHs pfIntHyps irisGoal.hyps ihFVars + let st ← addIHs pfIntHyps irisGoal.hyps ihFVars -- Remove the induction hypotheses from the regular Lean context let pf' ← withoutFVars (u := 0) ihFVars.toArray <| k st.newHyps irisGoal.goal @@ -291,12 +323,12 @@ elab "iinduction" colGt x:ident : tactic => do /-- Tactic with names of variables and induction hypotheses supplied by the user. -/ -elab "iinduction" colGt x:ident "with" alts:(colGe iinductionAlt)* : tactic => do +elab "iinduction" colGt x:ident "with" alts:(colGe inductionAlts)* : tactic => do -- Get the ID of the variable on which induction is being performed let fvar ← getFVarId x -- Parse the list of alternative names supplied by the user - let parsedAlts ← alts.mapM parseIInductionAlt + let parsedAlts ← alts.mapM parseInductionAlts ProofModeM.runTactic λ mvar { hyps, goal, .. } => do let pf ← iInductionCore hyps goal fvar (some parsedAlts) diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index d812dd79e..6dc7bd374 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2753,16 +2753,18 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : iinduction n with | zero => itrivial -/- Tests `iinduction` with induction on natural numbers with user-supplied - names, missing `succ` case -/ -/-- error: iinduction: invalid alternative names `a` -/ +/- Tests `iinduction` with induction on natural numbers with invalid + user-supplied names -/ +/-- error: iinduction: invalid alternative name(s): `invalidA`, `invalidB`, `invalidC` -/ #guard_msgs in example [BI PROP] {P Q R S T : PROP} {n : Nat} : ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by iintro HP #HQ #HR HS #HT iinduction n with + | invalidA => done | zero => itrivial - | a => itrivial + | invalidB => done | succ n IH => iframe; itrivial + | invalidC => done end iinduction From e376e7047db5551add8b0646ed4646fe07aa22fc Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sun, 31 May 2026 18:47:45 +0200 Subject: [PATCH 041/181] More code refactoring and formatting --- Iris/Iris/ProofMode/Tactics/Induction.lean | 55 ++++++++++++---------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index e21d91d3d..e7c853cc1 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -64,7 +64,7 @@ private def iHypsContaining {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} (hyps : Hyps bi e) (fvar : FVarId) : List SelTarget := let ivars := hyps.intuitionisticIVarIds.filter <| fun ivar => match hyps.getDecl? ivar with - | some (_, _, _, ty) => ty.containsFVar fvar + | some ⟨_, _, _, ty⟩ => ty.containsFVar fvar | none => false (ivars ++ hyps.spatialIVarIds).map ({ kind := .ipm ·, explicit := false }) @@ -126,7 +126,7 @@ private def addIHs {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e : Q($prop)} -- Iteratively move the induction hypotheses into the intuitionistic context for i in ihFVars do let p := mkFVar i - let φ ← inferType (mkFVar i) + let φ ← inferType <| mkFVar i st ← addIH st φ p i return st @@ -219,25 +219,33 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} let ⟨recName, ctors⟩ ← match fvarType.getAppFn with | .const indName _ => match (← getEnv).find? indName with - | some (.inductInfo val) => pure <| (Name.mkStr val.name "recOn", val.ctors) + | some (.inductInfo val) => pure <| (.mkStr val.name "recOn", val.ctors) | _ => throwError "iinduction: {indName} is not inductive" | _ => throwError "iinduction: unable to determine inductive type" + let matcher : + Name → + Name × Array (TSyntax `ident) × TSyntax `Lean.Parser.Tactic.tacticSeq → + Bool := fun ctor ⟨altCtor, _, _⟩ => matchesCtorName ctor altCtor + -- Define the names for variables and induction hypotheses if supplied by user let varNames : Array AltVarNames ← match parsedAlts with | none => - pure <| ctors.toArray.map (fun _ => {explicit := true, varNames := []}) + pure <| ctors.toArray.map <| fun _ => { explicit := true, varNames := [] } | some parsedAlts => do - ctors.toArray.mapM fun ctor => - match parsedAlts.find? (fun (altCtor, _, _) => matchesCtorName ctor altCtor) with + ctors.toArray.mapM <| fun ctor => + match parsedAlts.find? <| matcher ctor with | some (_, vars, _) => - pure ({ explicit := true, varNames := vars.toList.map (·.getId) } : AltVarNames) + pure { explicit := true, varNames := vars.toList.map (·.getId) } | none => throwMissingAlt ctor - -- Revert all hypotheses in the list - let pf ← iRevertIntro hyps goal targets ( - -- The function takes the hypotheses and goal after reverting + -- Check that all alternative names supplied by the user are valid + match parsedAlts with + | none => pure () + | some parsedAlts => checkCtors ctors <| parsedAlts.toList.map Prod.fst + + let pf ← iRevertIntro hyps goal targets <| fun hyps' goal' k => do -- Create a new metavariable for the proof goal upon reverting hypotheses let m ← mkBIGoal hyps' goal' @@ -246,25 +254,23 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} let subgoals ← m.mvarId!.withContext do m.mvarId!.induction fvar recName varNames - match parsedAlts with - | none => pure () - | some parsedAlts => checkCtors ctors <| parsedAlts.toList.map Prod.fst - -- Handle each subgoal generated by Lean's induction - for (s, ctor) in subgoals.toList.zip ctors do + for ⟨s, ctor⟩ in subgoals.toList.zip ctors do s.mvarId.withContext do + -- For pretty printing of arguments match parsedAlts with | none => pure () | some parsedAlts => - match parsedAlts.find? (fun (altCtor, _, _) => matchesCtorName ctor altCtor) with - | some (_, vars, _) => - for (fieldFVar, varStx) in s.fields.toList.zip vars.toList do + match parsedAlts.find? <| matcher ctor with + | some ⟨_, vars, _⟩ => + for ⟨fieldFVar, varStx⟩ in s.fields.toList.zip vars.toList do let lctx ← getLCtx let fieldType ← inferType fieldFVar - addLocalVarInfo varStx lctx fieldFVar (some fieldType) (isBinder := true) + addLocalVarInfo varStx lctx fieldFVar (some fieldType) true | none => throwMissingAlt ctor - let sType ← instantiateMVars (← s.mvarId.getType) + -- Obtain the type of the induction subgoal + let sType ← instantiateMVars <| ← s.mvarId.getType let some irisGoal := parseIrisGoal? sType -- This should not happen @@ -279,9 +285,10 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- Introduce the induction hypothesis back into the Iris proof state let st ← addIHs pfIntHyps irisGoal.hyps ihFVars - -- Remove the induction hypotheses from the regular Lean context + -- Generate the induction subgoal for the user let pf' ← withoutFVars (u := 0) ihFVars.toArray <| k st.newHyps irisGoal.goal + -- Fill the metavariable for the induction subgoal generated by Lean s.mvarId.assign <| q($(st.pf).trans <| intuitionistically_elim.trans $pf') -- Case applicable for `with` syntax only @@ -289,10 +296,9 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} | none => pure () | some parsedAlts => let biGoalMVar := (← getThe ProofModeM.State).goals.back! - modify (fun s => { s with goals := s.goals.pop }) + modify <| fun s => { s with goals := s.goals.pop } - let some (_, _, userTac) := - parsedAlts.find? (fun (altCtor, _, _) => matchesCtorName ctor altCtor) + let some ⟨_, _, userTac⟩ := parsedAlts.find? <| matcher ctor | throwMissingAlt ctor -- Run the user tactic on the BI goal @@ -304,7 +310,6 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} for r in remaining do addMVarGoal r return m - ) return pf From bff359b3be7e1cfb69247d35c9c290f5a9ebdd57 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 1 Jun 2026 11:14:58 +0200 Subject: [PATCH 042/181] Implement iinduction ... using ... (user-supplied recursor name) --- Iris/Iris/ProofMode/Tactics/Induction.lean | 32 +++++++++++++++++----- Iris/Iris/Tests/Tactics.lean | 17 +++++++++++- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index e7c853cc1..4707c7d27 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -209,17 +209,24 @@ private def checkCtors (ctors altCtors : List Name) : MetaM Unit := do -/ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} (hyps : Hyps bi e) (goal : Q($prop)) (fvar : FVarId) - (parsedAlts : Option <| Array <| Name × Array (TSyntax `ident) × TSyntax `Lean.Parser.Tactic.tacticSeq) : + (parsedAlts : Option <| Array <| Name × Array (TSyntax `ident) × TSyntax `Lean.Parser.Tactic.tacticSeq) + (altRecName : Option Name) : ProofModeM Q($e ⊢ $goal) := do -- Find all hypotheses that contain the variable being performed induction on let targets := iHypsContaining hyps fvar - -- Find the recursor name for induction + -- Find the recursor name and constructor names of the inductive datatype let fvarType ← whnf <| ← inferType <| mkFVar fvar let ⟨recName, ctors⟩ ← match fvarType.getAppFn with | .const indName _ => match (← getEnv).find? indName with - | some (.inductInfo val) => pure <| (.mkStr val.name "recOn", val.ctors) + | some (.inductInfo val) => + -- Use the user-supplied recursor name if available + let some recName ← match altRecName with + | none => getCustomEliminator? #[mkFVar fvar] true + | some altRecName => pure altRecName + | throwError "iinduction: unable to determine recursor name" + pure (recName, val.ctors) | _ => throwError "iinduction: {indName} is not inductive" | _ => throwError "iinduction: unable to determine inductive type" @@ -318,23 +325,34 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} generated by Lean automatically. -/ elab "iinduction" colGt x:ident : tactic => do - -- Get the ID of the variable on which induction is being performed let fvar ← getFVarId x ProofModeM.runTactic λ mvar { hyps, goal, .. } => do - let pf ← iInductionCore hyps goal fvar none + let pf ← iInductionCore hyps goal fvar none none mvar.assign pf /-- Tactic with names of variables and induction hypotheses supplied by the user. -/ elab "iinduction" colGt x:ident "with" alts:(colGe inductionAlts)* : tactic => do - -- Get the ID of the variable on which induction is being performed let fvar ← getFVarId x -- Parse the list of alternative names supplied by the user let parsedAlts ← alts.mapM parseInductionAlts ProofModeM.runTactic λ mvar { hyps, goal, .. } => do - let pf ← iInductionCore hyps goal fvar (some parsedAlts) + let pf ← iInductionCore hyps goal fvar (some parsedAlts) none + mvar.assign pf + +/-- + Tactic with the recursor name supplied by the user. +-/ +elab "iinduction" colGt x:ident "using" r:ident : tactic => do + let fvar ← getFVarId x + + -- Parse the recursor name provided by the user + let recName := r.getId + + ProofModeM.runTactic λ mvar { hyps, goal, .. } => do + let pf ← iInductionCore hyps goal fvar none recName mvar.assign pf diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 6dc7bd374..f8aad4420 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2735,7 +2735,7 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : iintro HP #HQ #HR HS #HT iinduction n with | Nat.zero => itrivial -- Using the full name of the constructor - | succ n IH => iframe; itrivial -- Using the short name of the constructor + | succ n ih => iframe; itrivial -- Using the short name of the constructor /- Tests `iinduction` with a non-inductive datatype -/ /-- error: iinduction: unable to determine inductive type -/ @@ -2767,4 +2767,19 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : | succ n IH => iframe; itrivial | invalidC => done +/-- Tests `iinduction` using a custom recursor name. -/ +example [BI PROP] {P Q R S T : PROP} {n : Nat} : + ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by + iintro HP #HQ #HR HS #HT + iinduction n using Nat.recOn + · itrivial + · iframe; itrivial + +/-- Tests `iinduction` using a custom recursor name. -/ +example [BI PROP] {P Q R S T : PROP} {n : Nat} : + ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by + iintro HP #HQ #HR HS #HT + iinduction n using Nat.strongRecOn -- TODO: IH with regular hypothesis + itrivial + end iinduction From e87b6078a30d308b31812234d40ad2e0ed6ec4a9 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 1 Jun 2026 11:43:56 +0200 Subject: [PATCH 043/181] Implement iinduction ... using ... with --- Iris/Iris/ProofMode/Tactics/Induction.lean | 15 +++++++-------- Iris/Iris/Tests/Tactics.lean | 8 ++++++++ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 4707c7d27..0dd45796d 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -331,9 +331,7 @@ elab "iinduction" colGt x:ident : tactic => do let pf ← iInductionCore hyps goal fvar none none mvar.assign pf -/-- - Tactic with names of variables and induction hypotheses supplied by the user. --/ +/-- Tactic with names of variables and induction hypotheses supplied by the user. -/ elab "iinduction" colGt x:ident "with" alts:(colGe inductionAlts)* : tactic => do let fvar ← getFVarId x @@ -344,15 +342,16 @@ elab "iinduction" colGt x:ident "with" alts:(colGe inductionAlts)* : tactic => d let pf ← iInductionCore hyps goal fvar (some parsedAlts) none mvar.assign pf -/-- - Tactic with the recursor name supplied by the user. --/ -elab "iinduction" colGt x:ident "using" r:ident : tactic => do +/-- Tactic with the recursor name and alternative names supplied by the user. -/ +elab "iinduction" colGt x:ident "using" r:ident "with" alts:(colGe inductionAlts)* : tactic => do let fvar ← getFVarId x -- Parse the recursor name provided by the user let recName := r.getId + -- Parse the list of alternative names supplied by the user + let parsedAlts ← alts.mapM parseInductionAlts + ProofModeM.runTactic λ mvar { hyps, goal, .. } => do - let pf ← iInductionCore hyps goal fvar none recName + let pf ← iInductionCore hyps goal fvar (some parsedAlts) recName mvar.assign pf diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index f8aad4420..81c3847da 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2782,4 +2782,12 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : iinduction n using Nat.strongRecOn -- TODO: IH with regular hypothesis itrivial +/-- Tests `iinduction` using a custom recursor name. -/ +example [BI PROP] {P Q R S T : PROP} {n : Nat} : + ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by + iintro HP #HQ #HR HS #HT + induction n using Nat.rec with + | zero => sorry + | succ _ _ => sorry + end iinduction From a8daced999fac59fd9576d3b59ec319a095a676b Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 1 Jun 2026 13:45:44 +0200 Subject: [PATCH 044/181] Implement iinduction ... generalizing ... --- Iris/Iris/ProofMode/Tactics/Induction.lean | 86 ++++++++++++++++++++-- 1 file changed, 78 insertions(+), 8 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 0dd45796d..68a668949 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -61,10 +61,10 @@ private structure InductionState {u} {prop : Q(Type u)} {bi} (origE : Q($prop)) that must be reverted before applying Lean's built-in `induction` tactic. -/ private def iHypsContaining {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} - (hyps : Hyps bi e) (fvar : FVarId) : List SelTarget := + (hyps : Hyps bi e) (fvars : List FVarId) : List SelTarget := let ivars := hyps.intuitionisticIVarIds.filter <| fun ivar => match hyps.getDecl? ivar with - | some ⟨_, _, _, ty⟩ => ty.containsFVar fvar + | some ⟨_, _, _, ty⟩ => fvars.any ty.containsFVar | none => false (ivars ++ hyps.spatialIVarIds).map ({ kind := .ipm ·, explicit := false }) @@ -210,10 +210,22 @@ private def checkCtors (ctors altCtors : List Name) : MetaM Unit := do private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} (hyps : Hyps bi e) (goal : Q($prop)) (fvar : FVarId) (parsedAlts : Option <| Array <| Name × Array (TSyntax `ident) × TSyntax `Lean.Parser.Tactic.tacticSeq) - (altRecName : Option Name) : + (altRecName : Option Name) + (genFVars : Option <| List FVarId) : ProofModeM Q($e ⊢ $goal) := do -- Find all hypotheses that contain the variable being performed induction on - let targets := iHypsContaining hyps fvar + let targets := + match genFVars with + | none => iHypsContaining hyps [fvar] + | some genFVars => iHypsContaining hyps <| [fvar] ++ genFVars + + -- User-supplied list of targets for reverting + let genTargets : List SelTarget := + match genFVars with + | some genFVars => genFVars.map ({ kind := .pure ·, explicit := true }) + | none => [] + + let targets := genTargets ++ targets -- Find the recursor name and constructor names of the inductive datatype let fvarType ← whnf <| ← inferType <| mkFVar fvar @@ -298,7 +310,7 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- Fill the metavariable for the induction subgoal generated by Lean s.mvarId.assign <| q($(st.pf).trans <| intuitionistically_elim.trans $pf') - -- Case applicable for `with` syntax only + -- Handle user-supplied tactics for specific constructors match parsedAlts with | none => pure () | some parsedAlts => @@ -328,7 +340,7 @@ elab "iinduction" colGt x:ident : tactic => do let fvar ← getFVarId x ProofModeM.runTactic λ mvar { hyps, goal, .. } => do - let pf ← iInductionCore hyps goal fvar none none + let pf ← iInductionCore hyps goal fvar none none none mvar.assign pf /-- Tactic with names of variables and induction hypotheses supplied by the user. -/ @@ -339,7 +351,18 @@ elab "iinduction" colGt x:ident "with" alts:(colGe inductionAlts)* : tactic => d let parsedAlts ← alts.mapM parseInductionAlts ProofModeM.runTactic λ mvar { hyps, goal, .. } => do - let pf ← iInductionCore hyps goal fvar (some parsedAlts) none + let pf ← iInductionCore hyps goal fvar parsedAlts none none + mvar.assign pf + +/-- Tactic with the recursor name supplied by the user. -/ +elab "iinduction" colGt x:ident "using" r:ident : tactic => do + let fvar ← getFVarId x + + -- Parse the recursor name provided by the user + let recName := r.getId + + ProofModeM.runTactic λ mvar { hyps, goal, .. } => do + let pf ← iInductionCore hyps goal fvar none recName none mvar.assign pf /-- Tactic with the recursor name and alternative names supplied by the user. -/ @@ -348,10 +371,57 @@ elab "iinduction" colGt x:ident "using" r:ident "with" alts:(colGe inductionAlts -- Parse the recursor name provided by the user let recName := r.getId + -- Parse the list of alternative names supplied by the user + let parsedAlts ← alts.mapM parseInductionAlts + + ProofModeM.runTactic λ mvar { hyps, goal, .. } => do + let pf ← iInductionCore hyps goal fvar parsedAlts recName none + mvar.assign pf + +elab "iinduction" colGt x:ident "generalizing" genIds:ident+ : tactic => do + let fvar ← getFVarId x + + -- Parse the user-supplied list of variables to be generalised + let genFVars ← genIds.toList.mapM <| fun id => getFVarId id.raw + + ProofModeM.runTactic λ mvar { hyps, goal, .. } => do + let pf ← iInductionCore hyps goal fvar none none genFVars + mvar.assign pf +elab "iinduction" colGt x:ident "generalizing" genIds:ident+ "with" alts:(colGe inductionAlts)* : tactic => do + let fvar ← getFVarId x + + -- Parse the user-supplied list of variables to be generalised + let genFVars ← genIds.toList.mapM <| fun id => getFVarId id.raw + -- Parse the list of alternative names supplied by the user + let parsedAlts ← alts.mapM parseInductionAlts + + ProofModeM.runTactic λ mvar { hyps, goal, .. } => do + let pf ← iInductionCore hyps goal fvar parsedAlts none genFVars + mvar.assign pf + +elab "iinduction" colGt x:ident "using" r:ident "generalizing" genIds:ident+ : tactic => do + let fvar ← getFVarId x + + -- Parse the recursor name provided by the user + let recName := r.getId + -- Parse the user-supplied list of variables to be generalised + let genFVars ← genIds.toList.mapM <| fun id => getFVarId id.raw + + ProofModeM.runTactic λ mvar { hyps, goal, .. } => do + let pf ← iInductionCore hyps goal fvar none recName genFVars + mvar.assign pf + +elab "iinduction" colGt x:ident "using" r:ident "generalizing" genIds:ident+ "with" alts:(colGe inductionAlts)* : tactic => do + let fvar ← getFVarId x + + -- Parse the recursor name provided by the user + let recName := r.getId + -- Parse the user-supplied list of variables to be generalised + let genFVars ← genIds.toList.mapM <| fun id => getFVarId id.raw -- Parse the list of alternative names supplied by the user let parsedAlts ← alts.mapM parseInductionAlts ProofModeM.runTactic λ mvar { hyps, goal, .. } => do - let pf ← iInductionCore hyps goal fvar (some parsedAlts) recName + let pf ← iInductionCore hyps goal fvar parsedAlts recName genFVars mvar.assign pf From ab75206e1de2596a2463d0d908b1cf6f4fe7fa25 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 1 Jun 2026 14:35:36 +0200 Subject: [PATCH 045/181] Fix iinduction ... generalizing ... 1. Revert Iris hypothesis explcitly specified by the user using `generalizing` syntax 2. Revert all spatial hypotheses, relevant intuitionistic hypotheses 3. Revert pure Lean variables specified by the user using `generalizing` syntax --- Iris/Iris/ProofMode/Tactics/Induction.lean | 93 +++++++++++++--------- 1 file changed, 57 insertions(+), 36 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 68a668949..7cc610efe 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -197,35 +197,49 @@ private def checkCtors (ctors altCtors : List Name) : MetaM Unit := do /-- The main function handling the steps for the `iinduction` tactic. - 1. Revert intutionistic hypotheses that include `fvar` on which induction is performed. - 2. Revert all spatial hypotheses in the Iris proof state. - 3. Prepare the arguments for `Lean.Meta.Tactic.induction`. - 4. Apply `Lean.Meta.Tactic.induction` to obtain the induction subgoals. - 5. For each subgoal, move the induction hypotheses from the regular Lean + + 1. Revert Iris hypotheses explicitly specified by the tactic user + (applicable when the `generalizing` syntax is used). + 2. Revert all spatial hypotheses in the Iris proof state as well as the + relevant hypotheses in the intuitionistic context. + 3. Revert pure Lean variables specified by the tactic user + (applicable when the `generalizing` syntax is used). + 4. Prepare the arguments for `Lean.Meta.Tactic.induction`. + 5. Apply `Lean.Meta.Tactic.induction` to obtain the induction subgoals. + 6. For each subgoal, move the induction hypotheses from the regular Lean context into the Iris intuitionistic context. - 6. Introduce reverted hypotheses back into the Iris proof state. + 7. Introduce hypotheses reverted in steps 1 and 2 back into the Iris proof state. - Step 1, 2 and 6 are handled by `iRevertIntro`, which is called by this function. + The relevant intuitionistic hypotheses to which Step 2 refers are those + involving `fvar` or pure Lean variables in `genSelTargets`. -/ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} (hyps : Hyps bi e) (goal : Q($prop)) (fvar : FVarId) (parsedAlts : Option <| Array <| Name × Array (TSyntax `ident) × TSyntax `Lean.Parser.Tactic.tacticSeq) (altRecName : Option Name) - (genFVars : Option <| List FVarId) : + (genSelTargets : Option <| List SelTarget) : ProofModeM Q($e ⊢ $goal) := do - -- Find all hypotheses that contain the variable being performed induction on - let targets := - match genFVars with - | none => iHypsContaining hyps [fvar] - | some genFVars => iHypsContaining hyps <| [fvar] ++ genFVars - - -- User-supplied list of targets for reverting - let genTargets : List SelTarget := - match genFVars with - | some genFVars => genFVars.map ({ kind := .pure ·, explicit := true }) + -- Iris hypotheses to be reverted as specified by the user using the `generalizing` syntax + let explicitIrisTargets : List SelTarget := + match genSelTargets with + | none => [] + | some genSelTargets => genSelTargets.filter <| fun t => match t.kind with | .ipm _ => true | _ => false + + -- `FVarID` values of pure hypothesis specified by the user using the `generalizing` syntax + let pureFVars : List FVarId := + match genSelTargets with | none => [] + | some genSelTargets => + genSelTargets.filterMap <| fun t => match t.kind with | .pure f => some f | _ => none + + -- Iris hypotheses in the spatial context and relevant intuitionistic context + let spatialAndIntuitionisticTargets := iHypsContaining hyps <| pureFVars.concat fvar + + -- Pure hypotheses to be reverted as specified by the user using the `generalizing` syntax + let pureTargets : List SelTarget := pureFVars.map ({ kind := .pure ·, explicit := true }) - let targets := genTargets ++ targets + -- Those in `explicitIrisTargets` get reverted first, so they are last in the list + let targets := pureTargets ++ spatialAndIntuitionisticTargets ++ explicitIrisTargets -- Find the recursor name and constructor names of the inductive datatype let fvarType ← whnf <| ← inferType <| mkFVar fvar @@ -378,50 +392,57 @@ elab "iinduction" colGt x:ident "using" r:ident "with" alts:(colGe inductionAlts let pf ← iInductionCore hyps goal fvar parsedAlts recName none mvar.assign pf -elab "iinduction" colGt x:ident "generalizing" genIds:ident+ : tactic => do +elab "iinduction" colGt x:ident "generalizing" genSelPats:(colGt selPat)+ : tactic => do let fvar ← getFVarId x - -- Parse the user-supplied list of variables to be generalised - let genFVars ← genIds.toList.mapM <| fun id => getFVarId id.raw - ProofModeM.runTactic λ mvar { hyps, goal, .. } => do - let pf ← iInductionCore hyps goal fvar none none genFVars + -- Parse the user-supplied list of variables to be generalised + let genSelPats ← liftMacroM <| SelPat.parse genSelPats + let genSelTargets ← SelPat.resolve hyps genSelPats + + let pf ← iInductionCore hyps goal fvar none none genSelTargets mvar.assign pf -elab "iinduction" colGt x:ident "generalizing" genIds:ident+ "with" alts:(colGe inductionAlts)* : tactic => do +elab "iinduction" colGt x:ident "generalizing" genSelPats:(colGt selPat)+ "with" alts:(colGe inductionAlts)* : tactic => do let fvar ← getFVarId x - -- Parse the user-supplied list of variables to be generalised - let genFVars ← genIds.toList.mapM <| fun id => getFVarId id.raw -- Parse the list of alternative names supplied by the user let parsedAlts ← alts.mapM parseInductionAlts ProofModeM.runTactic λ mvar { hyps, goal, .. } => do - let pf ← iInductionCore hyps goal fvar parsedAlts none genFVars + -- Parse the user-supplied list of variables to be generalised + let genSelPats ← liftMacroM <| SelPat.parse genSelPats + let genSelTargets ← SelPat.resolve hyps genSelPats + + let pf ← iInductionCore hyps goal fvar parsedAlts none genSelTargets mvar.assign pf -elab "iinduction" colGt x:ident "using" r:ident "generalizing" genIds:ident+ : tactic => do +elab "iinduction" colGt x:ident "using" r:ident "generalizing" genSelPats:(colGt selPat)+ : tactic => do let fvar ← getFVarId x -- Parse the recursor name provided by the user let recName := r.getId - -- Parse the user-supplied list of variables to be generalised - let genFVars ← genIds.toList.mapM <| fun id => getFVarId id.raw ProofModeM.runTactic λ mvar { hyps, goal, .. } => do - let pf ← iInductionCore hyps goal fvar none recName genFVars + -- Parse the user-supplied list of variables to be generalised + let genSelPats ← liftMacroM <| SelPat.parse genSelPats + let genSelTargets ← SelPat.resolve hyps genSelPats + + let pf ← iInductionCore hyps goal fvar none recName genSelTargets mvar.assign pf -elab "iinduction" colGt x:ident "using" r:ident "generalizing" genIds:ident+ "with" alts:(colGe inductionAlts)* : tactic => do +elab "iinduction" colGt x:ident "using" r:ident "generalizing" genSelPats:(colGt selPat)+ "with" alts:(colGe inductionAlts)* : tactic => do let fvar ← getFVarId x -- Parse the recursor name provided by the user let recName := r.getId - -- Parse the user-supplied list of variables to be generalised - let genFVars ← genIds.toList.mapM <| fun id => getFVarId id.raw -- Parse the list of alternative names supplied by the user let parsedAlts ← alts.mapM parseInductionAlts ProofModeM.runTactic λ mvar { hyps, goal, .. } => do - let pf ← iInductionCore hyps goal fvar parsedAlts recName genFVars + -- Parse the user-supplied list of variables to be generalised + let genSelPats ← liftMacroM <| SelPat.parse genSelPats + let genSelTargets ← SelPat.resolve hyps genSelPats + + let pf ← iInductionCore hyps goal fvar parsedAlts recName genSelTargets mvar.assign pf From 34fece6270b126fa329cf1353200b15ce0961598 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 1 Jun 2026 15:14:15 +0200 Subject: [PATCH 046/181] Use binderIdent instead of ident for `with` syntax to support underscores for inaccessible names --- Iris/Iris/ProofMode/Tactics/Induction.lean | 23 +++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 7cc610efe..3ef6eec31 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -153,7 +153,7 @@ private def buildPfIntHyps {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} /-- Tactic syntax for user-supplied alternative names. -/ -syntax inductionAlts := "| " ident+ " => " tacticSeq +syntax inductionAlts := "| " binderIdent+ " => " tacticSeq /-- Parse the tactic syntax for user-supplied alternative names. @@ -164,8 +164,8 @@ syntax inductionAlts := "| " ident+ " => " tacticSeq 3. the tactics for this induction subgoal. -/ private def parseInductionAlts (alt : TSyntax `Iris.ProofMode.inductionAlts) : - TacticM (Name × Array (TSyntax `ident) × TSyntax `Lean.Parser.Tactic.tacticSeq) := do - let `(inductionAlts| | $ctor:ident $vars:ident* => $tac:tacticSeq) := alt + TacticM (Name × Array (TSyntax `Lean.binderIdent) × TSyntax `Lean.Parser.Tactic.tacticSeq) := do + let `(inductionAlts| | $ctor:ident $vars:binderIdent* => $tac:tacticSeq) := alt | throwError "iinduction: invalid syntax" return ⟨ctor.getId, vars, tac⟩ @@ -215,7 +215,7 @@ private def checkCtors (ctors altCtors : List Name) : MetaM Unit := do -/ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} (hyps : Hyps bi e) (goal : Q($prop)) (fvar : FVarId) - (parsedAlts : Option <| Array <| Name × Array (TSyntax `ident) × TSyntax `Lean.Parser.Tactic.tacticSeq) + (parsedAlts : Option <| Array <| Name × Array (TSyntax `Lean.binderIdent) × TSyntax `Lean.Parser.Tactic.tacticSeq) (altRecName : Option Name) (genSelTargets : Option <| List SelTarget) : ProofModeM Q($e ⊢ $goal) := do @@ -258,7 +258,7 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} let matcher : Name → - Name × Array (TSyntax `ident) × TSyntax `Lean.Parser.Tactic.tacticSeq → + Name × Array (TSyntax `Lean.binderIdent) × TSyntax `Lean.Parser.Tactic.tacticSeq → Bool := fun ctor ⟨altCtor, _, _⟩ => matchesCtorName ctor altCtor -- Define the names for variables and induction hypotheses if supplied by user @@ -270,7 +270,11 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} ctors.toArray.mapM <| fun ctor => match parsedAlts.find? <| matcher ctor with | some (_, vars, _) => - pure { explicit := true, varNames := vars.toList.map (·.getId) } + pure { explicit := true, varNames := vars.toList.map <| + fun v => + match v.raw with + | `(binderIdent| $id:ident) => id.getId + | _ => Name.mkSimple "_" } | none => throwMissingAlt ctor -- Check that all alternative names supplied by the user are valid @@ -297,9 +301,10 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} match parsedAlts.find? <| matcher ctor with | some ⟨_, vars, _⟩ => for ⟨fieldFVar, varStx⟩ in s.fields.toList.zip vars.toList do - let lctx ← getLCtx - let fieldType ← inferType fieldFVar - addLocalVarInfo varStx lctx fieldFVar (some fieldType) true + if let `(binderIdent| $id:ident) := varStx then + let lctx ← getLCtx + let fieldType ← inferType fieldFVar + addLocalVarInfo id lctx fieldFVar (some fieldType) true | none => throwMissingAlt ctor -- Obtain the type of the induction subgoal From cc8136808d336476c6ae04829c0346ca773531d5 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 1 Jun 2026 16:01:28 +0200 Subject: [PATCH 047/181] Catch invalid, missing and duplicate constructor names when `with` syntax is used --- Iris/Iris/ProofMode/Tactics/Induction.lean | 44 +++++++++++++++------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 3ef6eec31..abd5cd121 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -73,7 +73,7 @@ private def iHypsContaining {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} Search for hypotheses in the regular Lean context that represent induction hypotheses and return their IDs as a list. -/ -private def findIHs (m : MVarId) : MetaM (List FVarId) := +private def findIHs (m : MVarId) : ProofModeM (List FVarId) := m.withContext do let lctx ← getLCtx let mut ihs := [] @@ -183,17 +183,35 @@ private def throwMissingAlt {α} (ctor : Name) : ProofModeM α := throwError "iinduction: alternative `{ctor.getString!}` has not been provided" /-- - Checks whether an invalid constructor names have been supplied by the user. - Throw an error if this is the case. + Checks whether an invalid, missing and/or duplicate constructor names have + been supplied by the user. Throw an error if this is the case. -/ -private def checkCtors (ctors altCtors : List Name) : MetaM Unit := do +private def checkCtors (ctors altCtors : List Name) : ProofModeM Unit := do + let mut errors : List String := [] + + -- Check for missing constructor names + let missingAltCtors := + ctors.filter (not <| altCtors.any <| fun altCtor => matchesCtorName · altCtor) + + -- Check for invalid constructor names let invalidAltCtors := - altCtors.filter fun alt => - not <| ctors.any fun ctor => matchesCtorName ctor alt + altCtors.filter (not <| ctors.any <| fun ctor => matchesCtorName ctor ·) + -- Check for duplicate constructor names + let dupAltCtors := (altCtors.filter (fun alt => altCtors.countP (fun alt' => matchesCtorName alt alt') > 1)).eraseDupsBy matchesCtorName + + if !missingAltCtors.isEmpty then + let missing := ", ".intercalate <| missingAltCtors.map (s!"`{·}`") + errors := errors.concat s!"iinduction: missing alternative name(s): {missing}" + if !dupAltCtors.isEmpty then + let dups := ", ".intercalate <| dupAltCtors.map (s!"`{·}`") + errors := errors.concat s!"iinduction: duplicate alternative name(s): {dups}" if !invalidAltCtors.isEmpty then - let invalids := String.intercalate ", " <| invalidAltCtors.map (s!"`{·}`") - throwError m!"iinduction: invalid alternative name(s): {invalids}" + let invalids := ", ".intercalate <| invalidAltCtors.map (s!"`{·}`") + errors := errors.concat s!"iinduction: invalid alternative name(s): {invalids}" + + if !errors.isEmpty then + throwError m!"{"\n".intercalate errors}" /-- The main function handling the steps for the `iinduction` tactic. @@ -261,6 +279,11 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} Name × Array (TSyntax `Lean.binderIdent) × TSyntax `Lean.Parser.Tactic.tacticSeq → Bool := fun ctor ⟨altCtor, _, _⟩ => matchesCtorName ctor altCtor + -- Check that all alternative names supplied by the user are valid + match parsedAlts with + | none => pure () + | some parsedAlts => checkCtors ctors <| parsedAlts.toList.map Prod.fst + -- Define the names for variables and induction hypotheses if supplied by user let varNames : Array AltVarNames ← match parsedAlts with @@ -277,11 +300,6 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} | _ => Name.mkSimple "_" } | none => throwMissingAlt ctor - -- Check that all alternative names supplied by the user are valid - match parsedAlts with - | none => pure () - | some parsedAlts => checkCtors ctors <| parsedAlts.toList.map Prod.fst - let pf ← iRevertIntro hyps goal targets <| fun hyps' goal' k => do -- Create a new metavariable for the proof goal upon reverting hypotheses From 1525cd2402601ab3a6d86ce5bd6e5160f14a868b Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 1 Jun 2026 16:30:42 +0200 Subject: [PATCH 048/181] Hypotheses explicitly reverted by the user using the `generalized` syntax do not require reverting again when other spatial and intuitionistic hypotheses are reverted --- Iris/Iris/ProofMode/Tactics/Induction.lean | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index abd5cd121..5490873c2 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -253,6 +253,9 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- Iris hypotheses in the spatial context and relevant intuitionistic context let spatialAndIntuitionisticTargets := iHypsContaining hyps <| pureFVars.concat fvar + let spatialAndIntuitionisticTargets := + spatialAndIntuitionisticTargets.filter (not <| (explicitIrisTargets.map (·.kind)).contains ·.kind) + -- Pure hypotheses to be reverted as specified by the user using the `generalizing` syntax let pureTargets : List SelTarget := pureFVars.map ({ kind := .pure ·, explicit := true }) From 559ba5ac37283ead19b92df7f03eb095ad95337b Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 1 Jun 2026 16:38:23 +0200 Subject: [PATCH 049/181] Consolidate tests for iinduction --- Iris/Iris/Tests/Tactics.lean | 76 +++++++++++++++--------------------- 1 file changed, 32 insertions(+), 44 deletions(-) diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 81c3847da..56526d4e9 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2718,24 +2718,37 @@ end iloeb section iinduction -/-- Tests `iinduction` with induction on natural numbers. Hypotheses in the - spatial context become premises of the induction hypothesis. -/ -example [BI PROP] {P Q R S T : PROP} {n : Nat} : - ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by - iintro HP #HQ #HR HS #HT - iinduction n - · itrivial - · iframe - itrivial +/-- + Tests `iinduction` with induction on natural numbers. + + For natural numbers, `Nat.recAux` is used as the default recursor name. Hence, + the tactic is equivalent to `iinduction n using Nat.recAux generalizing %P HQ %R`. + + Hypotheses in the spatial context necessarily become premises of the + induction hypothesis. + + With the `generalizing` syntax, `P` and `R` are universally quantified + in the induction hypothesis. Given they occur in `HP` and `HR`, respectively, + the two propositions are included as premises of the induction hypothesis. -/-- Tests `iinduction` with induction on natural numbers with user-supplied - names for variables and induction hypotheses. -/ + Meanwhile, `HQ` is included as a premise of the induction hypothesis without + `Q` being universally quantified. + + Note that the following variants of the tactic all produce equivalent subgoals: + - `induction n generalizing %P HP HQ %R` + - `induction n generalizing %P HQ %R HR` + - `induction n generalizing %P HP HQ %R HR` + - the tactics above with any permutations of `generalizing` arguments. +-/ example [BI PROP] {P Q R S T : PROP} {n : Nat} : ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by iintro HP #HQ #HR HS #HT - iinduction n with - | Nat.zero => itrivial -- Using the full name of the constructor - | succ n ih => iframe; itrivial -- Using the short name of the constructor + iinduction n generalizing %P HQ %R with + -- Using the full name of the constructor (`Nat.zero`) + | Nat.zero => itrivial + /- Using the short name of the constructor (`succ`), naming the induction + hypothesis as `ih`, but leaving the variable `n` inaccessible by using `_` -/ + | succ _ ih => iframe; itrivial /- Tests `iinduction` with a non-inductive datatype -/ /-- error: iinduction: unable to determine inductive type -/ @@ -2743,19 +2756,10 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : example [BI PROP] {P : PROP} : ⊢ P := by iinduction P -/- Tests `iinduction` with induction on natural numbers with user-supplied - names, missing `succ` case -/ -/-- error: iinduction: alternative `succ` has not been provided -/ -#guard_msgs in -example [BI PROP] {P Q R S T : PROP} {n : Nat} : - ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by - iintro HP #HQ #HR HS #HT - iinduction n with - | zero => itrivial - -/- Tests `iinduction` with induction on natural numbers with invalid - user-supplied names -/ -/-- error: iinduction: invalid alternative name(s): `invalidA`, `invalidB`, `invalidC` -/ +/- Tests `iinduction` with induction on natural numbers with invalid user-supplied names -/ +/-- error: iinduction: missing alternative name(s): `Nat.succ` +iinduction: duplicate alternative name(s): `zero` +iinduction: invalid alternative name(s): `invalidA`, `invalidB`, `invalidC` -/ #guard_msgs in example [BI PROP] {P Q R S T : PROP} {n : Nat} : ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by @@ -2764,17 +2768,9 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : | invalidA => done | zero => itrivial | invalidB => done - | succ n IH => iframe; itrivial + | Nat.zero => itrivial | invalidC => done -/-- Tests `iinduction` using a custom recursor name. -/ -example [BI PROP] {P Q R S T : PROP} {n : Nat} : - ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by - iintro HP #HQ #HR HS #HT - iinduction n using Nat.recOn - · itrivial - · iframe; itrivial - /-- Tests `iinduction` using a custom recursor name. -/ example [BI PROP] {P Q R S T : PROP} {n : Nat} : ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by @@ -2782,12 +2778,4 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : iinduction n using Nat.strongRecOn -- TODO: IH with regular hypothesis itrivial -/-- Tests `iinduction` using a custom recursor name. -/ -example [BI PROP] {P Q R S T : PROP} {n : Nat} : - ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by - iintro HP #HQ #HR HS #HT - induction n using Nat.rec with - | zero => sorry - | succ _ _ => sorry - end iinduction From 10589a5e2b66bb0e9db7ae11197f3737f2adc24d Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 1 Jun 2026 17:17:00 +0200 Subject: [PATCH 050/181] Define new function isIrisGoalWithForalls for findIHs: search for Iris goals with leading universal quantifiers --- Iris/Iris/ProofMode/Expr.lean | 5 +++++ Iris/Iris/ProofMode/Tactics/Induction.lean | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Iris/Iris/ProofMode/Expr.lean b/Iris/Iris/ProofMode/Expr.lean index a45f3e023..8c74c91fc 100644 --- a/Iris/Iris/ProofMode/Expr.lean +++ b/Iris/Iris/ProofMode/Expr.lean @@ -490,6 +490,11 @@ structure IrisGoal where def isIrisGoal (expr : Expr) : Bool := isAppOfArity expr ``Entails' 4 +partial def isIrisGoalWithForalls (expr : Expr) : Bool := + match expr.consumeMData with + | .forallE _ _ e _ => isIrisGoalWithForalls e + | e => isIrisGoal e + def parseIrisGoal? (expr : Expr) : Option IrisGoal := do -- remove top-level metadata when matching on the goal let expr := expr.consumeMData diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 5490873c2..a848f2101 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -79,7 +79,7 @@ private def findIHs (m : MVarId) : ProofModeM (List FVarId) := let mut ihs := [] for decl in lctx do let type ← instantiateMVars decl.type - if isIrisGoal type then + if isIrisGoalWithForalls type then ihs := decl.fvarId :: ihs return ihs.reverse From 58efe2c804163d1e5024d6f8f36830246dd0a9c5 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 1 Jun 2026 17:17:29 +0200 Subject: [PATCH 051/181] Fix priority of IntoIH instances --- Iris/Iris/ProofMode/Instances.lean | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Iris/Iris/ProofMode/Instances.lean b/Iris/Iris/ProofMode/Instances.lean index 5e309e3d8..762aca43c 100644 --- a/Iris/Iris/ProofMode/Instances.lean +++ b/Iris/Iris/ProofMode/Instances.lean @@ -924,7 +924,7 @@ instance intoIH_entails [BI PROP] (P Q : PROP) : IntoIH (P ⊢ Q) P Q where into_ih := λ hpq => intuitionistically_elim.trans hpq @[rocq_alias into_ih_forall] -instance intoIH_forall [BI PROP] (φ : α → Prop) (P : PROP) (Φ : α → PROP) +instance (priority := default - 2) intoIH_forall [BI PROP] (φ : α → Prop) (P : PROP) (Φ : α → PROP) [h : ∀ x, IntoIH (φ x) P (Φ x)] : IntoIH (∀ x, φ x) P (BI.forall Φ) where into_ih := by @@ -934,7 +934,7 @@ instance intoIH_forall [BI PROP] (φ : α → Prop) (P : PROP) (Φ : α → PROP exact (h x).into_ih (hφ x) @[rocq_alias into_ih_impl] -instance intoIH_imp [BI PROP] (φ ψ : Prop) (Δ P Q : PROP) +instance (priority := default - 1) intoIH_imp [BI PROP] (φ ψ : Prop) (Δ P Q : PROP) [h1 : MakeAffinely iprop(⌜φ⌝) P] [h2 : IntoIH ψ Δ Q] : IntoIH (φ → ψ) Δ iprop(P -∗ Q) where From 062ec019541d84cff34147ab5766c0c61f6b5d96 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 1 Jun 2026 17:18:03 +0200 Subject: [PATCH 052/181] More iinduction tests --- Iris/Iris/Tests/Tactics.lean | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 56526d4e9..309138311 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2725,7 +2725,8 @@ section iinduction the tactic is equivalent to `iinduction n using Nat.recAux generalizing %P HQ %R`. Hypotheses in the spatial context necessarily become premises of the - induction hypothesis. + induction hypothesis. The intuitionistic hypothesis `T n` is reverted + because it depends on `n`. With the `generalizing` syntax, `P` and `R` are universally quantified in the induction hypothesis. Given they occur in `HP` and `HR`, respectively, @@ -2740,8 +2741,8 @@ section iinduction - `induction n generalizing %P HP HQ %R HR` - the tactics above with any permutations of `generalizing` arguments. -/ -example [BI PROP] {P Q R S T : PROP} {n : Nat} : - ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by +example [BI PROP] {P Q R S : PROP} {T : Nat → PROP} {n : Nat} : + ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T n -∗ ⌜n + 0 = n⌝ := by iintro HP #HQ #HR HS #HT iinduction n generalizing %P HQ %R with -- Using the full name of the constructor (`Nat.zero`) @@ -2775,7 +2776,7 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : example [BI PROP] {P Q R S T : PROP} {n : Nat} : ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by iintro HP #HQ #HR HS #HT - iinduction n using Nat.strongRecOn -- TODO: IH with regular hypothesis + iinduction n using Nat.strongRecOn itrivial end iinduction From 9a17cbaf1e4a8abfc47f150876db086a42549b62 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 1 Jun 2026 17:50:39 +0200 Subject: [PATCH 053/181] Generalisation to provide support for expressions, not just idents --- Iris/Iris/ProofMode/Tactics/Induction.lean | 50 +++++++++++++++------- Iris/Iris/Tests/Tactics.lean | 8 ++-- 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index a848f2101..97cf03a58 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -372,20 +372,38 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} return pf +/-- + Given a term expression, check whether it is a `FVarId` value. If so, + return it directly. Otherwise, generalise the term expressions before + returning its `FVarId`. +-/ +private def termToFVar (x : TSyntax `term) : TacticM FVarId := do + let e ← withMainContext <| elabTerm x none + let e ← instantiateMVars e + + if e.isFVar then + return e.fvarId! + + let mvarId ← getMainGoal + let (fvars, newMVarId) ← mvarId.generalize #[{ expr := e }] + replaceMainGoal [newMVarId] + + return fvars[0]! + /-- Simple tactic with inaccessible names of variables and induction hypotheses generated by Lean automatically. -/ -elab "iinduction" colGt x:ident : tactic => do - let fvar ← getFVarId x +elab "iinduction" colGt x:term : tactic => do + let fvar ← termToFVar x ProofModeM.runTactic λ mvar { hyps, goal, .. } => do let pf ← iInductionCore hyps goal fvar none none none mvar.assign pf /-- Tactic with names of variables and induction hypotheses supplied by the user. -/ -elab "iinduction" colGt x:ident "with" alts:(colGe inductionAlts)* : tactic => do - let fvar ← getFVarId x +elab "iinduction" colGt x:term "with" alts:(colGe inductionAlts)* : tactic => do + let fvar ← termToFVar x -- Parse the list of alternative names supplied by the user let parsedAlts ← alts.mapM parseInductionAlts @@ -395,8 +413,8 @@ elab "iinduction" colGt x:ident "with" alts:(colGe inductionAlts)* : tactic => d mvar.assign pf /-- Tactic with the recursor name supplied by the user. -/ -elab "iinduction" colGt x:ident "using" r:ident : tactic => do - let fvar ← getFVarId x +elab "iinduction" colGt x:term "using" r:ident : tactic => do + let fvar ← termToFVar x -- Parse the recursor name provided by the user let recName := r.getId @@ -406,8 +424,8 @@ elab "iinduction" colGt x:ident "using" r:ident : tactic => do mvar.assign pf /-- Tactic with the recursor name and alternative names supplied by the user. -/ -elab "iinduction" colGt x:ident "using" r:ident "with" alts:(colGe inductionAlts)* : tactic => do - let fvar ← getFVarId x +elab "iinduction" colGt x:term "using" r:ident "with" alts:(colGe inductionAlts)* : tactic => do + let fvar ← termToFVar x -- Parse the recursor name provided by the user let recName := r.getId @@ -418,8 +436,8 @@ elab "iinduction" colGt x:ident "using" r:ident "with" alts:(colGe inductionAlts let pf ← iInductionCore hyps goal fvar parsedAlts recName none mvar.assign pf -elab "iinduction" colGt x:ident "generalizing" genSelPats:(colGt selPat)+ : tactic => do - let fvar ← getFVarId x +elab "iinduction" colGt x:term "generalizing" genSelPats:(colGt selPat)+ : tactic => do + let fvar ← termToFVar x ProofModeM.runTactic λ mvar { hyps, goal, .. } => do -- Parse the user-supplied list of variables to be generalised @@ -429,8 +447,8 @@ elab "iinduction" colGt x:ident "generalizing" genSelPats:(colGt selPat)+ : tact let pf ← iInductionCore hyps goal fvar none none genSelTargets mvar.assign pf -elab "iinduction" colGt x:ident "generalizing" genSelPats:(colGt selPat)+ "with" alts:(colGe inductionAlts)* : tactic => do - let fvar ← getFVarId x +elab "iinduction" colGt x:term "generalizing" genSelPats:(colGt selPat)+ "with" alts:(colGe inductionAlts)* : tactic => do + let fvar ← termToFVar x -- Parse the list of alternative names supplied by the user let parsedAlts ← alts.mapM parseInductionAlts @@ -443,8 +461,8 @@ elab "iinduction" colGt x:ident "generalizing" genSelPats:(colGt selPat)+ "with" let pf ← iInductionCore hyps goal fvar parsedAlts none genSelTargets mvar.assign pf -elab "iinduction" colGt x:ident "using" r:ident "generalizing" genSelPats:(colGt selPat)+ : tactic => do - let fvar ← getFVarId x +elab "iinduction" colGt x:term "using" r:ident "generalizing" genSelPats:(colGt selPat)+ : tactic => do + let fvar ← termToFVar x -- Parse the recursor name provided by the user let recName := r.getId @@ -457,8 +475,8 @@ elab "iinduction" colGt x:ident "using" r:ident "generalizing" genSelPats:(colGt let pf ← iInductionCore hyps goal fvar none recName genSelTargets mvar.assign pf -elab "iinduction" colGt x:ident "using" r:ident "generalizing" genSelPats:(colGt selPat)+ "with" alts:(colGe inductionAlts)* : tactic => do - let fvar ← getFVarId x +elab "iinduction" colGt x:term "using" r:ident "generalizing" genSelPats:(colGt selPat)+ "with" alts:(colGe inductionAlts)* : tactic => do + let fvar ← termToFVar x -- Parse the recursor name provided by the user let recName := r.getId diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 309138311..754884e4c 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2772,11 +2772,11 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : | Nat.zero => itrivial | invalidC => done -/-- Tests `iinduction` using a custom recursor name. -/ -example [BI PROP] {P Q R S T : PROP} {n : Nat} : - ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by +/-- Tests `iinduction` using a custom recursor name and expression -/ +example [BI PROP] {P R S : PROP} {Q T : Nat → PROP} {n : Nat} : + ⊢ P -∗ □ Q m -∗ □ R -∗ S -∗ □ T n -∗ ⌜n + m + 0 = n + m⌝ := by iintro HP #HQ #HR HS #HT - iinduction n using Nat.strongRecOn + iinduction n + m using Nat.strongRecOn itrivial end iinduction From 18a9e311dd9f9820f30702ecff8f571ee6310b13 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 1 Jun 2026 18:06:53 +0200 Subject: [PATCH 054/181] Code formatting --- Iris/Iris/ProofMode/Tactics/Induction.lean | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 97cf03a58..0426e7b84 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -198,7 +198,8 @@ private def checkCtors (ctors altCtors : List Name) : ProofModeM Unit := do altCtors.filter (not <| ctors.any <| fun ctor => matchesCtorName ctor ·) -- Check for duplicate constructor names - let dupAltCtors := (altCtors.filter (fun alt => altCtors.countP (fun alt' => matchesCtorName alt alt') > 1)).eraseDupsBy matchesCtorName + let dupAltCtors := (altCtors.filter <| + fun alt => altCtors.countP (matchesCtorName alt ·) > 1).eraseDupsBy matchesCtorName if !missingAltCtors.isEmpty then let missing := ", ".intercalate <| missingAltCtors.map (s!"`{·}`") @@ -233,7 +234,8 @@ private def checkCtors (ctors altCtors : List Name) : ProofModeM Unit := do -/ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} (hyps : Hyps bi e) (goal : Q($prop)) (fvar : FVarId) - (parsedAlts : Option <| Array <| Name × Array (TSyntax `Lean.binderIdent) × TSyntax `Lean.Parser.Tactic.tacticSeq) + (parsedAlts : Option <| Array <| + Name × Array (TSyntax `Lean.binderIdent) × TSyntax `Lean.Parser.Tactic.tacticSeq) (altRecName : Option Name) (genSelTargets : Option <| List SelTarget) : ProofModeM Q($e ⊢ $goal) := do @@ -424,7 +426,8 @@ elab "iinduction" colGt x:term "using" r:ident : tactic => do mvar.assign pf /-- Tactic with the recursor name and alternative names supplied by the user. -/ -elab "iinduction" colGt x:term "using" r:ident "with" alts:(colGe inductionAlts)* : tactic => do +elab "iinduction" colGt x:term "using" r:ident + "with" alts:(colGe inductionAlts)* : tactic => do let fvar ← termToFVar x -- Parse the recursor name provided by the user @@ -447,7 +450,8 @@ elab "iinduction" colGt x:term "generalizing" genSelPats:(colGt selPat)+ : tacti let pf ← iInductionCore hyps goal fvar none none genSelTargets mvar.assign pf -elab "iinduction" colGt x:term "generalizing" genSelPats:(colGt selPat)+ "with" alts:(colGe inductionAlts)* : tactic => do +elab "iinduction" colGt x:term "generalizing" genSelPats:(colGt selPat)+ + "with" alts:(colGe inductionAlts)* : tactic => do let fvar ← termToFVar x -- Parse the list of alternative names supplied by the user @@ -461,7 +465,8 @@ elab "iinduction" colGt x:term "generalizing" genSelPats:(colGt selPat)+ "with" let pf ← iInductionCore hyps goal fvar parsedAlts none genSelTargets mvar.assign pf -elab "iinduction" colGt x:term "using" r:ident "generalizing" genSelPats:(colGt selPat)+ : tactic => do +elab "iinduction" colGt x:term "using" r:ident + "generalizing" genSelPats:(colGt selPat)+ : tactic => do let fvar ← termToFVar x -- Parse the recursor name provided by the user @@ -475,7 +480,8 @@ elab "iinduction" colGt x:term "using" r:ident "generalizing" genSelPats:(colGt let pf ← iInductionCore hyps goal fvar none recName genSelTargets mvar.assign pf -elab "iinduction" colGt x:term "using" r:ident "generalizing" genSelPats:(colGt selPat)+ "with" alts:(colGe inductionAlts)* : tactic => do +elab "iinduction" colGt x:term "using" r:ident + "generalizing" genSelPats:(colGt selPat)+ "with" alts:(colGe inductionAlts)* : tactic => do let fvar ← termToFVar x -- Parse the recursor name provided by the user From c95dede5971bcfcf0e2258327f98b64a140d0061 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Tue, 2 Jun 2026 10:21:59 +0200 Subject: [PATCH 055/181] Catch extra arguments for alternative names --- Iris/Iris/ProofMode/Tactics/Induction.lean | 4 ++++ Iris/Iris/Tests/Tactics.lean | 18 +++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 0426e7b84..950388cdc 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -323,6 +323,10 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} | some parsedAlts => match parsedAlts.find? <| matcher ctor with | some ⟨_, vars, _⟩ => + if vars.size > s.fields.size then + throwError + s!"iinduction: too many variable names provided at alternative `{ctor}`: ".append + s!"{vars.size} provided, but {s.fields.size} expected" for ⟨fieldFVar, varStx⟩ in s.fields.toList.zip vars.toList do if let `(binderIdent| $id:ident) := varStx then let lctx ← getLCtx diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 754884e4c..5ffe9e315 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2772,11 +2772,23 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : | Nat.zero => itrivial | invalidC => done -/-- Tests `iinduction` using a custom recursor name and expression -/ +/- Tests `iinduction` with extra arguments supplied by the user -/ +/-- error: iinduction: too many variable names provided at alternative `Nat.succ`: 4 provided, but 2 expected -/ +#guard_msgs in +example [BI PROP] {P Q R S T : PROP} {n : Nat} : + ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by + iintro HP #HQ #HR HS #HT + iinduction n with + | zero => itrivial + | succ n ih extra1 extra2 => itrivial + +/-- Tests `iinduction` using a custom recursor name (strong induction), + performing induction on an expression `n + m` -/ example [BI PROP] {P R S : PROP} {Q T : Nat → PROP} {n : Nat} : ⊢ P -∗ □ Q m -∗ □ R -∗ S -∗ □ T n -∗ ⌜n + m + 0 = n + m⌝ := by iintro HP #HQ #HR HS #HT - iinduction n + m using Nat.strongRecOn - itrivial + iinduction n + m using Nat.caseStrongRecOn with + | zero => itrivial + | succ n ih => itrivial end iinduction From bd0fca73bfa3abaa9456a14cd87ccead28500a90 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Tue, 2 Jun 2026 10:54:11 +0200 Subject: [PATCH 056/181] Fallback recursor name when getCustomEliminator? fails --- Iris/Iris/ProofMode/Tactics/Induction.lean | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 950388cdc..0f9f200ee 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -270,11 +270,15 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} | .const indName _ => match (← getEnv).find? indName with | some (.inductInfo val) => - -- Use the user-supplied recursor name if available - let some recName ← match altRecName with - | none => getCustomEliminator? #[mkFVar fvar] true - | some altRecName => pure altRecName - | throwError "iinduction: unable to determine recursor name" + let recName ← + match altRecName, ← getCustomEliminator? #[mkFVar fvar] true with + -- Use the user-supplied recursor name if available + | some altRecName, _ => pure altRecName + -- Use the default recursor name if available + | none, some r => pure r + -- Use `.rec` as the fallback option + | none, none => pure <| mkRecName indName + -- | throwError "iinduction: unable to determine recursor name" pure (recName, val.ctors) | _ => throwError "iinduction: {indName} is not inductive" | _ => throwError "iinduction: unable to determine inductive type" From 76f321b13ad62197dcdce47405b83bab843577a3 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Tue, 2 Jun 2026 11:31:11 +0200 Subject: [PATCH 057/181] The constructor names should depend on the recursor For example, `zero` and `succ` for Nat, but `zero` and `ind` for Nat.caseStrongRecOn --- Iris/Iris/ProofMode/Tactics/Induction.lean | 18 +++++++++++------- Iris/Iris/Tests/Tactics.lean | 7 +++---- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 0f9f200ee..9ff68cda1 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -266,10 +266,10 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- Find the recursor name and constructor names of the inductive datatype let fvarType ← whnf <| ← inferType <| mkFVar fvar - let ⟨recName, ctors⟩ ← match fvarType.getAppFn with + let recName ← match fvarType.getAppFn with | .const indName _ => match (← getEnv).find? indName with - | some (.inductInfo val) => + | some (.inductInfo _) => let recName ← match altRecName, ← getCustomEliminator? #[mkFVar fvar] true with -- Use the user-supplied recursor name if available @@ -279,7 +279,7 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- Use `.rec` as the fallback option | none, none => pure <| mkRecName indName -- | throwError "iinduction: unable to determine recursor name" - pure (recName, val.ctors) + pure recName | _ => throwError "iinduction: {indName} is not inductive" | _ => throwError "iinduction: unable to determine inductive type" @@ -288,18 +288,21 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} Name × Array (TSyntax `Lean.binderIdent) × TSyntax `Lean.Parser.Tactic.tacticSeq → Bool := fun ctor ⟨altCtor, _, _⟩ => matchesCtorName ctor altCtor + -- Find the constructor names + let caseNames := ((← Lean.Meta.getElimInfo recName).altsInfo.map (·.name)).toList + -- Check that all alternative names supplied by the user are valid match parsedAlts with | none => pure () - | some parsedAlts => checkCtors ctors <| parsedAlts.toList.map Prod.fst + | some parsedAlts => checkCtors caseNames <| parsedAlts.toList.map Prod.fst -- Define the names for variables and induction hypotheses if supplied by user let varNames : Array AltVarNames ← match parsedAlts with | none => - pure <| ctors.toArray.map <| fun _ => { explicit := true, varNames := [] } + pure <| caseNames.toArray.map <| fun _ => { explicit := true, varNames := [] } | some parsedAlts => do - ctors.toArray.mapM <| fun ctor => + caseNames.toArray.mapM <| fun ctor => match parsedAlts.find? <| matcher ctor with | some (_, vars, _) => pure { explicit := true, varNames := vars.toList.map <| @@ -319,12 +322,13 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} m.mvarId!.induction fvar recName varNames -- Handle each subgoal generated by Lean's induction - for ⟨s, ctor⟩ in subgoals.toList.zip ctors do + for ⟨s, ctor⟩ in subgoals.toList.zip caseNames do s.mvarId.withContext do -- For pretty printing of arguments match parsedAlts with | none => pure () | some parsedAlts => + s.mvarId.setTag ctor match parsedAlts.find? <| matcher ctor with | some ⟨_, vars, _⟩ => if vars.size > s.fields.size then diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 5ffe9e315..2555b84a7 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2758,7 +2758,7 @@ example [BI PROP] {P : PROP} : ⊢ P := by iinduction P /- Tests `iinduction` with induction on natural numbers with invalid user-supplied names -/ -/-- error: iinduction: missing alternative name(s): `Nat.succ` +/-- error: iinduction: missing alternative name(s): `succ` iinduction: duplicate alternative name(s): `zero` iinduction: invalid alternative name(s): `invalidA`, `invalidB`, `invalidC` -/ #guard_msgs in @@ -2773,7 +2773,7 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : | invalidC => done /- Tests `iinduction` with extra arguments supplied by the user -/ -/-- error: iinduction: too many variable names provided at alternative `Nat.succ`: 4 provided, but 2 expected -/ +/-- error: iinduction: too many variable names provided at alternative `succ`: 4 provided, but 2 expected -/ #guard_msgs in example [BI PROP] {P Q R S T : PROP} {n : Nat} : ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by @@ -2789,6 +2789,5 @@ example [BI PROP] {P R S : PROP} {Q T : Nat → PROP} {n : Nat} : iintro HP #HQ #HR HS #HT iinduction n + m using Nat.caseStrongRecOn with | zero => itrivial - | succ n ih => itrivial - + | ind n ih => itrivial end iinduction From 98312085a87c8f7a8b82d61058f5df2dded563ac Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Tue, 2 Jun 2026 12:52:35 +0200 Subject: [PATCH 058/181] Remove redundant comment --- Iris/Iris/ProofMode/Tactics/Induction.lean | 1 - 1 file changed, 1 deletion(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 9ff68cda1..e3b65bf5c 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -278,7 +278,6 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} | none, some r => pure r -- Use `.rec` as the fallback option | none, none => pure <| mkRecName indName - -- | throwError "iinduction: unable to determine recursor name" pure recName | _ => throwError "iinduction: {indName} is not inductive" | _ => throwError "iinduction: unable to determine inductive type" From 7709bbf66c9040a6f2d729f6cc5674c5bded5c54 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Tue, 2 Jun 2026 17:48:05 +0200 Subject: [PATCH 059/181] Add test with binary tree --- Iris/Iris/Tests/Tactics.lean | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 2555b84a7..cb4c8d074 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2790,4 +2790,20 @@ example [BI PROP] {P R S : PROP} {Q T : Nat → PROP} {n : Nat} : iinduction n + m using Nat.caseStrongRecOn with | zero => itrivial | ind n ih => itrivial + +inductive Tree (α : Type u) where + | leaf : Tree α + | node : Tree α → α → Tree α → Tree α + deriving Repr + +example [BI PROP] {α} {t : Tree α} {P : Tree α → PROP} : + □ P .leaf -∗ □ (∀ l x r, P l -∗ P r -∗ P (.node l x r)) -∗ P t := by + iintro #H1 #H2 + iinduction t with + | leaf => iexact H1 + | node l y r ih1 ih2 => + iapply H2 + · iexact ih1 + · iexact ih2 + end iinduction From e0c690a7ae862f2989134ddf559bf0b04bc245cb Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Tue, 2 Jun 2026 18:02:52 +0200 Subject: [PATCH 060/181] Bug fix for handling more than one IH in addIH, simplify InductionState and relevant lemma --- Iris/Iris/ProofMode/Tactics/Induction.lean | 32 ++++++++++++---------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index e3b65bf5c..53e553a52 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -31,17 +31,18 @@ open BI Std Lean Elab Tactic Meta Qq intuitionistic context. -/ @[rocq_alias tac_revert_ih] -theorem revert_IH [BI PROP] {P Q : PROP} {φ} +theorem revert_IH [BI PROP] {Q R P : PROP} {φ} (ih : φ) - (h : R ⊢ □ P) - (inst : IntoIH φ P Q) : - R ⊢ □ (P ∗ □ Q) := calc - _ ⊢ □ P := h + (h1 : P ⊢ □ P) + (h2 : P ⊢ Q) + (inst : IntoIH φ P R) : + P ⊢ (Q ∗ □ R) := calc + _ ⊢ □ P := h1 _ ⊢ □ P ∗ □ P := intuitionistically_sep_dup.mp _ ⊢ □ P ∗ □ □ P := sep_mono_r intuitionistically_idem.mpr - _ ⊢ □ P ∗ □ Q := sep_mono_r <| intuitionistically_mono <| inst.into_ih ih - _ ⊢ □ P ∗ □ □ Q := sep_mono_r intuitionistically_idem.mpr - _ ⊢ □ (P ∗ □ Q) := intuitionistically_sep_2 + _ ⊢ □ P ∗ □ R := sep_mono_r <| intuitionistically_mono <| inst.into_ih ih + _ ⊢ □ Q ∗ □ R := sep_mono_l <| intuitionistically_mono h2 + _ ⊢ Q ∗ □ R := sep_mono_l intuitionistically_elim /-- Designed to be a mutable state such that `newHyps` contains induction @@ -50,7 +51,7 @@ theorem revert_IH [BI PROP] {P Q : PROP} {φ} private structure InductionState {u} {prop : Q(Type u)} {bi} (origE : Q($prop)) where {newE : Q($prop)} (newHyps : Hyps bi newE) - (pf : Q($origE ⊢ □ $newE)) + (pf : Q($origE ⊢ $newE)) /-- Given a collection of hypotheses (`hyps`) and a free variable `fvar`, return @@ -90,6 +91,7 @@ private def findIHs (m : MVarId) : ProofModeM (List FVarId) := -/ private def addIH {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} (st : @InductionState u prop bi e) + (pfIntHyps : Q($e ⊢ □ $e)) (φ : Q(Prop)) (p : Q($φ)) (hFVar : FVarId) : ProofModeM (@InductionState u prop bi e) := do let oldE := st.newE @@ -97,7 +99,7 @@ private def addIH {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- Obtain the proposition to be introduced into the intuitionistic context let Q ← mkFreshExprMVarQ q($prop) - let some inst ← ProofModeM.trySynthInstanceQ q(IntoIH $φ $oldE $Q) + let some inst ← ProofModeM.trySynthInstanceQ q(IntoIH $φ $e $Q) | throwError "iinduction: type class synthesis with IntoIH failed" -- Introduce the induction hypothesis into the intuitionistic context @@ -105,7 +107,7 @@ private def addIH {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} let binderIdent ← `(binderIdent| $nameIdent:ident) let ⟨_, newHyps⟩ ← Hyps.addWithInfo bi binderIdent q(true) Q oldHyps - return { newHyps, pf := q(revert_IH $p $st.pf $inst) } + return { newHyps, pf := q(revert_IH $p $pfIntHyps $st.pf $inst) } /-- Introduce a list of induction hypotheses into the intuitionistic context @@ -121,13 +123,13 @@ private def addIHs {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e : Q($prop)} ProofModeM (@InductionState u prop bi e) := do -- Initialise the mutable instance of `InductionState` - let mut st : InductionState e := { newHyps := hyps, pf := q($pfIntHyps) } + let mut st : InductionState e := { newHyps := hyps, pf := q(.rfl) } -- Iteratively move the induction hypotheses into the intuitionistic context for i in ihFVars do let p := mkFVar i - let φ ← inferType <| mkFVar i - st ← addIH st φ p i + let φ ← inferType p + st ← addIH st pfIntHyps φ p i return st @@ -361,7 +363,7 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} let pf' ← withoutFVars (u := 0) ihFVars.toArray <| k st.newHyps irisGoal.goal -- Fill the metavariable for the induction subgoal generated by Lean - s.mvarId.assign <| q($(st.pf).trans <| intuitionistically_elim.trans $pf') + s.mvarId.assign <| q($(st.pf).trans $pf') -- Handle user-supplied tactics for specific constructors match parsedAlts with From 7b893cafe903e87450c70348f934c24ad14ede48 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Tue, 2 Jun 2026 18:23:31 +0200 Subject: [PATCH 061/181] Pretty goal naming with clickable drop-down in InfoView --- Iris/Iris/ProofMode/Tactics/Induction.lean | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 53e553a52..af36614b1 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -365,11 +365,14 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- Fill the metavariable for the induction subgoal generated by Lean s.mvarId.assign <| q($(st.pf).trans $pf') + -- Label the induction subgoal for the user with a name + let biGoalMVar := (← getThe ProofModeM.State).goals.back! + biGoalMVar.setTag ctor + -- Handle user-supplied tactics for specific constructors match parsedAlts with | none => pure () | some parsedAlts => - let biGoalMVar := (← getThe ProofModeM.State).goals.back! modify <| fun s => { s with goals := s.goals.pop } let some ⟨_, _, userTac⟩ := parsedAlts.find? <| matcher ctor From 4d51583160e80ed1edd4d49acd39258205e70fa4 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Wed, 3 Jun 2026 11:03:02 +0200 Subject: [PATCH 062/181] Improve findIHs comment and remove redundant function isIrisGoalWithForalls --- Iris/Iris/ProofMode/Expr.lean | 5 ----- Iris/Iris/ProofMode/Tactics/Induction.lean | 8 +++++--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Iris/Iris/ProofMode/Expr.lean b/Iris/Iris/ProofMode/Expr.lean index 8c74c91fc..a45f3e023 100644 --- a/Iris/Iris/ProofMode/Expr.lean +++ b/Iris/Iris/ProofMode/Expr.lean @@ -490,11 +490,6 @@ structure IrisGoal where def isIrisGoal (expr : Expr) : Bool := isAppOfArity expr ``Entails' 4 -partial def isIrisGoalWithForalls (expr : Expr) : Bool := - match expr.consumeMData with - | .forallE _ _ e _ => isIrisGoalWithForalls e - | e => isIrisGoal e - def parseIrisGoal? (expr : Expr) : Option IrisGoal := do -- remove top-level metadata when matching on the goal let expr := expr.consumeMData diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index af36614b1..2fe90fd66 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -71,8 +71,10 @@ private def iHypsContaining {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} (ivars ++ hyps.spatialIVarIds).map ({ kind := .ipm ·, explicit := false }) /-- - Search for hypotheses in the regular Lean context that represent induction - hypotheses and return their IDs as a list. + Search for hypotheses in the regular Lean context that are the in the form + of Iris goals. These must be induction hypotheses generated by Lean's built-in + `induction` tactic, as there would otherwise not be Iris goals as assumptions + in the regular context. -/ private def findIHs (m : MVarId) : ProofModeM (List FVarId) := m.withContext do @@ -80,7 +82,7 @@ private def findIHs (m : MVarId) : ProofModeM (List FVarId) := let mut ihs := [] for decl in lctx do let type ← instantiateMVars decl.type - if isIrisGoalWithForalls type then + if isIrisGoal type.getForallBody then ihs := decl.fvarId :: ihs return ihs.reverse From 557027314edca2d92e591b4148e69f16cfe70d3e Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Wed, 3 Jun 2026 11:05:22 +0200 Subject: [PATCH 063/181] Add more specific error message for IntoIH synthesis failure in addIH --- Iris/Iris/ProofMode/Tactics/Induction.lean | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 2fe90fd66..44c01b4c1 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -102,7 +102,7 @@ private def addIH {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- Obtain the proposition to be introduced into the intuitionistic context let Q ← mkFreshExprMVarQ q($prop) let some inst ← ProofModeM.trySynthInstanceQ q(IntoIH $φ $e $Q) - | throwError "iinduction: type class synthesis with IntoIH failed" + | throwError "iinduction: unable to perform type class synthesis with IntoIH for {φ}" -- Introduce the induction hypothesis into the intuitionistic context let nameIdent := mkIdent <| ← hFVar.getUserName From 05b2cb9db6d1fd06bc800637468999f769986b25 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Wed, 3 Jun 2026 11:16:26 +0200 Subject: [PATCH 064/181] Refactor buildPfIntHyps as Hyps.buildIntuitionisticProof Return Option (...) instead of ProofModeM (...) --- Iris/Iris/ProofMode/Expr.lean | 18 ++++++++++++++++++ Iris/Iris/ProofMode/Tactics/Induction.lean | 22 ++-------------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Iris/Iris/ProofMode/Expr.lean b/Iris/Iris/ProofMode/Expr.lean index a45f3e023..d6c4ed390 100644 --- a/Iris/Iris/ProofMode/Expr.lean +++ b/Iris/Iris/ProofMode/Expr.lean @@ -544,3 +544,21 @@ def Hyps.addWithInfo {prop : Q(Type u)} (bi : Q(BI $prop)) addHypInfo nameRef nameTo ivar' prop ty (isBinder := true) let hyps := Hyps.add bi nameTo ivar' p ty h return ⟨ivar', hyps⟩ + +/-- + Given hypothesis `hyps` representing `e` where every hypothesis exist in the + intuitionistic context, return the proof of `e ⊢ □ e`. Return `none` if + `hyps` contains hypotheses in the spatial context. +-/ +def Hyps.buildIntuitionisticProof {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} + (hyps : Hyps bi e) : Option Q($e ⊢ □ $e) := + match hyps with + | .emp _ => some q(intuitionistically_emp.mpr) + | .hyp _ _ _ p _ _ => + match matchBool p with + | .inl _ => some q(intuitionistically_idem.mpr) + | .inr _ => none + | .sep _ _ _ _ lhs rhs => do + let pfL ← buildIntuitionisticProof lhs + let pfR ← buildIntuitionisticProof rhs + some q((sep_mono $pfL $pfR).trans intuitionistically_sep_2) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 44c01b4c1..67315efef 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -135,25 +135,6 @@ private def addIHs {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e : Q($prop)} return st -/-- - Given hypothesis `hyps` representing `e` where every hypothesis exist in the - intuitionistic context, return the proof of `e ⊢ □ e`. Throw an error if - `hyps` contains hypotheses in the spatial context. --/ -private def buildPfIntHyps {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} - (hyps : Hyps bi e) : ProofModeM Q($e ⊢ □ $e) := - match hyps with - | .emp _ => pure q(intuitionistically_emp.mpr) - | .hyp _ _ _ p _ _ => - match matchBool p with - | .inl _ => pure q(intuitionistically_idem.mpr) - | .inr _ => - throwError "iinduction: spatial context is not empty after reverting hypotheses" - | .sep _ _ _ _ lhs rhs => do - let pfL ← buildPfIntHyps lhs - let pfR ← buildPfIntHyps rhs - pure q((sep_mono $pfL $pfR).trans intuitionistically_sep_2) - /-- Tactic syntax for user-supplied alternative names. -/ @@ -356,7 +337,8 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} let ihFVars ← findIHs s.mvarId -- A proof that all hypotheses in the Iris goal exist in the intuitionistic context - let pfIntHyps ← buildPfIntHyps irisGoal.hyps + let some pfIntHyps := irisGoal.hyps.buildIntuitionisticProof + | throwError "iinduction: spatial context should be empty" -- Introduce the induction hypothesis back into the Iris proof state let st ← addIHs pfIntHyps irisGoal.hyps ihFVars From 2abdf66e350b470f3a9d84955e975bc49003a001 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Wed, 3 Jun 2026 11:18:01 +0200 Subject: [PATCH 065/181] Remove unused function: Hyps.intuitionisticProps --- Iris/Iris/ProofMode/Expr.lean | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Iris/Iris/ProofMode/Expr.lean b/Iris/Iris/ProofMode/Expr.lean index d6c4ed390..8fc1bd481 100644 --- a/Iris/Iris/ProofMode/Expr.lean +++ b/Iris/Iris/ProofMode/Expr.lean @@ -189,12 +189,6 @@ partial def Hyps.intuitionisticIVarIds {u prop bi} : | _, .hyp _ _ ivar p _ _ => if isTrue p then [ivar] else [] | _, .sep _ _ _ _ lhs rhs => lhs.intuitionisticIVarIds ++ rhs.intuitionisticIVarIds -partial def Hyps.intuitionisticProps {u prop bi} : - ∀ {s}, @Hyps u prop bi s → List Q($prop) - | _, .emp _ => [] - | _, .hyp tm _ _ p _ _ => if isTrue p then [tm] else [] - | _, .sep _ _ _ _ lhs rhs => lhs.intuitionisticProps ++ rhs.intuitionisticProps - variable (oldIVar : IVarId) (new : Name) {prop : Q(Type u)} {bi : Q(BI $prop)} in def Hyps.rename : ∀ {e}, Hyps bi e → Option (Hyps bi e) | _, .emp _ => none From 8412da68973e847974b4d364f84022c81cae58eb Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Wed, 3 Jun 2026 11:29:42 +0200 Subject: [PATCH 066/181] Rename iHypsContaining to iHypsToGeneralize, refine docstring --- Iris/Iris/ProofMode/Tactics/Induction.lean | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 67315efef..6a8905fb0 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -54,14 +54,16 @@ private structure InductionState {u} {prop : Q(Type u)} {bi} (origE : Q($prop)) (pf : Q($origE ⊢ $newE)) /-- - Given a collection of hypotheses (`hyps`) and a free variable `fvar`, return - the subset of intuitionistic hypotheses in `hyps` that contains the `fvar` - and all spatial hypotheses in the proof state. - - This function is used for identifying the hypotheses in the Iris proof state - that must be reverted before applying Lean's built-in `induction` tactic. + Given a collection of hypotheses (`hyps`) and a free variable `fvar` + representing the variable on which induction is applied, return the subset of + hypotheses in the Iris goal to be generalised. This includes the subset of + hypotheses in the intuitionistic context with occurrences of `fvar`, as well + as all spatial hypotheses. + + These hypotheses are then reverted from the the Iris contexts into the regular + Lean context before applying Lean's built-in `induction` tactic. -/ -private def iHypsContaining {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} +private def iHypsToGeneralize {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} (hyps : Hyps bi e) (fvars : List FVarId) : List SelTarget := let ivars := hyps.intuitionisticIVarIds.filter <| fun ivar => match hyps.getDecl? ivar with @@ -238,7 +240,7 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} genSelTargets.filterMap <| fun t => match t.kind with | .pure f => some f | _ => none -- Iris hypotheses in the spatial context and relevant intuitionistic context - let spatialAndIntuitionisticTargets := iHypsContaining hyps <| pureFVars.concat fvar + let spatialAndIntuitionisticTargets := iHypsToGeneralize hyps <| pureFVars.concat fvar let spatialAndIntuitionisticTargets := spatialAndIntuitionisticTargets.filter (not <| (explicitIrisTargets.map (·.kind)).contains ·.kind) From 73e28b66e1dfd1d9ff59f0d146bf43efd0b5b07c Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Wed, 3 Jun 2026 13:00:28 +0200 Subject: [PATCH 067/181] Move addIH into addIHs inline --- Iris/Iris/ProofMode/Tactics/Induction.lean | 47 ++++++++-------------- 1 file changed, 17 insertions(+), 30 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 6a8905fb0..a0fe8c2eb 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -88,39 +88,15 @@ private def findIHs (m : MVarId) : ProofModeM (List FVarId) := ihs := decl.fvarId :: ihs return ihs.reverse -/-- - Used for every induction hypothesis generated by Lean upon using the built-in - `induction` tactic. This function is called by `addIHs` and returns - an instance of `InductionState` after handling one induction hypothesis. --/ -private def addIH {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} - (st : @InductionState u prop bi e) - (pfIntHyps : Q($e ⊢ □ $e)) - (φ : Q(Prop)) (p : Q($φ)) (hFVar : FVarId) : - ProofModeM (@InductionState u prop bi e) := do - let oldE := st.newE - let oldHyps : Hyps bi oldE := st.newHyps - - -- Obtain the proposition to be introduced into the intuitionistic context - let Q ← mkFreshExprMVarQ q($prop) - let some inst ← ProofModeM.trySynthInstanceQ q(IntoIH $φ $e $Q) - | throwError "iinduction: unable to perform type class synthesis with IntoIH for {φ}" - - -- Introduce the induction hypothesis into the intuitionistic context - let nameIdent := mkIdent <| ← hFVar.getUserName - let binderIdent ← `(binderIdent| $nameIdent:ident) - let ⟨_, newHyps⟩ ← Hyps.addWithInfo bi binderIdent q(true) Q oldHyps - - return { newHyps, pf := q(revert_IH $p $pfIntHyps $st.pf $inst) } - /-- Introduce a list of induction hypotheses into the intuitionistic context of the Iris proof state. The list of `FVarId` values (`ihFVars`) should be the list returned by `findIHs`. The function returns the final `InductionState` with all induction hypotheses handled. - The argument `pfIntHyps` holds when the spatial context is empty, which - is indeed the case when all hypotheses therein have been reverted. + The argument `intuitionsiticProof` is obtained from the fact that the spatial + context is empty, which is indeed the case when all hypotheses therein have + been reverted into the regular Lean context. -/ private def addIHs {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e : Q($prop)} (pfIntHyps : Q($e ⊢ □ $e)) (hyps : Hyps bi e) (ihFVars : List FVarId) : @@ -131,9 +107,20 @@ private def addIHs {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e : Q($prop)} -- Iteratively move the induction hypotheses into the intuitionistic context for i in ihFVars do - let p := mkFVar i - let φ ← inferType p - st ← addIH st pfIntHyps φ p i + let ⟨_u, (ϕ : Q(Prop)), (p : Q($ϕ))⟩ ← inferTypeQ <| mkFVar i + + -- Obtain the proposition to be introduced into the intuitionistic context + let Q ← mkFreshExprMVarQ q($prop) + let some inst ← ProofModeM.trySynthInstanceQ q(IntoIH $ϕ $e $Q) + | throwError m!"iinduction: unable to perform type class synthesis with IntoIH for the induction hypothesis {ϕ}" + + -- Introduce the induction hypothesis into the intuitionistic context + let nameIdent := mkIdent <| ← i.getUserName + let binderIdent ← `(binderIdent| $nameIdent:ident) + let ⟨_, newHyps⟩ ← Hyps.addWithInfo bi binderIdent q(true) Q st.newHyps + let pf := q(revert_IH $p $pfIntHyps $st.pf $inst) + + st := { newHyps, pf } return st From 97f0ef7b25337b841fdf462551d40db318fffb37 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Thu, 4 Jun 2026 15:05:25 +0200 Subject: [PATCH 068/181] Improve revert_IH docstring explanation --- Iris/Iris/ProofMode/Tactics/Induction.lean | 27 ++++++++++++---------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index a0fe8c2eb..45ac5173b 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -18,20 +18,23 @@ public meta section open BI Std Lean Elab Tactic Meta Qq /-- - The user of the `iinduction` tactic provides the proof for the induction - subgoal. Induction hypotheses are available for the user in the intuitionistic - context of the Iris proof state. - - Meanwhile, the actual induction subgoal generated by Lean with the `induction` - tactic has induction hypotheses in the regular context. - - Given all hypotheses in the Iris proof state (represented by `P`) exist in - the intuitionistic context (that is, the spatial context is empty), we can - introduce another induction hypothesis generated by Lean into the - intuitionistic context. + This theorem is used for updating the proof in `InductionState` as `addIHs` + iterates through the list of induction hypotheses and introduces them from + the regular Lean context into the intuitionistic context. + + The initial set of hypotheses that exist in the Iris context after applying + Lean's built-in induction step is represented by `P`. Since the spatial + context is empty (that is, all hypotheses exist in intuitionistic context), + `P ⊢ □ P` must hold. + + The proposition `Q` represents these initial hypotheses as well as induction + hypotheses introduced into the intuitionistic context up until the most + recent `addIHs` iteration. At every iteration of `addIHs`, an induction + hypothesis `R` is obtained upon type class synthesis with `IntoIH`. The proof + for `InductionState` is obtained using this theorem accordingly. -/ @[rocq_alias tac_revert_ih] -theorem revert_IH [BI PROP] {Q R P : PROP} {φ} +theorem revert_IH [BI PROP] {P Q R : PROP} {φ} (ih : φ) (h1 : P ⊢ □ P) (h2 : P ⊢ Q) From d7d9da5cf46ea976d4a3400c2f0b3d5be635f89b Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Thu, 4 Jun 2026 15:14:31 +0200 Subject: [PATCH 069/181] Rename termToFVar as generalizeTermWithFVar, more details in the comment --- Iris/Iris/ProofMode/Tactics/Induction.lean | 24 ++++++++++++---------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 45ac5173b..f26683192 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -368,10 +368,12 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} /-- Given a term expression, check whether it is a `FVarId` value. If so, - return it directly. Otherwise, generalise the term expressions before - returning its `FVarId`. + return it directly. Otherwise, generalise the term expression before + returning its `FVarId`. This function has to be called before entering + `ProofModeM`, as it directly replaces the metavariable for the current + proof goal. -/ -private def termToFVar (x : TSyntax `term) : TacticM FVarId := do +private def generalizeTermWithFVar (x : TSyntax `term) : TacticM FVarId := do let e ← withMainContext <| elabTerm x none let e ← instantiateMVars e @@ -389,7 +391,7 @@ private def termToFVar (x : TSyntax `term) : TacticM FVarId := do generated by Lean automatically. -/ elab "iinduction" colGt x:term : tactic => do - let fvar ← termToFVar x + let fvar ← generalizeTermWithFVar x ProofModeM.runTactic λ mvar { hyps, goal, .. } => do let pf ← iInductionCore hyps goal fvar none none none @@ -397,7 +399,7 @@ elab "iinduction" colGt x:term : tactic => do /-- Tactic with names of variables and induction hypotheses supplied by the user. -/ elab "iinduction" colGt x:term "with" alts:(colGe inductionAlts)* : tactic => do - let fvar ← termToFVar x + let fvar ← generalizeTermWithFVar x -- Parse the list of alternative names supplied by the user let parsedAlts ← alts.mapM parseInductionAlts @@ -408,7 +410,7 @@ elab "iinduction" colGt x:term "with" alts:(colGe inductionAlts)* : tactic => do /-- Tactic with the recursor name supplied by the user. -/ elab "iinduction" colGt x:term "using" r:ident : tactic => do - let fvar ← termToFVar x + let fvar ← generalizeTermWithFVar x -- Parse the recursor name provided by the user let recName := r.getId @@ -420,7 +422,7 @@ elab "iinduction" colGt x:term "using" r:ident : tactic => do /-- Tactic with the recursor name and alternative names supplied by the user. -/ elab "iinduction" colGt x:term "using" r:ident "with" alts:(colGe inductionAlts)* : tactic => do - let fvar ← termToFVar x + let fvar ← generalizeTermWithFVar x -- Parse the recursor name provided by the user let recName := r.getId @@ -432,7 +434,7 @@ elab "iinduction" colGt x:term "using" r:ident mvar.assign pf elab "iinduction" colGt x:term "generalizing" genSelPats:(colGt selPat)+ : tactic => do - let fvar ← termToFVar x + let fvar ← generalizeTermWithFVar x ProofModeM.runTactic λ mvar { hyps, goal, .. } => do -- Parse the user-supplied list of variables to be generalised @@ -444,7 +446,7 @@ elab "iinduction" colGt x:term "generalizing" genSelPats:(colGt selPat)+ : tacti elab "iinduction" colGt x:term "generalizing" genSelPats:(colGt selPat)+ "with" alts:(colGe inductionAlts)* : tactic => do - let fvar ← termToFVar x + let fvar ← generalizeTermWithFVar x -- Parse the list of alternative names supplied by the user let parsedAlts ← alts.mapM parseInductionAlts @@ -459,7 +461,7 @@ elab "iinduction" colGt x:term "generalizing" genSelPats:(colGt selPat)+ elab "iinduction" colGt x:term "using" r:ident "generalizing" genSelPats:(colGt selPat)+ : tactic => do - let fvar ← termToFVar x + let fvar ← generalizeTermWithFVar x -- Parse the recursor name provided by the user let recName := r.getId @@ -474,7 +476,7 @@ elab "iinduction" colGt x:term "using" r:ident elab "iinduction" colGt x:term "using" r:ident "generalizing" genSelPats:(colGt selPat)+ "with" alts:(colGe inductionAlts)* : tactic => do - let fvar ← termToFVar x + let fvar ← generalizeTermWithFVar x -- Parse the recursor name provided by the user let recName := r.getId From 343af538c119e9efd13c56365270c51dac5ee45d Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Thu, 4 Jun 2026 19:01:28 +0200 Subject: [PATCH 070/181] Generalise ProofModeContinuation, towards avoiding directly manipulating ProofModeM state --- Iris/Iris/ProofMode/Tactics/Induction.lean | 6 ++---- Iris/Iris/ProofMode/Tactics/RevertIntro.lean | 6 +++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index f26683192..a4fdc875f 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -335,15 +335,13 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- Introduce the induction hypothesis back into the Iris proof state let st ← addIHs pfIntHyps irisGoal.hyps ihFVars - -- Generate the induction subgoal for the user - let pf' ← withoutFVars (u := 0) ihFVars.toArray <| k st.newHyps irisGoal.goal + -- Generate the induction subgoal for the user, label the induction subgoal with `ctor` + let pf' ← withoutFVars (u := 0) ihFVars.toArray <| k st.newHyps irisGoal.goal ctor -- Fill the metavariable for the induction subgoal generated by Lean s.mvarId.assign <| q($(st.pf).trans $pf') - -- Label the induction subgoal for the user with a name let biGoalMVar := (← getThe ProofModeM.State).goals.back! - biGoalMVar.setTag ctor -- Handle user-supplied tactics for specific constructors match parsedAlts with diff --git a/Iris/Iris/ProofMode/Tactics/RevertIntro.lean b/Iris/Iris/ProofMode/Tactics/RevertIntro.lean index 70d0e2e52..a538b6e52 100644 --- a/Iris/Iris/ProofMode/Tactics/RevertIntro.lean +++ b/Iris/Iris/ProofMode/Tactics/RevertIntro.lean @@ -15,7 +15,7 @@ public meta section abbrev ProofModeContinuation := ∀ {u : Level} {prop : Q(Type u)} {bi : Q(BI $prop)} {e : Q($prop)} - (_hyps : Hyps bi e) (goal: Q($prop)), + (_hyps : Hyps bi e) (goal: Q($prop)) (_ : Name := Name.anonymous), ProofModeM Q($e ⊢ $goal) def iRevertIntro @@ -36,7 +36,7 @@ def iRevertIntro return (name, .intro <| (if ivar.persistent? then .intuitionistic else id) <| .one ident) trace[irevertintro] s!"Calling `iRevertIntro` with {names.map (·.1)} on context {←ppExpr <| IrisGoal.toExpr {hyps, goal ..}}" iRevertCore hs hyps goal fun hyps goal => do - k hyps goal fun hyps goal => do - iIntroCore hyps goal names + k hyps goal fun hyps goal name => do + iIntroCore hyps goal names (addBIGoal · · name) initialize registerTraceClass `irevertintro From 258aad6c5968d5077a60917c282a3d0b67efca5a Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Fri, 5 Jun 2026 10:59:53 +0200 Subject: [PATCH 071/181] Rename inductionAlts as inductionAlt for consistency with Lean.Init.Tactics --- Iris/Iris/ProofMode/Tactics/Induction.lean | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index a4fdc875f..55724e990 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -130,7 +130,7 @@ private def addIHs {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e : Q($prop)} /-- Tactic syntax for user-supplied alternative names. -/ -syntax inductionAlts := "| " binderIdent+ " => " tacticSeq +syntax inductionAlt := "| " binderIdent+ " => " tacticSeq /-- Parse the tactic syntax for user-supplied alternative names. @@ -140,9 +140,9 @@ syntax inductionAlts := "| " binderIdent+ " => " tacticSeq 2. the alternative names for that constructor (`vars`), and 3. the tactics for this induction subgoal. -/ -private def parseInductionAlts (alt : TSyntax `Iris.ProofMode.inductionAlts) : +private def parseinductionAlt (alt : TSyntax `Iris.ProofMode.inductionAlt) : TacticM (Name × Array (TSyntax `Lean.binderIdent) × TSyntax `Lean.Parser.Tactic.tacticSeq) := do - let `(inductionAlts| | $ctor:ident $vars:binderIdent* => $tac:tacticSeq) := alt + let `(inductionAlt| | $ctor:ident $vars:binderIdent* => $tac:tacticSeq) := alt | throwError "iinduction: invalid syntax" return ⟨ctor.getId, vars, tac⟩ @@ -396,11 +396,11 @@ elab "iinduction" colGt x:term : tactic => do mvar.assign pf /-- Tactic with names of variables and induction hypotheses supplied by the user. -/ -elab "iinduction" colGt x:term "with" alts:(colGe inductionAlts)* : tactic => do +elab "iinduction" colGt x:term "with" alts:(colGe inductionAlt)* : tactic => do let fvar ← generalizeTermWithFVar x -- Parse the list of alternative names supplied by the user - let parsedAlts ← alts.mapM parseInductionAlts + let parsedAlts ← alts.mapM parseinductionAlt ProofModeM.runTactic λ mvar { hyps, goal, .. } => do let pf ← iInductionCore hyps goal fvar parsedAlts none none @@ -419,13 +419,13 @@ elab "iinduction" colGt x:term "using" r:ident : tactic => do /-- Tactic with the recursor name and alternative names supplied by the user. -/ elab "iinduction" colGt x:term "using" r:ident - "with" alts:(colGe inductionAlts)* : tactic => do + "with" alts:(colGe inductionAlt)* : tactic => do let fvar ← generalizeTermWithFVar x -- Parse the recursor name provided by the user let recName := r.getId -- Parse the list of alternative names supplied by the user - let parsedAlts ← alts.mapM parseInductionAlts + let parsedAlts ← alts.mapM parseinductionAlt ProofModeM.runTactic λ mvar { hyps, goal, .. } => do let pf ← iInductionCore hyps goal fvar parsedAlts recName none @@ -443,11 +443,11 @@ elab "iinduction" colGt x:term "generalizing" genSelPats:(colGt selPat)+ : tacti mvar.assign pf elab "iinduction" colGt x:term "generalizing" genSelPats:(colGt selPat)+ - "with" alts:(colGe inductionAlts)* : tactic => do + "with" alts:(colGe inductionAlt)* : tactic => do let fvar ← generalizeTermWithFVar x -- Parse the list of alternative names supplied by the user - let parsedAlts ← alts.mapM parseInductionAlts + let parsedAlts ← alts.mapM parseinductionAlt ProofModeM.runTactic λ mvar { hyps, goal, .. } => do -- Parse the user-supplied list of variables to be generalised @@ -473,13 +473,13 @@ elab "iinduction" colGt x:term "using" r:ident mvar.assign pf elab "iinduction" colGt x:term "using" r:ident - "generalizing" genSelPats:(colGt selPat)+ "with" alts:(colGe inductionAlts)* : tactic => do + "generalizing" genSelPats:(colGt selPat)+ "with" alts:(colGe inductionAlt)* : tactic => do let fvar ← generalizeTermWithFVar x -- Parse the recursor name provided by the user let recName := r.getId -- Parse the list of alternative names supplied by the user - let parsedAlts ← alts.mapM parseInductionAlts + let parsedAlts ← alts.mapM parseinductionAlt ProofModeM.runTactic λ mvar { hyps, goal, .. } => do -- Parse the user-supplied list of variables to be generalised From ff898f65b7e63b855961eb323c17d29a58cf0e8a Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Fri, 5 Jun 2026 11:51:14 +0200 Subject: [PATCH 072/181] Consolidate elaborations into one, move comment from text to elaboration, move code regarding syntax to top --- Iris/Iris/ProofMode/Tactics/Induction.lean | 215 +++++++++------------ Iris/Iris/Tests/Tactics.lean | 17 -- 2 files changed, 87 insertions(+), 145 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 55724e990..82539f1fc 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -17,6 +17,32 @@ namespace Iris.ProofMode public meta section open BI Std Lean Elab Tactic Meta Qq +/-- + Tactic syntax for user-supplied alternative names. +-/ +syntax inductionAlt := "| " binderIdent+ " => " tacticSeq + +/-- + Parse the tactic syntax for user-supplied alternative names. + + This function returns a triple, which includes: + 1. the name of the constructor supplied by the user (`ctor.getId`), + 2. the alternative names for that constructor (`vars`), and + 3. the tactics for this induction subgoal. +-/ +private def parseinductionAlt (alt : TSyntax `Iris.ProofMode.inductionAlt) : + TacticM (Name × Array (TSyntax `Lean.binderIdent) × TSyntax `Lean.Parser.Tactic.tacticSeq) := do + let `(inductionAlt| | $ctor:ident $vars:binderIdent* => $tac:tacticSeq) := alt + | throwError "iinduction: invalid syntax" + return ⟨ctor.getId, vars, tac⟩ + +/-- + Check whether a fully-qualified constructor name (e.g. `Nat.succ`) matches a + user-written short name (e.g. `succ`) or an already-qualified name. +-/ +private def matchesCtorName (fullName : Name) (userShort : Name) : Bool := + fullName == userShort || fullName.getString! == userShort.getString! + /-- This theorem is used for updating the proof in `InductionState` as `addIHs` iterates through the list of induction hypotheses and introduces them from @@ -127,32 +153,6 @@ private def addIHs {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e : Q($prop)} return st -/-- - Tactic syntax for user-supplied alternative names. --/ -syntax inductionAlt := "| " binderIdent+ " => " tacticSeq - -/-- - Parse the tactic syntax for user-supplied alternative names. - - This function returns a triple, which includes: - 1. the name of the constructor supplied by the user (`ctor.getId`), - 2. the alternative names for that constructor (`vars`), and - 3. the tactics for this induction subgoal. --/ -private def parseinductionAlt (alt : TSyntax `Iris.ProofMode.inductionAlt) : - TacticM (Name × Array (TSyntax `Lean.binderIdent) × TSyntax `Lean.Parser.Tactic.tacticSeq) := do - let `(inductionAlt| | $ctor:ident $vars:binderIdent* => $tac:tacticSeq) := alt - | throwError "iinduction: invalid syntax" - return ⟨ctor.getId, vars, tac⟩ - -/-- - Check whether a fully-qualified constructor name (e.g. `Nat.succ`) matches a - user-written short name (e.g. `succ`) or an already-qualified name. --/ -private def matchesCtorName (fullName : Name) (userShort : Name) : Bool := - fullName == userShort || fullName.getString! == userShort.getString! - /-- Throw an error if the user has supplied some but not all alternative names. -/ @@ -384,107 +384,66 @@ private def generalizeTermWithFVar (x : TSyntax `term) : TacticM FVarId := do return fvars[0]! +syntax (name := iinduction) "iinduction" colGt term + ("using" ident)? + ("generalizing" (colGt selPat)+)? + ("with" (colGe inductionAlt)*)? : tactic + /-- - Simple tactic with inaccessible names of variables and induction hypotheses - generated by Lean automatically. + The `iinduction` tactic applies induction in the Iris Proof Mode in a similar + way as the `induction` tactic. Given an expression `e`, the application of + `iinduction e` performs the following steps: + 1. Generalises the expression `e` using `Lean.Meta.Tactic.Generalize`. + 2. Reverts all hypotheses in the spatial context, as well as those in the + intuitionistic context that involves the variable to which induction is applied. + 3. Applies the built-in `induction` tactic in Lean to obtain the induction + subgoals. + 4. Moves all induction hypotheses into the intuitionstic context. + 5. Introduce hypotheses reverted in steps 2 and 3 back into the Iris contexts. + + Similar to the regular `induction` tactic, the following syntax is available. + - `iinduction e using r`: to specify the induction principle `r`. + - `iinduction e generalizing z₁ ... zₙ`: to generalise `z₁ ... zₙ`, + which is expressed as a selection pattern. Both Iris hypotheses and pure + Lean hypotheses can be generalised. + - `iinduction e with | constr₁ => tac₁ | ... | constrₙ => tacₙ`: + the constructor names can either be long (e.g., `Nat.zero`) or short (e.g., `zero`). + Arguments are optionally given names or otherwise remain inaccessible. + + As an example, consider the following Iris context, where `n : Nat`. + + ``` + ∗HP : P + □HQ : Q + □HR : R + ∗HS : S + □HT : T n + ``` + + By applying `iinduction n`, all spatial hypotheses (`HP` and `HS`) are + reverted. The hypothesis `HT` is also reverted because it involves `n`. + By applying `iinduction n generalizing HQ %R`, the hypotheses `HQ` and `HR` + are additionally reverted and thus included as premises in the induction + hypothesis. -/ -elab "iinduction" colGt x:term : tactic => do - let fvar ← generalizeTermWithFVar x - - ProofModeM.runTactic λ mvar { hyps, goal, .. } => do - let pf ← iInductionCore hyps goal fvar none none none - mvar.assign pf - -/-- Tactic with names of variables and induction hypotheses supplied by the user. -/ -elab "iinduction" colGt x:term "with" alts:(colGe inductionAlt)* : tactic => do - let fvar ← generalizeTermWithFVar x - - -- Parse the list of alternative names supplied by the user - let parsedAlts ← alts.mapM parseinductionAlt - - ProofModeM.runTactic λ mvar { hyps, goal, .. } => do - let pf ← iInductionCore hyps goal fvar parsedAlts none none - mvar.assign pf - -/-- Tactic with the recursor name supplied by the user. -/ -elab "iinduction" colGt x:term "using" r:ident : tactic => do - let fvar ← generalizeTermWithFVar x - - -- Parse the recursor name provided by the user - let recName := r.getId - - ProofModeM.runTactic λ mvar { hyps, goal, .. } => do - let pf ← iInductionCore hyps goal fvar none recName none - mvar.assign pf - -/-- Tactic with the recursor name and alternative names supplied by the user. -/ -elab "iinduction" colGt x:term "using" r:ident - "with" alts:(colGe inductionAlt)* : tactic => do - let fvar ← generalizeTermWithFVar x - - -- Parse the recursor name provided by the user - let recName := r.getId - -- Parse the list of alternative names supplied by the user - let parsedAlts ← alts.mapM parseinductionAlt - - ProofModeM.runTactic λ mvar { hyps, goal, .. } => do - let pf ← iInductionCore hyps goal fvar parsedAlts recName none - mvar.assign pf - -elab "iinduction" colGt x:term "generalizing" genSelPats:(colGt selPat)+ : tactic => do - let fvar ← generalizeTermWithFVar x - - ProofModeM.runTactic λ mvar { hyps, goal, .. } => do - -- Parse the user-supplied list of variables to be generalised - let genSelPats ← liftMacroM <| SelPat.parse genSelPats - let genSelTargets ← SelPat.resolve hyps genSelPats - - let pf ← iInductionCore hyps goal fvar none none genSelTargets - mvar.assign pf - -elab "iinduction" colGt x:term "generalizing" genSelPats:(colGt selPat)+ - "with" alts:(colGe inductionAlt)* : tactic => do - let fvar ← generalizeTermWithFVar x - - -- Parse the list of alternative names supplied by the user - let parsedAlts ← alts.mapM parseinductionAlt - - ProofModeM.runTactic λ mvar { hyps, goal, .. } => do - -- Parse the user-supplied list of variables to be generalised - let genSelPats ← liftMacroM <| SelPat.parse genSelPats - let genSelTargets ← SelPat.resolve hyps genSelPats - - let pf ← iInductionCore hyps goal fvar parsedAlts none genSelTargets - mvar.assign pf - -elab "iinduction" colGt x:term "using" r:ident - "generalizing" genSelPats:(colGt selPat)+ : tactic => do - let fvar ← generalizeTermWithFVar x - - -- Parse the recursor name provided by the user - let recName := r.getId - - ProofModeM.runTactic λ mvar { hyps, goal, .. } => do - -- Parse the user-supplied list of variables to be generalised - let genSelPats ← liftMacroM <| SelPat.parse genSelPats - let genSelTargets ← SelPat.resolve hyps genSelPats - - let pf ← iInductionCore hyps goal fvar none recName genSelTargets - mvar.assign pf - -elab "iinduction" colGt x:term "using" r:ident - "generalizing" genSelPats:(colGt selPat)+ "with" alts:(colGe inductionAlt)* : tactic => do - let fvar ← generalizeTermWithFVar x - - -- Parse the recursor name provided by the user - let recName := r.getId - -- Parse the list of alternative names supplied by the user - let parsedAlts ← alts.mapM parseinductionAlt - - ProofModeM.runTactic λ mvar { hyps, goal, .. } => do - -- Parse the user-supplied list of variables to be generalised - let genSelPats ← liftMacroM <| SelPat.parse genSelPats - let genSelTargets ← SelPat.resolve hyps genSelPats - - let pf ← iInductionCore hyps goal fvar parsedAlts recName genSelTargets - mvar.assign pf +elab_rules : tactic + | `(tactic| iinduction $x + $[using $r]? + $[generalizing $genSelPats*]? + $[with $alts*]?) => do + let fvar ← generalizeTermWithFVar x + + -- Parse the recursor name provided by the user + let recName : Option Name := r.map (·.getId) + + -- Parse the list of alternative names supplied by the user + let parsedAlts ← alts.mapM (·.mapM parseinductionAlt) + + ProofModeM.runTactic λ mvar { hyps, goal, .. } => do + -- Parse the user-supplied list of variables to be generalised + let genSelTargets ← genSelPats.mapM <| fun pats => do + let parsed ← liftMacroM <| SelPat.parse pats + SelPat.resolve hyps parsed + + let pf ← iInductionCore hyps goal fvar parsedAlts recName genSelTargets + mvar.assign pf diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index cb4c8d074..ac7c9ad6a 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2723,23 +2723,6 @@ section iinduction For natural numbers, `Nat.recAux` is used as the default recursor name. Hence, the tactic is equivalent to `iinduction n using Nat.recAux generalizing %P HQ %R`. - - Hypotheses in the spatial context necessarily become premises of the - induction hypothesis. The intuitionistic hypothesis `T n` is reverted - because it depends on `n`. - - With the `generalizing` syntax, `P` and `R` are universally quantified - in the induction hypothesis. Given they occur in `HP` and `HR`, respectively, - the two propositions are included as premises of the induction hypothesis. - - Meanwhile, `HQ` is included as a premise of the induction hypothesis without - `Q` being universally quantified. - - Note that the following variants of the tactic all produce equivalent subgoals: - - `induction n generalizing %P HP HQ %R` - - `induction n generalizing %P HQ %R HR` - - `induction n generalizing %P HP HQ %R HR` - - the tactics above with any permutations of `generalizing` arguments. -/ example [BI PROP] {P Q R S : PROP} {T : Nat → PROP} {n : Nat} : ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T n -∗ ⌜n + 0 = n⌝ := by From b22135d6ed42522a87c063107c3bccc690aa67bd Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Fri, 5 Jun 2026 15:01:58 +0200 Subject: [PATCH 073/181] Further generalise iRevertIntro, update usage of continuation function --- Iris/Iris/ProofMode/Tactics/Induction.lean | 2 +- Iris/Iris/ProofMode/Tactics/Loeb.lean | 2 +- Iris/Iris/ProofMode/Tactics/RevertIntro.lean | 15 ++++++++++----- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 82539f1fc..3ab6e0d0a 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -336,7 +336,7 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} let st ← addIHs pfIntHyps irisGoal.hyps ihFVars -- Generate the induction subgoal for the user, label the induction subgoal with `ctor` - let pf' ← withoutFVars (u := 0) ihFVars.toArray <| k st.newHyps irisGoal.goal ctor + let pf' ← withoutFVars (u := 0) ihFVars.toArray <| k st.newHyps irisGoal.goal (addBIGoal · · ctor) -- Fill the metavariable for the induction subgoal generated by Lean s.mvarId.assign <| q($(st.pf).trans $pf') diff --git a/Iris/Iris/ProofMode/Tactics/Loeb.lean b/Iris/Iris/ProofMode/Tactics/Loeb.lean index 5bf6e78bb..a122dc5ce 100644 --- a/Iris/Iris/ProofMode/Tactics/Loeb.lean +++ b/Iris/Iris/ProofMode/Tactics/Loeb.lean @@ -33,7 +33,7 @@ elab "iloeb" "as" IH:binderIdent "generalizing" hs:(colGt selPat)* : tactic => -- We have applied BI.loeb_wand_intuitionistically let goal := q(iprop(□ (□ ▷ $goal -∗ $goal))) iModIntroCore hyps goal (← `(_)) fun hyps goal => do - iIntroCore hyps goal [(IH, .intro <| .intuitionistic <| .one IH)] k + iIntroCore hyps goal [(IH, .intro <| .intuitionistic <| .one IH)] (k · · addBIGoal) return q($(pf').trans $pf) mvid.assign expr diff --git a/Iris/Iris/ProofMode/Tactics/RevertIntro.lean b/Iris/Iris/ProofMode/Tactics/RevertIntro.lean index a538b6e52..e567c1b38 100644 --- a/Iris/Iris/ProofMode/Tactics/RevertIntro.lean +++ b/Iris/Iris/ProofMode/Tactics/RevertIntro.lean @@ -13,16 +13,21 @@ open Lean Meta Elab.Tactic Qq public meta section -abbrev ProofModeContinuation := +abbrev ProofModeContinuationIntro := ∀ {u : Level} {prop : Q(Type u)} {bi : Q(BI $prop)} {e : Q($prop)} - (_hyps : Hyps bi e) (goal: Q($prop)) (_ : Name := Name.anonymous), + (_hyps : Hyps bi e) (goal: Q($prop)), + ProofModeM Q($e ⊢ $goal) + +abbrev ProofModeContinuationRevert := + ∀ {u : Level} {prop : Q(Type u)} {bi : Q(BI $prop)} {e : Q($prop)} + (_hyps : Hyps bi e) (goal : Q($prop)), ProofModeContinuationIntro → ProofModeM Q($e ⊢ $goal) def iRevertIntro {prop: Q(Type u)} {bi : Q(BI $prop)} {e : Q($prop)} (hyps : Hyps bi e) (goal: Q($prop)) (hs : List SelTarget) (k : ∀ {prop : Q(Type u)} {bi : Q(BI $prop)} {e : Q($prop)} - (_hyps : Hyps bi e) (goal: Q($prop)), ProofModeContinuation → + (_hyps : Hyps bi e) (goal: Q($prop)), ProofModeContinuationRevert → ProofModeM Q($e ⊢ $goal)) : ProofModeM Q($e ⊢ $goal) := do let names : List (Syntax × IntroPat) ← hs.mapM fun @@ -36,7 +41,7 @@ def iRevertIntro return (name, .intro <| (if ivar.persistent? then .intuitionistic else id) <| .one ident) trace[irevertintro] s!"Calling `iRevertIntro` with {names.map (·.1)} on context {←ppExpr <| IrisGoal.toExpr {hyps, goal ..}}" iRevertCore hs hyps goal fun hyps goal => do - k hyps goal fun hyps goal name => do - iIntroCore hyps goal names (addBIGoal · · name) + k hyps goal fun hyps goal k' => do + iIntroCore hyps goal names k' initialize registerTraceClass `irevertintro From fec0ee09ea061fe8cb7096f3c1c62a1e4a664040 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 6 Jun 2026 13:33:30 +0200 Subject: [PATCH 074/181] Move code that manipulates ProofModeM to ProofModeM.lean --- Iris/Iris/ProofMode/ProofModeM.lean | 24 ++++++++++++++++ Iris/Iris/ProofMode/Tactics/Induction.lean | 33 +++++++++------------- 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/Iris/Iris/ProofMode/ProofModeM.lean b/Iris/Iris/ProofMode/ProofModeM.lean index eb63c853a..83767c051 100644 --- a/Iris/Iris/ProofMode/ProofModeM.lean +++ b/Iris/Iris/ProofMode/ProofModeM.lean @@ -81,6 +81,30 @@ def addMVarGoal (m : MVarId) (name : Name := .anonymous) : ProofModeM Unit := do m.setUserName name modify ({goals := ·.goals.push m}) +/-- + Creates a new proof goal with the given hypotheses (`hyps`), conclusion + (`goal`) and run a sequence of tactics (`tacticSeq`) to the + newly generated goal before addition into the proof state. +-/ +def addBIGoalRunTactic {prop : Q(Type u)} {bi : Q(BI $prop)} + {e} (hyps : Hyps bi e) (goal : Q($prop)) (name : Name := .anonymous) + (tacticSeq : TSyntax `Lean.Parser.Tactic.tacticSeq) : + ProofModeM Q($e ⊢ $goal) := do + let m ← mkBIGoal hyps goal name + + -- Handle user-supplied tactics for specific constructors + modify <| fun s => { s with goals := s.goals.pop } + + -- Run the user tactic on the newly generated goal + let savedGoals ← getGoals + setGoals [m.mvarId!] + evalTactic tacticSeq + let remaining ← getGoals + setGoals savedGoals + for r in remaining do addMVarGoal r + + pure m + /-- Try to synthesize a typeclass instance, adding any created metavariables as proof mode goals. -/ def ProofModeM.trySynthInstanceQ (α : Q(Sort v)) : ProofModeM (Option Q($α)) := do let LOption.some (e, mvars) ← ProofMode.trySynthInstance α | return none diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 3ab6e0d0a..9f4b099b0 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -335,30 +335,23 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- Introduce the induction hypothesis back into the Iris proof state let st ← addIHs pfIntHyps irisGoal.hyps ihFVars - -- Generate the induction subgoal for the user, label the induction subgoal with `ctor` - let pf' ← withoutFVars (u := 0) ihFVars.toArray <| k st.newHyps irisGoal.goal (addBIGoal · · ctor) + -- Check + let tacticSeq := + match parsedAlts with + | none => none + | some parsedAlts => + match parsedAlts.find? <| matcher ctor with + | none => none + | some ⟨_, _, t⟩ => some t + + -- -- Generate the induction subgoal for the user, label the induction subgoal with `ctor` + let pf' ← match tacticSeq with + | none => withoutFVars (u := 0) ihFVars.toArray <| k st.newHyps irisGoal.goal (addBIGoal · · ctor) + | some tacticSeq => withoutFVars (u := 0) ihFVars.toArray <| k st.newHyps irisGoal.goal (addBIGoalRunTactic · · ctor tacticSeq) -- Fill the metavariable for the induction subgoal generated by Lean s.mvarId.assign <| q($(st.pf).trans $pf') - let biGoalMVar := (← getThe ProofModeM.State).goals.back! - - -- Handle user-supplied tactics for specific constructors - match parsedAlts with - | none => pure () - | some parsedAlts => - modify <| fun s => { s with goals := s.goals.pop } - - let some ⟨_, _, userTac⟩ := parsedAlts.find? <| matcher ctor - | throwMissingAlt ctor - - -- Run the user tactic on the BI goal - let savedGoals ← getGoals - setGoals [biGoalMVar] - evalTactic userTac - let remaining ← getGoals - setGoals savedGoals - for r in remaining do addMVarGoal r return m From 95c53c20d79be765f30cc93f1484e21c6c09ff46 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 6 Jun 2026 13:46:39 +0200 Subject: [PATCH 075/181] Simplify the handling of user-supplied tactics in iInductionCore --- Iris/Iris/ProofMode/Tactics/Induction.lean | 26 ++++++++++------------ 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 9f4b099b0..e3f1e934a 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -335,23 +335,21 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- Introduce the induction hypothesis back into the Iris proof state let st ← addIHs pfIntHyps irisGoal.hyps ihFVars - -- Check - let tacticSeq := - match parsedAlts with - | none => none - | some parsedAlts => - match parsedAlts.find? <| matcher ctor with - | none => none - | some ⟨_, _, t⟩ => some t - - -- -- Generate the induction subgoal for the user, label the induction subgoal with `ctor` + -- Check whether the user has supplied tactic sequences for this induction subgoal + let tacticSeq := parsedAlts.bind <| fun parsedAlts => + (parsedAlts.find? <| matcher ctor).map <| fun ⟨_, _, t⟩ => t + + -- Generate the induction subgoal for the user, label the induction subgoal with `ctor` let pf' ← match tacticSeq with - | none => withoutFVars (u := 0) ihFVars.toArray <| k st.newHyps irisGoal.goal (addBIGoal · · ctor) - | some tacticSeq => withoutFVars (u := 0) ihFVars.toArray <| k st.newHyps irisGoal.goal (addBIGoalRunTactic · · ctor tacticSeq) + | none => k st.newHyps irisGoal.goal (addBIGoal · · ctor) + -- Run the tactics supplied by the user, if available + | some tacticSeq => k st.newHyps irisGoal.goal (addBIGoalRunTactic · · ctor tacticSeq) - -- Fill the metavariable for the induction subgoal generated by Lean - s.mvarId.assign <| q($(st.pf).trans $pf') + -- Remove the induction hypotheses from the regular Lean context + let pf' ← withoutFVars (u := 0) ihFVars.toArray <| pure pf' + -- Fill the metavariable for the induction subgoal generated by Lean + s.mvarId.assign q($(st.pf).trans $pf') return m From d64a1015a7d36840dadc83e8f9a1efdda31bcead Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 6 Jun 2026 15:51:40 +0200 Subject: [PATCH 076/181] Eliminate custom-defined inductionAlt, reuse built-in definition instead --- Iris/Iris/ProofMode/Tactics/Induction.lean | 36 +++++++++++++--------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index e3f1e934a..32ea32df6 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -12,30 +12,36 @@ public meta import Iris.ProofMode.Patterns.CasesPattern public meta import Iris.ProofMode.ClassesMake public meta import Iris.ProofMode.Tactics.RevertIntro +public meta import Lean.Elab.Tactic.Induction +public meta import Lean.Parser.Tactic + namespace Iris.ProofMode public meta section -open BI Std Lean Elab Tactic Meta Qq +open BI Std Lean Elab Tactic Meta Qq Lean.Parser.Tactic /-- - Tactic syntax for user-supplied alternative names. --/ -syntax inductionAlt := "| " binderIdent+ " => " tacticSeq - -/-- - Parse the tactic syntax for user-supplied alternative names. + Parse the one branch of the tactic's `with` syntax for user-supplied alternative names. This function returns a triple, which includes: 1. the name of the constructor supplied by the user (`ctor.getId`), 2. the alternative names for that constructor (`vars`), and - 3. the tactics for this induction subgoal. + 3. the tactics for this induction subgoal (`tac`). -/ -private def parseinductionAlt (alt : TSyntax `Iris.ProofMode.inductionAlt) : +private def parseInductionAlt (alt : Syntax) : TacticM (Name × Array (TSyntax `Lean.binderIdent) × TSyntax `Lean.Parser.Tactic.tacticSeq) := do - let `(inductionAlt| | $ctor:ident $vars:binderIdent* => $tac:tacticSeq) := alt - | throwError "iinduction: invalid syntax" + let `(Lean.Parser.Tactic.inductionAlt| | $ctor:ident $[$vars]* => $tac:tacticSeq) := alt + | throwErrorAt alt "iinduction: invalid syntax" + let vars ← vars.mapM <| fun v => do + match v with + | `($id:ident) => `(binderIdent| $id:ident) + | _ => `(binderIdent| _) return ⟨ctor.getId, vars, tac⟩ +private def parseInductionAlts (alts : TSyntax `Lean.Parser.Tactic.inductionAlts) : + TacticM (Array <| Name × Array (TSyntax `Lean.binderIdent) × TSyntax `Lean.Parser.Tactic.tacticSeq) := do + return ← (alts : Syntax)[2].getArgs.mapM parseInductionAlt + /-- Check whether a fully-qualified constructor name (e.g. `Nat.succ`) matches a user-written short name (e.g. `succ`) or an already-qualified name. @@ -378,7 +384,7 @@ private def generalizeTermWithFVar (x : TSyntax `term) : TacticM FVarId := do syntax (name := iinduction) "iinduction" colGt term ("using" ident)? ("generalizing" (colGt selPat)+)? - ("with" (colGe inductionAlt)*)? : tactic + (inductionAlts)? : tactic /-- The `iinduction` tactic applies induction in the Iris Proof Mode in a similar @@ -421,14 +427,16 @@ elab_rules : tactic | `(tactic| iinduction $x $[using $r]? $[generalizing $genSelPats*]? - $[with $alts*]?) => do + $[$alts]?) => do let fvar ← generalizeTermWithFVar x -- Parse the recursor name provided by the user let recName : Option Name := r.map (·.getId) -- Parse the list of alternative names supplied by the user - let parsedAlts ← alts.mapM (·.mapM parseinductionAlt) + let parsedAlts ← match alts with + | none => pure none + | some alts => parseInductionAlts alts ProofModeM.runTactic λ mvar { hyps, goal, .. } => do -- Parse the user-supplied list of variables to be generalised From 4f2d10ba288b3ea6abb123d6252e73c1dcb4d31f Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 6 Jun 2026 16:08:44 +0200 Subject: [PATCH 077/181] Define structures for better code organisation --- Iris/Iris/ProofMode/Tactics/Induction.lean | 31 +++++++++++++--------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 32ea32df6..8820e2c93 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -20,6 +20,14 @@ namespace Iris.ProofMode public meta section open BI Std Lean Elab Tactic Meta Qq Lean.Parser.Tactic +private structure Alt where + ctor : Name + vars : Array (TSyntax `Lean.binderIdent) + tacs : TSyntax `Lean.Parser.Tactic.tacticSeq + +private structure Alts where + alts : Array Alt + /-- Parse the one branch of the tactic's `with` syntax for user-supplied alternative names. @@ -29,7 +37,7 @@ open BI Std Lean Elab Tactic Meta Qq Lean.Parser.Tactic 3. the tactics for this induction subgoal (`tac`). -/ private def parseInductionAlt (alt : Syntax) : - TacticM (Name × Array (TSyntax `Lean.binderIdent) × TSyntax `Lean.Parser.Tactic.tacticSeq) := do + TacticM Alt := do let `(Lean.Parser.Tactic.inductionAlt| | $ctor:ident $[$vars]* => $tac:tacticSeq) := alt | throwErrorAt alt "iinduction: invalid syntax" let vars ← vars.mapM <| fun v => do @@ -39,8 +47,8 @@ private def parseInductionAlt (alt : Syntax) : return ⟨ctor.getId, vars, tac⟩ private def parseInductionAlts (alts : TSyntax `Lean.Parser.Tactic.inductionAlts) : - TacticM (Array <| Name × Array (TSyntax `Lean.binderIdent) × TSyntax `Lean.Parser.Tactic.tacticSeq) := do - return ← (alts : Syntax)[2].getArgs.mapM parseInductionAlt + TacticM Alts := do + return ⟨← (alts : Syntax)[2].getArgs.mapM parseInductionAlt⟩ /-- Check whether a fully-qualified constructor name (e.g. `Nat.succ`) matches a @@ -217,8 +225,7 @@ private def checkCtors (ctors altCtors : List Name) : ProofModeM Unit := do -/ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} (hyps : Hyps bi e) (goal : Q($prop)) (fvar : FVarId) - (parsedAlts : Option <| Array <| - Name × Array (TSyntax `Lean.binderIdent) × TSyntax `Lean.Parser.Tactic.tacticSeq) + (parsedAlts : Option Alts) (altRecName : Option Name) (genSelTargets : Option <| List SelTarget) : ProofModeM Q($e ⊢ $goal) := do @@ -267,7 +274,7 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} let matcher : Name → - Name × Array (TSyntax `Lean.binderIdent) × TSyntax `Lean.Parser.Tactic.tacticSeq → + Alt → Bool := fun ctor ⟨altCtor, _, _⟩ => matchesCtorName ctor altCtor -- Find the constructor names @@ -276,7 +283,7 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- Check that all alternative names supplied by the user are valid match parsedAlts with | none => pure () - | some parsedAlts => checkCtors caseNames <| parsedAlts.toList.map Prod.fst + | some parsedAlts => checkCtors caseNames <| parsedAlts.alts.toList.map (·.ctor) -- Define the names for variables and induction hypotheses if supplied by user let varNames : Array AltVarNames ← @@ -285,9 +292,9 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} pure <| caseNames.toArray.map <| fun _ => { explicit := true, varNames := [] } | some parsedAlts => do caseNames.toArray.mapM <| fun ctor => - match parsedAlts.find? <| matcher ctor with - | some (_, vars, _) => - pure { explicit := true, varNames := vars.toList.map <| + match parsedAlts.alts.find? <| matcher ctor with + | some alt => + pure { explicit := true, varNames := alt.vars.toList.map <| fun v => match v.raw with | `(binderIdent| $id:ident) => id.getId @@ -311,7 +318,7 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} | none => pure () | some parsedAlts => s.mvarId.setTag ctor - match parsedAlts.find? <| matcher ctor with + match parsedAlts.alts.find? <| matcher ctor with | some ⟨_, vars, _⟩ => if vars.size > s.fields.size then throwError @@ -343,7 +350,7 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- Check whether the user has supplied tactic sequences for this induction subgoal let tacticSeq := parsedAlts.bind <| fun parsedAlts => - (parsedAlts.find? <| matcher ctor).map <| fun ⟨_, _, t⟩ => t + (parsedAlts.alts.find? <| matcher ctor).map <| fun ⟨_, _, t⟩ => t -- Generate the induction subgoal for the user, label the induction subgoal with `ctor` let pf' ← match tacticSeq with From 6b43be8a64d9385d4c30c76af145a7ae83d739d6 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 6 Jun 2026 16:23:45 +0200 Subject: [PATCH 078/181] Implement tactic before alts --- Iris/Iris/ProofMode/Tactics/Induction.lean | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 8820e2c93..e73c21012 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -20,12 +20,20 @@ namespace Iris.ProofMode public meta section open BI Std Lean Elab Tactic Meta Qq Lean.Parser.Tactic +/-- + Information obtained from parsing a case under the `with` syntax +-/ private structure Alt where + -- The name of the constructor ctor : Name + -- The alternative names supplied by the tactic user vars : Array (TSyntax `Lean.binderIdent) + -- User-supplied tactics for this case tacs : TSyntax `Lean.Parser.Tactic.tacticSeq private structure Alts where + -- User-supplied tactic to be applied before splitting into cases + tac : TSyntax `tactic alts : Array Alt /-- @@ -48,11 +56,13 @@ private def parseInductionAlt (alt : Syntax) : private def parseInductionAlts (alts : TSyntax `Lean.Parser.Tactic.inductionAlts) : TacticM Alts := do - return ⟨← (alts : Syntax)[2].getArgs.mapM parseInductionAlt⟩ + let `(tactic| $tac) := (alts : Syntax)[0] + return ⟨tac, ← (alts : Syntax)[2].getArgs.mapM parseInductionAlt⟩ /-- Check whether a fully-qualified constructor name (e.g. `Nat.succ`) matches a - user-written short name (e.g. `succ`) or an already-qualified name. + u + ser-written short name (e.g. `succ`) or an already-qualified name. -/ private def matchesCtorName (fullName : Name) (userShort : Name) : Bool := fullName == userShort || fullName.getString! == userShort.getString! @@ -350,7 +360,7 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- Check whether the user has supplied tactic sequences for this induction subgoal let tacticSeq := parsedAlts.bind <| fun parsedAlts => - (parsedAlts.alts.find? <| matcher ctor).map <| fun ⟨_, _, t⟩ => t + (parsedAlts.alts.find? <| matcher ctor).map (·.tacs) -- Generate the induction subgoal for the user, label the induction subgoal with `ctor` let pf' ← match tacticSeq with From 32bb9fb716a08b88368df7d3add1f2baa67ae2e2 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 6 Jun 2026 16:28:11 +0200 Subject: [PATCH 079/181] Typo fix --- Iris/Iris/ProofMode/Tactics/Induction.lean | 3 +-- Iris/Iris/Tests/Tactics.lean | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index e73c21012..52f3b3fc0 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -61,8 +61,7 @@ private def parseInductionAlts (alts : TSyntax `Lean.Parser.Tactic.inductionAlts /-- Check whether a fully-qualified constructor name (e.g. `Nat.succ`) matches a - u - ser-written short name (e.g. `succ`) or an already-qualified name. + user-written short name (e.g. `succ`) or an already-qualified name. -/ private def matchesCtorName (fullName : Name) (userShort : Name) : Bool := fullName == userShort || fullName.getString! == userShort.getString! diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index ac7c9ad6a..122b0c763 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2789,4 +2789,20 @@ example [BI PROP] {α} {t : Tree α} {P : Tree α → PROP} : · iexact ih1 · iexact ih2 + +example [BI PROP] {P Q R S T : PROP} {n : Nat} : + ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by + iintro HP #HQ #HR HS #HT + induction n with simp + | zero | succ => itrivial + + -- | invalidA => done + -- | zero => itrivial + -- | invalidB => done + -- | succ => itrivial + -- | invalidC => done + + + + end iinduction From 43562d1552a3a5d0401bf8a9e286e3e54bb6f530 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 6 Jun 2026 17:08:48 +0200 Subject: [PATCH 080/181] Allowing multiple constructors for the same set of tactic sequences --- Iris/Iris/ProofMode/Tactics/Induction.lean | 46 +++++++++++++--------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 52f3b3fc0..3fc45b783 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -36,28 +36,38 @@ private structure Alts where tac : TSyntax `tactic alts : Array Alt -/-- - Parse the one branch of the tactic's `with` syntax for user-supplied alternative names. - - This function returns a triple, which includes: - 1. the name of the constructor supplied by the user (`ctor.getId`), - 2. the alternative names for that constructor (`vars`), and - 3. the tactics for this induction subgoal (`tac`). --/ -private def parseInductionAlt (alt : Syntax) : - TacticM Alt := do - let `(Lean.Parser.Tactic.inductionAlt| | $ctor:ident $[$vars]* => $tac:tacticSeq) := alt - | throwErrorAt alt "iinduction: invalid syntax" - let vars ← vars.mapM <| fun v => do - match v with - | `($id:ident) => `(binderIdent| $id:ident) - | _ => `(binderIdent| _) - return ⟨ctor.getId, vars, tac⟩ +private def parseInductionAltLHS (lhs : Syntax) : + TacticM (Name × Array (TSyntax `Lean.binderIdent)) := do + let parseVars (vars : Array Syntax) : TacticM (Array (TSyntax `Lean.binderIdent)) := do + vars.mapM fun v => + match v with + | `(ident| $id:ident) => `(binderIdent| $id:ident) + | _ => `(binderIdent| _) + + match lhs with + | `(Lean.Parser.Tactic.inductionAltLHS| | $ctor:ident $[$vars]*) + | `(Lean.Parser.Tactic.inductionAltLHS| | @ $ctor:ident $[$vars]*) => + return (ctor.getId, ← parseVars vars) + | `(Lean.Parser.Tactic.inductionAltLHS| | $_:hole $[$vars]*) => + return (.anonymous, ← parseVars vars) + | _ => throwErrorAt lhs "iinduction: invalid syntax" private def parseInductionAlts (alts : TSyntax `Lean.Parser.Tactic.inductionAlts) : TacticM Alts := do + -- The user-supplied tactic to be applied before splitting into cases let `(tactic| $tac) := (alts : Syntax)[0] - return ⟨tac, ← (alts : Syntax)[2].getArgs.mapM parseInductionAlt⟩ + + let mut parsedAlts : Array Alt := #[] + + for alt in (alts : Syntax)[2].getArgs do + let `(inductionAlt| $[$lhs:inductionAltLHS]* => $tac:tacticSeq) := alt + | throwErrorAt alt "iinduction: invalid syntax" + + for l in lhs do + let ⟨ctor, vars⟩ ← parseInductionAltLHS l + parsedAlts := parsedAlts.push ⟨ctor, vars, tac⟩ + + return ⟨tac, parsedAlts⟩ /-- Check whether a fully-qualified constructor name (e.g. `Nat.succ`) matches a From d7408bbf61313163ffd463c107b5488a0083b27d Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 6 Jun 2026 17:54:27 +0200 Subject: [PATCH 081/181] Simplify parsing --- Iris/Iris/ProofMode/Tactics/Induction.lean | 23 +++++++++------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 3fc45b783..e363599eb 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -36,24 +36,14 @@ private structure Alts where tac : TSyntax `tactic alts : Array Alt -private def parseInductionAltLHS (lhs : Syntax) : - TacticM (Name × Array (TSyntax `Lean.binderIdent)) := do +private def parseInductionAlts (alts : TSyntax `Lean.Parser.Tactic.inductionAlts) : + TacticM Alts := do let parseVars (vars : Array Syntax) : TacticM (Array (TSyntax `Lean.binderIdent)) := do vars.mapM fun v => match v with | `(ident| $id:ident) => `(binderIdent| $id:ident) | _ => `(binderIdent| _) - match lhs with - | `(Lean.Parser.Tactic.inductionAltLHS| | $ctor:ident $[$vars]*) - | `(Lean.Parser.Tactic.inductionAltLHS| | @ $ctor:ident $[$vars]*) => - return (ctor.getId, ← parseVars vars) - | `(Lean.Parser.Tactic.inductionAltLHS| | $_:hole $[$vars]*) => - return (.anonymous, ← parseVars vars) - | _ => throwErrorAt lhs "iinduction: invalid syntax" - -private def parseInductionAlts (alts : TSyntax `Lean.Parser.Tactic.inductionAlts) : - TacticM Alts := do -- The user-supplied tactic to be applied before splitting into cases let `(tactic| $tac) := (alts : Syntax)[0] @@ -64,8 +54,13 @@ private def parseInductionAlts (alts : TSyntax `Lean.Parser.Tactic.inductionAlts | throwErrorAt alt "iinduction: invalid syntax" for l in lhs do - let ⟨ctor, vars⟩ ← parseInductionAltLHS l - parsedAlts := parsedAlts.push ⟨ctor, vars, tac⟩ + match l with + | `(Lean.Parser.Tactic.inductionAltLHS| | $ctor:ident $[$vars]*) + | `(Lean.Parser.Tactic.inductionAltLHS| | @ $ctor:ident $[$vars]*) => + parsedAlts := parsedAlts.push ⟨ctor.getId, ← parseVars vars, tac⟩ + | `(Lean.Parser.Tactic.inductionAltLHS| | $_:hole $[$vars]*) => + parsedAlts := parsedAlts.push ⟨.anonymous, ← parseVars vars, tac⟩ + | _ => throwErrorAt l "iinduction: invalid syntax" return ⟨tac, parsedAlts⟩ From 024b975a79f7d3b9650adf4e90d06ca0b136c9ac Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 6 Jun 2026 21:01:44 +0200 Subject: [PATCH 082/181] Debug: remove incorrect popping of goal from ProofModeM state --- Iris/Iris/ProofMode/ProofModeM.lean | 3 --- 1 file changed, 3 deletions(-) diff --git a/Iris/Iris/ProofMode/ProofModeM.lean b/Iris/Iris/ProofMode/ProofModeM.lean index 83767c051..6e574b19e 100644 --- a/Iris/Iris/ProofMode/ProofModeM.lean +++ b/Iris/Iris/ProofMode/ProofModeM.lean @@ -92,9 +92,6 @@ def addBIGoalRunTactic {prop : Q(Type u)} {bi : Q(BI $prop)} ProofModeM Q($e ⊢ $goal) := do let m ← mkBIGoal hyps goal name - -- Handle user-supplied tactics for specific constructors - modify <| fun s => { s with goals := s.goals.pop } - -- Run the user tactic on the newly generated goal let savedGoals ← getGoals setGoals [m.mvarId!] From b0bd59f75ad1a9ba4b3b6eb018c66528023671b8 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 6 Jun 2026 21:02:58 +0200 Subject: [PATCH 083/181] Implement hole and synthetic hole on RHS of `=>` using `with` syntax --- Iris/Iris/ProofMode/Tactics/Induction.lean | 17 +++++++------ Iris/Iris/Tests/Tactics.lean | 28 ++++++++++++++++------ 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index e363599eb..74488719f 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -28,8 +28,8 @@ private structure Alt where ctor : Name -- The alternative names supplied by the tactic user vars : Array (TSyntax `Lean.binderIdent) - -- User-supplied tactics for this case - tacs : TSyntax `Lean.Parser.Tactic.tacticSeq + -- User-supplied tactics for this case, `none` if a hole (`_` or `?_`) is given + tacs : Option <| TSyntax `Lean.Parser.Tactic.tacticSeq private structure Alts where -- User-supplied tactic to be applied before splitting into cases @@ -50,16 +50,19 @@ private def parseInductionAlts (alts : TSyntax `Lean.Parser.Tactic.inductionAlts let mut parsedAlts : Array Alt := #[] for alt in (alts : Syntax)[2].getArgs do - let `(inductionAlt| $[$lhs:inductionAltLHS]* => $tac:tacticSeq) := alt - | throwErrorAt alt "iinduction: invalid syntax" + let (lhs, tacs) ← match alt with + | `(inductionAlt| $[$lhs:inductionAltLHS]* => $tac:tacticSeq) => pure (lhs, some tac) + | `(inductionAlt| $[$lhs:inductionAltLHS]* => $_:hole) => pure (lhs, none) + | `(inductionAlt| $[$lhs:inductionAltLHS]* => $_:syntheticHole) => pure (lhs, none) + | _ => throwErrorAt alt "iinduction: invalid syntax" for l in lhs do match l with | `(Lean.Parser.Tactic.inductionAltLHS| | $ctor:ident $[$vars]*) | `(Lean.Parser.Tactic.inductionAltLHS| | @ $ctor:ident $[$vars]*) => - parsedAlts := parsedAlts.push ⟨ctor.getId, ← parseVars vars, tac⟩ + parsedAlts := parsedAlts.push ⟨ctor.getId, ← parseVars vars, tacs⟩ | `(Lean.Parser.Tactic.inductionAltLHS| | $_:hole $[$vars]*) => - parsedAlts := parsedAlts.push ⟨.anonymous, ← parseVars vars, tac⟩ + parsedAlts := parsedAlts.push ⟨.anonymous, ← parseVars vars, tacs⟩ | _ => throwErrorAt l "iinduction: invalid syntax" return ⟨tac, parsedAlts⟩ @@ -364,7 +367,7 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- Check whether the user has supplied tactic sequences for this induction subgoal let tacticSeq := parsedAlts.bind <| fun parsedAlts => - (parsedAlts.alts.find? <| matcher ctor).map (·.tacs) + (parsedAlts.alts.find? <| matcher ctor).bind (·.tacs) -- Generate the induction subgoal for the user, label the induction subgoal with `ctor` let pf' ← match tacticSeq with diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 122b0c763..2fce1fe57 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2789,20 +2789,34 @@ example [BI PROP] {α} {t : Tree α} {P : Tree α → PROP} : · iexact ih1 · iexact ih2 - example [BI PROP] {P Q R S T : PROP} {n : Nat} : ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by iintro HP #HQ #HR HS #HT induction n with simp | zero | succ => itrivial - -- | invalidA => done - -- | zero => itrivial - -- | invalidB => done - -- | succ => itrivial - -- | invalidC => done - +example [BI PROP] {P Q R S T : PROP} {n : Nat} : + ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by + iintro HP #HQ #HR HS #HT + iinduction n with + | zero | succ n ih => _ + itrivial + itrivial +example [BI PROP] {P Q R S T : PROP} {n : Nat} : + ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by + iintro HP #HQ #HR HS #HT + iinduction n with + | zero => itrivial + | succ n ih => _ + itrivial +example [BI PROP] {P Q R S T : PROP} {n : Nat} : + ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by + iintro HP #HQ #HR HS #HT + iinduction n with + | zero => _ + | succ n _ => itrivial + itrivial end iinduction From a333ac36876bc48cb7a56d765f7cb674fcd40bc1 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 6 Jun 2026 21:12:43 +0200 Subject: [PATCH 084/181] Use short syntax name --- Iris/Iris/ProofMode/Tactics/Induction.lean | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 74488719f..98100a786 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -58,10 +58,10 @@ private def parseInductionAlts (alts : TSyntax `Lean.Parser.Tactic.inductionAlts for l in lhs do match l with - | `(Lean.Parser.Tactic.inductionAltLHS| | $ctor:ident $[$vars]*) - | `(Lean.Parser.Tactic.inductionAltLHS| | @ $ctor:ident $[$vars]*) => + | `(inductionAltLHS| | $ctor:ident $[$vars]*) + | `(inductionAltLHS| | @ $ctor:ident $[$vars]*) => parsedAlts := parsedAlts.push ⟨ctor.getId, ← parseVars vars, tacs⟩ - | `(Lean.Parser.Tactic.inductionAltLHS| | $_:hole $[$vars]*) => + | `(inductionAltLHS| | $_:hole $[$vars]*) => parsedAlts := parsedAlts.push ⟨.anonymous, ← parseVars vars, tacs⟩ | _ => throwErrorAt l "iinduction: invalid syntax" From cf43a443ce04019882e8469693e252d8765ef583 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 6 Jun 2026 21:18:05 +0200 Subject: [PATCH 085/181] Improve pattern matching in parser --- Iris/Iris/ProofMode/Tactics/Induction.lean | 45 ++++++++++++---------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 98100a786..28fbdf384 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -33,7 +33,8 @@ private structure Alt where private structure Alts where -- User-supplied tactic to be applied before splitting into cases - tac : TSyntax `tactic + tac : Option <| TSyntax `tactic + -- The alternative cases supplied by the user alts : Array Alt private def parseInductionAlts (alts : TSyntax `Lean.Parser.Tactic.inductionAlts) : @@ -44,28 +45,30 @@ private def parseInductionAlts (alts : TSyntax `Lean.Parser.Tactic.inductionAlts | `(ident| $id:ident) => `(binderIdent| $id:ident) | _ => `(binderIdent| _) - -- The user-supplied tactic to be applied before splitting into cases - let `(tactic| $tac) := (alts : Syntax)[0] - let mut parsedAlts : Array Alt := #[] - for alt in (alts : Syntax)[2].getArgs do - let (lhs, tacs) ← match alt with - | `(inductionAlt| $[$lhs:inductionAltLHS]* => $tac:tacticSeq) => pure (lhs, some tac) - | `(inductionAlt| $[$lhs:inductionAltLHS]* => $_:hole) => pure (lhs, none) - | `(inductionAlt| $[$lhs:inductionAltLHS]* => $_:syntheticHole) => pure (lhs, none) - | _ => throwErrorAt alt "iinduction: invalid syntax" - - for l in lhs do - match l with - | `(inductionAltLHS| | $ctor:ident $[$vars]*) - | `(inductionAltLHS| | @ $ctor:ident $[$vars]*) => - parsedAlts := parsedAlts.push ⟨ctor.getId, ← parseVars vars, tacs⟩ - | `(inductionAltLHS| | $_:hole $[$vars]*) => - parsedAlts := parsedAlts.push ⟨.anonymous, ← parseVars vars, tacs⟩ - | _ => throwErrorAt l "iinduction: invalid syntax" - - return ⟨tac, parsedAlts⟩ + match alts with + | `(inductionAlts| with $[$cases]*) => + for alt in cases do + let (lhs, tacs) ← match alt with + | `(inductionAlt| $[$lhs:inductionAltLHS]* => $tac:tacticSeq) => pure (lhs, some tac) + | `(inductionAlt| $[$lhs:inductionAltLHS]* => $_:hole) => pure (lhs, none) + | `(inductionAlt| $[$lhs:inductionAltLHS]* => $_:syntheticHole) => pure (lhs, none) + | _ => throwErrorAt alt "iinduction: invalid syntax" + + for l in lhs do + match l with + | `(inductionAltLHS| | $ctor:ident $[$vars]*) + | `(inductionAltLHS| | @ $ctor:ident $[$vars]*) => + parsedAlts := parsedAlts.push ⟨ctor.getId, ← parseVars vars, tacs⟩ + | `(inductionAltLHS| | $_:hole $[$vars]*) => + parsedAlts := parsedAlts.push ⟨.anonymous, ← parseVars vars, tacs⟩ + | _ => throwErrorAt l "iinduction: invalid syntax" + | `(inductionAlts| with $tac $[$cases]*) => + throwErrorAt tac "iinduction: unsupported syntax, to be implemented" + | _ => throwErrorAt alts "iinduction: invalid syntax" + + return ⟨none, parsedAlts⟩ /-- Check whether a fully-qualified constructor name (e.g. `Nat.succ`) matches a From 4def6f061411db48da55b1bd42fb1d22ec497363 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 6 Jun 2026 21:19:33 +0200 Subject: [PATCH 086/181] Simplify imports --- Iris/Iris/ProofMode/Tactics/Induction.lean | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 28fbdf384..e20d4a993 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -11,14 +11,12 @@ public meta import Iris.ProofMode.Tactics.Cases public meta import Iris.ProofMode.Patterns.CasesPattern public meta import Iris.ProofMode.ClassesMake public meta import Iris.ProofMode.Tactics.RevertIntro - public meta import Lean.Elab.Tactic.Induction -public meta import Lean.Parser.Tactic namespace Iris.ProofMode public meta section -open BI Std Lean Elab Tactic Meta Qq Lean.Parser.Tactic +open BI Std Lean Elab Tactic Meta Qq Parser.Tactic /-- Information obtained from parsing a case under the `with` syntax From c53d592fab947d7af893762be48cdc498bd75df5 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 6 Jun 2026 21:43:36 +0200 Subject: [PATCH 087/181] Towards supporting wildcard --- Iris/Iris/ProofMode/Tactics/Induction.lean | 22 ++++++++++++++++------ Iris/Iris/Tests/Tactics.lean | 7 +++++-- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index e20d4a993..e18c96ef5 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -34,6 +34,8 @@ private structure Alts where tac : Option <| TSyntax `tactic -- The alternative cases supplied by the user alts : Array Alt + -- The wildcard case, if supplied by the user + wildcard : Option Alt private def parseInductionAlts (alts : TSyntax `Lean.Parser.Tactic.inductionAlts) : TacticM Alts := do @@ -60,13 +62,17 @@ private def parseInductionAlts (alts : TSyntax `Lean.Parser.Tactic.inductionAlts | `(inductionAltLHS| | @ $ctor:ident $[$vars]*) => parsedAlts := parsedAlts.push ⟨ctor.getId, ← parseVars vars, tacs⟩ | `(inductionAltLHS| | $_:hole $[$vars]*) => - parsedAlts := parsedAlts.push ⟨.anonymous, ← parseVars vars, tacs⟩ + if parsedAlts.size < cases.size - 1 then + throwErrorAt alt + s!"iinduction: invalid occurrence of the wildcard alternative `| _ => ...`:".append + "It must be the last alternative" + return ⟨none, parsedAlts, some ⟨.anonymous, ← parseVars vars, tacs⟩⟩ | _ => throwErrorAt l "iinduction: invalid syntax" | `(inductionAlts| with $tac $[$cases]*) => throwErrorAt tac "iinduction: unsupported syntax, to be implemented" | _ => throwErrorAt alts "iinduction: invalid syntax" - return ⟨none, parsedAlts⟩ + return ⟨none, parsedAlts, none⟩ /-- Check whether a fully-qualified constructor name (e.g. `Nat.succ`) matches a @@ -195,12 +201,16 @@ private def throwMissingAlt {α} (ctor : Name) : ProofModeM α := Checks whether an invalid, missing and/or duplicate constructor names have been supplied by the user. Throw an error if this is the case. -/ -private def checkCtors (ctors altCtors : List Name) : ProofModeM Unit := do +private def checkCtors (ctors : List Name) (parsedAlts : Alts) : ProofModeM Unit := do + -- Find the list of constructor names given by the user + let altCtors := parsedAlts.alts.toList.map (·.ctor) + let mut errors : List String := [] -- Check for missing constructor names - let missingAltCtors := - ctors.filter (not <| altCtors.any <| fun altCtor => matchesCtorName · altCtor) + let missingAltCtors := match parsedAlts.wildcard with + | none => ctors.filter (not <| altCtors.any <| fun altCtor => matchesCtorName · altCtor) + | _ => [] -- Check for invalid constructor names let invalidAltCtors := @@ -301,7 +311,7 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- Check that all alternative names supplied by the user are valid match parsedAlts with | none => pure () - | some parsedAlts => checkCtors caseNames <| parsedAlts.alts.toList.map (·.ctor) + | some parsedAlts => checkCtors caseNames parsedAlts -- Define the names for variables and induction hypotheses if supplied by user let varNames : Array AltVarNames ← diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 2fce1fe57..4f190a96a 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2799,7 +2799,8 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by iintro HP #HQ #HR HS #HT iinduction n with - | zero | succ n ih => _ + | zero => ?_ + | succ n ih => _ itrivial itrivial @@ -2807,8 +2808,10 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by iintro HP #HQ #HR HS #HT iinduction n with - | zero => itrivial + | zero => itrivial | succ n ih => _ + | _ => _ + itrivial example [BI PROP] {P Q R S T : PROP} {n : Nat} : From a023c04662c7ec0b21a612f976fdae9cd4d77f38 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 6 Jun 2026 21:52:39 +0200 Subject: [PATCH 088/181] Implement wildcard as last alternative --- Iris/Iris/ProofMode/Tactics/Induction.lean | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index e18c96ef5..cac8a5f82 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -300,27 +300,25 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} | _ => throwError "iinduction: {indName} is not inductive" | _ => throwError "iinduction: unable to determine inductive type" - let matcher : - Name → - Alt → - Bool := fun ctor ⟨altCtor, _, _⟩ => matchesCtorName ctor altCtor + let matcher : Name → Alt → Bool := + fun ctor alt => alt.ctor != .anonymous && matchesCtorName ctor alt.ctor -- Find the constructor names - let caseNames := ((← Lean.Meta.getElimInfo recName).altsInfo.map (·.name)).toList + let recCtors := ((← Lean.Meta.getElimInfo recName).altsInfo.map (·.name)).toList -- Check that all alternative names supplied by the user are valid match parsedAlts with | none => pure () - | some parsedAlts => checkCtors caseNames parsedAlts + | some parsedAlts => checkCtors recCtors parsedAlts -- Define the names for variables and induction hypotheses if supplied by user let varNames : Array AltVarNames ← match parsedAlts with | none => - pure <| caseNames.toArray.map <| fun _ => { explicit := true, varNames := [] } + pure <| recCtors.toArray.map <| fun _ => { explicit := true, varNames := [] } | some parsedAlts => do - caseNames.toArray.mapM <| fun ctor => - match parsedAlts.alts.find? <| matcher ctor with + recCtors.toArray.mapM <| fun ctor => + match parsedAlts.alts.find? (matcher ctor) <|> parsedAlts.wildcard with | some alt => pure { explicit := true, varNames := alt.vars.toList.map <| fun v => @@ -339,14 +337,14 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} m.mvarId!.induction fvar recName varNames -- Handle each subgoal generated by Lean's induction - for ⟨s, ctor⟩ in subgoals.toList.zip caseNames do + for ⟨s, ctor⟩ in subgoals.toList.zip recCtors do s.mvarId.withContext do -- For pretty printing of arguments match parsedAlts with | none => pure () | some parsedAlts => s.mvarId.setTag ctor - match parsedAlts.alts.find? <| matcher ctor with + match parsedAlts.alts.find? (matcher ctor) <|> parsedAlts.wildcard with | some ⟨_, vars, _⟩ => if vars.size > s.fields.size then throwError @@ -378,7 +376,7 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- Check whether the user has supplied tactic sequences for this induction subgoal let tacticSeq := parsedAlts.bind <| fun parsedAlts => - (parsedAlts.alts.find? <| matcher ctor).bind (·.tacs) + (parsedAlts.alts.find? (matcher ctor) <|> parsedAlts.wildcard).bind (·.tacs) -- Generate the induction subgoal for the user, label the induction subgoal with `ctor` let pf' ← match tacticSeq with From 931eabb4e4845e1eee5fd69a9c4b6993e12fb9c2 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 6 Jun 2026 22:14:46 +0200 Subject: [PATCH 089/181] More syntax tests --- Iris/Iris/ProofMode/Tactics/Induction.lean | 7 +++---- Iris/Iris/Tests/Tactics.lean | 22 ++++++++++++++-------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index cac8a5f82..8af266fb8 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -321,10 +321,9 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} match parsedAlts.alts.find? (matcher ctor) <|> parsedAlts.wildcard with | some alt => pure { explicit := true, varNames := alt.vars.toList.map <| - fun v => - match v.raw with - | `(binderIdent| $id:ident) => id.getId - | _ => Name.mkSimple "_" } + (match ·.raw with + | `(binderIdent| $id:ident) => id.getId + | _ => Name.mkSimple "_") } | none => throwMissingAlt ctor let pf ← iRevertIntro hyps goal targets <| diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 4f190a96a..d665971f4 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2789,12 +2789,14 @@ example [BI PROP] {α} {t : Tree α} {P : Tree α → PROP} : · iexact ih1 · iexact ih2 +/-- Testing `iinduction` with the same tactic sequence for two constructors -/ example [BI PROP] {P Q R S T : PROP} {n : Nat} : ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by iintro HP #HQ #HR HS #HT - induction n with simp + iinduction n with | zero | succ => itrivial +/-- Testing `iinduction` with the hole and synthetic hole -/ example [BI PROP] {P Q R S T : PROP} {n : Nat} : ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by iintro HP #HQ #HR HS #HT @@ -2804,22 +2806,26 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : itrivial itrivial +/- Testing `iinduction` with the hole and synthetic hole -/ +/-- error: iinduction: invalid occurrence of the wildcard alternative `| _ => ...`:It must be the last alternative -/ +#guard_msgs in example [BI PROP] {P Q R S T : PROP} {n : Nat} : ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by iintro HP #HQ #HR HS #HT iinduction n with | zero => itrivial - | succ n ih => _ | _ => _ + | succ n ih => itrivial - itrivial - +/- Testing `iinduction` with the hole and synthetic hole -/ +/-- error: iinduction: wildcard alternative is not needed -/ +#guard_msgs in example [BI PROP] {P Q R S T : PROP} {n : Nat} : ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by iintro HP #HQ #HR HS #HT - iinduction n with - | zero => _ - | succ n _ => itrivial - itrivial + induction n with + | zero => itrivial + | succ n ih => itrivial + | _ => _ end iinduction From 8f19e97b3a5d17939cfac449c1408acdcc818784 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 6 Jun 2026 22:26:33 +0200 Subject: [PATCH 090/181] Check for redundant wildcard alternative --- Iris/Iris/ProofMode/Tactics/Induction.lean | 5 ++++- Iris/Iris/Tests/Tactics.lean | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 8af266fb8..33234a75e 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -309,7 +309,10 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- Check that all alternative names supplied by the user are valid match parsedAlts with | none => pure () - | some parsedAlts => checkCtors recCtors parsedAlts + | some parsedAlts => + checkCtors recCtors parsedAlts + if recCtors.length == parsedAlts.alts.size && parsedAlts.wildcard.isSome then + throwError "iinduction: wildcard alternative is not needed" -- Define the names for variables and induction hypotheses if supplied by user let varNames : Array AltVarNames ← diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index d665971f4..202eecc90 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2823,7 +2823,7 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : example [BI PROP] {P Q R S T : PROP} {n : Nat} : ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by iintro HP #HQ #HR HS #HT - induction n with + iinduction n with | zero => itrivial | succ n ih => itrivial | _ => _ From f2fb850d518f1848ed4f3096f3300a14a3b6149b Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 6 Jun 2026 22:31:33 +0200 Subject: [PATCH 091/181] More tests for wildcard syntax --- Iris/Iris/Tests/Tactics.lean | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 202eecc90..10a628599 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2806,6 +2806,20 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : itrivial itrivial +/-- Testing `iinduction` with wildcard for one case -/ +example [BI PROP] {P Q R S T : PROP} {n : Nat} : + ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by + iintro HP #HQ #HR HS #HT + iinduction n with + | zero | _ => itrivial + +/-- Testing `iinduction` with wildcard for two cases -/ +example [BI PROP] {P Q R S T : PROP} {n : Nat} : + ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by + iintro HP #HQ #HR HS #HT + iinduction n with + | _ => itrivial + /- Testing `iinduction` with the hole and synthetic hole -/ /-- error: iinduction: invalid occurrence of the wildcard alternative `| _ => ...`:It must be the last alternative -/ #guard_msgs in From 0127a9f4598afc0e6931ef71cae2e1d0790cd583 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 6 Jun 2026 22:43:27 +0200 Subject: [PATCH 092/181] Minor refinements --- Iris/Iris/ProofMode/Tactics/Induction.lean | 26 ++++++++++++---------- Iris/Iris/Tests/Tactics.lean | 7 ++++++ 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 33234a75e..ed704c0e8 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -37,8 +37,13 @@ private structure Alts where -- The wildcard case, if supplied by the user wildcard : Option Alt -private def parseInductionAlts (alts : TSyntax `Lean.Parser.Tactic.inductionAlts) : +/-- + Given user-supplied alternative names and tactic sequences, parse the syntax + and return an `Alts` instance. +-/ +private def parseInductionAlts (altsSyntax : TSyntax `Lean.Parser.Tactic.inductionAlts) : TacticM Alts := do + -- For parsing the user-supplied names for variables and induction hypotheses let parseVars (vars : Array Syntax) : TacticM (Array (TSyntax `Lean.binderIdent)) := do vars.mapM fun v => match v with @@ -47,12 +52,12 @@ private def parseInductionAlts (alts : TSyntax `Lean.Parser.Tactic.inductionAlts let mut parsedAlts : Array Alt := #[] - match alts with - | `(inductionAlts| with $[$cases]*) => - for alt in cases do + match altsSyntax with + | `(inductionAlts| with $[$tac]? $[$alts]*) => + for alt in alts do let (lhs, tacs) ← match alt with | `(inductionAlt| $[$lhs:inductionAltLHS]* => $tac:tacticSeq) => pure (lhs, some tac) - | `(inductionAlt| $[$lhs:inductionAltLHS]* => $_:hole) => pure (lhs, none) + | `(inductionAlt| $[$lhs:inductionAltLHS]* => $_:hole) | `(inductionAlt| $[$lhs:inductionAltLHS]* => $_:syntheticHole) => pure (lhs, none) | _ => throwErrorAt alt "iinduction: invalid syntax" @@ -62,17 +67,14 @@ private def parseInductionAlts (alts : TSyntax `Lean.Parser.Tactic.inductionAlts | `(inductionAltLHS| | @ $ctor:ident $[$vars]*) => parsedAlts := parsedAlts.push ⟨ctor.getId, ← parseVars vars, tacs⟩ | `(inductionAltLHS| | $_:hole $[$vars]*) => - if parsedAlts.size < cases.size - 1 then + if parsedAlts.size < alts.size - 1 then throwErrorAt alt s!"iinduction: invalid occurrence of the wildcard alternative `| _ => ...`:".append "It must be the last alternative" - return ⟨none, parsedAlts, some ⟨.anonymous, ← parseVars vars, tacs⟩⟩ + return ⟨tac, parsedAlts, some ⟨.anonymous, ← parseVars vars, tacs⟩⟩ | _ => throwErrorAt l "iinduction: invalid syntax" - | `(inductionAlts| with $tac $[$cases]*) => - throwErrorAt tac "iinduction: unsupported syntax, to be implemented" - | _ => throwErrorAt alts "iinduction: invalid syntax" - - return ⟨none, parsedAlts, none⟩ + return ⟨tac, parsedAlts, none⟩ + | _ => throwErrorAt altsSyntax "iinduction: invalid syntax" /-- Check whether a fully-qualified constructor name (e.g. `Nat.succ`) matches a diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 10a628599..18cf94271 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2842,4 +2842,11 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : | succ n ih => itrivial | _ => _ +example [BI PROP] {P Q R S T : PROP} {n : Nat} : + ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by + iintro HP #HQ #HR HS #HT + iinduction n with simp + | zero => itrivial + | succ n ih => itrivial + end iinduction From afccd6c28dda22614d786ffd9ff4c32517566494 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 6 Jun 2026 23:59:11 +0200 Subject: [PATCH 093/181] Check for redundant alternative names after the first tactic --- Iris/Iris/ProofMode/ProofModeM.lean | 45 +++++++++---- Iris/Iris/ProofMode/Tactics/Induction.lean | 78 +++++++++++++--------- Iris/Iris/Tests/Tactics.lean | 30 ++++++++- 3 files changed, 107 insertions(+), 46 deletions(-) diff --git a/Iris/Iris/ProofMode/ProofModeM.lean b/Iris/Iris/ProofMode/ProofModeM.lean index 6e574b19e..2dc16a5d5 100644 --- a/Iris/Iris/ProofMode/ProofModeM.lean +++ b/Iris/Iris/ProofMode/ProofModeM.lean @@ -83,24 +83,43 @@ def addMVarGoal (m : MVarId) (name : Name := .anonymous) : ProofModeM Unit := do /-- Creates a new proof goal with the given hypotheses (`hyps`), conclusion - (`goal`) and run a sequence of tactics (`tacticSeq`) to the - newly generated goal before addition into the proof state. + (`goal`). + + If `firstTactic` is not `none`, run the tactic to the new proof goal. + This tactic may lead to one or more new proof goals. This tactic should not + solve the goal, or else `tacticSeq` is known to be redundant. If so, + throw an error to notify the user of the redundancy. + + Then, run a sequence of tactics (`tacticSeq`) before adding the resultant + goals into the proof state. + + The function returns: + 1. the metavariable of the initial goal, + 2. the metavariables of subgoals after running the first tactic, and + 3. the metavariables of subgoals after running the remaining tactic sequences. -/ -def addBIGoalRunTactic {prop : Q(Type u)} {bi : Q(BI $prop)} +def addBIGoalRunTactics {prop : Q(Type u)} {bi : Q(BI $prop)} {e} (hyps : Hyps bi e) (goal : Q($prop)) (name : Name := .anonymous) + (firstTactic : Option <| TSyntax `tactic) (tacticSeq : TSyntax `Lean.Parser.Tactic.tacticSeq) : - ProofModeM Q($e ⊢ $goal) := do + ProofModeM (Q($e ⊢ $goal) × Option (List MVarId) × List MVarId) := do let m ← mkBIGoal hyps goal name - -- Run the user tactic on the newly generated goal - let savedGoals ← getGoals - setGoals [m.mvarId!] - evalTactic tacticSeq - let remaining ← getGoals - setGoals savedGoals - for r in remaining do addMVarGoal r - - pure m + match firstTactic with + | none => + -- Run the user tactics on the newly generated goal + let subgoals ← evalTacticAt tacticSeq m.mvarId! + for s in subgoals do addMVarGoal s + pure (m, none, subgoals) + | some firstTactic => + let subgoals ← evalTacticAt firstTactic m.mvarId! + let mut subgoals' := [] + -- Run the user tactics on each newly generated goal after running `firstTactic` + for s in subgoals do + subgoals' := subgoals'.append <| ← evalTacticAt tacticSeq s + -- Add the result proof subgoals into the proof state + for s' in subgoals' do addMVarGoal s' + pure (m, subgoals, subgoals') /-- Try to synthesize a typeclass instance, adding any created metavariables as proof mode goals. -/ def ProofModeM.trySynthInstanceQ (α : Q(Sort v)) : ProofModeM (Option Q($α)) := do diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index ed704c0e8..a995ec0c3 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -28,6 +28,8 @@ private structure Alt where vars : Array (TSyntax `Lean.binderIdent) -- User-supplied tactics for this case, `none` if a hole (`_` or `?_`) is given tacs : Option <| TSyntax `Lean.Parser.Tactic.tacticSeq + -- The syntax, useful for error message printing + stx : TSyntax `Lean.Parser.Tactic.inductionAlt private structure Alts where -- User-supplied tactic to be applied before splitting into cases @@ -65,13 +67,13 @@ private def parseInductionAlts (altsSyntax : TSyntax `Lean.Parser.Tactic.inducti match l with | `(inductionAltLHS| | $ctor:ident $[$vars]*) | `(inductionAltLHS| | @ $ctor:ident $[$vars]*) => - parsedAlts := parsedAlts.push ⟨ctor.getId, ← parseVars vars, tacs⟩ + parsedAlts := parsedAlts.push ⟨ctor.getId, ← parseVars vars, tacs, alt⟩ | `(inductionAltLHS| | $_:hole $[$vars]*) => if parsedAlts.size < alts.size - 1 then throwErrorAt alt s!"iinduction: invalid occurrence of the wildcard alternative `| _ => ...`:".append "It must be the last alternative" - return ⟨tac, parsedAlts, some ⟨.anonymous, ← parseVars vars, tacs⟩⟩ + return ⟨tac, parsedAlts, some ⟨.anonymous, ← parseVars vars, tacs, alt⟩⟩ | _ => throwErrorAt l "iinduction: invalid syntax" return ⟨tac, parsedAlts, none⟩ | _ => throwErrorAt altsSyntax "iinduction: invalid syntax" @@ -313,8 +315,11 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} | none => pure () | some parsedAlts => checkCtors recCtors parsedAlts - if recCtors.length == parsedAlts.alts.size && parsedAlts.wildcard.isSome then - throwError "iinduction: wildcard alternative is not needed" + if recCtors.length == parsedAlts.alts.size then + match parsedAlts.wildcard with + | some w => + throwErrorAt w.stx "iinduction: wildcard alternative is not needed" + | none => pure () -- Define the names for variables and induction hypotheses if supplied by user let varNames : Array AltVarNames ← @@ -349,50 +354,61 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} | some parsedAlts => s.mvarId.setTag ctor match parsedAlts.alts.find? (matcher ctor) <|> parsedAlts.wildcard with - | some ⟨_, vars, _⟩ => + | some ⟨_, vars, _, stx⟩ => if vars.size > s.fields.size then - throwError + throwErrorAt stx s!"iinduction: too many variable names provided at alternative `{ctor}`: ".append s!"{vars.size} provided, but {s.fields.size} expected" + + -- For pretty printing of arguments for ⟨fieldFVar, varStx⟩ in s.fields.toList.zip vars.toList do if let `(binderIdent| $id:ident) := varStx then let lctx ← getLCtx let fieldType ← inferType fieldFVar addLocalVarInfo id lctx fieldFVar (some fieldType) true - | none => throwMissingAlt ctor - -- Obtain the type of the induction subgoal - let sType ← instantiateMVars <| ← s.mvarId.getType + -- Obtain the type of the induction subgoal + let sType ← instantiateMVars <| ← s.mvarId.getType - let some irisGoal := parseIrisGoal? sType - -- This should not happen - | throwError "iinduction: fail to parse induction subgoal" + let some irisGoal := parseIrisGoal? sType + -- This should not happen + | throwErrorAt stx "iinduction: fail to parse induction subgoal" - -- Find the induction hypotheses generated by Lean's `induction` tactic - let ihFVars ← findIHs s.mvarId + -- Find the induction hypotheses generated by Lean's `induction` tactic + let ihFVars ← findIHs s.mvarId - -- A proof that all hypotheses in the Iris goal exist in the intuitionistic context - let some pfIntHyps := irisGoal.hyps.buildIntuitionisticProof - | throwError "iinduction: spatial context should be empty" + -- A proof that all hypotheses in the Iris goal exist in the intuitionistic context + let some pfIntHyps := irisGoal.hyps.buildIntuitionisticProof + | throwErrorAt stx "iinduction: spatial context should be empty" - -- Introduce the induction hypothesis back into the Iris proof state - let st ← addIHs pfIntHyps irisGoal.hyps ihFVars + -- Introduce the induction hypothesis back into the Iris proof state + let st ← addIHs pfIntHyps irisGoal.hyps ihFVars - -- Check whether the user has supplied tactic sequences for this induction subgoal - let tacticSeq := parsedAlts.bind <| fun parsedAlts => - (parsedAlts.alts.find? (matcher ctor) <|> parsedAlts.wildcard).bind (·.tacs) + -- Check whether the user has supplied tactic sequences for this induction subgoal + let tacticSeq := (parsedAlts.alts.find? (matcher ctor) <|> parsedAlts.wildcard).bind (·.tacs) - -- Generate the induction subgoal for the user, label the induction subgoal with `ctor` - let pf' ← match tacticSeq with - | none => k st.newHyps irisGoal.goal (addBIGoal · · ctor) - -- Run the tactics supplied by the user, if available - | some tacticSeq => k st.newHyps irisGoal.goal (addBIGoalRunTactic · · ctor tacticSeq) + let firstTactic := parsedAlts.tac - -- Remove the induction hypotheses from the regular Lean context - let pf' ← withoutFVars (u := 0) ihFVars.toArray <| pure pf' + -- Generate the induction subgoal for the user, label the induction subgoal with `ctor` + let pf' ← match tacticSeq with + | none => k st.newHyps irisGoal.goal (addBIGoal · · ctor) + -- Run the tactics supplied by the user, if available + | some tacticSeq => k st.newHyps irisGoal.goal <| fun hyps goal => do + let ⟨pf, newMVars, _⟩ ← (addBIGoalRunTactics hyps goal ctor firstTactic tacticSeq) + -- Throw an error if the first tactic already solves the goal + match newMVars with + | some [] => + throwErrorAt stx + "iinduction: alternative `{ctor.getString!}` is not needed" + | _ => pure () + pure pf - -- Fill the metavariable for the induction subgoal generated by Lean - s.mvarId.assign q($(st.pf).trans $pf') + -- Remove the induction hypotheses from the regular Lean context + let pf' ← withoutFVars (u := 0) ihFVars.toArray <| pure pf' + + -- Fill the metavariable for the induction subgoal generated by Lean + s.mvarId.assign q($(st.pf).trans $pf') + | none => throwMissingAlt ctor return m diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 18cf94271..aa2dd273e 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2842,11 +2842,37 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : | succ n ih => itrivial | _ => _ -example [BI PROP] {P Q R S T : PROP} {n : Nat} : - ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by +/- Testing `iinduction` with first tactic after `with` syntax -/ +example [BI PROP] {P Q R S T : PROP} {m n : Nat} : + ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜m + 0 = m⌝ -∗ ⌜n + 0 = n⌝ := by iintro HP #HQ #HR HS #HT iinduction n with simp | zero => itrivial | succ n ih => itrivial +/- Testing `iinduction` with first tactic after `with` syntax -/ +example [BI PROP] {P Q R S T : PROP} {m n : Nat} : + ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜m + 0 = m⌝ -∗ ⌜n + 0 = n⌝ := by + iintro HP #HQ #HR HS #HT + iinduction n with (cases m) + | zero => itrivial + | succ n ih => itrivial + +/- Testing `iinduction` with first tactic after `with` syntax, redundant alternative name -/ +/-- error: iinduction: alternative `zero` is not needed -/ +#guard_msgs in +example [BI PROP] {P Q R S T : PROP} {n : Nat} : + ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜0 + 0 = 0⌝ -∗ ⌜n + 0 = n⌝ := by + iintro HP #HQ #HR HS #HT #H + iinduction n with (try iexact H) + | zero => itrivial + | succ n ih => itrivial + +/- Testing `iinduction` with first tactic after `with` syntax, no redundant alternative name -/ +example [BI PROP] {P Q R S T : PROP} {n : Nat} : + ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜0 + 0 = 0⌝ -∗ ⌜n + 0 = n⌝ := by + iintro HP #HQ #HR HS #HT #H + iinduction n with (try iexact H) + | succ n ih => itrivial + end iinduction From 812b3eb02736a774f0aa7c0dad9ae4f0269213f0 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sun, 7 Jun 2026 00:34:33 +0200 Subject: [PATCH 094/181] Error handling without explicit checks with a function --- Iris/Iris/ProofMode/Tactics/Induction.lean | 103 ++++++++++++--------- Iris/Iris/Tests/Tactics.lean | 11 ++- 2 files changed, 64 insertions(+), 50 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index a995ec0c3..6bb2f8c8f 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -38,6 +38,8 @@ private structure Alts where alts : Array Alt -- The wildcard case, if supplied by the user wildcard : Option Alt + -- The syntax, useful for error message printing + stx : TSyntax `Lean.Parser.Tactic.inductionAlts /-- Given user-supplied alternative names and tactic sequences, parse the syntax @@ -73,9 +75,9 @@ private def parseInductionAlts (altsSyntax : TSyntax `Lean.Parser.Tactic.inducti throwErrorAt alt s!"iinduction: invalid occurrence of the wildcard alternative `| _ => ...`:".append "It must be the last alternative" - return ⟨tac, parsedAlts, some ⟨.anonymous, ← parseVars vars, tacs, alt⟩⟩ + return ⟨tac, parsedAlts, some ⟨.anonymous, ← parseVars vars, tacs, alt⟩, altsSyntax⟩ | _ => throwErrorAt l "iinduction: invalid syntax" - return ⟨tac, parsedAlts, none⟩ + return ⟨tac, parsedAlts, none, altsSyntax⟩ | _ => throwErrorAt altsSyntax "iinduction: invalid syntax" /-- @@ -209,13 +211,6 @@ private def checkCtors (ctors : List Name) (parsedAlts : Alts) : ProofModeM Unit -- Find the list of constructor names given by the user let altCtors := parsedAlts.alts.toList.map (·.ctor) - let mut errors : List String := [] - - -- Check for missing constructor names - let missingAltCtors := match parsedAlts.wildcard with - | none => ctors.filter (not <| altCtors.any <| fun altCtor => matchesCtorName · altCtor) - | _ => [] - -- Check for invalid constructor names let invalidAltCtors := altCtors.filter (not <| ctors.any <| fun ctor => matchesCtorName ctor ·) @@ -224,18 +219,12 @@ private def checkCtors (ctors : List Name) (parsedAlts : Alts) : ProofModeM Unit let dupAltCtors := (altCtors.filter <| fun alt => altCtors.countP (matchesCtorName alt ·) > 1).eraseDupsBy matchesCtorName - if !missingAltCtors.isEmpty then - let missing := ", ".intercalate <| missingAltCtors.map (s!"`{·}`") - errors := errors.concat s!"iinduction: missing alternative name(s): {missing}" if !dupAltCtors.isEmpty then let dups := ", ".intercalate <| dupAltCtors.map (s!"`{·}`") - errors := errors.concat s!"iinduction: duplicate alternative name(s): {dups}" + throwOrLogError s!"iinduction: duplicate alternative name(s): {dups}" if !invalidAltCtors.isEmpty then let invalids := ", ".intercalate <| invalidAltCtors.map (s!"`{·}`") - errors := errors.concat s!"iinduction: invalid alternative name(s): {invalids}" - - if !errors.isEmpty then - throwError m!"{"\n".intercalate errors}" + throwOrLogError s!"iinduction: invalid alternative name(s): {invalids}" /-- The main function handling the steps for the `iinduction` tactic. @@ -318,7 +307,7 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} if recCtors.length == parsedAlts.alts.size then match parsedAlts.wildcard with | some w => - throwErrorAt w.stx "iinduction: wildcard alternative is not needed" + throwOrLogErrorAt w.stx "iinduction: wildcard alternative is not needed" | none => pure () -- Define the names for variables and induction hypotheses if supplied by user @@ -334,7 +323,12 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} (match ·.raw with | `(binderIdent| $id:ident) => id.getId | _ => Name.mkSimple "_") } - | none => throwMissingAlt ctor + | none => + -- Defer the check if the first tactic for the `with` syntax is given + if parsedAlts.tac.isSome then + pure { explicit := true, varNames := [] } + else + throwMissingAlt ctor let pf ← iRevertIntro hyps goal targets <| fun hyps' goal' k => do @@ -348,15 +342,32 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- Handle each subgoal generated by Lean's induction for ⟨s, ctor⟩ in subgoals.toList.zip recCtors do s.mvarId.withContext do - -- For pretty printing of arguments match parsedAlts with | none => pure () | some parsedAlts => s.mvarId.setTag ctor + -- Obtain the type of the induction subgoal + let sType ← instantiateMVars <| ← s.mvarId.getType + + let some irisGoal := parseIrisGoal? sType + -- This should not happen + | throwOrLogError "iinduction: fail to parse induction subgoal" + + -- Find the induction hypotheses generated by Lean's `induction` tactic + let ihFVars ← findIHs s.mvarId + + -- A proof that all hypotheses in the Iris goal exist in the intuitionistic context + let some pfIntHyps := irisGoal.hyps.buildIntuitionisticProof + | throwOrLogError "iinduction: spatial context should be empty" + + -- Introduce the induction hypothesis back into the Iris proof state + let st ← addIHs pfIntHyps irisGoal.hyps ihFVars + + -- Check whether the alternative names for this constructor are supplied by the user match parsedAlts.alts.find? (matcher ctor) <|> parsedAlts.wildcard with | some ⟨_, vars, _, stx⟩ => if vars.size > s.fields.size then - throwErrorAt stx + throwOrLogErrorAt stx <| s!"iinduction: too many variable names provided at alternative `{ctor}`: ".append s!"{vars.size} provided, but {s.fields.size} expected" @@ -367,23 +378,6 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} let fieldType ← inferType fieldFVar addLocalVarInfo id lctx fieldFVar (some fieldType) true - -- Obtain the type of the induction subgoal - let sType ← instantiateMVars <| ← s.mvarId.getType - - let some irisGoal := parseIrisGoal? sType - -- This should not happen - | throwErrorAt stx "iinduction: fail to parse induction subgoal" - - -- Find the induction hypotheses generated by Lean's `induction` tactic - let ihFVars ← findIHs s.mvarId - - -- A proof that all hypotheses in the Iris goal exist in the intuitionistic context - let some pfIntHyps := irisGoal.hyps.buildIntuitionisticProof - | throwErrorAt stx "iinduction: spatial context should be empty" - - -- Introduce the induction hypothesis back into the Iris proof state - let st ← addIHs pfIntHyps irisGoal.hyps ihFVars - -- Check whether the user has supplied tactic sequences for this induction subgoal let tacticSeq := (parsedAlts.alts.find? (matcher ctor) <|> parsedAlts.wildcard).bind (·.tacs) @@ -398,17 +392,34 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- Throw an error if the first tactic already solves the goal match newMVars with | some [] => - throwErrorAt stx - "iinduction: alternative `{ctor.getString!}` is not needed" + throwOrLogErrorAt stx + s!"iinduction: alternative `{ctor.getString!}` is not needed" | _ => pure () pure pf - -- Remove the induction hypotheses from the regular Lean context - let pf' ← withoutFVars (u := 0) ihFVars.toArray <| pure pf' - - -- Fill the metavariable for the induction subgoal generated by Lean - s.mvarId.assign q($(st.pf).trans $pf') - | none => throwMissingAlt ctor + -- Remove the induction hypotheses from the regular Lean context + let pf' ← withoutFVars (u := 0) ihFVars.toArray <| pure pf' + + -- Fill the metavariable for the induction subgoal generated by Lean + s.mvarId.assign q($(st.pf).trans $pf') + -- Alternative names not found, acceptable only when `firstTactic` solves it + | none => + match parsedAlts.tac with + | none => throwMissingAlt ctor + | some firstTactic => + let pf' ← k st.newHyps irisGoal.goal <| fun hyps goal => do + let m ← mkBIGoal hyps goal ctor + let subgoals ← evalTacticAt firstTactic m.mvarId! + if !subgoals.isEmpty then + throwOrLogErrorAt parsedAlts.stx + m!"iinduction: alternative `{ctor.getString!}` has not been provided" + pure m + + -- Remove the induction hypotheses from the regular Lean context + let pf' ← withoutFVars (u := 0) ihFVars.toArray <| pure pf' + + -- Fill the metavariable for the induction subgoal generated by Lean + s.mvarId.assign q($(st.pf).trans $pf') return m diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index aa2dd273e..d35031f80 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2741,9 +2741,11 @@ example [BI PROP] {P : PROP} : ⊢ P := by iinduction P /- Tests `iinduction` with induction on natural numbers with invalid user-supplied names -/ -/-- error: iinduction: missing alternative name(s): `succ` -iinduction: duplicate alternative name(s): `zero` -iinduction: invalid alternative name(s): `invalidA`, `invalidB`, `invalidC` -/ +/-- error: iinduction: duplicate alternative name(s): `zero` +--- +error: iinduction: invalid alternative name(s): `invalidA`, `invalidB`, `invalidC` +--- +error: iinduction: alternative `succ` has not been provided -/ #guard_msgs in example [BI PROP] {P Q R S T : PROP} {n : Nat} : ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by @@ -2865,7 +2867,7 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜0 + 0 = 0⌝ -∗ ⌜n + 0 = n⌝ := by iintro HP #HQ #HR HS #HT #H iinduction n with (try iexact H) - | zero => itrivial + | zero => itrivial -- Redundant case | succ n ih => itrivial /- Testing `iinduction` with first tactic after `with` syntax, no redundant alternative name -/ @@ -2873,6 +2875,7 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜0 + 0 = 0⌝ -∗ ⌜n + 0 = n⌝ := by iintro HP #HQ #HR HS #HT #H iinduction n with (try iexact H) + -- No complaints about missing `zero` case | succ n ih => itrivial end iinduction From 5b32861bdbd989c35fd6abdde2a5569fba5a129a Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sun, 7 Jun 2026 01:09:55 +0200 Subject: [PATCH 095/181] Error message refinements --- Iris/Iris/ProofMode/Tactics/Induction.lean | 35 +++++++++------------- Iris/Iris/Tests/Tactics.lean | 9 ++++-- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 6bb2f8c8f..70e5f2f89 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -199,32 +199,25 @@ private def addIHs {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e : Q($prop)} /-- Throw an error if the user has supplied some but not all alternative names. + Also throw an error if the user has supplied multiple sets of alternative + names for the same constructor. -/ private def throwMissingAlt {α} (ctor : Name) : ProofModeM α := throwError "iinduction: alternative `{ctor.getString!}` has not been provided" -/-- - Checks whether an invalid, missing and/or duplicate constructor names have - been supplied by the user. Throw an error if this is the case. --/ private def checkCtors (ctors : List Name) (parsedAlts : Alts) : ProofModeM Unit := do - -- Find the list of constructor names given by the user - let altCtors := parsedAlts.alts.toList.map (·.ctor) - - -- Check for invalid constructor names - let invalidAltCtors := - altCtors.filter (not <| ctors.any <| fun ctor => matchesCtorName ctor ·) - - -- Check for duplicate constructor names - let dupAltCtors := (altCtors.filter <| - fun alt => altCtors.countP (matchesCtorName alt ·) > 1).eraseDupsBy matchesCtorName - - if !dupAltCtors.isEmpty then - let dups := ", ".intercalate <| dupAltCtors.map (s!"`{·}`") - throwOrLogError s!"iinduction: duplicate alternative name(s): {dups}" - if !invalidAltCtors.isEmpty then - let invalids := ", ".intercalate <| invalidAltCtors.map (s!"`{·}`") - throwOrLogError s!"iinduction: invalid alternative name(s): {invalids}" + let alts := parsedAlts.alts.toList + + for alt in alts do + let isValid := ctors.any fun ctor => matchesCtorName ctor alt.ctor + let isDup := alts.countP (fun alt' => matchesCtorName alt.ctor alt'.ctor) > 1 + + if !isValid then + throwOrLogErrorAt alt.stx + m!"iinduction: invalid alternative name `{alt.ctor}`" + else if isDup then + throwOrLogErrorAt alt.stx + m!"iinduction: duplicate alternative name `{alt.ctor}`" /-- The main function handling the steps for the `iinduction` tactic. diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index d35031f80..cbd903444 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2741,9 +2741,13 @@ example [BI PROP] {P : PROP} : ⊢ P := by iinduction P /- Tests `iinduction` with induction on natural numbers with invalid user-supplied names -/ -/-- error: iinduction: duplicate alternative name(s): `zero` +/-- error: iinduction: invalid alternative name `invalidA` --- -error: iinduction: invalid alternative name(s): `invalidA`, `invalidB`, `invalidC` +error: iinduction: duplicate alternative name `zero` +--- +error: iinduction: invalid alternative name `invalidB` +--- +error: iinduction: duplicate alternative name `Nat.zero` --- error: iinduction: alternative `succ` has not been provided -/ #guard_msgs in @@ -2755,7 +2759,6 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : | zero => itrivial | invalidB => done | Nat.zero => itrivial - | invalidC => done /- Tests `iinduction` with extra arguments supplied by the user -/ /-- error: iinduction: too many variable names provided at alternative `succ`: 4 provided, but 2 expected -/ From d39bce3d31df53aff62b477c55123a6e8c2a7829 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sun, 7 Jun 2026 12:55:18 +0200 Subject: [PATCH 096/181] Minor refinements --- Iris/Iris/ProofMode/Tactics/Induction.lean | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 70e5f2f89..b113aaa03 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -1,7 +1,7 @@ /- Copyright (c) 2026 Yunsong Yang. All rights reserved. Released under Apache 2.0 license as described in the file LICENSE. -Authors: Yunsong Yang, Alvin Tang +Authors: Yunsong Yang, Michael Sammler, Alvin Tang -/ module @@ -11,7 +11,6 @@ public meta import Iris.ProofMode.Tactics.Cases public meta import Iris.ProofMode.Patterns.CasesPattern public meta import Iris.ProofMode.ClassesMake public meta import Iris.ProofMode.Tactics.RevertIntro -public meta import Lean.Elab.Tactic.Induction namespace Iris.ProofMode @@ -398,11 +397,13 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- Alternative names not found, acceptable only when `firstTactic` solves it | none => match parsedAlts.tac with + -- No first tactic given, the alternative name is missing | none => throwMissingAlt ctor | some firstTactic => let pf' ← k st.newHyps irisGoal.goal <| fun hyps goal => do let m ← mkBIGoal hyps goal ctor let subgoals ← evalTacticAt firstTactic m.mvarId! + -- First tactic supplied by the user, but it does not completely solve this case if !subgoals.isEmpty then throwOrLogErrorAt parsedAlts.stx m!"iinduction: alternative `{ctor.getString!}` has not been provided" From 0731db4df976aaf938908e53bc3690833cd8c5df Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sun, 7 Jun 2026 16:13:09 +0200 Subject: [PATCH 097/181] Implement checkDependentHyps and refactor the way targets for iRevertIntro is determined --- Iris/Iris/ProofMode/Tactics/Induction.lean | 111 ++++++++++++++++----- Iris/Iris/Tests/Tactics.lean | 41 +++++++- 2 files changed, 125 insertions(+), 27 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index b113aaa03..4a4217c6e 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -136,14 +136,88 @@ private structure InductionState {u} {prop : Q(Type u)} {bi} (origE : Q($prop)) Lean context before applying Lean's built-in `induction` tactic. -/ private def iHypsToGeneralize {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} - (hyps : Hyps bi e) (fvars : List FVarId) : List SelTarget := + (hyps : Hyps bi e) (fvar : FVarId) : List SelTarget := let ivars := hyps.intuitionisticIVarIds.filter <| fun ivar => match hyps.getDecl? ivar with - | some ⟨_, _, _, ty⟩ => fvars.any ty.containsFVar + | some ⟨_, _, _, ty⟩ => ty.containsFVar fvar | none => false (ivars ++ hyps.spatialIVarIds).map ({ kind := .ipm ·, explicit := false }) +/-- + When the tactic is `iinduction e generalizing z₁ ... zₙ` applied, + the variables `z₁ ... zₙ`, which can be regular Lean variables or Iris + hypotheses, are reverted from the context. + + The function `iHypsToGeneralize` is used for finding the Iris hypotheses + that references the induction target. + + However, it is possible that `x` is amongst the Lean variables explcitly + reverted by the user using the `generalizing` syntax while there exists + another Lean variable `y` such that `y` depends on `x`. In this case, this + function suggests that the user includes `%y` in the `generalizing` syntax. + + It is also possible that `x` is amongst the Lean variables being generalised + such that there exists an Iris hypothesis `HP` in the intuitionistic context + where: + 1. `P` does not depend on the induction target and thus not reverted automatically, + 2. `P` depends on `x` in the `generalizing` syntax, and + 3. `P` itself is not included in the `generalizing` syntax. + In this case, this function suggests the user to include `HP` in the + `generalizing` syntax. + + Note that Iris hypotheses in the spatial context are always reverted, so there + is no need for further checks by this function. +-/ +private def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} + (hyps : Hyps bi e) (explicitTargets : List SelTarget) : + ProofModeM Unit := do + + let explicitIrisIVarIds := explicitTargets.filterMap + (match ·.kind with | .ipm ivar => some ivar | _ => none) + let explicitPureFVars := explicitTargets.filterMap + (match ·.kind with | .pure f => some f | _ => none) + + -- Check Iris hypothesis that depend on hypotheses in the `generalizing` syntax + let missingIris : List (Name × FVarId) := + hyps.intuitionisticIVarIds.filterMap fun ivar => + if explicitIrisIVarIds.contains ivar then none + else match hyps.getDecl? ivar with + | some ⟨name, _, _, ty⟩ => + match explicitPureFVars.find? (ty.containsFVar ·) with + | some x => some (name, x) + | none => none + | none => none + + -- Pairs of `FVarId` values `(f1, f2)` indicating `f1` depends on `f2`. + let mut missingPure : List (FVarId × FVarId) := [] + + -- Check forward dependency of pure hypotheses in the `generalizing` syntax + for fvar in (explicitPureFVars ++ (missingIris.map (·.snd))).eraseDups do + let fwdDeps ← collectForwardDeps #[mkFVar fvar] false + for dep in fwdDeps do + let depId := dep.fvarId! + -- Skip `x` itself and variables already included in `explicitPureFVars` + if depId != fvar && !explicitPureFVars.contains depId then + -- Record the missing hypothesis to be generalised, if not already included + if !missingPure.any (·.fst == depId) then + missingPure := missingPure ++ [(depId, fvar)] + + -- Throw an error if there exists some pure/Lean hypotheses that should also be generalised + if !missingPure.isEmpty || !missingIris.isEmpty then + let leanLines ← missingPure.mapM fun (depId, srcId) => do + let depDecl ← depId.getDecl + let srcDecl ← srcId.getDecl + return s!" - Lean hypothesis `{depDecl.userName}` depends on \ + `{srcDecl.userName}`" + let irisLines ← missingIris.mapM fun (name, srcId) => do + let srcDecl ← srcId.getDecl + return s!" - Iris hypothesis in the intuitionstic context `{name}` depends on \ + `{srcDecl.userName}`" + throwError m!"iinduction: The following hypotheses depend on variables in \ + the `generalizing` clause but are not themselves included:\ + \n{"\n".intercalate (leanLines ++ irisLines)}" + /-- Search for hypotheses in the regular Lean context that are the in the form of Iris goals. These must be induction hypotheses generated by Lean's built-in @@ -242,30 +316,15 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} (altRecName : Option Name) (genSelTargets : Option <| List SelTarget) : ProofModeM Q($e ⊢ $goal) := do - -- Iris hypotheses to be reverted as specified by the user using the `generalizing` syntax - let explicitIrisTargets : List SelTarget := - match genSelTargets with - | none => [] - | some genSelTargets => genSelTargets.filter <| fun t => match t.kind with | .ipm _ => true | _ => false - - -- `FVarID` values of pure hypothesis specified by the user using the `generalizing` syntax - let pureFVars : List FVarId := - match genSelTargets with - | none => [] - | some genSelTargets => - genSelTargets.filterMap <| fun t => match t.kind with | .pure f => some f | _ => none - - -- Iris hypotheses in the spatial context and relevant intuitionistic context - let spatialAndIntuitionisticTargets := iHypsToGeneralize hyps <| pureFVars.concat fvar - - let spatialAndIntuitionisticTargets := - spatialAndIntuitionisticTargets.filter (not <| (explicitIrisTargets.map (·.kind)).contains ·.kind) - - -- Pure hypotheses to be reverted as specified by the user using the `generalizing` syntax - let pureTargets : List SelTarget := pureFVars.map ({ kind := .pure ·, explicit := true }) - - -- Those in `explicitIrisTargets` get reverted first, so they are last in the list - let targets := pureTargets ++ spatialAndIntuitionisticTargets ++ explicitIrisTargets + -- Find the regular pure/Iris hypotheses to be reverted + let targets ← do match genSelTargets with + | none => pure <| iHypsToGeneralize hyps fvar + | some genSelTargets => + checkDependentHyps hyps genSelTargets + let explicitIrisTargets := genSelTargets.filter (match ·.kind with | .ipm _ => true | _ => false) + let explicitPureTargets := genSelTargets.filter (match ·.kind with | .pure _ => true | _ => false) + let implicitIrisTargets := (iHypsToGeneralize hyps fvar).filter (not <| (explicitIrisTargets.map (·.kind)).contains ·.kind) + pure <| explicitPureTargets ++ implicitIrisTargets ++ explicitIrisTargets -- Find the recursor name and constructor names of the inductive datatype let fvarType ← whnf <| ← inferType <| mkFVar fvar diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index cbd903444..ec4ebb974 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2727,7 +2727,7 @@ section iinduction example [BI PROP] {P Q R S : PROP} {T : Nat → PROP} {n : Nat} : ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T n -∗ ⌜n + 0 = n⌝ := by iintro HP #HQ #HR HS #HT - iinduction n generalizing %P HQ %R with + iinduction n generalizing HQ %R HR HP %P with -- Using the full name of the constructor (`Nat.zero`) | Nat.zero => itrivial /- Using the short name of the constructor (`succ`), naming the induction @@ -2881,4 +2881,43 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : -- No complaints about missing `zero` case | succ n ih => itrivial +/- Testing `iinduction` on `n` generalising `m`, where *regular hypothesis* `h1 : Q m` + and `X : (Q m) → Prop` depend on `m`. Moreover, `h2 : X h1` depends on `h1`. + This dependency requires manual resolution. -/ +/-- error: irevert: Lean hypothesis Y depends on m -/ +#guard_msgs in +example [BI PROP] {P : PROP} {m n : Nat} {Q : Nat → Prop} {h1 : Q m} {X : (Q m) → Prop} {h2 : X h1} : + ⊢ P -∗ ⌜n + 0 = n⌝ := by + iintro HP + iinduction n generalizing %m with + | zero => itrivial + | succ n ih => itrivial + +/-- Testing `iinduction` on `n` generalising `m` and `H`, which depends on `m`. -/ +example [BI PROP] {P : PROP} {m n : Nat} {Q : Nat → Prop} {H : Q m} : + ⊢ P -∗ ⌜n + 0 = n⌝ := by + iintro HP + iinduction n generalizing %m %H with + | zero => itrivial + | succ n ih => itrivial + +/- Testing `iinduction` on `n` generalising `m`, where *Iris hypothesis* `□HQ : Q m` + depends on `m`. This requires manual resolution. -/ +/-- error: iinduction: proofmode hypothesis HQ depends on m -/ +#guard_msgs in +example [BI PROP] {P : PROP} {m n : Nat} {Q : Nat → PROP} : + ⊢ P -∗ □ Q m -∗ ⌜n + 0 = n⌝ := by + iintro HP #HQ + iinduction n generalizing %m with + | zero => itrivial + | succ n ih => itrivial + +/-- Testing `iinduction` on `n` generalising `m` and `HQ`, which depends on `m`. -/ +example [BI PROP] {P : PROP} {m n : Nat} {Q : Nat → PROP} : + ⊢ P -∗ □ Q m -∗ ⌜n + 0 = n⌝ := by + iintro HP #HQ + iinduction n generalizing %m HQ with + | zero => itrivial + | succ n ih => itrivial + end iinduction From 3a2f03ceb6a2bb62d9cf89ccb2d809fb00e313a0 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sun, 7 Jun 2026 17:11:22 +0200 Subject: [PATCH 098/181] Towards pretty suggestions --- Iris/Iris/ProofMode/Tactics/Induction.lean | 66 +++++++++++++++------- 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 4a4217c6e..0321eba7e 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -11,12 +11,18 @@ public meta import Iris.ProofMode.Tactics.Cases public meta import Iris.ProofMode.Patterns.CasesPattern public meta import Iris.ProofMode.ClassesMake public meta import Iris.ProofMode.Tactics.RevertIntro +public meta import Lean.Meta.Tactic.TryThis namespace Iris.ProofMode public meta section open BI Std Lean Elab Tactic Meta Qq Parser.Tactic +syntax (name := iinduction) "iinduction" colGt term + ("using" ident)? + ("generalizing" (colGt selPat)+)? + (inductionAlts)? : tactic + /-- Information obtained from parsing a case under the `with` syntax -/ @@ -208,16 +214,43 @@ private def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} let leanLines ← missingPure.mapM fun (depId, srcId) => do let depDecl ← depId.getDecl let srcDecl ← srcId.getDecl - return s!" - Lean hypothesis `{depDecl.userName}` depends on \ - `{srcDecl.userName}`" + return s!"• Lean hypothesis `{depDecl.userName}` depends on `{srcDecl.userName}`" let irisLines ← missingIris.mapM fun (name, srcId) => do let srcDecl ← srcId.getDecl - return s!" - Iris hypothesis in the intuitionstic context `{name}` depends on \ - `{srcDecl.userName}`" - throwError m!"iinduction: The following hypotheses depend on variables in \ + return s!"• Iris hypothesis in the intuitionstic context `{name}` depends on `{srcDecl.userName}`" + + -- Build `selPat` syntax nodes for each missing item + let newIrisPats : Array (TSyntax `selPat) ← + missingIris.toArray.mapM fun (name, _) => + `(selPat| $(mkIdent name):ident) + let newPurePats : Array (TSyntax `selPat) ← + missingPure.toArray.mapM fun (depId, _) => do + let depDecl ← depId.getDecl + let id := (Name.mkSimple depDecl.userName.toString) + `(selPat| %$(mkIdent id):ident) + + -- Reconstruct the iinduction tactic with the extended generalizing clause. + -- `← getRef` gives the original iinduction syntax node; we read the + -- other pieces (induction term, `using` clause, `with` alternatives) from it. + let oldTactic ← getRef + let `(tactic| iinduction $x $[using $r]? generalizing $genSelPats* $[$alts]?) := oldTactic + | throwError "iinduction: invalid syntax" + + -- Extend the existing generalizing patterns with the new ones + -- New items go first so the user can see what was added + let extendedPats : TSyntaxArray `selPat := + genSelPats.append <| newIrisPats ++ newPurePats + + let newTactic ← `(tactic| iinduction $x $[using $r]? generalizing $extendedPats* $[$alts]?) + + -- Log the error and attach the clickable suggestion + logError m!"iinduction: The following hypotheses depend on variables in \ the `generalizing` clause but are not themselves included:\ \n{"\n".intercalate (leanLines ++ irisLines)}" + -- Suggestion the fixed tactic + Lean.Meta.Tactic.TryThis.addSuggestion oldTactic newTactic + /-- Search for hypotheses in the regular Lean context that are the in the form of Iris goals. These must be induction hypotheses generated by Lean's built-in @@ -314,12 +347,13 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} (hyps : Hyps bi e) (goal : Q($prop)) (fvar : FVarId) (parsedAlts : Option Alts) (altRecName : Option Name) - (genSelTargets : Option <| List SelTarget) : + (genSelPats : Option <| List SelPat) : ProofModeM Q($e ⊢ $goal) := do -- Find the regular pure/Iris hypotheses to be reverted - let targets ← do match genSelTargets with + let targets ← do match genSelPats with | none => pure <| iHypsToGeneralize hyps fvar - | some genSelTargets => + | some genSelPats => + let genSelTargets ← SelPat.resolve hyps genSelPats checkDependentHyps hyps genSelTargets let explicitIrisTargets := genSelTargets.filter (match ·.kind with | .ipm _ => true | _ => false) let explicitPureTargets := genSelTargets.filter (match ·.kind with | .pure _ => true | _ => false) @@ -498,11 +532,6 @@ private def generalizeTermWithFVar (x : TSyntax `term) : TacticM FVarId := do return fvars[0]! -syntax (name := iinduction) "iinduction" colGt term - ("using" ident)? - ("generalizing" (colGt selPat)+)? - (inductionAlts)? : tactic - /-- The `iinduction` tactic applies induction in the Iris Proof Mode in a similar way as the `induction` tactic. Given an expression `e`, the application of @@ -555,11 +584,10 @@ elab_rules : tactic | none => pure none | some alts => parseInductionAlts alts - ProofModeM.runTactic λ mvar { hyps, goal, .. } => do - -- Parse the user-supplied list of variables to be generalised - let genSelTargets ← genSelPats.mapM <| fun pats => do - let parsed ← liftMacroM <| SelPat.parse pats - SelPat.resolve hyps parsed + -- Parse the selection patterns for generalising hypotheses + let genSelPats ← genSelPats.mapM <| fun pats => do + liftMacroM <| SelPat.parse pats - let pf ← iInductionCore hyps goal fvar parsedAlts recName genSelTargets + ProofModeM.runTactic λ mvar { hyps, goal, .. } => do + let pf ← iInductionCore hyps goal fvar parsedAlts recName genSelPats mvar.assign pf From e568514131a6142375a4f498975878a327a55942 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sun, 7 Jun 2026 17:33:16 +0200 Subject: [PATCH 099/181] Fix spacing in pretty TryThis suggestion using ppSpace, adjust order of messages --- Iris/Iris/ProofMode/Tactics/Induction.lean | 25 +++++++------------ Iris/Iris/Tests/Tactics.lean | 29 ++++++++++++++++------ 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 0321eba7e..787a955a2 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -20,7 +20,7 @@ open BI Std Lean Elab Tactic Meta Qq Parser.Tactic syntax (name := iinduction) "iinduction" colGt term ("using" ident)? - ("generalizing" (colGt selPat)+)? + ("generalizing" (ppSpace colGt selPat)+)? (inductionAlts)? : tactic /-- @@ -226,31 +226,24 @@ private def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} let newPurePats : Array (TSyntax `selPat) ← missingPure.toArray.mapM fun (depId, _) => do let depDecl ← depId.getDecl - let id := (Name.mkSimple depDecl.userName.toString) - `(selPat| %$(mkIdent id):ident) + let id := mkIdent (Name.mkSimple depDecl.userName.toString) + `(selPat| %$id:ident) - -- Reconstruct the iinduction tactic with the extended generalizing clause. - -- `← getRef` gives the original iinduction syntax node; we read the - -- other pieces (induction term, `using` clause, `with` alternatives) from it. + -- Find the old tactic syntax and generate the new one with missing hypotheses added let oldTactic ← getRef let `(tactic| iinduction $x $[using $r]? generalizing $genSelPats* $[$alts]?) := oldTactic | throwError "iinduction: invalid syntax" - - -- Extend the existing generalizing patterns with the new ones - -- New items go first so the user can see what was added - let extendedPats : TSyntaxArray `selPat := - genSelPats.append <| newIrisPats ++ newPurePats - + let extendedPats : TSyntaxArray `selPat := genSelPats.append <| newPurePats ++ newIrisPats let newTactic ← `(tactic| iinduction $x $[using $r]? generalizing $extendedPats* $[$alts]?) + -- Suggestion the fixed tactic + Lean.Meta.Tactic.TryThis.addSuggestion oldTactic newTactic + -- Log the error and attach the clickable suggestion - logError m!"iinduction: The following hypotheses depend on variables in \ + throwError m!"iinduction: The following hypotheses depend on variables in \ the `generalizing` clause but are not themselves included:\ \n{"\n".intercalate (leanLines ++ irisLines)}" - -- Suggestion the fixed tactic - Lean.Meta.Tactic.TryThis.addSuggestion oldTactic newTactic - /-- Search for hypotheses in the regular Lean context that are the in the form of Iris goals. These must be induction hypotheses generated by Lean's built-in diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index ec4ebb974..017492094 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2882,11 +2882,17 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : | succ n ih => itrivial /- Testing `iinduction` on `n` generalising `m`, where *regular hypothesis* `h1 : Q m` - and `X : (Q m) → Prop` depend on `m`. Moreover, `h2 : X h1` depends on `h1`. - This dependency requires manual resolution. -/ -/-- error: irevert: Lean hypothesis Y depends on m -/ + and `X : (Q m) → Prop` depend on `m`. This dependency requires manual resolution. -/ +/-- info: Try this: + [apply] iinduction n generalizing %m %h1 %X with + | zero => itrivial + | succ n ih => itrivial +--- +error: iinduction: The following hypotheses depend on variables in the `generalizing` clause but are not themselves included: +• Lean hypothesis `h1` depends on `m` +• Lean hypothesis `X` depends on `m` -/ #guard_msgs in -example [BI PROP] {P : PROP} {m n : Nat} {Q : Nat → Prop} {h1 : Q m} {X : (Q m) → Prop} {h2 : X h1} : +example [BI PROP] {P : PROP} {m n : Nat} {Q : Nat → Prop} {h1 : Q m} {X : (Q m) → Prop} : ⊢ P -∗ ⌜n + 0 = n⌝ := by iintro HP iinduction n generalizing %m with @@ -2903,11 +2909,18 @@ example [BI PROP] {P : PROP} {m n : Nat} {Q : Nat → Prop} {H : Q m} : /- Testing `iinduction` on `n` generalising `m`, where *Iris hypothesis* `□HQ : Q m` depends on `m`. This requires manual resolution. -/ -/-- error: iinduction: proofmode hypothesis HQ depends on m -/ +/-- info: Try this: + [apply] iinduction n generalizing %m HQ HR with + | zero => itrivial + | succ n ih => itrivial +--- +error: iinduction: The following hypotheses depend on variables in the `generalizing` clause but are not themselves included: +• Iris hypothesis in the intuitionstic context `HQ` depends on `m` +• Iris hypothesis in the intuitionstic context `HR` depends on `m` -/ #guard_msgs in -example [BI PROP] {P : PROP} {m n : Nat} {Q : Nat → PROP} : - ⊢ P -∗ □ Q m -∗ ⌜n + 0 = n⌝ := by - iintro HP #HQ +example [BI PROP] {P : PROP} {m n : Nat} {Q R S : Nat → PROP} : + ⊢ P -∗ □ Q m -∗ □ R m -∗ □ S n -∗ ⌜n + 0 = n⌝ := by + iintro HP #HQ #HR #HS iinduction n generalizing %m with | zero => itrivial | succ n ih => itrivial From 269d7b647449d6ad624d728d704a54c30e7d5c77 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sun, 7 Jun 2026 17:46:53 +0200 Subject: [PATCH 100/181] Fix missing introduction subgoal introduction when `with` syntax is not used --- Iris/Iris/ProofMode/Tactics/Induction.lean | 128 +++++++++++---------- 1 file changed, 66 insertions(+), 62 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 787a955a2..6bbb93102 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -420,10 +420,7 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- Handle each subgoal generated by Lean's induction for ⟨s, ctor⟩ in subgoals.toList.zip recCtors do s.mvarId.withContext do - match parsedAlts with - | none => pure () - | some parsedAlts => - s.mvarId.setTag ctor + s.mvarId.setTag ctor -- Obtain the type of the induction subgoal let sType ← instantiateMVars <| ← s.mvarId.getType @@ -441,65 +438,72 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- Introduce the induction hypothesis back into the Iris proof state let st ← addIHs pfIntHyps irisGoal.hyps ihFVars - -- Check whether the alternative names for this constructor are supplied by the user - match parsedAlts.alts.find? (matcher ctor) <|> parsedAlts.wildcard with - | some ⟨_, vars, _, stx⟩ => - if vars.size > s.fields.size then - throwOrLogErrorAt stx <| - s!"iinduction: too many variable names provided at alternative `{ctor}`: ".append - s!"{vars.size} provided, but {s.fields.size} expected" - - -- For pretty printing of arguments - for ⟨fieldFVar, varStx⟩ in s.fields.toList.zip vars.toList do - if let `(binderIdent| $id:ident) := varStx then - let lctx ← getLCtx - let fieldType ← inferType fieldFVar - addLocalVarInfo id lctx fieldFVar (some fieldType) true - - -- Check whether the user has supplied tactic sequences for this induction subgoal - let tacticSeq := (parsedAlts.alts.find? (matcher ctor) <|> parsedAlts.wildcard).bind (·.tacs) - - let firstTactic := parsedAlts.tac - - -- Generate the induction subgoal for the user, label the induction subgoal with `ctor` - let pf' ← match tacticSeq with - | none => k st.newHyps irisGoal.goal (addBIGoal · · ctor) - -- Run the tactics supplied by the user, if available - | some tacticSeq => k st.newHyps irisGoal.goal <| fun hyps goal => do - let ⟨pf, newMVars, _⟩ ← (addBIGoalRunTactics hyps goal ctor firstTactic tacticSeq) - -- Throw an error if the first tactic already solves the goal - match newMVars with - | some [] => - throwOrLogErrorAt stx - s!"iinduction: alternative `{ctor.getString!}` is not needed" - | _ => pure () - pure pf - - -- Remove the induction hypotheses from the regular Lean context - let pf' ← withoutFVars (u := 0) ihFVars.toArray <| pure pf' - - -- Fill the metavariable for the induction subgoal generated by Lean - s.mvarId.assign q($(st.pf).trans $pf') - -- Alternative names not found, acceptable only when `firstTactic` solves it + match parsedAlts with | none => - match parsedAlts.tac with - -- No first tactic given, the alternative name is missing - | none => throwMissingAlt ctor - | some firstTactic => - let pf' ← k st.newHyps irisGoal.goal <| fun hyps goal => do - let m ← mkBIGoal hyps goal ctor - let subgoals ← evalTacticAt firstTactic m.mvarId! - -- First tactic supplied by the user, but it does not completely solve this case - if !subgoals.isEmpty then - throwOrLogErrorAt parsedAlts.stx - m!"iinduction: alternative `{ctor.getString!}` has not been provided" - pure m - - -- Remove the induction hypotheses from the regular Lean context - let pf' ← withoutFVars (u := 0) ihFVars.toArray <| pure pf' - - -- Fill the metavariable for the induction subgoal generated by Lean - s.mvarId.assign q($(st.pf).trans $pf') + -- Remove the induction hypotheses from the regular Lean context + let pf' ← withoutFVars (u := 0) ihFVars.toArray <| k st.newHyps irisGoal.goal (addBIGoal · · ctor) + -- Fill the metavariable for the induction subgoal generated by Lean + s.mvarId.assign q($(st.pf).trans $pf') + + | some parsedAlts => + -- Check whether the alternative names for this constructor are supplied by the user + match parsedAlts.alts.find? (matcher ctor) <|> parsedAlts.wildcard with + | some ⟨_, vars, _, stx⟩ => + if vars.size > s.fields.size then + throwOrLogErrorAt stx <| + s!"iinduction: too many variable names provided at alternative `{ctor}`: ".append + s!"{vars.size} provided, but {s.fields.size} expected" + + -- For pretty printing of arguments + for ⟨fieldFVar, varStx⟩ in s.fields.toList.zip vars.toList do + if let `(binderIdent| $id:ident) := varStx then + let lctx ← getLCtx + let fieldType ← inferType fieldFVar + addLocalVarInfo id lctx fieldFVar (some fieldType) true + + -- Check whether the user has supplied tactic sequences for this induction subgoal + let tacticSeq := (parsedAlts.alts.find? (matcher ctor) <|> parsedAlts.wildcard).bind (·.tacs) + + let firstTactic := parsedAlts.tac + + -- Generate the induction subgoal for the user, label the induction subgoal with `ctor` + let pf' ← match tacticSeq with + | none => k st.newHyps irisGoal.goal (addBIGoal · · ctor) + -- Run the tactics supplied by the user, if available + | some tacticSeq => k st.newHyps irisGoal.goal <| fun hyps goal => do + let ⟨pf, newMVars, _⟩ ← (addBIGoalRunTactics hyps goal ctor firstTactic tacticSeq) + -- Throw an error if the first tactic already solves the goal + match newMVars with + | some [] => + throwOrLogErrorAt stx + s!"iinduction: alternative `{ctor.getString!}` is not needed" + | _ => pure () + pure pf + + -- Remove the induction hypotheses from the regular Lean context + let pf' ← withoutFVars (u := 0) ihFVars.toArray <| pure pf' + -- Fill the metavariable for the induction subgoal generated by Lean + s.mvarId.assign q($(st.pf).trans $pf') + + -- Alternative names not found, acceptable only when `firstTactic` solves it + | none => + match parsedAlts.tac with + -- No first tactic given, the alternative name is missing + | none => throwMissingAlt ctor + | some firstTactic => + let pf' ← k st.newHyps irisGoal.goal <| fun hyps goal => do + let m ← mkBIGoal hyps goal ctor + let subgoals ← evalTacticAt firstTactic m.mvarId! + -- First tactic supplied by the user, but it does not completely solve this case + if !subgoals.isEmpty then + throwOrLogErrorAt parsedAlts.stx + m!"iinduction: alternative `{ctor.getString!}` has not been provided" + pure m + + -- Remove the induction hypotheses from the regular Lean context + let pf' ← withoutFVars (u := 0) ihFVars.toArray <| pure pf' + -- Fill the metavariable for the induction subgoal generated by Lean + s.mvarId.assign q($(st.pf).trans $pf') return m From febf3f21610fac5525cbf62d237190d035393c9b Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sun, 7 Jun 2026 21:51:11 +0200 Subject: [PATCH 101/181] Bug fix: withoutFVars should wrap over addBIGoal --- Iris/Iris/ProofMode/Tactics/Induction.lean | 29 ++++++++++------------ 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 6bbb93102..b554b362a 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -467,7 +467,7 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} let firstTactic := parsedAlts.tac -- Generate the induction subgoal for the user, label the induction subgoal with `ctor` - let pf' ← match tacticSeq with + let pf' ← withoutFVars (u := 0) ihFVars.toArray <| match tacticSeq with | none => k st.newHyps irisGoal.goal (addBIGoal · · ctor) -- Run the tactics supplied by the user, if available | some tacticSeq => k st.newHyps irisGoal.goal <| fun hyps goal => do @@ -480,10 +480,8 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} | _ => pure () pure pf - -- Remove the induction hypotheses from the regular Lean context - let pf' ← withoutFVars (u := 0) ihFVars.toArray <| pure pf' - -- Fill the metavariable for the induction subgoal generated by Lean - s.mvarId.assign q($(st.pf).trans $pf') + -- Fill the metavariable for the induction subgoal generated by Lean + s.mvarId.assign q($(st.pf).trans $pf') -- Alternative names not found, acceptable only when `firstTactic` solves it | none => @@ -491,17 +489,16 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- No first tactic given, the alternative name is missing | none => throwMissingAlt ctor | some firstTactic => - let pf' ← k st.newHyps irisGoal.goal <| fun hyps goal => do - let m ← mkBIGoal hyps goal ctor - let subgoals ← evalTacticAt firstTactic m.mvarId! - -- First tactic supplied by the user, but it does not completely solve this case - if !subgoals.isEmpty then - throwOrLogErrorAt parsedAlts.stx - m!"iinduction: alternative `{ctor.getString!}` has not been provided" - pure m - - -- Remove the induction hypotheses from the regular Lean context - let pf' ← withoutFVars (u := 0) ihFVars.toArray <| pure pf' + let pf' ← withoutFVars (u := 0) ihFVars.toArray <| + k st.newHyps irisGoal.goal <| fun hyps goal => do + let m ← mkBIGoal hyps goal ctor + let subgoals ← evalTacticAt firstTactic m.mvarId! + -- First tactic supplied by the user, but it does not completely solve this case + if !subgoals.isEmpty then + throwOrLogErrorAt parsedAlts.stx + m!"iinduction: alternative `{ctor.getString!}` has not been provided" + pure m + -- Fill the metavariable for the induction subgoal generated by Lean s.mvarId.assign q($(st.pf).trans $pf') From 196450a36d238ce45eaa8963932eb2547b19ea8c Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sun, 7 Jun 2026 22:13:24 +0200 Subject: [PATCH 102/181] Minor formatting issue --- Iris/Iris/ProofMode/Tactics/Induction.lean | 2 +- Iris/Iris/Tests/Tactics.lean | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index b554b362a..8669febd0 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -78,7 +78,7 @@ private def parseInductionAlts (altsSyntax : TSyntax `Lean.Parser.Tactic.inducti | `(inductionAltLHS| | $_:hole $[$vars]*) => if parsedAlts.size < alts.size - 1 then throwErrorAt alt - s!"iinduction: invalid occurrence of the wildcard alternative `| _ => ...`:".append + s!"iinduction: invalid occurrence of the wildcard alternative `| _ => ...`: ".append "It must be the last alternative" return ⟨tac, parsedAlts, some ⟨.anonymous, ← parseVars vars, tacs, alt⟩, altsSyntax⟩ | _ => throwErrorAt l "iinduction: invalid syntax" diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 017492094..100443335 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2826,7 +2826,7 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : | _ => itrivial /- Testing `iinduction` with the hole and synthetic hole -/ -/-- error: iinduction: invalid occurrence of the wildcard alternative `| _ => ...`:It must be the last alternative -/ +/-- error: iinduction: invalid occurrence of the wildcard alternative `| _ => ...`: It must be the last alternative -/ #guard_msgs in example [BI PROP] {P Q R S T : PROP} {n : Nat} : ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by @@ -2929,7 +2929,8 @@ example [BI PROP] {P : PROP} {m n : Nat} {Q R S : Nat → PROP} : example [BI PROP] {P : PROP} {m n : Nat} {Q : Nat → PROP} : ⊢ P -∗ □ Q m -∗ ⌜n + 0 = n⌝ := by iintro HP #HQ - iinduction n generalizing %m HQ with + iinduction n generalizing %m HQ + with | zero => itrivial | succ n ih => itrivial From 4b85ced7a0ef39caedefe4a3ecda0159029e80d8 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sun, 7 Jun 2026 22:36:51 +0200 Subject: [PATCH 103/181] Minor refinements: tactic explanation --- Iris/Iris/ProofMode/Tactics/Induction.lean | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 8669febd0..c012f0830 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -558,10 +558,15 @@ private def generalizeTermWithFVar (x : TSyntax `term) : TacticM FVarId := do ``` By applying `iinduction n`, all spatial hypotheses (`HP` and `HS`) are - reverted. The hypothesis `HT` is also reverted because it involves `n`. - By applying `iinduction n generalizing HQ %R`, the hypotheses `HQ` and `HR` + reverted. The hypothesis `HT` is also reverted because it involves the + induction target `n`. + By applying `iinduction n generalizing HQ`, the hypotheses `HQ` are additionally reverted and thus included as premises in the induction hypothesis. + One can also generalise pure variables in the regular Lean context. However, + if there exists some another pure/Iris hypothesis that is forward-dependent. + For example, `iinduction n generalizing %R` is not valid as `HR` depends on `R`. + Instead, one can use `iinduction n generalizing %R HR`. -/ elab_rules : tactic | `(tactic| iinduction $x From 5d00ede250629bfd7e7601cbf0932f17244b9245 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sun, 14 Jun 2026 16:04:44 +0200 Subject: [PATCH 104/181] Update intuitionistically_sep_2 as intuitionistically_sep_mpr in Expr.lean for Hyps.buildIntuitionisticProof --- Iris/Iris/ProofMode/Expr.lean | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Iris/Iris/ProofMode/Expr.lean b/Iris/Iris/ProofMode/Expr.lean index 3b5f8f9f0..4ab7ceeb5 100644 --- a/Iris/Iris/ProofMode/Expr.lean +++ b/Iris/Iris/ProofMode/Expr.lean @@ -555,4 +555,4 @@ def Hyps.buildIntuitionisticProof {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} | .sep _ _ _ _ lhs rhs => do let pfL ← buildIntuitionisticProof lhs let pfR ← buildIntuitionisticProof rhs - some q((sep_mono $pfL $pfR).trans intuitionistically_sep_2) + some q((sep_mono $pfL $pfR).trans intuitionistically_sep_mpr) From 8c29bedcc377b09f6a16f67d1d2329ac4a900fa0 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sun, 14 Jun 2026 16:06:25 +0200 Subject: [PATCH 105/181] Update sep_mono_l and sep_mono_r as sep_mono_left and sep_mono_right in Induction.lean --- Iris/Iris/ProofMode/Tactics/Induction.lean | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index c012f0830..2b69ded76 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -117,10 +117,10 @@ theorem revert_IH [BI PROP] {P Q R : PROP} {φ} P ⊢ (Q ∗ □ R) := calc _ ⊢ □ P := h1 _ ⊢ □ P ∗ □ P := intuitionistically_sep_dup.mp - _ ⊢ □ P ∗ □ □ P := sep_mono_r intuitionistically_idem.mpr - _ ⊢ □ P ∗ □ R := sep_mono_r <| intuitionistically_mono <| inst.into_ih ih - _ ⊢ □ Q ∗ □ R := sep_mono_l <| intuitionistically_mono h2 - _ ⊢ Q ∗ □ R := sep_mono_l intuitionistically_elim + _ ⊢ □ P ∗ □ □ P := sep_mono_right intuitionistically_idem.mpr + _ ⊢ □ P ∗ □ R := sep_mono_right <| intuitionistically_mono <| inst.into_ih ih + _ ⊢ □ Q ∗ □ R := sep_mono_left <| intuitionistically_mono h2 + _ ⊢ Q ∗ □ R := sep_mono_left intuitionistically_elim /-- Designed to be a mutable state such that `newHyps` contains induction From 17aefd6fe2995c06870298fc5d67d91fe1ca1371 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sun, 14 Jun 2026 16:13:19 +0200 Subject: [PATCH 106/181] iinduction usage: wp_makeList in HeapLang/Lib/Quicksort.lean --- Iris/Iris/HeapLang/Lib/Quicksort.lean | 5 ++--- Iris/Iris/Tests/Tactics.lean | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Iris/Iris/HeapLang/Lib/Quicksort.lean b/Iris/Iris/HeapLang/Lib/Quicksort.lean index 9df5c720a..a2288fc17 100644 --- a/Iris/Iris/HeapLang/Lib/Quicksort.lean +++ b/Iris/Iris/HeapLang/Lib/Quicksort.lean @@ -285,13 +285,12 @@ theorem quicksort_spec l ls Φ : theorem wp_makeList (l : List Int) (Φ : Val → IProp GF) : (∀ v, isList v l -∗ Φ v) -∗ WP hl(&(makeList l)) {{ Φ }} := by - induction l generalizing Φ with + iintro HΦ + iinduction l generalizing %Φ with | nil => - iintro HΦ unfold makeList iapply nil_spec $$ HΦ | cons l ls ih => - iintro HΦ rw [makeList] wp_pures wp_bind &(makeList _) diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 74f2b1c88..76b55fc6f 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2990,8 +2990,7 @@ example [BI PROP] {P : PROP} {m n : Nat} {Q R S : Nat → PROP} : example [BI PROP] {P : PROP} {m n : Nat} {Q : Nat → PROP} : ⊢ P -∗ □ Q m -∗ ⌜n + 0 = n⌝ := by iintro HP #HQ - iinduction n generalizing %m HQ - with + iinduction n generalizing %m HQ with | zero => itrivial | succ n ih => itrivial From f1f02636e789afee3f97f2972231694ff29ba218 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sun, 14 Jun 2026 16:17:03 +0200 Subject: [PATCH 107/181] iinduction usage: Instances/Lib/FUpd.lean --- Iris/Iris/Instances/Lib/FUpd.lean | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/Iris/Iris/Instances/Lib/FUpd.lean b/Iris/Iris/Instances/Lib/FUpd.lean index b150420b0..453a85bd3 100644 --- a/Iris/Iris/Instances/Lib/FUpd.lean +++ b/Iris/Iris/Instances/Lib/FUpd.lean @@ -170,28 +170,26 @@ theorem lc_fupd_add_later {E1 E2 : CoPset} {P : IProp GF} : ⊢ £ 1 -∗ (▷ | @[rocq_alias lc_fupd_add_laterN] theorem lc_fupd_add_laterN (n : Nat) {E : CoPset} {P : IProp GF} : ⊢ £ n -∗ (▷^[n] |={E}=> P) -∗ |={E}=> P := by - induction n generalizing P with + iintro Hf Hupd + iinduction n with | zero => - iintro _ H simp [BIBase.laterN] - iexact H + iexact Hupd | succ n IH => - iintro Hf Hupd icases Hf with ⟨H1, Hf⟩ iapply lc_fupd_add_later $$ H1 inext iapply IH $$ [$] [$] -@[rocq_alias lc_fupd_add_step_fupdN ] +@[rocq_alias lc_fupd_add_step_fupdN] theorem lc_fupd_add_step_fupdN (E1 E2 E3: CoPset) (P : IProp GF) (n : Nat) : - £ n -∗ (|={E1}[E2]▷=>^[n] |={E1,E3}=> P) -∗ |={E1,E3}=> P := by - induction n generalizing P with + £ n -∗ (|={E1}[E2]▷=>^[n] |={E1,E3}=> P) -∗ |={E1,E3}=> P := by + iintro Hf Hupd + iinduction n with | zero => - iintro _ H simp only [Nat.repeat] - iexact H + iexact Hupd | succ n IH => - iintro Hf Hupd simp only [Nat.repeat] imod Hupd icases Hf with ⟨H1, Hf⟩ @@ -404,12 +402,12 @@ instance elimModal_fupd_fupd_finally p (E1 E2 : CoPset) (P Q : IProp GF) : @[rocq_alias step_fupdN_fupd_finally] theorem step_fupdN_fupd_finally (E1 E2 : CoPset) (n : Nat) (P : IProp GF) : (|={E1}[E2]▷=>^[n] |={E1|}=> P) ⊢ |={E1|}=> ▷^[n] ◇ P := by - induction n with + iintro HP + iinduction n with | zero => simp only [Nat.repeat] exact fupd_finally_mono except0_intro | succ n IH => - iintro HP simp only [Nat.repeat] imod HP iapply fupd_finally_mono (later_laterN n).mpr From 2d26cef9dff8a0bc6bab825d418c84ea96e98984 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sun, 14 Jun 2026 16:34:42 +0200 Subject: [PATCH 108/181] iinduction usage: wp_pure_step_fupd in ProgramLogic/Lifting.lean --- Iris/Iris/ProgramLogic/Lifting.lean | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Iris/Iris/ProgramLogic/Lifting.lean b/Iris/Iris/ProgramLogic/Lifting.lean index ef4d2ca9f..ea75f9012 100644 --- a/Iris/Iris/ProgramLogic/Lifting.lean +++ b/Iris/Iris/ProgramLogic/Lifting.lean @@ -188,9 +188,8 @@ theorem wp_pure_step_fupd [Inhabited State] (E₂ : CoPset) (|={E}[E₂]▷=>^[n] £ n -∗ WP e₂ @ s; E {{ Φ }}) ⊢ WP e₁ @ s; E {{ Φ }} := by iintro Hwp replace Hexec := Hexec.pureExec Hφ - induction Hexec using Relation.Iterate.head_induction_on with + iinduction Hexec using Relation.Iterate.head_induction_on with simp only [Nat.repeat] | rfl => - simp only [Nat.repeat] rw (occs := [2]) [fupd_wp_iff.to_eq] icases lc_zero with >Hz iapply Hwp $$ Hz @@ -201,7 +200,6 @@ theorem wp_pure_step_fupd [Inhabited State] (E₂ : CoPset) intro σ have _ : PrimStep.Reducible (e₁, σ) := by grind only [reducible_of_reducibleNoObs] grind [cases Stuckness] - simp only [Nat.repeat] iapply step_fupd_wand $$ Hwp iintro Hwp Hone iapply IH From bf2fbe21208eb33a6a778ba0bbf7c020c4ec3e72 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 15 Jun 2026 09:48:14 +0200 Subject: [PATCH 109/181] Simplify alternative name comparison --- Iris/Iris/ProofMode/Tactics/Induction.lean | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 2b69ded76..2a3168fac 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -85,13 +85,6 @@ private def parseInductionAlts (altsSyntax : TSyntax `Lean.Parser.Tactic.inducti return ⟨tac, parsedAlts, none, altsSyntax⟩ | _ => throwErrorAt altsSyntax "iinduction: invalid syntax" -/-- - Check whether a fully-qualified constructor name (e.g. `Nat.succ`) matches a - user-written short name (e.g. `succ`) or an already-qualified name. --/ -private def matchesCtorName (fullName : Name) (userShort : Name) : Bool := - fullName == userShort || fullName.getString! == userShort.getString! - /-- This theorem is used for updating the proof in `InductionState` as `addIHs` iterates through the list of induction hypotheses and introduces them from @@ -308,8 +301,8 @@ private def checkCtors (ctors : List Name) (parsedAlts : Alts) : ProofModeM Unit let alts := parsedAlts.alts.toList for alt in alts do - let isValid := ctors.any fun ctor => matchesCtorName ctor alt.ctor - let isDup := alts.countP (fun alt' => matchesCtorName alt.ctor alt'.ctor) > 1 + let isValid := ctors.any (· == alt.ctor) + let isDup := alts.countP (alt.ctor == ·.ctor) > 1 if !isValid then throwOrLogErrorAt alt.stx @@ -372,7 +365,7 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} | _ => throwError "iinduction: unable to determine inductive type" let matcher : Name → Alt → Bool := - fun ctor alt => alt.ctor != .anonymous && matchesCtorName ctor alt.ctor + fun ctor alt => alt.ctor != .anonymous && ctor == alt.ctor -- Find the constructor names let recCtors := ((← Lean.Meta.getElimInfo recName).altsInfo.map (·.name)).toList From 02678a2a3ae81d74b20c6200907bd3111559b70d Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 15 Jun 2026 09:59:39 +0200 Subject: [PATCH 110/181] Minor refinements: tests for error messages and alternative names --- Iris/Iris/ProofMode/Tactics/Induction.lean | 16 ++++++++++++---- Iris/Iris/Tests/Tactics.lean | 11 ++++------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 2a3168fac..3c52593d0 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -297,12 +297,16 @@ private def addIHs {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e : Q($prop)} private def throwMissingAlt {α} (ctor : Name) : ProofModeM α := throwError "iinduction: alternative `{ctor.getString!}` has not been provided" +/-- + Check that all the alternative names are valid and that there are no duplicates. + Otherwise, throw an error on the corresponding line. +-/ private def checkCtors (ctors : List Name) (parsedAlts : Alts) : ProofModeM Unit := do - let alts := parsedAlts.alts.toList - - for alt in alts do + let rec checkCtorsRec : List Alt → ProofModeM Unit + | [] => pure () + | alt :: alts => do let isValid := ctors.any (· == alt.ctor) - let isDup := alts.countP (alt.ctor == ·.ctor) > 1 + let isDup := alts.any (·.ctor == alt.ctor) if !isValid then throwOrLogErrorAt alt.stx @@ -311,6 +315,10 @@ private def checkCtors (ctors : List Name) (parsedAlts : Alts) : ProofModeM Unit throwOrLogErrorAt alt.stx m!"iinduction: duplicate alternative name `{alt.ctor}`" + checkCtorsRec alts + + checkCtorsRec parsedAlts.alts.toList + /-- The main function handling the steps for the `iinduction` tactic. diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 76b55fc6f..fa60d0c87 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2789,10 +2789,9 @@ example [BI PROP] {P Q R S : PROP} {T : Nat → PROP} {n : Nat} : ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T n -∗ ⌜n + 0 = n⌝ := by iintro HP #HQ #HR HS #HT iinduction n generalizing HQ %R HR HP %P with - -- Using the full name of the constructor (`Nat.zero`) - | Nat.zero => itrivial - /- Using the short name of the constructor (`succ`), naming the induction - hypothesis as `ih`, but leaving the variable `n` inaccessible by using `_` -/ + | zero => itrivial + /- Naming the induction hypothesis as `ih`, but leaving the variable `n` + inaccessible by using `_` -/ | succ _ ih => iframe; itrivial /- Tests `iinduction` with a non-inductive datatype -/ @@ -2808,8 +2807,6 @@ error: iinduction: duplicate alternative name `zero` --- error: iinduction: invalid alternative name `invalidB` --- -error: iinduction: duplicate alternative name `Nat.zero` ---- error: iinduction: alternative `succ` has not been provided -/ #guard_msgs in example [BI PROP] {P Q R S T : PROP} {n : Nat} : @@ -2819,7 +2816,7 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : | invalidA => done | zero => itrivial | invalidB => done - | Nat.zero => itrivial + | zero => itrivial /- Tests `iinduction` with extra arguments supplied by the user -/ /-- error: iinduction: too many variable names provided at alternative `succ`: 4 provided, but 2 expected -/ From ae4fd2dc27a0eb4746e49a4556861f2123d51106 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 15 Jun 2026 10:25:45 +0200 Subject: [PATCH 111/181] Tests with binary tree --- Iris/Iris/Tests/Tactics.lean | 65 +++++++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index fa60d0c87..d8ec311b8 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2779,6 +2779,41 @@ end iloeb section iinduction +/-- Inductively defined binary tree data structure -/ +inductive Tree (α : Type u) where + | leaf : Tree α + | node : Tree α → α → Tree α → Tree α + deriving Repr + +/-- + Tests `iinduction` with simple induction on binary trees. + All propositions involved are in the intuitionistic context in this example. + Tests the use of a hole (`_`) for leaving a variable unnamed. +-/ +example [BI PROP] {α} {t : Tree α} {P : Tree α → PROP} : + □ P .leaf -∗ □ (∀ l x r, P l -∗ P r -∗ P (.node l x r)) -∗ P t := by + iintro #H1 #H2 + iinduction t with + | leaf => iexact H1 + | node l _ r IH1 IH2 => + iapply H2 + · iexact IH1 + · iexact IH2 + +/-- + Tests `iinduction` with simple induction on binary trees. + Tries `iframe` to solve induction subgoals before splitting into cases. + Tests the use of a synthetic hole (`?_`) for delaying the induction subgoal. +-/ +example [BI PROP] {n : Nat} {P : Nat → PROP} : + □ (∀ m, P m -∗ P (m + 1)) -∗ P 0 -∗ P n := by + iintro #H1 H2 + iinduction n with iframe + | succ n IH => ?_ + iapply H1 + iapply IH + iexact H2 + /-- Tests `iinduction` with induction on natural numbers. @@ -2794,6 +2829,21 @@ example [BI PROP] {P Q R S : PROP} {T : Nat → PROP} {n : Nat} : inaccessible by using `_` -/ | succ _ ih => iframe; itrivial + + + + + + + + + + + + + + + /- Tests `iinduction` with a non-inductive datatype -/ /-- error: iinduction: unable to determine inductive type -/ #guard_msgs in @@ -2837,21 +2887,6 @@ example [BI PROP] {P R S : PROP} {Q T : Nat → PROP} {n : Nat} : | zero => itrivial | ind n ih => itrivial -inductive Tree (α : Type u) where - | leaf : Tree α - | node : Tree α → α → Tree α → Tree α - deriving Repr - -example [BI PROP] {α} {t : Tree α} {P : Tree α → PROP} : - □ P .leaf -∗ □ (∀ l x r, P l -∗ P r -∗ P (.node l x r)) -∗ P t := by - iintro #H1 #H2 - iinduction t with - | leaf => iexact H1 - | node l y r ih1 ih2 => - iapply H2 - · iexact ih1 - · iexact ih2 - /-- Testing `iinduction` with the same tactic sequence for two constructors -/ example [BI PROP] {P Q R S T : PROP} {n : Nat} : ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by From a5fcc553abc77f4acfa1a8d057c890127fae2065 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 15 Jun 2026 10:38:35 +0200 Subject: [PATCH 112/181] Tests with natural numbers and lists --- Iris/Iris/Tests/Tactics.lean | 38 +++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index d8ec311b8..639295940 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2801,19 +2801,36 @@ example [BI PROP] {α} {t : Tree α} {P : Tree α → PROP} : · iexact IH2 /-- - Tests `iinduction` with simple induction on binary trees. + Tests `iinduction` with simple induction on natural numbers. Tries `iframe` to solve induction subgoals before splitting into cases. + Tests the `using` clause for custom recursor name. Tests the use of a synthetic hole (`?_`) for delaying the induction subgoal. -/ example [BI PROP] {n : Nat} {P : Nat → PROP} : - □ (∀ m, P m -∗ P (m + 1)) -∗ P 0 -∗ P n := by + □ (∀ k, P k -∗ P (k + 1)) -∗ P 0 -∗ P n := by iintro #H1 H2 - iinduction n with iframe + iinduction n using Nat.rec with iframe | succ n IH => ?_ iapply H1 iapply IH iexact H2 +/-- + Tests `iinduction` with induction on lists where it is necessary to + generalise some variables. +-/ +example [BI PROP] {α} {xs : List α} {acc : List α} {P : List α → List α → PROP} : + □ (∀ acc, P [] acc) -∗ + □ (∀ x xs acc, P xs (x :: acc) -∗ P (x :: xs) acc) -∗ + P xs acc := by + iintro #Hnil #Hcons + iinduction xs generalizing %acc with + | nil => + iapply Hnil + | cons x xs ih => + iapply Hcons + iexact ih + /-- Tests `iinduction` with induction on natural numbers. @@ -2829,21 +2846,6 @@ example [BI PROP] {P Q R S : PROP} {T : Nat → PROP} {n : Nat} : inaccessible by using `_` -/ | succ _ ih => iframe; itrivial - - - - - - - - - - - - - - - /- Tests `iinduction` with a non-inductive datatype -/ /-- error: iinduction: unable to determine inductive type -/ #guard_msgs in From 7714f791e22f529f7a58de0427e16aa319170d6f Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 15 Jun 2026 10:46:52 +0200 Subject: [PATCH 113/181] Consolidate iinduction tests --- Iris/Iris/Tests/Tactics.lean | 69 ++++++++++++------------------------ 1 file changed, 23 insertions(+), 46 deletions(-) diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 639295940..17c698cb2 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2831,28 +2831,16 @@ example [BI PROP] {α} {xs : List α} {acc : List α} {P : List α → List α iapply Hcons iexact ih -/-- - Tests `iinduction` with induction on natural numbers. - - For natural numbers, `Nat.recAux` is used as the default recursor name. Hence, - the tactic is equivalent to `iinduction n using Nat.recAux generalizing %P HQ %R`. --/ -example [BI PROP] {P Q R S : PROP} {T : Nat → PROP} {n : Nat} : - ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T n -∗ ⌜n + 0 = n⌝ := by - iintro HP #HQ #HR HS #HT - iinduction n generalizing HQ %R HR HP %P with - | zero => itrivial - /- Naming the induction hypothesis as `ih`, but leaving the variable `n` - inaccessible by using `_` -/ - | succ _ ih => iframe; itrivial - -/- Tests `iinduction` with a non-inductive datatype -/ +/- Tests `iinduction` with a non-inductive datatype. -/ /-- error: iinduction: unable to determine inductive type -/ #guard_msgs in example [BI PROP] {P : PROP} : ⊢ P := by iinduction P -/- Tests `iinduction` with induction on natural numbers with invalid user-supplied names -/ +/- + Tests `iinduction` with induction on natural numbers with invalid, duplicate + and missing user-supplied alternative names. +-/ /-- error: iinduction: invalid alternative name `invalidA` --- error: iinduction: duplicate alternative name `zero` @@ -2861,9 +2849,8 @@ error: iinduction: invalid alternative name `invalidB` --- error: iinduction: alternative `succ` has not been provided -/ #guard_msgs in -example [BI PROP] {P Q R S T : PROP} {n : Nat} : - ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by - iintro HP #HQ #HR HS #HT +example [BI PROP] {n : Nat} : + ⊢@{PROP} ⌜n + 0 = n⌝ := by iinduction n with | invalidA => done | zero => itrivial @@ -2873,38 +2860,28 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : /- Tests `iinduction` with extra arguments supplied by the user -/ /-- error: iinduction: too many variable names provided at alternative `succ`: 4 provided, but 2 expected -/ #guard_msgs in -example [BI PROP] {P Q R S T : PROP} {n : Nat} : - ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by - iintro HP #HQ #HR HS #HT +example [BI PROP] {n : Nat} : + ⊢@{PROP} ⌜n + 0 = n⌝ := by iinduction n with | zero => itrivial | succ n ih extra1 extra2 => itrivial -/-- Tests `iinduction` using a custom recursor name (strong induction), - performing induction on an expression `n + m` -/ +/-- + Tests `iinduction` using a custom recursor name (strong induction). + Tests induction on an expression `n + m`, which requires generalisation. + Tests the use of the same tactic sequences for multiple alternative names. + Note that `P` and `S` are reverted and thus included as wand premises + in the induction hypothesis. + Meanwhile, `T (n + m)` is also reverted because it involves the induction + target `n + m`. + The proposition `Q m` is reverted manually using the `generalizing` clause. + On the contrary, `R` is not reverted. +-/ example [BI PROP] {P R S : PROP} {Q T : Nat → PROP} {n : Nat} : - ⊢ P -∗ □ Q m -∗ □ R -∗ S -∗ □ T n -∗ ⌜n + m + 0 = n + m⌝ := by - iintro HP #HQ #HR HS #HT - iinduction n + m using Nat.caseStrongRecOn with - | zero => itrivial - | ind n ih => itrivial - -/-- Testing `iinduction` with the same tactic sequence for two constructors -/ -example [BI PROP] {P Q R S T : PROP} {n : Nat} : - ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by - iintro HP #HQ #HR HS #HT - iinduction n with - | zero | succ => itrivial - -/-- Testing `iinduction` with the hole and synthetic hole -/ -example [BI PROP] {P Q R S T : PROP} {n : Nat} : - ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by + ⊢ P -∗ □ Q m -∗ □ R -∗ S -∗ □ T (n + m) -∗ ⌜n + m + 0 = n + m⌝ := by iintro HP #HQ #HR HS #HT - iinduction n with - | zero => ?_ - | succ n ih => _ - itrivial - itrivial + iinduction n + m using Nat.caseStrongRecOn generalizing %m HQ with + | zero | ind _ _ => itrivial /-- Testing `iinduction` with wildcard for one case -/ example [BI PROP] {P Q R S T : PROP} {n : Nat} : From 5782aee95b59432dda31018e73238d7de9bce7e0 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 15 Jun 2026 10:58:10 +0200 Subject: [PATCH 114/181] More consolidation of iinduction tests --- Iris/Iris/Tests/Tactics.lean | 39 ++++++++++++++---------------------- 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 17c698cb2..1adda9149 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2818,6 +2818,7 @@ example [BI PROP] {n : Nat} {P : Nat → PROP} : /-- Tests `iinduction` with induction on lists where it is necessary to generalise some variables. + Tests the use of the wildcard (`_`) for remaining cases. -/ example [BI PROP] {α} {xs : List α} {acc : List α} {P : List α → List α → PROP} : □ (∀ acc, P [] acc) -∗ @@ -2825,11 +2826,11 @@ example [BI PROP] {α} {xs : List α} {acc : List α} {P : List α → List α P xs acc := by iintro #Hnil #Hcons iinduction xs generalizing %acc with - | nil => - iapply Hnil | cons x xs ih => iapply Hcons iexact ih + | _ => + iapply Hnil /- Tests `iinduction` with a non-inductive datatype. -/ /-- error: iinduction: unable to determine inductive type -/ @@ -2883,37 +2884,27 @@ example [BI PROP] {P R S : PROP} {Q T : Nat → PROP} {n : Nat} : iinduction n + m using Nat.caseStrongRecOn generalizing %m HQ with | zero | ind _ _ => itrivial -/-- Testing `iinduction` with wildcard for one case -/ -example [BI PROP] {P Q R S T : PROP} {n : Nat} : - ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by - iintro HP #HQ #HR HS #HT - iinduction n with - | zero | _ => itrivial - -/-- Testing `iinduction` with wildcard for two cases -/ -example [BI PROP] {P Q R S T : PROP} {n : Nat} : - ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by - iintro HP #HQ #HR HS #HT - iinduction n with - | _ => itrivial - -/- Testing `iinduction` with the hole and synthetic hole -/ +/- + Tests `iinduction` with invalid use of the wildcard. The wildcard + should always be the last case. +-/ /-- error: iinduction: invalid occurrence of the wildcard alternative `| _ => ...`: It must be the last alternative -/ #guard_msgs in -example [BI PROP] {P Q R S T : PROP} {n : Nat} : - ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by - iintro HP #HQ #HR HS #HT +example [BI PROP] {n : Nat} : + ⊢@{PROP} ⌜n + 0 = n⌝ := by iinduction n with | zero => itrivial | _ => _ | succ n ih => itrivial -/- Testing `iinduction` with the hole and synthetic hole -/ +/- + Testing `iinduction` with redundant use of the wildcard. The wildcard + is not required when all cases have already been handled. +-/ /-- error: iinduction: wildcard alternative is not needed -/ #guard_msgs in -example [BI PROP] {P Q R S T : PROP} {n : Nat} : - ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜n + 0 = n⌝ := by - iintro HP #HQ #HR HS #HT +example [BI PROP] {n : Nat} : + ⊢@{PROP} ⌜n + 0 = n⌝ := by iinduction n with | zero => itrivial | succ n ih => itrivial From b71df06ddd7932b74d0f65b9134579242f45b8a3 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 15 Jun 2026 11:07:18 +0200 Subject: [PATCH 115/181] More consolidation of iinduction tests: `generalize` syntax --- Iris/Iris/Tests/Tactics.lean | 97 ++++++++++++------------------------ 1 file changed, 32 insertions(+), 65 deletions(-) diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 1adda9149..2f51925fb 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2826,9 +2826,9 @@ example [BI PROP] {α} {xs : List α} {acc : List α} {P : List α → List α P xs acc := by iintro #Hnil #Hcons iinduction xs generalizing %acc with - | cons x xs ih => + | cons x xs IH => iapply Hcons - iexact ih + iexact IH | _ => iapply Hnil @@ -2865,7 +2865,7 @@ example [BI PROP] {n : Nat} : ⊢@{PROP} ⌜n + 0 = n⌝ := by iinduction n with | zero => itrivial - | succ n ih extra1 extra2 => itrivial + | succ n IH extra1 extra2 => itrivial /-- Tests `iinduction` using a custom recursor name (strong induction). @@ -2895,7 +2895,7 @@ example [BI PROP] {n : Nat} : iinduction n with | zero => itrivial | _ => _ - | succ n ih => itrivial + | succ n IH => itrivial /- Testing `iinduction` with redundant use of the wildcard. The wildcard @@ -2907,26 +2907,14 @@ example [BI PROP] {n : Nat} : ⊢@{PROP} ⌜n + 0 = n⌝ := by iinduction n with | zero => itrivial - | succ n ih => itrivial + | succ n IH => itrivial | _ => _ -/- Testing `iinduction` with first tactic after `with` syntax -/ -example [BI PROP] {P Q R S T : PROP} {m n : Nat} : - ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜m + 0 = m⌝ -∗ ⌜n + 0 = n⌝ := by - iintro HP #HQ #HR HS #HT - iinduction n with simp - | zero => itrivial - | succ n ih => itrivial - -/- Testing `iinduction` with first tactic after `with` syntax -/ -example [BI PROP] {P Q R S T : PROP} {m n : Nat} : - ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜m + 0 = m⌝ -∗ ⌜n + 0 = n⌝ := by - iintro HP #HQ #HR HS #HT - iinduction n with (cases m) - | zero => itrivial - | succ n ih => itrivial - -/- Testing `iinduction` with first tactic after `with` syntax, redundant alternative name -/ +/- + Testing `iinduction` with the tactic after `with` syntax. + One of the alternative names (`zero`) becomes redundant and therefore should + be detected by the tactic. +-/ /-- error: iinduction: alternative `zero` is not needed -/ #guard_msgs in example [BI PROP] {P Q R S T : PROP} {n : Nat} : @@ -2934,66 +2922,45 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : iintro HP #HQ #HR HS #HT #H iinduction n with (try iexact H) | zero => itrivial -- Redundant case - | succ n ih => itrivial + | succ n IH => itrivial -/- Testing `iinduction` with first tactic after `with` syntax, no redundant alternative name -/ +/- + Testing `iinduction` with a tactic after `with` syntax. + One of the alternative names (`zero`) is redundant and therefore not required. + The tactic should not complain about any missing alternative names. +-/ example [BI PROP] {P Q R S T : PROP} {n : Nat} : ⊢ P -∗ □ Q -∗ □ R -∗ S -∗ □ T -∗ ⌜0 + 0 = 0⌝ -∗ ⌜n + 0 = n⌝ := by iintro HP #HQ #HR HS #HT #H iinduction n with (try iexact H) -- No complaints about missing `zero` case - | succ n ih => itrivial - -/- Testing `iinduction` on `n` generalising `m`, where *regular hypothesis* `h1 : Q m` - and `X : (Q m) → Prop` depend on `m`. This dependency requires manual resolution. -/ -/-- info: Try this: - [apply] iinduction n generalizing %m %h1 %X with - | zero => itrivial - | succ n ih => itrivial ---- -error: iinduction: The following hypotheses depend on variables in the `generalizing` clause but are not themselves included: -• Lean hypothesis `h1` depends on `m` -• Lean hypothesis `X` depends on `m` -/ -#guard_msgs in -example [BI PROP] {P : PROP} {m n : Nat} {Q : Nat → Prop} {h1 : Q m} {X : (Q m) → Prop} : - ⊢ P -∗ ⌜n + 0 = n⌝ := by - iintro HP - iinduction n generalizing %m with - | zero => itrivial - | succ n ih => itrivial + | succ n IH => itrivial -/-- Testing `iinduction` on `n` generalising `m` and `H`, which depends on `m`. -/ -example [BI PROP] {P : PROP} {m n : Nat} {Q : Nat → Prop} {H : Q m} : - ⊢ P -∗ ⌜n + 0 = n⌝ := by - iintro HP - iinduction n generalizing %m %H with - | zero => itrivial - | succ n ih => itrivial - -/- Testing `iinduction` on `n` generalising `m`, where *Iris hypothesis* `□HQ : Q m` - depends on `m`. This requires manual resolution. -/ +/- + Testing `iinduction` on `n` generalising `m`, where: + - *regular hypotheses* `h : T m` and `U : (T m) → Prop` depend on `m`; + - *regular hypothesis* `h2 : U h` depends on `h`, which indirectly depends on `m`; and + - *Iris hypothesis* `□HQ : Q m` depends on `m`. + This requires manual resolution. +-/ /-- info: Try this: - [apply] iinduction n generalizing %m HQ HR with + [apply] iinduction n generalizing %m %h %U %h2 HQ HR with | zero => itrivial - | succ n ih => itrivial + | succ n IH => itrivial --- error: iinduction: The following hypotheses depend on variables in the `generalizing` clause but are not themselves included: +• Lean hypothesis `h` depends on `m` +• Lean hypothesis `U` depends on `m` +• Lean hypothesis `h2` depends on `m` • Iris hypothesis in the intuitionstic context `HQ` depends on `m` • Iris hypothesis in the intuitionstic context `HR` depends on `m` -/ #guard_msgs in -example [BI PROP] {P : PROP} {m n : Nat} {Q R S : Nat → PROP} : +example [BI PROP] {P : PROP} {m n : Nat} {Q R S : Nat → PROP} {T : Nat → Prop} + {h : T m} {U : (T m) → Prop} {h2 : U h} : ⊢ P -∗ □ Q m -∗ □ R m -∗ □ S n -∗ ⌜n + 0 = n⌝ := by iintro HP #HQ #HR #HS iinduction n generalizing %m with | zero => itrivial - | succ n ih => itrivial - -/-- Testing `iinduction` on `n` generalising `m` and `HQ`, which depends on `m`. -/ -example [BI PROP] {P : PROP} {m n : Nat} {Q : Nat → PROP} : - ⊢ P -∗ □ Q m -∗ ⌜n + 0 = n⌝ := by - iintro HP #HQ - iinduction n generalizing %m HQ with - | zero => itrivial - | succ n ih => itrivial + | succ n IH => itrivial end iinduction From 5f81ec0c63a0c83efdc13c15734239734aad852a Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 15 Jun 2026 11:17:24 +0200 Subject: [PATCH 116/181] Minor comment fix --- Iris/Iris/Tests/Tactics.lean | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 2f51925fb..ff4d658ec 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2898,7 +2898,7 @@ example [BI PROP] {n : Nat} : | succ n IH => itrivial /- - Testing `iinduction` with redundant use of the wildcard. The wildcard + Tests `iinduction` with redundant use of the wildcard. The wildcard is not required when all cases have already been handled. -/ /-- error: iinduction: wildcard alternative is not needed -/ @@ -2911,7 +2911,7 @@ example [BI PROP] {n : Nat} : | _ => _ /- - Testing `iinduction` with the tactic after `with` syntax. + Tests `iinduction` with the tactic after `with` syntax. One of the alternative names (`zero`) becomes redundant and therefore should be detected by the tactic. -/ @@ -2925,7 +2925,7 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : | succ n IH => itrivial /- - Testing `iinduction` with a tactic after `with` syntax. + Tests `iinduction` with a tactic after `with` syntax. One of the alternative names (`zero`) is redundant and therefore not required. The tactic should not complain about any missing alternative names. -/ @@ -2937,10 +2937,10 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : | succ n IH => itrivial /- - Testing `iinduction` on `n` generalising `m`, where: + Tests `iinduction` on `n` generalising `m`, where: - *regular hypotheses* `h : T m` and `U : (T m) → Prop` depend on `m`; - *regular hypothesis* `h2 : U h` depends on `h`, which indirectly depends on `m`; and - - *Iris hypothesis* `□HQ : Q m` depends on `m`. + - *Iris hypotheses* `□HQ : Q m` and `□HR : R m` depends on `m`. This requires manual resolution. -/ /-- info: Try this: From 107e19d6606a4bb0f8f9d96e589157cf43e87f38 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 15 Jun 2026 11:57:22 +0200 Subject: [PATCH 117/181] Another test with Tree.mirror --- Iris/Iris/Tests/Tactics.lean | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index ff4d658ec..6d02db781 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2800,6 +2800,24 @@ example [BI PROP] {α} {t : Tree α} {P : Tree α → PROP} : · iexact IH1 · iexact IH2 +/-- A simple function on the inductive structure `Tree` -/ +def Tree.mirror : Tree α → Tree α + | .leaf => .leaf + | .node l x r => .node (.mirror r) x (.mirror l) + +/-- + Tests `iinduction` with `Tree` and `Tree.mirror`. +-/ +example [BI PROP] {α} {t : Tree α} : + ⊢@{PROP} ⌜.mirror (.mirror t) = t⌝ := by + iinduction t with simp [Tree.mirror] + | leaf => + itrivial + | node l x r ihl ihr => + isplit + · iexact ihl + · iexact ihr + /-- Tests `iinduction` with simple induction on natural numbers. Tries `iframe` to solve induction subgoals before splitting into cases. From 57ecb8112977d843bcee0ff0bb3e9f717fdbed2a Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 15 Jun 2026 12:55:30 +0200 Subject: [PATCH 118/181] Test iinduction for pure hypotheses and spatial hypotheses --- Iris/Iris/Tests/Tactics.lean | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 6d02db781..ae9b42129 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2806,7 +2806,7 @@ def Tree.mirror : Tree α → Tree α | .node l x r => .node (.mirror r) x (.mirror l) /-- - Tests `iinduction` with `Tree` and `Tree.mirror`. + Tests `iinduction` with a pure hypothesis that involves `Tree.mirror`. -/ example [BI PROP] {α} {t : Tree α} : ⊢@{PROP} ⌜.mirror (.mirror t) = t⌝ := by @@ -2818,6 +2818,25 @@ example [BI PROP] {α} {t : Tree α} : · iexact ihl · iexact ihr +/-- An inductively defined predicate on `Tree` -/ +def Tree.pred [BI PROP] (P : α → PROP) : Tree α → PROP + | .leaf => emp + | .node l x r => iprop(Tree.pred P l ∗ (P x ∗ Tree.pred P r)) + +/-- + Tests `iinduction` with spatial hypotheses that involve `Tree.mirror` and `Tree.pred`. +-/ +example [BI PROP] {α} {t : Tree α} {P : α → PROP} : + Tree.pred P t -∗ Tree.pred P (.mirror t) := by + iintro H + iinduction t with simp [Tree.mirror, Tree.pred] + | node l x r ihl ihr => + icases H with ⟨Hl, Hx, Hr⟩ + iframe + isplitl [Hr] + · iapply ihr $$ Hr + · iapply ihl $$ Hl + /-- Tests `iinduction` with simple induction on natural numbers. Tries `iframe` to solve induction subgoals before splitting into cases. From 9ba70988f3f64d892ab776c2240404a0834d316e Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 15 Jun 2026 17:14:50 +0200 Subject: [PATCH 119/181] irevert bug fix --- Iris/Iris/ProofMode/Tactics/Revert.lean | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Iris/Iris/ProofMode/Tactics/Revert.lean b/Iris/Iris/ProofMode/Tactics/Revert.lean index 8027b4ba2..b88ffc329 100644 --- a/Iris/Iris/ProofMode/Tactics/Revert.lean +++ b/Iris/Iris/ProofMode/Tactics/Revert.lean @@ -88,7 +88,7 @@ private def RevertState.revertLeanHyp let ldecl ← st.hyps.checkRemovableFVar "irevert" f none st.reverted.contains let v : Level ← Meta.getLevel ldecl.type have α : Q(Sort v) := ldecl.type - if ← Meta.isProp α then + if (← Meta.isProp α) && !st.goal.containsFVar f then have φ : Q(Prop) := α st.revertLeanPropHyp f φ else From 341e82723e8c222a9a1fba4afe14c9d2011e42f9 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 15 Jun 2026 17:26:01 +0200 Subject: [PATCH 120/181] Fix missing Iris hypotheses in "Try this" suggestion --- Iris/Iris/ProofMode/Tactics/Induction.lean | 26 ++++++++++--------- Iris/Iris/Tests/Tactics.lean | 29 +++++++++++++--------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 3c52593d0..1881b9645 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -177,22 +177,11 @@ private def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} let explicitPureFVars := explicitTargets.filterMap (match ·.kind with | .pure f => some f | _ => none) - -- Check Iris hypothesis that depend on hypotheses in the `generalizing` syntax - let missingIris : List (Name × FVarId) := - hyps.intuitionisticIVarIds.filterMap fun ivar => - if explicitIrisIVarIds.contains ivar then none - else match hyps.getDecl? ivar with - | some ⟨name, _, _, ty⟩ => - match explicitPureFVars.find? (ty.containsFVar ·) with - | some x => some (name, x) - | none => none - | none => none - -- Pairs of `FVarId` values `(f1, f2)` indicating `f1` depends on `f2`. let mut missingPure : List (FVarId × FVarId) := [] -- Check forward dependency of pure hypotheses in the `generalizing` syntax - for fvar in (explicitPureFVars ++ (missingIris.map (·.snd))).eraseDups do + for fvar in explicitPureFVars.eraseDups do let fwdDeps ← collectForwardDeps #[mkFVar fvar] false for dep in fwdDeps do let depId := dep.fvarId! @@ -202,6 +191,19 @@ private def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} if !missingPure.any (·.fst == depId) then missingPure := missingPure ++ [(depId, fvar)] + let allPureFVars := explicitPureFVars ++ missingPure.map (·.fst) + + -- Check Iris hypothesis that depend on hypotheses in the `generalizing` syntax + let missingIris : List (Name × FVarId) := + hyps.intuitionisticIVarIds.filterMap fun ivar => + if explicitIrisIVarIds.contains ivar then none + else match hyps.getDecl? ivar with + | some ⟨name, _, _, ty⟩ => + match allPureFVars.find? (ty.containsFVar ·) with + | some x => some (name, x) + | none => none + | none => none + -- Throw an error if there exists some pure/Lean hypotheses that should also be generalised if !missingPure.isEmpty || !missingIris.isEmpty then let leanLines ← missingPure.mapM fun (depId, srcId) => do diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index ae9b42129..1b8d92a5d 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2975,29 +2975,34 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : /- Tests `iinduction` on `n` generalising `m`, where: - - *regular hypotheses* `h : T m` and `U : (T m) → Prop` depend on `m`; - - *regular hypothesis* `h2 : U h` depends on `h`, which indirectly depends on `m`; and - - *Iris hypotheses* `□HQ : Q m` and `□HR : R m` depends on `m`. + - *regular hypotheses* `h : T m` and `U1 : (T m) → Prop` depend on `m`; + - *regular hypotheses* `h2 : U1 h1` and `U2 : (U1 h1) → PROP` depends on `h1`, + which in turn depends on `m`; + - *Iris hypotheses* `□HQ : Q m` and `□HR : R m` depends on `m`; + - *Iris hypothesis* `□HU2 : U2 h2` depends on `h2` and `U2`, which depends + depend on `h1`, which in turn depends on `m`. This requires manual resolution. -/ /-- info: Try this: - [apply] iinduction n generalizing %m %h %U %h2 HQ HR with - | zero => itrivial + [apply] iinduction n generalizing %m %h1 %U1 %h2 %U2 HQ HR HU2 with + | zero | succ n IH => itrivial --- error: iinduction: The following hypotheses depend on variables in the `generalizing` clause but are not themselves included: -• Lean hypothesis `h` depends on `m` -• Lean hypothesis `U` depends on `m` +• Lean hypothesis `h1` depends on `m` +• Lean hypothesis `U1` depends on `m` • Lean hypothesis `h2` depends on `m` +• Lean hypothesis `U2` depends on `m` • Iris hypothesis in the intuitionstic context `HQ` depends on `m` -• Iris hypothesis in the intuitionstic context `HR` depends on `m` -/ +• Iris hypothesis in the intuitionstic context `HR` depends on `m` +• Iris hypothesis in the intuitionstic context `HU2` depends on `h2` -/ #guard_msgs in example [BI PROP] {P : PROP} {m n : Nat} {Q R S : Nat → PROP} {T : Nat → Prop} - {h : T m} {U : (T m) → Prop} {h2 : U h} : - ⊢ P -∗ □ Q m -∗ □ R m -∗ □ S n -∗ ⌜n + 0 = n⌝ := by - iintro HP #HQ #HR #HS + {h1 : T m} {U1 : (T m) → Prop} {h2 : U1 h1} {U2 : (U1 h1) → PROP} : + ⊢ P -∗ □ Q m -∗ □ R m -∗ □ S n -∗ □ U2 h2 -∗ ⌜n + 0 = n⌝ := by + iintro HP #HQ #HR #HS #HU2 iinduction n generalizing %m with - | zero => itrivial + | zero | succ n IH => itrivial end iinduction From dcd1206fcc1b567c4dc2323375960e81d704640c Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 15 Jun 2026 17:35:59 +0200 Subject: [PATCH 121/181] Ensure the "Try this" suggestion handles order of hypotheses in `generalizing` clause according to dependency --- Iris/Iris/ProofMode/Tactics/Induction.lean | 23 ++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 1881b9645..6cb27485f 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -214,21 +214,28 @@ private def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} let srcDecl ← srcId.getDecl return s!"• Iris hypothesis in the intuitionstic context `{name}` depends on `{srcDecl.userName}`" + -- Sort the pure Lean hypothesis according to the dependency + let allPureFVarsSorted := (← getLCtx).sortFVarsByContextOrder allPureFVars.eraseDups.toArray + let sortedPurePats : Array (TSyntax `selPat) ← allPureFVarsSorted.mapM fun fvarId => do + let decl ← fvarId.getDecl + let id := mkIdent (.mkSimple decl.userName.toString) + `(selPat| %$id:ident) + -- Build `selPat` syntax nodes for each missing item let newIrisPats : Array (TSyntax `selPat) ← - missingIris.toArray.mapM fun (name, _) => - `(selPat| $(mkIdent name):ident) - let newPurePats : Array (TSyntax `selPat) ← - missingPure.toArray.mapM fun (depId, _) => do - let depDecl ← depId.getDecl - let id := mkIdent (Name.mkSimple depDecl.userName.toString) - `(selPat| %$id:ident) + missingIris.toArray.mapM fun (name, _) => `(selPat| $(mkIdent name):ident) -- Find the old tactic syntax and generate the new one with missing hypotheses added let oldTactic ← getRef let `(tactic| iinduction $x $[using $r]? generalizing $genSelPats* $[$alts]?) := oldTactic | throwError "iinduction: invalid syntax" - let extendedPats : TSyntaxArray `selPat := genSelPats.append <| newPurePats ++ newIrisPats + + let existingIrisPats := genSelPats.filter fun p => + match p.raw with + | `(selPat| %$_:ident) => false + | _ => true + + let extendedPats : TSyntaxArray `selPat := sortedPurePats ++ existingIrisPats ++ newIrisPats let newTactic ← `(tactic| iinduction $x $[using $r]? generalizing $extendedPats* $[$alts]?) -- Suggestion the fixed tactic From 38090e0dc36570af9fd8f0c74cec78133d40f68d Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 15 Jun 2026 17:42:54 +0200 Subject: [PATCH 122/181] Typo fix --- Iris/Iris/Tests/Tactics.lean | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 1b8d92a5d..d79315a90 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2975,7 +2975,7 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : /- Tests `iinduction` on `n` generalising `m`, where: - - *regular hypotheses* `h : T m` and `U1 : (T m) → Prop` depend on `m`; + - *regular hypotheses* `h1 : T m` and `U1 : (T m) → Prop` depend on `m`; - *regular hypotheses* `h2 : U1 h1` and `U2 : (U1 h1) → PROP` depends on `h1`, which in turn depends on `m`; - *Iris hypotheses* `□HQ : Q m` and `□HR : R m` depends on `m`; From ae2dc76762a42f704581f6c10fe545e1d62d0342 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 15 Jun 2026 18:27:16 +0200 Subject: [PATCH 123/181] Implement "Try this" suggestion for iloeb and irevert --- Iris/Iris/ProofMode/Tactics/Induction.lean | 123 ++++---------------- Iris/Iris/ProofMode/Tactics/Loeb.lean | 54 ++++++--- Iris/Iris/ProofMode/Tactics/Revert.lean | 126 ++++++++++++++++++++- Iris/Iris/Tests/Tactics.lean | 29 ++++- 4 files changed, 209 insertions(+), 123 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 6cb27485f..1ebfd37c1 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -11,6 +11,7 @@ public meta import Iris.ProofMode.Tactics.Cases public meta import Iris.ProofMode.Patterns.CasesPattern public meta import Iris.ProofMode.ClassesMake public meta import Iris.ProofMode.Tactics.RevertIntro +public meta import Iris.ProofMode.Tactics.Revert public meta import Lean.Meta.Tactic.TryThis namespace Iris.ProofMode @@ -85,6 +86,22 @@ private def parseInductionAlts (altsSyntax : TSyntax `Lean.Parser.Tactic.inducti return ⟨tac, parsedAlts, none, altsSyntax⟩ | _ => throwErrorAt altsSyntax "iinduction: invalid syntax" +/-- + A helper function for `checkDependentHyps` to generate the fixed tactic. +-/ +private def checkDependentHypsMkSuggestion + (purePats : Array (TSyntax `selPat)) (newIrisPats : Array (TSyntax `selPat)) : + ProofModeM (TSyntax `tactic) := do + let oldTactic ← getRef + let `(tactic| iinduction $x $[using $r]? generalizing $genSelPats* $[$alts]?) := oldTactic + | throwError "iinduction: invalid syntax" + let existingIrisPats := genSelPats.filter fun p => + match p.raw with + | `(selPat| %$_:ident) => false + | _ => true + let extendedPats : TSyntaxArray `selPat := purePats ++ existingIrisPats ++ newIrisPats + `(tactic| iinduction $x $[using $r]? generalizing $extendedPats* $[$alts]?) + /-- This theorem is used for updating the proof in `InductionState` as `addIHs` iterates through the list of induction hypotheses and introduces them from @@ -143,109 +160,6 @@ private def iHypsToGeneralize {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} (ivars ++ hyps.spatialIVarIds).map ({ kind := .ipm ·, explicit := false }) -/-- - When the tactic is `iinduction e generalizing z₁ ... zₙ` applied, - the variables `z₁ ... zₙ`, which can be regular Lean variables or Iris - hypotheses, are reverted from the context. - - The function `iHypsToGeneralize` is used for finding the Iris hypotheses - that references the induction target. - - However, it is possible that `x` is amongst the Lean variables explcitly - reverted by the user using the `generalizing` syntax while there exists - another Lean variable `y` such that `y` depends on `x`. In this case, this - function suggests that the user includes `%y` in the `generalizing` syntax. - - It is also possible that `x` is amongst the Lean variables being generalised - such that there exists an Iris hypothesis `HP` in the intuitionistic context - where: - 1. `P` does not depend on the induction target and thus not reverted automatically, - 2. `P` depends on `x` in the `generalizing` syntax, and - 3. `P` itself is not included in the `generalizing` syntax. - In this case, this function suggests the user to include `HP` in the - `generalizing` syntax. - - Note that Iris hypotheses in the spatial context are always reverted, so there - is no need for further checks by this function. --/ -private def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} - (hyps : Hyps bi e) (explicitTargets : List SelTarget) : - ProofModeM Unit := do - - let explicitIrisIVarIds := explicitTargets.filterMap - (match ·.kind with | .ipm ivar => some ivar | _ => none) - let explicitPureFVars := explicitTargets.filterMap - (match ·.kind with | .pure f => some f | _ => none) - - -- Pairs of `FVarId` values `(f1, f2)` indicating `f1` depends on `f2`. - let mut missingPure : List (FVarId × FVarId) := [] - - -- Check forward dependency of pure hypotheses in the `generalizing` syntax - for fvar in explicitPureFVars.eraseDups do - let fwdDeps ← collectForwardDeps #[mkFVar fvar] false - for dep in fwdDeps do - let depId := dep.fvarId! - -- Skip `x` itself and variables already included in `explicitPureFVars` - if depId != fvar && !explicitPureFVars.contains depId then - -- Record the missing hypothesis to be generalised, if not already included - if !missingPure.any (·.fst == depId) then - missingPure := missingPure ++ [(depId, fvar)] - - let allPureFVars := explicitPureFVars ++ missingPure.map (·.fst) - - -- Check Iris hypothesis that depend on hypotheses in the `generalizing` syntax - let missingIris : List (Name × FVarId) := - hyps.intuitionisticIVarIds.filterMap fun ivar => - if explicitIrisIVarIds.contains ivar then none - else match hyps.getDecl? ivar with - | some ⟨name, _, _, ty⟩ => - match allPureFVars.find? (ty.containsFVar ·) with - | some x => some (name, x) - | none => none - | none => none - - -- Throw an error if there exists some pure/Lean hypotheses that should also be generalised - if !missingPure.isEmpty || !missingIris.isEmpty then - let leanLines ← missingPure.mapM fun (depId, srcId) => do - let depDecl ← depId.getDecl - let srcDecl ← srcId.getDecl - return s!"• Lean hypothesis `{depDecl.userName}` depends on `{srcDecl.userName}`" - let irisLines ← missingIris.mapM fun (name, srcId) => do - let srcDecl ← srcId.getDecl - return s!"• Iris hypothesis in the intuitionstic context `{name}` depends on `{srcDecl.userName}`" - - -- Sort the pure Lean hypothesis according to the dependency - let allPureFVarsSorted := (← getLCtx).sortFVarsByContextOrder allPureFVars.eraseDups.toArray - let sortedPurePats : Array (TSyntax `selPat) ← allPureFVarsSorted.mapM fun fvarId => do - let decl ← fvarId.getDecl - let id := mkIdent (.mkSimple decl.userName.toString) - `(selPat| %$id:ident) - - -- Build `selPat` syntax nodes for each missing item - let newIrisPats : Array (TSyntax `selPat) ← - missingIris.toArray.mapM fun (name, _) => `(selPat| $(mkIdent name):ident) - - -- Find the old tactic syntax and generate the new one with missing hypotheses added - let oldTactic ← getRef - let `(tactic| iinduction $x $[using $r]? generalizing $genSelPats* $[$alts]?) := oldTactic - | throwError "iinduction: invalid syntax" - - let existingIrisPats := genSelPats.filter fun p => - match p.raw with - | `(selPat| %$_:ident) => false - | _ => true - - let extendedPats : TSyntaxArray `selPat := sortedPurePats ++ existingIrisPats ++ newIrisPats - let newTactic ← `(tactic| iinduction $x $[using $r]? generalizing $extendedPats* $[$alts]?) - - -- Suggestion the fixed tactic - Lean.Meta.Tactic.TryThis.addSuggestion oldTactic newTactic - - -- Log the error and attach the clickable suggestion - throwError m!"iinduction: The following hypotheses depend on variables in \ - the `generalizing` clause but are not themselves included:\ - \n{"\n".intercalate (leanLines ++ irisLines)}" - /-- Search for hypotheses in the regular Lean context that are the in the form of Iris goals. These must be induction hypotheses generated by Lean's built-in @@ -357,7 +271,8 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} | none => pure <| iHypsToGeneralize hyps fvar | some genSelPats => let genSelTargets ← SelPat.resolve hyps genSelPats - checkDependentHyps hyps genSelTargets + checkDependentHyps "iinduction" hyps genSelTargets checkDependentHypsMkSuggestion + let explicitIrisTargets := genSelTargets.filter (match ·.kind with | .ipm _ => true | _ => false) let explicitPureTargets := genSelTargets.filter (match ·.kind with | .pure _ => true | _ => false) let implicitIrisTargets := (iHypsToGeneralize hyps fvar).filter (not <| (explicitIrisTargets.map (·.kind)).contains ·.kind) diff --git a/Iris/Iris/ProofMode/Tactics/Loeb.lean b/Iris/Iris/ProofMode/Tactics/Loeb.lean index a122dc5ce..3a11b1b7f 100644 --- a/Iris/Iris/ProofMode/Tactics/Loeb.lean +++ b/Iris/Iris/ProofMode/Tactics/Loeb.lean @@ -4,6 +4,8 @@ Released under Apache 2.0 license as described in the file LICENSE. Authors: Fernando Leal -/ module + +public import Iris.ProofMode.Tactics.Revert public import Iris.ProofMode.Tactics.RevertIntro namespace Iris.ProofMode @@ -12,6 +14,24 @@ open Lean Meta Elab.Tactic Qq public meta section +syntax (name := iloeb) "iloeb" "as" binderIdent "generalizing" (colGt ppSpace selPat)* : tactic + +/-- + A helper function for `checkDependentHyps` to generate the fixed tactic. +-/ +private def checkDependentHypsMkSuggestion + (purePats : Array (TSyntax `selPat)) (newIrisPats : Array (TSyntax `selPat)) : + ProofModeM (TSyntax `tactic) := do + let oldTactic ← getRef + let `(tactic| iloeb as $bi:binderIdent generalizing $pats:selPat*) := oldTactic + | throwError "iloeb: invalid syntax" + let existingIrisPats := pats.filter fun p => + match p.raw with + | `(selPat| %$_:ident) => false + | _ => true + let extendedPats : TSyntaxArray `selPat := purePats ++ existingIrisPats ++ newIrisPats + `(tactic| iloeb as $bi generalizing $extendedPats*) + /-- Apply Löb induction in the current goal. @@ -21,21 +41,23 @@ public meta section Optionally, other variables can be generalized over through the `generalizing selPat*` syntax. -/ -elab "iloeb" "as" IH:binderIdent "generalizing" hs:(colGt selPat)* : tactic => do - let pats ← Elab.liftMacroM <| SelPat.parse hs - ProofModeM.runTactic fun mvid {hyps, goal, ..} => do - let targets : List SelTarget ← SelPat.resolve hyps (pats ++ [.spatial]) - let expr ← iRevertIntro hyps goal targets fun {prop _ _} hyps goal k => do - let some _ ← ProofModeM.trySynthInstanceQ q(BI.BILoeb $prop) - | throwError m!"iloeb: no `{←ppExpr q(BI.BILoeb $prop)}` instance found" - let pf := q(BI.loeb_wand_intuitionistically (P := $goal)) - let pf' ← do - -- We have applied BI.loeb_wand_intuitionistically - let goal := q(iprop(□ (□ ▷ $goal -∗ $goal))) - iModIntroCore hyps goal (← `(_)) fun hyps goal => do - iIntroCore hyps goal [(IH, .intro <| .intuitionistic <| .one IH)] (k · · addBIGoal) - return q($(pf').trans $pf) - - mvid.assign expr +elab_rules : tactic + | `(tactic| iloeb as $IH:binderIdent generalizing $hs:selPat*) => do + let pats ← Elab.liftMacroM <| SelPat.parse hs + ProofModeM.runTactic fun mvid {hyps, goal, ..} => do + let targets : List SelTarget ← SelPat.resolve hyps (pats ++ [.spatial]) + checkDependentHyps "iloeb" hyps targets checkDependentHypsMkSuggestion + let expr ← iRevertIntro hyps goal targets fun {prop _ _} hyps goal k => do + let some _ ← ProofModeM.trySynthInstanceQ q(BI.BILoeb $prop) + | throwError m!"iloeb: no `{←ppExpr q(BI.BILoeb $prop)}` instance found" + let pf := q(BI.loeb_wand_intuitionistically (P := $goal)) + let pf' ← do + -- We have applied BI.loeb_wand_intuitionistically + let goal := q(iprop(□ (□ ▷ $goal -∗ $goal))) + iModIntroCore hyps goal (← `(_)) fun hyps goal => do + iIntroCore hyps goal [(IH, .intro <| .intuitionistic <| .one IH)] (k · · addBIGoal) + return q($(pf').trans $pf) + + mvid.assign expr macro "iloeb" "as" IH:binderIdent : tactic => `(tactic | iloeb as $IH generalizing) diff --git a/Iris/Iris/ProofMode/Tactics/Revert.lean b/Iris/Iris/ProofMode/Tactics/Revert.lean index b88ffc329..65813832b 100644 --- a/Iris/Iris/ProofMode/Tactics/Revert.lean +++ b/Iris/Iris/ProofMode/Tactics/Revert.lean @@ -9,6 +9,11 @@ public import Iris.ProofMode.ClassesMake public meta import Iris.ProofMode.Patterns.SelPattern public meta import Iris.ProofMode.Tactics.Basic +public meta import Iris.ProofMode.Tactics.Assumption +public meta import Iris.ProofMode.Tactics.Cases +public meta import Iris.ProofMode.Patterns.CasesPattern +public meta import Lean.Meta.Tactic.TryThis + namespace Iris.ProofMode public section @@ -94,9 +99,127 @@ private def RevertState.revertLeanHyp else st.revertLeanForallHyp f α +/-- + When the tactic is `iinduction e generalizing z₁ ... zₙ` applied, + the variables `z₁ ... zₙ`, which can be regular Lean variables or Iris + hypotheses, are reverted from the context. + + The function `iHypsToGeneralize` is used for finding the Iris hypotheses + that references the induction target. + + However, it is possible that `x` is amongst the Lean variables explcitly + reverted by the user using the `generalizing` syntax while there exists + another Lean variable `y` such that `y` depends on `x`. In this case, this + function suggests that the user includes `%y` in the `generalizing` syntax. + + It is also possible that `x` is amongst the Lean variables being generalised + such that there exists an Iris hypothesis `HP` in the intuitionistic context + where: + 1. `P` does not depend on the induction target and thus not reverted automatically, + 2. `P` depends on `x` in the `generalizing` syntax, and + 3. `P` itself is not included in the `generalizing` syntax. + In this case, this function suggests the user to include `HP` in the + `generalizing` syntax. + + Note that Iris hypotheses in the spatial context are always reverted, so there + is no need for further checks by this function. +-/ +def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} + (tacticName : String) (hyps : Hyps bi e) (explicitTargets : List SelTarget) + (mkSuggestion : + Array (TSyntax `selPat) → + Array (TSyntax `selPat) → + ProofModeM (TSyntax `tactic)): + ProofModeM Unit := do + + let explicitIrisIVarIds := explicitTargets.filterMap + (match ·.kind with | .ipm ivar => some ivar | _ => none) + let explicitPureFVars := explicitTargets.filterMap + (match ·.kind with | .pure f => some f | _ => none) + + -- Pairs of `FVarId` values `(f1, f2)` indicating `f1` depends on `f2`. + let mut missingPure : List (FVarId × FVarId) := [] + + -- Check forward dependency of pure hypotheses in the `generalizing` syntax + for fvar in explicitPureFVars.eraseDups do + let fwdDeps ← collectForwardDeps #[mkFVar fvar] false + for dep in fwdDeps do + let depId := dep.fvarId! + -- Skip `x` itself and variables already included in `explicitPureFVars` + if depId != fvar && !explicitPureFVars.contains depId then + -- Record the missing hypothesis to be generalised, if not already included + if !missingPure.any (·.fst == depId) then + missingPure := missingPure ++ [(depId, fvar)] + + let allPureFVars := explicitPureFVars ++ missingPure.map (·.fst) + + -- Check Iris hypothesis that depend on hypotheses in the `generalizing` syntax + let missingIris : List (Name × FVarId) := + hyps.intuitionisticIVarIds.filterMap fun ivar => + if explicitIrisIVarIds.contains ivar then none + else match hyps.getDecl? ivar with + | some ⟨name, _, _, ty⟩ => + match allPureFVars.find? (ty.containsFVar ·) with + | some x => some (name, x) + | none => none + | none => none + + -- Throw an error if there exists some pure/Lean hypotheses that should also be generalised + if !missingPure.isEmpty || !missingIris.isEmpty then + let leanLines ← missingPure.mapM fun (depId, srcId) => do + let depDecl ← depId.getDecl + let srcDecl ← srcId.getDecl + return s!"• Lean hypothesis `{depDecl.userName}` depends on `{srcDecl.userName}`" + let irisLines ← missingIris.mapM fun (name, srcId) => do + let srcDecl ← srcId.getDecl + return s!"• Iris hypothesis in the intuitionstic context `{name}` depends on `{srcDecl.userName}`" + + -- Sort the pure Lean hypothesis according to the dependency + let allPureFVarsSorted := (← getLCtx).sortFVarsByContextOrder allPureFVars.eraseDups.toArray + let sortedPurePats : Array (TSyntax `selPat) ← allPureFVarsSorted.mapM fun fvarId => do + let decl ← fvarId.getDecl + let id := mkIdent (.mkSimple decl.userName.toString) + `(selPat| %$id:ident) + + -- Build `selPat` syntax nodes for each missing item + let newIrisPats : Array (TSyntax `selPat) ← + missingIris.toArray.mapM fun (name, _) => `(selPat| $(mkIdent name):ident) + + -- Find the old tactic syntax and generate the new one with missing hypotheses added + let oldTactic ← getRef + let newTactic ← mkSuggestion sortedPurePats newIrisPats + + -- Suggestion the fixed tactic + Lean.Meta.Tactic.TryThis.addSuggestion oldTactic newTactic + + -- Log the error and attach the clickable suggestion + throwError m!"{tacticName}: The following hypotheses depend on variables in \ + the `generalizing` clause but are not themselves included:\ + \n{"\n".intercalate (leanLines ++ irisLines)}" + +syntax (name := irevert) "irevert" (colGt ppSpace selPat)+ : tactic + +/-- + A helper function for `checkDependentHyps` to generate the fixed tactic. +-/ +private def checkDependentHypsMkSuggestion + (purePats : Array (TSyntax `selPat)) (newIrisPats : Array (TSyntax `selPat)) : + ProofModeM (TSyntax `tactic) := do + let oldTactic ← getRef + let `(tactic| irevert $pats:selPat*) := oldTactic + | throwError "irevert: invalid syntax" + let existingIrisPats := pats.filter fun p => + match p.raw with + | `(selPat| %$_:ident) => false + | _ => true + let extendedPats : TSyntaxArray `selPat := purePats ++ existingIrisPats ++ newIrisPats + `(tactic| irevert $extendedPats*) + def iRevertCore (targets : List SelTarget) {u : Level}{prop: Q(Type $u)}{bi : Q(BI $prop)}{e : Q($prop)}(hyps : Hyps bi e)(goal: Q($prop)) (k : ∀ {e : Q($prop)}, Hyps bi e → (goal: Q($prop)) → ProofModeM Q($e ⊢ $goal) := addBIGoal) : ProofModeM Q($e ⊢ $goal) := do + checkDependentHyps "irevert" hyps targets checkDependentHypsMkSuggestion + let init : RevertState e goal := { e, hyps, goal, pf := q(id) } let st ← targets.reverse.foldlM (init := init) fun st target => do match target.kind with @@ -106,7 +229,8 @@ def iRevertCore (targets : List SelTarget) {u : Level}{prop: Q(Type $u)}{bi : Q( let pf' : Q($(st.e) ⊢ $(st.goal)) ← withoutFVars (u:=0) st.reverted (k st.hyps st.goal) return q($(st.pf) $pf') -elab "irevert" pats:(colGt selPat)+ : tactic => do +elab_rules : tactic + | `(tactic| irevert $pats:selPat*) => do let pats ← liftMacroM <| SelPat.parse pats ProofModeM.runTactic fun mvar {hyps, goal, ..} => do diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index d79315a90..1c9912724 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -377,14 +377,22 @@ example [BI PROP] (P : PROP) {x : Nat} : ⊢ P := by irevert %x /- Tests `irevert` failing with dependency -/ -/-- error: irevert: proofmode hypothesis H depends on x -/ +/-- info: Try this: + [apply] irevert %x %hp +--- +error: irevert: The following hypotheses depend on variables in the `generalizing` clause but are not themselves included: +• Lean hypothesis `hp` depends on `x` -/ #guard_msgs in example [BI PROP] (Φ : Bool → PROP) : ⊢ ∀ x, ⌜x = true⌝ -∗ Φ x -∗ Φ x := by iintro %x %hp H irevert %x /- Tests `irevert` failing with dependency -/ -/-- error: irevert: Lean hypothesis hp depends on x -/ +/-- info: Try this: + [apply] irevert %x %hp H +--- +error: irevert: The following hypotheses depend on variables in the `generalizing` clause but are not themselves included: +• Lean hypothesis `hp` depends on `x` -/ #guard_msgs in example [BI PROP] (Φ : Bool → PROP) : ⊢ ∀ x, ⌜x = true⌝ -∗ Φ x -∗ Φ x := by iintro %x %hp H @@ -2775,6 +2783,23 @@ example (P Q : PROP) : ⊢ P -∗ Q := by iloeb as IH +-- Tests `iloeb` where the `generalizing` clause has dependency +/-- +info: Try this: + [apply] iloeb as IH generalizing %n %h1 %U HT +--- +error: iloeb: The following hypotheses depend on variables in the `generalizing` clause but are not themselves included: +• Lean hypothesis `h1` depends on `n` +• Lean hypothesis `U` depends on `n` +• Iris hypothesis in the intuitionstic context `HT` depends on `n` +-/ +#guard_msgs in +example [BI PROP] [BILoeb PROP] {n : Nat} {P T : Nat → PROP} {Q : Nat → Prop} + {h1 : Q n} {U : (Q n) → Prop} : + ⊢ □ T n -∗ □ P n := by + iintro #HT + iloeb as IH generalizing %n + end iloeb section iinduction From ab12f977c141fd5987a65a2bd12a3505e6742fe9 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 15 Jun 2026 22:31:23 +0200 Subject: [PATCH 124/181] Adjust the priority of IntoIH type class instances --- Iris/Iris/ProofMode/Instances.lean | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Iris/Iris/ProofMode/Instances.lean b/Iris/Iris/ProofMode/Instances.lean index f16d65c5c..0663801c5 100644 --- a/Iris/Iris/ProofMode/Instances.lean +++ b/Iris/Iris/ProofMode/Instances.lean @@ -950,7 +950,7 @@ instance (priority := default - 1) intoIH_imp [BI PROP] (φ ψ : Prop) (Δ P Q : `∀ l, Forall P l → P (Tree l)` arising from nested inductive types like `inductive ntree := Tree : List ntree → ntree`. -/ @[rocq_alias into_ih_Forall] -instance intoIH_listForall [BI PROP] (φ : α → Bool) (l : List α) (P : PROP) (Φ : α → PROP) +instance (priority := default - 2) intoIH_listForall [BI PROP] (φ : α → Bool) (l : List α) (P : PROP) (Φ : α → PROP) [h : ∀ x, IntoIH (φ x) P (Φ x)] : IntoIH (l.all φ) P (bigSepL (fun _ a => iprop(□ Φ a)) l) where into_ih := by @@ -971,7 +971,7 @@ instance intoIH_listForall [BI PROP] (φ : α → Bool) (l : List α) (P : PROP) /-- Support for induction principles whose IH is guarded by `List.Forall₂`, e.g. arising from mutual inductive types relating two lists element-wise. -/ @[rocq_alias into_ih_Forall2] -instance intoIH_listForall₂ [BI PROP] (φ : α → β → Prop) (l1 : List α) (l2 : List β) +instance (priority := default - 2) intoIH_listForall₂ [BI PROP] (φ : α → β → Prop) (l1 : List α) (l2 : List β) (P : PROP) (Φ : α → β → PROP) [h : ∀ x1 x2, IntoIH (φ x1 x2) P (Φ x1 x2)] : IntoIH (List.Forall₂ φ l1 l2) P (bigSepL2 (fun _ x1 x2 => iprop(□ Φ x1 x2)) l1 l2) where From 78c7e28b528947f5b5afa4223c46126c98b67f14 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 15 Jun 2026 22:36:22 +0200 Subject: [PATCH 125/181] =?UTF-8?q?Tests=20with=20NTree=20for=20`List.Fora?= =?UTF-8?q?ll=E2=82=82`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Iris/Iris/Tests/Tactics.lean | 74 +++++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 1c9912724..76de24bf5 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2862,6 +2862,78 @@ example [BI PROP] {α} {t : Tree α} {P : α → PROP} : · iapply ihr $$ Hr · iapply ihl $$ Hl +/-- + Definition of n-tree and its induction principle from: + https://leanprover.zulipchat.com/#narrow/channel/113489-new-members/topic/.E2.9C.94.20Induction.20principle.20for.20nested.20inductive.20types/near/437905021 +-/ +inductive NTree (α : Type) +| leaf +| node : α → List (NTree α) → NTree α + +@[induction_eliminator] +def NTree.induction_principle {α} (p : NTree α → Prop) (h_leaf : p leaf) + (h_node : (x : α) → (ts : List (NTree α)) → (ih : ∀ t ∈ ts, p t) → p (node x ts)) : + ∀ t : NTree α, p t := + @NTree.rec α p (λ ts => ∀ t ∈ ts, p t) h_leaf h_node (List.forall_mem_nil p) + (λ _ _ h_head h_tail => List.forall_mem_cons.mpr (And.intro h_head h_tail)) + +/-- A recursive "mirror" on `NTree` that mirrors every child subtree. -/ +def NTree.mirror : NTree α → NTree α + | .leaf => .leaf + | .node x ts => .node x (ts.map NTree.mirror) + +/-- + Direct analogue of the first `Tree` example. + The node case uses the manual induction principle for `NTree`, + which gives an induction hypothesis of the form `∀ t ∈ ts, P t`. +-/ +example [BI PROP] {α} {t : NTree α} : + ⊢@{PROP} ⌜.mirror (.mirror t) = t⌝ := by + iinduction t with simp [NTree.mirror] + | h_leaf => + itrivial + | h_node x ts IH => + sorry + +def NTree.childCount : NTree α → Nat + | .leaf => 0 + | .node _ ts => ts.length + +inductive NTree.Rel {α β} (R : α → β → Prop) : + NTree α → NTree β → Prop + | leaf : Rel R .leaf .leaf + | node : ∀ a b ts₁ ts₂, + R a b → + List.Forall₂ (Rel R) ts₁ ts₂ → + Rel R (.node a ts₁) (.node b ts₂) + +@[induction_eliminator] +def NTree.Rel.induction_principle {α β} {R : α → β → Prop} + (p : ∀ {t1 : NTree α} {t2 : NTree β}, NTree.Rel R t1 t2 → Prop) + (h_leaf : p .leaf) + (h_node : ∀ (a : α) (b : β) (ts1 : List (NTree α)) (ts2 : List (NTree β)) + (ra : R a b) + (f2 : List.Forall₂ (NTree.Rel R) ts1 ts2), + List.Forall₂ (fun t1 t2 => ∀ h : NTree.Rel R t1 t2, p h) ts1 ts2 → + p (.node a b ts1 ts2 ra f2)) : + ∀ {t1 : NTree α} {t2 : NTree β} (h : NTree.Rel R t1 t2), p h := + @NTree.Rel.rec α β R + (fun _ _ h => p h) + (fun a b _ => List.Forall₂ (fun t1 t2 => ∀ h : NTree.Rel R t1 t2, p h) a b) + h_leaf + (fun a b ts1 ts2 ra f2 ih_ts => h_node a b ts1 ts2 ra f2 ih_ts) + .nil + (fun _ _ ih_h ih_hs => .cons (fun _ => ih_h) ih_hs) + +example [BI PROP] {α β} {R : α → β → Prop} + {t₁ : NTree α} {t₂ : NTree β} (H : NTree.Rel R t₁ t₂) : + ⊢@{PROP} ⌜ NTree.childCount t₁ = NTree.childCount t₂ ⌝ := by + iinduction H with + | h_leaf => + ipureintro + apply rfl + | h_node x1 ts1 x2 ts2 r IH1 IH2 => sorry + /-- Tests `iinduction` with simple induction on natural numbers. Tries `iframe` to solve induction subgoals before splitting into cases. @@ -2940,7 +3012,7 @@ example [BI PROP] {n : Nat} : The proposition `Q m` is reverted manually using the `generalizing` clause. On the contrary, `R` is not reverted. -/ -example [BI PROP] {P R S : PROP} {Q T : Nat → PROP} {n : Nat} : +example [BI PROP] {P R S : PROP} {Q T : Nat → PROP} {m n : Nat} : ⊢ P -∗ □ Q m -∗ □ R -∗ S -∗ □ T (n + m) -∗ ⌜n + m + 0 = n + m⌝ := by iintro HP #HQ #HR HS #HT iinduction n + m using Nat.caseStrongRecOn generalizing %m HQ with From 1386255028e59b5ce66c72f013009d68cc586066 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 15 Jun 2026 22:38:25 +0200 Subject: [PATCH 126/181] Define containsIrisGoal to recursive traverse an expression to see if it contains an Iris entailment, and if so, it is an induction hypothesis --- Iris/Iris/ProofMode/Expr.lean | 11 +++++++++++ Iris/Iris/ProofMode/Tactics/Induction.lean | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Iris/Iris/ProofMode/Expr.lean b/Iris/Iris/ProofMode/Expr.lean index 4ab7ceeb5..20f02b560 100644 --- a/Iris/Iris/ProofMode/Expr.lean +++ b/Iris/Iris/ProofMode/Expr.lean @@ -484,6 +484,17 @@ structure IrisGoal where def isIrisGoal (expr : Expr) : Bool := isAppOfArity expr ``Entails' 4 +def containsIrisGoal (e : Expr) : Bool := + isIrisGoal e || + match e with + | .app f a => containsIrisGoal f || containsIrisGoal a + | .lam _ d b _ => containsIrisGoal d || containsIrisGoal b + | .forallE _ d b _ => containsIrisGoal d || containsIrisGoal b + | .letE _ t v b _ => containsIrisGoal t || containsIrisGoal v || containsIrisGoal b + | .mdata _ inner => containsIrisGoal inner + | .proj _ _ inner => containsIrisGoal inner + | _ => false + def parseIrisGoal? (expr : Expr) : Option IrisGoal := do -- remove top-level metadata when matching on the goal let expr := expr.consumeMData diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 1ebfd37c1..aac3bbf26 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -172,7 +172,7 @@ private def findIHs (m : MVarId) : ProofModeM (List FVarId) := let mut ihs := [] for decl in lctx do let type ← instantiateMVars decl.type - if isIrisGoal type.getForallBody then + if containsIrisGoal type.getForallBody then ihs := decl.fvarId :: ihs return ihs.reverse From 23b6d270487b2fa3075e9372c52694e1ac66e664 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 15 Jun 2026 22:53:24 +0200 Subject: [PATCH 127/181] =?UTF-8?q?Complete=20proof=20example=20that=20use?= =?UTF-8?q?s=20`iinduction`=20with=20`intoIH=5FlistForall=E2=82=82`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Iris/Iris/ProofMode/Expr.lean | 1 + Iris/Iris/Tests/Tactics.lean | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Iris/Iris/ProofMode/Expr.lean b/Iris/Iris/ProofMode/Expr.lean index 20f02b560..63b1f44c1 100644 --- a/Iris/Iris/ProofMode/Expr.lean +++ b/Iris/Iris/ProofMode/Expr.lean @@ -484,6 +484,7 @@ structure IrisGoal where def isIrisGoal (expr : Expr) : Bool := isAppOfArity expr ``Entails' 4 +/-- Recursively expression traversal to check whether it contains an Iris entailment -/ def containsIrisGoal (e : Expr) : Bool := isIrisGoal e || match e with diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 76de24bf5..4fbacb731 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2899,6 +2899,7 @@ def NTree.childCount : NTree α → Nat | .leaf => 0 | .node _ ts => ts.length +/-- An binary relation defined using nested induction -/ inductive NTree.Rel {α β} (R : α → β → Prop) : NTree α → NTree β → Prop | leaf : Rel R .leaf .leaf @@ -2925,14 +2926,18 @@ def NTree.Rel.induction_principle {α β} {R : α → β → Prop} .nil (fun _ _ ih_h ih_hs => .cons (fun _ => ih_h) ih_hs) +/-- Tests `iinduction` with induction that uses the type class instance `intoIH_listForall₂`. -/ example [BI PROP] {α β} {R : α → β → Prop} {t₁ : NTree α} {t₂ : NTree β} (H : NTree.Rel R t₁ t₂) : - ⊢@{PROP} ⌜ NTree.childCount t₁ = NTree.childCount t₂ ⌝ := by + ⊢@{PROP} ⌜NTree.childCount t₁ = NTree.childCount t₂⌝ := by iinduction H with | h_leaf => ipureintro apply rfl - | h_node x1 ts1 x2 ts2 r IH1 IH2 => sorry + | h_node x1 x2 t1 t2 r IH1 IH2 => + ipureintro + simp only [NTree.childCount] + induction IH1 with simp_all /-- Tests `iinduction` with simple induction on natural numbers. From f21db0a60de0ae2cb1ff04e9132cbcfddcc21b65 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 15 Jun 2026 22:57:42 +0200 Subject: [PATCH 128/181] Remove unused definitions in tests --- Iris/Iris/Tests/Tactics.lean | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 4fbacb731..d712fe838 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2877,24 +2877,6 @@ def NTree.induction_principle {α} (p : NTree α → Prop) (h_leaf : p leaf) @NTree.rec α p (λ ts => ∀ t ∈ ts, p t) h_leaf h_node (List.forall_mem_nil p) (λ _ _ h_head h_tail => List.forall_mem_cons.mpr (And.intro h_head h_tail)) -/-- A recursive "mirror" on `NTree` that mirrors every child subtree. -/ -def NTree.mirror : NTree α → NTree α - | .leaf => .leaf - | .node x ts => .node x (ts.map NTree.mirror) - -/-- - Direct analogue of the first `Tree` example. - The node case uses the manual induction principle for `NTree`, - which gives an induction hypothesis of the form `∀ t ∈ ts, P t`. --/ -example [BI PROP] {α} {t : NTree α} : - ⊢@{PROP} ⌜.mirror (.mirror t) = t⌝ := by - iinduction t with simp [NTree.mirror] - | h_leaf => - itrivial - | h_node x ts IH => - sorry - def NTree.childCount : NTree α → Nat | .leaf => 0 | .node _ ts => ts.length From 53d1c3da630ab067663859588a430382c8be8275 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Mon, 15 Jun 2026 23:34:47 +0200 Subject: [PATCH 129/181] Rewrite comment for checkDependentHyps --- Iris/Iris/ProofMode/Tactics/Revert.lean | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Revert.lean b/Iris/Iris/ProofMode/Tactics/Revert.lean index 65813832b..11b15ffdc 100644 --- a/Iris/Iris/ProofMode/Tactics/Revert.lean +++ b/Iris/Iris/ProofMode/Tactics/Revert.lean @@ -100,17 +100,18 @@ private def RevertState.revertLeanHyp st.revertLeanForallHyp f α /-- - When the tactic is `iinduction e generalizing z₁ ... zₙ` applied, - the variables `z₁ ... zₙ`, which can be regular Lean variables or Iris - hypotheses, are reverted from the context. - - The function `iHypsToGeneralize` is used for finding the Iris hypotheses - that references the induction target. - - However, it is possible that `x` is amongst the Lean variables explcitly - reverted by the user using the `generalizing` syntax while there exists - another Lean variable `y` such that `y` depends on `x`. In this case, this - function suggests that the user includes `%y` in the `generalizing` syntax. + When the tactic `irevert z₁ ... zₙ` is applied, `z₁ ... zₙ`, which can be + regular Lean variables or Iris hypotheses, are reverted from the context. + This function checks that hypotheses depending on any of `z₁ ... zₙ` is also + included for reverting. + + This function is also used for `iinduction e generalizing z₁ ... zₙ` and + `iloeb as IH generalizing z₁ ... zₙ`. When such a tactic applied, + the variables `z₁ ... zₙ`, it is possible that `x` is amongst the Lean + variables explcitly reverted by the user using the `generalizing` syntax while + there exists another Lean variable `y` such that `y` depends on `x`. In this + case, this function suggests that the user includes `%y` in the `generalizing` + syntax. It is also possible that `x` is amongst the Lean variables being generalised such that there exists an Iris hypothesis `HP` in the intuitionistic context From c6840290af3091319c9c629c9477d5b42d3ee4c3 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Tue, 16 Jun 2026 10:46:44 +0200 Subject: [PATCH 130/181] Remove intoIH_listForall --- Iris/Iris/ProofMode/Instances.lean | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/Iris/Iris/ProofMode/Instances.lean b/Iris/Iris/ProofMode/Instances.lean index 0663801c5..386ac47de 100644 --- a/Iris/Iris/ProofMode/Instances.lean +++ b/Iris/Iris/ProofMode/Instances.lean @@ -946,27 +946,7 @@ instance (priority := default - 1) intoIH_imp [BI PROP] (φ ψ : Prop) (Δ P Q : refine persistent_and_affinely_sep_right.2.trans ?_ exact pure_elim_right (fun hφ => h2.into_ih (hImp hφ)) -/-- Support for induction principles whose IH is guarded by `List.Forall`, e.g. - `∀ l, Forall P l → P (Tree l)` arising from nested inductive types like - `inductive ntree := Tree : List ntree → ntree`. -/ -@[rocq_alias into_ih_Forall] -instance (priority := default - 2) intoIH_listForall [BI PROP] (φ : α → Bool) (l : List α) (P : PROP) (Φ : α → PROP) - [h : ∀ x, IntoIH (φ x) P (Φ x)] : - IntoIH (l.all φ) P (bigSepL (fun _ a => iprop(□ Φ a)) l) where - into_ih := by - intro h1 - induction l generalizing Φ with - | nil => - simp [affine] - | cons x xs ih => - simp [List.all, bigSepL] at h1 ⊢ - rcases h1 with ⟨hx, hxs⟩ - apply intuitionistically_sep_idem.mpr.trans - refine sep_mono ?_ ?_ - · exact intuitionistically_intro_intuitionistically ((h x).into_ih hx) - · apply ih - apply (List.all_eq_true.mpr) - apply hxs +#rocq_ignore into_ih_Forall "List.Forall does not exist in the core Lean libraries, and ∀ x ∈ l, p x is used instead" /-- Support for induction principles whose IH is guarded by `List.Forall₂`, e.g. arising from mutual inductive types relating two lists element-wise. -/ From d2dfc2f160a32f6fd63b9c07670cdab9b3da8096 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Wed, 17 Jun 2026 10:38:27 +0200 Subject: [PATCH 131/181] Add proof for testing `iinduction` with `NTree.induction_principle` --- Iris/Iris/Tests/Tactics.lean | 49 +++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index d712fe838..037476878 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2877,35 +2877,50 @@ def NTree.induction_principle {α} (p : NTree α → Prop) (h_leaf : p leaf) @NTree.rec α p (λ ts => ∀ t ∈ ts, p t) h_leaf h_node (List.forall_mem_nil p) (λ _ _ h_head h_tail => List.forall_mem_cons.mpr (And.intro h_head h_tail)) +def NTree.id : NTree α → NTree α + | .leaf => .leaf + | .node x ts => .node x (ts.map .id) + +/-- Tests `iinduction` with the mutual induction principle -/ +example [BI PROP] {α} {t : NTree α} : ⊢@{PROP} ⌜t.id = t⌝ := by + iinduction t with simp [NTree.id] + | h_leaf => itrivial + | h_node x ts IH1 => + iinduction ts with simp + | nil => itrivial + | cons t ts IH2 => + isplit + · iapply IH1 + itrivial + · iapply IH2 + iintro !> %x H + iapply IH1 + imodintro + iright + iexact H + def NTree.childCount : NTree α → Nat | .leaf => 0 | .node _ ts => ts.length /-- An binary relation defined using nested induction -/ -inductive NTree.Rel {α β} (R : α → β → Prop) : - NTree α → NTree β → Prop +inductive NTree.Rel {α β} (R : α → β → Prop) : NTree α → NTree β → Prop | leaf : Rel R .leaf .leaf | node : ∀ a b ts₁ ts₂, - R a b → - List.Forall₂ (Rel R) ts₁ ts₂ → - Rel R (.node a ts₁) (.node b ts₂) + R a b → List.Forall₂ (Rel R) ts₁ ts₂ → Rel R (.node a ts₁) (.node b ts₂) @[induction_eliminator] def NTree.Rel.induction_principle {α β} {R : α → β → Prop} (p : ∀ {t1 : NTree α} {t2 : NTree β}, NTree.Rel R t1 t2 → Prop) - (h_leaf : p .leaf) - (h_node : ∀ (a : α) (b : β) (ts1 : List (NTree α)) (ts2 : List (NTree β)) - (ra : R a b) - (f2 : List.Forall₂ (NTree.Rel R) ts1 ts2), - List.Forall₂ (fun t1 t2 => ∀ h : NTree.Rel R t1 t2, p h) ts1 ts2 → - p (.node a b ts1 ts2 ra f2)) : - ∀ {t1 : NTree α} {t2 : NTree β} (h : NTree.Rel R t1 t2), p h := + (h_base : p .leaf) + (h_step : ∀ a b ts1 ts2 ra f2, + List.Forall₂ (fun t1 t2 => ∀ h : NTree.Rel R t1 t2, p h) ts1 ts2 → + p (.node a b ts1 ts2 ra f2)) : + ∀ t1 t2 (h : NTree.Rel R t1 t2), p h := @NTree.Rel.rec α β R (fun _ _ h => p h) (fun a b _ => List.Forall₂ (fun t1 t2 => ∀ h : NTree.Rel R t1 t2, p h) a b) - h_leaf - (fun a b ts1 ts2 ra f2 ih_ts => h_node a b ts1 ts2 ra f2 ih_ts) - .nil + h_base h_step .nil (fun _ _ ih_h ih_hs => .cons (fun _ => ih_h) ih_hs) /-- Tests `iinduction` with induction that uses the type class instance `intoIH_listForall₂`. -/ @@ -2913,10 +2928,10 @@ example [BI PROP] {α β} {R : α → β → Prop} {t₁ : NTree α} {t₂ : NTree β} (H : NTree.Rel R t₁ t₂) : ⊢@{PROP} ⌜NTree.childCount t₁ = NTree.childCount t₂⌝ := by iinduction H with - | h_leaf => + | h_base => ipureintro apply rfl - | h_node x1 x2 t1 t2 r IH1 IH2 => + | h_step x1 x2 t1 t2 r IH1 IH2 => ipureintro simp only [NTree.childCount] induction IH1 with simp_all From 0724695071f9acac541b589511a3fb9dd4cd2912 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Wed, 17 Jun 2026 13:03:11 +0200 Subject: [PATCH 132/181] Check for forward dependencies in `iHypsToGeneralize` --- Iris/Iris/ProofMode/Tactics/Induction.lean | 31 +++++++++++++--------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index aac3bbf26..413f878f3 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -152,13 +152,16 @@ private structure InductionState {u} {prop : Q(Type u)} {bi} (origE : Q($prop)) Lean context before applying Lean's built-in `induction` tactic. -/ private def iHypsToGeneralize {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} - (hyps : Hyps bi e) (fvar : FVarId) : List SelTarget := + (hyps : Hyps bi e) (fvar : FVarId) : ProofModeM (List SelTarget) := do + let fwdDeps ← collectForwardDeps #[mkFVar fvar] false + let dependentFVars := fwdDeps.map (·.fvarId!) + let ivars := hyps.intuitionisticIVarIds.filter <| fun ivar => match hyps.getDecl? ivar with - | some ⟨_, _, _, ty⟩ => ty.containsFVar fvar + | some ⟨_, _, _, ty⟩ => dependentFVars.any (ty.containsFVar ·) | none => false - (ivars ++ hyps.spatialIVarIds).map ({ kind := .ipm ·, explicit := false }) + return (ivars ++ hyps.spatialIVarIds).map ({ kind := .ipm ·, explicit := false }) /-- Search for hypotheses in the regular Lean context that are the in the form @@ -267,16 +270,18 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} (genSelPats : Option <| List SelPat) : ProofModeM Q($e ⊢ $goal) := do -- Find the regular pure/Iris hypotheses to be reverted - let targets ← do match genSelPats with - | none => pure <| iHypsToGeneralize hyps fvar - | some genSelPats => - let genSelTargets ← SelPat.resolve hyps genSelPats - checkDependentHyps "iinduction" hyps genSelTargets checkDependentHypsMkSuggestion - - let explicitIrisTargets := genSelTargets.filter (match ·.kind with | .ipm _ => true | _ => false) - let explicitPureTargets := genSelTargets.filter (match ·.kind with | .pure _ => true | _ => false) - let implicitIrisTargets := (iHypsToGeneralize hyps fvar).filter (not <| (explicitIrisTargets.map (·.kind)).contains ·.kind) - pure <| explicitPureTargets ++ implicitIrisTargets ++ explicitIrisTargets + let implicitIrisTargets ← iHypsToGeneralize hyps fvar + let targets ← do + match genSelPats with + | none => pure implicitIrisTargets + | some genSelPats => + let genSelTargets ← SelPat.resolve hyps genSelPats + -- Check for dependencies with hypotheses in the `generalizing` clause + checkDependentHyps "iinduction" hyps genSelTargets checkDependentHypsMkSuggestion + let explicitIrisTargets := genSelTargets.filter (match ·.kind with | .ipm _ => true | _ => false) + let explicitPureTargets := genSelTargets.filter (match ·.kind with | .pure _ => true | _ => false) + let implicitIrisTargets := implicitIrisTargets.filter (not <| (explicitIrisTargets.map (·.kind)).contains ·.kind) + pure <| explicitPureTargets ++ implicitIrisTargets ++ explicitIrisTargets -- Find the recursor name and constructor names of the inductive datatype let fvarType ← whnf <| ← inferType <| mkFVar fvar From 4d9d5a126875ea404a7f16b78011bd5a23f31ed5 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Wed, 17 Jun 2026 14:38:19 +0200 Subject: [PATCH 133/181] Avoid repetitive code for `checkDependentHyps` --- Iris/Iris/ProofMode/Tactics/Induction.lean | 27 ++++++++----- Iris/Iris/ProofMode/Tactics/Loeb.lean | 19 +-------- Iris/Iris/ProofMode/Tactics/Revert.lean | 47 ++++++++-------------- 3 files changed, 34 insertions(+), 59 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 413f878f3..a7eedc630 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -267,17 +267,14 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} (hyps : Hyps bi e) (goal : Q($prop)) (fvar : FVarId) (parsedAlts : Option Alts) (altRecName : Option Name) - (genSelPats : Option <| List SelPat) : + (genSelTargets : Option <| List SelTarget) : ProofModeM Q($e ⊢ $goal) := do -- Find the regular pure/Iris hypotheses to be reverted let implicitIrisTargets ← iHypsToGeneralize hyps fvar let targets ← do - match genSelPats with + match genSelTargets with | none => pure implicitIrisTargets - | some genSelPats => - let genSelTargets ← SelPat.resolve hyps genSelPats - -- Check for dependencies with hypotheses in the `generalizing` clause - checkDependentHyps "iinduction" hyps genSelTargets checkDependentHypsMkSuggestion + | some genSelTargets => let explicitIrisTargets := genSelTargets.filter (match ·.kind with | .ipm _ => true | _ => false) let explicitPureTargets := genSelTargets.filter (match ·.kind with | .pure _ => true | _ => false) let implicitIrisTargets := implicitIrisTargets.filter (not <| (explicitIrisTargets.map (·.kind)).contains ·.kind) @@ -513,10 +510,18 @@ elab_rules : tactic | none => pure none | some alts => parseInductionAlts alts - -- Parse the selection patterns for generalising hypotheses - let genSelPats ← genSelPats.mapM <| fun pats => do - liftMacroM <| SelPat.parse pats - ProofModeM.runTactic λ mvar { hyps, goal, .. } => do - let pf ← iInductionCore hyps goal fvar parsedAlts recName genSelPats + let genSelTargets ← do + match genSelPats with + | none => pure none + | some genSelPats => + -- Parse the selection patterns for generalising hypotheses + let parsedGenSelPats ← liftMacroM <| SelPat.parse genSelPats + let genSelTargets ← SelPat.resolve hyps parsedGenSelPats + -- Check for dependencies with the hypotheses in the selection targets + checkDependentHyps "iinduction" hyps genSelTargets genSelPats + fun newPats => `(tactic| iinduction $x $[using $r]? generalizing $newPats* $[$alts]?) + pure <| some genSelTargets + + let pf ← iInductionCore hyps goal fvar parsedAlts recName genSelTargets mvar.assign pf diff --git a/Iris/Iris/ProofMode/Tactics/Loeb.lean b/Iris/Iris/ProofMode/Tactics/Loeb.lean index 3a11b1b7f..0af1249ac 100644 --- a/Iris/Iris/ProofMode/Tactics/Loeb.lean +++ b/Iris/Iris/ProofMode/Tactics/Loeb.lean @@ -16,22 +16,6 @@ public meta section syntax (name := iloeb) "iloeb" "as" binderIdent "generalizing" (colGt ppSpace selPat)* : tactic -/-- - A helper function for `checkDependentHyps` to generate the fixed tactic. --/ -private def checkDependentHypsMkSuggestion - (purePats : Array (TSyntax `selPat)) (newIrisPats : Array (TSyntax `selPat)) : - ProofModeM (TSyntax `tactic) := do - let oldTactic ← getRef - let `(tactic| iloeb as $bi:binderIdent generalizing $pats:selPat*) := oldTactic - | throwError "iloeb: invalid syntax" - let existingIrisPats := pats.filter fun p => - match p.raw with - | `(selPat| %$_:ident) => false - | _ => true - let extendedPats : TSyntaxArray `selPat := purePats ++ existingIrisPats ++ newIrisPats - `(tactic| iloeb as $bi generalizing $extendedPats*) - /-- Apply Löb induction in the current goal. @@ -46,7 +30,8 @@ elab_rules : tactic let pats ← Elab.liftMacroM <| SelPat.parse hs ProofModeM.runTactic fun mvid {hyps, goal, ..} => do let targets : List SelTarget ← SelPat.resolve hyps (pats ++ [.spatial]) - checkDependentHyps "iloeb" hyps targets checkDependentHypsMkSuggestion + checkDependentHyps "iloeb" hyps targets hs + (fun newPats => `(tactic| iloeb as $IH generalizing $newPats*)) let expr ← iRevertIntro hyps goal targets fun {prop _ _} hyps goal k => do let some _ ← ProofModeM.trySynthInstanceQ q(BI.BILoeb $prop) | throwError m!"iloeb: no `{←ppExpr q(BI.BILoeb $prop)}` instance found" diff --git a/Iris/Iris/ProofMode/Tactics/Revert.lean b/Iris/Iris/ProofMode/Tactics/Revert.lean index 11b15ffdc..b8101ca0b 100644 --- a/Iris/Iris/ProofMode/Tactics/Revert.lean +++ b/Iris/Iris/ProofMode/Tactics/Revert.lean @@ -127,12 +127,9 @@ private def RevertState.revertLeanHyp -/ def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} (tacticName : String) (hyps : Hyps bi e) (explicitTargets : List SelTarget) - (mkSuggestion : - Array (TSyntax `selPat) → - Array (TSyntax `selPat) → - ProofModeM (TSyntax `tactic)): + (selPats : TSyntaxArray `selPat) + (mkTactic : TSyntaxArray `selPat → ProofModeM (TSyntax `tactic)): ProofModeM Unit := do - let explicitIrisIVarIds := explicitTargets.filterMap (match ·.kind with | .ipm ivar => some ivar | _ => none) let explicitPureFVars := explicitTargets.filterMap @@ -188,7 +185,12 @@ def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} -- Find the old tactic syntax and generate the new one with missing hypotheses added let oldTactic ← getRef - let newTactic ← mkSuggestion sortedPurePats newIrisPats + let existingIrisPats := selPats.filter fun p => + match p.raw with + | `(selPat| %$_:ident) => false + | _ => true + let extendedPats := sortedPurePats ++ existingIrisPats ++ newIrisPats + let newTactic ← mkTactic extendedPats -- Suggestion the fixed tactic Lean.Meta.Tactic.TryThis.addSuggestion oldTactic newTactic @@ -198,29 +200,9 @@ def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} the `generalizing` clause but are not themselves included:\ \n{"\n".intercalate (leanLines ++ irisLines)}" -syntax (name := irevert) "irevert" (colGt ppSpace selPat)+ : tactic - -/-- - A helper function for `checkDependentHyps` to generate the fixed tactic. --/ -private def checkDependentHypsMkSuggestion - (purePats : Array (TSyntax `selPat)) (newIrisPats : Array (TSyntax `selPat)) : - ProofModeM (TSyntax `tactic) := do - let oldTactic ← getRef - let `(tactic| irevert $pats:selPat*) := oldTactic - | throwError "irevert: invalid syntax" - let existingIrisPats := pats.filter fun p => - match p.raw with - | `(selPat| %$_:ident) => false - | _ => true - let extendedPats : TSyntaxArray `selPat := purePats ++ existingIrisPats ++ newIrisPats - `(tactic| irevert $extendedPats*) - def iRevertCore (targets : List SelTarget) {u : Level}{prop: Q(Type $u)}{bi : Q(BI $prop)}{e : Q($prop)}(hyps : Hyps bi e)(goal: Q($prop)) (k : ∀ {e : Q($prop)}, Hyps bi e → (goal: Q($prop)) → ProofModeM Q($e ⊢ $goal) := addBIGoal) : ProofModeM Q($e ⊢ $goal) := do - checkDependentHyps "irevert" hyps targets checkDependentHypsMkSuggestion - let init : RevertState e goal := { e, hyps, goal, pf := q(id) } let st ← targets.reverse.foldlM (init := init) fun st target => do match target.kind with @@ -230,11 +212,14 @@ def iRevertCore (targets : List SelTarget) {u : Level}{prop: Q(Type $u)}{bi : Q( let pf' : Q($(st.e) ⊢ $(st.goal)) ← withoutFVars (u:=0) st.reverted (k st.hyps st.goal) return q($(st.pf) $pf') -elab_rules : tactic - | `(tactic| irevert $pats:selPat*) => do - let pats ← liftMacroM <| SelPat.parse pats +syntax (name := irevert) "irevert" (colGt ppSpace selPat)+ : tactic + +elab_rules : tactic | `(tactic| irevert $pats:selPat*) => do + let parsedPats ← liftMacroM <| SelPat.parse pats - ProofModeM.runTactic fun mvar {hyps, goal, ..} => do - let targets ← SelPat.resolve hyps pats + ProofModeM.runTactic fun mvar { hyps, goal, .. } => do + let targets ← SelPat.resolve hyps parsedPats + checkDependentHyps "irevert" hyps targets pats + fun newPats => `(tactic| irevert $newPats*) let expr ← iRevertCore targets hyps goal mvar.assign expr From 2721180c95998963ce204271d62b087d3185b939 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Wed, 17 Jun 2026 15:30:23 +0200 Subject: [PATCH 134/181] Refactor code in `iInductionCore` --- Iris/Iris/ProofMode/Tactics/Induction.lean | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index a7eedc630..6c948ecf4 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -365,12 +365,10 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- Introduce the induction hypothesis back into the Iris proof state let st ← addIHs pfIntHyps irisGoal.hyps ihFVars - match parsedAlts with + let pf' ← match parsedAlts with | none => -- Remove the induction hypotheses from the regular Lean context - let pf' ← withoutFVars (u := 0) ihFVars.toArray <| k st.newHyps irisGoal.goal (addBIGoal · · ctor) - -- Fill the metavariable for the induction subgoal generated by Lean - s.mvarId.assign q($(st.pf).trans $pf') + withoutFVars (u := 0) ihFVars.toArray <| k st.newHyps irisGoal.goal (addBIGoal · · ctor) | some parsedAlts => -- Check whether the alternative names for this constructor are supplied by the user @@ -394,7 +392,7 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} let firstTactic := parsedAlts.tac -- Generate the induction subgoal for the user, label the induction subgoal with `ctor` - let pf' ← withoutFVars (u := 0) ihFVars.toArray <| match tacticSeq with + withoutFVars (u := 0) ihFVars.toArray <| match tacticSeq with | none => k st.newHyps irisGoal.goal (addBIGoal · · ctor) -- Run the tactics supplied by the user, if available | some tacticSeq => k st.newHyps irisGoal.goal <| fun hyps goal => do @@ -407,16 +405,13 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} | _ => pure () pure pf - -- Fill the metavariable for the induction subgoal generated by Lean - s.mvarId.assign q($(st.pf).trans $pf') - -- Alternative names not found, acceptable only when `firstTactic` solves it | none => match parsedAlts.tac with -- No first tactic given, the alternative name is missing | none => throwMissingAlt ctor | some firstTactic => - let pf' ← withoutFVars (u := 0) ihFVars.toArray <| + withoutFVars (u := 0) ihFVars.toArray <| k st.newHyps irisGoal.goal <| fun hyps goal => do let m ← mkBIGoal hyps goal ctor let subgoals ← evalTacticAt firstTactic m.mvarId! @@ -426,9 +421,8 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} m!"iinduction: alternative `{ctor.getString!}` has not been provided" pure m - -- Fill the metavariable for the induction subgoal generated by Lean - s.mvarId.assign q($(st.pf).trans $pf') - + -- Fill the metavariable for the induction subgoal generated by Lean + s.mvarId.assign q($(st.pf).trans $pf') return m return pf From 65b92b3e1c434b6412f941d3b835b4e2db69a366 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Wed, 17 Jun 2026 15:50:32 +0200 Subject: [PATCH 135/181] Minor refactoring --- Iris/Iris/ProofMode/Tactics/Induction.lean | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 6c948ecf4..ae1986d98 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -369,11 +369,10 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} | none => -- Remove the induction hypotheses from the regular Lean context withoutFVars (u := 0) ihFVars.toArray <| k st.newHyps irisGoal.goal (addBIGoal · · ctor) - | some parsedAlts => -- Check whether the alternative names for this constructor are supplied by the user match parsedAlts.alts.find? (matcher ctor) <|> parsedAlts.wildcard with - | some ⟨_, vars, _, stx⟩ => + | some ⟨_, vars, tacticSeq, stx⟩ => if vars.size > s.fields.size then throwOrLogErrorAt stx <| s!"iinduction: too many variable names provided at alternative `{ctor}`: ".append @@ -386,17 +385,12 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} let fieldType ← inferType fieldFVar addLocalVarInfo id lctx fieldFVar (some fieldType) true - -- Check whether the user has supplied tactic sequences for this induction subgoal - let tacticSeq := (parsedAlts.alts.find? (matcher ctor) <|> parsedAlts.wildcard).bind (·.tacs) - - let firstTactic := parsedAlts.tac - -- Generate the induction subgoal for the user, label the induction subgoal with `ctor` withoutFVars (u := 0) ihFVars.toArray <| match tacticSeq with | none => k st.newHyps irisGoal.goal (addBIGoal · · ctor) -- Run the tactics supplied by the user, if available | some tacticSeq => k st.newHyps irisGoal.goal <| fun hyps goal => do - let ⟨pf, newMVars, _⟩ ← (addBIGoalRunTactics hyps goal ctor firstTactic tacticSeq) + let ⟨pf, newMVars, _⟩ ← addBIGoalRunTactics hyps goal ctor parsedAlts.tac tacticSeq -- Throw an error if the first tactic already solves the goal match newMVars with | some [] => @@ -404,7 +398,6 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} s!"iinduction: alternative `{ctor.getString!}` is not needed" | _ => pure () pure pf - -- Alternative names not found, acceptable only when `firstTactic` solves it | none => match parsedAlts.tac with From 3bd91283fdd3ca9635d66e3b9c2e434d1bc07056 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Wed, 17 Jun 2026 16:08:13 +0200 Subject: [PATCH 136/181] Factorise the continuation function within `iInductionCore` --- Iris/Iris/ProofMode/Tactics/Induction.lean | 43 +++++++++++++--------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index ae1986d98..17c0fc32f 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -365,14 +365,11 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- Introduce the induction hypothesis back into the Iris proof state let st ← addIHs pfIntHyps irisGoal.hyps ihFVars - let pf' ← match parsedAlts with - | none => - -- Remove the induction hypotheses from the regular Lean context - withoutFVars (u := 0) ihFVars.toArray <| k st.newHyps irisGoal.goal (addBIGoal · · ctor) + -- For pretty printing of arguments + match parsedAlts with | some parsedAlts => - -- Check whether the alternative names for this constructor are supplied by the user match parsedAlts.alts.find? (matcher ctor) <|> parsedAlts.wildcard with - | some ⟨_, vars, tacticSeq, stx⟩ => + | some ⟨_, vars, _, stx⟩ => if vars.size > s.fields.size then throwOrLogErrorAt stx <| s!"iinduction: too many variable names provided at alternative `{ctor}`: ".append @@ -384,12 +381,21 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} let lctx ← getLCtx let fieldType ← inferType fieldFVar addLocalVarInfo id lctx fieldFVar (some fieldType) true + | none => pure () + | none => pure () + let k' : ProofModeContinuationIntro := fun hyps goal => do match parsedAlts with + -- Remove the induction hypotheses from the regular Lean context + | none => addBIGoal hyps goal ctor + | some parsedAlts => + -- Check whether the alternative names for this constructor are supplied by the user + match parsedAlts.alts.find? (matcher ctor) <|> parsedAlts.wildcard with + | some ⟨_, _, tacticSeq, stx⟩ => -- Generate the induction subgoal for the user, label the induction subgoal with `ctor` - withoutFVars (u := 0) ihFVars.toArray <| match tacticSeq with - | none => k st.newHyps irisGoal.goal (addBIGoal · · ctor) + match tacticSeq with + | none => addBIGoal hyps goal ctor -- Run the tactics supplied by the user, if available - | some tacticSeq => k st.newHyps irisGoal.goal <| fun hyps goal => do + | some tacticSeq => let ⟨pf, newMVars, _⟩ ← addBIGoalRunTactics hyps goal ctor parsedAlts.tac tacticSeq -- Throw an error if the first tactic already solves the goal match newMVars with @@ -404,18 +410,19 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- No first tactic given, the alternative name is missing | none => throwMissingAlt ctor | some firstTactic => - withoutFVars (u := 0) ihFVars.toArray <| - k st.newHyps irisGoal.goal <| fun hyps goal => do - let m ← mkBIGoal hyps goal ctor - let subgoals ← evalTacticAt firstTactic m.mvarId! - -- First tactic supplied by the user, but it does not completely solve this case - if !subgoals.isEmpty then - throwOrLogErrorAt parsedAlts.stx - m!"iinduction: alternative `{ctor.getString!}` has not been provided" - pure m + let m ← mkBIGoal hyps goal ctor + let subgoals ← evalTacticAt firstTactic m.mvarId! + -- First tactic supplied by the user, but it does not completely solve this case + if !subgoals.isEmpty then + throwOrLogErrorAt parsedAlts.stx + m!"iinduction: alternative `{ctor.getString!}` has not been provided" + pure m + + let pf' ← withoutFVars (u := 0) ihFVars.toArray <| k st.newHyps irisGoal.goal k' -- Fill the metavariable for the induction subgoal generated by Lean s.mvarId.assign q($(st.pf).trans $pf') + return m return pf From 2cbe8efe850b82b43191065031e2d9354c4f989e Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Wed, 17 Jun 2026 16:10:48 +0200 Subject: [PATCH 137/181] Use `Option.forM` to avoid unnecessary no-ops --- Iris/Iris/ProofMode/Tactics/Induction.lean | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 17c0fc32f..6d65558ef 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -366,8 +366,7 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} let st ← addIHs pfIntHyps irisGoal.hyps ihFVars -- For pretty printing of arguments - match parsedAlts with - | some parsedAlts => + parsedAlts.forM fun parsedAlts => do match parsedAlts.alts.find? (matcher ctor) <|> parsedAlts.wildcard with | some ⟨_, vars, _, stx⟩ => if vars.size > s.fields.size then @@ -382,7 +381,6 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} let fieldType ← inferType fieldFVar addLocalVarInfo id lctx fieldFVar (some fieldType) true | none => pure () - | none => pure () let k' : ProofModeContinuationIntro := fun hyps goal => do match parsedAlts with -- Remove the induction hypotheses from the regular Lean context From 9c4fc02526e53d6816095378ec42ef2ad85fbcb3 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Wed, 17 Jun 2026 16:17:27 +0200 Subject: [PATCH 138/181] Use `Std.HashSet` in `checkCtors` for efficient check for duplicates and more refactoring --- Iris/Iris/ProofMode/Tactics/Induction.lean | 63 +++++++++------------- Iris/Iris/Tests/Tactics.lean | 4 +- 2 files changed, 27 insertions(+), 40 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 6d65558ef..9c6db00ef 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -228,22 +228,18 @@ private def throwMissingAlt {α} (ctor : Name) : ProofModeM α := Otherwise, throw an error on the corresponding line. -/ private def checkCtors (ctors : List Name) (parsedAlts : Alts) : ProofModeM Unit := do - let rec checkCtorsRec : List Alt → ProofModeM Unit - | [] => pure () - | alt :: alts => do - let isValid := ctors.any (· == alt.ctor) - let isDup := alts.any (·.ctor == alt.ctor) + let mut handledCtors : Std.HashSet Name := {} - if !isValid then + for alt in parsedAlts.alts do + if !ctors.contains alt.ctor then throwOrLogErrorAt alt.stx m!"iinduction: invalid alternative name `{alt.ctor}`" - else if isDup then + + if handledCtors.contains alt.ctor then throwOrLogErrorAt alt.stx m!"iinduction: duplicate alternative name `{alt.ctor}`" - checkCtorsRec alts - - checkCtorsRec parsedAlts.alts.toList + handledCtors := handledCtors.insert alt.ctor /-- The main function handling the steps for the `iinduction` tactic. @@ -305,15 +301,11 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} let recCtors := ((← Lean.Meta.getElimInfo recName).altsInfo.map (·.name)).toList -- Check that all alternative names supplied by the user are valid - match parsedAlts with - | none => pure () - | some parsedAlts => + parsedAlts.forM fun parsedAlts => do checkCtors recCtors parsedAlts if recCtors.length == parsedAlts.alts.size then - match parsedAlts.wildcard with - | some w => + parsedAlts.wildcard.forM fun w => throwOrLogErrorAt w.stx "iinduction: wildcard alternative is not needed" - | none => pure () -- Define the names for variables and induction hypotheses if supplied by user let varNames : Array AltVarNames ← @@ -367,20 +359,19 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- For pretty printing of arguments parsedAlts.forM fun parsedAlts => do - match parsedAlts.alts.find? (matcher ctor) <|> parsedAlts.wildcard with - | some ⟨_, vars, _, stx⟩ => - if vars.size > s.fields.size then - throwOrLogErrorAt stx <| - s!"iinduction: too many variable names provided at alternative `{ctor}`: ".append - s!"{vars.size} provided, but {s.fields.size} expected" - - -- For pretty printing of arguments - for ⟨fieldFVar, varStx⟩ in s.fields.toList.zip vars.toList do - if let `(binderIdent| $id:ident) := varStx then - let lctx ← getLCtx - let fieldType ← inferType fieldFVar - addLocalVarInfo id lctx fieldFVar (some fieldType) true - | none => pure () + (parsedAlts.alts.find? (matcher ctor) <|> parsedAlts.wildcard).forM + fun ⟨_, vars, _, stx⟩ => do + if vars.size > s.fields.size then + throwOrLogErrorAt stx <| + s!"iinduction: too many variable names provided at alternative `{ctor}`: ".append + s!"{vars.size} provided, but {s.fields.size} expected" + + -- For pretty printing of arguments + for ⟨fieldFVar, varStx⟩ in s.fields.toList.zip vars.toList do + if let `(binderIdent| $id:ident) := varStx then + let lctx ← getLCtx + let fieldType ← inferType fieldFVar + addLocalVarInfo id lctx fieldFVar (some fieldType) true let k' : ProofModeContinuationIntro := fun hyps goal => do match parsedAlts with -- Remove the induction hypotheses from the regular Lean context @@ -396,11 +387,9 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} | some tacticSeq => let ⟨pf, newMVars, _⟩ ← addBIGoalRunTactics hyps goal ctor parsedAlts.tac tacticSeq -- Throw an error if the first tactic already solves the goal - match newMVars with - | some [] => + if let some [] := newMVars then throwOrLogErrorAt stx s!"iinduction: alternative `{ctor.getString!}` is not needed" - | _ => pure () pure pf -- Alternative names not found, acceptable only when `firstTactic` solves it | none => @@ -503,17 +492,15 @@ elab_rules : tactic | some alts => parseInductionAlts alts ProofModeM.runTactic λ mvar { hyps, goal, .. } => do - let genSelTargets ← do - match genSelPats with - | none => pure none - | some genSelPats => + let genSelTargets ← + genSelPats.mapM fun genSelPats => do -- Parse the selection patterns for generalising hypotheses let parsedGenSelPats ← liftMacroM <| SelPat.parse genSelPats let genSelTargets ← SelPat.resolve hyps parsedGenSelPats -- Check for dependencies with the hypotheses in the selection targets checkDependentHyps "iinduction" hyps genSelTargets genSelPats fun newPats => `(tactic| iinduction $x $[using $r]? generalizing $newPats* $[$alts]?) - pure <| some genSelTargets + pure genSelTargets let pf ← iInductionCore hyps goal fvar parsedAlts recName genSelTargets mvar.assign pf diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 037476878..983d7b84e 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2980,10 +2980,10 @@ example [BI PROP] {P : PROP} : ⊢ P := by -/ /-- error: iinduction: invalid alternative name `invalidA` --- -error: iinduction: duplicate alternative name `zero` ---- error: iinduction: invalid alternative name `invalidB` --- +error: iinduction: duplicate alternative name `zero` +--- error: iinduction: alternative `succ` has not been provided -/ #guard_msgs in example [BI PROP] {n : Nat} : From 3c4e87a8baebd95cd6c907b2b70959e563fe071e Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Wed, 17 Jun 2026 16:50:16 +0200 Subject: [PATCH 139/181] Condense comments and syntax formatting --- Iris/Iris/ProofMode/Tactics/Induction.lean | 119 +++++---------------- Iris/Iris/ProofMode/Tactics/Revert.lean | 59 +++------- 2 files changed, 44 insertions(+), 134 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 9c6db00ef..50bd5864e 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -24,19 +24,18 @@ syntax (name := iinduction) "iinduction" colGt term ("generalizing" (ppSpace colGt selPat)+)? (inductionAlts)? : tactic -/-- - Information obtained from parsing a case under the `with` syntax --/ +/-- Information from the tactic user for an induction subgoal -/ private structure Alt where -- The name of the constructor ctor : Name -- The alternative names supplied by the tactic user - vars : Array (TSyntax `Lean.binderIdent) + vars : Array <| TSyntax `Lean.binderIdent -- User-supplied tactics for this case, `none` if a hole (`_` or `?_`) is given tacs : Option <| TSyntax `Lean.Parser.Tactic.tacticSeq -- The syntax, useful for error message printing stx : TSyntax `Lean.Parser.Tactic.inductionAlt +/-- Information from the tactic user for all induction subgoals -/ private structure Alts where -- User-supplied tactic to be applied before splitting into cases tac : Option <| TSyntax `tactic @@ -60,7 +59,7 @@ private def parseInductionAlts (altsSyntax : TSyntax `Lean.Parser.Tactic.inducti | `(ident| $id:ident) => `(binderIdent| $id:ident) | _ => `(binderIdent| _) - let mut parsedAlts : Array Alt := #[] + let mut parsedAlts := #[] match altsSyntax with | `(inductionAlts| with $[$tac]? $[$alts]*) => @@ -86,22 +85,6 @@ private def parseInductionAlts (altsSyntax : TSyntax `Lean.Parser.Tactic.inducti return ⟨tac, parsedAlts, none, altsSyntax⟩ | _ => throwErrorAt altsSyntax "iinduction: invalid syntax" -/-- - A helper function for `checkDependentHyps` to generate the fixed tactic. --/ -private def checkDependentHypsMkSuggestion - (purePats : Array (TSyntax `selPat)) (newIrisPats : Array (TSyntax `selPat)) : - ProofModeM (TSyntax `tactic) := do - let oldTactic ← getRef - let `(tactic| iinduction $x $[using $r]? generalizing $genSelPats* $[$alts]?) := oldTactic - | throwError "iinduction: invalid syntax" - let existingIrisPats := genSelPats.filter fun p => - match p.raw with - | `(selPat| %$_:ident) => false - | _ => true - let extendedPats : TSyntaxArray `selPat := purePats ++ existingIrisPats ++ newIrisPats - `(tactic| iinduction $x $[using $r]? generalizing $extendedPats* $[$alts]?) - /-- This theorem is used for updating the proof in `InductionState` as `addIHs` iterates through the list of induction hypotheses and introduces them from @@ -142,14 +125,8 @@ private structure InductionState {u} {prop : Q(Type u)} {bi} (origE : Q($prop)) (pf : Q($origE ⊢ $newE)) /-- - Given a collection of hypotheses (`hyps`) and a free variable `fvar` - representing the variable on which induction is applied, return the subset of - hypotheses in the Iris goal to be generalised. This includes the subset of - hypotheses in the intuitionistic context with occurrences of `fvar`, as well - as all spatial hypotheses. - - These hypotheses are then reverted from the the Iris contexts into the regular - Lean context before applying Lean's built-in `induction` tactic. + Finds spatial hypotheses in the spatial context as well as the intuitionistic + hypotheses that depend on `fvar`. -/ private def iHypsToGeneralize {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} (hyps : Hyps bi e) (fvar : FVarId) : ProofModeM (List SelTarget) := do @@ -163,38 +140,26 @@ private def iHypsToGeneralize {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} return (ivars ++ hyps.spatialIVarIds).map ({ kind := .ipm ·, explicit := false }) -/-- - Search for hypotheses in the regular Lean context that are the in the form - of Iris goals. These must be induction hypotheses generated by Lean's built-in - `induction` tactic, as there would otherwise not be Iris goals as assumptions - in the regular context. --/ +/-- Search for induction hypotheses in the pure Lean context -/ private def findIHs (m : MVarId) : ProofModeM (List FVarId) := m.withContext do let lctx ← getLCtx + -- let mut ihs := [] for decl in lctx do let type ← instantiateMVars decl.type + -- The hypothesis is an IH when it is an Iris entailment if containsIrisGoal type.getForallBody then ihs := decl.fvarId :: ihs return ihs.reverse -/-- - Introduce a list of induction hypotheses into the intuitionistic context - of the Iris proof state. The list of `FVarId` values (`ihFVars`) should be - the list returned by `findIHs`. The function returns the final - `InductionState` with all induction hypotheses handled. - - The argument `intuitionsiticProof` is obtained from the fact that the spatial - context is empty, which is indeed the case when all hypotheses therein have - been reverted into the regular Lean context. --/ +/-- Introduce into the intuitionistic context of the Iris proof state. -/ private def addIHs {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e : Q($prop)} (pfIntHyps : Q($e ⊢ □ $e)) (hyps : Hyps bi e) (ihFVars : List FVarId) : ProofModeM (@InductionState u prop bi e) := do -- Initialise the mutable instance of `InductionState` - let mut st : InductionState e := { newHyps := hyps, pf := q(.rfl) } + let mut st := { newHyps := hyps, pf := q(.rfl) } -- Iteratively move the induction hypotheses into the intuitionistic context for i in ihFVars do @@ -215,11 +180,6 @@ private def addIHs {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e : Q($prop)} return st -/-- - Throw an error if the user has supplied some but not all alternative names. - Also throw an error if the user has supplied multiple sets of alternative - names for the same constructor. --/ private def throwMissingAlt {α} (ctor : Name) : ProofModeM α := throwError "iinduction: alternative `{ctor.getString!}` has not been provided" @@ -241,24 +201,6 @@ private def checkCtors (ctors : List Name) (parsedAlts : Alts) : ProofModeM Unit handledCtors := handledCtors.insert alt.ctor -/-- - The main function handling the steps for the `iinduction` tactic. - - 1. Revert Iris hypotheses explicitly specified by the tactic user - (applicable when the `generalizing` syntax is used). - 2. Revert all spatial hypotheses in the Iris proof state as well as the - relevant hypotheses in the intuitionistic context. - 3. Revert pure Lean variables specified by the tactic user - (applicable when the `generalizing` syntax is used). - 4. Prepare the arguments for `Lean.Meta.Tactic.induction`. - 5. Apply `Lean.Meta.Tactic.induction` to obtain the induction subgoals. - 6. For each subgoal, move the induction hypotheses from the regular Lean - context into the Iris intuitionistic context. - 7. Introduce hypotheses reverted in steps 1 and 2 back into the Iris proof state. - - The relevant intuitionistic hypotheses to which Step 2 refers are those - involving `fvar` or pure Lean variables in `genSelTargets`. --/ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} (hyps : Hyps bi e) (goal : Q($prop)) (fvar : FVarId) (parsedAlts : Option Alts) @@ -294,8 +236,7 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} | _ => throwError "iinduction: {indName} is not inductive" | _ => throwError "iinduction: unable to determine inductive type" - let matcher : Name → Alt → Bool := - fun ctor alt => alt.ctor != .anonymous && ctor == alt.ctor + let matcher := fun ctor alt => alt.ctor != .anonymous && ctor == alt.ctor -- Find the constructor names let recCtors := ((← Lean.Meta.getElimInfo recName).altsInfo.map (·.name)).toList @@ -308,7 +249,7 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} throwOrLogErrorAt w.stx "iinduction: wildcard alternative is not needed" -- Define the names for variables and induction hypotheses if supplied by user - let varNames : Array AltVarNames ← + let varNames ← match parsedAlts with | none => pure <| recCtors.toArray.map <| fun _ => { explicit := true, varNames := [] } @@ -414,20 +355,15 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} return pf -/-- - Given a term expression, check whether it is a `FVarId` value. If so, - return it directly. Otherwise, generalise the term expression before - returning its `FVarId`. This function has to be called before entering - `ProofModeM`, as it directly replaces the metavariable for the current - proof goal. --/ private def generalizeTermWithFVar (x : TSyntax `term) : TacticM FVarId := do let e ← withMainContext <| elabTerm x none let e ← instantiateMVars e + -- Return the `FVarId` value directly if the term expresson is a free variable if e.isFVar then return e.fvarId! + -- Otherwise, generalise the term expression and return the `FVarId` let mvarId ← getMainGoal let (fvars, newMVarId) ← mvarId.generalize #[{ expr := e }] replaceMainGoal [newMVarId] @@ -440,9 +376,8 @@ private def generalizeTermWithFVar (x : TSyntax `term) : TacticM FVarId := do `iinduction e` performs the following steps: 1. Generalises the expression `e` using `Lean.Meta.Tactic.Generalize`. 2. Reverts all hypotheses in the spatial context, as well as those in the - intuitionistic context that involves the variable to which induction is applied. - 3. Applies the built-in `induction` tactic in Lean to obtain the induction - subgoals. + intuitionistic context that depend on the induction target. + 3. Obtain the induction subgoals. 4. Moves all induction hypotheses into the intuitionstic context. 5. Introduce hypotheses reverted in steps 2 and 3 back into the Iris contexts. @@ -452,14 +387,13 @@ private def generalizeTermWithFVar (x : TSyntax `term) : TacticM FVarId := do which is expressed as a selection pattern. Both Iris hypotheses and pure Lean hypotheses can be generalised. - `iinduction e with | constr₁ => tac₁ | ... | constrₙ => tacₙ`: - the constructor names can either be long (e.g., `Nat.zero`) or short (e.g., `zero`). - Arguments are optionally given names or otherwise remain inaccessible. + arguments are optionally given names or otherwise remain inaccessible. As an example, consider the following Iris context, where `n : Nat`. ``` ∗HP : P - □HQ : Q + □HQ : Q m □HR : R ∗HS : S □HT : T n @@ -468,13 +402,16 @@ private def generalizeTermWithFVar (x : TSyntax `term) : TacticM FVarId := do By applying `iinduction n`, all spatial hypotheses (`HP` and `HS`) are reverted. The hypothesis `HT` is also reverted because it involves the induction target `n`. + By applying `iinduction n generalizing HQ`, the hypotheses `HQ` - are additionally reverted and thus included as premises in the induction + are additionally reverted and thus included as a wand premise in the induction hypothesis. - One can also generalise pure variables in the regular Lean context. However, - if there exists some another pure/Iris hypothesis that is forward-dependent. - For example, `iinduction n generalizing %R` is not valid as `HR` depends on `R`. - Instead, one can use `iinduction n generalizing %R HR`. + + One can also generalise pure variables in the regular Lean context. + If there exists some pure/Iris hypotheses that is forward-dependent, they + should also be included in the `generalizing` clause. In the example above, + instead of `iinduction n generalizing %m`, one should use + `iinduction n generalizing %m HQ`. -/ elab_rules : tactic | `(tactic| iinduction $x @@ -484,7 +421,7 @@ elab_rules : tactic let fvar ← generalizeTermWithFVar x -- Parse the recursor name provided by the user - let recName : Option Name := r.map (·.getId) + let recName := r.map (·.getId) -- Parse the list of alternative names supplied by the user let parsedAlts ← match alts with diff --git a/Iris/Iris/ProofMode/Tactics/Revert.lean b/Iris/Iris/ProofMode/Tactics/Revert.lean index b8101ca0b..e2fa8c02c 100644 --- a/Iris/Iris/ProofMode/Tactics/Revert.lean +++ b/Iris/Iris/ProofMode/Tactics/Revert.lean @@ -100,35 +100,13 @@ private def RevertState.revertLeanHyp st.revertLeanForallHyp f α /-- - When the tactic `irevert z₁ ... zₙ` is applied, `z₁ ... zₙ`, which can be - regular Lean variables or Iris hypotheses, are reverted from the context. - This function checks that hypotheses depending on any of `z₁ ... zₙ` is also - included for reverting. - - This function is also used for `iinduction e generalizing z₁ ... zₙ` and - `iloeb as IH generalizing z₁ ... zₙ`. When such a tactic applied, - the variables `z₁ ... zₙ`, it is possible that `x` is amongst the Lean - variables explcitly reverted by the user using the `generalizing` syntax while - there exists another Lean variable `y` such that `y` depends on `x`. In this - case, this function suggests that the user includes `%y` in the `generalizing` - syntax. - - It is also possible that `x` is amongst the Lean variables being generalised - such that there exists an Iris hypothesis `HP` in the intuitionistic context - where: - 1. `P` does not depend on the induction target and thus not reverted automatically, - 2. `P` depends on `x` in the `generalizing` syntax, and - 3. `P` itself is not included in the `generalizing` syntax. - In this case, this function suggests the user to include `HP` in the - `generalizing` syntax. - - Note that Iris hypotheses in the spatial context are always reverted, so there - is no need for further checks by this function. + Throw an error if there exists hypotheses that are depend on any hypothesis + in `explicitTargets` but are not themselves in the list. -/ def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} (tacticName : String) (hyps : Hyps bi e) (explicitTargets : List SelTarget) (selPats : TSyntaxArray `selPat) - (mkTactic : TSyntaxArray `selPat → ProofModeM (TSyntax `tactic)): + (mkTactic : TSyntaxArray `selPat → ProofModeM (TSyntax `tactic)): ProofModeM Unit := do let explicitIrisIVarIds := explicitTargets.filterMap (match ·.kind with | .ipm ivar => some ivar | _ => none) @@ -138,7 +116,7 @@ def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} -- Pairs of `FVarId` values `(f1, f2)` indicating `f1` depends on `f2`. let mut missingPure : List (FVarId × FVarId) := [] - -- Check forward dependency of pure hypotheses in the `generalizing` syntax + -- Check forward dependency of pure hypotheses for fvar in explicitPureFVars.eraseDups do let fwdDeps ← collectForwardDeps #[mkFVar fvar] false for dep in fwdDeps do @@ -147,22 +125,18 @@ def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} if depId != fvar && !explicitPureFVars.contains depId then -- Record the missing hypothesis to be generalised, if not already included if !missingPure.any (·.fst == depId) then - missingPure := missingPure ++ [(depId, fvar)] + missingPure := missingPure.cons (depId, fvar) let allPureFVars := explicitPureFVars ++ missingPure.map (·.fst) - -- Check Iris hypothesis that depend on hypotheses in the `generalizing` syntax + -- Check forward dependency of Iris hypotheses let missingIris : List (Name × FVarId) := hyps.intuitionisticIVarIds.filterMap fun ivar => if explicitIrisIVarIds.contains ivar then none - else match hyps.getDecl? ivar with - | some ⟨name, _, _, ty⟩ => - match allPureFVars.find? (ty.containsFVar ·) with - | some x => some (name, x) - | none => none - | none => none - - -- Throw an error if there exists some pure/Lean hypotheses that should also be generalised + else hyps.getDecl? ivar >>= fun ⟨name, _, _, ty⟩ => + (allPureFVars.find? (ty.containsFVar ·)).map (name, ·) + + -- Add an error message if there exists some pure/Lean hypotheses that should also be generalised if !missingPure.isEmpty || !missingIris.isEmpty then let leanLines ← missingPure.mapM fun (depId, srcId) => do let depDecl ← depId.getDecl @@ -176,7 +150,7 @@ def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} let allPureFVarsSorted := (← getLCtx).sortFVarsByContextOrder allPureFVars.eraseDups.toArray let sortedPurePats : Array (TSyntax `selPat) ← allPureFVarsSorted.mapM fun fvarId => do let decl ← fvarId.getDecl - let id := mkIdent (.mkSimple decl.userName.toString) + let id := mkIdent <| .mkSimple decl.userName.toString `(selPat| %$id:ident) -- Build `selPat` syntax nodes for each missing item @@ -186,9 +160,7 @@ def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} -- Find the old tactic syntax and generate the new one with missing hypotheses added let oldTactic ← getRef let existingIrisPats := selPats.filter fun p => - match p.raw with - | `(selPat| %$_:ident) => false - | _ => true + match p.raw with | `(selPat| %$_:ident) => false | _ => true let extendedPats := sortedPurePats ++ existingIrisPats ++ newIrisPats let newTactic ← mkTactic extendedPats @@ -200,8 +172,9 @@ def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} the `generalizing` clause but are not themselves included:\ \n{"\n".intercalate (leanLines ++ irisLines)}" -def iRevertCore (targets : List SelTarget) {u : Level}{prop: Q(Type $u)}{bi : Q(BI $prop)}{e : Q($prop)}(hyps : Hyps bi e)(goal: Q($prop)) - (k : ∀ {e : Q($prop)}, Hyps bi e → (goal: Q($prop)) → ProofModeM Q($e ⊢ $goal) := addBIGoal) : +def iRevertCore (targets : List SelTarget) {u : Level} {prop: Q(Type $u)} + {bi : Q(BI $prop)} {e : Q($prop)} (hyps : Hyps bi e) (goal: Q($prop)) + (k : ∀ {e : Q($prop)}, Hyps bi e → (goal: Q($prop)) → ProofModeM Q($e ⊢ $goal) := addBIGoal) : ProofModeM Q($e ⊢ $goal) := do let init : RevertState e goal := { e, hyps, goal, pf := q(id) } let st ← targets.reverse.foldlM (init := init) fun st target => do @@ -209,7 +182,7 @@ def iRevertCore (targets : List SelTarget) {u : Level}{prop: Q(Type $u)}{bi : Q( | .ipm ivar => st.revertProofModeHyp ivar | .pure fvar => st.revertLeanHyp fvar - let pf' : Q($(st.e) ⊢ $(st.goal)) ← withoutFVars (u:=0) st.reverted (k st.hyps st.goal) + let pf' : Q($(st.e) ⊢ $(st.goal)) ← withoutFVars (u := 0) st.reverted (k st.hyps st.goal) return q($(st.pf) $pf') syntax (name := irevert) "irevert" (colGt ppSpace selPat)+ : tactic From 8f71934b135e8b447dc3e8bf4aea73e97081fc4f Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Wed, 17 Jun 2026 17:13:49 +0200 Subject: [PATCH 140/181] Minor formatting --- Iris/Iris/ProofMode/Tactics/Revert.lean | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Iris/Iris/ProofMode/Tactics/Revert.lean b/Iris/Iris/ProofMode/Tactics/Revert.lean index e2fa8c02c..a1a3c40cf 100644 --- a/Iris/Iris/ProofMode/Tactics/Revert.lean +++ b/Iris/Iris/ProofMode/Tactics/Revert.lean @@ -185,7 +185,7 @@ def iRevertCore (targets : List SelTarget) {u : Level} {prop: Q(Type $u)} let pf' : Q($(st.e) ⊢ $(st.goal)) ← withoutFVars (u := 0) st.reverted (k st.hyps st.goal) return q($(st.pf) $pf') -syntax (name := irevert) "irevert" (colGt ppSpace selPat)+ : tactic +syntax (name := irevert) "irevert " (colGt ppSpace selPat)+ : tactic elab_rules : tactic | `(tactic| irevert $pats:selPat*) => do let parsedPats ← liftMacroM <| SelPat.parse pats From bee1c3cac5d6231a02b6d1e0e076ef0e6ef7e66a Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Wed, 17 Jun 2026 17:24:29 +0200 Subject: [PATCH 141/181] Minor fix --- Iris/Iris/ProofMode/Tactics/Induction.lean | 1 - Iris/Iris/ProofMode/Tactics/Revert.lean | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 50bd5864e..4e73871d1 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -144,7 +144,6 @@ private def iHypsToGeneralize {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} private def findIHs (m : MVarId) : ProofModeM (List FVarId) := m.withContext do let lctx ← getLCtx - -- let mut ihs := [] for decl in lctx do let type ← instantiateMVars decl.type diff --git a/Iris/Iris/ProofMode/Tactics/Revert.lean b/Iris/Iris/ProofMode/Tactics/Revert.lean index a1a3c40cf..42350befa 100644 --- a/Iris/Iris/ProofMode/Tactics/Revert.lean +++ b/Iris/Iris/ProofMode/Tactics/Revert.lean @@ -126,6 +126,7 @@ def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} -- Record the missing hypothesis to be generalised, if not already included if !missingPure.any (·.fst == depId) then missingPure := missingPure.cons (depId, fvar) + missingPure := missingPure.reverse let allPureFVars := explicitPureFVars ++ missingPure.map (·.fst) From 2c57b4ade1d470643f9caa02e9c25558c7da95a4 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Fri, 19 Jun 2026 13:14:44 +0200 Subject: [PATCH 142/181] Update `intoIH_entails` in response to #469 --- Iris/Iris/ProgramLogic/Lifting.lean | 1 - Iris/Iris/ProofMode/Instances.lean | 3 ++- Iris/Iris/Tests/Tactics.lean | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Iris/Iris/ProgramLogic/Lifting.lean b/Iris/Iris/ProgramLogic/Lifting.lean index 3e7106f45..28ce47eba 100644 --- a/Iris/Iris/ProgramLogic/Lifting.lean +++ b/Iris/Iris/ProgramLogic/Lifting.lean @@ -190,7 +190,6 @@ theorem wp_pure_step_fupd [Inhabited State] (E₂ : CoPset) iinduction Hexec using Relation.Iterate.head_induction_on with simp only [Nat.repeat] | rfl => iintro Hwp - simp only [Nat.repeat] rw (occs := [2]) [fupd_wp_iff.to_eq] icases lc_zero with >Hz iapply Hwp $$ Hz diff --git a/Iris/Iris/ProofMode/Instances.lean b/Iris/Iris/ProofMode/Instances.lean index 386ac47de..1cd553fc1 100644 --- a/Iris/Iris/ProofMode/Instances.lean +++ b/Iris/Iris/ProofMode/Instances.lean @@ -9,6 +9,7 @@ public import Iris.BI public import Iris.ProofMode.Classes public import Iris.ProofMode.ClassesMake public import Iris.ProofMode.ModalityInstances +public import Iris.ProofMode.Expr public import Iris.Std.TC public import Iris.Std.RocqPorting @@ -921,7 +922,7 @@ instance combineSepGives_persistently [BI PROP] (Q1 Q2 P : PROP) combine_sep_gives := persistently_sep_mpr.trans (persistently_mono h.combine_sep_gives) @[rocq_alias into_ih_entails] -instance intoIH_entails [BI PROP] (P Q : PROP) : IntoIH (P ⊢ Q) P Q where +instance intoIH_entails [BI PROP] (P Q : PROP) : IntoIH (Entails' P Q) P Q where into_ih := λ hpq => intuitionistically_elim.trans hpq @[rocq_alias into_ih_forall] diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 5f3bc515a..8e26b5aef 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2855,6 +2855,7 @@ example [BI PROP] {α} {t : Tree α} {P : α → PROP} : Tree.pred P t -∗ Tree.pred P (.mirror t) := by iintro H iinduction t with simp [Tree.mirror, Tree.pred] + | leaf => itrivial | node l x r ihl ihr => icases H with ⟨Hl, Hx, Hr⟩ iframe From 0a6fd190af87eff2d799737164cf1a6e3e51c950 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Fri, 19 Jun 2026 09:57:41 +0200 Subject: [PATCH 143/181] Eliminate unnecessary use of `Option` for `genSelTargets` in `iInductionCore` --- Iris/Iris/ProofMode/Tactics/Induction.lean | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 4e73871d1..89f323e5d 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -202,20 +202,15 @@ private def checkCtors (ctors : List Name) (parsedAlts : Alts) : ProofModeM Unit private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} (hyps : Hyps bi e) (goal : Q($prop)) (fvar : FVarId) - (parsedAlts : Option Alts) - (altRecName : Option Name) - (genSelTargets : Option <| List SelTarget) : + (parsedAlts : Option Alts) (altRecName : Option Name) (genSelTargets : List SelTarget) : ProofModeM Q($e ⊢ $goal) := do -- Find the regular pure/Iris hypotheses to be reverted let implicitIrisTargets ← iHypsToGeneralize hyps fvar let targets ← do - match genSelTargets with - | none => pure implicitIrisTargets - | some genSelTargets => - let explicitIrisTargets := genSelTargets.filter (match ·.kind with | .ipm _ => true | _ => false) - let explicitPureTargets := genSelTargets.filter (match ·.kind with | .pure _ => true | _ => false) - let implicitIrisTargets := implicitIrisTargets.filter (not <| (explicitIrisTargets.map (·.kind)).contains ·.kind) - pure <| explicitPureTargets ++ implicitIrisTargets ++ explicitIrisTargets + let explicitIrisTargets := genSelTargets.filter (match ·.kind with | .ipm _ => true | _ => false) + let explicitPureTargets := genSelTargets.filter (match ·.kind with | .pure _ => true | _ => false) + let implicitIrisTargets := implicitIrisTargets.filter (not <| (explicitIrisTargets.map (·.kind)).contains ·.kind) + pure <| explicitPureTargets ++ implicitIrisTargets ++ explicitIrisTargets -- Find the recursor name and constructor names of the inductive datatype let fvarType ← whnf <| ← inferType <| mkFVar fvar @@ -429,7 +424,7 @@ elab_rules : tactic ProofModeM.runTactic λ mvar { hyps, goal, .. } => do let genSelTargets ← - genSelPats.mapM fun genSelPats => do + genSelPats.elim (pure []) fun genSelPats => do -- Parse the selection patterns for generalising hypotheses let parsedGenSelPats ← liftMacroM <| SelPat.parse genSelPats let genSelTargets ← SelPat.resolve hyps parsedGenSelPats From 40141f388f557572d1a73a9e479a4cb7499d4065 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Fri, 19 Jun 2026 11:19:38 +0200 Subject: [PATCH 144/181] Rename `k` as `kIntro`, use `k'` inline to avoid variable declaration --- Iris/Iris/ProofMode/Tactics/Induction.lean | 66 +++++++++++----------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 89f323e5d..78f7a3224 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -263,7 +263,7 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} throwMissingAlt ctor let pf ← iRevertIntro hyps goal targets <| - fun hyps' goal' k => do + fun hyps' goal' kIntro => do -- Create a new metavariable for the proof goal upon reverting hypotheses let m ← mkBIGoal hyps' goal' @@ -308,39 +308,39 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} let fieldType ← inferType fieldFVar addLocalVarInfo id lctx fieldFVar (some fieldType) true - let k' : ProofModeContinuationIntro := fun hyps goal => do match parsedAlts with - -- Remove the induction hypotheses from the regular Lean context - | none => addBIGoal hyps goal ctor - | some parsedAlts => - -- Check whether the alternative names for this constructor are supplied by the user - match parsedAlts.alts.find? (matcher ctor) <|> parsedAlts.wildcard with - | some ⟨_, _, tacticSeq, stx⟩ => - -- Generate the induction subgoal for the user, label the induction subgoal with `ctor` - match tacticSeq with + let pf' ← withoutFVars (u := 0) ihFVars.toArray <| kIntro st.newHyps irisGoal.goal <| + fun hyps goal => do match parsedAlts with + -- Remove the induction hypotheses from the regular Lean context | none => addBIGoal hyps goal ctor - -- Run the tactics supplied by the user, if available - | some tacticSeq => - let ⟨pf, newMVars, _⟩ ← addBIGoalRunTactics hyps goal ctor parsedAlts.tac tacticSeq - -- Throw an error if the first tactic already solves the goal - if let some [] := newMVars then - throwOrLogErrorAt stx - s!"iinduction: alternative `{ctor.getString!}` is not needed" - pure pf - -- Alternative names not found, acceptable only when `firstTactic` solves it - | none => - match parsedAlts.tac with - -- No first tactic given, the alternative name is missing - | none => throwMissingAlt ctor - | some firstTactic => - let m ← mkBIGoal hyps goal ctor - let subgoals ← evalTacticAt firstTactic m.mvarId! - -- First tactic supplied by the user, but it does not completely solve this case - if !subgoals.isEmpty then - throwOrLogErrorAt parsedAlts.stx - m!"iinduction: alternative `{ctor.getString!}` has not been provided" - pure m - - let pf' ← withoutFVars (u := 0) ihFVars.toArray <| k st.newHyps irisGoal.goal k' + | some parsedAlts => + -- Check if the alternative names for this constructor are supplied by the user + match parsedAlts.alts.find? (matcher ctor) <|> parsedAlts.wildcard with + | some ⟨_, _, tacticSeq, stx⟩ => + -- Generate the induction subgoal, label the induction subgoal with `ctor` + match tacticSeq with + | none => addBIGoal hyps goal ctor + -- Run the tactics supplied by the user, if available + | some tacticSeq => + let ⟨pf, newMVars, _⟩ ← + addBIGoalRunTactics hyps goal ctor parsedAlts.tac tacticSeq + -- Throw an error if the first tactic already solves the goal + if let some [] := newMVars then + throwOrLogErrorAt stx + s!"iinduction: alternative `{ctor.getString!}` is not needed" + pure pf + -- Alternative names not found, acceptable only when `firstTactic` solves it + | none => + match parsedAlts.tac with + -- No first tactic given, the alternative name is missing + | none => throwMissingAlt ctor + | some firstTactic => + let m ← mkBIGoal hyps goal ctor + let subgoals ← evalTacticAt firstTactic m.mvarId! + -- First tactic supplied by the user but does not completely solve this case + if !subgoals.isEmpty then + throwOrLogErrorAt parsedAlts.stx + m!"iinduction: alternative `{ctor.getString!}` has not been provided" + pure m -- Fill the metavariable for the induction subgoal generated by Lean s.mvarId.assign q($(st.pf).trans $pf') From 7df0c36ca214e389f8ca9a5416fb20eb51a18ccb Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Fri, 19 Jun 2026 11:23:14 +0200 Subject: [PATCH 145/181] Minor comment improvements --- Iris/Iris/ProofMode/Tactics/Induction.lean | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 78f7a3224..e4292ac7f 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -292,21 +292,20 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- Introduce the induction hypothesis back into the Iris proof state let st ← addIHs pfIntHyps irisGoal.hyps ihFVars - -- For pretty printing of arguments parsedAlts.forM fun parsedAlts => do - (parsedAlts.alts.find? (matcher ctor) <|> parsedAlts.wildcard).forM + parsedAlts.alts.find? (matcher ctor) <|> parsedAlts.wildcard |>.forM fun ⟨_, vars, _, stx⟩ => do + -- Check that the number of arguments matches what are needed if vars.size > s.fields.size then throwOrLogErrorAt stx <| s!"iinduction: too many variable names provided at alternative `{ctor}`: ".append s!"{vars.size} provided, but {s.fields.size} expected" - - -- For pretty printing of arguments + -- Label the arguments with their types, shown when the user hovers over them for ⟨fieldFVar, varStx⟩ in s.fields.toList.zip vars.toList do if let `(binderIdent| $id:ident) := varStx then let lctx ← getLCtx let fieldType ← inferType fieldFVar - addLocalVarInfo id lctx fieldFVar (some fieldType) true + addLocalVarInfo id lctx fieldFVar fieldType true let pf' ← withoutFVars (u := 0) ihFVars.toArray <| kIntro st.newHyps irisGoal.goal <| fun hyps goal => do match parsedAlts with From 2cbd1ad011823179d4c18ed231e2b42efaadca0f Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Fri, 19 Jun 2026 11:34:18 +0200 Subject: [PATCH 146/181] Refactor `addBIGoalRunTactics` --- Iris/Iris/ProofMode/ProofModeM.lean | 42 +++++++--------------- Iris/Iris/ProofMode/Tactics/Induction.lean | 4 +-- 2 files changed, 15 insertions(+), 31 deletions(-) diff --git a/Iris/Iris/ProofMode/ProofModeM.lean b/Iris/Iris/ProofMode/ProofModeM.lean index 668e4039b..9ce7ac093 100644 --- a/Iris/Iris/ProofMode/ProofModeM.lean +++ b/Iris/Iris/ProofMode/ProofModeM.lean @@ -1,7 +1,7 @@ /- Copyright (c) 2025 Michael Sammler. All rights reserved. Released under Apache 2.0 license as described in the file LICENSE. -Authors: Michael Sammler, Zongyuan Liu, Yunsong Yang +Authors: Michael Sammler, Zongyuan Liu, Yunsong Yang, Alvin Tang -/ module @@ -98,43 +98,27 @@ def addMVarGoal (m : MVarId) (name : Name := .anonymous) : ProofModeM Unit := do /-- Creates a new proof goal with the given hypotheses (`hyps`), conclusion - (`goal`). - - If `firstTactic` is not `none`, run the tactic to the new proof goal. - This tactic may lead to one or more new proof goals. This tactic should not - solve the goal, or else `tacticSeq` is known to be redundant. If so, - throw an error to notify the user of the redundancy. - - Then, run a sequence of tactics (`tacticSeq`) before adding the resultant - goals into the proof state. + (`goal`). Run a sequence of tactics with this proof goal and push all + subgoals into the proof state. The function returns: 1. the metavariable of the initial goal, - 2. the metavariables of subgoals after running the first tactic, and - 3. the metavariables of subgoals after running the remaining tactic sequences. + 2. a Boolean value indicating whether the `firstTactic` solves all goals, + `false` if `firstTactic` is `none`. -/ def addBIGoalRunTactics {prop : Q(Type u)} {bi : Q(BI $prop)} {e} (hyps : Hyps bi e) (goal : Q($prop)) (name : Name := .anonymous) (firstTactic : Option <| TSyntax `tactic) (tacticSeq : TSyntax `Lean.Parser.Tactic.tacticSeq) : - ProofModeM (Q($e ⊢ $goal) × Option (List MVarId) × List MVarId) := do + ProofModeM (Q($e ⊢ $goal) × Bool) := do let m ← mkBIGoal hyps goal name - - match firstTactic with - | none => - -- Run the user tactics on the newly generated goal - let subgoals ← evalTacticAt tacticSeq m.mvarId! - for s in subgoals do addMVarGoal s - pure (m, none, subgoals) - | some firstTactic => - let subgoals ← evalTacticAt firstTactic m.mvarId! - let mut subgoals' := [] - -- Run the user tactics on each newly generated goal after running `firstTactic` - for s in subgoals do - subgoals' := subgoals'.append <| ← evalTacticAt tacticSeq s - -- Add the result proof subgoals into the proof state - for s' in subgoals' do addMVarGoal s' - pure (m, subgoals, subgoals') + -- Run `firstTactic`, if available + let subgoals ← firstTactic.elim (pure [m.mvarId!]) (evalTacticAt · m.mvarId!) + -- Run the user tactics on each newly generated goal after running `firstTactic` + let mut subgoals' := [] + for s in subgoals do + subgoals' := subgoals'.append <| ← evalTacticAt tacticSeq s + pure (m, firstTactic.isSome && subgoals.isEmpty) /-- Try to synthesize a typeclass instance, adding any created metavariables as proof mode goals. -/ def ProofModeM.trySynthInstanceQ (α : Q(Sort v)) : ProofModeM (Option Q($α)) := do diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index e4292ac7f..d9048dee1 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -320,10 +320,10 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} | none => addBIGoal hyps goal ctor -- Run the tactics supplied by the user, if available | some tacticSeq => - let ⟨pf, newMVars, _⟩ ← + let ⟨pf, allSolved⟩ ← addBIGoalRunTactics hyps goal ctor parsedAlts.tac tacticSeq -- Throw an error if the first tactic already solves the goal - if let some [] := newMVars then + if allSolved then throwOrLogErrorAt stx s!"iinduction: alternative `{ctor.getString!}` is not needed" pure pf From 50ee146f8aaa0492950abb27cf8aeebb038296eb Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Fri, 19 Jun 2026 13:04:17 +0200 Subject: [PATCH 147/181] Towards a one-liner `findIHs` --- Iris/Iris/ProofMode/Tactics/Induction.lean | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index d9048dee1..52ba06120 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -143,14 +143,9 @@ private def iHypsToGeneralize {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} /-- Search for induction hypotheses in the pure Lean context -/ private def findIHs (m : MVarId) : ProofModeM (List FVarId) := m.withContext do - let lctx ← getLCtx - let mut ihs := [] - for decl in lctx do - let type ← instantiateMVars decl.type - -- The hypothesis is an IH when it is an Iris entailment - if containsIrisGoal type.getForallBody then - ihs := decl.fvarId :: ihs - return ihs.reverse + return (← (← getLCtx).decls.toList.reduceOption.filterMapM fun decl => do + pure <| if containsIrisGoal (← instantiateMVars decl.type).getForallBody then + some decl.fvarId else none).reverse /-- Introduce into the intuitionistic context of the Iris proof state. -/ private def addIHs {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e : Q($prop)} From 7afbc84554e3d74b3c4c430c5c4bae64c7088bee Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Fri, 19 Jun 2026 13:54:16 +0200 Subject: [PATCH 148/181] Move `findIHs` inline and further condensation --- Iris/Iris/ProofMode/Tactics/Induction.lean | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 52ba06120..ac1552e32 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -140,13 +140,6 @@ private def iHypsToGeneralize {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} return (ivars ++ hyps.spatialIVarIds).map ({ kind := .ipm ·, explicit := false }) -/-- Search for induction hypotheses in the pure Lean context -/ -private def findIHs (m : MVarId) : ProofModeM (List FVarId) := - m.withContext do - return (← (← getLCtx).decls.toList.reduceOption.filterMapM fun decl => do - pure <| if containsIrisGoal (← instantiateMVars decl.type).getForallBody then - some decl.fvarId else none).reverse - /-- Introduce into the intuitionistic context of the Iris proof state. -/ private def addIHs {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e : Q($prop)} (pfIntHyps : Q($e ⊢ □ $e)) (hyps : Hyps bi e) (ihFVars : List FVarId) : @@ -278,7 +271,9 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} | throwOrLogError "iinduction: fail to parse induction subgoal" -- Find the induction hypotheses generated by Lean's `induction` tactic - let ihFVars ← findIHs s.mvarId + let ihFVars ← s.mvarId.withContext do + return (← (← getLCtx).decls.toList.reduceOption.filterM + (fun decl => do return containsIrisGoal (← instantiateMVars decl.type).getForallBody)).map (·.fvarId) -- A proof that all hypotheses in the Iris goal exist in the intuitionistic context let some pfIntHyps := irisGoal.hyps.buildIntuitionisticProof From 2943d7a4813719d919eedcbbbf9659772dfdce28 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Fri, 19 Jun 2026 14:04:56 +0200 Subject: [PATCH 149/181] Further condense `ihFVars` --- Iris/Iris/ProofMode/Tactics/Induction.lean | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index ac1552e32..cf422d139 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -271,9 +271,9 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} | throwOrLogError "iinduction: fail to parse induction subgoal" -- Find the induction hypotheses generated by Lean's `induction` tactic - let ihFVars ← s.mvarId.withContext do - return (← (← getLCtx).decls.toList.reduceOption.filterM - (fun decl => do return containsIrisGoal (← instantiateMVars decl.type).getForallBody)).map (·.fvarId) + let ihFVars ← s.mvarId.withContext <| getLCtx >>= fun lctx => + (·.map (·.fvarId)) <$> lctx.decls.toList.reduceOption.filterM + ((containsIrisGoal ∘ (·.getForallBody)) <$> instantiateMVars ·.type) -- A proof that all hypotheses in the Iris goal exist in the intuitionistic context let some pfIntHyps := irisGoal.hyps.buildIntuitionisticProof From 572f50a7d03da90ca2260b0a972a6a3dafa2060f Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Fri, 19 Jun 2026 14:40:01 +0200 Subject: [PATCH 150/181] Minor simplifications regarding monadic operations --- Iris/Iris/ProofMode/Tactics/Induction.lean | 31 +++++++++------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index cf422d139..092ff8e33 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -206,15 +206,9 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} | .const indName _ => match (← getEnv).find? indName with | some (.inductInfo _) => - let recName ← - match altRecName, ← getCustomEliminator? #[mkFVar fvar] true with - -- Use the user-supplied recursor name if available - | some altRecName, _ => pure altRecName - -- Use the default recursor name if available - | none, some r => pure r - -- Use `.rec` as the fallback option - | none, none => pure <| mkRecName indName - pure recName + altRecName.elim + (getCustomEliminator? #[mkFVar fvar] true <&> (·.getD <| mkRecName indName)) + pure | _ => throwError "iinduction: {indName} is not inductive" | _ => throwError "iinduction: unable to determine inductive type" @@ -240,9 +234,9 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} match parsedAlts.alts.find? (matcher ctor) <|> parsedAlts.wildcard with | some alt => pure { explicit := true, varNames := alt.vars.toList.map <| - (match ·.raw with + fun stx => match stx.raw with | `(binderIdent| $id:ident) => id.getId - | _ => Name.mkSimple "_") } + | _ => Name.mkSimple "_" } | none => -- Defer the check if the first tactic for the `with` syntax is given if parsedAlts.tac.isSome then @@ -264,16 +258,17 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} s.mvarId.withContext do s.mvarId.setTag ctor -- Obtain the type of the induction subgoal - let sType ← instantiateMVars <| ← s.mvarId.getType + let sType ← s.mvarId.getType >>= instantiateMVars let some irisGoal := parseIrisGoal? sType -- This should not happen | throwOrLogError "iinduction: fail to parse induction subgoal" -- Find the induction hypotheses generated by Lean's `induction` tactic - let ihFVars ← s.mvarId.withContext <| getLCtx >>= fun lctx => - (·.map (·.fvarId)) <$> lctx.decls.toList.reduceOption.filterM - ((containsIrisGoal ∘ (·.getForallBody)) <$> instantiateMVars ·.type) + let ihFVars ← s.mvarId.withContext getLCtx >>= fun lctx => + lctx.decls.toList.reduceOption.filterM + (instantiateMVars ·.type <&> (containsIrisGoal ∘ (·.getForallBody))) <&> + (·.map (·.fvarId)) -- A proof that all hypotheses in the Iris goal exist in the intuitionistic context let some pfIntHyps := irisGoal.hyps.buildIntuitionisticProof @@ -291,7 +286,7 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} s!"iinduction: too many variable names provided at alternative `{ctor}`: ".append s!"{vars.size} provided, but {s.fields.size} expected" -- Label the arguments with their types, shown when the user hovers over them - for ⟨fieldFVar, varStx⟩ in s.fields.toList.zip vars.toList do + for ⟨fieldFVar, varStx⟩ in s.fields.zip vars do if let `(binderIdent| $id:ident) := varStx then let lctx ← getLCtx let fieldType ← inferType fieldFVar @@ -407,9 +402,7 @@ elab_rules : tactic let recName := r.map (·.getId) -- Parse the list of alternative names supplied by the user - let parsedAlts ← match alts with - | none => pure none - | some alts => parseInductionAlts alts + let parsedAlts ← alts.mapM parseInductionAlts ProofModeM.runTactic λ mvar { hyps, goal, .. } => do let genSelTargets ← From fc2239a96f5d9a92aacaa438a8d496e6f1075368 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Fri, 19 Jun 2026 14:41:57 +0200 Subject: [PATCH 151/181] Minor simplifications --- Iris/Iris/ProofMode/Tactics/Induction.lean | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 092ff8e33..fcb5a0a4b 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -201,7 +201,7 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} pure <| explicitPureTargets ++ implicitIrisTargets ++ explicitIrisTargets -- Find the recursor name and constructor names of the inductive datatype - let fvarType ← whnf <| ← inferType <| mkFVar fvar + let fvarType ← liftM <| (inferType <| mkFVar fvar) >>= whnf let recName ← match fvarType.getAppFn with | .const indName _ => match (← getEnv).find? indName with @@ -250,7 +250,7 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} let m ← mkBIGoal hyps' goal' -- Use built-in induction in Lean to generate the subgoals for induction - let subgoals ← m.mvarId!.withContext do + let subgoals ← m.mvarId!.withContext do m.mvarId!.induction fvar recName varNames -- Handle each subgoal generated by Lean's induction @@ -265,10 +265,10 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} | throwOrLogError "iinduction: fail to parse induction subgoal" -- Find the induction hypotheses generated by Lean's `induction` tactic - let ihFVars ← s.mvarId.withContext getLCtx >>= fun lctx => - lctx.decls.toList.reduceOption.filterM + let ihFVars ← s.mvarId.withContext getLCtx >>= + (·.decls.toList.reduceOption.filterM (instantiateMVars ·.type <&> (containsIrisGoal ∘ (·.getForallBody))) <&> - (·.map (·.fvarId)) + (·.map (·.fvarId))) -- A proof that all hypotheses in the Iris goal exist in the intuitionistic context let some pfIntHyps := irisGoal.hyps.buildIntuitionisticProof From 89a82269bb9f6f03d4f257031f3f5c913e4a3c95 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Fri, 19 Jun 2026 15:16:23 +0200 Subject: [PATCH 152/181] Eliminate `containsIrisGoal` by moving it inline --- Iris/Iris/ProofMode/Expr.lean | 12 ------------ Iris/Iris/ProofMode/Tactics/Induction.lean | 2 +- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/Iris/Iris/ProofMode/Expr.lean b/Iris/Iris/ProofMode/Expr.lean index e969ed138..e02b9ab2b 100644 --- a/Iris/Iris/ProofMode/Expr.lean +++ b/Iris/Iris/ProofMode/Expr.lean @@ -485,18 +485,6 @@ structure IrisGoal where def isIrisGoal (expr : Expr) : Bool := isAppOfArity expr ``Entails' 4 -/-- Recursively expression traversal to check whether it contains an Iris entailment -/ -def containsIrisGoal (e : Expr) : Bool := - isIrisGoal e || - match e with - | .app f a => containsIrisGoal f || containsIrisGoal a - | .lam _ d b _ => containsIrisGoal d || containsIrisGoal b - | .forallE _ d b _ => containsIrisGoal d || containsIrisGoal b - | .letE _ t v b _ => containsIrisGoal t || containsIrisGoal v || containsIrisGoal b - | .mdata _ inner => containsIrisGoal inner - | .proj _ _ inner => containsIrisGoal inner - | _ => false - def parseIrisGoal? (expr : Expr) : Option IrisGoal := do -- remove top-level metadata when matching on the goal let expr := expr.consumeMData diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index fcb5a0a4b..ad93d51ed 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -267,7 +267,7 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} -- Find the induction hypotheses generated by Lean's `induction` tactic let ihFVars ← s.mvarId.withContext getLCtx >>= (·.decls.toList.reduceOption.filterM - (instantiateMVars ·.type <&> (containsIrisGoal ∘ (·.getForallBody))) <&> + (instantiateMVars ·.type <&> (·.find? isIrisGoal |>.isSome)) <&> (·.map (·.fvarId))) -- A proof that all hypotheses in the Iris goal exist in the intuitionistic context From e54087d24856b8afb0b471016ed867ed0a419eb3 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Fri, 19 Jun 2026 16:57:52 +0200 Subject: [PATCH 153/181] Always require manual resolution of hypotheses dependent on the induction target --- Iris/Iris/ProofMode/Tactics/Induction.lean | 46 +++++++--------------- Iris/Iris/ProofMode/Tactics/Loeb.lean | 2 +- Iris/Iris/ProofMode/Tactics/Revert.lean | 35 ++++++++++------ Iris/Iris/Tests/Tactics.lean | 26 ++++++++++-- 4 files changed, 61 insertions(+), 48 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index ad93d51ed..1692efce8 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -124,22 +124,6 @@ private structure InductionState {u} {prop : Q(Type u)} {bi} (origE : Q($prop)) (newHyps : Hyps bi newE) (pf : Q($origE ⊢ $newE)) -/-- - Finds spatial hypotheses in the spatial context as well as the intuitionistic - hypotheses that depend on `fvar`. --/ -private def iHypsToGeneralize {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} - (hyps : Hyps bi e) (fvar : FVarId) : ProofModeM (List SelTarget) := do - let fwdDeps ← collectForwardDeps #[mkFVar fvar] false - let dependentFVars := fwdDeps.map (·.fvarId!) - - let ivars := hyps.intuitionisticIVarIds.filter <| fun ivar => - match hyps.getDecl? ivar with - | some ⟨_, _, _, ty⟩ => dependentFVars.any (ty.containsFVar ·) - | none => false - - return (ivars ++ hyps.spatialIVarIds).map ({ kind := .ipm ·, explicit := false }) - /-- Introduce into the intuitionistic context of the Iris proof state. -/ private def addIHs {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e : Q($prop)} (pfIntHyps : Q($e ⊢ □ $e)) (hyps : Hyps bi e) (ihFVars : List FVarId) : @@ -192,13 +176,8 @@ private def iInductionCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} (hyps : Hyps bi e) (goal : Q($prop)) (fvar : FVarId) (parsedAlts : Option Alts) (altRecName : Option Name) (genSelTargets : List SelTarget) : ProofModeM Q($e ⊢ $goal) := do - -- Find the regular pure/Iris hypotheses to be reverted - let implicitIrisTargets ← iHypsToGeneralize hyps fvar - let targets ← do - let explicitIrisTargets := genSelTargets.filter (match ·.kind with | .ipm _ => true | _ => false) - let explicitPureTargets := genSelTargets.filter (match ·.kind with | .pure _ => true | _ => false) - let implicitIrisTargets := implicitIrisTargets.filter (not <| (explicitIrisTargets.map (·.kind)).contains ·.kind) - pure <| explicitPureTargets ++ implicitIrisTargets ++ explicitIrisTargets + let targets := genSelTargets ++ + (hyps.spatialIVarIds.map ({ kind := .ipm ·, explicit := false })).filter (not <| (genSelTargets.map (·.kind)).contains ·.kind) -- Find the recursor name and constructor names of the inductive datatype let fvarType ← liftM <| (inferType <| mkFVar fvar) >>= whnf @@ -405,15 +384,18 @@ elab_rules : tactic let parsedAlts ← alts.mapM parseInductionAlts ProofModeM.runTactic λ mvar { hyps, goal, .. } => do - let genSelTargets ← - genSelPats.elim (pure []) fun genSelPats => do - -- Parse the selection patterns for generalising hypotheses - let parsedGenSelPats ← liftMacroM <| SelPat.parse genSelPats - let genSelTargets ← SelPat.resolve hyps parsedGenSelPats - -- Check for dependencies with the hypotheses in the selection targets - checkDependentHyps "iinduction" hyps genSelTargets genSelPats - fun newPats => `(tactic| iinduction $x $[using $r]? generalizing $newPats* $[$alts]?) - pure genSelTargets + let mkTactic := fun newPats => `(tactic| iinduction $x $[using $r]? generalizing $newPats* $[$alts]?) + let genSelTargets ← do match genSelPats with + | none => + checkDependentHyps "iinduction" hyps [] fvar #[] mkTactic + pure [] + | some genSelPats => + -- Parse the selection patterns for generalising hypotheses + let parsedGenSelPats ← liftMacroM <| SelPat.parse genSelPats + let genSelTargets ← SelPat.resolve hyps parsedGenSelPats + -- Check for dependencies with the hypotheses in the selection targets + checkDependentHyps "iinduction" hyps genSelTargets fvar genSelPats mkTactic + pure genSelTargets let pf ← iInductionCore hyps goal fvar parsedAlts recName genSelTargets mvar.assign pf diff --git a/Iris/Iris/ProofMode/Tactics/Loeb.lean b/Iris/Iris/ProofMode/Tactics/Loeb.lean index db7991fd4..2e8ef632d 100644 --- a/Iris/Iris/ProofMode/Tactics/Loeb.lean +++ b/Iris/Iris/ProofMode/Tactics/Loeb.lean @@ -28,7 +28,7 @@ elab_rules : tactic let pats ← Elab.liftMacroM <| SelPat.parse hs ProofModeM.runTactic fun mvid {hyps, goal, ..} => do let targets : List SelTarget ← SelPat.resolve hyps (pats ++ [.spatial]) - checkDependentHyps "iloeb" hyps targets hs + checkDependentHyps "iloeb" hyps targets none hs (fun newPats => `(tactic| iloeb as $IH generalizing $newPats*)) let expr ← iRevertIntro hyps goal targets fun {prop _ _} hyps goal k => do let some _ ← ProofModeM.trySynthInstanceQ q(BI.BILoeb $prop) diff --git a/Iris/Iris/ProofMode/Tactics/Revert.lean b/Iris/Iris/ProofMode/Tactics/Revert.lean index b757b14c4..244cee97e 100644 --- a/Iris/Iris/ProofMode/Tactics/Revert.lean +++ b/Iris/Iris/ProofMode/Tactics/Revert.lean @@ -104,14 +104,18 @@ private def RevertState.revertLeanHyp in `explicitTargets` but are not themselves in the list. -/ def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} - (tacticName : String) (hyps : Hyps bi e) (explicitTargets : List SelTarget) + (tacticName : String) (hyps : Hyps bi e) + (explicitTargets : List SelTarget) + (inductionTarget : Option FVarId) (selPats : TSyntaxArray `selPat) (mkTactic : TSyntaxArray `selPat → ProofModeM (TSyntax `tactic)): ProofModeM Unit := do let explicitIrisIVarIds := explicitTargets.filterMap (match ·.kind with | .ipm ivar => some ivar | _ => none) - let explicitPureFVars := explicitTargets.filterMap - (match ·.kind with | .pure f => some f | _ => none) + let explicitPureFVars := + explicitTargets.filterMap + (match ·.kind with | .pure f => some f | _ => none) |>.append <| + inductionTarget.elim [] (.singleton ·) -- Pairs of `FVarId` values `(f1, f2)` indicating `f1` depends on `f2`. let mut missingPure : List (FVarId × FVarId) := [] @@ -132,23 +136,32 @@ def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} -- Check forward dependency of Iris hypotheses let missingIris : List (Name × FVarId) := - hyps.intuitionisticIVarIds.filterMap fun ivar => - if explicitIrisIVarIds.contains ivar then none - else hyps.getDecl? ivar >>= fun ⟨name, _, _, ty⟩ => - (allPureFVars.find? (ty.containsFVar ·)).map (name, ·) + hyps.intuitionisticIVarIds.filterMap fun ivar => + if explicitIrisIVarIds.contains ivar then none + else hyps.getDecl? ivar >>= fun ⟨name, _, _, ty⟩ => + (allPureFVars.find? (ty.containsFVar ·)).map (name, ·) -- Add an error message if there exists some pure/Lean hypotheses that should also be generalised if !missingPure.isEmpty || !missingIris.isEmpty then let leanLines ← missingPure.mapM fun (depId, srcId) => do let depDecl ← depId.getDecl let srcDecl ← srcId.getDecl - return s!"• Lean hypothesis `{depDecl.userName}` depends on `{srcDecl.userName}`" + let srcName := "`" ++ srcDecl.userName.toString ++ "`" + let srcName := inductionTarget.elim srcName + (if · == srcId then "the induction target" else srcName) + return s!"• Lean hypothesis `{depDecl.userName}` depends on {srcName}" let irisLines ← missingIris.mapM fun (name, srcId) => do let srcDecl ← srcId.getDecl - return s!"• Iris hypothesis in the intuitionstic context `{name}` depends on `{srcDecl.userName}`" + let srcName := "`" ++ srcDecl.userName.toString ++ "`" + let srcName := inductionTarget.elim srcName + (if · == srcId then "the induction target" else srcName) + return s!"• Iris hypothesis in the intuitionstic context `{name}` depends on {srcName}" + + let allPureFVars := allPureFVars.eraseDups.filter <| + fun fvar => inductionTarget.all (fvar != ·) -- Sort the pure Lean hypothesis according to the dependency - let allPureFVarsSorted := (← getLCtx).sortFVarsByContextOrder allPureFVars.eraseDups.toArray + let allPureFVarsSorted ← getLCtx <&> (·.sortFVarsByContextOrder allPureFVars.toArray) let sortedPurePats : Array (TSyntax `selPat) ← allPureFVarsSorted.mapM fun fvarId => do let decl ← fvarId.getDecl let id := mkIdent <| .mkSimple decl.userName.toString @@ -197,7 +210,7 @@ elab_rules : tactic | `(tactic| irevert $pats:selPat*) => do ProofModeM.runTactic fun mvar { hyps, goal, .. } => do let targets ← SelPat.resolve hyps parsedPats - checkDependentHyps "irevert" hyps targets pats + checkDependentHyps "irevert" hyps targets none pats fun newPats => `(tactic| irevert $newPats*) let expr ← iRevertCore targets hyps goal mvar.assign expr diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 8e26b5aef..f7c81df01 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2887,7 +2887,7 @@ example [BI PROP] {α} {t : NTree α} : ⊢@{PROP} ⌜t.id = t⌝ := by iinduction t with simp [NTree.id] | h_leaf => itrivial | h_node x ts IH1 => - iinduction ts with simp + iinduction ts generalizing IH1 with simp | nil => itrivial | cons t ts IH2 => isplit @@ -3018,7 +3018,7 @@ example [BI PROP] {n : Nat} : example [BI PROP] {P R S : PROP} {Q T : Nat → PROP} {m n : Nat} : ⊢ P -∗ □ Q m -∗ □ R -∗ S -∗ □ T (n + m) -∗ ⌜n + m + 0 = n + m⌝ := by iintro HP #HQ #HR HS #HT - iinduction n + m using Nat.caseStrongRecOn generalizing %m HQ with + iinduction n + m using Nat.caseStrongRecOn generalizing %m HQ HT with | zero | ind _ _ => itrivial /- @@ -3078,13 +3078,14 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : - *regular hypotheses* `h1 : T m` and `U1 : (T m) → Prop` depend on `m`; - *regular hypotheses* `h2 : U1 h1` and `U2 : (U1 h1) → PROP` depends on `h1`, which in turn depends on `m`; - - *Iris hypotheses* `□HQ : Q m` and `□HR : R m` depends on `m`; + - *Iris hypotheses* `□HQ : Q m` and `□HR : R m` depend on `m`; + - *Iris hypothesis* `□HS : S n` depends on the induction target `n`; - *Iris hypothesis* `□HU2 : U2 h2` depends on `h2` and `U2`, which depends depend on `h1`, which in turn depends on `m`. This requires manual resolution. -/ /-- info: Try this: - [apply] iinduction n generalizing %m %h1 %U1 %h2 %U2 HQ HR HU2 with + [apply] iinduction n generalizing %m %h1 %U1 %h2 %U2 HQ HR HS HU2 with | zero | succ n IH => itrivial --- @@ -3095,6 +3096,7 @@ error: iinduction: The following hypotheses depend on variables in the `generali • Lean hypothesis `U2` depends on `m` • Iris hypothesis in the intuitionstic context `HQ` depends on `m` • Iris hypothesis in the intuitionstic context `HR` depends on `m` +• Iris hypothesis in the intuitionstic context `HS` depends on the induction target • Iris hypothesis in the intuitionstic context `HU2` depends on `h2` -/ #guard_msgs in example [BI PROP] {P : PROP} {m n : Nat} {Q R S : Nat → PROP} {T : Nat → Prop} @@ -3105,4 +3107,20 @@ example [BI PROP] {P : PROP} {m n : Nat} {Q R S : Nat → PROP} {T : Nat → Pro | zero | succ n IH => itrivial +/- + Tests `iinduction` for hypotheses dependent on the induction target + when `generalizing` is not used. +-/ +/-- info: Try this: + [apply] iinduction ts generalizing IH1 with simp +--- +error: iinduction: The following hypotheses depend on variables in the `generalizing` clause but are not themselves included: +• Iris hypothesis in the intuitionstic context `IH1` depends on the induction target -/ +#guard_msgs in +example [BI PROP] {α} {t : NTree α} : ⊢@{PROP} ⌜t.id = t⌝ := by + iinduction t with simp [NTree.id] + | h_leaf => itrivial + | h_node x ts IH1 => + iinduction ts with simp + end iinduction From 93d858552fea09a4f441df9ff491beec5d5ea975 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Fri, 19 Jun 2026 17:02:06 +0200 Subject: [PATCH 154/181] Update comments --- Iris/Iris/ProofMode/Tactics/Induction.lean | 16 ++++++++-------- Iris/Iris/ProofMode/Tactics/Revert.lean | 3 +++ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 1692efce8..b6baaa551 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -332,8 +332,8 @@ private def generalizeTermWithFVar (x : TSyntax `term) : TacticM FVarId := do way as the `induction` tactic. Given an expression `e`, the application of `iinduction e` performs the following steps: 1. Generalises the expression `e` using `Lean.Meta.Tactic.Generalize`. - 2. Reverts all hypotheses in the spatial context, as well as those in the - intuitionistic context that depend on the induction target. + 2. Reverts all hypotheses in the spatial context, as well as those specified + in the `generalizing` clause. 3. Obtain the induction subgoals. 4. Moves all induction hypotheses into the intuitionstic context. 5. Introduce hypotheses reverted in steps 2 and 3 back into the Iris contexts. @@ -356,19 +356,19 @@ private def generalizeTermWithFVar (x : TSyntax `term) : TacticM FVarId := do □HT : T n ``` - By applying `iinduction n`, all spatial hypotheses (`HP` and `HS`) are - reverted. The hypothesis `HT` is also reverted because it involves the - induction target `n`. + By applying `iinduction n generalizing HT`, all spatial hypotheses + (`HP` and `HS`) are reverted. The hypothesis `HT` must also be reverted + because it involves the induction target `n`. - By applying `iinduction n generalizing HQ`, the hypotheses `HQ` + By applying `iinduction n generalizing HQ HT`, the hypotheses `HQ` are additionally reverted and thus included as a wand premise in the induction hypothesis. One can also generalise pure variables in the regular Lean context. If there exists some pure/Iris hypotheses that is forward-dependent, they should also be included in the `generalizing` clause. In the example above, - instead of `iinduction n generalizing %m`, one should use - `iinduction n generalizing %m HQ`. + instead of `iinduction n generalizing %m HT`, one should use + `iinduction n generalizing %m HQ HT`. -/ elab_rules : tactic | `(tactic| iinduction $x diff --git a/Iris/Iris/ProofMode/Tactics/Revert.lean b/Iris/Iris/ProofMode/Tactics/Revert.lean index 244cee97e..234f40f9b 100644 --- a/Iris/Iris/ProofMode/Tactics/Revert.lean +++ b/Iris/Iris/ProofMode/Tactics/Revert.lean @@ -102,6 +102,9 @@ private def RevertState.revertLeanHyp /-- Throw an error if there exists hypotheses that are depend on any hypothesis in `explicitTargets` but are not themselves in the list. + + The value `inductionTarget` can optionally be supplied. In this case, + hypotheses dependent on it should also be generalised. -/ def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} (tacticName : String) (hyps : Hyps bi e) From d4073c4857194e44bcfe5869cb371695dc75a01b Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Fri, 19 Jun 2026 22:45:04 +0200 Subject: [PATCH 155/181] Typo fix --- Iris/Iris/ProofMode/Tactics/Revert.lean | 2 +- Iris/Iris/Tests/Tactics.lean | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Revert.lean b/Iris/Iris/ProofMode/Tactics/Revert.lean index 234f40f9b..ffffc48e1 100644 --- a/Iris/Iris/ProofMode/Tactics/Revert.lean +++ b/Iris/Iris/ProofMode/Tactics/Revert.lean @@ -158,7 +158,7 @@ def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} let srcName := "`" ++ srcDecl.userName.toString ++ "`" let srcName := inductionTarget.elim srcName (if · == srcId then "the induction target" else srcName) - return s!"• Iris hypothesis in the intuitionstic context `{name}` depends on {srcName}" + return s!"• Iris hypothesis in the intuitionistic context `{name}` depends on {srcName}" let allPureFVars := allPureFVars.eraseDups.filter <| fun fvar => inductionTarget.all (fvar != ·) diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index f7c81df01..a6d10aa36 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2791,7 +2791,7 @@ info: Try this: error: iloeb: The following hypotheses depend on variables in the `generalizing` clause but are not themselves included: • Lean hypothesis `h1` depends on `n` • Lean hypothesis `U` depends on `n` -• Iris hypothesis in the intuitionstic context `HT` depends on `n` +• Iris hypothesis in the intuitionistic context `HT` depends on `n` -/ #guard_msgs in example [BI PROP] [BILoeb PROP] {n : Nat} {P T : Nat → PROP} {Q : Nat → Prop} @@ -3094,10 +3094,10 @@ error: iinduction: The following hypotheses depend on variables in the `generali • Lean hypothesis `U1` depends on `m` • Lean hypothesis `h2` depends on `m` • Lean hypothesis `U2` depends on `m` -• Iris hypothesis in the intuitionstic context `HQ` depends on `m` -• Iris hypothesis in the intuitionstic context `HR` depends on `m` -• Iris hypothesis in the intuitionstic context `HS` depends on the induction target -• Iris hypothesis in the intuitionstic context `HU2` depends on `h2` -/ +• Iris hypothesis in the intuitionistic context `HQ` depends on `m` +• Iris hypothesis in the intuitionistic context `HR` depends on `m` +• Iris hypothesis in the intuitionistic context `HS` depends on the induction target +• Iris hypothesis in the intuitionistic context `HU2` depends on `h2` -/ #guard_msgs in example [BI PROP] {P : PROP} {m n : Nat} {Q R S : Nat → PROP} {T : Nat → Prop} {h1 : T m} {U1 : (T m) → Prop} {h2 : U1 h1} {U2 : (U1 h1) → PROP} : @@ -3115,7 +3115,7 @@ example [BI PROP] {P : PROP} {m n : Nat} {Q R S : Nat → PROP} {T : Nat → Pro [apply] iinduction ts generalizing IH1 with simp --- error: iinduction: The following hypotheses depend on variables in the `generalizing` clause but are not themselves included: -• Iris hypothesis in the intuitionstic context `IH1` depends on the induction target -/ +• Iris hypothesis in the intuitionistic context `IH1` depends on the induction target -/ #guard_msgs in example [BI PROP] {α} {t : NTree α} : ⊢@{PROP} ⌜t.id = t⌝ := by iinduction t with simp [NTree.id] From 27d03ebf0ad9e2b03a87491f7246bc98a6633ea1 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 20 Jun 2026 15:02:48 +0200 Subject: [PATCH 156/181] Refactor code in `Revert.lean`: introduce `getDependentHyps` --- Iris/Iris/ProofMode/Tactics/Revert.lean | 48 +++++++++++++++---------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Revert.lean b/Iris/Iris/ProofMode/Tactics/Revert.lean index ffffc48e1..5d7c7dc8a 100644 --- a/Iris/Iris/ProofMode/Tactics/Revert.lean +++ b/Iris/Iris/ProofMode/Tactics/Revert.lean @@ -99,20 +99,10 @@ private def RevertState.revertLeanHyp else st.revertLeanForallHyp f α -/-- - Throw an error if there exists hypotheses that are depend on any hypothesis - in `explicitTargets` but are not themselves in the list. - - The value `inductionTarget` can optionally be supplied. In this case, - hypotheses dependent on it should also be generalised. --/ -def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} - (tacticName : String) (hyps : Hyps bi e) - (explicitTargets : List SelTarget) - (inductionTarget : Option FVarId) - (selPats : TSyntaxArray `selPat) - (mkTactic : TSyntaxArray `selPat → ProofModeM (TSyntax `tactic)): - ProofModeM Unit := do +def getDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} + (hyps : Hyps bi e) + (explicitTargets : List SelTarget) (inductionTarget : Option FVarId) : + ProofModeM <| List (FVarId × FVarId) × List (Name × FVarId) × Array FVarId := do let explicitIrisIVarIds := explicitTargets.filterMap (match ·.kind with | .ipm ivar => some ivar | _ => none) let explicitPureFVars := @@ -144,6 +134,31 @@ def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} else hyps.getDecl? ivar >>= fun ⟨name, _, _, ty⟩ => (allPureFVars.find? (ty.containsFVar ·)).map (name, ·) + let allPureFVars := allPureFVars.eraseDups.filter <| + fun fvar => inductionTarget.all (fvar != ·) + + -- Sort the pure Lean hypothesis according to the dependency + let allPureFVarsSorted ← getLCtx <&> (·.sortFVarsByContextOrder allPureFVars.toArray) + + return ⟨missingPure, missingIris, allPureFVarsSorted⟩ + +/-- + Throw an error if there exists hypotheses that are depend on any hypothesis + in `explicitTargets` but are not themselves in the list. + + The value `inductionTarget` can optionally be supplied. In this case, + hypotheses dependent on it should also be generalised. +-/ +def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} + (tacticName : String) (hyps : Hyps bi e) + (explicitTargets : List SelTarget) + (inductionTarget : Option FVarId) + (selPats : TSyntaxArray `selPat) + (mkTactic : TSyntaxArray `selPat → ProofModeM (TSyntax `tactic)): + ProofModeM Unit := do + let ⟨missingPure, missingIris, allPureFVarsSorted⟩ ← + getDependentHyps hyps explicitTargets inductionTarget + -- Add an error message if there exists some pure/Lean hypotheses that should also be generalised if !missingPure.isEmpty || !missingIris.isEmpty then let leanLines ← missingPure.mapM fun (depId, srcId) => do @@ -160,11 +175,6 @@ def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} (if · == srcId then "the induction target" else srcName) return s!"• Iris hypothesis in the intuitionistic context `{name}` depends on {srcName}" - let allPureFVars := allPureFVars.eraseDups.filter <| - fun fvar => inductionTarget.all (fvar != ·) - - -- Sort the pure Lean hypothesis according to the dependency - let allPureFVarsSorted ← getLCtx <&> (·.sortFVarsByContextOrder allPureFVars.toArray) let sortedPurePats : Array (TSyntax `selPat) ← allPureFVarsSorted.mapM fun fvarId => do let decl ← fvarId.getDecl let id := mkIdent <| .mkSimple decl.userName.toString From c73bcbb19345f5a80b8b6fc417484764487c307b Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 20 Jun 2026 15:28:05 +0200 Subject: [PATCH 157/181] New function `getCompleteSelTargets` for the fixed selection targets --- Iris/Iris/ProofMode/Tactics/Revert.lean | 39 ++++++++++++++++--------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Revert.lean b/Iris/Iris/ProofMode/Tactics/Revert.lean index 5d7c7dc8a..5b753bafd 100644 --- a/Iris/Iris/ProofMode/Tactics/Revert.lean +++ b/Iris/Iris/ProofMode/Tactics/Revert.lean @@ -102,7 +102,7 @@ private def RevertState.revertLeanHyp def getDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} (hyps : Hyps bi e) (explicitTargets : List SelTarget) (inductionTarget : Option FVarId) : - ProofModeM <| List (FVarId × FVarId) × List (Name × FVarId) × Array FVarId := do + ProofModeM <| List (FVarId × FVarId) × List (Name × IVarId × FVarId) × Array FVarId := do let explicitIrisIVarIds := explicitTargets.filterMap (match ·.kind with | .ipm ivar => some ivar | _ => none) let explicitPureFVars := @@ -111,7 +111,7 @@ def getDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} inductionTarget.elim [] (.singleton ·) -- Pairs of `FVarId` values `(f1, f2)` indicating `f1` depends on `f2`. - let mut missingPure : List (FVarId × FVarId) := [] + let mut missingPureHyps : List (FVarId × FVarId) := [] -- Check forward dependency of pure hypotheses for fvar in explicitPureFVars.eraseDups do @@ -121,18 +121,18 @@ def getDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} -- Skip `x` itself and variables already included in `explicitPureFVars` if depId != fvar && !explicitPureFVars.contains depId then -- Record the missing hypothesis to be generalised, if not already included - if !missingPure.any (·.fst == depId) then - missingPure := missingPure.cons (depId, fvar) - missingPure := missingPure.reverse + if !missingPureHyps.any (·.fst == depId) then + missingPureHyps := missingPureHyps.cons (depId, fvar) + missingPureHyps := missingPureHyps.reverse - let allPureFVars := explicitPureFVars ++ missingPure.map (·.fst) + let allPureFVars := explicitPureFVars ++ missingPureHyps.map (·.fst) -- Check forward dependency of Iris hypotheses - let missingIris : List (Name × FVarId) := + let missingIrisHyps : List (Name × IVarId × FVarId) := hyps.intuitionisticIVarIds.filterMap fun ivar => if explicitIrisIVarIds.contains ivar then none else hyps.getDecl? ivar >>= fun ⟨name, _, _, ty⟩ => - (allPureFVars.find? (ty.containsFVar ·)).map (name, ·) + (allPureFVars.find? (ty.containsFVar ·)).map (name, ivar, ·) let allPureFVars := allPureFVars.eraseDups.filter <| fun fvar => inductionTarget.all (fvar != ·) @@ -140,7 +140,18 @@ def getDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} -- Sort the pure Lean hypothesis according to the dependency let allPureFVarsSorted ← getLCtx <&> (·.sortFVarsByContextOrder allPureFVars.toArray) - return ⟨missingPure, missingIris, allPureFVarsSorted⟩ + return ⟨missingPureHyps, missingIrisHyps, allPureFVarsSorted⟩ + +def getCompleteSelTargets (explicitTargets : List SelTarget) + (missingIrisHyps : List (Name × IVarId × FVarId)) + (allPureVarsSorted : Array FVarId) : + List SelTarget := + let pureTargets := allPureVarsSorted.toList.map (⟨.pure ·, true⟩) + let explicitIrisTargets := explicitTargets.filter <| + fun t => match t.kind with | .ipm _ => true | _ => false + let implicitIrisTargets := missingIrisHyps.map <| + fun ⟨_, ivar, _⟩ => ⟨.ipm ivar, false⟩ + pureTargets ++ explicitIrisTargets ++ implicitIrisTargets /-- Throw an error if there exists hypotheses that are depend on any hypothesis @@ -156,19 +167,19 @@ def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} (selPats : TSyntaxArray `selPat) (mkTactic : TSyntaxArray `selPat → ProofModeM (TSyntax `tactic)): ProofModeM Unit := do - let ⟨missingPure, missingIris, allPureFVarsSorted⟩ ← + let ⟨missingPureHyps, missingIrisHyps, allPureFVarsSorted⟩ ← getDependentHyps hyps explicitTargets inductionTarget -- Add an error message if there exists some pure/Lean hypotheses that should also be generalised - if !missingPure.isEmpty || !missingIris.isEmpty then - let leanLines ← missingPure.mapM fun (depId, srcId) => do + if !missingPureHyps.isEmpty || !missingIrisHyps.isEmpty then + let leanLines ← missingPureHyps.mapM fun ⟨depId, srcId⟩ => do let depDecl ← depId.getDecl let srcDecl ← srcId.getDecl let srcName := "`" ++ srcDecl.userName.toString ++ "`" let srcName := inductionTarget.elim srcName (if · == srcId then "the induction target" else srcName) return s!"• Lean hypothesis `{depDecl.userName}` depends on {srcName}" - let irisLines ← missingIris.mapM fun (name, srcId) => do + let irisLines ← missingIrisHyps.mapM fun ⟨name, _, srcId⟩ => do let srcDecl ← srcId.getDecl let srcName := "`" ++ srcDecl.userName.toString ++ "`" let srcName := inductionTarget.elim srcName @@ -182,7 +193,7 @@ def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} -- Build `selPat` syntax nodes for each missing item let newIrisPats : Array (TSyntax `selPat) ← - missingIris.toArray.mapM fun (name, _) => `(selPat| $(mkIdent name):ident) + missingIrisHyps.toArray.mapM fun ⟨name, _⟩ => `(selPat| $(mkIdent name):ident) -- Find the old tactic syntax and generate the new one with missing hypotheses added let oldTactic ← getRef From 5337208ffeb01fde1da2742b353afe2c66164d86 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 20 Jun 2026 15:53:40 +0200 Subject: [PATCH 158/181] Implement `generalizing!` syntax: implicit reverting --- Iris/Iris/ProofMode/Tactics/Induction.lean | 48 ++++++++++++++++------ Iris/Iris/Tests/Tactics.lean | 18 +------- 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index b6baaa551..efa172359 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -19,10 +19,13 @@ namespace Iris.ProofMode public meta section open BI Std Lean Elab Tactic Meta Qq Parser.Tactic -syntax (name := iinduction) "iinduction" colGt term - ("using" ident)? - ("generalizing" (ppSpace colGt selPat)+)? - (inductionAlts)? : tactic +declare_syntax_cat genSelPats + +syntax " generalizing " (ppSpace colGt selPat)+ : genSelPats +syntax " generalizing! " (ppSpace colGt selPat)+ : genSelPats + +syntax (name := iinduction) "iinduction " colGt term + (" using " ident)? (genSelPats)? (inductionAlts)? : tactic /-- Information from the tactic user for an induction subgoal -/ private structure Alt where @@ -371,10 +374,7 @@ private def generalizeTermWithFVar (x : TSyntax `term) : TacticM FVarId := do `iinduction n generalizing %m HQ HT`. -/ elab_rules : tactic - | `(tactic| iinduction $x - $[using $r]? - $[generalizing $genSelPats*]? - $[$alts]?) => do + | `(tactic| iinduction $x $[using $r]? generalizing $genSelPats* $[$alts]?) => do let fvar ← generalizeTermWithFVar x -- Parse the recursor name provided by the user @@ -385,11 +385,7 @@ elab_rules : tactic ProofModeM.runTactic λ mvar { hyps, goal, .. } => do let mkTactic := fun newPats => `(tactic| iinduction $x $[using $r]? generalizing $newPats* $[$alts]?) - let genSelTargets ← do match genSelPats with - | none => - checkDependentHyps "iinduction" hyps [] fvar #[] mkTactic - pure [] - | some genSelPats => + let genSelTargets ← do -- Parse the selection patterns for generalising hypotheses let parsedGenSelPats ← liftMacroM <| SelPat.parse genSelPats let genSelTargets ← SelPat.resolve hyps parsedGenSelPats @@ -399,3 +395,29 @@ elab_rules : tactic let pf ← iInductionCore hyps goal fvar parsedAlts recName genSelTargets mvar.assign pf + | `(tactic| iinduction $x $[using $r]? $[generalizing! $genSelPats*]? $[$alts]?) => do + let fvar ← generalizeTermWithFVar x + + -- Parse the recursor name provided by the user + let recName := r.map (·.getId) + + -- Parse the list of alternative names supplied by the user + let parsedAlts ← alts.mapM parseInductionAlts + + ProofModeM.runTactic λ mvar { hyps, goal, .. } => do + let genSelTargets ← do + match genSelPats with + | none => + let ⟨_, missingIrisHyps, allPureFVarsSorted⟩ ← + getDependentHyps hyps [] fvar + pure <| getCompleteSelTargets [] missingIrisHyps allPureFVarsSorted + | some genSelPats => + -- Parse the selection patterns for generalising hypotheses + let parsedGenSelPats ← liftMacroM <| SelPat.parse genSelPats + let genSelTargets ← SelPat.resolve hyps parsedGenSelPats + let ⟨_, missingIrisHyps, allPureFVarsSorted⟩ ← + getDependentHyps hyps genSelTargets fvar + pure <| getCompleteSelTargets genSelTargets missingIrisHyps allPureFVarsSorted + + let pf ← iInductionCore hyps goal fvar parsedAlts recName genSelTargets + mvar.assign pf diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index a6d10aa36..8cf11dae9 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2887,7 +2887,7 @@ example [BI PROP] {α} {t : NTree α} : ⊢@{PROP} ⌜t.id = t⌝ := by iinduction t with simp [NTree.id] | h_leaf => itrivial | h_node x ts IH1 => - iinduction ts generalizing IH1 with simp + iinduction ts with simp | nil => itrivial | cons t ts IH2 => isplit @@ -3107,20 +3107,4 @@ example [BI PROP] {P : PROP} {m n : Nat} {Q R S : Nat → PROP} {T : Nat → Pro | zero | succ n IH => itrivial -/- - Tests `iinduction` for hypotheses dependent on the induction target - when `generalizing` is not used. --/ -/-- info: Try this: - [apply] iinduction ts generalizing IH1 with simp ---- -error: iinduction: The following hypotheses depend on variables in the `generalizing` clause but are not themselves included: -• Iris hypothesis in the intuitionistic context `IH1` depends on the induction target -/ -#guard_msgs in -example [BI PROP] {α} {t : NTree α} : ⊢@{PROP} ⌜t.id = t⌝ := by - iinduction t with simp [NTree.id] - | h_leaf => itrivial - | h_node x ts IH1 => - iinduction ts with simp - end iinduction From 6ca077e984b913f10bb40e97acaab6c6c0a5fc2f Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 20 Jun 2026 15:59:26 +0200 Subject: [PATCH 159/181] Minor formatting --- Iris/Iris/ProofMode/Tactics/Revert.lean | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Revert.lean b/Iris/Iris/ProofMode/Tactics/Revert.lean index 5b753bafd..93d87cf50 100644 --- a/Iris/Iris/ProofMode/Tactics/Revert.lean +++ b/Iris/Iris/ProofMode/Tactics/Revert.lean @@ -134,6 +134,7 @@ def getDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} else hyps.getDecl? ivar >>= fun ⟨name, _, _, ty⟩ => (allPureFVars.find? (ty.containsFVar ·)).map (name, ivar, ·) + -- Missing pure hypotheses does not include the induction target itself let allPureFVars := allPureFVars.eraseDups.filter <| fun fvar => inductionTarget.all (fvar != ·) @@ -146,11 +147,11 @@ def getCompleteSelTargets (explicitTargets : List SelTarget) (missingIrisHyps : List (Name × IVarId × FVarId)) (allPureVarsSorted : Array FVarId) : List SelTarget := - let pureTargets := allPureVarsSorted.toList.map (⟨.pure ·, true⟩) + let pureTargets := allPureVarsSorted.toList.map ({ kind := .pure ·, explicit := true}) let explicitIrisTargets := explicitTargets.filter <| fun t => match t.kind with | .ipm _ => true | _ => false let implicitIrisTargets := missingIrisHyps.map <| - fun ⟨_, ivar, _⟩ => ⟨.ipm ivar, false⟩ + fun ⟨_, ivar, _⟩ => { kind := .ipm ivar, explicit := false} pureTargets ++ explicitIrisTargets ++ implicitIrisTargets /-- From 7aa10c2d753d72972d832832fd85575eeb86e068 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 20 Jun 2026 16:00:50 +0200 Subject: [PATCH 160/181] Test `generalizing!` syntax --- Iris/Iris/Tests/Tactics.lean | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 8cf11dae9..eaf80b4d5 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -3107,4 +3107,16 @@ example [BI PROP] {P : PROP} {m n : Nat} {Q R S : Nat → PROP} {T : Nat → Pro | zero | succ n IH => itrivial +/-- + The same example with `generalizing!` clause does not require any manual + resolution of dependencies. +-/ +example [BI PROP] {P : PROP} {m n : Nat} {Q R S : Nat → PROP} {T : Nat → Prop} + {h1 : T m} {U1 : (T m) → Prop} {h2 : U1 h1} {U2 : (U1 h1) → PROP} : + ⊢ P -∗ □ Q m -∗ □ R m -∗ □ S n -∗ □ U2 h2 -∗ ⌜n + 0 = n⌝ := by + iintro HP #HQ #HR #HS #HU2 + iinduction n generalizing! %m with + | zero + | succ n IH => itrivial + end iinduction From 9603821923a0139bc7b6ccfc8b38d3eb4065f252 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 20 Jun 2026 16:15:21 +0200 Subject: [PATCH 161/181] Print "Try this" suggestion only when all names are accessible --- Iris/Iris/ProofMode/Tactics/Revert.lean | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Revert.lean b/Iris/Iris/ProofMode/Tactics/Revert.lean index 93d87cf50..bdcd88524 100644 --- a/Iris/Iris/ProofMode/Tactics/Revert.lean +++ b/Iris/Iris/ProofMode/Tactics/Revert.lean @@ -196,15 +196,22 @@ def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} let newIrisPats : Array (TSyntax `selPat) ← missingIrisHyps.toArray.mapM fun ⟨name, _⟩ => `(selPat| $(mkIdent name):ident) - -- Find the old tactic syntax and generate the new one with missing hypotheses added - let oldTactic ← getRef - let existingIrisPats := selPats.filter fun p => - match p.raw with | `(selPat| %$_:ident) => false | _ => true - let extendedPats := sortedPurePats ++ existingIrisPats ++ newIrisPats - let newTactic ← mkTactic extendedPats - - -- Suggestion the fixed tactic - Lean.Meta.Tactic.TryThis.addSuggestion oldTactic newTactic + -- Check whether the new selecton pattern may contain any inaccessible names + let allNamesAccessible := + (missingIrisHyps.all fun ⟨name, _, _⟩ => !name.hasMacroScopes) && + !(← allPureFVarsSorted.anyM fun fvarId => do return (← fvarId.getDecl).userName.hasMacroScopes) + + -- Suggest a fixed tactic with explicit generalisations + if allNamesAccessible then + -- Find the old tactic syntax and generate the new one with missing hypotheses added + let oldTactic ← getRef + let existingIrisPats := selPats.filter fun p => + match p.raw with | `(selPat| %$_:ident) => false | _ => true + let extendedPats := sortedPurePats ++ existingIrisPats ++ newIrisPats + let newTactic ← mkTactic extendedPats + + -- Suggestion the fixed tactic + Lean.Meta.Tactic.TryThis.addSuggestion oldTactic newTactic -- Log the error and attach the clickable suggestion throwError m!"{tacticName}: The following hypotheses depend on variables in \ From bbaa9ac4201c436b9b8b4c52d8246b9b6ca007b3 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 20 Jun 2026 16:30:08 +0200 Subject: [PATCH 162/181] Print second "Try this" suggestion --- Iris/Iris/ProofMode/Tactics/Induction.lean | 11 +++++------ Iris/Iris/ProofMode/Tactics/Loeb.lean | 2 +- Iris/Iris/ProofMode/Tactics/Revert.lean | 18 ++++++++++++------ 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index efa172359..f80916865 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -384,13 +384,14 @@ elab_rules : tactic let parsedAlts ← alts.mapM parseInductionAlts ProofModeM.runTactic λ mvar { hyps, goal, .. } => do - let mkTactic := fun newPats => `(tactic| iinduction $x $[using $r]? generalizing $newPats* $[$alts]?) let genSelTargets ← do -- Parse the selection patterns for generalising hypotheses let parsedGenSelPats ← liftMacroM <| SelPat.parse genSelPats let genSelTargets ← SelPat.resolve hyps parsedGenSelPats -- Check for dependencies with the hypotheses in the selection targets - checkDependentHyps "iinduction" hyps genSelTargets fvar genSelPats mkTactic + checkDependentHyps "iinduction" hyps genSelTargets fvar genSelPats + (fun pats => `(tactic| iinduction $x $[using $r]? generalizing $pats* $[$alts]?)) + (some fun pats => `(tactic| iinduction $x $[using $r]? generalizing! $pats* $[$alts]?)) pure genSelTargets let pf ← iInductionCore hyps goal fvar parsedAlts recName genSelTargets @@ -405,18 +406,16 @@ elab_rules : tactic let parsedAlts ← alts.mapM parseInductionAlts ProofModeM.runTactic λ mvar { hyps, goal, .. } => do + let ⟨_, missingIrisHyps, allPureFVarsSorted⟩ ← + getDependentHyps hyps [] fvar let genSelTargets ← do match genSelPats with | none => - let ⟨_, missingIrisHyps, allPureFVarsSorted⟩ ← - getDependentHyps hyps [] fvar pure <| getCompleteSelTargets [] missingIrisHyps allPureFVarsSorted | some genSelPats => -- Parse the selection patterns for generalising hypotheses let parsedGenSelPats ← liftMacroM <| SelPat.parse genSelPats let genSelTargets ← SelPat.resolve hyps parsedGenSelPats - let ⟨_, missingIrisHyps, allPureFVarsSorted⟩ ← - getDependentHyps hyps genSelTargets fvar pure <| getCompleteSelTargets genSelTargets missingIrisHyps allPureFVarsSorted let pf ← iInductionCore hyps goal fvar parsedAlts recName genSelTargets diff --git a/Iris/Iris/ProofMode/Tactics/Loeb.lean b/Iris/Iris/ProofMode/Tactics/Loeb.lean index 2e8ef632d..e15791689 100644 --- a/Iris/Iris/ProofMode/Tactics/Loeb.lean +++ b/Iris/Iris/ProofMode/Tactics/Loeb.lean @@ -29,7 +29,7 @@ elab_rules : tactic ProofModeM.runTactic fun mvid {hyps, goal, ..} => do let targets : List SelTarget ← SelPat.resolve hyps (pats ++ [.spatial]) checkDependentHyps "iloeb" hyps targets none hs - (fun newPats => `(tactic| iloeb as $IH generalizing $newPats*)) + (fun newPats => `(tactic| iloeb as $IH generalizing $newPats*)) none let expr ← iRevertIntro hyps goal targets fun {prop _ _} hyps goal k => do let some _ ← ProofModeM.trySynthInstanceQ q(BI.BILoeb $prop) | throwError m!"iloeb: no `{←ppExpr q(BI.BILoeb $prop)}` instance found" diff --git a/Iris/Iris/ProofMode/Tactics/Revert.lean b/Iris/Iris/ProofMode/Tactics/Revert.lean index bdcd88524..13313efcf 100644 --- a/Iris/Iris/ProofMode/Tactics/Revert.lean +++ b/Iris/Iris/ProofMode/Tactics/Revert.lean @@ -166,7 +166,8 @@ def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} (explicitTargets : List SelTarget) (inductionTarget : Option FVarId) (selPats : TSyntaxArray `selPat) - (mkTactic : TSyntaxArray `selPat → ProofModeM (TSyntax `tactic)): + (mkTacticExplicit : TSyntaxArray `selPat → ProofModeM (TSyntax `tactic)) + (mkTacticImplicit : Option <| TSyntaxArray `selPat → ProofModeM (TSyntax `tactic)) : ProofModeM Unit := do let ⟨missingPureHyps, missingIrisHyps, allPureFVarsSorted⟩ ← getDependentHyps hyps explicitTargets inductionTarget @@ -201,16 +202,20 @@ def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} (missingIrisHyps.all fun ⟨name, _, _⟩ => !name.hasMacroScopes) && !(← allPureFVarsSorted.anyM fun fvarId => do return (← fvarId.getDecl).userName.hasMacroScopes) + -- Find the old tactic syntax and generate the new one with missing hypotheses added + let oldTactic ← getRef + -- Suggest a fixed tactic with explicit generalisations if allNamesAccessible then - -- Find the old tactic syntax and generate the new one with missing hypotheses added - let oldTactic ← getRef let existingIrisPats := selPats.filter fun p => match p.raw with | `(selPat| %$_:ident) => false | _ => true let extendedPats := sortedPurePats ++ existingIrisPats ++ newIrisPats - let newTactic ← mkTactic extendedPats + let newTactic ← mkTacticExplicit extendedPats + Lean.Meta.Tactic.TryThis.addSuggestion oldTactic newTactic - -- Suggestion the fixed tactic + -- Suggest a fixed tactic with implicit generalisations + mkTacticImplicit.forM fun mkTacticImplicit => do + let newTactic ← mkTacticImplicit selPats Lean.Meta.Tactic.TryThis.addSuggestion oldTactic newTactic -- Log the error and attach the clickable suggestion @@ -243,6 +248,7 @@ elab_rules : tactic | `(tactic| irevert $pats:selPat*) => do ProofModeM.runTactic fun mvar { hyps, goal, .. } => do let targets ← SelPat.resolve hyps parsedPats checkDependentHyps "irevert" hyps targets none pats - fun newPats => `(tactic| irevert $newPats*) + (fun newPats => `(tactic| irevert $newPats*)) none + let expr ← iRevertCore targets hyps goal mvar.assign expr From 697ddf49305e2d8dc7284415f36a240fe6a7c91d Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 20 Jun 2026 16:54:07 +0200 Subject: [PATCH 163/181] Implement `generalizing!` for `iloeb` --- Iris/Iris/ProofMode/Tactics/Induction.lean | 7 +--- Iris/Iris/ProofMode/Tactics/Loeb.lean | 41 ++++++++++++++++------ Iris/Iris/ProofMode/Tactics/Revert.lean | 5 +++ 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index f80916865..39f5c8598 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -19,13 +19,8 @@ namespace Iris.ProofMode public meta section open BI Std Lean Elab Tactic Meta Qq Parser.Tactic -declare_syntax_cat genSelPats - -syntax " generalizing " (ppSpace colGt selPat)+ : genSelPats -syntax " generalizing! " (ppSpace colGt selPat)+ : genSelPats - syntax (name := iinduction) "iinduction " colGt term - (" using " ident)? (genSelPats)? (inductionAlts)? : tactic + (" using " ident)? (generalizingSelPats)? (inductionAlts)? : tactic /-- Information from the tactic user for an induction subgoal -/ private structure Alt where diff --git a/Iris/Iris/ProofMode/Tactics/Loeb.lean b/Iris/Iris/ProofMode/Tactics/Loeb.lean index e15791689..9800a770e 100644 --- a/Iris/Iris/ProofMode/Tactics/Loeb.lean +++ b/Iris/Iris/ProofMode/Tactics/Loeb.lean @@ -14,7 +14,7 @@ open Lean Meta Elab.Tactic Qq public meta section -syntax (name := iloeb) "iloeb" " as " binderIdent " generalizing " (colGt ppSpace selPat)* : tactic +syntax (name := iloeb) "iloeb" " as " binderIdent (generalizingSelPats)? : tactic /-- `iloeb as IH generalizing hs` applies Löb induction in the current goal @@ -26,27 +26,48 @@ syntax (name := iloeb) "iloeb" " as " binderIdent " generalizing " (colGt ppSpac elab_rules : tactic | `(tactic| iloeb as $IH:binderIdent generalizing $hs:selPat*) => do let pats ← Elab.liftMacroM <| SelPat.parse hs - ProofModeM.runTactic fun mvid {hyps, goal, ..} => do + + ProofModeM.runTactic fun mvid { hyps, goal, .. } => do let targets : List SelTarget ← SelPat.resolve hyps (pats ++ [.spatial]) + checkDependentHyps "iloeb" hyps targets none hs - (fun newPats => `(tactic| iloeb as $IH generalizing $newPats*)) none + (fun pats => `(tactic| iloeb as $IH generalizing $pats*)) + (some fun pats => `(tactic| iloeb as $IH generalizing! $pats*)) + let expr ← iRevertIntro hyps goal targets fun {prop _ _} hyps goal k => do let some _ ← ProofModeM.trySynthInstanceQ q(BI.BILoeb $prop) | throwError m!"iloeb: no `{←ppExpr q(BI.BILoeb $prop)}` instance found" let pf := q(BI.loeb_wand_intuitionistically (P := $goal)) let pf' ← do - -- We have applied BI.loeb_wand_intuitionistically + -- We have applied `BI.loeb_wand_intuitionistically` let goal := q(iprop(□ (□ ▷ $goal -∗ $goal))) iModIntroCore hyps goal (← `(_)) fun hyps goal => do iIntroCore hyps goal [(IH, .intro <| .intuitionistic <| .one IH)] (k · · addBIGoal) + return q($(pf').trans $pf) mvid.assign expr + | `(tactic| iloeb as $IH:binderIdent $[generalizing! $hs:selPat*]?) => do + ProofModeM.runTactic fun mvid { hyps, goal, .. } => do + let targets ← do match hs with + | none => + SelPat.resolve hyps [.spatial] + | some hs => + let pats ← Elab.liftMacroM <| SelPat.parse hs + let targets ← SelPat.resolve hyps (pats ++ [.spatial]) + let ⟨_, missingIrisHyps, allPureFVarsSorted⟩ ← getDependentHyps hyps targets none + pure <| getCompleteSelTargets targets missingIrisHyps allPureFVarsSorted -/-- - `iloeb as IH` applies Löb induction in the current goal - using the induction hypothesis `IH`. + let expr ← iRevertIntro hyps goal targets fun {prop _ _} hyps goal k => do + let some _ ← ProofModeM.trySynthInstanceQ q(BI.BILoeb $prop) + | throwError m!"iloeb: no `{←ppExpr q(BI.BILoeb $prop)}` instance found" + let pf := q(BI.loeb_wand_intuitionistically (P := $goal)) + let pf' ← do + -- We have applied `BI.loeb_wand_intuitionistically` + let goal := q(iprop(□ (□ ▷ $goal -∗ $goal))) + iModIntroCore hyps goal (← `(_)) fun hyps goal => do + iIntroCore hyps goal [(IH, .intro <| .intuitionistic <| .one IH)] (k · · addBIGoal) - All spatial hypothesis are generalized in the induction hypothesis. --/ -macro "iloeb" " as " colGt IH:binderIdent : tactic => `(tactic | iloeb as $IH generalizing) + return q($(pf').trans $pf) + + mvid.assign expr diff --git a/Iris/Iris/ProofMode/Tactics/Revert.lean b/Iris/Iris/ProofMode/Tactics/Revert.lean index 13313efcf..a480fe22b 100644 --- a/Iris/Iris/ProofMode/Tactics/Revert.lean +++ b/Iris/Iris/ProofMode/Tactics/Revert.lean @@ -19,6 +19,11 @@ namespace Iris.ProofMode public section open BI Std +declare_syntax_cat generalizingSelPats + +syntax " generalizing " (ppSpace colGt selPat)* : generalizingSelPats +syntax " generalizing! " (ppSpace colGt selPat)* : generalizingSelPats + theorem wand_revert [BI PROP] {Δ Δ' P Q : PROP} (h1 : Δ ⊣⊢ Δ' ∗ P) (h2 : Δ' ⊢ P -∗ Q) : Δ ⊢ Q := h1.mp.trans (wand_elim h2) From aa7060ca080e4e7b06a4fb1d0f0b88f77e627af2 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 20 Jun 2026 17:06:15 +0200 Subject: [PATCH 164/181] Towards better error printing with inaccessible names --- Iris/Iris/ProofMode/Tactics/Revert.lean | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Revert.lean b/Iris/Iris/ProofMode/Tactics/Revert.lean index a480fe22b..68ccdc68a 100644 --- a/Iris/Iris/ProofMode/Tactics/Revert.lean +++ b/Iris/Iris/ProofMode/Tactics/Revert.lean @@ -159,6 +159,9 @@ def getCompleteSelTargets (explicitTargets : List SelTarget) fun ⟨_, ivar, _⟩ => { kind := .ipm ivar, explicit := false} pureTargets ++ explicitIrisTargets ++ implicitIrisTargets +private def ppHypName (name : Name) : String := + if name.hasMacroScopes then s!"{name.eraseMacroScopes}✝" else name.toString + /-- Throw an error if there exists hypotheses that are depend on any hypothesis in `explicitTargets` but are not themselves in the list. @@ -182,16 +185,16 @@ def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} let leanLines ← missingPureHyps.mapM fun ⟨depId, srcId⟩ => do let depDecl ← depId.getDecl let srcDecl ← srcId.getDecl - let srcName := "`" ++ srcDecl.userName.toString ++ "`" + let srcName := "`" ++ ppHypName srcDecl.userName ++ "`" let srcName := inductionTarget.elim srcName (if · == srcId then "the induction target" else srcName) - return s!"• Lean hypothesis `{depDecl.userName}` depends on {srcName}" + return s!"• Lean hypothesis `{ppHypName depDecl.userName}` depends on {srcName}" let irisLines ← missingIrisHyps.mapM fun ⟨name, _, srcId⟩ => do let srcDecl ← srcId.getDecl let srcName := "`" ++ srcDecl.userName.toString ++ "`" let srcName := inductionTarget.elim srcName (if · == srcId then "the induction target" else srcName) - return s!"• Iris hypothesis in the intuitionistic context `{name}` depends on {srcName}" + return s!"• Iris hypothesis in the intuitionistic context `{ppHypName name}` depends on {srcName}" let sortedPurePats : Array (TSyntax `selPat) ← allPureFVarsSorted.mapM fun fvarId => do let decl ← fvarId.getDecl From 869d8e353148a26172ffc6d618b80b2662795159 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 20 Jun 2026 17:17:32 +0200 Subject: [PATCH 165/181] Implement `irevert!` as an alternative to `irevert` for auto-resolution of dependent hypotheses --- Iris/Iris/ProofMode/Tactics/Induction.lean | 23 ++++++++-------- Iris/Iris/ProofMode/Tactics/Loeb.lean | 5 ++++ Iris/Iris/ProofMode/Tactics/Revert.lean | 31 ++++++++++++++++++++-- 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 39f5c8598..1b344f7da 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -401,17 +401,18 @@ elab_rules : tactic let parsedAlts ← alts.mapM parseInductionAlts ProofModeM.runTactic λ mvar { hyps, goal, .. } => do - let ⟨_, missingIrisHyps, allPureFVarsSorted⟩ ← - getDependentHyps hyps [] fvar - let genSelTargets ← do - match genSelPats with - | none => - pure <| getCompleteSelTargets [] missingIrisHyps allPureFVarsSorted - | some genSelPats => - -- Parse the selection patterns for generalising hypotheses - let parsedGenSelPats ← liftMacroM <| SelPat.parse genSelPats - let genSelTargets ← SelPat.resolve hyps parsedGenSelPats - pure <| getCompleteSelTargets genSelTargets missingIrisHyps allPureFVarsSorted + let genSelTargets ← do match genSelPats with + | none => + let ⟨_, missingIrisHyps, allPureFVarsSorted⟩ ← getDependentHyps hyps [] fvar + pure <| getCompleteSelTargets [] missingIrisHyps allPureFVarsSorted + | some genSelPats => + -- Parse the selection patterns provided by the tactic user + let parsedGenSelPats ← liftMacroM <| SelPat.parse genSelPats + let genSelTargets ← SelPat.resolve hyps parsedGenSelPats + -- Find all dependent hypotheses + let ⟨_, missingIrisHyps, allPureFVarsSorted⟩ ← getDependentHyps hyps genSelTargets fvar + -- Obtain the selection targets, including dependent ones + pure <| getCompleteSelTargets genSelTargets missingIrisHyps allPureFVarsSorted let pf ← iInductionCore hyps goal fvar parsedAlts recName genSelTargets mvar.assign pf diff --git a/Iris/Iris/ProofMode/Tactics/Loeb.lean b/Iris/Iris/ProofMode/Tactics/Loeb.lean index 9800a770e..ef08e78c8 100644 --- a/Iris/Iris/ProofMode/Tactics/Loeb.lean +++ b/Iris/Iris/ProofMode/Tactics/Loeb.lean @@ -28,8 +28,10 @@ elab_rules : tactic let pats ← Elab.liftMacroM <| SelPat.parse hs ProofModeM.runTactic fun mvid { hyps, goal, .. } => do + -- Parse the selection patterns provided by the tactic user let targets : List SelTarget ← SelPat.resolve hyps (pats ++ [.spatial]) + -- Check for dependencies with the hypotheses in the selection targets checkDependentHyps "iloeb" hyps targets none hs (fun pats => `(tactic| iloeb as $IH generalizing $pats*)) (some fun pats => `(tactic| iloeb as $IH generalizing! $pats*)) @@ -53,9 +55,12 @@ elab_rules : tactic | none => SelPat.resolve hyps [.spatial] | some hs => + -- Parse the selection patterns provided by the tactic user let pats ← Elab.liftMacroM <| SelPat.parse hs let targets ← SelPat.resolve hyps (pats ++ [.spatial]) + -- Find all dependent hypotheses let ⟨_, missingIrisHyps, allPureFVarsSorted⟩ ← getDependentHyps hyps targets none + -- Obtain the selection targets, including dependent ones pure <| getCompleteSelTargets targets missingIrisHyps allPureFVarsSorted let expr ← iRevertIntro hyps goal targets fun {prop _ _} hyps goal k => do diff --git a/Iris/Iris/ProofMode/Tactics/Revert.lean b/Iris/Iris/ProofMode/Tactics/Revert.lean index 68ccdc68a..8b6e72206 100644 --- a/Iris/Iris/ProofMode/Tactics/Revert.lean +++ b/Iris/Iris/ProofMode/Tactics/Revert.lean @@ -246,17 +246,44 @@ def iRevertCore (targets : List SelTarget) {u : Level} {prop: Q(Type $u)} /-- `irevert pats` reverts the hypotheses specified by the selection pattern `pats` - from the Iris contexts back into the regular Lean context. + from the Iris contexts back into the regular Lean context. Pure/Iris hypotheses + dependent on any hypothesis specified by `pats` must also themselves be + included in the selection pattern. -/ syntax (name := irevert) "irevert " (colGt ppSpace selPat)+ : tactic +/-- + `irevert! pats` reverts the hypotheses specified by the selection pattern `pats` + from the Iris contexts back into the regular Lean context. Pure/Iris hypotheses + dependent on any hypothesis specified by `pats` are also reverted. +-/ +syntax (name := irevert!) "irevert! " (colGt ppSpace selPat)+ : tactic + elab_rules : tactic | `(tactic| irevert $pats:selPat*) => do let parsedPats ← liftMacroM <| SelPat.parse pats ProofModeM.runTactic fun mvar { hyps, goal, .. } => do + -- Parse the selection patterns provided by the tactic user let targets ← SelPat.resolve hyps parsedPats + + -- Check for dependencies with the hypotheses in the selection targets checkDependentHyps "irevert" hyps targets none pats - (fun newPats => `(tactic| irevert $newPats*)) none + (fun pats => `(tactic| irevert $pats*)) + (some fun pats => `(tactic| irevert! $pats*)) + + let expr ← iRevertCore targets hyps goal + mvar.assign expr + +elab_rules : tactic | `(tactic| irevert! $pats:selPat*) => do + let parsedPats ← liftMacroM <| SelPat.parse pats + + ProofModeM.runTactic fun mvar { hyps, goal, .. } => do + -- Parse the selection patterns provided by the tactic user + let explicitTargets ← SelPat.resolve hyps parsedPats + -- Find all dependent hypotheses + let ⟨_, missingIrisHyps, allPureFVarsSorted⟩ ← getDependentHyps hyps explicitTargets none + -- Obtain the selection targets, including dependent ones + let targets := getCompleteSelTargets explicitTargets missingIrisHyps allPureFVarsSorted let expr ← iRevertCore targets hyps goal mvar.assign expr From 7f053e12cb2741c76ffde5ea8997bcbc05a4427f Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 20 Jun 2026 17:34:21 +0200 Subject: [PATCH 166/181] Bug fix: spatial hypotheses should also be checked for `irevert` --- Iris/Iris/ProofMode/Tactics/Induction.lean | 4 ++-- Iris/Iris/ProofMode/Tactics/Loeb.lean | 2 +- Iris/Iris/ProofMode/Tactics/Revert.lean | 15 ++++++++++----- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 1b344f7da..96c22d680 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -403,14 +403,14 @@ elab_rules : tactic ProofModeM.runTactic λ mvar { hyps, goal, .. } => do let genSelTargets ← do match genSelPats with | none => - let ⟨_, missingIrisHyps, allPureFVarsSorted⟩ ← getDependentHyps hyps [] fvar + let ⟨_, missingIrisHyps, allPureFVarsSorted⟩ ← getDependentHyps hyps [] fvar false pure <| getCompleteSelTargets [] missingIrisHyps allPureFVarsSorted | some genSelPats => -- Parse the selection patterns provided by the tactic user let parsedGenSelPats ← liftMacroM <| SelPat.parse genSelPats let genSelTargets ← SelPat.resolve hyps parsedGenSelPats -- Find all dependent hypotheses - let ⟨_, missingIrisHyps, allPureFVarsSorted⟩ ← getDependentHyps hyps genSelTargets fvar + let ⟨_, missingIrisHyps, allPureFVarsSorted⟩ ← getDependentHyps hyps genSelTargets fvar false -- Obtain the selection targets, including dependent ones pure <| getCompleteSelTargets genSelTargets missingIrisHyps allPureFVarsSorted diff --git a/Iris/Iris/ProofMode/Tactics/Loeb.lean b/Iris/Iris/ProofMode/Tactics/Loeb.lean index ef08e78c8..9a218f931 100644 --- a/Iris/Iris/ProofMode/Tactics/Loeb.lean +++ b/Iris/Iris/ProofMode/Tactics/Loeb.lean @@ -59,7 +59,7 @@ elab_rules : tactic let pats ← Elab.liftMacroM <| SelPat.parse hs let targets ← SelPat.resolve hyps (pats ++ [.spatial]) -- Find all dependent hypotheses - let ⟨_, missingIrisHyps, allPureFVarsSorted⟩ ← getDependentHyps hyps targets none + let ⟨_, missingIrisHyps, allPureFVarsSorted⟩ ← getDependentHyps hyps targets none false -- Obtain the selection targets, including dependent ones pure <| getCompleteSelTargets targets missingIrisHyps allPureFVarsSorted diff --git a/Iris/Iris/ProofMode/Tactics/Revert.lean b/Iris/Iris/ProofMode/Tactics/Revert.lean index 8b6e72206..9720d6b13 100644 --- a/Iris/Iris/ProofMode/Tactics/Revert.lean +++ b/Iris/Iris/ProofMode/Tactics/Revert.lean @@ -106,7 +106,9 @@ private def RevertState.revertLeanHyp def getDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} (hyps : Hyps bi e) - (explicitTargets : List SelTarget) (inductionTarget : Option FVarId) : + (explicitTargets : List SelTarget) + (inductionTarget : Option FVarId) + (includeSpatialHyps : Bool) : ProofModeM <| List (FVarId × FVarId) × List (Name × IVarId × FVarId) × Array FVarId := do let explicitIrisIVarIds := explicitTargets.filterMap (match ·.kind with | .ipm ivar => some ivar | _ => none) @@ -132,9 +134,12 @@ def getDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} let allPureFVars := explicitPureFVars ++ missingPureHyps.map (·.fst) + let irisHypsToBeChecked := + hyps.intuitionisticIVarIds ++ + if includeSpatialHyps then hyps.spatialIVarIds else [] -- Check forward dependency of Iris hypotheses let missingIrisHyps : List (Name × IVarId × FVarId) := - hyps.intuitionisticIVarIds.filterMap fun ivar => + irisHypsToBeChecked.filterMap fun ivar => if explicitIrisIVarIds.contains ivar then none else hyps.getDecl? ivar >>= fun ⟨name, _, _, ty⟩ => (allPureFVars.find? (ty.containsFVar ·)).map (name, ivar, ·) @@ -178,7 +183,7 @@ def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} (mkTacticImplicit : Option <| TSyntaxArray `selPat → ProofModeM (TSyntax `tactic)) : ProofModeM Unit := do let ⟨missingPureHyps, missingIrisHyps, allPureFVarsSorted⟩ ← - getDependentHyps hyps explicitTargets inductionTarget + getDependentHyps hyps explicitTargets inductionTarget true -- Add an error message if there exists some pure/Lean hypotheses that should also be generalised if !missingPureHyps.isEmpty || !missingIrisHyps.isEmpty then @@ -194,7 +199,7 @@ def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} let srcName := "`" ++ srcDecl.userName.toString ++ "`" let srcName := inductionTarget.elim srcName (if · == srcId then "the induction target" else srcName) - return s!"• Iris hypothesis in the intuitionistic context `{ppHypName name}` depends on {srcName}" + return s!"• Iris hypothesis `{ppHypName name}` depends on {srcName}" let sortedPurePats : Array (TSyntax `selPat) ← allPureFVarsSorted.mapM fun fvarId => do let decl ← fvarId.getDecl @@ -281,7 +286,7 @@ elab_rules : tactic | `(tactic| irevert! $pats:selPat*) => do -- Parse the selection patterns provided by the tactic user let explicitTargets ← SelPat.resolve hyps parsedPats -- Find all dependent hypotheses - let ⟨_, missingIrisHyps, allPureFVarsSorted⟩ ← getDependentHyps hyps explicitTargets none + let ⟨_, missingIrisHyps, allPureFVarsSorted⟩ ← getDependentHyps hyps explicitTargets none true -- Obtain the selection targets, including dependent ones let targets := getCompleteSelTargets explicitTargets missingIrisHyps allPureFVarsSorted From 07a1c6bd4f4c7e39bcad5690b9a2ecd65c11a37a Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 20 Jun 2026 17:52:17 +0200 Subject: [PATCH 167/181] Handle the printing of inaccessible names properly --- Iris/Iris/ProofMode/Tactics/Revert.lean | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Revert.lean b/Iris/Iris/ProofMode/Tactics/Revert.lean index 9720d6b13..b9c3f1bd6 100644 --- a/Iris/Iris/ProofMode/Tactics/Revert.lean +++ b/Iris/Iris/ProofMode/Tactics/Revert.lean @@ -164,9 +164,6 @@ def getCompleteSelTargets (explicitTargets : List SelTarget) fun ⟨_, ivar, _⟩ => { kind := .ipm ivar, explicit := false} pureTargets ++ explicitIrisTargets ++ implicitIrisTargets -private def ppHypName (name : Name) : String := - if name.hasMacroScopes then s!"{name.eraseMacroScopes}✝" else name.toString - /-- Throw an error if there exists hypotheses that are depend on any hypothesis in `explicitTargets` but are not themselves in the list. @@ -185,21 +182,21 @@ def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} let ⟨missingPureHyps, missingIrisHyps, allPureFVarsSorted⟩ ← getDependentHyps hyps explicitTargets inductionTarget true + -- Handle the printing of inaccessible names, if necessary + let ppHypName := fun name => + if name.hasMacroScopes then + s!"`{name.eraseMacroScopes}` (inaccessible name)" + else s!"`{name.toString}`" + -- Add an error message if there exists some pure/Lean hypotheses that should also be generalised if !missingPureHyps.isEmpty || !missingIrisHyps.isEmpty then let leanLines ← missingPureHyps.mapM fun ⟨depId, srcId⟩ => do let depDecl ← depId.getDecl let srcDecl ← srcId.getDecl - let srcName := "`" ++ ppHypName srcDecl.userName ++ "`" - let srcName := inductionTarget.elim srcName - (if · == srcId then "the induction target" else srcName) - return s!"• Lean hypothesis `{ppHypName depDecl.userName}` depends on {srcName}" - let irisLines ← missingIrisHyps.mapM fun ⟨name, _, srcId⟩ => do + return s!"• Lean hypothesis {ppHypName depDecl.userName} depends on {ppHypName srcDecl.userName}" + let irisLines ← missingIrisHyps.mapM fun ⟨depName, _, srcId⟩ => do let srcDecl ← srcId.getDecl - let srcName := "`" ++ srcDecl.userName.toString ++ "`" - let srcName := inductionTarget.elim srcName - (if · == srcId then "the induction target" else srcName) - return s!"• Iris hypothesis `{ppHypName name}` depends on {srcName}" + return s!"• Iris hypothesis {ppHypName depName} depends on {ppHypName srcDecl.userName}" let sortedPurePats : Array (TSyntax `selPat) ← allPureFVarsSorted.mapM fun fvarId => do let decl ← fvarId.getDecl From 74602000f77d846dd1ecbc85dcb83efb5ce4a49d Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 20 Jun 2026 17:52:34 +0200 Subject: [PATCH 168/181] Update tests --- Iris/Iris/Tests/Tactics.lean | 76 ++++++++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 11 deletions(-) diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index eaf80b4d5..2ac422a08 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -378,24 +378,30 @@ example [BI PROP] (P : PROP) {x : Nat} : ⊢ P := by /- Tests `irevert` failing with dependency -/ /-- info: Try this: - [apply] irevert %x %hp + [apply] irevert %x %hp H +--- +info: Try this: + [apply] irevert! %x --- error: irevert: The following hypotheses depend on variables in the `generalizing` clause but are not themselves included: -• Lean hypothesis `hp` depends on `x` -/ +• Lean hypothesis `hp` depends on `x` +• Iris hypothesis `H` depends on `x` -/ #guard_msgs in example [BI PROP] (Φ : Bool → PROP) : ⊢ ∀ x, ⌜x = true⌝ -∗ Φ x -∗ Φ x := by iintro %x %hp H irevert %x -/- Tests `irevert` failing with dependency -/ +/- + Tests `irevert` failing with dependency, involving an inaccessible name +-/ /-- info: Try this: - [apply] irevert %x %hp H + [apply] irevert! %x H --- error: irevert: The following hypotheses depend on variables in the `generalizing` clause but are not themselves included: -• Lean hypothesis `hp` depends on `x` -/ +• Lean hypothesis `x` (inaccessible name) depends on `x` -/ #guard_msgs in example [BI PROP] (Φ : Bool → PROP) : ⊢ ∀ x, ⌜x = true⌝ -∗ Φ x -∗ Φ x := by - iintro %x %hp H + iintro %x %_ H irevert %x H end revert @@ -2788,10 +2794,13 @@ example (P Q : PROP) : info: Try this: [apply] iloeb as IH generalizing %n %h1 %U HT --- +info: Try this: + [apply] iloeb as IH generalizing! %n +--- error: iloeb: The following hypotheses depend on variables in the `generalizing` clause but are not themselves included: • Lean hypothesis `h1` depends on `n` • Lean hypothesis `U` depends on `n` -• Iris hypothesis in the intuitionistic context `HT` depends on `n` +• Iris hypothesis `HT` depends on `n` -/ #guard_msgs in example [BI PROP] [BILoeb PROP] {n : Nat} {P T : Nat → PROP} {Q : Nat → Prop} @@ -2800,6 +2809,23 @@ example [BI PROP] [BILoeb PROP] {n : Nat} {P T : Nat → PROP} {Q : Nat → Prop iintro #HT iloeb as IH generalizing %n +-- Same test as above, involving inaccessible names +/-- +info: Try this: + [apply] iloeb as IH generalizing! %n +--- +error: iloeb: The following hypotheses depend on variables in the `generalizing` clause but are not themselves included: +• Lean hypothesis `h1` depends on `n` +• Lean hypothesis `x` (inaccessible name) depends on `n` +• Iris hypothesis `x` (inaccessible name) depends on `n` +-/ +#guard_msgs in +example [BI PROP] [BILoeb PROP] {n : Nat} {P T : Nat → PROP} {Q : Nat → Prop} + {h1 : Q n} {_ : (Q n) → Prop} : + ⊢ □ T n -∗ □ P n := by + iintro #_ + iloeb as IH generalizing %n + end iloeb section iinduction @@ -3089,15 +3115,20 @@ example [BI PROP] {P Q R S T : PROP} {n : Nat} : | zero | succ n IH => itrivial --- +info: Try this: + [apply] iinduction n generalizing! %m with + | zero + | succ n IH => itrivial +--- error: iinduction: The following hypotheses depend on variables in the `generalizing` clause but are not themselves included: • Lean hypothesis `h1` depends on `m` • Lean hypothesis `U1` depends on `m` • Lean hypothesis `h2` depends on `m` • Lean hypothesis `U2` depends on `m` -• Iris hypothesis in the intuitionistic context `HQ` depends on `m` -• Iris hypothesis in the intuitionistic context `HR` depends on `m` -• Iris hypothesis in the intuitionistic context `HS` depends on the induction target -• Iris hypothesis in the intuitionistic context `HU2` depends on `h2` -/ +• Iris hypothesis `HQ` depends on `m` +• Iris hypothesis `HR` depends on `m` +• Iris hypothesis `HS` depends on `n` +• Iris hypothesis `HU2` depends on `h2` -/ #guard_msgs in example [BI PROP] {P : PROP} {m n : Nat} {Q R S : Nat → PROP} {T : Nat → Prop} {h1 : T m} {U1 : (T m) → Prop} {h2 : U1 h1} {U2 : (U1 h1) → PROP} : @@ -3119,4 +3150,27 @@ example [BI PROP] {P : PROP} {m n : Nat} {Q R S : Nat → PROP} {T : Nat → Pro | zero | succ n IH => itrivial +/- Similar test as above, except that some hypotheses have inaccessible names. -/ +/-- info: Try this: + [apply] iinduction n generalizing! %m with + | zero + | succ n IH => itrivial +--- +error: iinduction: The following hypotheses depend on variables in the `generalizing` clause but are not themselves included: +• Lean hypothesis `h1` depends on `m` +• Lean hypothesis `U1` depends on `m` +• Lean hypothesis `h2` depends on `m` +• Lean hypothesis `U2` depends on `m` +• Lean hypothesis `x` (inaccessible name) depends on `n` +• Iris hypothesis `x` (inaccessible name) depends on `h2` -/ +#guard_msgs in +example [BI PROP] {P : PROP} {m n : Nat} {T : Nat → Prop} + {h1 : T m} {_ : T n} {U1 : (T m) → Prop} + {h2 : U1 h1} {U2 : (U1 h1) → PROP} : + ⊢ P -∗ □ U2 h2 -∗ ⌜n + 0 = n⌝ := by + iintro HP #_ + iinduction n generalizing %m with + | zero + | succ n IH => itrivial + end iinduction From c69fac34be6e06e62e2f77216d2042840254c0ae Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 20 Jun 2026 17:55:26 +0200 Subject: [PATCH 169/181] Minor bug fix --- Iris/Iris/HeapLang/Lib/Quicksort.lean | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Iris/Iris/HeapLang/Lib/Quicksort.lean b/Iris/Iris/HeapLang/Lib/Quicksort.lean index ab6065822..350260687 100644 --- a/Iris/Iris/HeapLang/Lib/Quicksort.lean +++ b/Iris/Iris/HeapLang/Lib/Quicksort.lean @@ -283,7 +283,7 @@ theorem wp_makeList (l : List Int) (Φ : Val → IProp GF) : (∀ v, isList v l -∗ Φ v) -∗ WP hl(&(makeList l)) {{ Φ }} := by iintro HΦ - iinduction l generalizing %Φ with + iinduction l generalizing %Φ HΦ with | nil => unfold makeList iapply nil_spec $$ HΦ From bffe8095ed6b8ee413e79977dd81060e1bd1b56c Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 20 Jun 2026 17:58:36 +0200 Subject: [PATCH 170/181] Code formatting 1 --- Iris/Iris/ProofMode/Tactics/Induction.lean | 2 +- Iris/Iris/ProofMode/Tactics/Loeb.lean | 2 +- Iris/Iris/ProofMode/Tactics/Revert.lean | 10 ++++------ 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 96c22d680..79781593d 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -386,7 +386,7 @@ elab_rules : tactic -- Check for dependencies with the hypotheses in the selection targets checkDependentHyps "iinduction" hyps genSelTargets fvar genSelPats (fun pats => `(tactic| iinduction $x $[using $r]? generalizing $pats* $[$alts]?)) - (some fun pats => `(tactic| iinduction $x $[using $r]? generalizing! $pats* $[$alts]?)) + (fun pats => `(tactic| iinduction $x $[using $r]? generalizing! $pats* $[$alts]?)) pure genSelTargets let pf ← iInductionCore hyps goal fvar parsedAlts recName genSelTargets diff --git a/Iris/Iris/ProofMode/Tactics/Loeb.lean b/Iris/Iris/ProofMode/Tactics/Loeb.lean index 9a218f931..6350ccc4d 100644 --- a/Iris/Iris/ProofMode/Tactics/Loeb.lean +++ b/Iris/Iris/ProofMode/Tactics/Loeb.lean @@ -34,7 +34,7 @@ elab_rules : tactic -- Check for dependencies with the hypotheses in the selection targets checkDependentHyps "iloeb" hyps targets none hs (fun pats => `(tactic| iloeb as $IH generalizing $pats*)) - (some fun pats => `(tactic| iloeb as $IH generalizing! $pats*)) + (fun pats => `(tactic| iloeb as $IH generalizing! $pats*)) let expr ← iRevertIntro hyps goal targets fun {prop _ _} hyps goal k => do let some _ ← ProofModeM.trySynthInstanceQ q(BI.BILoeb $prop) diff --git a/Iris/Iris/ProofMode/Tactics/Revert.lean b/Iris/Iris/ProofMode/Tactics/Revert.lean index b9c3f1bd6..0a65ff461 100644 --- a/Iris/Iris/ProofMode/Tactics/Revert.lean +++ b/Iris/Iris/ProofMode/Tactics/Revert.lean @@ -176,8 +176,7 @@ def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} (explicitTargets : List SelTarget) (inductionTarget : Option FVarId) (selPats : TSyntaxArray `selPat) - (mkTacticExplicit : TSyntaxArray `selPat → ProofModeM (TSyntax `tactic)) - (mkTacticImplicit : Option <| TSyntaxArray `selPat → ProofModeM (TSyntax `tactic)) : + (mkTacticExplicit mkTacticImplicit : TSyntaxArray `selPat → ProofModeM (TSyntax `tactic)) : ProofModeM Unit := do let ⟨missingPureHyps, missingIrisHyps, allPureFVarsSorted⟩ ← getDependentHyps hyps explicitTargets inductionTarget true @@ -224,9 +223,8 @@ def checkDependentHyps {u} {prop : Q(Type $u)} {bi} {e : Q($prop)} Lean.Meta.Tactic.TryThis.addSuggestion oldTactic newTactic -- Suggest a fixed tactic with implicit generalisations - mkTacticImplicit.forM fun mkTacticImplicit => do - let newTactic ← mkTacticImplicit selPats - Lean.Meta.Tactic.TryThis.addSuggestion oldTactic newTactic + let newTactic ← mkTacticImplicit selPats + Lean.Meta.Tactic.TryThis.addSuggestion oldTactic newTactic -- Log the error and attach the clickable suggestion throwError m!"{tacticName}: The following hypotheses depend on variables in \ @@ -271,7 +269,7 @@ elab_rules : tactic | `(tactic| irevert $pats:selPat*) => do -- Check for dependencies with the hypotheses in the selection targets checkDependentHyps "irevert" hyps targets none pats (fun pats => `(tactic| irevert $pats*)) - (some fun pats => `(tactic| irevert! $pats*)) + (fun pats => `(tactic| irevert! $pats*)) let expr ← iRevertCore targets hyps goal mvar.assign expr From bc5684dd1fe8bc50da34e38dd3bc028c8952c199 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 20 Jun 2026 18:15:18 +0200 Subject: [PATCH 171/181] Code formatting 2 --- Iris/Iris/ProofMode/Tactics/Induction.lean | 30 +++++---- Iris/Iris/ProofMode/Tactics/Loeb.lean | 9 ++- Iris/Iris/ProofMode/Tactics/Revert.lean | 73 +++++++++++----------- 3 files changed, 60 insertions(+), 52 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 79781593d..8382ad513 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -338,11 +338,19 @@ private def generalizeTermWithFVar (x : TSyntax `term) : TacticM FVarId := do Similar to the regular `induction` tactic, the following syntax is available. - `iinduction e using r`: to specify the induction principle `r`. - - `iinduction e generalizing z₁ ... zₙ`: to generalise `z₁ ... zₙ`, + - `iinduction e generalizing z₁ … zₙ`: to generalise `z₁ … zₙ`, which is expressed as a selection pattern. Both Iris hypotheses and pure - Lean hypotheses can be generalised. - - `iinduction e with | constr₁ => tac₁ | ... | constrₙ => tacₙ`: - arguments are optionally given names or otherwise remain inaccessible. + Lean hypotheses can be generalised. Hypotheses dependent on any of + `z₁ … zₙ` or the induction target `e` must themselves be included in + the `generalizing` clause. + - `iinduction e generalizing! z₁ … zₙ`: similar to + `iinduction e generalizing z₁ … zₙ`, except that hypotheses dependent + on any of `z₁ … zₙ` or the induction target `e` are implicitly generalised. + - `iinduction e with tac | constr₁ => tac₁ | … | constrₙ => tacₙ`: + where `constr₁ … constrₙ` are names of the constructor, variables and + induction hypotheses, while `tac₁ … tacₙ` are the corresponding tactic + sequences. The optional first tactic `tac` is applied to all induction + subgoals. As an example, consider the following Iris context, where `n : Nat`. @@ -354,19 +362,17 @@ private def generalizeTermWithFVar (x : TSyntax `term) : TacticM FVarId := do □HT : T n ``` - By applying `iinduction n generalizing HT`, all spatial hypotheses - (`HP` and `HS`) are reverted. The hypothesis `HT` must also be reverted - because it involves the induction target `n`. - By applying `iinduction n generalizing HQ HT`, the hypotheses `HQ` are additionally reverted and thus included as a wand premise in the induction - hypothesis. + hypothesis. The hypothesis `HT` must also be reverted + because it involves the induction target `n`. Alternatively, one can use + `iinduction n generalizing! HQ` so that `HT` is generalised implicitly. One can also generalise pure variables in the regular Lean context. If there exists some pure/Iris hypotheses that is forward-dependent, they - should also be included in the `generalizing` clause. In the example above, - instead of `iinduction n generalizing %m HT`, one should use - `iinduction n generalizing %m HQ HT`. + should also be included in the `generalizing` clause. For the above example, + `iinduction n generalizing %m HQ HT` and `iinduction n generalizing! %m` + are equivalent. -/ elab_rules : tactic | `(tactic| iinduction $x $[using $r]? generalizing $genSelPats* $[$alts]?) => do diff --git a/Iris/Iris/ProofMode/Tactics/Loeb.lean b/Iris/Iris/ProofMode/Tactics/Loeb.lean index 6350ccc4d..fcd938504 100644 --- a/Iris/Iris/ProofMode/Tactics/Loeb.lean +++ b/Iris/Iris/ProofMode/Tactics/Loeb.lean @@ -19,9 +19,14 @@ syntax (name := iloeb) "iloeb" " as " binderIdent (generalizingSelPats)? : tacti /-- `iloeb as IH generalizing hs` applies Löb induction in the current goal using the induction hypothesis `IH`, optionally with other variables can be - generalized over through the `generalizing selPat*` syntax. + generalised over through the `generalizing selPat*` syntax. All spatial + hypothesis are generalised in the induction hypothesis. Hypotheses dependent + on those included in the selection pattern `hs` must also themselves be + included in `hs`. - All spatial hypothesis are generalized in the induction hypothesis. + `iloeb as IH generalizing! hs` is the same as `iloeb as IH generalizing hs`, + except that all hypotheses dependent on any hypothesis in the selection + pattern `hs` are implicitly reverted. -/ elab_rules : tactic | `(tactic| iloeb as $IH:binderIdent generalizing $hs:selPat*) => do diff --git a/Iris/Iris/ProofMode/Tactics/Revert.lean b/Iris/Iris/ProofMode/Tactics/Revert.lean index 0a65ff461..216c9110e 100644 --- a/Iris/Iris/ProofMode/Tactics/Revert.lean +++ b/Iris/Iris/ProofMode/Tactics/Revert.lean @@ -19,8 +19,8 @@ namespace Iris.ProofMode public section open BI Std +/- Syntax for `iinduction` and `iloeb` -/ declare_syntax_cat generalizingSelPats - syntax " generalizing " (ppSpace colGt selPat)* : generalizingSelPats syntax " generalizing! " (ppSpace colGt selPat)* : generalizingSelPats @@ -244,46 +244,43 @@ def iRevertCore (targets : List SelTarget) {u : Level} {prop: Q(Type $u)} let pf' : Q($(st.e) ⊢ $(st.goal)) ← withoutFVars (u := 0) st.reverted (k st.hyps st.goal) return q($(st.pf) $pf') +syntax (name := irevert) "irevert " (colGt ppSpace selPat)+ : tactic +syntax (name := irevert!) "irevert! " (colGt ppSpace selPat)+ : tactic + /-- `irevert pats` reverts the hypotheses specified by the selection pattern `pats` - from the Iris contexts back into the regular Lean context. Pure/Iris hypotheses + from context back into the proof goal. Pure/Iris hypotheses dependent on any hypothesis specified by `pats` must also themselves be included in the selection pattern. --/ -syntax (name := irevert) "irevert " (colGt ppSpace selPat)+ : tactic -/-- - `irevert! pats` reverts the hypotheses specified by the selection pattern `pats` - from the Iris contexts back into the regular Lean context. Pure/Iris hypotheses - dependent on any hypothesis specified by `pats` are also reverted. + `irevert! pats` is similar to `irevert pats`, except that pure/Iris hypotheses + dependent on any hypothesis specified by `pats` are automatically reverted. -/ -syntax (name := irevert!) "irevert! " (colGt ppSpace selPat)+ : tactic - -elab_rules : tactic | `(tactic| irevert $pats:selPat*) => do - let parsedPats ← liftMacroM <| SelPat.parse pats - - ProofModeM.runTactic fun mvar { hyps, goal, .. } => do - -- Parse the selection patterns provided by the tactic user - let targets ← SelPat.resolve hyps parsedPats - - -- Check for dependencies with the hypotheses in the selection targets - checkDependentHyps "irevert" hyps targets none pats - (fun pats => `(tactic| irevert $pats*)) - (fun pats => `(tactic| irevert! $pats*)) - - let expr ← iRevertCore targets hyps goal - mvar.assign expr - -elab_rules : tactic | `(tactic| irevert! $pats:selPat*) => do - let parsedPats ← liftMacroM <| SelPat.parse pats - - ProofModeM.runTactic fun mvar { hyps, goal, .. } => do - -- Parse the selection patterns provided by the tactic user - let explicitTargets ← SelPat.resolve hyps parsedPats - -- Find all dependent hypotheses - let ⟨_, missingIrisHyps, allPureFVarsSorted⟩ ← getDependentHyps hyps explicitTargets none true - -- Obtain the selection targets, including dependent ones - let targets := getCompleteSelTargets explicitTargets missingIrisHyps allPureFVarsSorted - - let expr ← iRevertCore targets hyps goal - mvar.assign expr +elab_rules : tactic + | `(tactic| irevert $pats:selPat*) => do + let parsedPats ← liftMacroM <| SelPat.parse pats + + ProofModeM.runTactic fun mvar { hyps, goal, .. } => do + -- Parse the selection patterns provided by the tactic user + let targets ← SelPat.resolve hyps parsedPats + + -- Check for dependencies with the hypotheses in the selection targets + checkDependentHyps "irevert" hyps targets none pats + (fun pats => `(tactic| irevert $pats*)) + (fun pats => `(tactic| irevert! $pats*)) + + let expr ← iRevertCore targets hyps goal + mvar.assign expr + | `(tactic| irevert! $pats:selPat*) => do + let parsedPats ← liftMacroM <| SelPat.parse pats + + ProofModeM.runTactic fun mvar { hyps, goal, .. } => do + -- Parse the selection patterns provided by the tactic user + let explicitTargets ← SelPat.resolve hyps parsedPats + -- Find all dependent hypotheses + let ⟨_, missingIrisHyps, allPureFVarsSorted⟩ ← getDependentHyps hyps explicitTargets none true + -- Obtain the selection targets, including dependent ones + let targets := getCompleteSelTargets explicitTargets missingIrisHyps allPureFVarsSorted + + let expr ← iRevertCore targets hyps goal + mvar.assign expr From ff45f2b1c73ae092450dcb03a0288994b57d0218 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 20 Jun 2026 18:31:23 +0200 Subject: [PATCH 172/181] Code formatting 3 --- Iris/Iris/ProofMode/Tactics/Induction.lean | 28 ++++++++++------------ Iris/Iris/ProofMode/Tactics/Revert.lean | 2 +- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 8382ad513..554fe2833 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -377,10 +377,8 @@ private def generalizeTermWithFVar (x : TSyntax `term) : TacticM FVarId := do elab_rules : tactic | `(tactic| iinduction $x $[using $r]? generalizing $genSelPats* $[$alts]?) => do let fvar ← generalizeTermWithFVar x - -- Parse the recursor name provided by the user let recName := r.map (·.getId) - -- Parse the list of alternative names supplied by the user let parsedAlts ← alts.mapM parseInductionAlts @@ -399,26 +397,26 @@ elab_rules : tactic mvar.assign pf | `(tactic| iinduction $x $[using $r]? $[generalizing! $genSelPats*]? $[$alts]?) => do let fvar ← generalizeTermWithFVar x - -- Parse the recursor name provided by the user let recName := r.map (·.getId) - -- Parse the list of alternative names supplied by the user let parsedAlts ← alts.mapM parseInductionAlts ProofModeM.runTactic λ mvar { hyps, goal, .. } => do - let genSelTargets ← do match genSelPats with - | none => - let ⟨_, missingIrisHyps, allPureFVarsSorted⟩ ← getDependentHyps hyps [] fvar false - pure <| getCompleteSelTargets [] missingIrisHyps allPureFVarsSorted - | some genSelPats => - -- Parse the selection patterns provided by the tactic user - let parsedGenSelPats ← liftMacroM <| SelPat.parse genSelPats - let genSelTargets ← SelPat.resolve hyps parsedGenSelPats - -- Find all dependent hypotheses - let ⟨_, missingIrisHyps, allPureFVarsSorted⟩ ← getDependentHyps hyps genSelTargets fvar false - -- Obtain the selection targets, including dependent ones + let mkGenSelTargets := fun genSelTargets => do + let ⟨_, missingIrisHyps, allPureFVarsSorted⟩ ← + getDependentHyps hyps genSelTargets fvar false pure <| getCompleteSelTargets genSelTargets missingIrisHyps allPureFVarsSorted + let genSelTargets ← + match genSelPats with + | none => mkGenSelTargets [] + | some genSelPats => + -- Parse the selection patterns provided by the tactic user + let parsedGenSelPats ← liftMacroM <| SelPat.parse genSelPats + let genSelTargets ← SelPat.resolve hyps parsedGenSelPats + -- Include dependent hypotheses as well + mkGenSelTargets genSelTargets + let pf ← iInductionCore hyps goal fvar parsedAlts recName genSelTargets mvar.assign pf diff --git a/Iris/Iris/ProofMode/Tactics/Revert.lean b/Iris/Iris/ProofMode/Tactics/Revert.lean index 216c9110e..704529f9b 100644 --- a/Iris/Iris/ProofMode/Tactics/Revert.lean +++ b/Iris/Iris/ProofMode/Tactics/Revert.lean @@ -161,7 +161,7 @@ def getCompleteSelTargets (explicitTargets : List SelTarget) let explicitIrisTargets := explicitTargets.filter <| fun t => match t.kind with | .ipm _ => true | _ => false let implicitIrisTargets := missingIrisHyps.map <| - fun ⟨_, ivar, _⟩ => { kind := .ipm ivar, explicit := false} + fun ⟨_, ivar, _⟩ => { kind := .ipm ivar, explicit := false } pureTargets ++ explicitIrisTargets ++ implicitIrisTargets /-- From aabdbe23a153dd63ff7dccebea1d30cf6b650f32 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 20 Jun 2026 18:47:21 +0200 Subject: [PATCH 173/181] Reduce code repetition with `iLoebCore` --- Iris/Iris/ProofMode/Tactics/Loeb.lean | 40 +++++++++++---------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Loeb.lean b/Iris/Iris/ProofMode/Tactics/Loeb.lean index fcd938504..dad78526e 100644 --- a/Iris/Iris/ProofMode/Tactics/Loeb.lean +++ b/Iris/Iris/ProofMode/Tactics/Loeb.lean @@ -16,6 +16,20 @@ public meta section syntax (name := iloeb) "iloeb" " as " binderIdent (generalizingSelPats)? : tactic +private def iLoebCore {u} {prop : Q(Type u)} {bi : Q(BI $prop)} {e} + (hyps : Hyps bi e) (goal : Q($prop)) (targets : List SelTarget) + (IH : TSyntax `Lean.binderIdent) : ProofModeM Q($e ⊢ $goal) := + iRevertIntro hyps goal targets fun {prop _ _} hyps goal k => do + let some _ ← ProofModeM.trySynthInstanceQ q(BI.BILoeb $prop) + | throwError m!"iloeb: no `{←ppExpr q(BI.BILoeb $prop)}` instance found" + let pf := q(BI.loeb_wand_intuitionistically (P := $goal)) + let pf' ← do + -- We have applied `BI.loeb_wand_intuitionistically` + let goal := q(iprop(□ (□ ▷ $goal -∗ $goal))) + iModIntroCore hyps goal (← `(_)) fun hyps goal => do + iIntroCore hyps goal [(IH, .intro <| .intuitionistic <| .one IH)] (k · · addBIGoal) + return q($(pf').trans $pf) + /-- `iloeb as IH generalizing hs` applies Löb induction in the current goal using the induction hypothesis `IH`, optionally with other variables can be @@ -41,18 +55,7 @@ elab_rules : tactic (fun pats => `(tactic| iloeb as $IH generalizing $pats*)) (fun pats => `(tactic| iloeb as $IH generalizing! $pats*)) - let expr ← iRevertIntro hyps goal targets fun {prop _ _} hyps goal k => do - let some _ ← ProofModeM.trySynthInstanceQ q(BI.BILoeb $prop) - | throwError m!"iloeb: no `{←ppExpr q(BI.BILoeb $prop)}` instance found" - let pf := q(BI.loeb_wand_intuitionistically (P := $goal)) - let pf' ← do - -- We have applied `BI.loeb_wand_intuitionistically` - let goal := q(iprop(□ (□ ▷ $goal -∗ $goal))) - iModIntroCore hyps goal (← `(_)) fun hyps goal => do - iIntroCore hyps goal [(IH, .intro <| .intuitionistic <| .one IH)] (k · · addBIGoal) - - return q($(pf').trans $pf) - + let expr ← iLoebCore hyps goal targets IH mvid.assign expr | `(tactic| iloeb as $IH:binderIdent $[generalizing! $hs:selPat*]?) => do ProofModeM.runTactic fun mvid { hyps, goal, .. } => do @@ -68,16 +71,5 @@ elab_rules : tactic -- Obtain the selection targets, including dependent ones pure <| getCompleteSelTargets targets missingIrisHyps allPureFVarsSorted - let expr ← iRevertIntro hyps goal targets fun {prop _ _} hyps goal k => do - let some _ ← ProofModeM.trySynthInstanceQ q(BI.BILoeb $prop) - | throwError m!"iloeb: no `{←ppExpr q(BI.BILoeb $prop)}` instance found" - let pf := q(BI.loeb_wand_intuitionistically (P := $goal)) - let pf' ← do - -- We have applied `BI.loeb_wand_intuitionistically` - let goal := q(iprop(□ (□ ▷ $goal -∗ $goal))) - iModIntroCore hyps goal (← `(_)) fun hyps goal => do - iIntroCore hyps goal [(IH, .intro <| .intuitionistic <| .one IH)] (k · · addBIGoal) - - return q($(pf').trans $pf) - + let expr ← iLoebCore hyps goal targets IH mvid.assign expr From 11d0baa3dbcb764b81021f00a30531a91e60037c Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 20 Jun 2026 20:52:52 +0200 Subject: [PATCH 174/181] Update `ProofMode/Porting.lean` --- Iris/Iris/ProofMode/Porting.lean | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Iris/Iris/ProofMode/Porting.lean b/Iris/Iris/ProofMode/Porting.lean index dad0da8f9..f239a4f0a 100644 --- a/Iris/Iris/ProofMode/Porting.lean +++ b/Iris/Iris/ProofMode/Porting.lean @@ -51,7 +51,7 @@ import Iris.Std.RocqPorting #rocq_concept proofmode "Tactics" "iCombine" ported "icombine" #rocq_concept proofmode "Tactics" "iIntros (basic)" ported "iintro" #rocq_concept proofmode "Tactics" "iIntros (all intro patterns)" missing "" -#rocq_concept proofmode "Tactics" "iInduction" missing "" +#rocq_concept proofmode "Tactics" "iInduction" ported "iinduction" #rocq_concept proofmode "Tactics" "iLöb" ported "iloeb" #rocq_concept proofmode "Tactics" "iAssert" ported "ihave _ : _" #rocq_concept proofmode "Tactics" "iRewrite" ported "irewrite" From 0ae8fb352000c2577b5cd65b93fee1fd356d6c8c Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 20 Jun 2026 21:27:48 +0200 Subject: [PATCH 175/181] Update `tactics.md` --- Iris/tactics.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Iris/tactics.md b/Iris/tactics.md index f9e465ec0..c41902d2a 100644 --- a/Iris/tactics.md +++ b/Iris/tactics.md @@ -12,7 +12,8 @@ The proof mode maintains three contexts: the *pure* (Lean) context, the *intuiti - `irename` *H* `=>` *H'* — Rename the hypothesis *H* to *H'*. - `irename :` *term* `=>` *H'* — Rename the hypothesis whose statement matches *term* to *H'*. - `iclear` [*selPats*](#selection-patterns) — Discard the hypotheses selected by [*selPats*](#selection-patterns). -- `irevert` [*selPats*](#selection-patterns) — Revert the selected hypotheses (proof mode or pure Lean hypotheses) into the goal. +- `irevert` [*selPats*](#selection-patterns) — Revert the selected hypotheses (proof mode or pure Lean hypotheses) into the goal. An Iris hypothesis *H* in *selPats* is reverted as a wand premise. If a pure hypothesis *H* in *selPats* has a type `φ` such that `φ : Prop`, then *H* is reverted as a premise. If *x* in *selPats* has a type `α` such that `α` is non-`Prop`, then *x* is reverted as a universally quantified variable. For every hypothesis *H* being reverted, all hypotheses dependent on *H* must also be reverted. +- `irevert!` [*selPats*](#selection-patterns) — similar to `irevert` [*selPats*](#selection-patterns), except that for every hypothesis in *selPats*, hypotheses dependent on *H* are also implicitly reverted. - `ipure` *H* — Move the pure hypothesis *H* into the Lean context. - `iintuitionistic` *H* — Move *H* to the intuitionistic context. Equivalent to `icases H with #H`. - `ispatial` *H* — Move *H* to the spatial context. Equivalent to `icases H with ∗H`. @@ -57,7 +58,10 @@ The proof mode maintains three contexts: the *pure* (Lean) context, the *intuiti ## Rewriting and Induction - `irewrite [`*rules*`]` (`at` *H* | `at ⊢`)? — Rewrite with internal equalities (`≡`). Each rule is a [*pmTerm*](#proof-mode-terms), optionally prefixed with `←` for right-to-left rewriting. Rewrites in the goal by default or in hypothesis *H*. Supports `(occs := ...)` config. Example: `irewrite [← Heq $$ %b] at H`. -- `iloeb as` *IH* (`generalizing` [*selPats*](#selection-patterns))? — Löb induction: adds the induction hypothesis *IH* (guarded by `▷`) to the intuitionistic context. All spatial hypotheses — plus anything selected by [*selPats*](#selection-patterns), including pure variables via `%x` — are generalized into the induction hypothesis. +- `iloeb as` *IH* (`generalizing` [*selPats*](#selection-patterns))? — Löb induction: adds the induction hypothesis *IH* (guarded by `▷`) to the intuitionistic context. All spatial hypotheses — plus anything selected by [*selPats*](#selection-patterns), including pure variables via `%x` — are generalized into the induction hypothesis. For every hypothesis *H* in *selPats*, all hypotheses dependent on *H* must also be included in *selPats*. +- `iloeb as` *IH* (`generalizing!` [*selPats*](#selection-patterns))? — same as `iloeb as` *IH* `generalizing!` [*selPats*](#selection-patterns), except that for every hypothesis *H* in [*selPats*](#selection-patterns), all hypotheses dependent on *H* are implicitly also generalised. +- `iinduction` *e* (`using` *r*)? (`generalizing` [*selPats*](#selection-patterns))? (`with` (*tac*)? `|` *constr₁* `=>` *tac₁* `|` ... `|` *constrₙ* `=>` *tacₙ*)? — All spatial hypotheses, as well as hypotheses dependent on *e*, are generalised and included as premises in the induction hypotheses. Furthermore, generalise the hypotheses specified by [*selPats*](#selection-patterns). For every hypothesis *H* in [*selPats*](#selection-patterns), all hypotheses dependent on *H* must also be included in [*selPats*](#selection-patterns). The induction principle is determined by the recursor name *r*, if given, or otherwise the default induction principle is chosen. The `with` clause enables alternative names to be given to variables and induction hypotheses. Each of *constr₁*, ..., *constrₙ* is the constructor name followed by the alternative names. Each of *tac₁*, ..., *tacₙ* is either a tactic sequence for the induction subgoal or a hole (`_` or `?_`); the latter defers the induction subgoal. A tactic *tac* is optionally given in the `with` clause, which is the first tactic applied to all induction subgoals. +- `iinduction` *e* (`using` *r*)? `generalizing!` [*selPats*](#selection-patterns) (`with` (*tac*)? `|` *constr₁* `=>` *tac₁* `|` ... `|` *constrₙ* `=>` *tacₙ*)? — Same as above, except that for every hypothesis *H* in *selPats*, hypotheses dependent on *H* are implicitly generalised. ## Solving Simple Goals From c3ff1e7d259f248062452230077c1ca59afccda4 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 20 Jun 2026 21:39:54 +0200 Subject: [PATCH 176/181] Typo fix --- Iris/tactics.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Iris/tactics.md b/Iris/tactics.md index c41902d2a..19eecced3 100644 --- a/Iris/tactics.md +++ b/Iris/tactics.md @@ -59,7 +59,7 @@ The proof mode maintains three contexts: the *pure* (Lean) context, the *intuiti - `irewrite [`*rules*`]` (`at` *H* | `at ⊢`)? — Rewrite with internal equalities (`≡`). Each rule is a [*pmTerm*](#proof-mode-terms), optionally prefixed with `←` for right-to-left rewriting. Rewrites in the goal by default or in hypothesis *H*. Supports `(occs := ...)` config. Example: `irewrite [← Heq $$ %b] at H`. - `iloeb as` *IH* (`generalizing` [*selPats*](#selection-patterns))? — Löb induction: adds the induction hypothesis *IH* (guarded by `▷`) to the intuitionistic context. All spatial hypotheses — plus anything selected by [*selPats*](#selection-patterns), including pure variables via `%x` — are generalized into the induction hypothesis. For every hypothesis *H* in *selPats*, all hypotheses dependent on *H* must also be included in *selPats*. -- `iloeb as` *IH* (`generalizing!` [*selPats*](#selection-patterns))? — same as `iloeb as` *IH* `generalizing!` [*selPats*](#selection-patterns), except that for every hypothesis *H* in [*selPats*](#selection-patterns), all hypotheses dependent on *H* are implicitly also generalised. +- `iloeb as` *IH* `generalizing!` [*selPats*](#selection-patterns) — same as `iloeb as` *IH* `generalizing` [*selPats*](#selection-patterns), except that for every hypothesis *H* in [*selPats*](#selection-patterns), all hypotheses dependent on *H* are implicitly also generalised. - `iinduction` *e* (`using` *r*)? (`generalizing` [*selPats*](#selection-patterns))? (`with` (*tac*)? `|` *constr₁* `=>` *tac₁* `|` ... `|` *constrₙ* `=>` *tacₙ*)? — All spatial hypotheses, as well as hypotheses dependent on *e*, are generalised and included as premises in the induction hypotheses. Furthermore, generalise the hypotheses specified by [*selPats*](#selection-patterns). For every hypothesis *H* in [*selPats*](#selection-patterns), all hypotheses dependent on *H* must also be included in [*selPats*](#selection-patterns). The induction principle is determined by the recursor name *r*, if given, or otherwise the default induction principle is chosen. The `with` clause enables alternative names to be given to variables and induction hypotheses. Each of *constr₁*, ..., *constrₙ* is the constructor name followed by the alternative names. Each of *tac₁*, ..., *tacₙ* is either a tactic sequence for the induction subgoal or a hole (`_` or `?_`); the latter defers the induction subgoal. A tactic *tac* is optionally given in the `with` clause, which is the first tactic applied to all induction subgoals. - `iinduction` *e* (`using` *r*)? `generalizing!` [*selPats*](#selection-patterns) (`with` (*tac*)? `|` *constr₁* `=>` *tac₁* `|` ... `|` *constrₙ* `=>` *tacₙ*)? — Same as above, except that for every hypothesis *H* in *selPats*, hypotheses dependent on *H* are implicitly generalised. From 77335b65f276e28e8512b958a66b07a353ca5a49 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Sat, 20 Jun 2026 21:47:32 +0200 Subject: [PATCH 177/181] More accurate descriptions in `tactics.md` --- Iris/tactics.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Iris/tactics.md b/Iris/tactics.md index 19eecced3..6b4f2cc97 100644 --- a/Iris/tactics.md +++ b/Iris/tactics.md @@ -60,8 +60,9 @@ The proof mode maintains three contexts: the *pure* (Lean) context, the *intuiti - `irewrite [`*rules*`]` (`at` *H* | `at ⊢`)? — Rewrite with internal equalities (`≡`). Each rule is a [*pmTerm*](#proof-mode-terms), optionally prefixed with `←` for right-to-left rewriting. Rewrites in the goal by default or in hypothesis *H*. Supports `(occs := ...)` config. Example: `irewrite [← Heq $$ %b] at H`. - `iloeb as` *IH* (`generalizing` [*selPats*](#selection-patterns))? — Löb induction: adds the induction hypothesis *IH* (guarded by `▷`) to the intuitionistic context. All spatial hypotheses — plus anything selected by [*selPats*](#selection-patterns), including pure variables via `%x` — are generalized into the induction hypothesis. For every hypothesis *H* in *selPats*, all hypotheses dependent on *H* must also be included in *selPats*. - `iloeb as` *IH* `generalizing!` [*selPats*](#selection-patterns) — same as `iloeb as` *IH* `generalizing` [*selPats*](#selection-patterns), except that for every hypothesis *H* in [*selPats*](#selection-patterns), all hypotheses dependent on *H* are implicitly also generalised. -- `iinduction` *e* (`using` *r*)? (`generalizing` [*selPats*](#selection-patterns))? (`with` (*tac*)? `|` *constr₁* `=>` *tac₁* `|` ... `|` *constrₙ* `=>` *tacₙ*)? — All spatial hypotheses, as well as hypotheses dependent on *e*, are generalised and included as premises in the induction hypotheses. Furthermore, generalise the hypotheses specified by [*selPats*](#selection-patterns). For every hypothesis *H* in [*selPats*](#selection-patterns), all hypotheses dependent on *H* must also be included in [*selPats*](#selection-patterns). The induction principle is determined by the recursor name *r*, if given, or otherwise the default induction principle is chosen. The `with` clause enables alternative names to be given to variables and induction hypotheses. Each of *constr₁*, ..., *constrₙ* is the constructor name followed by the alternative names. Each of *tac₁*, ..., *tacₙ* is either a tactic sequence for the induction subgoal or a hole (`_` or `?_`); the latter defers the induction subgoal. A tactic *tac* is optionally given in the `with` clause, which is the first tactic applied to all induction subgoals. -- `iinduction` *e* (`using` *r*)? `generalizing!` [*selPats*](#selection-patterns) (`with` (*tac*)? `|` *constr₁* `=>` *tac₁* `|` ... `|` *constrₙ* `=>` *tacₙ*)? — Same as above, except that for every hypothesis *H* in *selPats*, hypotheses dependent on *H* are implicitly generalised. +- `iinduction` *e* (`using` *r*)? (`with` (*tac*)? `|` *constr₁* `=>` *tac₁* `|` ... `|` *constrₙ* `=>` *tacₙ*)? — All spatial hypotheses, as well as pure/intuitionistic hypotheses dependent on *e*, are generalised and included as premises in the induction hypotheses. The induction principle is determined by the recursor name *r*, if given, or otherwise the default induction principle is chosen. The `with` clause enables alternative names to be given to variables and induction hypotheses. Each of *constr₁*, ..., *constrₙ* is the constructor name followed by the alternative names. Alternative names can be replaced with holes (`_`) for them to remain inaccessible. Each of *tac₁*, ..., *tacₙ* is either a tactic sequence for the induction subgoal or a hole (`_` or `?_`); the latter defers the induction subgoal. The tactic *tac* is optionally given, which is the first tactic applied to all induction subgoals. +- `iinduction` *e* (`using` *r*)? `generalizing` [*selPats*](#selection-patterns) (`with` (*tac*)? `|` *constr₁* `=>` *tac₁* `|` ... `|` *constrₙ* `=>` *tacₙ*)? — Same as above, except that the hypotheses specified by [*selPats*](#selection-patterns) are also generalised. For every hypothesis *H* in [*selPats*](#selection-patterns), all hypotheses dependent on *H* or *e* must also be included in [*selPats*](#selection-patterns). +- `iinduction` *e* (`using` *r*)? `generalizing!` [*selPats*](#selection-patterns) (`with` (*tac*)? `|` *constr₁* `=>` *tac₁* `|` ... `|` *constrₙ* `=>` *tacₙ*)? — Same as above, except that for every hypothesis *H* in *selPats*, hypotheses dependent on *H* or *e* are implicitly generalised. ## Solving Simple Goals From e91f09eebdd5c9a9943f875861dedc8c564b6e09 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Fri, 26 Jun 2026 10:07:24 +0200 Subject: [PATCH 178/181] Add a test for `irevert!` --- Iris/Iris/Tests/Tactics.lean | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 2ac422a08..2549b8351 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -404,6 +404,13 @@ example [BI PROP] (Φ : Bool → PROP) : ⊢ ∀ x, ⌜x = true⌝ -∗ iintro %x %_ H irevert %x H +/-- Tests `irevert!` which reverts `H2` and `H3` automatically -/ +example [BI PROP] (Φ : Bool → PROP) : + (∀ x, (Φ x -∗ Φ y) -∗ Φ x -∗ Φ y) ∗ (Φ x -∗ Φ y) ∗ Φ x ⊢ Φ y := by + iintro ⟨H1, H2, H3⟩ + irevert! %x + iassumption + end revert -- exists @@ -2933,8 +2940,7 @@ def NTree.childCount : NTree α → Nat /-- An binary relation defined using nested induction -/ inductive NTree.Rel {α β} (R : α → β → Prop) : NTree α → NTree β → Prop | leaf : Rel R .leaf .leaf - | node : ∀ a b ts₁ ts₂, - R a b → List.Forall₂ (Rel R) ts₁ ts₂ → Rel R (.node a ts₁) (.node b ts₂) + | node : ∀ a b ts₁ ts₂, R a b → List.Forall₂ (Rel R) ts₁ ts₂ → Rel R (.node a ts₁) (.node b ts₂) @[induction_eliminator] def NTree.Rel.induction_principle {α β} {R : α → β → Prop} From 348b09378e7f97024f144951ea4b90716244a83b Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Fri, 26 Jun 2026 10:27:12 +0200 Subject: [PATCH 179/181] Add a test for `iloeb` with `generalizing!` clause --- Iris/Iris/Tests/Tactics.lean | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index 2549b8351..e857697a9 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -2671,6 +2671,7 @@ end icombine section iloeb variable {PROP : Type u} [ι₁ : BI PROP] [ι₂ : BILoeb PROP] + -- Tests `iloeb` basic /-- error: unsolved goals @@ -2810,8 +2811,7 @@ error: iloeb: The following hypotheses depend on variables in the `generalizing` • Iris hypothesis `HT` depends on `n` -/ #guard_msgs in -example [BI PROP] [BILoeb PROP] {n : Nat} {P T : Nat → PROP} {Q : Nat → Prop} - {h1 : Q n} {U : (Q n) → Prop} : +example {n : Nat} {P T : Nat → PROP} {Q : Nat → Prop} {h1 : Q n} {U : (Q n) → Prop} : ⊢ □ T n -∗ □ P n := by iintro #HT iloeb as IH generalizing %n @@ -2827,12 +2827,33 @@ error: iloeb: The following hypotheses depend on variables in the `generalizing` • Iris hypothesis `x` (inaccessible name) depends on `n` -/ #guard_msgs in -example [BI PROP] [BILoeb PROP] {n : Nat} {P T : Nat → PROP} {Q : Nat → Prop} - {h1 : Q n} {_ : (Q n) → Prop} : +example {n : Nat} {P T : Nat → PROP} {Q : Nat → Prop} {h1 : Q n} {_ : (Q n) → Prop} : ⊢ □ T n -∗ □ P n := by iintro #_ iloeb as IH generalizing %n +-- Same test as above, except `generalizing!` is used +/-- +error: unsolved goals +PROP : Type u +ι₁ : BI PROP +ι₂ : BILoeb PROP +P T : Nat → PROP +Q : Nat → Prop +n : Nat +h1 : Q n +x✝ : Q n → Prop +⊢ ⏎ + □IH : ▷ ∀ n, ⌜Q n⌝ -∗ ∀ x, □ T n -∗ □ P n + □x✝ : T n + ⊢ □ P n +-/ +#guard_msgs in +example {n : Nat} {P T : Nat → PROP} {Q : Nat → Prop} {h1 : Q n} {_ : (Q n) → Prop} : + ⊢ □ T n -∗ □ P n := by + iintro #_ + iloeb as IH generalizing! %n + end iloeb section iinduction From 462d2d3d21491fd56ed0cd33d551273219f9e5f0 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Fri, 26 Jun 2026 10:35:32 +0200 Subject: [PATCH 180/181] Minor formatting --- Iris/Iris/Tests/Tactics.lean | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Iris/Iris/Tests/Tactics.lean b/Iris/Iris/Tests/Tactics.lean index e857697a9..0624ddcc6 100644 --- a/Iris/Iris/Tests/Tactics.lean +++ b/Iris/Iris/Tests/Tactics.lean @@ -405,7 +405,7 @@ example [BI PROP] (Φ : Bool → PROP) : ⊢ ∀ x, ⌜x = true⌝ -∗ irevert %x H /-- Tests `irevert!` which reverts `H2` and `H3` automatically -/ -example [BI PROP] (Φ : Bool → PROP) : +example [BI PROP] (Φ : Bool → PROP) (x y : Bool) : (∀ x, (Φ x -∗ Φ y) -∗ Φ x -∗ Φ y) ∗ (Φ x -∗ Φ y) ∗ Φ x ⊢ Φ y := by iintro ⟨H1, H2, H3⟩ irevert! %x From 8295517ee7c449e71281635991615862c6d662a5 Mon Sep 17 00:00:00 2001 From: Alvin Tang Date: Fri, 3 Jul 2026 10:07:01 +0200 Subject: [PATCH 181/181] Code refactoring: moving theorem to `public section` --- Iris/Iris/ProofMode/Tactics/Induction.lean | 63 +++++++++++----------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/Iris/Iris/ProofMode/Tactics/Induction.lean b/Iris/Iris/ProofMode/Tactics/Induction.lean index 554fe2833..ef61688be 100644 --- a/Iris/Iris/ProofMode/Tactics/Induction.lean +++ b/Iris/Iris/ProofMode/Tactics/Induction.lean @@ -16,6 +16,39 @@ public meta import Lean.Meta.Tactic.TryThis namespace Iris.ProofMode +public section +open BI + +/-- + This theorem is used for updating the proof in `InductionState` as `addIHs` + iterates through the list of induction hypotheses and introduces them from + the regular Lean context into the intuitionistic context. + + The initial set of hypotheses that exist in the Iris context after applying + Lean's built-in induction step is represented by `P`. Since the spatial + context is empty (that is, all hypotheses exist in intuitionistic context), + `P ⊢ □ P` must hold. + + The proposition `Q` represents these initial hypotheses as well as induction + hypotheses introduced into the intuitionistic context up until the most + recent `addIHs` iteration. At every iteration of `addIHs`, an induction + hypothesis `R` is obtained upon type class synthesis with `IntoIH`. The proof + for `InductionState` is obtained using this theorem accordingly. +-/ +@[rocq_alias tac_revert_ih] +theorem revert_IH [BI PROP] {P Q R : PROP} {φ} + (ih : φ) + (h1 : P ⊢ □ P) + (h2 : P ⊢ Q) + (inst : IntoIH φ P R) : + P ⊢ (Q ∗ □ R) := calc + _ ⊢ □ P := h1 + _ ⊢ □ P ∗ □ P := intuitionistically_sep_dup.mp + _ ⊢ □ P ∗ □ □ P := sep_mono_right intuitionistically_idem.mpr + _ ⊢ □ P ∗ □ R := sep_mono_right <| intuitionistically_mono <| inst.into_ih ih + _ ⊢ □ Q ∗ □ R := sep_mono_left <| intuitionistically_mono h2 + _ ⊢ Q ∗ □ R := sep_mono_left intuitionistically_elim + public meta section open BI Std Lean Elab Tactic Meta Qq Parser.Tactic @@ -83,36 +116,6 @@ private def parseInductionAlts (altsSyntax : TSyntax `Lean.Parser.Tactic.inducti return ⟨tac, parsedAlts, none, altsSyntax⟩ | _ => throwErrorAt altsSyntax "iinduction: invalid syntax" -/-- - This theorem is used for updating the proof in `InductionState` as `addIHs` - iterates through the list of induction hypotheses and introduces them from - the regular Lean context into the intuitionistic context. - - The initial set of hypotheses that exist in the Iris context after applying - Lean's built-in induction step is represented by `P`. Since the spatial - context is empty (that is, all hypotheses exist in intuitionistic context), - `P ⊢ □ P` must hold. - - The proposition `Q` represents these initial hypotheses as well as induction - hypotheses introduced into the intuitionistic context up until the most - recent `addIHs` iteration. At every iteration of `addIHs`, an induction - hypothesis `R` is obtained upon type class synthesis with `IntoIH`. The proof - for `InductionState` is obtained using this theorem accordingly. --/ -@[rocq_alias tac_revert_ih] -theorem revert_IH [BI PROP] {P Q R : PROP} {φ} - (ih : φ) - (h1 : P ⊢ □ P) - (h2 : P ⊢ Q) - (inst : IntoIH φ P R) : - P ⊢ (Q ∗ □ R) := calc - _ ⊢ □ P := h1 - _ ⊢ □ P ∗ □ P := intuitionistically_sep_dup.mp - _ ⊢ □ P ∗ □ □ P := sep_mono_right intuitionistically_idem.mpr - _ ⊢ □ P ∗ □ R := sep_mono_right <| intuitionistically_mono <| inst.into_ih ih - _ ⊢ □ Q ∗ □ R := sep_mono_left <| intuitionistically_mono h2 - _ ⊢ Q ∗ □ R := sep_mono_left intuitionistically_elim - /-- Designed to be a mutable state such that `newHyps` contains induction hypotheses generated by Lean's built-in induction.