Skip to content

Commit 5db4f96

Browse files
authored
feat: lake: resolve module clashes on import (#11270)
This PR adds a module resolution procedure to Lake to disambiguate modules that are defined in multiple packages. On an `import`, Lake will now check if multiple packages within the workspace define the module. If so, it will verify that modules have sufficiently similar definitions (i.e., artifacts with the same content hashes). If not, Lake will report an error. This verification is currently only done for direct imports. Transitive imports are not checked for consistency. An overhaul of transitive imports will come later.
1 parent 8bc3eb1 commit 5db4f96

File tree

16 files changed

+138
-28
lines changed

16 files changed

+138
-28
lines changed

src/lake/Lake/Build/Facets.lean

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,15 @@ public structure ModuleImportInfo where
7474
allTransTrace : BuildTrace
7575
/-- Transitive import trace for an `import` of the module without the module system enabled. -/
7676
legacyTransTrace : BuildTrace
77+
deriving Inhabited
7778

7879
/-- **For internal use only.** Information about the imports of this module. -/
7980
builtin_facet importInfo : Module => ModuleImportInfo
8081

8182
/-- Information useful to importers of a module. -/
8283
public structure ModuleExportInfo where
84+
/-- The trace of the module's source file. -/
85+
srcTrace : BuildTrace
8386
/-- Artifacts directly needed for an `import` of the module with the module system enabled. -/
8487
arts : ImportArtifacts
8588
/-- The trace of the module's public olean. -/
@@ -101,6 +104,7 @@ public structure ModuleExportInfo where
101104
allTransTrace : BuildTrace
102105
/-- Transitive import trace for an `import` of the module without the module system enabled. -/
103106
legacyTransTrace : BuildTrace
107+
deriving Inhabited
104108

105109
/-- **For internal use only.** Information useful to importers of this module. -/
106110
builtin_facet exportInfo : Module => ModuleExportInfo

src/lake/Lake/Build/Infos.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public structure ModuleImport extends Import where
6767
/-- A module's source file path plus its parsed header. -/
6868
public structure ModuleInput where
6969
path : FilePath
70+
trace : BuildTrace
7071
header : ModuleHeader
7172
imports : Array ModuleImport
7273

src/lake/Lake/Build/Job/Monad.lean

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ results of `a` and `b`. The job `c` errors if either `a` or `b` error.
230230
-/
231231
@[inline] public def zipResultWith
232232
[OptDataKind γ] (f : JobResult α → JobResult β → JobResult γ) (self : Job α) (other : Job β)
233-
(prio := Task.Priority.default) (sync := true)
233+
(prio := Task.Priority.default) (sync := false)
234234
: Job γ := Job.ofTask $
235235
self.task.bind (prio := prio) (sync := true) fun rx =>
236236
other.task.map (prio := prio) (sync := sync) fun ry =>
@@ -242,7 +242,7 @@ results of `a` and `b`. The job `c` errors if either `a` or `b` error.
242242
-/
243243
@[inline] public def zipWith
244244
[OptDataKind γ] (f : α → β → γ) (self : Job α) (other : Job β)
245-
(prio := Task.Priority.default) (sync := true)
245+
(prio := Task.Priority.default) (sync := false)
246246
: Job γ :=
247247
self.zipResultWith (other := other) (prio := prio) (sync := sync) fun
248248
| .ok a sa, .ok b sb => .ok (f a b) (sa.merge sb)
@@ -257,7 +257,7 @@ public def add (self : Job α) (other : Job β) : Job α :=
257257

258258
/-- Merges this job with another, discarding both outputs. -/
259259
public def mix (self : Job α) (other : Job β) : Job Unit :=
260-
self.zipWith (fun _ _ => ()) other
260+
self.zipWith (sync := true) (fun _ _ => ()) other
261261

262262
/-- Merge a `List` of jobs into one, discarding their outputs. -/
263263
public def mixList (jobs : List (Job α)) (traceCaption := "<collection>") : Job Unit :=
@@ -269,10 +269,18 @@ public def mixArray (jobs : Array (Job α)) (traceCaption := "<collection>") :
269269

270270
/-- Merge a `List` of jobs into one, collecting their outputs into a `List`. -/
271271
public def collectList (jobs : List (Job α)) (traceCaption := "<collection>") : Job (List α) :=
272-
jobs.foldr (zipWith List.cons) (traceRoot [] traceCaption)
272+
jobs.foldr (zipWith (sync := true) List.cons) (traceRoot [] traceCaption)
273273

274274
/-- Merge an `Array` of jobs into one, collecting their outputs into an `Array`. -/
275275
public def collectArray (jobs : Array (Job α)) (traceCaption := "<collection>") : Job (Array α) :=
276-
jobs.foldl (zipWith Array.push) (traceRoot (Array.mkEmpty jobs.size) traceCaption)
276+
jobs.foldl (zipWith (sync := true) Array.push) (traceRoot (Array.mkEmpty jobs.size) traceCaption)
277+
278+
private instance : Nonempty ({α : Type u} → [Nonempty α] → α) := ⟨Classical.ofNonempty⟩
279+
280+
/-- Merge an `Vector` of jobs into one, collecting their outputs into an `Array`. -/
281+
public def collectVector {α : Type u} [Nonempty α] (jobs : Vector (Job α) n) (traceCaption := "<collection>") : Job (Vector α n) :=
282+
let placeholder := unsafe have : Nonempty α := inferInstance; (unsafeCast () : α)
283+
n.fold (init := traceRoot (Vector.replicate n placeholder) traceCaption) fun i h job =>
284+
job.zipWith (sync := true) (Vector.set · i · h) jobs[i]
277285

278286
end Job

src/lake/Lake/Build/Module.lean

Lines changed: 75 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,12 @@ Build function definitions for a module's builtin facets.
3030
private def Module.recFetchInput (mod : Module) : FetchM (Job ModuleInput) := Job.async do
3131
let path := mod.leanFile
3232
let contents ← IO.FS.readFile path
33-
setTrace {caption := path.toString, mtime := ← getMTime path, hash := .ofText contents}
33+
let trace := {caption := path.toString, mtime := ← getMTime path, hash := .ofText contents}
34+
setTrace trace
3435
let header ← Lean.parseImports' contents path.toString
3536
let imports ← header.imports.mapM fun imp => do
3637
return ⟨imp, (← findModule? imp.module)⟩
37-
return {path, header, imports}
38+
return {path, header, imports, trace}
3839

3940
/-- The `ModuleFacetConfig` for the builtin `inputFacet`. -/
4041
public def Module.inputFacetConfig : ModuleFacetConfig inputFacet :=
@@ -261,25 +262,37 @@ private def ModuleImportInfo.nil (modName : Name) : ModuleImportInfo where
261262
allTransTrace := .nil s!"{modName} transitive imports (all)"
262263
legacyTransTrace := .nil s!"{modName} transitive imports (legacy)"
263264

265+
private def ModuleExportInfo.disambiguationHash
266+
(self : ModuleExportInfo) (nonModule : Bool) (imp : Import)
267+
: Hash :=
268+
if nonModule then
269+
self.legacyTransTrace.hash.mix self.allArtsTrace.hash
270+
else if imp.importAll then
271+
self.allTransTrace.hash.mix self.allArtsTrace.hash
272+
else if imp.isMeta then
273+
self.metaTransTrace.hash.mix self.metaArtsTrace.hash
274+
else
275+
self.transTrace.hash.mix self.artsTrace.hash
276+
264277
private def ModuleImportInfo.addImport
265278
(info : ModuleImportInfo) (nonModule : Bool)
266-
(mod : Module) (imp : Import) (expInfo : ModuleExportInfo)
279+
(imp : Import) (expInfo : ModuleExportInfo)
267280
: ModuleImportInfo :=
268281
let info :=
269282
if nonModule then
270283
{info with
271-
directArts := info.directArts.insert mod.name expInfo.allArts
284+
directArts := info.directArts.insert imp.module expInfo.allArts
272285
trace := info.trace.mix expInfo.legacyTransTrace |>.mix expInfo.allArtsTrace.withoutInputs
273286
}
274287
else if imp.importAll then
275288
{info with
276-
directArts := info.directArts.insert mod.name expInfo.allArts
289+
directArts := info.directArts.insert imp.module expInfo.allArts
277290
trace := info.trace.mix expInfo.allTransTrace |>.mix expInfo.allArtsTrace.withoutInputs
278291
}
279292
else
280293
let info :=
281-
if !info.directArts.contains mod.name then -- do not demote `import all`
282-
{info with directArts := info.directArts.insert mod.name expInfo.arts}
294+
if !info.directArts.contains imp.module then -- do not demote `import all`
295+
{info with directArts := info.directArts.insert imp.module expInfo.arts}
283296
else
284297
info
285298
if imp.isMeta then
@@ -338,6 +351,12 @@ private def ModuleImportInfo.addImport
338351
else
339352
info
340353

354+
private def Package.discriminant (self : Package) :=
355+
if self.version == {} then
356+
self.name.toString
357+
else
358+
s!"{self.name}@{self.version}"
359+
341360
private def fetchImportInfo
342361
(fileName : String) (pkgName modName : Name) (header : ModuleHeader)
343362
: FetchM (Job ModuleImportInfo) := do
@@ -348,13 +367,51 @@ private def fetchImportInfo
348367
if modName = imp.module then
349368
logError s!"{fileName}: module imports itself"
350369
return .error
351-
let some mod ← findModule? imp.module
352-
| return s
353-
if imp.importAll && !mod.allowImportAll && pkgName != mod.pkg.name then
354-
logError s!"{fileName}: cannot 'import all' across packages"
355-
return .error
356-
let importJob ← mod.exportInfo.fetch
357-
return s.zipWith (·.addImport nonModule mod imp ·) importJob
370+
let mods ← findModules imp.module
371+
let n := mods.size
372+
if h : n = 0 then
373+
return s
374+
else if n = 1 then -- common fast path
375+
let mod := mods[0]
376+
if imp.importAll && !mod.allowImportAll && pkgName != mod.pkg.name then
377+
logError s!"{fileName}: cannot `import all` \
378+
the module `{imp.module}` from the package `{mod.pkg.discriminant}`"
379+
return .error
380+
let importJob ← mod.exportInfo.fetch
381+
return s.zipWith (sync := true) (·.addImport nonModule imp ·) importJob
382+
else
383+
let isImportable (mod) :=
384+
mod.allowImportAll || pkgName == mod.pkg.name
385+
let allImportable :=
386+
if imp.importAll then
387+
mods.all isImportable
388+
else true
389+
unless allImportable do
390+
let msg := s!"{fileName}: cannot `import all` the module `{imp.module}` \
391+
from the following packages:"
392+
let msg := mods.foldl (init := msg) fun msg mod =>
393+
if isImportable mod then
394+
msg
395+
else
396+
s!"{msg}\n {mod.pkg.discriminant}"
397+
logError msg
398+
return .error
399+
let mods : Vector Module n := .mk mods rfl
400+
let expInfosJob ← Job.collectVector <$> mods.mapM (·.exportInfo.fetch)
401+
s.bindM (sync := true) fun impInfo => do
402+
expInfosJob.mapM (sync := true) fun expInfos => do
403+
let expInfo := expInfos[0]
404+
let impHash := expInfo.disambiguationHash nonModule imp
405+
let allEquiv := expInfos.toArray.all (start := 1) fun expInfo =>
406+
impHash == expInfo.disambiguationHash nonModule imp
407+
unless allEquiv do
408+
let msg := s!"{fileName}: could not disambiguate the module `{imp.module}`; \
409+
multiple packages provide distinct definitions:"
410+
let msg := n.fold (init := msg) fun i h s =>
411+
let hash := expInfos[i].disambiguationHash nonModule imp
412+
s!"{s}\n {mods[i].pkg.discriminant} (hash: {hash})"
413+
error msg
414+
return impInfo.addImport nonModule imp expInfo
358415

359416
/-- The `ModuleFacetConfig` for the builtin `importInfoFacet`. -/
360417
public def Module.importInfoFacetConfig : ModuleFacetConfig importInfoFacet :=
@@ -374,20 +431,21 @@ private def noIRError :=
374431
/-- Computes the import artifacts and transitive import trace of a module's imports. -/
375432
private def Module.computeExportInfo (mod : Module) : FetchM (Job ModuleExportInfo) := do
376433
(← mod.leanArts.fetch).mapM (sync := true) fun arts => do
377-
let header ← (← mod.header.fetch).await
434+
let input ← (← mod.input.fetch).await
378435
let importInfo ← (← mod.importInfo.fetch).await
379436
let artsTrace := BuildTrace.nil s!"{mod.name}:importArts"
380437
let metaArtsTrace := BuildTrace.nil s!"{mod.name}:importArts (meta)"
381438
let allArtsTrace := BuildTrace.nil s!"{mod.name}:importAllArts"
382439
let olean := arts.olean
383-
if header.isModule then
440+
if input.header.isModule then
384441
let some oleanServer := arts.oleanServer?
385442
| error noServerOLeanError
386443
let some ir := arts.ir?
387444
| error noIRError
388445
let some oleanPrivate := arts.oleanPrivate?
389446
| error noPrivateOLeanError
390447
return {
448+
srcTrace := input.trace
391449
arts := .ofArray #[olean.path, ir.path, oleanServer.path]
392450
artsTrace := artsTrace.mix olean.trace
393451
metaArtsTrace := metaArtsTrace.mix olean.trace |>.mix ir.trace
@@ -401,6 +459,7 @@ private def Module.computeExportInfo (mod : Module) : FetchM (Job ModuleExportIn
401459
}
402460
else
403461
return {
462+
srcTrace := input.trace
404463
arts := ⟨#[olean.path]⟩
405464
artsTrace := artsTrace.mix olean.trace
406465
metaArtsTrace := metaArtsTrace.mix olean.trace

src/lake/Lake/Config/Monad.lean

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ public def findPackage? (name : Name) : m (Option (NPackage name)) :=
8383
public def findModule? (name : Name) : m (Option Module) :=
8484
(·.findModule? name) <$> getWorkspace
8585

86+
@[inherit_doc Workspace.findModules, inline]
87+
public def findModules (name : Name) : m (Array Module) :=
88+
(·.findModules name) <$> getWorkspace
89+
8690
@[inherit_doc Workspace.findModuleBySrc?, inline]
8791
public def findModuleBySrc? (path : FilePath) : m (Option Module) :=
8892
(·.findModuleBySrc? path) <$> getWorkspace

src/lake/Lake/Config/Package.lean

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ namespace Package
131131

132132
/-- The identifier passed to Lean to disambiguate the package's native symbols. -/
133133
public def id? (self : Package) : Option PkgId :=
134-
if self.bootstrap then none else some <| self.name.toString (escape := false)
134+
if self.bootstrap then none else some <| self.origName.toString (escape := false)
135135

136136
/-- The package version. -/
137137
@[inline] public def version (self : Package) : LeanVer :=

src/lake/Lake/Config/Workspace.lean

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ public def isBuildableModule (mod : Name) (self : Workspace) : Bool :=
132132
public protected def findModule? (mod : Name) (self : Workspace) : Option Module :=
133133
self.packages.findSome? (·.findModule? mod)
134134

135+
/-- For each package in the workspace, locate the named, buildable, importable, local module. -/
136+
public protected def findModules (mod : Name) (self : Workspace) : Array Module :=
137+
self.packages.filterMap (·.findModule? mod)
138+
135139
/-- Locate the named, buildable, but not necessarily importable, module in the workspace. -/
136140
public def findTargetModule? (mod : Name) (self : Workspace) : Option Module :=
137141
self.packages.findSome? (·.findTargetModule? mod)

tests/lake/tests/module/test.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public def foo : String := "bar"
2020
EOF
2121

2222
# Test cross-package `import all` is prevented by default
23-
test_err "cannot 'import all' across packages" build ErrorTest.CrossPackageImportAll
23+
test_err 'cannot `import all` the module' build ErrorTest.CrossPackageImportAll
2424
# Test cross-package `import all` is allowed when `allowImportAll = true` is set
2525
test_run build Test.CrossPackageImportAll
2626

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module
2+
3+
import Similar
4+
5+
def testSimilar := similar
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module
2+
3+
public def similar : String := "A"

0 commit comments

Comments
 (0)