Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Incr tactic rc1 #4087

Closed
wants to merge 48 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
1fb344a
feat: incremental elaboration of definition headers, bodies, and top-…
Kha Mar 15, 2024
a111a4b
refactor: make initial elaboration snapshot `Dynamic`
Kha Mar 15, 2024
9ab9863
fix: tracking command errors throughout incremental reporting
Kha Mar 19, 2024
aa47762
try-finally does not use `MonadExcept`
Kha Mar 20, 2024
bac5d75
fix: promise deadlock
Kha Mar 20, 2024
70bdb18
cleanup
Kha Mar 21, 2024
e28eb2e
`withNarrowedReuse`
Kha Mar 21, 2024
a9bf2e5
more combinators
Kha Mar 21, 2024
feb8d43
withNarrowedArgTacticReuse
Kha Mar 21, 2024
5b9192f
fix incremental reporting in combinators
Kha Mar 22, 2024
66e0df6
fix structRangeEq
Kha Mar 22, 2024
1fbe26a
incremental `induction`
Kha Mar 22, 2024
4fe24b1
enhanced reuse tracing
Kha Mar 27, 2024
559b482
refine testing
Kha Mar 27, 2024
8ee1ddd
consolidate tests
Kha Mar 27, 2024
b941f10
basic mutual tests
Kha Mar 27, 2024
3a2e1b5
fix other tests
Kha Mar 27, 2024
b486b35
promise combinator
Kha Apr 3, 2024
aef9fc6
unify *SnapshotBundle
Kha Apr 4, 2024
2bb17c6
start Incremental Command Elaboration library note
Kha Apr 5, 2024
ca2a8df
document incremental induction
Kha Apr 5, 2024
2780e20
refactor `evalAlt`
Kha Apr 5, 2024
24c325d
doc: evalAlts refactoring
Kha Apr 17, 2024
ab3c0c4
Reuse in any `induction` branch
Kha Apr 18, 2024
84fdd8e
Be consistent about `^ sync`
Kha Apr 18, 2024
c3bf2be
Better names in evalAlts
Kha Apr 18, 2024
b3d2936
evalSepTactics docs
Kha Apr 18, 2024
c70e5fa
simplify `withNarrowedTacticReuse`
Kha Apr 18, 2024
1bda8d9
more docs
Kha Apr 18, 2024
ff8a4ff
use old frontend on cmdline again, clearly nothing is breaking withou…
Kha Apr 18, 2024
3718514
refactor: avoid unscoped promises in elaborator
Kha Apr 18, 2024
e32b119
cleanup
Kha Apr 19, 2024
c24bff3
rebased merge
Kha Apr 19, 2024
06effc2
good-enough approximation of "true" heartbeats in case of reuse
Kha Apr 24, 2024
592441d
clean up naming
Kha Apr 25, 2024
4595e26
better final snapshot range
Kha Apr 25, 2024
e1c6577
disable incrementality for induction pre-tac
Kha Apr 26, 2024
2d69d8e
better trace.reuse formatting
Kha Apr 26, 2024
53c9c5b
document and enforce correct construction of `Snapshot.Diagnostics`
Kha Apr 26, 2024
8d342d4
incremental error reporting in tactics
Kha Apr 26, 2024
b8c2cef
fix: rework elaboration interruption
Kha Apr 29, 2024
2321b55
fix: MessageLog.hadErrors
Kha May 2, 2024
72b3e36
Merge tag 'v4.8.0-rc1' into incr-tactic
Kha May 7, 2024
cd4f254
merge fix
Kha May 7, 2024
fc9fadf
oh
Kha May 7, 2024
b831e7c
chore: Nix: denylist test suddenly requiring network
Kha May 7, 2024
9b65e53
finish `next?` renaming
Kha May 7, 2024
9def479
fix wrong reuse
Kha May 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/examples/test_single.sh
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 1 addition & 1 deletion nix/bootstrap.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions src/Init/Data/String/Basic.lean
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions src/Init/Prelude.lean
Original file line number Diff line number Diff line change
Expand Up @@ -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

/--
Expand Down
33 changes: 33 additions & 0 deletions src/Init/System/IO.lean
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down Expand Up @@ -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

Expand Down
80 changes: 64 additions & 16 deletions src/Lean/CoreM.lean
Original file line number Diff line number Diff line change
Expand Up @@ -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 := {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 })
Expand Down Expand Up @@ -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} <num>` to set the limit\nuse `set_option {diagnostics.name} true` to get diagnostic information"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -409,19 +451,25 @@ def ImportM.runCoreM (x : CoreM α) : ImportM α := do
let (a, _) ← (withOptions (fun _ => ctx.opts) x).toIO { fileName := "<ImportM>", 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
Expand Down
33 changes: 29 additions & 4 deletions src/Lean/Elab/Command.lean
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,18 @@ 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`.

Invariant: if the bundle's `old?` is set, the context and state at the beginning of current and
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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -534,6 +558,7 @@ def liftCommandElabM (cmd : CommandElabM α) : CoreM α := do
ref := ← getRef
tacticCache? := none
snap? := none
cancelTk? := (← read).cancelTk?
} |>.run {
env := ← getEnv
maxRecDepth := ← getMaxRecDepth
Expand Down
82 changes: 82 additions & 0 deletions src/Lean/Elab/DefView.lean
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
Loading
Loading