[llvm] [DWARFLinker] Keep module type definitions in the parallel linker (PR #215435)

Jonas Devlieghere via llvm-commits llvm-commits at lists.llvm.org
Mon Aug 10 22:51:08 PDT 2026


https://github.com/JDevlieghere updated https://github.com/llvm/llvm-project/pull/215435

>From b6baf97772d9abd4be877935aa3ef9c6acd41f16 Mon Sep 17 00:00:00 2001
From: Jonas Devlieghere <jonas at devlieghere.com>
Date: Mon, 10 Aug 2026 17:00:45 -0700
Subject: [PATCH] [DWARFLinker] Keep module type definitions in the parallel
 linker

A Clang module compile unit carries the only definitions of the types
its module owns. No definition in it is reachable from a live address.
Similar to the classic linker, we need to mark everything as kept.

rdar://184559034
---
 .../Parallel/DependencyTracker.cpp            | 41 +++++++----
 .../dsymutil/X86/module-type-definition.test  | 70 +++++++++++++++++++
 2 files changed, 97 insertions(+), 14 deletions(-)
 create mode 100644 llvm/test/tools/dsymutil/X86/module-type-definition.test

diff --git a/llvm/lib/DWARFLinker/Parallel/DependencyTracker.cpp b/llvm/lib/DWARFLinker/Parallel/DependencyTracker.cpp
index 36712b9d0cfec..124e73b1a15bd 100644
--- a/llvm/lib/DWARFLinker/Parallel/DependencyTracker.cpp
+++ b/llvm/lib/DWARFLinker/Parallel/DependencyTracker.cpp
@@ -105,6 +105,18 @@ void DependencyTracker::verifyKeepChain() {
 #endif
 }
 
+static bool isNamespaceLikeEntry(const DWARFDebugInfoEntry *Entry) {
+  switch (Entry->getTag()) {
+  case dwarf::DW_TAG_compile_unit:
+  case dwarf::DW_TAG_module:
+  case dwarf::DW_TAG_namespace:
+    return true;
+
+  default:
+    return false;
+  }
+}
+
 bool DependencyTracker::resolveDependenciesAndMarkLiveness(
     bool InterCUProcessingStarted, std::atomic<bool> &HasNewInterconnectedCUs) {
   RootEntriesWorkList.clear();
@@ -216,7 +228,19 @@ void DependencyTracker::collectRootsToKeep(
       llvm_unreachable("Called for incorrect DIE");
     } break;
     default:
-      // A forward-declared type nested in a DW_TAG_module is the module's
+      // A module compile unit has no relocations, so liveness analysis never
+      // reaches a type definition that nothing else in the unit references. The
+      // module owns the only copy of those definitions, so keep them.
+      if (Entry.CU->isClangModule() && isNamespaceLikeEntry(Entry.DieEntry) &&
+          dwarf::isType(CurChild->getTag())) {
+        addActionToRootEntriesWorkList(
+            LiveRootWorklistActionTy::MarkTypeEntryRec, ChildEntry,
+            ReferencedBy);
+        break;
+      }
+
+      // An importing unit emits a skeleton of the module it imports, so a
+      // forward-declared type nested in a DW_TAG_module there is the module's
       // record that the name exists, even when no full definition has been
       // emitted. Route it through the type pool: when another CU emits a
       // real definition for the same synthetic name, the existing
@@ -224,7 +248,8 @@ void DependencyTracker::collectRootsToKeep(
       // the definition and drops this declaration at emission time. For
       // non-ODR languages getFinalPlacementForEntry forces PlainDwarf,
       // so the forward decl is kept in place under its module.
-      if (Entry.DieEntry->getTag() == dwarf::DW_TAG_module &&
+      if (!Entry.CU->isClangModule() &&
+          Entry.DieEntry->getTag() == dwarf::DW_TAG_module &&
           dwarf::isType(CurChild->getTag()) &&
           dwarf::toUnsigned(Entry.CU->find(CurChild, dwarf::DW_AT_declaration),
                             0)) {
@@ -314,18 +339,6 @@ void DependencyTracker::setPlainDwarfPlacementRec(
     setPlainDwarfPlacementRec(UnitEntryPairTy{Entry.CU, CurChild});
 }
 
-static bool isNamespaceLikeEntry(const DWARFDebugInfoEntry *Entry) {
-  switch (Entry->getTag()) {
-  case dwarf::DW_TAG_compile_unit:
-  case dwarf::DW_TAG_module:
-  case dwarf::DW_TAG_namespace:
-    return true;
-
-  default:
-    return false;
-  }
-}
-
 bool isAlreadyMarked(const CompileUnit::DIEInfo &Info,
                      CompileUnit::DieOutputPlacement NewPlacement) {
   if (!Info.getKeep())
diff --git a/llvm/test/tools/dsymutil/X86/module-type-definition.test b/llvm/test/tools/dsymutil/X86/module-type-definition.test
new file mode 100644
index 0000000000000..fa661b57cd0af
--- /dev/null
+++ b/llvm/test/tools/dsymutil/X86/module-type-definition.test
@@ -0,0 +1,70 @@
+# A Clang module compile unit has no relocations, so liveness analysis never
+# reaches the type definitions the module owns, even though its compile unit
+# holds the only copy of them. They must be kept anyway, or everything importing
+# the module is left with only forward declarations. Covered for an ODR
+# language, where the definitions land in the artificial type unit, and a
+# non-ODR one, where they stay under the module in plain DWARF.
+
+RUN: dsymutil --linker parallel -f \
+RUN:     -oso-prepend-path=%p/../Inputs/modules-pruning \
+RUN:     -y %p/dummy-debug-map.map -o - \
+RUN:   | llvm-dwarfdump --debug-info - | FileCheck %s --check-prefix=ODR
+
+ODR:      DW_TAG_compile_unit
+ODR:        DW_AT_name {{.*}}"__artificial_type_unit"
+ODR:        DW_TAG_module
+ODR-NEXT:     DW_AT_name {{.*}}"Outer"
+ODR:          DW_TAG_module
+ODR-NEXT:       DW_AT_name {{.*}}"Template"
+ODR:            DW_TAG_namespace
+ODR-NEXT:         DW_AT_name {{.*}}"M"
+ODR:              DW_TAG_structure_type
+ODR:                DW_AT_name {{.*}}"false_type"
+ODR-NEXT:           DW_AT_byte_size {{.*}}(0x01)
+ODR:                DW_TAG_member
+ODR-NEXT:             DW_AT_name {{.*}}"value"
+ODR:                  DW_AT_const_value {{.*}}(0)
+ODR:              DW_TAG_structure_type
+ODR:                DW_AT_name {{.*}}"true_type"
+ODR-NEXT:           DW_AT_byte_size {{.*}}(0x01)
+ODR:                DW_TAG_member
+ODR-NEXT:             DW_AT_name {{.*}}"value"
+ODR:                  DW_AT_const_value {{.*}}(1)
+
+RUN: dsymutil --linker parallel -f \
+RUN:     -oso-prepend-path=%p/../Inputs/modules \
+RUN:     -y %p/dummy-debug-map.map -o - \
+RUN:   | llvm-dwarfdump --debug-info - | FileCheck %s --check-prefix=NOODR
+
+NOODR:      DW_TAG_module
+NOODR-NEXT:   DW_AT_name {{.*}}"Bar"
+NOODR:        DW_TAG_structure_type
+NOODR-NEXT:     DW_AT_name {{.*}}"Bar"
+NOODR-NEXT:     DW_AT_byte_size {{.*}}(0x04)
+NOODR:          DW_TAG_member
+NOODR-NEXT:       DW_AT_name {{.*}}"value"
+
+NOODR:      DW_TAG_module
+NOODR-NEXT:   DW_AT_name {{.*}}"Foo"
+NOODR:        DW_TAG_typedef
+NOODR-NEXT:     DW_AT_type {{.*}}"Bar::Bar"
+NOODR-NEXT:     DW_AT_name {{.*}}"Bar"
+NOODR:        DW_TAG_structure_type
+NOODR-NEXT:     DW_AT_name {{.*}}"S"
+NOODR-NEXT:     DW_AT_byte_size {{.*}}(0x00)
+NOODR:        DW_TAG_structure_type
+NOODR-NEXT:     DW_AT_name {{.*}}"Foo"
+NOODR-NEXT:     DW_AT_byte_size {{.*}}(0x04)
+NOODR:          DW_TAG_member
+NOODR-NEXT:       DW_AT_name {{.*}}"ivar"
+
+# Without ODR merging there is nothing to redirect the typedef above at the
+# definition kept in the module's own unit, so it still resolves to the forward
+# declaration its importer recorded.
+
+NOODR:      DW_TAG_module
+NOODR-NEXT:   DW_AT_name {{.*}}"Bar"
+NOODR:        DW_TAG_structure_type
+NOODR-NEXT:     DW_AT_name {{.*}}"Bar"
+NOODR-NEXT:     DW_AT_byte_size {{.*}}(0x04)
+NOODR-NEXT:     DW_AT_declaration {{.*}}(true)



More information about the llvm-commits mailing list