Skip to content

Commit 4952b9c

Browse files
committed
feat: include async elab tasks in info tree reported to linters and request handlers
1 parent a51ef25 commit 4952b9c

13 files changed

+270
-216
lines changed

src/Lean/CoreM.lean

+2-1
Original file line numberDiff line numberDiff line change
@@ -462,11 +462,12 @@ def wrapAsyncAsSnapshot (act : Unit → CoreM Unit) (cancelTk? : Option IO.Cance
462462
let t ← wrapAsync (cancelTk? := cancelTk?) fun _ => do
463463
IO.FS.withIsolatedStreams (isolateStderr := stderrAsMessages.get (← getOptions)) do
464464
let tid ← IO.getTID
465-
-- reset trace state and message log so as not to report them twice
465+
-- reset trace/info state and message log so as not to report them twice
466466
modify fun st => { st with
467467
messages := st.messages.markAllReported
468468
traceState := { tid }
469469
snapshotTasks := #[]
470+
infoState := {}
470471
}
471472
try
472473
withTraceNode `Elab.async (fun _ => return desc) do

src/Lean/Elab/Command.lean

+28-23
Original file line numberDiff line numberDiff line change
@@ -226,13 +226,16 @@ private def runCore (x : CoreM α) : CommandElabM α := do
226226
}
227227
let (ea, coreS) ← liftM x
228228
modify fun s => { s with
229-
env := coreS.env
230-
nextMacroScope := coreS.nextMacroScope
231-
ngen := coreS.ngen
232-
infoState.trees := s.infoState.trees.append coreS.infoState.trees
233-
traceState.traces := coreS.traceState.traces.map fun t => { t with ref := replaceRef t.ref ctx.ref }
234-
snapshotTasks := coreS.snapshotTasks
235-
messages := s.messages ++ coreS.messages
229+
env := coreS.env
230+
nextMacroScope := coreS.nextMacroScope
231+
ngen := coreS.ngen
232+
infoState.trees := s.infoState.trees.append coreS.infoState.trees
233+
-- we assume substitution of `assingment` has already happened, but for lazy assignments we only
234+
-- do it at the very end
235+
infoState.lazyAssignment := coreS.infoState.lazyAssignment
236+
traceState.traces := coreS.traceState.traces.map fun t => { t with ref := replaceRef t.ref ctx.ref }
237+
snapshotTasks := coreS.snapshotTasks
238+
messages := s.messages ++ coreS.messages
236239
}
237240
return ea
238241

@@ -299,20 +302,20 @@ Interrupt and abort exceptions are caught but not logged.
299302
EIO.catchExceptions (withLogging x ctx ref) (fun _ => pure ())
300303

301304
@[inherit_doc Core.wrapAsync]
302-
def wrapAsync (act : Unit → CommandElabM α) (cancelTk? : Option IO.CancelToken) :
303-
CommandElabM (EIO Exception α) := do
305+
def wrapAsync {α β : Type} (act : α → CommandElabM β) (cancelTk? : Option IO.CancelToken) :
306+
CommandElabM (α → EIO Exception β) := do
304307
let ctx ← read
305308
let ctx := { ctx with cancelTk? }
306309
let st ← get
307-
return act () |>.run ctx |>.run' st
310+
return (act · |>.run ctx |>.run' st)
308311

309312
open Language in
310313
@[inherit_doc Core.wrapAsyncAsSnapshot]
311314
-- `CoreM` and `CommandElabM` are too different to meaningfully share this code
312-
def wrapAsyncAsSnapshot (act : Unit → CommandElabM Unit) (cancelTk? : Option IO.CancelToken)
315+
def wrapAsyncAsSnapshot {α : Type} (act : α → CommandElabM Unit) (cancelTk? : Option IO.CancelToken)
313316
(desc : String := by exact decl_name%.toString) :
314-
CommandElabM (BaseIO SnapshotTree) := do
315-
let t ← wrapAsync (cancelTk? := cancelTk?) fun _ => do
317+
CommandElabM (α → BaseIO SnapshotTree) := do
318+
let f ← wrapAsync (cancelTk? := cancelTk?) fun a => do
316319
IO.FS.withIsolatedStreams (isolateStderr := Core.stderrAsMessages.get (← getOptions)) do
317320
let tid ← IO.getTID
318321
-- reset trace state and message log so as not to report them twice
@@ -323,15 +326,15 @@ def wrapAsyncAsSnapshot (act : Unit → CommandElabM Unit) (cancelTk? : Option I
323326
}
324327
try
325328
withTraceNode `Elab.async (fun _ => return desc) do
326-
act ()
329+
act a
327330
catch e =>
328331
logError e.toMessageData
329332
finally
330333
addTraceAsMessages
331334
get
332335
let ctx ← read
333-
return do
334-
match (← t.toBaseIO) with
336+
return fun a => do
337+
match (← (f a).toBaseIO) with
335338
| .ok (output, st) =>
336339
let mut msgs := st.messages
337340
if !output.isEmpty then
@@ -353,6 +356,7 @@ def wrapAsyncAsSnapshot (act : Unit → CommandElabM Unit) (cancelTk? : Option I
353356
def logSnapshotTask (task : Language.SnapshotTask Language.SnapshotTree) : CommandElabM Unit :=
354357
modify fun s => { s with snapshotTasks := s.snapshotTasks.push task }
355358

359+
open Language in
356360
def runLintersAsync (stx : Syntax) : CommandElabM Unit := do
357361
if !Elab.async.get (← getOptions) then
358362
withoutModifyingEnv do
@@ -362,8 +366,13 @@ def runLintersAsync (stx : Syntax) : CommandElabM Unit := do
362366
-- We only start one task for all linters for now as most linters are fast and we simply want
363367
-- to unblock elaboration of the next command
364368
let cancelTk ← IO.CancelToken.new
365-
let lintAct ← wrapAsyncAsSnapshot (cancelTk? := cancelTk) fun _ => runLinters stx
366-
logSnapshotTask { stx? := none, task := (← BaseIO.asTask lintAct), cancelTk? := cancelTk }
369+
let lintAct ← wrapAsyncAsSnapshot (cancelTk? := cancelTk) fun infoSt => do
370+
modifyInfoState fun _ => infoSt
371+
runLinters stx
372+
373+
-- linters should have access to the complete info tree
374+
let task ← BaseIO.mapTask (t := (← getInfoState).substituteLazy) lintAct
375+
logSnapshotTask { stx? := none, task, cancelTk? := cancelTk }
367376

368377
protected def getCurrMacroScope : CommandElabM Nat := do pure (← read).currMacroScope
369378
protected def getMainModule : CommandElabM Name := do pure (← getEnv).mainModule
@@ -579,11 +588,7 @@ def elabCommandTopLevel (stx : Syntax) : CommandElabM Unit := withRef stx do pro
579588
if let some snap := (← read).snap? then
580589
snap.new.resolve default
581590

582-
-- note the order: first process current messages & info trees, then add back old messages & trees,
583-
-- then convert new traces to messages
584-
let mut msgs := (← get).messages
585-
for tree in (← getInfoTrees) do
586-
trace[Elab.info] (← tree.format)
591+
let msgs := (← get).messages
587592
modify fun st => { st with
588593
messages := initMsgs ++ msgs
589594
infoState := { st.infoState with trees := initInfoTrees ++ st.infoState.trees }

src/Lean/Elab/Frontend.lean

+2-2
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,9 @@ where
112112
|>.foldl (· ++ ·) {}
113113
-- In contrast to messages, we should collect info trees only from the top-level command
114114
-- snapshots as they subsume any info trees reported incrementally by their children.
115-
let trees := commands.map (·.finishedSnap.get.infoTree?) |>.filterMap id |>.toPArray'
115+
let trees := commands.map (·.infoTreeSnap.get.infoTree?) |>.filterMap id |>.toPArray'
116116
return {
117-
commandState := { snap.finishedSnap.get.cmdState with messages, infoState.trees := trees }
117+
commandState := { snap.resultSnap.get.cmdState with messages, infoState.trees := trees }
118118
parserState := snap.parserState
119119
cmdPos := snap.parserState.pos
120120
commands := commands.map (·.stx)

src/Lean/Elab/InfoTree/Main.lean

+8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Released under Apache 2.0 license as described in the file LICENSE.
55
Authors: Wojciech Nawrocki, Leonardo de Moura, Sebastian Ullrich
66
-/
77
prelude
8+
import Init.Task
89
import Lean.Meta.PPGoal
910
import Lean.ReservedNameAction
1011

@@ -95,6 +96,13 @@ partial def InfoTree.substitute (tree : InfoTree) (assignment : PersistentHashMa
9596
| none => hole id
9697
| some tree => substitute tree assignment
9798

99+
/-- Applies `s.lazyAssignment` to `s.trees`, asynchronously. -/
100+
def InfoState.substituteLazy (s : InfoState) : Task InfoState :=
101+
Task.mapList (tasks := s.lazyAssignment.toList.map (·.2)) fun _ => { s with
102+
trees := s.trees.map (·.substitute <| s.lazyAssignment.map (·.get))
103+
lazyAssignment := {}
104+
}
105+
98106
/-- Embeds a `CoreM` action in `IO` by supplying the information stored in `info`. -/
99107
def ContextInfo.runCoreM (info : ContextInfo) (x : CoreM α) : IO α := do
100108
/-

src/Lean/Elab/InfoTree/Types.lean

+5
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,11 @@ structure InfoState where
255255
enabled : Bool := true
256256
/-- Map from holes in the infotree to child infotrees. -/
257257
assignment : PersistentHashMap MVarId InfoTree := {}
258+
/--
259+
Assignments fulfilled by other elaboration tasks. We substitute them only just before reporting
260+
the info tree via a snapshot to avoid premature blocking.
261+
-/
262+
lazyAssignment : PersistentHashMap MVarId (Task InfoTree) := {}
258263
/-- Pending child trees of a node. -/
259264
trees : PersistentArray InfoTree := {}
260265
deriving Inhabited

src/Lean/Elab/InfoTrees.lean

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def elabInfoTrees : CommandElab
1717
logError "Info trees are disabled, can not use `#info_trees`."
1818
else
1919
elabCommand cmd
20-
let infoTrees ← getInfoTrees
20+
let infoTrees := (← getInfoState).substituteLazy.get.trees
2121
for t in infoTrees do
2222
logInfoAt tk (← t.format)
2323
| _ => throwUnsupportedSyntax

src/Lean/Language/Lean.lean

+45-26
Original file line numberDiff line numberDiff line change
@@ -524,12 +524,12 @@ where
524524
if let some oldNext := old.nextCmdSnap? then do
525525
let newProm ← IO.Promise.new
526526
-- can reuse range, syntax unchanged
527-
BaseIO.chainTask (sync := true) old.finishedSnap.task fun oldFinished =>
527+
BaseIO.chainTask (sync := true) old.resultSnap.task fun oldResult =>
528528
-- also wait on old command parse snapshot as parsing is cheap and may allow for
529529
-- elaboration reuse
530530
BaseIO.chainTask (sync := true) oldNext.task fun oldNext => do
531531
let cancelTk ← IO.CancelToken.new
532-
parseCmd oldNext newParserState oldFinished.cmdState newProm sync cancelTk ctx
532+
parseCmd oldNext newParserState oldResult.cmdState newProm sync cancelTk ctx
533533
prom.resolve <| { old with nextCmdSnap? := some {
534534
stx? := none
535535
reportingRange? := some ⟨newParserState.pos, ctx.input.endPos⟩
@@ -582,7 +582,8 @@ where
582582
prom.resolve <| {
583583
diagnostics := .empty, stx := .missing, parserState
584584
elabSnap := default
585-
finishedSnap := .finished none { diagnostics := .empty, cmdState }
585+
resultSnap := .finished none { diagnostics := .empty, cmdState }
586+
infoTreeSnap := .finished none { diagnostics := .empty }
586587
reportSnap := default
587588
nextCmdSnap? := none
588589
}
@@ -592,6 +593,7 @@ where
592593
let _ ← (if sync then BaseIO.asTask else (.pure <$> ·)) do
593594
-- definitely resolved in `doElab` task
594595
let elabPromise ← IO.Promise.new
596+
let resultPromise ← IO.Promise.new
595597
let finishedPromise ← IO.Promise.new
596598
let reportPromise ← IO.Promise.new
597599
let minimalSnapshots := internal.cmdlineSnapshots.get cmdState.scopes.head!.opts
@@ -602,11 +604,6 @@ where
602604
-- report terminal tasks on first line of decl such as not to hide incremental tactics'
603605
-- progress
604606
let initRange? := getNiceCommandStartPos? stx |>.map fun pos => ⟨pos, pos⟩
605-
let finishedSnap := {
606-
stx? := stx'
607-
reportingRange? := initRange?
608-
task := finishedPromise.result!
609-
}
610607
let next? ← if Parser.isTerminalCommand stx then pure none
611608
-- for now, wait on "command finished" snapshot before parsing next command
612609
else some <$> IO.Promise.new
@@ -621,21 +618,55 @@ where
621618
-- use per-command cancellation token for elaboration so that
622619
let elabCmdCancelTk ← IO.CancelToken.new
623620
prom.resolve {
624-
diagnostics, finishedSnap, nextCmdSnap?
621+
diagnostics, nextCmdSnap?
625622
stx := stx', parserState := parserState'
626623
elabSnap := { stx? := stx', task := elabPromise.result!, cancelTk? := some elabCmdCancelTk }
624+
resultSnap := { stx? := stx', reportingRange? := initRange?, task := resultPromise.result! }
625+
infoTreeSnap := { stx? := stx', reportingRange? := initRange?, task := finishedPromise.result! }
627626
reportSnap := { stx? := none, reportingRange? := initRange?, task := reportPromise.result! }
628627
}
629628
let cmdState ← doElab stx cmdState beginPos
630629
{ old? := old?.map fun old => ⟨old.stx, old.elabSnap⟩, new := elabPromise }
631-
finishedPromise elabCmdCancelTk ctx
630+
elabCmdCancelTk ctx
631+
632+
let mut reportedCmdState := cmdState
633+
let cmdline := internal.cmdlineSnapshots.get scope.opts && !Parser.isTerminalCommand stx
634+
if cmdline then
635+
-- discard all metadata apart from the environment; see `internal.cmdlineSnapshots`
636+
reportedCmdState := { env := reportedCmdState.env, maxRecDepth := 0 }
637+
resultPromise.resolve {
638+
diagnostics := (← Snapshot.Diagnostics.ofMessageLog cmdState.messages)
639+
traces := cmdState.traceState
640+
cmdState := reportedCmdState
641+
}
642+
643+
-- report info tree when relevant tasks are finished
644+
BaseIO.chainTask (sync := true) (t := cmdState.infoState.substituteLazy) fun infoSt => do
645+
let infoTree := infoSt.trees[0]!
646+
let opts := cmdState.scopes.head!.opts
647+
let mut msgLog := MessageLog.empty
648+
if (← isTracingEnabledForCore `Elab.info opts) then
649+
if let .ok msg ← infoTree.format.toBaseIO then
650+
let data := .tagged `trace <| .trace { cls := `Elab.info } .nil #[msg]
651+
msgLog := msgLog.add {
652+
fileName := ctx.fileName
653+
severity := MessageSeverity.information
654+
pos := ctx.fileMap.toPosition beginPos
655+
data := data
656+
}
657+
finishedPromise.resolve {
658+
diagnostics := (← Snapshot.Diagnostics.ofMessageLog msgLog)
659+
infoTree? := infoTree
660+
}
661+
662+
-- report traces when *all* tasks are finished
632663
let traceTask ←
633664
if (← isTracingEnabledForCore `Elab.snapshotTree cmdState.scopes.head!.opts) then
634665
-- We want to trace all of `CommandParsedSnapshot` but `traceTask` is part of it, so let's
635666
-- create a temporary snapshot tree containing all tasks but it
636667
let snaps := #[
637668
{ stx? := stx', task := elabPromise.result!.map (sync := true) toSnapshotTree },
638-
{ stx? := stx', task := finishedPromise.result!.map (sync := true) toSnapshotTree }] ++
669+
{ stx? := stx', task := resultPromise.result!.map (sync := true) toSnapshotTree }] ++
639670
cmdState.snapshotTasks
640671
let tree := SnapshotTree.mk { diagnostics := .empty } snaps
641672
BaseIO.bindTask (← tree.waitAll) fun _ => do
@@ -665,8 +696,8 @@ where
665696
parseCmd none parserState cmdState next (sync := false) elabCmdCancelTk ctx
666697

667698
doElab (stx : Syntax) (cmdState : Command.State) (beginPos : String.Pos)
668-
(snap : SnapshotBundle DynamicSnapshot) (finishedPromise : IO.Promise CommandFinishedSnapshot)
669-
(cancelTk : IO.CancelToken) : LeanProcessingM Command.State := do
699+
(snap : SnapshotBundle DynamicSnapshot) (cancelTk : IO.CancelToken) :
700+
LeanProcessingM Command.State := do
670701
let ctx ← read
671702
let scope := cmdState.scopes.head!
672703
-- reset per-command state
@@ -693,21 +724,9 @@ where
693724
data := output
694725
}
695726
let cmdState : Command.State := { cmdState with messages }
696-
let mut reportedCmdState := cmdState
697727
-- definitely resolve eventually
698728
snap.new.resolve <| .ofTyped { diagnostics := .empty : SnapshotLeaf }
699729

700-
let infoTree : InfoTree := cmdState.infoState.trees[0]!
701-
let cmdline := internal.cmdlineSnapshots.get scope.opts && !Parser.isTerminalCommand stx
702-
if cmdline then
703-
-- discard all metadata apart from the environment; see `internal.cmdlineSnapshots`
704-
reportedCmdState := { env := reportedCmdState.env, maxRecDepth := 0 }
705-
finishedPromise.resolve {
706-
diagnostics := (← Snapshot.Diagnostics.ofMessageLog cmdState.messages)
707-
infoTree? := infoTree
708-
traces := cmdState.traceState
709-
cmdState := reportedCmdState
710-
}
711730
-- The reported `cmdState` in the snapshot may be minimized as seen above, so we return the full
712731
-- state here for further processing on the same thread
713732
return cmdState
@@ -735,6 +754,6 @@ where goCmd snap :=
735754
if let some next := snap.nextCmdSnap? then
736755
goCmd next.get
737756
else
738-
snap.finishedSnap.get.cmdState
757+
snap.resultSnap.get.cmdState
739758

740759
end Lean

src/Lean/Language/Lean/Types.lean

+14-6
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,15 @@ private def pushOpt (a? : Option α) (as : Array α) : Array α :=
2525

2626
/-! The hierarchy of Lean snapshot types -/
2727

28-
/-- Snapshot after elaboration of the entire command. -/
29-
structure CommandFinishedSnapshot extends Language.Snapshot where
28+
/--
29+
Snapshot after command elaborator has returned. Also contains diagnostics from the elaborator's main
30+
task. Asynchronous elaboration tasks may not yet be finished.
31+
-/
32+
structure CommandResultSnapshot extends Language.Snapshot where
3033
/-- Resulting elaboration state. -/
3134
cmdState : Command.State
3235
deriving Nonempty
33-
instance : ToSnapshotTree CommandFinishedSnapshot where
36+
instance : ToSnapshotTree CommandResultSnapshot where
3437
toSnapshotTree s := ⟨s.toSnapshot, #[]⟩
3538

3639
/-- State after a command has been parsed. -/
@@ -44,8 +47,12 @@ structure CommandParsedSnapshot extends Snapshot where
4447
elaborator.
4548
-/
4649
elabSnap : SnapshotTask DynamicSnapshot
47-
/-- State after processing is finished. -/
48-
finishedSnap : SnapshotTask CommandFinishedSnapshot
50+
/-- State after command elaborator has returned. -/
51+
resultSnap : SnapshotTask CommandResultSnapshot
52+
/--
53+
State after all elaborator tasks are finished. In particular, contains the complete info tree.
54+
-/
55+
infoTreeSnap : SnapshotTask SnapshotLeaf
4956
/-- Additional, untyped snapshots used for reporting, not reuse. -/
5057
reportSnap : SnapshotTask SnapshotTree
5158
/-- Next command, unless this is a terminal command. -/
@@ -55,7 +62,8 @@ partial instance : ToSnapshotTree CommandParsedSnapshot where
5562
toSnapshotTree := go where
5663
go s := ⟨s.toSnapshot,
5764
#[s.elabSnap.map (sync := true) toSnapshotTree,
58-
s.finishedSnap.map (sync := true) toSnapshotTree,
65+
s.resultSnap.map (sync := true) toSnapshotTree,
66+
s.infoTreeSnap.map (sync := true) toSnapshotTree,
5967
s.reportSnap] |>
6068
pushOpt (s.nextCmdSnap?.map (·.map (sync := true) go))⟩
6169

src/Lean/Server/FileWorker/Utils.lean

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ private partial def mkCmdSnaps (initSnap : Language.Lean.InitialSnapshot) :
2828
} <| .delayed <| headerSuccess.firstCmdSnap.task.asServerTask.bindCheap go
2929
where
3030
go cmdParsed :=
31-
cmdParsed.finishedSnap.task.asServerTask.mapCheap fun finished =>
31+
cmdParsed.resultSnap.task.asServerTask.mapCheap fun result =>
3232
.ok <| .cons {
3333
stx := cmdParsed.stx
3434
mpState := cmdParsed.parserState
35-
cmdState := finished.cmdState
35+
cmdState := result.cmdState
3636
} (match cmdParsed.nextCmdSnap? with
3737
| some next => .delayed <| next.task.asServerTask.bindCheap go
3838
| none => .nil)

src/Lean/Server/Requests.lean

+3-4
Original file line numberDiff line numberDiff line change
@@ -304,10 +304,9 @@ def findCmdDataAtPos
304304
findCmdParsedSnap doc hoverPos |>.bindCheap fun
305305
| some cmdParsed => toSnapshotTree cmdParsed |>.findInfoTreeAtPos doc.meta.text hoverPos includeStop |>.bindCheap fun
306306
| some infoTree => .pure <| some (cmdParsed.stx, infoTree)
307-
| none => cmdParsed.finishedSnap.task.asServerTask.mapCheap fun s =>
308-
-- the parser returns exactly one command per snapshot, and the elaborator creates exactly one node per command
309-
assert! s.cmdState.infoState.trees.size == 1
310-
some (cmdParsed.stx, s.cmdState.infoState.trees[0]!)
307+
| none => cmdParsed.infoTreeSnap.task.asServerTask.mapCheap fun s =>
308+
assert! s.infoTree?.isSome
309+
some (cmdParsed.stx, s.infoTree?.get!)
311310
| none => .pure none
312311

313312
open Language in

0 commit comments

Comments
 (0)