Skip to content

Commit cdd8b85

Browse files
feat: selection patterns and iclear/irevert support (#189)
* feat: selection patterns and iclear refactor --------- Co-authored-by: Michael Sammler <noreply@sammler.me>
1 parent 74ce211 commit cdd8b85

File tree

8 files changed

+282
-57
lines changed

8 files changed

+282
-57
lines changed

PORTING.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -361,9 +361,9 @@ Some porting tasks will require other tasks as dependencies, the GitHub issues p
361361
- [ ] with bi specified
362362
- [x] iStopProof
363363
- [x] iRename
364-
- [ ] iClear
364+
- [x] iClear
365365
- [x] basic
366-
- [ ] selection patterns
366+
- [x] selection patterns
367367
- [ ] iEval
368368
- [ ] iSimpl
369369
- [ ] iUnfold
@@ -381,7 +381,7 @@ Some porting tasks will require other tasks as dependencies, the GitHub issues p
381381
- [ ] iRevert
382382
- [x] basic
383383
- [ ] `intoIH` revert
384-
- [ ] selection patterns
384+
- [x] selection patterns
385385
- [x] iPoseProof (Lean: ihave _ := _)
386386
- [ ] iSpecialize
387387
- [x] basic functionality
@@ -437,7 +437,7 @@ Some porting tasks will require other tasks as dependencies, the GitHub issues p
437437
- [ ] `monpred.v`
438438
- [x] `proofmode.v` (ProofMode.lean)
439439
- [-] `reduction.v` (not necessary in Lean)
440-
- [ ] `sel_patterns.v`
440+
- [x] `sel_patterns.v`
441441
- [ ] `spec_patterns.v`
442442
- [x] SIdent
443443
- [x] SPureGoal

src/Iris/ProofMode/Expr.lean

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,18 @@ partial def Hyps.find? {u prop bi} (name : Name) :
9797
| _, .hyp _ name' uniq p ty _ => if name == name' then (uniq, p, ty) else none
9898
| _, .sep _ _ _ _ lhs rhs => rhs.find? name <|> lhs.find? name
9999

100+
partial def Hyps.spatialUniqs {u prop bi} :
101+
∀ {s}, @Hyps u prop bi s → List Name
102+
| _, .emp _ => []
103+
| _, .hyp _ _ uniq p _ _ => if isTrue p then [] else [uniq]
104+
| _, .sep _ _ _ _ lhs rhs => lhs.spatialUniqs ++ rhs.spatialUniqs
105+
106+
partial def Hyps.intuitionisticUniqs {u prop bi} :
107+
∀ {s}, @Hyps u prop bi s → List Name
108+
| _, .emp _ => []
109+
| _, .hyp _ _ uniq p _ _ => if isTrue p then [uniq] else []
110+
| _, .sep _ _ _ _ lhs rhs => lhs.intuitionisticUniqs ++ rhs.intuitionisticUniqs
111+
100112
variable (oldUniq new : Name) {prop : Q(Type u)} {bi : Q(BI $prop)} in
101113
def Hyps.rename : ∀ {e}, Hyps bi e → Option (Hyps bi e)
102114
| _, .emp _ => none
@@ -331,6 +343,25 @@ partial def Hyps.findDependencyOnFVar {prop : Q(Type u)} {bi : Q(BI $prop)}
331343
if (ty : Expr).containsFVar fvarId then some (name, uniq, p, ty)
332344
else none
333345

346+
/-- Check that removing the Lean local `fvarId` leaves no dangling dependencies in the
347+
proofmode context, an optional goal, or remaining Lean locals not accepted by `allowedDep`. -/
348+
def Hyps.checkRemovableFVar {prop : Q(Type u)} {bi : Q(BI $prop)} {e}
349+
(hyps : Hyps bi e) (tac : String) (fvarId : FVarId)
350+
(goal? : Option Expr := none) (allowedDep : FVarId → Bool := fun _ => false) :
351+
MetaM LocalDecl := do
352+
let ldecl ← fvarId.getDecl
353+
if let some (name, _, _, _) := hyps.findDependencyOnFVar fvarId then
354+
throwError "{tac}: proofmode hypothesis {name} depends on {ldecl.userName}"
355+
if let some goal := goal? then
356+
let goal ← instantiateMVars goal
357+
if goal.containsFVar fvarId then
358+
throwError "{tac}: goal depends on {ldecl.userName}"
359+
let deps ← collectForwardDeps #[mkFVar fvarId] false
360+
if let some dep := deps.find? (fun e => e.fvarId! != fvarId && !allowedDep e.fvarId!) then
361+
let depDecl := (← getLCtx).getFVar! dep
362+
throwError "{tac}: Lean hypothesis {depDecl.userName} depends on {ldecl.userName}"
363+
return ldecl
364+
334365
end dependency
335366

336367
end hyps

src/Iris/ProofMode/Patterns.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ module
33
public import Iris.ProofMode.Patterns.CasesPattern
44
public import Iris.ProofMode.Patterns.IntroPattern
55
public import Iris.ProofMode.Patterns.ProofModeTerm
6+
public import Iris.ProofMode.Patterns.SelPattern
67
public import Iris.ProofMode.Patterns.SpecPattern
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/-
2+
Copyright (c) 2026 Yunsong Yang. All rights reserved.
3+
Released under Apache 2.0 license as described in the file LICENSE.
4+
Authors: Yunsong Yang
5+
-/
6+
module
7+
8+
public meta import Iris.ProofMode.ProofModeM
9+
10+
@[expose] public section
11+
12+
namespace Iris.ProofMode
13+
open Lean Meta Std
14+
15+
declare_syntax_cat selPat
16+
17+
syntax ident : selPat
18+
syntax "%" : selPat
19+
syntax "%" noWs ident : selPat
20+
syntax "#" : selPat
21+
syntax "∗" : selPat
22+
23+
inductive SelPat
24+
| pure
25+
| intuitionistic
26+
| spatial
27+
| ident (name : Ident)
28+
| leanIdent (name : Ident)
29+
deriving Repr, Inhabited
30+
31+
partial def SelPat.parseOne (pat : TSyntax `selPat) : MacroM SelPat := do
32+
match go ⟨← expandMacros pat⟩ with
33+
| none => Macro.throwUnsupported
34+
| some pat => return pat
35+
where
36+
go : TSyntax `selPat → Option SelPat
37+
| `(selPat| %$name:ident) => some <| .leanIdent name
38+
| `(selPat| %) => some .pure
39+
| `(selPat| #) => some .intuitionistic
40+
| `(selPat| ∗) => some .spatial
41+
| `(selPat| $name:ident) => some <| .ident name
42+
| _ => none
43+
44+
partial def SelPat.parse (pats : TSyntaxArray `selPat) : MacroM (List SelPat) := do
45+
return (← pats.mapM SelPat.parseOne).toList
46+
47+
public meta section
48+
49+
abbrev SelTarget := Name ⊕ FVarId
50+
51+
/-- Resolve selection patterns to concrete proofmode hypotheses (`.inl`) and Lean locals (`.inr`). -/
52+
def SelPat.resolveOne (hyps : Hyps bi e) : SelPat → ProofModeM (List SelTarget)
53+
| .ident name =>
54+
return [.inl (← hyps.findWithInfo name)]
55+
| .leanIdent name => do
56+
let ldecl ← getLocalDeclFromUserName name.getId
57+
return [.inr ldecl.fvarId]
58+
| .intuitionistic =>
59+
return hyps.intuitionisticUniqs.map .inl
60+
| .spatial =>
61+
return hyps.spatialUniqs.map .inl
62+
| .pure => do
63+
-- `%` selects user-facing Lean pure assumptions, so we keep only `Prop` hypotheses.
64+
let mut hyps := #[]
65+
for ldecl in ← getLCtx do
66+
if ldecl.isAuxDecl || ldecl.isImplementationDetail then
67+
continue
68+
if ! (← isProp ldecl.type) then
69+
continue
70+
hyps := hyps.push (.inr ldecl.fvarId)
71+
return hyps.toList
72+
73+
def SelPat.resolve (hyps : Hyps bi e) (pats : List SelPat) :
74+
ProofModeM (List SelTarget) := do
75+
return (← pats.flatMapM (SelPat.resolveOne hyps)).eraseDups
76+
77+
end
78+
79+
end Iris.ProofMode

src/Iris/ProofMode/ProofModeM.lean

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,19 @@ def addBIGoal {prop : Q(Type u)} {bi : Q(BI $prop)}
4545
modify ({goals := ·.goals.push m.mvarId!})
4646
pure m
4747

48+
/-- Create a new BI goal with the given hypotheses and goal, but without some fvars, and add it to the proof mode state.
49+
It is the responsibility of the user of this function to check that the variables to clear can actually be cleared (e.g. using
50+
`Hyps.checkRemovableFVar`). -/
51+
def addBIGoalWithoutFVars {prop : Q(Type u)} {bi : Q(BI $prop)}
52+
{e} (hyps : Hyps bi e) (goal : Q($prop)) (toClear : Array FVarId) (name : Name := .anonymous) : ProofModeM Q($e ⊢ $goal) := do
53+
let goal ← mkBIGoal hyps goal name
54+
let (clearedGoalId, cleared) ← goal.mvarId!.tryClearMany' toClear
55+
unless cleared.size == toClear.size do
56+
-- this should not happen since the caller of this function should call Hyps.checkRemovableFVar first
57+
throwError "internal error: failed to clear all selected Lean hypotheses"
58+
modify ({goals := ·.goals.push clearedGoalId})
59+
return Expr.mvar clearedGoalId
60+
4861
/-- Add an existing metavariable as a goal to the proof mode state if it is not already assigned or present. -/
4962
def addMVarGoal (m : MVarId) (name : Name := .anonymous) : ProofModeM Unit := do
5063
if ← m.isAssignedOrDelayedAssigned then

src/Iris/ProofMode/Tactics/Clear.lean

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
/-
22
Copyright (c) 2022 Lars König. All rights reserved.
33
Released under Apache 2.0 license as described in the file LICENSE.
4-
Authors: Lars König, Mario Carneiro, Michael Sammler
4+
Authors: Lars König, Mario Carneiro, Michael Sammler, Yunsong Yang
55
-/
66
module
77

88
import Iris.BI
99
import Iris.ProofMode.Classes
10+
public meta import Iris.ProofMode.Patterns.SelPattern
1011
public meta import Iris.ProofMode.Tactics.Basic
1112

1213
namespace Iris.ProofMode
@@ -34,10 +35,32 @@ def iClearCore {prop : Q(Type u)} (_bi : Q(BI $prop)) (e e' : Q($prop))
3435
| throwError "iclear: {out} is not affine and the goal not absorbing"
3536
return q(clear_spatial (A:=$out) $pf)
3637

37-
elab "iclear" colGt hyp:ident : tactic => do
38-
ProofModeM.runTactic λ mvar { bi, e, hyps, goal, .. } => do
38+
private structure ClearState {u} {prop : Q(Type u)} {bi : Q(BI $prop)} (origE goal : Q($prop)) where
39+
(e : Q($prop)) (hyps : Hyps bi e)
40+
pf : Q(($e ⊢ $goal) → ($origE ⊢ $goal))
3941

40-
let uniq ← hyps.findWithInfo hyp
41-
let ⟨e', hyps', _, out', p, _, pf⟩ := hyps.remove true uniq
42-
let m ← addBIGoal hyps' goal
43-
mvar.assign ((← iClearCore bi e e' p out' goal pf).app m)
42+
private def ClearState.clearProofModeHyp {u prop bi origE goal} :
43+
@ClearState u prop bi origE goal → Name →
44+
ProofModeM (@ClearState u prop bi origE goal)
45+
| { e, hyps, pf }, uniq => do
46+
let ⟨e', hyps', _, out', p, _, hrem⟩ := hyps.remove true uniq
47+
let step ← iClearCore bi e e' p out' goal hrem
48+
let pf' : Q(($e' ⊢ $goal) → ($origE ⊢ $goal)) := q(λ h => $pf ($step h))
49+
return { e := e', hyps := hyps', pf := pf' }
50+
51+
elab "iclear" pats:(colGt selPat)+ : tactic => do
52+
let pats ← liftMacroM <| SelPat.parse pats
53+
54+
ProofModeM.runTactic λ mvar { e, hyps, goal, .. } => do
55+
let (uniqs, fvars) := (← SelPat.resolve hyps pats).partitionMap id
56+
57+
-- Clear the selected Iris hypotheses first, updating the proof-mode context and proof term.
58+
let mut st : ClearState e goal := { e, hyps, pf := q(fun h => h) }
59+
for uniq in uniqs do st ← st.clearProofModeHyp uniq
60+
61+
-- Lean locals are cleared afterwards; first ensure no remaining hypothesis or goal depends on them.
62+
for fvar in fvars do
63+
let _ ← st.hyps.checkRemovableFVar "iclear" fvar (some goal) fvars.contains
64+
65+
let pf' ← addBIGoalWithoutFVars st.hyps goal fvars.reverse.toArray
66+
mvar.assign q($(st.pf) $pf')

src/Iris/ProofMode/Tactics/Revert.lean

Lines changed: 22 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Authors: Oliver Soeser, Yunsong Yang
66
module
77

88
public import Iris.ProofMode.ClassesMake
9+
public meta import Iris.ProofMode.Patterns.SelPattern
910
public meta import Iris.ProofMode.Tactics.Basic
1011

1112
namespace Iris.ProofMode
@@ -36,40 +37,24 @@ open Lean Elab Tactic Meta Qq
3637
`reverted` collects lean variables already reverted. This is necessary for dependency checks
3738
since they are only cleared from the Lean context for the final goal.
3839
-/
39-
private structure RevertState {prop : Q(Type u)} (bi : Q(BI $prop)) (origE origGoal : Q($prop)) where
40+
private structure RevertState {prop : Q(Type u)} {bi : Q(BI $prop)} (origE origGoal : Q($prop)) where
4041
(e : Q($prop)) (hyps : Hyps bi e) (goal : Q($prop))
4142
(reverted : Array FVarId := #[])
4243
pf : Q(($e ⊢ $goal) → ($origE ⊢ $origGoal))
4344

4445
/-- Revert a proofmode hypothesis by turning it into a wand premise. -/
4546
private def RevertState.revertProofModeHyp
46-
: @RevertState u prop bi origE origGoal → TSyntax `ident →
47-
ProofModeM (RevertState bi origE origGoal)
48-
| { hyps, goal, reverted, pf, .. }, hyp => do
49-
let uniq ← hyps.findWithInfo hyp
47+
: @RevertState u prop bi origE origGoal → Name →
48+
ProofModeM (@RevertState u prop bi origE origGoal)
49+
| { hyps, goal, reverted, pf, .. }, uniq => do
5050
let ⟨e', hyps', out, _, _, _, hΔ⟩ := hyps.remove true uniq
5151
return { e := e', hyps := hyps', goal := q(wand $out $goal), reverted,
5252
pf := q(fun h => $pf (wand_revert $hΔ h)) }
5353

54-
/-- Check that reverting the Lean local named by `hyp` will not leave dangling dependencies. -/
55-
private def RevertState.checkLeanDependencies
56-
: @RevertState u prop bi origE origGoal → TSyntax `ident → MetaM LocalDecl
57-
| { hyps, reverted, .. }, hyp => do
58-
let ldecl ← getLocalDeclFromUserName hyp.getId
59-
let f := ldecl.fvarId
60-
if let some (name, _, _, _) := hyps.findDependencyOnFVar f then
61-
throwError "irevert: proofmode hypothesis {name} depends on {hyp.getId}"
62-
let deps ← collectForwardDeps #[mkFVar f] false
63-
-- check if there is a dependency on a variable that has not been reverted before
64-
if let some dep := deps.find? (fun e => e.fvarId! != f && !reverted.contains e.fvarId!) then
65-
let depDecl := (← getLCtx).getFVar! dep
66-
throwError "irevert: Lean hypothesis {depDecl.userName} depends on {hyp.getId}"
67-
return ldecl
68-
6954
/-- Revert a Lean proposition by turning it into the `MakeAffinely` pure premise. -/
7055
private def RevertState.revertLeanPropHyp
7156
(st : @RevertState u prop bi origE origGoal) (f : FVarId) (φ : Q(Prop)) :
72-
ProofModeM (RevertState bi origE origGoal) := do
57+
ProofModeM (@RevertState u prop bi origE origGoal) := do
7358
let { e, hyps, goal, reverted, pf } := st
7459
let P ← mkFreshExprMVarQ prop
7560
let _hA : Q(MakeAffinely iprop(⌜$φ⌝) $P) ← synthInstanceQ q(MakeAffinely iprop(⌜$φ⌝) $P)
@@ -81,7 +66,7 @@ private def RevertState.revertLeanPropHyp
8166
/-- Revert a Lean non-`Prop` local by turning the current goal into a forall. -/
8267
private def RevertState.revertLeanForallHyp
8368
(st : @RevertState u prop bi origE origGoal) (f : FVarId) (α : Q(Sort v)) :
84-
ProofModeM (RevertState bi origE origGoal) := do
69+
ProofModeM (@RevertState u prop bi origE origGoal) := do
8570
let { e, hyps, goal, reverted, pf } := st
8671
let x : Q($α) := mkFVar f
8772
have Φ : Q($α → $prop) := ← mkLambdaFVars #[x] goal
@@ -93,10 +78,9 @@ private def RevertState.revertLeanForallHyp
9378

9479
/-- Revert a Lean local after checking proofmode and local-context dependencies. -/
9580
private def RevertState.revertLeanHyp
96-
(st : @RevertState u prop bi origE origGoal) (hyp : TSyntax `ident) :
97-
ProofModeM (RevertState bi origE origGoal) := do
98-
let ldecl ← st.checkLeanDependencies hyp
99-
let f := ldecl.fvarId
81+
(st : @RevertState u prop bi origE origGoal) (f : FVarId) :
82+
ProofModeM (@RevertState u prop bi origE origGoal) := do
83+
let ldecl ← st.hyps.checkRemovableFVar "irevert" f none st.reverted.contains
10084
let v : Level ← Meta.getLevel ldecl.type
10185
have α : Q(Sort v) := ldecl.type
10286
if ← Meta.isProp α then
@@ -105,17 +89,16 @@ private def RevertState.revertLeanHyp
10589
else
10690
st.revertLeanForallHyp f α
10791

108-
-- TODO: when extending this to selection patterns, consider adding a selection pattern %x for pure hypotheses such that it is syntactically clear whether an argument of irevert refers to a pure or an Iris hypothesis
109-
elab "irevert" hs:(colGt ident)+ : tactic => do
110-
ProofModeM.runTactic fun mvar { bi, e, hyps, goal, .. } => do
111-
let init : RevertState bi e goal := { e, hyps, goal, pf := q(id) }
112-
let st ← hs.reverse.toList.foldlM (init := init) fun st hyp => do
113-
if let some _ := st.hyps.find? hyp.getId then
114-
st.revertProofModeHyp hyp
115-
else
116-
st.revertLeanHyp hyp
92+
elab "irevert" pats:(colGt selPat)+ : tactic => do
93+
let pats ← liftMacroM <| SelPat.parse pats
94+
95+
ProofModeM.runTactic fun mvar { e, hyps, goal, .. } => do
96+
let targets ← SelPat.resolve hyps pats
97+
let init : RevertState e goal := { e, hyps, goal, pf := q(id) }
98+
let st ← targets.reverse.foldlM (init := init) fun st target => do
99+
match target with
100+
| .inl uniq => st.revertProofModeHyp uniq
101+
| .inr fvar => st.revertLeanHyp fvar
117102

118-
-- Clear Lean locals already reverted into the accumulated BI goal from the final generated subgoal.
119-
let finalGoalId ← (← mkBIGoal st.hyps st.goal).mvarId!.tryClearMany st.reverted.reverse
120-
addMVarGoal finalGoalId
121-
mvar.assign (mkApp st.pf (Expr.mvar finalGoalId))
103+
let pf' ← addBIGoalWithoutFVars st.hyps st.goal st.reverted.reverse
104+
mvar.assign q($(st.pf) $pf')

0 commit comments

Comments
 (0)