Skip to content

[InstrRef] Skip clobbered EntryValue register recovery #142478

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 27, 2025

Conversation

rastogishubham
Copy link
Contributor

This changes the final stage of InstrRef, i.e. the TransferTracker (which combines the values locations with the variable values), so that it treats a DEBUG_VALUE of an EntryValue just like a DEBUG_VALUE of a constant: a location that is never clobbered and can be propagated to subsequent BBs as long as no other DEBUG_VALUE intrinsics updated the variable.

We add two tests here:

  1. entry_value_clobbered_stack_copy that saves a register on the stack, uses this register as an entry value DBG_VALUE location, and then clobbers it. Prior to this patch, this test would crash because we would try to describe a new location for the variable in terms of what was saved on the stack, and use an invalid expression to do so. This is not needed as an EntryValue can never be clobbered.

  2. entry_value_gets_propagated, that tests that an EntryValue DBG_VALUE is propagated in a diamond-shaped CFG.

This patch is trying to reland #77938 but also fixes the bug with InstrRef based LiveDebugValues, where entry values were not being propagated in a diamond-shaped CFG.

@llvmbot
Copy link
Member

llvmbot commented Jun 2, 2025

@llvm/pr-subscribers-debuginfo

Author: Shubham Sandeep Rastogi (rastogishubham)

Changes

This changes the final stage of InstrRef, i.e. the TransferTracker (which combines the values locations with the variable values), so that it treats a DEBUG_VALUE of an EntryValue just like a DEBUG_VALUE of a constant: a location that is never clobbered and can be propagated to subsequent BBs as long as no other DEBUG_VALUE intrinsics updated the variable.

We add two tests here:

  1. entry_value_clobbered_stack_copy that saves a register on the stack, uses this register as an entry value DBG_VALUE location, and then clobbers it. Prior to this patch, this test would crash because we would try to describe a new location for the variable in terms of what was saved on the stack, and use an invalid expression to do so. This is not needed as an EntryValue can never be clobbered.

  2. entry_value_gets_propagated, that tests that an EntryValue DBG_VALUE is propagated in a diamond-shaped CFG.

This patch is trying to reland #77938 but also fixes the bug with InstrRef based LiveDebugValues, where entry values were not being propagated in a diamond-shaped CFG.


Full diff: https://github.com/llvm/llvm-project/pull/142478.diff

3 Files Affected:

  • (modified) llvm/lib/CodeGen/LiveDebugValues/InstrRefBasedImpl.cpp (+11-1)
  • (added) llvm/test/DebugInfo/MIR/InstrRef/entry_value_clobbered_stack_copy.mir (+50)
  • (added) llvm/test/DebugInfo/MIR/InstrRef/entry_value_gets_propagated.mir (+91)
diff --git a/llvm/lib/CodeGen/LiveDebugValues/InstrRefBasedImpl.cpp b/llvm/lib/CodeGen/LiveDebugValues/InstrRefBasedImpl.cpp
index fdf50188fbcd8..85ecfebb0cf68 100644
--- a/llvm/lib/CodeGen/LiveDebugValues/InstrRefBasedImpl.cpp
+++ b/llvm/lib/CodeGen/LiveDebugValues/InstrRefBasedImpl.cpp
@@ -668,6 +668,16 @@ class TransferTracker {
 
     auto &[Var, DILoc] = DVMap.lookupDVID(VarID);
 
+    // If the expression is a DW_OP_entry_value, emit the variable location
+    // as-is.
+    if (DIExpr->isEntryValue()) {
+      Register Reg = MTracker->LocIdxToLocID[Num.getLoc()];
+      MachineOperand MO = MachineOperand::CreateReg(Reg, false);
+      PendingDbgValues.push_back(std::make_pair(
+          VarID, &*emitMOLoc(MO, Var, {DIExpr, Prop.Indirect, false})));
+      return true;
+    }
+
     // Is the variable appropriate for entry values (i.e., is a parameter).
     if (!isEntryValueVariable(Var, DIExpr))
       return false;
@@ -694,7 +704,7 @@ class TransferTracker {
     DebugVariableID VarID = DVMap.getDVID(Var);
 
     // Ignore non-register locations, we don't transfer those.
-    if (MI.isUndefDebugValue() ||
+    if (MI.isUndefDebugValue() || MI.getDebugExpression()->isEntryValue() ||
         all_of(MI.debug_operands(),
                [](const MachineOperand &MO) { return !MO.isReg(); })) {
       auto It = ActiveVLocs.find(VarID);
diff --git a/llvm/test/DebugInfo/MIR/InstrRef/entry_value_clobbered_stack_copy.mir b/llvm/test/DebugInfo/MIR/InstrRef/entry_value_clobbered_stack_copy.mir
new file mode 100644
index 0000000000000..3461c40f5ad8c
--- /dev/null
+++ b/llvm/test/DebugInfo/MIR/InstrRef/entry_value_clobbered_stack_copy.mir
@@ -0,0 +1,50 @@
+# RUN: llc --run-pass=livedebugvalues -o - %s | FileCheck %s
+# REQUIRES: x86-registered-target
+
+--- |
+  target triple = "x86_64-"
+  define void @foo(ptr swiftasync %0) !dbg !4 {
+    call void @llvm.dbg.value(metadata ptr %0, metadata !9, metadata !DIExpression(DW_OP_LLVM_entry_value, 1)), !dbg !17
+    ret void
+  }
+  declare void @llvm.dbg.value(metadata, metadata, metadata)
+
+  !llvm.module.flags = !{!0}
+  !llvm.dbg.cu = !{!1}
+
+  !0 = !{i32 2, !"Debug Info Version", i32 3}
+  !1 = distinct !DICompileUnit(language: DW_LANG_Swift, file: !2, producer: "blah", isOptimized: true, flags: "blah", runtimeVersion: 5, emissionKind: FullDebug)
+  !2 = !DIFile(filename: "blah", directory: "blah")
+  !3 = !{}
+  !4 = distinct !DISubprogram(name: "blah", linkageName: "blah", scope: !2, file: !2, line: 284, type: !7, unit: !1)
+  !7 = !DISubroutineType(types: !3)
+  !9 = !DILocalVariable(name: "self", arg: 3, scope: !4, file: !2, line: 328, type: !12, flags: DIFlagArtificial)
+  !12 = !DICompositeType(tag: DW_TAG_structure_type, name: "blah", scope: !2, file: !2, size: 64, elements: !3)
+  !17 = !DILocation(line: 328, column: 17, scope: !4)
+
+...
+---
+name:            foo
+alignment:       16
+debugInstrRef:   true
+tracksDebugUserValues: true
+liveins:
+  - { reg: '$r14', virtual-reg: '' }
+stack:
+  - { id: 0, name: '', type: spill-slot, offset: -64, size: 8, alignment: 8,
+      stack-id: default, callee-saved-register: '', callee-saved-restored: true,
+      debug-info-variable: '', debug-info-expression: '', debug-info-location: '' }
+body:             |
+  bb.0:
+    liveins: $r14
+    ; Put a copy of r14 on the stack.
+    MOV64mr $rbp, 1, $noreg, -48, $noreg, $r14 :: (store (s64) into %stack.0)
+    DBG_VALUE $r14, $noreg, !9, !DIExpression(DW_OP_LLVM_entry_value, 1), debug-location !17
+    MOV64mi32 $noreg, 1, $noreg, 0, $noreg, 0, debug-location !17 :: (store (s64) into `ptr null`)
+    $r14 = MOV64rr killed $r13
+    ; Clobber $r14
+    RETI64 24
+# CHECK: bb.0:
+# CHECK:      MOV64mr $rbp, 1, $noreg, -48, $noreg, $r14
+# CHECK-NEXT: DBG_VALUE $r14, {{.*}}, !DIExpression(DW_OP_LLVM_entry_value, 1)
+# CHECK-NOT:  DBG_VALUE
\ No newline at end of file
diff --git a/llvm/test/DebugInfo/MIR/InstrRef/entry_value_gets_propagated.mir b/llvm/test/DebugInfo/MIR/InstrRef/entry_value_gets_propagated.mir
new file mode 100644
index 0000000000000..292aa2d9c0a30
--- /dev/null
+++ b/llvm/test/DebugInfo/MIR/InstrRef/entry_value_gets_propagated.mir
@@ -0,0 +1,91 @@
+# RUN: llc --run-pass=livedebugvalues -o - %s | FileCheck %s
+# REQUIRES: x86-registered-target
+--- |
+  target triple = "x86_64-"
+
+  define i32 @baz(i32 swiftasync %arg1, i32 noundef %arg2, i1 %cond) !dbg !9 {
+    tail call void @llvm.dbg.value(metadata i32 %arg1, metadata !17, metadata !DIExpression(DW_OP_LLVM_entry_value, 1)), !dbg !19
+    br i1 %cond, label %if.then, label %if.else, !dbg !22
+  if.then:
+    %call = call i32 @foo(i32 noundef %arg1), !dbg !23
+    br label %if.end, !dbg !25
+  if.else:
+    %call1 = call i32 @foo(i32 noundef %arg2), !dbg !26
+    br label %if.end
+  if.end:
+    %temp.0 = phi i32 [ %call, %if.then ], [ %call1, %if.else ], !dbg !28
+    ret i32 %temp.0, !dbg !29
+  }
+
+  declare i32 @foo(i32)
+  declare void @llvm.dbg.value(metadata, metadata, metadata)
+
+  !llvm.dbg.cu = !{!0}
+  !llvm.module.flags = !{!2, !3}
+
+  !0 = distinct !DICompileUnit(language: DW_LANG_C11, file: !1, producer: "ha", isOptimized: true, runtimeVersion: 0, emissionKind: FullDebug)
+  !1 = !DIFile(filename: "test.c", directory: "hah")
+  !2 = !{i32 7, !"Dwarf Version", i32 4}
+  !3 = !{i32 2, !"Debug Info Version", i32 3}
+  !9 = distinct !DISubprogram(name: "baz", scope: !1, file: !1, line: 3, type: !10, scopeLine: 3, unit: !0, retainedNodes: !13)
+  !10 = !DISubroutineType(types: !11)
+  !11 = !{!12, !12, !12, !12}
+  !12 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+  !13 = !{!14, !15, !16, !17}
+  !14 = !DILocalVariable(name: "arg1", arg: 1, scope: !9, file: !1, line: 3, type: !12)
+  !15 = !DILocalVariable(name: "arg2", arg: 2, scope: !9, file: !1, line: 3, type: !12)
+  !16 = !DILocalVariable(name: "cond", arg: 3, scope: !9, file: !1, line: 3, type: !12)
+  !17 = !DILocalVariable(name: "local", scope: !9, file: !1, line: 4, type: !12)
+  !19 = !DILocation(line: 0, scope: !9)
+  !20 = !DILocation(line: 7, column: 7, scope: !21)
+  !21 = distinct !DILexicalBlock(scope: !9, file: !1, line: 7, column: 7)
+  !22 = !DILocation(line: 7, column: 7, scope: !9)
+  !23 = !DILocation(line: 8, column: 12, scope: !24)
+  !24 = distinct !DILexicalBlock(scope: !21, file: !1, line: 7, column: 13)
+  !25 = !DILocation(line: 9, column: 3, scope: !24)
+  !26 = !DILocation(line: 10, column: 12, scope: !27)
+  !27 = distinct !DILexicalBlock(scope: !21, file: !1, line: 9, column: 10)
+  !28 = !DILocation(line: 0, scope: !21)
+  !29 = !DILocation(line: 13, column: 3, scope: !9)
+
+...
+---
+name:            baz
+alignment:       16
+debugInstrRef: true
+tracksDebugUserValues: true
+liveins:
+  - { reg: '$r14', virtual-reg: '' }
+  - { reg: '$edi', virtual-reg: '' }
+  - { reg: '$esi', virtual-reg: '' }
+  - { reg: '$edx', virtual-reg: '' }
+body:             |
+  bb.0:
+    successors: %bb.2(0x40000000), %bb.1(0x40000000)
+    liveins: $r14, $edi, $edx, $esi
+    DBG_VALUE $r14, $noreg, !14, !DIExpression(DW_OP_LLVM_entry_value, 1), debug-location !19
+    $r14 = MOV64ri 0, debug-location !20
+    CMP32ri killed renamable $edx, 0, implicit-def $eflags, debug-location !20
+    JCC_1 %bb.2, 4, implicit killed $eflags, debug-location !22
+  bb.1.if.then:
+    successors: %bb.3(0x80000000)
+    liveins: $edi, $r13
+    CALL64pcrel32 @foo, csr_64, implicit $rsp, implicit $ssp, implicit $edi, implicit-def $eax, debug-location !23
+    JMP_1 %bb.3, debug-location !25
+  bb.2.if.else:
+    successors: %bb.3(0x80000000)
+    liveins: $esi, $r13
+    $edi = MOV32rr killed $esi, debug-location !26
+    CALL64pcrel32 @foo, csr_64, implicit $rsp, implicit $ssp, implicit $edi, implicit-def $eax, debug-location !26
+  bb.3.if.end:
+    liveins: $eax
+    $rbp = frame-destroy POP64r implicit-def $rsp, implicit $rsp, debug-location !29
+    RET64 implicit $eax, debug-location !29
+# CHECK-LABEL: bb.0:
+# CHECK: DBG_VALUE $r14, {{.*}}, !DIExpression(DW_OP_LLVM_entry_value, 1)
+# CHECK-LABEL: bb.1.if.then:
+# CHECK: DBG_VALUE $r14, {{.*}}, !DIExpression(DW_OP_LLVM_entry_value, 1)
+# CHECK-LABEL: bb.2.if.else:
+# CHECK: DBG_VALUE $r14, {{.*}}, !DIExpression(DW_OP_LLVM_entry_value, 1)
+# CHECK-LABEL: bb.3.if.end:
+# CHECK: DBG_VALUE $r14, {{.*}}, !DIExpression(DW_OP_LLVM_entry_value, 1)
\ No newline at end of file

Copy link
Contributor

@felipepiovezan felipepiovezan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This LGTM, but it would be nice to hear from Jeremy as well, as it's been a while since I touched this code!

Copy link
Member

@jmorse jmorse left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've got some nits inline, otherwise this looks good.

I suppose this works slightly differently to how I envisaged in past PRs, where the entry-value-ness of a variable location would be transformed into the value-numbers used to propagate variable-values around the function. I guess the differences are:

  • In my mental model, LiveDebugValues would always work out that a value is an entry-value from first principles and add DW_OP_entry_value, requiring us to produce an expression with DW_OP_entry_value removed from it somewhere,
  • In this model, the variable value with entry-value expression gets propagated around the function. We then don't try to re-generate the entry-value-expression when the location gets clobbered, because it's already an entry value.

This is fine -- with the side-effect that I reckon entry-value variable locations will be re-stated in certain basic blocks with clobbers. Your adjustment to TransferTracker stops the variable location (with DW_OP_entry_value) from being tracked in the transfer tracker when an entry-value DBG_VALUE is encountered -- however at the start of each block TransferTracker::loadInLocs takes a collection of variable values and directly tracks them. You might need to filter out entry-value-expression variable values there too.

As an example, note line 73 in entry_value_gets_propagated.mir that I've marked. I reckon if you sink the clobber of 14 to that block:

  • You'll have an entry-value DBG_VALUE propagated to bb.1 as expected,
  • TransferTracker will track that variable as a normal live-location,
  • When r14 is clobbered in bb1, TransferTracker will try to recover the variable location, and emit another entry-value DBG_VALUE after the clobber.

This isn't incorrect, and I reckon it won't affect the variable location output at all, but you might want to tighten it up by putting a filter in loadVarInLoc or similar. (My confidence level on this: about 93%, I could be wrong).

@rastogishubham
Copy link
Contributor Author

rastogishubham commented Jun 4, 2025

@jmorse Thanks for the feedback and the detailed explanation.

You are correct, if we move the clobber to line 73 as you mentioned, we do see two DBG_VALUEs. I think filtering becomes complicated. We would have to filter only in the case where there is a clobber to the same register as the entry_value somewhere in the basic block. However, we want to ensure we don't filter for other basic blocks that don't get affected by the clobber at all, such as in a diamond cfg.

Screenshot 2025-06-04 at 4 53 07 PM

If we look at the screenshot above, if the basic block 2 has the clobber, then 2 and 4 will have to propagate the entry_value via recovering the DBG_VALUE because of a clobber, however, 3 should propagate the entry value in the standard case due to TransferTracker::loadInLocs. So we only have to filter in the case where the basic block or one of it's parents have a clobber in them.

Now, we could just leave it as-is, but I don't like that either. So I went a different direction. If we just treat an entry_value as a constant, something like a DBG_VALUE 1, $noreg, !1, debug-location !2 then it will be propagated without an issue.

What do you think?

Note: I have the new patch as a second commit to make it easier to review, I will squash the commits before I land it

@rastogishubham
Copy link
Contributor Author

@jmorse Just pinging you again, can you please take a look at the patch again to make sure this way of handling entry-values looks good?

@jmorse
Copy link
Member

jmorse commented Jun 9, 2025

Hmmm, I'm less of a fan of the most recent refinement -- for the base patch, I think it's reasonable to use an entry-value DIExpression as part of the variable-value being propagated around a function, as it's easily understandable. Putting a register-location into the set of constant-value operands is much more confusing and moderate hack, when the pass is designed to avoid tracking register-locations until the last moment.

I don't believe a filter in loadInLocs would be so hard (I appreciate this is all pretty much undocumented). We can rely on two things:

  • Transfer tracker runs after all inter-block analysis has completed, each variable-value at the start of each block has been fixed. So each block can be considered in isolation,
  • The code that interprets the output of LiveDebugValues (DbgEntityHistoryCalculator) already has carve-outs for entry-value variable locations (it ignores clobbers to any registers they refer to), and it only considers locations on an individual block basis.

Following the diamond-diagram, assuming we have a DBG_VALUE setting a variable to an entry-value expression in block 1, and no other DBG_VALUEs for the variable, we'll begin blocks 2, 3, and 4 with a DbgValue struct recording the ValueIDNum for the entry value, and an expression. In each block one of two things happens:

  • If the entry value is no longer available in any register, we take the recoverAsEntryValue path out of loadVarInLoc, produce a DBG_VALUE with an entry-value expression, and don't record anything to be tracked,
  • If the entry value is available in a register, we get to the end of loadVarInLoc and produce a DBG_VALUE with an entry-value expression, and we record the location to be tracked.

It's the tracking at the end of the 2nd item that can be filtered: I think it's just a case of guarding the insertion to ActiveVLocs.

Through all the blocks, DbgEntityHistoryCalculator will keep the entry-value locations alive regardless of any clobbers. The only thing that'll make it change the variable location is if there's another DBG_VALUE for the same variable.

@rastogishubham
Copy link
Contributor Author

rastogishubham commented Jun 13, 2025

Hi @jmorse

Thanks for the explanation, however, I have two questions:

  1. If we move the clobber to the second basic block in the example MIR test case shown. We get to the end of loadVarInLoc and track the location before we see the clobber, which then takes the recoverAsEntryValue path. How would the filtering work in that case? What is the condition to guard against the insertion into loadVarInLoc? We only want to do it in the case there is a clobber in the basic block right, but that is detected later. Unless, the idea is to remove the entry in ActiveVLocs once the clobber is detected.

  2. In clobberMLoc we have the loop

for (DebugVariableID VarID : ActiveMLocIt->second) {
        auto &Prop = ActiveVLocs.find(VarID)->second.Properties;
        recoverAsEntryValue(VarID, Prop, OldValue);
      }

The VarID that has been filtered from ActiveVLocs will still exist in ActiveMLocs and therefore cause a crash. I think we need to do more than just filter it from ActiveVLocs

@jmorse
Copy link
Member

jmorse commented Jun 16, 2025

If we move the clobber to the second basic block in the example MIR test case shown. We get to the end of loadVarInLoc and track the location before we see the clobber, which then takes the recoverAsEntryValue path. How would the filtering work in that case? What is the condition to guard against the insertion into loadVarInLoc? We only want to do it in the case there is a clobber in the basic block right, but that is detected later. Unless, the idea is to remove the entry in ActiveVLocs once the clobber is detected.

For this I believe we can make use of the differences between LDV and how DbgEntityHistoryCalculator in the asm-printing phase works. DbgEntityHistoryCalculator only sees DBG_* instructions, LDV is effectively communicating with it through the creation of DBG_* instructions, with the internal state of TransferTracker helping to decide what DBG_* instructions to produce. We know several things about locations with entry-value-expressions at the start of a block:

  • The "Value" that they correspond to isn't really loaded into a stack/register location, the register is effectively a dummy,
  • DbgEntityHistoryCalculator will ignore any clobbers of registers for a variable-location that's an entry value

Thus, there's actually no difference in DBG_* instructions we want created for entry-value locations in a block, regardless of whether there's a clobber or not. Looking closely at that block, without a clobber:

  bb.1.if.then:
    successors: %bb.3(0x80000000)
    liveins: $edi, $r13
    CALL64pcrel32 @foo, csr_64, implicit $rsp, implicit $ssp, implicit $edi, implicit-def $eax, debug-location !23
    JMP_1 %bb.3, debug-location !25

If we start this block with an entry value, we'll create something like DBG_VALUE $r14, $noreg, !9, !DIExpression(DW_OP_LLVM_entry_value, 1), to tell DbgEntityHistoryCalculator about it, and there's no further need to consider r14 in the block. If we have a clobber in the block,

  bb.1.if.then:
    successors: %bb.3(0x80000000)
    liveins: $edi, $r13
    CALL64pcrel32 @foo, csr_64, implicit $rsp, implicit $ssp, implicit $edi, implicit-def $eax,  debug-location !15
    $r14 = MOV64ri 0,  debug-location !DILocation(line: 7, column: 7, scope: !17)
    JMP_1 %bb.3,  debug-location !18

Then we'll start the block by creating the same DBG_VALUE with an entry-value expression. And when we get to the clobber of $r14, we know that DbgEntityHistoryCalculator will ignore the clobbering of r14 for any entry-value expressions at that location. Thus there's no need to produce any different DBG_* instructions in that block even though there's a clobber. Finally if we consider a scenario where the variable location is modified in the block:

  bb.1.if.then:
    successors: %bb.3(0x80000000)
    liveins: $edi, $r13
    CALL64pcrel32 @foo, csr_64, implicit $rsp, implicit $ssp, implicit $edi, implicit-def $eax,  debug-location !15
    DBG_INSTR_REF !9, !DIExpression(), [other args?]
    $r14 = MOV64ri 0,  debug-location !DILocation(line: 7, column: 7, scope: !17)
    JMP_1 %bb.3,  debug-location !18

The first thing TransferTracker will do on encountering the DBG_INSTR_REF is remove the previous location of the variable from its tracking maps. Regardless of whether the DBG_INSTR_REF is before or after the clobber.

Thus: I think the only thing we need to do for an entry-value location at the start of the block is produce the initial DBG_VALUE for it, there's no need to enter it into the value tracking maps through the block.

The VarID that has been filtered from ActiveVLocs will still exist in ActiveMLocs and therefore cause a crash. I think we need to do more than just filter it from ActiveVLocs

Ah, right you are, we would need to not enter the information into both ActiveVLocs and ActiveMLocs. I think both those things are handled at the end of loadVarInloc, we'd just need to emit the DBG_VALUE first and then early-exit if it's an entry-value location?

@jmorse
Copy link
Member

jmorse commented Jun 16, 2025

I suppose one of the awkward things about this area is how the entry-value flag affects different pieces of code -- in an ideal world we would represent entry-value locations in some /other/ way than "a reference to a register that isn't really a reference". But this is how it works today!

@adrian-prantl
Copy link
Collaborator

  • If the entry value is no longer available in any register, we take the recoverAsEntryValue path out of loadVarInLoc, produce a DBG_VALUE with an entry-value expression, and don't record anything to be tracked,

@jmorse Maybe we're using slightly different terminology here, but to me an entry value is not something that can be clobbered. It's an abstract concept that refers to a the value a register has at the beginning of a function that is divorced from the register inside the function. To an entry value of r0, it doesn't matter that r0 gets clobbered, because it doesn't refer to the current value of r0. That's why I think it makes more sense to treat incoming entry values similar to constants. They are not affected by clobbers.

@jmorse
Copy link
Member

jmorse commented Jun 19, 2025

I guess I can see how that works -- this is becoming complicated because we're trying to bridge a constant-like-location that lives outside of "value liveness" and similar concepts, with portions of the pass that really want to consider liveness. Hence there are several bits of code that stop working well together. I also suppose you might say I'm letting perfect be the enemy of good here, and it's better to have something that does work versus creating more complexity.

Part of the problem is that I can put my finger exactly on what "perfect" (to me) looks like -- instr-ref already supports describing entry values and recovering variable-locations at the appropriate time, what's abnormal here is that the variable-locations entering LiveDebugValues already have DW_OP_LLVM_entry_value attached to them. I've cooked up a prototype here [0] that "decompiles" the entry-value flag out of the DIExpression and enters the entry-value-variable-location into the usual tracking processes. The result is here [1] (input file then output file), where the entry-value-location gets propagated around the block like any other variable. However, LiveDebugValues prefers to create references to the live value in a register if it's available, instead of everything being an entry-value location.

Is having an entry-value expression in all the variable locations, right from the entry block, part of the requirements here? (I'm assuming this is something swift-specific as AFAIUI LLVM doesn't generate these expressions).

If we can't take the "let LiveDebugValues generate entry-values itself" route, I suppose it's cleanest to fall back on the "treat entry-values like a constant" and admit their register-locations to the operand store, rather than push the complexity into other bit sof code.

[0] jmorse@68635db
[1] https://gist.github.com/jmorse/4e74f69f8cfa9b6a07d7c2972b3e4963

@adrian-prantl
Copy link
Collaborator

I guess I can see how that works -- this is becoming complicated because we're trying to bridge a constant-like-location that lives outside of "value liveness" and similar concepts, with portions of the pass that really want to consider liveness. Hence there are several bits of code that stop working well together. I also suppose you might say I'm letting perfect be the enemy of good here, and it's better to have something that does work versus creating more complexity.

Part of the problem is that I can put my finger exactly on what "perfect" (to me) looks like -- instr-ref already supports describing entry values and recovering variable-locations at the appropriate time, what's abnormal here is that the variable-locations entering LiveDebugValues already have DW_OP_LLVM_entry_value attached to them. I've cooked up a prototype here [0] that "decompiles" the entry-value flag out of the DIExpression and enters the entry-value-variable-location into the usual tracking processes. The result is here [1] (input file then output file), where the entry-value-location gets propagated around the block like any other variable. However, LiveDebugValues prefers to create references to the live value in a register if it's available, instead of everything being an entry-value location.

One thing that is important to point out here is that there are different kinds of entry value that may need different treatment.

  1. The entry values that are incoming in LDV are produced by the LLVM CoroSplit pass and they are needed to recover variables in virtual coroutine parent frames (see https://llvm.org/devmtg/2023-10/slides/quicktalks/Prantl-DebugInfoForConcurrency.pdf). A debugger can only make use of them if they are entry values.

  2. LDV itself should recover unavailable variables by introducing new entry values where applicable. Here entry entry values should be the last resort and be replaced by other locations where possible

I believe that (1) should be treated like a constant and just remain undisturbed by LDV. This doesn't prevent us from doing something more clever with (2).

Is having an entry-value expression in all the variable locations, right from the entry block, part of the requirements here? (I'm assuming this is something swift-specific as AFAIUI LLVM doesn't generate these expressions).

If we can't take the "let LiveDebugValues generate entry-values itself" route, I suppose it's cleanest to fall back on the "treat entry-values like a constant" and admit their register-locations to the operand store, rather than push the complexity into other bit sof code.

[0] jmorse@68635db [1] https://gist.github.com/jmorse/4e74f69f8cfa9b6a07d7c2972b3e4963

@rastogishubham
Copy link
Contributor Author

@jmorse @adrian-prantl

I got the patch working how Jeremy wanted. We now use the recoverAsEntryValue path if the location is not available at the start of the block, however, we also filter the ActiveVLocs and the ActiveMLocs if the location is available at the start of the block

Copy link
Member

@jmorse jmorse left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see how we end up with two kinds of entry value (i.e. pre and post LDV) that need to be supported. I'd much prefer it if there wasn't, but it's got a clear use case.

With that in mind, I suppose it's less important to fixate on whether the locations are considered constants or not; either the current patch is fine or the original -- although for the original I think we'd need some more comments on what the operand-store might contain ("Register locations entered into the operand store are considered constant-entry-values" or something like that).

@@ -0,0 +1,53 @@
# RUN: llc --run-pass=livedebugvalues -o - %s | FileCheck %s
# REQUIRES: x86-registered-target
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one also needs to be moved.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh that's my bad, this is a duplicate

@@ -0,0 +1,98 @@
# RUN: llc --run-pass=livedebugvalues -o - %s | FileCheck %s --implicit-check-not=DBG_VALUE
# REQUIRES: x86-registered-target

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And this one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also a duplicate

Copy link
Collaborator

@adrian-prantl adrian-prantl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM apart from moving the MIR tests into the right folder.

This changes the final stage of InstrRef, i.e. the TransferTracker (which
combines the values locations with the variable values), so that it treats a
DEBUG_VALUE of an EntryValue just like a DEBUG_VALUE of a constant: a location
that is never clobbered and can be propagated to subsequent BBs as long as no
other DEBUG_VALUE intrinsics updated the variable.

We add two tests here:

1. `entry_value_clobbered_stack_copy` that saves a register on the stack, uses
this register as an entry value DBG_VALUE location, and then clobbers it. Prior
to this patch, this test would crash because we would try to describe a new
location for the variable in terms of what was saved on the stack, and use an
invalid expression to do so. This is not needed as an EntryValue can never be
clobbered.

2. `entry_value_gets_propagated`, that tests that an EntryValue DBG_VALUE is
propagated in a diamond-shaped CFG.

This patch is trying to reland llvm#77938
but also fixes the bug with InstrRef based LiveDebugValues, where entry
values were not being propagated in a diamond-shaped CFG.
@rastogishubham rastogishubham merged commit 32ef4ce into llvm:main Jun 27, 2025
7 checks passed
@rastogishubham rastogishubham deleted the EntryValFixInstrRef branch June 27, 2025 17:30
rastogishubham added a commit to rastogishubham/llvm-project that referenced this pull request Jun 27, 2025
This changes the final stage of InstrRef, i.e. the TransferTracker
(which combines the values locations with the variable values), so that
it treats a DEBUG_VALUE of an EntryValue just like a DEBUG_VALUE of a
constant: a location that is never clobbered and can be propagated to
subsequent BBs as long as no other DEBUG_VALUE intrinsics updated the
variable.

We add two tests here:

1. `entry_value_clobbered_stack_copy` that saves a register on the
stack, uses this register as an entry value DBG_VALUE location, and then
clobbers it. Prior to this patch, this test would crash because we would
try to describe a new location for the variable in terms of what was
saved on the stack, and use an invalid expression to do so. This is not
needed as an EntryValue can never be clobbered.

2. `entry_value_gets_propagated`, that tests that an EntryValue
DBG_VALUE is propagated in a diamond-shaped CFG.

This patch is trying to reland
llvm#77938 but also fixes the bug
with InstrRef based LiveDebugValues, where entry values were not being
propagated in a diamond-shaped CFG.

(cherry picked from commit 32ef4ce)
rastogishubham added a commit to rastogishubham/llvm-project that referenced this pull request Jun 27, 2025
This changes the final stage of InstrRef, i.e. the TransferTracker
(which combines the values locations with the variable values), so that
it treats a DEBUG_VALUE of an EntryValue just like a DEBUG_VALUE of a
constant: a location that is never clobbered and can be propagated to
subsequent BBs as long as no other DEBUG_VALUE intrinsics updated the
variable.

We add two tests here:

1. `entry_value_clobbered_stack_copy` that saves a register on the
stack, uses this register as an entry value DBG_VALUE location, and then
clobbers it. Prior to this patch, this test would crash because we would
try to describe a new location for the variable in terms of what was
saved on the stack, and use an invalid expression to do so. This is not
needed as an EntryValue can never be clobbered.

2. `entry_value_gets_propagated`, that tests that an EntryValue
DBG_VALUE is propagated in a diamond-shaped CFG.

This patch is trying to reland
llvm#77938 but also fixes the bug
with InstrRef based LiveDebugValues, where entry values were not being
propagated in a diamond-shaped CFG.

(cherry picked from commit 32ef4ce)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants