fix: restore synchronous fast-forwarding path in language processor (#5802)

Between #3106 and this, it was possible that reparsing the file up to
the current position was stuck waiting in the threadpool queue,
displaying a yellow bar and not displaying any info on the unchanged
prefix.
This commit is contained in:
Sebastian Ullrich 2024-10-22 11:50:30 +02:00 committed by GitHub
parent f752ce2db9
commit d0abe1d382
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -300,7 +300,9 @@ General notes:
* We must make sure to trigger `oldCancelTk?` as soon as discarding `old?`.
* Control flow up to finding the last still-valid snapshot (which should be quick) is synchronous so
as not to report this "fast forwarding" to the user as well as to make sure the next run sees all
fast-forwarded snapshots without having to wait on tasks.
fast-forwarded snapshots without having to wait on tasks. It also ensures this part cannot be
delayed by threadpool starvation. We track whether we are still on the fast-forwarding path using
the `sync` parameter on `parseCmd` and spawn an elaboration task when we leave it.
-/
partial def process
(setupImports : Syntax → ProcessingT IO (Except HeaderProcessedSnapshot SetupImportsResult))
@ -330,7 +332,7 @@ where
-- elaboration reuse
oldProcSuccess.firstCmdSnap.bindIO (sync := true) fun oldCmd => do
let prom ← IO.Promise.new
let _ ← IO.asTask (parseCmd oldCmd newParserState oldProcSuccess.cmdState prom ctx)
parseCmd oldCmd newParserState oldProcSuccess.cmdState prom (sync := true) ctx
return .pure {
diagnostics := oldProcessed.diagnostics
result? := some {
@ -446,7 +448,7 @@ where
let parserState := Runtime.markPersistent parserState
let cmdState := Runtime.markPersistent cmdState
let ctx := Runtime.markPersistent ctx
let _ ← IO.asTask (parseCmd none parserState cmdState prom ctx)
parseCmd none parserState cmdState prom (sync := true) ctx
return {
diagnostics
infoTree? := cmdState.infoState.trees[0]!
@ -457,24 +459,10 @@ where
}
parseCmd (old? : Option CommandParsedSnapshot) (parserState : Parser.ModuleParserState)
(cmdState : Command.State) (prom : IO.Promise CommandParsedSnapshot) :
(cmdState : Command.State) (prom : IO.Promise CommandParsedSnapshot) (sync : Bool) :
LeanProcessingM Unit := do
let ctx ← read
-- check for cancellation, most likely during elaboration of previous command, before starting
-- processing of next command
if (← ctx.newCancelTk.isSet) then
-- this is a bit ugly as we don't want to adjust our API with `Option`s just for cancellation
-- (as no-one should look at this result in that case) but anything containing `Environment`
-- is not `Inhabited`
prom.resolve <| .mk (nextCmdSnap? := none) {
diagnostics := .empty, stx := .missing, parserState
elabSnap := .pure <| .ofTyped { diagnostics := .empty : SnapshotLeaf }
finishedSnap := .pure { diagnostics := .empty, cmdState }
tacticCache := (← IO.mkRef {})
}
return
let unchanged old newParserState : BaseIO Unit :=
-- when syntax is unchanged, reuse command processing task as is
-- NOTE: even if the syntax tree is functionally unchanged, the new parser state may still
@ -482,11 +470,11 @@ where
-- from `old`
if let some oldNext := old.nextCmdSnap? then do
let newProm ← IO.Promise.new
let _ ← old.data.finishedSnap.bindIO fun oldFinished =>
let _ ← old.data.finishedSnap.bindIO (sync := true) fun oldFinished =>
-- also wait on old command parse snapshot as parsing is cheap and may allow for
-- elaboration reuse
oldNext.bindIO (sync := true) fun oldNext => do
parseCmd oldNext newParserState oldFinished.cmdState newProm ctx
parseCmd oldNext newParserState oldFinished.cmdState newProm sync ctx
return .pure ()
prom.resolve <| .mk (data := old.data) (nextCmdSnap? := some { range? := none, task := newProm.result })
else prom.resolve old -- terminal command, we're done!
@ -521,44 +509,61 @@ where
if let some tk := ctx.oldCancelTk? then
tk.set
-- definitely resolved in `doElab` task
let elabPromise ← IO.Promise.new
let finishedPromise ← IO.Promise.new
-- (Try to) use last line of command as range for final snapshot task. This ensures we do not
-- retract the progress bar to a previous position in case the command support incremental
-- reporting but has significant work after resolving its last incremental promise, such as
-- final type checking; if it does not support incrementality, `elabSnap` constructed in
-- `parseCmd` and containing the entire range of the command will determine the reported
-- progress and be resolved effectively at the same time as this snapshot task, so `tailPos` is
-- irrelevant in this case.
let endRange? := stx.getTailPos?.map fun pos => ⟨pos, pos⟩
let finishedSnap := { range? := endRange?, task := finishedPromise.result }
let tacticCache ← old?.map (·.data.tacticCache) |>.getDM (IO.mkRef {})
-- check for cancellation, most likely during elaboration of previous command, before starting
-- processing of next command
if (← ctx.newCancelTk.isSet) then
-- this is a bit ugly as we don't want to adjust our API with `Option`s just for cancellation
-- (as no-one should look at this result in that case) but anything containing `Environment`
-- is not `Inhabited`
prom.resolve <| .mk (nextCmdSnap? := none) {
diagnostics := .empty, stx := .missing, parserState
elabSnap := .pure <| .ofTyped { diagnostics := .empty : SnapshotLeaf }
finishedSnap := .pure { diagnostics := .empty, cmdState }
tacticCache := (← IO.mkRef {})
}
return
let minimalSnapshots := internal.cmdlineSnapshots.get cmdState.scopes.head!.opts
let next? ← if Parser.isTerminalCommand stx then pure none
-- for now, wait on "command finished" snapshot before parsing next command
else some <$> IO.Promise.new
let diagnostics ← Snapshot.Diagnostics.ofMessageLog msgLog
let data := if minimalSnapshots && !Parser.isTerminalCommand stx then {
diagnostics
stx := .missing
parserState := {}
elabSnap := { range? := stx.getRange?, task := elabPromise.result }
finishedSnap
tacticCache
} else {
diagnostics, stx, parserState, tacticCache
elabSnap := { range? := stx.getRange?, task := elabPromise.result }
finishedSnap
}
prom.resolve <| .mk (nextCmdSnap? := next?.map
({ range? := some ⟨parserState.pos, ctx.input.endPos⟩, task := ·.result })) data
let cmdState ← doElab stx cmdState beginPos
{ old? := old?.map fun old => ⟨old.data.stx, old.data.elabSnap⟩, new := elabPromise }
finishedPromise tacticCache ctx
if let some next := next? then
parseCmd none parserState cmdState next ctx
-- Start new task when leaving fast-forwarding path; see "General notes" above
let _ ← (if sync then BaseIO.asTask else (.pure <$> ·)) do
-- definitely resolved in `doElab` task
let elabPromise ← IO.Promise.new
let finishedPromise ← IO.Promise.new
-- (Try to) use last line of command as range for final snapshot task. This ensures we do not
-- retract the progress bar to a previous position in case the command support incremental
-- reporting but has significant work after resolving its last incremental promise, such as
-- final type checking; if it does not support incrementality, `elabSnap` constructed in
-- `parseCmd` and containing the entire range of the command will determine the reported
-- progress and be resolved effectively at the same time as this snapshot task, so `tailPos` is
-- irrelevant in this case.
let endRange? := stx.getTailPos?.map fun pos => ⟨pos, pos⟩
let finishedSnap := { range? := endRange?, task := finishedPromise.result }
let tacticCache ← old?.map (·.data.tacticCache) |>.getDM (IO.mkRef {})
let minimalSnapshots := internal.cmdlineSnapshots.get cmdState.scopes.head!.opts
let next? ← if Parser.isTerminalCommand stx then pure none
-- for now, wait on "command finished" snapshot before parsing next command
else some <$> IO.Promise.new
let diagnostics ← Snapshot.Diagnostics.ofMessageLog msgLog
let data := if minimalSnapshots && !Parser.isTerminalCommand stx then {
diagnostics
stx := .missing
parserState := {}
elabSnap := { range? := stx.getRange?, task := elabPromise.result }
finishedSnap
tacticCache
} else {
diagnostics, stx, parserState, tacticCache
elabSnap := { range? := stx.getRange?, task := elabPromise.result }
finishedSnap
}
prom.resolve <| .mk (nextCmdSnap? := next?.map
({ range? := some ⟨parserState.pos, ctx.input.endPos⟩, task := ·.result })) data
let cmdState ← doElab stx cmdState beginPos
{ old? := old?.map fun old => ⟨old.data.stx, old.data.elabSnap⟩, new := elabPromise }
finishedPromise tacticCache ctx
if let some next := next? then
-- We're definitely off the fast-forwarding path now
parseCmd none parserState cmdState next (sync := false) ctx
doElab (stx : Syntax) (cmdState : Command.State) (beginPos : String.Pos)
(snap : SnapshotBundle DynamicSnapshot) (finishedPromise : IO.Promise CommandFinishedSnapshot)
@ -625,7 +630,7 @@ def processCommands (inputCtx : Parser.InputContext) (parserState : Parser.Modul
(old? : Option (Parser.InputContext × CommandParsedSnapshot) := none) :
BaseIO (Task CommandParsedSnapshot) := do
let prom ← IO.Promise.new
process.parseCmd (old?.map (·.2)) parserState commandState prom
process.parseCmd (old?.map (·.2)) parserState commandState prom (sync := true)
|>.run (old?.map (·.1))
|>.run { inputCtx with }
return prom.result