feat(goal): Add unshielded tactic execution mode #219

Merged
aniva merged 12 commits from goal/automatic-mode into dev 2025-06-26 15:52:17 -07:00
3 changed files with 102 additions and 23 deletions
Showing only changes of commit 4b02d73374 - Show all commits

View File

@ -4,6 +4,13 @@ Here, a unified system handles all fragments.
Inside a tactic fragment, the parser category may be different. An incomplete Inside a tactic fragment, the parser category may be different. An incomplete
fragmented tactic may not be elaboratable.. fragmented tactic may not be elaboratable..
In line with continuation/resumption paradigms, the exit function of a fragment
tactic is responsible for resuming incomplete goals with fragments. For example,
when a conversion tactic finishes, the sentinels should resume the root of the
conversion tactic goal. The user cannot be expected to execute this resumption,
since the root is automatically dormanted at the entry of the conversion tactic
mode.
-/ -/
import Lean.Meta import Lean.Meta
import Lean.Elab import Lean.Elab
@ -17,7 +24,7 @@ inductive Fragment where
| conv (rhs : MVarId) | conv (rhs : MVarId)
-- This goal is spawned from a `conv` -- This goal is spawned from a `conv`
| convSentinel (parent : MVarId) | convSentinel (parent : MVarId)
deriving BEq deriving BEq, Inhabited
abbrev FragmentMap := Std.HashMap MVarId Fragment abbrev FragmentMap := Std.HashMap MVarId Fragment
def FragmentMap.empty : FragmentMap := Std.HashMap.emptyWithCapacity 2 def FragmentMap.empty : FragmentMap := Std.HashMap.emptyWithCapacity 2
@ -45,7 +52,7 @@ protected def Fragment.enterConv : Elab.Tactic.TacticM FragmentMap := do
|>.insert goal (.conv rhs) |>.insert goal (.conv rhs)
|>.insert newGoal (.convSentinel goal) |>.insert newGoal (.convSentinel goal)
protected def Fragment.exit (fragment : Fragment) (goal : MVarId) (fragments : FragmentMap) protected partial def Fragment.exit (fragment : Fragment) (goal : MVarId) (fragments : FragmentMap)
: Elab.Tactic.TacticM FragmentMap := : Elab.Tactic.TacticM FragmentMap :=
match fragment with match fragment with
| .calc .. => do | .calc .. => do
@ -59,18 +66,25 @@ protected def Fragment.exit (fragment : Fragment) (goal : MVarId) (fragments : F
-- Close all existing goals with `refl` -- Close all existing goals with `refl`
for mvarId in goals do for mvarId in goals do
liftM <| mvarId.refl <|> mvarId.inferInstance <|> pure () liftM <| mvarId.refl <|> mvarId.inferInstance <|> pure ()
Elab.Tactic.pruneSolvedGoals
unless (← goals.filterM (·.isAssignedOrDelayedAssigned)).isEmpty do unless (← goals.filterM (·.isAssignedOrDelayedAssigned)).isEmpty do
throwError "convert tactic failed, there are unsolved goals\n{Elab.goalsToMessageData (goals)}" throwError "convert tactic failed, there are unsolved goals\n{Elab.goalsToMessageData (goals)}"
Elab.Tactic.replaceMainGoal [goal] -- Ensure the meta tactic runs on `goal` even if its dormant by forcing resumption
Elab.Tactic.setGoals $ goal :: (← Elab.Tactic.getGoals)
let targetNew ← instantiateMVars (.mvar rhs) let targetNew ← instantiateMVars (.mvar rhs)
let proof ← instantiateMVars (.mvar goal) let proof ← instantiateMVars (.mvar goal)
Elab.Tactic.liftMetaTactic1 (·.replaceTargetEq targetNew proof) Elab.Tactic.liftMetaTactic1 (·.replaceTargetEq targetNew proof)
-- Try to solve maiinline by rfl
let mvarId ← Elab.Tactic.getMainGoal
liftM <| mvarId.refl <|> mvarId.inferInstance <|> pure ()
Elab.Tactic.pruneSolvedGoals
-- FIXME: Erase all sibling fragments
return fragments.erase goal return fragments.erase goal
| .convSentinel _ => | .convSentinel parent =>
return fragments.erase goal let parentFragment := fragments[parent]!
parentFragment.exit parent (fragments.erase goal)
protected def Fragment.step (fragment : Fragment) (goal : MVarId) (s : String) (map : FragmentMap) protected def Fragment.step (fragment : Fragment) (goal : MVarId) (s : String) (map : FragmentMap)
: Elab.Tactic.TacticM FragmentMap := goal.withContext do : Elab.Tactic.TacticM FragmentMap := goal.withContext do
@ -143,14 +157,25 @@ protected def Fragment.step (fragment : Fragment) (goal : MVarId) (s : String) (
let oldGoals ← Elab.Tactic.getGoals let oldGoals ← Elab.Tactic.getGoals
-- Label newly generated goals as conv sentinels -- Label newly generated goals as conv sentinels
Elab.Tactic.evalTactic tactic Elab.Tactic.evalTactic tactic
let newGoals ← (← Elab.Tactic.getUnsolvedGoals).filterM λ g => do let newConvGoals ← (← Elab.Tactic.getUnsolvedGoals).filterM λ g => do
-- conv tactic might generate non-conv goals -- conv tactic might generate non-conv goals
if oldGoals.contains g then if oldGoals.contains g then
return false return false
let target ← g.getType
return isLHSGoal? (← g.getType) |>.isSome return isLHSGoal? (← g.getType) |>.isSome
-- FIXME: Conclude the conv by exiting the parent fragment if new goals is empty -- Conclude the conv by exiting the parent fragment if new goals is empty
return newGoals.foldl (init := map) λ acc g => if newConvGoals.isEmpty then
let hasSiblingFragment := map.fold (init := false) λ flag _ fragment =>
if flag then
true
else match fragment with
| .convSentinel parent' => parent == parent'
| _ => false
if ¬ hasSiblingFragment then
-- This fragment must exist since we have conv goals
let parentFragment := map[parent]!
-- All descendants exhausted. Exit from the parent conv.
return ← parentFragment.exit parent map
return newConvGoals.foldl (init := map) λ acc g =>
acc.insert g fragment acc.insert g fragment
end Pantograph end Pantograph

View File

@ -71,8 +71,6 @@ def GoalState.get! (state: GoalState) (i: Nat): MVarId := state.goals[i]!
def GoalState.tacticOn (state: GoalState) (goalId: Nat) (tactic: String) := state.tryTactic (state.get! goalId) tactic def GoalState.tacticOn (state: GoalState) (goalId: Nat) (tactic: String) := state.tryTactic (state.get! goalId) tactic
def GoalState.tacticOn' (state: GoalState) (goalId: Nat) (tactic: TSyntax `tactic) := def GoalState.tacticOn' (state: GoalState) (goalId: Nat) (tactic: TSyntax `tactic) :=
state.tryTacticM (state.get! goalId) (Elab.Tactic.evalTactic tactic) true state.tryTacticM (state.get! goalId) (Elab.Tactic.evalTactic tactic) true
def GoalState.tryTacticOnMain (state : GoalState) (tactic : String) :=
state.tryTactic (.prefer state.goals[0]!) tactic
def TacticResult.toString : TacticResult → String def TacticResult.toString : TacticResult → String
| .success state _messages => s!".success ({state.goals.length} goals)" | .success state _messages => s!".success ({state.goals.length} goals)"

View File

@ -26,7 +26,7 @@ example : ∀ (a b c1 c2: Nat), (b + a) + c1 = (b + a) + c2 → (a + b) + c1 = (
. rfl . rfl
exact h exact h
def test_conv: TestM Unit := do def test_conv_simple: TestM Unit := do
let rootTarget ← parseSentence "∀ (a b c1 c2: Nat), (b + a) + c1 = (b + a) + c2 → (a + b) + c1 = (b + a) + c2" let rootTarget ← parseSentence "∀ (a b c1 c2: Nat), (b + a) + c1 = (b + a) + c2 → (a + b) + c1 = (b + a) + c2"
let state0 ← GoalState.create rootTarget let state0 ← GoalState.create rootTarget
@ -103,19 +103,21 @@ def test_conv: TestM Unit := do
return () return ()
let convTactic := "rfl" let convTactic := "rfl"
let state6 ← match ← state4_1.tacticOn (goalId := 0) convTactic with let state1_1 ← match ← state4_1.tacticOn (goalId := 0) convTactic with
| .success state _ => pure state | .success state _ => pure state
| other => do | other => do
addTest $ assertUnreachable $ other.toString addTest $ assertUnreachable $ other.toString
return () return ()
addTest $ LSpec.check s!" · {convTactic}" ((← state6.serializeGoals).map (·.devolatilize) = addTest $ LSpec.check s!" · {convTactic}" ((← state1_1.serializeGoals).map (·.devolatilize) =
#[]) #[interiorGoal [] "b + a + c1 = b + a + c2"])
/-
let state1_1 ← match ← state6.fragmentExit goalConv with let state1_1 ← match ← state6.fragmentExit goalConv with
| .success state _ => pure state | .success state _ => pure state
| other => do | other => do
addTest $ assertUnreachable $ other.toString addTest $ assertUnreachable $ other.toString
return () return ()
-/
let tactic := "exact h" let tactic := "exact h"
let stateF ← match ← state1_1.tacticOn (goalId := 0) (tactic := tactic) with let stateF ← match ← state1_1.tacticOn (goalId := 0) (tactic := tactic) with
@ -151,19 +153,19 @@ def test_conv_unshielded : TestM Unit := do
let tactic := "apply And.intro" let tactic := "apply And.intro"
let .success state _ ← state.tacticOn 0 tactic | fail "apply failed" let .success state _ ← state.tacticOn 0 tactic | fail "apply failed"
let .success state _ ← state.convEnter (.prefer state.goals[0]!) | fail "Cannot enter conversion tactic mode" let .success state _ ← state.convEnter (.prefer state.goals[0]!) | fail "Cannot enter conversion tactic mode"
let .success state _ ← state.tryTacticOnMain "rhs" | fail "rhs failed" let .success state _ ← state.tryTactic .unfocus "rhs" | fail "rhs failed"
let tactic := "arg 1" let tactic := "arg 1"
let .success state _ ← state.tryTacticOnMain tactic | fail s!"{tactic} failed" let .success state _ ← state.tryTactic .unfocus tactic | fail s!"{tactic} failed"
checkEq s!" {tactic}" ((← state.serializeGoals).map (·.devolatilize)) checkEq s!" {tactic}" ((← state.serializeGoals).map (·.devolatilize))
#[ #[
{ interiorGoal [] "y" with isConversion := true }, { interiorGoal [] "y" with isConversion := true },
{ interiorGoal [] "p" with userName? := "right", }, { interiorGoal [] "p" with userName? := "right", },
] ]
let tactic := "rw [←hi]" let tactic := "rw [←hi]"
let .success state _ ← state.tryTacticOnMain tactic | fail s!"{tactic} failed" let .success state _ ← state.tryTactic .unfocus tactic | fail s!"{tactic} failed"
checkEq s!" {tactic}" state.goals.length 3 checkEq s!" {tactic}" state.goals.length 3
let tactic := "rfl" let tactic := "rfl"
let .success state _ ← state.tryTacticOnMain tactic | fail s!"{tactic} failed" let .success state _ ← state.tryTactic .unfocus tactic | fail s!"{tactic} failed"
checkEq s!" {tactic}" ((← state.serializeGoals).map (·.devolatilize)) checkEq s!" {tactic}" ((← state.serializeGoals).map (·.devolatilize))
#[ #[
interiorGoal [] "p", interiorGoal [] "p",
@ -171,9 +173,9 @@ def test_conv_unshielded : TestM Unit := do
] ]
checkEq " (n goals)" state.goals.length 2 checkEq " (n goals)" state.goals.length 2
let tactic := "exact hp" let tactic := "exact hp"
let .success state _ ← state.tryTacticOnMain tactic | fail s!"{tactic} failed" let .success state _ ← state.tryTactic .unfocus tactic | fail s!"{tactic} failed"
let tactic := "exact hp" let tactic := "exact hp"
let .success state _ ← state.tryTacticOnMain tactic | fail s!"{tactic} failed" let .success state _ ← state.tryTactic .unfocus tactic | fail s!"{tactic} failed"
let root? := state.rootExpr? let root? := state.rootExpr?
checkTrue "root" root?.isSome checkTrue "root" root?.isSome
where where
@ -181,6 +183,59 @@ def test_conv_unshielded : TestM Unit := do
let free := [("p", "Prop"), ("x", "Nat"), ("y", "Nat"), ("z", "Nat"), ("hp", "p"), ("hi", "p → x = y")] ++ free let free := [("p", "Prop"), ("x", "Nat"), ("y", "Nat"), ("z", "Nat"), ("hp", "p"), ("hi", "p → x = y")] ++ free
buildGoal free target buildGoal free target
example : ∀ (x y z w : Nat), y = z → x + z = w → x + y = w := by
intro x y z w hyz hxzw
conv =>
lhs
arg 2
rw [hyz]
rfl
exact hxzw
def test_conv_unfinished : TestM Unit := do
let rootTarget ← parseSentence "∀ (x y z w : Nat), y = z → x + z = w → x + y = w"
let state ← GoalState.create rootTarget
let tactic := "intro x y z w hyz hxzw"
let .success state _ ← state.tacticOn 0 tactic | fail "intro failed"
let convParent := state.goals[0]!
let .success state _ ← state.convEnter (.prefer convParent) | fail "Cannot enter conversion tactic mode"
let .success state _ ← state.tryTactic .unfocus "lhs" | fail "rhs failed"
let tactic := "arg 2"
let .success state _ ← state.tryTactic .unfocus tactic | fail s!"{tactic} failed"
checkEq s!" {tactic}" ((← state.serializeGoals).map (·.devolatilize))
#[
{ interiorGoal [] "y" with isConversion := true },
]
let tactic := "rw [hyz]"
let .success state _ ← state.tryTactic .unfocus tactic | fail s!"{tactic} failed"
checkEq s!" {tactic}" ((← state.serializeGoals).map (·.devolatilize))
#[
{ interiorGoal [] "z" with isConversion := true },
]
checkTrue " (fragment)" $ state.fragments.contains state.mainGoal?.get!
checkTrue " (fragment parent)" $ state.fragments.contains convParent
checkTrue " (main goal)" state.mainGoal?.isSome
let tactic := "rfl"
let state? ← state.tryTactic .unfocus tactic
let state ← match state? with
| .success state _ => pure state
| .failure messages => fail s!"rfl {messages}"; return
| .invalidAction messages => fail s!"rfl {messages}"; return
| .parseError messages => fail s!"rfl {messages}"; return
checkEq s!" {tactic}" ((← state.serializeGoals).map (·.devolatilize))
#[
interiorGoal [] "x + z = w",
]
checkEq s!" {tactic}" state.goals.length 1
let tactic := "exact hxzw"
let .success state _ ← state.tryTactic .unfocus tactic | fail s!"{tactic} failed"
let root? := state.rootExpr?
checkTrue "root" root?.isSome
where
interiorGoal (free: List (String × String)) (target: String) :=
let free := [("x", "Nat"), ("y", "Nat"), ("z", "Nat"), ("w", "Nat"), ("hyz", "y = z"), ("hxzw", "x + z = w")] ++ free
buildGoal free target
example : ∀ (a b c d: Nat), a + b = b + c → b + c = c + d → a + b = c + d := by example : ∀ (a b c d: Nat), a + b = b + c → b + c = c + d → a + b = c + d := by
intro a b c d h1 h2 intro a b c d h1 h2
calc a + b = b + c := by apply h1 calc a + b = b + c := by apply h1
@ -249,8 +304,9 @@ def test_calc: TestM Unit := do
def suite (env: Environment): List (String × IO LSpec.TestSeq) := def suite (env: Environment): List (String × IO LSpec.TestSeq) :=
[ [
("conv", test_conv), ("conv simple", test_conv_simple),
("conv unshielded", test_conv_unshielded), ("conv unshielded", test_conv_unshielded),
("conv unfinished", test_conv_unfinished),
("calc", test_calc), ("calc", test_calc),
] |>.map (λ (name, t) => (name, runTestTermElabM env t)) ] |>.map (λ (name, t) => (name, runTestTermElabM env t))