Skip to content

Commit

Permalink
fix: snapshot subtree was not restored on reuse (leanprover#4643)
Browse files Browse the repository at this point in the history
This could lead to nested error messages and info trees vanishing on
edits (strictly) below them

Fixes the second issue in leanprover#4623
  • Loading branch information
Kha committed Jul 9, 2024
1 parent c24de9d commit 400ef29
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 19 deletions.
9 changes: 2 additions & 7 deletions src/Lean/Elab/MutualDef.lean
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ private def elabHeaders (views : Array DefView)
else
reuseBody := false

let mut (newHeader, newState) ← withRestoreOrSaveFull reusableResult? do
let mut (newHeader, newState) ← withRestoreOrSaveFull reusableResult? none do
withRef view.headerRef do
addDeclarationRanges declName view.ref -- NOTE: this should be the full `ref`
applyAttributesAt declName view.modifiers.attrs .beforeElaboration
Expand Down Expand Up @@ -337,14 +337,9 @@ private def elabFunValues (headers : Array DefViewElabHeader) : TermElabM (Array
-- elaboration
if let some old := old.val.get then
snap.new.resolve <| some old
-- also make sure to reuse tactic snapshots if present so that body reuse does not lead to
-- missed tactic reuse on further changes
if let some tacSnap := header.tacSnap? then
if let some oldTacSnap := tacSnap.old? then
tacSnap.new.resolve oldTacSnap.val.get
reusableResult? := some (old.value, old.state)

let (val, state) ← withRestoreOrSaveFull reusableResult? do
let (val, state) ← withRestoreOrSaveFull reusableResult? header.tacSnap? do
withDeclName header.declName <| withLevelNames header.levelNames do
let valStx ← liftMacroM <| declValToTerm header.value
forallBoundedTelescope header.type header.numParams fun xs type => do
Expand Down
7 changes: 4 additions & 3 deletions src/Lean/Elab/Tactic/Basic.lean
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,15 @@ def SavedState.restore (b : SavedState) (restoreInfo := false) : TacticM Unit :=
b.term.restore restoreInfo
set b.tactic

@[specialize, inherit_doc Core.withRestoreOrSaveFull]
def withRestoreOrSaveFull (reusableResult? : Option (α × SavedState)) (act : TacticM α) :
@[specialize, inherit_doc Term.withRestoreOrSaveFull]
def withRestoreOrSaveFull (reusableResult? : Option (α × SavedState))
(tacSnap? : Option (Language.SnapshotBundle Tactic.TacticParsedSnapshot)) (act : TacticM α) :
TacticM (α × SavedState) := do
if let some (_, state) := reusableResult? then
set state.tactic
let reusableResult? := reusableResult?.map (fun (val, state) => (val, state.term))
let (a, term) ← controlAt TermElabM fun runInBase => do
Term.withRestoreOrSaveFull reusableResult? <| runInBase act
Term.withRestoreOrSaveFull reusableResult? tacSnap? <| runInBase act
return (a, { term, tactic := (← get) })

protected def getCurrMacroScope : TacticM MacroScope := do pure (← readThe Core.Context).currMacroScope
Expand Down
9 changes: 4 additions & 5 deletions src/Lean/Elab/Tactic/BuiltinTactic.lean
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,10 @@ where
{
range? := stxs |>.getRange?
task := next.result }]
let (_, state) ← withRestoreOrSaveFull reusableResult? do
-- set up nested reuse; `evalTactic` will check for `isIncrementalElab`
withTheReader Term.Context ({ · with
tacSnap? := some { old? := oldInner?, new := inner } }) do
evalTactic tac
let (_, state) ← withRestoreOrSaveFull reusableResult?
-- set up nested reuse; `evalTactic` will check for `isIncrementalElab`
(tacSnap? := some { old? := oldInner?, new := inner }) do
evalTactic tac
finished.resolve { state? := state }

withTheReader Term.Context ({ · with tacSnap? := some {
Expand Down
27 changes: 23 additions & 4 deletions src/Lean/Elab/Term.lean
Original file line number Diff line number Diff line change
Expand Up @@ -327,14 +327,33 @@ def SavedState.restore (s : SavedState) (restoreInfo : Bool := false) : TermElab
unless restoreInfo do
setInfoState infoState

@[specialize, inherit_doc Core.withRestoreOrSaveFull]
def withRestoreOrSaveFull (reusableResult? : Option (α × SavedState)) (act : TermElabM α) :
/--
Like `Meta.withRestoreOrSaveFull` for `TermElabM`, but also takes a `tacSnap?` that
* when running `act`, is set as `Context.tacSnap?`
* otherwise (i.e. on restore) is used to update the new snapshot promise to the old task's
value.
This extra restore step is necessary because while `reusableResult?` can be used to replay any
effects on `State`, `Context.tacSnap?` is not part of it but changed via an `IO` side effect, so
it needs to be replayed separately.
We use an explicit parameter instead of accessing `Context.tacSnap?` directly because this prevents
`withRestoreOrSaveFull` and `withReader` from being used in the wrong order.
-/
@[specialize]
def withRestoreOrSaveFull (reusableResult? : Option (α × SavedState))
(tacSnap? : Option (Language.SnapshotBundle Tactic.TacticParsedSnapshot)) (act : TermElabM α) :
TermElabM (α × SavedState) := do
if let some (_, state) := reusableResult? then
set state.elab
if let some snap := tacSnap? then
let some old := snap.old?
| throwError "withRestoreOrSaveFull: expected old snapshot in `tacSnap?`"
snap.new.resolve old.val.get

let reusableResult? := reusableResult?.map (fun (val, state) => (val, state.meta))
let (a, meta) ← controlAt MetaM fun runInBase => do
Meta.withRestoreOrSaveFull reusableResult? <| runInBase act
let (a, meta) ← withReader ({ · with tacSnap? }) do
controlAt MetaM fun runInBase => do
Meta.withRestoreOrSaveFull reusableResult? <| runInBase act
return (a, { meta, «elab» := (← get) })

instance : MonadBacktrack SavedState TermElabM where
Expand Down
13 changes: 13 additions & 0 deletions tests/lean/interactive/incrementalTactic.lean
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,16 @@ def tacInTermInTac2 : True := by
| zero => done
| succ => sorry
--^ collectDiagnostics

/-!
Error messages (or rather, whole snapshot subtrees) may vanish if we do not properly restore them on
reuse (which `Term.withRestoreOrSaveFull` now makes sure of).
-/
-- RESET
example : True := by
· · done
· done
· skip
--^ sync
--^ insert: " "
--^ collectDiagnostics
24 changes: 24 additions & 0 deletions tests/lean/interactive/incrementalTactic.lean.expected.out
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,27 @@ s
"fullRange":
{"start": {"line": 3, "character": 12},
"end": {"line": 3, "character": 16}}}]}
{"version": 2,
"uri": "file:///incrementalTactic.lean",
"diagnostics":
[{"source": "Lean 4",
"severity": 1,
"range":
{"start": {"line": 2, "character": 6}, "end": {"line": 2, "character": 10}},
"message": "unsolved goals\n⊢ True",
"fullRange":
{"start": {"line": 2, "character": 6}, "end": {"line": 2, "character": 10}}},
{"source": "Lean 4",
"severity": 1,
"range":
{"start": {"line": 3, "character": 4}, "end": {"line": 3, "character": 10}},
"message": "no goals to be solved",
"fullRange":
{"start": {"line": 3, "character": 4}, "end": {"line": 3, "character": 10}}},
{"source": "Lean 4",
"severity": 1,
"range":
{"start": {"line": 4, "character": 2}, "end": {"line": 4, "character": 8}},
"message": "no goals to be solved",
"fullRange":
{"start": {"line": 4, "character": 2}, "end": {"line": 4, "character": 8}}}]}

0 comments on commit 400ef29

Please sign in to comment.