[llvm] [DWARFLinker] Patch DW_AT_LLVM_stmt_sequence in the parallel linker (PR #195388)

Jonas Devlieghere via llvm-commits llvm-commits at lists.llvm.org
Mon May 4 13:49:27 PDT 2026


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

>From 8186fd07edcf369bd8613c548b38637ec1544410 Mon Sep 17 00:00:00 2001
From: Jonas Devlieghere <jonas at devlieghere.com>
Date: Fri, 1 May 2026 16:59:57 -0700
Subject: [PATCH 1/5] [DWARFLinker] Patch DW_AT_LLVM_stmt_sequence in the
 parallel linker

Mirror dsymutil's stmt-sequence rewriting in the parallel linker so each
attribute ends up pointing at the DW_LNE_set_address that opens its
containing output sequence, with the correct offset in the combined
.debug_line.

At DIE cloning time we resolve each attribute's input offset to the
address of its first row and record the pair (DIEValue, address) on the
CompileUnit, alongside a DebugOffsetPatch on the .debug_info section so
combination adds the CU's .debug_line start offset. The line-table
emitter then fills a map from row address to the byte offset of the
sequence-opening DW_LNE_set_address.

After emission, each recorded attribute is rewritten by relocating its
input address through the CU's function ranges and looking the result up
in the map. When resolution fails the DWARF max-offset sentinel is
written instead, and the patch applier preserves it unchanged.

First-row lookups share a lazy per-CU cache to keep resolution O(1) per
attribute.
---
 .../Parallel/DIEAttributeCloner.cpp           |  30 ++-
 .../Parallel/DWARFLinkerCompileUnit.cpp       | 186 ++++++++++++------
 .../Parallel/DWARFLinkerCompileUnit.h         |  49 +++++
 .../DWARFLinker/Parallel/DWARFLinkerUnit.cpp  |   7 +-
 .../DWARFLinker/Parallel/DWARFLinkerUnit.h    |   6 +-
 .../Parallel/DebugLineSectionEmitter.h        |  17 +-
 .../DWARFLinker/Parallel/OutputSections.cpp   |  16 +-
 .../dsymutil/AArch64/stmt-seq-macho.test      |  25 ++-
 8 files changed, 256 insertions(+), 80 deletions(-)

diff --git a/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp b/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp
index 07ebd55e2c46f..6a6f77d8406e2 100644
--- a/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp
+++ b/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp
@@ -514,8 +514,34 @@ size_t DIEAttributeCloner::cloneScalarAttr(
   } else if (AttrSpec.Attr == dwarf::DW_AT_declaration && Value)
     AttrInfo.IsDeclaration = true;
 
-  return Generator.addScalarAttribute(AttrSpec.Attr, ResultingForm, Value)
-      .second;
+  auto Result =
+      Generator.addScalarAttribute(AttrSpec.Attr, ResultingForm, Value);
+  // Record DW_AT_LLVM_stmt_sequence so the attribute value can be
+  // rewritten with the correct .debug_line offset after the line table
+  // for this CU has been emitted. We also register a DebugOffsetPatch so
+  // that the final-section offset of .debug_line gets added when the
+  // section is placed in the combined output. The assert encodes
+  // dsymutil's placement policy: subprograms (which carry
+  // DW_AT_LLVM_stmt_sequence) are placed in compile units, not type
+  // units, so this attribute should never appear on a type-unit DIE.
+  if (AttrSpec.Attr == dwarf::DW_AT_LLVM_stmt_sequence) {
+    assert(OutUnit.isCompileUnit() &&
+           "DW_AT_LLVM_stmt_sequence on a non-compile-unit DIE");
+    // Resolve the attribute's stmt-sequence offset to the address of the
+    // referenced sequence's first row, so that after line-table emission
+    // the attribute can be matched to the output sequence by address.
+    std::optional<uint64_t> InputFirstAddr =
+        InUnit.getStmtSeqFirstAddress(Value);
+    OutUnit.getAsCompileUnit()->noteStmtSeqListAttribute(&Result.first,
+                                                         InputFirstAddr);
+    DebugInfoOutputSection.notePatchWithOffsetUpdate(
+        DebugOffsetPatch{
+            AttrOutOffset,
+            &OutUnit->getOrCreateSectionDescriptor(DebugSectionKind::DebugLine),
+            /*AddLocalValue=*/true},
+        PatchesOffsets);
+  }
+  return Result.second;
 }
 
 size_t DIEAttributeCloner::cloneBlockAttr(
diff --git a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp
index 1e16716b29082..1ef4d39ed1314 100644
--- a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp
+++ b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp
@@ -1553,74 +1553,142 @@ Error CompileUnit::cloneAndEmitLineTable(const Triple &TargetTriple) {
       OutLineTable.Rows.clear();
 
     OutLineTable.Sequences = InputLineTable->Sequences;
-  } else {
-    // This vector is the output line table.
-    std::vector<DWARFDebugLine::Row> NewRows;
-    NewRows.reserve(InputLineTable->Rows.size());
-
-    // Current sequence of rows being extracted, before being inserted
-    // in NewRows.
-    std::vector<DWARFDebugLine::Row> Seq;
-
-    const auto &FunctionRanges = getFunctionRanges();
-    std::optional<AddressRangeValuePair> CurrRange;
-
-    // FIXME: This logic is meant to generate exactly the same output as
-    // Darwin's classic dsymutil. There is a nicer way to implement this
-    // by simply putting all the relocated line info in NewRows and simply
-    // sorting NewRows before passing it to emitLineTableForUnit. This
-    // should be correct as sequences for a function should stay
-    // together in the sorted output. There are a few corner cases that
-    // look suspicious though, and that required to implement the logic
-    // this way. Revisit that once initial validation is finished.
-
-    // Iterate over the object file line info and extract the sequences
-    // that correspond to linked functions.
-    for (DWARFDebugLine::Row Row : InputLineTable->Rows) {
-      // Check whether we stepped out of the range. The range is
-      // half-open, but consider accept the end address of the range if
-      // it is marked as end_sequence in the input (because in that
-      // case, the relocation offset is accurate and that entry won't
-      // serve as the start of another function).
-      if (!CurrRange || !CurrRange->Range.contains(Row.Address.Address)) {
-        // We just stepped out of a known range. Insert a end_sequence
-        // corresponding to the end of the range.
-        uint64_t StopAddress =
-            CurrRange ? CurrRange->Range.end() + CurrRange->Value : -1ULL;
-        CurrRange = FunctionRanges.getRangeThatContains(Row.Address.Address);
-        if (StopAddress != -1ULL && !Seq.empty()) {
-          // Insert end sequence row with the computed end address, but
-          // the same line as the previous one.
-          auto NextLine = Seq.back();
-          NextLine.Address.Address = StopAddress;
-          NextLine.EndSequence = 1;
-          NextLine.PrologueEnd = 0;
-          NextLine.BasicBlock = 0;
-          NextLine.EpilogueBegin = 0;
-          Seq.push_back(NextLine);
-          insertLineSequence(Seq, NewRows);
-        }
+    return emitDebugLine(TargetTriple, OutLineTable);
+  }
 
-        if (!CurrRange)
-          continue;
+  filterLineTableRows(*InputLineTable, OutLineTable.Rows);
+
+  if (StmtSeqListAttributes.empty())
+    return emitDebugLine(TargetTriple, OutLineTable);
+
+  // When DW_AT_LLVM_stmt_sequence attributes on this CU need their values
+  // rewritten to point at the correct output sequence, have the emitter
+  // record, for every row, the byte offset of the DW_LNE_set_address that
+  // opens the sequence containing that row.
+  //
+  // The patching below MUST run before emitDebugInfo() serializes the
+  // DIE bytes and before OutputSections::applyPatches() runs for this
+  // unit's .debug_info — it writes a local offset into the DIEValue that
+  // the serializer then emits, and a DebugOffsetPatch (registered at DIE
+  // cloning time) later adds the CU's .debug_line start offset to reach
+  // the final absolute value.
+  DenseMap<uint64_t, uint64_t> AddrToSeqStartOffset;
+  if (Error Err =
+          emitDebugLine(TargetTriple, OutLineTable, &AddrToSeqStartOffset))
+    return Err;
+
+  patchStmtSeqAttributes(AddrToSeqStartOffset);
+  return Error::success();
+}
+
+void CompileUnit::filterLineTableRows(
+    const DWARFDebugLine::LineTable &InputLineTable,
+    std::vector<DWARFDebugLine::Row> &NewRows) {
+  NewRows.reserve(InputLineTable.Rows.size());
+
+  // Current sequence of rows being extracted, before being inserted
+  // in NewRows.
+  std::vector<DWARFDebugLine::Row> Seq;
+
+  const auto &FunctionRanges = getFunctionRanges();
+  std::optional<AddressRangeValuePair> CurrRange;
+
+  // FIXME: This logic is meant to generate exactly the same output as
+  // Darwin's classic dsymutil. There is a nicer way to implement this
+  // by simply putting all the relocated line info in NewRows and simply
+  // sorting NewRows before passing it to emitLineTableForUnit. This
+  // should be correct as sequences for a function should stay
+  // together in the sorted output. There are a few corner cases that
+  // look suspicious though, and that required to implement the logic
+  // this way. Revisit that once initial validation is finished.
+
+  // Iterate over the object file line info and extract the sequences
+  // that correspond to linked functions.
+  for (DWARFDebugLine::Row Row : InputLineTable.Rows) {
+    // Check whether we stepped out of the range. The range is
+    // half-open, but consider accept the end address of the range if
+    // it is marked as end_sequence in the input (because in that
+    // case, the relocation offset is accurate and that entry won't
+    // serve as the start of another function).
+    if (!CurrRange || !CurrRange->Range.contains(Row.Address.Address)) {
+      // We just stepped out of a known range. Insert a end_sequence
+      // corresponding to the end of the range.
+      uint64_t StopAddress =
+          CurrRange ? CurrRange->Range.end() + CurrRange->Value : -1ULL;
+      CurrRange = FunctionRanges.getRangeThatContains(Row.Address.Address);
+      if (StopAddress != -1ULL && !Seq.empty()) {
+        // Insert end sequence row with the computed end address, but
+        // the same line as the previous one.
+        auto NextLine = Seq.back();
+        NextLine.Address.Address = StopAddress;
+        NextLine.EndSequence = 1;
+        NextLine.PrologueEnd = 0;
+        NextLine.BasicBlock = 0;
+        NextLine.EpilogueBegin = 0;
+        Seq.push_back(NextLine);
+        insertLineSequence(Seq, NewRows);
       }
 
-      // Ignore empty sequences.
-      if (Row.EndSequence && Seq.empty())
+      if (!CurrRange)
         continue;
+    }
+
+    // Ignore empty sequences.
+    if (Row.EndSequence && Seq.empty())
+      continue;
 
-      // Relocate row address and add it to the current sequence.
-      Row.Address.Address += CurrRange->Value;
-      Seq.emplace_back(Row);
+    // Relocate row address and add it to the current sequence.
+    Row.Address.Address += CurrRange->Value;
+    Seq.emplace_back(Row);
 
-      if (Row.EndSequence)
-        insertLineSequence(Seq, NewRows);
-    }
+    if (Row.EndSequence)
+      insertLineSequence(Seq, NewRows);
+  }
+}
 
-    OutLineTable.Rows = std::move(NewRows);
+void CompileUnit::patchStmtSeqAttributes(
+    const DenseMap<uint64_t, uint64_t> &AddrToSeqStartOffset) {
+  const uint64_t InvalidOffset = getFormParams().getDwarfMaxOffset();
+  const auto &FunctionRanges = getFunctionRanges();
+
+  for (const CompileUnit::StmtSeqPatch &Patch : StmtSeqListAttributes) {
+    uint64_t NewStmtSeq = InvalidOffset;
+    if (Patch.InputFirstAddr) {
+      if (auto Range =
+              FunctionRanges.getRangeThatContains(*Patch.InputFirstAddr)) {
+        uint64_t OutAddr = *Patch.InputFirstAddr + Range->Value;
+        auto It = AddrToSeqStartOffset.find(OutAddr);
+        if (It != AddrToSeqStartOffset.end())
+          NewStmtSeq = It->second;
+      }
+    }
+    // When resolution fails, the InvalidOffset sentinel must survive the
+    // combination-time section-offset fixup. The patch applier preserves
+    // InvalidOffset as-is so consumers see a clean invalid marker rather
+    // than StartOffset - 1.
+    *Patch.Value = DIEValue(Patch.Value->getAttribute(), Patch.Value->getForm(),
+                            DIEInteger(NewStmtSeq));
   }
+}
 
-  return emitDebugLine(TargetTriple, OutLineTable);
+std::optional<uint64_t>
+CompileUnit::getStmtSeqFirstAddress(uint64_t StmtSeqOffset) {
+  if (!StmtSeqOffsetToFirstAddr) {
+    StmtSeqOffsetToFirstAddr.emplace();
+    if (const DWARFDebugLine::LineTable *InputLT =
+            getContaingFile().Dwarf->getLineTableForUnit(&getOrigUnit())) {
+      for (const DWARFDebugLine::Sequence &Seq : InputLT->Sequences) {
+        assert(Seq.FirstRowIndex < InputLT->Rows.size() &&
+               "sequence's first-row index out of range");
+        (*StmtSeqOffsetToFirstAddr)[Seq.StmtSeqOffset] =
+            InputLT->Rows[Seq.FirstRowIndex].Address.Address;
+      }
+    }
+  }
+  auto It = StmtSeqOffsetToFirstAddr->find(StmtSeqOffset);
+  if (It == StmtSeqOffsetToFirstAddr->end())
+    return std::nullopt;
+  return It->second;
 }
 
 void CompileUnit::insertLineSequence(std::vector<DWARFDebugLine::Row> &Seq,
diff --git a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h
index 1c4a62c06934d..a0c8f36f8463a 100644
--- a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h
+++ b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h
@@ -407,6 +407,25 @@ class alignas(8) CompileUnit : public DwarfUnit {
   /// Returns function ranges of this unit.
   const RangesTy &getFunctionRanges() const { return Ranges; }
 
+  /// Record that a DW_AT_LLVM_stmt_sequence attribute on this unit
+  /// references the input line-table sequence starting at \p InputFirstAddr
+  /// (std::nullopt if the input offset couldn't be resolved to an input
+  /// row). After the line table for this unit has been emitted, \p V is
+  /// rewritten to the byte offset of the output sequence that contains
+  /// the relocated address (or to an invalid sentinel if the sequence
+  /// wasn't kept).
+  void noteStmtSeqListAttribute(DIEValue *V,
+                                std::optional<uint64_t> InputFirstAddr) {
+    StmtSeqListAttributes.push_back({V, InputFirstAddr});
+  }
+
+  /// Return the input-side first-row address of the line-table sequence
+  /// whose header sits at \p StmtSeqOffset in this unit's input
+  /// .debug_line contribution, or std::nullopt if no such sequence was
+  /// recognised by the DWARF parser. Builds and caches a lookup table on
+  /// first call to keep stmt_sequence attribute resolution O(1).
+  std::optional<uint64_t> getStmtSeqFirstAddress(uint64_t StmtSeqOffset);
+
   /// Clone and emit this compilation unit.
   Error
   cloneAndEmit(std::optional<std::reference_wrapper<const Triple>> TargetTriple,
@@ -651,6 +670,19 @@ class alignas(8) CompileUnit : public DwarfUnit {
   void insertLineSequence(std::vector<DWARFDebugLine::Row> &Seq,
                           std::vector<DWARFDebugLine::Row> &Rows);
 
+  /// Filter \p InputLineTable's rows to those covered by this unit's
+  /// function ranges, relocating addresses in the process, and store the
+  /// result in \p NewRows.
+  void filterLineTableRows(const DWARFDebugLine::LineTable &InputLineTable,
+                           std::vector<DWARFDebugLine::Row> &NewRows);
+
+  /// Rewrite every DW_AT_LLVM_stmt_sequence DIEValue recorded on this
+  /// unit with the local .debug_line offset of the output sequence
+  /// corresponding to its input-sequence first-row address. The map is
+  /// built during line-table emission.
+  void patchStmtSeqAttributes(
+      const DenseMap<uint64_t, uint64_t> &AddrToSeqStartOffset);
+
   /// Emits body for both macro sections.
   void emitMacroTableImpl(const DWARFDebugMacro *MacroTable,
                           uint64_t OffsetToMacroTable, bool hasDWARFv5Header);
@@ -721,6 +753,23 @@ class alignas(8) CompileUnit : public DwarfUnit {
   /// The DW_AT_low_pc of each DW_TAG_label.
   using LabelMapTy = SmallDenseMap<uint64_t, uint64_t, 1>;
   LabelMapTy Labels;
+
+  /// Recorded DW_AT_LLVM_stmt_sequence attributes for this unit. Each
+  /// entry pairs the DIEValue holding the attribute with the input-side
+  /// first-row address of the referenced line-table sequence (empty if
+  /// the parser couldn't resolve the input offset); the value is
+  /// rewritten with the matching output offset after the line table has
+  /// been emitted.
+  struct StmtSeqPatch {
+    DIEValue *Value = nullptr;
+    std::optional<uint64_t> InputFirstAddr;
+  };
+  SmallVector<StmtSeqPatch, 4> StmtSeqListAttributes;
+
+  /// Lazily-built lookup table from an input DW_AT_LLVM_stmt_sequence
+  /// byte offset to the address of the corresponding sequence's first
+  /// row. Populated on the first call to getStmtSeqFirstAddress().
+  std::optional<DenseMap<uint64_t, uint64_t>> StmtSeqOffsetToFirstAddr;
   std::mutex LabelsMutex;
 
   /// This field keeps current stage of overall compile unit processing.
diff --git a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerUnit.cpp b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerUnit.cpp
index c992a2321c14b..9c9d62365e8b1 100644
--- a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerUnit.cpp
+++ b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerUnit.cpp
@@ -118,11 +118,12 @@ Error DwarfUnit::emitDebugInfo(const Triple &TargetTriple) {
   return Error::success();
 }
 
-Error DwarfUnit::emitDebugLine(const Triple &TargetTriple,
-                               const DWARFDebugLine::LineTable &OutLineTable) {
+Error DwarfUnit::emitDebugLine(
+    const Triple &TargetTriple, const DWARFDebugLine::LineTable &OutLineTable,
+    DenseMap<uint64_t, uint64_t> *AddrToSeqStartOffset) {
   DebugLineSectionEmitter DebugLineEmitter(TargetTriple, *this);
 
-  return DebugLineEmitter.emit(OutLineTable);
+  return DebugLineEmitter.emit(OutLineTable, AddrToSeqStartOffset);
 }
 
 Error DwarfUnit::emitDebugStringOffsetSection() {
diff --git a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerUnit.h b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerUnit.h
index 970abdc38f417..2871ef0c35962 100644
--- a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerUnit.h
+++ b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerUnit.h
@@ -96,8 +96,10 @@ class DwarfUnit : public OutputSections {
   Error emitDebugInfo(const Triple &TargetTriple);
 
   /// Emit .debug_line section.
-  Error emitDebugLine(const Triple &TargetTriple,
-                      const DWARFDebugLine::LineTable &OutLineTable);
+  Error
+  emitDebugLine(const Triple &TargetTriple,
+                const DWARFDebugLine::LineTable &OutLineTable,
+                DenseMap<uint64_t, uint64_t> *AddrToSeqStartOffset = nullptr);
 
   /// Emit the .debug_str_offsets section for current unit.
   Error emitDebugStringOffsetSection();
diff --git a/llvm/lib/DWARFLinker/Parallel/DebugLineSectionEmitter.h b/llvm/lib/DWARFLinker/Parallel/DebugLineSectionEmitter.h
index 21cdf5887813f..d96e1b9301752 100644
--- a/llvm/lib/DWARFLinker/Parallel/DebugLineSectionEmitter.h
+++ b/llvm/lib/DWARFLinker/Parallel/DebugLineSectionEmitter.h
@@ -26,7 +26,8 @@ class DebugLineSectionEmitter {
   DebugLineSectionEmitter(const Triple &TheTriple, DwarfUnit &U)
       : TheTriple(TheTriple), U(U) {}
 
-  Error emit(const DWARFDebugLine::LineTable &LineTable) {
+  Error emit(const DWARFDebugLine::LineTable &LineTable,
+             DenseMap<uint64_t, uint64_t> *AddrToSeqStartOffset = nullptr) {
     // FIXME: remove dependence on MCDwarfLineAddr::encode.
     // As we reuse MCDwarfLineAddr::encode, we need to create/initialize
     // some MC* classes.
@@ -45,7 +46,7 @@ class DebugLineSectionEmitter {
     emitLineTablePrologue(LineTable.Prologue, OutSection);
 
     // Emit rows.
-    emitLineTableRows(LineTable, OutSection);
+    emitLineTableRows(LineTable, OutSection, AddrToSeqStartOffset);
     uint64_t OffsetAfterEnd = OutSection.OS.tell();
 
     // Update unit length field with actual length value.
@@ -292,8 +293,9 @@ class DebugLineSectionEmitter {
       emitLineTablePrologueV5IncludeAndFileTable(P, Section);
   }
 
-  void emitLineTableRows(const DWARFDebugLine::LineTable &LineTable,
-                         SectionDescriptor &Section) {
+  void emitLineTableRows(
+      const DWARFDebugLine::LineTable &LineTable, SectionDescriptor &Section,
+      DenseMap<uint64_t, uint64_t> *AddrToSeqStartOffset = nullptr) {
 
     MCDwarfLineTableParams Params;
     Params.DWARF2LineOpcodeBase = LineTable.Prologue.OpcodeBase;
@@ -321,10 +323,15 @@ class DebugLineSectionEmitter {
     uint64_t Address = -1ULL;
 
     unsigned RowsSinceLastSequence = 0;
+    // Offset of the DW_LNE_set_address opcode that opens the sequence
+    // currently being emitted. Recorded for each row's address so that
+    // DW_AT_LLVM_stmt_sequence attributes can be resolved by address.
+    uint64_t CurrentSeqStartOffset = 0;
 
     for (const DWARFDebugLine::Row &Row : LineTable.Rows) {
       int64_t AddressDelta;
       if (Address == -1ULL) {
+        CurrentSeqStartOffset = Section.OS.tell();
         Section.emitIntVal(dwarf::DW_LNS_extended_op, 1);
         encodeULEB128(Section.getFormParams().AddrSize + 1, Section.OS);
         Section.emitIntVal(dwarf::DW_LNE_set_address, 1);
@@ -335,6 +342,8 @@ class DebugLineSectionEmitter {
         AddressDelta =
             (Row.Address.Address - Address) / LineTable.Prologue.MinInstLength;
       }
+      if (AddrToSeqStartOffset)
+        (*AddrToSeqStartOffset)[Row.Address.Address] = CurrentSeqStartOffset;
 
       // FIXME: code copied and transformed from
       // MCDwarf.cpp::EmitDwarfLineTable. We should find a way to share this
diff --git a/llvm/lib/DWARFLinker/Parallel/OutputSections.cpp b/llvm/lib/DWARFLinker/Parallel/OutputSections.cpp
index 9bcd2b895be88..079be51ace24a 100644
--- a/llvm/lib/DWARFLinker/Parallel/OutputSections.cpp
+++ b/llvm/lib/DWARFLinker/Parallel/OutputSections.cpp
@@ -460,9 +460,21 @@ void OutputSections::applyPatches(
     uint64_t FinalValue = Patch.SectionPtr.getPointer()->StartOffset;
 
     // Check whether we need to read value from the original location.
-    if (Patch.SectionPtr.getInt())
-      FinalValue +=
+    if (Patch.SectionPtr.getInt()) {
+      uint64_t LocalValue =
           Section.getIntVal(Patch.PatchOffset, Format.getDwarfOffsetByteSize());
+      // DebugOffsetPatch treats the DWARF "invalid offset" sentinel
+      // (0xffffffff for DWARF32) as pass-through: callers that can't
+      // resolve the target write that value and expect it to survive
+      // section combination unchanged. Adding StartOffset would turn it
+      // into a plausible-looking but meaningless offset. Callers that
+      // genuinely want `StartOffset + MaxOffset` don't exist today and
+      // would need a different patch type.
+      if (LocalValue == Format.getDwarfMaxOffset())
+        FinalValue = LocalValue;
+      else
+        FinalValue += LocalValue;
+    }
 
     Section.apply(Patch.PatchOffset, dwarf::DW_FORM_sec_offset, FinalValue);
   });
diff --git a/llvm/test/tools/dsymutil/AArch64/stmt-seq-macho.test b/llvm/test/tools/dsymutil/AArch64/stmt-seq-macho.test
index 5cb3557b5becd..b3ecb3968125c 100644
--- a/llvm/test/tools/dsymutil/AArch64/stmt-seq-macho.test
+++ b/llvm/test/tools/dsymutil/AArch64/stmt-seq-macho.test
@@ -1,10 +1,11 @@
-## Test that verifies DW_AT_LLVM_stmt_sequence attributes are correctly patched in the dSYM
-## even when line table sequences need to be reordered due to address ordering.
-##
-## This test uses two object files (a.o and b.o) linked with b.o first so that
-## b's functions get lower addresses than a's functions. When dsymutil processes
-## the debug info, it must correctly handle the case where line table sequences
-## from different compilation units need to be interleaved by address.
+## Test that verifies DW_AT_LLVM_stmt_sequence attributes are correctly
+## patched in the dSYM. Each input object (a.o and b.o) carries one
+## compile unit whose line table has two sequences (one per function).
+## After linking, dsymutil merges each CU's two function sequences into a
+## single output sequence (adjacent addresses). We verify that every
+## DW_AT_LLVM_stmt_sequence attribute in the output points at a real
+## DW_LNE_set_address opcode in the emitted .debug_line, for both the
+## classic and parallel linker backends.
 
 # RUN: rm -rf %t && split-file %s %t && cd %t
 # RUN: yaml2obj %t/stmt_seq_macho.exe.yaml -o %t/stmt_seq_macho.exe
@@ -26,7 +27,15 @@
 # RUN: cat %t/stmt_seq_macho.dSYM.txt | FileCheck %s -check-prefix=CHECK_NO_INVALID_OFFSET
 # CHECK_NO_INVALID_OFFSET-NOT: DW_AT_LLVM_stmt_sequence{{.*}}0xfffffff
 
-## FIXME: Support --linker parallel
+# RUN: dsymutil --linker parallel --flat --verify-dwarf=none -oso-prepend-path %t %t/stmt_seq_macho.exe -o %t/stmt_seq_macho.dSYM
+# RUN: llvm-dwarfdump --debug-info --debug-line -v %t/stmt_seq_macho.dSYM > %t/stmt_seq_macho.dSYM.txt
+# RUN: cat %t/stmt_seq_macho.dSYM.txt | grep "DW_AT_LLVM_stmt_sequence" | \
+# RUN:   sed 's/.*(\(0x[0-9a-f]*\)).*/\1/' | sort -u > %t/stmt_offsets.txt
+# RUN: cat %t/stmt_seq_macho.dSYM.txt | grep "DW_LNE_set_address" | \
+# RUN:   sed 's/^\(0x[0-9a-f]*\):.*/\1/' | sort -u > %t/setaddr_offsets.txt
+# RUN: comm -23 %t/stmt_offsets.txt %t/setaddr_offsets.txt > %t/bad_offsets.txt
+# RUN: test ! -s %t/bad_offsets.txt
+# RUN: cat %t/stmt_seq_macho.dSYM.txt | FileCheck %s -check-prefix=CHECK_NO_INVALID_OFFSET
 
 #--- stmt_seq_macho.cpp
 // This file is split into a.cpp and b.cpp by the gen script.

>From 819fd0d18fa92d94bb9b968f2bd9cb0703fa46b7 Mon Sep 17 00:00:00 2001
From: Jonas Devlieghere <jonas at devlieghere.com>
Date: Mon, 4 May 2026 11:27:49 -0700
Subject: [PATCH 2/5] key on the input row index

---
 .../Parallel/DIEAttributeCloner.cpp           | 14 +--
 .../Parallel/DWARFLinkerCompileUnit.cpp       | 94 ++++++++++++-------
 .../Parallel/DWARFLinkerCompileUnit.h         | 56 ++++++-----
 .../DWARFLinker/Parallel/DWARFLinkerUnit.cpp  |  6 +-
 .../DWARFLinker/Parallel/DWARFLinkerUnit.h    | 17 +++-
 .../Parallel/DebugLineSectionEmitter.h        | 28 ++++--
 6 files changed, 139 insertions(+), 76 deletions(-)

diff --git a/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp b/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp
index 6a6f77d8406e2..55658c33895a8 100644
--- a/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp
+++ b/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp
@@ -527,13 +527,15 @@ size_t DIEAttributeCloner::cloneScalarAttr(
   if (AttrSpec.Attr == dwarf::DW_AT_LLVM_stmt_sequence) {
     assert(OutUnit.isCompileUnit() &&
            "DW_AT_LLVM_stmt_sequence on a non-compile-unit DIE");
-    // Resolve the attribute's stmt-sequence offset to the address of the
-    // referenced sequence's first row, so that after line-table emission
-    // the attribute can be matched to the output sequence by address.
-    std::optional<uint64_t> InputFirstAddr =
-        InUnit.getStmtSeqFirstAddress(Value);
+    // Resolve the attribute's stmt-sequence offset to the index of the
+    // referenced sequence's first row in the input .debug_line rows
+    // vector, so that after line-table emission the attribute can be
+    // matched to the output sequence by input row index (immune to the
+    // address collisions that ICF would otherwise cause).
+    std::optional<uint64_t> InputFirstRowIndex =
+        InUnit.getStmtSeqFirstRowIndex(Value);
     OutUnit.getAsCompileUnit()->noteStmtSeqListAttribute(&Result.first,
-                                                         InputFirstAddr);
+                                                         InputFirstRowIndex);
     DebugInfoOutputSection.notePatchWithOffsetUpdate(
         DebugOffsetPatch{
             AttrOutOffset,
diff --git a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp
index 1ef4d39ed1314..fbb0513f3243d 100644
--- a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp
+++ b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp
@@ -1556,15 +1556,20 @@ Error CompileUnit::cloneAndEmitLineTable(const Triple &TargetTriple) {
     return emitDebugLine(TargetTriple, OutLineTable);
   }
 
-  filterLineTableRows(*InputLineTable, OutLineTable.Rows);
+  SmallVector<uint64_t> OrigRowIndices;
+  filterLineTableRows(*InputLineTable, OutLineTable.Rows, OrigRowIndices);
 
   if (StmtSeqListAttributes.empty())
     return emitDebugLine(TargetTriple, OutLineTable);
 
   // When DW_AT_LLVM_stmt_sequence attributes on this CU need their values
   // rewritten to point at the correct output sequence, have the emitter
-  // record, for every row, the byte offset of the DW_LNE_set_address that
-  // opens the sequence containing that row.
+  // record, for every row that originated from an input row, the byte
+  // offset of the DW_LNE_set_address that opens the sequence containing
+  // that row. Keying the map on the input row index (rather than on an
+  // output address) avoids collisions when two input sequences would
+  // relocate to the same output address — e.g. ICF folding two functions
+  // from the same CU to a single output range.
   //
   // The patching below MUST run before emitDebugInfo() serializes the
   // DIE bytes and before OutputSections::applyPatches() runs for this
@@ -1572,23 +1577,29 @@ Error CompileUnit::cloneAndEmitLineTable(const Triple &TargetTriple) {
   // the serializer then emits, and a DebugOffsetPatch (registered at DIE
   // cloning time) later adds the CU's .debug_line start offset to reach
   // the final absolute value.
-  DenseMap<uint64_t, uint64_t> AddrToSeqStartOffset;
-  if (Error Err =
-          emitDebugLine(TargetTriple, OutLineTable, &AddrToSeqStartOffset))
+  DenseMap<uint64_t, uint64_t> RowIndexToSeqStartOffset;
+  if (Error Err = emitDebugLine(TargetTriple, OutLineTable, OrigRowIndices,
+                                &RowIndexToSeqStartOffset))
     return Err;
 
-  patchStmtSeqAttributes(AddrToSeqStartOffset);
+  patchStmtSeqAttributes(RowIndexToSeqStartOffset);
   return Error::success();
 }
 
 void CompileUnit::filterLineTableRows(
     const DWARFDebugLine::LineTable &InputLineTable,
-    std::vector<DWARFDebugLine::Row> &NewRows) {
+    std::vector<DWARFDebugLine::Row> &NewRows,
+    SmallVectorImpl<uint64_t> &NewRowIndices) {
   NewRows.reserve(InputLineTable.Rows.size());
+  NewRowIndices.reserve(InputLineTable.Rows.size());
 
   // Current sequence of rows being extracted, before being inserted
-  // in NewRows.
+  // in NewRows. Kept in lockstep with SeqIndices, which stores the
+  // originating input row index (or InvalidRowIndex for manufactured
+  // end-of-range rows).
   std::vector<DWARFDebugLine::Row> Seq;
+  SmallVector<uint64_t> SeqIndices;
+  constexpr uint64_t InvalidRowIndex = std::numeric_limits<uint64_t>::max();
 
   const auto &FunctionRanges = getFunctionRanges();
   std::optional<AddressRangeValuePair> CurrRange;
@@ -1604,7 +1615,8 @@ void CompileUnit::filterLineTableRows(
 
   // Iterate over the object file line info and extract the sequences
   // that correspond to linked functions.
-  for (DWARFDebugLine::Row Row : InputLineTable.Rows) {
+  for (auto [InputRowIdx, InputRow] : llvm::enumerate(InputLineTable.Rows)) {
+    DWARFDebugLine::Row Row = InputRow;
     // Check whether we stepped out of the range. The range is
     // half-open, but consider accept the end address of the range if
     // it is marked as end_sequence in the input (because in that
@@ -1618,7 +1630,9 @@ void CompileUnit::filterLineTableRows(
       CurrRange = FunctionRanges.getRangeThatContains(Row.Address.Address);
       if (StopAddress != -1ULL && !Seq.empty()) {
         // Insert end sequence row with the computed end address, but
-        // the same line as the previous one.
+        // the same line as the previous one. This row is synthesised
+        // and has no input counterpart, so tag it with
+        // InvalidRowIndex.
         auto NextLine = Seq.back();
         NextLine.Address.Address = StopAddress;
         NextLine.EndSequence = 1;
@@ -1626,7 +1640,8 @@ void CompileUnit::filterLineTableRows(
         NextLine.BasicBlock = 0;
         NextLine.EpilogueBegin = 0;
         Seq.push_back(NextLine);
-        insertLineSequence(Seq, NewRows);
+        SeqIndices.push_back(InvalidRowIndex);
+        insertLineSequence(Seq, SeqIndices, NewRows, NewRowIndices);
       }
 
       if (!CurrRange)
@@ -1640,27 +1655,23 @@ void CompileUnit::filterLineTableRows(
     // Relocate row address and add it to the current sequence.
     Row.Address.Address += CurrRange->Value;
     Seq.emplace_back(Row);
+    SeqIndices.push_back(InputRowIdx);
 
     if (Row.EndSequence)
-      insertLineSequence(Seq, NewRows);
+      insertLineSequence(Seq, SeqIndices, NewRows, NewRowIndices);
   }
 }
 
 void CompileUnit::patchStmtSeqAttributes(
-    const DenseMap<uint64_t, uint64_t> &AddrToSeqStartOffset) {
+    const DenseMap<uint64_t, uint64_t> &RowIndexToSeqStartOffset) {
   const uint64_t InvalidOffset = getFormParams().getDwarfMaxOffset();
-  const auto &FunctionRanges = getFunctionRanges();
 
   for (const CompileUnit::StmtSeqPatch &Patch : StmtSeqListAttributes) {
     uint64_t NewStmtSeq = InvalidOffset;
-    if (Patch.InputFirstAddr) {
-      if (auto Range =
-              FunctionRanges.getRangeThatContains(*Patch.InputFirstAddr)) {
-        uint64_t OutAddr = *Patch.InputFirstAddr + Range->Value;
-        auto It = AddrToSeqStartOffset.find(OutAddr);
-        if (It != AddrToSeqStartOffset.end())
-          NewStmtSeq = It->second;
-      }
+    if (Patch.InputFirstRowIndex) {
+      auto It = RowIndexToSeqStartOffset.find(*Patch.InputFirstRowIndex);
+      if (It != RowIndexToSeqStartOffset.end())
+        NewStmtSeq = It->second;
     }
     // When resolution fails, the InvalidOffset sentinel must survive the
     // combination-time section-offset fixup. The patch applier preserves
@@ -1672,39 +1683,51 @@ void CompileUnit::patchStmtSeqAttributes(
 }
 
 std::optional<uint64_t>
-CompileUnit::getStmtSeqFirstAddress(uint64_t StmtSeqOffset) {
-  if (!StmtSeqOffsetToFirstAddr) {
-    StmtSeqOffsetToFirstAddr.emplace();
+CompileUnit::getStmtSeqFirstRowIndex(uint64_t StmtSeqOffset) {
+  if (!StmtSeqOffsetToFirstRowIndex) {
+    StmtSeqOffsetToFirstRowIndex.emplace();
     if (const DWARFDebugLine::LineTable *InputLT =
             getContaingFile().Dwarf->getLineTableForUnit(&getOrigUnit())) {
       for (const DWARFDebugLine::Sequence &Seq : InputLT->Sequences) {
         assert(Seq.FirstRowIndex < InputLT->Rows.size() &&
                "sequence's first-row index out of range");
-        (*StmtSeqOffsetToFirstAddr)[Seq.StmtSeqOffset] =
-            InputLT->Rows[Seq.FirstRowIndex].Address.Address;
+        (*StmtSeqOffsetToFirstRowIndex)[Seq.StmtSeqOffset] = Seq.FirstRowIndex;
       }
     }
   }
-  auto It = StmtSeqOffsetToFirstAddr->find(StmtSeqOffset);
-  if (It == StmtSeqOffsetToFirstAddr->end())
+  auto It = StmtSeqOffsetToFirstRowIndex->find(StmtSeqOffset);
+  if (It == StmtSeqOffsetToFirstRowIndex->end())
     return std::nullopt;
   return It->second;
 }
 
 void CompileUnit::insertLineSequence(std::vector<DWARFDebugLine::Row> &Seq,
-                                     std::vector<DWARFDebugLine::Row> &Rows) {
+                                     SmallVectorImpl<uint64_t> &SeqIndices,
+                                     std::vector<DWARFDebugLine::Row> &Rows,
+                                     SmallVectorImpl<uint64_t> &RowIndices) {
+  assert(Seq.size() == SeqIndices.size() &&
+         "Seq and SeqIndices must be kept in lockstep");
+  assert(Rows.size() == RowIndices.size() &&
+         "Rows and RowIndices must be kept in lockstep");
   if (Seq.empty())
     return;
 
+  auto ClearSeq = [&] {
+    Seq.clear();
+    SeqIndices.clear();
+  };
+
   if (!Rows.empty() && Rows.back().Address < Seq.front().Address) {
     llvm::append_range(Rows, Seq);
-    Seq.clear();
+    llvm::append_range(RowIndices, SeqIndices);
+    ClearSeq();
     return;
   }
 
   object::SectionedAddress Front = Seq.front().Address;
   auto InsertPoint = partition_point(
       Rows, [=](const DWARFDebugLine::Row &O) { return O.Address < Front; });
+  size_t InsertIdx = std::distance(Rows.begin(), InsertPoint);
 
   // FIXME: this only removes the unneeded end_sequence if the
   // sequences have been inserted in order. Using a global sort like
@@ -1713,12 +1736,17 @@ void CompileUnit::insertLineSequence(std::vector<DWARFDebugLine::Row> &Seq,
   if (InsertPoint != Rows.end() && InsertPoint->Address == Front &&
       InsertPoint->EndSequence) {
     *InsertPoint = Seq.front();
+    RowIndices[InsertIdx] = SeqIndices.front();
     Rows.insert(InsertPoint + 1, Seq.begin() + 1, Seq.end());
+    RowIndices.insert(RowIndices.begin() + InsertIdx + 1,
+                      SeqIndices.begin() + 1, SeqIndices.end());
   } else {
     Rows.insert(InsertPoint, Seq.begin(), Seq.end());
+    RowIndices.insert(RowIndices.begin() + InsertIdx, SeqIndices.begin(),
+                      SeqIndices.end());
   }
 
-  Seq.clear();
+  ClearSeq();
 }
 
 #if !defined(NDEBUG) || defined(LLVM_ENABLE_DUMP)
diff --git a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h
index a0c8f36f8463a..b87f051e78275 100644
--- a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h
+++ b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h
@@ -408,23 +408,25 @@ class alignas(8) CompileUnit : public DwarfUnit {
   const RangesTy &getFunctionRanges() const { return Ranges; }
 
   /// Record that a DW_AT_LLVM_stmt_sequence attribute on this unit
-  /// references the input line-table sequence starting at \p InputFirstAddr
-  /// (std::nullopt if the input offset couldn't be resolved to an input
-  /// row). After the line table for this unit has been emitted, \p V is
-  /// rewritten to the byte offset of the output sequence that contains
-  /// the relocated address (or to an invalid sentinel if the sequence
-  /// wasn't kept).
+  /// references the input line-table sequence whose first row sits at
+  /// \p InputFirstRowIndex in the input .debug_line Rows vector
+  /// (std::nullopt if the input offset couldn't be resolved). After the
+  /// line table for this unit has been emitted, \p V is rewritten to the
+  /// byte offset of the output sequence that contains the corresponding
+  /// output row (or to an invalid sentinel if the sequence wasn't kept).
+  /// Keying on row index rather than address avoids collisions when two
+  /// input sequences would relocate to the same output address (e.g. ICF).
   void noteStmtSeqListAttribute(DIEValue *V,
-                                std::optional<uint64_t> InputFirstAddr) {
-    StmtSeqListAttributes.push_back({V, InputFirstAddr});
+                                std::optional<uint64_t> InputFirstRowIndex) {
+    StmtSeqListAttributes.push_back({V, InputFirstRowIndex});
   }
 
-  /// Return the input-side first-row address of the line-table sequence
+  /// Return the input-side first-row index of the line-table sequence
   /// whose header sits at \p StmtSeqOffset in this unit's input
   /// .debug_line contribution, or std::nullopt if no such sequence was
   /// recognised by the DWARF parser. Builds and caches a lookup table on
   /// first call to keep stmt_sequence attribute resolution O(1).
-  std::optional<uint64_t> getStmtSeqFirstAddress(uint64_t StmtSeqOffset);
+  std::optional<uint64_t> getStmtSeqFirstRowIndex(uint64_t StmtSeqOffset);
 
   /// Clone and emit this compilation unit.
   Error
@@ -666,22 +668,31 @@ class alignas(8) CompileUnit : public DwarfUnit {
                              SectionDescriptor &OutRangeSection);
 
   /// Insert the new line info sequence \p Seq into the current
-  /// set of already linked line info \p Rows.
+  /// set of already linked line info \p Rows. \p SeqIndices carries the
+  /// input Row index that each entry in \p Seq originated from (or the
+  /// invalid-row-index sentinel for manufactured end-of-range rows), and
+  /// is kept in lockstep with \p RowIndices.
   void insertLineSequence(std::vector<DWARFDebugLine::Row> &Seq,
-                          std::vector<DWARFDebugLine::Row> &Rows);
+                          SmallVectorImpl<uint64_t> &SeqIndices,
+                          std::vector<DWARFDebugLine::Row> &Rows,
+                          SmallVectorImpl<uint64_t> &RowIndices);
 
   /// Filter \p InputLineTable's rows to those covered by this unit's
   /// function ranges, relocating addresses in the process, and store the
-  /// result in \p NewRows.
+  /// result in \p NewRows. \p NewRowIndices is populated in lockstep with
+  /// \p NewRows and carries, for each output row, the index of the input
+  /// row it originated from — or InvalidRowIndex for manufactured
+  /// end-of-range rows.
   void filterLineTableRows(const DWARFDebugLine::LineTable &InputLineTable,
-                           std::vector<DWARFDebugLine::Row> &NewRows);
+                           std::vector<DWARFDebugLine::Row> &NewRows,
+                           SmallVectorImpl<uint64_t> &NewRowIndices);
 
   /// Rewrite every DW_AT_LLVM_stmt_sequence DIEValue recorded on this
   /// unit with the local .debug_line offset of the output sequence
-  /// corresponding to its input-sequence first-row address. The map is
-  /// built during line-table emission.
+  /// containing the corresponding input first row. The map is built
+  /// during line-table emission and is keyed by input row index.
   void patchStmtSeqAttributes(
-      const DenseMap<uint64_t, uint64_t> &AddrToSeqStartOffset);
+      const DenseMap<uint64_t, uint64_t> &RowIndexToSeqStartOffset);
 
   /// Emits body for both macro sections.
   void emitMacroTableImpl(const DWARFDebugMacro *MacroTable,
@@ -756,20 +767,21 @@ class alignas(8) CompileUnit : public DwarfUnit {
 
   /// Recorded DW_AT_LLVM_stmt_sequence attributes for this unit. Each
   /// entry pairs the DIEValue holding the attribute with the input-side
-  /// first-row address of the referenced line-table sequence (empty if
+  /// first-row index of the referenced line-table sequence (empty if
   /// the parser couldn't resolve the input offset); the value is
   /// rewritten with the matching output offset after the line table has
   /// been emitted.
   struct StmtSeqPatch {
     DIEValue *Value = nullptr;
-    std::optional<uint64_t> InputFirstAddr;
+    std::optional<uint64_t> InputFirstRowIndex;
   };
   SmallVector<StmtSeqPatch, 4> StmtSeqListAttributes;
 
   /// Lazily-built lookup table from an input DW_AT_LLVM_stmt_sequence
-  /// byte offset to the address of the corresponding sequence's first
-  /// row. Populated on the first call to getStmtSeqFirstAddress().
-  std::optional<DenseMap<uint64_t, uint64_t>> StmtSeqOffsetToFirstAddr;
+  /// byte offset to the index (within the input LineTable::Rows vector)
+  /// of the corresponding sequence's first row. Populated on the first
+  /// call to getStmtSeqFirstRowIndex().
+  std::optional<DenseMap<uint64_t, uint64_t>> StmtSeqOffsetToFirstRowIndex;
   std::mutex LabelsMutex;
 
   /// This field keeps current stage of overall compile unit processing.
diff --git a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerUnit.cpp b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerUnit.cpp
index 9c9d62365e8b1..f596d40e9f7de 100644
--- a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerUnit.cpp
+++ b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerUnit.cpp
@@ -120,10 +120,12 @@ Error DwarfUnit::emitDebugInfo(const Triple &TargetTriple) {
 
 Error DwarfUnit::emitDebugLine(
     const Triple &TargetTriple, const DWARFDebugLine::LineTable &OutLineTable,
-    DenseMap<uint64_t, uint64_t> *AddrToSeqStartOffset) {
+    ArrayRef<uint64_t> OrigRowIndices,
+    DenseMap<uint64_t, uint64_t> *RowIndexToSeqStartOffset) {
   DebugLineSectionEmitter DebugLineEmitter(TargetTriple, *this);
 
-  return DebugLineEmitter.emit(OutLineTable, AddrToSeqStartOffset);
+  return DebugLineEmitter.emit(OutLineTable, OrigRowIndices,
+                               RowIndexToSeqStartOffset);
 }
 
 Error DwarfUnit::emitDebugStringOffsetSection() {
diff --git a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerUnit.h b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerUnit.h
index 2871ef0c35962..a4e31656e29bc 100644
--- a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerUnit.h
+++ b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerUnit.h
@@ -95,11 +95,18 @@ class DwarfUnit : public OutputSections {
   /// Emit .debug_info section for unit DIEs.
   Error emitDebugInfo(const Triple &TargetTriple);
 
-  /// Emit .debug_line section.
-  Error
-  emitDebugLine(const Triple &TargetTriple,
-                const DWARFDebugLine::LineTable &OutLineTable,
-                DenseMap<uint64_t, uint64_t> *AddrToSeqStartOffset = nullptr);
+  /// Emit .debug_line section. When \p OrigRowIndices is non-empty it
+  /// must be the same length as \p OutLineTable.Rows and carry the input
+  /// row index each output row originated from (or an invalid-row
+  /// sentinel for manufactured end-of-range rows); if
+  /// \p RowIndexToSeqStartOffset is non-null, the emitter populates it
+  /// with an entry for each real row mapping input row index to the
+  /// byte offset of the DW_LNE_set_address that opens the output
+  /// sequence containing the row.
+  Error emitDebugLine(
+      const Triple &TargetTriple, const DWARFDebugLine::LineTable &OutLineTable,
+      ArrayRef<uint64_t> OrigRowIndices = {},
+      DenseMap<uint64_t, uint64_t> *RowIndexToSeqStartOffset = nullptr);
 
   /// Emit the .debug_str_offsets section for current unit.
   Error emitDebugStringOffsetSection();
diff --git a/llvm/lib/DWARFLinker/Parallel/DebugLineSectionEmitter.h b/llvm/lib/DWARFLinker/Parallel/DebugLineSectionEmitter.h
index d96e1b9301752..8e1e57907e4f6 100644
--- a/llvm/lib/DWARFLinker/Parallel/DebugLineSectionEmitter.h
+++ b/llvm/lib/DWARFLinker/Parallel/DebugLineSectionEmitter.h
@@ -27,7 +27,8 @@ class DebugLineSectionEmitter {
       : TheTriple(TheTriple), U(U) {}
 
   Error emit(const DWARFDebugLine::LineTable &LineTable,
-             DenseMap<uint64_t, uint64_t> *AddrToSeqStartOffset = nullptr) {
+             ArrayRef<uint64_t> OrigRowIndices = {},
+             DenseMap<uint64_t, uint64_t> *RowIndexToSeqStartOffset = nullptr) {
     // FIXME: remove dependence on MCDwarfLineAddr::encode.
     // As we reuse MCDwarfLineAddr::encode, we need to create/initialize
     // some MC* classes.
@@ -46,7 +47,8 @@ class DebugLineSectionEmitter {
     emitLineTablePrologue(LineTable.Prologue, OutSection);
 
     // Emit rows.
-    emitLineTableRows(LineTable, OutSection, AddrToSeqStartOffset);
+    emitLineTableRows(LineTable, OutSection, OrigRowIndices,
+                      RowIndexToSeqStartOffset);
     uint64_t OffsetAfterEnd = OutSection.OS.tell();
 
     // Update unit length field with actual length value.
@@ -295,7 +297,8 @@ class DebugLineSectionEmitter {
 
   void emitLineTableRows(
       const DWARFDebugLine::LineTable &LineTable, SectionDescriptor &Section,
-      DenseMap<uint64_t, uint64_t> *AddrToSeqStartOffset = nullptr) {
+      ArrayRef<uint64_t> OrigRowIndices = {},
+      DenseMap<uint64_t, uint64_t> *RowIndexToSeqStartOffset = nullptr) {
 
     MCDwarfLineTableParams Params;
     Params.DWARF2LineOpcodeBase = LineTable.Prologue.OpcodeBase;
@@ -324,11 +327,17 @@ class DebugLineSectionEmitter {
 
     unsigned RowsSinceLastSequence = 0;
     // Offset of the DW_LNE_set_address opcode that opens the sequence
-    // currently being emitted. Recorded for each row's address so that
-    // DW_AT_LLVM_stmt_sequence attributes can be resolved by address.
+    // currently being emitted. Recorded per input row index so that
+    // DW_AT_LLVM_stmt_sequence attributes can be resolved by row — which
+    // is collision-free under ICF, unlike keying by output address.
     uint64_t CurrentSeqStartOffset = 0;
+    constexpr uint64_t InvalidRowIndex = std::numeric_limits<uint64_t>::max();
+    assert(
+        (!RowIndexToSeqStartOffset ||
+         OrigRowIndices.size() == LineTable.Rows.size()) &&
+        "OrigRowIndices must be supplied alongside RowIndexToSeqStartOffset");
 
-    for (const DWARFDebugLine::Row &Row : LineTable.Rows) {
+    for (auto [Idx, Row] : llvm::enumerate(LineTable.Rows)) {
       int64_t AddressDelta;
       if (Address == -1ULL) {
         CurrentSeqStartOffset = Section.OS.tell();
@@ -342,8 +351,11 @@ class DebugLineSectionEmitter {
         AddressDelta =
             (Row.Address.Address - Address) / LineTable.Prologue.MinInstLength;
       }
-      if (AddrToSeqStartOffset)
-        (*AddrToSeqStartOffset)[Row.Address.Address] = CurrentSeqStartOffset;
+      if (RowIndexToSeqStartOffset) {
+        uint64_t InputRowIdx = OrigRowIndices[Idx];
+        if (InputRowIdx != InvalidRowIndex)
+          (*RowIndexToSeqStartOffset)[InputRowIdx] = CurrentSeqStartOffset;
+      }
 
       // FIXME: code copied and transformed from
       // MCDwarf.cpp::EmitDwarfLineTable. We should find a way to share this

>From 4ce80f11375df93a6498caaef1f4c014bdea822d Mon Sep 17 00:00:00 2001
From: Jonas Devlieghere <jonas at devlieghere.com>
Date: Mon, 4 May 2026 12:26:01 -0700
Subject: [PATCH 3/5] Early exit when OutUnit is a type unit

---
 .../Parallel/DIEAttributeCloner.cpp            | 18 ++++++++++++------
 1 file changed, 12 insertions(+), 6 deletions(-)

diff --git a/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp b/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp
index 55658c33895a8..f5e7812c599c7 100644
--- a/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp
+++ b/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp
@@ -514,19 +514,25 @@ size_t DIEAttributeCloner::cloneScalarAttr(
   } else if (AttrSpec.Attr == dwarf::DW_AT_declaration && Value)
     AttrInfo.IsDeclaration = true;
 
+  // DW_AT_LLVM_stmt_sequence refers to line info in this unit's
+  // .debug_line contribution, which only exists for compile units.
+  // DependencyTracker can route a module-scope subprogram to a type
+  // unit when ODR deduplication applies (see
+  // DependencyTracker.cpp: DW_TAG_subprogram case), so drop the
+  // attribute on that path — mirroring how cloneBlockAttr handles
+  // type-unit placement.
+  if (AttrSpec.Attr == dwarf::DW_AT_LLVM_stmt_sequence &&
+      !OutUnit.isCompileUnit())
+    return 0;
+
   auto Result =
       Generator.addScalarAttribute(AttrSpec.Attr, ResultingForm, Value);
   // Record DW_AT_LLVM_stmt_sequence so the attribute value can be
   // rewritten with the correct .debug_line offset after the line table
   // for this CU has been emitted. We also register a DebugOffsetPatch so
   // that the final-section offset of .debug_line gets added when the
-  // section is placed in the combined output. The assert encodes
-  // dsymutil's placement policy: subprograms (which carry
-  // DW_AT_LLVM_stmt_sequence) are placed in compile units, not type
-  // units, so this attribute should never appear on a type-unit DIE.
+  // section is placed in the combined output.
   if (AttrSpec.Attr == dwarf::DW_AT_LLVM_stmt_sequence) {
-    assert(OutUnit.isCompileUnit() &&
-           "DW_AT_LLVM_stmt_sequence on a non-compile-unit DIE");
     // Resolve the attribute's stmt-sequence offset to the index of the
     // referenced sequence's first row in the input .debug_line rows
     // vector, so that after line-table emission the attribute can be

>From a973c70418ad99949eb77f044eb0cc5c605c542d Mon Sep 17 00:00:00 2001
From: Jonas Devlieghere <jonas at devlieghere.com>
Date: Mon, 4 May 2026 12:33:10 -0700
Subject: [PATCH 4/5] Clear StmtSeqListAttributes

---
 llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp | 1 +
 1 file changed, 1 insertion(+)

diff --git a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp
index fbb0513f3243d..1e7616643fada 100644
--- a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp
+++ b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp
@@ -100,6 +100,7 @@ void CompileUnit::maybeResetToLoadedStage() {
   Abbreviations.clear();
   OutUnitDIE = nullptr;
   DebugAddrIndexMap.clear();
+  StmtSeqListAttributes.clear();
 
   llvm::fill(OutDieOffsetArray, 0);
   llvm::fill(TypeEntries, nullptr);

>From 82a229344a9f9d103ea4e3c3d5d506ab87386566 Mon Sep 17 00:00:00 2001
From: Jonas Devlieghere <jonas at devlieghere.com>
Date: Mon, 4 May 2026 12:46:04 -0700
Subject: [PATCH 5/5] Share buildStmtSeqOffsetToFirstRowIndex between linkers,
 add unit tests

---
 llvm/include/llvm/DWARFLinker/Utils.h         |  18 +++
 llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp  | 121 ++----------------
 .../Parallel/DIEAttributeCloner.cpp           |  15 +--
 .../Parallel/DWARFLinkerCompileUnit.cpp       |  46 +++----
 .../Parallel/DWARFLinkerCompileUnit.h         |  63 ++++-----
 llvm/lib/DWARFLinker/Utils.cpp                | 100 +++++++++++++++
 .../DWARFLinkerParallel/DWARFLinkerTest.cpp   | 118 +++++++++++++++++
 7 files changed, 312 insertions(+), 169 deletions(-)

diff --git a/llvm/include/llvm/DWARFLinker/Utils.h b/llvm/include/llvm/DWARFLinker/Utils.h
index 8bf5ea1025a1e..8bc45706107f7 100644
--- a/llvm/include/llvm/DWARFLinker/Utils.h
+++ b/llvm/include/llvm/DWARFLinker/Utils.h
@@ -9,8 +9,11 @@
 #ifndef LLVM_DWARFLINKER_UTILS_H
 #define LLVM_DWARFLINKER_UTILS_H
 
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/DenseMap.h"
 #include "llvm/ADT/SmallString.h"
 #include "llvm/ADT/Twine.h"
+#include "llvm/DebugInfo/DWARF/DWARFDebugLine.h"
 #include "llvm/Support/Error.h"
 #include "llvm/Support/FileSystem.h"
 #include "llvm/Support/Path.h"
@@ -18,6 +21,21 @@
 namespace llvm {
 namespace dwarf_linker {
 
+/// Build a map from an input DW_AT_LLVM_stmt_sequence byte offset to
+/// the first-row index (in \p LT.Rows) of the corresponding line-table
+/// sequence. Seeds the map from \p LT.Sequences (the DWARF parser's
+/// discovered sequences), then augments it by walking row boundaries
+/// (DW_LNE_end_sequence markers) and matching them against the sorted
+/// input offsets in \p SortedStmtSeqOffsets, using the parser's results
+/// as ground-truth anchors. This recovers sequences the parser may not
+/// have registered and keeps the classic and parallel DWARFLinkers in
+/// lockstep. Caller passes \p SortedStmtSeqOffsets sorted ascending
+/// and deduplicated.
+void buildStmtSeqOffsetToFirstRowIndex(
+    const DWARFDebugLine::LineTable &LT,
+    ArrayRef<uint64_t> SortedStmtSeqOffsets,
+    DenseMap<uint64_t, uint64_t> &SeqOffToFirstRow);
+
 /// This function calls \p Iteration() until it returns false.
 /// If number of iterations exceeds \p MaxCounter then an Error is returned.
 /// This function should be used for loops which assumed to have number of
diff --git a/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp b/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp
index 06c0bfb5d4a2f..95cddaa7684d4 100644
--- a/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp
+++ b/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp
@@ -473,113 +473,20 @@ void DWARFLinker::cleanupAuxiliarryData(LinkContext &Context) {
 
 static void constructSeqOffsettoOrigRowMapping(
     CompileUnit &Unit, const DWARFDebugLine::LineTable &LT,
-    DenseMap<uint64_t, unsigned> &SeqOffToOrigRow) {
-
-  // Use std::map for ordered iteration.
-  std::map<uint64_t, unsigned> LineTableMapping;
-
-  // First, trust the sequences that the DWARF parser did identify.
-  for (const DWARFDebugLine::Sequence &Seq : LT.Sequences)
-    LineTableMapping[Seq.StmtSeqOffset] = Seq.FirstRowIndex;
-
-  // Second, manually find sequence boundaries and match them to the
-  // sorted attributes to handle sequences the parser might have missed.
+    DenseMap<uint64_t, uint64_t> &SeqOffToOrigRow) {
+  // Collect this unit's DW_AT_LLVM_stmt_sequence attribute values
+  // (input offsets), sorted ascending and deduplicated, to drive the
+  // shared mapping builder.
   auto StmtAttrs = Unit.getStmtSeqListAttributes();
-  llvm::sort(StmtAttrs, [](const PatchLocation &A, const PatchLocation &B) {
-    return A.get() < B.get();
-  });
-
-  std::vector<unsigned> SeqStartRows;
-  SeqStartRows.push_back(0);
-  for (auto [I, Row] : llvm::enumerate(ArrayRef(LT.Rows).drop_back()))
-    if (Row.EndSequence)
-      SeqStartRows.push_back(I + 1);
-
-  // While SeqOffToOrigRow parsed from CU could be the ground truth,
-  // e.g.
-  //
-  // SeqOff     Row
-  // 0x08        9
-  // 0x14       15
-  //
-  // The StmtAttrs and SeqStartRows may not match perfectly, e.g.
-  //
-  // StmtAttrs  SeqStartRows
-  // 0x04        3
-  // 0x08        5
-  // 0x10        9
-  // 0x12       11
-  // 0x14       15
-  //
-  // In this case, we don't want to assign 5 to 0x08, since we know 0x08
-  // maps to 9. If we do a dummy 1:1 mapping 0x10 will be mapped to 9
-  // which is incorrect. The expected behavior is ignore 5, realign the
-  // table based on the result from the line table:
-  //
-  // StmtAttrs  SeqStartRows
-  // 0x04        3
-  //   --        5
-  // 0x08        9 <- LineTableMapping ground truth
-  // 0x10       11
-  // 0x12       --
-  // 0x14       15 <- LineTableMapping ground truth
-
-  ArrayRef StmtAttrsRef(StmtAttrs);
-  ArrayRef SeqStartRowsRef(SeqStartRows);
-
-  // Dummy last element to make sure StmtAttrsRef and SeqStartRowsRef always
-  // run out first.
-  constexpr uint64_t DummyKey = UINT64_MAX;
-  constexpr unsigned DummyVal = UINT32_MAX;
-  LineTableMapping[DummyKey] = DummyVal;
-
-  for (auto [NextSeqOff, NextRow] : LineTableMapping) {
-    // Explict capture to avoid capturing structured bindings and make C++17
-    // happy.
-    auto StmtAttrSmallerThanNext = [N = NextSeqOff](const PatchLocation &SA) {
-      return SA.get() < N;
-    };
-    auto SeqStartSmallerThanNext = [N = NextRow](const unsigned &Row) {
-      return Row < N;
-    };
-    // If both StmtAttrs and SeqStartRows points to value not in
-    // the LineTableMapping yet, we do a dummy one to one mapping and
-    // move the pointer.
-    while (!StmtAttrsRef.empty() && !SeqStartRowsRef.empty() &&
-           StmtAttrSmallerThanNext(StmtAttrsRef.front()) &&
-           SeqStartSmallerThanNext(SeqStartRowsRef.front())) {
-      SeqOffToOrigRow[StmtAttrsRef.consume_front().get()] =
-          SeqStartRowsRef.consume_front();
-    }
-    // One of the pointer points to the value at or past Next in the
-    // LineTableMapping, We move the pointer to re-align with the
-    // LineTableMapping
-    StmtAttrsRef = StmtAttrsRef.drop_while(StmtAttrSmallerThanNext);
-    SeqStartRowsRef = SeqStartRowsRef.drop_while(SeqStartSmallerThanNext);
-    // Use the LineTableMapping's result as the ground truth and move
-    // on.
-    if (NextSeqOff != DummyKey) {
-      SeqOffToOrigRow[NextSeqOff] = NextRow;
-    }
-    // Move the pointers if they are pointed at Next.
-    // It is possible that they point to later entries in LineTableMapping.
-    // Therefore we only increment the pointers after we validate they are
-    // pointing to the `Next` entry. e.g.
-    //
-    // LineTableMapping
-    // SeqOff      Row
-    // 0x08         9    <- NextSeqOff/NextRow
-    // 0x14        15
-    //
-    // StmtAttrs  SeqStartRows
-    // 0x14       13    <- StmtAttrsRef.front() / SeqStartRowsRef.front()
-    // 0x16       15
-    //  --        17
-    if (!StmtAttrsRef.empty() && StmtAttrsRef.front().get() == NextSeqOff)
-      StmtAttrsRef.consume_front();
-    if (!SeqStartRowsRef.empty() && SeqStartRowsRef.front() == NextRow)
-      SeqStartRowsRef.consume_front();
-  }
+  SmallVector<uint64_t> SortedOffsets;
+  SortedOffsets.reserve(StmtAttrs.size());
+  for (const PatchLocation &P : StmtAttrs)
+    SortedOffsets.push_back(P.get());
+  llvm::sort(SortedOffsets);
+  SortedOffsets.erase(llvm::unique(SortedOffsets), SortedOffsets.end());
+
+  dwarf_linker::buildStmtSeqOffsetToFirstRowIndex(LT, SortedOffsets,
+                                                  SeqOffToOrigRow);
 }
 
 std::pair<bool, std::optional<int64_t>>
@@ -2506,7 +2413,7 @@ Error DWARFLinker::DIECloner::generateLineTableForUnit(CompileUnit &Unit) {
                "must have an offset for each row");
 
         // Create a map of stmt sequence offsets to original row indices.
-        DenseMap<uint64_t, unsigned> SeqOffToOrigRow;
+        DenseMap<uint64_t, uint64_t> SeqOffToOrigRow;
         // The DWARF parser's discovery of sequences can be incomplete. To
         // ensure all DW_AT_LLVM_stmt_sequence attributes can be patched, we
         // build a map from both the parser's results and a manual
diff --git a/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp b/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp
index f5e7812c599c7..0f8fdc246d49c 100644
--- a/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp
+++ b/llvm/lib/DWARFLinker/Parallel/DIEAttributeCloner.cpp
@@ -533,15 +533,12 @@ size_t DIEAttributeCloner::cloneScalarAttr(
   // that the final-section offset of .debug_line gets added when the
   // section is placed in the combined output.
   if (AttrSpec.Attr == dwarf::DW_AT_LLVM_stmt_sequence) {
-    // Resolve the attribute's stmt-sequence offset to the index of the
-    // referenced sequence's first row in the input .debug_line rows
-    // vector, so that after line-table emission the attribute can be
-    // matched to the output sequence by input row index (immune to the
-    // address collisions that ICF would otherwise cause).
-    std::optional<uint64_t> InputFirstRowIndex =
-        InUnit.getStmtSeqFirstRowIndex(Value);
-    OutUnit.getAsCompileUnit()->noteStmtSeqListAttribute(&Result.first,
-                                                         InputFirstRowIndex);
+    // Record the attribute's raw input stmt-sequence offset. Resolution
+    // to a first-row index — including the boundary-walk fallback for
+    // sequences the DWARF parser may not have registered — happens in
+    // a post-cloning pass (buildStmtSeqOffsetToFirstRowIndex), so that
+    // matches the classic linker's behaviour.
+    OutUnit.getAsCompileUnit()->noteStmtSeqListAttribute(&Result.first, Value);
     DebugInfoOutputSection.notePatchWithOffsetUpdate(
         DebugOffsetPatch{
             AttrOutOffset,
diff --git a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp
index 1e7616643fada..17ee20c6f6dcc 100644
--- a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp
+++ b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.cpp
@@ -1583,7 +1583,9 @@ Error CompileUnit::cloneAndEmitLineTable(const Triple &TargetTriple) {
                                 &RowIndexToSeqStartOffset))
     return Err;
 
-  patchStmtSeqAttributes(RowIndexToSeqStartOffset);
+  DenseMap<uint64_t, uint64_t> SeqOffsetToFirstRowIndex =
+      buildStmtSeqOffsetToFirstRowIndex(*InputLineTable);
+  patchStmtSeqAttributes(SeqOffsetToFirstRowIndex, RowIndexToSeqStartOffset);
   return Error::success();
 }
 
@@ -1664,15 +1666,17 @@ void CompileUnit::filterLineTableRows(
 }
 
 void CompileUnit::patchStmtSeqAttributes(
+    const DenseMap<uint64_t, uint64_t> &SeqOffsetToFirstRowIndex,
     const DenseMap<uint64_t, uint64_t> &RowIndexToSeqStartOffset) {
   const uint64_t InvalidOffset = getFormParams().getDwarfMaxOffset();
 
   for (const CompileUnit::StmtSeqPatch &Patch : StmtSeqListAttributes) {
     uint64_t NewStmtSeq = InvalidOffset;
-    if (Patch.InputFirstRowIndex) {
-      auto It = RowIndexToSeqStartOffset.find(*Patch.InputFirstRowIndex);
-      if (It != RowIndexToSeqStartOffset.end())
-        NewStmtSeq = It->second;
+    auto RowIt = SeqOffsetToFirstRowIndex.find(Patch.InputStmtSeqOffset);
+    if (RowIt != SeqOffsetToFirstRowIndex.end()) {
+      auto OffIt = RowIndexToSeqStartOffset.find(RowIt->second);
+      if (OffIt != RowIndexToSeqStartOffset.end())
+        NewStmtSeq = OffIt->second;
     }
     // When resolution fails, the InvalidOffset sentinel must survive the
     // combination-time section-offset fixup. The patch applier preserves
@@ -1683,23 +1687,21 @@ void CompileUnit::patchStmtSeqAttributes(
   }
 }
 
-std::optional<uint64_t>
-CompileUnit::getStmtSeqFirstRowIndex(uint64_t StmtSeqOffset) {
-  if (!StmtSeqOffsetToFirstRowIndex) {
-    StmtSeqOffsetToFirstRowIndex.emplace();
-    if (const DWARFDebugLine::LineTable *InputLT =
-            getContaingFile().Dwarf->getLineTableForUnit(&getOrigUnit())) {
-      for (const DWARFDebugLine::Sequence &Seq : InputLT->Sequences) {
-        assert(Seq.FirstRowIndex < InputLT->Rows.size() &&
-               "sequence's first-row index out of range");
-        (*StmtSeqOffsetToFirstRowIndex)[Seq.StmtSeqOffset] = Seq.FirstRowIndex;
-      }
-    }
-  }
-  auto It = StmtSeqOffsetToFirstRowIndex->find(StmtSeqOffset);
-  if (It == StmtSeqOffsetToFirstRowIndex->end())
-    return std::nullopt;
-  return It->second;
+DenseMap<uint64_t, uint64_t> CompileUnit::buildStmtSeqOffsetToFirstRowIndex(
+    const DWARFDebugLine::LineTable &InputLineTable) const {
+  // Collect this CU's stmt-sequence attribute values (input offsets),
+  // sorted ascending and deduplicated.
+  SmallVector<uint64_t> StmtAttrs;
+  StmtAttrs.reserve(StmtSeqListAttributes.size());
+  for (const StmtSeqPatch &Patch : StmtSeqListAttributes)
+    StmtAttrs.push_back(Patch.InputStmtSeqOffset);
+  llvm::sort(StmtAttrs);
+  StmtAttrs.erase(llvm::unique(StmtAttrs), StmtAttrs.end());
+
+  DenseMap<uint64_t, uint64_t> Result;
+  dwarf_linker::buildStmtSeqOffsetToFirstRowIndex(InputLineTable, StmtAttrs,
+                                                  Result);
+  return Result;
 }
 
 void CompileUnit::insertLineSequence(std::vector<DWARFDebugLine::Row> &Seq,
diff --git a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h
index b87f051e78275..b3b39f3b8b96c 100644
--- a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h
+++ b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerCompileUnit.h
@@ -408,25 +408,16 @@ class alignas(8) CompileUnit : public DwarfUnit {
   const RangesTy &getFunctionRanges() const { return Ranges; }
 
   /// Record that a DW_AT_LLVM_stmt_sequence attribute on this unit
-  /// references the input line-table sequence whose first row sits at
-  /// \p InputFirstRowIndex in the input .debug_line Rows vector
-  /// (std::nullopt if the input offset couldn't be resolved). After the
-  /// line table for this unit has been emitted, \p V is rewritten to the
-  /// byte offset of the output sequence that contains the corresponding
-  /// output row (or to an invalid sentinel if the sequence wasn't kept).
-  /// Keying on row index rather than address avoids collisions when two
-  /// input sequences would relocate to the same output address (e.g. ICF).
-  void noteStmtSeqListAttribute(DIEValue *V,
-                                std::optional<uint64_t> InputFirstRowIndex) {
-    StmtSeqListAttributes.push_back({V, InputFirstRowIndex});
-  }
-
-  /// Return the input-side first-row index of the line-table sequence
-  /// whose header sits at \p StmtSeqOffset in this unit's input
-  /// .debug_line contribution, or std::nullopt if no such sequence was
-  /// recognised by the DWARF parser. Builds and caches a lookup table on
-  /// first call to keep stmt_sequence attribute resolution O(1).
-  std::optional<uint64_t> getStmtSeqFirstRowIndex(uint64_t StmtSeqOffset);
+  /// references the input line-table sequence whose header sits at
+  /// \p InputStmtSeqOffset. Resolution of that offset to an input
+  /// first-row index (via parser results plus a manual boundary-based
+  /// fallback) happens in a post-cloning pass, before \p V is rewritten
+  /// to the byte offset of the matching output sequence. Keying on row
+  /// index rather than address avoids collisions when two input
+  /// sequences would relocate to the same output address (e.g. ICF).
+  void noteStmtSeqListAttribute(DIEValue *V, uint64_t InputStmtSeqOffset) {
+    StmtSeqListAttributes.push_back({V, InputStmtSeqOffset});
+  }
 
   /// Clone and emit this compilation unit.
   Error
@@ -689,11 +680,27 @@ class alignas(8) CompileUnit : public DwarfUnit {
 
   /// Rewrite every DW_AT_LLVM_stmt_sequence DIEValue recorded on this
   /// unit with the local .debug_line offset of the output sequence
-  /// containing the corresponding input first row. The map is built
-  /// during line-table emission and is keyed by input row index.
+  /// containing the corresponding input first row.
+  /// \p SeqOffsetToFirstRowIndex maps an input stmt-sequence offset to
+  /// its first-row index (built by buildStmtSeqOffsetToFirstRowIndex so
+  /// that sequences missed by the DWARF parser are recovered from row
+  /// boundaries). \p RowIndexToSeqStartOffset maps an input first-row
+  /// index to the byte offset of the output DW_LNE_set_address that
+  /// opens the matching output sequence.
   void patchStmtSeqAttributes(
+      const DenseMap<uint64_t, uint64_t> &SeqOffsetToFirstRowIndex,
       const DenseMap<uint64_t, uint64_t> &RowIndexToSeqStartOffset);
 
+  /// Build a map from input stmt-sequence offset to the first-row index
+  /// of the corresponding sequence in \p InputLineTable. Seeds the map
+  /// from \p InputLineTable.Sequences (the DWARF parser's results), then
+  /// augments it by manually walking row boundaries and realigning them
+  /// against the recorded DW_AT_LLVM_stmt_sequence values so that
+  /// sequences missed by the parser still resolve. Mirrors the
+  /// classic DWARFLinker's constructSeqOffsettoOrigRowMapping.
+  DenseMap<uint64_t, uint64_t> buildStmtSeqOffsetToFirstRowIndex(
+      const DWARFDebugLine::LineTable &InputLineTable) const;
+
   /// Emits body for both macro sections.
   void emitMacroTableImpl(const DWARFDebugMacro *MacroTable,
                           uint64_t OffsetToMacroTable, bool hasDWARFv5Header);
@@ -767,21 +774,15 @@ class alignas(8) CompileUnit : public DwarfUnit {
 
   /// Recorded DW_AT_LLVM_stmt_sequence attributes for this unit. Each
   /// entry pairs the DIEValue holding the attribute with the input-side
-  /// first-row index of the referenced line-table sequence (empty if
-  /// the parser couldn't resolve the input offset); the value is
+  /// byte offset of the referenced line-table sequence. The value is
   /// rewritten with the matching output offset after the line table has
-  /// been emitted.
+  /// been emitted; resolution from input offset to input first-row
+  /// index (including the parser-miss fallback) happens at patch time.
   struct StmtSeqPatch {
     DIEValue *Value = nullptr;
-    std::optional<uint64_t> InputFirstRowIndex;
+    uint64_t InputStmtSeqOffset = 0;
   };
   SmallVector<StmtSeqPatch, 4> StmtSeqListAttributes;
-
-  /// Lazily-built lookup table from an input DW_AT_LLVM_stmt_sequence
-  /// byte offset to the index (within the input LineTable::Rows vector)
-  /// of the corresponding sequence's first row. Populated on the first
-  /// call to getStmtSeqFirstRowIndex().
-  std::optional<DenseMap<uint64_t, uint64_t>> StmtSeqOffsetToFirstRowIndex;
   std::mutex LabelsMutex;
 
   /// This field keeps current stage of overall compile unit processing.
diff --git a/llvm/lib/DWARFLinker/Utils.cpp b/llvm/lib/DWARFLinker/Utils.cpp
index 52508c998532d..d699b25e3c349 100644
--- a/llvm/lib/DWARFLinker/Utils.cpp
+++ b/llvm/lib/DWARFLinker/Utils.cpp
@@ -7,3 +7,103 @@
 //===----------------------------------------------------------------------===//
 
 #include "llvm/DWARFLinker/Utils.h"
+#include "llvm/ADT/STLExtras.h"
+#include <limits>
+#include <map>
+
+namespace llvm {
+namespace dwarf_linker {
+
+void buildStmtSeqOffsetToFirstRowIndex(
+    const DWARFDebugLine::LineTable &LT,
+    ArrayRef<uint64_t> SortedStmtSeqOffsets,
+    DenseMap<uint64_t, uint64_t> &SeqOffToFirstRow) {
+  // Use std::map for ordered iteration by input stmt-sequence offset.
+  std::map<uint64_t, uint64_t> LineTableMapping;
+  for (const DWARFDebugLine::Sequence &Seq : LT.Sequences)
+    LineTableMapping[Seq.StmtSeqOffset] = Seq.FirstRowIndex;
+
+  if (LT.Rows.empty()) {
+    for (const auto &[Off, Row] : LineTableMapping)
+      SeqOffToFirstRow[Off] = Row;
+    return;
+  }
+
+  // Row indices that look like sequence starts: row 0, plus every row
+  // immediately following an end_sequence marker.
+  SmallVector<uint64_t> SeqStartRows;
+  SeqStartRows.push_back(0);
+  for (auto [I, Row] : llvm::enumerate(ArrayRef(LT.Rows).drop_back()))
+    if (Row.EndSequence)
+      SeqStartRows.push_back(I + 1);
+
+  ArrayRef<uint64_t> StmtAttrsRef(SortedStmtSeqOffsets);
+  ArrayRef<uint64_t> SeqStartRowsRef(SeqStartRows);
+
+  // While SeqOffToFirstRow parsed from LT could be the ground truth, e.g.
+  //
+  // SeqOff     Row
+  // 0x08        9
+  // 0x14       15
+  //
+  // The StmtAttrs and SeqStartRows may not match perfectly, e.g.
+  //
+  // StmtAttrs  SeqStartRows
+  // 0x04        3
+  // 0x08        5
+  // 0x10        9
+  // 0x12       11
+  // 0x14       15
+  //
+  // In this case, we don't want to assign 5 to 0x08, since we know 0x08
+  // maps to 9. If we do a dummy 1:1 mapping 0x10 will be mapped to 9
+  // which is incorrect. The expected behavior is ignore 5, realign the
+  // table based on the result from the line table:
+  //
+  // StmtAttrs  SeqStartRows
+  // 0x04        3
+  //   --        5
+  // 0x08        9 <- LineTableMapping ground truth
+  // 0x10       11
+  // 0x12       --
+  // 0x14       15 <- LineTableMapping ground truth
+
+  // Dummy trailing anchor so both refs always drain before we run out
+  // of map entries to walk.
+  constexpr uint64_t DummyKey = std::numeric_limits<uint64_t>::max();
+  constexpr uint64_t DummyVal = std::numeric_limits<uint64_t>::max();
+  LineTableMapping[DummyKey] = DummyVal;
+
+  for (auto [NextSeqOff, NextRow] : LineTableMapping) {
+    auto StmtAttrSmallerThanNext = [N = NextSeqOff](uint64_t SA) {
+      return SA < N;
+    };
+    auto SeqStartSmallerThanNext = [N = NextRow](uint64_t Row) {
+      return Row < N;
+    };
+    // While both lists still point strictly before the next anchor,
+    // pair them up 1:1 — this captures sequences the parser missed.
+    while (!StmtAttrsRef.empty() && !SeqStartRowsRef.empty() &&
+           StmtAttrSmallerThanNext(StmtAttrsRef.front()) &&
+           SeqStartSmallerThanNext(SeqStartRowsRef.front())) {
+      SeqOffToFirstRow[StmtAttrsRef.consume_front()] =
+          SeqStartRowsRef.consume_front();
+    }
+    // Either list may now be ahead of or at the anchor: drop entries we
+    // can't safely pair, then use the parser's (NextSeqOff,NextRow)
+    // mapping as ground truth.
+    StmtAttrsRef = StmtAttrsRef.drop_while(StmtAttrSmallerThanNext);
+    SeqStartRowsRef = SeqStartRowsRef.drop_while(SeqStartSmallerThanNext);
+    if (NextSeqOff != DummyKey)
+      SeqOffToFirstRow[NextSeqOff] = NextRow;
+    // Advance each list past the anchor only if it was pointing exactly
+    // at it.
+    if (!StmtAttrsRef.empty() && StmtAttrsRef.front() == NextSeqOff)
+      StmtAttrsRef = StmtAttrsRef.drop_front();
+    if (!SeqStartRowsRef.empty() && SeqStartRowsRef.front() == NextRow)
+      SeqStartRowsRef = SeqStartRowsRef.drop_front();
+  }
+}
+
+} // namespace dwarf_linker
+} // namespace llvm
diff --git a/llvm/unittests/DWARFLinkerParallel/DWARFLinkerTest.cpp b/llvm/unittests/DWARFLinkerParallel/DWARFLinkerTest.cpp
index 50c91396d6a1c..0c415b98ffbe1 100644
--- a/llvm/unittests/DWARFLinkerParallel/DWARFLinkerTest.cpp
+++ b/llvm/unittests/DWARFLinkerParallel/DWARFLinkerTest.cpp
@@ -6,7 +6,9 @@
 //
 //===----------------------------------------------------------------------===//
 
+#include "llvm/ADT/DenseMap.h"
 #include "llvm/DWARFLinker/Utils.h"
+#include "llvm/DebugInfo/DWARF/DWARFDebugLine.h"
 #include "gtest/gtest.h"
 
 using namespace llvm;
@@ -32,4 +34,120 @@ TEST(DWARFLinker, PathTest) {
   EXPECT_FALSE(isInToolchainDir("/Foo/not-an.xctoolchain/Bar/Baz"));
 }
 
+// Helpers for building DWARFDebugLine::LineTable fixtures. Only the fields
+// that buildStmtSeqOffsetToFirstRowIndex inspects are populated; everything
+// else defaults to Row's/Sequence's own defaults.
+namespace {
+void addRows(DWARFDebugLine::LineTable &LT, unsigned Count,
+             ArrayRef<unsigned> EndSequenceIndices) {
+  for (unsigned I = 0; I < Count; ++I)
+    LT.Rows.emplace_back();
+  for (unsigned I : EndSequenceIndices)
+    LT.Rows[I].EndSequence = 1;
+}
+
+void addParsedSequence(DWARFDebugLine::LineTable &LT, uint64_t StmtSeqOffset,
+                       unsigned FirstRowIndex) {
+  DWARFDebugLine::Sequence S;
+  S.StmtSeqOffset = StmtSeqOffset;
+  S.FirstRowIndex = FirstRowIndex;
+  LT.Sequences.push_back(S);
+}
+} // namespace
+
+TEST(DWARFLinker, BuildStmtSeqOffsetToFirstRowIndex_ParserOnly) {
+  // Single parser-registered sequence covering rows [0..3). The attribute
+  // at offset 0x10 resolves straight from LT.Sequences.
+  DWARFDebugLine::LineTable LT;
+  addRows(LT, /*Count=*/3, /*EndSequenceIndices=*/{2});
+  addParsedSequence(LT, /*StmtSeqOffset=*/0x10, /*FirstRowIndex=*/0);
+
+  DenseMap<uint64_t, uint64_t> Result;
+  buildStmtSeqOffsetToFirstRowIndex(LT, /*SortedStmtSeqOffsets=*/{0x10},
+                                    Result);
+
+  EXPECT_EQ(Result.lookup(0x10), 0u);
+  EXPECT_EQ(Result.size(), 1u);
+}
+
+TEST(DWARFLinker, BuildStmtSeqOffsetToFirstRowIndex_ParserMissFallback) {
+  // Two real sequences in the table (rows [0..3) and [3..5)), but the
+  // parser only registered the second one. The fallback must recover
+  // the first from the end_sequence boundary at row 2.
+  DWARFDebugLine::LineTable LT;
+  addRows(LT, /*Count=*/5, /*EndSequenceIndices=*/{2, 4});
+  addParsedSequence(LT, /*StmtSeqOffset=*/0x20, /*FirstRowIndex=*/3);
+
+  DenseMap<uint64_t, uint64_t> Result;
+  buildStmtSeqOffsetToFirstRowIndex(LT, /*SortedStmtSeqOffsets=*/{0x10, 0x20},
+                                    Result);
+
+  EXPECT_EQ(Result.lookup(0x10), 0u); // recovered from row boundaries
+  EXPECT_EQ(Result.lookup(0x20), 3u); // parser-registered ground truth
+}
+
+TEST(DWARFLinker, BuildStmtSeqOffsetToFirstRowIndex_RealignIgnoresStale) {
+  // SeqStartRows is seeded with row 0 and then every row immediately
+  // following an end_sequence marker. For 16 rows with end_sequence at
+  // {2, 4, 8, 10, 14} that's {0, 3, 5, 9, 11, 15}.
+  //
+  //   StmtAttrs    {0x04, 0x08, 0x10, 0x12, 0x14}
+  //   SeqStartRows {0,    3,    5,    9,    11,   15}
+  //   Parser       {0x08 -> 9, 0x14 -> 15}
+  //
+  // Walk:
+  //   anchor (0x08, 9): (0x04, 0) < anchor  -> pair  0x04 -> 0
+  //                     0x08 not < 0x08     -> exit while
+  //                     drop seq-starts < 9 -> {9, 11, 15}
+  //                     ground truth        ->      0x08 -> 9
+  //   anchor (0x14, 15): (0x10, 11) < anchor -> pair 0x10 -> 11
+  //                      (0x12, 15) not < 15 -> exit while
+  //                      drop stmt-attrs<0x14-> {0x14}
+  //                      ground truth        ->      0x14 -> 15
+  //
+  // 0x12 is dropped because no safe row to pair it with remains.
+  DWARFDebugLine::LineTable LT;
+  addRows(LT, /*Count=*/16, /*EndSequenceIndices=*/{2, 4, 8, 10, 14});
+  addParsedSequence(LT, 0x08, 9);
+  addParsedSequence(LT, 0x14, 15);
+
+  DenseMap<uint64_t, uint64_t> Result;
+  buildStmtSeqOffsetToFirstRowIndex(LT, {0x04, 0x08, 0x10, 0x12, 0x14}, Result);
+
+  EXPECT_EQ(Result.lookup(0x04), 0u);
+  EXPECT_EQ(Result.lookup(0x08), 9u);
+  EXPECT_EQ(Result.lookup(0x10), 11u);
+  EXPECT_FALSE(Result.contains(0x12));
+  EXPECT_EQ(Result.lookup(0x14), 15u);
+}
+
+TEST(DWARFLinker, BuildStmtSeqOffsetToFirstRowIndex_EmptyRows) {
+  // When LT.Rows is empty the builder just copies the parser-registered
+  // sequences over and returns — no fallback work possible.
+  DWARFDebugLine::LineTable LT;
+  addParsedSequence(LT, 0x10, 0);
+  addParsedSequence(LT, 0x20, 42);
+
+  DenseMap<uint64_t, uint64_t> Result;
+  buildStmtSeqOffsetToFirstRowIndex(LT, {0x10, 0x20, 0x30}, Result);
+
+  EXPECT_EQ(Result.lookup(0x10), 0u);
+  EXPECT_EQ(Result.lookup(0x20), 42u);
+  EXPECT_FALSE(Result.contains(0x30)); // no way to recover without rows
+}
+
+TEST(DWARFLinker, BuildStmtSeqOffsetToFirstRowIndex_NoAttributes) {
+  // No stmt-seq attributes to resolve: the builder still seeds the map
+  // from parser-registered sequences (harmless, helps shared callers).
+  DWARFDebugLine::LineTable LT;
+  addRows(LT, /*Count=*/3, /*EndSequenceIndices=*/{2});
+  addParsedSequence(LT, 0x10, 0);
+
+  DenseMap<uint64_t, uint64_t> Result;
+  buildStmtSeqOffsetToFirstRowIndex(LT, /*SortedStmtSeqOffsets=*/{}, Result);
+
+  EXPECT_EQ(Result.lookup(0x10), 0u);
+  EXPECT_EQ(Result.size(), 1u);
+}
+
 } // anonymous namespace



More information about the llvm-commits mailing list