[llvm] [CodeGen] TwoAddress: only keep undef REG_SEQUENCE COPY when tracking subreg liveness (PR #202599)

via llvm-commits llvm-commits at lists.llvm.org
Tue Jun 9 05:59:07 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-backend-arm

Author: HelloWorldU

<details>
<summary>Changes</summary>

This patch fixes a register coalescer assertion that fires when lowering a `REG_SEQUENCE` with `undef` operands on targets that do not track subregister liveness.

Fixes #<!-- -->202263, a regression from the fix for #<!-- -->175596 (#<!-- -->175598).

## Background

In the optimized register-allocation pipeline, `TwoAddressInstructionPass` runs *before* `LiveIntervals` (the `EarlyLiveIntervals` pass ordering is off by default):

    PHIElimination
    if (EarlyLiveIntervals) LiveIntervals   // only with -early-live-intervals; off by default
    TwoAddressInstruction                    // -> LIS == null here
    RegisterCoalescer                        // LiveIntervals is computed as its dependency

So `eliminateRegSequence` normally lowers `REG_SEQUENCE` with no `LiveIntervals` available, and in that `!LIS` case it emits no `COPY` for an `undef` operand. On a target that tracks subregister liveness (e.g. AMDGPU), `LiveIntervals` later builds per-lane subranges, so the use of that undefined lane has no live subrange:

    *** Bad machine code: No live subrange at use ***

That was #<!-- -->175596, fixed by #<!-- -->175598: when LIS is unavailable, scan the use list and, if an `undef` lane is actually used later, keep the `COPY` so the lane gets a definition:

    undef %1.dsub_0:qpr = COPY undef %2:dpr

This works, but it quietly makes correctness depend on a second factor — **liveness granularity**, i.e. whether the target tracks subregister liveness:

* **Per-lane liveness** (tracked, e.g. AMDGPU): each lane has its own live range, so a used undef lane needs its *own* definition — keeping the `COPY` is necessary.
* **Whole-register liveness** (not tracked, e.g. ARM without MVE): the lane has no live range of its own; it is already covered by the register's liveness once the first partial def is marked read-undef — so the `COPY` is unnecessary.

On a non-tracking target the kept `COPY` is also actively harmful: the register coalescer erases a `COPY` of an undef value, which removes the lane's only definition again, recreating a use-without-def and asserting in `JoinVals::analyzeValue`:

      (TrackSubRegLiveness || V.RedefVNI) && "Instruction is reading nonexistent value"

That is #<!-- -->202263.


## Fix: only keep the COPY where it can survive

In `eliminateRegSequence`, gate the `!LIS` use-list scan on `MRI->shouldTrackSubRegLiveness(DstReg)` — the second factor above. Non-tracking targets then skip the `undef` COPY as they did before #<!-- -->175598 (the long-standing, correct behavior); tracking targets are unchanged.

On the ARM reproducer, the lowering goes from (asserts later):

    undef %1.dsub_0:qpr = COPY undef %2:dpr   ; erased by the coalescer -> crash
    %1.dsub_1:qpr       = COPY %0
    %3:dpr              = COPY %1.dsub_0

to (no spurious COPY; whole-register liveness covers dsub_0):

    undef %1.dsub_1:qpr = COPY %0
    %3:dpr              = COPY %1.dsub_0

## Testing & validation

* New `llvm/test/CodeGen/ARM/twoaddr-regsequence-undef-subreg-use.mir`, which fails without this change.
* Existing AMDGPU `twoaddr-regsequence-keep-copy-on-use.mir` still passes (tracking targets are unaffected).
* #<!-- -->175598 landed without modifying any existing test, so this path is barely exercised by the suite; restoring the non-tracking behavior is very unlikely to change existing test output. Full cross-target coverage is left to premerge CI.

## Related work

#<!-- -->178387 and #<!-- -->189153 explore an IMPLICIT_DEF-based approach (emit a subregister `IMPLICIT_DEF` and preserve it through coalescing). They target the AMDGPU / subregister-liveness path and operate on subranges, so they are largely
orthogonal to the non-tracking-target path fixed here. A similar IMPLICIT_DEF approach could also let used undef lanes work on non-tracking targets, but that is a larger change; this patch restores the known-good behavior with minimal risk.

## Note on the approach

Gating the scan on `shouldTrackSubRegLiveness` in the shared `TwoAddressInstructionPass` is a bit blunt and may not be the ideal long-term fix. Since the assertion really fires in the coalescer — on MIR that was well-formed until it erased the undef COPY — the more robust place to absorb this might be the coalescer itself, the direction @<!-- -->arsenm has leaned toward.
This patch is the smaller, low-risk regression fix, but I'm happy to move it into the coalescer if preferred.



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


2 Files Affected:

- (modified) llvm/lib/CodeGen/TwoAddressInstructionPass.cpp (+6-1) 
- (added) llvm/test/CodeGen/ARM/twoaddr-regsequence-undef-subreg-use.mir (+24) 


``````````diff
diff --git a/llvm/lib/CodeGen/TwoAddressInstructionPass.cpp b/llvm/lib/CodeGen/TwoAddressInstructionPass.cpp
index fb3014d87f40a..c5292580dccec 100644
--- a/llvm/lib/CodeGen/TwoAddressInstructionPass.cpp
+++ b/llvm/lib/CodeGen/TwoAddressInstructionPass.cpp
@@ -2057,8 +2057,13 @@ void TwoAddressInstructionImpl::eliminateRegSequence(
 
   // If there are no live intervals information, we scan the use list once
   // in order to find which subregisters are used.
+  // Only do this when the target tracks subregister liveness. Otherwise
+  // liveness is tracked per whole register, so a used undef lane is already
+  // covered by the register's liveness and does not need its own def. Keeping
+  // a COPY for it would be pointless: the register coalescer erases such an
+  // undef COPY, recreating a use-without-def and crashing (issue #202263).
   LaneBitmask UsedLanes = LaneBitmask::getNone();
-  if (!LIS) {
+  if (!LIS && MRI->shouldTrackSubRegLiveness(DstReg)) {
     for (MachineOperand &Use : MRI->use_nodbg_operands(DstReg)) {
       if (unsigned SubReg = Use.getSubReg())
         UsedLanes |= TRI->getSubRegIndexLaneMask(SubReg);
diff --git a/llvm/test/CodeGen/ARM/twoaddr-regsequence-undef-subreg-use.mir b/llvm/test/CodeGen/ARM/twoaddr-regsequence-undef-subreg-use.mir
new file mode 100644
index 0000000000000..f15e1a8f6c19c
--- /dev/null
+++ b/llvm/test/CodeGen/ARM/twoaddr-regsequence-undef-subreg-use.mir
@@ -0,0 +1,24 @@
+# RUN: llc -mtriple=armv8a-unknown-linux-gnueabi -run-pass=twoaddressinstruction -o - %s | FileCheck %s
+
+# On a target that does not track subregister liveness, lowering a REG_SEQUENCE
+# with an undef operand must skip the COPY for that operand even when the
+# subregister is used later: liveness is tracked per whole register, so the
+# undef lane is already covered by the register's liveness and needs no def of
+# its own. Emitting a COPY of the undef value would later be erased by the
+# register coalescer, recreating a use-without-def and crashing (issue #202263).
+
+---
+name:            undef_regsequence_subreg_use
+tracksRegLiveness: true
+body:             |
+  bb.0:
+    ; CHECK-LABEL: name: undef_regsequence_subreg_use
+    ; CHECK: [[VMOV:%[0-9]+]]:dpr = VMOVv2i32
+    ; CHECK-NEXT: undef [[REG:%[0-9]+]].dsub_1:qpr = COPY [[VMOV]]
+    ; CHECK-NEXT: %{{[0-9]+}}:dpr = COPY [[REG]].dsub_0
+    ; CHECK-NOT: COPY undef
+    %0:dpr = VMOVv2i32 0, 14 /* CC::al */, $noreg
+    %1:qpr = REG_SEQUENCE undef %2:dpr, %subreg.dsub_0, %0:dpr, %subreg.dsub_1
+    %3:dpr = COPY %1.dsub_0
+    VST1d64 $noreg, 0, %3:dpr, 14 /* CC::al */, $noreg
+...

``````````

</details>


https://github.com/llvm/llvm-project/pull/202599


More information about the llvm-commits mailing list