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

Eric Christopher via lldb-commits lldb-commits at lists.llvm.org
Thu Jul 23 22:58:37 PDT 2026


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

>From ab4cecf7ddb25e85c1a7a8bbfc38743dc2125ac2 Mon Sep 17 00:00:00 2001
From: Eric Christopher <echristopher at nvidia.com>
Date: Wed, 15 Jul 2026 08:51:28 -0700
Subject: [PATCH 1/7] [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. We reject expressions
that combine it with DW_OP_LLVM_arg or DW_OP_LLVM_implicit_pointer, 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             | 103 ++++++++++--
 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, 1054 insertions(+), 35 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 a6b3146cf6749..05162290f330a 100644
--- a/llvm/docs/LangRef.md
+++ b/llvm/docs/LangRef.md
@@ -7159,6 +7159,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 94208f8e36d19..988740a7e5e39 100644
--- a/llvm/docs/ReleaseNotes.md
+++ b/llvm/docs/ReleaseNotes.md
@@ -109,6 +109,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
 
 ### Changes to LLDB
diff --git a/llvm/docs/SourceLevelDebugging.md b/llvm/docs/SourceLevelDebugging.md
index 74bb8840a56e4..645e441e6fe2a 100644
--- a/llvm/docs/SourceLevelDebugging.md
+++ b/llvm/docs/SourceLevelDebugging.md
@@ -386,10 +386,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`.
@@ -470,6 +469,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
@@ -482,11 +525,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 75eb3a163e06f..c3cff08eccb5c 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 9ede7ebbe20ad..431d01c953768 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 cfbb35d516534..45f1f13e999fc 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 9b676f88a0cff..4054817abb95d 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;
@@ -530,8 +533,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::ExprOperand> 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::ExprOperand> PrevConvertOp;
   while (ExprCursor) {
     auto Op = ExprCursor.take();
     uint64_t OpNum = Op->getOp();
@@ -545,9 +571,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(Op->getArg(0), ExprCursor)) {
         LocationKind = Unknown;
+        if (HasSymbolicBranches)
+          disableTemporaryBuffer();
         return false;
       }
       break;
@@ -579,7 +623,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: {
@@ -744,9 +791,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");
@@ -757,6 +807,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 2525938c78d86..f5c37140a1ef9 100644
--- a/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.h
+++ b/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.h
@@ -130,7 +130,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;
 
@@ -150,6 +154,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);
 
@@ -285,10 +298,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(
@@ -328,13 +345,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;
@@ -358,13 +377,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 78c0769e49161..c1c1218a87974 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 88f7f2f6240e0..518e4c130e34c 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:
@@ -1758,12 +1762,81 @@ unsigned DIExpression::ExprOperand::getSize() const {
 }
 
 bool DIExpression::isValid() const {
+  auto IsEntryValueValid = [this](const ExprOperand &EntryValue) {
+    auto FirstOp = expr_op_begin();
+    if (FirstOp->getOp() == dwarf::DW_OP_LLVM_arg && FirstOp->getArg(0) == 0)
+      ++FirstOp;
+    return EntryValue.get() == FirstOp->get() && EntryValue.getArg(0) == 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))
       return true;
@@ -1784,7 +1857,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
@@ -1795,22 +1868,20 @@ 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 (FirstOp->getOp() == dwarf::DW_OP_LLVM_arg && FirstOp->getArg(0) == 0)
-        ++FirstOp;
-      return I->get() == FirstOp->get() && I->getArg(0) == 1;
-    }
+      return IsEntryValueValid(*I);
     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:
@@ -2327,13 +2398,15 @@ DIExpression *DIExpression::appendToStack(const DIExpression *Expr,
   // has no DW_OP_stack_value.
   //
   // Match .* DW_OP_stack_value (DW_OP_LLVM_fragment A B)?.
-  std::optional<FragmentInfo> FI = Expr->getFragmentInfo();
-  unsigned DropUntilStackValue = FI ? 3 : 0;
-  ArrayRef<uint64_t> ExprOpsBeforeFragment =
-      Expr->getElements().drop_back(DropUntilStackValue);
-  bool NeedsDeref = (Expr->getNumElements() > DropUntilStackValue) &&
-                    (ExprOpsBeforeFragment.back() != dwarf::DW_OP_stack_value);
-  bool NeedsStackValue = NeedsDeref || ExprOpsBeforeFragment.empty();
+  std::optional<uint64_t> LastOp;
+  // A raw operand can have the same value as an opcode, so use expr_ops().
+  // Labels and branches can't follow DW_OP_stack_value, so LastOp is still
+  // DW_OP_stack_value when the expression has a value suffix.
+  for (auto Op : Expr->expr_ops())
+    if (Op.getOp() != dwarf::DW_OP_LLVM_fragment)
+      LastOp = Op.getOp();
+  bool NeedsDeref = LastOp && *LastOp != dwarf::DW_OP_stack_value;
+  bool NeedsStackValue = NeedsDeref || !LastOp;
 
   // Append a DW_OP_deref after Expr's current op list if needed, then append
   // the new ops, and finally ensure that a single DW_OP_stack_value is present.
diff --git a/llvm/lib/IR/Verifier.cpp b/llvm/lib/IR/Verifier.cpp
index b6ee0170904e8..be14be6a43656 100644
--- a/llvm/lib/IR/Verifier.cpp
+++ b/llvm/lib/IR/Verifier.cpp
@@ -7182,6 +7182,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 ba7d59182ea53..6ffb5a921510c 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 65398b5ca2b1f..9be5384f9647b 100644
--- a/llvm/unittests/IR/MetadataTest.cpp
+++ b/llvm/unittests/IR/MetadataTest.cpp
@@ -4251,6 +4251,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 {                                                                         \
@@ -4271,6 +4351,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);
@@ -4279,12 +4360,31 @@ TEST_F(DIExpressionTest, isValid) {
                dwarf::DW_OP_LLVM_fragment, 3, 7);
   EXPECT_VALID(dwarf::DW_OP_LLVM_entry_value, 1);
   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);
@@ -4294,6 +4394,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);
 
 #undef EXPECT_VALID
 #undef EXPECT_INVALID
@@ -4371,6 +4513,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 57cf90ad67b0e05c411e79518212f90e3faa39bc Mon Sep 17 00:00:00 2001
From: Eric Christopher <echristopher at nvidia.com>
Date: Tue, 21 Jul 2026 12:53:45 -0700
Subject: [PATCH 2/7] [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 299dbe97d6176..73bdd18d82cae 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 e1e1ba8492cb9a32cb924caadb78ff4129cc9581 Mon Sep 17 00:00:00 2001
From: Eric Christopher <echristopher at nvidia.com>
Date: Tue, 21 Jul 2026 12:58:04 -0700
Subject: [PATCH 3/7] [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 645e441e6fe2a..834781444c607 100644
--- a/llvm/docs/SourceLevelDebugging.md
+++ b/llvm/docs/SourceLevelDebugging.md
@@ -492,15 +492,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,
@@ -508,6 +516,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 7d3fcc07c09bcf1d8150f79394720988d8810aae Mon Sep 17 00:00:00 2001
From: Eric Christopher <echristopher at nvidia.com>
Date: Tue, 21 Jul 2026 14:14:38 -0700
Subject: [PATCH 4/7] [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 c3cff08eccb5c..177c50be7b374 100644
--- a/llvm/include/llvm/BinaryFormat/Dwarf.h
+++ b/llvm/include/llvm/BinaryFormat/Dwarf.h
@@ -1108,6 +1108,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 4054817abb95d..f90edb1e68157 100644
--- a/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp
+++ b/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp
@@ -546,8 +546,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;
@@ -816,6 +815,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 c1c1218a87974..ea2a6ddca3c61 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 be14be6a43656..0dc59163fa864 100644
--- a/llvm/lib/IR/Verifier.cpp
+++ b/llvm/lib/IR/Verifier.cpp
@@ -7184,15 +7184,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 c173ccf8a312d19de4b574813c9df6f2cda66671 Mon Sep 17 00:00:00 2001
From: Eric Christopher <echristopher at nvidia.com>
Date: Tue, 21 Jul 2026 18:17:17 -0700
Subject: [PATCH 5/7] [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 177c50be7b374..c3cff08eccb5c 100644
--- a/llvm/include/llvm/BinaryFormat/Dwarf.h
+++ b/llvm/include/llvm/BinaryFormat/Dwarf.h
@@ -1108,16 +1108,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 846342bee6071..2d0d6a1e228cb 100644
--- a/llvm/include/llvm/IR/DebugInfoMetadata.h
+++ b/llvm/include/llvm/IR/DebugInfoMetadata.h
@@ -3539,6 +3539,15 @@ class DIExpression : public MDNode {
     /// Get the operand code.
     uint64_t getOp() const { return *Op; }
 
+    /// 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.
diff --git a/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp b/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp
index f90edb1e68157..6bf604aae814c 100644
--- a/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp
+++ b/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp
@@ -546,7 +546,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;
@@ -577,12 +577,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 0dc59163fa864..48e7f88d9c07a 100644
--- a/llvm/lib/IR/Verifier.cpp
+++ b/llvm/lib/IR/Verifier.cpp
@@ -7188,7 +7188,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 cc43f3efed3c84cc291f8483c7b1b952e151bbe7 Mon Sep 17 00:00:00 2001
From: Eric Christopher <echristopher at nvidia.com>
Date: Thu, 23 Jul 2026 18:33:53 -0700
Subject: [PATCH 6/7] [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 6bf604aae814c..56dd5ff03ebf4 100644
--- a/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp
+++ b/llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp
@@ -806,31 +806,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 109bc0e91e481290ad541872ccced007aa155a91 Mon Sep 17 00:00:00 2001
From: Eric Christopher <echristopher at nvidia.com>
Date: Thu, 23 Jul 2026 21:48:42 -0700
Subject: [PATCH 7/7] 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 a95644a1cc006..776e6267c7c24 100644
--- a/llvm/docs/LangRef.md
+++ b/llvm/docs/LangRef.md
@@ -7182,13 +7182,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
 



More information about the lldb-commits mailing list