lean4-htt/src/Lean/ExtraModUses.lean
Sebastian Ullrich e0650a0336
feat: shake: make Mathlib-ready (#11496)
This PR implements new flags and annotations for `shake` for use in
Mathlib:

> Options:
>   --keep-implied
> Preserves existing imports that are implied by other imports and thus
not technically needed
>     anymore
> 
>   --keep-prefix
> If an import `X` would be replaced in favor of a more specific import
`X.Y...` it implies,
> preserves the original import instead. More generally, prefers
inserting `import X` even if it
> was not part of the original imports as long as it was in the original
transitive import closure
>     of the current module.
> 
>   --keep-public
> Preserves all `public` imports to avoid breaking changes for external
downstream modules
> 
>   --add-public
> Adds new imports as `public` if they have been in the original public
closure of that module.
> In other words, public imports will not be removed from a module
unless they are unused even
> in the private scope, and those that are removed will be re-added as
`public` in downstream
> modules even if only needed in the private scope there. Unlike
`--keep-public`, this may
> introduce breaking changes but will still limit the number of inserted
imports.
> 
> Annotations:
> The following annotations can be added to Lean files in order to
configure the behavior of
> `shake`. Only the substring `shake: ` directly followed by a directive
is checked for, so multiple
> directives can be mixed in one line such as `-- shake:
keep-downstream, shake: keep-all`, and they
> can be surrounded by arbitrary comments such as `-- shake: keep
(metaprogram output dependency)`.
> 
>   * `module -- shake: keep-downstream`:
> Preserves this module in all (current) downstream modules, adding new
imports of it if needed.
> 
>   * `module -- shake: keep-all`:
> Preserves all existing imports in this module as is. New imports now
needed because of upstream
>     changes may still be added.
> 
>   * `import X -- shake: keep`:
> Preserves this specific import in the current module. The most common
use case is to preserve a
> public import that will be needed in downstream modules to make sense
of the output of a
> metaprogram defined in this module. For example, if a tactic is
defined that may synthesize a
> reference to a theorem when run, there is no way for `shake` to detect
this by itself and the
> module of that theorem should be publicly imported and annotated with
`keep` in the tactic's
>     module.
>     ```
>     public import X  -- shake: keep (metaprogram output dependency)
> 
>     ...
> 
>     elab \"my_tactic\" : tactic => do
> ... mkConst ``f -- `f`, defined in `X`, may appear in the output of
this tactic
>     ```
2025-12-05 09:37:58 +00:00

139 lines
6 KiB
Text

/-
Copyright (c) 2025 Lean FRO. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Sebastian Ullrich
-/
module
prelude
public import Lean.CoreM
public import Lean.Compiler.MetaAttr -- TODO: public because of specializations
import Init.Data.Range.Polymorphic.Stream
/-!
Infrastructure for recording extra import dependencies not implied by the environment constants for
the benefit of `shake`.
-/
namespace Lean
public structure IndirectModUse where
kind : String
declName : Name
deriving BEq
public builtin_initialize indirectModUseExt : SimplePersistentEnvExtension IndirectModUse (Std.HashMap Name (Array ModuleIdx)) ←
registerSimplePersistentEnvExtension {
addEntryFn s _ := s
addImportedFn es := Id.run do
let mut s := {}
for es in es, modIdx in 0...* do
for e in es do
s := s.alter e.declName (·.getD #[] |>.push modIdx)
return s
asyncMode := .sync
}
public def getIndirectModUses (env : Environment) (modIdx : ModuleIdx) : Array IndirectModUse :=
indirectModUseExt.getModuleEntries env modIdx
variable [Monad m] [MonadEnv m] [MonadTrace m] [MonadOptions m] [MonadRef m] [AddMessageContext m]
/--
Lets `shake` know that references to `declName` may also require importing the current module due to
some additional metaprogramming dependency expressed by `kind`. Currently this is always the name of
an attribute applied to `declName`, which is not from the current module, in the current module.
`kind` is not currently used to further filter what references to `declName` require importing the
current module but may in the future.
-/
public def recordIndirectModUse (kind : String) (declName : Name) : m Unit := do
-- We can assume this is called from the main thread only and that the list of entries is short
if !(indirectModUseExt.getEntries (asyncMode := .mainOnly) (← getEnv) |>.contains { kind, declName }) then
trace[extraModUses] "recording indirect mod use of `{declName}` ({kind})"
modifyEnv (indirectModUseExt.addEntry · { kind, declName })
/-- Additional import dependency for elaboration. -/
public structure ExtraModUse where
/-- Dependency's module name. -/
module : Name
/-- Whether dependency must be imported as `public`. -/
isExported : Bool
/-- Whether dependency must be imported as `meta`. -/
isMeta : Bool
deriving BEq, Hashable, Repr
builtin_initialize extraModUses : SimplePersistentEnvExtension ExtraModUse (PHashSet ExtraModUse) ←
registerSimplePersistentEnvExtension {
addEntryFn m k := m.insert k
addImportedFn _ := {}
exportEntriesFnEx? := some fun _ _ entries => fun
| .private => entries.toArray
| _ => #[]
asyncMode := .sync
replay? := some <| SimplePersistentEnvExtension.replayOfFilter (·.contains ·) (·.insert ·)
}
/-- Returns additional recorded import dependencies of the given module. -/
public def getExtraModUses (env : Environment) (modIdx : ModuleIdx) : Array ExtraModUse :=
extraModUses.getModuleEntries env modIdx
/-- Copies additional recorded import dependencies from one environment to another. -/
public def copyExtraModUses (src dest : Environment) : Environment := Id.run do
let mut env := dest
for entry in extraModUses.getEntries (asyncMode := .local) src do
if !(extraModUses.getState (asyncMode := .local) env).contains entry then
env := extraModUses.addEntry env entry
env
def recordExtraModUseCore (mod : Name) (isMeta : Bool) (hint : Name := .anonymous) : m Unit := do
let entry := { module := mod, isExported := (← getEnv).isExporting, isMeta }
if !(extraModUses.getState (asyncMode := .local) (← getEnv)).contains entry then
trace[extraModUses] "recording {if entry.isExported then "public" else "private"} \
{if isMeta then "meta" else "regular"} extra mod use {mod}\
{if hint.isAnonymous then m!"" else m!" of {hint}"}"
modifyEnv (extraModUses.addEntry · entry)
/--
Records an additional import dependency for the current module, using `Environment.isExporting` as
the visibility level.
NOTE: Directly recording a module name does not trigger including indirect dependencies recorded via
`recordIndirectModUse`, prefer `recordExtraModUseFromDecl` instead.
-/
public def recordExtraModUse (modName : Name) (isMeta : Bool) : m Unit := do
if modName != (← getEnv).mainModule then
recordExtraModUseCore modName isMeta
/--
Records the module of the given declaration as an additional import dependency for the current
module, using `Environment.isExporting` as the visibility level. If the declaration itself is
already `meta`, the module dependency is recorded as a non-`meta` dependency.
-/
public def recordExtraModUseFromDecl (declName : Name) (isMeta : Bool) : m Unit := do
let env ← getEnv
if let some mod := env.getModuleIdxFor? declName |>.bind (env.header.modules[·]?) then
-- If the declaration itself is already `meta`, no need to mark the import.
let isMeta := isMeta && !isMarkedMeta (← getEnv) declName
recordExtraModUseCore mod.module isMeta (hint := declName)
for modIdx in (indirectModUseExt.getState (asyncMode := .local) env)[declName]?.getD #[] do
recordExtraModUseCore env.header.modules[modIdx]!.module (isMeta := false) (hint := declName)
builtin_initialize isExtraRevModUseExt : SimplePersistentEnvExtension Unit Unit ←
registerSimplePersistentEnvExtension {
addEntryFn s e := ()
addImportedFn _ := ()
asyncMode := .sync
}
/-- Checks whether this module should be preserved as an import by `shake`. -/
public def isExtraRevModUse (env : Environment) (modIdx : ModuleIdx) : Bool :=
!(isExtraRevModUseExt.getModuleEntries env modIdx |>.isEmpty)
/-- Records this module to be preserved as an import by `shake`. -/
public def recordExtraRevUseOfCurrentModule : m Unit := do
if isExtraRevModUseExt.getEntries (asyncMode := .local) (← getEnv) |>.isEmpty then
trace[extraModUses] "recording extra reverse use of current module"
modifyEnv (isExtraRevModUseExt.addEntry · ())
builtin_initialize
registerTraceClass `extraModUses