fix: reject attribute uses whose module is reachable only via IR (#13613)

This PR makes the elaborator reject `@[foo]` when the module that
registers `foo` is not visibly imported into the current file but merely
loaded as IR. Previously such uses silently elaborated but led to
divergence of cmdline and server behavior and caused `lake shake --fix`
to flip-flop on successive runs (#13599).
This commit is contained in:
Sebastian Ullrich 2026-05-06 13:55:43 +02:00 committed by GitHub
parent 0e2088fc83
commit e6dfdfdcee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 50 additions and 8 deletions

View file

@ -63,6 +63,14 @@ def elabAttr [Monad m] [MonadEnv m] [MonadResolveName m] [MonadError m] [MonadMa
| throwError "Unknown attribute `[{attrName}]`"
if let .ok impl := getAttributeImpl (← getEnv) attrName then
if regularInitAttr.getParam? (← getEnv) impl.ref |>.isSome then -- skip `builtin_initialize` attributes
-- Reject attribute uses where the implementation's module has been loaded for IR only as
-- this would make it `inServer`-dependent and confused shake as well (#13599).
if let some idx := (← getEnv).getModuleIdxFor? impl.ref then
let env ← getEnv
if env.header.modules[idx]?.any (!·.hasData) then
let modName := env.header.modules[idx]!.module
throwError m!"Cannot use attribute `[{attrName}]`: module `{modName}` is loaded for IR \
only (reached as a private `meta` dependency). Add an import of `{modName}`."
recordExtraModUseFromDecl (isMeta := true) impl.ref
/- The `AttrM` does not have sufficient information for expanding macros in `args`.
So, we expand them before here before we invoke the attributer handlers implemented using `AttrM`. -/

View file

@ -147,6 +147,8 @@ structure ModuleData where
structure EffectiveImport extends Import where
/-- Phases for which the import's IR is available. -/
irPhases : IRPhases
/-- Whether the import's `.olean*` data has been loaded (otherwise only the `.ir` is). -/
hasData : Bool
deriving Inhabited
/-- Environment fields that are not used often. -/
@ -1980,14 +1982,12 @@ private structure ImportedModule extends EffectiveImport where
parts : Array (ModuleData × CompactedRegion)
/-- `.ir` data, if loaded. -/
irData? : Option (ModuleData × CompactedRegion)
/-- If true, `.olean*` data should be imported. -/
needsData : Bool
/-- If true, IR is loaded transitively. -/
needsIRTrans : Bool
/-- The main module data that will eventually be used to construct the publicly accessible constants. -/
private def ImportedModule.publicModule? (self : ImportedModule) : Option ModuleData := do
if self.needsData then
if self.hasData then
self.parts[0]?.map (·.1)
else
-- (should not have any constants)
@ -2000,7 +2000,7 @@ private def ImportedModule.getData? (self : ImportedModule) (level : OLeanLevel)
/-- The main module data that will eventually be used to construct the kernel environment. -/
private def ImportedModule.mainModule? (self : ImportedModule) : Option ModuleData :=
if self.needsData then
if self.hasData then
self.getData? (if self.importAll then .private else .exported)
else
self.irData?.map (·.1)
@ -2150,16 +2150,16 @@ where
-- when module is already imported, bump flags
let importAll := importAll || mod.importAll
let isExported := isExported || mod.isExported
let needsData := needsData || mod.needsData
let needsData := needsData || mod.hasData
let needsIRTrans := needsIRTrans || mod.needsIRTrans
let needsIR := needsIRTrans || importAll
let irPhases := if irPhases == mod.irPhases then irPhases else .all
let parts ← if needsData && mod.parts.isEmpty then loadData i else pure mod.parts
let irData? ← if needsIR && mod.irData?.isNone then loadIR? i else pure mod.irData?
if importAll != mod.importAll || isExported != mod.isExported ||
needsIRTrans != mod.needsIRTrans || needsData != mod.needsData || irPhases != mod.irPhases then
needsIRTrans != mod.needsIRTrans || needsData != mod.hasData || irPhases != mod.irPhases then
modify fun s => { s with moduleNameMap := s.moduleNameMap.insert i.module { mod with
importAll, isExported, irPhases, parts, irData?, needsData, needsIRTrans }}
importAll, isExported, irPhases, parts, irData?, hasData := needsData, needsIRTrans }}
-- bump entire closure
goRec mod
continue
@ -2167,7 +2167,7 @@ where
-- newly discovered module
let parts ← if needsData then loadData i else pure #[]
let irData? ← if needsIR then loadIR? i else pure none
let mod := { i with importAll, isExported, irPhases, parts, irData?, needsIRTrans, needsData }
let mod := { i with importAll, isExported, irPhases, parts, irData?, needsIRTrans, hasData := needsData }
goRec mod
modify fun s => { s with
moduleNameMap := s.moduleNameMap.insert i.module mod

View file

@ -1,4 +1,5 @@
import UserAttr.Tst
import UserAttr.MetaUser
open Lean

View file

@ -0,0 +1,13 @@
module
import UserAttr.BlaAttr
/-! Middle module that plain-imports `UserAttr.BlaAttr`, used by `UserAttr.MetaUser` to set up the
`meta import` chain that triggers the regression in
https://github.com/leanprover/lean4/issues/13599. -/
@[expose] public section
def midVal : Nat := 17
end

View file

@ -0,0 +1,20 @@
module
public meta import UserAttr.MetaMid
meta import Lean.Elab.GuardMsgs
/-! Regression test for https://github.com/leanprover/lean4/issues/13599: using `@[my_simp]` (an
attribute registered in `UserAttr.BlaAttr`) while reaching `BlaAttr` only via a `meta import`
chain (`UserAttr.MetaMid → import UserAttr.BlaAttr`) used to make `shake --fix` flip-flop. The
elaborator now rejects such uses with a clear error pointing at the missing import. -/
@[expose] public section
/--
error: Cannot use attribute `[my_simp]`: module `UserAttr.BlaAttr` is loaded for IR only (reached as a private `meta` dependency). Add an import of `UserAttr.BlaAttr`.
-/
#guard_msgs in
@[my_simp]
theorem midVal_eq : midVal = 17 := rfl
end