Merge pull request 'fix: Tactic failure on synthesizing placeholder' (#139) from bug/tactic-failure-placeholder into dev
Reviewed-on: #139
This commit is contained in:
commit
e9fbce7b4d
|
@ -177,16 +177,51 @@ protected def GoalState.getMVarEAssignment (goalState: GoalState) (mvarId: MVarI
|
||||||
|
|
||||||
--- Tactic execution functions ---
|
--- Tactic execution functions ---
|
||||||
|
|
||||||
protected def GoalState.step (state: GoalState) (goal: MVarId) (tacticM: Elab.Tactic.TacticM Unit)
|
-- Mimics `Elab.Term.logUnassignedUsingErrorInfos`
|
||||||
|
private def collectAllErroredMVars (src : MVarId) : Elab.TermElabM (List MVarId) := do
|
||||||
|
-- These descendants serve as "seed" mvars. If a MVarError's mvar is related
|
||||||
|
-- to one of these seed mvars, it means an error has occurred when a tactic
|
||||||
|
-- was executing on `src`. `evalTactic`, will not capture these mvars, so we
|
||||||
|
-- need to manually find them and save them into the goal list.
|
||||||
|
let descendants ← Meta.getMVars $ ← instantiateMVars (.mvar src)
|
||||||
|
--let _ ← Elab.Term.logUnassignedUsingErrorInfos descendants
|
||||||
|
let mut alreadyVisited : MVarIdSet := {}
|
||||||
|
let mut result : MVarIdSet := {}
|
||||||
|
for { mvarId, .. } in (← get).mvarErrorInfos do
|
||||||
|
unless alreadyVisited.contains mvarId do
|
||||||
|
alreadyVisited := alreadyVisited.insert mvarId
|
||||||
|
/- The metavariable `mvarErrorInfo.mvarId` may have been assigned or
|
||||||
|
delayed assigned to another metavariable that is unassigned. -/
|
||||||
|
let mvarDeps ← Meta.getMVars (.mvar mvarId)
|
||||||
|
if mvarDeps.any descendants.contains then do
|
||||||
|
result := mvarDeps.foldl (·.insert ·) result
|
||||||
|
return result.toList
|
||||||
|
|
||||||
|
private def mergeMVarLists (li1 li2 : List MVarId) : List MVarId :=
|
||||||
|
let li2' := li2.filter (¬ li1.contains ·)
|
||||||
|
li1 ++ li2'
|
||||||
|
|
||||||
|
/--
|
||||||
|
Set `guardMVarErrors` to true to capture mvar errors. Lean will not
|
||||||
|
automatically collect mvars from text tactics (vide
|
||||||
|
`test_tactic_failure_synthesize_placeholder`)
|
||||||
|
-/
|
||||||
|
protected def GoalState.step (state: GoalState) (goal: MVarId) (tacticM: Elab.Tactic.TacticM Unit) (guardMVarErrors : Bool := false)
|
||||||
: Elab.TermElabM GoalState := do
|
: Elab.TermElabM GoalState := do
|
||||||
unless (← getMCtx).decls.contains goal do
|
unless (← getMCtx).decls.contains goal do
|
||||||
throwError s!"Goal is not in context: {goal.name}"
|
throwError s!"Goal is not in context: {goal.name}"
|
||||||
goal.checkNotAssigned `GoalState.step
|
goal.checkNotAssigned `GoalState.step
|
||||||
let (_, newGoals) ← tacticM { elaborator := .anonymous } |>.run { goals := [goal] }
|
let (_, { goals }) ← tacticM { elaborator := .anonymous } |>.run { goals := [goal] }
|
||||||
let nextElabState ← MonadBacktrack.saveState
|
let nextElabState ← MonadBacktrack.saveState
|
||||||
|
Elab.Term.synthesizeSyntheticMVarsNoPostponing
|
||||||
|
|
||||||
|
let goals ← if guardMVarErrors then
|
||||||
|
pure $ mergeMVarLists goals (← collectAllErroredMVars goal)
|
||||||
|
else
|
||||||
|
pure goals
|
||||||
return {
|
return {
|
||||||
state with
|
state with
|
||||||
savedState := { term := nextElabState, tactic := newGoals },
|
savedState := { term := nextElabState, tactic := { goals }, },
|
||||||
parentMVar? := .some goal,
|
parentMVar? := .some goal,
|
||||||
calcPrevRhs? := .none,
|
calcPrevRhs? := .none,
|
||||||
}
|
}
|
||||||
|
@ -203,13 +238,13 @@ inductive TacticResult where
|
||||||
| invalidAction (message: String)
|
| invalidAction (message: String)
|
||||||
|
|
||||||
/-- Executes a `TacticM` monad on this `GoalState`, collecting the errors as necessary -/
|
/-- Executes a `TacticM` monad on this `GoalState`, collecting the errors as necessary -/
|
||||||
protected def GoalState.tryTacticM (state: GoalState) (goal: MVarId) (tacticM: Elab.Tactic.TacticM Unit):
|
protected def GoalState.tryTacticM (state: GoalState) (goal: MVarId) (tacticM: Elab.Tactic.TacticM Unit) (guardMVarErrors : Bool := false):
|
||||||
Elab.TermElabM TacticResult := do
|
Elab.TermElabM TacticResult := do
|
||||||
try
|
try
|
||||||
let nextState ← state.step goal tacticM
|
let nextState ← state.step goal tacticM guardMVarErrors
|
||||||
|
|
||||||
-- Check if error messages have been generated in the core.
|
-- Check if error messages have been generated in the core.
|
||||||
let newMessages ← (← Core.getMessageLog).toList.drop state.coreState.messages.toList.length
|
let newMessages ← (← Core.getMessageLog).toList --.drop state.coreState.messages.toList.length
|
||||||
|>.filterMapM λ m => do
|
|>.filterMapM λ m => do
|
||||||
if m.severity == .error then
|
if m.severity == .error then
|
||||||
return .some $ ← m.toString
|
return .some $ ← m.toString
|
||||||
|
@ -233,7 +268,7 @@ protected def GoalState.tryTactic (state: GoalState) (goal: MVarId) (tactic: Str
|
||||||
(fileName := ← getFileName) with
|
(fileName := ← getFileName) with
|
||||||
| .ok stx => pure $ stx
|
| .ok stx => pure $ stx
|
||||||
| .error error => return .parseError error
|
| .error error => return .parseError error
|
||||||
state.tryTacticM goal $ Elab.Tactic.evalTactic tactic
|
state.tryTacticM goal (Elab.Tactic.evalTactic tactic) true
|
||||||
|
|
||||||
protected def GoalState.tryAssign (state: GoalState) (goal: MVarId) (expr: String):
|
protected def GoalState.tryAssign (state: GoalState) (goal: MVarId) (expr: String):
|
||||||
Elab.TermElabM TacticResult := do
|
Elab.TermElabM TacticResult := do
|
||||||
|
|
|
@ -7,7 +7,7 @@ A Machine-to-Machine interaction system for Lean 4.
|
||||||
Pantograph provides interfaces to execute proofs, construct expressions, and
|
Pantograph provides interfaces to execute proofs, construct expressions, and
|
||||||
examine the symbol list of a Lean project for machine learning.
|
examine the symbol list of a Lean project for machine learning.
|
||||||
|
|
||||||
See [documentations](doc/) for design rationale and references.
|
See [documentations](doc/rationale.md) for design rationale and references.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|
|
@ -701,7 +701,7 @@ def test_nat_zero_add_alt: TestM Unit := do
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
def test_composite_tactic_failure: TestM Unit := do
|
def test_tactic_failure_unresolved_goals : TestM Unit := do
|
||||||
let state? ← startProof (.expr "∀ (p : Nat → Prop), ∃ (x : Nat), p (0 + x + 0)")
|
let state? ← startProof (.expr "∀ (p : Nat → Prop), ∃ (x : Nat), p (0 + x + 0)")
|
||||||
let state0 ← match state? with
|
let state0 ← match state? with
|
||||||
| .some state => pure state
|
| .some state => pure state
|
||||||
|
@ -720,6 +720,37 @@ def test_composite_tactic_failure: TestM Unit := do
|
||||||
let .failure messages ← state1.tacticOn 0 tactic | addTest $ assertUnreachable s!"{tactic} should fail"
|
let .failure messages ← state1.tacticOn 0 tactic | addTest $ assertUnreachable s!"{tactic} should fail"
|
||||||
checkEq s!"{tactic} fails" messages #[s!"{← getFileName}:0:12: error: unsolved goals\np : Nat → Prop\n⊢ p 0\n"]
|
checkEq s!"{tactic} fails" messages #[s!"{← getFileName}:0:12: error: unsolved goals\np : Nat → Prop\n⊢ p 0\n"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_tactic_failure_synthesize_placeholder : TestM Unit := do
|
||||||
|
let state? ← startProof (.expr "∀ (p q r : Prop) (h : p → q), q ∧ r")
|
||||||
|
let state0 ← match state? with
|
||||||
|
| .some state => pure state
|
||||||
|
| .none => do
|
||||||
|
addTest $ assertUnreachable "Goal could not parse"
|
||||||
|
return ()
|
||||||
|
|
||||||
|
let tactic := "intro p q r h"
|
||||||
|
let state1 ← match ← state0.tacticOn 0 tactic with
|
||||||
|
| .success state => pure state
|
||||||
|
| other => do
|
||||||
|
addTest $ assertUnreachable $ other.toString
|
||||||
|
return ()
|
||||||
|
|
||||||
|
let tactic := "simpa [h] using And.imp_left h _"
|
||||||
|
let state2 ← match ← state1.tacticOn 0 tactic with
|
||||||
|
| .success state => pure state
|
||||||
|
| other => do
|
||||||
|
addTest $ assertUnreachable $ other.toString
|
||||||
|
return ()
|
||||||
|
|
||||||
|
checkEq tactic ((← state2.serializeGoals).map (·.devolatilize)) #[
|
||||||
|
buildGoal [("p", "Prop"), ("q", "Prop"), ("r", "Prop"), ("h", "p → q")] "p ∧ r"
|
||||||
|
]
|
||||||
|
|
||||||
|
--let .failure messages ← state1.tacticOn 0 tactic | addTest $ assertUnreachable s!"{tactic} should fail"
|
||||||
|
--let message := s!"<Pantograph>:0:31: error: don't know how to synthesize placeholder\ncontext:\np q r : Prop\nh : p → q\n⊢ p ∧ r\n"
|
||||||
|
--checkEq s!"{tactic} fails" messages #[message]
|
||||||
|
|
||||||
def suite (env: Environment): List (String × IO LSpec.TestSeq) :=
|
def suite (env: Environment): List (String × IO LSpec.TestSeq) :=
|
||||||
let tests := [
|
let tests := [
|
||||||
("identity", test_identity),
|
("identity", test_identity),
|
||||||
|
@ -732,7 +763,8 @@ def suite (env: Environment): List (String × IO LSpec.TestSeq) :=
|
||||||
("calc", test_calc),
|
("calc", test_calc),
|
||||||
("Nat.zero_add", test_nat_zero_add),
|
("Nat.zero_add", test_nat_zero_add),
|
||||||
("Nat.zero_add alt", test_nat_zero_add_alt),
|
("Nat.zero_add alt", test_nat_zero_add_alt),
|
||||||
("composite tactic failure", test_composite_tactic_failure),
|
("tactic failure with unresolved goals", test_tactic_failure_unresolved_goals),
|
||||||
|
("tactic failure with synthesize placeholder", test_tactic_failure_synthesize_placeholder),
|
||||||
]
|
]
|
||||||
tests.map (fun (name, test) => (name, proofRunner env test))
|
tests.map (fun (name, test) => (name, proofRunner env test))
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,22 @@ The name Pantograph is a pun. It means two things
|
||||||
a locomotive. In comparison the (relatively) simple Pantograph software powers
|
a locomotive. In comparison the (relatively) simple Pantograph software powers
|
||||||
theorem proving projects.
|
theorem proving projects.
|
||||||
|
|
||||||
|
## Caveats
|
||||||
|
|
||||||
|
Pantograph does not exactly mimic Lean LSP's behaviour. That would not grant the
|
||||||
|
flexibility it offers. To support tree search means Pantograph has to act
|
||||||
|
differently from Lean in some times, but never at the sacrifice of soundness.
|
||||||
|
|
||||||
|
- When Lean LSP says "don't know how to synthesize placeholder", this indicates
|
||||||
|
the human operator needs to manually move the cursor to the placeholder and
|
||||||
|
type in the correct expression. This error therefore should not halt the proof
|
||||||
|
process, and the placeholder should be turned into a goal.
|
||||||
|
- When Lean LSP says "unresolved goals", that means a proof cannot finish where
|
||||||
|
it is supposed to finish at the end of a `by` block. Pantograph will raise the
|
||||||
|
error in this case, since it indicates the termination of a proof search branch.
|
||||||
|
- `pick_goal` or `swap` will not work since they run contrary to tree search
|
||||||
|
paradigms.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
* [Pantograph Paper](https://arxiv.org/abs/2410.16429)
|
* [Pantograph Paper](https://arxiv.org/abs/2410.16429)
|
||||||
|
|
Loading…
Reference in New Issue