Skip to content

[DWARFLinker] Adjust DW_AT_LLVM_stmt_sequence for rewritten line tables #128953

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 13, 2025

Conversation

alx32
Copy link
Contributor

@alx32 alx32 commented Feb 26, 2025

Summary:
This update adds handling for DW_AT_LLVM_stmt_sequence attributes in the DWARF linker. These attributes point to rows in the line table, which gets rewritten during linking. Since the row positions change, the offsets in these attributes need to be updated to match the new layout in the output .debug_line section. The changes add new data structures and tweak existing functions to track and fix these attributes.

Background
In #110192 we added support to clang to generate the DW_AT_LLVM_stmt_sequence attribute for DW_TAG_subprogram's. Corresponding RFC: New DWARF Attribute for Symbolication of Merged Functions. This attribute holds a label pointing to the offset in the line table where the function's line entries begin.

Implementation details:
Here’s what’s changed in the code:

  • New Tracking in CompileUnit: A StmtSeqListAttributes vector is added to the CompileUnit class. It stores the locations where DW_AT_LLVM_stmt_sequence attributes need to be patched, recorded when cloning DIEs (debug info entries).
  • Updated emitLineTableForUnit Function: This function now has an optional RowOffsets parameter. It collects the byte offsets of each row in the output line table. We only need to use this functionality if DW_AT_LLVM_stmt_sequence attributes are present in the unit.
  • Row Tracking with TrackedRow: A TrackedRow struct keeps track of each input row’s original index and whether it starts a sequence in the output table. This links old rows to their new positions in the rewritten line table. Several implementations were considered and prototyped here, but so far this has proven the simplest and cleanest approach.
  • Patching Step: After the line table is written, the linker uses the data in TrackedRow's objects and RowOffsets array to update the DW_AT_LLVM_stmt_sequence attributes with the correct offsets.

@alx32 alx32 force-pushed the 21_dwarf_linker_stmt_seq branch 2 times, most recently from 563ab54 to 3840c2e Compare February 26, 2025 23:30
@alx32 alx32 marked this pull request as ready for review February 26, 2025 23:31
@alx32 alx32 requested a review from JDevlieghere as a code owner February 26, 2025 23:31
@llvmbot
Copy link
Member

llvmbot commented Feb 26, 2025

@llvm/pr-subscribers-debuginfo

Author: None (alx32)

Changes

Summary:
This update adds handling for DW_AT_LLVM_stmt_sequence attributes in the DWARF linker. These attributes point to rows in the line table, which gets rewritten during linking. Since the row positions change, the offsets in these attributes need to be updated to match the new layout in the output .debug_line section. The changes add new data structures and tweak existing functions to track and fix these attributes.

Background
In #110192 we added support to clang to generate the DW_AT_LLVM_stmt_sequence attribute for DW_TAG_subprogram's. Corresponding RFC: New DWARF Attribute for Symbolication of Merged Functions. This attribute holds a label pointing to the offset in the line table where the function's line entries begin.

Implementation details:
Here’s what’s changed in the code:

  • New Tracking in CompileUnit: A StmtSeqListAttributes vector is added to the CompileUnit class. It stores the locations where DW_AT_LLVM_stmt_sequence attributes need to be patched, recorded when cloning DIEs (debug info entries).
  • Updated emitLineTableForUnit Function: This function now has an optional RowOffsets parameter. It collects the byte offsets of each row in the output line table. We only need to use this functionality if DW_AT_LLVM_stmt_sequence attributes are present in the unit.
  • Row Tracking with TrackedRow: A TrackedRow struct keeps track of each input row’s original index and whether it starts a sequence in the output table. This links old rows to their new positions in the rewritten line table. Several implementations were considered and prototyped here, but so far this has proven the simplest and cleanest approach.
  • Patching Step: After the line table is written, the linker uses the data in TrackedRow's objects and RowOffsets array to update the DW_AT_LLVM_stmt_sequence attributes with the correct offsets.

Patch is 21.13 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/128953.diff

9 Files Affected:

  • (modified) llvm/include/llvm/DWARFLinker/Classic/DWARFLinker.h (+7-4)
  • (modified) llvm/include/llvm/DWARFLinker/Classic/DWARFLinkerCompileUnit.h (+17)
  • (modified) llvm/include/llvm/DWARFLinker/Classic/DWARFStreamer.h (+9-5)
  • (modified) llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp (+124-29)
  • (modified) llvm/lib/DWARFLinker/Classic/DWARFLinkerCompileUnit.cpp (+4)
  • (modified) llvm/lib/DWARFLinker/Classic/DWARFStreamer.cpp (+9-3)
  • (added) llvm/test/tools/dsymutil/ARM/stmt-seq-macho.test (+74)
  • (added) llvm/test/tools/dsymutil/Inputs/private/tmp/stmt_seq/stmt_seq_macho.exe ()
  • (added) llvm/test/tools/dsymutil/Inputs/private/tmp/stmt_seq/stmt_seq_macho.o ()
diff --git a/llvm/include/llvm/DWARFLinker/Classic/DWARFLinker.h b/llvm/include/llvm/DWARFLinker/Classic/DWARFLinker.h
index b1d3f03394f5e..3608e7821bbc4 100644
--- a/llvm/include/llvm/DWARFLinker/Classic/DWARFLinker.h
+++ b/llvm/include/llvm/DWARFLinker/Classic/DWARFLinker.h
@@ -122,10 +122,13 @@ class DwarfEmitter {
                              const AddressRanges &LinkedRanges) = 0;
 
   /// Emit specified \p LineTable into .debug_line table.
-  virtual void emitLineTableForUnit(const DWARFDebugLine::LineTable &LineTable,
-                                    const CompileUnit &Unit,
-                                    OffsetsStringPool &DebugStrPool,
-                                    OffsetsStringPool &DebugLineStrPool) = 0;
+  /// The optional parameter RowOffsets, if provided, will be populated with the
+  /// offsets of each line table row in the output .debug_line section.
+  virtual void
+  emitLineTableForUnit(const DWARFDebugLine::LineTable &LineTable,
+                       const CompileUnit &Unit, OffsetsStringPool &DebugStrPool,
+                       OffsetsStringPool &DebugLineStrPool,
+                       std::vector<uint64_t> *RowOffsets = nullptr) = 0;
 
   /// Emit the .debug_pubnames contribution for \p Unit.
   virtual void emitPubNamesForUnit(const CompileUnit &Unit) = 0;
diff --git a/llvm/include/llvm/DWARFLinker/Classic/DWARFLinkerCompileUnit.h b/llvm/include/llvm/DWARFLinker/Classic/DWARFLinkerCompileUnit.h
index cdb6f4a4443ab..9fae62a62e442 100644
--- a/llvm/include/llvm/DWARFLinker/Classic/DWARFLinkerCompileUnit.h
+++ b/llvm/include/llvm/DWARFLinker/Classic/DWARFLinkerCompileUnit.h
@@ -57,6 +57,7 @@ struct PatchLocation {
 
 using RngListAttributesTy = SmallVector<PatchLocation>;
 using LocListAttributesTy = SmallVector<PatchLocation>;
+using StmtSeqListAttributesTy = SmallVector<PatchLocation>;
 
 /// Stores all information relating to a compile unit, be it in its original
 /// instance in the object file to its brand new cloned and generated DIE tree.
@@ -175,6 +176,12 @@ class CompileUnit {
     return LocationAttributes;
   }
 
+  // Provide access to the list of DW_AT_LLVM_stmt_sequence attributes that may
+  // need to be patched
+  const StmtSeqListAttributesTy &getStmtSeqListAttributes() const {
+    return StmtSeqListAttributes;
+  }
+
   /// Mark every DIE in this unit as kept. This function also
   /// marks variables as InDebugMap so that they appear in the
   /// reconstructed accelerator tables.
@@ -210,6 +217,10 @@ class CompileUnit {
   /// debug_loc section.
   void noteLocationAttribute(PatchLocation Attr);
 
+  // Record that the given DW_AT_LLVM_stmt_sequence attribute may need to be
+  // patched later
+  void noteStmtSeqListAttribute(PatchLocation Attr);
+
   /// Add a name accelerator entry for \a Die with \a Name.
   void addNamespaceAccelerator(const DIE *Die, DwarfStringPoolEntryRef Name);
 
@@ -309,6 +320,12 @@ class CompileUnit {
   /// location expression.
   LocListAttributesTy LocationAttributes;
 
+  // List of DW_AT_LLVM_stmt_sequence attributes that may need to be patched
+  // after the dwarf linker rewrites the line table. During line table rewrite
+  // the line table format might change, so we have to patch any offsets that
+  // reference its contents.
+  StmtSeqListAttributesTy StmtSeqListAttributes;
+
   /// Accelerator entries for the unit, both for the pub*
   /// sections and the apple* ones.
   /// @{
diff --git a/llvm/include/llvm/DWARFLinker/Classic/DWARFStreamer.h b/llvm/include/llvm/DWARFLinker/Classic/DWARFStreamer.h
index e7a1a3cd838c2..40740a3f2210b 100644
--- a/llvm/include/llvm/DWARFLinker/Classic/DWARFStreamer.h
+++ b/llvm/include/llvm/DWARFLinker/Classic/DWARFStreamer.h
@@ -149,10 +149,13 @@ class DwarfStreamer : public DwarfEmitter {
   }
 
   /// Emit .debug_line table entry for specified \p LineTable
-  void emitLineTableForUnit(const DWARFDebugLine::LineTable &LineTable,
-                            const CompileUnit &Unit,
-                            OffsetsStringPool &DebugStrPool,
-                            OffsetsStringPool &DebugLineStrPool) override;
+  /// The optional parameter RowOffsets, if provided, will be populated with the
+  /// offsets of each line table row in the output .debug_line section.
+  void
+  emitLineTableForUnit(const DWARFDebugLine::LineTable &LineTable,
+                       const CompileUnit &Unit, OffsetsStringPool &DebugStrPool,
+                       OffsetsStringPool &DebugLineStrPool,
+                       std::vector<uint64_t> *RowOffsets = nullptr) override;
 
   uint64_t getLineSectionSize() const override { return LineSectionSize; }
 
@@ -266,7 +269,8 @@ class DwarfStreamer : public DwarfEmitter {
       const DWARFDebugLine::Prologue &P, OffsetsStringPool &DebugStrPool,
       OffsetsStringPool &DebugLineStrPool);
   void emitLineTableRows(const DWARFDebugLine::LineTable &LineTable,
-                         MCSymbol *LineEndSym, unsigned AddressByteSize);
+                         MCSymbol *LineEndSym, unsigned AddressByteSize,
+                         std::vector<uint64_t> *RowOffsets = nullptr);
   void emitIntOffset(uint64_t Offset, dwarf::DwarfFormat Format,
                      uint64_t &SectionSize);
   void emitLabelDifference(const MCSymbol *Hi, const MCSymbol *Lo,
diff --git a/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp b/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp
index d2b3561ee1c80..db0684e56e0c0 100644
--- a/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp
+++ b/llvm/lib/DWARFLinker/Classic/DWARFLinker.cpp
@@ -1447,6 +1447,18 @@ unsigned DWARFLinker::DIECloner::cloneScalarAttribute(
         ->sizeOf(Unit.getOrigUnit().getFormParams());
   }
 
+  if (AttrSpec.Attr == dwarf::DW_AT_LLVM_stmt_sequence) {
+    // If needed, we'll patch this sec_offset later with the correct offset.
+    auto Patch = Die.addValue(DIEAlloc, dwarf::Attribute(AttrSpec.Attr),
+                              dwarf::DW_FORM_sec_offset,
+                              DIEInteger(*Val.getAsSectionOffset()));
+
+    // Record this patch location so that it can be fixed up later.
+    Unit.noteStmtSeqListAttribute(Patch);
+
+    return Unit.getOrigUnit().getFormParams().getDwarfOffsetByteSize();
+  }
+
   if (LLVM_UNLIKELY(Linker.Options.Update)) {
     if (auto OptionalValue = Val.getAsUnsignedConstant())
       Value = *OptionalValue;
@@ -2081,29 +2093,43 @@ void DWARFLinker::DIECloner::emitDebugAddrSection(
   Emitter->emitDwarfDebugAddrsFooter(Unit, EndLabel);
 }
 
+/// A helper struct to help keep track of the association between the input and
+/// output rows during line table rewriting. This is used to patch
+/// DW_AT_LLVM_stmt_sequence attributes, which reference a particular line table
+/// row.
+struct TrackedRow {
+  DWARFDebugLine::Row Row;
+  size_t OriginalRowIndex;
+  bool isStartSeqInOutput;
+};
+
 /// Insert the new line info sequence \p Seq into the current
 /// set of already linked line info \p Rows.
-static void insertLineSequence(std::vector<DWARFDebugLine::Row> &Seq,
-                               std::vector<DWARFDebugLine::Row> &Rows) {
+static void insertLineSequence(std::vector<TrackedRow> &Seq,
+                               std::vector<TrackedRow> &Rows) {
   if (Seq.empty())
     return;
 
-  if (!Rows.empty() && Rows.back().Address < Seq.front().Address) {
+  // Mark the first row in Seq to indicate it is the start of a sequence
+  // in the output line table.
+  Seq.front().isStartSeqInOutput = true;
+
+  if (!Rows.empty() && Rows.back().Row.Address < Seq.front().Row.Address) {
     llvm::append_range(Rows, Seq);
     Seq.clear();
     return;
   }
 
-  object::SectionedAddress Front = Seq.front().Address;
+  object::SectionedAddress Front = Seq.front().Row.Address;
   auto InsertPoint = partition_point(
-      Rows, [=](const DWARFDebugLine::Row &O) { return O.Address < Front; });
+      Rows, [=](const TrackedRow &O) { return O.Row.Address < Front; });
 
   // FIXME: this only removes the unneeded end_sequence if the
   // sequences have been inserted in order. Using a global sort like
-  // described in generateLineTableForUnit() and delaying the end_sequene
+  // described in generateLineTableForUnit() and delaying the end_sequence
   // elimination to emitLineTableForUnit() we can get rid of all of them.
-  if (InsertPoint != Rows.end() && InsertPoint->Address == Front &&
-      InsertPoint->EndSequence) {
+  if (InsertPoint != Rows.end() && InsertPoint->Row.Address == Front &&
+      InsertPoint->Row.EndSequence) {
     *InsertPoint = Seq.front();
     Rows.insert(InsertPoint + 1, Seq.begin() + 1, Seq.end());
   } else {
@@ -2171,14 +2197,24 @@ void DWARFLinker::DIECloner::generateLineTableForUnit(CompileUnit &Unit) {
         LineTable.Rows.clear();
 
       LineTable.Sequences = LT->Sequences;
+
+      Emitter->emitLineTableForUnit(LineTable, Unit, DebugStrPool,
+                                    DebugLineStrPool);
     } else {
-      // This vector is the output line table.
-      std::vector<DWARFDebugLine::Row> NewRows;
-      NewRows.reserve(LT->Rows.size());
+      // Create TrackedRow objects for all input rows.
+      std::vector<TrackedRow> AllTrackedRows;
+      AllTrackedRows.reserve(LT->Rows.size());
+      for (size_t i = 0; i < LT->Rows.size(); i++)
+        AllTrackedRows.emplace_back(TrackedRow{LT->Rows[i], i, false});
+
+      // This vector is the output line table (still in TrackedRow form).
+      std::vector<TrackedRow> NewRows;
+      NewRows.reserve(AllTrackedRows.size());
 
       // Current sequence of rows being extracted, before being inserted
       // in NewRows.
-      std::vector<DWARFDebugLine::Row> Seq;
+      std::vector<TrackedRow> Seq;
+      Seq.reserve(AllTrackedRows.size());
 
       const auto &FunctionRanges = Unit.getFunctionRanges();
       std::optional<AddressRangeValuePair> CurrRange;
@@ -2194,27 +2230,30 @@ void DWARFLinker::DIECloner::generateLineTableForUnit(CompileUnit &Unit) {
 
       // Iterate over the object file line info and extract the sequences
       // that correspond to linked functions.
-      for (DWARFDebugLine::Row Row : LT->Rows) {
+      for (size_t i = 0; i < AllTrackedRows.size(); i++) {
+        TrackedRow TR = AllTrackedRows[i];
+
         // Check whether we stepped out of the range. The range is
-        // half-open, but consider accept the end address of the range if
+        // half-open, but consider accepting 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
+        if (!CurrRange || !CurrRange->Range.contains(TR.Row.Address.Address)) {
+          // We just stepped out of a known range. Insert an 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);
+          CurrRange =
+              FunctionRanges.getRangeThatContains(TR.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;
+            NextLine.Row.Address.Address = StopAddress;
+            NextLine.Row.EndSequence = 1;
+            NextLine.Row.PrologueEnd = 0;
+            NextLine.Row.BasicBlock = 0;
+            NextLine.Row.EpilogueBegin = 0;
             Seq.push_back(NextLine);
             insertLineSequence(Seq, NewRows);
           }
@@ -2224,22 +2263,78 @@ void DWARFLinker::DIECloner::generateLineTableForUnit(CompileUnit &Unit) {
         }
 
         // Ignore empty sequences.
-        if (Row.EndSequence && Seq.empty())
+        if (TR.Row.EndSequence && Seq.empty())
           continue;
 
         // Relocate row address and add it to the current sequence.
-        Row.Address.Address += CurrRange->Value;
-        Seq.emplace_back(Row);
+        TR.Row.Address.Address += CurrRange->Value;
+        Seq.push_back(TR);
 
-        if (Row.EndSequence)
+        if (TR.Row.EndSequence)
           insertLineSequence(Seq, NewRows);
       }
 
-      LineTable.Rows = std::move(NewRows);
+      // Materialize the tracked rows into final DWARFDebugLine::Row objects.
+      LineTable.Rows.clear();
+      LineTable.Rows.reserve(NewRows.size());
+      for (auto &TR : NewRows)
+        LineTable.Rows.push_back(TR.Row);
+
+      // Use OutputRowOffsets to store the offsets of each line table row in the
+      // output .debug_line section.
+      std::vector<uint64_t> OutputRowOffsets;
+
+      // The unit might not have any DW_AT_LLVM_stmt_sequence attributes, so use
+      // hasStmtSeq to skip the patching logic.
+      bool hasStmtSeq = Unit.getStmtSeqListAttributes().size() > 0;
+      Emitter->emitLineTableForUnit(LineTable, Unit, DebugStrPool,
+                                    DebugLineStrPool,
+                                    hasStmtSeq ? &OutputRowOffsets : nullptr);
+
+      if (hasStmtSeq) {
+        assert(OutputRowOffsets.size() == NewRows.size() &&
+               "OutputRowOffsets size mismatch");
+
+        // Create a map of stmt sequence offsets to original row indices.
+        DenseMap<uint64_t, unsigned> SeqOffToOrigRow;
+        for (const DWARFDebugLine::Sequence &Seq : LT->Sequences)
+          SeqOffToOrigRow[Seq.StmtSeqOffset] = Seq.FirstRowIndex;
+
+        // Create a map of original row indices to new row indices.
+        DenseMap<size_t, size_t> OrigRowToNewRow;
+        for (size_t i = 0; i < NewRows.size(); ++i)
+          OrigRowToNewRow[NewRows[i].OriginalRowIndex] = i;
+
+        // Patch DW_AT_LLVM_stmt_sequence attributes in the compile unit DIE
+        // with the correct offset into the .debug_line section.
+        for (const auto &StmtSeq : Unit.getStmtSeqListAttributes()) {
+          uint64_t OrigStmtSeq = StmtSeq.get();
+          // 1. Get the original row index from the stmt list offset.
+          auto OrigRowIter = SeqOffToOrigRow.find(OrigStmtSeq);
+          assert(OrigRowIter != SeqOffToOrigRow.end() &&
+                 "Stmt list offset not found in sequence offsets map");
+          size_t OrigRowIndex = OrigRowIter->second;
+
+          // 2. Get the new row index from the original row index.
+          auto NewRowIter = OrigRowToNewRow.find(OrigRowIndex);
+          if (NewRowIter == OrigRowToNewRow.end()) {
+            // If the original row index is not found in the map, update the
+            // stmt_sequence attribute to the 'invalid offset' magic value.
+            StmtSeq.set(UINT64_MAX);
+            continue;
+          }
+
+          // 3. Get the offset of the new row in the output .debug_line section.
+          assert(NewRowIter->second < OutputRowOffsets.size() &&
+                 "New row index out of bounds");
+          uint64_t NewStmtSeqOffset = OutputRowOffsets[NewRowIter->second];
+
+          // 4. Patch the stmt_list attribute with the new offset.
+          StmtSeq.set(NewStmtSeqOffset);
+        }
+      }
     }
 
-    Emitter->emitLineTableForUnit(LineTable, Unit, DebugStrPool,
-                                  DebugLineStrPool);
   } else
     Linker.reportWarning("Cann't load line table.", ObjFile);
 }
diff --git a/llvm/lib/DWARFLinker/Classic/DWARFLinkerCompileUnit.cpp b/llvm/lib/DWARFLinker/Classic/DWARFLinkerCompileUnit.cpp
index 1eb3a70a55135..66bf158e60f1d 100644
--- a/llvm/lib/DWARFLinker/Classic/DWARFLinkerCompileUnit.cpp
+++ b/llvm/lib/DWARFLinker/Classic/DWARFLinkerCompileUnit.cpp
@@ -185,6 +185,10 @@ void CompileUnit::noteLocationAttribute(PatchLocation Attr) {
   LocationAttributes.emplace_back(Attr);
 }
 
+void CompileUnit::noteStmtSeqListAttribute(PatchLocation Attr) {
+  StmtSeqListAttributes.emplace_back(Attr);
+}
+
 void CompileUnit::addNamespaceAccelerator(const DIE *Die,
                                           DwarfStringPoolEntryRef Name) {
   Namespaces.emplace_back(Name, Die);
diff --git a/llvm/lib/DWARFLinker/Classic/DWARFStreamer.cpp b/llvm/lib/DWARFLinker/Classic/DWARFStreamer.cpp
index 947db9cbcd92d..3bfbdd36ad150 100644
--- a/llvm/lib/DWARFLinker/Classic/DWARFStreamer.cpp
+++ b/llvm/lib/DWARFLinker/Classic/DWARFStreamer.cpp
@@ -809,7 +809,8 @@ void DwarfStreamer::emitDwarfDebugLocListsTableFragment(
 
 void DwarfStreamer::emitLineTableForUnit(
     const DWARFDebugLine::LineTable &LineTable, const CompileUnit &Unit,
-    OffsetsStringPool &DebugStrPool, OffsetsStringPool &DebugLineStrPool) {
+    OffsetsStringPool &DebugStrPool, OffsetsStringPool &DebugLineStrPool,
+    std::vector<uint64_t> *RowOffsets) {
   // Switch to the section where the table will be emitted into.
   MS->switchSection(MC->getObjectFileInfo()->getDwarfLineSection());
 
@@ -830,7 +831,7 @@ void DwarfStreamer::emitLineTableForUnit(
 
   // Emit rows.
   emitLineTableRows(LineTable, LineEndSym,
-                    Unit.getOrigUnit().getAddressByteSize());
+                    Unit.getOrigUnit().getAddressByteSize(), RowOffsets);
 }
 
 void DwarfStreamer::emitLineTablePrologue(const DWARFDebugLine::Prologue &P,
@@ -1036,7 +1037,7 @@ void DwarfStreamer::emitLineTableProloguePayload(
 
 void DwarfStreamer::emitLineTableRows(
     const DWARFDebugLine::LineTable &LineTable, MCSymbol *LineEndSym,
-    unsigned AddressByteSize) {
+    unsigned AddressByteSize, std::vector<uint64_t> *RowOffsets) {
 
   MCDwarfLineTableParams Params;
   Params.DWARF2LineOpcodeBase = LineTable.Prologue.OpcodeBase;
@@ -1068,6 +1069,11 @@ void DwarfStreamer::emitLineTableRows(
   unsigned RowsSinceLastSequence = 0;
 
   for (const DWARFDebugLine::Row &Row : LineTable.Rows) {
+    // If we're tracking row offsets, record the current section size as the
+    // offset of this row.
+    if (RowOffsets)
+      RowOffsets->push_back(LineSectionSize);
+
     int64_t AddressDelta;
     if (Address == -1ULL) {
       MS->emitIntValue(dwarf::DW_LNS_extended_op, 1);
diff --git a/llvm/test/tools/dsymutil/ARM/stmt-seq-macho.test b/llvm/test/tools/dsymutil/ARM/stmt-seq-macho.test
new file mode 100644
index 0000000000000..b5093ba767894
--- /dev/null
+++ b/llvm/test/tools/dsymutil/ARM/stmt-seq-macho.test
@@ -0,0 +1,74 @@
+RUN: dsymutil --flat -oso-prepend-path %p/../Inputs %p/../Inputs/private/tmp/stmt_seq/stmt_seq_macho.exe -o %t.stmt_seq_macho.dSYM
+RUN: llvm-dwarfdump --debug-info --debug-line -v %t.stmt_seq_macho.dSYM | sort | FileCheck %s -check-prefix=CHECK_DSYM
+
+# CHECK_DSYM: DW_AT_LLVM_stmt_sequence [DW_FORM_sec_offset] ([[OFFSET1:(0x[0-9a-f]+)]])
+# CHECK_DSYM: DW_AT_LLVM_stmt_sequence [DW_FORM_sec_offset] ([[OFFSET2:(0x[0-9a-f]+)]])
+# CHECK_DSYM: DW_AT_LLVM_stmt_sequence [DW_FORM_sec_offset] ([[OFFSET3:(0x[0-9a-f]+)]])
+# CHECK_DSYM: DW_AT_LLVM_stmt_sequence [DW_FORM_sec_offset] ([[OFFSET4:(0x[0-9a-f]+)]])
+
+# CHECK_DSYM: [[OFFSET1]]: 00 DW_LNE_set_address
+# CHECK_DSYM: [[OFFSET2]]: 00 DW_LNE_set_address
+# CHECK_DSYM: [[OFFSET3]]: 00 DW_LNE_set_address
+# CHECK_DSYM: [[OFFSET4]]: 00 DW_LNE_set_address
+
+
+########  Generate stmt_seq_macho.exe & stmt_seq_macho.o via script:  ##########
+# ------------------------------------------------------------------------------
+#!/bin/bash
+TOOLCHAIN=/path/to/llvm/bin
+
+# ------------------------------------------------------------------------------
+# Create the stmt_seq_macho.cpp source file
+# ------------------------------------------------------------------------------
+cat > stmt_seq_macho.cpp << 'EOF'
+#define ATTRIB extern "C" __attribute__((noinline))
+
+ATTRIB int function3_copy1(int a) {
+    int b = a + 3;
+    return b + 1;
+}
+ 
+ATTRIB int function2_copy1(int a) {
+    return a - 22;
+}
+ 
+ATTRIB int function3_copy2(int a) {
+    int b = a + 3;
+    return b + 1;
+}
+
+ATTRIB int function2_copy2(int a) {
+    int result = a - 22;
+    return result;
+}
+ 
+int main() {
+    int sum = 0;
+    sum += function2_copy2(3);
+    sum += function3...
[truncated]

@alx32 alx32 force-pushed the 21_dwarf_linker_stmt_seq branch from 3840c2e to c3b0163 Compare February 27, 2025 18:38
alx32 added a commit that referenced this pull request Mar 3, 2025
This patch introduces support for the `DW_AT_LLVM_stmt_sequence`
attribute in the GSYM DWARF transformer. With this change, the DWARF
GSYM creation process can now accurately associate debug information
with the correct functions, even when multiple functions have been
merged together.

The `macho-gsym-merged-callsites-dsym.yaml` test data is regenerated to
include the fixes in the DWARF linker
(#128953) and the test is
updated to check that debug data is correctly associated for merged
functions.
llvm-sync bot pushed a commit to arm/arm-toolchain that referenced this pull request Mar 3, 2025
This patch introduces support for the `DW_AT_LLVM_stmt_sequence`
attribute in the GSYM DWARF transformer. With this change, the DWARF
GSYM creation process can now accurately associate debug information
with the correct functions, even when multiple functions have been
merged together.

The `macho-gsym-merged-callsites-dsym.yaml` test data is regenerated to
include the fixes in the DWARF linker
(llvm/llvm-project#128953) and the test is
updated to check that debug data is correctly associated for merged
functions.
@alx32
Copy link
Contributor Author

alx32 commented Mar 6, 2025

@JDevlieghere - I see you are the code owner for this area. Do you have time to take a look at this ? Happy to explain context or if anything is unclear.

Copy link
Member

@JDevlieghere JDevlieghere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When reading the patch I kept thinking "can we pre-compute any of these mappings" but I convinced myself that this is the most efficient approach as we're limiting most lookups to offsets belonging to DIEs that are actually kept. LGTM modulo nits.

@alx32 alx32 force-pushed the 21_dwarf_linker_stmt_seq branch from deffcc0 to b8491fa Compare March 13, 2025 05:09
@alx32 alx32 merged commit abdbaff into llvm:main Mar 13, 2025
8 of 10 checks passed
@llvm-ci
Copy link
Collaborator

llvm-ci commented Mar 13, 2025

LLVM Buildbot has detected a new failure on builder clang-cmake-x86_64-avx512-win running on avx512-intel64-win while building llvm at step 6 "ninja check 1".

Full details are available at: https://lab.llvm.org/buildbot/#/builders/81/builds/5197

Here is the relevant piece of the build log for the reference
Step 6 (ninja check 1) failure: stage 1 checked (failure)
******************** TEST 'Clang :: Driver/offload-Xarch.c' FAILED ********************
Exit Code: 1

Command Output (stdout):
--
# RUN: at line 3
d:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\clang.exe --target=x86_64-unknown-linux-gnu -x cuda D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c -Xarch_nvptx64 -O3 -S -nogpulib -nogpuinc -### 2>&1 | d:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\filecheck.exe -check-prefix=O3ONCE D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c
# executed command: 'd:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\clang.exe' --target=x86_64-unknown-linux-gnu -x cuda 'D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c' -Xarch_nvptx64 -O3 -S -nogpulib -nogpuinc '-###'
# executed command: 'd:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\filecheck.exe' -check-prefix=O3ONCE 'D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c'
# RUN: at line 4
d:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\clang.exe -x cuda D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c -Xarch_device -O3 -S -nogpulib -nogpuinc -### 2>&1 | d:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\filecheck.exe -check-prefix=O3ONCE D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c
# executed command: 'd:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\clang.exe' -x cuda 'D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c' -Xarch_device -O3 -S -nogpulib -nogpuinc '-###'
# executed command: 'd:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\filecheck.exe' -check-prefix=O3ONCE 'D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c'
# RUN: at line 5
d:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\clang.exe -x hip D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c -Xarch_amdgcn -O3 -S -nogpulib -nogpuinc -### 2>&1 | d:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\filecheck.exe -check-prefix=O3ONCE D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c
# executed command: 'd:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\clang.exe' -x hip 'D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c' -Xarch_amdgcn -O3 -S -nogpulib -nogpuinc '-###'
# executed command: 'd:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\filecheck.exe' -check-prefix=O3ONCE 'D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c'
# RUN: at line 6
d:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\clang.exe -fopenmp=libomp -fopenmp-targets=amdgcn-amd-amdhsa -nogpulib -nogpuinc    -Xarch_amdgcn -march=gfx90a -Xarch_amdgcn -O3 -S -### D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c 2>&1  | d:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\filecheck.exe -check-prefix=O3ONCE D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c
# executed command: 'd:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\clang.exe' -fopenmp=libomp -fopenmp-targets=amdgcn-amd-amdhsa -nogpulib -nogpuinc -Xarch_amdgcn -march=gfx90a -Xarch_amdgcn -O3 -S '-###' 'D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c'
# executed command: 'd:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\filecheck.exe' -check-prefix=O3ONCE 'D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c'
# RUN: at line 9
d:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\clang.exe -fopenmp=libomp -fopenmp-targets=nvptx64-nvidia-cuda -nogpulib -nogpuinc    -Xarch_nvptx64 -march=sm_52 -Xarch_nvptx64 -O3 -S -### D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c 2>&1  | d:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\filecheck.exe -check-prefix=O3ONCE D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c
# executed command: 'd:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\clang.exe' -fopenmp=libomp -fopenmp-targets=nvptx64-nvidia-cuda -nogpulib -nogpuinc -Xarch_nvptx64 -march=sm_52 -Xarch_nvptx64 -O3 -S '-###' 'D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c'
# executed command: 'd:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\filecheck.exe' -check-prefix=O3ONCE 'D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c'
# RUN: at line 15
d:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\clang.exe -fopenmp=libomp -fopenmp-targets=nvptx64-nvidia-cuda,amdgcn-amd-amdhsa -nogpulib    --target=x86_64-unknown-linux-gnu -Xopenmp-target=nvptx64-nvidia-cuda --offload-arch=sm_52,sm_60 -nogpuinc    -Xopenmp-target=amdgcn-amd-amdhsa --offload-arch=gfx90a,gfx1030 -ccc-print-bindings -### D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c 2>&1  | d:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\filecheck.exe -check-prefix=OPENMP D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c
# executed command: 'd:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\clang.exe' -fopenmp=libomp -fopenmp-targets=nvptx64-nvidia-cuda,amdgcn-amd-amdhsa -nogpulib --target=x86_64-unknown-linux-gnu -Xopenmp-target=nvptx64-nvidia-cuda --offload-arch=sm_52,sm_60 -nogpuinc -Xopenmp-target=amdgcn-amd-amdhsa --offload-arch=gfx90a,gfx1030 -ccc-print-bindings '-###' 'D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c'
# executed command: 'd:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\filecheck.exe' -check-prefix=OPENMP 'D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c'
# RUN: at line 31
d:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\clang.exe -x cuda D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c --offload-arch=sm_52,sm_60 -Xarch_sm_52 -O3 -Xarch_sm_60 -O0    --target=x86_64-unknown-linux-gnu -Xarch_host -O3 -S -nogpulib -nogpuinc -### 2>&1  | d:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\filecheck.exe -check-prefix=CUDA D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c
# executed command: 'd:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\clang.exe' -x cuda 'D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c' --offload-arch=sm_52,sm_60 -Xarch_sm_52 -O3 -Xarch_sm_60 -O0 --target=x86_64-unknown-linux-gnu -Xarch_host -O3 -S -nogpulib -nogpuinc '-###'
# executed command: 'd:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\filecheck.exe' -check-prefix=CUDA 'D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c'
# RUN: at line 39
d:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\clang.exe -fopenmp=libomp --offload-arch=gfx90a -nogpulib -nogpuinc    --target=x86_64-unknown-linux-gnu -Xarch_amdgcn -Wl,-lfoo -### D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c 2>&1  | d:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\filecheck.exe -check-prefix=LIBS D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c
# executed command: 'd:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\clang.exe' -fopenmp=libomp --offload-arch=gfx90a -nogpulib -nogpuinc --target=x86_64-unknown-linux-gnu -Xarch_amdgcn -Wl,-lfoo '-###' 'D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c'
# executed command: 'd:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\stage1\bin\filecheck.exe' -check-prefix=LIBS 'D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c'
# .---command stderr------------
# | D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c:45:10: error: LIBS: expected string not found in input
# | // LIBS: "--device-linker=amdgcn-amd-amdhsa=-lfoo"
# |          ^
# | <stdin>:1:1: note: scanning from here
# | clang version 21.0.0git (https://github.com/llvm/llvm-project.git abdbaff5441e35a6e26f770145b62d73f4a55f48)
# | ^
# | <stdin>:6:1442: note: possible intended match here
# |  "D:\\buildbot\\llvm-worker\\clang-cmake-x86_64-avx512-win\\stage1\\bin\\clang.exe" "-cc1" "-triple" "x86_64-unknown-linux-gnu" "-emit-llvm-bc" "-emit-llvm-uselists" "-dumpdir" "a-" "-disable-free" "-clear-ast-before-backend" "-main-file-name" "offload-Xarch.c" "-mrelocation-model" "pic" "-pic-level" "2" "-pic-is-pie" "-mframe-pointer=all" "-fmath-errno" "-ffp-contract=on" "-fno-rounding-math" "-mconstructor-aliases" "-funwind-tables=2" "-target-cpu" "x86-64" "-tune-cpu" "generic" "-debugger-tuning=gdb" "-fdebug-compilation-dir=D:\\buildbot\\llvm-worker\\clang-cmake-x86_64-avx512-win\\stage1\\tools\\clang\\test\\Driver" "-fcoverage-compilation-dir=D:\\buildbot\\llvm-worker\\clang-cmake-x86_64-avx512-win\\stage1\\tools\\clang\\test\\Driver" "-resource-dir" "D:\\buildbot\\llvm-worker\\clang-cmake-x86_64-avx512-win\\stage1\\lib\\clang\\21" "-internal-isystem" "D:\\buildbot\\llvm-worker\\clang-cmake-x86_64-avx512-win\\stage1\\lib\\clang\\21\\include" "-internal-isystem" "/usr/local/include" "-internal-externc-isystem" "/include" "-internal-externc-isystem" "/usr/include" "-internal-isystem" "D:\\buildbot\\llvm-worker\\clang-cmake-x86_64-avx512-win\\stage1\\lib\\clang\\21\\include" "-internal-isystem" "/usr/local/include" "-internal-externc-isystem" "/include" "-internal-externc-isystem" "/usr/include" "-ferror-limit" "19" "-fopenmp" "--no-offloadlib" "-fgnuc-version=4.2.1" "-fskip-odr-check-in-gmf" "-disable-llvm-passes" "-fopenmp-targets=amdgcn-amd-amdhsa" "-faddrsig" "-D__GCC_HAVE_DWARF2_CFI_ASM=1" "-o" "C:\\Users\\tianfei\\AppData\\Local\\Temp\\1\\lit-tmp-pdlfe_w2\\offload-Xarch-35b28b.bc" "-x" "c" "D:\\buildbot\\llvm-worker\\clang-cmake-x86_64-avx512-win\\llvm\\clang\\test\\Driver\\offload-Xarch.c"
# |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  ^
# | 
# | Input file: <stdin>
# | Check file: D:\buildbot\llvm-worker\clang-cmake-x86_64-avx512-win\llvm\clang\test\Driver\offload-Xarch.c
...

frederik-h pushed a commit to frederik-h/llvm-project that referenced this pull request Mar 18, 2025
…es (llvm#128953)

**Summary:**  
This update adds handling for `DW_AT_LLVM_stmt_sequence` attributes in
the DWARF linker. These attributes point to rows in the line table,
which gets rewritten during linking. Since the row positions change, the
offsets in these attributes need to be updated to match the new layout
in the output `.debug_line` section. The changes add new data structures
and tweak existing functions to track and fix these attributes.

**Background**
In llvm#110192 we added support to
clang to generate the `DW_AT_LLVM_stmt_sequence` attribute for
`DW_TAG_subprogram`'s. Corresponding RFC: [New DWARF Attribute for
Symbolication of Merged
Functions](https://discourse.llvm.org/t/rfc-new-dwarf-attribute-for-symbolication-of-merged-functions/79434).
This attribute holds a label pointing to the offset in the line table
where the function's line entries begin.

**Implementation details:**  
Here’s what’s changed in the code:  
- **New Tracking in `CompileUnit`:** A `StmtSeqListAttributes` vector is
added to the `CompileUnit` class. It stores the locations where
`DW_AT_LLVM_stmt_sequence` attributes need to be patched, recorded when
cloning DIEs (debug info entries).
- **Updated `emitLineTableForUnit` Function:** This function now has an
optional `RowOffsets` parameter. It collects the byte offsets of each
row in the output line table. We only need to use this functionality if
`DW_AT_LLVM_stmt_sequence` attributes are present in the unit.
- **Row Tracking with `TrackedRow`:** A `TrackedRow` struct keeps track
of each input row’s original index and whether it starts a sequence in
the output table. This links old rows to their new positions in the
rewritten line table. Several implementations were considered and
prototyped here, but so far this has proven the simplest and cleanest
approach.
- **Patching Step:** After the line table is written, the linker uses
the data in `TrackedRow`'s objects and `RowOffsets` array to update the
`DW_AT_LLVM_stmt_sequence` attributes with the correct offsets.
jph-13 pushed a commit to jph-13/llvm-project that referenced this pull request Mar 21, 2025
This patch introduces support for the `DW_AT_LLVM_stmt_sequence`
attribute in the GSYM DWARF transformer. With this change, the DWARF
GSYM creation process can now accurately associate debug information
with the correct functions, even when multiple functions have been
merged together.

The `macho-gsym-merged-callsites-dsym.yaml` test data is regenerated to
include the fixes in the DWARF linker
(llvm#128953) and the test is
updated to check that debug data is correctly associated for merged
functions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants