[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 10:49:10 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/2] [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/2] 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
More information about the llvm-commits
mailing list