[llvm] [DSE] Stop double-counting the dead slice offset in shortenAssignment (PR #216227)

Eric Christopher via llvm-commits llvm-commits at lists.llvm.org
Sun Aug 16 23:22:47 PDT 2026


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

>From 093170643c2482467a12997c50274cdcf6dc8078 Mon Sep 17 00:00:00 2001
From: Eric Christopher <echristopher at nvidia.com>
Date: Thu, 13 Aug 2026 17:48:20 -0700
Subject: [PATCH 1/3] [DSE] Stop double-counting the dead slice offset in
 shortenAssignment

A dead store that starts somewhere other than its base object marked the wrong
bits of the variable dead, so a debugger read back stale values for bits the
shortened store no longer covers. shortenAssignment hands
calculateFragmentIntersect the dead store's dest pointer and DeadStart * 8 as
the slice offset, but DeadStart is that pointer's own offset from the base and
calculateFragmentIntersect already measures the pointer against the
dbg.assign's address, so the same distance lands in the result twice.

The other callers hold a base pointer and a separate offset into it: SROA
passes the alloca with the slice's offset, and AssignmentTrackingAnalysis
passes the assignment's base with its offset. DSE already holds the
materialized dest, so its offset is zero, or the surviving size when the tail
is what died. Drop the parameter since nothing else in shortenAssignment used
it.

The bad offset produces three shapes and there's a test for each now: in
shortenEnd the slice is clipped to the wrong part of the fragment, in
shortenEndPartial it's pushed off the end of the variable, and in
shortenBeginPartial it keeps its size and moves. The first two are in
shorten-offset.ll, and shortenBeginPartial is new in shorten-offset-begin.ll
with IR taken from clang rather than adjusted by hand. Every other
assignment-tracking DSE test has DeadStart == 0, so nothing covered this. The
pseudo-source header in shorten-offset.ll describes different memsets from the
ones in the IR, so correct that too.

No regressions on check-llvm or check-lldb; unfortunately none of the nine
-ggdb/-O2 tests exercise this, so I read the values back in lldb directly.
---
 .../Scalar/DeadStoreElimination.cpp           | 16 ++--
 .../dse/shorten-offset-begin.ll               | 96 +++++++++++++++++++
 .../assignment-tracking/dse/shorten-offset.ll | 80 ++++++++++++++--
 3 files changed, 177 insertions(+), 15 deletions(-)
 create mode 100644 llvm/test/DebugInfo/Generic/assignment-tracking/dse/shorten-offset-begin.ll

diff --git a/llvm/lib/Transforms/Scalar/DeadStoreElimination.cpp b/llvm/lib/Transforms/Scalar/DeadStoreElimination.cpp
index 3e9857c1947e7..f59131df035ff 100644
--- a/llvm/lib/Transforms/Scalar/DeadStoreElimination.cpp
+++ b/llvm/lib/Transforms/Scalar/DeadStoreElimination.cpp
@@ -513,12 +513,17 @@ memoryIsNotModifiedBetween(Instruction *FirstI, Instruction *SecondI,
 }
 
 static void shortenAssignment(Instruction *Inst, Value *OriginalDest,
-                              uint64_t OldOffsetInBits, uint64_t OldSizeInBits,
-                              uint64_t NewSizeInBits, bool IsOverwriteEnd) {
+                              uint64_t OldSizeInBits, uint64_t NewSizeInBits,
+                              bool IsOverwriteEnd) {
   const DataLayout &DL = Inst->getDataLayout();
   uint64_t DeadSliceSizeInBits = OldSizeInBits - NewSizeInBits;
-  uint64_t DeadSliceOffsetInBits =
-      OldOffsetInBits + (IsOverwriteEnd ? NewSizeInBits : 0);
+  // The dead slice offset is relative to OriginalDest, the slice start we hand
+  // calculateFragmentIntersect. Shortening the end keeps the front of the
+  // store, so the dead bits start where the new store ends; shortening the
+  // beginning kills the bits at OriginalDest itself. Don't add OriginalDest's
+  // offset from its base object here. calculateFragmentIntersect already
+  // measures that pointer against the marker's address.
+  uint64_t DeadSliceOffsetInBits = IsOverwriteEnd ? NewSizeInBits : 0;
   auto SetDeadFragExpr = [](auto *Assign,
                             DIExpression::FragmentInfo DeadFragment) {
     // createFragmentExpression expects an offset relative to the existing
@@ -702,8 +707,7 @@ static bool tryToShorten(Instruction *DeadI, int64_t &DeadStart,
   }
 
   // Update attached dbg.assign intrinsics. Assume 8-bit byte.
-  shortenAssignment(DeadI, OrigDest, DeadStart * 8, DeadSize * 8, NewSize * 8,
-                    IsOverwriteEnd);
+  shortenAssignment(DeadI, OrigDest, DeadSize * 8, NewSize * 8, IsOverwriteEnd);
 
   // Finally update start and size of dead access.
   if (!IsOverwriteEnd)
diff --git a/llvm/test/DebugInfo/Generic/assignment-tracking/dse/shorten-offset-begin.ll b/llvm/test/DebugInfo/Generic/assignment-tracking/dse/shorten-offset-begin.ll
new file mode 100644
index 0000000000000..6de28612ef498
--- /dev/null
+++ b/llvm/test/DebugInfo/Generic/assignment-tracking/dse/shorten-offset-begin.ll
@@ -0,0 +1,96 @@
+; RUN: opt %s -S -passes=dse -o - | FileCheck %s --implicit-check-not="#dbg_"
+
+;; The IR is clang's, cleaned up but not otherwise adjusted, so the variable
+;; fills its alloca and the offsets are the ones clang emitted. That gives up
+;; the coverage shorten-offset.ll buys by adjusting them, and gets a source
+;; listing whose numbers can be checked against the test.
+;;
+;; $ cat shorten.c
+;; void esc(char *);
+;; void shortenBeginPartial(void) {
+;;   char local[80];
+;;   __builtin_memset(local + 8, 0, 72);
+;;   __builtin_memset(local + 4, 8, 64);
+;;   esc(local);
+;; }
+;;
+;; $ clang -O2 -g -c -Xclang -fexperimental-assignment-tracking=forced \
+;;       -mllvm -print-before=dse -mllvm -print-module-scope shorten.c \
+;;       -o /dev/null
+;;
+;; and then, by hand: dropped the target triple and datalayout so this runs
+;; anywhere, dropped the tbaa metadata, llvm.ident, the producer/checksum/
+;; sysroot strings, the key-instruction atoms, the lifetime intrinsics and the
+;; function attributes, converted the debug records to intrinsics, and
+;; renumbered the metadata. The remaining non-debug instructions keep clang's
+;; order and operands, and the assignment offsets are unchanged.
+
+;; 'local' is 640 bits and starts at the alloca, so variable bit N is alloca
+;; byte N/8 throughout, which is what makes the arithmetic readable.
+;;
+;; The first memset covers bytes [8, 80) and the second covers [4, 68), so the
+;; second kills the front of the first and DSE shortens it to [68, 80). The
+;; bytes that died are [8, 68), which is variable bits [64, 544): fragment
+;; (64, 480).
+;;
+;; The dead slice stays inside the fragment, so the record keeps its size and
+;; only the offset can be wrong, and an offset that is off by the store's
+;; distance from the alloca still describes 60 plausible bytes. shorten-offset.ll
+;; covers the two shapes where the slice moves far enough to clip the fragment
+;; or to leave the variable entirely.
+
+; CHECK: @shortenBeginPartial
+; CHECK:      #dbg_assign({{.*}}, ![[VAR:[0-9]+]], !DIExpression(), {{.*}}, ptr %local, !DIExpression(),
+; CHECK:      call void @llvm.memset{{.*}}, !DIAssignID ![[ID:[0-9]+]]
+; CHECK-NEXT: #dbg_assign(i8 0, ![[VAR]], !DIExpression(DW_OP_LLVM_fragment, 64, 576), ![[ID]], ptr %add.ptr, !DIExpression(),
+; CHECK-NEXT: #dbg_assign(i8 0, ![[VAR]], !DIExpression(DW_OP_LLVM_fragment, 64, 480), ![[UniqueID:[0-9]+]], ptr poison, !DIExpression(),
+; CHECK:      call void @llvm.memset{{.*}}, !DIAssignID ![[ID2:[0-9]+]]
+; CHECK-NEXT: #dbg_assign(i1 poison, ![[VAR]], !DIExpression(DW_OP_LLVM_fragment, 32, 512), ![[ID2]], ptr %add.ptr2, !DIExpression(),
+
+; CHECK-DAG: ![[ID]] = distinct !DIAssignID()
+; CHECK-DAG: ![[ID2]] = distinct !DIAssignID()
+; CHECK-DAG: ![[UniqueID]] = distinct !DIAssignID()
+
+define void @shortenBeginPartial() !dbg !7 {
+entry:
+  %local = alloca [80 x i8], align 1, !DIAssignID !13
+  call void @llvm.dbg.assign(metadata i1 poison, metadata !11, metadata !DIExpression(), metadata !13, metadata ptr %local, metadata !DIExpression()), !dbg !14
+  %add.ptr = getelementptr inbounds nuw i8, ptr %local, i64 8, !dbg !15
+  call void @llvm.memset.p0.i64(ptr noundef nonnull align 1 dereferenceable(72) %add.ptr, i8 0, i64 72, i1 false), !dbg !15, !DIAssignID !16
+  call void @llvm.dbg.assign(metadata i8 0, metadata !11, metadata !DIExpression(DW_OP_LLVM_fragment, 64, 576), metadata !16, metadata ptr %add.ptr, metadata !DIExpression()), !dbg !14
+  %add.ptr2 = getelementptr inbounds nuw i8, ptr %local, i64 4, !dbg !17
+  call void @llvm.memset.p0.i64(ptr noundef nonnull align 1 dereferenceable(64) %add.ptr2, i8 8, i64 64, i1 false), !dbg !17, !DIAssignID !18
+  call void @llvm.dbg.assign(metadata i1 poison, metadata !11, metadata !DIExpression(DW_OP_LLVM_fragment, 32, 512), metadata !18, metadata ptr %add.ptr2, metadata !DIExpression()), !dbg !14
+  call void @esc(ptr noundef nonnull %local), !dbg !19
+  ret void, !dbg !20
+}
+
+declare void @llvm.memset.p0.i64(ptr nocapture writeonly, i8, i64, i1 immarg)
+declare void @llvm.dbg.assign(metadata, metadata, metadata, metadata, metadata, metadata)
+declare void @esc(ptr noundef)
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3, !21}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C11, file: !1, isOptimized: true, runtimeVersion: 0, emissionKind: FullDebug, splitDebugInlining: false)
+!1 = !DIFile(filename: "shorten.c", directory: "/")
+!2 = !{i32 7, !"Dwarf Version", i32 5}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!7 = distinct !DISubprogram(name: "shortenBeginPartial", scope: !1, file: !1, line: 2, type: !8, scopeLine: 2, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !10)
+!8 = !DISubroutineType(types: !9)
+!9 = !{null}
+!10 = !{!11}
+!11 = !DILocalVariable(name: "local", scope: !7, file: !1, line: 3, type: !12)
+!12 = !DICompositeType(tag: DW_TAG_array_type, baseType: !4, size: 640, elements: !5)
+!4 = !DIBasicType(name: "char", size: 8, encoding: DW_ATE_signed_char)
+!5 = !{!6}
+!6 = !DISubrange(count: 80)
+!13 = distinct !DIAssignID()
+!14 = !DILocation(line: 0, scope: !7)
+!15 = !DILocation(line: 4, column: 3, scope: !7)
+!16 = distinct !DIAssignID()
+!17 = !DILocation(line: 5, column: 3, scope: !7)
+!18 = distinct !DIAssignID()
+!19 = !DILocation(line: 6, column: 3, scope: !7)
+!20 = !DILocation(line: 7, column: 1, scope: !7)
+!21 = !{i32 7, !"debug-info-assignment-tracking", i1 true}
diff --git a/llvm/test/DebugInfo/Generic/assignment-tracking/dse/shorten-offset.ll b/llvm/test/DebugInfo/Generic/assignment-tracking/dse/shorten-offset.ll
index 0f6be3916a2de..c83088bd99d84 100644
--- a/llvm/test/DebugInfo/Generic/assignment-tracking/dse/shorten-offset.ll
+++ b/llvm/test/DebugInfo/Generic/assignment-tracking/dse/shorten-offset.ll
@@ -6,8 +6,8 @@
 ;; void esc(char*);
 ;; void shortenEnd() {
 ;;   char local[80];                      //        bits    frag
-;;   __builtin_memset(local + 8,  0, 24); // local:  64-160 ( 64, 96)
-;;   __builtin_memset(local + 16, 8, 40); // local: 128-160 (128, 32)
+;;   __builtin_memset(local + 8, 0, 72);  // local:  64-160 ( 64, 96)
+;;   __builtin_memset(local + 4, 8, 64);  // local:  32-160 killed
 ;;   esc(local);
 ;; }
 ;; void shortenStart() {
@@ -16,22 +16,37 @@
 ;;   __builtin_memset(local2, 8, 16); // local2:  0-128  (0, 128)
 ;;   esc(local2);
 ;; }
+;; void shortenEndPartial() {
+;;   char local3[80];                      //         bits    frag
+;;   __builtin_memset(local3 + 8,  0, 8);  // local3:  64-128 (64, 96)
+;;   __builtin_memset(local3 + 12, 8, 4);  // local3:  96-128 ( 96, 32)
+;;   esc(local3);
+;; }
 
 ;; The variables and intrinsics have been adjusted with by hand to test
 ;; what happens when the variable doesn't fill the whole alloca, and
 ;; when offsets are encoded with both the address component of the dbg.assign
 ;; and the address modifying DIExpression.
 
-;; DeadStoreElimination will shorten the first store in shortenEnd from [64,
-;; 192) bits to [64, 128) bits. Variable 'local' has been adjusted to be 160
-;; bits large. Check that we get an unlinked dbg.assign covering the deleted
-;; bits that overlap the dbg.assign's fagment: [128, 160) (offset=128 size=32).
+;; 'local' and 'local3' have both been adjusted to be 160 bits, so the bit
+;; ranges above are the written bytes clamped to the variable.
+
+;; In shortenEnd the killing store starts before the dead one, at local + 4
+;; against local + 8, so despite the name it takes the overwrite-begin path.
+;; shortenEndPartial is the overwrite-end one, and between them they cover
+;; both arms of the offset shortenAssignment computes.
+
+;; DSE shortens the first store in shortenEnd from bytes [8, 80) of 'local' to
+;; [56, 80), so the dead bytes are [8, 56). The dbg.assign's address is
+;; %offset_4_bytes + 4, i.e. local + 8, so its fragment (64, 96) describes
+;; local bytes [8, 20), all of which the dead slice covers. There's no live
+;; part left to describe, so instead of inserting a fragment we unlink the
+;; dbg.assign from the store and kill its address.
 
 ; CHECK: @_Z10shortenEndv
-; CHECK:      #dbg_assign({{.*}}, ptr %local, !DIExpression(),
+; CHECK:      #dbg_assign({{.*}}, ![[VAR:[0-9]+]], !DIExpression(), {{.*}}, ptr %local, !DIExpression(),
 ; CHECK:      call void @llvm.memset{{.*}}, !DIAssignID ![[ID:[0-9]+]]
-; CHECK-NEXT: #dbg_assign(i8 0, ![[VAR:[0-9]+]], !DIExpression(DW_OP_LLVM_fragment, 64, 96), ![[ID:[0-9]+]], ptr %offset_4_bytes, !DIExpression(DW_OP_plus_uconst, 4),
-; CHECK-NEXT: #dbg_assign(i8 0, ![[VAR]], !DIExpression(DW_OP_LLVM_fragment, 128, 32), ![[UniqueID1:[0-9]+]], ptr poison, !DIExpression({{.*}}),
+; CHECK-NEXT: #dbg_assign(i8 0, ![[VAR]], !DIExpression(DW_OP_LLVM_fragment, 64, 96), ![[UniqueID1:[0-9]+]], ptr poison, !DIExpression(DW_OP_plus_uconst, 4),
 
 ;; DSE will shorten the first store in shortenStart from [0, 160) bits to [128,
 ;; 160) bits. Variable 'local2' has been adjusted to be 160 bits.  Check we get
@@ -45,9 +60,24 @@
 ; CHECK-NEXT: #dbg_assign(i8 0, ![[VAR2:[0-9]+]], !DIExpression(), ![[ID2]], ptr %local2, !DIExpression(),
 ; CHECK-NEXT: #dbg_assign(i8 0, ![[VAR2]], !DIExpression(DW_OP_LLVM_fragment, 0, 128), ![[UniqueID2:[0-9]+]], ptr poison, !DIExpression(),
 
+;; DSE shortens the store in shortenEndPartial from bytes [8, 16) of 'local3'
+;; to [8, 12), so the dead bytes are [12, 16). The dbg.assign's address is
+;; %offset_4_bytes + 4, i.e. local3 + 8, where its fragment (64, 96) starts, so
+;; the dead bytes are variable bits [96, 128). Check we get an unlinked
+;; dbg.assign for (96, 32). Counting the dead store's offset from local3 twice
+;; pushes the slice past the end of the fragment instead, and nothing at all
+;; gets inserted.
+
+; CHECK: @_Z17shortenEndPartialv
+; CHECK:      #dbg_assign({{.*}}, ptr %local3, !DIExpression(),
+; CHECK:      call void @llvm.memset{{.*}}, !DIAssignID ![[ID3:[0-9]+]]
+; CHECK-NEXT: #dbg_assign(i8 0, ![[VAR3:[0-9]+]], !DIExpression(DW_OP_LLVM_fragment, 64, 96), ![[ID3]], ptr %offset_4_bytes, !DIExpression(DW_OP_plus_uconst, 4),
+; CHECK-NEXT: #dbg_assign(i8 0, ![[VAR3]], !DIExpression(DW_OP_LLVM_fragment, 96, 32), ![[UniqueID3:[0-9]+]], ptr poison, !DIExpression(DW_OP_plus_uconst, 4),
+
 ; CHECK-DAG: ![[ID]] = distinct !DIAssignID()
 ; CHECK-DAG: ![[UniqueID1]] = distinct !DIAssignID()
 ; CHECK-DAG: ![[UniqueID2]] = distinct !DIAssignID()
+; CHECK-DAG: ![[UniqueID3]] = distinct !DIAssignID()
 
 define dso_local void @_Z10shortenEndv() local_unnamed_addr #0 !dbg !7 {
 entry:
@@ -79,6 +109,27 @@ entry:
   ret void, !dbg !45
 }
 
+;; The killing store starts after the dead one here, so this is the
+;; overwrite-end path, where the dead slice starts at the end of what survives
+;; rather than at the store's own address. The slice also covers only part of
+;; the dbg.assign's fragment, so we get a fragment for the dead part rather
+;; than a kill. The memset is align 4 so DSE doesn't round the removed tail
+;; away; at align 16 it declines to shorten at all.
+define dso_local void @_Z17shortenEndPartialv() local_unnamed_addr #0 !dbg !46 {
+entry:
+  %local3 = alloca [80 x i8], align 16, !DIAssignID !50
+  call void @llvm.dbg.assign(metadata i1 poison, metadata !48, metadata !DIExpression(), metadata !50, metadata ptr %local3, metadata !DIExpression()), !dbg !49
+  %arraydecay = getelementptr inbounds [80 x i8], ptr %local3, i64 0, i64 0, !dbg !53
+  %offset_4_bytes = getelementptr inbounds [80 x i8], ptr %local3, i64 0, i64 4, !dbg !53
+  %offset_8_bytes = getelementptr inbounds [80 x i8], ptr %local3, i64 0, i64 8, !dbg !53
+  call void @llvm.memset.p0.i64(ptr noundef nonnull align 4 %offset_8_bytes, i8 0, i64 8, i1 false), !dbg !53, !DIAssignID !51
+  call void @llvm.dbg.assign(metadata i8 0, metadata !48, metadata !DIExpression(DW_OP_LLVM_fragment, 64, 96), metadata !51, metadata ptr %offset_4_bytes, metadata !DIExpression(DW_OP_plus_uconst, 4)), !dbg !49
+  %offset_12_bytes = getelementptr inbounds [80 x i8], ptr %local3, i64 0, i64 12, !dbg !53
+  call void @llvm.memset.p0.i64(ptr noundef nonnull align 4 %offset_12_bytes, i8 8, i64 4, i1 false), !dbg !54, !DIAssignID !52
+  call void @_Z3escPi(ptr noundef nonnull %arraydecay), !dbg !55
+  ret void, !dbg !56
+}
+
 declare void @llvm.dbg.assign(metadata, metadata, metadata, metadata, metadata, metadata)
 
 !llvm.dbg.cu = !{!0}
@@ -131,4 +182,15 @@ declare void @llvm.dbg.assign(metadata, metadata, metadata, metadata, metadata,
 !43 = distinct !DIAssignID()
 !44 = !DILocation(line: 12, column: 3, scope: !31)
 !45 = !DILocation(line: 13, column: 1, scope: !31)
+!46 = distinct !DISubprogram(name: "shortenEndPartial", linkageName: "_Z17shortenEndPartialv", scope: !1, file: !1, line: 14, type: !8, scopeLine: 14, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !47)
+!47 = !{!48}
+!48 = !DILocalVariable(name: "local3", scope: !46, file: !1, line: 15, type: !12)
+!49 = !DILocation(line: 0, scope: !46)
+!50 = distinct !DIAssignID()
+!51 = distinct !DIAssignID()
+!52 = distinct !DIAssignID()
+!53 = !DILocation(line: 16, column: 3, scope: !46)
+!54 = !DILocation(line: 17, column: 3, scope: !46)
+!55 = !DILocation(line: 18, column: 3, scope: !46)
+!56 = !DILocation(line: 19, column: 1, scope: !46)
 !1000 = !{i32 7, !"debug-info-assignment-tracking", i1 true}

>From c46c921bb6d3e59bd41318e77d0b27ddd285cf7c Mon Sep 17 00:00:00 2001
From: Eric Christopher <echristopher at nvidia.com>
Date: Fri, 14 Aug 2026 16:43:23 -0700
Subject: [PATCH 2/3] Respond to reviewer comments.

---
 .../dse/shorten-offset-begin.ll               | 36 ++++++++----------
 .../assignment-tracking/dse/shorten-offset.ll | 38 +++++++++----------
 2 files changed, 33 insertions(+), 41 deletions(-)

diff --git a/llvm/test/DebugInfo/Generic/assignment-tracking/dse/shorten-offset-begin.ll b/llvm/test/DebugInfo/Generic/assignment-tracking/dse/shorten-offset-begin.ll
index 6de28612ef498..48fe1f4fe5c67 100644
--- a/llvm/test/DebugInfo/Generic/assignment-tracking/dse/shorten-offset-begin.ll
+++ b/llvm/test/DebugInfo/Generic/assignment-tracking/dse/shorten-offset-begin.ll
@@ -1,9 +1,7 @@
 ; RUN: opt %s -S -passes=dse -o - | FileCheck %s --implicit-check-not="#dbg_"
 
-;; The IR is clang's, cleaned up but not otherwise adjusted, so the variable
-;; fills its alloca and the offsets are the ones clang emitted. That gives up
-;; the coverage shorten-offset.ll buys by adjusting them, and gets a source
-;; listing whose numbers can be checked against the test.
+;; The IR is clang's, with only the cleanup listed below. The variable fills its
+;; alloca, so the source byte offsets can be checked directly against the test.
 ;;
 ;; $ cat shorten.c
 ;; void esc(char *);
@@ -14,30 +12,27 @@
 ;;   esc(local);
 ;; }
 ;;
-;; $ clang -O2 -g -c -Xclang -fexperimental-assignment-tracking=forced \
+;; $ clang -O2 -g -gno-key-instructions -c \
+;;       -Xclang -fexperimental-assignment-tracking=forced \
 ;;       -mllvm -print-before=dse -mllvm -print-module-scope shorten.c \
 ;;       -o /dev/null
 ;;
 ;; and then, by hand: dropped the target triple and datalayout so this runs
 ;; anywhere, dropped the tbaa metadata, llvm.ident, the producer/checksum/
-;; sysroot strings, the key-instruction atoms, the lifetime intrinsics and the
-;; function attributes, converted the debug records to intrinsics, and
+;; sysroot strings, the lifetime intrinsics and the function attributes, and
 ;; renumbered the metadata. The remaining non-debug instructions keep clang's
 ;; order and operands, and the assignment offsets are unchanged.
 
 ;; 'local' is 640 bits and starts at the alloca, so variable bit N is alloca
 ;; byte N/8 throughout, which is what makes the arithmetic readable.
 ;;
-;; The first memset covers bytes [8, 80) and the second covers [4, 68), so the
-;; second kills the front of the first and DSE shortens it to [68, 80). The
-;; bytes that died are [8, 68), which is variable bits [64, 544): fragment
-;; (64, 480).
-;;
-;; The dead slice stays inside the fragment, so the record keeps its size and
-;; only the offset can be wrong, and an offset that is off by the store's
-;; distance from the alloca still describes 60 plausible bytes. shorten-offset.ll
-;; covers the two shapes where the slice moves far enough to clip the fragment
-;; or to leave the variable entirely.
+;; The first memset covers bytes [8, 80) and the second covers [4, 68), so DSE
+;; shortens the first to [68, 80). The dead slice is [8, 68), or variable bits
+;; [64, 544), which gives fragment (64, 480). Counting the same eight-byte
+;; offset again gives (128, 480). Both fit inside the record's original
+;; (64, 576) fragment, so only the offset exposes the bug. shorten-offset.ll
+;; covers the cases where the extra offset clips the fragment or moves the
+;; slice past it.
 
 ; CHECK: @shortenBeginPartial
 ; CHECK:      #dbg_assign({{.*}}, ![[VAR:[0-9]+]], !DIExpression(), {{.*}}, ptr %local, !DIExpression(),
@@ -54,19 +49,18 @@
 define void @shortenBeginPartial() !dbg !7 {
 entry:
   %local = alloca [80 x i8], align 1, !DIAssignID !13
-  call void @llvm.dbg.assign(metadata i1 poison, metadata !11, metadata !DIExpression(), metadata !13, metadata ptr %local, metadata !DIExpression()), !dbg !14
+    #dbg_assign(i1 poison, !11, !DIExpression(), !13, ptr %local, !DIExpression(), !14)
   %add.ptr = getelementptr inbounds nuw i8, ptr %local, i64 8, !dbg !15
   call void @llvm.memset.p0.i64(ptr noundef nonnull align 1 dereferenceable(72) %add.ptr, i8 0, i64 72, i1 false), !dbg !15, !DIAssignID !16
-  call void @llvm.dbg.assign(metadata i8 0, metadata !11, metadata !DIExpression(DW_OP_LLVM_fragment, 64, 576), metadata !16, metadata ptr %add.ptr, metadata !DIExpression()), !dbg !14
+    #dbg_assign(i8 0, !11, !DIExpression(DW_OP_LLVM_fragment, 64, 576), !16, ptr %add.ptr, !DIExpression(), !14)
   %add.ptr2 = getelementptr inbounds nuw i8, ptr %local, i64 4, !dbg !17
   call void @llvm.memset.p0.i64(ptr noundef nonnull align 1 dereferenceable(64) %add.ptr2, i8 8, i64 64, i1 false), !dbg !17, !DIAssignID !18
-  call void @llvm.dbg.assign(metadata i1 poison, metadata !11, metadata !DIExpression(DW_OP_LLVM_fragment, 32, 512), metadata !18, metadata ptr %add.ptr2, metadata !DIExpression()), !dbg !14
+    #dbg_assign(i1 poison, !11, !DIExpression(DW_OP_LLVM_fragment, 32, 512), !18, ptr %add.ptr2, !DIExpression(), !14)
   call void @esc(ptr noundef nonnull %local), !dbg !19
   ret void, !dbg !20
 }
 
 declare void @llvm.memset.p0.i64(ptr nocapture writeonly, i8, i64, i1 immarg)
-declare void @llvm.dbg.assign(metadata, metadata, metadata, metadata, metadata, metadata)
 declare void @esc(ptr noundef)
 
 !llvm.dbg.cu = !{!0}
diff --git a/llvm/test/DebugInfo/Generic/assignment-tracking/dse/shorten-offset.ll b/llvm/test/DebugInfo/Generic/assignment-tracking/dse/shorten-offset.ll
index c83088bd99d84..c175a2c7a6da9 100644
--- a/llvm/test/DebugInfo/Generic/assignment-tracking/dse/shorten-offset.ll
+++ b/llvm/test/DebugInfo/Generic/assignment-tracking/dse/shorten-offset.ll
@@ -4,22 +4,22 @@
 ;;
 ;; $ cat test.cpp
 ;; void esc(char*);
-;; void shortenEnd() {
+;; void shortenBeginWholeFragment() {
 ;;   char local[80];                      //        bits    frag
-;;   __builtin_memset(local + 8, 0, 72);  // local:  64-160 ( 64, 96)
+;;   __builtin_memset(local + 8, 0, 72);  // local:  64-160 (64, 96)
 ;;   __builtin_memset(local + 4, 8, 64);  // local:  32-160 killed
 ;;   esc(local);
 ;; }
 ;; void shortenStart() {
 ;;   char local2[40];                 //          bits   frag
-;;   __builtin_memset(local2, 0, 40); // local2:  0-160  (0, 160)
+;;   __builtin_memset(local2, 0, 36); // local2:  0-160  (0, 160)
 ;;   __builtin_memset(local2, 8, 16); // local2:  0-128  (0, 128)
 ;;   esc(local2);
 ;; }
 ;; void shortenEndPartial() {
 ;;   char local3[80];                      //         bits    frag
 ;;   __builtin_memset(local3 + 8,  0, 8);  // local3:  64-128 (64, 96)
-;;   __builtin_memset(local3 + 12, 8, 4);  // local3:  96-128 ( 96, 32)
+;;   __builtin_memset(local3 + 12, 8, 4);  // local3:  96-128 (96, 32)
 ;;   esc(local3);
 ;; }
 
@@ -31,19 +31,19 @@
 ;; 'local' and 'local3' have both been adjusted to be 160 bits, so the bit
 ;; ranges above are the written bytes clamped to the variable.
 
-;; In shortenEnd the killing store starts before the dead one, at local + 4
-;; against local + 8, so despite the name it takes the overwrite-begin path.
-;; shortenEndPartial is the overwrite-end one, and between them they cover
-;; both arms of the offset shortenAssignment computes.
+;; shortenBeginWholeFragment takes the overwrite-begin path because the killing
+;; store starts before the dead one, at local + 4 against local + 8.
+;; shortenEndPartial takes the overwrite-end path, so the two cases cover both
+;; offsets shortenAssignment computes.
 
-;; DSE shortens the first store in shortenEnd from bytes [8, 80) of 'local' to
-;; [56, 80), so the dead bytes are [8, 56). The dbg.assign's address is
-;; %offset_4_bytes + 4, i.e. local + 8, so its fragment (64, 96) describes
-;; local bytes [8, 20), all of which the dead slice covers. There's no live
-;; part left to describe, so instead of inserting a fragment we unlink the
-;; dbg.assign from the store and kill its address.
+;; DSE shortens the first store in shortenBeginWholeFragment from bytes [8, 80)
+;; of 'local' to [56, 80), so the dead bytes are [8, 56). The dbg.assign's
+;; address is %offset_4_bytes + 4, i.e. local + 8, so its fragment (64, 96)
+;; describes local bytes [8, 20), all of which the dead slice covers. There's
+;; no live part left to describe, so instead of inserting a fragment we unlink
+;; the dbg.assign from the store and kill its address.
 
-; CHECK: @_Z10shortenEndv
+; CHECK: @_Z25shortenBeginWholeFragmentv
 ; CHECK:      #dbg_assign({{.*}}, ![[VAR:[0-9]+]], !DIExpression(), {{.*}}, ptr %local, !DIExpression(),
 ; CHECK:      call void @llvm.memset{{.*}}, !DIAssignID ![[ID:[0-9]+]]
 ; CHECK-NEXT: #dbg_assign(i8 0, ![[VAR]], !DIExpression(DW_OP_LLVM_fragment, 64, 96), ![[UniqueID1:[0-9]+]], ptr poison, !DIExpression(DW_OP_plus_uconst, 4),
@@ -64,9 +64,7 @@
 ;; to [8, 12), so the dead bytes are [12, 16). The dbg.assign's address is
 ;; %offset_4_bytes + 4, i.e. local3 + 8, where its fragment (64, 96) starts, so
 ;; the dead bytes are variable bits [96, 128). Check we get an unlinked
-;; dbg.assign for (96, 32). Counting the dead store's offset from local3 twice
-;; pushes the slice past the end of the fragment instead, and nothing at all
-;; gets inserted.
+;; dbg.assign for (96, 32).
 
 ; CHECK: @_Z17shortenEndPartialv
 ; CHECK:      #dbg_assign({{.*}}, ptr %local3, !DIExpression(),
@@ -79,7 +77,7 @@
 ; CHECK-DAG: ![[UniqueID2]] = distinct !DIAssignID()
 ; CHECK-DAG: ![[UniqueID3]] = distinct !DIAssignID()
 
-define dso_local void @_Z10shortenEndv() local_unnamed_addr #0 !dbg !7 {
+define dso_local void @_Z25shortenBeginWholeFragmentv() local_unnamed_addr #0 !dbg !7 {
 entry:
   %local = alloca [80 x i8], align 16, !DIAssignID !16
   call void @llvm.dbg.assign(metadata i1 poison, metadata !11, metadata !DIExpression(), metadata !16, metadata ptr %local, metadata !DIExpression()), !dbg !17
@@ -143,7 +141,7 @@ declare void @llvm.dbg.assign(metadata, metadata, metadata, metadata, metadata,
 !4 = !{i32 1, !"wchar_size", i32 4}
 !5 = !{i32 7, !"uwtable", i32 1}
 !6 = !{!"clang version 14.0.0"}
-!7 = distinct !DISubprogram(name: "shortenEnd", linkageName: "_Z10shortenEndv", scope: !1, file: !1, line: 2, type: !8, scopeLine: 2, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !10)
+!7 = distinct !DISubprogram(name: "shortenBeginWholeFragment", linkageName: "_Z25shortenBeginWholeFragmentv", scope: !1, file: !1, line: 2, type: !8, scopeLine: 2, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !10)
 !8 = !DISubroutineType(types: !9)
 !9 = !{null}
 !10 = !{!11}

>From 769dfd70041ef0ce73aee8eef961d771dcbad92d Mon Sep 17 00:00:00 2001
From: Eric Christopher <echristopher at nvidia.com>
Date: Sun, 16 Aug 2026 19:07:18 -0700
Subject: [PATCH 3/3] Clarify the DSE fragment test comment.

Spell out the byte offsets and why the expected fragment starts at bit 64 so the reader doesn't have to translate half-open ranges.

No regressions on the assignment-tracking DSE tests.

Assisted by AI.
---
 .../dse/shorten-offset-begin.ll               | 21 +++++++++++--------
 1 file changed, 12 insertions(+), 9 deletions(-)

diff --git a/llvm/test/DebugInfo/Generic/assignment-tracking/dse/shorten-offset-begin.ll b/llvm/test/DebugInfo/Generic/assignment-tracking/dse/shorten-offset-begin.ll
index 48fe1f4fe5c67..466878d294efd 100644
--- a/llvm/test/DebugInfo/Generic/assignment-tracking/dse/shorten-offset-begin.ll
+++ b/llvm/test/DebugInfo/Generic/assignment-tracking/dse/shorten-offset-begin.ll
@@ -23,16 +23,19 @@
 ;; renumbered the metadata. The remaining non-debug instructions keep clang's
 ;; order and operands, and the assignment offsets are unchanged.
 
-;; 'local' is 640 bits and starts at the alloca, so variable bit N is alloca
-;; byte N/8 throughout, which is what makes the arithmetic readable.
+;; 'local' fills its 80-byte alloca, so an offset into the alloca is also an
+;; offset into the variable.
 ;;
-;; The first memset covers bytes [8, 80) and the second covers [4, 68), so DSE
-;; shortens the first to [68, 80). The dead slice is [8, 68), or variable bits
-;; [64, 544), which gives fragment (64, 480). Counting the same eight-byte
-;; offset again gives (128, 480). Both fit inside the record's original
-;; (64, 576) fragment, so only the offset exposes the bug. shorten-offset.ll
-;; covers the cases where the extra offset clips the fragment or moves the
-;; slice past it.
+;; Check that the dead fragment describes exactly what DSE removes from the
+;; first memset. That memset writes 72 bytes starting eight bytes into 'local'.
+;; The second memset overwrites the first 60 of those bytes (bytes 8 through
+;; 67), so DSE keeps the final 12 bytes, starting at byte 68. The removed part
+;; starts at bit 64 of the variable and is 480 bits long.
+;;
+;; DW_OP_LLVM_fragment stores the starting bit followed by the size, so the
+;; CHECK expects (64, 480). Make sure the starting bit is 64 so that we know
+;; the dead range begins at byte 8 of 'local', where the overwritten part
+;; starts.
 
 ; CHECK: @shortenBeginPartial
 ; CHECK:      #dbg_assign({{.*}}, ![[VAR:[0-9]+]], !DIExpression(), {{.*}}, ptr %local, !DIExpression(),



More information about the llvm-commits mailing list