[llvm] [DWARFLinker] Let a clang module unit outrank every object file (PR #218079)
Jonas Devlieghere via llvm-commits
llvm-commits at lists.llvm.org
Fri Aug 21 17:49:10 PDT 2026
https://github.com/JDevlieghere created https://github.com/llvm/llvm-project/pull/218079
A module holds the definitive description of what it defines, so when several units contribute a copy of the same DIE to the type pool, the one built from the .pcm has to win. Priorities are packed as (object file index, unit index) and the type pool resolves a race in favour of the lowest, but a module unit took the index of whichever object file referenced the .pcm first. A module first referenced by a later object file therefore lost to an earlier object file's copy, and consumers read a DW_TAG_module carrying an importer's DW_AT_LLVM_include_path.
Reserve object file index zero for the module units of the whole link and number the object files from one.
rdar://185156929
Assisted-by: Claude
>From 22d279573359239c6fab6c60108d4272cc2c0e88 Mon Sep 17 00:00:00 2001
From: Jonas Devlieghere <jonas at devlieghere.com>
Date: Thu, 20 Aug 2026 09:56:20 -0700
Subject: [PATCH] [DWARFLinker] Let a clang module unit outrank every object
file
A module holds the definitive description of what it defines, so when
several units contribute a copy of the same DIE to the type pool, the
one built from the .pcm has to win. Priorities are packed as
(object file index, unit index) and the type pool resolves a race in
favour of the lowest, but a module unit took the index of whichever
object file referenced the .pcm first. A module first referenced by a
later object file therefore lost to an earlier object file's copy, and
consumers read a DW_TAG_module carrying an importer's
DW_AT_LLVM_include_path.
Reserve object file index zero for the module units of the whole link
and number the object files from one.
rdar://185156929
Assisted-by: Claude
---
.../Parallel/DWARFLinkerCompileUnit.h | 3 +-
.../DWARFLinker/Parallel/DWARFLinkerImpl.cpp | 20 ++++----
.../DWARFLinker/Parallel/DWARFLinkerImpl.h | 17 ++++++-
.../Inputs/module-import-canonical/1.ll | 35 +++++++++++++
.../Inputs/module-import-canonical/3.ll | 37 ++++++++++++++
.../Inputs/module-import-canonical/M-odr.ll | 24 +++++++++
.../Inputs/module-import-canonical/N-odr.ll | 24 +++++++++
.../debug-map-priority.map | 10 ++++
.../X86/module-import-canonical-priority.test | 51 +++++++++++++++++++
9 files changed, 210 insertions(+), 11 deletions(-)
create mode 100644 llvm/test/tools/dsymutil/Inputs/module-import-canonical/1.ll
create mode 100644 llvm/test/tools/dsymutil/Inputs/module-import-canonical/3.ll
create mode 100644 llvm/test/tools/dsymutil/Inputs/module-import-canonical/M-odr.ll
create mode 100644 llvm/test/tools/dsymutil/Inputs/module-import-canonical/N-odr.ll
create mode 100644 llvm/test/tools/dsymutil/Inputs/module-import-canonical/debug-map-priority.map
create mode 100644 llvm/test/tools/dsymutil/X86/module-import-canonical-priority.test
diff --git a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h
index 42bae8e6f08de..e8fc4b91bfd45 100644
--- a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h
+++ b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h
@@ -114,7 +114,8 @@ class alignas(8) CompileUnit : public DwarfUnit {
/// Returns DWARFFile containing this compile unit.
const DWARFFile &getContainingFile() const { return File; }
- /// Set deterministic priority for type DIE allocation ordering.
+ /// Set deterministic priority for type DIE allocation ordering. Units compare
+ /// by \p ObjFileIdx first and by \p LocalIdx second.
/// Lower priority values win when multiple CUs race to define the same type.
llvm::Error setPriority(uint64_t ObjFileIdx, uint64_t LocalIdx);
diff --git a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerImpl.cpp b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerImpl.cpp
index 119e0eb7dcc15..0999e58ef1a5d 100644
--- a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerImpl.cpp
+++ b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerImpl.cpp
@@ -29,10 +29,11 @@ DWARFLinkerImpl::DWARFLinkerImpl(MessageHandlerTy ErrorHandler,
DWARFLinkerImpl::LinkContext::LinkContext(LinkingGlobalData &GlobalData,
DWARFFile &File, uint64_t ObjFileIdx,
StringMap<uint64_t> &ClangModules,
+ uint64_t &ModuleUnitIdx,
std::atomic<size_t> &UniqueUnitID)
: OutputSections(GlobalData), InputDWARFFile(File),
ObjectFileIdx(ObjFileIdx), ClangModules(ClangModules),
- UniqueUnitID(UniqueUnitID) {
+ ModuleUnitIdx(ModuleUnitIdx), UniqueUnitID(UniqueUnitID) {
if (File.Dwarf) {
if (!File.Dwarf->compile_units().empty())
@@ -49,7 +50,8 @@ DWARFLinkerImpl::LinkContext::LinkContext(LinkingGlobalData &GlobalData,
void DWARFLinkerImpl::addObjectFile(DWARFFile &File, ObjFileLoaderTy Loader,
CompileUnitHandlerTy OnCUDieLoaded) {
ObjectContexts.emplace_back(std::make_unique<LinkContext>(
- GlobalData, File, ObjectContexts.size(), ClangModules, UniqueUnitID));
+ GlobalData, File, FirstObjFileIdx + ObjectContexts.size(), ClangModules,
+ ModuleUnitIdx, UniqueUnitID));
if (ObjectContexts.back()->InputDWARFFile.Dwarf) {
for (const std::unique_ptr<DWARFUnit> &CU :
@@ -470,6 +472,12 @@ Error DWARFLinkerImpl::LinkContext::loadClangModule(
}
if (Unit) {
+ // Incrementing the shared counter needs no synchronization: reaching this
+ // point requires a loader, and only the serial pass over the object files
+ // supplies one.
+ if (Error E = Unit->setPriority(ModuleUnitObjFileIdx, ModuleUnitIdx++))
+ return E;
+
ModulesCompileUnits.emplace_back(std::move(Unit));
// Preload line table, as it can't be loaded asynchronously.
ModulesCompileUnits.back()->loadLineTable();
@@ -487,13 +495,6 @@ Error DWARFLinkerImpl::LinkContext::link(TypeUnit *ArtificialTypeUnit) {
InputDWARFFile.Dwarf->getDebugMacinfo();
InputDWARFFile.Dwarf->getDebugMacro();
- // Assign deterministic priorities to module CUs for type DIE allocation.
- uint64_t LocalCUIdx = 0;
- for (std::unique_ptr<CompileUnit> &Mod : ModulesCompileUnits) {
- if (Error E = Mod->setPriority(ObjectFileIdx, LocalCUIdx++))
- return E;
- }
-
// Link modules compile units first.
parallelForEach(ModulesCompileUnits, [&](std::unique_ptr<CompileUnit> &Mod) {
// A module unit describes DIEs which no address reaches, so nothing marks
@@ -516,6 +517,7 @@ Error DWARFLinkerImpl::LinkContext::link(TypeUnit *ArtificialTypeUnit) {
// Create CompileUnit structures to keep information about source
// DWARFUnit`s, load line tables.
+ uint64_t LocalCUIdx = 0;
for (const auto &OrigCU : InputDWARFFile.Dwarf->compile_units()) {
// Load only unit DIE at this stage.
auto CUDie = OrigCU->getUnitDIE();
diff --git a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerImpl.h b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerImpl.h
index e44a1df599029..b7286bbb82a28 100644
--- a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerImpl.h
+++ b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerImpl.h
@@ -180,6 +180,8 @@ class DWARFLinkerImpl : public DWARFLinker {
StringMap<uint64_t> &ClangModules;
+ uint64_t &ModuleUnitIdx;
+
/// Flag indicating that new inter-connected compilation units were
/// discovered. It is used for restarting units processing
/// if new inter-connected units were found.
@@ -192,7 +194,7 @@ class DWARFLinkerImpl : public DWARFLinker {
LinkContext(LinkingGlobalData &GlobalData, DWARFFile &File,
uint64_t ObjFileIdx, StringMap<uint64_t> &ClangModules,
- std::atomic<size_t> &UniqueUnitID);
+ uint64_t &ModuleUnitIdx, std::atomic<size_t> &UniqueUnitID);
/// Check whether specified \p CUDie is a Clang module reference.
/// if \p Quiet is false then display error messages.
@@ -384,6 +386,15 @@ class DWARFLinkerImpl : public DWARFLinker {
/// Enumerate common sections and put their data into the output stream.
void writeCommonSectionsToTheOutput();
+ /// The object file index given to every clang module unit. It sorts below
+ /// every object file's, which makes a module unit outrank all of them: the
+ /// definition a module gives of what it defines wins over the copy an
+ /// importer carries.
+ static constexpr uint64_t ModuleUnitObjFileIdx = 0;
+
+ /// Object file indices follow the module units'.
+ static constexpr uint64_t FirstObjFileIdx = ModuleUnitObjFileIdx + 1;
+
/// \defgroup Data members accessed asinchroniously.
///
/// @{
@@ -395,6 +406,10 @@ class DWARFLinkerImpl : public DWARFLinker {
/// addObjectFile(), which runs serially, so it needs no synchronization.
StringMap<uint64_t> ClangModules;
+ /// Numbers the clang module units of the whole link, so that they form one
+ /// priority sequence regardless of which object file referenced a .pcm first.
+ uint64_t ModuleUnitIdx = 0;
+
/// Type unit.
std::unique_ptr<TypeUnit> ArtificialTypeUnit;
/// @}
diff --git a/llvm/test/tools/dsymutil/Inputs/module-import-canonical/1.ll b/llvm/test/tools/dsymutil/Inputs/module-import-canonical/1.ll
new file mode 100644
index 0000000000000..d551930703fd4
--- /dev/null
+++ b/llvm/test/tools/dsymutil/Inputs/module-import-canonical/1.ll
@@ -0,0 +1,35 @@
+; Test input for ../X86/module-import-canonical-priority.test.
+;
+; An object file importing module M, whose own DW_TAG_module names a different
+; include path than the module's own unit does.
+
+target triple = "x86_64-apple-darwin"
+
+define void @main() !dbg !100 { ret void }
+
+!llvm.dbg.cu = !{!0, !20}
+!llvm.module.flags = !{!10, !11}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_ObjC, file: !1,
+ producer: "test", emissionKind: FullDebug,
+ imports: !30)
+!1 = !DIFile(filename: "1.m", directory: "")
+!100 = distinct !DISubprogram(name: "main", scope: !0, file: !1, line: 1,
+ type: !101, unit: !0,
+ spFlags: DISPFlagDefinition)
+!101 = !DISubroutineType(types: !102)
+!102 = !{null}
+!30 = !{!31}
+!31 = !DIImportedEntity(tag: DW_TAG_imported_declaration, scope: !0,
+ entity: !32, line: 1)
+!32 = !DIModule(scope: !0, name: "M",
+ includePath: "/tmp/M.framework/Modules/M.swiftmodule")
+
+; The skeleton unit referencing M.pcm. Its dwo id has to match M-odr.ll's.
+!20 = distinct !DICompileUnit(language: DW_LANG_ObjC, file: !21,
+ producer: "test", emissionKind: FullDebug,
+ splitDebugFilename: "M.pcm", dwoId: 42)
+!21 = !DIFile(filename: "M", directory: "")
+
+!10 = !{i32 2, !"Dwarf Version", i32 4}
+!11 = !{i32 2, !"Debug Info Version", i32 3}
diff --git a/llvm/test/tools/dsymutil/Inputs/module-import-canonical/3.ll b/llvm/test/tools/dsymutil/Inputs/module-import-canonical/3.ll
new file mode 100644
index 0000000000000..3e55e8f7a6d42
--- /dev/null
+++ b/llvm/test/tools/dsymutil/Inputs/module-import-canonical/3.ll
@@ -0,0 +1,37 @@
+; Test input for ../X86/module-import-canonical-priority.test.
+;
+; Like 1.ll, but in an ODR language and without a skeleton unit for M.pcm, so
+; this object file contributes a DW_TAG_module for M to the type table without
+; owning the unit which describes M in full. It does own N.pcm, which puts the
+; module units of the link in a different order than the object files.
+
+target triple = "x86_64-apple-darwin"
+
+define void @main3() !dbg !100 { ret void }
+
+!llvm.dbg.cu = !{!0, !40}
+!llvm.module.flags = !{!10, !11}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_ObjC_plus_plus, file: !1,
+ producer: "test", emissionKind: FullDebug,
+ imports: !30)
+!1 = !DIFile(filename: "3.mm", directory: "")
+!100 = distinct !DISubprogram(name: "main3", scope: !0, file: !1, line: 1,
+ type: !101, unit: !0,
+ spFlags: DISPFlagDefinition)
+!101 = !DISubroutineType(types: !102)
+!102 = !{null}
+!30 = !{!31}
+!31 = !DIImportedEntity(tag: DW_TAG_imported_declaration, scope: !0,
+ entity: !32, line: 1)
+!32 = !DIModule(scope: !0, name: "M",
+ includePath: "/tmp/M.framework/Modules/M.swiftmodule")
+
+; The skeleton unit referencing N.pcm. Its dwo id has to match N-odr.ll's.
+!40 = distinct !DICompileUnit(language: DW_LANG_ObjC_plus_plus, file: !41,
+ producer: "test", emissionKind: FullDebug,
+ splitDebugFilename: "N.pcm", dwoId: 43)
+!41 = !DIFile(filename: "N", directory: "")
+
+!10 = !{i32 2, !"Dwarf Version", i32 4}
+!11 = !{i32 2, !"Debug Info Version", i32 3}
diff --git a/llvm/test/tools/dsymutil/Inputs/module-import-canonical/M-odr.ll b/llvm/test/tools/dsymutil/Inputs/module-import-canonical/M-odr.ll
new file mode 100644
index 0000000000000..974f0e44867b3
--- /dev/null
+++ b/llvm/test/tools/dsymutil/Inputs/module-import-canonical/M-odr.ll
@@ -0,0 +1,24 @@
+; Test input for ../X86/module-import-canonical-priority.test.
+;
+; The module M in an ODR language, which places the DW_TAG_module describing it
+; in the type table rather than in the module unit.
+
+target triple = "x86_64-apple-darwin"
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!10, !11}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_ObjC_plus_plus, file: !1,
+ producer: "test", emissionKind: FullDebug,
+ retainedTypes: !2, dwoId: 42)
+!1 = !DIFile(filename: "M", directory: "")
+!2 = !{!3}
+!3 = !DICompositeType(tag: DW_TAG_structure_type, name: "S", scope: !4,
+ file: !1, line: 1, size: 32, elements: !5)
+!4 = !DIModule(scope: null, name: "M", includePath: "/tmp/M.framework")
+!5 = !{!6}
+!6 = !DIDerivedType(tag: DW_TAG_member, name: "value", scope: !3, file: !1,
+ line: 2, baseType: !7, size: 32)
+!7 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!10 = !{i32 2, !"Dwarf Version", i32 4}
+!11 = !{i32 2, !"Debug Info Version", i32 3}
diff --git a/llvm/test/tools/dsymutil/Inputs/module-import-canonical/N-odr.ll b/llvm/test/tools/dsymutil/Inputs/module-import-canonical/N-odr.ll
new file mode 100644
index 0000000000000..1ff8b6b79c9e9
--- /dev/null
+++ b/llvm/test/tools/dsymutil/Inputs/module-import-canonical/N-odr.ll
@@ -0,0 +1,24 @@
+; Test input for ../X86/module-import-canonical-priority.test.
+;
+; A second module, in an ODR language like M-odr.ll, so that the link holds more
+; than one module unit and they are owned by different object files.
+
+target triple = "x86_64-apple-darwin"
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!10, !11}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_ObjC_plus_plus, file: !1,
+ producer: "test", emissionKind: FullDebug,
+ retainedTypes: !2, dwoId: 43)
+!1 = !DIFile(filename: "N", directory: "")
+!2 = !{!3}
+!3 = !DICompositeType(tag: DW_TAG_structure_type, name: "T", scope: !4,
+ file: !1, line: 1, size: 32, elements: !5)
+!4 = !DIModule(scope: null, name: "N", includePath: "/tmp/N.framework")
+!5 = !{!6}
+!6 = !DIDerivedType(tag: DW_TAG_member, name: "count", scope: !3, file: !1,
+ line: 2, baseType: !7, size: 32)
+!7 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!10 = !{i32 2, !"Dwarf Version", i32 4}
+!11 = !{i32 2, !"Debug Info Version", i32 3}
diff --git a/llvm/test/tools/dsymutil/Inputs/module-import-canonical/debug-map-priority.map b/llvm/test/tools/dsymutil/Inputs/module-import-canonical/debug-map-priority.map
new file mode 100644
index 0000000000000..da8ad616e1d89
--- /dev/null
+++ b/llvm/test/tools/dsymutil/Inputs/module-import-canonical/debug-map-priority.map
@@ -0,0 +1,10 @@
+---
+triple: 'x86_64-apple-darwin'
+objects:
+ - filename: 3.o
+ symbols:
+ - { sym: _main3, objAddr: 0x0, binAddr: 0x30000, size: 0x10 }
+ - filename: 1.o
+ symbols:
+ - { sym: _main, objAddr: 0x0, binAddr: 0x10000, size: 0x10 }
+...
diff --git a/llvm/test/tools/dsymutil/X86/module-import-canonical-priority.test b/llvm/test/tools/dsymutil/X86/module-import-canonical-priority.test
new file mode 100644
index 0000000000000..8212912c29e72
--- /dev/null
+++ b/llvm/test/tools/dsymutil/X86/module-import-canonical-priority.test
@@ -0,0 +1,51 @@
+# A clang module holds the definitive description of what it defines, so when
+# several units contribute a copy of the same DW_TAG_module to the type table,
+# the one from the unit built from the .pcm has to win. Otherwise the module DIE
+# consumers read carries an importer's DW_AT_LLVM_include_path.
+#
+# The object file which owns the module unit is not necessarily the first one:
+# it is whichever object file referenced the .pcm first. Here 3.o comes first and
+# contributes a DW_TAG_module for M without referencing M.pcm, and 1.o, which
+# does reference it, comes second. 3.o owns N.pcm instead, so the unit which
+# describes M is not the first module unit of the link either.
+
+RUN: rm -rf %t && mkdir -p %t
+RUN: llc -O0 -mtriple=x86_64-apple-darwin -filetype=obj \
+RUN: -o %t/M.pcm %p/../Inputs/module-import-canonical/M-odr.ll
+RUN: llc -O0 -mtriple=x86_64-apple-darwin -filetype=obj \
+RUN: -o %t/N.pcm %p/../Inputs/module-import-canonical/N-odr.ll
+RUN: llc -O0 -mtriple=x86_64-apple-darwin -filetype=obj \
+RUN: -o %t/1.o %p/../Inputs/module-import-canonical/1.ll
+RUN: llc -O0 -mtriple=x86_64-apple-darwin -filetype=obj \
+RUN: -o %t/3.o %p/../Inputs/module-import-canonical/3.ll
+
+RUN: dsymutil --linker parallel -f \
+RUN: -y %p/../Inputs/module-import-canonical/debug-map-priority.map \
+RUN: -oso-prepend-path %t -o %t/out.dwarf
+RUN: llvm-dwarfdump --verify %t/out.dwarf
+RUN: llvm-dwarfdump --debug-info %t/out.dwarf | FileCheck %s
+
+RUN: dsymutil --linker parallel -f --num-threads 1 \
+RUN: -y %p/../Inputs/module-import-canonical/debug-map-priority.map \
+RUN: -oso-prepend-path %t -o %t/serial.dwarf
+RUN: cmp %t/out.dwarf %t/serial.dwarf
+
+# The type table holds one DW_TAG_module for M, and it is the module unit's copy.
+# Both module units of the link reach the type table, and no third module DIE
+# joins them there.
+
+CHECK: DW_TAG_compile_unit
+CHECK: DW_AT_name {{.*}}"__artificial_type_unit"
+CHECK: 0x0[[MODULE:[0-9a-f]+]]: DW_TAG_module
+CHECK-NEXT: DW_AT_name {{.*}}"M"
+CHECK-NEXT: DW_AT_LLVM_include_path {{.*}}"/tmp/M.framework"
+CHECK: DW_TAG_module
+CHECK-NEXT: DW_AT_name {{.*}}"N"
+CHECK-NEXT: DW_AT_LLVM_include_path {{.*}}"/tmp/N.framework"
+CHECK-NOT: DW_TAG_module
+CHECK: Compile Unit
+
+# An importer whose own DW_TAG_module was uniqued into the type table shares that
+# entry, so its DW_AT_import resolves to the winner.
+
+CHECK: DW_AT_import {{.*}}(0x{{0*}}[[MODULE]] "M")
More information about the llvm-commits
mailing list