diff --git a/doc/examples/test_single.sh b/doc/examples/test_single.sh index e341426aca9e..1ab18770de45 100755 --- a/doc/examples/test_single.sh +++ b/doc/examples/test_single.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash source ../../tests/common.sh -exec_check lean -j 0 -Dlinter.all=false "$f" +exec_check lean -Dlinter.all=false "$f" diff --git a/nix/bootstrap.nix b/nix/bootstrap.nix index 5bd2cfff22c3..85130e70d924 100644 --- a/nix/bootstrap.nix +++ b/nix/bootstrap.nix @@ -170,7 +170,7 @@ rec { ln -sf ${lean-all}/* . ''; buildPhase = '' - ctest --output-junit test-results.xml --output-on-failure -E 'leancomptest_(doc_example|foreign)' -j$NIX_BUILD_CORES + ctest --output-junit test-results.xml --output-on-failure -E 'leancomptest_(doc_example|foreign)|leanlaketest_init' -j$NIX_BUILD_CORES ''; installPhase = '' mkdir $out diff --git a/src/Init/Data/String/Basic.lean b/src/Init/Data/String/Basic.lean index 1dbe48e753a1..96d65a20ff71 100644 --- a/src/Init/Data/String/Basic.lean +++ b/src/Init/Data/String/Basic.lean @@ -815,6 +815,10 @@ def beq (ss1 ss2 : Substring) : Bool := instance hasBeq : BEq Substring := ⟨beq⟩ +/-- Checks whether two substrings have the same position and content. -/ +def sameAs (ss1 ss2 : Substring) : Bool := + ss1.startPos == ss2.startPos && ss1 == ss2 + end Substring namespace String diff --git a/src/Init/Prelude.lean b/src/Init/Prelude.lean index 36cbb836b2d2..f959e5de22a0 100644 --- a/src/Init/Prelude.lean +++ b/src/Init/Prelude.lean @@ -3644,6 +3644,17 @@ def getPos? (info : SourceInfo) (canonicalOnly := false) : Option String.Pos := | synthetic (pos := pos) .., false => some pos | _, _ => none +/-- +Gets the end position information from a `SourceInfo`, if available. +If `originalOnly` is true, then `.synthetic` syntax will also return `none`. +-/ +def getTailPos? (info : SourceInfo) (canonicalOnly := false) : Option String.Pos := + match info, canonicalOnly with + | original (endPos := endPos) .., _ + | synthetic (endPos := endPos) (canonical := true) .., _ + | synthetic (endPos := endPos) .., false => some endPos + | _, _ => none + end SourceInfo /-- diff --git a/src/Init/System/IO.lean b/src/Init/System/IO.lean index 3d53b77ecfbf..505910e1ce3b 100644 --- a/src/Init/System/IO.lean +++ b/src/Init/System/IO.lean @@ -228,6 +228,13 @@ local macro "nonempty_list" : tactic => /-- Helper method for implementing "deterministic" timeouts. It is the number of "small" memory allocations performed by the current execution thread. -/ @[extern "lean_io_get_num_heartbeats"] opaque getNumHeartbeats : BaseIO Nat +/-- +Adjusts the heartbeat counter of the current thread by the given amount. This can be useful to give +allocation-avoiding code additional "weight" and is also used to adjust the counter after resuming +from a snapshot. +-/ +@[extern "lean_io_add_heartbeats"] opaque addHeartbeats (count : UInt64) : BaseIO Unit + /-- The mode of a file handle (i.e., a set of `open` flags and an `fdopen` mode). @@ -744,6 +751,32 @@ instance : MonadLift (ST IO.RealWorld) BaseIO := ⟨id⟩ def mkRef (a : α) : BaseIO (IO.Ref α) := ST.mkRef a +/-- +Mutable cell that can be passed around for purposes of cooperative task cancellation: request +cancellation with `CancelToken.set` and check for it with `CancelToken.isSet`. + +This is a more flexible alternative to `Task.cancel` as the token can be shared between multiple +tasks. +-/ +structure CancelToken where + private ref : IO.Ref Bool + +namespace CancelToken + +/-- Creates a new cancellation token. -/ +def new : BaseIO CancelToken := + CancelToken.mk <$> IO.mkRef false + +/-- Activates a cancellation token. Idempotent. -/ +def set (tk : CancelToken) : BaseIO Unit := + tk.ref.set true + +/-- Checks whether the cancellation token has been activated. -/ +def isSet (tk : CancelToken) : BaseIO Bool := + tk.ref.get + +end CancelToken + namespace FS namespace Stream diff --git a/src/Lean/CoreM.lean b/src/Lean/CoreM.lean index 76f9c36315f7..a7f904ed4dc4 100644 --- a/src/Lean/CoreM.lean +++ b/src/Lean/CoreM.lean @@ -11,6 +11,7 @@ import Lean.Eval import Lean.ResolveName import Lean.Elab.InfoTree.Types import Lean.MonadEnv +import Lean.Elab.Exception namespace Lean register_builtin_option diagnostics : Bool := { @@ -89,6 +90,8 @@ structure Context where Use the `set_option diag true` to set it to true. -/ diag : Bool := false + /-- If set, used to cancel elaboration from outside when results are not needed anymore. -/ + cancelTk? : Option IO.CancelToken := none deriving Nonempty /-- CoreM is a monad for manipulating the Lean environment. @@ -198,16 +201,45 @@ instance : MonadTrace CoreM where getTraceState := return (← get).traceState modifyTraceState f := modify fun s => { s with traceState := f s.traceState } -/-- Restore backtrackable parts of the state. -/ -def restore (b : State) : CoreM Unit := - modify fun s => { s with env := b.env, messages := b.messages, infoState := b.infoState } +structure SavedState extends State where + /-- Number of heartbeats passed inside `withRestoreOrSaveFull`, not used otherwise. -/ + passedHearbeats : Nat +deriving Nonempty + +def saveState : CoreM SavedState := do + let s ← get + return { toState := s, passedHearbeats := 0 } /-- -Restores full state including sources for unique identifiers. Only intended for incremental reuse -between elaboration runs, not for backtracking within a single run. +Incremental reuse primitive: if `old?` is `none`, runs `cont` with an action `save` that on +execution returns the saved monadic state at this point including the heartbeats used by `cont` so +far. If `old?` on the other hand is `some (a, state)`, restores full `state` including heartbeats +used and returns `a`. + +The intention is for steps that support incremental reuse to initially pass `none` as `old?` and +call `save` as late as possible in `cont`. In a further run, if reuse is possible, `old?` should be +set to the previous state and result, ensuring that the state after running `withRestoreOrSaveFull` +is identical in both runs. Note however that necessarily this is only an approximation in the case +of heartbeats as heartbeats used by `withRestoreOrSaveFull`, by the remainder of `cont` after +calling `save`, as well as by reuse-handling code such as the one supplying `old?` are not accounted +for. -/ -def restoreFull (b : State) : CoreM Unit := - set b +@[specialize] def withRestoreOrSaveFull (old? : Option (α × SavedState)) + (cont : (save : CoreM SavedState) → CoreM α) : CoreM α := do + if let some (oldVal, oldState) := old? then + set oldState.toState + IO.addHeartbeats oldState.passedHearbeats.toUInt64 + return oldVal + + let startHeartbeats ← IO.getNumHeartbeats + cont (do + let s ← get + let stopHeartbeats ← IO.getNumHeartbeats + return { toState := s, passedHearbeats := stopHeartbeats - startHeartbeats }) + +/-- Restore backtrackable parts of the state. -/ +def SavedState.restore (b : SavedState) : CoreM Unit := + modify fun s => { s with env := b.env, messages := b.messages, infoState := b.infoState } private def mkFreshNameImp (n : Name) : CoreM Name := do let fresh ← modifyGet fun s => (s.nextMacroScope, { s with nextMacroScope := s.nextMacroScope + 1 }) @@ -238,10 +270,18 @@ instance [MetaEval α] : MetaEval (CoreM α) where protected def withIncRecDepth [Monad m] [MonadControlT CoreM m] (x : m α) : m α := controlAt CoreM fun runInBase => withIncRecDepth (runInBase x) +builtin_initialize interruptExceptionId : InternalExceptionId ← registerInternalExceptionId `interrupt + +/-- +Throws an internal interrupt exception if cancellation has been requested. The exception is not +caught by `try catch` but is intended to be caught by `Command.withLoggingExceptions` at the top +level of elaboration. In particular, we want to skip producing further incremental snapshots after +the exception has been thrown. + -/ @[inline] def checkInterrupted : CoreM Unit := do - if (← IO.checkCanceled) then - -- should never be visible to users! - throw <| Exception.error .missing "elaboration interrupted" + if let some tk := (← read).cancelTk? then + if (← tk.isSet) then + throw <| .internal interruptExceptionId def throwMaxHeartbeat (moduleName : Name) (optionName : Name) (max : Nat) : CoreM Unit := do let msg := s!"(deterministic) timeout at `{moduleName}`, maximum number of heartbeats ({max/1000}) has been reached\nuse `set_option {optionName} ` to set the limit\nuse `set_option {diagnostics.name} true` to get diagnostic information" @@ -278,11 +318,13 @@ def getMessageLog : CoreM MessageLog := return (← get).messages /-- -Returns the current log and then resets its messages but does NOT reset `MessageLog.hadErrors`. Used +Returns the current log and then resets its messages while adjusting `MessageLog.hadErrors`. Used for incremental reporting during elaboration of a single command. -/ def getAndEmptyMessageLog : CoreM MessageLog := - modifyGet fun log => ({ log with msgs := {} }, log) + modifyGet fun s => (s.messages, { s with + messages.msgs := {} + messages.hadErrors := s.messages.hasErrors }) instance : MonadLog CoreM where getRef := getRef @@ -409,19 +451,25 @@ def ImportM.runCoreM (x : CoreM α) : ImportM α := do let (a, _) ← (withOptions (fun _ => ctx.opts) x).toIO { fileName := "", fileMap := default } { env := ctx.env } return a -/-- Return `true` if the exception was generated by one our resource limits. -/ +/-- Return `true` if the exception was generated by one of our resource limits. -/ def Exception.isRuntime (ex : Exception) : Bool := ex.isMaxHeartbeat || ex.isMaxRecDepth +/-- Returns `true` if the exception is an interrupt generated by `checkInterrupted`. -/ +def Exception.isInterrupt : Exception → Bool + | Exception.internal id _ => id == Core.interruptExceptionId + | _ => false + /-- -Custom `try-catch` for all monads based on `CoreM`. We don't want to catch "runtime exceptions" -in these monads, but on `CommandElabM`. See issues #2775 and #2744 as well as `MonadAlwayExcept`. +Custom `try-catch` for all monads based on `CoreM`. We usually don't want to catch "runtime +exceptions" these monads, but on `CommandElabM`. See issues #2775 and #2744 as well as +`MonadAlwaysExcept`. Also, we never want to catch interrupt exceptions inside the elaborator. -/ @[inline] protected def Core.tryCatch (x : CoreM α) (h : Exception → CoreM α) : CoreM α := do try x catch ex => - if ex.isRuntime && !(← read).catchRuntimeEx then + if ex.isInterrupt || (ex.isRuntime && !(← read).catchRuntimeEx) then throw ex else h ex diff --git a/src/Lean/Elab/Command.lean b/src/Lean/Elab/Command.lean index e2bf601aaeee..71c523aa14ba 100644 --- a/src/Lean/Elab/Command.lean +++ b/src/Lean/Elab/Command.lean @@ -47,8 +47,9 @@ structure Context where ref : Syntax := Syntax.missing tacticCache? : Option (IO.Ref Tactic.Cache) /-- - Snapshot for incremental reuse and reporting of command elaboration. Currently unused in Lean - itself. + Snapshot for incremental reuse and reporting of command elaboration. Currently only used for + (mutual) defs and contained tactics, in which case the `DynamicSnapshot` is a + `HeadersParsedSnapshot`. Definitely resolved in `Language.Lean.process.doElab`. @@ -56,6 +57,8 @@ structure Context where old elaboration are identical. -/ snap? : Option (Language.SnapshotBundle Language.DynamicSnapshot) + /-- Cancellation token forwarded to `Core.cancelTk?`. -/ + cancelTk? : Option IO.CancelToken abbrev CommandElabCoreM (ε) := ReaderT Context $ StateRefT State $ EIO ε abbrev CommandElabM := CommandElabCoreM Exception @@ -73,6 +76,21 @@ Remark: see comment at TermElabM @[always_inline] instance : Monad CommandElabM := let i := inferInstanceAs (Monad CommandElabM); { pure := i.pure, bind := i.bind } +/-- Like `Core.tryCatch` but do catch runtime exceptions. -/ +@[inline] protected def tryCatch (x : CommandElabM α) (h : Exception → CommandElabM α) : + CommandElabM α := do + try + x + catch ex => + if ex.isInterrupt then + throw ex + else + h ex + +instance : MonadExceptOf Exception CommandElabM where + throw := throw + tryCatch := Command.tryCatch + def mkState (env : Environment) (messages : MessageLog := {}) (opts : Options := {}) : State := { env := env messages := messages @@ -140,7 +158,8 @@ private def mkCoreContext (ctx : Context) (s : State) (heartbeats : Nat) : Core. openDecls := scope.openDecls initHeartbeats := heartbeats currMacroScope := ctx.currMacroScope - diag := getDiag scope.opts } + diag := getDiag scope.opts + cancelTk? := ctx.cancelTk? } private def addTraceAsMessagesCore (ctx : Context) (log : MessageLog) (traceState : TraceState) : MessageLog := Id.run do if traceState.traces.isEmpty then return log @@ -468,7 +487,12 @@ def runTermElabM (elabFn : Array Expr → TermElabM α) : CommandElabM α := do Term.addAutoBoundImplicits' xs someType fun xs _ => Term.withoutAutoBoundImplicit <| elabFn xs -@[inline] def catchExceptions (x : CommandElabM Unit) : CommandElabCoreM Empty Unit := fun ctx ref => +/-- +Catches and logs exceptions occurring in `x`. Unlike `try catch` in `CommandElabM`, this function +catches interrupt exceptions as well and thus is intended for use at the top level of elaboration. +Interrupt and abort exceptions are caught but not logged. +-/ +@[inline] def withLoggingExceptions (x : CommandElabM Unit) : CommandElabCoreM Empty Unit := fun ctx ref => EIO.catchExceptions (withLogging x ctx ref) (fun _ => pure ()) private def liftAttrM {α} (x : AttrM α) : CommandElabM α := do @@ -534,6 +558,7 @@ def liftCommandElabM (cmd : CommandElabM α) : CoreM α := do ref := ← getRef tacticCache? := none snap? := none + cancelTk? := (← read).cancelTk? } |>.run { env := ← getEnv maxRecDepth := ← getMaxRecDepth diff --git a/src/Lean/Elab/DefView.lean b/src/Lean/Elab/DefView.lean index 091e3a3f3c74..96cf72c0cade 100644 --- a/src/Lean/Elab/DefView.lean +++ b/src/Lean/Elab/DefView.lean @@ -28,6 +28,81 @@ def DefKind.isExample : DefKind → Bool | .example => true | _ => false +/-- Header elaboration data of a `DefView`. -/ +structure DefViewElabHeaderData where + /-- + Short name. Recall that all declarations in Lean 4 are potentially recursive. We use `shortDeclName` to refer + to them at `valueStx`, and other declarations in the same mutual block. -/ + shortDeclName : Name + /-- Full name for this declaration. This is the name that will be added to the `Environment`. -/ + declName : Name + /-- Universe level parameter names explicitly provided by the user. -/ + levelNames : List Name + /-- Syntax objects for the binders occurring before `:`, we use them to populate the `InfoTree` when elaborating `valueStx`. -/ + binderIds : Array Syntax + /-- Number of parameters before `:`, it also includes auto-implicit parameters automatically added by Lean. -/ + numParams : Nat + /-- Type including parameters. -/ + type : Expr +deriving Inhabited + +section Snapshots +open Language + +/-- Snapshot after processing of a definition body. -/ +structure BodyProcessedSnapshot extends Language.Snapshot where + /-- State after elaboration. -/ + state : Term.SavedState + /-- Elaboration result. -/ + value : Expr +deriving Nonempty +instance : Language.ToSnapshotTree BodyProcessedSnapshot where + toSnapshotTree s := ⟨s.toSnapshot, #[]⟩ + +/-- Snapshot after elaboration of a definition header. -/ +structure HeaderProcessedSnapshot extends Language.Snapshot where + /-- Elaboration results. -/ + view : DefViewElabHeaderData + /-- Resulting elaboration state, including any environment additions. -/ + state : Term.SavedState + /-- Syntax of top-level tactic block if any, for checking reuse of `tacSnap?`. -/ + tacStx? : Option Syntax + /-- Incremental execution of main tactic block, if any. -/ + tacSnap? : Option (SnapshotTask Tactic.TacticParsedSnapshot) + /-- Syntax of definition body, for checking reuse of `bodySnap`. -/ + bodyStx : Syntax + /-- Result of body elaboration. -/ + bodySnap : SnapshotTask (Option BodyProcessedSnapshot) +deriving Nonempty +instance : Language.ToSnapshotTree HeaderProcessedSnapshot where + toSnapshotTree s := ⟨s.toSnapshot, + (match s.tacSnap? with + | some tac => #[tac.map (sync := true) toSnapshotTree] + | none => #[]) ++ + #[s.bodySnap.map (sync := true) toSnapshotTree]⟩ + +/-- State before elaboration of a mutual definition. -/ +structure DefParsed where + /-- + Input substring uniquely identifying header elaboration result given the same `Environment`. + If missing, results should never be reused. + -/ + headerSubstr? : Option Substring + /-- Elaboration result, unless fatal exception occurred. -/ + headerProcessedSnap : SnapshotTask (Option HeaderProcessedSnapshot) +deriving Nonempty + +/-- Snapshot after syntax tree has been split into separate mutual def headers. -/ +structure DefsParsedSnapshot extends Language.Snapshot where + /-- Definitions of this mutual block. -/ + defs : Array DefParsed +deriving Nonempty, TypeName +instance : Language.ToSnapshotTree DefsParsedSnapshot where + toSnapshotTree s := ⟨s.toSnapshot, + s.defs.map (·.headerProcessedSnap.map (sync := true) toSnapshotTree)⟩ + +end Snapshots + structure DefView where kind : DefKind ref : Syntax @@ -36,6 +111,13 @@ structure DefView where binders : Syntax type? : Option Syntax value : Syntax + /-- + Snapshot for incremental processing of this definition. + + Invariant: If the bundle's `old?` is set, then elaboration of the header is guaranteed to result + in the same elaboration result and state, i.e. reuse is possible. + -/ + headerSnap? : Option (Language.SnapshotBundle (Option HeaderProcessedSnapshot)) := none deriving? : Option (Array Syntax) := none deriving Inhabited diff --git a/src/Lean/Elab/Exception.lean b/src/Lean/Elab/Exception.lean index 518915864e4d..0a27f965e284 100644 --- a/src/Lean/Elab/Exception.lean +++ b/src/Lean/Elab/Exception.lean @@ -5,7 +5,7 @@ Authors: Leonardo de Moura -/ prelude import Lean.InternalExceptionId -import Lean.Meta.Basic +import Lean.Exception namespace Lean.Elab diff --git a/src/Lean/Elab/Frontend.lean b/src/Lean/Elab/Frontend.lean index 0048cb4dab45..384ff3bd629b 100644 --- a/src/Lean/Elab/Frontend.lean +++ b/src/Lean/Elab/Frontend.lean @@ -16,6 +16,7 @@ structure State where parserState : Parser.ModuleParserState cmdPos : String.Pos commands : Array Syntax := #[] +deriving Nonempty structure Context where inputCtx : Parser.InputContext @@ -34,6 +35,7 @@ def setCommandState (commandState : Command.State) : FrontendM Unit := fileMap := ctx.inputCtx.fileMap tacticCache? := none snap? := none + cancelTk? := none } match (← liftM <| EIO.toIO' <| (x cmdCtx).run s.commandState) with | Except.error e => throw <| IO.Error.userError s!"unexpected internal error: {← e.toMessageData.toString}" @@ -50,9 +52,10 @@ def elabCommandAtFrontend (stx : Syntax) : FrontendM Unit := do stx.hasMissing then -- discard elaboration errors, except for a few important and unlikely misleading ones, on -- parse error - msgs := ⟨msgs.msgs.filter fun msg => - msg.data.hasTag (fun tag => tag == `Elab.synthPlaceholder || - tag == `Tactic.unsolvedGoals || (`_traceMsg).isSuffixOf tag)⟩ + msgs := { msgs with + msgs := msgs.msgs.filter fun msg => + msg.data.hasTag (fun tag => tag == `Elab.synthPlaceholder || + tag == `Tactic.unsolvedGoals || (`_traceMsg).isSuffixOf tag) } modify ({ · with messages := initMsgs ++ msgs }) def updateCmdPos : FrontendM Unit := do @@ -92,6 +95,47 @@ def IO.processCommands (inputCtx : Parser.InputContext) (parserState : Parser.Mo let (_, s) ← (Frontend.processCommands.run { inputCtx := inputCtx }).run { commandState := commandState, parserState := parserState, cmdPos := parserState.pos } pure s +structure IncrementalState extends State where + inputCtx : Parser.InputContext + initialSnap : Language.Lean.CommandParsedSnapshot +deriving Nonempty + +open Language in +/-- +Variant of `IO.processCommands` that uses the new Lean language processor implementation for +potential incremental reuse. Pass in result of a previous invocation done with the same state +(but usually different input context) to allow for reuse. +-/ +-- `IO.processCommands` can be reimplemented on top of this as soon as the additional tasks speed up +-- things instead of slowing them down +partial def IO.processCommandsIncrementally (inputCtx : Parser.InputContext) + (parserState : Parser.ModuleParserState) (commandState : Command.State) + (old? : Option IncrementalState) : + BaseIO IncrementalState := do + let task ← Language.Lean.processCommands inputCtx parserState commandState + (old?.map fun old => (old.inputCtx, old.initialSnap)) + go task.get task #[] +where + go initialSnap t commands := + let snap := t.get + let commands := commands.push snap.data.stx + if let some next := snap.nextCmdSnap? then + go initialSnap next commands + else + -- Opting into reuse also enables incremental reporting, so make sure to collect messages from + -- all snapshots + let messages := toSnapshotTree initialSnap + |>.getAll.map (·.diagnostics.msgLog) + |>.foldl (· ++ ·) {} + let trees := toSnapshotTree initialSnap + |>.getAll.map (·.infoTree?) |>.filterMap id |>.toPArray' + return { + commandState := { snap.data.finishedSnap.get.cmdState with messages, infoState.trees := trees } + parserState := snap.data.parserState + cmdPos := snap.data.parserState.pos + inputCtx, initialSnap, commands + } + def process (input : String) (env : Environment) (opts : Options) (fileName : Option String := none) : IO (Environment × MessageLog) := do let fileName := fileName.getD "" let inputCtx := Parser.mkInputContext input fileName @@ -113,8 +157,7 @@ def runFrontend : IO (Environment × Bool) := do let startTime := (← IO.monoNanosNow).toFloat / 1000000000 let inputCtx := Parser.mkInputContext input fileName - -- TODO: replace with `#lang` processing - if /- Lean #lang? -/ true then + if true then -- Temporarily keep alive old cmdline driver for the Lean language so that we don't pay the -- overhead of passing the environment between snapshots until we actually make good use of it -- outside the server diff --git a/src/Lean/Elab/MutualDef.lean b/src/Lean/Elab/MutualDef.lean index ed8c16c386c7..7caaf43f9503 100644 --- a/src/Lean/Elab/MutualDef.lean +++ b/src/Lean/Elab/MutualDef.lean @@ -20,28 +20,24 @@ import Lean.Elab.DeclarationRange namespace Lean.Elab open Lean.Parser.Term -/-- `DefView` after elaborating the header. -/ -structure DefViewElabHeader where - ref : Syntax - modifiers : Modifiers - /-- Stores whether this is the header of a definition, theorem, ... -/ - kind : DefKind +open Language + +/-- `DefView` plus header elaboration data and snapshot. -/ +structure DefViewElabHeader extends DefView, DefViewElabHeaderData where + /-- + Snapshot for incremental processing of top-level tactic block, if any. + + Invariant: if the bundle's `old?` is set, then the state *up to the start* of the tactic block is + unchanged, i.e. reuse is possible. + -/ + tacSnap? : Option (Language.SnapshotBundle Tactic.TacticParsedSnapshot) /-- - Short name. Recall that all declarations in Lean 4 are potentially recursive. We use `shortDeclName` to refer - to them at `valueStx`, and other declarations in the same mutual block. -/ - shortDeclName : Name - /-- Full name for this declaration. This is the name that will be added to the `Environment`. -/ - declName : Name - /-- Universe level parameter names explicitly provided by the user. -/ - levelNames : List Name - /-- Syntax objects for the binders occurring before `:`, we use them to populate the `InfoTree` when elaborating `valueStx`. -/ - binderIds : Array Syntax - /-- Number of parameters before `:`, it also includes auto-implicit parameters automatically added by Lean. -/ - numParams : Nat - /-- Type including parameters. -/ - type : Expr - /-- `Syntax` object the body/value of the definition. -/ - valueStx : Syntax + Snapshot for incremental processing of definition body. + + Invariant: if the bundle's `old?` is set, then elaboration of the body is guaranteed to result in + the same elaboration result and state, i.e. reuse is possible. + -/ + bodySnap? : Option (Language.SnapshotBundle (Option BodyProcessedSnapshot)) deriving Inhabited namespace Term @@ -127,14 +123,63 @@ private def cleanupOfNat (type : Expr) : MetaM Expr := do let eNew := mkApp e.appFn! argArgs[1]! return .done eNew -/-- Elaborate only the declaration headers. We have to elaborate the headers first because we support mutually recursive declarations in Lean 4. -/ -private def elabHeaders (views : Array DefView) : TermElabM (Array DefViewElabHeader) := do +/-- +Elaborates only the declaration view headers. We have to elaborate the headers first because we +support mutually recursive declarations in Lean 4. +-/ +private def elabHeaders (views : Array DefView) + (bodyPromises : Array (IO.Promise (Option BodyProcessedSnapshot))) + (tacPromises : Array (IO.Promise Tactic.TacticParsedSnapshot)) : + TermElabM (Array DefViewElabHeader) := do let expandedDeclIds ← views.mapM fun view => withRef view.ref do Term.expandDeclId (← getCurrNamespace) (← getLevelNames) view.declId view.modifiers withAutoBoundImplicitForbiddenPred (fun n => expandedDeclIds.any (·.shortName == n)) do let mut headers := #[] - for view in views, ⟨shortDeclName, declName, levelNames⟩ in expandedDeclIds do - let newHeader ← withRef view.ref do + -- Can we reuse the result for a body? For starters, all headers (even those below the body) + -- must be reusable + let mut reuseBody := views.all (·.headerSnap?.any (·.old?.isSome)) + for view in views, ⟨shortDeclName, declName, levelNames⟩ in expandedDeclIds, + tacPromise in tacPromises, bodyPromise in bodyPromises do + let mut reusableResult? := none + if let some snap := view.headerSnap? then + -- by the `DefView.headerSnap?` invariant, safe to reuse results at this point, so let's + -- wait for them! + if let some old := snap.old?.bind (·.val.get) then + let (tacStx?, newTacTask?) ← mkTacTask view.value tacPromise + snap.new.resolve <| some { old with + tacStx? + tacSnap? := newTacTask? + bodyStx := view.value + bodySnap := mkBodyTask view.value bodyPromise + } + -- Transition from `DefView.snap?` to `DefViewElabHeader.tacSnap?` invariant: if all + -- headers and all previous bodies could be reused, then the state at the *start* of the + -- top-level tactic block (if any) is unchanged + let reuseTac := reuseBody + -- Transition from `DefView.snap?` to `DefViewElabHeader.bodySnap?` invariant: if all + -- headers and all previous bodies could be reused and this body syntax is unchanged, then + -- we can reuse the result + reuseBody := reuseBody && + view.value.structRangeEqWithTraceReuse (← getOptions) old.bodyStx + let header := { old.view, view with + tacSnap? := some { + old? := do + guard reuseTac + some ⟨(← old.tacStx?), (← old.tacSnap?)⟩ + new := tacPromise + } + bodySnap? := some { + -- no syntax guard to store, we already did the necessary checks + old? := guard reuseBody *> pure ⟨.missing, old.bodySnap⟩ + new := bodyPromise + } + } + reusableResult? := some (header, old.state) + else + reuseBody := false + + let header ← withRestoreOrSaveFull reusableResult? fun save => do + withRef view.ref do addDeclarationRanges declName view.ref applyAttributesAt declName view.modifiers.attrs .beforeElaboration withDeclName declName <| withAutoBoundImplicit <| withLevelNames levelNames <| @@ -164,21 +209,60 @@ private def elabHeaders (views : Array DefView) : TermElabM (Array DefViewElabHe let pendingMVarIds ← getMVars type discard <| logUnassignedUsingErrorInfos pendingMVarIds <| getPendindMVarErrorMessage views - let newHeader := { - ref := view.ref - modifiers := view.modifiers - kind := view.kind - shortDeclName := shortDeclName - declName, type, levelNames, binderIds - numParams := xs.size - valueStx := view.value : DefViewElabHeader } + let newHeader : DefViewElabHeaderData := { + declName, shortDeclName, type, levelNames, binderIds + numParams := xs.size + } + let mut newHeader : DefViewElabHeader := { view, newHeader with + bodySnap? := none, tacSnap? := none } + if let some snap := view.headerSnap? then + let (tacStx?, newTacTask?) ← mkTacTask view.value tacPromise + snap.new.resolve <| some { + diagnostics := + (← Language.Snapshot.Diagnostics.ofMessageLog (← Core.getAndEmptyMessageLog)) + view := newHeader.toDefViewElabHeaderData + state := (← save) + tacStx? + tacSnap? := newTacTask? + bodyStx := view.value + bodySnap := mkBodyTask view.value bodyPromise + } + newHeader := { newHeader with + tacSnap? := some { old? := none, new := tacPromise } + bodySnap? := some { old? := none, new := bodyPromise } + } check headers newHeader return newHeader - headers := headers.push newHeader + headers := headers.push header return headers +where + getBodyTerm? (stx : Syntax) : Option Syntax := + -- TODO: does not work with partial syntax + --| `(Parser.Command.declVal| := $body $_suffix:suffix $[$_where]?) => body + guard (stx.isOfKind ``Parser.Command.declValSimple) *> some stx[1] + + /-- Creates snapshot task with appropriate range from body syntax and promise. -/ + mkBodyTask (body : Syntax) (new : IO.Promise (Option BodyProcessedSnapshot)) : + Language.SnapshotTask (Option BodyProcessedSnapshot) := + let rangeStx := getBodyTerm? body |>.getD body + { range? := rangeStx.getRange?, task := new.result } + + /-- + If `body` allows for incremental tactic reporting and reuse, creates a snapshot task out of the + passed promise with appropriate range, otherwise immediately resolves the promise to a dummy + value. + -/ + mkTacTask (body : Syntax) (tacPromise : IO.Promise Tactic.TacticParsedSnapshot) : + TermElabM (Option Syntax × Option (Language.SnapshotTask Tactic.TacticParsedSnapshot)) + := do + if let some e := getBodyTerm? body then + if let `(by $tacs*) := e then + return (e, some { range? := mkNullNode tacs |>.getRange?, task := tacPromise.result }) + tacPromise.resolve default + return (none, none) /-- - Create auxiliary local declarations `fs` for the given hearders using their `shortDeclName` and `type`, given hearders, and execute `k fs`. + Create auxiliary local declarations `fs` for the given headers using their `shortDeclName` and `type`, given headers, and execute `k fs`. The new free variables are tagged as `auxDecl`. Remark: `fs.size = headers.size`. -/ @@ -250,15 +334,40 @@ private def declValToTerminationHint (declVal : Syntax) : TermElabM WF.Terminati return .none private def elabFunValues (headers : Array DefViewElabHeader) : TermElabM (Array Expr) := - headers.mapM fun header => withDeclName header.declName <| withLevelNames header.levelNames do - let valStx ← liftMacroM <| declValToTerm header.valueStx - forallBoundedTelescope header.type header.numParams fun xs type => do - -- Add new info nodes for new fvars. The server will detect all fvars of a binder by the binder's source location. - for i in [0:header.binderIds.size] do - -- skip auto-bound prefix in `xs` - addLocalVarInfo header.binderIds[i]! xs[header.numParams - header.binderIds.size + i]! - let val ← elabTermEnsuringType valStx type - mkLambdaFVars xs val + headers.mapM fun header => do + let mut reusableResult? := none + if let some snap := header.bodySnap? then + if let some old := snap.old? then + -- guaranteed reusable as by the `bodySnap?` invariant, so let's wait on the previous + -- 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) + + withRestoreOrSaveFull reusableResult? fun save => do + withDeclName header.declName <| withLevelNames header.levelNames do + let valStx ← liftMacroM <| declValToTerm header.value + forallBoundedTelescope header.type header.numParams fun xs type => do + -- Add new info nodes for new fvars. The server will detect all fvars of a binder by the binder's source location. + for i in [0:header.binderIds.size] do + -- skip auto-bound prefix in `xs` + addLocalVarInfo header.binderIds[i]! xs[header.numParams - header.binderIds.size + i]! + let val ← withReader ({ · with tacSnap? := header.tacSnap? }) do + elabTermEnsuringType valStx type <* Term.synthesizeSyntheticMVarsNoPostponing + let val ← mkLambdaFVars xs val + if let some snap := header.bodySnap? then + snap.new.resolve <| some { + diagnostics := + (← Language.Snapshot.Diagnostics.ofMessageLog (← Core.getAndEmptyMessageLog)) + state := (← save) + value := val + } + return val private def collectUsed (headers : Array DefViewElabHeader) (values : Array Expr) (toLift : List LetRecToLift) : StateRefT CollectFVars.State MetaM Unit := do @@ -640,7 +749,7 @@ def pushMain (preDefs : Array PreDefinition) (sectionVars : Array Expr) (mainHea : TermElabM (Array PreDefinition) := mainHeaders.size.foldM (init := preDefs) fun i preDefs => do let header := mainHeaders[i]! - let termination ← declValToTerminationHint header.valueStx + let termination ← declValToTerminationHint header.value let termination := termination.rememberExtraParams header.numParams mainVals[i]! let value ← mkLambdaFVars sectionVars mainVals[i]! let type ← mkForallFVars sectionVars header.type @@ -796,38 +905,40 @@ def elabMutualDef (vars : Array Expr) (views : Array DefView) : TermElabM Unit : else go where - go := do - let scopeLevelNames ← getLevelNames - let headers ← elabHeaders views - let headers ← levelMVarToParamHeaders views headers - let allUserLevelNames := getAllUserLevelNames headers - withFunLocalDecls headers fun funFVars => do - for view in views, funFVar in funFVars do - addLocalVarInfo view.declId funFVar - let values ← - try - let values ← elabFunValues headers - Term.synthesizeSyntheticMVarsNoPostponing - values.mapM (instantiateMVars ·) - catch ex => - logException ex - headers.mapM fun header => mkSorry header.type (synthetic := true) - let headers ← headers.mapM instantiateMVarsAtHeader - let letRecsToLift ← getLetRecsToLift - let letRecsToLift ← letRecsToLift.mapM instantiateMVarsAtLetRecToLift - checkLetRecsToLiftTypes funFVars letRecsToLift - withUsed vars headers values letRecsToLift fun vars => do - let preDefs ← MutualClosure.main vars headers funFVars values letRecsToLift - for preDef in preDefs do - trace[Elab.definition] "{preDef.declName} : {preDef.type} :=\n{preDef.value}" - let preDefs ← withLevelNames allUserLevelNames <| levelMVarToParamPreDecls preDefs - let preDefs ← instantiateMVarsAtPreDecls preDefs - let preDefs ← fixLevelParams preDefs scopeLevelNames allUserLevelNames - for preDef in preDefs do - trace[Elab.definition] "after eraseAuxDiscr, {preDef.declName} : {preDef.type} :=\n{preDef.value}" - checkForHiddenUnivLevels allUserLevelNames preDefs - addPreDefinitions preDefs - processDeriving headers + go := + withAlwaysResolvedPromises views.size fun bodyPromises => + withAlwaysResolvedPromises views.size fun tacPromises => do + let scopeLevelNames ← getLevelNames + let headers ← elabHeaders views bodyPromises tacPromises + let headers ← levelMVarToParamHeaders views headers + let allUserLevelNames := getAllUserLevelNames headers + withFunLocalDecls headers fun funFVars => do + for view in views, funFVar in funFVars do + addLocalVarInfo view.declId funFVar + let values ← + try + let values ← elabFunValues headers + Term.synthesizeSyntheticMVarsNoPostponing + values.mapM (instantiateMVars ·) + catch ex => + logException ex + headers.mapM fun header => mkSorry header.type (synthetic := true) + let headers ← headers.mapM instantiateMVarsAtHeader + let letRecsToLift ← getLetRecsToLift + let letRecsToLift ← letRecsToLift.mapM instantiateMVarsAtLetRecToLift + checkLetRecsToLiftTypes funFVars letRecsToLift + withUsed vars headers values letRecsToLift fun vars => do + let preDefs ← MutualClosure.main vars headers funFVars values letRecsToLift + for preDef in preDefs do + trace[Elab.definition] "{preDef.declName} : {preDef.type} :=\n{preDef.value}" + let preDefs ← withLevelNames allUserLevelNames <| levelMVarToParamPreDecls preDefs + let preDefs ← instantiateMVarsAtPreDecls preDefs + let preDefs ← fixLevelParams preDefs scopeLevelNames allUserLevelNames + for preDef in preDefs do + trace[Elab.definition] "after eraseAuxDiscr, {preDef.declName} : {preDef.type} :=\n{preDef.value}" + checkForHiddenUnivLevels allUserLevelNames preDefs + addPreDefinitions preDefs + processDeriving headers processDeriving (headers : Array DefViewElabHeader) := do for header in headers, view in views do @@ -842,12 +953,45 @@ end Term namespace Command def elabMutualDef (ds : Array Syntax) : CommandElabM Unit := do - let views ← ds.mapM fun d => do - let modifiers ← elabModifiers d[0] - if ds.size > 1 && modifiers.isNonrec then - throwErrorAt d "invalid use of 'nonrec' modifier in 'mutual' block" - mkDefView modifiers d[1] - runTermElabM fun vars => Term.elabMutualDef vars views + withAlwaysResolvedPromises ds.size fun headerPromises => do + let substr? := (mkNullNode ds).getSubstring? + let snap? := (← read).snap? + let mut views := #[] + let mut defs := #[] + for h : i in [0:ds.size], headerPromise in headerPromises do + let d := ds[i] + let modifiers ← elabModifiers d[0] + if ds.size > 1 && modifiers.isNonrec then + throwErrorAt d "invalid use of 'nonrec' modifier in 'mutual' block" + let mut view ← mkDefView modifiers d[1] + if let some snap := snap? then + -- overapproximation: includes previous bodies as well + let headerSubstr? := return { (← substr?) with stopPos := (← view.value.getPos?) } + view := { view with headerSnap? := some { + old? := do + -- transitioning from `Context.snap?` to `DefView.snap?` invariant: if the elaboration + -- context and state are unchanged, and the substring from the beginning of the first + -- header to the beginning of the current body is unchanged, then the elaboration result for + -- this header (which includes state from elaboration of previous headers!) should be + -- unchanged. + let old ← snap.old? + -- blocking wait, `HeadersParsedSnapshot` (and hopefully others) should be quick + let old ← old.val.get.toTyped? DefsParsedSnapshot + let oldParsed ← old.defs[i]? + guard <| (← headerSubstr?).sameAs (← oldParsed.headerSubstr?) + -- no syntax guard to store, we already did the necessary checks + return ⟨.missing, oldParsed.headerProcessedSnap⟩ + new := headerPromise + } } + defs := defs.push { + headerSubstr? + headerProcessedSnap := { range? := d.getRange?, task := headerPromise.result } + } + views := views.push view + if let some snap := snap? then + -- no non-fatal diagnostics at this point + snap.new.resolve <| .ofTyped { defs, diagnostics := .empty : DefsParsedSnapshot } + runTermElabM fun vars => Term.elabMutualDef vars views end Command end Lean.Elab diff --git a/src/Lean/Elab/SyntheticMVars.lean b/src/Lean/Elab/SyntheticMVars.lean index a39d02766165..264148e048ad 100644 --- a/src/Lean/Elab/SyntheticMVars.lean +++ b/src/Lean/Elab/SyntheticMVars.lean @@ -298,7 +298,6 @@ mutual If `report := false`, then `runTactic` will not capture exceptions nor will report unsolved goals. Unsolved goals become exceptions. -/ partial def runTactic (mvarId : MVarId) (tacticCode : Syntax) (report := true) : TermElabM Unit := withoutAutoBoundImplicit do - let code := tacticCode[1] instantiateMVarDeclMVars mvarId /- TODO: consider using `runPendingTacticsAt` at `mvarId` local context and target type. @@ -320,7 +319,7 @@ mutual -- also put an info node on the `by` keyword specifically -- the token may be `canonical` and thus shown in the info -- view even though it is synthetic while a node like `tacticCode` never is (#1990) withTacticInfoContext tacticCode[0] do - evalTactic code + withNarrowedArgTacticReuse (argIdx := 1) (evalTactic ·) tacticCode synthesizeSyntheticMVars (mayPostpone := false) unless remainingGoals.isEmpty do if report then diff --git a/src/Lean/Elab/Tactic/Basic.lean b/src/Lean/Elab/Tactic/Basic.lean index 70064dfcdbf3..a8305c7e2b76 100644 --- a/src/Lean/Elab/Tactic/Basic.lean +++ b/src/Lean/Elab/Tactic/Basic.lean @@ -34,10 +34,6 @@ structure Context where -/ recover : Bool := true -structure SavedState where - term : Term.SavedState - tactic : State - abbrev TacticM := ReaderT Context $ StateRefT State TermElabM abbrev Tactic := Syntax → TacticM Unit @@ -100,6 +96,16 @@ def SavedState.restore (b : SavedState) (restoreInfo := false) : TacticM Unit := b.term.restore restoreInfo set b.tactic +@[specialize, inherit_doc Core.withRestoreOrSaveFull] +def withRestoreOrSaveFull (old? : Option (α × SavedState)) + (cont : TacticM SavedState → TacticM α) : TacticM α := do + if let some (_, oldState) := old? then + set oldState.tactic + let old? := old?.map (fun (oldVal, oldState) => (oldVal, oldState.term)) + controlAt TermElabM fun runInBase => + Term.withRestoreOrSaveFull old? fun restore => + runInBase <| cont (return { term := (← restore), tactic := (← get) }) + protected def getCurrMacroScope : TacticM MacroScope := do pure (← readThe Core.Context).currMacroScope protected def getMainModule : TacticM Name := do pure (← getEnv).mainModule @@ -432,6 +438,12 @@ def getNameOfIdent' (id : Syntax) : Name := def withCaseRef [Monad m] [MonadRef m] (arrow body : Syntax) (x : m α) : m α := withRef (mkNullNode #[arrow, body]) x +-- TODO: attribute(s) +builtin_initialize builtinIncrementalTactics : IO.Ref NameSet ← IO.mkRef {} + +def registerBuiltinIncrementalTactic (kind : Name) : IO Unit := do + builtinIncrementalTactics.modify fun s => s.insert kind + builtin_initialize registerTraceClass `Elab.tactic builtin_initialize registerTraceClass `Elab.tactic.backtrack diff --git a/src/Lean/Elab/Tactic/BuiltinTactic.lean b/src/Lean/Elab/Tactic/BuiltinTactic.lean index ea40cb6f0fd3..2968b1f7ec4e 100644 --- a/src/Lean/Elab/Tactic/BuiltinTactic.lean +++ b/src/Lean/Elab/Tactic/BuiltinTactic.lean @@ -29,13 +29,95 @@ open Parser.Tactic @[builtin_tactic Lean.Parser.Tactic.«done»] def evalDone : Tactic := fun _ => done -@[builtin_tactic seq1] def evalSeq1 : Tactic := fun stx => do - let args := stx[0].getArgs - for i in [:args.size] do - if i % 2 == 0 then - evalTactic args[i]! - else - saveTacticInfoForToken args[i]! -- add `TacticInfo` node for `;` +open Language in +/-- +Evaluates a tactic script in form of a syntax node with alternating tactics and separators as +children. + -/ +partial def evalSepTactics : Tactic := goEven +where + -- `stx[0]` is the next tactic step, if any + goEven stx := do + if stx.getNumArgs == 0 then + return + let tac := stx[0] + /- + Each `goEven` step creates three promises under incrementality and reuses their older versions + where possible: + * `finished` is resolved when `tac` finishes execution; if `tac` is wholly unchanged from the + previous version, its state is reused and `tac` execution is skipped. Note that this promise + is never turned into a `SnapshotTask` and added to the snapshot tree as incremental reporting + is already covered by the next two promises. + * `inner` is passed to `tac` if it is marked as supporting incrementality and can be used for + reporting and partial reuse inside of it; if the tactic is unsupported or `finished` is wholly + reused, it is ignored. + * `next` is used as the context when invoking `goOdd` and thus eventually used for the next + `goEven` step. Thus, the incremental state of a tactic script is ultimately represented as a + chain of `next` snapshots. Its reuse is disabled if `tac` or its following separator are + changed in any way. + -/ + let mut oldInner? := none + if let some snap := (← readThe Term.Context).tacSnap? then + if let some old := snap.old? then + let oldParsed := old.val.get + oldInner? := oldParsed.next.get? 0 |>.map (⟨oldParsed.data.stx, ·⟩) + -- compare `stx[0]` for `finished`/`next` reuse, focus on remainder of script + Term.withNarrowedTacticReuse (stx := stx) (fun stx => (stx[0], mkNullNode stx.getArgs[1:])) fun stxs => do + let some snap := (← readThe Term.Context).tacSnap? + | do evalTactic tac; goOdd stxs + let mut reusableResult? := none + let mut oldNext? := none + if let some old := snap.old? then + -- `tac` must be unchanged given the narrow above; let's reuse `finished`'s state! + let oldParsed := old.val.get + if let some state := oldParsed.data.finished.get.state? then + reusableResult? := some (state, state) + -- only allow `next` reuse in this case + oldNext? := oldParsed.next.get? 1 |>.map (⟨old.stx, ·⟩) + + withAlwaysResolvedPromise fun next => do + withAlwaysResolvedPromise fun finished => do + withAlwaysResolvedPromise fun inner => do + snap.new.resolve <| .mk { + stx := tac + diagnostics := (← Language.Snapshot.Diagnostics.ofMessageLog + (← Core.getAndEmptyMessageLog)) + finished := finished.result + } #[ + { + range? := tac.getRange? + task := inner.result }, + { + range? := stxs |>.getRange? + task := next.result }] + let state ← withRestoreOrSaveFull reusableResult? fun save => do + -- allow nested reuse for allowlisted tactics + withTheReader Term.Context ({ · with + tacSnap? := + guard ((← builtinIncrementalTactics.get).contains tac.getKind) *> + some { + old? := oldInner? + new := inner + } }) do + evalTactic tac + save + finished.resolve { state? := state } + + withTheReader Term.Context ({ · with tacSnap? := some { + new := next + old? := oldNext? + } }) do + goOdd stxs + -- `stx[0]` is the next separator, if any + goOdd stx := do + if stx.getNumArgs == 0 then + return + saveTacticInfoForToken stx[0] -- add `TacticInfo` node for `;` + -- disable further reuse on separator change as to not reuse wrong `TacticInfo` + Term.withNarrowedTacticReuse (fun stx => (stx[0], mkNullNode stx.getArgs[1:])) goEven stx + +@[builtin_tactic seq1] def evalSeq1 : Tactic := fun stx => + evalSepTactics stx[0] @[builtin_tactic paren] def evalParen : Tactic := fun stx => evalTactic stx[1] @@ -104,25 +186,19 @@ def addCheckpoints (stx : Syntax) : TacticM Syntax := do output := output ++ currentCheckpointBlock return stx.setArgs output -/-- Evaluate `sepByIndent tactic "; " -/ -def evalSepByIndentTactic (stx : Syntax) : TacticM Unit := do - let stx ← addCheckpoints stx - for arg in stx.getArgs, i in [:stx.getArgs.size] do - if i % 2 == 0 then - evalTactic arg - else - saveTacticInfoForToken arg - -@[builtin_tactic tacticSeq1Indented] def evalTacticSeq1Indented : Tactic := fun stx => - evalSepByIndentTactic stx[0] +builtin_initialize registerBuiltinIncrementalTactic ``tacticSeq1Indented +@[builtin_tactic tacticSeq1Indented] def evalTacticSeq1Indented : Tactic := + Term.withNarrowedArgTacticReuse (argIdx := 0) evalSepTactics +builtin_initialize registerBuiltinIncrementalTactic ``tacticSeqBracketed @[builtin_tactic tacticSeqBracketed] def evalTacticSeqBracketed : Tactic := fun stx => do let initInfo ← mkInitialTacticInfo stx[0] withRef stx[2] <| closeUsingOrAdmit do -- save state before/after entering focus on `{` withInfoContext (pure ()) initInfo - evalSepByIndentTactic stx[1] + Term.withNarrowedArgTacticReuse (argIdx := 1) evalSepTactics stx +builtin_initialize registerBuiltinIncrementalTactic ``cdot @[builtin_tactic cdot] def evalTacticCDot : Tactic := fun stx => do -- adjusted copy of `evalTacticSeqBracketed`; we used to use the macro -- ``| `(tactic| $cdot:cdotTk $tacs) => `(tactic| {%$cdot ($tacs) }%$cdot)`` @@ -132,7 +208,7 @@ def evalSepByIndentTactic (stx : Syntax) : TacticM Unit := do withRef stx[0] <| closeUsingOrAdmit do -- save state before/after entering focus on `·` withInfoContext (pure ()) initInfo - evalSepByIndentTactic stx[1] + Term.withNarrowedArgTacticReuse (argIdx := 1) evalTactic stx @[builtin_tactic Parser.Tactic.focus] def evalFocus : Tactic := fun stx => do let mkInfo ← mkInitialTacticInfo stx[0] @@ -205,8 +281,9 @@ private def getOptRotation (stx : Syntax) : Nat := throwError "failed on all goals" setGoals mvarIdsNew.toList -@[builtin_tactic tacticSeq] def evalTacticSeq : Tactic := fun stx => - evalTactic stx[0] +builtin_initialize registerBuiltinIncrementalTactic ``tacticSeq +@[builtin_tactic tacticSeq] def evalTacticSeq : Tactic := + Term.withNarrowedArgTacticReuse (argIdx := 0) evalTactic partial def evalChoiceAux (tactics : Array Syntax) (i : Nat) : TacticM Unit := if h : i < tactics.size then @@ -426,16 +503,16 @@ where .group <| .nest 2 <| .ofFormat .line ++ .joinSep items sep - +builtin_initialize registerBuiltinIncrementalTactic ``case @[builtin_tactic «case»] def evalCase : Tactic - | stx@`(tactic| case $[$tag $hs*]|* =>%$arr $tac:tacticSeq) => + | stx@`(tactic| case $[$tag $hs*]|* =>%$arr $tac:tacticSeq1Indented) => for tag in tag, hs in hs do let (g, gs) ← getCaseGoals tag let g ← renameInaccessibles g hs setGoals [g] g.setTag Name.anonymous - withCaseRef arr tac do - closeUsingOrAdmit (withTacticInfoContext stx (evalTactic tac)) + withCaseRef arr tac <| closeUsingOrAdmit <| withTacticInfoContext stx <| + Term.withNarrowedArgTacticReuse (argIdx := 3) (evalTactic ·) stx setGoals gs | _ => throwUnsupportedSyntax diff --git a/src/Lean/Elab/Tactic/Induction.lean b/src/Lean/Elab/Tactic/Induction.lean index e74577c6aea1..2d895af10910 100644 --- a/src/Lean/Elab/Tactic/Induction.lean +++ b/src/Lean/Elab/Tactic/Induction.lean @@ -54,23 +54,25 @@ private def getAltDArrow (alt : Syntax) : Syntax := def isHoleRHS (rhs : Syntax) : Bool := rhs.isOfKind ``Parser.Term.syntheticHole || rhs.isOfKind ``Parser.Term.hole -def evalAlt (mvarId : MVarId) (alt : Syntax) (addInfo : TermElabM Unit) (remainingGoals : Array MVarId) : TacticM (Array MVarId) := +def evalAlt (mvarId : MVarId) (alt : Syntax) (addInfo : TermElabM Unit) : TacticM Unit := let rhs := getAltRHS alt withCaseRef (getAltDArrow alt) rhs do if isHoleRHS rhs then addInfo - let gs' ← mvarId.withContext <| withTacticInfoContext rhs do + mvarId.withContext <| withTacticInfoContext rhs do let mvarDecl ← mvarId.getDecl let val ← elabTermEnsuringType rhs mvarDecl.type mvarId.assign val let gs' ← getMVarsNoDelayed val tagUntaggedGoals mvarDecl.userName `induction gs'.toList - pure gs' - return remainingGoals ++ gs' + setGoals <| (← getGoals) ++ gs'.toList else - setGoals [mvarId] - closeUsingOrAdmit (withTacticInfoContext alt (addInfo *> evalTactic rhs)) - return remainingGoals + let goals ← getGoals + try + setGoals [mvarId] + closeUsingOrAdmit (withTacticInfoContext alt (addInfo *> evalTactic rhs)) + finally + setGoals goals /-! Helper method for creating an user-defined eliminator/recursor application. @@ -199,6 +201,9 @@ private def getAltNumFields (elimInfo : ElimInfo) (altName : Name) : TermElabM N return altInfo.numFields throwError "unknown alternative name '{altName}'" +private def isWildcard (altStx : Syntax) : Bool := + getAltName altStx == `_ + private def checkAltNames (alts : Array Alt) (altsSyntax : Array Syntax) : TacticM Unit := for i in [:altsSyntax.size] do let altStx := altsSyntax[i]! @@ -229,151 +234,184 @@ private def saveAltVarsInfo (altMVarId : MVarId) (altStx : Syntax) (fvarIds : Ar Term.addLocalVarInfo altVars[i]! (mkFVar fvarId) i := i + 1 -/-- - If `altsSyntax` is not empty we reorder `alts` using the order the alternatives have been provided - in `altsSyntax`. Motivations: - - 1- It improves the effectiveness of the `checkpoint` and `save` tactics. Consider the following example: - ```lean - example (h₁ : p ∨ q) (h₂ : p → x = 0) (h₃ : q → y = 0) : x * y = 0 := by - cases h₁ with - | inr h => - sleep 5000 -- sleeps for 5 seconds - save - have : y = 0 := h₃ h - -- We can confortably work here - | inl h => stop ... - ``` - If we do reorder, the `inl` alternative will be executed first. Moreover, as we type in the `inr` alternative, - type errors will "swallow" the `inl` alternative and affect the tactic state at `save` making it ineffective. - - 2- The errors are produced in the same order the appear in the code above. This is not super important when using IDEs. --/ -def reorderAlts (alts : Array Alt) (altsSyntax : Array Syntax) : Array Alt := Id.run do - if altsSyntax.isEmpty then - return alts - else - let mut alts := alts - let mut result := #[] - for altStx in altsSyntax do - let altName := getAltName altStx - let some i := alts.findIdx? (·.1 == altName) | return result ++ alts - result := result.push alts[i]! - alts := alts.eraseIdx i - return result ++ alts - -def evalAlts (elimInfo : ElimInfo) (alts : Array Alt) (optPreTac : Syntax) (altsSyntax : Array Syntax) +open Language in +def evalAlts (elimInfo : ElimInfo) (alts : Array Alt) (optPreTac : Syntax) (altStxs : Array Syntax) (initialInfo : Info) (numEqs : Nat := 0) (numGeneralized : Nat := 0) (toClear : Array FVarId := #[]) (toTag : Array (Ident × FVarId) := #[]) : TacticM Unit := do - let hasAlts := altsSyntax.size > 0 + let hasAlts := altStxs.size > 0 if hasAlts then -- default to initial state outside of alts -- HACK: because this node has the same span as the original tactic, -- we need to take all the info trees we have produced so far and re-nest them -- inside this node as well let treesSaved ← getResetInfoTrees - withInfoContext ((modifyInfoState fun s => { s with trees := treesSaved }) *> go) (pure initialInfo) - else go + withInfoContext ((modifyInfoState fun s => { s with trees := treesSaved }) *> goWithInfo) (pure initialInfo) + else goWithInfo where - go := do - checkAltNames alts altsSyntax - let alts := reorderAlts alts altsSyntax - let hasAlts := altsSyntax.size > 0 - let mut usedWildcard := false - let mut subgoals := #[] -- when alternatives are not provided, we accumulate subgoals here - let mut altsSyntax := altsSyntax + -- continuation in the correct info context + goWithInfo := do + let hasAlts := altStxs.size > 0 + + if hasAlts then + if let some tacSnap := (← readThe Term.Context).tacSnap? then + -- incrementality: create a new promise for each alternative, resolve current snapshot to + -- them, eventually put each of them back in `Context.tacSnap?` in `applyAltStx` + withAlwaysResolvedPromise fun finished => do + withAlwaysResolvedPromises altStxs.size fun altPromises => do + tacSnap.new.resolve <| .mk { + -- save all relevant syntax here for comparison with next document version + stx := mkNullNode altStxs + diagnostics := .empty + finished := finished.result + } (altStxs.zipWith altPromises fun stx prom => + { range? := stx.getRange?, task := prom.result }) + goWithIncremental <| altPromises.mapIdx fun i prom => { + old? := do + let old ← tacSnap.old? + -- waiting is fine here: this is the old version of the snapshot resolved above + -- immediately at the beginning of the tactic + let old := old.val.get + -- use old version of `mkNullNode altsSyntax` as guard, will be compared with new + -- version and picked apart in `applyAltStx` + return ⟨old.data.stx, (← old.next[i]?)⟩ + new := prom + } + finished.resolve { state? := (← saveState) } + return + + goWithIncremental #[] + + -- continuation in the correct incrementality context + goWithIncremental (tacSnaps : Array (SnapshotBundle TacticParsedSnapshot)) := do + let hasAlts := altStxs.size > 0 + let mut alts := alts + + -- initial sanity checks: named cases should be known, wildcards should be last + checkAltNames alts altStxs + + /- + First process `altsSyntax` in order, removing covered alternatives from `alts`. Previously we + did one loop through `alts`, looking up suitable alternatives from `altsSyntax`. + Motivations for the change: + + 1- It improves the effectiveness of incremental reuse. Consider the following example: + ```lean + example (h₁ : p ∨ q) (h₂ : p → x = 0) (h₃ : q → y = 0) : x * y = 0 := by + cases h₁ with + | inr h => + sleep 5000 -- sleeps for 5 seconds + save + have : y = 0 := h₃ h + -- We can comfortably work here + | inl h => stop ... + ``` + If we iterated through `alts` instead of `altsSyntax`, the `inl` alternative would be executed + first, making partial reuse in `inr` impossible (without support for reuse with position + adjustments). + + 2- The errors are produced in the same order the appear in the code above. This is not super + important when using IDEs. + -/ + for altStxIdx in [0:altStxs.size] do + let altStx := altStxs[altStxIdx]! + let altName := getAltName altStx + if let some i := alts.findIdx? (·.1 == altName) then + -- cover named alternative + applyAltStx tacSnaps altStxIdx altStx alts[i]! + alts := alts.eraseIdx i + else if !alts.isEmpty && isWildcard altStx then + -- cover all alternatives + for alt in alts do + applyAltStx tacSnaps altStxIdx altStx alt + alts := #[] + else + throwError "unused alternative '{altName}'" + + -- now process remaining alternatives; these might either be unreachable or we're in `induction` + -- without `with`. In all other cases, remaining alternatives are flagged as errors. for { name := altName, info, mvarId := altMVarId } in alts do let numFields ← getAltNumFields elimInfo altName - let mut isWildcard := false - let altStx? ← - match altsSyntax.findIdx? (fun alt => getAltName alt == altName) with - | some idx => - let altStx := altsSyntax[idx]! - altsSyntax := altsSyntax.eraseIdx idx - pure (some altStx) - | none => match altsSyntax.findIdx? (fun alt => getAltName alt == `_) with - | some idx => - isWildcard := true - pure (some altsSyntax[idx]!) - | none => - pure none - match altStx? with - | none => - let mut (_, altMVarId) ← altMVarId.introN numFields - match (← Cases.unifyEqs? numEqs altMVarId {}) with - | none => pure () -- alternative is not reachable - | some (altMVarId', subst) => - altMVarId ← if info.provesMotive then - (_, altMVarId) ← altMVarId'.introNP numGeneralized - pure altMVarId - else - pure altMVarId' - for fvarId in toClear do - altMVarId ← altMVarId.tryClear fvarId - altMVarId.withContext do - for (stx, fvar) in toTag do - Term.addLocalVarInfo stx (subst.get fvar) - let altMVarIds ← applyPreTac altMVarId - if !hasAlts then - -- User did not provide alternatives using `|` - subgoals := subgoals ++ altMVarIds.toArray - else if altMVarIds.isEmpty then - pure () - else - logError m!"alternative '{altName}' has not been provided" - altMVarIds.forM fun mvarId => admitGoal mvarId - | some altStx => - (subgoals, usedWildcard) ← withRef altStx do - let altVars := getAltVars altStx - let numFieldsToName ← if altHasExplicitModifier altStx then pure numFields else getNumExplicitFields altMVarId numFields - if altVars.size > numFieldsToName then - logError m!"too many variable names provided at alternative '{altName}', #{altVars.size} provided, but #{numFieldsToName} expected" - let mut (fvarIds, altMVarId) ← altMVarId.introN numFields (altVars.toList.map getNameOfIdent') (useNamesForExplicitOnly := !altHasExplicitModifier altStx) - -- Delay adding the infos for the pattern LHS because we want them to nest - -- inside tacticInfo for the current alternative (in `evalAlt`) - let addInfo : TermElabM Unit := do - if (← getInfoState).enabled then - if let some declName := info.declName? then - addConstInfo (getAltNameStx altStx) declName - saveAltVarsInfo altMVarId altStx fvarIds - let unusedAlt := do - addInfo - if isWildcard then - pure (#[], usedWildcard) - else - throwError "alternative '{altName}' is not needed" - match (← Cases.unifyEqs? numEqs altMVarId {}) with - | none => unusedAlt - | some (altMVarId', subst) => - altMVarId ← if info.provesMotive then - (_, altMVarId) ← altMVarId'.introNP numGeneralized - pure altMVarId - else - pure altMVarId' - for fvarId in toClear do - altMVarId ← altMVarId.tryClear fvarId - altMVarId.withContext do - for (stx, fvar) in toTag do - Term.addLocalVarInfo stx (subst.get fvar) - let altMVarIds ← applyPreTac altMVarId - if altMVarIds.isEmpty then - unusedAlt - else - let mut subgoals := subgoals - for altMVarId' in altMVarIds do - subgoals ← evalAlt altMVarId' altStx addInfo subgoals - pure (subgoals, usedWildcard || isWildcard) - if usedWildcard then - altsSyntax := altsSyntax.filter fun alt => getAltName alt != `_ - unless altsSyntax.isEmpty do - logErrorAt altsSyntax[0]! "unused alternative" - setGoals subgoals.toList + let mut (_, altMVarId) ← altMVarId.introN numFields + let some (altMVarId', subst) ← Cases.unifyEqs? numEqs altMVarId {} + | continue -- alternative is not reachable + altMVarId ← if info.provesMotive then + (_, altMVarId) ← altMVarId'.introNP numGeneralized + pure altMVarId + else + pure altMVarId' + for fvarId in toClear do + altMVarId ← altMVarId.tryClear fvarId + altMVarId.withContext do + for (stx, fvar) in toTag do + Term.addLocalVarInfo stx (subst.get fvar) + let altMVarIds ← applyPreTac altMVarId + if !hasAlts then + -- User did not provide alternatives using `|` + setGoals <| (← getGoals) ++ altMVarIds + else if !altMVarIds.isEmpty then + logError m!"alternative '{altName}' has not been provided" + altMVarIds.forM fun mvarId => admitGoal mvarId + + /-- Applies syntactic alternative to alternative goal. -/ + applyAltStx tacSnaps altStxIdx altStx alt := withRef altStx do + let { name := altName, info, mvarId := altMVarId } := alt + -- also checks for unknown alternatives + let numFields ← getAltNumFields elimInfo altName + let altVars := getAltVars altStx + let numFieldsToName ← if altHasExplicitModifier altStx then pure numFields else getNumExplicitFields altMVarId numFields + if altVars.size > numFieldsToName then + logError m!"too many variable names provided at alternative '{altName}', #{altVars.size} provided, but #{numFieldsToName} expected" + let mut (fvarIds, altMVarId) ← altMVarId.introN numFields (altVars.toList.map getNameOfIdent') (useNamesForExplicitOnly := !altHasExplicitModifier altStx) + -- Delay adding the infos for the pattern LHS because we want them to nest + -- inside tacticInfo for the current alternative (in `evalAlt`) + let addInfo : TermElabM Unit := do + if (← getInfoState).enabled then + if let some declName := info.declName? then + addConstInfo (getAltNameStx altStx) declName + saveAltVarsInfo altMVarId altStx fvarIds + let unusedAlt := do + addInfo + if !isWildcard altStx then + throwError "alternative '{altName}' is not needed" + let some (altMVarId', subst) ← Cases.unifyEqs? numEqs altMVarId {} + | unusedAlt + altMVarId ← if info.provesMotive then + (_, altMVarId) ← altMVarId'.introNP numGeneralized + pure altMVarId + else + pure altMVarId' + for fvarId in toClear do + altMVarId ← altMVarId.tryClear fvarId + altMVarId.withContext do + for (stx, fvar) in toTag do + Term.addLocalVarInfo stx (subst.get fvar) + let altMVarIds ← applyPreTac altMVarId + if altMVarIds.isEmpty then + return (← unusedAlt) + + -- select corresponding snapshot bundle for incrementality of this alternative + -- note that `tacSnaps[altStxIdx]?` is `none` if `tacSnap?` was `none` to begin with + withTheReader Term.Context ({ · with tacSnap? := tacSnaps[altStxIdx]? }) do + -- all previous alternatives have to be unchanged for reuse + Term.withNarrowedArgTacticReuse (stx := mkNullNode altStxs) (argIdx := altStxIdx) fun altStx => do + -- everything up to rhs has to be unchanged for reuse + Term.withNarrowedArgTacticReuse (stx := altStx) (argIdx := 2) fun _rhs => do + -- disable reuse if rhs is run multiple times + Term.withoutTacticIncrementality (altMVarIds.length != 1 || isWildcard altStx) do + for altMVarId' in altMVarIds do + evalAlt altMVarId' altStx addInfo + + /-- Applies `induction .. with $preTac | ..`, if any, to an alternative goal. -/ applyPreTac (mvarId : MVarId) : TacticM (List MVarId) := if optPreTac.isNone then return [mvarId] else - evalTacticAt optPreTac[0] mvarId + -- disable incrementality for the pre-tactic to avoid non-monotonic progress reporting; it + -- would be possible to include a custom task around the pre-tac with an appropriate range in + -- the snapshot such that it is cached as well if it turns out that this is valuable + Term.withoutTacticIncrementality true do + evalTacticAt optPreTac[0] mvarId end ElimApp @@ -582,12 +620,23 @@ private def generalizeTargets (exprs : Array Expr) : TacticM (Array Expr) := do else return exprs +builtin_initialize registerBuiltinIncrementalTactic ``Lean.Parser.Tactic.induction @[builtin_tactic Lean.Parser.Tactic.induction] def evalInduction : Tactic := fun stx => match expandInduction? stx with | some stxNew => withMacroExpansion stx stxNew <| evalTactic stxNew | _ => focus do - let optInductionAlts := stx[4] - let alts := getAltsOfOptInductionAlts optInductionAlts + -- drill down into old and new syntax: allow reuse of an rhs only if everything before it is + -- unchanged + -- everything up to the alternatives must be unchanged for reuse + Term.withNarrowedArgTacticReuse (stx := stx) (argIdx := 4) fun optInductionAlts => do + Term.withNarrowedTacticReuse (stx := optInductionAlts) (fun optInductionAlts => + if optInductionAlts.isNone then + -- if there are no alternatives, what to compare is irrelevant as there will be no reuse + (mkNullNode #[], mkNullNode #[]) + else + -- `with` and tactic applied to all branches must be unchanged for reuse + (mkNullNode optInductionAlts[0].getArgs[:2], optInductionAlts[0].getArg 2)) fun alts => do + let alts := alts.getArgs let targets ← withMainContext <| stx[1].getSepArgs.mapM (elabTerm · none) let targets ← generalizeTargets targets let elimInfo ← withMainContext <| getElimNameInfo stx[2] targets (induction := true) diff --git a/src/Lean/Elab/Term.lean b/src/Lean/Elab/Term.lean index ce929262f801..bdc19a971714 100644 --- a/src/Lean/Elab/Term.lean +++ b/src/Lean/Elab/Term.lean @@ -13,6 +13,7 @@ import Lean.Elab.Config import Lean.Elab.Level import Lean.Elab.DeclModifiers import Lean.Elab.PreDefinition.WF.TerminationHint +import Lean.Language.Basic namespace Lean.Elab @@ -112,6 +113,14 @@ structure State where letRecsToLift : List LetRecToLift := [] deriving Inhabited +/-- + Backtrackable state for the `TermElabM` monad. +-/ +structure SavedState where + meta : Meta.SavedState + «elab» : State + deriving Nonempty + end Term namespace Tactic @@ -152,6 +161,42 @@ structure Cache where post : PHashMap CacheKey Snapshot := {} deriving Inhabited +section Snapshot +open Language + +structure SavedState where + term : Term.SavedState + tactic : State + +/-- State after finishing execution of a tactic. -/ +structure TacticFinished where + /-- Reusable state, if no fatal exception occurred. -/ + state? : Option SavedState +deriving Inhabited + +/-- Snapshot just before execution of a tactic. -/ +structure TacticParsedSnapshotData extends Language.Snapshot where + /-- Syntax tree of the tactic, stored and compared for incremental reuse. -/ + stx : Syntax + /-- Task for state after tactic execution. -/ + finished : Task TacticFinished +deriving Inhabited + +/-- State after execution of a single synchronous tactic step. -/ +inductive TacticParsedSnapshot where + | mk (data : TacticParsedSnapshotData) (next : Array (SnapshotTask TacticParsedSnapshot)) +deriving Inhabited +abbrev TacticParsedSnapshot.data : TacticParsedSnapshot → TacticParsedSnapshotData + | .mk data _ => data +/-- Potential, potentially parallel, follow-up tactic executions. -/ +-- In the first, non-parallel version, each task will depend on its predecessor +abbrev TacticParsedSnapshot.next : TacticParsedSnapshot → Array (SnapshotTask TacticParsedSnapshot) + | .mk _ next => next +partial instance : ToSnapshotTree TacticParsedSnapshot where + toSnapshotTree := go where + go := fun ⟨s, next⟩ => ⟨s.toSnapshot, next.map (·.map (sync := true) go)⟩ + +end Snapshot end Tactic namespace Term @@ -211,6 +256,13 @@ structure Context where /-- Cache for the `save` tactic. It is only `some` in the LSP server. -/ tacticCache? : Option (IO.Ref Tactic.Cache) := none /-- + Snapshot for incremental processing of current tactic, if any. + + Invariant: if the bundle's `old?` is set, then the state *up to the start* of the tactic is + unchanged, i.e. reuse is possible. + -/ + tacSnap? : Option (Language.SnapshotBundle Tactic.TacticParsedSnapshot) := none + /-- If `true`, we store in the `Expr` the `Syntax` for recursive applications (i.e., applications of free variables tagged with `isAuxDecl`). We store the `Syntax` using `mkRecAppWithSyntax`. We use the `Syntax` object to produce better error messages at `Structural.lean` and `WF.lean`. -/ @@ -241,14 +293,6 @@ open Meta instance : Inhabited (TermElabM α) where default := throw default -/-- - Backtrackable state for the `TermElabM` monad. --/ -structure SavedState where - meta : Meta.SavedState - «elab» : State - deriving Nonempty - protected def saveState : TermElabM SavedState := return { meta := (← Meta.saveState), «elab» := (← get) } @@ -261,18 +305,87 @@ def SavedState.restore (s : SavedState) (restoreInfo : Bool := false) : TermElab unless restoreInfo do setInfoState infoState -/-- -Restores full state including sources for unique identifiers. Only intended for incremental reuse -between elaboration runs, not for backtracking within a single run. --/ -def SavedState.restoreFull (s : SavedState) : TermElabM Unit := do - s.meta.restoreFull - set s.elab +@[specialize, inherit_doc Core.withRestoreOrSaveFull] +def withRestoreOrSaveFull (old? : Option (α × SavedState)) + (cont : TermElabM SavedState → TermElabM α) : TermElabM α := do + if let some (_, oldState) := old? then + set oldState.elab + let old? := old?.map (fun (oldVal, oldState) => (oldVal, oldState.meta)) + controlAt MetaM fun runInBase => + Meta.withRestoreOrSaveFull old? fun restore => + runInBase <| cont (return { meta := (← restore), «elab» := (← get) }) instance : MonadBacktrack SavedState TermElabM where saveState := Term.saveState restoreState b := b.restore +/-- +Manages reuse information for nested tactics by `split`ting given syntax into an outer and inner +part. `act` is then run on the inner part but with reuse information adjusted as following: +* If the old (from `tacSnap?`'s `SyntaxGuarded.stx`) and new (from `stx`) outer syntax are not + identical according to `Syntax.structRangeEq`, reuse is disabled. +* Otherwise, the old syntax as stored in `tacSnap?` is updated to the old *inner* syntax. +* In any case, we also use `withRef` on the inner syntax to avoid leakage of the outer syntax into + `act` via this route. + +For any tactic that participates in reuse, `withNarrowedTacticReuse` should be applied to the +tactic's syntax and `act` should be used to do recursive tactic evaluation of nested parts. +-/ +def withNarrowedTacticReuse [Monad m] [MonadExceptOf Exception m] [MonadWithReaderOf Context m] + [MonadOptions m] [MonadRef m] (split : Syntax → Syntax × Syntax) (act : Syntax → m α) + (stx : Syntax) : m α := do + let (outer, inner) := split stx + let opts ← getOptions + withTheReader Term.Context (fun ctx => { ctx with tacSnap? := ctx.tacSnap?.map fun tacSnap => + { tacSnap with old? := tacSnap.old?.bind fun old => do + let (oldOuter, oldInner) := split old.stx + guard <| outer.structRangeEqWithTraceReuse opts oldOuter + return { old with stx := oldInner } + } + }) do + withRef inner do + act inner + +/-- +A variant of `withNarrowedTacticReuse` that uses `stx[argIdx]` as the inner syntax and all `stx` +child nodes before that as the outer syntax, i.e. reuse is disabled if there was any change before +`argIdx`. + +NOTE: child nodes after `argIdx` are not tested (which would almost always disable reuse as they are +necessarily shifted by changes at `argIdx`) so it must be ensured that the result of `arg` does not +depend on them (i.e. they should not be inspected beforehand). +-/ +def withNarrowedArgTacticReuse [Monad m] [MonadExceptOf Exception m] [MonadWithReaderOf Context m] + [MonadOptions m] [MonadRef m] (argIdx : Nat) (act : Syntax → m α) (stx : Syntax) : m α := + withNarrowedTacticReuse (fun stx => (mkNullNode stx.getArgs[:argIdx], stx[argIdx])) act stx + +/-- +Disables incremental tactic reuse *and* reporting for `act` if `cond` is true by setting `tacSnap?` +to `none`. This should be done for tactic blocks that are run multiple times as otherwise the +reported progress will jump back and forth (and partial reuse for these kinds of tact blocks is +similarly questionable). +-/ +def withoutTacticIncrementality [Monad m] [MonadWithReaderOf Context m] [MonadOptions m] [MonadRef m] + (cond : Bool) (act : m α) : m α := do + let opts ← getOptions + withTheReader Term.Context (fun ctx => { ctx with tacSnap? := ctx.tacSnap?.filter fun tacSnap => Id.run do + if let some old := tacSnap.old? then + if cond && opts.getBool `trace.Elab.reuse then + dbg_trace "reuse stopped: guard failed at {old.stx}" + return !cond + }) act + +/-- Disables incremental tactic reuse for `act` if `cond` is true. -/ +def withoutTacticReuse [Monad m] [MonadWithReaderOf Context m] [MonadOptions m] [MonadRef m] + (cond : Bool) (act : m α) : m α := do + let opts ← getOptions + withTheReader Term.Context (fun ctx => { ctx with tacSnap? := ctx.tacSnap?.map fun tacSnap => + { tacSnap with old? := tacSnap.old?.filter fun old => Id.run do + if cond && opts.getBool `trace.Elab.reuse then + dbg_trace "reuse stopped: guard failed at {old.stx}" + return !cond } + }) act + abbrev TermElabResult (α : Type) := EStateM.Result Exception SavedState α /-- diff --git a/src/Lean/Elab/Util.lean b/src/Lean/Elab/Util.lean index f453d1d14c24..8cd4b1995cca 100644 --- a/src/Lean/Elab/Util.lean +++ b/src/Lean/Elab/Util.lean @@ -205,7 +205,7 @@ def logException [Monad m] [MonadLog m] [AddMessageContext m] [MonadOptions m] [ match ex with | Exception.error ref msg => logErrorAt ref msg | Exception.internal id _ => - unless isAbortExceptionId id do + unless isAbortExceptionId id || id == Core.interruptExceptionId do let name ← id.getName logError m!"internal exception: {name}" diff --git a/src/Lean/Language/Basic.lean b/src/Lean/Language/Basic.lean index 9ef860cb2deb..55eff5ca91c5 100644 --- a/src/Lean/Language/Basic.lean +++ b/src/Lean/Language/Basic.lean @@ -17,8 +17,13 @@ set_option linter.missingDocs true namespace Lean.Language -/-- `MessageLog` with interactive diagnostics. -/ +/-- +`MessageLog` with interactive diagnostics. + +Can be created using `Diagnostics.empty` or `Diagnostics.ofMessageLog`. +-/ structure Snapshot.Diagnostics where + private mk :: /-- Non-interactive message log. -/ msgLog : MessageLog /-- @@ -133,8 +138,7 @@ checking if we can reuse `old?` if set or else redoing the corresponding elabora case, we derive new bundles for nested snapshots, if any, and finally `resolve` `new` to the result. Note that failing to `resolve` a created promise will block the language server indefinitely! -Corresponding `IO.Promise.new` calls should come with a "definitely resolved in ..." comment -explaining how this is avoided in each case. +We use `withAlwaysResolvedPromise`/`withAlwaysResolvedPromises` to ensure this doesn't happen. In the future, the 1-element history `old?` may be replaced with a global cache indexed by strong hashes but the promise will still need to be passed through the elaborator. @@ -151,6 +155,36 @@ structure SnapshotBundle (α : Type) where -/ new : IO.Promise α +/-- +Runs `act` with a newly created promise and finally resolves it to `default` if not done by `act`. + +Always resolving promises involved in the snapshot tree is important to avoid deadlocking the +language server. +-/ +def withAlwaysResolvedPromise [Monad m] [MonadLiftT BaseIO m] [MonadFinally m] [Inhabited α] + (act : IO.Promise α → m Unit) : m Unit := do + let p ← IO.Promise.new + try + act p + finally + p.resolve default + +/-- +Runs `act` with `count` newly created promises and finally resolves them to `default` if not done by +`act`. + +Always resolving promises involved in the snapshot tree is important to avoid deadlocking the +language server. +-/ +def withAlwaysResolvedPromises [Monad m] [MonadLiftT BaseIO m] [MonadFinally m] [Inhabited α] + (count : Nat) (act : Array (IO.Promise α) → m Unit) : m Unit := do + let ps ← List.iota count |>.toArray.mapM fun _ => IO.Promise.new + try + act ps + finally + for p in ps do + p.resolve default + /-- Tree of snapshots where each snapshot comes with an array of asynchronous further subtrees. Used for asynchronously collecting information about the entirety of snapshots in the language server. diff --git a/src/Lean/Language/Lean.lean b/src/Lean/Language/Lean.lean index 3f0f9fcb4178..01c01d59bdb2 100644 --- a/src/Lean/Language/Lean.lean +++ b/src/Lean/Language/Lean.lean @@ -63,36 +63,61 @@ we remain at "go two commands up" at this point. Because of Lean's use of persistent data structures, incremental reuse of fully elaborated commands is easy because we can simply snapshot the entire state after each command and then restart -elaboration using the stored state at the point of change. However, incrementality within -elaboration of a single command such as between tactic steps is much harder because we cannot simply -return from those points to the language processor in a way that we can later resume from there. -Instead, we exchange the need for continuations with some limited mutability: by allocating an -`IO.Promise` "cell" in the language processor, we can both pass it to the elaborator to eventually -fill it using `Promise.resolve` as well as convert it to a `Task` that will wait on that resolution -using `Promise.result` and return it as part of the command snapshot created by the language -processor. The elaborator can then create new promises itself and store their `result` when -resolving an outer promise to create an arbitrary tree of promise-backed snapshot tasks. Thus, we -can enable incremental reporting and reuse inside the elaborator using the same snapshot tree data -structures as outside without having to change the elaborator's control flow. +elaboration using the stored state at the next command above the point of change. However, +incrementality *within* elaboration of a single command such as between tactic steps is much harder +because the existing control flow does not allow us to simply return from those points to the +language processor in a way that we can later resume from there. Instead, we exchange the need for +continuations with some limited mutability: by allocating an `IO.Promise` "cell" in the language +processor, we can both pass it to the elaborator to eventually fill it using `Promise.resolve` as +well as convert it to a `Task` that will wait on that resolution using `Promise.result` and return +it as part of the command snapshot created by the language processor. The elaborator can then in +turn create new promises itself and store their `result` when resolving an outer promise to create +an arbitrary tree of promise-backed snapshot tasks. Thus, we can enable incremental reporting and +reuse inside the elaborator using the same snapshot tree data structures as outside without having +to change the elaborator's control flow. While ideally we would decide what can be reused during command elaboration using strong hashes over -the state and inputs, currently we rely on simpler syntactic checks: if all the syntax inspected up -to a certain point is unchanged, we can assume that the old state can be reused. The central -`SnapshotBundle` type passed inwards through the elaborator for this purpose combines the following -data: +the full state and inputs, currently we rely on simpler syntactic checks: if all the syntax +inspected up to a certain point is unchanged, we can assume that the old state can be reused. The +central `SnapshotBundle` type passed inwards through the elaborator for this purpose combines the +following data: * the `IO.Promise` to be resolved to an elaborator snapshot (whose type depends on the specific - elaborator part we're in, e.g. `) + elaborator part we're in, e.g. `TacticParsedSnapshot`, `BodyProcessedSnapshot`) * if there was a previous run: * a `SnapshotTask` holding the corresponding snapshot of the run * the relevant `Syntax` of the previous run to be compared before any reuse Note that as we do not wait for the previous run to finish before starting to elaborate the next -one, the `SnapshotTask` task may not be finished yet. Indeed, if we do find that we can reuse the -contained state, we will want to explicitly wait for it instead of redoing the work. On the other -hand, the `Syntax` is not surrounded by a task so that we can immediately access it for comparisons, -even if the snapshot task may, eventually, give access to the same syntax tree. - -TODO: tactic examples +one, the old `SnapshotTask` task may not be finished yet. Indeed, if we do find that we can reuse +the contained state because of a successful syntax comparison, we always want to explicitly wait for +the task instead of redoing the work. On the other hand, the `Syntax` is not surrounded by a task so +that we can immediately access it for comparisons, even if the snapshot task may, eventually, give +access to the same syntax tree. + +For the most part, inside an elaborator participating in incrementality, we just have to ensure that +we stop forwarding the old run's data as soon as we notice a relevant difference between old and new +syntax tree. For example, allowing incrementality inside the cdot tactic combinator is as simple as +``` +builtin_initialize registerBuiltinIncrementalTactic ``cdot +@[builtin_tactic cdot] def evalTacticCDot : Tactic := fun stx => do + ... + closeUsingOrAdmit do + -- save state before/after entering focus on `·` + ... + Term.withNarrowedArgTacticReuse (argIdx := 1) evalTactic stx +``` +The `Term.withNarrowedArgTacticReuse` combinator will focus on the given argument of `stx`, which in +this case is the nested tactic sequence, and run `evalTactic` on it. But crucially, it will first +compare all preceding arguments, in this case the cdot token itself, with the old syntax in the +current snapshot bundle, which in the case of tactics is stored in `Term.Context.tacSnap?`. Indeed +it is important here to check if the cdot token is identical because its position has been saved in +the info tree, so it would be bad if we later restored some old state that uses a different position +for it even if everything else is unchanged. If there is any mismatch, the bundle's old value is +set to `none` in order to prevent reuse from this point on. Note that in any case we still want to +forward the "new" promise in order to provide incremental reporting as well as to construct a +snapshot tree for reuse in future document versions! Note also that we explicitly opted into +incrementality using `registerBuiltinIncrementalTactic` as any tactic combinator not written with +these concerns in mind would likely misbehave under incremental reuse. While it is generally true that we can provide incremental reporting even without reuse, we generally want to avoid that when it would be confusing/annoying, e.g. when a tactic block is run @@ -101,12 +126,24 @@ purpose, we can disable both incremental modes using `Term.withoutTacticIncremen opted into incrementality because of other parts of the combinator. `induction` is an example of this because there are some induction alternatives that are run multiple times, so we disable all of incrementality for them. + +Using `induction` as a more complex example than `cdot` as it calls into `evalTactic` multiple +times, here is a summary of what it has to do to implement incrementality: +* `Narrow` down to the syntax of alternatives, disabling reuse if anything before them changed +* allocate one new promise for each given alternative, immediately resolve passed promise to a new + snapshot tree node holding them so that the language server can wait on them +* when executing an alternative, + * we put the corresponding promise into the context + * we disable reuse if anything in front of the contained tactic block has changed, including + previous alternatives + * we disable reuse *and reporting* if the tactic block is run multiple times, e.g. in the case of + a wildcard pattern -/ set_option linter.missingDocs true namespace Lean.Language.Lean -open Lean.Elab +open Lean.Elab Command open Lean.Parser private def pushOpt (a? : Option α) (as : Array α) : Array α := @@ -165,7 +202,7 @@ deriving Nonempty abbrev CommandParsedSnapshot.data : CommandParsedSnapshot → CommandParsedSnapshotData | mk data _ => data /-- Next command, unless this is a terminal command. -/ -abbrev CommandParsedSnapshot.next? : CommandParsedSnapshot → +abbrev CommandParsedSnapshot.nextCmdSnap? : CommandParsedSnapshot → Option (SnapshotTask CommandParsedSnapshot) | mk _ next? => next? partial instance : ToSnapshotTree CommandParsedSnapshot where @@ -173,18 +210,7 @@ partial instance : ToSnapshotTree CommandParsedSnapshot where go s := ⟨s.data.toSnapshot, #[s.data.elabSnap.map (sync := true) toSnapshotTree, s.data.finishedSnap.map (sync := true) toSnapshotTree] |> - pushOpt (s.next?.map (·.map (sync := true) go))⟩ - - -/-- Cancels all significant computations from this snapshot onwards. -/ -partial def CommandParsedSnapshot.cancel (snap : CommandParsedSnapshot) : BaseIO Unit := do - -- This is the only relevant computation right now, everything else is promises - -- TODO: cancel additional elaboration tasks (which will be tricky with `DynamicSnapshot`) if we - -- add them without switching to implicit cancellation - snap.data.finishedSnap.cancel - if let some next := snap.next? then - -- recurse on next command (which may have been spawned just before we cancelled above) - let _ ← IO.mapTask (sync := true) (·.cancel) next.task + pushOpt (s.nextCmdSnap?.map (·.map (sync := true) go))⟩ /-- State after successful importing. -/ structure HeaderProcessedState where @@ -218,6 +244,8 @@ structure HeaderParsedSnapshot extends Snapshot where /-- State after successful parsing. -/ result? : Option HeaderParsedState isFatal := result?.isNone + /-- Cancellation token for interrupting processing of this run. -/ + cancelTk? : Option IO.CancelToken instance : ToSnapshotTree HeaderParsedSnapshot where toSnapshotTree s := ⟨s.toSnapshot, @@ -235,6 +263,10 @@ abbrev InitialSnapshot := HeaderParsedSnapshot structure LeanProcessingContext extends ProcessingContext where /-- Position of the first file difference if there was a previous invocation. -/ firstDiffPos? : Option String.Pos + /-- Cancellation token of the previous invocation, if any. -/ + oldCancelTk? : Option IO.CancelToken + /-- Cancellation token of the current run. -/ + newCancelTk : IO.CancelToken /-- Monad transformer holding all relevant data for Lean processing. -/ abbrev LeanProcessingT m := ReaderT LeanProcessingContext m @@ -247,6 +279,18 @@ instance : MonadLift LeanProcessingM (LeanProcessingT IO) where instance : MonadLift (ProcessingT m) (LeanProcessingT m) where monadLift := fun act ctx => act ctx.toProcessingContext +/-- +Embeds a `LeanProcessingM` action into `ProcessingM`, optionally using the old input string to speed +up reuse analysis and supplying a cancellation token that should be triggered as soon as reuse is +ruled out. +-/ +def LeanProcessingM.run (act : LeanProcessingM α) (oldInputCtx? : Option InputContext) + (oldCancelTk? : Option IO.CancelToken := none) : ProcessingM α := do + -- compute position of syntactic change once + let firstDiffPos? := oldInputCtx?.map (·.input.firstDiffPos (← read).input) + let newCancelTk ← IO.CancelToken.new + ReaderT.adapt ({ · with firstDiffPos?, oldCancelTk?, newCancelTk }) act + /-- Returns true if there was a previous run and the given position is before any textual change compared to it. @@ -270,8 +314,7 @@ General notes: state. As there is no cheap way to check whether the `Environment` is unchanged, i.e. *semantic* change detection is currently not possible, we must make sure to pass `none` as all follow-up "previous states" from the first *syntactic* change onwards. -* We must make sure to use `CommandParsedSnapshot.cancel` on such tasks when discarding them, i.e. - when not passing them along in `old?`. +* 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. @@ -280,10 +323,7 @@ partial def process (setupImports : Syntax → ProcessingT IO (Except HeaderProcessedSnapshot Options) := fun _ => pure <| .ok {}) (old? : Option InitialSnapshot) : ProcessingM InitialSnapshot := do - -- compute position of syntactic change once - let firstDiffPos? := old?.map (·.ictx.input.firstDiffPos (← read).input) - ReaderT.adapt ({ · with firstDiffPos? }) do - parseHeader old? + parseHeader old? |>.run (old?.map (·.ictx)) (old?.bind (·.cancelTk?)) where parseHeader (old? : Option HeaderParsedSnapshot) : LeanProcessingM HeaderParsedSnapshot := do let ctx ← read @@ -292,16 +332,21 @@ where -- when header syntax is unchanged, reuse import processing task as is and continue with -- parsing the first command, synchronously if possible if let some oldSuccess := old.result? then - return { old with ictx, result? := some { oldSuccess with - processedSnap := (← oldSuccess.processedSnap.bindIO (sync := true) fun oldProcessed => do - if let some oldProcSuccess := oldProcessed.result? then - -- also wait on old command parse snapshot as parsing is cheap and may allow for - -- elaboration reuse - oldProcSuccess.firstCmdSnap.bindIO (sync := true) fun oldCmd => - return .pure { oldProcessed with result? := some { oldProcSuccess with - firstCmdSnap := (← parseCmd oldCmd oldSuccess.parserState oldProcSuccess.cmdState ctx) } } - else - return .pure oldProcessed) } } + return { + ictx + stx := old.stx + diagnostics := old.diagnostics + cancelTk? := ctx.newCancelTk + result? := some { oldSuccess with + processedSnap := (← oldSuccess.processedSnap.bindIO (sync := true) fun oldProcessed => do + if let some oldProcSuccess := oldProcessed.result? then + -- also wait on old command parse snapshot as parsing is cheap and may allow for + -- elaboration reuse + oldProcSuccess.firstCmdSnap.bindIO (sync := true) fun oldCmd => + return .pure { oldProcessed with result? := some { oldProcSuccess with + firstCmdSnap := (← parseCmd oldCmd oldSuccess.parserState oldProcSuccess.cmdState ctx) } } + else + return .pure oldProcessed) } } else return old -- fast path: if we have parsed the header successfully... @@ -313,7 +358,8 @@ where -- ...go immediately to next snapshot return (← unchanged old) - withHeaderExceptions ({ · with ictx, stx := .missing, result? := none }) do + withHeaderExceptions ({ · with + ictx, stx := .missing, result? := none, cancelTk? := none }) do -- parsing the header should be cheap enough to do synchronously let (stx, parserState, msgLog) ← Parser.parseHeader ictx if msgLog.hasErrors then @@ -321,6 +367,7 @@ where ictx, stx diagnostics := (← Snapshot.Diagnostics.ofMessageLog msgLog) result? := none + cancelTk? := none } -- semi-fast path: go to next snapshot if syntax tree is unchanged AND we're still in front @@ -332,13 +379,9 @@ where if let some old := old? then if (← isBeforeEditPos parserState.pos) && old.stx == stx then return (← unchanged old) - -- on first change, make sure to cancel all further old tasks - if let some oldSuccess := old.result? then - oldSuccess.processedSnap.cancel - let _ ← BaseIO.mapTask (t := oldSuccess.processedSnap.task) fun processed => do - if let some oldProcSuccess := processed.result? then - let _ ← BaseIO.mapTask (·.cancel) oldProcSuccess.firstCmdSnap.task - + -- on first change, make sure to cancel old invocation + if let some tk := ctx.oldCancelTk? then + tk.set return { ictx, stx diagnostics := (← Snapshot.Diagnostics.ofMessageLog msgLog) @@ -346,6 +389,7 @@ where parserState processedSnap := (← processHeader stx parserState) } + cancelTk? := ctx.newCancelTk } processHeader (stx : Syntax) (parserState : Parser.ModuleParserState) : @@ -399,7 +443,7 @@ where -- check for cancellation, most likely during elaboration of previous command, before starting -- processing of next command - if (← IO.checkCanceled) then + 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` @@ -412,18 +456,18 @@ where let unchanged old : BaseIO CommandParsedSnapshot := -- when syntax is unchanged, reuse command processing task as is - if let some oldNext := old.next? then + if let some oldNext := old.nextCmdSnap? then return .mk (data := old.data) (nextCmdSnap? := (← 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 old.data.parserState oldFinished.cmdState ctx)) + -- 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 old.data.parserState oldFinished.cmdState ctx)) else return old -- terminal command, we're done! -- fast path, do not even start new task for this snapshot if let some old := old? then - if let some nextCom ← old.next?.bindM (·.get?) then + if let some nextCom ← old.nextCmdSnap?.bindM (·.get?) then if (← isBeforeEditPos nextCom.data.parserState.pos) then return .pure (← unchanged old) @@ -441,8 +485,11 @@ where if let some old := old? then if (← isBeforeEditPos parserState.pos ctx) && old.data.stx == stx then return (← unchanged old) - -- on first change, make sure to cancel all further old tasks - old.cancel + -- on first change, make sure to cancel old invocation + -- TODO: pass token into incrementality-aware elaborators to improve reuse of still-valid, + -- still-running elaboration steps? + if let some tk := ctx.oldCancelTk? then + tk.set -- definitely resolved in `doElab` task let elabPromise ← IO.Promise.new @@ -461,7 +508,7 @@ where diagnostics := (← Snapshot.Diagnostics.ofMessageLog msgLog) stx parserState - elabSnap := { range? := finishedSnap.range?, task := elabPromise.result } + elabSnap := { range? := stx.getRange?, task := elabPromise.result } finishedSnap tacticCache } @@ -470,10 +517,15 @@ where (snap : SnapshotBundle DynamicSnapshot) (tacticCache : IO.Ref Tactic.Cache) : LeanProcessingM (SnapshotTask CommandFinishedSnapshot) := do let ctx ← read - - -- signature elaboration task; for now, does full elaboration - -- TODO: do tactic snapshots, reuse old state for them - SnapshotTask.ofIO (stx.getRange?.getD ⟨beginPos, beginPos⟩) do + -- (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 tailPos := stx.getTailPos? |>.getD beginPos + SnapshotTask.ofIO (some ⟨tailPos, tailPos⟩) do let scope := cmdState.scopes.head! let cmdStateRef ← IO.mkRef { cmdState with messages := .empty } /- @@ -487,11 +539,12 @@ where cmdPos := beginPos tacticCache? := some tacticCacheNew snap? := some snap + cancelTk? := some ctx.newCancelTk } let (output, _) ← IO.FS.withIsolatedStreams (isolateStderr := stderrAsMessages.get scope.opts) do liftM (m := BaseIO) do - Elab.Command.catchExceptions + withLoggingExceptions (getResetInfoTrees *> Elab.Command.elabCommandTopLevel stx) cmdCtx cmdStateRef let postNew := (← tacticCacheNew.get).post @@ -504,9 +557,10 @@ where stx.hasMissing then -- discard elaboration errors, except for a few important and unlikely misleading ones, on -- parse error - messages := ⟨messages.msgs.filter fun msg => - msg.data.hasTag (fun tag => tag == `Elab.synthPlaceholder || - tag == `Tactic.unsolvedGoals || (`_traceMsg).isSuffixOf tag)⟩ + messages := { messages with + msgs := messages.msgs.filter fun msg => + msg.data.hasTag (fun tag => tag == `Elab.synthPlaceholder || + tag == `Tactic.unsolvedGoals || (`_traceMsg).isSuffixOf tag) } if !output.isEmpty then messages := messages.add { fileName := ctx.fileName @@ -523,13 +577,28 @@ where cmdState } +/-- +Convenience function for tool uses of the language processor that skips header handling. +-/ +def processCommands (inputCtx : Parser.InputContext) (parserState : Parser.ModuleParserState) + (commandState : Command.State) + (old? : Option (Parser.InputContext × CommandParsedSnapshot) := none) : + BaseIO (SnapshotTask CommandParsedSnapshot) := do + process.parseCmd (old?.map (·.2)) parserState commandState + |>.run (old?.map (·.1)) + |>.run { inputCtx with + mainModuleName := commandState.env.mainModule + opts := commandState.scopes.head!.opts + } + + /-- Waits for and returns final environment, if importing was successful. -/ partial def waitForFinalEnv? (snap : InitialSnapshot) : Option Environment := do let snap ← snap.result? let snap ← snap.processedSnap.get.result? goCmd snap.firstCmdSnap.get where goCmd snap := - if let some next := snap.next? then + if let some next := snap.nextCmdSnap? then goCmd next.get else snap.data.finishedSnap.get.cmdState.env diff --git a/src/Lean/Message.lean b/src/Lean/Message.lean index 099ca0fcfb27..37b67df8b90c 100644 --- a/src/Lean/Message.lean +++ b/src/Lean/Message.lean @@ -283,28 +283,35 @@ end Message /-- A persistent array of messages. -/ structure MessageLog where + /-- + If true, there was an error in the log previously that has already been reported to the user and + removed from the log. Used in `MessageLog.hasErrors` accordingly. + + We use this to incrementally report diagnostics during elaboration of a single command but still + keep track of whether there was an error at all in this command. We use a fresh message log for + the next command. + -/ + hadErrors : Bool := false msgs : PersistentArray Message := {} deriving Inhabited namespace MessageLog -def empty : MessageLog := ⟨{}⟩ +def empty : MessageLog := { msgs := {} } def isEmpty (log : MessageLog) : Bool := log.msgs.isEmpty def add (msg : Message) (log : MessageLog) : MessageLog := - ⟨log.msgs.push msg⟩ + { log with msgs := log.msgs.push msg } protected def append (l₁ l₂ : MessageLog) : MessageLog := - ⟨l₁.msgs ++ l₂.msgs⟩ + { hadErrors := l₁.hadErrors || l₂.hadErrors, msgs := l₁.msgs ++ l₂.msgs } instance : Append MessageLog := ⟨MessageLog.append⟩ def hasErrors (log : MessageLog) : Bool := - log.msgs.any fun m => match m.severity with - | MessageSeverity.error => true - | _ => false + log.hadErrors || log.msgs.any (·.severity matches .error) def errorsToWarnings (log : MessageLog) : MessageLog := { msgs := log.msgs.map (fun m => match m.severity with | MessageSeverity.error => { m with severity := MessageSeverity.warning } | _ => m) } diff --git a/src/Lean/Meta/Basic.lean b/src/Lean/Meta/Basic.lean index b4f00c86251e..5eaddff015d2 100644 --- a/src/Lean/Meta/Basic.lean +++ b/src/Lean/Meta/Basic.lean @@ -292,7 +292,7 @@ structure State where Backtrackable state for the `MetaM` monad. -/ structure SavedState where - core : Core.State + core : Core.SavedState meta : State deriving Nonempty @@ -393,20 +393,22 @@ instance : AddMessageContext MetaM where addMessageContext := addMessageContextFull protected def saveState : MetaM SavedState := - return { core := (← getThe Core.State), meta := (← get) } + return { core := (← Core.saveState), meta := (← get) } /-- Restore backtrackable parts of the state. -/ def SavedState.restore (b : SavedState) : MetaM Unit := do - Core.restore b.core + b.core.restore modify fun s => { s with mctx := b.meta.mctx, zetaDeltaFVarIds := b.meta.zetaDeltaFVarIds, postponed := b.meta.postponed } -/-- -Restores full state including sources for unique identifiers. Only intended for incremental reuse -between elaboration runs, not for backtracking within a single run. --/ -def SavedState.restoreFull (b : SavedState) : MetaM Unit := do - Core.restoreFull b.core - set b.meta +@[specialize, inherit_doc Core.withRestoreOrSaveFull] +def withRestoreOrSaveFull (old? : Option (α × SavedState)) + (cont : MetaM SavedState → MetaM α) : MetaM α := do + if let some (_, oldState) := old? then + set oldState.meta + let old? := old?.map (fun (oldVal, oldState) => (oldVal, oldState.core)) + controlAt CoreM fun runInCoreM => + Core.withRestoreOrSaveFull old? fun restore => + runInCoreM <| cont (return { core := (← restore), meta := (← get) }) instance : MonadBacktrack SavedState MetaM where saveState := Meta.saveState diff --git a/src/Lean/Server/FileWorker/Utils.lean b/src/Lean/Server/FileWorker/Utils.lean index b14f08819a98..241df0501f0e 100644 --- a/src/Lean/Server/FileWorker/Utils.lean +++ b/src/Lean/Server/FileWorker/Utils.lean @@ -15,22 +15,6 @@ namespace Lean.Server.FileWorker open Snapshots open IO -structure CancelToken where - ref : IO.Ref Bool - -namespace CancelToken - -def new : IO CancelToken := - CancelToken.mk <$> IO.mkRef false - -def set (tk : CancelToken) : BaseIO Unit := - tk.ref.set true - -def isSet (tk : CancelToken) : BaseIO Bool := - tk.ref.get - -end CancelToken - -- TEMP: translate from new heterogeneous snapshot tree to old homogeneous async list private partial def mkCmdSnaps (initSnap : Language.Lean.InitialSnapshot) : AsyncList IO.Error Snapshot := Id.run do @@ -49,7 +33,7 @@ where stx := cmdParsed.data.stx mpState := cmdParsed.data.parserState cmdState := finished.cmdState - } (match cmdParsed.next? with + } (match cmdParsed.nextCmdSnap? with | some next => .delayed <| next.task.bind go | none => .nil) diff --git a/src/Lean/Server/Snapshots.lean b/src/Lean/Server/Snapshots.lean index 5d3070f75230..b02337465079 100644 --- a/src/Lean/Server/Snapshots.lean +++ b/src/Lean/Server/Snapshots.lean @@ -59,6 +59,7 @@ def runCommandElabM (snap : Snapshot) (meta : DocumentMeta) (c : CommandElabM α fileMap := meta.text, tacticCache? := none snap? := none + cancelTk? := none } c.run ctx |>.run' snap.cmdState diff --git a/src/Lean/Syntax.lean b/src/Lean/Syntax.lean index 2c4d283f7711..06c09e16d2d8 100644 --- a/src/Lean/Syntax.lean +++ b/src/Lean/Syntax.lean @@ -30,6 +30,9 @@ def SourceInfo.updateTrailing (trailing : Substring) : SourceInfo → SourceInfo | SourceInfo.original leading pos _ endPos => SourceInfo.original leading pos trailing endPos | info => info +def SourceInfo.getRange? (canonicalOnly := false) (info : SourceInfo) : Option String.Range := + return ⟨(← info.getPos? canonicalOnly), (← info.getTailPos? canonicalOnly)⟩ + /-! # Syntax AST -/ inductive IsNode : Syntax → Prop where @@ -80,6 +83,34 @@ end SyntaxNode namespace Syntax +/-- +Compare syntax structures and position ranges, but not whitespace. +We generally assume that if syntax trees equal in this way generate the same elaboration output, +including positions contained in e.g. diagnostics and the info tree. +-/ +partial def structRangeEq : Syntax → Syntax → Bool + | .missing, .missing => true + | .node info k args, .node info' k' args' => + info.getRange? == info'.getRange? && k == k' && args.isEqv args' structRangeEq + | .atom info val, .atom info' val' => info.getRange? == info'.getRange? && val == val' + | .ident info rawVal val preresolved, .ident info' rawVal' val' preresolved' => + info.getRange? == info'.getRange? && rawVal == rawVal' && val == val' && + preresolved == preresolved' + | _, _ => false + +/-- Like `structRangeEq` but prints trace on failure if `trace.Elab.reuse` is activated. -/ +def structRangeEqWithTraceReuse (opts : Options) (stx1 stx2 : Syntax) : Bool := + if stx1.structRangeEq stx2 then + true + else + if opts.getBool `trace.Elab.reuse then + dbg_trace "reuse stopped: +{stx1.formatStx (showInfo := true)} != +{stx2.formatStx (showInfo := true)}" + false + else + false + def getAtomVal : Syntax → String | atom _ val => val | _ => "" @@ -187,13 +218,6 @@ partial def updateTrailing (trailing : Substring) : Syntax → Syntax Syntax.node info k args | s => s -partial def getTailWithPos : Syntax → Option Syntax - | stx@(atom info _) => info.getPos?.map fun _ => stx - | stx@(ident info ..) => info.getPos?.map fun _ => stx - | node SourceInfo.none _ args => args.findSomeRev? getTailWithPos - | stx@(node ..) => stx - | _ => none - open SourceInfo in /-- Split an `ident` into its dot-separated components while preserving source info. Macro scopes are first erased. For example, `` `foo.bla.boo._@._hyg.4 `` ↦ `` [`foo, `bla, `boo] ``. diff --git a/src/runtime/alloc.cpp b/src/runtime/alloc.cpp index ea0a438311f4..6ac4804e5eaf 100644 --- a/src/runtime/alloc.cpp +++ b/src/runtime/alloc.cpp @@ -467,16 +467,20 @@ void finalize_alloc() { LEAN_THREAD_VALUE(uint64_t, g_heartbeat, 0); #endif -/* Helper function for increasing heartbeat even when LEAN_SMALL_ALLOCATOR is not defined */ -extern "C" LEAN_EXPORT void lean_inc_heartbeat() { +void add_heartbeats(uint64_t count) { #ifdef LEAN_SMALL_ALLOCATOR if (g_heap) - g_heap->m_heartbeat++; + g_heap->m_heartbeat += count; #else - g_heartbeat++; + g_heartbeat += count; #endif } +/* Helper function for increasing heartbeat even when LEAN_SMALL_ALLOCATOR is not defined */ +extern "C" LEAN_EXPORT void lean_inc_heartbeat() { + add_heartbeats(1); +} + uint64_t get_num_heartbeats() { #ifdef LEAN_SMALL_ALLOCATOR if (g_heap) diff --git a/src/runtime/alloc.h b/src/runtime/alloc.h index c626b5f7ad82..5aaa19cd71f3 100644 --- a/src/runtime/alloc.h +++ b/src/runtime/alloc.h @@ -12,6 +12,7 @@ namespace lean { void init_thread_heap(); void * alloc(size_t sz); void dealloc(void * o, size_t sz); +void add_heartbeats(uint64_t count); uint64_t get_num_heartbeats(); void initialize_alloc(); void finalize_alloc(); diff --git a/src/runtime/io.cpp b/src/runtime/io.cpp index 4dddd7afc0b5..41f271ef76db 100644 --- a/src/runtime/io.cpp +++ b/src/runtime/io.cpp @@ -639,6 +639,12 @@ extern "C" LEAN_EXPORT obj_res lean_io_get_num_heartbeats(obj_arg /* w */) { return io_result_mk_ok(lean_uint64_to_nat(get_num_heartbeats())); } +/* addHeartbeats (count : Int64) : BaseIO Unit */ +extern "C" LEAN_EXPORT obj_res lean_io_add_heartbeats(int64_t count, obj_arg /* w */) { + add_heartbeats(count); + return io_result_mk_ok(box(0)); +} + extern "C" LEAN_EXPORT obj_res lean_io_getenv(b_obj_arg env_var, obj_arg) { #if defined(LEAN_EMSCRIPTEN) // HACK(WN): getenv doesn't seem to work in Emscripten even though it should diff --git a/tests/lean/inductionErrors.lean b/tests/lean/inductionErrors.lean index 9bc246952fb7..4f0ea926e3de 100644 --- a/tests/lean/inductionErrors.lean +++ b/tests/lean/inductionErrors.lean @@ -77,5 +77,5 @@ theorem ex11 (p q : Nat) : p ≤ q ∨ p > q := by cases p, q using elimEx with | lower d => apply Or.inl; admit | upper d => apply Or.inr; admit - | lower d /- error unused -/ => apply Or.inl; admit + | lower d /- error duplicate -/ => apply Or.inl; admit | diag => apply Or.inl; apply Nat.le_refl diff --git a/tests/lean/inductionErrors.lean.expected.out b/tests/lean/inductionErrors.lean.expected.out index e6e762991e1e..348a96863394 100644 --- a/tests/lean/inductionErrors.lean.expected.out +++ b/tests/lean/inductionErrors.lean.expected.out @@ -27,5 +27,5 @@ inductionErrors.lean:50:2-50:16: error: alternative 'cons' is not needed inductionErrors.lean:55:2-55:16: error: alternative 'cons' is not needed inductionErrors.lean:60:2-60:40: error: invalid alternative name 'upper2' inductionErrors.lean:66:2-66:28: error: invalid occurrence of wildcard alternative, it must be the last alternative -inductionErrors.lean:74:2-74:34: error: unused alternative -inductionErrors.lean:80:2-80:53: error: unused alternative +inductionErrors.lean:70:2-74:34: error: unused alternative '_' +inductionErrors.lean:77:2-81:46: error: unused alternative 'lower' diff --git a/tests/lean/interactive/editAfterError.lean b/tests/lean/interactive/editAfterError.lean index aa28b67e868e..6ced0a92a426 100644 --- a/tests/lean/interactive/editAfterError.lean +++ b/tests/lean/interactive/editAfterError.lean @@ -1,4 +1,4 @@ #check tru #check fal - --^ insert: s + --^ insert: "s" --^ collectDiagnostics diff --git a/tests/lean/interactive/editAfterError.lean.expected.out b/tests/lean/interactive/editAfterError.lean.expected.out index 7206de2b27de..0fb95fcbeee4 100644 --- a/tests/lean/interactive/editAfterError.lean.expected.out +++ b/tests/lean/interactive/editAfterError.lean.expected.out @@ -1,9 +1,3 @@ -{"textDocument": {"version": 2, "uri": "file:///editAfterError.lean"}, - "contentChanges": - [{"text": "s", - "range": - {"start": {"line": 1, "character": 10}, - "end": {"line": 1, "character": 11}}}]} {"version": 2, "uri": "file:///editAfterError.lean", "diagnostics": diff --git a/tests/lean/interactive/editCompletion.lean b/tests/lean/interactive/editCompletion.lean index cafa9b2d1762..703498a60cdc 100644 --- a/tests/lean/interactive/editCompletion.lean +++ b/tests/lean/interactive/editCompletion.lean @@ -2,5 +2,5 @@ structure Foo where foo : Nat example (f : Foo) : f - --^ insert: . + --^ insert: "." --^ textDocument/completion diff --git a/tests/lean/interactive/editCompletion.lean.expected.out b/tests/lean/interactive/editCompletion.lean.expected.out index dc9667a3460e..104ba7b4bf6e 100644 --- a/tests/lean/interactive/editCompletion.lean.expected.out +++ b/tests/lean/interactive/editCompletion.lean.expected.out @@ -1,9 +1,3 @@ -{"textDocument": {"version": 2, "uri": "file:///editCompletion.lean"}, - "contentChanges": - [{"text": ".", - "range": - {"start": {"line": 3, "character": 21}, - "end": {"line": 3, "character": 22}}}]} {"textDocument": {"uri": "file:///editCompletion.lean"}, "position": {"line": 3, "character": 22}} {"items": diff --git a/tests/lean/interactive/incrementalCombinator.lean b/tests/lean/interactive/incrementalCombinator.lean new file mode 100644 index 000000000000..abb3a4e24ba8 --- /dev/null +++ b/tests/lean/interactive/incrementalCombinator.lean @@ -0,0 +1,22 @@ +/-! Incremental reuse in combinator -/ + +def case (h : a ∨ b ∨ c) : True := by + cases h + case inr h => + cases h + case inl => + dbg_trace "c 0" + dbg_trace "c 1" + dbg_trace "c 2" + --^ sync + --^ insert: ".5" + +-- RESET +def case (h : a ∨ b) : True := by + cases h + . dbg_trace "d 0" + dbg_trace "d 1" + dbg_trace "d 2" + --^ sync + --^ insert: ".5" + dbg_trace "d 3" diff --git a/tests/lean/interactive/incrementalCombinator.lean.expected.out b/tests/lean/interactive/incrementalCombinator.lean.expected.out new file mode 100644 index 000000000000..340cceda2212 --- /dev/null +++ b/tests/lean/interactive/incrementalCombinator.lean.expected.out @@ -0,0 +1,10 @@ +c 0 +c 1 +c 2 +c 2.5 +d 0 +d 1 +d 2 +d 3 +d 2.5 +d 3 diff --git a/tests/lean/interactive/incrementalInduction.lean b/tests/lean/interactive/incrementalInduction.lean new file mode 100644 index 000000000000..da7f4bb7150b --- /dev/null +++ b/tests/lean/interactive/incrementalInduction.lean @@ -0,0 +1,59 @@ +/-! Incremental reuse in `induction` -/ + +--set_option trace.Elab.reuse true + +theorem basic (n : Nat) : True := by + induction n with + | zero => sorry + | succ => + dbg_trace "b 0" + dbg_trace "b 1" + dbg_trace "b 2" + --^ sync + --^ insert: ".5" + +-- RESET +theorem nonFirst (n : Nat) : True := by + induction n with + | zero => + dbg_trace "n 0" + dbg_trace "n 1" + --^ sync + --^ insert: ".5" + | succ => + dbg_trace "n 2" + --^ sync + --^ insert: ".5" + +-- RESET +-- currently the pre-tac will be re-executed even if we can reuse a specific branch's tactics +theorem preTac (n : Nat) : True := by + induction n with + dbg_trace "p -1" + | zero => sorry + | succ => + dbg_trace "p 0" + dbg_trace "p 1" + --^ sync + --^ insert: ".5" + +/-! No reuse in cases where branch is run more than once -/ + +-- RESET +theorem wildcard (n : Nat) : True := by + induction n with + | _ => + dbg_trace "w 0" + dbg_trace "w 1" + --^ sync + --^ insert: ".5" + +-- RESET +theorem preTacMulti (x : Nat × Nat × Nat) : True := by + induction x with + cases x + | mk x => + dbg_trace "pm 0" + dbg_trace "pm 1" + --^ sync + --^ insert: ".5" diff --git a/tests/lean/interactive/incrementalInduction.lean.expected.out b/tests/lean/interactive/incrementalInduction.lean.expected.out new file mode 100644 index 000000000000..969774fd85a2 --- /dev/null +++ b/tests/lean/interactive/incrementalInduction.lean.expected.out @@ -0,0 +1,33 @@ +b 0 +b 1 +b 2 +b 2.5 +n 0 +n 1 +n 2 +n 1.5 +n 2 +n 2.5 +p -1 +p -1 +p 0 +p 1 +p -1 +p -1 +p 1.5 +w 0 +w 1 +w 0 +w 1 +w 0 +w 1.5 +w 0 +w 1.5 +pm 0 +pm 1 +pm 0 +pm 1 +pm 0 +pm 1.5 +pm 0 +pm 1.5 diff --git a/tests/lean/interactive/incrementalMutual.lean b/tests/lean/interactive/incrementalMutual.lean new file mode 100644 index 000000000000..bef3cb010a80 --- /dev/null +++ b/tests/lean/interactive/incrementalMutual.lean @@ -0,0 +1,31 @@ +/-! Incremental reuse in `mutual` -/ + +/-! should invalidate body of `b1` only -/ + +mutual +def b0 : (by dbg_trace "b 0 0"; exact Nat) := (by dbg_trace "b 0 1"; exact 0) + +def b1 : (by dbg_trace "b 1 0"; exact Nat) := (by dbg_trace "b 1 1"; exact 0) + --^ sync + --^ insert: ".5" +end + +/-! should invalidate both bodies (and, in current impl, second header) -/ + +-- RESET +mutual +def f0 : (by dbg_trace "f 0 0"; exact Nat) := (by dbg_trace "f 0 1"; exact 0) + --^ sync + --^ insert: ".5" +def f1 : (by dbg_trace "f 1 0"; exact Nat) := (by dbg_trace "f 1 1"; exact 0) +end + +/-! should invalidate everything but header of `h0` -/ + +-- RESET +mutual +def h0 : (by dbg_trace "h 0 0"; exact Nat) := (by dbg_trace "h 0 1"; exact 0) +def h1 : (by dbg_trace "h 1 0"; exact Nat) := (by dbg_trace "h 1 1"; exact 0) + --^ sync + --^ insert: ".5" +end diff --git a/tests/lean/interactive/incrementalMutual.lean.expected.out b/tests/lean/interactive/incrementalMutual.lean.expected.out new file mode 100644 index 000000000000..c6fb189472dd --- /dev/null +++ b/tests/lean/interactive/incrementalMutual.lean.expected.out @@ -0,0 +1,19 @@ +b 0 0 +b 1 0 +b 0 1 +b 1 1 +b 1 1.5 +f 0 0 +f 1 0 +f 0 1 +f 1 1 +f 1 0 +f 0 1.5 +f 1 1 +h 0 0 +h 1 0 +h 0 1 +h 1 1 +h 1 0.5 +h 0 1 +h 1 1 diff --git a/tests/lean/interactive/incrementalTactic.lean b/tests/lean/interactive/incrementalTactic.lean new file mode 100644 index 000000000000..b8dd0a3bb651 --- /dev/null +++ b/tests/lean/interactive/incrementalTactic.lean @@ -0,0 +1,33 @@ +/-! Basic incremental reuse in top-level tactic block -/ + +-- set_option trace.Elab.reuse true + +def basic : True := by + dbg_trace "b 0" + apply id + dbg_trace "b 1" + apply id + dbg_trace "b 2" + --^ sync + --^ insert: ".5" + +-- RESET +def trailingWhitespace : True := by + dbg_trace "t 0" + dbg_trace "t 1" + dbg_trace "t 2" + --^ sync + --^ insert: "\n " + + +-- RESET +-- this used to restore the wrong elab state because of input context mis-tracking +def haveBug : True := by + have (a : Nat) : Nat → True := by + intro n m + --^ sync + --^ delete: "intro n m" + --^ sync + --^ insert: "intro n m" + --^ collectDiagnostics + exact m diff --git a/tests/lean/interactive/incrementalTactic.lean.expected.out b/tests/lean/interactive/incrementalTactic.lean.expected.out new file mode 100644 index 000000000000..9de66231ce4d --- /dev/null +++ b/tests/lean/interactive/incrementalTactic.lean.expected.out @@ -0,0 +1,26 @@ +b 0 +b 1 +b 2 +b 2.5 +t 0 +t 1 +t 2 +{"version": 3, + "uri": "file:///incrementalTactic.lean", + "diagnostics": + [{"source": "Lean 4", + "severity": 1, + "range": + {"start": {"line": 4, "character": 4}, "end": {"line": 4, "character": 13}}, + "message": + "tactic 'introN' failed, insufficient number of binders\na n : Nat\n⊢ True", + "fullRange": + {"start": {"line": 4, "character": 4}, "end": {"line": 4, "character": 13}}}, + {"source": "Lean 4", + "severity": 1, + "range": + {"start": {"line": 2, "character": 22}, "end": {"line": 3, "character": 0}}, + "message": "unsolved goals\nthis : Nat → Nat → True\n⊢ True", + "fullRange": + {"start": {"line": 2, "character": 22}, + "end": {"line": 10, "character": 11}}}]} diff --git a/tests/lean/interactive/plainGoal.lean.expected.out b/tests/lean/interactive/plainGoal.lean.expected.out index 91a2fdd633bb..968e7331cf8e 100644 --- a/tests/lean/interactive/plainGoal.lean.expected.out +++ b/tests/lean/interactive/plainGoal.lean.expected.out @@ -173,7 +173,7 @@ null {"rendered": "```lean\nx✝ : Nat\n⊢ True\n```", "goals": ["x✝ : Nat\n⊢ True"]} {"textDocument": {"uri": "file:///plainGoal.lean"}, "position": {"line": 142, "character": 34}} -{"rendered": "```lean\nx✝ : Nat\n⊢ True\n```", "goals": ["x✝ : Nat\n⊢ True"]} +{"rendered": "```lean\n⊢ True\n```", "goals": ["⊢ True"]} {"textDocument": {"uri": "file:///plainGoal.lean"}, "position": {"line": 145, "character": 34}} {"rendered": "```lean\nx✝ : Nat\n⊢ True\n```", "goals": ["x✝ : Nat\n⊢ True"]} diff --git a/tests/lean/interactive/run.lean b/tests/lean/interactive/run.lean index b0536e939180..bca1b6b9197d 100644 --- a/tests/lean/interactive/run.lean +++ b/tests/lean/interactive/run.lean @@ -67,7 +67,8 @@ def ident : Parsec Name := do partial def main (args : List String) : IO Unit := do let uri := s!"file:///{args.head!}" - Ipc.runWith (←IO.appPath) #["--server"] do + -- We want `dbg_trace` tactics to write directly to stderr instead of being caught in reuse + Ipc.runWith (←IO.appPath) #["--server", "-DstderrAsMessages=false"] do let capabilities := { textDocument? := some { completion? := some { @@ -82,136 +83,154 @@ partial def main (args : List String) : IO Unit := do Ipc.writeNotification ⟨"initialized", InitializedParams.mk⟩ let text ← IO.FS.readFile args.head! - Ipc.writeNotification ⟨"textDocument/didOpen", { - textDocument := { uri := uri, languageId := "lean", version := 1, text := text } : DidOpenTextDocumentParams }⟩ - let _ ← Ipc.collectDiagnostics 1 uri 1 - let mut lineNo := 0 - let mut lastActualLineNo := 0 - let mut versionNo : Nat := 2 - let mut requestNo : Nat := 2 - let mut rpcSessionId : Option UInt64 := none - for line in text.splitOn "\n" do - match line.splitOn "--" with - | [ws, directive] => - let line ← match directive.front with - | 'v' => pure <| lineNo + 1 -- TODO: support subsequent 'v'... or not - | '^' => pure <| lastActualLineNo - | _ => - lastActualLineNo := lineNo - lineNo := lineNo + 1 - continue - let directive := directive.drop 1 - let colon := directive.posOf ':' - let method := directive.extract 0 colon |>.trim - -- TODO: correctly compute in presence of Unicode - let column := ws.endPos + "--" - let pos : Lsp.Position := { line := line, character := column.byteIdx } - let params := if colon < directive.endPos then directive.extract (colon + ':') directive.endPos |>.trim else "{}" - match method with - | "insert" => - let params : DidChangeTextDocumentParams := { - textDocument := { - uri := uri - version? := versionNo + let mut requestNo : Nat := 1 + for text in text.splitOn "-- RESET" do + Ipc.writeNotification ⟨"textDocument/didOpen", { + textDocument := { uri := uri, languageId := "lean", version := 1, text := text } : DidOpenTextDocumentParams }⟩ + let _ ← Ipc.collectDiagnostics requestNo uri 1 + requestNo := requestNo + 1 + let mut lineNo := 0 + let mut lastActualLineNo := 0 + let mut versionNo : Nat := 2 + let mut rpcSessionId : Option UInt64 := none + for line in text.splitOn "\n" do + match line.splitOn "--" with + | [ws, directive] => + let line ← match directive.front with + | 'v' => pure <| lineNo + 1 -- TODO: support subsequent 'v'... or not + | '^' => pure <| lastActualLineNo + | _ => + lastActualLineNo := lineNo + lineNo := lineNo + 1 + continue + let directive := directive.drop 1 + let colon := directive.posOf ':' + let method := directive.extract 0 colon |>.trim + -- TODO: correctly compute in presence of Unicode + let column := ws.endPos + "--" + let pos : Lsp.Position := { line := line, character := column.byteIdx } + let params := if colon < directive.endPos then directive.extract (colon + ':') directive.endPos |>.trim else "{}" + match method with + -- `delete: "foo"` deletes the given string's number of characters at the given position. + -- We do NOT check currently that the text at this position is indeed that string. + | "delete" + -- `insert: "foo"` inserts the given string at the given position. + | "insert" => + let some text := Syntax.decodeStrLit params + | throw <| IO.userError s!"failed to parse {params}" + let params : DidChangeTextDocumentParams := { + textDocument := { + uri := uri + version? := versionNo + } + contentChanges := #[TextDocumentContentChangeEvent.rangeChange { + start := pos + «end» := match method with + | "delete" => { pos with character := pos.character + text.length } + | _ => pos + } (match method with + | "delete" => "" + | _ => text)] } - contentChanges := #[TextDocumentContentChangeEvent.rangeChange { - start := pos - «end» := { pos with character := pos.character + params.endPos.byteIdx } - } params] - } - let params := toJson params - IO.eprintln params - Ipc.writeNotification ⟨"textDocument/didChange", params⟩ - -- We don't want to wait for changes to be processed so we can test concurrency - --let _ ← Ipc.collectDiagnostics requestNo uri versionNo - requestNo := requestNo + 1 - versionNo := versionNo + 1 - | "collectDiagnostics" => - if let some diags ← Ipc.collectDiagnostics requestNo uri (versionNo - 1) then - IO.eprintln (toJson diags.param) - requestNo := requestNo + 1 - | "codeAction" => - let params : CodeActionParams := { - textDocument := {uri := uri}, - range := ⟨pos, pos⟩ - } - Ipc.writeRequest ⟨requestNo, "textDocument/codeAction", params⟩ - let r ← Ipc.readResponseAs requestNo (Array Json) - for x in r.result do - IO.eprintln x - requestNo := requestNo + 1 - | "goals" => - if rpcSessionId.isNone then - Ipc.writeRequest ⟨requestNo, "$/lean/rpc/connect", RpcConnectParams.mk uri⟩ - let r ← Ipc.readResponseAs requestNo RpcConnected - rpcSessionId := some r.result.sessionId + let params := toJson params + Ipc.writeNotification ⟨"textDocument/didChange", params⟩ + -- We don't want to wait for changes to be processed so we can test concurrency + --let _ ← Ipc.collectDiagnostics requestNo uri versionNo requestNo := requestNo + 1 - let params : Lsp.PlainGoalParams := { - textDocument := { uri } - position := pos, - } - let ps : RpcCallParams := { - params := toJson params - textDocument := { uri } - position := pos, - sessionId := rpcSessionId.get!, - method := `Lean.Widget.getInteractiveGoals - } - Ipc.writeRequest ⟨requestNo, "$/lean/rpc/call", ps⟩ - let response ← Ipc.readResponseAs requestNo Client.InteractiveGoals - requestNo := requestNo + 1 - IO.eprintln (repr response.result) - IO.eprintln "" - | "widgets" => - if rpcSessionId.isNone then - Ipc.writeRequest ⟨requestNo, "$/lean/rpc/connect", RpcConnectParams.mk uri⟩ - let r ← Ipc.readResponseAs requestNo RpcConnected - rpcSessionId := some r.result.sessionId + versionNo := versionNo + 1 + | "collectDiagnostics" => + if let some diags ← Ipc.collectDiagnostics requestNo uri (versionNo - 1) then + IO.eprintln (toJson diags.param) requestNo := requestNo + 1 - let ps : RpcCallParams := { - textDocument := {uri := uri}, - position := pos, - sessionId := rpcSessionId.get!, - method := `Lean.Widget.getWidgets, - params := toJson pos, - } - Ipc.writeRequest ⟨requestNo, "$/lean/rpc/call", ps⟩ - let response ← Ipc.readResponseAs requestNo Lean.Widget.GetWidgetsResponse - requestNo := requestNo + 1 - IO.eprintln response.result.debugJson - for w in response.result.widgets do - let params : Lean.Widget.GetWidgetSourceParams := { pos, hash := w.javascriptHash } + | "sync" => -- wait for processing but do not print diagnostics + let _ ← Ipc.collectDiagnostics requestNo uri (versionNo - 1) + requestNo := requestNo + 1 + | "codeAction" => + let params : CodeActionParams := { + textDocument := {uri := uri}, + range := ⟨pos, pos⟩ + } + Ipc.writeRequest ⟨requestNo, "textDocument/codeAction", params⟩ + let r ← Ipc.readResponseAs requestNo (Array Json) + for x in r.result do + IO.eprintln x + requestNo := requestNo + 1 + | "goals" => + if rpcSessionId.isNone then + Ipc.writeRequest ⟨requestNo, "$/lean/rpc/connect", RpcConnectParams.mk uri⟩ + let r ← Ipc.readResponseAs requestNo RpcConnected + rpcSessionId := some r.result.sessionId + requestNo := requestNo + 1 + let params : Lsp.PlainGoalParams := { + textDocument := { uri } + position := pos, + } + let ps : RpcCallParams := { + params := toJson params + textDocument := { uri } + position := pos, + sessionId := rpcSessionId.get!, + method := `Lean.Widget.getInteractiveGoals + } + Ipc.writeRequest ⟨requestNo, "$/lean/rpc/call", ps⟩ + let response ← Ipc.readResponseAs requestNo Client.InteractiveGoals + requestNo := requestNo + 1 + IO.eprintln (repr response.result) + IO.eprintln "" + | "widgets" => + if rpcSessionId.isNone then + Ipc.writeRequest ⟨requestNo, "$/lean/rpc/connect", RpcConnectParams.mk uri⟩ + let r ← Ipc.readResponseAs requestNo RpcConnected + rpcSessionId := some r.result.sessionId + requestNo := requestNo + 1 let ps : RpcCallParams := { - ps with - method := `Lean.Widget.getWidgetSource, - params := toJson params, + textDocument := {uri := uri}, + position := pos, + sessionId := rpcSessionId.get!, + method := `Lean.Widget.getWidgets, + params := toJson pos, } Ipc.writeRequest ⟨requestNo, "$/lean/rpc/call", ps⟩ - let resp ← Ipc.readResponseAs requestNo Lean.Widget.WidgetSource - IO.eprintln (toJson resp.result) + let response ← Ipc.readResponseAs requestNo Lean.Widget.GetWidgetsResponse + requestNo := requestNo + 1 + IO.eprintln response.result.debugJson + for w in response.result.widgets do + let params : Lean.Widget.GetWidgetSourceParams := { pos, hash := w.javascriptHash } + let ps : RpcCallParams := { + ps with + method := `Lean.Widget.getWidgetSource, + params := toJson params, + } + Ipc.writeRequest ⟨requestNo, "$/lean/rpc/call", ps⟩ + let resp ← Ipc.readResponseAs requestNo Lean.Widget.WidgetSource + IO.eprintln (toJson resp.result) + requestNo := requestNo + 1 + | _ => + let Except.ok params ← pure <| Json.parse params + | throw <| IO.userError s!"failed to parse {params}" + let params := params.setObjVal! "textDocument" (toJson { uri := uri : TextDocumentIdentifier }) + -- TODO: correctly compute in presence of Unicode + let params := params.setObjVal! "position" (toJson pos) + IO.eprintln params + Ipc.writeRequest ⟨requestNo, method, params⟩ + let rec readFirstResponse := do + match ← Ipc.readMessage with + | Message.response id r => + assert! id == requestNo + return r + | Message.notification .. => readFirstResponse + | Message.request .. => readFirstResponse + | msg => throw <| IO.userError s!"unexpected message {toJson msg}" + let resp ← readFirstResponse + IO.eprintln resp requestNo := requestNo + 1 | _ => - let Except.ok params ← pure <| Json.parse params - | throw <| IO.userError s!"failed to parse {params}" - let params := params.setObjVal! "textDocument" (toJson { uri := uri : TextDocumentIdentifier }) - -- TODO: correctly compute in presence of Unicode - let params := params.setObjVal! "position" (toJson pos) - IO.eprintln params - Ipc.writeRequest ⟨requestNo, method, params⟩ - let rec readFirstResponse := do - match ← Ipc.readMessage with - | Message.response id r => - assert! id == requestNo - return r - | Message.notification .. => readFirstResponse - | Message.request .. => readFirstResponse - | msg => throw <| IO.userError s!"unexpected message {toJson msg}" - let resp ← readFirstResponse - IO.eprintln resp - requestNo := requestNo + 1 - | _ => - lastActualLineNo := lineNo - lineNo := lineNo + 1 + lastActualLineNo := lineNo + lineNo := lineNo + 1 + + let _ ← Ipc.collectDiagnostics requestNo uri (versionNo - 1) + Ipc.writeNotification ⟨"textDocument/didClose", { + textDocument := { uri } : DidCloseTextDocumentParams }⟩ Ipc.shutdown requestNo discard <| Ipc.waitForExit diff --git a/tests/lean/interactive/unterminatedDocComment.lean b/tests/lean/interactive/unterminatedDocComment.lean index d8c5cc5bc545..dfdfc29061cc 100644 --- a/tests/lean/interactive/unterminatedDocComment.lean +++ b/tests/lean/interactive/unterminatedDocComment.lean @@ -4,6 +4,6 @@ def a2 := sorry def a3 := sorry ... - ---^ insert: / +--^ insert: "/" --^ collectDiagnostics def a4 := 0 diff --git a/tests/lean/interactive/unterminatedDocComment.lean.expected.out b/tests/lean/interactive/unterminatedDocComment.lean.expected.out index e9aaf499a7a7..039bdea99e6e 100644 --- a/tests/lean/interactive/unterminatedDocComment.lean.expected.out +++ b/tests/lean/interactive/unterminatedDocComment.lean.expected.out @@ -1,6 +1 @@ -{"textDocument": {"version": 2, "uri": "file:///unterminatedDocComment.lean"}, - "contentChanges": - [{"text": "/", - "range": - {"start": {"line": 5, "character": 2}, "end": {"line": 5, "character": 3}}}]} {"version": 2, "uri": "file:///unterminatedDocComment.lean", "diagnostics": []} diff --git a/tests/lean/server/test_single.sh b/tests/lean/server/test_single.sh index fc64ae8f137b..93756ff05bdd 100755 --- a/tests/lean/server/test_single.sh +++ b/tests/lean/server/test_single.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash source ../../common.sh -exec_check lean -j 0 -Dlinter.all=false --run "$f" +exec_check lean -Dlinter.all=false --run "$f"