[llvm] [DebugInfo][AT] Treat escaping calls as untagged stores in assignment tracking (PR #183979)

Shivam Kunwar via llvm-commits llvm-commits at lists.llvm.org
Mon Mar 9 23:53:43 PDT 2026


https://github.com/phyBrackets updated https://github.com/llvm/llvm-project/pull/183979

>From 8e8993db25b6fcaa2371a7002041edac40b565f4 Mon Sep 17 00:00:00 2001
From: Shivam Kunwar <shivam.kunwar at kdab.com>
Date: Sun, 1 Mar 2026 11:25:41 +0530
Subject: [PATCH 1/3] [DebugInfo][AT] Treat escaping calls as untagged stores
 in assignment tracking

---
 .../CodeGen/AssignmentTrackingAnalysis.cpp    | 104 ++++++++++-
 .../assignment-tracking/X86/diamond-3.ll      |  17 +-
 .../assignment-tracking/X86/escaping-call.ll  | 167 ++++++++++++++++++
 .../assignment-tracking/X86/loop-hoist.ll     |   3 +
 .../X86/mem-loc-frag-fill.ll                  |   2 +
 .../X86/negative-offset.ll                    |   3 +
 6 files changed, 287 insertions(+), 9 deletions(-)
 create mode 100644 llvm/test/DebugInfo/assignment-tracking/X86/escaping-call.ll

diff --git a/llvm/lib/CodeGen/AssignmentTrackingAnalysis.cpp b/llvm/lib/CodeGen/AssignmentTrackingAnalysis.cpp
index 1ca3a2cc6850d..b46c822ef8af0 100644
--- a/llvm/lib/CodeGen/AssignmentTrackingAnalysis.cpp
+++ b/llvm/lib/CodeGen/AssignmentTrackingAnalysis.cpp
@@ -15,6 +15,7 @@
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/ADT/Statistic.h"
 #include "llvm/ADT/UniqueVector.h"
+#include "llvm/Analysis/ValueTracking.h"
 #include "llvm/BinaryFormat/Dwarf.h"
 #include "llvm/IR/BasicBlock.h"
 #include "llvm/IR/DataLayout.h"
@@ -1078,6 +1079,9 @@ class AssignmentTrackingLowering {
                SmallVector<std::pair<VariableID, at::AssignmentInfo>>>;
   using UnknownStoreAssignmentMap =
       DenseMap<const Instruction *, SmallVector<VariableID>>;
+  using EscapingCallVarsMap =
+      DenseMap<const Instruction *,
+               SmallVector<std::pair<VariableID, const AllocaInst *>>>;
 
 private:
   /// The highest numbered VariableID for partially promoted variables plus 1,
@@ -1091,6 +1095,10 @@ class AssignmentTrackingLowering {
   /// Map untagged unknown stores (e.g. strided/masked store intrinsics)
   /// to the variables they may assign to. Used by processUntaggedInstruction.
   UnknownStoreAssignmentMap UnknownStoreVars;
+  /// Map escaping calls (calls that receive a pointer to a tracked alloca as
+  /// an argument) to the variables they may modify. Used by
+  /// processEscapingCall.
+  EscapingCallVarsMap EscapingCallVars;
 
   // Machinery to defer inserting dbg.values.
   using InstInsertMap = MapVector<VarLocInsertPt, SmallVector<VarLocInfo>>;
@@ -1326,6 +1334,7 @@ class AssignmentTrackingLowering {
   void processUntaggedInstruction(Instruction &I, BlockInfo *LiveSet);
   void processUnknownStoreToVariable(Instruction &I, VariableID &Var,
                                      BlockInfo *LiveSet);
+  void processEscapingCall(Instruction &I, BlockInfo *LiveSet);
   void processDbgAssign(DbgVariableRecord *Assign, BlockInfo *LiveSet);
   void processDbgVariableRecord(DbgVariableRecord &DVR, BlockInfo *LiveSet);
   void processDbgValue(DbgVariableRecord *DbgValue, BlockInfo *LiveSet);
@@ -1549,6 +1558,11 @@ void AssignmentTrackingLowering::processNonDbgInstruction(
     processTaggedInstruction(I, LiveSet);
   else
     processUntaggedInstruction(I, LiveSet);
+
+  // Handle calls that pass tracked alloca pointers as arguments.
+  // The callee may modify the pointed-to memory.
+  if (isa<CallBase>(I))
+    processEscapingCall(I, LiveSet);
 }
 
 void AssignmentTrackingLowering::processUnknownStoreToVariable(
@@ -1672,6 +1686,56 @@ void AssignmentTrackingLowering::processUntaggedInstruction(
   }
 }
 
+void AssignmentTrackingLowering::processEscapingCall(
+    Instruction &I, AssignmentTrackingLowering::BlockInfo *LiveSet) {
+  auto It = EscapingCallVars.find(&I);
+  if (It == EscapingCallVars.end())
+    return;
+
+  LLVM_DEBUG(dbgs() << "processEscapingCall on " << I << "\n");
+
+  for (auto &[Var, Base] : It->second) {
+    // An escaping call is treated like an untagged store, whatever value is
+    // now in memory is the current value of the variable. We set both the
+    // stack and debug assignments to NoneOrPhi (we don't know which source
+    // assignment this corresponds to) and set the location to Mem (memory
+    // is valid).
+    addMemDef(LiveSet, Var, Assignment::makeNoneOrPhi());
+    addDbgDef(LiveSet, Var, Assignment::makeNoneOrPhi());
+    setLocKind(LiveSet, Var, LocKind::Mem);
+
+    LLVM_DEBUG(dbgs() << " escaping call may modify "
+                      << FnVarLocs->getVariable(Var).getVariable()->getName()
+                      << ", setting LocKind to Mem\n");
+
+    // Emit a memory location def, the variable lives at `*Base`
+    DebugVariable V = FnVarLocs->getVariable(Var);
+    DIExpression *DIE = DIExpression::get(I.getContext(), {});
+    if (auto Frag = V.getFragment()) {
+      auto R = DIExpression::createFragmentExpression(DIE, Frag->OffsetInBits,
+                                                      Frag->SizeInBits);
+      assert(R && "unexpected createFragmentExpression failue");
+      DIE = *R;
+    }
+    // Add implicit deref (the alloca address points to the variable's memory)
+    DIE = DIExpression::prepend(DIE, DIExpression::DerefAfter, /*Offset=*/0);
+
+    auto InsertBefore = getNextNode(&I);
+    assert(InsertBefore && "Shouldn't be inserting after a terminator");
+
+    DILocation *InlinedAt = const_cast<DILocation *>(V.getInlinedAt());
+    const DILocation *DILoc = DILocation::get(
+        Fn.getContext(), 0, 0, V.getVariable()->getScope(), InlinedAt);
+
+    VarLocInfo VarLoc;
+    VarLoc.VariableID = Var;
+    VarLoc.Expr = DIE;
+    VarLoc.Values = RawLocationWrapper(
+        ValueAsMetadata::get(const_cast<AllocaInst *>(Base)));
+    VarLoc.DL = DILoc;
+    InsertBeforeMap[InsertBefore].push_back(VarLoc);
+  }
+}
 void AssignmentTrackingLowering::processTaggedInstruction(
     Instruction &I, AssignmentTrackingLowering::BlockInfo *LiveSet) {
   auto LinkedDPAssigns = at::getDVRAssignmentMarkers(&I);
@@ -2116,6 +2180,7 @@ static AssignmentTrackingLowering::OverlapMap buildOverlapMapAndRecordDeclares(
     const DenseSet<DebugAggregate> &VarsWithStackSlot,
     AssignmentTrackingLowering::UntaggedStoreAssignmentMap &UntaggedStoreVars,
     AssignmentTrackingLowering::UnknownStoreAssignmentMap &UnknownStoreVars,
+    AssignmentTrackingLowering::EscapingCallVarsMap &EscapingCallVars,
     unsigned &TrackedVariablesVectorSize) {
   DenseSet<DebugVariable> Seen;
   // Map of Variable: [Fragments].
@@ -2200,6 +2265,43 @@ static AssignmentTrackingLowering::OverlapMap buildOverlapMapAndRecordDeclares(
         for (DbgVariableRecord *DVR : at::getDVRAssignmentMarkers(AI))
           HandleDbgAssignForUnknownStore(DVR);
       }
+
+      // Check for escaping calls
+      if (auto *CB = dyn_cast<CallBase>(&I)) {
+        // Skip intrinsics, their memory effects are modeled individually
+        if (!isa<IntrinsicInst>(CB) && !CB->onlyReadsMemory()) {
+          DenseSet<VariableID> SeenVars;
+          for (unsigned ArgIdx = 0; ArgIdx < CB->arg_size(); ++ArgIdx) {
+            Value *Arg = CB->getArgOperand(ArgIdx);
+            if (!Arg->getType()->isPointerTy())
+              continue;
+            if (CB->paramHasAttr(ArgIdx, Attribute::ReadOnly) ||
+                CB->paramHasAttr(ArgIdx, Attribute::ReadNone))
+              continue;
+            if (CB->paramHasAttr(ArgIdx, Attribute::ByVal))
+              continue;
+
+            auto *AI = dyn_cast<AllocaInst>(getUnderlyingObject(Arg));
+            if (!AI)
+              continue;
+
+            // Find tracked variables on this alloca. Use the whole-variable
+            // (no fragment) because we don't know which part the callee
+            // modifies. addMemDef/addDbgDef/setLocKind will propagate to
+            // contained fragments.
+            for (DbgVariableRecord *DVR : at::getDVRAssignmentMarkers(AI)) {
+              DebugVariable DV(DVR->getVariable(), std::nullopt,
+                               DVR->getDebugLoc().getInlinedAt());
+              DebugAggregate DA = {DV.getVariable(), DV.getInlinedAt()};
+              if (!VarsWithStackSlot.contains(DA))
+                continue;
+              VariableID VarID = FnVarLocs->insertVariable(DV);
+              if (SeenVars.insert(VarID).second)
+                EscapingCallVars[&I].push_back({VarID, AI});
+            }
+          }
+        }
+      }
     }
   }
 
@@ -2271,7 +2373,7 @@ bool AssignmentTrackingLowering::run(FunctionVarLocsBuilder *FnVarLocsBuilder) {
   // appears to be rare occurance.
   VarContains = buildOverlapMapAndRecordDeclares(
       Fn, FnVarLocs, *VarsWithStackSlot, UntaggedStoreVars, UnknownStoreVars,
-      TrackedVariablesVectorSize);
+      EscapingCallVars, TrackedVariablesVectorSize);
 
   // Prepare for traversal.
   ReversePostOrderTraversal<Function *> RPOT(&Fn);
diff --git a/llvm/test/DebugInfo/assignment-tracking/X86/diamond-3.ll b/llvm/test/DebugInfo/assignment-tracking/X86/diamond-3.ll
index b20b166cb9cd4..67eee5ceff473 100644
--- a/llvm/test/DebugInfo/assignment-tracking/X86/diamond-3.ll
+++ b/llvm/test/DebugInfo/assignment-tracking/X86/diamond-3.ll
@@ -15,10 +15,9 @@
 ;; if.end:
 ;;    mem(a) = !20 ; two preds disagree that !20 is the last assignment, don't
 ;;                 ; use mem loc.
-;;    ; This feels highly unfortunate, and highlights the need to reinstate the
-;;    ; memory location at call sites leaking the address (in an ideal world,
-;;    ; the memory location would always be in use at that point and so this
-;;    ; wouldn't be necessary).
+;;    ; The escaping call reinstates the memory location. The analysis treats
+;;    ; calls that leak the alloca address as untagged stores, so the memory
+;;    ; location is valid after the call.
 ;;    esc(a)       ; force the memory location
 
 ;; In real world examples this is caused by InstCombine sinking common code
@@ -30,10 +29,12 @@
 ; CHECK-LABEL: bb.1.if.then:
 ; CHECK:         DBG_VALUE 0, $noreg, ![[A]], !DIExpression()
 
-;; === TODO / WISHLIST ===
-; LEBAL-KCEHC: bb.2.if.end:
-; KCEHC:         CALL64pcrel32 target-flags(x86-plt) @es
-; KCEHC:         DBG_VALUE %stack.0.a.addr, $noreg, ![[A]], !DIExpression(DW_OP_deref)
+;; After the escaping call to @es, the memory location for 'a' is reinstated.
+;; The analysis treats escaping calls (calls receiving a pointer to a tracked
+;; alloca) like untagged stores, validating the memory location.
+; CHECK-LABEL: bb.2.if.end:
+; CHECK:         CALL64pcrel32 {{.*}}@es
+; CHECK:         DBG_VALUE %stack.0.a.addr, $noreg, ![[A]], !DIExpression(DW_OP_deref)
 
 target triple = "x86_64-unknown-linux-gnu"
 
diff --git a/llvm/test/DebugInfo/assignment-tracking/X86/escaping-call.ll b/llvm/test/DebugInfo/assignment-tracking/X86/escaping-call.ll
new file mode 100644
index 0000000000000..75332def8af3f
--- /dev/null
+++ b/llvm/test/DebugInfo/assignment-tracking/X86/escaping-call.ll
@@ -0,0 +1,167 @@
+; RUN: llc %s -stop-after=finalize-isel -o - \
+; RUN:   | FileCheck %s
+
+;; Test that assignment tracking correctly handles calls where a pointer to a
+;; tracked alloca escapes as an argument. After such a call, the memory
+;; location should be reinstated because the callee may have modified the
+;; variable through the pointer.
+;;
+;; Each function uses a #dbg_value to force LocKind::Val at some point, which
+;; prevents the variable from being "always stack homed" and causes the
+;; analysis to emit per-instruction DBG_VALUE records.
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+declare void @clobber(ptr)
+declare i32 @readonly_func(ptr readonly)
+declare void @byval_func(ptr byval(i32))
+
+;;Test 1: Basic escaping call reinstates memory location
+;;
+;; After the #dbg_value switches to Val (DBG_VALUE $noreg because %a has no
+;; vreg), the escaping call to @clobber should reinstate the memory location.
+;;
+; CHECK-LABEL: name: test_basic_escaping_call
+; CHECK:       bb.0.entry:
+; CHECK:         DBG_VALUE %stack.0.x, $noreg, !{{[0-9]+}}, !DIExpression(DW_OP_deref)
+; CHECK:         MOV32mi %stack.0.x
+; CHECK:         DBG_VALUE $noreg, $noreg, !{{[0-9]+}}, !DIExpression()
+; CHECK:         CALL64pcrel32 {{.*}}@clobber
+;; After the escaping call, memory location is reinstated:
+; CHECK:         DBG_VALUE %stack.0.x, $noreg, !{{[0-9]+}}, !DIExpression(DW_OP_deref)
+; CHECK:         RET 0
+
+define void @test_basic_escaping_call(i32 %a) !dbg !7 {
+entry:
+  %x = alloca i32, align 4, !DIAssignID !20
+    #dbg_assign(i1 poison, !11, !DIExpression(), !20, ptr %x, !DIExpression(), !12)
+  store i32 1, ptr %x, align 4, !DIAssignID !21
+    #dbg_assign(i32 1, !11, !DIExpression(), !21, ptr %x, !DIExpression(), !12)
+  #dbg_value(i32 %a, !11, !DIExpression(), !12)
+  call void @clobber(ptr %x)
+  ret void, !dbg !13
+}
+
+;;Test 2: Escaping call followed by a tagged store
+;;
+;; Verifies that the escaping call resets state so the subsequent tagged
+;; store correctly shows Mem (no stale value from before the call).
+;;
+; CHECK-LABEL: name: test_escaping_then_store
+; CHECK:       bb.0.entry:
+; CHECK:         DBG_VALUE %stack.0.y, $noreg, !{{[0-9]+}}, !DIExpression(DW_OP_deref)
+; CHECK:         MOV32mi %stack.0.y
+; CHECK:         DBG_VALUE $noreg, $noreg, !{{[0-9]+}}, !DIExpression()
+; CHECK:         CALL64pcrel32 {{.*}}@clobber
+;; After escaping call, memory location reinstated:
+; CHECK:         DBG_VALUE %stack.0.y, $noreg, !{{[0-9]+}}, !DIExpression(DW_OP_deref)
+;; Then the second store (still Mem, redundant DBG_VALUE elided):
+; CHECK:         MOV32mi %stack.0.y
+; CHECK:         RET 0
+
+define void @test_escaping_then_store(i32 %a) !dbg !30 {
+entry:
+  %y = alloca i32, align 4, !DIAssignID !40
+    #dbg_assign(i1 poison, !31, !DIExpression(), !40, ptr %y, !DIExpression(), !32)
+  store i32 1, ptr %y, align 4, !DIAssignID !41
+    #dbg_assign(i32 1, !31, !DIExpression(), !41, ptr %y, !DIExpression(), !32)
+  #dbg_value(i32 %a, !31, !DIExpression(), !32)
+  call void @clobber(ptr %y)
+  store i32 2, ptr %y, align 4, !DIAssignID !42
+    #dbg_assign(i32 2, !31, !DIExpression(), !42, ptr %y, !DIExpression(), !32)
+  ret void, !dbg !33
+}
+
+;;Test 3: Readonly call should NOT reinstate memory location
+;;
+;; A readonly call cannot modify memory, so no DBG_VALUE after the call.
+;;
+; CHECK-LABEL: name: test_readonly_not_escaping
+; CHECK:       bb.0.entry:
+; CHECK:         DBG_VALUE %stack.0.z, $noreg, !{{[0-9]+}}, !DIExpression(DW_OP_deref)
+; CHECK:         MOV32mi %stack.0.z
+; CHECK:         DBG_VALUE $noreg, $noreg, !{{[0-9]+}}, !DIExpression()
+; CHECK:         CALL64pcrel32 {{.*}}@readonly_func
+; CHECK-NOT:     DBG_VALUE
+; CHECK:         RET 0
+
+define void @test_readonly_not_escaping(i32 %a) !dbg !50 {
+entry:
+  %z = alloca i32, align 4, !DIAssignID !60
+    #dbg_assign(i1 poison, !51, !DIExpression(), !60, ptr %z, !DIExpression(), !52)
+  store i32 42, ptr %z, align 4, !DIAssignID !61
+    #dbg_assign(i32 42, !51, !DIExpression(), !61, ptr %z, !DIExpression(), !52)
+  #dbg_value(i32 %a, !51, !DIExpression(), !52)
+  %r = call i32 @readonly_func(ptr readonly %z)
+  ret void, !dbg !53
+}
+
+;;Test 4: Byval call should NOT reinstate memory location
+;;
+;; A byval argument passes a copy. The callee cannot modify the original.
+;;
+; CHECK-LABEL: name: test_byval_not_escaping
+; CHECK:       bb.0.entry:
+; CHECK:         DBG_VALUE %stack.0.w, $noreg, !{{[0-9]+}}, !DIExpression(DW_OP_deref)
+; CHECK:         MOV32mi %stack.0.w
+; CHECK:         DBG_VALUE $noreg, $noreg, !{{[0-9]+}}, !DIExpression()
+; CHECK:         CALL64pcrel32 {{.*}}@byval_func
+; CHECK-NOT:     DBG_VALUE
+; CHECK:         RET 0
+
+define void @test_byval_not_escaping(i32 %a) !dbg !70 {
+entry:
+  %w = alloca i32, align 4, !DIAssignID !80
+    #dbg_assign(i1 poison, !71, !DIExpression(), !80, ptr %w, !DIExpression(), !72)
+  store i32 10, ptr %w, align 4, !DIAssignID !81
+    #dbg_assign(i32 10, !71, !DIExpression(), !81, ptr %w, !DIExpression(), !72)
+  #dbg_value(i32 %a, !71, !DIExpression(), !72)
+  call void @byval_func(ptr byval(i32) %w)
+  ret void, !dbg !73
+}
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!3, !4, !5}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C11, file: !1, isOptimized: true, runtimeVersion: 0, emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.c", directory: "/tmp")
+!2 = !{}
+!3 = !{i32 7, !"Dwarf Version", i32 5}
+!4 = !{i32 2, !"Debug Info Version", i32 3}
+!5 = !{i32 7, !"debug-info-assignment-tracking", i1 true}
+!14 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+
+; Function 1 metadata
+!6 = !DISubroutineType(types: !2)
+!7 = distinct !DISubprogram(name: "test_basic_escaping_call", scope: !1, file: !1, line: 1, type: !6, scopeLine: 1, flags: DIFlagPrototyped, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0)
+!11 = !DILocalVariable(name: "x", scope: !7, file: !1, line: 2, type: !14)
+!12 = !DILocation(line: 2, column: 1, scope: !7)
+!13 = !DILocation(line: 5, column: 1, scope: !7)
+!20 = distinct !DIAssignID()
+!21 = distinct !DIAssignID()
+
+; Function 2 metadata
+!30 = distinct !DISubprogram(name: "test_escaping_then_store", scope: !1, file: !1, line: 10, type: !6, scopeLine: 10, flags: DIFlagPrototyped, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0)
+!31 = !DILocalVariable(name: "y", scope: !30, file: !1, line: 11, type: !14)
+!32 = !DILocation(line: 11, column: 1, scope: !30)
+!33 = !DILocation(line: 15, column: 1, scope: !30)
+!40 = distinct !DIAssignID()
+!41 = distinct !DIAssignID()
+!42 = distinct !DIAssignID()
+
+; Function 3 metadata
+!50 = distinct !DISubprogram(name: "test_readonly_not_escaping", scope: !1, file: !1, line: 20, type: !6, scopeLine: 20, flags: DIFlagPrototyped, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0)
+!51 = !DILocalVariable(name: "z", scope: !50, file: !1, line: 21, type: !14)
+!52 = !DILocation(line: 21, column: 1, scope: !50)
+!53 = !DILocation(line: 25, column: 1, scope: !50)
+!60 = distinct !DIAssignID()
+!61 = distinct !DIAssignID()
+
+; Function 4 metadata
+!70 = distinct !DISubprogram(name: "test_byval_not_escaping", scope: !1, file: !1, line: 30, type: !6, scopeLine: 30, flags: DIFlagPrototyped, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0)
+!71 = !DILocalVariable(name: "w", scope: !70, file: !1, line: 31, type: !14)
+!72 = !DILocation(line: 31, column: 1, scope: !70)
+!73 = !DILocation(line: 35, column: 1, scope: !70)
+!80 = distinct !DIAssignID()
+!81 = distinct !DIAssignID()
\ No newline at end of file
diff --git a/llvm/test/DebugInfo/assignment-tracking/X86/loop-hoist.ll b/llvm/test/DebugInfo/assignment-tracking/X86/loop-hoist.ll
index 559cdc59dffd9..49f2f2a57f5d2 100644
--- a/llvm/test/DebugInfo/assignment-tracking/X86/loop-hoist.ll
+++ b/llvm/test/DebugInfo/assignment-tracking/X86/loop-hoist.ll
@@ -28,6 +28,9 @@
 
 ; CHECK: bb.1.do.body:
 ; CHECK: DBG_VALUE %stack.0.a.addr, $noreg, ![[A]], !DIExpression(DW_OP_deref)
+;; After the escaping call to @_Z2esPi, the memory location is reinstated.
+; CHECK: CALL64pcrel32 {{.*}}@_Z2esPi
+; CHECK: DBG_VALUE %stack.0.a.addr, $noreg, ![[A]], !DIExpression(DW_OP_deref)
 
 target triple = "x86_64-unknown-linux-gnu"
 
diff --git a/llvm/test/DebugInfo/assignment-tracking/X86/mem-loc-frag-fill.ll b/llvm/test/DebugInfo/assignment-tracking/X86/mem-loc-frag-fill.ll
index 3964ee51382f7..23bb78e6a7792 100644
--- a/llvm/test/DebugInfo/assignment-tracking/X86/mem-loc-frag-fill.ll
+++ b/llvm/test/DebugInfo/assignment-tracking/X86/mem-loc-frag-fill.ll
@@ -65,6 +65,8 @@ entry:
 ; CHECK-NEXT: DBG_VALUE %stack.0.nums, $noreg, ![[nums]], !DIExpression(DW_OP_deref, DW_OP_LLVM_fragment, 0, 80)
   tail call void @_Z4stepv(), !dbg !32
   call void @_Z3escP4Nums(ptr noundef nonnull %nums), !dbg !33
+; CHECK: CALL64pcrel32 @_Z3escP4Nums
+; CHECK: DBG_VALUE %stack.0.nums, $noreg, ![[nums]], !DIExpression(DW_OP_deref)
   ret i32 0, !dbg !35
 }
 
diff --git a/llvm/test/DebugInfo/assignment-tracking/X86/negative-offset.ll b/llvm/test/DebugInfo/assignment-tracking/X86/negative-offset.ll
index df85e3e50f77b..81a0582911457 100644
--- a/llvm/test/DebugInfo/assignment-tracking/X86/negative-offset.ll
+++ b/llvm/test/DebugInfo/assignment-tracking/X86/negative-offset.ll
@@ -34,6 +34,9 @@
 ; CHECK-NEXT: successors
 ; CHECK-NEXT: {{^ *$}}
 ; CHECK-NEXT: DBG_VALUE 0, $noreg, ![[#]], !DIExpression()
+;; After the escaping call to @a, the memory location is reinstated.
+; CHECK: CALL64pcrel32 {{.*}}@a
+; CHECK: DBG_VALUE %stack.0.c, $noreg, ![[#]], !DIExpression(DW_OP_deref)
 
 target triple = "x86_64-unknown-linux-gnu"
 

>From 540423c68c579ee47750b7b472ac649a8bffea70 Mon Sep 17 00:00:00 2001
From: Shivam Kunwar <shivam.kunwar at kdab.com>
Date: Mon, 9 Mar 2026 23:18:00 +0530
Subject: [PATCH 2/3] address review comments

---
 .../CodeGen/AssignmentTrackingAnalysis.cpp    | 87 +++++++++++--------
 .../assignment-tracking/X86/escaping-call.ll  | 77 +++++++++++++---
 2 files changed, 116 insertions(+), 48 deletions(-)

diff --git a/llvm/lib/CodeGen/AssignmentTrackingAnalysis.cpp b/llvm/lib/CodeGen/AssignmentTrackingAnalysis.cpp
index b46c822ef8af0..2e60a34e3e833 100644
--- a/llvm/lib/CodeGen/AssignmentTrackingAnalysis.cpp
+++ b/llvm/lib/CodeGen/AssignmentTrackingAnalysis.cpp
@@ -1708,7 +1708,7 @@ void AssignmentTrackingLowering::processEscapingCall(
                       << FnVarLocs->getVariable(Var).getVariable()->getName()
                       << ", setting LocKind to Mem\n");
 
-    // Emit a memory location def, the variable lives at `*Base`
+    // Emit a memory location def, the variable lives at `*Base`.
     DebugVariable V = FnVarLocs->getVariable(Var);
     DIExpression *DIE = DIExpression::get(I.getContext(), {});
     if (auto Frag = V.getFragment()) {
@@ -1717,7 +1717,7 @@ void AssignmentTrackingLowering::processEscapingCall(
       assert(R && "unexpected createFragmentExpression failue");
       DIE = *R;
     }
-    // Add implicit deref (the alloca address points to the variable's memory)
+    // Add implicit deref (the alloca address points to the variable's memory).
     DIE = DIExpression::prepend(DIE, DIExpression::DerefAfter, /*Offset=*/0);
 
     auto InsertBefore = getNextNode(&I);
@@ -2170,8 +2170,10 @@ AllocaInst *getUnknownStore(const Instruction &I, const DataLayout &Layout) {
 /// subsequent variables are either stack homed or fully promoted.
 ///
 /// Finally, populate UntaggedStoreVars with a mapping of untagged stores to
-/// the stored-to variable fragments, and UnknownStoreVars with a mapping
-/// of untagged unknown stores to the stored-to variable aggregates.
+/// the stored-to variable fragments, UnknownStoreVars with a mapping of
+/// untagged unknown stores to the stored-to variable aggregates, and
+/// EscapingCallVars with a mapping of calls that receive a pointer to a
+/// tracked alloca as an argument to the variables they may modify.
 ///
 /// These tasks are bundled together to reduce the number of times we need
 /// to iterate over the function as they can be achieved together in one pass.
@@ -2266,40 +2268,49 @@ static AssignmentTrackingLowering::OverlapMap buildOverlapMapAndRecordDeclares(
           HandleDbgAssignForUnknownStore(DVR);
       }
 
-      // Check for escaping calls
-      if (auto *CB = dyn_cast<CallBase>(&I)) {
-        // Skip intrinsics, their memory effects are modeled individually
-        if (!isa<IntrinsicInst>(CB) && !CB->onlyReadsMemory()) {
-          DenseSet<VariableID> SeenVars;
-          for (unsigned ArgIdx = 0; ArgIdx < CB->arg_size(); ++ArgIdx) {
-            Value *Arg = CB->getArgOperand(ArgIdx);
-            if (!Arg->getType()->isPointerTy())
-              continue;
-            if (CB->paramHasAttr(ArgIdx, Attribute::ReadOnly) ||
-                CB->paramHasAttr(ArgIdx, Attribute::ReadNone))
-              continue;
-            if (CB->paramHasAttr(ArgIdx, Attribute::ByVal))
-              continue;
-
-            auto *AI = dyn_cast<AllocaInst>(getUnderlyingObject(Arg));
-            if (!AI)
-              continue;
-
-            // Find tracked variables on this alloca. Use the whole-variable
-            // (no fragment) because we don't know which part the callee
-            // modifies. addMemDef/addDbgDef/setLocKind will propagate to
-            // contained fragments.
-            for (DbgVariableRecord *DVR : at::getDVRAssignmentMarkers(AI)) {
-              DebugVariable DV(DVR->getVariable(), std::nullopt,
-                               DVR->getDebugLoc().getInlinedAt());
-              DebugAggregate DA = {DV.getVariable(), DV.getInlinedAt()};
-              if (!VarsWithStackSlot.contains(DA))
-                continue;
-              VariableID VarID = FnVarLocs->insertVariable(DV);
-              if (SeenVars.insert(VarID).second)
-                EscapingCallVars[&I].push_back({VarID, AI});
-            }
-          }
+      // Check for escaping calls.
+      auto *CB = dyn_cast<CallBase>(&I);
+      if (!CB)
+        continue;
+
+      // Skip intrinsics.  Their memory effects are modeled individually.
+      if (isa<IntrinsicInst>(CB))
+        continue;
+
+      // Skip calls that cannot write to memory at all.
+      if (CB->onlyReadsMemory())
+        continue;
+
+      SmallDenseSet<VariableID, 4> SeenVars;
+      for (unsigned ArgIdx = 0; ArgIdx < CB->arg_size(); ++ArgIdx) {
+        Value *Arg = CB->getArgOperand(ArgIdx);
+        if (!Arg->getType()->isPointerTy())
+          continue;
+        // Skip args the callee cannot write through.
+        if (CB->paramHasAttr(ArgIdx, Attribute::ReadOnly) ||
+            CB->paramHasAttr(ArgIdx, Attribute::ReadNone))
+          continue;
+        // Skip byval args.  The callee gets a copy, not the original.
+        if (CB->paramHasAttr(ArgIdx, Attribute::ByVal))
+          continue;
+
+        auto *AI = dyn_cast<AllocaInst>(getUnderlyingObject(Arg));
+        if (!AI)
+          continue;
+
+        // Find tracked variables on this alloca.  We use the whole-variable
+        // (no fragment) because we don't know which part the callee
+        // modifies.  addMemDef/addDbgDef/setLocKind will propagate to
+        // contained fragments.
+        for (DbgVariableRecord *DVR : at::getDVRAssignmentMarkers(AI)) {
+          DebugVariable DV(DVR->getVariable(), std::nullopt,
+                           DVR->getDebugLoc().getInlinedAt());
+          DebugAggregate DA = {DV.getVariable(), DV.getInlinedAt()};
+          if (!VarsWithStackSlot.contains(DA))
+            continue;
+          VariableID VarID = FnVarLocs->insertVariable(DV);
+          if (SeenVars.insert(VarID).second)
+            EscapingCallVars[&I].push_back({VarID, AI});
         }
       }
     }
diff --git a/llvm/test/DebugInfo/assignment-tracking/X86/escaping-call.ll b/llvm/test/DebugInfo/assignment-tracking/X86/escaping-call.ll
index 75332def8af3f..c9db25bf9f694 100644
--- a/llvm/test/DebugInfo/assignment-tracking/X86/escaping-call.ll
+++ b/llvm/test/DebugInfo/assignment-tracking/X86/escaping-call.ll
@@ -16,8 +16,9 @@ target triple = "x86_64-unknown-linux-gnu"
 declare void @clobber(ptr)
 declare i32 @readonly_func(ptr readonly)
 declare void @byval_func(ptr byval(i32))
+declare void @clobber_pair(ptr)
 
-;;Test 1: Basic escaping call reinstates memory location
+;; Test 1: Basic escaping call reinstates memory location.
 ;;
 ;; After the #dbg_value switches to Val (DBG_VALUE $noreg because %a has no
 ;; vreg), the escaping call to @clobber should reinstate the memory location.
@@ -38,12 +39,12 @@ entry:
     #dbg_assign(i1 poison, !11, !DIExpression(), !20, ptr %x, !DIExpression(), !12)
   store i32 1, ptr %x, align 4, !DIAssignID !21
     #dbg_assign(i32 1, !11, !DIExpression(), !21, ptr %x, !DIExpression(), !12)
-  #dbg_value(i32 %a, !11, !DIExpression(), !12)
+    #dbg_value(i32 %a, !11, !DIExpression(), !12)
   call void @clobber(ptr %x)
   ret void, !dbg !13
 }
 
-;;Test 2: Escaping call followed by a tagged store
+;; Test 2: Escaping call followed by a tagged store.
 ;;
 ;; Verifies that the escaping call resets state so the subsequent tagged
 ;; store correctly shows Mem (no stale value from before the call).
@@ -66,14 +67,14 @@ entry:
     #dbg_assign(i1 poison, !31, !DIExpression(), !40, ptr %y, !DIExpression(), !32)
   store i32 1, ptr %y, align 4, !DIAssignID !41
     #dbg_assign(i32 1, !31, !DIExpression(), !41, ptr %y, !DIExpression(), !32)
-  #dbg_value(i32 %a, !31, !DIExpression(), !32)
+    #dbg_value(i32 %a, !31, !DIExpression(), !32)
   call void @clobber(ptr %y)
   store i32 2, ptr %y, align 4, !DIAssignID !42
     #dbg_assign(i32 2, !31, !DIExpression(), !42, ptr %y, !DIExpression(), !32)
   ret void, !dbg !33
 }
 
-;;Test 3: Readonly call should NOT reinstate memory location
+;; Test 3: Readonly call should NOT reinstate memory location.
 ;;
 ;; A readonly call cannot modify memory, so no DBG_VALUE after the call.
 ;;
@@ -92,12 +93,12 @@ entry:
     #dbg_assign(i1 poison, !51, !DIExpression(), !60, ptr %z, !DIExpression(), !52)
   store i32 42, ptr %z, align 4, !DIAssignID !61
     #dbg_assign(i32 42, !51, !DIExpression(), !61, ptr %z, !DIExpression(), !52)
-  #dbg_value(i32 %a, !51, !DIExpression(), !52)
+    #dbg_value(i32 %a, !51, !DIExpression(), !52)
   %r = call i32 @readonly_func(ptr readonly %z)
   ret void, !dbg !53
 }
 
-;;Test 4: Byval call should NOT reinstate memory location
+;; Test 4: Byval call should NOT reinstate memory location.
 ;;
 ;; A byval argument passes a copy. The callee cannot modify the original.
 ;;
@@ -116,11 +117,53 @@ entry:
     #dbg_assign(i1 poison, !71, !DIExpression(), !80, ptr %w, !DIExpression(), !72)
   store i32 10, ptr %w, align 4, !DIAssignID !81
     #dbg_assign(i32 10, !71, !DIExpression(), !81, ptr %w, !DIExpression(), !72)
-  #dbg_value(i32 %a, !71, !DIExpression(), !72)
+    #dbg_value(i32 %a, !71, !DIExpression(), !72)
   call void @byval_func(ptr byval(i32) %w)
   ret void, !dbg !73
 }
 
+;; Test 5: Variable at an offset within its alloca (structured binding).
+;;
+;; A single variable "p" of struct type {int, int} (64 bits total) is
+;; described using two fragments: (0,32) for the first field and (32,32)
+;; for the second.  After an escaping call, both fragments should be
+;; reinstated to memory locations with DW_OP_deref plus their fragment.
+;;
+;; NOTE: The variable must have the struct type (64 bits) so that the
+;; 32-bit fragments are valid sub-ranges.  Using int (32 bits) as the
+;; type would make the fragment at offset 32 invalid.
+
+; CHECK-LABEL: name: test_offset_within_alloca
+; CHECK:       bb.0.entry:
+; CHECK-DAG:     DBG_VALUE %stack.0.p, $noreg, ![[P:[0-9]+]], !DIExpression(DW_OP_deref, DW_OP_LLVM_fragment, 0, 32)
+; CHECK-DAG:     DBG_VALUE %stack.0.p, $noreg, ![[P]], !DIExpression(DW_OP_deref, DW_OP_LLVM_fragment, 32, 32)
+;; The two i32 stores may be merged into a single i64 store by ISel:
+; CHECK:         {{MOV32mi|MOV64mr}} %stack.0.p
+; CHECK-DAG:     DBG_VALUE $noreg, $noreg, ![[P]], !DIExpression(DW_OP_LLVM_fragment, 0, 32)
+; CHECK-DAG:     DBG_VALUE $noreg, $noreg, ![[P]], !DIExpression(DW_OP_LLVM_fragment, 32, 32)
+; CHECK:         CALL64pcrel32 {{.*}}@clobber_pair
+;; After the escaping call, the whole variable is reinstated to memory.
+;; processEscapingCall uses the whole-variable (no fragment) so a single
+;; DW_OP_deref covers both fields:
+; CHECK:         DBG_VALUE %stack.0.p, $noreg, ![[P]], !DIExpression(DW_OP_deref)
+; CHECK:         RET 0
+
+define void @test_offset_within_alloca(i32 %val) !dbg !90 {
+entry:
+  %p = alloca { i32, i32 }, align 4, !DIAssignID !100
+    #dbg_assign(i1 poison, !91, !DIExpression(DW_OP_LLVM_fragment, 0, 32), !100, ptr %p, !DIExpression(), !93)
+    #dbg_assign(i1 poison, !91, !DIExpression(DW_OP_LLVM_fragment, 32, 32), !100, ptr %p, !DIExpression(), !93)
+  store i32 1, ptr %p, align 4, !DIAssignID !101
+    #dbg_assign(i32 1, !91, !DIExpression(DW_OP_LLVM_fragment, 0, 32), !101, ptr %p, !DIExpression(), !93)
+  %p.b = getelementptr inbounds i8, ptr %p, i64 4
+  store i32 2, ptr %p.b, align 4, !DIAssignID !102
+    #dbg_assign(i32 2, !91, !DIExpression(DW_OP_LLVM_fragment, 32, 32), !102, ptr %p, !DIExpression(), !93)
+    #dbg_value(i32 %val, !91, !DIExpression(DW_OP_LLVM_fragment, 0, 32), !93)
+    #dbg_value(i32 %val, !91, !DIExpression(DW_OP_LLVM_fragment, 32, 32), !93)
+  call void @clobber_pair(ptr %p)
+  ret void, !dbg !94
+}
+
 !llvm.dbg.cu = !{!0}
 !llvm.module.flags = !{!3, !4, !5}
 
@@ -130,10 +173,10 @@ entry:
 !3 = !{i32 7, !"Dwarf Version", i32 5}
 !4 = !{i32 2, !"Debug Info Version", i32 3}
 !5 = !{i32 7, !"debug-info-assignment-tracking", i1 true}
+!6 = !DISubroutineType(types: !2)
 !14 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
 
 ; Function 1 metadata
-!6 = !DISubroutineType(types: !2)
 !7 = distinct !DISubprogram(name: "test_basic_escaping_call", scope: !1, file: !1, line: 1, type: !6, scopeLine: 1, flags: DIFlagPrototyped, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0)
 !11 = !DILocalVariable(name: "x", scope: !7, file: !1, line: 2, type: !14)
 !12 = !DILocation(line: 2, column: 1, scope: !7)
@@ -164,4 +207,18 @@ entry:
 !72 = !DILocation(line: 31, column: 1, scope: !70)
 !73 = !DILocation(line: 35, column: 1, scope: !70)
 !80 = distinct !DIAssignID()
-!81 = distinct !DIAssignID()
\ No newline at end of file
+!81 = distinct !DIAssignID()
+
+; Function 5 metadata
+;; Variable "p" has struct type (64 bits) so fragments (0,32) and (32,32) are valid.
+!85 = !DICompositeType(tag: DW_TAG_structure_type, name: "Pair", file: !1, line: 40, size: 64, elements: !86)
+!86 = !{!87, !88}
+!87 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !85, file: !1, line: 41, baseType: !14, size: 32)
+!88 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !85, file: !1, line: 42, baseType: !14, size: 32, offset: 32)
+!90 = distinct !DISubprogram(name: "test_offset_within_alloca", scope: !1, file: !1, line: 44, type: !6, scopeLine: 44, flags: DIFlagPrototyped, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0)
+!91 = !DILocalVariable(name: "p", scope: !90, file: !1, line: 45, type: !85)
+!93 = !DILocation(line: 45, column: 1, scope: !90)
+!94 = !DILocation(line: 48, column: 1, scope: !90)
+!100 = distinct !DIAssignID()
+!101 = distinct !DIAssignID()
+!102 = distinct !DIAssignID()
\ No newline at end of file

>From 838185b9b042ff2f1eef98da1341a928298eb6ec Mon Sep 17 00:00:00 2001
From: Shivam Kunwar <shivam.kunwar at kdab.com>
Date: Tue, 10 Mar 2026 11:49:38 +0530
Subject: [PATCH 3/3] update the document

---
 llvm/docs/AssignmentTracking.md | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/llvm/docs/AssignmentTracking.md b/llvm/docs/AssignmentTracking.md
index ae4891236c259..5d6d68367aa80 100644
--- a/llvm/docs/AssignmentTracking.md
+++ b/llvm/docs/AssignmentTracking.md
@@ -198,9 +198,6 @@ the choice at each instruction, iteratively joining the results for each block.
 
 Outstanding improvements:
 
-* As mentioned in test llvm/test/DebugInfo/assignment-tracking/X86/diamond-3.ll,
-  the analysis should treat escaping calls like untagged stores.
-
 * The system expects locals to be backed by a local alloca. This isn't always
   the case - sometimes a pointer to storage is passed into a function
   (e.g. sret, byval). We need to be able to handle those cases. See



More information about the llvm-commits mailing list