[llvm] [DWARFLinker] Scope type uniquing on the unit root, not on its tag (PR #219619)
Arcadiy Ivanov via llvm-commits
llvm-commits at lists.llvm.org
Fri Aug 28 20:38:17 PDT 2026
https://github.com/arcivanov created https://github.com/llvm/llvm-project/pull/219619
Depends on #219615.
ODR deduplication gives a type one definition shared by the units that use it. Both backends decided which types were candidates by looking for an enclosing `DW_TAG_compile_unit`, so under a `DW_TAG_partial_unit` root — what `dwz` emits, and what Fedora, RHEL and Debian debuginfo packaging therefore ships — nothing was a candidate, every unit kept its own copy, and the parallel backend produced no artificial type unit at all. That is on exactly the inputs where duplicate definitions are most abundant, since hoisting them into partial units is the whole point of `dwz`.
**Classic.** `getChildDeclContext()` returned a null context for any root that was not a compile unit, and a null context propagates to every descendant, so the entire unit dropped out of uniquing. It now returns the root context for any `dwarf::isUnitType()` tag. The local is retyped from `unsigned` to `dwarf::Tag` because `isUnitType` is overloaded on `uint8_t` as well, and an `unsigned` argument binds that overload silently — comparing tag numbers against `DW_UT_*` codes, which is false for every unit tag and true for `DW_TAG_class_type`.
That half is not only a missed optimization. With the definition excluded from uniquing it could be dropped while another unit still referenced it through `DW_FORM_ref_addr`, and emitting that reference asserted in `DIE::getDebugSectionOffset()` — `"DIE must be owned by a DIEUnit to get its absolute offset"`. The compile unit control on the same input is clean.
**Parallel.** `isNamespaceLikeEntry()` named the tag, and two callers wanted the unit root:
1. `getRootForSpecifiedEntry()` climbed past a partial unit root instead of stopping under it, so a type at unit scope was never offered to the type table.
2. `markParentsAsKeepingChildren()` asked the same predicate, and answering no for a partial unit root enqueued `MarkLiveChildrenRec` on it, which force-kept every type below it in plain DWARF. Fixing only the first leaves each type deduplicated into the artificial type unit *and* still present in its original unit.
Both now call `isUnitRootOrNamespaceLikeEntry()`, which reuses the `CompileUnit::isUnitRootDIE()` added by #219615 rather than spelling the same invariant a second way.
`isNamespaceLikeEntry()` itself stays narrow, and is now documented as deliberately narrow, because `maybeAddReferencedRoots()` uses it on the `DW_AT_import` branch to choose single-entry marking over recursive marking. Widening it there makes a `DW_TAG_imported_unit` of a partial unit mark only the root, so the imported unit loses everything it holds and the import dangles — a regression on the same `dwz` output this change is for. I measured that before splitting the predicates.
Deduplicating types under a partial unit root is also what makes the assertion #219615 fixes reachable on ordinary input, rather than only on the contrived shape that found it. That is the dependency, and it is why the two are ordered rather than independent.
**Tests.** `dwarf5-partial-unit-odr.test` is the new behaviour test: two units each defining a `struct S` at unit scope and an unrelated `S` in `namespace ns`, asserting exactly two definitions survive with all four references pointing at them, both backends, with a `DW_TAG_compile_unit` control and a `--no-odr-deduplication` arm. `dwarf5-partial-unit-imported-unit.test` is a guard rather than a repro — it passes before and after, and fails only if the two predicates are merged back together; there was previously no `DW_TAG_imported_unit` coverage anywhere in `llvm-dwarfutil` or `dsymutil`. `dwarf5-partial-unit-types-only.test` is amended: with deduplication reaching partial units its two links are now byte identical, so the block pinning the old asymmetry becomes `cmp`, and the classic backend is linked there now that it no longer aborts.
One expectation worth stating rather than discovering: on the classic side a partial unit root used to yield a null context that made its whole subtree skip `getChildDeclContext` entirely. On `dwz` output, where most types live under partial units, `DeclContextTree` therefore goes from nearly empty to a full tree. That is the fix doing its job, and it is repaid in output size, but it is a real change in peak memory for `llvm-dwarfutil` on those inputs.
Fixes #219393.
>From be6e72d198c28c4321ca528fbe946e4d26229059 Mon Sep 17 00:00:00 2001
From: Arcadiy Ivanov <arcadiy at ivanov.biz>
Date: Fri, 28 Aug 2026 22:31:55 -0400
Subject: [PATCH 1/2] [DWARFLinker][Parallel] Identify a unit root by position,
not by tag
cloneDIE() has two gates that must recognize a unit's root DIE. The root
is never assigned a type entry, so it must not be cloned into the
artificial type unit; and because it is not cloned there, the recursion
into its type children has to be enabled by a separate test. Both gates
asked whether the tag was DW_TAG_compile_unit.
A DW_TAG_partial_unit root answers no to both. That is the unit dwz
emits, so it arrives through any Fedora, RHEL or Debian debuginfo
package, and it reaches the linker on the same path a compile unit root
does. Marking a type for the artificial type unit sets KeepTypeChildren
on every ancestor of that type, unit root included, and
needToPlaceInTypeTable() is true for anything carrying that flag, so the
first gate let the partial unit root through and
createTypeDIEandCloneAttributes() asked for the type entry that no unit
root is ever assigned - an assertion failure on Entry != nullptr with
assertions enabled, a null dereference without them. Where the root's
only kept content is types, the second gate then dropped the whole
subtree after its type names had already been registered, which fails
later at patch application with "No data for type <name>".
Both gates now test the DIE index instead of the tag. The root is always
at index zero in DWARFUnit's DIE array whatever its tag, which is the
invariant the tag comparison was standing in for. The producer side is
already tag-agnostic - getTypeDeduplicationCandidate() returns nullopt
for every unit tag and assignTypeNames() names only children of
getDebugInfoEntry(0) - so this makes the consumer match.
Only the parallel backend is affected.
Fixes #219613.
Assisted-by: Claude Code
---
.../Parallel/DWARFLinkerCompileUnit.cpp | 6 +-
.../Parallel/DWARFLinkerCompileUnit.h | 5 +
.../X86/dwarf5-partial-unit-namespace.test | 178 ++++++++++++++++++
.../X86/dwarf5-partial-unit-types-only.test | 168 +++++++++++++++++
4 files changed, 353 insertions(+), 4 deletions(-)
create mode 100644 llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-namespace.test
create mode 100644 llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-types-only.test
diff --git a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp
index 4138a9fd69aca..126cac340a49d 100644
--- a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp
+++ b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp
@@ -1419,8 +1419,7 @@ std::pair<DIE *, TypeEntry *> CompileUnit::cloneDIE(
bool NeedToClonePlainDIE = Info.needToKeepInPlainDwarf();
bool NeedToCloneTypeDIE =
- (InputDieEntry->getTag() != dwarf::DW_TAG_compile_unit) &&
- Info.needToPlaceInTypeTable();
+ !isUnitRootDIE(InputDieIdx) && Info.needToPlaceInTypeTable();
std::pair<DIE *, TypeEntry *> ClonedDIE;
DIEGenerator PlainDIEGenerator(Allocator, *this);
@@ -1449,8 +1448,7 @@ std::pair<DIE *, TypeEntry *> CompileUnit::cloneDIE(
(ClonedDIE.first && Info.getKeepPlainChildren());
bool HasTypeChildrenToClone =
- ((ClonedDIE.second ||
- InputDieEntry->getTag() == dwarf::DW_TAG_compile_unit) &&
+ ((ClonedDIE.second || isUnitRootDIE(InputDieIdx)) &&
Info.getKeepTypeChildren());
// Recursively clone children.
diff --git a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h
index c8c1b8822aa64..70b90727f4df4 100644
--- a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h
+++ b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h
@@ -335,6 +335,11 @@ class alignas(8) CompileUnit : public DwarfUnit {
///
/// @{
+ /// \p DieIdx index of the DIE.
+ /// \returns true if the DIE is the unit's root, which is always at index
+ /// zero, whatever tag it carries.
+ static bool isUnitRootDIE(uint32_t DieIdx) { return DieIdx == 0; }
+
/// \p Idx index of the DIE.
/// \returns DieInfo descriptor.
DIEInfo &getDIEInfo(unsigned Idx) { return DieInfoArray[Idx]; }
diff --git a/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-namespace.test b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-namespace.test
new file mode 100644
index 0000000000000..357d829a34b2c
--- /dev/null
+++ b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-namespace.test
@@ -0,0 +1,178 @@
+## A DW_TAG_partial_unit root - what dwz emits, and what Fedora, RHEL and
+## Debian debuginfo packaging therefore ships - was kept out of the artificial
+## type unit by nothing but a test on its tag, so it was cloned as a type DIE
+## and asked for the type entry that no unit root is ever assigned. Reaching
+## that point needs a scope between the type and the unit root, because the
+## walk that looks for a type's enclosing scope stops at a namespace but runs
+## past a unit root. A DW_TAG_compile_unit root is linked alongside as the
+## control.
+##
+## namespace ns {
+## struct S { int m1; };
+## }
+##
+## ns::S foo1() { ... }
+
+# RUN: yaml2obj %s -DROOT=DW_TAG_compile_unit -DUNITTYPE=DW_UT_compile -o %t.cu.o
+# RUN: llvm-dwarfutil %t.cu.o %t.cu.classic
+# RUN: llvm-dwarfdump --debug-info %t.cu.classic | FileCheck %s --check-prefixes=CLASSIC,CLASSIC-CU
+# RUN: llvm-dwarfutil --linker parallel %t.cu.o %t.cu.parallel
+# RUN: llvm-dwarfdump --debug-info %t.cu.parallel | FileCheck %s --check-prefixes=PARALLEL,PARALLEL-CU
+
+# RUN: yaml2obj %s -DROOT=DW_TAG_partial_unit -DUNITTYPE=DW_UT_partial -o %t.pu.o
+# RUN: llvm-dwarfutil %t.pu.o %t.pu.classic
+# RUN: llvm-dwarfdump --debug-info %t.pu.classic | FileCheck %s --check-prefixes=CLASSIC,CLASSIC-PU
+# RUN: llvm-dwarfutil --linker parallel %t.pu.o %t.pu.parallel
+# RUN: llvm-dwarfdump --debug-info %t.pu.parallel | FileCheck %s --check-prefixes=PARALLEL,PARALLEL-PU
+
+## The classic backend leaves the type where it found it, and the root tag
+## survives linking. llvm-dwarfdump qualifies a type name with the names of its
+## enclosing scopes and a partial unit root is one, which a compile unit root
+## is not, so the type is printed as "U01::ns::S" for the partial unit run and
+## as "ns::S" for the control. The reference target is the same DIE either way.
+# CLASSIC-CU: DW_TAG_compile_unit
+# CLASSIC-PU: DW_TAG_partial_unit
+# CLASSIC: DW_TAG_subprogram
+# CLASSIC-NEXT: DW_AT_name ("foo1")
+# CLASSIC-CU: DW_AT_type (0x00000000[[S:[0-9a-f]{8}]] "ns::S")
+# CLASSIC-PU: DW_AT_type (0x00000000[[S:[0-9a-f]{8}]] "U01::ns::S")
+# CLASSIC: DW_TAG_namespace
+# CLASSIC-NEXT: DW_AT_name ("ns")
+# CLASSIC: 0x[[S]]: DW_TAG_structure_type
+# CLASSIC-NEXT: DW_AT_name ("S")
+# CLASSIC: DW_TAG_member
+# CLASSIC-NEXT: DW_AT_name ("m1")
+
+## The parallel backend puts the type in the artificial type unit, under a copy
+## of the namespace that held it, and the unit's reference resolves there. The
+## unit root itself stays out of that unit, which is what this pins: it has no
+## name of its own to be deduplicated under, and cloning it as a type DIE is
+## what used to crash.
+# PARALLEL: DW_AT_name ("__artificial_type_unit")
+# PARALLEL: DW_TAG_namespace
+# PARALLEL-NEXT: DW_AT_name ("ns")
+# PARALLEL: 0x[[S:[0-9a-f]{8}]]: DW_TAG_structure_type
+# PARALLEL-NEXT: DW_AT_name ("S")
+# PARALLEL: DW_TAG_member
+# PARALLEL-NEXT: DW_AT_name ("m1")
+# PARALLEL-CU: DW_TAG_compile_unit
+# PARALLEL-PU: DW_TAG_partial_unit
+# PARALLEL-NEXT: DW_AT_producer ("by_hand")
+# PARALLEL-NEXT: DW_AT_name ("U01")
+# PARALLEL: DW_TAG_subprogram
+# PARALLEL-NEXT: DW_AT_name ("foo1")
+# PARALLEL: DW_AT_type (0x00000000[[S]] "ns::S")
+
+## One DWARFv5 unit with a function in .text returning struct S, which is
+## defined in namespace ns. The root carries its own DW_AT_str_offsets_base, so
+## the strings the linker rewrites into DW_FORM_strx stay resolvable on output;
+## the linker does not synthesize that attribute for a partial unit root by
+## itself.
+--- !ELF
+FileHeader:
+ Class: ELFCLASS64
+ Data: ELFDATA2LSB
+ Type: ET_REL
+ Machine: EM_X86_64
+Sections:
+ - Name: .text
+ Type: SHT_PROGBITS
+ Flags: [ SHF_ALLOC, SHF_EXECINSTR ]
+ Address: 0x1130
+ Size: 0x10
+ - Name: .debug_str_offsets
+ Type: SHT_PROGBITS
+ Flags: [ ]
+ Content: "0400000005000000"
+DWARF:
+ debug_abbrev:
+ - Table:
+ - Tag: [[ROOT]]
+ Children: DW_CHILDREN_yes
+ Attributes:
+ - Attribute: DW_AT_producer
+ Form: DW_FORM_string
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Attribute: DW_AT_language
+ Form: DW_FORM_data2
+ - Attribute: DW_AT_low_pc
+ Form: DW_FORM_addr
+ - Attribute: DW_AT_high_pc
+ Form: DW_FORM_data8
+ - Attribute: DW_AT_str_offsets_base
+ Form: DW_FORM_sec_offset
+ - Tag: DW_TAG_subprogram
+ Children: DW_CHILDREN_no
+ Attributes:
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Attribute: DW_AT_low_pc
+ Form: DW_FORM_addr
+ - Attribute: DW_AT_high_pc
+ Form: DW_FORM_data8
+ - Attribute: DW_AT_type
+ Form: DW_FORM_ref4
+ - Tag: DW_TAG_namespace
+ Children: DW_CHILDREN_yes
+ Attributes:
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Tag: DW_TAG_structure_type
+ Children: DW_CHILDREN_yes
+ Attributes:
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Attribute: DW_AT_byte_size
+ Form: DW_FORM_data1
+ - Tag: DW_TAG_member
+ Children: DW_CHILDREN_no
+ Attributes:
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Attribute: DW_AT_type
+ Form: DW_FORM_ref4
+ - Attribute: DW_AT_data_member_location
+ Form: DW_FORM_data1
+ - Tag: DW_TAG_base_type
+ Children: DW_CHILDREN_no
+ Attributes:
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ debug_info:
+ - Version: 5
+ UnitType: [[UNITTYPE]]
+ Entries:
+ - AbbrCode: 1
+ Values:
+ - CStr: by_hand
+ - CStr: U01
+ - Value: 0x04
+ - Value: 0x1130
+ - Value: 0x10
+ - Value: 0x8
+ - AbbrCode: 2
+ Values:
+ - CStr: foo1
+ - Value: 0x1130
+ - Value: 0x10
+ - Value: 0x4d
+ - AbbrCode: 3
+ Values:
+ - CStr: ns
+ - AbbrCode: 4
+ Values:
+ - CStr: S
+ - Value: 0x8
+ - AbbrCode: 5
+ Values:
+ - CStr: m1
+ - Value: 0x5c
+ - Value: 0x0
+ - AbbrCode: 0
+ - AbbrCode: 0
+ - AbbrCode: 6
+ Values:
+ - CStr: int
+ - AbbrCode: 0
+...
diff --git a/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-types-only.test b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-types-only.test
new file mode 100644
index 0000000000000..312b8d0e1921c
--- /dev/null
+++ b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-types-only.test
@@ -0,0 +1,168 @@
+## A unit root is never assigned a type entry, so it is never cloned into the
+## artificial type unit and the recursion into its type children is allowed by
+## a separate test - which named DW_TAG_compile_unit only. A
+## DW_TAG_partial_unit root holding types and no code, the unit dwz produces to
+## carry the definitions it hoisted out of the compile units, therefore reached
+## neither branch and its subtree was dropped after the type name had already
+## been registered. A DW_TAG_compile_unit root is linked alongside as the
+## control.
+##
+## U01: namespace ns { struct S {}; }
+## U02: ns::S foo1() { ... } // DW_FORM_ref_addr reference into U01
+##
+## The classic backend is not linked here. It fails on this input for an
+## unrelated reason, a cross-unit reference to a DIE it never gave an output
+## unit, which is a separate defect from the one this test covers.
+
+# RUN: yaml2obj %s -DROOT=DW_TAG_compile_unit -DUNITTYPE=DW_UT_compile -o %t.cu.o
+# RUN: llvm-dwarfutil --linker parallel %t.cu.o %t.cu.out
+# RUN: llvm-dwarfdump --debug-info %t.cu.out | FileCheck %s --implicit-check-not='DW_AT_name ("U01")'
+
+# RUN: yaml2obj %s -DROOT=DW_TAG_partial_unit -DUNITTYPE=DW_UT_partial -o %t.pu.o
+# RUN: llvm-dwarfutil --linker parallel %t.pu.o %t.pu.out
+# RUN: llvm-dwarfdump --debug-info %t.pu.out | FileCheck %s --check-prefixes=CHECK,CHECK-PU
+
+## The type reaches the artificial type unit under a copy of the namespace that
+## held it, and the second unit's reference resolves into the type unit rather
+## than to a dropped DIE.
+# CHECK: DW_AT_name ("__artificial_type_unit")
+# CHECK: DW_TAG_namespace
+# CHECK-NEXT: DW_AT_name ("ns")
+# CHECK: 0x[[S:[0-9a-f]{8}]]: DW_TAG_structure_type
+# CHECK-NEXT: DW_AT_name ("S")
+# CHECK-NEXT: DW_AT_byte_size (0x08)
+
+## The first unit is emptied either way, since its only child moved to the type
+## unit, but the two roots are then treated differently: a compile unit with
+## nothing left is dropped whole, while a partial unit root is still emitted.
+## That difference is the linker's existing behaviour for a unit with no live
+## contents, not something this fix introduces, so it is pinned rather than
+## made uniform.
+# CHECK-PU: DW_TAG_partial_unit
+# CHECK-PU-NEXT: DW_AT_producer ("by_hand")
+# CHECK-PU-NEXT: DW_AT_name ("U01")
+# CHECK: DW_TAG_compile_unit
+# CHECK-NEXT: DW_AT_producer ("by_hand")
+# CHECK-NEXT: DW_AT_name ("U02")
+# CHECK: DW_TAG_subprogram
+# CHECK-NEXT: DW_AT_name ("foo1")
+# CHECK: DW_AT_type (0x00000000[[S]] "ns::S")
+
+## Two DWARFv5 units. The first defines struct S in namespace ns and has no
+## code of its own; the second has the only function, which returns S through a
+## DW_FORM_ref_addr reference into the first. S is deliberately memberless, so
+## that the first unit holds nothing the linker would keep in plain DWARF - a
+## member would pull in a DW_TAG_base_type, which is always kept, and that
+## alone would give the root plain children to descend for. Each root carries
+## its own DW_AT_str_offsets_base, so the strings the linker rewrites into
+## DW_FORM_strx stay resolvable on output; the linker does not synthesize that
+## attribute for a partial unit root by itself.
+--- !ELF
+FileHeader:
+ Class: ELFCLASS64
+ Data: ELFDATA2LSB
+ Type: ET_REL
+ Machine: EM_X86_64
+Sections:
+ - Name: .text
+ Type: SHT_PROGBITS
+ Flags: [ SHF_ALLOC, SHF_EXECINSTR ]
+ Address: 0x1130
+ Size: 0x10
+ - Name: .debug_str_offsets
+ Type: SHT_PROGBITS
+ Flags: [ ]
+ Content: "0400000005000000"
+DWARF:
+ debug_abbrev:
+ - ID: 0
+ Table:
+ - Tag: [[ROOT]]
+ Children: DW_CHILDREN_yes
+ Attributes:
+ - Attribute: DW_AT_producer
+ Form: DW_FORM_string
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Attribute: DW_AT_language
+ Form: DW_FORM_data2
+ - Attribute: DW_AT_str_offsets_base
+ Form: DW_FORM_sec_offset
+ - Tag: DW_TAG_namespace
+ Children: DW_CHILDREN_yes
+ Attributes:
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Tag: DW_TAG_structure_type
+ Children: DW_CHILDREN_no
+ Attributes:
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Attribute: DW_AT_byte_size
+ Form: DW_FORM_data1
+ - Tag: DW_TAG_compile_unit
+ Children: DW_CHILDREN_yes
+ Attributes:
+ - Attribute: DW_AT_producer
+ Form: DW_FORM_string
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Attribute: DW_AT_language
+ Form: DW_FORM_data2
+ - Attribute: DW_AT_low_pc
+ Form: DW_FORM_addr
+ - Attribute: DW_AT_high_pc
+ Form: DW_FORM_data8
+ - Attribute: DW_AT_str_offsets_base
+ Form: DW_FORM_sec_offset
+ - Tag: DW_TAG_subprogram
+ Children: DW_CHILDREN_no
+ Attributes:
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Attribute: DW_AT_low_pc
+ Form: DW_FORM_addr
+ - Attribute: DW_AT_high_pc
+ Form: DW_FORM_data8
+ - Attribute: DW_AT_type
+ Form: DW_FORM_ref_addr
+ debug_info:
+ - Version: 5
+ UnitType: [[UNITTYPE]]
+ AbbrevTableID: 0
+ Entries:
+ - AbbrCode: 1
+ Values:
+ - CStr: by_hand
+ - CStr: U01
+ - Value: 0x04
+ - Value: 0x8
+ - AbbrCode: 2
+ Values:
+ - CStr: ns
+ - AbbrCode: 3
+ Values:
+ - CStr: S
+ - Value: 0x8
+ - AbbrCode: 0
+ - AbbrCode: 0
+ - Version: 5
+ UnitType: DW_UT_compile
+ AbbrevTableID: 0
+ Entries:
+ - AbbrCode: 4
+ Values:
+ - CStr: by_hand
+ - CStr: U02
+ - Value: 0x04
+ - Value: 0x1130
+ - Value: 0x10
+ - Value: 0x8
+ - AbbrCode: 5
+ Values:
+ - CStr: foo1
+ - Value: 0x1130
+ - Value: 0x10
+ - Value: 0x23
+ - AbbrCode: 0
+...
>From aea0d3519421d7ce8508bf4c5039306df0f0897f Mon Sep 17 00:00:00 2001
From: Arcadiy Ivanov <arcadiy at ivanov.biz>
Date: Fri, 28 Aug 2026 23:37:48 -0400
Subject: [PATCH 2/2] [DWARFLinker] Scope type uniquing on the unit root, not
on its tag
ODR deduplication gives a type one definition shared by the units that
use it. Both backends decided which types were candidates by looking for
an enclosing DW_TAG_compile_unit, so under a DW_TAG_partial_unit root -
what dwz emits, and what Fedora, RHEL and Debian debuginfo packaging
therefore ships - nothing was a candidate, every unit kept its own copy,
and the parallel backend produced no artificial type unit at all. That
is on exactly the inputs where duplicate definitions are most abundant,
since hoisting them into partial units is what dwz does.
In the classic backend getChildDeclContext() returned a null context for
any root that was not a compile unit, and a null context propagates to
every descendant, so the whole unit dropped out of uniquing. It now
returns the root context for any dwarf::isUnitType() tag. The local is
retyped from unsigned to dwarf::Tag because isUnitType is overloaded on
uint8_t as well, and an unsigned argument would silently bind the
uint8_t overload and compare tag numbers against DW_UT_* codes.
That half is not only a missed optimization. With the definition
excluded from uniquing it could be dropped while another unit still
referenced it through DW_FORM_ref_addr, and emitting that reference
asserted in DIE::getDebugSectionOffset() - "DIE must be owned by a
DIEUnit to get its absolute offset". The compile unit control on the
same input is clean.
In the parallel backend isNamespaceLikeEntry() named the tag, so
getRootForSpecifiedEntry() climbed past a partial unit root instead of
stopping under it and the type was never offered to the type table.
markParentsAsKeepingChildren() asked the same predicate, and answering
no for a partial unit root enqueued MarkLiveChildrenRec on it, which
kept every type below it in plain DWARF as well. Both now ask
isUnitRootOrNamespaceLikeEntry(), which reuses CompileUnit's existing
isUnitRootDIE().
isNamespaceLikeEntry() itself stays narrow, and is documented as such,
because maybeAddReferencedRoots() uses it on the DW_AT_import branch to
choose single-entry over recursive marking. Widening it there makes a
DW_TAG_imported_unit of a partial unit mark only the root, so the
imported unit loses everything it holds and the import dangles.
dwarf5-partial-unit-imported-unit.test guards that; it passes before and
after this change and fails if the two predicates are merged.
Fixes #219393.
Assisted-by: Claude Code
---
.../Classic/DWARFLinkerDeclContext.cpp | 17 +-
.../Parallel/DependencyTracker.cpp | 21 +-
.../dwarf5-partial-unit-imported-unit.test | 162 +++++++++
.../ELF/X86/dwarf5-partial-unit-odr.test | 334 ++++++++++++++++++
.../X86/dwarf5-partial-unit-types-only.test | 79 +++--
5 files changed, 578 insertions(+), 35 deletions(-)
create mode 100644 llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-imported-unit.test
create mode 100644 llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-odr.test
diff --git a/llvm/lib/DWARFLinker/Classic/DWARFLinkerDeclContext.cpp b/llvm/lib/DWARFLinker/Classic/DWARFLinkerDeclContext.cpp
index bfec342526ea4..a965416ea8ec1 100644
--- a/llvm/lib/DWARFLinker/Classic/DWARFLinkerDeclContext.cpp
+++ b/llvm/lib/DWARFLinker/Classic/DWARFLinkerDeclContext.cpp
@@ -60,22 +60,31 @@ bool DeclContext::setLastSeenDIE(CompileUnit &U, const DWARFDie &Die) {
PointerIntPair<DeclContext *, 1>
DeclContextTree::getChildDeclContext(DeclContext &Context, const DWARFDie &DIE,
CompileUnit &U, bool InClangModule) {
- unsigned Tag = DIE.getTag();
+ dwarf::Tag Tag = DIE.getTag();
// FIXME: dsymutil-classic compat: We should bail out here if we
// have a specification or an abstract_origin. We will get the
// parent context wrong here.
+ // A unit root is the root of the unit's context tree, whatever tag it
+ // carries: a null context here would propagate to every descendant and take
+ // the whole unit out of uniquing. The tag recognizes the root because this
+ // API is handed a DIE rather than its index, and it degrades safely - a
+ // nested DIE carrying a unit tag would merely be transparent to its
+ // enclosing context.
+ if (dwarf::isUnitType(Tag))
+ return PointerIntPair<DeclContext *, 1>(&Context);
+
switch (Tag) {
default:
// By default stop gathering child contexts.
return PointerIntPair<DeclContext *, 1>(nullptr);
case dwarf::DW_TAG_module:
break;
- case dwarf::DW_TAG_compile_unit:
- return PointerIntPair<DeclContext *, 1>(&Context);
case dwarf::DW_TAG_subprogram:
- // Do not unique anything inside CU local functions.
+ // Do not unique anything inside CU local functions. The root context is
+ // default constructed, and DeclContext's Tag defaults to
+ // DW_TAG_compile_unit, so this covers a partial unit root too.
if ((Context.getTag() == dwarf::DW_TAG_namespace ||
Context.getTag() == dwarf::DW_TAG_compile_unit) &&
!dwarf::toUnsigned(DIE.find(dwarf::DW_AT_external), 0))
diff --git a/llvm/lib/DWARFLinker/Parallel/DependencyTracker.cpp b/llvm/lib/DWARFLinker/Parallel/DependencyTracker.cpp
index 669ea2ceb16c9..5e001be605aa4 100644
--- a/llvm/lib/DWARFLinker/Parallel/DependencyTracker.cpp
+++ b/llvm/lib/DWARFLinker/Parallel/DependencyTracker.cpp
@@ -106,6 +106,11 @@ void DependencyTracker::verifyKeepChain() {
#endif
}
+/// \returns true if \p Entry names a scope a DW_AT_import may refer to, whose
+/// contents that import then pulls in wholesale. Deliberately narrow: widening
+/// it to the other unit root tags makes a DW_TAG_imported_unit of a partial
+/// unit mark only the root and drop everything the imported unit holds. See
+/// dwarf5-partial-unit-imported-unit.test.
static bool isNamespaceLikeEntry(const DWARFDebugInfoEntry *Entry) {
switch (Entry->getTag()) {
case dwarf::DW_TAG_compile_unit:
@@ -118,6 +123,14 @@ static bool isNamespaceLikeEntry(const DWARFDebugInfoEntry *Entry) {
}
}
+/// \returns true if the DIE at \p Idx bounds the scope of the DIEs below it.
+/// The unit root bounds that scope whatever tag it carries, and
+/// DW_TAG_partial_unit - what dwz emits - is not namespace-like.
+static bool isUnitRootOrNamespaceLikeEntry(uint32_t Idx,
+ const DWARFDebugInfoEntry *Entry) {
+ return CompileUnit::isUnitRootDIE(Idx) || isNamespaceLikeEntry(Entry);
+}
+
bool DependencyTracker::resolveDependenciesAndMarkLiveness(
bool InterCUProcessingStarted, std::atomic<bool> &HasNewInterconnectedCUs) {
RootEntriesWorkList.clear();
@@ -482,7 +495,8 @@ void DependencyTracker::markParentsAsKeepingChildren(
bool AddToWorklist = !isAlreadyMarked(
ParentInfo, CompileUnit::DieOutputPlacement::TypeTable);
ParentInfo.setKeepTypeChildren();
- if (AddToWorklist && !isNamespaceLikeEntry(ParentEntry)) {
+ if (AddToWorklist &&
+ !isUnitRootOrNamespaceLikeEntry(*ParentIdx, ParentEntry)) {
addActionToRootEntriesWorkList(
LiveRootWorklistActionTy::MarkTypeChildrenRec,
UnitEntryPairTy{Entry.CU, ParentEntry}, std::nullopt);
@@ -497,7 +511,8 @@ void DependencyTracker::markParentsAsKeepingChildren(
bool AddToWorklist = !isAlreadyMarked(
ParentInfo, CompileUnit::DieOutputPlacement::PlainDwarf);
ParentInfo.setKeepPlainChildren();
- if (AddToWorklist && !isNamespaceLikeEntry(ParentEntry)) {
+ if (AddToWorklist &&
+ !isUnitRootOrNamespaceLikeEntry(*ParentIdx, ParentEntry)) {
addActionToRootEntriesWorkList(
LiveRootWorklistActionTy::MarkLiveChildrenRec,
UnitEntryPairTy{Entry.CU, ParentEntry}, std::nullopt);
@@ -952,7 +967,7 @@ DependencyTracker::getRootForSpecifiedEntry(UnitEntryPairTy Entry) {
const DWARFDebugInfoEntry *ParentEntry =
Result.CU->getDebugInfoEntry(*ParentIdx);
- if (isNamespaceLikeEntry(ParentEntry))
+ if (isUnitRootOrNamespaceLikeEntry(*ParentIdx, ParentEntry))
break;
Result.DieEntry = ParentEntry;
} while (true);
diff --git a/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-imported-unit.test b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-imported-unit.test
new file mode 100644
index 0000000000000..7ee174105c71b
--- /dev/null
+++ b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-imported-unit.test
@@ -0,0 +1,162 @@
+## Deciding which types are deduplication candidates means asking where the
+## scope holding a type ends, and a unit root ends it whatever tag it carries.
+## Answering that with "is this entry namespace-like", which the linker already
+## had, would also answer a second question asked of the same predicate: how
+## much of an imported unit a DW_TAG_imported_unit drags in. A partial unit is
+## what dwz emits and what DW_TAG_imported_unit names, so this test pins the
+## import path against the scope change.
+##
+## U01: namespace ns { struct S {}; }
+## U02: // DW_TAG_imported_unit of U01
+## void foo1() { ... }
+##
+## Nothing in U01 is referenced from U02, so the import is the only thing
+## keeping it alive and its contents disappear if the import stops recursing.
+## This test passes both before and after the fix: it exists to fail if the two
+## predicates are ever merged back together.
+##
+## Only the parallel backend is linked. The classic backend aborts on
+## DW_TAG_imported_unit in fixupForwardReferences() with "Offset being queried
+## before it's been computed", for a DW_TAG_compile_unit root just as much as
+## for a DW_TAG_partial_unit one, which makes it a separate defect from
+## anything about partial units.
+
+# RUN: yaml2obj %s -o %t.o
+# RUN: llvm-dwarfutil --linker parallel %t.o %t.out
+# RUN: llvm-dwarfdump --debug-info %t.out | FileCheck %s
+
+# CHECK: 0x[[U01:[0-9a-f]{8}]]: DW_TAG_partial_unit
+# CHECK-NEXT: DW_AT_producer ("by_hand")
+# CHECK-NEXT: DW_AT_name ("U01")
+# CHECK: DW_TAG_namespace
+# CHECK-NEXT: DW_AT_name ("ns")
+# CHECK: DW_TAG_structure_type
+# CHECK-NEXT: DW_AT_name ("S")
+# CHECK: DW_TAG_compile_unit
+# CHECK-NEXT: DW_AT_producer ("by_hand")
+# CHECK-NEXT: DW_AT_name ("U02")
+# CHECK: DW_TAG_imported_unit
+# CHECK-NEXT: DW_AT_import (0x00000000[[U01]] "U01")
+# CHECK: DW_TAG_subprogram
+# CHECK-NEXT: DW_AT_name ("foo1")
+
+## Two DWARFv5 units. The first is a partial unit defining struct S in
+## namespace ns and holding no code; the second is a compile unit that imports
+## it and has the only function. Each root carries its own
+## DW_AT_str_offsets_base, so the strings the linker rewrites into
+## DW_FORM_strx stay resolvable on output; the linker does not synthesize that
+## attribute for a partial unit root by itself.
+--- !ELF
+FileHeader:
+ Class: ELFCLASS64
+ Data: ELFDATA2LSB
+ Type: ET_REL
+ Machine: EM_X86_64
+Sections:
+ - Name: .text
+ Type: SHT_PROGBITS
+ Flags: [ SHF_ALLOC, SHF_EXECINSTR ]
+ Address: 0x1130
+ Size: 0x10
+ - Name: .debug_str_offsets
+ Type: SHT_PROGBITS
+ Flags: [ ]
+ Content: "0400000005000000"
+DWARF:
+ debug_abbrev:
+ - ID: 0
+ Table:
+ - Tag: DW_TAG_partial_unit
+ Children: DW_CHILDREN_yes
+ Attributes:
+ - Attribute: DW_AT_producer
+ Form: DW_FORM_string
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Attribute: DW_AT_language
+ Form: DW_FORM_data2
+ - Attribute: DW_AT_str_offsets_base
+ Form: DW_FORM_sec_offset
+ - Tag: DW_TAG_namespace
+ Children: DW_CHILDREN_yes
+ Attributes:
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Tag: DW_TAG_structure_type
+ Children: DW_CHILDREN_no
+ Attributes:
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Attribute: DW_AT_byte_size
+ Form: DW_FORM_data1
+ - Tag: DW_TAG_compile_unit
+ Children: DW_CHILDREN_yes
+ Attributes:
+ - Attribute: DW_AT_producer
+ Form: DW_FORM_string
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Attribute: DW_AT_language
+ Form: DW_FORM_data2
+ - Attribute: DW_AT_low_pc
+ Form: DW_FORM_addr
+ - Attribute: DW_AT_high_pc
+ Form: DW_FORM_data8
+ - Attribute: DW_AT_str_offsets_base
+ Form: DW_FORM_sec_offset
+ - Tag: DW_TAG_subprogram
+ Children: DW_CHILDREN_no
+ Attributes:
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Attribute: DW_AT_low_pc
+ Form: DW_FORM_addr
+ - Attribute: DW_AT_high_pc
+ Form: DW_FORM_data8
+ - Tag: DW_TAG_imported_unit
+ Children: DW_CHILDREN_no
+ Attributes:
+ - Attribute: DW_AT_import
+ Form: DW_FORM_ref_addr
+ debug_info:
+ - Version: 5
+ UnitType: DW_UT_partial
+ AbbrevTableID: 0
+ Entries:
+ - AbbrCode: 1
+ Values:
+ - CStr: by_hand
+ - CStr: U01
+ - Value: 0x04
+ - Value: 0x8
+ - AbbrCode: 2
+ Values:
+ - CStr: ns
+ - AbbrCode: 3
+ Values:
+ - CStr: S
+ - Value: 0x8
+ - AbbrCode: 0
+ - AbbrCode: 0
+ - Version: 5
+ UnitType: DW_UT_compile
+ AbbrevTableID: 0
+ Entries:
+ - AbbrCode: 4
+ Values:
+ - CStr: by_hand
+ - CStr: U02
+ - Value: 0x04
+ - Value: 0x1130
+ - Value: 0x10
+ - Value: 0x8
+ - AbbrCode: 6
+ Values:
+ - Value: 0x0c
+ - AbbrCode: 5
+ Values:
+ - CStr: foo1
+ - Value: 0x1130
+ - Value: 0x10
+ - AbbrCode: 0
+...
diff --git a/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-odr.test b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-odr.test
new file mode 100644
index 0000000000000..f2b82f56f4f41
--- /dev/null
+++ b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-odr.test
@@ -0,0 +1,334 @@
+## ODR deduplication gives every type a single definition shared by the units
+## that use it. Both backends decided which types were candidates by walking up
+## to the enclosing DW_TAG_compile_unit, so under a DW_TAG_partial_unit root -
+## what dwz emits, and what Fedora, RHEL and Debian debuginfo packaging
+## therefore ships - no type was ever a candidate and every unit kept its own
+## copy, on exactly the input where duplicate definitions are most abundant.
+##
+## The input holds two units that each define a struct S at unit scope and a
+## second, unrelated struct S inside namespace ns, and each returns both from a
+## function. A deduplicated link therefore keeps exactly two definitions, one
+## per scope, and points all four references at them; a link that stopped
+## distinguishing scopes would keep one, and a link that deduplicates nothing
+## keeps four. A DW_TAG_compile_unit root is linked alongside as the control:
+## the partial unit output below is what the compile unit output already was.
+
+## DW_TAG_compile_unit roots.
+# RUN: yaml2obj %s -DROOT=DW_TAG_compile_unit -DUNITTYPE=DW_UT_compile -o %t.cu.o
+# RUN: llvm-dwarfutil %t.cu.o %t.cu.classic
+# RUN: llvm-dwarfdump --debug-info %t.cu.classic | FileCheck %s --check-prefixes=CLASSIC,CLASSIC-CU --implicit-check-not=DW_TAG_structure_type
+# RUN: llvm-dwarfutil --linker parallel %t.cu.o %t.cu.parallel
+# RUN: llvm-dwarfdump --debug-info %t.cu.parallel | FileCheck %s --check-prefix=PARALLEL --implicit-check-not=DW_TAG_structure_type
+
+## DW_TAG_partial_unit roots.
+# RUN: yaml2obj %s -DROOT=DW_TAG_partial_unit -DUNITTYPE=DW_UT_partial -o %t.pu.o
+# RUN: llvm-dwarfutil %t.pu.o %t.pu.classic
+# RUN: llvm-dwarfdump --debug-info %t.pu.classic | FileCheck %s --check-prefixes=CLASSIC,CLASSIC-PU --implicit-check-not=DW_TAG_structure_type
+# RUN: llvm-dwarfutil --linker parallel %t.pu.o %t.pu.parallel
+# RUN: llvm-dwarfdump --debug-info %t.pu.parallel | FileCheck %s --check-prefix=PARALLEL --implicit-check-not=DW_TAG_structure_type
+
+## The same partial unit input with deduplication turned off, which is what
+## tells a deduplicated link apart from one that merely dropped a definition.
+# RUN: llvm-dwarfutil --no-odr-deduplication %t.pu.o %t.pu-noodr.classic
+# RUN: llvm-dwarfdump --debug-info %t.pu-noodr.classic | FileCheck %s --check-prefix=NOODR
+# RUN: llvm-dwarfutil --linker parallel --no-odr-deduplication %t.pu.o %t.pu-noodr.parallel
+# RUN: llvm-dwarfdump --debug-info %t.pu-noodr.parallel | FileCheck %s --check-prefix=NOODR
+
+## The classic backend keeps the first definition it saw and redirects the
+## other unit's references to it, so U01 owns both definitions and U02 owns
+## none. llvm-dwarfdump qualifies a type name with the names of its enclosing
+## scopes and a partial unit root is one, which a compile unit root is not.
+## The reference target is the same DIE either way, and that is what the
+## captured offsets pin.
+# CLASSIC: DW_AT_name ("U01")
+# CLASSIC: DW_TAG_subprogram
+# CLASSIC-NEXT: DW_AT_name ("foo1")
+# CLASSIC-CU: DW_AT_type (0x00000000[[S:[0-9a-f]{8}]] "S")
+# CLASSIC-PU: DW_AT_type (0x00000000[[S:[0-9a-f]{8}]] "U01::S")
+# CLASSIC: DW_TAG_subprogram
+# CLASSIC-NEXT: DW_AT_name ("foo2")
+# CLASSIC-CU: DW_AT_type (0x00000000[[NSS:[0-9a-f]{8}]] "ns::S")
+# CLASSIC-PU: DW_AT_type (0x00000000[[NSS:[0-9a-f]{8}]] "U01::ns::S")
+# CLASSIC: 0x[[S]]: DW_TAG_structure_type
+# CLASSIC-NEXT: DW_AT_name ("S")
+# CLASSIC: DW_TAG_member
+# CLASSIC-NEXT: DW_AT_name ("m1")
+# CLASSIC: DW_TAG_member
+# CLASSIC-NEXT: DW_AT_name ("m2")
+# CLASSIC: DW_TAG_namespace
+# CLASSIC-NEXT: DW_AT_name ("ns")
+# CLASSIC: 0x[[NSS]]: DW_TAG_structure_type
+# CLASSIC-NEXT: DW_AT_name ("S")
+# CLASSIC: DW_TAG_member
+# CLASSIC-NEXT: DW_AT_name ("m1")
+# CLASSIC: DW_TAG_member
+# CLASSIC-NEXT: DW_AT_name ("m2")
+# CLASSIC: DW_AT_name ("U02")
+# CLASSIC: DW_TAG_subprogram
+# CLASSIC-NEXT: DW_AT_name ("bar1")
+# CLASSIC-CU: DW_AT_type (0x00000000[[S]] "S")
+# CLASSIC-PU: DW_AT_type (0x00000000[[S]] "U01::S")
+# CLASSIC: DW_TAG_subprogram
+# CLASSIC-NEXT: DW_AT_name ("bar2")
+# CLASSIC-CU: DW_AT_type (0x00000000[[NSS]] "ns::S")
+# CLASSIC-PU: DW_AT_type (0x00000000[[NSS]] "U01::ns::S")
+
+## The parallel backend moves the deduplicated types into an artificial type
+## unit that precedes both input units, and every reference resolves there. It
+## emits that unit's children in its own order, namespaces before the types at
+## unit scope, rather than in the order the input listed them.
+# PARALLEL: DW_AT_name ("__artificial_type_unit")
+# PARALLEL: DW_TAG_namespace
+# PARALLEL-NEXT: DW_AT_name ("ns")
+# PARALLEL: 0x[[NSS:[0-9a-f]{8}]]: DW_TAG_structure_type
+# PARALLEL-NEXT: DW_AT_name ("S")
+# PARALLEL: DW_TAG_member
+# PARALLEL-NEXT: DW_AT_name ("m1")
+# PARALLEL: DW_TAG_member
+# PARALLEL-NEXT: DW_AT_name ("m2")
+# PARALLEL: 0x[[S:[0-9a-f]{8}]]: DW_TAG_structure_type
+# PARALLEL-NEXT: DW_AT_name ("S")
+# PARALLEL: DW_TAG_member
+# PARALLEL-NEXT: DW_AT_name ("m1")
+# PARALLEL: DW_TAG_member
+# PARALLEL-NEXT: DW_AT_name ("m2")
+# PARALLEL: DW_AT_name ("U01")
+# PARALLEL: DW_TAG_subprogram
+# PARALLEL-NEXT: DW_AT_name ("foo1")
+# PARALLEL: DW_AT_type (0x00000000[[S]] "S")
+# PARALLEL: DW_TAG_subprogram
+# PARALLEL-NEXT: DW_AT_name ("foo2")
+# PARALLEL: DW_AT_type (0x00000000[[NSS]] "ns::S")
+# PARALLEL: DW_AT_name ("U02")
+# PARALLEL: DW_TAG_subprogram
+# PARALLEL-NEXT: DW_AT_name ("bar1")
+# PARALLEL: DW_AT_type (0x00000000[[S]] "S")
+# PARALLEL: DW_TAG_subprogram
+# PARALLEL-NEXT: DW_AT_name ("bar2")
+# PARALLEL: DW_AT_type (0x00000000[[NSS]] "ns::S")
+
+## Without deduplication each unit keeps the definitions it came in with, and
+## no artificial type unit is produced.
+# NOODR: DW_AT_name ("U01")
+# NOODR: DW_TAG_structure_type
+# NOODR-NEXT: DW_AT_name ("S")
+# NOODR: DW_TAG_namespace
+# NOODR-NEXT: DW_AT_name ("ns")
+# NOODR: DW_TAG_structure_type
+# NOODR-NEXT: DW_AT_name ("S")
+# NOODR: DW_AT_name ("U02")
+# NOODR: DW_TAG_structure_type
+# NOODR-NEXT: DW_AT_name ("S")
+# NOODR: DW_TAG_namespace
+# NOODR-NEXT: DW_AT_name ("ns")
+# NOODR: DW_TAG_structure_type
+# NOODR-NEXT: DW_AT_name ("S")
+# NOODR-NOT: __artificial_type_unit
+
+## Two DWARFv5 units, each with two functions in .text, one returning the
+## struct S defined at unit scope and one returning the unrelated struct S
+## defined in namespace ns. The units cover disjoint halves of .text so that
+## neither is dropped and neither overlaps the other. Each root carries its own
+## DW_AT_str_offsets_base, so the strings the linker rewrites into
+## DW_FORM_strx stay resolvable on output; the linker does not synthesize that
+## attribute for a partial unit root by itself.
+--- !ELF
+FileHeader:
+ Class: ELFCLASS64
+ Data: ELFDATA2LSB
+ Type: ET_REL
+ Machine: EM_X86_64
+Sections:
+ - Name: .text
+ Type: SHT_PROGBITS
+ Flags: [ SHF_ALLOC, SHF_EXECINSTR ]
+ Address: 0x1130
+ Size: 0x60
+ - Name: .debug_str_offsets
+ Type: SHT_PROGBITS
+ Flags: [ ]
+ Content: "0400000005000000"
+DWARF:
+ debug_abbrev:
+ - ID: 0
+ Table:
+ - Tag: [[ROOT]]
+ Children: DW_CHILDREN_yes
+ Attributes:
+ - Attribute: DW_AT_producer
+ Form: DW_FORM_string
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Attribute: DW_AT_language
+ Form: DW_FORM_data2
+ - Attribute: DW_AT_low_pc
+ Form: DW_FORM_addr
+ - Attribute: DW_AT_high_pc
+ Form: DW_FORM_data8
+ - Attribute: DW_AT_str_offsets_base
+ Form: DW_FORM_sec_offset
+ - Tag: DW_TAG_subprogram
+ Children: DW_CHILDREN_no
+ Attributes:
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Attribute: DW_AT_low_pc
+ Form: DW_FORM_addr
+ - Attribute: DW_AT_high_pc
+ Form: DW_FORM_data8
+ - Attribute: DW_AT_type
+ Form: DW_FORM_ref4
+ - Tag: DW_TAG_structure_type
+ Children: DW_CHILDREN_yes
+ Attributes:
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Attribute: DW_AT_byte_size
+ Form: DW_FORM_data1
+ - Tag: DW_TAG_member
+ Children: DW_CHILDREN_no
+ Attributes:
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Attribute: DW_AT_type
+ Form: DW_FORM_ref4
+ - Attribute: DW_AT_data_member_location
+ Form: DW_FORM_data1
+ - Tag: DW_TAG_base_type
+ Children: DW_CHILDREN_no
+ Attributes:
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ - Tag: DW_TAG_namespace
+ Children: DW_CHILDREN_yes
+ Attributes:
+ - Attribute: DW_AT_name
+ Form: DW_FORM_string
+ debug_info:
+ - Version: 5
+ UnitType: [[UNITTYPE]]
+ AbbrevTableID: 0
+ AbbrOffset: 0x0
+ Entries:
+ - AbbrCode: 1
+ Values:
+ - CStr: by_hand
+ - CStr: U01
+ - Value: 0x04
+ - Value: 0x1130
+ - Value: 0x30
+ - Value: 0x8
+ - AbbrCode: 2
+ Values:
+ - CStr: foo1
+ - Value: 0x1130
+ - Value: 0x10
+ - Value: 0x63
+ - AbbrCode: 2
+ Values:
+ - CStr: foo2
+ - Value: 0x1150
+ - Value: 0x10
+ - Value: 0x7e
+ - AbbrCode: 3
+ Values:
+ - CStr: S
+ - Value: 0x8
+ - AbbrCode: 4
+ Values:
+ - CStr: m1
+ - Value: 0x96
+ - Value: 0x0
+ - AbbrCode: 4
+ Values:
+ - CStr: m2
+ - Value: 0x96
+ - Value: 0x4
+ - AbbrCode: 0
+ - AbbrCode: 6
+ Values:
+ - CStr: ns
+ - AbbrCode: 3
+ Values:
+ - CStr: S
+ - Value: 0x8
+ - AbbrCode: 4
+ Values:
+ - CStr: m1
+ - Value: 0x96
+ - Value: 0x0
+ - AbbrCode: 4
+ Values:
+ - CStr: m2
+ - Value: 0x96
+ - Value: 0x4
+ - AbbrCode: 0
+ - AbbrCode: 0
+ - AbbrCode: 5
+ Values:
+ - CStr: int
+ - AbbrCode: 0
+ - Version: 5
+ UnitType: [[UNITTYPE]]
+ AbbrevTableID: 0
+ AbbrOffset: 0x0
+ Entries:
+ - AbbrCode: 1
+ Values:
+ - CStr: by_hand
+ - CStr: U02
+ - Value: 0x04
+ - Value: 0x1160
+ - Value: 0x30
+ - Value: 0x8
+ - AbbrCode: 2
+ Values:
+ - CStr: bar1
+ - Value: 0x1160
+ - Value: 0x10
+ - Value: 0x63
+ - AbbrCode: 2
+ Values:
+ - CStr: bar2
+ - Value: 0x1180
+ - Value: 0x10
+ - Value: 0x7e
+ - AbbrCode: 3
+ Values:
+ - CStr: S
+ - Value: 0x8
+ - AbbrCode: 4
+ Values:
+ - CStr: m1
+ - Value: 0x96
+ - Value: 0x0
+ - AbbrCode: 4
+ Values:
+ - CStr: m2
+ - Value: 0x96
+ - Value: 0x4
+ - AbbrCode: 0
+ - AbbrCode: 6
+ Values:
+ - CStr: ns
+ - AbbrCode: 3
+ Values:
+ - CStr: S
+ - Value: 0x8
+ - AbbrCode: 4
+ Values:
+ - CStr: m1
+ - Value: 0x96
+ - Value: 0x0
+ - AbbrCode: 4
+ Values:
+ - CStr: m2
+ - Value: 0x96
+ - Value: 0x4
+ - AbbrCode: 0
+ - AbbrCode: 0
+ - AbbrCode: 5
+ Values:
+ - CStr: int
+ - AbbrCode: 0
+...
diff --git a/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-types-only.test b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-types-only.test
index 312b8d0e1921c..8929ccec84f10 100644
--- a/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-types-only.test
+++ b/llvm/test/tools/llvm-dwarfutil/ELF/X86/dwarf5-partial-unit-types-only.test
@@ -10,43 +10,66 @@
## U01: namespace ns { struct S {}; }
## U02: ns::S foo1() { ... } // DW_FORM_ref_addr reference into U01
##
-## The classic backend is not linked here. It fails on this input for an
-## unrelated reason, a cross-unit reference to a DIE it never gave an output
-## unit, which is a separate defect from the one this test covers.
+## The classic backend links the same input. Until types under a partial unit
+## root were given a declaration context, this shape aborted there too: the
+## definition was dropped and the second unit's DW_FORM_ref_addr into it hit
+## "DIE must be owned by a DIEUnit to get its absolute offset". The compile
+## unit control was clean, so that was the same defect wearing another symptom.
# RUN: yaml2obj %s -DROOT=DW_TAG_compile_unit -DUNITTYPE=DW_UT_compile -o %t.cu.o
-# RUN: llvm-dwarfutil --linker parallel %t.cu.o %t.cu.out
-# RUN: llvm-dwarfdump --debug-info %t.cu.out | FileCheck %s --implicit-check-not='DW_AT_name ("U01")'
+# RUN: llvm-dwarfutil %t.cu.o %t.cu.classic
+# RUN: llvm-dwarfdump --debug-info %t.cu.classic | FileCheck %s --check-prefixes=CLASSIC,CLASSIC-CU
+# RUN: llvm-dwarfutil --linker parallel %t.cu.o %t.cu.parallel
+# RUN: llvm-dwarfdump --debug-info %t.cu.parallel | FileCheck %s --check-prefix=PARALLEL --implicit-check-not='DW_AT_name ("U01")'
# RUN: yaml2obj %s -DROOT=DW_TAG_partial_unit -DUNITTYPE=DW_UT_partial -o %t.pu.o
-# RUN: llvm-dwarfutil --linker parallel %t.pu.o %t.pu.out
-# RUN: llvm-dwarfdump --debug-info %t.pu.out | FileCheck %s --check-prefixes=CHECK,CHECK-PU
+# RUN: llvm-dwarfutil %t.pu.o %t.pu.classic
+# RUN: llvm-dwarfdump --debug-info %t.pu.classic | FileCheck %s --check-prefixes=CLASSIC,CLASSIC-PU
+# RUN: llvm-dwarfutil --linker parallel %t.pu.o %t.pu.parallel
+# RUN: llvm-dwarfdump --debug-info %t.pu.parallel | FileCheck %s --check-prefix=PARALLEL --implicit-check-not='DW_AT_name ("U01")'
+
+## The two parallel links agree byte for byte.
+# RUN: cmp %t.cu.parallel %t.pu.parallel
+
+## The classic backend leaves the type where it found it, so the first unit
+## survives and the cross-unit reference resolves to the definition under it.
+## llvm-dwarfdump qualifies a type name with the names of its enclosing scopes
+## and a partial unit root is one, which a compile unit root is not.
+# CLASSIC-CU: DW_TAG_compile_unit
+# CLASSIC-PU: DW_TAG_partial_unit
+# CLASSIC-NEXT: DW_AT_producer ("by_hand")
+# CLASSIC-NEXT: DW_AT_name ("U01")
+# CLASSIC: DW_TAG_namespace
+# CLASSIC-NEXT: DW_AT_name ("ns")
+# CLASSIC: 0x[[S:[0-9a-f]{8}]]: DW_TAG_structure_type
+# CLASSIC-NEXT: DW_AT_name ("S")
+# CLASSIC: DW_TAG_compile_unit
+# CLASSIC-NEXT: DW_AT_producer ("by_hand")
+# CLASSIC-NEXT: DW_AT_name ("U02")
+# CLASSIC: DW_TAG_subprogram
+# CLASSIC-NEXT: DW_AT_name ("foo1")
+# CLASSIC-CU: DW_AT_type (0x00000000[[S]] "ns::S")
+# CLASSIC-PU: DW_AT_type (0x00000000[[S]] "U01::ns::S")
## The type reaches the artificial type unit under a copy of the namespace that
## held it, and the second unit's reference resolves into the type unit rather
## than to a dropped DIE.
-# CHECK: DW_AT_name ("__artificial_type_unit")
-# CHECK: DW_TAG_namespace
-# CHECK-NEXT: DW_AT_name ("ns")
-# CHECK: 0x[[S:[0-9a-f]{8}]]: DW_TAG_structure_type
-# CHECK-NEXT: DW_AT_name ("S")
-# CHECK-NEXT: DW_AT_byte_size (0x08)
+# PARALLEL: DW_AT_name ("__artificial_type_unit")
+# PARALLEL: DW_TAG_namespace
+# PARALLEL-NEXT: DW_AT_name ("ns")
+# PARALLEL: 0x[[S:[0-9a-f]{8}]]: DW_TAG_structure_type
+# PARALLEL-NEXT: DW_AT_name ("S")
+# PARALLEL-NEXT: DW_AT_byte_size (0x08)
-## The first unit is emptied either way, since its only child moved to the type
-## unit, but the two roots are then treated differently: a compile unit with
-## nothing left is dropped whole, while a partial unit root is still emitted.
-## That difference is the linker's existing behaviour for a unit with no live
-## contents, not something this fix introduces, so it is pinned rather than
-## made uniform.
-# CHECK-PU: DW_TAG_partial_unit
-# CHECK-PU-NEXT: DW_AT_producer ("by_hand")
-# CHECK-PU-NEXT: DW_AT_name ("U01")
-# CHECK: DW_TAG_compile_unit
-# CHECK-NEXT: DW_AT_producer ("by_hand")
-# CHECK-NEXT: DW_AT_name ("U02")
-# CHECK: DW_TAG_subprogram
-# CHECK-NEXT: DW_AT_name ("foo1")
-# CHECK: DW_AT_type (0x00000000[[S]] "ns::S")
+## The first unit is emptied, since its only child moved to the type unit, and
+## a unit root with nothing left under it is dropped whole - a partial unit
+## root exactly as a compile unit root - so only the second unit survives.
+# PARALLEL: DW_TAG_compile_unit
+# PARALLEL-NEXT: DW_AT_producer ("by_hand")
+# PARALLEL-NEXT: DW_AT_name ("U02")
+# PARALLEL: DW_TAG_subprogram
+# PARALLEL-NEXT: DW_AT_name ("foo1")
+# PARALLEL: DW_AT_type (0x00000000[[S]] "ns::S")
## Two DWARFv5 units. The first defines struct S in namespace ns and has no
## code of its own; the second has the only function, which returns S through a
More information about the llvm-commits
mailing list