[Mlir-commits] [lldb] [llvm] [mlir] [DebugInfo] Add symbolic branches to DIExpression (PR #210850)

Eric Christopher llvmlistbot at llvm.org
Thu Aug 13 23:21:43 PDT 2026


https://github.com/echristo updated https://github.com/llvm/llvm-project/pull/210850

>From c9917a8e60fc23aabbe66050f7acdab853c3acad Mon Sep 17 00:00:00 2001
From: Eric Christopher <echristopher at nvidia.com>
Date: Thu, 13 Aug 2026 23:16:56 -0700
Subject: [PATCH 01/19] [DebugInfo] Add symbolic branches to DIExpression

Added three pseudo-ops to DIExpression:

- DW_OP_LLVM_label declares a label and emits no bytes.
- DW_OP_LLVM_bra branches to a label when the top of the stack is non-zero.
- DW_OP_LLVM_skip always branches to a label.

DW_OP_bra and DW_OP_skip store a two-byte offset in [-32768, 32767], but
we don't know those offsets until we emit the expression. DIExpression keeps
label IDs, and we fix them up late in CodeGen.

CodeGen records each label's byte offset and leaves a zero placeholder for each
branch. Once it has seen the whole expression, it patches the placeholders in
the existing temporary buffers used for DIE and location-list expressions.
Both paths keep their current encoding and target byte order.

The verifier makes sure every branch has a label in the same expression and
each label ID is declared once. Labels don't need a branch, and branches can go
forward, backward, to themselves, or form cycles. Raw DW_OP_bra and DW_OP_skip
remain invalid in IR because their offsets depend on the final encoding.

Symbolic control flow only works with one location operand, so we reject
DW_OP_LLVM_implicit_pointer alongside it and diagnose DIArgList records.
Existing expression rewrites keep the pseudo-ops and their IDs in place.

CodeGen reports an error if the final offset is outside [-32768, 32767], or if a
label, branch, or skip splits a deferred conversion. DWARF 5 emits DW_OP_convert
directly, but older versions may defer one conversion until they see the next.

Tests cover IR and MLIR round trips, verifier failures, forward and backward
branches, self-branches and cycles, both byte orders, conversion boundaries,
and out-of-range offsets.
---
 llvm/docs/LangRef.md                          |  16 ++
 llvm/docs/ReleaseNotes.md                     |   6 +
 llvm/docs/SourceLevelDebugging.md             |  61 ++++++-
 llvm/include/llvm/BinaryFormat/Dwarf.h        |   5 +
 llvm/lib/BinaryFormat/Dwarf.cpp               |   9 +
 llvm/lib/CodeGen/AsmPrinter/DwarfDebug.cpp    |  27 ++-
 .../CodeGen/AsmPrinter/DwarfExpression.cpp    |  83 +++++++++-
 llvm/lib/CodeGen/AsmPrinter/DwarfExpression.h |  27 ++-
 llvm/lib/CodeGen/AsmPrinter/DwarfUnit.cpp     |  52 +++++-
 llvm/lib/IR/DebugInfoMetadata.cpp             |  89 +++++++++-
 llvm/lib/IR/Verifier.cpp                      |  13 ++
 .../di-expression-symbolic-branches.ll        |  12 ++
 .../di-expression-symbolic-branches.ll        |  12 ++
 .../X86/di-expression-symbolic-branches.mir   | 102 ++++++++++++
 .../di-expression-symbolic-branches.ll        |  44 +++++
 ...expression-symbolic-branch-out-of-range.py |  33 ++++
 ...ession-symbolic-branch-convert-boundary.ll |  96 +++++++++++
 ...expression-symbolic-branch-out-of-range.ll |   7 +
 .../X86/di-expression-symbolic-branches.ll    |  84 ++++++++++
 .../di-expression-symbolic-branches.ll        |  93 +++++++++++
 llvm/unittests/BinaryFormat/DwarfTest.cpp     |   7 +
 llvm/unittests/IR/MetadataTest.cpp            | 155 ++++++++++++++++++
 .../Import/debug-info-symbolic-branches.ll    |  23 +++
 .../llvmir-debug-symbolic-branches.mlir       |  19 +++
 24 files changed, 1046 insertions(+), 29 deletions(-)
 create mode 100644 llvm/test/Assembler/di-expression-symbolic-branches.ll
 create mode 100644 llvm/test/Bitcode/di-expression-symbolic-branches.ll
 create mode 100644 llvm/test/DebugInfo/MIR/X86/di-expression-symbolic-branches.mir
 create mode 100644 llvm/test/DebugInfo/PowerPC/di-expression-symbolic-branches.ll
 create mode 100644 llvm/test/DebugInfo/X86/Inputs/generate-di-expression-symbolic-branch-out-of-range.py
 create mode 100644 llvm/test/DebugInfo/X86/di-expression-symbolic-branch-convert-boundary.ll
 create mode 100644 llvm/test/DebugInfo/X86/di-expression-symbolic-branch-out-of-range.ll
 create mode 100644 llvm/test/DebugInfo/X86/di-expression-symbolic-branches.ll
 create mode 100644 llvm/test/Verifier/di-expression-symbolic-branches.ll
 create mode 100644 mlir/test/Target/LLVMIR/Import/debug-info-symbolic-branches.ll
 create mode 100644 mlir/test/Target/LLVMIR/llvmir-debug-symbolic-branches.mlir

diff --git a/llvm/docs/LangRef.md b/llvm/docs/LangRef.md
index b36b7c052646a..3f7100a2c4d1c 100644
--- a/llvm/docs/LangRef.md
+++ b/llvm/docs/LangRef.md
@@ -7215,6 +7215,22 @@ Some examples of expressions:
 !DIExpression(DW_OP_constu, 42, DW_OP_stack_value)
 ```
 
+`DIExpression` uses three pseudo-ops for local control flow:
+
+```text
+DW_OP_LLVM_label, <label-id>
+DW_OP_LLVM_bra,   <label-id>
+DW_OP_LLVM_skip,  <label-id>
+```
+
+`DW_OP_LLVM_label` declares a label ID and emits no bytes. `DW_OP_LLVM_bra`
+branches when the top of the stack is non-zero; `DW_OP_LLVM_skip` always
+branches. Label IDs are local to the expression. Every branch needs a matching
+label, labels without branches are valid, and each ID can only be declared
+once. Raw `DW_OP_bra` and `DW_OP_skip` aren't valid in LLVM IR.
+
+See {ref}`symbolic control flow <symbolic-control-flow>` for the full rules.
+
 ##### DIAssignID
 
 `DIAssignID` nodes have no operands and are always distinct. They are used to
diff --git a/llvm/docs/ReleaseNotes.md b/llvm/docs/ReleaseNotes.md
index f87f791a49e6f..7c216a243752c 100644
--- a/llvm/docs/ReleaseNotes.md
+++ b/llvm/docs/ReleaseNotes.md
@@ -131,6 +131,12 @@ Makes programs 10x faster by doing Special New Thing.
 
 ### Changes to the Debug Info
 
+* Added `DW_OP_LLVM_label`, `DW_OP_LLVM_bra`, and `DW_OP_LLVM_skip` for symbolic
+  branches in `DIExpression`. These operations use label IDs that CodeGen
+  resolves when it emits the expression. `DW_OP_LLVM_convert` can appear before
+  or after them, but when CodeGen can't emit `DW_OP_convert`, it reports an
+  error if a deferred conversion reaches a label, branch, or skip.
+
 ### Changes to the LLVM tools
 
 * llvm-mca no longer defaults -mcpu to "native"
diff --git a/llvm/docs/SourceLevelDebugging.md b/llvm/docs/SourceLevelDebugging.md
index 1f8ad9a795cff..eaec125ed2762 100644
--- a/llvm/docs/SourceLevelDebugging.md
+++ b/llvm/docs/SourceLevelDebugging.md
@@ -383,10 +383,9 @@ call void @llvm.dbg.assign(
 
 Debug expressions are represented as {ref}`specialized-metadata`.
 
-Debug expressions are interpreted left-to-right: start by pushing the
-value/address operand of the record onto a stack, then repeatedly push and
-evaluate opcodes from the `DIExpression` until the final variable description
-is produced.
+A debug expression starts with the record's value or address operand on the
+stack, then evaluates operations from left to right unless a symbolic branch
+jumps to a label in the same `DIExpression`.
 
 The opcodes available in these expressions are described in
 {ref}`dwarf-opcodes` and {ref}`internal-opcodes`.
@@ -467,6 +466,50 @@ Some opcodes do not influence the final DWARF expression directly, instead
 encoding information logically belonging to the debug records which use
 them.
 :::
+
+(symbolic-control-flow)=
+
+##### Symbolic Control Flow
+
+DWARF `DW_OP_bra` and `DW_OP_skip` have a two-byte offset in
+`[-32768, 32767]`, but we don't know that offset until we emit the expression,
+so we use three pseudo-ops with label IDs that CodeGen resolves during
+emission:
+
+- `DW_OP_LLVM_label, ID` marks a destination and emits no bytes.
+- `DW_OP_LLVM_bra, ID` branches to label `ID` when the value on top of the
+  expression stack is non-zero.
+- `DW_OP_LLVM_skip, ID` always branches to label `ID`.
+
+Label IDs are local to an expression:
+
+- Each ID can have at most one label, and every branch needs a matching label
+  in the same expression.
+- Labels don't need branches, and consecutive labels have the same byte
+  offset.
+- Branches can go forward, backward, to themselves, or form cycles.
+
+There are a few other restrictions:
+
+- Put labels, branches, and skips before `DW_OP_stack_value` and
+  `DW_OP_LLVM_fragment`. Only a fragment can follow `DW_OP_stack_value`.
+- `DIArgList`, `DW_OP_LLVM_arg`, and `DW_OP_LLVM_implicit_pointer` are lowered
+  separately, so they can't be used with symbolic control flow.
+- Put `DW_OP_LLVM_tag_offset` before the first label, branch, or skip so it
+  applies to every path.
+
+We don't check reachability, termination, or stack state where paths meet.
+
+`DW_OP_LLVM_convert` can appear before or after labels, branches, and skips,
+and we don't match conversions on different paths. When CodeGen can't emit
+`DW_OP_convert`, it may defer one conversion until it sees the next; if a label,
+branch, or skip would split the pair, CodeGen reports an error.
+
+Local expression rewrites stop at labels, branches, and skips; they can still
+add operations to either end, but they don't move, remove, or copy labels.
+
+##### Other Internal Opcodes
+
 - `DW_OP_LLVM_fragment, <offset>, <size>` may appear at most once in an
   expression, and must be the last opcode. It specifies the bit offset and bit
   size of the variable fragment being described by the record or intrinsic
@@ -479,11 +522,15 @@ them.
 - `DW_OP_LLVM_convert, 16, DW_ATE_signed` specifies a bit size and encoding
   (`16` and `DW_ATE_signed` here, respectively) to which the top of the
   expression stack is to be converted. Maps into a `DW_OP_convert` operation
-  that references a base type constructed from the supplied values.
+  that references a base type constructed from the supplied values. See
+  {ref}`symbolic control flow <symbolic-control-flow>` for how conversions work
+  with labels, branches, and skips.
 - `DW_OP_LLVM_tag_offset, tag_offset` specifies that a memory tag should be
   optionally applied to the pointer. The memory tag is derived from the given
-  tag offset in an implementation-defined manner. (This does not affect the
-  semantics of the expression containing it.)
+  tag offset in an implementation-defined manner. See
+  {ref}`symbolic control flow <symbolic-control-flow>` for its placement with
+  labels, branches, and skips.
+  (This does not affect the semantics of the expression containing it.)
 - `DW_OP_LLVM_entry_value, N` evaluates a sub-expression as-if it were
   evaluated upon entry to the current call frame.
 
diff --git a/llvm/include/llvm/BinaryFormat/Dwarf.h b/llvm/include/llvm/BinaryFormat/Dwarf.h
index 46124019206f2..a6ab11c4afb8e 100644
--- a/llvm/include/llvm/BinaryFormat/Dwarf.h
+++ b/llvm/include/llvm/BinaryFormat/Dwarf.h
@@ -149,6 +149,11 @@ enum LocationAtom {
   DW_OP_LLVM_arg = 0x1005,               ///< Only used in LLVM metadata.
   DW_OP_LLVM_extract_bits_sext = 0x1006, ///< Only used in LLVM metadata.
   DW_OP_LLVM_extract_bits_zext = 0x1007, ///< Only used in LLVM metadata.
+  // Labels declare IDs local to each expression, and branches use them as
+  // targets.
+  DW_OP_LLVM_label = 0x1008, ///< Only used in LLVM metadata.
+  DW_OP_LLVM_bra = 0x1009,   ///< Only used in LLVM metadata.
+  DW_OP_LLVM_skip = 0x100a,  ///< Only used in LLVM metadata.
 };
 
 enum LlvmUserLocationAtom {
diff --git a/llvm/lib/BinaryFormat/Dwarf.cpp b/llvm/lib/BinaryFormat/Dwarf.cpp
index 0027f1bbf0d9b..eb4d449f9b2b7 100644
--- a/llvm/lib/BinaryFormat/Dwarf.cpp
+++ b/llvm/lib/BinaryFormat/Dwarf.cpp
@@ -159,6 +159,12 @@ StringRef llvm::dwarf::OperationEncodingString(unsigned Encoding) {
     return "DW_OP_LLVM_extract_bits_sext";
   case DW_OP_LLVM_extract_bits_zext:
     return "DW_OP_LLVM_extract_bits_zext";
+  case DW_OP_LLVM_label:
+    return "DW_OP_LLVM_label";
+  case DW_OP_LLVM_bra:
+    return "DW_OP_LLVM_bra";
+  case DW_OP_LLVM_skip:
+    return "DW_OP_LLVM_skip";
   }
 }
 
@@ -175,6 +181,9 @@ unsigned llvm::dwarf::getOperationEncoding(StringRef OperationEncodingString) {
       .Case("DW_OP_LLVM_arg", DW_OP_LLVM_arg)
       .Case("DW_OP_LLVM_extract_bits_sext", DW_OP_LLVM_extract_bits_sext)
       .Case("DW_OP_LLVM_extract_bits_zext", DW_OP_LLVM_extract_bits_zext)
+      .Case("DW_OP_LLVM_label", DW_OP_LLVM_label)
+      .Case("DW_OP_LLVM_bra", DW_OP_LLVM_bra)
+      .Case("DW_OP_LLVM_skip", DW_OP_LLVM_skip)
       .Default(0);
 }
 
diff --git a/llvm/lib/CodeGen/AsmPrinter/DwarfDebug.cpp b/llvm/lib/CodeGen/AsmPrinter/DwarfDebug.cpp
index 88e75e3c96106..593a68c34cc74 100644
--- a/llvm/lib/CodeGen/AsmPrinter/DwarfDebug.cpp
+++ b/llvm/lib/CodeGen/AsmPrinter/DwarfDebug.cpp
@@ -191,8 +191,14 @@ void DebugLocDwarfExpression::emitUnsigned(uint64_t Value) {
   getActiveStreamer().emitULEB128(Value, Twine(Value));
 }
 
-void DebugLocDwarfExpression::emitData1(uint8_t Value) {
-  getActiveStreamer().emitInt8(Value, Twine(Value));
+void DebugLocDwarfExpression::emitData(uint64_t Value, unsigned Size) {
+  assert((Size == 1 || Size == 2 || Size == 4 || Size == 8) &&
+         "fixed-width data size must be 1, 2, 4, or 8 bytes");
+  bool IsLittleEndian = CU.getAsmPrinter()->getDataLayout().isLittleEndian();
+  for (unsigned I = 0; I != Size; ++I) {
+    unsigned Byte = IsLittleEndian ? I : Size - I - 1;
+    getActiveStreamer().emitInt8(Value >> (Byte * 8), Twine(Value));
+  }
 }
 
 void DebugLocDwarfExpression::emitBaseTypeRef(uint64_t Idx) {
@@ -232,6 +238,23 @@ void DebugLocDwarfExpression::commitTemporaryBuffer() {
   TmpBuf->Comments.clear();
 }
 
+void DebugLocDwarfExpression::replaceTemporaryBufferData(unsigned Offset,
+                                                         uint64_t Value,
+                                                         unsigned Size) {
+  assert((Size == 1 || Size == 2 || Size == 4 || Size == 8) &&
+         "fixed-width data size must be 1, 2, 4, or 8 bytes");
+  assert(TmpBuf && Offset < TmpBuf->Bytes.size() &&
+         Size <= TmpBuf->Bytes.size() - Offset &&
+         "invalid temporary buffer offset");
+  bool IsLittleEndian = CU.getAsmPrinter()->getDataLayout().isLittleEndian();
+  for (unsigned I = 0; I != Size; ++I) {
+    unsigned Byte = IsLittleEndian ? I : Size - I - 1;
+    TmpBuf->Bytes[Offset + I] = Value >> (Byte * 8);
+    if (Offset + I < TmpBuf->Comments.size())
+      TmpBuf->Comments[Offset + I].clear();
+  }
+}
+
 const DIType *DbgVariable::getType() const {
   return getVariable()->getType();
 }
diff --git a/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp b/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp
index 2e022f30842e7..8cd9457421e92 100644
--- a/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp
+++ b/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp
@@ -13,13 +13,16 @@
 #include "DwarfExpression.h"
 #include "DwarfCompileUnit.h"
 #include "llvm/ADT/APInt.h"
+#include "llvm/ADT/STLExtras.h"
 #include "llvm/ADT/SmallBitVector.h"
+#include "llvm/ADT/Twine.h"
 #include "llvm/BinaryFormat/Dwarf.h"
 #include "llvm/CodeGen/Register.h"
 #include "llvm/CodeGen/TargetRegisterInfo.h"
 #include "llvm/IR/DataLayout.h"
 #include "llvm/MC/MCAsmInfo.h"
 #include "llvm/Support/ErrorHandling.h"
+#include "llvm/Support/MathExtras.h"
 #include <algorithm>
 
 using namespace llvm;
@@ -555,8 +558,31 @@ bool DwarfExpression::addExpression(
   // and not any other parts of the following DWARF expression.
   assert(!IsEmittingEntryValue && "Can't emit entry value around expression");
 
-  std::optional<DIExpression::ConvertOp> PrevConvertOp;
+  struct LabelOffset {
+    uint64_t ID;
+    uint64_t Offset;
+  };
+  struct BranchFixup {
+    uint64_t LabelID;
+    uint64_t PlaceholderOffset;
+  };
+  constexpr unsigned BranchOffsetByteSize = 2;
+
+  // Iterating over ExprCursor doesn't consume it.
+  bool HasSymbolicBranches =
+      llvm::any_of(ExprCursor, [](DIExpression::ExprOperand Op) {
+        return Op.getOp() == dwarf::DW_OP_LLVM_bra ||
+               Op.getOp() == dwarf::DW_OP_LLVM_skip;
+      });
+
+  SmallVector<LabelOffset, 4> Labels;
+  SmallVector<BranchFixup, 4> Fixups;
+  // Buffer the expression until every label has a byte offset, then patch the
+  // branches.
+  if (HasSymbolicBranches)
+    enableTemporaryBuffer();
 
+  std::optional<DIExpression::ConvertOp> PrevConvertOp;
   while (ExprCursor) {
     auto Op = ExprCursor.take();
     uint64_t OpNum = Op->getOp();
@@ -570,9 +596,27 @@ bool DwarfExpression::addExpression(
     }
 
     switch (OpNum) {
+    case dwarf::DW_OP_LLVM_label:
+    case dwarf::DW_OP_LLVM_bra:
+    case dwarf::DW_OP_LLVM_skip:
+      if (PrevConvertOp)
+        report_fatal_error(Twine("cannot lower DW_OP_LLVM_convert across ") +
+                           dwarf::OperationEncodingString(OpNum) +
+                           " without DW_OP_convert support");
+      if (OpNum == dwarf::DW_OP_LLVM_label) {
+        Labels.push_back({Op->getArg(0), getTemporaryBufferSize()});
+        break;
+      }
+      emitOp(OpNum == dwarf::DW_OP_LLVM_bra ? dwarf::DW_OP_bra
+                                            : dwarf::DW_OP_skip);
+      Fixups.push_back({Op->getArg(0), getTemporaryBufferSize()});
+      emitData2(0);
+      break;
     case dwarf::DW_OP_LLVM_arg:
       if (!InsertArg(cast<DIExpression::ArgOp>(*Op).getIndex(), ExprCursor)) {
         LocationKind = Unknown;
+        if (HasSymbolicBranches)
+          disableTemporaryBuffer();
         return false;
       }
       break;
@@ -605,7 +649,10 @@ bool DwarfExpression::addExpression(
       setSubRegisterPiece(0, 0);
       // Reset the location description kind.
       LocationKind = Unknown;
-      return true;
+      if (!HasSymbolicBranches)
+        return true;
+      // Keep going so we apply the branch fixups before returning.
+      break;
     }
     case dwarf::DW_OP_LLVM_extract_bits_sext:
     case dwarf::DW_OP_LLVM_extract_bits_zext: {
@@ -772,9 +819,12 @@ bool DwarfExpression::addExpression(
       break;
     case dwarf::DW_OP_LLVM_implicit_pointer:
       // Handled in DwarfCompileUnit::emitImplicitPointerLocation for
-      // Loc::Single variables. If we reach here, the variable has a
-      // location list or other unsupported path. Drop the
-      // location rather than crashing.
+      // Loc::Single variables. If we reach here, the variable has a location
+      // list or another unsupported path, so stop emitting the expression.
+      // We buffer expressions with symbolic branches, so disable the buffer
+      // before returning.
+      if (HasSymbolicBranches)
+        disableTemporaryBuffer();
       return false;
     default:
       llvm_unreachable("unhandled opcode found in expression");
@@ -785,6 +835,29 @@ bool DwarfExpression::addExpression(
     // Turn this into an implicit location description.
     addStackValue();
 
+  if (HasSymbolicBranches) {
+    for (const BranchFixup &Fixup : Fixups) {
+      auto Label = llvm::find_if(Labels, [&](const LabelOffset &Candidate) {
+        return Candidate.ID == Fixup.LabelID;
+      });
+      if (Label == Labels.end())
+        report_fatal_error(Twine("DWARF expression branch to label ") +
+                           Twine(Fixup.LabelID) + " has no matching label");
+
+      int64_t Displacement =
+          static_cast<int64_t>(Label->Offset) -
+          static_cast<int64_t>(Fixup.PlaceholderOffset + BranchOffsetByteSize);
+      if (!isInt<16>(Displacement))
+        report_fatal_error(Twine("DWARF expression branch offset ") +
+                           Twine(Displacement) + " is outside [-32768, 32767]");
+
+      replaceTemporaryBufferData2(Fixup.PlaceholderOffset,
+                                  static_cast<uint16_t>(Displacement));
+    }
+
+    disableTemporaryBuffer();
+    commitTemporaryBuffer();
+  }
   return true;
 }
 
diff --git a/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.h b/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.h
index c6f3ab432e3f4..17bd867247f9d 100644
--- a/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.h
+++ b/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.h
@@ -132,7 +132,11 @@ class DwarfExpression {
   /// Emit a raw unsigned value.
   virtual void emitUnsigned(uint64_t Value) = 0;
 
-  virtual void emitData1(uint8_t Value) = 0;
+  /// Emit the low 1, 2, 4, or 8 bytes of a value in the target byte order.
+  virtual void emitData(uint64_t Value, unsigned Size) = 0;
+
+  void emitData1(uint8_t Value) { emitData(Value, 1); }
+  void emitData2(uint16_t Value) { emitData(Value, 2); }
 
   virtual void emitBaseTypeRef(uint64_t Idx) = 0;
 
@@ -152,6 +156,15 @@ class DwarfExpression {
   /// Commit the data stored in the temporary buffer to the main output.
   virtual void commitTemporaryBuffer() = 0;
 
+  /// Replace a 1-, 2-, 4-, or 8-byte zero placeholder at Offset with the low
+  /// bytes of Value in the target byte order.
+  virtual void replaceTemporaryBufferData(unsigned Offset, uint64_t Value,
+                                          unsigned Size) = 0;
+
+  void replaceTemporaryBufferData2(unsigned Offset, uint16_t Value) {
+    replaceTemporaryBufferData(Offset, Value, 2);
+  }
+
   /// Emit a normalized unsigned constant.
   void emitConstu(uint64_t Value);
 
@@ -287,10 +300,14 @@ class DwarfExpression {
 
   /// Emit all remaining operations in the DIExpressionCursor. The
   /// cursor must not contain any DW_OP_LLVM_arg operations.
+  /// CodeGen reports an error if a branch offset is outside [-32768, 32767] or
+  /// a deferred DW_OP_LLVM_convert reaches a label, branch, or skip.
   void addExpression(DIExpressionCursor &&Expr);
 
   /// Emit all remaining operations in the DIExpressionCursor.
   /// DW_OP_LLVM_arg operations are resolved by calling (\p InsertArg).
+  /// CodeGen reports an error if a branch offset is outside [-32768, 32767] or
+  /// a deferred DW_OP_LLVM_convert reaches a label, branch, or skip.
   //
   /// \return false if any call to (\p InsertArg) returns false.
   bool addExpression(
@@ -330,13 +347,15 @@ class DebugLocDwarfExpression final : public DwarfExpression {
   void emitOp(uint8_t Op, const char *Comment = nullptr) override;
   void emitSigned(int64_t Value) override;
   void emitUnsigned(uint64_t Value) override;
-  void emitData1(uint8_t Value) override;
+  void emitData(uint64_t Value, unsigned Size) override;
   void emitBaseTypeRef(uint64_t Idx) override;
 
   void enableTemporaryBuffer() override;
   void disableTemporaryBuffer() override;
   unsigned getTemporaryBufferSize() override;
   void commitTemporaryBuffer() override;
+  void replaceTemporaryBufferData(unsigned Offset, uint64_t Value,
+                                  unsigned Size) override;
 
   bool isFrameRegister(const TargetRegisterInfo &TRI,
                        llvm::Register MachineReg) override;
@@ -360,13 +379,15 @@ class DIEDwarfExpression final : public DwarfExpression {
   void emitOp(uint8_t Op, const char *Comment = nullptr) override;
   void emitSigned(int64_t Value) override;
   void emitUnsigned(uint64_t Value) override;
-  void emitData1(uint8_t Value) override;
+  void emitData(uint64_t Value, unsigned Size) override;
   void emitBaseTypeRef(uint64_t Idx) override;
 
   void enableTemporaryBuffer() override;
   void disableTemporaryBuffer() override;
   unsigned getTemporaryBufferSize() override;
   void commitTemporaryBuffer() override;
+  void replaceTemporaryBufferData(unsigned Offset, uint64_t Value,
+                                  unsigned Size) override;
 
   bool isFrameRegister(const TargetRegisterInfo &TRI,
                        llvm::Register MachineReg) override;
diff --git a/llvm/lib/CodeGen/AsmPrinter/DwarfUnit.cpp b/llvm/lib/CodeGen/AsmPrinter/DwarfUnit.cpp
index dd16095f0e823..6d4eeebf0703a 100644
--- a/llvm/lib/CodeGen/AsmPrinter/DwarfUnit.cpp
+++ b/llvm/lib/CodeGen/AsmPrinter/DwarfUnit.cpp
@@ -37,6 +37,27 @@ using namespace llvm;
 
 #define DEBUG_TYPE "dwarfdebug"
 
+static dwarf::Form getDataForm(unsigned Size) {
+  switch (Size) {
+  case 1:
+    return dwarf::DW_FORM_data1;
+  case 2:
+    return dwarf::DW_FORM_data2;
+  case 4:
+    return dwarf::DW_FORM_data4;
+  case 8:
+    return dwarf::DW_FORM_data8;
+  default:
+    llvm_unreachable("fixed-width data size must be 1, 2, 4, or 8 bytes");
+  }
+}
+
+static uint64_t getDataValue(uint64_t Value, unsigned Size) {
+  if (Size == sizeof(Value))
+    return Value;
+  return Value & ((uint64_t(1) << (Size * 8)) - 1);
+}
+
 DIEDwarfExpression::DIEDwarfExpression(const AsmPrinter &AP,
                                        DwarfCompileUnit &CU, DIELoc &DIE)
     : DwarfExpression(AP.getDwarfVersion(), CU), AP(AP), OutDIE(DIE) {}
@@ -53,8 +74,8 @@ void DIEDwarfExpression::emitUnsigned(uint64_t Value) {
   CU.addUInt(getActiveDIE(), dwarf::DW_FORM_udata, Value);
 }
 
-void DIEDwarfExpression::emitData1(uint8_t Value) {
-  CU.addUInt(getActiveDIE(), dwarf::DW_FORM_data1, Value);
+void DIEDwarfExpression::emitData(uint64_t Value, unsigned Size) {
+  CU.addUInt(getActiveDIE(), getDataForm(Size), getDataValue(Value, Size));
 }
 
 void DIEDwarfExpression::emitBaseTypeRef(uint64_t Idx) {
@@ -69,11 +90,36 @@ void DIEDwarfExpression::enableTemporaryBuffer() {
 void DIEDwarfExpression::disableTemporaryBuffer() { IsBuffering = false; }
 
 unsigned DIEDwarfExpression::getTemporaryBufferSize() {
-  return TmpDIE.computeSize(AP.getDwarfFormParams());
+  unsigned Size = 0;
+  for (const DIEValue &V : TmpDIE.values())
+    Size += V.sizeOf(AP.getDwarfFormParams());
+  return Size;
 }
 
 void DIEDwarfExpression::commitTemporaryBuffer() { OutDIE.takeValues(TmpDIE); }
 
+void DIEDwarfExpression::replaceTemporaryBufferData(unsigned Offset,
+                                                    uint64_t Value,
+                                                    unsigned Size) {
+  dwarf::Form Form = getDataForm(Size);
+  // Keep the form so replacing the value doesn't move later labels.
+  unsigned CurrentOffset = 0;
+  for (DIEValue &V : TmpDIE.values()) {
+    unsigned ValueSize = V.sizeOf(AP.getDwarfFormParams());
+    if (Offset < CurrentOffset + ValueSize) {
+      assert(Offset == CurrentOffset && ValueSize == Size &&
+             V.getType() == DIEValue::isInteger && V.getForm() == Form &&
+             V.getDIEInteger().getValue() == 0 &&
+             "symbolic branch fixup does not match its placeholder");
+      V = DIEValue(V.getAttribute(), V.getForm(),
+                   DIEInteger(getDataValue(Value, Size)));
+      return;
+    }
+    CurrentOffset += ValueSize;
+  }
+  llvm_unreachable("invalid temporary DIE offset");
+}
+
 bool DIEDwarfExpression::isFrameRegister(const TargetRegisterInfo &TRI,
                                          llvm::Register MachineReg) {
   return MachineReg == TRI.getFrameRegister(*AP.MF);
diff --git a/llvm/lib/IR/DebugInfoMetadata.cpp b/llvm/lib/IR/DebugInfoMetadata.cpp
index 134f1382dfc25..695b59fe809e7 100644
--- a/llvm/lib/IR/DebugInfoMetadata.cpp
+++ b/llvm/lib/IR/DebugInfoMetadata.cpp
@@ -13,6 +13,7 @@
 #include "llvm/IR/DebugInfoMetadata.h"
 #include "LLVMContextImpl.h"
 #include "MetadataImpl.h"
+#include "llvm/ADT/STLExtras.h"
 #include "llvm/ADT/SetVector.h"
 #include "llvm/ADT/StringSwitch.h"
 #include "llvm/BinaryFormat/Dwarf.h"
@@ -1750,6 +1751,9 @@ unsigned DIExpression::ExprOperand::getSize() const {
   case dwarf::DW_OP_LLVM_tag_offset:
   case dwarf::DW_OP_LLVM_entry_value:
   case dwarf::DW_OP_LLVM_arg:
+  case dwarf::DW_OP_LLVM_label:
+  case dwarf::DW_OP_LLVM_bra:
+  case dwarf::DW_OP_LLVM_skip:
   case dwarf::DW_OP_regx:
     return 2;
   default:
@@ -1799,12 +1803,82 @@ bool DIExpression::PlusUconstOp::classof(const ExprOperand *Op) {
 }
 
 bool DIExpression::isValid() const {
+  auto IsEntryValueValid = [this](const ExprOperand &EntryValue) {
+    auto FirstOp = expr_op_begin();
+    if (auto Arg = dyn_cast<ArgOp>(*FirstOp); Arg && Arg.getIndex() == 0)
+      ++FirstOp;
+    return EntryValue.get() == FirstOp->get() &&
+           cast<EntryValueOp>(EntryValue).getNumOperations() == 1;
+  };
+
+  SmallDenseSet<uint64_t, 4> Labels;
+  SmallVector<uint64_t, 4> LabelReferences;
+  bool HasControlFlow = false;
+  bool HasControlFlowConflict = false;
+  bool HasStackValue = false;
+  bool HasFragment = false;
+  bool HasInvalidControlFlowSuffix = false;
+
+  // Collect labels and branch targets before running the checks below, which
+  // may return early.
   for (auto I = expr_op_begin(), E = expr_op_end(); I != E; ++I) {
     // Check that there's space for the operand.
     if (I->get() + I->getSize() > E->get())
       return false;
 
     uint64_t Op = I->getOp();
+
+    // Only DW_OP_LLVM_fragment may follow DW_OP_stack_value, and nothing may
+    // follow the fragment.
+    HasInvalidControlFlowSuffix |=
+        HasFragment || (HasStackValue && Op != dwarf::DW_OP_LLVM_fragment);
+
+    switch (Op) {
+    case dwarf::DW_OP_LLVM_label:
+      if (!Labels.insert(I->getArg(0)).second)
+        return false;
+      HasControlFlow = true;
+      break;
+    case dwarf::DW_OP_LLVM_bra:
+    case dwarf::DW_OP_LLVM_skip:
+      LabelReferences.push_back(I->getArg(0));
+      HasControlFlow = true;
+      break;
+    // We don't know the DW_OP_bra and DW_OP_skip offsets until CodeGen, so
+    // DIExpression uses the symbolic ops.
+    case dwarf::DW_OP_bra:
+    case dwarf::DW_OP_skip:
+      return false;
+    case dwarf::DW_OP_LLVM_tag_offset:
+      if (HasControlFlow)
+        return false;
+      break;
+    // These ops use lowering paths that don't support symbolic branches.
+    case dwarf::DW_OP_LLVM_arg:
+    case dwarf::DW_OP_LLVM_implicit_pointer:
+      HasControlFlowConflict = true;
+      break;
+    case dwarf::DW_OP_LLVM_entry_value:
+      HasControlFlowConflict |= !IsEntryValueValid(*I);
+      break;
+    default:
+      break;
+    }
+
+    if (Op == dwarf::DW_OP_stack_value)
+      HasStackValue = true;
+    else if (Op == dwarf::DW_OP_LLVM_fragment)
+      HasFragment = true;
+  }
+
+  if (HasControlFlow && (HasControlFlowConflict || HasInvalidControlFlowSuffix))
+    return false;
+  for (uint64_t Label : LabelReferences)
+    if (!Labels.contains(Label))
+      return false;
+
+  for (auto I = expr_op_begin(), E = expr_op_end(); I != E; ++I) {
+    uint64_t Op = I->getOp();
     if ((Op >= dwarf::DW_OP_reg0 && Op <= dwarf::DW_OP_reg31) ||
         (Op >= dwarf::DW_OP_breg0 && Op <= dwarf::DW_OP_breg31))
       continue;
@@ -1825,7 +1899,7 @@ bool DIExpression::isValid() const {
         return false;
       break;
     }
-    case dwarf::DW_OP_swap: {
+    case dwarf::DW_OP_swap:
       // Must be more than one implicit element on the stack.
 
       // FIXME: A better way to implement this would be to add a local variable
@@ -1836,25 +1910,22 @@ bool DIExpression::isValid() const {
       if (getNumElements() == 1)
         return false;
       break;
-    }
-    case dwarf::DW_OP_LLVM_entry_value: {
+    case dwarf::DW_OP_LLVM_entry_value:
       // An entry value operator must appear at the beginning or immediately
       // following `DW_OP_LLVM_arg 0`, and the number of operations it cover can
       // currently only be 1, because we support only entry values of a simple
       // register location. One reason for this is that we currently can't
       // calculate the size of the resulting DWARF block for other expressions.
-      auto FirstOp = expr_op_begin();
-      if (auto Arg = dyn_cast<ArgOp>(*FirstOp); Arg && Arg.getIndex() == 0)
-        ++FirstOp;
-      if (I->get() != FirstOp->get() ||
-          cast<EntryValueOp>(*I).getNumOperations() != 1)
+      if (!IsEntryValueValid(*I))
         return false;
       break;
-    }
     case dwarf::DW_OP_LLVM_implicit_pointer:
     case dwarf::DW_OP_LLVM_convert:
     case dwarf::DW_OP_LLVM_arg:
     case dwarf::DW_OP_LLVM_tag_offset:
+    case dwarf::DW_OP_LLVM_label:
+    case dwarf::DW_OP_LLVM_bra:
+    case dwarf::DW_OP_LLVM_skip:
     case dwarf::DW_OP_LLVM_extract_bits_sext:
     case dwarf::DW_OP_LLVM_extract_bits_zext:
     case dwarf::DW_OP_constu:
diff --git a/llvm/lib/IR/Verifier.cpp b/llvm/lib/IR/Verifier.cpp
index b813069d035f6..2ed1eb92a875f 100644
--- a/llvm/lib/IR/Verifier.cpp
+++ b/llvm/lib/IR/Verifier.cpp
@@ -7215,6 +7215,19 @@ void Verifier::visit(DbgVariableRecord &DVR) {
           F);
   visitMDNode(*DVR.getExpression(), AreDebugLocsAllowed::No);
 
+  // A DIArgList can have a valid branch expression which doesn't use
+  // DW_OP_LLVM_arg, so check the record as well.
+  if (DVR.hasArgList() && DVR.getExpression()->isValid()) {
+    bool HasControlFlow = llvm::any_of(
+        DVR.getExpression()->expr_ops(), [](DIExpression::ExprOperand Op) {
+          return Op.getOp() == dwarf::DW_OP_LLVM_label ||
+                 Op.getOp() == dwarf::DW_OP_LLVM_bra ||
+                 Op.getOp() == dwarf::DW_OP_LLVM_skip;
+        });
+    CheckDI(!HasControlFlow, "DIArgList doesn't support symbolic branches",
+            &DVR, MD, DVR.getExpression(), BB, F);
+  }
+
   if (DVR.isDbgAssign()) {
     CheckDI(isa_and_nonnull<DIAssignID>(DVR.getRawAssignID()),
             "invalid #dbg_assign DIAssignID", &DVR, DVR.getRawAssignID(), BB,
diff --git a/llvm/test/Assembler/di-expression-symbolic-branches.ll b/llvm/test/Assembler/di-expression-symbolic-branches.ll
new file mode 100644
index 0000000000000..475594c62d444
--- /dev/null
+++ b/llvm/test/Assembler/di-expression-symbolic-branches.ll
@@ -0,0 +1,12 @@
+; RUN: llvm-as < %s | llvm-dis | FileCheck %s
+
+; Labels use IDs in a DIExpression, so make sure the assembler keeps both
+; forward and backward references.
+
+!named = !{!0}
+
+; CHECK: !DIExpression(DW_OP_LLVM_label, 0, DW_OP_LLVM_bra, 42, DW_OP_LLVM_skip, 0, DW_OP_LLVM_label, 42)
+!0 = !DIExpression(DW_OP_LLVM_label, 0,
+                   DW_OP_LLVM_bra, 42,
+                   DW_OP_LLVM_skip, 0,
+                   DW_OP_LLVM_label, 42)
diff --git a/llvm/test/Bitcode/di-expression-symbolic-branches.ll b/llvm/test/Bitcode/di-expression-symbolic-branches.ll
new file mode 100644
index 0000000000000..82b3c630360fe
--- /dev/null
+++ b/llvm/test/Bitcode/di-expression-symbolic-branches.ll
@@ -0,0 +1,12 @@
+; RUN: llvm-as < %s | llvm-dis | llvm-as | llvm-dis | FileCheck %s
+
+; Label IDs are uint64_t values, so use the largest one while checking that the
+; opcodes and IDs survive two bitcode round-trips.
+
+!named = !{!0}
+
+; CHECK: !DIExpression(DW_OP_LLVM_label, 18446744073709551615, DW_OP_LLVM_bra, 7, DW_OP_LLVM_skip, 18446744073709551615, DW_OP_LLVM_label, 7)
+!0 = !DIExpression(DW_OP_LLVM_label, 18446744073709551615,
+                   DW_OP_LLVM_bra, 7,
+                   DW_OP_LLVM_skip, 18446744073709551615,
+                   DW_OP_LLVM_label, 7)
diff --git a/llvm/test/DebugInfo/MIR/X86/di-expression-symbolic-branches.mir b/llvm/test/DebugInfo/MIR/X86/di-expression-symbolic-branches.mir
new file mode 100644
index 0000000000000..eda9f210cd378
--- /dev/null
+++ b/llvm/test/DebugInfo/MIR/X86/di-expression-symbolic-branches.mir
@@ -0,0 +1,102 @@
+# RUN: split-file %s %t
+# RUN: llc -start-after=patchable-function -O0 -mtriple=x86_64-unknown-linux-gnu -filetype=asm -o - %t/valid.mir | FileCheck %s
+# RUN: llc -start-after=patchable-function -O0 -mtriple=x86_64-unknown-linux-gnu -filetype=obj -o - %t/valid.mir | llvm-dwarfdump - | FileCheck %s --check-prefix=ENTRY
+# RUN: llc -start-after=patchable-function -O0 -mtriple=x86_64-unknown-linux-gnu -dwarf-version=5 -filetype=obj -o - %t/valid.mir | llvm-dwarfdump - | FileCheck %s --check-prefix=NATIVE
+# RUN: not --crash llc -disable-verify -start-after=patchable-function -O0 -mtriple=x86_64-unknown-linux-gnu -filetype=obj -o /dev/null %t/missing-label.mir 2>&1 | FileCheck %s --check-prefix=MISSING
+
+# This checks the machine debug-expression path:
+#
+# - DWARF 4 expands conversions before we patch a location-list branch, while
+#   DWARF 5 emits DW_OP_convert.
+# - Entry values keep their branches when we emit an inline expression.
+# - With verification disabled, CodeGen still reports a missing label.
+
+#--- valid.mir
+--- |
+  define void @f() !dbg !5 {
+  entry:
+    ret void, !dbg !12
+  }
+
+  !llvm.dbg.cu = !{!0}
+  !llvm.module.flags = !{!3, !4}
+
+  !0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1, emissionKind: FullDebug)
+  !1 = !DIFile(filename: "test.c", directory: "/")
+  !3 = !{i32 2, !"Dwarf Version", i32 4}
+  !4 = !{i32 2, !"Debug Info Version", i32 3}
+  !5 = distinct !DISubprogram(name: "f", scope: !1, file: !1, type: !6, spFlags: DISPFlagDefinition, unit: !0, retainedNodes: !8)
+  !6 = !DISubroutineType(types: !7)
+  !7 = !{null}
+  !8 = !{!9, !10}
+  !9 = !DILocalVariable(name: "before", scope: !5, type: !11)
+  !10 = !DILocalVariable(name: "entry", arg: 1, scope: !5, type: !11)
+  !11 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
+  !12 = !DILocation(line: 1, scope: !5)
+...
+---
+name: f
+body: |
+  bb.0.entry:
+    $rcx = MOV64ri 1
+    DBG_VALUE $rdi, $noreg, !10, !DIExpression(DW_OP_LLVM_entry_value, 1, DW_OP_lit0, DW_OP_plus, DW_OP_LLVM_bra, 3, DW_OP_plus_uconst, 1, DW_OP_LLVM_label, 3), debug-location !12
+    DBG_VALUE $rcx, 0, !9, !DIExpression(DW_OP_LLVM_label, 1, DW_OP_LLVM_convert, 32, DW_ATE_signed, DW_OP_LLVM_convert, 64, DW_ATE_signed, DW_OP_plus_uconst, 1, DW_OP_LLVM_skip, 1), debug-location !12
+    $rcx = MOV64ri 2
+    RET64 debug-location !12
+...
+
+# CHECK: .Ldebug_loc0:
+# CHECK: .byte 114{{.*}}# DW_OP_breg2
+# CHECK-NEXT: .byte 0
+# CHECK-NEXT: .byte 18{{.*}}# DW_OP_dup
+# CHECK-NEXT: .byte 16{{.*}}# DW_OP_constu
+# CHECK-NEXT: .byte 31
+# CHECK-NEXT: .byte 37{{.*}}# DW_OP_shr
+# CHECK-NEXT: .byte 48{{.*}}# DW_OP_lit0
+# CHECK-NEXT: .byte 32{{.*}}# DW_OP_not
+# CHECK-NEXT: .byte 30{{.*}}# DW_OP_mul
+# CHECK-NEXT: .byte 16{{.*}}# DW_OP_constu
+# CHECK-NEXT: .byte 32
+# CHECK-NEXT: .byte 36{{.*}}# DW_OP_shl
+# CHECK-NEXT: .byte 33{{.*}}# DW_OP_or
+# CHECK-NEXT: .byte 35{{.*}}# DW_OP_plus_uconst
+# CHECK-NEXT: .byte 1
+# CHECK-NEXT: .byte 47{{.*}}# DW_OP_skip
+# CHECK-NEXT: .byte 240
+# CHECK-NEXT: .byte 255
+
+# ENTRY: DW_AT_location (DW_OP_GNU_entry_value(DW_OP_reg5 RDI), DW_OP_lit0, DW_OP_plus, DW_OP_bra +2, DW_OP_plus_uconst 0x1)
+
+# NATIVE: DW_OP_breg2 RCX+0, DW_OP_convert {{.*}} "DW_ATE_signed_32", DW_OP_convert {{.*}} "DW_ATE_signed_64", DW_OP_plus_uconst 0x1, DW_OP_skip -15
+
+# MISSING: LLVM ERROR: DWARF expression branch to label 2 has no matching label
+
+#--- missing-label.mir
+--- |
+  define void @missing_label() !dbg !5 {
+  entry:
+    ret void, !dbg !10
+  }
+
+  !llvm.dbg.cu = !{!0}
+  !llvm.module.flags = !{!3, !4}
+
+  !0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1, emissionKind: FullDebug)
+  !1 = !DIFile(filename: "test.c", directory: "/")
+  !3 = !{i32 2, !"Dwarf Version", i32 4}
+  !4 = !{i32 2, !"Debug Info Version", i32 3}
+  !5 = distinct !DISubprogram(name: "missing_label", scope: !1, file: !1, type: !6, spFlags: DISPFlagDefinition, unit: !0, retainedNodes: !8)
+  !6 = !DISubroutineType(types: !7)
+  !7 = !{null}
+  !8 = !{!9}
+  !9 = !DILocalVariable(name: "x", scope: !5, type: !11)
+  !10 = !DILocation(line: 1, scope: !5)
+  !11 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
+...
+---
+name: missing_label
+body: |
+  bb.0.entry:
+    DBG_VALUE 0, $noreg, !9, !DIExpression(DW_OP_plus_uconst, 1, DW_OP_LLVM_bra, 2), debug-location !10
+    RET64 debug-location !10
+...
diff --git a/llvm/test/DebugInfo/PowerPC/di-expression-symbolic-branches.ll b/llvm/test/DebugInfo/PowerPC/di-expression-symbolic-branches.ll
new file mode 100644
index 0000000000000..44159954127be
--- /dev/null
+++ b/llvm/test/DebugInfo/PowerPC/di-expression-symbolic-branches.ll
@@ -0,0 +1,44 @@
+; RUN: llc -mtriple=powerpc64-unknown-linux-gnu -filetype=asm -o - %s | FileCheck %s
+
+; We patch branches in two temporary buffers, so make sure both use the PowerPC
+; byte order: a forward skip in a location list and a backward branch in an
+; inline expression.
+
+define void @f(i64 %x) !dbg !5 {
+entry:
+  #dbg_value(i64 0, !9,
+             !DIExpression(DW_OP_LLVM_label, 2, DW_OP_LLVM_bra, 2), !11)
+  #dbg_value(i64 %x, !10,
+             !DIExpression(DW_OP_LLVM_skip, 3, DW_OP_plus_uconst, 1,
+                           DW_OP_LLVM_label, 3), !11)
+  call void @clobber(), !dbg !11
+  ret void, !dbg !11
+}
+
+declare void @clobber()
+
+; CHECK: .section .debug_loclists
+; CHECK: .byte 47{{.*}}# DW_OP_skip
+; CHECK-NEXT: .byte 0
+; CHECK-NEXT: .byte 2
+; CHECK: .byte 5{{.*}}# DW_AT_location
+; CHECK-NEXT: .byte 48
+; CHECK-NEXT: .byte 40
+; CHECK-NEXT: .short 65533
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!3, !4}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1,
+                             emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.c", directory: "/")
+!3 = !{i32 2, !"Dwarf Version", i32 5}
+!4 = !{i32 2, !"Debug Info Version", i32 3}
+!5 = distinct !DISubprogram(name: "f", scope: !1, type: !6,
+                            spFlags: DISPFlagDefinition, unit: !0)
+!6 = !DISubroutineType(types: !7)
+!7 = !{null}
+!9 = !DILocalVariable(name: "backward", scope: !5, type: !12)
+!10 = !DILocalVariable(name: "value", arg: 1, scope: !5, type: !12)
+!11 = !DILocation(line: 1, column: 1, scope: !5)
+!12 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
diff --git a/llvm/test/DebugInfo/X86/Inputs/generate-di-expression-symbolic-branch-out-of-range.py b/llvm/test/DebugInfo/X86/Inputs/generate-di-expression-symbolic-branch-out-of-range.py
new file mode 100644
index 0000000000000..fc7e0246a6ae2
--- /dev/null
+++ b/llvm/test/DebugInfo/X86/Inputs/generate-di-expression-symbolic-branch-out-of-range.py
@@ -0,0 +1,33 @@
+import sys
+
+# Put 32768 one-byte DW_OP_dup operations between the branch and its label,
+# which gives us the first offset that won't fit.
+DUPLICATE_OPS = "DW_OP_dup, " * 32768
+
+sys.stdout.write(
+    f"""\
+define void @f() !dbg !5 {{
+entry:
+  #dbg_value(i64 0, !9,
+             !DIExpression(DW_OP_LLVM_bra, 1, {DUPLICATE_OPS}
+                           DW_OP_LLVM_label, 1), !10)
+  ret void, !dbg !10
+}}
+
+!llvm.dbg.cu = !{{!0}}
+!llvm.module.flags = !{{!3}}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1,
+                             emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.c", directory: "/")
+!3 = !{{i32 2, !"Debug Info Version", i32 3}}
+!5 = distinct !DISubprogram(name: "f", scope: !1, file: !1, type: !6,
+                            spFlags: DISPFlagDefinition,
+                            unit: !0)
+!6 = !DISubroutineType(types: !7)
+!7 = !{{null}}
+!9 = !DILocalVariable(name: "x", scope: !5, type: !11)
+!10 = !DILocation(line: 1, scope: !5)
+!11 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
+"""
+)
diff --git a/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-convert-boundary.ll b/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-convert-boundary.ll
new file mode 100644
index 0000000000000..5cb9861502585
--- /dev/null
+++ b/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-convert-boundary.ll
@@ -0,0 +1,96 @@
+; RUN: split-file %s %t
+; RUN: llc -mtriple=x86_64-unknown-linux-gnu -dwarf-version=5 -filetype=obj -o - %t/skip.ll | llvm-dwarfdump -v - | FileCheck %s --check-prefix=SKIP-NATIVE
+; RUN: not --crash llc -mtriple=x86_64-unknown-linux-gnu -dwarf-version=4 -filetype=obj -o /dev/null %t/skip.ll 2>&1 | FileCheck %s --check-prefix=SKIP-LEGACY
+; RUN: llc -mtriple=x86_64-unknown-linux-gnu -dwarf-version=5 -filetype=obj -o - %t/bra.ll | llvm-dwarfdump -v - | FileCheck %s --check-prefix=BRA-NATIVE
+; RUN: not --crash llc -mtriple=x86_64-unknown-linux-gnu -dwarf-version=4 -filetype=obj -o /dev/null %t/bra.ll 2>&1 | FileCheck %s --check-prefix=BRA-LEGACY
+; RUN: llc -mtriple=x86_64-unknown-linux-gnu -dwarf-version=5 -filetype=obj -o - %t/label.ll | llvm-dwarfdump -v - | FileCheck %s --check-prefix=LABEL-NATIVE
+; RUN: not --crash llc -mtriple=x86_64-unknown-linux-gnu -dwarf-version=4 -filetype=obj -o /dev/null %t/label.ll 2>&1 | FileCheck %s --check-prefix=LABEL-LEGACY
+
+; DWARF 5 emits DW_OP_convert directly, so labels and branches can appear
+; between conversions. With DWARF 4 we may defer one conversion until the next;
+; if a label, branch, or skip splits the pair, CodeGen reports an error.
+
+; SKIP-NATIVE: DW_AT_location [DW_FORM_exprloc] (DW_OP_breg5 RDI+0, DW_OP_convert {{.*}} "DW_ATE_signed_32", DW_OP_skip +5, DW_OP_convert {{.*}} "DW_ATE_signed_64")
+; SKIP-LEGACY: LLVM ERROR: cannot lower DW_OP_LLVM_convert across DW_OP_LLVM_skip without DW_OP_convert support
+
+; BRA-NATIVE: DW_AT_location [DW_FORM_exprloc] (DW_OP_breg5 RDI+0, DW_OP_convert {{.*}} "DW_ATE_signed_32", DW_OP_dup, DW_OP_bra +5, DW_OP_convert {{.*}} "DW_ATE_signed_32")
+; BRA-LEGACY: LLVM ERROR: cannot lower DW_OP_LLVM_convert across DW_OP_LLVM_bra without DW_OP_convert support
+
+; LABEL-NATIVE: DW_AT_location [DW_FORM_exprloc] (DW_OP_breg5 RDI+0, DW_OP_convert {{.*}} "DW_ATE_signed_32")
+; LABEL-LEGACY: LLVM ERROR: cannot lower DW_OP_LLVM_convert across DW_OP_LLVM_label without DW_OP_convert support
+
+;--- skip.ll
+define void @skip(i64 %x) !dbg !5 {
+entry:
+  #dbg_value(i64 %x, !9,
+             !DIExpression(DW_OP_LLVM_convert, 32, DW_ATE_signed,
+                           DW_OP_LLVM_skip, 1,
+                           DW_OP_LLVM_convert, 64, DW_ATE_signed,
+                           DW_OP_LLVM_label, 1), !10)
+  ret void, !dbg !10
+}
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!4}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1,
+                             emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.c", directory: "/")
+!4 = !{i32 2, !"Debug Info Version", i32 3}
+!5 = distinct !DISubprogram(name: "skip", scope: !1, type: !6,
+                            spFlags: DISPFlagDefinition, unit: !0)
+!6 = !DISubroutineType(types: !7)
+!7 = !{null}
+!9 = !DILocalVariable(name: "skip", arg: 1, scope: !5, type: !11)
+!10 = !DILocation(line: 1, column: 1, scope: !5)
+!11 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
+
+;--- label.ll
+define void @label(i64 %x) !dbg !5 {
+entry:
+  #dbg_value(i64 %x, !9,
+             !DIExpression(DW_OP_LLVM_convert, 32, DW_ATE_signed,
+                           DW_OP_LLVM_label, 3), !10)
+  ret void, !dbg !10
+}
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!4}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1,
+                             emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.c", directory: "/")
+!4 = !{i32 2, !"Debug Info Version", i32 3}
+!5 = distinct !DISubprogram(name: "label", scope: !1, type: !6,
+                            spFlags: DISPFlagDefinition, unit: !0)
+!6 = !DISubroutineType(types: !7)
+!7 = !{null}
+!9 = !DILocalVariable(name: "label", arg: 1, scope: !5, type: !11)
+!10 = !DILocation(line: 1, column: 1, scope: !5)
+!11 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
+
+;--- bra.ll
+define void @bra(i64 %x) !dbg !5 {
+entry:
+  #dbg_value(i64 %x, !9,
+             !DIExpression(DW_OP_LLVM_convert, 32, DW_ATE_signed, DW_OP_dup,
+                           DW_OP_LLVM_bra, 2,
+                           DW_OP_LLVM_convert, 32, DW_ATE_signed,
+                           DW_OP_LLVM_label, 2), !10)
+  ret void, !dbg !10
+}
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!4}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1,
+                             emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.c", directory: "/")
+!4 = !{i32 2, !"Debug Info Version", i32 3}
+!5 = distinct !DISubprogram(name: "bra", scope: !1, type: !6,
+                            spFlags: DISPFlagDefinition, unit: !0)
+!6 = !DISubroutineType(types: !7)
+!7 = !{null}
+!9 = !DILocalVariable(name: "bra", arg: 1, scope: !5, type: !11)
+!10 = !DILocation(line: 1, column: 1, scope: !5)
+!11 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
diff --git a/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-out-of-range.ll b/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-out-of-range.ll
new file mode 100644
index 0000000000000..677032dc9c7a7
--- /dev/null
+++ b/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-out-of-range.ll
@@ -0,0 +1,7 @@
+; RUN: %python %S/Inputs/generate-di-expression-symbolic-branch-out-of-range.py > %t.ll
+; RUN: not --crash llc -mtriple=x86_64-unknown-linux-gnu -filetype=obj -o /dev/null %t.ll 2>&1 | FileCheck %s
+
+; The first positive offset that doesn't fit is 32768, so make sure CodeGen
+; reports it instead of truncating it.
+
+; CHECK: LLVM ERROR: DWARF expression branch offset 32768 is outside [-32768, 32767]
diff --git a/llvm/test/DebugInfo/X86/di-expression-symbolic-branches.ll b/llvm/test/DebugInfo/X86/di-expression-symbolic-branches.ll
new file mode 100644
index 0000000000000..163c3c24c6927
--- /dev/null
+++ b/llvm/test/DebugInfo/X86/di-expression-symbolic-branches.ll
@@ -0,0 +1,84 @@
+; RUN: llc -mtriple=x86_64-unknown-linux-gnu -filetype=obj -o - %s | llvm-dwarfdump -v - | FileCheck %s
+; RUN: llc -mtriple=x86_64-unknown-linux-gnu -dwarf-version=4 -filetype=obj -o - %s | llvm-dwarfdump - | FileCheck %s --check-prefix=LEGACY
+
+; Branch offsets come from the emitted bytes, so check:
+;
+; - zero, forward, backward, and cyclic branches;
+; - ops which grow during lowering;
+; - labels on either side of an emitted op; and
+; - the fragment return path and DWARF 4 convert expansion.
+
+define void @f() !dbg !5 {
+entry:
+  #dbg_value(i64 0, !9,
+             !DIExpression(DW_OP_LLVM_bra, 1, DW_OP_LLVM_label, 1), !18)
+  #dbg_value(i64 0, !10,
+             !DIExpression(DW_OP_LLVM_label, 2, DW_OP_LLVM_skip, 2), !18)
+  #dbg_value(i64 0, !11,
+             !DIExpression(DW_OP_LLVM_label, 3, DW_OP_LLVM_bra, 4,
+                           DW_OP_LLVM_skip, 3, DW_OP_LLVM_label, 4), !18)
+  #dbg_value(i64 0, !12,
+             !DIExpression(DW_OP_LLVM_bra, 5, DW_OP_deref_size, 1,
+                           DW_OP_plus_uconst, 128,
+                           DW_OP_LLVM_extract_bits_sext, 4, 4,
+                           DW_OP_LLVM_label, 5), !18)
+  #dbg_value(i64 0, !13,
+             !DIExpression(DW_OP_LLVM_skip, 6,
+                           DW_OP_LLVM_convert, 32, DW_ATE_signed,
+                           DW_OP_LLVM_convert, 64, DW_ATE_signed,
+                           DW_OP_LLVM_label, 6), !18)
+  #dbg_value(i64 0, !14,
+             !DIExpression(DW_OP_LLVM_bra, 7, DW_OP_LLVM_label, 7,
+                           DW_OP_stack_value, DW_OP_LLVM_fragment, 0, 32), !18)
+  #dbg_value(i64 0, !15,
+             !DIExpression(DW_OP_plus_uconst, 1, DW_OP_LLVM_label, 8,
+                           DW_OP_LLVM_skip, 8), !18)
+  #dbg_value(i64 0, !16,
+             !DIExpression(DW_OP_LLVM_label, 9, DW_OP_plus_uconst, 1,
+                           DW_OP_LLVM_skip, 9), !18)
+  ret void, !dbg !18
+}
+
+; CHECK: DW_AT_location [DW_FORM_exprloc] (DW_OP_lit0, DW_OP_bra +0, DW_OP_stack_value)
+; CHECK: DW_AT_name{{.*}}"zero"
+; CHECK: DW_AT_location [DW_FORM_exprloc] (DW_OP_lit0, DW_OP_skip -3, DW_OP_stack_value)
+; CHECK: DW_AT_name{{.*}}"backward"
+; CHECK: DW_AT_location [DW_FORM_exprloc] (DW_OP_lit0, DW_OP_bra +3, DW_OP_skip -6, DW_OP_stack_value)
+; CHECK: DW_AT_name{{.*}}"cycle"
+; CHECK: DW_AT_location [DW_FORM_exprloc] (DW_OP_lit0, DW_OP_bra +11, DW_OP_deref_size 0x1, DW_OP_plus_uconst 0x80, DW_OP_constu 0x38, DW_OP_shl, DW_OP_constu 0x3c, DW_OP_shra, DW_OP_stack_value)
+; CHECK: DW_AT_name{{.*}}"expanded"
+; CHECK: DW_AT_location [DW_FORM_exprloc] (DW_OP_lit0, DW_OP_skip +10, DW_OP_convert {{.*}} "DW_ATE_signed_32", DW_OP_convert {{.*}} "DW_ATE_signed_64", DW_OP_stack_value)
+; CHECK: DW_AT_name{{.*}}"convert"
+; CHECK: DW_AT_location [DW_FORM_exprloc] (DW_OP_lit0, DW_OP_bra +0, DW_OP_stack_value, DW_OP_piece 0x4)
+; CHECK: DW_AT_name{{.*}}"fragment"
+; CHECK: DW_AT_location [DW_FORM_exprloc] (DW_OP_lit0, DW_OP_plus_uconst 0x1, DW_OP_skip -3, DW_OP_stack_value)
+; CHECK: DW_AT_name{{.*}}"label_after_offset"
+; CHECK: DW_AT_location [DW_FORM_exprloc] (DW_OP_lit0, DW_OP_plus_uconst 0x1, DW_OP_skip -5, DW_OP_stack_value)
+; CHECK: DW_AT_name{{.*}}"label_before_offset"
+
+; LEGACY: DW_AT_location (DW_OP_lit0, DW_OP_skip +11, DW_OP_dup, DW_OP_constu 0x1f, DW_OP_shr, DW_OP_lit0, DW_OP_not, DW_OP_mul, DW_OP_constu 0x20, DW_OP_shl, DW_OP_or, DW_OP_stack_value)
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!3, !4}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1,
+                             emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.c", directory: "/")
+!3 = !{i32 2, !"Dwarf Version", i32 5}
+!4 = !{i32 2, !"Debug Info Version", i32 3}
+!5 = distinct !DISubprogram(name: "f", scope: !1, file: !1, type: !6,
+                            spFlags: DISPFlagDefinition,
+                            unit: !0, retainedNodes: !8)
+!6 = !DISubroutineType(types: !7)
+!7 = !{null}
+!8 = !{!9, !10, !11, !12, !13, !14, !15, !16}
+!9 = !DILocalVariable(name: "zero", scope: !5, type: !19)
+!10 = !DILocalVariable(name: "backward", scope: !5, type: !19)
+!11 = !DILocalVariable(name: "cycle", scope: !5, type: !19)
+!12 = !DILocalVariable(name: "expanded", scope: !5, type: !19)
+!13 = !DILocalVariable(name: "convert", scope: !5, type: !19)
+!14 = !DILocalVariable(name: "fragment", scope: !5, type: !19)
+!15 = !DILocalVariable(name: "label_after_offset", scope: !5, type: !19)
+!16 = !DILocalVariable(name: "label_before_offset", scope: !5, type: !19)
+!18 = !DILocation(line: 1, scope: !5)
+!19 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
diff --git a/llvm/test/Verifier/di-expression-symbolic-branches.ll b/llvm/test/Verifier/di-expression-symbolic-branches.ll
new file mode 100644
index 0000000000000..db0e0dacdf32f
--- /dev/null
+++ b/llvm/test/Verifier/di-expression-symbolic-branches.ll
@@ -0,0 +1,93 @@
+; RUN: split-file %s %t
+; RUN: opt -passes=verify -disable-output %t/valid.ll
+; RUN: not opt -passes=verify -disable-output %t/arity.ll 2>&1 | FileCheck %s --check-prefix=INVALID --implicit-check-not="invalid expression"
+; RUN: not opt -passes=verify -disable-output %t/duplicate.ll 2>&1 | FileCheck %s --check-prefix=INVALID --implicit-check-not="invalid expression"
+; RUN: not opt -passes=verify -disable-output %t/missing.ll 2>&1 | FileCheck %s --check-prefix=INVALID --implicit-check-not="invalid expression"
+; RUN: not opt -passes=verify -disable-output %t/raw.ll 2>&1 | FileCheck %s --check-prefix=INVALID --implicit-check-not="invalid expression"
+; RUN: not opt -passes=verify -disable-output %t/raw-after-register.ll 2>&1 | FileCheck %s --check-prefix=INVALID --implicit-check-not="invalid expression"
+; RUN: not opt -passes=verify -disable-output %t/incompatible.ll 2>&1 | FileCheck %s --check-prefix=INVALID --implicit-check-not="invalid expression"
+; RUN: not opt -passes=verify -disable-output %t/location-arg.ll 2>&1 | FileCheck %s --check-prefix=INVALID --implicit-check-not="invalid expression"
+; RUN: not opt -passes=verify -disable-output %t/tag-ordering.ll 2>&1 | FileCheck %s --check-prefix=INVALID --implicit-check-not="invalid expression"
+; RUN: not opt -passes=verify -disable-output %t/terminal.ll 2>&1 | FileCheck %s --check-prefix=INVALID --implicit-check-not="invalid expression"
+
+; Check the verifier rules separately:
+;
+; - a label needs an ID;
+; - labels are unique and each branch target exists;
+; - raw branches and incompatible ops are rejected; and
+; - tag_offset stays before control flow, which stays before stack_value.
+
+; DIArgList doesn't support symbolic branches, but normal IR loading drops the
+; bad debug info, so opt still succeeds.
+; RUN: opt -passes=verify -disable-output %t/arg-list.ll 2>&1 | FileCheck %s --check-prefix=ARG-LIST
+
+; INVALID: invalid expression
+; ARG-LIST: DIArgList doesn't support symbolic branches
+; ARG-LIST: warning: ignoring invalid debug info
+
+;--- valid.ll
+!named = !{!0}
+!0 = !DIExpression(DW_OP_LLVM_bra, 0, DW_OP_LLVM_skip, 0,
+                   DW_OP_LLVM_label, 0)
+
+;--- arity.ll
+!named = !{!0}
+!0 = !DIExpression(DW_OP_LLVM_label)
+
+;--- duplicate.ll
+!named = !{!0}
+!0 = !DIExpression(DW_OP_LLVM_label, 1, DW_OP_LLVM_label, 1)
+
+;--- missing.ll
+!named = !{!0}
+!0 = !DIExpression(DW_OP_LLVM_bra, 1)
+
+;--- raw.ll
+!named = !{!0}
+!0 = !DIExpression(DW_OP_bra, 0)
+
+;--- raw-after-register.ll
+; A register normally ends validation, but it must not hide a raw branch later
+; in the expression.
+!named = !{!0}
+!0 = !DIExpression(DW_OP_reg0, DW_OP_skip, 0)
+
+;--- incompatible.ll
+!named = !{!0}
+!0 = !DIExpression(DW_OP_LLVM_implicit_pointer, DW_OP_LLVM_label, 1)
+
+;--- location-arg.ll
+!named = !{!0}
+!0 = !DIExpression(DW_OP_LLVM_arg, 0, DW_OP_LLVM_skip, 1,
+                   DW_OP_LLVM_label, 1)
+
+;--- tag-ordering.ll
+!named = !{!0}
+!0 = !DIExpression(DW_OP_LLVM_label, 1, DW_OP_LLVM_tag_offset, 0)
+
+;--- terminal.ll
+!named = !{!0}
+!0 = !DIExpression(DW_OP_stack_value, DW_OP_LLVM_label, 1)
+
+;--- arg-list.ll
+; The expression is valid by itself, so this diagnostic comes from DIArgList.
+define void @f(i32 %x) !dbg !4 {
+entry:
+  #dbg_value(!DIArgList(i32 %x), !7,
+             !DIExpression(DW_OP_LLVM_skip, 1, DW_OP_LLVM_label, 1), !8)
+  ret void, !dbg !8
+}
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1,
+                             emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.c", directory: "/")
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!4 = distinct !DISubprogram(name: "f", scope: !1, type: !5,
+                            spFlags: DISPFlagDefinition, unit: !0)
+!5 = !DISubroutineType(types: !6)
+!6 = !{null}
+!7 = !DILocalVariable(name: "x", scope: !4)
+!8 = !DILocation(line: 1, column: 1, scope: !4)
diff --git a/llvm/unittests/BinaryFormat/DwarfTest.cpp b/llvm/unittests/BinaryFormat/DwarfTest.cpp
index c9522226c6031..078482639be22 100644
--- a/llvm/unittests/BinaryFormat/DwarfTest.cpp
+++ b/llvm/unittests/BinaryFormat/DwarfTest.cpp
@@ -47,6 +47,13 @@ TEST(DwarfTest, getOperationEncoding) {
   // Some valid ops.
   EXPECT_EQ(DW_OP_deref, getOperationEncoding("DW_OP_deref"));
   EXPECT_EQ(DW_OP_bit_piece, getOperationEncoding("DW_OP_bit_piece"));
+  // These are metadata-only ops, but they still need names in both directions.
+  EXPECT_EQ(DW_OP_LLVM_label, getOperationEncoding("DW_OP_LLVM_label"));
+  EXPECT_EQ(DW_OP_LLVM_bra, getOperationEncoding("DW_OP_LLVM_bra"));
+  EXPECT_EQ(DW_OP_LLVM_skip, getOperationEncoding("DW_OP_LLVM_skip"));
+  EXPECT_EQ("DW_OP_LLVM_label", OperationEncodingString(DW_OP_LLVM_label));
+  EXPECT_EQ("DW_OP_LLVM_bra", OperationEncodingString(DW_OP_LLVM_bra));
+  EXPECT_EQ("DW_OP_LLVM_skip", OperationEncodingString(DW_OP_LLVM_skip));
 
   // Invalid ops.
   EXPECT_EQ(0u, getOperationEncoding("DW_OP_otherthings"));
diff --git a/llvm/unittests/IR/MetadataTest.cpp b/llvm/unittests/IR/MetadataTest.cpp
index 07496cec696ca..ce09eb6d40bbe 100644
--- a/llvm/unittests/IR/MetadataTest.cpp
+++ b/llvm/unittests/IR/MetadataTest.cpp
@@ -4417,6 +4417,86 @@ TEST_F(DIExpressionTest, Append) {
   EXPECT_EQ(ResExpr, AppendExpr);
 }
 
+TEST_F(DIExpressionTest, SymbolicBranchRewrites) {
+  // A symbolic op has an ID operand, so fold the math on both sides without
+  // consuming it.
+  for (uint64_t Barrier : {dwarf::DW_OP_LLVM_label, dwarf::DW_OP_LLVM_bra,
+                           dwarf::DW_OP_LLVM_skip}) {
+    SmallVector<uint64_t> Ops = {dwarf::DW_OP_constu,
+                                 1,
+                                 dwarf::DW_OP_constu,
+                                 2,
+                                 dwarf::DW_OP_plus,
+                                 dwarf::DW_OP_constu,
+                                 4,
+                                 Barrier,
+                                 7,
+                                 dwarf::DW_OP_constu,
+                                 5,
+                                 dwarf::DW_OP_plus,
+                                 dwarf::DW_OP_constu,
+                                 6,
+                                 dwarf::DW_OP_constu,
+                                 7,
+                                 dwarf::DW_OP_plus};
+    SmallVector<uint64_t> Expected = {
+        dwarf::DW_OP_constu,      3, dwarf::DW_OP_constu, 4, Barrier, 7,
+        dwarf::DW_OP_plus_uconst, 5, dwarf::DW_OP_constu, 13};
+    if (Barrier != dwarf::DW_OP_LLVM_label) {
+      Ops.append({dwarf::DW_OP_LLVM_label, 7});
+      Expected.append({dwarf::DW_OP_LLVM_label, 7});
+    }
+    EXPECT_EQ(DIExpression::get(Context, Expected),
+              DIExpression::get(Context, Ops)->foldConstantMath());
+  }
+
+  // Adding ops before or after an expression leaves its labels in place.
+  SmallVector<uint64_t> Ops = {dwarf::DW_OP_LLVM_label, 7,
+                               dwarf::DW_OP_plus_uconst, 1};
+  auto *Expr = DIExpression::get(Context, Ops);
+  auto *Prepended = DIExpression::prepend(Expr, DIExpression::DerefBefore, 0);
+  SmallVector<uint64_t> Expected = {dwarf::DW_OP_deref, dwarf::DW_OP_LLVM_label,
+                                    7, dwarf::DW_OP_plus_uconst, 1};
+  EXPECT_EQ(DIExpression::get(Context, Expected), Prepended);
+
+  SmallVector<uint64_t> Prefix = {dwarf::DW_OP_LLVM_tag_offset, 3};
+  Prepended = DIExpression::prependOpcodes(Expr, Prefix);
+  Expected = {dwarf::DW_OP_LLVM_tag_offset, 3, dwarf::DW_OP_LLVM_label, 7,
+              dwarf::DW_OP_plus_uconst,     1};
+  EXPECT_EQ(DIExpression::get(Context, Expected), Prepended);
+  EXPECT_TRUE(Prepended->isValid());
+
+  SmallVector<uint64_t> AppendOps = {
+      dwarf::DW_OP_LLVM_convert, 32, dwarf::DW_ATE_signed,
+      dwarf::DW_OP_LLVM_convert, 64, dwarf::DW_ATE_signed};
+  auto *Appended = DIExpression::append(Expr, AppendOps);
+  Expected = {dwarf::DW_OP_LLVM_label,
+              7,
+              dwarf::DW_OP_plus_uconst,
+              1,
+              dwarf::DW_OP_LLVM_convert,
+              32,
+              dwarf::DW_ATE_signed,
+              dwarf::DW_OP_LLVM_convert,
+              64,
+              dwarf::DW_ATE_signed};
+  EXPECT_EQ(DIExpression::get(Context, Expected), Appended);
+  EXPECT_TRUE(Appended->isValid());
+
+  // Label IDs are arbitrary uint64_t values, so appendExt must not mistake one
+  // for DW_OP_stack_value.
+  uint64_t LabelID = dwarf::DW_OP_stack_value;
+  Ops = {dwarf::DW_OP_LLVM_label, LabelID};
+  Appended =
+      DIExpression::appendExt(DIExpression::get(Context, Ops), 32, 64, true);
+  Expected = {dwarf::DW_OP_LLVM_label,   LabelID, dwarf::DW_OP_deref,
+              dwarf::DW_OP_LLVM_convert, 32,      dwarf::DW_ATE_signed,
+              dwarf::DW_OP_LLVM_convert, 64,      dwarf::DW_ATE_signed,
+              dwarf::DW_OP_stack_value};
+  EXPECT_EQ(DIExpression::get(Context, Expected), Appended);
+  EXPECT_TRUE(Appended->isValid());
+}
+
 TEST_F(DIExpressionTest, isValid) {
 #define EXPECT_VALID(...)                                                      \
   do {                                                                         \
@@ -4437,6 +4517,7 @@ TEST_F(DIExpressionTest, isValid) {
   EXPECT_VALID(dwarf::DW_OP_constu, 6, dwarf::DW_OP_plus);
   EXPECT_VALID(dwarf::DW_OP_constu, 5, dwarf::DW_OP_swap);
   EXPECT_VALID(dwarf::DW_OP_deref);
+  EXPECT_VALID(dwarf::DW_OP_stack_value);
   EXPECT_VALID(dwarf::DW_OP_LLVM_fragment, 3, 7);
   EXPECT_VALID(dwarf::DW_OP_plus_uconst, 6, dwarf::DW_OP_deref);
   EXPECT_VALID(dwarf::DW_OP_deref, dwarf::DW_OP_plus_uconst, 6);
@@ -4448,12 +4529,31 @@ TEST_F(DIExpressionTest, isValid) {
   EXPECT_VALID(dwarf::DW_OP_LLVM_entry_value, 1);
   EXPECT_VALID(dwarf::DW_OP_LLVM_entry_value, 1, dwarf::DW_OP_plus_uconst, 6);
   EXPECT_VALID(dwarf::DW_OP_LLVM_arg, 0, dwarf::DW_OP_LLVM_entry_value, 1);
+  // A label may be unused, and branches may refer forward or backward.
+  EXPECT_VALID(dwarf::DW_OP_LLVM_label, 1, dwarf::DW_OP_LLVM_bra, 1);
+  EXPECT_VALID(dwarf::DW_OP_LLVM_skip, 2, dwarf::DW_OP_LLVM_label, 2);
+  EXPECT_VALID(dwarf::DW_OP_LLVM_label, 3, dwarf::DW_OP_LLVM_label, 4);
+  EXPECT_VALID(dwarf::DW_OP_LLVM_label, 4, dwarf::DW_OP_LLVM_skip, 5,
+               dwarf::DW_OP_LLVM_label, 5, dwarf::DW_OP_LLVM_skip, 4);
+  // Label IDs can use the full uint64_t range.
+  EXPECT_VALID(
+      dwarf::DW_OP_LLVM_label, std::numeric_limits<uint64_t>::max() - 1,
+      dwarf::DW_OP_LLVM_label, std::numeric_limits<uint64_t>::max(),
+      dwarf::DW_OP_LLVM_skip, std::numeric_limits<uint64_t>::max() - 1);
+  EXPECT_VALID(dwarf::DW_OP_LLVM_label, 5, dwarf::DW_OP_stack_value);
+  EXPECT_VALID(dwarf::DW_OP_LLVM_label, 6, dwarf::DW_OP_stack_value,
+               dwarf::DW_OP_LLVM_fragment, 0, 32);
+  EXPECT_VALID(dwarf::DW_OP_LLVM_tag_offset, 1, dwarf::DW_OP_LLVM_label, 6,
+               dwarf::DW_OP_LLVM_skip, 6, dwarf::DW_OP_LLVM_convert, 32,
+               dwarf::DW_ATE_signed, dwarf::DW_OP_stack_value,
+               dwarf::DW_OP_LLVM_fragment, 0, 32);
 
   // Invalid constructions.
   EXPECT_INVALID(~0u);
   EXPECT_INVALID(dwarf::DW_OP_plus, 0);
   EXPECT_INVALID(dwarf::DW_OP_plus_uconst);
   EXPECT_INVALID(dwarf::DW_OP_swap);
+  EXPECT_INVALID(dwarf::DW_OP_stack_value, dwarf::DW_OP_deref);
   EXPECT_INVALID(dwarf::DW_OP_LLVM_fragment);
   EXPECT_INVALID(dwarf::DW_OP_LLVM_fragment, 3);
   EXPECT_INVALID(dwarf::DW_OP_LLVM_fragment, 3, 7, dwarf::DW_OP_plus_uconst, 3);
@@ -4463,6 +4563,48 @@ TEST_F(DIExpressionTest, isValid) {
   EXPECT_INVALID(dwarf::DW_OP_LLVM_arg, 0, dwarf::DW_OP_plus_uconst, 5,
                  dwarf::DW_OP_LLVM_entry_value, 1);
   EXPECT_INVALID(dwarf::DW_OP_LLVM_arg, 1, dwarf::DW_OP_LLVM_entry_value, 1);
+  EXPECT_INVALID(dwarf::DW_OP_LLVM_label);
+  EXPECT_INVALID(dwarf::DW_OP_LLVM_bra);
+  EXPECT_INVALID(dwarf::DW_OP_LLVM_skip);
+  EXPECT_INVALID(dwarf::DW_OP_LLVM_label, 1, dwarf::DW_OP_LLVM_label, 1);
+  EXPECT_INVALID(dwarf::DW_OP_LLVM_bra, 1);
+  EXPECT_INVALID(dwarf::DW_OP_LLVM_skip, 1);
+  EXPECT_INVALID(dwarf::DW_OP_bra, 0);
+  EXPECT_INVALID(dwarf::DW_OP_skip, 0);
+  EXPECT_INVALID(dwarf::DW_OP_reg0, dwarf::DW_OP_bra, 0);
+  EXPECT_INVALID(dwarf::DW_OP_reg0, dwarf::DW_OP_skip, 0);
+  EXPECT_INVALID(dwarf::DW_OP_LLVM_arg, 0, dwarf::DW_OP_LLVM_label, 1);
+  EXPECT_INVALID(dwarf::DW_OP_LLVM_implicit_pointer, dwarf::DW_OP_LLVM_label,
+                 1);
+  EXPECT_INVALID(dwarf::DW_OP_LLVM_label, 1, dwarf::DW_OP_LLVM_tag_offset, 0);
+  // Converts and other stack ops can appear around labels and branches.
+  EXPECT_VALID(dwarf::DW_OP_LLVM_convert, 32, dwarf::DW_ATE_signed,
+               dwarf::DW_OP_LLVM_label, 1);
+  EXPECT_VALID(dwarf::DW_OP_LLVM_convert, 32, dwarf::DW_ATE_signed,
+               dwarf::DW_OP_LLVM_skip, 7, dwarf::DW_OP_LLVM_label, 7);
+  EXPECT_VALID(dwarf::DW_OP_LLVM_skip, 1, dwarf::DW_OP_LLVM_convert, 32,
+               dwarf::DW_ATE_signed, dwarf::DW_OP_LLVM_convert, 64,
+               dwarf::DW_ATE_signed, dwarf::DW_OP_LLVM_label, 1);
+  EXPECT_VALID(dwarf::DW_OP_LLVM_convert, 32, dwarf::DW_ATE_signed,
+               dwarf::DW_OP_dup, dwarf::DW_OP_LLVM_bra, 1,
+               dwarf::DW_OP_LLVM_convert, 32, dwarf::DW_ATE_signed,
+               dwarf::DW_OP_LLVM_label, 1);
+  EXPECT_INVALID(dwarf::DW_OP_stack_value, dwarf::DW_OP_LLVM_label, 1);
+  EXPECT_INVALID(dwarf::DW_OP_LLVM_fragment, 0, 32, dwarf::DW_OP_LLVM_label, 1);
+  // reg0 and entry_value normally end validation early, but they must not hide
+  // invalid control flow later in the expression.
+  EXPECT_INVALID(dwarf::DW_OP_LLVM_entry_value, 1, dwarf::DW_OP_stack_value,
+                 dwarf::DW_OP_LLVM_bra, 1, dwarf::DW_OP_LLVM_label, 1);
+  EXPECT_INVALID(dwarf::DW_OP_reg0, dwarf::DW_OP_LLVM_fragment, 0, 32,
+                 dwarf::DW_OP_LLVM_label, 1);
+  EXPECT_INVALID(dwarf::DW_OP_reg0, dwarf::DW_OP_LLVM_bra, 1,
+                 dwarf::DW_OP_LLVM_label, 1, dwarf::DW_OP_LLVM_fragment, 0, 32,
+                 dwarf::DW_OP_plus);
+  EXPECT_INVALID(dwarf::DW_OP_LLVM_entry_value, 1, dwarf::DW_OP_LLVM_bra, 1,
+                 dwarf::DW_OP_LLVM_label, 1, dwarf::DW_OP_stack_value,
+                 dwarf::DW_OP_plus);
+  EXPECT_INVALID(dwarf::DW_OP_reg0, dwarf::DW_OP_LLVM_label, 1,
+                 dwarf::DW_OP_LLVM_entry_value, 1);
 
   // A valid operation doesn't make a malformed suffix valid.
   EXPECT_INVALID(dwarf::DW_OP_reg0, dwarf::DW_OP_stack_value,
@@ -4547,6 +4689,19 @@ TEST_F(DIExpressionTest, createFragmentExpression) {
   EXPECT_INVALID_FRAGMENT(0, 32, dwarf::DW_OP_deref, dwarf::DW_OP_plus_uconst,
                           2, dwarf::DW_OP_stack_value);
 
+  // Creating a fragment leaves the branches alone and adds the fragment at the
+  // end.
+  SmallVector<uint64_t> ControlFlowOps = {dwarf::DW_OP_LLVM_label, 1,
+                                          dwarf::DW_OP_LLVM_bra,   1,
+                                          dwarf::DW_OP_LLVM_skip,  1};
+  DIExpression *ControlFlowExpr = DIExpression::get(Context, ControlFlowOps);
+  auto Fragment =
+      DIExpression::createFragmentExpression(ControlFlowExpr, 0, 32);
+  ASSERT_TRUE(Fragment.has_value());
+  ControlFlowOps.append({dwarf::DW_OP_LLVM_fragment, 0, 32});
+  EXPECT_EQ(DIExpression::get(Context, ControlFlowOps), *Fragment);
+  EXPECT_TRUE((*Fragment)->isValid());
+
 #undef EXPECT_VALID_FRAGMENT
 #undef EXPECT_INVALID_FRAGMENT
 }
diff --git a/mlir/test/Target/LLVMIR/Import/debug-info-symbolic-branches.ll b/mlir/test/Target/LLVMIR/Import/debug-info-symbolic-branches.ll
new file mode 100644
index 0000000000000..7ba2b0802fa3c
--- /dev/null
+++ b/mlir/test/Target/LLVMIR/Import/debug-info-symbolic-branches.ll
@@ -0,0 +1,23 @@
+; RUN: mlir-translate -import-llvm -mlir-print-debuginfo -emit-expensive-warnings %s 2>&1 | FileCheck %s
+
+; Import a forward branch and a backward skip, and make sure MLIR keeps the op
+; order and label IDs.
+
+; CHECK: llvm.intr.dbg.value {{.*}} #llvm.di_expression<[DW_OP_LLVM_label(0), DW_OP_LLVM_bra(42), DW_OP_LLVM_skip(0), DW_OP_LLVM_label(42)]> = {{.*}} : i64
+define void @f(i64 %x) !dbg !4 {
+  #dbg_value(i64 %x, !DILocalVariable(scope: !4),
+             !DIExpression(DW_OP_LLVM_label, 0, DW_OP_LLVM_bra, 42,
+                           DW_OP_LLVM_skip, 0, DW_OP_LLVM_label, 42),
+             !DILocation(scope: !4))
+  ret void
+}
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1)
+!1 = !DIFile(filename: "test.c", directory: "/")
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!4 = distinct !DISubprogram(name: "f", scope: !1,
+                            type: !DISubroutineType(types: !{null}),
+                            spFlags: DISPFlagDefinition, unit: !0)
diff --git a/mlir/test/Target/LLVMIR/llvmir-debug-symbolic-branches.mlir b/mlir/test/Target/LLVMIR/llvmir-debug-symbolic-branches.mlir
new file mode 100644
index 0000000000000..74a549b952327
--- /dev/null
+++ b/mlir/test/Target/LLVMIR/llvmir-debug-symbolic-branches.mlir
@@ -0,0 +1,19 @@
+// RUN: mlir-translate -mlir-to-llvmir %s | FileCheck %s
+
+// Export a forward branch and a backward skip, and make sure LLVM IR keeps the
+// op order and label IDs.
+
+#file = #llvm.di_file<"test.c" in "/">
+#cu = #llvm.di_compile_unit<id = distinct[0]<>, sourceLanguage = DW_LANG_C,
+                            file = #file>
+#sp = #llvm.di_subprogram<compileUnit = #cu, scope = #file, name = "f",
+                          subprogramFlags = "Definition",
+                          type = #llvm.di_subroutine_type<types = #llvm.di_null_type>>
+
+// CHECK: #dbg_value(i64 %{{.*}}, !{{.*}}, !DIExpression(DW_OP_LLVM_label, 0, DW_OP_LLVM_bra, 42, DW_OP_LLVM_skip, 0, DW_OP_LLVM_label, 42), !{{.*}})
+llvm.func @f(%arg: i64) {
+  llvm.intr.dbg.value #llvm.di_local_variable<scope = #sp> #llvm.di_expression<[
+    DW_OP_LLVM_label(0), DW_OP_LLVM_bra(42), DW_OP_LLVM_skip(0),
+    DW_OP_LLVM_label(42)]> = %arg : i64
+  llvm.return
+} loc(fused<#sp>["test.c":1:1])

>From 2d711d7a0fcc238643f2a6f5d1707ef5caf81fe0 Mon Sep 17 00:00:00 2001
From: Eric Christopher <echristopher at nvidia.com>
Date: Thu, 13 Aug 2026 23:16:56 -0700
Subject: [PATCH 02/19] [LLDB] Handle symbolic DIExpression opcodes

These opcodes only appear in LLVM metadata, but adding them to the shared DWARF enum makes LLDB's switch incomplete. List them with the other metadata-only opcodes so a -Wswitch build stays clean.
---
 lldb/source/Expression/DWARFExpression.cpp | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/lldb/source/Expression/DWARFExpression.cpp b/lldb/source/Expression/DWARFExpression.cpp
index 2dbf61a14eac4..1c0140bb16207 100644
--- a/lldb/source/Expression/DWARFExpression.cpp
+++ b/lldb/source/Expression/DWARFExpression.cpp
@@ -205,6 +205,9 @@ GetOpcodeDataSize(const DataExtractor &data, const lldb::offset_t data_offset,
   case DW_OP_LLVM_arg:
   case DW_OP_LLVM_extract_bits_sext:
   case DW_OP_LLVM_extract_bits_zext:
+  case DW_OP_LLVM_label:
+  case DW_OP_LLVM_bra:
+  case DW_OP_LLVM_skip:
     break;
   // Vendor extensions:
   case DW_OP_HP_is_value:

>From 608c49bea04f38e8b3d2bc7d1cb8cdbd1fe2e5dd Mon Sep 17 00:00:00 2001
From: Eric Christopher <echristopher at nvidia.com>
Date: Thu, 13 Aug 2026 23:16:56 -0700
Subject: [PATCH 03/19] [DebugInfo] Clarify symbolic branch limitations

Separate the DIExpression validity rules from the combinations we don't handle yet. DIArgList and DW_OP_LLVM_arg need more work in expressions and expression writers, while DW_OP_LLVM_implicit_pointer takes a different emission path. Also document the final branch offset range check.
---
 llvm/docs/SourceLevelDebugging.md | 17 ++++++++++++++---
 1 file changed, 14 insertions(+), 3 deletions(-)

diff --git a/llvm/docs/SourceLevelDebugging.md b/llvm/docs/SourceLevelDebugging.md
index eaec125ed2762..8757ce215a005 100644
--- a/llvm/docs/SourceLevelDebugging.md
+++ b/llvm/docs/SourceLevelDebugging.md
@@ -489,15 +489,23 @@ Label IDs are local to an expression:
   offset.
 - Branches can go forward, backward, to themselves, or form cycles.
 
-There are a few other restrictions:
+`DIExpression` validation also checks the following:
 
 - Put labels, branches, and skips before `DW_OP_stack_value` and
   `DW_OP_LLVM_fragment`. Only a fragment can follow `DW_OP_stack_value`.
-- `DIArgList`, `DW_OP_LLVM_arg`, and `DW_OP_LLVM_implicit_pointer` are lowered
-  separately, so they can't be used with symbolic control flow.
 - Put `DW_OP_LLVM_tag_offset` before the first label, branch, or skip so it
   applies to every path.
 
+There are also a couple of cases we don't handle yet:
+
+- `DIArgList` and `DW_OP_LLVM_arg` are currently rejected. CodeGen expands each
+  argument before it resolves the label offsets, so there isn't a representation
+  problem here; it mostly needs work handling it in expressions and expression
+  writers.
+- `DW_OP_LLVM_implicit_pointer` bypasses normal expression emission and only
+  handles a single location today. Supporting branches there is a bit more
+  work, since we'll need to work it back into our normal emission order.
+
 We don't check reachability, termination, or stack state where paths meet.
 
 `DW_OP_LLVM_convert` can appear before or after labels, branches, and skips,
@@ -505,6 +513,9 @@ and we don't match conversions on different paths. When CodeGen can't emit
 `DW_OP_convert`, it may defer one conversion until it sees the next; if a label,
 branch, or skip would split the pair, CodeGen reports an error.
 
+CodeGen also reports an error if the final branch offset is outside
+`[-32768, 32767]`.
+
 Local expression rewrites stop at labels, branches, and skips; they can still
 add operations to either end, but they don't move, remove, or copy labels.
 

>From 9a199325fa0d9770e3242b669f1992564130c4a3 Mon Sep 17 00:00:00 2001
From: Eric Christopher <echristopher at nvidia.com>
Date: Thu, 13 Aug 2026 23:16:56 -0700
Subject: [PATCH 04/19] [DebugInfo] Clean up symbolic DIExpression branch
 handling

Pull the symbolic branch checks into two small helpers, since CodeGen only needs branches while the verifier also needs labels.

Use maskTrailingOnes when storing fixed-width DIE values, and make the placeholder walk use a byte cursor with names that describe what we're patching. We still keep the existing form and size so later fixup offsets don't move.

Also make the displacement calculation and DIArgList verifier check a little clearer. There should be no behavior change here.
---
 llvm/include/llvm/BinaryFormat/Dwarf.h        | 10 +++++
 .../CodeGen/AsmPrinter/DwarfExpression.cpp    |  5 ++-
 llvm/lib/CodeGen/AsmPrinter/DwarfUnit.cpp     | 42 ++++++++++---------
 llvm/lib/IR/Verifier.cpp                      | 16 +++----
 4 files changed, 44 insertions(+), 29 deletions(-)

diff --git a/llvm/include/llvm/BinaryFormat/Dwarf.h b/llvm/include/llvm/BinaryFormat/Dwarf.h
index a6ab11c4afb8e..2132b5b3c320d 100644
--- a/llvm/include/llvm/BinaryFormat/Dwarf.h
+++ b/llvm/include/llvm/BinaryFormat/Dwarf.h
@@ -1185,6 +1185,16 @@ inline bool isTlsAddressOp(uint8_t O) {
   return O == DW_OP_form_tls_address || O == DW_OP_GNU_push_tls_address;
 }
 
+/// Return true if Op is a symbolic branch to a label.
+inline bool isSymbolicBranchOp(uint64_t Op) {
+  return Op == DW_OP_LLVM_bra || Op == DW_OP_LLVM_skip;
+}
+
+/// Return true if Op is a symbolic label or branch.
+inline bool isSymbolicControlFlowOp(uint64_t Op) {
+  return Op == DW_OP_LLVM_label || isSymbolicBranchOp(Op);
+}
+
 LLVM_ABI std::optional<unsigned> LanguageLowerBound(SourceLanguage L);
 
 /// The size of a reference determined by the DWARF 32/64-bit format.
diff --git a/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp b/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp
index 8cd9457421e92..7b9409d5f149d 100644
--- a/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp
+++ b/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp
@@ -571,8 +571,7 @@ bool DwarfExpression::addExpression(
   // Iterating over ExprCursor doesn't consume it.
   bool HasSymbolicBranches =
       llvm::any_of(ExprCursor, [](DIExpression::ExprOperand Op) {
-        return Op.getOp() == dwarf::DW_OP_LLVM_bra ||
-               Op.getOp() == dwarf::DW_OP_LLVM_skip;
+        return dwarf::isSymbolicBranchOp(Op.getOp());
       });
 
   SmallVector<LabelOffset, 4> Labels;
@@ -844,6 +843,8 @@ bool DwarfExpression::addExpression(
         report_fatal_error(Twine("DWARF expression branch to label ") +
                            Twine(Fixup.LabelID) + " has no matching label");
 
+      // DW_OP_bra and DW_OP_skip apply the displacement after reading their
+      // two-byte operand, so use the byte after the placeholder as the base.
       int64_t Displacement =
           static_cast<int64_t>(Label->Offset) -
           static_cast<int64_t>(Fixup.PlaceholderOffset + BranchOffsetByteSize);
diff --git a/llvm/lib/CodeGen/AsmPrinter/DwarfUnit.cpp b/llvm/lib/CodeGen/AsmPrinter/DwarfUnit.cpp
index 6d4eeebf0703a..4085f8628548a 100644
--- a/llvm/lib/CodeGen/AsmPrinter/DwarfUnit.cpp
+++ b/llvm/lib/CodeGen/AsmPrinter/DwarfUnit.cpp
@@ -27,6 +27,7 @@
 #include "llvm/MC/MCSection.h"
 #include "llvm/MC/MCStreamer.h"
 #include "llvm/Support/Casting.h"
+#include "llvm/Support/MathExtras.h"
 #include "llvm/Target/TargetLoweringObjectFile.h"
 #include <cassert>
 #include <cstdint>
@@ -53,9 +54,7 @@ static dwarf::Form getDataForm(unsigned Size) {
 }
 
 static uint64_t getDataValue(uint64_t Value, unsigned Size) {
-  if (Size == sizeof(Value))
-    return Value;
-  return Value & ((uint64_t(1) << (Size * 8)) - 1);
+  return Value & maskTrailingOnes<uint64_t>(Size * 8);
 }
 
 DIEDwarfExpression::DIEDwarfExpression(const AsmPrinter &AP,
@@ -98,26 +97,31 @@ unsigned DIEDwarfExpression::getTemporaryBufferSize() {
 
 void DIEDwarfExpression::commitTemporaryBuffer() { OutDIE.takeValues(TmpDIE); }
 
-void DIEDwarfExpression::replaceTemporaryBufferData(unsigned Offset,
-                                                    uint64_t Value,
-                                                    unsigned Size) {
-  dwarf::Form Form = getDataForm(Size);
-  // Keep the form so replacing the value doesn't move later labels.
-  unsigned CurrentOffset = 0;
+void DIEDwarfExpression::replaceTemporaryBufferData(unsigned PlaceholderOffset,
+                                                    uint64_t Replacement,
+                                                    unsigned PlaceholderSize) {
+  // Walk the encoded values until the cursor reaches the placeholder.
+  unsigned ByteCursor = 0;
   for (DIEValue &V : TmpDIE.values()) {
     unsigned ValueSize = V.sizeOf(AP.getDwarfFormParams());
-    if (Offset < CurrentOffset + ValueSize) {
-      assert(Offset == CurrentOffset && ValueSize == Size &&
-             V.getType() == DIEValue::isInteger && V.getForm() == Form &&
-             V.getDIEInteger().getValue() == 0 &&
-             "symbolic branch fixup does not match its placeholder");
-      V = DIEValue(V.getAttribute(), V.getForm(),
-                   DIEInteger(getDataValue(Value, Size)));
-      return;
+    unsigned ValueEnd = ByteCursor + ValueSize;
+    if (ValueEnd <= PlaceholderOffset) {
+      ByteCursor = ValueEnd;
+      continue;
     }
-    CurrentOffset += ValueSize;
+
+    // Replace the whole zero placeholder and keep its form, since changing its
+    // encoded size would move every later fixup.
+    assert(PlaceholderOffset == ByteCursor && ValueSize == PlaceholderSize &&
+           V.getType() == DIEValue::isInteger &&
+           V.getForm() == getDataForm(PlaceholderSize) &&
+           V.getDIEInteger().getValue() == 0 &&
+           "symbolic branch fixup does not match its placeholder");
+    V = DIEValue(V.getAttribute(), V.getForm(),
+                 DIEInteger(getDataValue(Replacement, PlaceholderSize)));
+    return;
   }
-  llvm_unreachable("invalid temporary DIE offset");
+  llvm_unreachable("temporary DIE placeholder not found");
 }
 
 bool DIEDwarfExpression::isFrameRegister(const TargetRegisterInfo &TRI,
diff --git a/llvm/lib/IR/Verifier.cpp b/llvm/lib/IR/Verifier.cpp
index 2ed1eb92a875f..3c7257d8bd7e8 100644
--- a/llvm/lib/IR/Verifier.cpp
+++ b/llvm/lib/IR/Verifier.cpp
@@ -7217,15 +7217,15 @@ void Verifier::visit(DbgVariableRecord &DVR) {
 
   // A DIArgList can have a valid branch expression which doesn't use
   // DW_OP_LLVM_arg, so check the record as well.
-  if (DVR.hasArgList() && DVR.getExpression()->isValid()) {
-    bool HasControlFlow = llvm::any_of(
-        DVR.getExpression()->expr_ops(), [](DIExpression::ExprOperand Op) {
-          return Op.getOp() == dwarf::DW_OP_LLVM_label ||
-                 Op.getOp() == dwarf::DW_OP_LLVM_bra ||
-                 Op.getOp() == dwarf::DW_OP_LLVM_skip;
+  const DIExpression *Expr = DVR.getExpression();
+  if (DVR.hasArgList() && Expr->isValid()) {
+    bool HasSymbolicControlFlow =
+        llvm::any_of(Expr->expr_ops(), [](DIExpression::ExprOperand Op) {
+          return dwarf::isSymbolicControlFlowOp(Op.getOp());
         });
-    CheckDI(!HasControlFlow, "DIArgList doesn't support symbolic branches",
-            &DVR, MD, DVR.getExpression(), BB, F);
+    CheckDI(!HasSymbolicControlFlow,
+            "DIArgList doesn't support symbolic branches", &DVR, MD, Expr, BB,
+            F);
   }
 
   if (DVR.isDbgAssign()) {

>From 4f9c60f2ddbd7f0b5420996915d008f15b63dbfc Mon Sep 17 00:00:00 2001
From: Eric Christopher <echristopher at nvidia.com>
Date: Thu, 13 Aug 2026 23:16:56 -0700
Subject: [PATCH 05/19] [DebugInfo] Add opcode queries to DIExpression operands

Add is and isOneOf to DIExpression::ExprOperand and use them for the
symbolic branch checks. This keeps the checks on the parsed operation and
lets us remove the branch-specific helpers from Dwarf.h.
---
 llvm/include/llvm/BinaryFormat/Dwarf.h          | 10 ----------
 llvm/include/llvm/IR/DebugInfoMetadata.h        |  9 +++++++++
 llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp |  8 ++++----
 llvm/lib/IR/Verifier.cpp                        |  3 ++-
 4 files changed, 15 insertions(+), 15 deletions(-)

diff --git a/llvm/include/llvm/BinaryFormat/Dwarf.h b/llvm/include/llvm/BinaryFormat/Dwarf.h
index 2132b5b3c320d..a6ab11c4afb8e 100644
--- a/llvm/include/llvm/BinaryFormat/Dwarf.h
+++ b/llvm/include/llvm/BinaryFormat/Dwarf.h
@@ -1185,16 +1185,6 @@ inline bool isTlsAddressOp(uint8_t O) {
   return O == DW_OP_form_tls_address || O == DW_OP_GNU_push_tls_address;
 }
 
-/// Return true if Op is a symbolic branch to a label.
-inline bool isSymbolicBranchOp(uint64_t Op) {
-  return Op == DW_OP_LLVM_bra || Op == DW_OP_LLVM_skip;
-}
-
-/// Return true if Op is a symbolic label or branch.
-inline bool isSymbolicControlFlowOp(uint64_t Op) {
-  return Op == DW_OP_LLVM_label || isSymbolicBranchOp(Op);
-}
-
 LLVM_ABI std::optional<unsigned> LanguageLowerBound(SourceLanguage L);
 
 /// The size of a reference determined by the DWARF 32/64-bit format.
diff --git a/llvm/include/llvm/IR/DebugInfoMetadata.h b/llvm/include/llvm/IR/DebugInfoMetadata.h
index dd11edf7f6935..182d64a895055 100644
--- a/llvm/include/llvm/IR/DebugInfoMetadata.h
+++ b/llvm/include/llvm/IR/DebugInfoMetadata.h
@@ -3550,6 +3550,15 @@ class DIExpression : public MDNode {
     /// Return true if this is \p Opcode.
     bool is(uint64_t Opcode) const { return getOp() == Opcode; }
 
+    /// Return true if this is \p Opcode.
+    bool is(uint64_t Opcode) const { return getOp() == Opcode; }
+
+    /// Return true if this is one of \p Opcodes.
+    template <typename... Ts> bool isOneOf(Ts... Opcodes) const {
+      static_assert(sizeof...(Ts) > 0, "requires at least one opcode");
+      return (is(Opcodes) || ...);
+    }
+
     /// Get an argument to the operand.
     ///
     /// Never returns the operand itself. The operand has to be present and \p I
diff --git a/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp b/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp
index 7b9409d5f149d..c3d97f1b1542a 100644
--- a/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp
+++ b/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp
@@ -571,7 +571,7 @@ bool DwarfExpression::addExpression(
   // Iterating over ExprCursor doesn't consume it.
   bool HasSymbolicBranches =
       llvm::any_of(ExprCursor, [](DIExpression::ExprOperand Op) {
-        return dwarf::isSymbolicBranchOp(Op.getOp());
+        return Op.isOneOf(dwarf::DW_OP_LLVM_bra, dwarf::DW_OP_LLVM_skip);
       });
 
   SmallVector<LabelOffset, 4> Labels;
@@ -602,12 +602,12 @@ bool DwarfExpression::addExpression(
         report_fatal_error(Twine("cannot lower DW_OP_LLVM_convert across ") +
                            dwarf::OperationEncodingString(OpNum) +
                            " without DW_OP_convert support");
-      if (OpNum == dwarf::DW_OP_LLVM_label) {
+      if (Op->is(dwarf::DW_OP_LLVM_label)) {
         Labels.push_back({Op->getArg(0), getTemporaryBufferSize()});
         break;
       }
-      emitOp(OpNum == dwarf::DW_OP_LLVM_bra ? dwarf::DW_OP_bra
-                                            : dwarf::DW_OP_skip);
+      emitOp(Op->is(dwarf::DW_OP_LLVM_bra) ? dwarf::DW_OP_bra
+                                           : dwarf::DW_OP_skip);
       Fixups.push_back({Op->getArg(0), getTemporaryBufferSize()});
       emitData2(0);
       break;
diff --git a/llvm/lib/IR/Verifier.cpp b/llvm/lib/IR/Verifier.cpp
index 3c7257d8bd7e8..56c49aa580e8b 100644
--- a/llvm/lib/IR/Verifier.cpp
+++ b/llvm/lib/IR/Verifier.cpp
@@ -7221,7 +7221,8 @@ void Verifier::visit(DbgVariableRecord &DVR) {
   if (DVR.hasArgList() && Expr->isValid()) {
     bool HasSymbolicControlFlow =
         llvm::any_of(Expr->expr_ops(), [](DIExpression::ExprOperand Op) {
-          return dwarf::isSymbolicControlFlowOp(Op.getOp());
+          return Op.isOneOf(dwarf::DW_OP_LLVM_label, dwarf::DW_OP_LLVM_bra,
+                            dwarf::DW_OP_LLVM_skip);
         });
     CheckDI(!HasSymbolicControlFlow,
             "DIArgList doesn't support symbolic branches", &DVR, MD, Expr, BB,

>From b8d42d6907a9b463a9a0a2943b5cbc386c206762 Mon Sep 17 00:00:00 2001
From: Eric Christopher <echristopher at nvidia.com>
Date: Thu, 13 Aug 2026 23:16:57 -0700
Subject: [PATCH 06/19] [DebugInfo] Add an early return to
 DwarfExpression::addExpression

Add an early return to streamline DwarfExpression::addExpression's handling
of symbolic branches.
---
 .../CodeGen/AsmPrinter/DwarfExpression.cpp    | 47 ++++++++++---------
 1 file changed, 24 insertions(+), 23 deletions(-)

diff --git a/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp b/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp
index c3d97f1b1542a..3e1ecf2fabee3 100644
--- a/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp
+++ b/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp
@@ -834,31 +834,32 @@ bool DwarfExpression::addExpression(
     // Turn this into an implicit location description.
     addStackValue();
 
-  if (HasSymbolicBranches) {
-    for (const BranchFixup &Fixup : Fixups) {
-      auto Label = llvm::find_if(Labels, [&](const LabelOffset &Candidate) {
-        return Candidate.ID == Fixup.LabelID;
-      });
-      if (Label == Labels.end())
-        report_fatal_error(Twine("DWARF expression branch to label ") +
-                           Twine(Fixup.LabelID) + " has no matching label");
-
-      // DW_OP_bra and DW_OP_skip apply the displacement after reading their
-      // two-byte operand, so use the byte after the placeholder as the base.
-      int64_t Displacement =
-          static_cast<int64_t>(Label->Offset) -
-          static_cast<int64_t>(Fixup.PlaceholderOffset + BranchOffsetByteSize);
-      if (!isInt<16>(Displacement))
-        report_fatal_error(Twine("DWARF expression branch offset ") +
-                           Twine(Displacement) + " is outside [-32768, 32767]");
-
-      replaceTemporaryBufferData2(Fixup.PlaceholderOffset,
-                                  static_cast<uint16_t>(Displacement));
-    }
+  if (!HasSymbolicBranches)
+    return true;
 
-    disableTemporaryBuffer();
-    commitTemporaryBuffer();
+  for (const BranchFixup &Fixup : Fixups) {
+    auto Label = llvm::find_if(Labels, [&](const LabelOffset &Candidate) {
+      return Candidate.ID == Fixup.LabelID;
+    });
+    if (Label == Labels.end())
+      report_fatal_error(Twine("DWARF expression branch to label ") +
+                         Twine(Fixup.LabelID) + " has no matching label");
+
+    // DW_OP_bra and DW_OP_skip apply the displacement after reading their
+    // two-byte operand, so use the byte after the placeholder as the base.
+    int64_t Displacement =
+        static_cast<int64_t>(Label->Offset) -
+        static_cast<int64_t>(Fixup.PlaceholderOffset + BranchOffsetByteSize);
+    if (!isInt<16>(Displacement))
+      report_fatal_error(Twine("DWARF expression branch offset ") +
+                         Twine(Displacement) + " is outside [-32768, 32767]");
+
+    replaceTemporaryBufferData2(Fixup.PlaceholderOffset,
+                                static_cast<uint16_t>(Displacement));
   }
+
+  disableTemporaryBuffer();
+  commitTemporaryBuffer();
   return true;
 }
 

>From ba7d23718480b621cd172ad54a71a428ef5b05bd Mon Sep 17 00:00:00 2001
From: Eric Christopher <echristopher at nvidia.com>
Date: Thu, 13 Aug 2026 23:16:57 -0700
Subject: [PATCH 07/19] Update LangRef.md around DWARF branch ops

---
 llvm/docs/LangRef.md | 20 +++++++++++++-------
 1 file changed, 13 insertions(+), 7 deletions(-)

diff --git a/llvm/docs/LangRef.md b/llvm/docs/LangRef.md
index 3f7100a2c4d1c..9a8b3eac986ef 100644
--- a/llvm/docs/LangRef.md
+++ b/llvm/docs/LangRef.md
@@ -7223,13 +7223,19 @@ DW_OP_LLVM_bra,   <label-id>
 DW_OP_LLVM_skip,  <label-id>
 ```
 
-`DW_OP_LLVM_label` declares a label ID and emits no bytes. `DW_OP_LLVM_bra`
-branches when the top of the stack is non-zero; `DW_OP_LLVM_skip` always
-branches. Label IDs are local to the expression. Every branch needs a matching
-label, labels without branches are valid, and each ID can only be declared
-once. Raw `DW_OP_bra` and `DW_OP_skip` aren't valid in LLVM IR.
-
-See {ref}`symbolic control flow <symbolic-control-flow>` for the full rules.
+`DW_OP_LLVM_bra` and `DW_OP_LLVM_skip` have the same semantics as the standard
+DWARF `DW_OP_bra` and `DW_OP_skip` operations. The LLVM forms use a label ID
+because the byte offset isn't known until emission. CodeGen resolves each
+branch's label ID and replaces the LLVM form with the corresponding standard
+operation and byte offset. `DW_OP_LLVM_label` declares the target for a label
+ID and emits no bytes.
+
+Label IDs are local to the expression. Every branch needs a matching label,
+labels without branches are valid, and each ID can only be declared once. The
+standard `DW_OP_bra` and `DW_OP_skip` operations aren't valid in LLVM IR. See
+[the DWARF standard](https://dwarfstd.org/) for the standard operations and
+{ref}`symbolic control flow <symbolic-control-flow>` for the LLVM-specific
+rules.
 
 ##### DIAssignID
 

>From 9914c8ab25908f4b83bcc3271f193806cd50d281 Mon Sep 17 00:00:00 2001
From: Eric Christopher <echristopher at nvidia.com>
Date: Thu, 13 Aug 2026 23:16:57 -0700
Subject: [PATCH 08/19] [DebugInfo] Skip non-emitting ops in register
 expressions

Labels and tag offsets don't add anything to the DWARF expression, so look
past them when deciding whether a register expression is complex.

This means:
- label-only expressions stay as register locations,
- labels before fragments stay on the simple register path, and
- tag-only expressions stay as register locations.
---
 llvm/lib/IR/DebugInfoMetadata.cpp             |  2 +-
 .../di-expression-symbolic-branch-register.ll | 46 +++++++++++++++++++
 llvm/unittests/IR/MetadataTest.cpp            | 17 +++++--
 3 files changed, 59 insertions(+), 6 deletions(-)
 create mode 100644 llvm/test/DebugInfo/X86/di-expression-symbolic-branch-register.ll

diff --git a/llvm/lib/IR/DebugInfoMetadata.cpp b/llvm/lib/IR/DebugInfoMetadata.cpp
index 695b59fe809e7..8d28b090ed45a 100644
--- a/llvm/lib/IR/DebugInfoMetadata.cpp
+++ b/llvm/lib/IR/DebugInfoMetadata.cpp
@@ -1762,7 +1762,7 @@ unsigned DIExpression::ExprOperand::getSize() const {
 }
 
 bool DIExpression::ExprOperand::isNonEmitting() const {
-  return getOp() == dwarf::DW_OP_LLVM_tag_offset;
+  return isOneOf(dwarf::DW_OP_LLVM_tag_offset, dwarf::DW_OP_LLVM_label);
 }
 
 bool DIExpression::ArgOp::classof(const ExprOperand *Op) {
diff --git a/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-register.ll b/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-register.ll
new file mode 100644
index 0000000000000..c347993922a66
--- /dev/null
+++ b/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-register.ll
@@ -0,0 +1,46 @@
+; RUN: llc -mtriple=x86_64-unknown-linux-gnu -filetype=obj -o - %s | llvm-dwarfdump - | FileCheck %s
+
+; Labels emit no expression bytes, so they do not turn a simple register
+; location into a memory location or interfere with fragment lookahead.
+
+declare void @clobber()
+
+define void @registers(i64 %x, i32 %simple_subreg, i32 %complex_subreg) !dbg !5 {
+entry:
+  #dbg_value(i64 %x, !9, !DIExpression(DW_OP_LLVM_label, 1), !12)
+  #dbg_value(i32 %simple_subreg, !15,
+             !DIExpression(DW_OP_LLVM_label, 3,
+                           DW_OP_LLVM_fragment, 0, 32), !12)
+  #dbg_value(i32 %complex_subreg, !16,
+             !DIExpression(DW_OP_LLVM_label, 4,
+                           DW_OP_plus_uconst, 1), !12)
+  call void @clobber(), !dbg !12
+  ret void, !dbg !12
+}
+
+; CHECK: DW_OP_reg5 RDI)
+; CHECK: DW_AT_name {{.*}}"label_only"
+; CHECK: DW_OP_reg4 RSI, DW_OP_piece 0x4)
+; CHECK: DW_AT_name {{.*}}"simple_subreg"
+; CHECK: DW_OP_breg1 RDX+0, DW_OP_constu 0xffffffff, DW_OP_and, DW_OP_plus_uconst 0x1)
+; CHECK: DW_AT_name {{.*}}"complex_subreg"
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1,
+                             emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.c", directory: "/")
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!5 = distinct !DISubprogram(name: "registers", scope: !1, file: !1,
+                            type: !6, spFlags: DISPFlagDefinition, unit: !0,
+                            retainedNodes: !8)
+!6 = !DISubroutineType(types: !7)
+!7 = !{null}
+!8 = !{!9, !15, !16}
+!9 = !DILocalVariable(name: "label_only", scope: !5, type: !11)
+!11 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
+!12 = !DILocation(line: 1, scope: !5)
+!15 = !DILocalVariable(name: "simple_subreg", scope: !5, type: !11)
+!16 = !DILocalVariable(name: "complex_subreg", scope: !5, type: !17)
+!17 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
diff --git a/llvm/unittests/IR/MetadataTest.cpp b/llvm/unittests/IR/MetadataTest.cpp
index ce09eb6d40bbe..26b21776abc9d 100644
--- a/llvm/unittests/IR/MetadataTest.cpp
+++ b/llvm/unittests/IR/MetadataTest.cpp
@@ -4483,15 +4483,22 @@ TEST_F(DIExpressionTest, SymbolicBranchRewrites) {
   EXPECT_EQ(DIExpression::get(Context, Expected), Appended);
   EXPECT_TRUE(Appended->isValid());
 
-  // Label IDs are arbitrary uint64_t values, so appendExt must not mistake one
-  // for DW_OP_stack_value.
+  // A label emits no bytes, so a label-only expression doesn't need a deref
+  // before the appended ops. The ID collides with DW_OP_stack_value on purpose:
+  // label IDs are arbitrary, so appendExt has to read it as an argument rather
+  // than as an opcode.
   uint64_t LabelID = dwarf::DW_OP_stack_value;
   Ops = {dwarf::DW_OP_LLVM_label, LabelID};
   Appended =
       DIExpression::appendExt(DIExpression::get(Context, Ops), 32, 64, true);
-  Expected = {dwarf::DW_OP_LLVM_label,   LabelID, dwarf::DW_OP_deref,
-              dwarf::DW_OP_LLVM_convert, 32,      dwarf::DW_ATE_signed,
-              dwarf::DW_OP_LLVM_convert, 64,      dwarf::DW_ATE_signed,
+  Expected = {dwarf::DW_OP_LLVM_label,
+              LabelID,
+              dwarf::DW_OP_LLVM_convert,
+              32,
+              dwarf::DW_ATE_signed,
+              dwarf::DW_OP_LLVM_convert,
+              64,
+              dwarf::DW_ATE_signed,
               dwarf::DW_OP_stack_value};
   EXPECT_EQ(DIExpression::get(Context, Expected), Appended);
   EXPECT_TRUE(Appended->isValid());

>From 438f82843ed79490e6f0d07665d966d0053efb61 Mon Sep 17 00:00:00 2001
From: Eric Christopher <echristopher at nvidia.com>
Date: Thu, 13 Aug 2026 23:16:57 -0700
Subject: [PATCH 09/19] [DebugInfo] Add a symbolic control-flow query to
 DIExpression operands

Verifier.cpp spelled the symbolic control-flow opcode set out inline when it
checks that a DIArgList-owned record doesn't carry branches, and the patches
after this one need the same set in two more places. Give it a name and put
it next to isNonEmitting, which is the query it belongs with.

isSymbolicControlFlow covers DW_OP_LLVM_label, DW_OP_LLVM_bra and
DW_OP_LLVM_skip. That's exactly the list Verifier::visit(DbgVariableRecord &)
was testing for through isOneOf, so the "DIArgList doesn't support symbolic
branches" check rejects the same expressions it did before.

It's deliberately not the same set as isNonEmitting, and the two shouldn't be
merged. isNonEmitting asks whether CodeGen adds bytes for an operand, so it
covers label and tag_offset; bra and skip both emit a real DWARF branch, and
tag_offset isn't control flow at all. They overlap on label because a label
is both control flow and zero-width.

NFC.
---
 llvm/include/llvm/IR/DebugInfoMetadata.h | 3 +++
 llvm/lib/IR/DebugInfoMetadata.cpp        | 5 +++++
 llvm/lib/IR/Verifier.cpp                 | 3 +--
 3 files changed, 9 insertions(+), 2 deletions(-)

diff --git a/llvm/include/llvm/IR/DebugInfoMetadata.h b/llvm/include/llvm/IR/DebugInfoMetadata.h
index 182d64a895055..57b02a44f3af8 100644
--- a/llvm/include/llvm/IR/DebugInfoMetadata.h
+++ b/llvm/include/llvm/IR/DebugInfoMetadata.h
@@ -3579,6 +3579,9 @@ class DIExpression : public MDNode {
     /// DWARF expression.
     LLVM_ABI bool isNonEmitting() const;
 
+    /// Return true if this is a symbolic control-flow operation.
+    LLVM_ABI bool isSymbolicControlFlow() const;
+
     /// Append the elements of this operand to \p V.
     void appendToVector(SmallVectorImpl<uint64_t> &V) const {
       V.append(get(), get() + getSize());
diff --git a/llvm/lib/IR/DebugInfoMetadata.cpp b/llvm/lib/IR/DebugInfoMetadata.cpp
index 8d28b090ed45a..59e2c51d9e1c1 100644
--- a/llvm/lib/IR/DebugInfoMetadata.cpp
+++ b/llvm/lib/IR/DebugInfoMetadata.cpp
@@ -1765,6 +1765,11 @@ bool DIExpression::ExprOperand::isNonEmitting() const {
   return isOneOf(dwarf::DW_OP_LLVM_tag_offset, dwarf::DW_OP_LLVM_label);
 }
 
+bool DIExpression::ExprOperand::isSymbolicControlFlow() const {
+  return isOneOf(dwarf::DW_OP_LLVM_label, dwarf::DW_OP_LLVM_bra,
+                 dwarf::DW_OP_LLVM_skip);
+}
+
 bool DIExpression::ArgOp::classof(const ExprOperand *Op) {
   return Op->is(dwarf::DW_OP_LLVM_arg);
 }
diff --git a/llvm/lib/IR/Verifier.cpp b/llvm/lib/IR/Verifier.cpp
index 56c49aa580e8b..eb9eae1f33f72 100644
--- a/llvm/lib/IR/Verifier.cpp
+++ b/llvm/lib/IR/Verifier.cpp
@@ -7221,8 +7221,7 @@ void Verifier::visit(DbgVariableRecord &DVR) {
   if (DVR.hasArgList() && Expr->isValid()) {
     bool HasSymbolicControlFlow =
         llvm::any_of(Expr->expr_ops(), [](DIExpression::ExprOperand Op) {
-          return Op.isOneOf(dwarf::DW_OP_LLVM_label, dwarf::DW_OP_LLVM_bra,
-                            dwarf::DW_OP_LLVM_skip);
+          return Op.isSymbolicControlFlow();
         });
     CheckDI(!HasSymbolicControlFlow,
             "DIArgList doesn't support symbolic branches", &DVR, MD, Expr, BB,

>From 0e0602ec7abbffedcb132e0917bbe4a5574d125c Mon Sep 17 00:00:00 2001
From: Eric Christopher <echristopher at nvidia.com>
Date: Thu, 13 Aug 2026 23:16:57 -0700
Subject: [PATCH 10/19] [DebugInfo] Check salvaged expressions before folding

Salvaging a multi-value operation can add DW_OP_LLVM_arg to an
expression with symbolic control flow. Check that the expression is valid
before folding it and use a kill location when it isn't.
---
 llvm/lib/Transforms/Utils/Local.cpp           | 10 +++++++---
 llvm/test/DebugInfo/salvage-nonconst-binop.ll | 15 ++++++++++++---
 2 files changed, 19 insertions(+), 6 deletions(-)

diff --git a/llvm/lib/Transforms/Utils/Local.cpp b/llvm/lib/Transforms/Utils/Local.cpp
index 7a73cabb4c762..217af3ed16073 100644
--- a/llvm/lib/Transforms/Utils/Local.cpp
+++ b/llvm/lib/Transforms/Utils/Local.cpp
@@ -2125,10 +2125,14 @@ void llvm::salvageDebugInfoForDbgValues(Instruction &I,
     if (!Op0)
       break;
 
-    SalvagedExpr = SalvagedExpr->foldConstantMath();
+    // Salvaging a multi-value operation can add DW_OP_LLVM_arg to an expression
+    // with symbolic control flow, so check that it is valid before folding it.
+    bool IsValidSalvageExpr = SalvagedExpr->isValid();
+    if (IsValidSalvageExpr) {
+      SalvagedExpr = SalvagedExpr->foldConstantMath();
+      IsValidSalvageExpr = SalvagedExpr->getNumElements() <= MaxExpressionSize;
+    }
     DVR->replaceVariableLocationOp(&I, Op0);
-    bool IsValidSalvageExpr =
-        SalvagedExpr->getNumElements() <= MaxExpressionSize;
     if (AdditionalValues.empty() && IsValidSalvageExpr) {
       DVR->setExpression(SalvagedExpr);
     } else if (DVR->getType() != DbgVariableRecord::LocationType::Declare &&
diff --git a/llvm/test/DebugInfo/salvage-nonconst-binop.ll b/llvm/test/DebugInfo/salvage-nonconst-binop.ll
index f6e5c255b589f..8c996aa45c56b 100644
--- a/llvm/test/DebugInfo/salvage-nonconst-binop.ll
+++ b/llvm/test/DebugInfo/salvage-nonconst-binop.ll
@@ -1,13 +1,17 @@
-; RUN: opt %s -passes=dce -S | FileCheck %s
+; RUN: opt %s -passes='dce,verify' -S | FileCheck %s
 
-; Tests the salvaging of binary operators that use more than one non-constant
-; SSA value.
+; Salvage the ordinary ADD into a DIArgList. Adding location arguments to the
+; symbolic expression would make it invalid, so keep its expression on a kill
+; location instead.
 
 ; CHECK: #dbg_value(!DIArgList(i32 %a, i32 %b),
 ; CHECK-SAME: ![[VAR_C:[0-9]+]],
 ; CHECK-SAME: !DIExpression(DW_OP_LLVM_arg, 0, DW_OP_LLVM_arg, 1, DW_OP_plus, DW_OP_stack_value),
+; CHECK: #dbg_value(i32 poison, ![[VAR_CF:[0-9]+]],
+; CHECK-SAME: !DIExpression(DW_OP_LLVM_bra, 1, DW_OP_LLVM_label, 1),
 
 ; CHECK: ![[VAR_C]] = !DILocalVariable(name: "c"
+; CHECK: ![[VAR_CF]] = !DILocalVariable(name: "cf"
 
 define i32 @"?multiply@@YAHHH at Z"(i32 %a, i32 %b) !dbg !8 {
 entry:
@@ -15,6 +19,10 @@ entry:
   call void @llvm.dbg.value(metadata i32 %a, metadata !14, metadata !DIExpression()), !dbg !13
   %add = add nsw i32 %a, %b, !dbg !15
   call void @llvm.dbg.value(metadata i32 %add, metadata !16, metadata !DIExpression()), !dbg !13
+  %add.cf = add nsw i32 %a, %b, !dbg !15
+  call void @llvm.dbg.value(
+      metadata i32 %add.cf, metadata !18,
+      metadata !DIExpression(DW_OP_LLVM_bra, 1, DW_OP_LLVM_label, 1)), !dbg !13
   %mul = mul nsw i32 %a, %b, !dbg !17
   ret i32 %mul, !dbg !17
 }
@@ -43,3 +51,4 @@ declare void @llvm.dbg.value(metadata, metadata, metadata)
 !15 = !DILocation(line: 2, scope: !8)
 !16 = !DILocalVariable(name: "c", scope: !8, file: !1, line: 2, type: !11)
 !17 = !DILocation(line: 3, scope: !8)
+!18 = !DILocalVariable(name: "cf", scope: !8, file: !1, line: 2, type: !11)

>From ff4e7d6becc5820aa12072974cf9898196a2731e Mon Sep 17 00:00:00 2001
From: Eric Christopher <echristopher at nvidia.com>
Date: Thu, 13 Aug 2026 23:16:57 -0700
Subject: [PATCH 11/19] [DebugInfo] Don't salvage a nonconstant ADD into a
 symbolic-branch expression

salvageDebugInfo's ISD::ADD case has two shapes. With a constant RHS it folds
the offset into the existing expression and the location operand count doesn't
change. With a nonconstant RHS it has to go variadic: it calls
convertToVariadicExpression, appends DW_OP_LLVM_arg <n> and DW_OP_plus, and
pushes a second operand onto NewLocOps (SelectionDAG.cpp:13000-13013).

An expression carrying symbolic control flow can only reference one location,
and only through a leading DW_OP_LLVM_arg 0. The DW_OP_LLVM_arg 1 the
nonconstant path adds makes isValid() false, and nothing between here and the
machine instruction re-checks, so llc aborted in BuildMI:

  Assertion failed: (cast<DIExpression>(Expr)->isValid() && "not an
  expression"), function BuildMI, file MachineInstr.cpp, line 2398

Bail out the way the indirect case just above already does. Skipping the
DbgValue leaves it on the node being replaced, so the location drops when that
node goes away. Dropping a location is what we already do when salvage isn't
possible, and it beats the alternative here, which is a crash.

The check sits next to the isIndirect() bail-out on purpose. Both say "this
salvage would need a variadic expression and we can't have one", so keeping
them together means whoever adds the next restriction finds both.

di-expression-symbolic-branch-sdag-salvage.ll sets up the fold, a nonconstant
ADD feeding a load address, and checks that no DBG_VALUE_LIST or DBG_INSTR_REF
survives. Removing just this guard aborts that test on the assertion above, so
it pins the crash rather than only the output.
---
 .../lib/CodeGen/SelectionDAG/SelectionDAG.cpp | 10 +++++
 ...expression-symbolic-branch-sdag-salvage.ll | 37 +++++++++++++++++++
 2 files changed, 47 insertions(+)
 create mode 100644 llvm/test/DebugInfo/X86/di-expression-symbolic-branch-sdag-salvage.ll

diff --git a/llvm/lib/CodeGen/SelectionDAG/SelectionDAG.cpp b/llvm/lib/CodeGen/SelectionDAG/SelectionDAG.cpp
index 6a279ef7b3f6a..27ae091449eb1 100644
--- a/llvm/lib/CodeGen/SelectionDAG/SelectionDAG.cpp
+++ b/llvm/lib/CodeGen/SelectionDAG/SelectionDAG.cpp
@@ -13121,6 +13121,16 @@ void SelectionDAG::salvageDebugInfo(SDNode &N) {
         // don't salvage those.
         if (!RHSConstant && DV->isIndirect())
           continue;
+        // Salvaging a nonconstant ADD adds a second location operand, and an
+        // expression with symbolic control flow can only reference the first.
+        // Leave the debug value on the old node so deleting it drops the
+        // location.
+        if (!RHSConstant &&
+            llvm::any_of(DV->getExpression()->expr_ops(),
+                         [](DIExpression::ExprOperand Op) {
+                           return Op.isSymbolicControlFlow();
+                         }))
+          continue;
 
         // Rewrite an ADD constant node into a DIExpression. Since we are
         // performing arithmetic to compute the variable's *value* in the
diff --git a/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-sdag-salvage.ll b/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-sdag-salvage.ll
new file mode 100644
index 0000000000000..4f3e955ad8fc3
--- /dev/null
+++ b/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-sdag-salvage.ll
@@ -0,0 +1,37 @@
+; RUN: llc -mtriple=x86_64-unknown-linux-gnu -stop-before=finalize-isel -o - %s | FileCheck %s
+
+; SelectionDAG folds the nonconstant ADD into the load address. Salvaging the
+; debug value would need a second location argument, which an expression with
+; symbolic control flow can't reference, so drop the location instead.
+
+define i8 @sdag_salvage(i64 %base, i64 %index) !dbg !5 {
+entry:
+  %sum = add i64 %base, %index
+  #dbg_value(i64 %sum, !9,
+             !DIExpression(DW_OP_LLVM_bra, 1, DW_OP_LLVM_label, 1), !12)
+  %address = inttoptr i64 %sum to ptr
+  %value = load i8, ptr %address
+  ret i8 %value
+}
+
+; CHECK-LABEL: name: sdag_salvage
+; CHECK-NOT: DBG_INSTR_REF
+; CHECK-NOT: DBG_VALUE_LIST
+; CHECK: MOV8rm
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1,
+                             emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.c", directory: "/")
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!5 = distinct !DISubprogram(name: "sdag_salvage", scope: !1, file: !1,
+                            type: !6, spFlags: DISPFlagDefinition, unit: !0,
+                            retainedNodes: !8)
+!6 = !DISubroutineType(types: !7)
+!7 = !{null}
+!8 = !{!9}
+!9 = !DILocalVariable(name: "sum", scope: !5, type: !11)
+!11 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
+!12 = !DILocation(line: 1, scope: !5)

>From 9eec3bdf7011630a153a69b5a906dc8415b9407e Mon Sep 17 00:00:00 2001
From: Eric Christopher <echristopher at nvidia.com>
Date: Thu, 13 Aug 2026 23:16:57 -0700
Subject: [PATCH 12/19] [DebugInfo] Test that labels don't make an expression
 complex

The merge brought upstream's appendToStack, which looks past non-emitting ops,
together with this branch's isNonEmitting, which covers DW_OP_LLVM_label. That
combination is what decides a label-only expression stays a register location,
and nothing tested it directly.

Add SymbolicLabelsAreNotComplex for the three cases isComplex has to get right:
a label alone, a label before a fragment, and a label before a real operation.
Add the appendExt case where an operation precedes the label, so the label
isn't hiding the operation that decides whether a deref is needed.

Both fail if isNonEmitting goes back to tag offsets alone. Also fix the
isComplex comment, which still said tag offsets were the only non-emitting op.
---
 llvm/lib/IR/DebugInfoMetadata.cpp  |  4 ++--
 llvm/unittests/IR/MetadataTest.cpp | 36 ++++++++++++++++++++++++++++++
 2 files changed, 38 insertions(+), 2 deletions(-)

diff --git a/llvm/lib/IR/DebugInfoMetadata.cpp b/llvm/lib/IR/DebugInfoMetadata.cpp
index 59e2c51d9e1c1..460eaa80d91a9 100644
--- a/llvm/lib/IR/DebugInfoMetadata.cpp
+++ b/llvm/lib/IR/DebugInfoMetadata.cpp
@@ -1998,8 +1998,8 @@ bool DIExpression::isComplex() const {
   if (getNumElements() == 0)
     return false;
 
-  // Tag offsets are non-emitting. They, fragments, and location operands don't
-  // perform a computation by themselves.
+  // Labels and tag offsets are non-emitting. They, fragments, and location
+  // operands don't perform a computation by themselves.
   for (const auto &It : expr_ops()) {
     if (It.isNonEmitting())
       continue;
diff --git a/llvm/unittests/IR/MetadataTest.cpp b/llvm/unittests/IR/MetadataTest.cpp
index 26b21776abc9d..c130ddabb2eed 100644
--- a/llvm/unittests/IR/MetadataTest.cpp
+++ b/llvm/unittests/IR/MetadataTest.cpp
@@ -4502,6 +4502,42 @@ TEST_F(DIExpressionTest, SymbolicBranchRewrites) {
               dwarf::DW_OP_stack_value};
   EXPECT_EQ(DIExpression::get(Context, Expected), Appended);
   EXPECT_TRUE(Appended->isValid());
+
+  // A real operation before the label still needs its deref, so the label
+  // isn't hiding the operation that decides.
+  Ops = {dwarf::DW_OP_plus_uconst, 1, dwarf::DW_OP_LLVM_label, LabelID};
+  Appended =
+      DIExpression::appendExt(DIExpression::get(Context, Ops), 32, 64, true);
+  Expected = {dwarf::DW_OP_plus_uconst,
+              1,
+              dwarf::DW_OP_LLVM_label,
+              LabelID,
+              dwarf::DW_OP_deref,
+              dwarf::DW_OP_LLVM_convert,
+              32,
+              dwarf::DW_ATE_signed,
+              dwarf::DW_OP_LLVM_convert,
+              64,
+              dwarf::DW_ATE_signed,
+              dwarf::DW_OP_stack_value};
+  EXPECT_EQ(DIExpression::get(Context, Expected), Appended);
+  EXPECT_TRUE(Appended->isValid());
+}
+
+TEST_F(DIExpressionTest, SymbolicLabelsAreNotComplex) {
+  auto *Label =
+      DIExpression::get(Context, {dwarf::DW_OP_LLVM_label, uint64_t{1}});
+  EXPECT_FALSE(Label->isComplex());
+
+  auto *LabelAndFragment = DIExpression::get(
+      Context, {dwarf::DW_OP_LLVM_label, uint64_t{1},
+                dwarf::DW_OP_LLVM_fragment, uint64_t{0}, uint64_t{32}});
+  EXPECT_FALSE(LabelAndFragment->isComplex());
+
+  auto *LabelAndOperation =
+      DIExpression::get(Context, {dwarf::DW_OP_LLVM_label, uint64_t{1},
+                                  dwarf::DW_OP_plus_uconst, uint64_t{1}});
+  EXPECT_TRUE(LabelAndOperation->isComplex());
 }
 
 TEST_F(DIExpressionTest, isValid) {

>From c2ba44f7ba0285b0003141fd13a741ba7a7331fb Mon Sep 17 00:00:00 2001
From: Eric Christopher <echristopher at nvidia.com>
Date: Thu, 13 Aug 2026 23:16:58 -0700
Subject: [PATCH 13/19] [DebugInfo] Accept a leading DW_OP_LLVM_arg 0 with
 symbolic control flow

isValid() rejected DW_OP_LLVM_arg outright in an expression carrying symbolic
control flow, on the grounds that location arguments lower through a path that
can't resolve branches. That's right for a real DIArgList operand, but it also
caught an expression CodeGen builds for itself, and CodeGen then asserted on
it.

InstrEmitter turns a non-variadic SDDbgValue into a variadic one before
emitting DBG_INSTR_REF or DBG_VALUE_LIST, and convertToVariadicExpression does
that by prepending DW_OP_LLVM_arg 0 to whatever it's handed. It doesn't consult
isValid() and has no way to decline. So under
-experimental-debug-variable-locations a symbolic-branch expression that
verified fine in IR came back out of instruction referencing in a shape
isValid() called invalid, and EmitDbgInstrRef walked it straight into BuildMI's
"not an expression" assertion.

Allow exactly that shape and nothing more: DW_OP_LLVM_arg is fine when it's the
first operation and its argument is 0, and still conflicts anywhere else.
HasControlFlowConflict is only consulted when HasControlFlow is set, so
expressions without branches are unaffected either way. A record that really
carries a DIArgList is still rejected, since the verifier checks for symbolic
control flow on any expression attached to one, so what this loosens is only
the shape CodeGen builds for itself. It's arg 0 and not arg 1 because
convertToNonVariadicExpression erases a leading arg 0 again before emission and
the emitter sees the expression it always saw, while a leading arg 1 fails that
conversion, stays variadic, and reaches the path that writes each argument's
location description into the same buffer the branch offsets are measured
against.

isValid() already had this idiom. IsEntryValueValid skips a leading
DW_OP_LLVM_arg 0 before checking where the entry value sits, for the same
reason: CodeGen put it there after the IR was verified.

di-expression-symbolic-branch-arg0.ll runs the instruction-referencing path end
to end and checks both the MIR expression and the emitted DWARF, on release
builds as well as asserts ones. Reverting the isValid() hunk aborts in BuildMI
with assertions on; with them off AsmWriter prints the expression as raw
integers instead of opcode names, so the MIR check can't match either way. In
the verifier test the case moves out of the rejected list into valid.ll, and
MetadataTest pins the three shapes: leading arg 0 valid, arg 1 invalid, and a
doubled arg 0 invalid.

The DIExpression section of SourceLevelDebugging.md said a debug expression
starts with the record's operand on the stack, which was only true for the
non-variadic case. Say what a variadic expression starts with too, since that's
the distinction this patch turns on.
---
 llvm/docs/SourceLevelDebugging.md             |  8 ++--
 llvm/lib/IR/DebugInfoMetadata.cpp             |  8 +++-
 .../X86/di-expression-symbolic-branch-arg0.ll | 40 +++++++++++++++++++
 .../di-expression-symbolic-branches.ll        | 12 +++---
 llvm/unittests/IR/MetadataTest.cpp            |  8 +++-
 5 files changed, 64 insertions(+), 12 deletions(-)
 create mode 100644 llvm/test/DebugInfo/X86/di-expression-symbolic-branch-arg0.ll

diff --git a/llvm/docs/SourceLevelDebugging.md b/llvm/docs/SourceLevelDebugging.md
index 8757ce215a005..a202bcf3c6471 100644
--- a/llvm/docs/SourceLevelDebugging.md
+++ b/llvm/docs/SourceLevelDebugging.md
@@ -383,9 +383,11 @@ call void @llvm.dbg.assign(
 
 Debug expressions are represented as {ref}`specialized-metadata`.
 
-A debug expression starts with the record's value or address operand on the
-stack, then evaluates operations from left to right unless a symbolic branch
-jumps to a label in the same `DIExpression`.
+A non-variadic debug expression starts with the record's value or address
+operand on the stack. Variadic expressions start with an empty stack and use
+`DW_OP_LLVM_arg` to push operands from the `DIArgList`. Operations evaluate
+from left to right unless a symbolic branch jumps to a label in the same
+`DIExpression`.
 
 The opcodes available in these expressions are described in
 {ref}`dwarf-opcodes` and {ref}`internal-opcodes`.
diff --git a/llvm/lib/IR/DebugInfoMetadata.cpp b/llvm/lib/IR/DebugInfoMetadata.cpp
index 460eaa80d91a9..4402f421b5283 100644
--- a/llvm/lib/IR/DebugInfoMetadata.cpp
+++ b/llvm/lib/IR/DebugInfoMetadata.cpp
@@ -1858,8 +1858,14 @@ bool DIExpression::isValid() const {
       if (HasControlFlow)
         return false;
       break;
-    // These ops use lowering paths that don't support symbolic branches.
+    // CodeGen may make a single-location expression variadic by prepending
+    // DW_OP_LLVM_arg 0. Other location arguments use lowering paths that
+    // don't support symbolic branches.
     case dwarf::DW_OP_LLVM_arg:
+      if (I != expr_op_begin() || I->getArg(0) != 0)
+        HasControlFlowConflict = true;
+      break;
+    // This op uses a lowering path that doesn't support symbolic branches.
     case dwarf::DW_OP_LLVM_implicit_pointer:
       HasControlFlowConflict = true;
       break;
diff --git a/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-arg0.ll b/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-arg0.ll
new file mode 100644
index 0000000000000..07e6b79e24be3
--- /dev/null
+++ b/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-arg0.ll
@@ -0,0 +1,40 @@
+; RUN: llc -experimental-debug-variable-locations -mtriple=x86_64-unknown-linux-gnu -stop-after=finalize-isel -o - %s | FileCheck %s --check-prefix=MIR
+; RUN: llc -experimental-debug-variable-locations -mtriple=x86_64-unknown-linux-gnu -filetype=obj -o - %s | llvm-dwarfdump - | FileCheck %s --check-prefix=DWARF
+
+; Instruction-reference lowering makes a single-location expression variadic
+; by prepending DW_OP_LLVM_arg 0. This internal adapter remains valid with
+; symbolic control flow.
+
+declare void @use(i64)
+
+define void @instr_ref(i64 %x) !dbg !5 {
+entry:
+  %y = add i64 %x, 1
+  #dbg_value(i64 %y, !9,
+             !DIExpression(DW_OP_LLVM_skip, 1, DW_OP_plus_uconst, 1,
+                           DW_OP_LLVM_label, 1), !12)
+  call void @use(i64 %y), !dbg !12
+  ret void, !dbg !12
+}
+
+; MIR: DBG_INSTR_REF !{{[0-9]+}}, !DIExpression(DW_OP_LLVM_arg, 0, DW_OP_LLVM_skip, 1, DW_OP_plus_uconst, 1, DW_OP_LLVM_label, 1)
+
+; DWARF: DW_OP_breg5 RDI+0, DW_OP_skip
+; DWARF: DW_AT_name {{.*}}"instr_ref"
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1,
+                             emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.c", directory: "/")
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!5 = distinct !DISubprogram(name: "instr_ref", scope: !1, file: !1, type: !6,
+                            spFlags: DISPFlagDefinition, unit: !0,
+                            retainedNodes: !8)
+!6 = !DISubroutineType(types: !7)
+!7 = !{null}
+!8 = !{!9}
+!9 = !DILocalVariable(name: "instr_ref", scope: !5, type: !11)
+!11 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
+!12 = !DILocation(line: 1, scope: !5)
diff --git a/llvm/test/Verifier/di-expression-symbolic-branches.ll b/llvm/test/Verifier/di-expression-symbolic-branches.ll
index db0e0dacdf32f..bd531c7acbde1 100644
--- a/llvm/test/Verifier/di-expression-symbolic-branches.ll
+++ b/llvm/test/Verifier/di-expression-symbolic-branches.ll
@@ -6,7 +6,6 @@
 ; RUN: not opt -passes=verify -disable-output %t/raw.ll 2>&1 | FileCheck %s --check-prefix=INVALID --implicit-check-not="invalid expression"
 ; RUN: not opt -passes=verify -disable-output %t/raw-after-register.ll 2>&1 | FileCheck %s --check-prefix=INVALID --implicit-check-not="invalid expression"
 ; RUN: not opt -passes=verify -disable-output %t/incompatible.ll 2>&1 | FileCheck %s --check-prefix=INVALID --implicit-check-not="invalid expression"
-; RUN: not opt -passes=verify -disable-output %t/location-arg.ll 2>&1 | FileCheck %s --check-prefix=INVALID --implicit-check-not="invalid expression"
 ; RUN: not opt -passes=verify -disable-output %t/tag-ordering.ll 2>&1 | FileCheck %s --check-prefix=INVALID --implicit-check-not="invalid expression"
 ; RUN: not opt -passes=verify -disable-output %t/terminal.ll 2>&1 | FileCheck %s --check-prefix=INVALID --implicit-check-not="invalid expression"
 
@@ -26,9 +25,13 @@
 ; ARG-LIST: warning: ignoring invalid debug info
 
 ;--- valid.ll
-!named = !{!0}
+!named = !{!0, !1}
 !0 = !DIExpression(DW_OP_LLVM_bra, 0, DW_OP_LLVM_skip, 0,
                    DW_OP_LLVM_label, 0)
+; A leading arg 0 is structurally valid because CodeGen may introduce it after
+; IR verification when making a single-location expression variadic.
+!1 = !DIExpression(DW_OP_LLVM_arg, 0, DW_OP_LLVM_skip, 1,
+                   DW_OP_LLVM_label, 1)
 
 ;--- arity.ll
 !named = !{!0}
@@ -56,11 +59,6 @@
 !named = !{!0}
 !0 = !DIExpression(DW_OP_LLVM_implicit_pointer, DW_OP_LLVM_label, 1)
 
-;--- location-arg.ll
-!named = !{!0}
-!0 = !DIExpression(DW_OP_LLVM_arg, 0, DW_OP_LLVM_skip, 1,
-                   DW_OP_LLVM_label, 1)
-
 ;--- tag-ordering.ll
 !named = !{!0}
 !0 = !DIExpression(DW_OP_LLVM_label, 1, DW_OP_LLVM_tag_offset, 0)
diff --git a/llvm/unittests/IR/MetadataTest.cpp b/llvm/unittests/IR/MetadataTest.cpp
index c130ddabb2eed..7bcf9835d9f2e 100644
--- a/llvm/unittests/IR/MetadataTest.cpp
+++ b/llvm/unittests/IR/MetadataTest.cpp
@@ -4616,7 +4616,13 @@ TEST_F(DIExpressionTest, isValid) {
   EXPECT_INVALID(dwarf::DW_OP_skip, 0);
   EXPECT_INVALID(dwarf::DW_OP_reg0, dwarf::DW_OP_bra, 0);
   EXPECT_INVALID(dwarf::DW_OP_reg0, dwarf::DW_OP_skip, 0);
-  EXPECT_INVALID(dwarf::DW_OP_LLVM_arg, 0, dwarf::DW_OP_LLVM_label, 1);
+  // CodeGen may prepend arg 0 when making a single-location expression
+  // variadic, but other argument placements remain incompatible with symbolic
+  // control flow.
+  EXPECT_VALID(dwarf::DW_OP_LLVM_arg, 0, dwarf::DW_OP_LLVM_label, 1);
+  EXPECT_INVALID(dwarf::DW_OP_LLVM_arg, 1, dwarf::DW_OP_LLVM_label, 1);
+  EXPECT_INVALID(dwarf::DW_OP_LLVM_arg, 0, dwarf::DW_OP_LLVM_arg, 0,
+                 dwarf::DW_OP_LLVM_label, 1);
   EXPECT_INVALID(dwarf::DW_OP_LLVM_implicit_pointer, dwarf::DW_OP_LLVM_label,
                  1);
   EXPECT_INVALID(dwarf::DW_OP_LLVM_label, 1, dwarf::DW_OP_LLVM_tag_offset, 0);

>From e45710750733e03953fda199209e39a3180cefba Mon Sep 17 00:00:00 2001
From: Eric Christopher <echristopher at nvidia.com>
Date: Thu, 13 Aug 2026 23:16:58 -0700
Subject: [PATCH 14/19] [DebugInfo] Track the temporary buffer size as it grows

getTemporaryBufferSize() walked the whole DIE value list, once per label and
once per branch, from inside the per-operand loop. Have the emit overrides
add up what they append instead. Don't put DIELoc::computeSize() back: it
memoizes into a mutable field that takeValues() never resets, so every
expression after the first would be told the first one's size.
---
 llvm/lib/CodeGen/AsmPrinter/DwarfExpression.h | 15 ++++++++++
 llvm/lib/CodeGen/AsmPrinter/DwarfUnit.cpp     | 30 ++++++++++++++-----
 2 files changed, 37 insertions(+), 8 deletions(-)

diff --git a/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.h b/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.h
index 17bd867247f9d..c9c3663e3d8c0 100644
--- a/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.h
+++ b/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.h
@@ -373,9 +373,24 @@ class DIEDwarfExpression final : public DwarfExpression {
   DIELoc TmpDIE;
   bool IsBuffering = false;
 
+  /// Running size, in bytes, of the values buffered in TmpDIE.
+  ///
+  /// This is tracked incrementally because addExpression() asks for the buffer
+  /// size once per label and once per branch, from inside its per-operand
+  /// loop. Don't replace it with DIELoc::computeSize(): that memoizes into a
+  /// mutable field which takeValues() never resets, so every expression after
+  /// the first would be told the first one's size.
+  unsigned TmpDIESize = 0;
+
   /// Return the DIE that currently is being emitted to.
   DIELoc &getActiveDIE() { return IsBuffering ? TmpDIE : OutDIE; }
 
+  /// Account for the \p Bytes an emit override just appended to TmpDIE.
+  void addBufferedBytes(unsigned Bytes) {
+    if (IsBuffering)
+      TmpDIESize += Bytes;
+  }
+
   void emitOp(uint8_t Op, const char *Comment = nullptr) override;
   void emitSigned(int64_t Value) override;
   void emitUnsigned(uint64_t Value) override;
diff --git a/llvm/lib/CodeGen/AsmPrinter/DwarfUnit.cpp b/llvm/lib/CodeGen/AsmPrinter/DwarfUnit.cpp
index 4085f8628548a..5ba03965fbba7 100644
--- a/llvm/lib/CodeGen/AsmPrinter/DwarfUnit.cpp
+++ b/llvm/lib/CodeGen/AsmPrinter/DwarfUnit.cpp
@@ -57,28 +57,44 @@ static uint64_t getDataValue(uint64_t Value, unsigned Size) {
   return Value & maskTrailingOnes<uint64_t>(Size * 8);
 }
 
+/// Return the number of bytes \p Integer occupies when encoded with \p Form.
+static unsigned getEncodedIntegerSize(const AsmPrinter &AP, dwarf::Form Form,
+                                      uint64_t Integer) {
+  return DIEInteger(Integer).sizeOf(AP.getDwarfFormParams(), Form);
+}
+
 DIEDwarfExpression::DIEDwarfExpression(const AsmPrinter &AP,
                                        DwarfCompileUnit &CU, DIELoc &DIE)
     : DwarfExpression(AP.getDwarfVersion(), CU), AP(AP), OutDIE(DIE) {}
 
 void DIEDwarfExpression::emitOp(uint8_t Op, const char* Comment) {
   CU.addUInt(getActiveDIE(), dwarf::DW_FORM_data1, Op);
+  addBufferedBytes(getEncodedIntegerSize(AP, dwarf::DW_FORM_data1, Op));
 }
 
 void DIEDwarfExpression::emitSigned(int64_t Value) {
   CU.addSInt(getActiveDIE(), dwarf::DW_FORM_sdata, Value);
+  addBufferedBytes(getEncodedIntegerSize(AP, dwarf::DW_FORM_sdata, Value));
 }
 
 void DIEDwarfExpression::emitUnsigned(uint64_t Value) {
   CU.addUInt(getActiveDIE(), dwarf::DW_FORM_udata, Value);
+  addBufferedBytes(getEncodedIntegerSize(AP, dwarf::DW_FORM_udata, Value));
 }
 
 void DIEDwarfExpression::emitData(uint64_t Value, unsigned Size) {
-  CU.addUInt(getActiveDIE(), getDataForm(Size), getDataValue(Value, Size));
+  dwarf::Form Form = getDataForm(Size);
+  uint64_t Data = getDataValue(Value, Size);
+  CU.addUInt(getActiveDIE(), Form, Data);
+  addBufferedBytes(getEncodedIntegerSize(AP, Form, Data));
 }
 
 void DIEDwarfExpression::emitBaseTypeRef(uint64_t Idx) {
   CU.addBaseTypeRef(getActiveDIE(), Idx);
+  // A DIEBaseTypeRef pads its ULEB128 out to a fixed width so that base type
+  // DIE offsets can be filled in later, so ask it rather than the index.
+  DIEBaseTypeRef Ref(&CU, Idx);
+  addBufferedBytes(Ref.sizeOf(AP.getDwarfFormParams(), dwarf::DW_FORM_udata));
 }
 
 void DIEDwarfExpression::enableTemporaryBuffer() {
@@ -88,14 +104,12 @@ void DIEDwarfExpression::enableTemporaryBuffer() {
 
 void DIEDwarfExpression::disableTemporaryBuffer() { IsBuffering = false; }
 
-unsigned DIEDwarfExpression::getTemporaryBufferSize() {
-  unsigned Size = 0;
-  for (const DIEValue &V : TmpDIE.values())
-    Size += V.sizeOf(AP.getDwarfFormParams());
-  return Size;
-}
+unsigned DIEDwarfExpression::getTemporaryBufferSize() { return TmpDIESize; }
 
-void DIEDwarfExpression::commitTemporaryBuffer() { OutDIE.takeValues(TmpDIE); }
+void DIEDwarfExpression::commitTemporaryBuffer() {
+  OutDIE.takeValues(TmpDIE);
+  TmpDIESize = 0;
+}
 
 void DIEDwarfExpression::replaceTemporaryBufferData(unsigned PlaceholderOffset,
                                                     uint64_t Replacement,

>From 66094d201224100527ed0bea6dd62089b2e54ec7 Mon Sep 17 00:00:00 2001
From: Eric Christopher <echristopher at nvidia.com>
Date: Thu, 13 Aug 2026 23:16:58 -0700
Subject: [PATCH 15/19] [DebugInfo] Fix the emitter's failure modes for
 symbolic branches

Three things, all in how the emitter fails rather than what it emits.

A label emits nothing, so on its own it can't separate a deferred legacy
DW_OP_LLVM_convert from the conversion that completes it, and with no branch
around no label offset is ever consumed. It was aborting the compile anyway
under DWARF 4, where the same expression without the label emits normally and
drops the unpaired conversion. Gate the label arm on the expression actually
branching and leave the branch and skip arms alone.

The convert-across-branch and out-of-range-offset errors are properties of the
input, so use reportFatalUsageError and stop asking the user to file an LLVM
bug for their own IR. A branch to a missing label is a compiler bug, since the
verifier rejects it, so that one stays internal and keeps its crash trace.

Abandoning an expression left whatever was buffered for the next one to commit
as its own. Neither path can reach that today, but a leading DW_OP_LLVM_arg 0
is valid now and InsertArg is what resolves it, so assert the buffer is empty
the way cancelEntryValue already does.
---
 .../CodeGen/AsmPrinter/DwarfExpression.cpp    | 58 +++++++++++++------
 llvm/lib/CodeGen/AsmPrinter/DwarfExpression.h | 10 ++--
 ...ession-symbolic-branch-convert-boundary.ll | 45 ++++++++++++--
 ...expression-symbolic-branch-out-of-range.ll |  2 +-
 4 files changed, 88 insertions(+), 27 deletions(-)

diff --git a/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp b/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp
index 3e1ecf2fabee3..617c8a185e499 100644
--- a/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp
+++ b/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp
@@ -29,6 +29,14 @@ using namespace llvm;
 
 #define DEBUG_TYPE "dwarfdebug"
 
+/// Report that a deferred legacy DW_OP_LLVM_convert can't be lowered because
+/// \p OpNum comes between it and the conversion that would complete it.
+[[noreturn]] static void reportConvertAcross(uint64_t OpNum) {
+  reportFatalUsageError(Twine("cannot lower DW_OP_LLVM_convert across ") +
+                        dwarf::OperationEncodingString(OpNum) +
+                        " without DW_OP_convert support");
+}
+
 /// Return whether the rest of the expression needs the complex register path.
 /// We use this to decide whether we can emit a simple register location and
 /// whether a subregister needs to be masked. Non-emitting operations don't
@@ -581,6 +589,18 @@ bool DwarfExpression::addExpression(
   if (HasSymbolicBranches)
     enableTemporaryBuffer();
 
+  // Give up on the expression without committing the temporary buffer. The
+  // buffer can't be emptied, so mirror cancelEntryValue() and assert that
+  // nothing was emitted into it: this DwarfExpression is reused for the next
+  // expression, which would otherwise commit the abandoned bytes as its own.
+  auto AbandonTemporaryBuffer = [&] {
+    if (!HasSymbolicBranches)
+      return;
+    disableTemporaryBuffer();
+    assert(getTemporaryBufferSize() == 0 &&
+           "Began emitting the expression before abandoning it");
+  };
+
   std::optional<DIExpression::ConvertOp> PrevConvertOp;
   while (ExprCursor) {
     auto Op = ExprCursor.take();
@@ -596,16 +616,17 @@ bool DwarfExpression::addExpression(
 
     switch (OpNum) {
     case dwarf::DW_OP_LLVM_label:
+      // A label emits nothing, so on its own it can't separate a deferred
+      // legacy DW_OP_LLVM_convert from the conversion that completes it. It
+      // only matters once a branch is around to jump to it.
+      if (PrevConvertOp && HasSymbolicBranches)
+        reportConvertAcross(OpNum);
+      Labels.push_back({Op->getArg(0), getTemporaryBufferSize()});
+      break;
     case dwarf::DW_OP_LLVM_bra:
     case dwarf::DW_OP_LLVM_skip:
       if (PrevConvertOp)
-        report_fatal_error(Twine("cannot lower DW_OP_LLVM_convert across ") +
-                           dwarf::OperationEncodingString(OpNum) +
-                           " without DW_OP_convert support");
-      if (Op->is(dwarf::DW_OP_LLVM_label)) {
-        Labels.push_back({Op->getArg(0), getTemporaryBufferSize()});
-        break;
-      }
+        reportConvertAcross(OpNum);
       emitOp(Op->is(dwarf::DW_OP_LLVM_bra) ? dwarf::DW_OP_bra
                                            : dwarf::DW_OP_skip);
       Fixups.push_back({Op->getArg(0), getTemporaryBufferSize()});
@@ -614,8 +635,9 @@ bool DwarfExpression::addExpression(
     case dwarf::DW_OP_LLVM_arg:
       if (!InsertArg(cast<DIExpression::ArgOp>(*Op).getIndex(), ExprCursor)) {
         LocationKind = Unknown;
-        if (HasSymbolicBranches)
-          disableTemporaryBuffer();
+        // Control flow only allows a leading DW_OP_LLVM_arg 0, and InsertArg
+        // fails before it emits anything, so nothing is buffered yet.
+        AbandonTemporaryBuffer();
         return false;
       }
       break;
@@ -820,10 +842,9 @@ bool DwarfExpression::addExpression(
       // Handled in DwarfCompileUnit::emitImplicitPointerLocation for
       // Loc::Single variables. If we reach here, the variable has a location
       // list or another unsupported path, so stop emitting the expression.
-      // We buffer expressions with symbolic branches, so disable the buffer
-      // before returning.
-      if (HasSymbolicBranches)
-        disableTemporaryBuffer();
+      // DIExpression::isValid rejects this op alongside control flow, so
+      // nothing is buffered yet.
+      AbandonTemporaryBuffer();
       return false;
     default:
       llvm_unreachable("unhandled opcode found in expression");
@@ -841,9 +862,11 @@ bool DwarfExpression::addExpression(
     auto Label = llvm::find_if(Labels, [&](const LabelOffset &Candidate) {
       return Candidate.ID == Fixup.LabelID;
     });
+    // The verifier rejects a branch to a label that isn't in the expression,
+    // so getting here means something dropped the label after that check.
     if (Label == Labels.end())
-      report_fatal_error(Twine("DWARF expression branch to label ") +
-                         Twine(Fixup.LabelID) + " has no matching label");
+      reportFatalInternalError(Twine("DWARF expression branch to label ") +
+                               Twine(Fixup.LabelID) + " has no matching label");
 
     // DW_OP_bra and DW_OP_skip apply the displacement after reading their
     // two-byte operand, so use the byte after the placeholder as the base.
@@ -851,8 +874,9 @@ bool DwarfExpression::addExpression(
         static_cast<int64_t>(Label->Offset) -
         static_cast<int64_t>(Fixup.PlaceholderOffset + BranchOffsetByteSize);
     if (!isInt<16>(Displacement))
-      report_fatal_error(Twine("DWARF expression branch offset ") +
-                         Twine(Displacement) + " is outside [-32768, 32767]");
+      reportFatalUsageError(Twine("DWARF expression branch offset ") +
+                            Twine(Displacement) +
+                            " is outside [-32768, 32767]");
 
     replaceTemporaryBufferData2(Fixup.PlaceholderOffset,
                                 static_cast<uint16_t>(Displacement));
diff --git a/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.h b/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.h
index c9c3663e3d8c0..5cc62d5d95f84 100644
--- a/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.h
+++ b/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.h
@@ -300,14 +300,16 @@ class DwarfExpression {
 
   /// Emit all remaining operations in the DIExpressionCursor. The
   /// cursor must not contain any DW_OP_LLVM_arg operations.
-  /// CodeGen reports an error if a branch offset is outside [-32768, 32767] or
-  /// a deferred DW_OP_LLVM_convert reaches a label, branch, or skip.
+  /// CodeGen reports an error if a branch offset is outside [-32768, 32767],
+  /// or if a deferred DW_OP_LLVM_convert reaches a branch, a skip, or a label
+  /// in an expression that branches.
   void addExpression(DIExpressionCursor &&Expr);
 
   /// Emit all remaining operations in the DIExpressionCursor.
   /// DW_OP_LLVM_arg operations are resolved by calling (\p InsertArg).
-  /// CodeGen reports an error if a branch offset is outside [-32768, 32767] or
-  /// a deferred DW_OP_LLVM_convert reaches a label, branch, or skip.
+  /// CodeGen reports an error if a branch offset is outside [-32768, 32767],
+  /// or if a deferred DW_OP_LLVM_convert reaches a branch, a skip, or a label
+  /// in an expression that branches.
   //
   /// \return false if any call to (\p InsertArg) returns false.
   bool addExpression(
diff --git a/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-convert-boundary.ll b/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-convert-boundary.ll
index 5cb9861502585..a02df9ced2bfe 100644
--- a/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-convert-boundary.ll
+++ b/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-convert-boundary.ll
@@ -1,14 +1,19 @@
 ; RUN: split-file %s %t
 ; RUN: llc -mtriple=x86_64-unknown-linux-gnu -dwarf-version=5 -filetype=obj -o - %t/skip.ll | llvm-dwarfdump -v - | FileCheck %s --check-prefix=SKIP-NATIVE
-; RUN: not --crash llc -mtriple=x86_64-unknown-linux-gnu -dwarf-version=4 -filetype=obj -o /dev/null %t/skip.ll 2>&1 | FileCheck %s --check-prefix=SKIP-LEGACY
+; RUN: not llc -mtriple=x86_64-unknown-linux-gnu -dwarf-version=4 -filetype=obj -o /dev/null %t/skip.ll 2>&1 | FileCheck %s --check-prefix=SKIP-LEGACY
 ; RUN: llc -mtriple=x86_64-unknown-linux-gnu -dwarf-version=5 -filetype=obj -o - %t/bra.ll | llvm-dwarfdump -v - | FileCheck %s --check-prefix=BRA-NATIVE
-; RUN: not --crash llc -mtriple=x86_64-unknown-linux-gnu -dwarf-version=4 -filetype=obj -o /dev/null %t/bra.ll 2>&1 | FileCheck %s --check-prefix=BRA-LEGACY
+; RUN: not llc -mtriple=x86_64-unknown-linux-gnu -dwarf-version=4 -filetype=obj -o /dev/null %t/bra.ll 2>&1 | FileCheck %s --check-prefix=BRA-LEGACY
 ; RUN: llc -mtriple=x86_64-unknown-linux-gnu -dwarf-version=5 -filetype=obj -o - %t/label.ll | llvm-dwarfdump -v - | FileCheck %s --check-prefix=LABEL-NATIVE
-; RUN: not --crash llc -mtriple=x86_64-unknown-linux-gnu -dwarf-version=4 -filetype=obj -o /dev/null %t/label.ll 2>&1 | FileCheck %s --check-prefix=LABEL-LEGACY
+; RUN: llc -mtriple=x86_64-unknown-linux-gnu -dwarf-version=4 -filetype=obj -o - %t/label.ll | llvm-dwarfdump -v - | FileCheck %s --check-prefix=LABEL-LEGACY
+; RUN: llc -mtriple=x86_64-unknown-linux-gnu -dwarf-version=5 -filetype=obj -o - %t/label-branch.ll | llvm-dwarfdump -v - | FileCheck %s --check-prefix=LABEL-BRANCH-NATIVE
+; RUN: not llc -mtriple=x86_64-unknown-linux-gnu -dwarf-version=4 -filetype=obj -o /dev/null %t/label-branch.ll 2>&1 | FileCheck %s --check-prefix=LABEL-BRANCH-LEGACY
 
 ; DWARF 5 emits DW_OP_convert directly, so labels and branches can appear
 ; between conversions. With DWARF 4 we may defer one conversion until the next;
-; if a label, branch, or skip splits the pair, CodeGen reports an error.
+; if a branch, a skip, or a label a branch can reach splits the pair, CodeGen
+; reports an error. A label on its own emits nothing and nothing consumes its
+; offset, so it lowers like any other lone conversion under DWARF 4: the
+; conversion is dropped.
 
 ; SKIP-NATIVE: DW_AT_location [DW_FORM_exprloc] (DW_OP_breg5 RDI+0, DW_OP_convert {{.*}} "DW_ATE_signed_32", DW_OP_skip +5, DW_OP_convert {{.*}} "DW_ATE_signed_64")
 ; SKIP-LEGACY: LLVM ERROR: cannot lower DW_OP_LLVM_convert across DW_OP_LLVM_skip without DW_OP_convert support
@@ -17,7 +22,10 @@
 ; BRA-LEGACY: LLVM ERROR: cannot lower DW_OP_LLVM_convert across DW_OP_LLVM_bra without DW_OP_convert support
 
 ; LABEL-NATIVE: DW_AT_location [DW_FORM_exprloc] (DW_OP_breg5 RDI+0, DW_OP_convert {{.*}} "DW_ATE_signed_32")
-; LABEL-LEGACY: LLVM ERROR: cannot lower DW_OP_LLVM_convert across DW_OP_LLVM_label without DW_OP_convert support
+; LABEL-LEGACY: DW_AT_location [DW_FORM_exprloc] (DW_OP_breg5 RDI+0)
+
+; LABEL-BRANCH-NATIVE: DW_AT_location [DW_FORM_exprloc] (DW_OP_breg5 RDI+0, DW_OP_skip +10, DW_OP_convert {{.*}} "DW_ATE_signed_32", DW_OP_convert {{.*}} "DW_ATE_signed_64")
+; LABEL-BRANCH-LEGACY: LLVM ERROR: cannot lower DW_OP_LLVM_convert across DW_OP_LLVM_label without DW_OP_convert support
 
 ;--- skip.ll
 define void @skip(i64 %x) !dbg !5 {
@@ -94,3 +102,30 @@ entry:
 !9 = !DILocalVariable(name: "bra", arg: 1, scope: !5, type: !11)
 !10 = !DILocation(line: 1, column: 1, scope: !5)
 !11 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
+
+;--- label-branch.ll
+define void @label_branch(i64 %x) !dbg !5 {
+entry:
+  #dbg_value(i64 %x, !9,
+             !DIExpression(DW_OP_LLVM_skip, 2,
+                           DW_OP_LLVM_convert, 32, DW_ATE_signed,
+                           DW_OP_LLVM_label, 1,
+                           DW_OP_LLVM_convert, 64, DW_ATE_signed,
+                           DW_OP_LLVM_label, 2), !10)
+  ret void, !dbg !10
+}
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!4}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1,
+                             emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.c", directory: "/")
+!4 = !{i32 2, !"Debug Info Version", i32 3}
+!5 = distinct !DISubprogram(name: "label_branch", scope: !1, type: !6,
+                            spFlags: DISPFlagDefinition, unit: !0)
+!6 = !DISubroutineType(types: !7)
+!7 = !{null}
+!9 = !DILocalVariable(name: "label_branch", arg: 1, scope: !5, type: !11)
+!10 = !DILocation(line: 1, column: 1, scope: !5)
+!11 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
diff --git a/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-out-of-range.ll b/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-out-of-range.ll
index 677032dc9c7a7..7bd5867440fee 100644
--- a/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-out-of-range.ll
+++ b/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-out-of-range.ll
@@ -1,5 +1,5 @@
 ; RUN: %python %S/Inputs/generate-di-expression-symbolic-branch-out-of-range.py > %t.ll
-; RUN: not --crash llc -mtriple=x86_64-unknown-linux-gnu -filetype=obj -o /dev/null %t.ll 2>&1 | FileCheck %s
+; RUN: not llc -mtriple=x86_64-unknown-linux-gnu -filetype=obj -o /dev/null %t.ll 2>&1 | FileCheck %s
 
 ; The first positive offset that doesn't fit is 32768, so make sure CodeGen
 ; reports it instead of truncating it.

>From 8eb29f1280525103b82fdc2e061f80e026629578 Mon Sep 17 00:00:00 2001
From: Eric Christopher <echristopher at nvidia.com>
Date: Thu, 13 Aug 2026 23:16:58 -0700
Subject: [PATCH 16/19] [DebugInfo] Drop the redundant control-flow suffix
 checks from isValid

The first loop tracked whether anything followed a DW_OP_stack_value or a
DW_OP_LLVM_fragment and folded that into the control-flow verdict, but the
second loop already rejects both, for every expression rather than only the
ones that branch. Two encodings of one rule invite a one-sided edit, which
would leave validity disagreeing between expressions that branch and
expressions that don't.

Enumerating 4360015 operand sequences through a model of both loops changes no
verdict, and the model agrees with all 58 validity vectors in MetadataTest.

Five unit assertions sat under a comment saying register and entry-value ops
end validation early. They stopped doing that in #214057, and every one of the
five is rejected by a rule that has nothing to do with control flow: swap
their branches for DW_OP_plus and they still fail. Replace them with two that
put the defect before a trailing fragment, which is the one place validation
really can return early, and say so in the comment.

The verifier's DIArgList rule is the only thing rejecting a record that
carries an arg list and a symbolic branch, now that a leading DW_OP_LLVM_arg 0
is valid on its own. Test it.
---
 llvm/lib/IR/DebugInfoMetadata.cpp             | 22 ++-----------
 .../di-expression-symbolic-branches.ll        | 32 +++++++++++++++++--
 llvm/unittests/IR/MetadataTest.cpp            | 19 +++--------
 3 files changed, 36 insertions(+), 37 deletions(-)

diff --git a/llvm/lib/IR/DebugInfoMetadata.cpp b/llvm/lib/IR/DebugInfoMetadata.cpp
index 4402f421b5283..a8409576b846f 100644
--- a/llvm/lib/IR/DebugInfoMetadata.cpp
+++ b/llvm/lib/IR/DebugInfoMetadata.cpp
@@ -1820,9 +1820,6 @@ bool DIExpression::isValid() const {
   SmallVector<uint64_t, 4> LabelReferences;
   bool HasControlFlow = false;
   bool HasControlFlowConflict = false;
-  bool HasStackValue = false;
-  bool HasFragment = false;
-  bool HasInvalidControlFlowSuffix = false;
 
   // Collect labels and branch targets before running the checks below, which
   // may return early.
@@ -1831,14 +1828,7 @@ bool DIExpression::isValid() const {
     if (I->get() + I->getSize() > E->get())
       return false;
 
-    uint64_t Op = I->getOp();
-
-    // Only DW_OP_LLVM_fragment may follow DW_OP_stack_value, and nothing may
-    // follow the fragment.
-    HasInvalidControlFlowSuffix |=
-        HasFragment || (HasStackValue && Op != dwarf::DW_OP_LLVM_fragment);
-
-    switch (Op) {
+    switch (I->getOp()) {
     case dwarf::DW_OP_LLVM_label:
       if (!Labels.insert(I->getArg(0)).second)
         return false;
@@ -1869,20 +1859,12 @@ bool DIExpression::isValid() const {
     case dwarf::DW_OP_LLVM_implicit_pointer:
       HasControlFlowConflict = true;
       break;
-    case dwarf::DW_OP_LLVM_entry_value:
-      HasControlFlowConflict |= !IsEntryValueValid(*I);
-      break;
     default:
       break;
     }
-
-    if (Op == dwarf::DW_OP_stack_value)
-      HasStackValue = true;
-    else if (Op == dwarf::DW_OP_LLVM_fragment)
-      HasFragment = true;
   }
 
-  if (HasControlFlow && (HasControlFlowConflict || HasInvalidControlFlowSuffix))
+  if (HasControlFlow && HasControlFlowConflict)
     return false;
   for (uint64_t Label : LabelReferences)
     if (!Labels.contains(Label))
diff --git a/llvm/test/Verifier/di-expression-symbolic-branches.ll b/llvm/test/Verifier/di-expression-symbolic-branches.ll
index bd531c7acbde1..7d8c39dfc64df 100644
--- a/llvm/test/Verifier/di-expression-symbolic-branches.ll
+++ b/llvm/test/Verifier/di-expression-symbolic-branches.ll
@@ -18,7 +18,8 @@
 
 ; DIArgList doesn't support symbolic branches, but normal IR loading drops the
 ; bad debug info, so opt still succeeds.
-; RUN: opt -passes=verify -disable-output %t/arg-list.ll 2>&1 | FileCheck %s --check-prefix=ARG-LIST
+; RUN: opt -passes=verify -disable-output %t/arg-list.ll 2>&1 | FileCheck %s --check-prefix=ARG-LIST --implicit-check-not="invalid expression"
+; RUN: opt -passes=verify -disable-output %t/arg-list-leading-arg.ll 2>&1 | FileCheck %s --check-prefix=ARG-LIST --implicit-check-not="invalid expression"
 
 ; INVALID: invalid expression
 ; ARG-LIST: DIArgList doesn't support symbolic branches
@@ -50,8 +51,8 @@
 !0 = !DIExpression(DW_OP_bra, 0)
 
 ;--- raw-after-register.ll
-; A register normally ends validation, but it must not hide a raw branch later
-; in the expression.
+; A register operation doesn't end validation, so a raw branch after one is
+; still rejected.
 !named = !{!0}
 !0 = !DIExpression(DW_OP_reg0, DW_OP_skip, 0)
 
@@ -89,3 +90,28 @@ entry:
 !6 = !{null}
 !7 = !DILocalVariable(name: "x", scope: !4)
 !8 = !DILocation(line: 1, column: 1, scope: !4)
+
+;--- arg-list-leading-arg.ll
+; A leading arg 0 no longer makes the expression invalid on its own, so the
+; DIArgList rule is all that rejects this record.
+define void @f(i32 %x) !dbg !4 {
+entry:
+  #dbg_value(!DIArgList(i32 %x), !7,
+             !DIExpression(DW_OP_LLVM_arg, 0, DW_OP_LLVM_skip, 1,
+                           DW_OP_LLVM_label, 1), !8)
+  ret void, !dbg !8
+}
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1,
+                             emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.c", directory: "/")
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!4 = distinct !DISubprogram(name: "f", scope: !1, type: !5,
+                            spFlags: DISPFlagDefinition, unit: !0)
+!5 = !DISubroutineType(types: !6)
+!6 = !{null}
+!7 = !DILocalVariable(name: "x", scope: !4)
+!8 = !DILocation(line: 1, column: 1, scope: !4)
diff --git a/llvm/unittests/IR/MetadataTest.cpp b/llvm/unittests/IR/MetadataTest.cpp
index 7bcf9835d9f2e..cc059c3430738 100644
--- a/llvm/unittests/IR/MetadataTest.cpp
+++ b/llvm/unittests/IR/MetadataTest.cpp
@@ -4640,20 +4640,11 @@ TEST_F(DIExpressionTest, isValid) {
                dwarf::DW_OP_LLVM_label, 1);
   EXPECT_INVALID(dwarf::DW_OP_stack_value, dwarf::DW_OP_LLVM_label, 1);
   EXPECT_INVALID(dwarf::DW_OP_LLVM_fragment, 0, 32, dwarf::DW_OP_LLVM_label, 1);
-  // reg0 and entry_value normally end validation early, but they must not hide
-  // invalid control flow later in the expression.
-  EXPECT_INVALID(dwarf::DW_OP_LLVM_entry_value, 1, dwarf::DW_OP_stack_value,
-                 dwarf::DW_OP_LLVM_bra, 1, dwarf::DW_OP_LLVM_label, 1);
-  EXPECT_INVALID(dwarf::DW_OP_reg0, dwarf::DW_OP_LLVM_fragment, 0, 32,
-                 dwarf::DW_OP_LLVM_label, 1);
-  EXPECT_INVALID(dwarf::DW_OP_reg0, dwarf::DW_OP_LLVM_bra, 1,
-                 dwarf::DW_OP_LLVM_label, 1, dwarf::DW_OP_LLVM_fragment, 0, 32,
-                 dwarf::DW_OP_plus);
-  EXPECT_INVALID(dwarf::DW_OP_LLVM_entry_value, 1, dwarf::DW_OP_LLVM_bra, 1,
-                 dwarf::DW_OP_LLVM_label, 1, dwarf::DW_OP_stack_value,
-                 dwarf::DW_OP_plus);
-  EXPECT_INVALID(dwarf::DW_OP_reg0, dwarf::DW_OP_LLVM_label, 1,
-                 dwarf::DW_OP_LLVM_entry_value, 1);
+  // Validation stops at a trailing DW_OP_LLVM_fragment, so a control-flow
+  // problem before one still has to be caught.
+  EXPECT_INVALID(dwarf::DW_OP_LLVM_bra, 1, dwarf::DW_OP_LLVM_fragment, 0, 32);
+  EXPECT_INVALID(dwarf::DW_OP_LLVM_arg, 1, dwarf::DW_OP_LLVM_label, 1,
+                 dwarf::DW_OP_LLVM_fragment, 0, 32);
 
   // A valid operation doesn't make a malformed suffix valid.
   EXPECT_INVALID(dwarf::DW_OP_reg0, dwarf::DW_OP_stack_value,

>From f4f98c236f65ef7d99da7b5fa65d2d7bf8348b78 Mon Sep 17 00:00:00 2001
From: Eric Christopher <echristopher at nvidia.com>
Date: Thu, 13 Aug 2026 23:16:58 -0700
Subject: [PATCH 17/19] [DebugInfo] Pin what the symbolic branch tests claim to
 pin

The arg0 test stopped one token short of the branch displacement, which is the
only number a leading DW_OP_LLVM_arg 0 could have moved: label and branch
offsets are measured against a temporary buffer and patched afterwards. Pin it.
It comes out +2, and the leading arg turns out to be offset-neutral either way,
since the label and the placeholder shift together.

Two fixtures used an expression whose DW_OP_LLVM_bra pops the only value on the
stack, leaving nothing and no DW_OP_stack_value, so they described no location
at all. isValid doesn't track stack depth, which is why they passed. Use the
shape the arg0 test uses.

The salvage test's CHECK-NOT lines only covered the text above the first match,
and nothing said a debug value had ever existed, so it could pass by emitting
nothing. Move them to the RUN line and check for the kill the emitter really
produces.

The PowerPC comment claimed both buffers were checked for byte order. The
location list half is; the DIE half hands a .short to the assembler, which
orders it the same way on a little-endian target, so what it pins is the
patched value. The legacy run covered one variable of eight, and the other
seven cost nothing to add.
---
 .../PowerPC/di-expression-symbolic-branches.ll |  8 +++++---
 .../X86/di-expression-symbolic-branch-arg0.ll  |  6 ++++--
 ...-expression-symbolic-branch-sdag-salvage.ll | 16 ++++++++++------
 .../X86/di-expression-symbolic-branches.ll     | 18 ++++++++++++++++++
 llvm/test/DebugInfo/salvage-nonconst-binop.ll  |  5 +++--
 5 files changed, 40 insertions(+), 13 deletions(-)

diff --git a/llvm/test/DebugInfo/PowerPC/di-expression-symbolic-branches.ll b/llvm/test/DebugInfo/PowerPC/di-expression-symbolic-branches.ll
index 44159954127be..dea5ff80fe3c7 100644
--- a/llvm/test/DebugInfo/PowerPC/di-expression-symbolic-branches.ll
+++ b/llvm/test/DebugInfo/PowerPC/di-expression-symbolic-branches.ll
@@ -1,8 +1,10 @@
 ; RUN: llc -mtriple=powerpc64-unknown-linux-gnu -filetype=asm -o - %s | FileCheck %s
 
-; We patch branches in two temporary buffers, so make sure both use the PowerPC
-; byte order: a forward skip in a location list and a backward branch in an
-; inline expression.
+; We patch branches in two temporary buffers, and only one of them orders the
+; bytes itself. The location-list buffer writes raw bytes, so the forward skip
+; below pins the PowerPC byte order. The DIE buffer patches a DW_FORM_data2
+; value the assembler orders for us, so the backward branch only pins that the
+; patched displacement reached the DIE.
 
 define void @f(i64 %x) !dbg !5 {
 entry:
diff --git a/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-arg0.ll b/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-arg0.ll
index 07e6b79e24be3..ddb2208846075 100644
--- a/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-arg0.ll
+++ b/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-arg0.ll
@@ -3,7 +3,9 @@
 
 ; Instruction-reference lowering makes a single-location expression variadic
 ; by prepending DW_OP_LLVM_arg 0. This internal adapter remains valid with
-; symbolic control flow.
+; symbolic control flow, and the register bytes it emits must not move the
+; skip displacement, which spans only the DW_OP_plus_uconst between the skip
+; and its label.
 
 declare void @use(i64)
 
@@ -19,7 +21,7 @@ entry:
 
 ; MIR: DBG_INSTR_REF !{{[0-9]+}}, !DIExpression(DW_OP_LLVM_arg, 0, DW_OP_LLVM_skip, 1, DW_OP_plus_uconst, 1, DW_OP_LLVM_label, 1)
 
-; DWARF: DW_OP_breg5 RDI+0, DW_OP_skip
+; DWARF: DW_OP_breg5 RDI+0, DW_OP_skip +2, DW_OP_plus_uconst 0x1)
 ; DWARF: DW_AT_name {{.*}}"instr_ref"
 
 !llvm.dbg.cu = !{!0}
diff --git a/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-sdag-salvage.ll b/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-sdag-salvage.ll
index 4f3e955ad8fc3..03dd499095c37 100644
--- a/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-sdag-salvage.ll
+++ b/llvm/test/DebugInfo/X86/di-expression-symbolic-branch-sdag-salvage.ll
@@ -1,23 +1,27 @@
-; RUN: llc -mtriple=x86_64-unknown-linux-gnu -stop-before=finalize-isel -o - %s | FileCheck %s
+; RUN: llc -mtriple=x86_64-unknown-linux-gnu -stop-before=finalize-isel -o - %s \
+; RUN:   | FileCheck %s --implicit-check-not=DBG_INSTR_REF \
+; RUN:                  --implicit-check-not=DBG_VALUE_LIST
 
 ; SelectionDAG folds the nonconstant ADD into the load address. Salvaging the
 ; debug value would need a second location argument, which an expression with
-; symbolic control flow can't reference, so drop the location instead.
+; symbolic control flow can't reference, so drop the location instead. The
+; record still reaches CodeGen, so we get an undef DBG_VALUE rather than
+; nothing at all, and the earlier location can't leak past the fold.
 
 define i8 @sdag_salvage(i64 %base, i64 %index) !dbg !5 {
 entry:
   %sum = add i64 %base, %index
   #dbg_value(i64 %sum, !9,
-             !DIExpression(DW_OP_LLVM_bra, 1, DW_OP_LLVM_label, 1), !12)
+             !DIExpression(DW_OP_LLVM_skip, 1, DW_OP_plus_uconst, 1,
+                           DW_OP_LLVM_label, 1), !12)
   %address = inttoptr i64 %sum to ptr
   %value = load i8, ptr %address
   ret i8 %value
 }
 
 ; CHECK-LABEL: name: sdag_salvage
-; CHECK-NOT: DBG_INSTR_REF
-; CHECK-NOT: DBG_VALUE_LIST
-; CHECK: MOV8rm
+; CHECK-DAG: DBG_VALUE $noreg, $noreg, !{{[0-9]+}}, !DIExpression()
+; CHECK-DAG: MOV8rm
 
 !llvm.dbg.cu = !{!0}
 !llvm.module.flags = !{!3}
diff --git a/llvm/test/DebugInfo/X86/di-expression-symbolic-branches.ll b/llvm/test/DebugInfo/X86/di-expression-symbolic-branches.ll
index 163c3c24c6927..601ccc9aa8852 100644
--- a/llvm/test/DebugInfo/X86/di-expression-symbolic-branches.ll
+++ b/llvm/test/DebugInfo/X86/di-expression-symbolic-branches.ll
@@ -56,7 +56,25 @@ entry:
 ; CHECK: DW_AT_location [DW_FORM_exprloc] (DW_OP_lit0, DW_OP_plus_uconst 0x1, DW_OP_skip -5, DW_OP_stack_value)
 ; CHECK: DW_AT_name{{.*}}"label_before_offset"
 
+; Only the convert expansion changes under DWARF 4, but every displacement is
+; recomputed against the expanded bytes, so check them all.
+
+; LEGACY: DW_AT_location (DW_OP_lit0, DW_OP_bra +0, DW_OP_stack_value)
+; LEGACY: DW_AT_name{{.*}}"zero"
+; LEGACY: DW_AT_location (DW_OP_lit0, DW_OP_skip -3, DW_OP_stack_value)
+; LEGACY: DW_AT_name{{.*}}"backward"
+; LEGACY: DW_AT_location (DW_OP_lit0, DW_OP_bra +3, DW_OP_skip -6, DW_OP_stack_value)
+; LEGACY: DW_AT_name{{.*}}"cycle"
+; LEGACY: DW_AT_location (DW_OP_lit0, DW_OP_bra +11, DW_OP_deref_size 0x1, DW_OP_plus_uconst 0x80, DW_OP_constu 0x38, DW_OP_shl, DW_OP_constu 0x3c, DW_OP_shra, DW_OP_stack_value)
+; LEGACY: DW_AT_name{{.*}}"expanded"
 ; LEGACY: DW_AT_location (DW_OP_lit0, DW_OP_skip +11, DW_OP_dup, DW_OP_constu 0x1f, DW_OP_shr, DW_OP_lit0, DW_OP_not, DW_OP_mul, DW_OP_constu 0x20, DW_OP_shl, DW_OP_or, DW_OP_stack_value)
+; LEGACY: DW_AT_name{{.*}}"convert"
+; LEGACY: DW_AT_location (DW_OP_lit0, DW_OP_bra +0, DW_OP_stack_value, DW_OP_piece 0x4)
+; LEGACY: DW_AT_name{{.*}}"fragment"
+; LEGACY: DW_AT_location (DW_OP_lit0, DW_OP_plus_uconst 0x1, DW_OP_skip -3, DW_OP_stack_value)
+; LEGACY: DW_AT_name{{.*}}"label_after_offset"
+; LEGACY: DW_AT_location (DW_OP_lit0, DW_OP_plus_uconst 0x1, DW_OP_skip -5, DW_OP_stack_value)
+; LEGACY: DW_AT_name{{.*}}"label_before_offset"
 
 !llvm.dbg.cu = !{!0}
 !llvm.module.flags = !{!3, !4}
diff --git a/llvm/test/DebugInfo/salvage-nonconst-binop.ll b/llvm/test/DebugInfo/salvage-nonconst-binop.ll
index 8c996aa45c56b..7ce2273310f10 100644
--- a/llvm/test/DebugInfo/salvage-nonconst-binop.ll
+++ b/llvm/test/DebugInfo/salvage-nonconst-binop.ll
@@ -8,7 +8,7 @@
 ; CHECK-SAME: ![[VAR_C:[0-9]+]],
 ; CHECK-SAME: !DIExpression(DW_OP_LLVM_arg, 0, DW_OP_LLVM_arg, 1, DW_OP_plus, DW_OP_stack_value),
 ; CHECK: #dbg_value(i32 poison, ![[VAR_CF:[0-9]+]],
-; CHECK-SAME: !DIExpression(DW_OP_LLVM_bra, 1, DW_OP_LLVM_label, 1),
+; CHECK-SAME: !DIExpression(DW_OP_LLVM_skip, 1, DW_OP_plus_uconst, 1, DW_OP_LLVM_label, 1),
 
 ; CHECK: ![[VAR_C]] = !DILocalVariable(name: "c"
 ; CHECK: ![[VAR_CF]] = !DILocalVariable(name: "cf"
@@ -22,7 +22,8 @@ entry:
   %add.cf = add nsw i32 %a, %b, !dbg !15
   call void @llvm.dbg.value(
       metadata i32 %add.cf, metadata !18,
-      metadata !DIExpression(DW_OP_LLVM_bra, 1, DW_OP_LLVM_label, 1)), !dbg !13
+      metadata !DIExpression(DW_OP_LLVM_skip, 1, DW_OP_plus_uconst, 1,
+                             DW_OP_LLVM_label, 1)), !dbg !13
   %mul = mul nsw i32 %a, %b, !dbg !17
   ret i32 %mul, !dbg !17
 }

>From a5bd87d9c4c38f8df8b4aa128b77e671930113de Mon Sep 17 00:00:00 2001
From: Eric Christopher <echristopher at nvidia.com>
Date: Thu, 13 Aug 2026 23:16:58 -0700
Subject: [PATCH 18/19] [DebugInfo] Fix the symbolic branch docs and give
 LangRef an example

A record carrying a DIArgList is rejected, and the verifier is what rejects it.
A leading DW_OP_LLVM_arg 0 is valid, because CodeGen prepends one when it makes
a single-location expression variadic. The text said both were rejected, which
stopped being true when the expression rule was relaxed.

DW_OP_LLVM_bra pops the value it tests. Every other stack-consuming operation
in that list says so, and isValid doesn't model the stack, so nothing catches a
producer that reads this the other way.

A label between a deferred conversion pair only fails the compilation when the
expression also branches, since a label on its own emits nothing.

Local rewrites leave labels alone because they match runs of adjacent
operations and a label breaks the run. That was stated as a guarantee with no
mechanism behind it, which tells a contributor adding a peephole nothing.

LangRef had the encodings but no expression to look at, so add the one from the
Assembler test, which shows a forward reference, a backward one, and that the
IDs don't have to be sequential. Its rule list read as complete and left out
the DIArgList restriction too, which isn't in expression validation at all.

The release note spent two thirds of itself on the deferred-conversion corner
case, which needs a target that can't emit DW_OP_convert, and didn't mention
either thing a producer actually runs into.
---
 llvm/docs/LangRef.md              |  8 ++++++--
 llvm/docs/ReleaseNotes.md         |  6 +++---
 llvm/docs/SourceLevelDebugging.md | 31 ++++++++++++++++++++-----------
 3 files changed, 29 insertions(+), 16 deletions(-)

diff --git a/llvm/docs/LangRef.md b/llvm/docs/LangRef.md
index 9a8b3eac986ef..504adeea3314d 100644
--- a/llvm/docs/LangRef.md
+++ b/llvm/docs/LangRef.md
@@ -7213,6 +7213,7 @@ Some examples of expressions:
 !DIExpression(DW_OP_deref, DW_OP_constu, 3, DW_OP_plus, DW_OP_LLVM_fragment, 3, 7)
 !DIExpression(DW_OP_constu, 2, DW_OP_swap, DW_OP_xderef)
 !DIExpression(DW_OP_constu, 42, DW_OP_stack_value)
+!DIExpression(DW_OP_LLVM_label, 0, DW_OP_LLVM_bra, 42, DW_OP_LLVM_skip, 0, DW_OP_LLVM_label, 42)
 ```
 
 `DIExpression` uses three pseudo-ops for local control flow:
@@ -7231,8 +7232,11 @@ operation and byte offset. `DW_OP_LLVM_label` declares the target for a label
 ID and emits no bytes.
 
 Label IDs are local to the expression. Every branch needs a matching label,
-labels without branches are valid, and each ID can only be declared once. The
-standard `DW_OP_bra` and `DW_OP_skip` operations aren't valid in LLVM IR. See
+labels without branches are valid, and each ID can only be declared once.
+Branches can go forward or backward, and the IDs don't need to be sequential.
+The standard `DW_OP_bra` and `DW_OP_skip` operations aren't valid in LLVM IR.
+A debug record carrying a `DIArgList` can't use the LLVM forms either; that
+combination is rejected by the verifier, not by expression validation. See
 [the DWARF standard](https://dwarfstd.org/) for the standard operations and
 {ref}`symbolic control flow <symbolic-control-flow>` for the LLVM-specific
 rules.
diff --git a/llvm/docs/ReleaseNotes.md b/llvm/docs/ReleaseNotes.md
index 7c216a243752c..ed51e5b7c7153 100644
--- a/llvm/docs/ReleaseNotes.md
+++ b/llvm/docs/ReleaseNotes.md
@@ -133,9 +133,9 @@ Makes programs 10x faster by doing Special New Thing.
 
 * Added `DW_OP_LLVM_label`, `DW_OP_LLVM_bra`, and `DW_OP_LLVM_skip` for symbolic
   branches in `DIExpression`. These operations use label IDs that CodeGen
-  resolves when it emits the expression. `DW_OP_LLVM_convert` can appear before
-  or after them, but when CodeGen can't emit `DW_OP_convert`, it reports an
-  error if a deferred conversion reaches a label, branch, or skip.
+  resolves to byte offsets when it emits the expression. A debug record carrying
+  a `DIArgList` can't use them, and the compilation fails if a resolved branch
+  offset lands outside `[-32768, 32767]`.
 
 ### Changes to the LLVM tools
 
diff --git a/llvm/docs/SourceLevelDebugging.md b/llvm/docs/SourceLevelDebugging.md
index a202bcf3c6471..9fc46a02f707d 100644
--- a/llvm/docs/SourceLevelDebugging.md
+++ b/llvm/docs/SourceLevelDebugging.md
@@ -479,8 +479,8 @@ so we use three pseudo-ops with label IDs that CodeGen resolves during
 emission:
 
 - `DW_OP_LLVM_label, ID` marks a destination and emits no bytes.
-- `DW_OP_LLVM_bra, ID` branches to label `ID` when the value on top of the
-  expression stack is non-zero.
+- `DW_OP_LLVM_bra, ID` pops the top value of the expression stack and branches
+  to label `ID` when that value is non-zero.
 - `DW_OP_LLVM_skip, ID` always branches to label `ID`.
 
 Label IDs are local to an expression:
@@ -500,10 +500,13 @@ Label IDs are local to an expression:
 
 There are also a couple of cases we don't handle yet:
 
-- `DIArgList` and `DW_OP_LLVM_arg` are currently rejected. CodeGen expands each
-  argument before it resolves the label offsets, so there isn't a representation
-  problem here; it mostly needs work handling it in expressions and expression
-  writers.
+- A debug record carrying a `DIArgList` is rejected. The verifier reports
+  `DIArgList doesn't support symbolic branches` when such a record's expression
+  has a label, branch, or skip. A leading `DW_OP_LLVM_arg, 0` is still valid,
+  because CodeGen prepends it when it makes a single-location expression
+  variadic; any other `DW_OP_LLVM_arg` is not. CodeGen expands each argument
+  before it resolves the label offsets, so there isn't a representation problem
+  here; it mostly needs work handling it in expressions and expression writers.
 - `DW_OP_LLVM_implicit_pointer` bypasses normal expression emission and only
   handles a single location today. Supporting branches there is a bit more
   work, since we'll need to work it back into our normal emission order.
@@ -512,14 +515,20 @@ We don't check reachability, termination, or stack state where paths meet.
 
 `DW_OP_LLVM_convert` can appear before or after labels, branches, and skips,
 and we don't match conversions on different paths. When CodeGen can't emit
-`DW_OP_convert`, it may defer one conversion until it sees the next; if a label,
-branch, or skip would split the pair, CodeGen reports an error.
+`DW_OP_convert`, it may defer one conversion until it sees the next, and the
+compilation fails if a branch, a skip, or a label in an expression that branches
+comes between the pair. A label emits nothing, so a label on its own can't
+separate the pair: an expression with no branch or skip emits normally, losing
+the unpaired conversion the same way a lone `DW_OP_LLVM_convert` is lost.
 
-CodeGen also reports an error if the final branch offset is outside
+The compilation also fails if the final branch offset is outside
 `[-32768, 32767]`.
 
-Local expression rewrites stop at labels, branches, and skips; they can still
-add operations to either end, but they don't move, remove, or copy labels.
+No rewrite path looks for labels, branches, or skips. Local rewrites leave them
+alone because they match runs of adjacent operations, and a label, branch, or
+skip in the middle of a run stops the match. Rewrites can still add operations
+to either end. A new rewrite has to keep that property, since moving, removing,
+or copying a label changes which operations a branch reaches.
 
 ##### Other Internal Opcodes
 

>From baae9f630324ad2eeaf05267266ea1bd1af015c7 Mon Sep 17 00:00:00 2001
From: Eric Christopher <echristopher at nvidia.com>
Date: Thu, 13 Aug 2026 23:16:59 -0700
Subject: [PATCH 19/19] [DebugInfo] Drop ExprOperand::isOneOf and our copy of
 is

ExprOperand::is arrived upstream in #215682, so this series defines it a second
time. Keep the upstream one.

That leaves isOneOf with three call sites, each naming two or three opcodes.
Spell them as is() chains and drop the helper.
---
 llvm/include/llvm/IR/DebugInfoMetadata.h        | 9 ---------
 llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp | 2 +-
 llvm/lib/IR/DebugInfoMetadata.cpp               | 6 +++---
 3 files changed, 4 insertions(+), 13 deletions(-)

diff --git a/llvm/include/llvm/IR/DebugInfoMetadata.h b/llvm/include/llvm/IR/DebugInfoMetadata.h
index 57b02a44f3af8..e0aca422c9a5d 100644
--- a/llvm/include/llvm/IR/DebugInfoMetadata.h
+++ b/llvm/include/llvm/IR/DebugInfoMetadata.h
@@ -3550,15 +3550,6 @@ class DIExpression : public MDNode {
     /// Return true if this is \p Opcode.
     bool is(uint64_t Opcode) const { return getOp() == Opcode; }
 
-    /// Return true if this is \p Opcode.
-    bool is(uint64_t Opcode) const { return getOp() == Opcode; }
-
-    /// Return true if this is one of \p Opcodes.
-    template <typename... Ts> bool isOneOf(Ts... Opcodes) const {
-      static_assert(sizeof...(Ts) > 0, "requires at least one opcode");
-      return (is(Opcodes) || ...);
-    }
-
     /// Get an argument to the operand.
     ///
     /// Never returns the operand itself. The operand has to be present and \p I
diff --git a/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp b/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp
index 617c8a185e499..e5f64c370fc84 100644
--- a/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp
+++ b/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp
@@ -579,7 +579,7 @@ bool DwarfExpression::addExpression(
   // Iterating over ExprCursor doesn't consume it.
   bool HasSymbolicBranches =
       llvm::any_of(ExprCursor, [](DIExpression::ExprOperand Op) {
-        return Op.isOneOf(dwarf::DW_OP_LLVM_bra, dwarf::DW_OP_LLVM_skip);
+        return Op.is(dwarf::DW_OP_LLVM_bra) || Op.is(dwarf::DW_OP_LLVM_skip);
       });
 
   SmallVector<LabelOffset, 4> Labels;
diff --git a/llvm/lib/IR/DebugInfoMetadata.cpp b/llvm/lib/IR/DebugInfoMetadata.cpp
index a8409576b846f..73a9a2aebbe6d 100644
--- a/llvm/lib/IR/DebugInfoMetadata.cpp
+++ b/llvm/lib/IR/DebugInfoMetadata.cpp
@@ -1762,12 +1762,12 @@ unsigned DIExpression::ExprOperand::getSize() const {
 }
 
 bool DIExpression::ExprOperand::isNonEmitting() const {
-  return isOneOf(dwarf::DW_OP_LLVM_tag_offset, dwarf::DW_OP_LLVM_label);
+  return is(dwarf::DW_OP_LLVM_tag_offset) || is(dwarf::DW_OP_LLVM_label);
 }
 
 bool DIExpression::ExprOperand::isSymbolicControlFlow() const {
-  return isOneOf(dwarf::DW_OP_LLVM_label, dwarf::DW_OP_LLVM_bra,
-                 dwarf::DW_OP_LLVM_skip);
+  return is(dwarf::DW_OP_LLVM_label) || is(dwarf::DW_OP_LLVM_bra) ||
+         is(dwarf::DW_OP_LLVM_skip);
 }
 
 bool DIExpression::ArgOp::classof(const ExprOperand *Op) {



More information about the Mlir-commits mailing list