struct FragmentInfo {
uint64_t SizeInBits;
uint64_t OffsetInBits;
+ /// Return the index of the first bit of the fragment.
+ uint64_t startInBits() const { return OffsetInBits; }
+ /// Return the index of the bit after the end of the fragment, e.g. for
+ /// fragment offset=16 and size=32 return their sum, 48.
+ uint64_t endInBits() const { return OffsetInBits + SizeInBits; }
};
/// Retrieve the details of this fragment expression.
/// GEPs.
static cl::opt<bool> SROAStrictInbounds("sroa-strict-inbounds", cl::init(false),
cl::Hidden);
+/// Disable running mem2reg during SROA in order to test or debug SROA.
+static cl::opt<bool> SROASkipMem2Reg("sroa-skip-mem2reg", cl::init(false),
+ cl::Hidden);
namespace {
+
+/// Calculate the fragment of a variable to use when slicing a store
+/// based on the slice dimensions, existing fragment, and base storage
+/// fragment.
+/// Note that a returned value of std::nullopt indicates that there is
+/// no appropriate fragment available (rather than meaning use the whole
+/// variable, which is a common usage). Because the store is being sliced
+/// we always expect a fragment - there's never a case where the whole
+/// variable should be used.
+static std::optional<DIExpression::FragmentInfo>
+calculateFragment(uint64_t NewStorageSliceOffsetInBits,
+ uint64_t NewStorageSliceSizeInBits,
+ std::optional<DIExpression::FragmentInfo> StorageFragment,
+ std::optional<DIExpression::FragmentInfo> CurrentFragment) {
+ DIExpression::FragmentInfo Target;
+ // If the base storage describes part of the variable apply the offset and
+ // the size constraint.
+ if (StorageFragment) {
+ Target.SizeInBits =
+ std::min(NewStorageSliceSizeInBits, StorageFragment->SizeInBits);
+ Target.OffsetInBits =
+ NewStorageSliceOffsetInBits + StorageFragment->OffsetInBits;
+ } else {
+ Target.SizeInBits = NewStorageSliceSizeInBits;
+ Target.OffsetInBits = NewStorageSliceOffsetInBits;
+ }
+
+ // No additional work to do if there isn't a fragment already, or there is
+ // but it already exactly describes the new assignment.
+ if (!CurrentFragment || *CurrentFragment == Target)
+ return Target;
+
+ // Reject the target fragment if it doesn't fit wholly within the current
+ // fragment. TODO: We could instead chop up the target to fit in the case of
+ // a partial overlap.
+ if (Target.startInBits() < CurrentFragment->startInBits() ||
+ Target.endInBits() > CurrentFragment->endInBits())
+ return std::nullopt;
+
+ // Target fits within the current fragment, return it.
+ return Target;
+}
+
+static DebugVariable getAggregateVariable(DbgVariableIntrinsic *DVI) {
+ return DebugVariable(DVI->getVariable(), std::nullopt,
+ DVI->getDebugLoc().getInlinedAt());
+}
+
/// Find linked dbg.assign and generate a new one with the correct
/// FragmentInfo. Link Inst to the new dbg.assign. If Value is nullptr the
/// value component is copied from the old dbg.assign to the new.
/// \param OldAlloca Alloca for the variable before splitting.
-/// \param RelativeOffsetInBits Offset into \p OldAlloca relative to the
-/// offset prior to splitting (change in offset).
+/// \param IsSplit True if the store (not necessarily alloca)
+/// is being split.
+/// \param OldAllocaOffsetInBits Offset of the slice taken from OldAlloca.
/// \param SliceSizeInBits New number of bits being written to.
/// \param OldInst Instruction that is being split.
/// \param Inst New instruction performing this part of the
/// \param Dest Store destination.
/// \param Value Stored value.
/// \param DL Datalayout.
-static void migrateDebugInfo(AllocaInst *OldAlloca,
- uint64_t RelativeOffsetInBits,
+static void migrateDebugInfo(AllocaInst *OldAlloca, bool IsSplit,
+ uint64_t OldAllocaOffsetInBits,
uint64_t SliceSizeInBits, Instruction *OldInst,
Instruction *Inst, Value *Dest, Value *Value,
const DataLayout &DL) {
LLVM_DEBUG(dbgs() << " migrateDebugInfo\n");
LLVM_DEBUG(dbgs() << " OldAlloca: " << *OldAlloca << "\n");
- LLVM_DEBUG(dbgs() << " RelativeOffset: " << RelativeOffsetInBits << "\n");
+ LLVM_DEBUG(dbgs() << " IsSplit: " << IsSplit << "\n");
+ LLVM_DEBUG(dbgs() << " OldAllocaOffsetInBits: " << OldAllocaOffsetInBits
+ << "\n");
LLVM_DEBUG(dbgs() << " SliceSizeInBits: " << SliceSizeInBits << "\n");
LLVM_DEBUG(dbgs() << " OldInst: " << *OldInst << "\n");
LLVM_DEBUG(dbgs() << " Inst: " << *Inst << "\n");
if (Value)
LLVM_DEBUG(dbgs() << " Value: " << *Value << "\n");
+ /// Map of aggregate variables to their fragment associated with OldAlloca.
+ DenseMap<DebugVariable, std::optional<DIExpression::FragmentInfo>>
+ BaseFragments;
+ for (auto *DAI : at::getAssignmentMarkers(OldAlloca))
+ BaseFragments[getAggregateVariable(DAI)] =
+ DAI->getExpression()->getFragmentInfo();
+
// The new inst needs a DIAssignID unique metadata tag (if OldInst has
// one). It shouldn't already have one: assert this assumption.
assert(!Inst->getMetadata(LLVMContext::MD_DIAssignID));
DIAssignID *NewID = nullptr;
auto &Ctx = Inst->getContext();
DIBuilder DIB(*OldInst->getModule(), /*AllowUnresolved*/ false);
- uint64_t AllocaSizeInBits = *OldAlloca->getAllocationSizeInBits(DL);
assert(OldAlloca->isStaticAlloca());
for (DbgAssignIntrinsic *DbgAssign : MarkerRange) {
<< "\n");
auto *Expr = DbgAssign->getExpression();
- // Check if the dbg.assign already describes a fragment.
- auto GetCurrentFragSize = [AllocaSizeInBits, DbgAssign,
- Expr]() -> uint64_t {
- if (auto FI = Expr->getFragmentInfo())
- return FI->SizeInBits;
- if (auto VarSize = DbgAssign->getVariable()->getSizeInBits())
- return *VarSize;
- // The variable type has an unspecified size. This can happen in the
- // case of DW_TAG_unspecified_type types, e.g. std::nullptr_t. Because
- // there is no fragment and we do not know the size of the variable type,
- // we'll guess by looking at the alloca.
- return AllocaSizeInBits;
- };
- uint64_t CurrentFragSize = GetCurrentFragSize();
- bool MakeNewFragment = CurrentFragSize != SliceSizeInBits;
- assert(MakeNewFragment || RelativeOffsetInBits == 0);
-
- assert(SliceSizeInBits <= AllocaSizeInBits);
- if (MakeNewFragment) {
- assert(RelativeOffsetInBits + SliceSizeInBits <= CurrentFragSize);
- auto E = DIExpression::createFragmentExpression(
- Expr, RelativeOffsetInBits, SliceSizeInBits);
- assert(E && "Failed to create fragment expr!");
- Expr = *E;
+ if (IsSplit) {
+ std::optional<DIExpression::FragmentInfo> BaseFragment = std::nullopt;
+ {
+ auto R = BaseFragments.find(getAggregateVariable(DbgAssign));
+ if (R == BaseFragments.end())
+ continue;
+ BaseFragment = R->second;
+ }
+ std::optional<DIExpression::FragmentInfo> CurrentFragment =
+ Expr->getFragmentInfo();
+ std::optional<DIExpression::FragmentInfo> NewFragment =
+ calculateFragment(OldAllocaOffsetInBits, SliceSizeInBits,
+ BaseFragment, CurrentFragment);
+ // Note that std::nullopt here means "skip this fragment" rather than
+ // "there is no fragment / use the whole variable".
+ if (!NewFragment)
+ continue;
+
+ if (!(NewFragment == CurrentFragment)) {
+ if (CurrentFragment) {
+ // Rewrite NewFragment to be relative to the existing one (this is
+ // what createFragmentExpression wants). CalculateFragment has
+ // already resolved the size for us. FIXME: Should it return the
+ // relative fragment too?
+ NewFragment->OffsetInBits -= CurrentFragment->OffsetInBits;
+ }
+
+ auto E = DIExpression::createFragmentExpression(
+ Expr, NewFragment->OffsetInBits, NewFragment->SizeInBits);
+ assert(E && "Failed to create fragment expr!");
+ Expr = *E;
+ }
}
// If we haven't created a DIAssignID ID do that now and attach it to Inst.
// original alloca.
uint64_t NewBeginOffset = 0, NewEndOffset = 0;
- uint64_t RelativeOffset = 0;
uint64_t SliceSize = 0;
bool IsSplittable = false;
bool IsSplit = false;
NewBeginOffset = std::max(BeginOffset, NewAllocaBeginOffset);
NewEndOffset = std::min(EndOffset, NewAllocaEndOffset);
- RelativeOffset = NewBeginOffset - BeginOffset;
SliceSize = NewEndOffset - NewBeginOffset;
LLVM_DEBUG(dbgs() << " Begin:(" << BeginOffset << ", " << EndOffset
<< ") NewBegin:(" << NewBeginOffset << ", "
<< NewEndOffset << ") NewAllocaBegin:("
<< NewAllocaBeginOffset << ", " << NewAllocaEndOffset
<< ")\n");
- assert(IsSplit || RelativeOffset == 0);
+ assert(IsSplit || NewBeginOffset == BeginOffset);
OldUse = I->getUse();
OldPtr = cast<Instruction>(OldUse->get());
Pass.DeadInsts.push_back(&SI);
// NOTE: Careful to use OrigV rather than V.
- migrateDebugInfo(&OldAI, RelativeOffset * 8, SliceSize * 8, &SI, Store,
- Store->getPointerOperand(), OrigV, DL);
+ migrateDebugInfo(&OldAI, IsSplit, NewBeginOffset * 8, SliceSize * 8, &SI,
+ Store, Store->getPointerOperand(), OrigV, DL);
LLVM_DEBUG(dbgs() << " to: " << *Store << "\n");
return true;
}
if (AATags)
Store->setAAMetadata(AATags.shift(NewBeginOffset - BeginOffset));
- migrateDebugInfo(&OldAI, RelativeOffset * 8, SliceSize * 8, &SI, Store,
- Store->getPointerOperand(), Store->getValueOperand(), DL);
+ migrateDebugInfo(&OldAI, IsSplit, NewBeginOffset * 8, SliceSize * 8, &SI,
+ Store, Store->getPointerOperand(),
+ Store->getValueOperand(), DL);
Pass.DeadInsts.push_back(&SI);
LLVM_DEBUG(dbgs() << " to: " << *Store << "\n");
if (NewSI->isAtomic())
NewSI->setAlignment(SI.getAlign());
- migrateDebugInfo(&OldAI, RelativeOffset * 8, SliceSize * 8, &SI, NewSI,
- NewSI->getPointerOperand(), NewSI->getValueOperand(), DL);
+ migrateDebugInfo(&OldAI, IsSplit, NewBeginOffset * 8, SliceSize * 8, &SI,
+ NewSI, NewSI->getPointerOperand(),
+ NewSI->getValueOperand(), DL);
Pass.DeadInsts.push_back(&SI);
deleteIfTriviallyDead(OldOp);
if (AATags)
New->setAAMetadata(AATags.shift(NewBeginOffset - BeginOffset));
- migrateDebugInfo(&OldAI, RelativeOffset * 8, SliceSize * 8, &II, New,
- New->getRawDest(), nullptr, DL);
+ migrateDebugInfo(&OldAI, IsSplit, NewBeginOffset * 8, SliceSize * 8, &II,
+ New, New->getRawDest(), nullptr, DL);
LLVM_DEBUG(dbgs() << " to: " << *New << "\n");
return false;
if (AATags)
New->setAAMetadata(AATags.shift(NewBeginOffset - BeginOffset));
- migrateDebugInfo(&OldAI, RelativeOffset * 8, SliceSize * 8, &II, New,
- New->getPointerOperand(), V, DL);
+ migrateDebugInfo(&OldAI, IsSplit, NewBeginOffset * 8, SliceSize * 8, &II,
+ New, New->getPointerOperand(), V, DL);
LLVM_DEBUG(dbgs() << " to: " << *New << "\n");
return !II.isVolatile();
if (AATags)
New->setAAMetadata(AATags.shift(NewBeginOffset - BeginOffset));
- migrateDebugInfo(&OldAI, RelativeOffset * 8, SliceSize * 8, &II, New,
- DestPtr, nullptr, DL);
+ APInt Offset(DL.getIndexTypeSizeInBits(DestPtr->getType()), 0);
+ if (IsDest) {
+ migrateDebugInfo(&OldAI, IsSplit, NewBeginOffset * 8, SliceSize * 8,
+ &II, New, DestPtr, nullptr, DL);
+ } else if (AllocaInst *Base = dyn_cast<AllocaInst>(
+ DestPtr->stripAndAccumulateConstantOffsets(
+ DL, Offset, /*AllowNonInbounds*/ true))) {
+ migrateDebugInfo(Base, IsSplit, Offset.getZExtValue() * 8,
+ SliceSize * 8, &II, New, DestPtr, nullptr, DL);
+ }
LLVM_DEBUG(dbgs() << " to: " << *New << "\n");
return false;
}
if (AATags)
Store->setAAMetadata(AATags.shift(NewBeginOffset - BeginOffset));
- migrateDebugInfo(&OldAI, RelativeOffset * 8, SliceSize * 8, &II, Store,
- DstPtr, Src, DL);
+ APInt Offset(DL.getIndexTypeSizeInBits(DstPtr->getType()), 0);
+ if (IsDest) {
+
+ migrateDebugInfo(&OldAI, IsSplit, NewBeginOffset * 8, SliceSize * 8, &II,
+ Store, DstPtr, Src, DL);
+ } else if (AllocaInst *Base = dyn_cast<AllocaInst>(
+ DstPtr->stripAndAccumulateConstantOffsets(
+ DL, Offset, /*AllowNonInbounds*/ true))) {
+ migrateDebugInfo(Base, IsSplit, Offset.getZExtValue() * 8, SliceSize * 8,
+ &II, Store, DstPtr, Src, DL);
+ }
+
LLVM_DEBUG(dbgs() << " to: " << *Store << "\n");
return !II.isVolatile();
}
APInt Offset(
DL.getIndexSizeInBits(Ptr->getType()->getPointerAddressSpace()), 0);
- if (AATags &&
- GEPOperator::accumulateConstantOffset(BaseTy, GEPIndices, DL, Offset))
+ GEPOperator::accumulateConstantOffset(BaseTy, GEPIndices, DL, Offset);
+ if (AATags)
Store->setAAMetadata(AATags.shift(Offset.getZExtValue()));
// migrateDebugInfo requires the base Alloca. Walk to it from this gep.
// If we cannot (because there's an intervening non-const or unbounded
// gep) then we wouldn't expect to see dbg.assign intrinsics linked to
// this instruction.
- APInt OffsetInBytes(DL.getTypeSizeInBits(Ptr->getType()), false);
- Value *Base = InBoundsGEP->stripAndAccumulateInBoundsConstantOffsets(
- DL, OffsetInBytes);
+ Value *Base = AggStore->getPointerOperand()->stripInBoundsOffsets();
if (auto *OldAI = dyn_cast<AllocaInst>(Base)) {
uint64_t SizeInBits =
DL.getTypeSizeInBits(Store->getValueOperand()->getType());
- migrateDebugInfo(OldAI, OffsetInBytes.getZExtValue() * 8, SizeInBits,
- AggStore, Store, Store->getPointerOperand(),
- Store->getValueOperand(), DL);
+ migrateDebugInfo(OldAI, /*IsSplit*/ true, Offset.getZExtValue() * 8,
+ SizeInBits, AggStore, Store,
+ Store->getPointerOperand(), Store->getValueOperand(),
+ DL);
} else {
assert(at::getAssignmentMarkers(Store).empty() &&
"AT: unexpected debug.assign linked to store through "
NumPromoted += PromotableAllocas.size();
- LLVM_DEBUG(dbgs() << "Promoting allocas with mem2reg...\n");
- PromoteMemToReg(PromotableAllocas, DTU->getDomTree(), AC);
+ if (SROASkipMem2Reg) {
+ LLVM_DEBUG(dbgs() << "Not promoting allocas with mem2reg!\n");
+ } else {
+ LLVM_DEBUG(dbgs() << "Promoting allocas with mem2reg...\n");
+ PromoteMemToReg(PromotableAllocas, DTU->getDomTree(), AC);
+ }
+
PromotableAllocas.clear();
return true;
}
--- /dev/null
+; RUN: opt -S -passes=sroa -sroa-skip-mem2reg %s \
+; RUN: | FileCheck %s --implicit-check-not="call void @llvm.dbg"
+
+;; NOTE: This is the same as split-pre-fragmented-store.ll except the base
+;; alloca's dbg.assign has been altered to contain a fragment of the full
+;; variable - the variable's size has been modified from 64 to 96 bits.
+;; This version of the test ensures that the behaviour being tested is still
+;; correct when the base alloca doesn't hold an entire variable.
+
+;; IR hand-modified, originally generated from:
+;; struct Pair { int a; int b; };
+;; Pair getVar();
+;; int fun() {
+;; Pair var;
+;; var = getVar();
+;; return var.b;
+;; }
+;; Modification: split the dbg.assign linked the the memcpy(64 bits) into two,
+;; each describing a 32 bit fragment.
+;;
+;; Check that assignment tracking updates in SROA work when the store being
+;; split is described with one dbg.assign (covering different fragments). The
+;; store may have been already split and then merged again at some point.
+
+;; Alloca for var.a and associated dbg.assign:
+; CHECK: %var.sroa.0 = alloca i32, align 4, !DIAssignID ![[id_1:[0-9]+]]
+; CHECK-NEXT: call void @llvm.dbg.assign(metadata i1 undef, metadata ![[var:[0-9]+]], metadata !DIExpression(DW_OP_LLVM_fragment, 32, 32), metadata ![[id_1]], metadata ptr %var.sroa.0, metadata !DIExpression())
+
+;; Alloca for var.b and associated dbg.assign:
+; CHECK-NEXT: %var.sroa.1 = alloca i32, align 4, !DIAssignID ![[id_2:[0-9]+]]
+; CHECK-NEXT: call void @llvm.dbg.assign(metadata i1 undef, metadata ![[var]], metadata !DIExpression(DW_OP_LLVM_fragment, 64, 32), metadata ![[id_2]], metadata ptr %var.sroa.1, metadata !DIExpression())
+
+;; Store to var.b (split from store to var) and associated dbg.assigns. The
+;; dbg.assign for the fragment covering the (pre-split) assignment to var.a
+;; should not be linked to the store.
+; CHECK: store i32 %[[v:.*]], ptr %var.sroa.1,{{.*}}!DIAssignID ![[id_3:[0-9]+]]
+; CHECK-NEXT: call void @llvm.dbg.assign(metadata i32 %{{.*var\.sroa\.0.*}}, metadata ![[var]], metadata !DIExpression(DW_OP_LLVM_fragment, 32, 32), metadata ![[id_4:[0-9]+]], metadata ptr %var.sroa.0, metadata !DIExpression())
+; CHECK-NEXT: call void @llvm.dbg.assign(metadata i32 %[[v]], metadata ![[var]], metadata !DIExpression(DW_OP_LLVM_fragment, 64, 32), metadata ![[id_3]], metadata ptr %var.sroa.1, metadata !DIExpression())
+
+; CHECK-DAG: ![[id_1]] = distinct !DIAssignID()
+; CHECK-DAG: ![[id_2]] = distinct !DIAssignID()
+; CHECK-DAG: ![[id_3]] = distinct !DIAssignID()
+; CHECK-DAG: ![[id_4]] = distinct !DIAssignID()
+
+%struct.Tuple = type { i32, i32, i32 }
+
+define dso_local noundef i32 @_Z3funv() !dbg !9 {
+entry:
+ %var = alloca [2 x i32], align 4, !DIAssignID !19
+ call void @llvm.dbg.assign(metadata i1 undef, metadata !14, metadata !DIExpression(DW_OP_LLVM_fragment, 32, 64), metadata !19, metadata ptr %var, metadata !DIExpression()), !dbg !20
+ %ref.tmp = alloca %struct.Tuple, align 4
+ %call = call i64 @_Z6getVarv(), !dbg !22
+ store i64 %call, ptr %ref.tmp, align 4, !dbg !22
+ call void @llvm.memcpy.p0.p0.i64(ptr align 4 %var, ptr align 4 %ref.tmp, i64 8, i1 false), !dbg !23, !DIAssignID !29
+ call void @llvm.dbg.assign(metadata i1 undef, metadata !14, metadata !DIExpression(DW_OP_LLVM_fragment, 32, 32), metadata !29, metadata ptr %var, metadata !DIExpression()), !dbg !20
+ call void @llvm.dbg.assign(metadata i1 undef, metadata !14, metadata !DIExpression(DW_OP_LLVM_fragment, 64, 32), metadata !29, metadata ptr %var, metadata !DIExpression(DW_OP_plus, DW_OP_constu, 4)), !dbg !20
+ %b = getelementptr inbounds %struct.Tuple, ptr %var, i32 0, i32 1, !dbg !31
+ %0 = load i32, ptr %b, align 4, !dbg !31
+ ret i32 %0, !dbg !35
+}
+
+declare !dbg !36 i64 @_Z6getVarv()
+declare void @llvm.dbg.declare(metadata, metadata, metadata)
+declare void @llvm.memcpy.p0.p0.i64(ptr noalias nocapture writeonly, ptr noalias nocapture readonly, i64, i1 immarg)
+declare void @llvm.dbg.assign(metadata, metadata, metadata, metadata, metadata, metadata)
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3, !4, !5, !6, !7}
+!llvm.ident = !{!8}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus_14, file: !1, producer: "clang version 16.0.0", isOptimized: true, runtimeVersion: 0, emissionKind: FullDebug, splitDebugInlining: false, nameTableKind: None)
+!1 = !DIFile(filename: "test.cpp", directory: "/")
+!2 = !{i32 7, !"Dwarf Version", i32 5}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!4 = !{i32 1, !"wchar_size", i32 4}
+!5 = !{i32 8, !"PIC Level", i32 2}
+!6 = !{i32 7, !"PIE Level", i32 2}
+!7 = !{i32 7, !"uwtable", i32 2}
+!8 = !{!"clang version 16.0.0"}
+!9 = distinct !DISubprogram(name: "fun", linkageName: "_Z3funv", scope: !1, file: !1, line: 3, type: !10, scopeLine: 3, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !13)
+!10 = !DISubroutineType(types: !11)
+!11 = !{!12}
+!12 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!13 = !{!14}
+!14 = !DILocalVariable(name: "var", scope: !9, file: !1, line: 4, type: !15)
+!15 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "Tuple", file: !1, line: 1, size: 96, flags: DIFlagTypePassByValue, elements: !16, identifier: "_ZTS4Pair")
+!16 = !{!17, !18, !40}
+!17 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !15, file: !1, line: 1, baseType: !12, size: 32)
+!18 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !15, file: !1, line: 1, baseType: !12, size: 32, offset: 32)
+!19 = distinct !DIAssignID()
+!20 = !DILocation(line: 0, scope: !9)
+!21 = !DILocation(line: 4, column: 3, scope: !9)
+!22 = !DILocation(line: 5, column: 9, scope: !9)
+!23 = !DILocation(line: 5, column: 7, scope: !9)
+!29 = distinct !DIAssignID()
+!30 = !DILocation(line: 5, column: 3, scope: !9)
+!31 = !DILocation(line: 6, column: 14, scope: !9)
+!34 = !DILocation(line: 7, column: 1, scope: !9)
+!35 = !DILocation(line: 6, column: 3, scope: !9)
+!36 = !DISubprogram(name: "getVar", linkageName: "_Z6getVarv", scope: !1, file: !1, line: 2, type: !37, flags: DIFlagPrototyped, spFlags: DISPFlagOptimized, retainedNodes: !39)
+!37 = !DISubroutineType(types: !38)
+!38 = !{!15}
+!39 = !{}
+!40 = !DIDerivedType(tag: DW_TAG_member, name: "c", scope: !15, file: !1, line: 1, baseType: !12, size: 32, offset: 64)
--- /dev/null
+; RUN: opt -S -passes=sroa -sroa-skip-mem2reg %s \
+; RUN: | FileCheck %s --implicit-check-not="call void @llvm.dbg"
+
+;; IR hand-modified, originally generated from:
+;; struct Pair { int a; int b; };
+;; Pair getVar();
+;; int fun() {
+;; Pair var;
+;; var = getVar();
+;; return var.b;
+;; }
+;; Modification: split the dbg.assign linked the the memcpy(64 bits) into two,
+;; each describing a 32 bit fragment.
+;;
+;; Check that assignment tracking updates in SROA work when the store being
+;; split is described with one dbg.assign (covering different fragments). The
+;; store may have been already split and then merged again at some point.
+
+;; Alloca for var.a and associated dbg.assign:
+; CHECK: %var.sroa.0 = alloca i32, align 4, !DIAssignID ![[id_1:[0-9]+]]
+; CHECK-NEXT: call void @llvm.dbg.assign(metadata i1 undef, metadata ![[var:[0-9]+]], metadata !DIExpression(DW_OP_LLVM_fragment, 0, 32), metadata ![[id_1]], metadata ptr %var.sroa.0, metadata !DIExpression())
+
+;; Alloca for var.b and associated dbg.assign:
+; CHECK-NEXT: %var.sroa.1 = alloca i32, align 4, !DIAssignID ![[id_2:[0-9]+]]
+; CHECK-NEXT: call void @llvm.dbg.assign(metadata i1 undef, metadata ![[var]], metadata !DIExpression(DW_OP_LLVM_fragment, 32, 32), metadata ![[id_2]], metadata ptr %var.sroa.1, metadata !DIExpression())
+
+;; Store to var.b (split from store to var) and associated dbg.assigns. The
+;; dbg.assign for the fragment covering the (pre-split) assignment to var.a
+;; should not be linked to the store.
+; CHECK: store i32 %[[v:.*]], ptr %var.sroa.1,{{.*}}!DIAssignID ![[id_3:[0-9]+]]
+; CHECK-NEXT: call void @llvm.dbg.assign(metadata i32 %{{.*var\.sroa\.0.*}}, metadata ![[var]], metadata !DIExpression(DW_OP_LLVM_fragment, 0, 32), metadata ![[id_4:[0-9]+]], metadata ptr %var.sroa.0, metadata !DIExpression())
+; CHECK-NEXT: call void @llvm.dbg.assign(metadata i32 %[[v]], metadata ![[var]], metadata !DIExpression(DW_OP_LLVM_fragment, 32, 32), metadata ![[id_3]], metadata ptr %var.sroa.1, metadata !DIExpression())
+
+; CHECK-DAG: ![[id_1]] = distinct !DIAssignID()
+; CHECK-DAG: ![[id_2]] = distinct !DIAssignID()
+; CHECK-DAG: ![[id_3]] = distinct !DIAssignID()
+; CHECK-DAG: ![[id_4]] = distinct !DIAssignID()
+
+%struct.Pair = type { i32, i32 }
+
+define dso_local noundef i32 @_Z3funv() !dbg !9 {
+entry:
+ %var = alloca %struct.Pair, align 4, !DIAssignID !19
+ call void @llvm.dbg.assign(metadata i1 undef, metadata !14, metadata !DIExpression(), metadata !19, metadata ptr %var, metadata !DIExpression()), !dbg !20
+ %ref.tmp = alloca %struct.Pair, align 4
+ %call = call i64 @_Z6getVarv(), !dbg !22
+ store i64 %call, ptr %ref.tmp, align 4, !dbg !22
+ call void @llvm.memcpy.p0.p0.i64(ptr align 4 %var, ptr align 4 %ref.tmp, i64 8, i1 false), !dbg !23, !DIAssignID !29
+ call void @llvm.dbg.assign(metadata i1 undef, metadata !14, metadata !DIExpression(DW_OP_LLVM_fragment, 0, 32), metadata !29, metadata ptr %var, metadata !DIExpression()), !dbg !20
+ call void @llvm.dbg.assign(metadata i1 undef, metadata !14, metadata !DIExpression(DW_OP_LLVM_fragment, 32, 32), metadata !29, metadata ptr %var, metadata !DIExpression(DW_OP_plus, DW_OP_constu, 4)), !dbg !20
+ %b = getelementptr inbounds %struct.Pair, ptr %var, i32 0, i32 1, !dbg !31
+ %0 = load i32, ptr %b, align 4, !dbg !31
+ ret i32 %0, !dbg !35
+}
+
+declare !dbg !36 i64 @_Z6getVarv()
+declare void @llvm.dbg.declare(metadata, metadata, metadata)
+declare void @llvm.memcpy.p0.p0.i64(ptr noalias nocapture writeonly, ptr noalias nocapture readonly, i64, i1 immarg)
+declare void @llvm.dbg.assign(metadata, metadata, metadata, metadata, metadata, metadata)
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3, !4, !5, !6, !7}
+!llvm.ident = !{!8}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus_14, file: !1, producer: "clang version 16.0.0", isOptimized: true, runtimeVersion: 0, emissionKind: FullDebug, splitDebugInlining: false, nameTableKind: None)
+!1 = !DIFile(filename: "test.cpp", directory: "/")
+!2 = !{i32 7, !"Dwarf Version", i32 5}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!4 = !{i32 1, !"wchar_size", i32 4}
+!5 = !{i32 8, !"PIC Level", i32 2}
+!6 = !{i32 7, !"PIE Level", i32 2}
+!7 = !{i32 7, !"uwtable", i32 2}
+!8 = !{!"clang version 16.0.0"}
+!9 = distinct !DISubprogram(name: "fun", linkageName: "_Z3funv", scope: !1, file: !1, line: 3, type: !10, scopeLine: 3, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !13)
+!10 = !DISubroutineType(types: !11)
+!11 = !{!12}
+!12 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!13 = !{!14}
+!14 = !DILocalVariable(name: "var", scope: !9, file: !1, line: 4, type: !15)
+!15 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "Pair", file: !1, line: 1, size: 64, flags: DIFlagTypePassByValue, elements: !16, identifier: "_ZTS4Pair")
+!16 = !{!17, !18}
+!17 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !15, file: !1, line: 1, baseType: !12, size: 32)
+!18 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !15, file: !1, line: 1, baseType: !12, size: 32, offset: 32)
+!19 = distinct !DIAssignID()
+!20 = !DILocation(line: 0, scope: !9)
+!21 = !DILocation(line: 4, column: 3, scope: !9)
+!22 = !DILocation(line: 5, column: 9, scope: !9)
+!23 = !DILocation(line: 5, column: 7, scope: !9)
+!29 = distinct !DIAssignID()
+!30 = !DILocation(line: 5, column: 3, scope: !9)
+!31 = !DILocation(line: 6, column: 14, scope: !9)
+!34 = !DILocation(line: 7, column: 1, scope: !9)
+!35 = !DILocation(line: 6, column: 3, scope: !9)
+!36 = !DISubprogram(name: "getVar", linkageName: "_Z6getVarv", scope: !1, file: !1, line: 2, type: !37, flags: DIFlagPrototyped, spFlags: DISPFlagOptimized, retainedNodes: !39)
+!37 = !DISubroutineType(types: !38)
+!38 = !{!15}
+!39 = !{}