[llvm] [CodeGen] Fix fixupKills incorrectly killing sub-registers via super-register implicit defs (PR #181518)

Brian Cain via llvm-commits llvm-commits at lists.llvm.org
Sun Feb 15 16:01:01 PST 2026


https://github.com/androm3da updated https://github.com/llvm/llvm-project/pull/181518

>From 339bce09ef2f228c5eedc157a13dfdcc79ff7a8a Mon Sep 17 00:00:00 2001
From: Brian Cain <brian.cain at oss.qualcomm.com>
Date: Sat, 14 Feb 2026 16:56:56 -0800
Subject: [PATCH 1/3] [CodeGen] Fix fixupKills incorrectly killing
 sub-registers via super-register implicit defs
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

fixupKills() walks backward through instructions, removing defined
registers from a LiveRegUnits set, then checking which used registers
are "available" (dead) to set kill flags. When an instruction defines
a sub-register (e.g. $r1) and has an implicit-def of its
super-register ($d0), removeReg($d0) clears the register units of
all sub-registers — including siblings like $r0 that may still be
live. This causes available($r0) to incorrectly return true, setting
a wrong kill flag.

Skip removeReg for implicit defs of super-registers when a
sub-register is also defined by the same instruction. The implicit
def is an annotation of partial modification, not a full
redefinition of the super-register.
---
 llvm/lib/CodeGen/ScheduleDAGInstrs.cpp        | 20 +++++++++++
 .../Hexagon/post-ra-kill-superreg-def.mir     | 36 +++++++++++++++++++
 2 files changed, 56 insertions(+)
 create mode 100644 llvm/test/CodeGen/Hexagon/post-ra-kill-superreg-def.mir

diff --git a/llvm/lib/CodeGen/ScheduleDAGInstrs.cpp b/llvm/lib/CodeGen/ScheduleDAGInstrs.cpp
index 9662511e584c0..4dbb348ca5c22 100644
--- a/llvm/lib/CodeGen/ScheduleDAGInstrs.cpp
+++ b/llvm/lib/CodeGen/ScheduleDAGInstrs.cpp
@@ -1163,6 +1163,26 @@ void ScheduleDAGInstrs::fixupKills(MachineBasicBlock &MBB) {
         Register Reg = MO.getReg();
         if (!Reg)
           continue;
+        // Skip implicit defs of super-registers when a sub-register is
+        // also defined by this instruction. The implicit def indicates a
+        // partial modification of the super-register, not a full
+        // redefinition. Removing the super-register from the live set
+        // would incorrectly clear the liveness of sibling sub-registers
+        // that may still be live, causing toggleKills to set wrong kill
+        // flags on their uses.
+        if (MO.isImplicit()) {
+          bool HasSubRegDef = false;
+          for (ConstMIBundleOperands O2(MI); O2.isValid(); ++O2) {
+            if (!O2->isReg() || !O2->isDef() || !O2->getReg())
+              continue;
+            if (O2->getReg() != Reg && TRI->isSubRegister(Reg, O2->getReg())) {
+              HasSubRegDef = true;
+              break;
+            }
+          }
+          if (HasSubRegDef)
+            continue;
+        }
         LiveRegs.removeReg(Reg);
       } else if (MO.isRegMask()) {
         LiveRegs.removeRegsNotPreserved(MO.getRegMask());
diff --git a/llvm/test/CodeGen/Hexagon/post-ra-kill-superreg-def.mir b/llvm/test/CodeGen/Hexagon/post-ra-kill-superreg-def.mir
new file mode 100644
index 0000000000000..1d899fcdacf30
--- /dev/null
+++ b/llvm/test/CodeGen/Hexagon/post-ra-kill-superreg-def.mir
@@ -0,0 +1,36 @@
+# RUN: llc -mtriple=hexagon -mcpu=hexagonv60 -run-pass post-RA-sched \
+# RUN:   -verify-machineinstrs -o - %s | FileCheck %s
+
+# The fixupKills() function in ScheduleDAGInstrs walks backward through
+# instructions, maintaining a LiveRegUnits bitvector. When an instruction
+# defines a sub-register ($r1) and has an implicit-def of a super-register
+# ($d0), the def processing calls removeReg($d0), which clears the register
+# units of all sub-registers of $d0 — including $r0. If $r0 is live (used
+# by a subsequent instruction), its liveness is incorrectly cleared, causing
+# available($r0) to return true and a wrong kill flag to be set.
+#
+# In this test, A2_abs defines $r1 with an implicit-def of $d0. The
+# subsequent A2_add uses both $r0 and $r1, so $r0 must not be killed
+# on the A2_abs instruction.
+
+# CHECK-LABEL: name: test_kill_superreg_def
+# CHECK: $r1 = A2_abs $r0, implicit-def $d0
+# CHECK-NEXT: $r0 = A2_add killed $r0, killed $r1
+
+--- |
+  define void @test_kill_superreg_def() {
+    ret void
+  }
+...
+
+---
+name: test_kill_superreg_def
+tracksRegLiveness: true
+body: |
+  bb.0:
+    liveins: $r0
+
+    $r1 = A2_abs $r0, implicit-def $d0
+    $r0 = A2_add $r0, $r1
+    PS_jmpret $r31, implicit-def dead $pc, implicit $r0
+...

>From 781a4b026ebf5f0d585b724b722664fab44a1663 Mon Sep 17 00:00:00 2001
From: Brian Cain <brian.cain at oss.qualcomm.com>
Date: Sun, 15 Feb 2026 10:39:11 -0800
Subject: [PATCH 2/3] [CodeGen] Fix LiveRegUnits::stepBackward() removing live
 sibling sub-registers
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

When an instruction defines a sub-register (e.g. $r1) and has an
implicit-def of its super-register (e.g. $d0), stepBackward() calls
removeReg($d0) which clears the register units for ALL sub-registers
of $d0 — including siblings like $r0 that may still be live. This
causes DeadMachineInstructionElim to incorrectly consider definitions
of those siblings as dead and delete them.

Fix this by skipping removeReg for implicit defs of super-registers
when the instruction also defines one of their sub-registers. The
implicit def in this case is an annotation of partial modification,
not a full redefinition.

This is the same fix previously applied to ScheduleDAGInstrs::fixupKills()
in d7b0aab97b3e, now applied to LiveRegUnits::stepBackward().
---
 llvm/lib/CodeGen/LiveRegUnits.cpp             | 24 +++++++++++-
 .../CodeGen/Hexagon/dead-mi-superreg-def.mir  | 39 +++++++++++++++++++
 2 files changed, 62 insertions(+), 1 deletion(-)
 create mode 100644 llvm/test/CodeGen/Hexagon/dead-mi-superreg-def.mir

diff --git a/llvm/lib/CodeGen/LiveRegUnits.cpp b/llvm/lib/CodeGen/LiveRegUnits.cpp
index 348ccd85f4c45..87b2c15f0395a 100644
--- a/llvm/lib/CodeGen/LiveRegUnits.cpp
+++ b/llvm/lib/CodeGen/LiveRegUnits.cpp
@@ -45,8 +45,30 @@ void LiveRegUnits::stepBackward(const MachineInstr &MI) {
   // Remove defined registers and regmask kills from the set.
   for (const MachineOperand &MOP : MI.operands()) {
     if (MOP.isReg()) {
-      if (MOP.isDef() && MOP.getReg().isPhysical())
+      if (MOP.isDef() && MOP.getReg().isPhysical()) {
+        // Skip implicit defs of super-registers when a sub-register is
+        // also defined by this instruction. The implicit def is an
+        // annotation of partial modification, not a full redefinition of
+        // the super-register. Removing the super-register from the live
+        // set would incorrectly clear the liveness of sibling
+        // sub-registers that may still be live.
+        if (MOP.isImplicit()) {
+          MCRegister Reg = MOP.getReg().asMCReg();
+          bool HasSubRegDef = false;
+          for (const MachineOperand &MOP2 : MI.operands()) {
+            if (!MOP2.isReg() || !MOP2.isDef() || !MOP2.getReg().isPhysical())
+              continue;
+            if (MOP2.getReg() != Reg &&
+                TRI->isSubRegister(Reg, MOP2.getReg().asMCReg())) {
+              HasSubRegDef = true;
+              break;
+            }
+          }
+          if (HasSubRegDef)
+            continue;
+        }
         removeReg(MOP.getReg());
+      }
       continue;
     }
 
diff --git a/llvm/test/CodeGen/Hexagon/dead-mi-superreg-def.mir b/llvm/test/CodeGen/Hexagon/dead-mi-superreg-def.mir
new file mode 100644
index 0000000000000..4a210863ac89b
--- /dev/null
+++ b/llvm/test/CodeGen/Hexagon/dead-mi-superreg-def.mir
@@ -0,0 +1,39 @@
+# RUN: llc -mtriple=hexagon -run-pass dead-mi-elimination \
+# RUN:   -verify-machineinstrs -o - %s | FileCheck %s
+
+# LiveRegUnits::stepBackward() walks backward through instructions,
+# removing defined registers from a LiveRegUnits set. When an
+# instruction defines a sub-register ($r1) and has an implicit-def of
+# its super-register ($d0), removeReg($d0) clears the register units
+# of all sub-registers of $d0 — including siblings like $r0 that may
+# still be live. This causes available($r0) to incorrectly return
+# true, and dead MI elimination deletes the instruction defining $r0.
+#
+# In this test, A2_tfrsi defines $r1 with an implicit-def of $d0. The
+# subsequent J2_call uses both $r0 and $r1 as implicit operands, so
+# $r0 must remain live and "$r0 = A2_tfrsi @g" must not be deleted.
+
+# CHECK-LABEL: name: test_dead_mi_superreg
+# CHECK: $r0 = A2_tfrsi @g
+# CHECK: $r1 = A2_tfrsi 42, implicit-def $d0
+# CHECK: J2_call @foo
+
+--- |
+  @g = external global i32
+  define void @test_dead_mi_superreg() {
+    ret void
+  }
+  declare void @foo(ptr, i32)
+...
+
+---
+name: test_dead_mi_superreg
+tracksRegLiveness: true
+body: |
+  bb.0:
+    $r0 = A2_tfrsi @g
+    $r1 = A2_tfrsi 42, implicit-def $d0
+    J2_call @foo, implicit $r0, implicit $r1, implicit-def $r0, implicit-def $r1, implicit-def $d0
+    $r0 = A2_tfrsi 0
+    PS_jmpret $r31, implicit-def dead $pc, implicit $r0
+...

>From c834012518d199b436e6bc4c740bff41388ff51e Mon Sep 17 00:00:00 2001
From: Brian Cain <brian.cain at oss.qualcomm.com>
Date: Sun, 15 Feb 2026 10:39:26 -0800
Subject: [PATCH 3/3] [CodeGen] Fix LivePhysRegs::removeDefs() removing live
 sibling sub-registers
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

When an instruction defines a sub-register (e.g. $r1) and has an
implicit-def of its super-register (e.g. $d0), removeDefs() calls
removeReg($d0) which clears the liveness of ALL sub-registers of $d0
— including siblings like $r0 that may still be live. This causes
incorrect live-in computation in passes that use LivePhysRegs
(branch-folder, register scavenging, liveness recomputation, etc.),
which can lead to downstream miscompilation or crashes.

Fix this by skipping removeReg for implicit defs of super-registers
when the instruction also defines one of their sub-registers. The
implicit def in this case is an annotation of partial modification,
not a full redefinition.

This is the same fix previously applied to ScheduleDAGInstrs::fixupKills()
and LiveRegUnits::stepBackward(), now applied to LivePhysRegs::removeDefs().
---
 llvm/lib/CodeGen/LivePhysRegs.cpp             | 24 ++++++-
 .../Hexagon/livephysregs-superreg-def.mir     | 67 +++++++++++++++++++
 2 files changed, 90 insertions(+), 1 deletion(-)
 create mode 100644 llvm/test/CodeGen/Hexagon/livephysregs-superreg-def.mir

diff --git a/llvm/lib/CodeGen/LivePhysRegs.cpp b/llvm/lib/CodeGen/LivePhysRegs.cpp
index 5c8f060c09e5f..7c45c2f62f8b1 100644
--- a/llvm/lib/CodeGen/LivePhysRegs.cpp
+++ b/llvm/lib/CodeGen/LivePhysRegs.cpp
@@ -49,8 +49,30 @@ void LivePhysRegs::removeDefs(const MachineInstr &MI) {
       continue;
     }
 
-    if (MOP.isDef())
+    if (MOP.isDef()) {
+      // Skip implicit defs of super-registers when a sub-register is
+      // also defined by this instruction. The implicit def is an
+      // annotation of partial modification, not a full redefinition of
+      // the super-register. Removing the super-register from the live
+      // set would incorrectly clear the liveness of sibling
+      // sub-registers that may still be live.
+      if (MOP.isImplicit()) {
+        MCRegister Reg = MOP.getReg().asMCReg();
+        bool HasSubRegDef = false;
+        for (const MachineOperand &MOP2 : phys_regs_and_masks(MI)) {
+          if (!MOP2.isReg() || !MOP2.isDef())
+            continue;
+          MCRegister Reg2 = MOP2.getReg().asMCReg();
+          if (Reg2 != Reg && TRI->isSubRegister(Reg, Reg2)) {
+            HasSubRegDef = true;
+            break;
+          }
+        }
+        if (HasSubRegDef)
+          continue;
+      }
       removeReg(MOP.getReg());
+    }
   }
 }
 
diff --git a/llvm/test/CodeGen/Hexagon/livephysregs-superreg-def.mir b/llvm/test/CodeGen/Hexagon/livephysregs-superreg-def.mir
new file mode 100644
index 0000000000000..beaa02ffa8a71
--- /dev/null
+++ b/llvm/test/CodeGen/Hexagon/livephysregs-superreg-def.mir
@@ -0,0 +1,67 @@
+# RUN: llc -mtriple=hexagon -run-pass branch-folder \
+# RUN:   -verify-machineinstrs -o - %s | FileCheck %s
+
+# LivePhysRegs::removeDefs() has the same super-register implicit def
+# issue as LiveRegUnits::stepBackward() and ScheduleDAGInstrs::fixupKills():
+# when an instruction defines a sub-register ($r1) and has an implicit-def
+# of its super-register ($d0), removeReg($d0) clears the register units of
+# all sub-registers — including siblings like $r0 that may still be live.
+#
+# This test has two blocks (bb.1 and bb.2) with a common tail. The common
+# tail includes "$r1 = A2_tfrsi 42, implicit-def $d0" followed by a call
+# that uses both $r0 and $r1. The branch folder merges the common tails.
+# After merging, the common tail block's live-in computation must include
+# $r0, because it is used by the call instruction.
+#
+# Without the fix to LivePhysRegs::removeDefs(), stepBackward over
+# "$r1 = A2_tfrsi 42, implicit-def $d0" incorrectly removes $r0 from the
+# live set, so $r0 would not appear in the merged block's live-ins.
+
+# CHECK-LABEL: name: test_livephysregs_superreg
+
+# The common tail block (merged from bb.1/bb.2 tails) must have
+# $r0 in its live-ins since J2_call uses it.
+# CHECK: liveins: $r0
+# CHECK: $r1 = A2_tfrsi 42, implicit-def $d0
+# CHECK-NEXT: J2_call @foo
+
+--- |
+  @g = external global i32
+  define void @test_livephysregs_superreg() {
+    ret void
+  }
+  declare void @foo(ptr, i32)
+...
+
+---
+name: test_livephysregs_superreg
+tracksRegLiveness: true
+body: |
+  bb.0:
+    liveins: $r0, $r31
+    successors: %bb.1, %bb.2
+    J2_jumpt undef $p0, %bb.2, implicit-def $pc
+    J2_jump %bb.1, implicit-def $pc
+
+  bb.1:
+    liveins: $r0, $r31
+    successors: %bb.3
+    $r2 = A2_tfrsi 1
+    $r1 = A2_tfrsi 42, implicit-def $d0
+    J2_call @foo, implicit $r0, implicit $r1, implicit-def $r0, implicit-def $r1, implicit-def $d0
+    $r0 = A2_tfrsi 0
+    J2_jump %bb.3, implicit-def $pc
+
+  bb.2:
+    liveins: $r0, $r31
+    successors: %bb.3
+    $r2 = A2_tfrsi 2
+    $r1 = A2_tfrsi 42, implicit-def $d0
+    J2_call @foo, implicit $r0, implicit $r1, implicit-def $r0, implicit-def $r1, implicit-def $d0
+    $r0 = A2_tfrsi 0
+    J2_jump %bb.3, implicit-def $pc
+
+  bb.3:
+    liveins: $r0, $r31
+    PS_jmpret $r31, implicit-def dead $pc, implicit $r0
+...



More information about the llvm-commits mailing list