Skip to content

[LiveVariables] Keep prior partial defs live across the closing partial def#204963

Open
ravn wants to merge 1 commit into
llvm:mainfrom
ravn:livevars-partial-def-156428
Open

[LiveVariables] Keep prior partial defs live across the closing partial def#204963
ravn wants to merge 1 commit into
llvm:mainfrom
ravn:livevars-partial-def-156428

Conversation

@ravn

@ravn ravn commented Jun 21, 2026

Copy link
Copy Markdown

Fixes #156428.

Summary

On targets with sub-register pairs like AVR (and downstream Z80 work) byte-sized arithmetic typically writes the two halves of a 16-bit register pair from independent instructions before passing the 16-bit register pair to a function. For Z80, the D and E halves of the DE pair are written by separate instructions before DE is passed as an argument.

This PR restores a critical piece of bookkeeping in LiveVariables::HandlePhysRegUse allowing LiveVariables to handle this correctly, plus a safety check that prevents extending a live range into a region with no reaching definition.

+55 / -0 in LiveVariables.cpp — most of which is explanatory comments; the actual logic change is about a dozen lines — plus a new MIR lit test on AVR with three cases: the bug's reduction, the same shape with an intervening unrelated instruction, and the partial-def-of-subreg case the 2026 commit was originally handling.

I have done this using Claude, but have tried to do this as clear and simple from the issue as I could.

The text below explains things in more detail as written by Claude. I have reviewed it but not rewritten it, as it was clear enough on its own.

/Thorbjørn


Attribution boundary. Everything below this line was drafted by Claude (Anthropic; model claude-opus-4-7) at my direction. I have reviewed it and confirm it matches the actual patch, but I did not rewrite the wording.
Thorbjørn


Detailed analysis (by Claude claude-opus-4-7, reviewed by Thorbjørn)

The bug

LiveVariables::HandlePhysRegUse handles a use of a super-register whose value was assembled by independent writes to its sub-registers — the canonical pattern on small-int targets where an ABI parameter pair (e.g. AVR's $r25r24) is composed by two independent 8-bit writes before being passed to a function.

When the super-register has no direct prior def, the pass routes the use through the last partial def by adding implicit-def Reg to it. That alone is fine.

What's missing in current main: the earlier partial def of the other sub-register has no remaining user. Concretely, given

$r25 = MOVRdRr $r22
$r24 = LDIRdK 42
RCALLk @use, implicit $r25r24

LiveVariables tags the LDIRdK with implicit-def $r25r24. The first MOVRdRr (which wrote $r25) is now seen by DeadMachineInstrElim as unused — $r25 is "redefined" before any consumer — and gets deleted. The downstream code reads an undefined $r25. Miscompile.

Where it came from

git bisect (single commit) points to e8e3e6e893a2c944c8ce1878f290aa62843323e0 (#119446). That PR was fixing a separate bug — the same iteration could add an implicit $sub for a sub-register that the same instruction already defines, which is invalid (you can't read a register that's defined on the same instruction unless it's a tied operand).

The submitted fix in PR #119446 added an IsDefinedHere guard that converted the would-be implicit-use into an implicit-def for overlapping cases. The merged version reworked it differently — by removing the iteration entirely. That over-correction silently deleted the live-keeping bookkeeping for the case in this issue.

The fix

HandlePhysRegUse, after the existing implicit-def Reg line, gains ~30 lines of restored bookkeeping with two guards.

  1. Snapshot which sub-registers are already covered by LastPartialDef's existing explicit defs, before mutating the operand list with implicit-def Reg. This is necessary because modifiesRegister(Sub, TRI) looks at all defs including the just-added one, which would falsely report every sub-register as "covered" and skip the entire iteration.

  2. Add implicit-use for each remaining sub-register that has a prior partial def in this block. This is what keeps the earlier writer alive against DeadMachineInstrElim.

  3. Skip sub-registers with no prior def. Without this guard, adding an implicit-use for an undefined sub-register extends a live range into a region with no reaching def, which crashes LiveIntervals::computeRegUnitRange in MachineScheduler with LLVM ERROR: Invalid global physical register / Use not jointly dominated by defs. (Found by regression testing against three X86 tests on the first pass; this guard was the minimal fix.)

The 2026 PR's IsDefinedHere-becomes-implicit-def widening for partially-overlapping sub-registers (e.g. promoting implicit $sgpr2_sgpr3 to implicit-def $sgpr2_sgpr3 when $sgpr3 is explicitly defined) is deliberately not part of this patch. The super-register's existing implicit-def Reg already covers those bits for live-tracking purposes; the operand list stays shorter and the soundness story for this PR stays narrowly focused on the deletion bug.

Test

llvm/test/CodeGen/AVR/livevars-implicitdef.mir adds three cases:

  • pair_two_halves_eqv_dist — the bug's reduction. Two consecutive independent writes followed by a super-reg use.
  • pair_two_halves_hole — same shape with an unrelated instruction between the two partial defs (so LastPartialDef is not immediately adjacent to the earlier partial def).
  • pair_partial_def_subreg_of_use — both halves written by MOV instructions, exercises the case PR [LiveVariables] Mark use without implicit if defined at instr #119446 was originally trying to make sound. Confirms my fix doesn't reintroduce the implicit $sub-where-sub-is-defined shape (it doesn't, because the AlreadyDefined snapshot via modifiesRegister catches the overlap and skips).

Each positive case fails on stock main and passes with the patch.

Regression sweep

Across Transforms/CodeGen/{AVR,AArch64,X86} after building all required tools:

Total Discovered Tests: 9790
  Unsupported      :    3 (0.03%)
  Passed           : 9764 (99.73%)
  Expectedly Failed:   23 (0.23%)

Zero failures, zero regressions.

Note for reviewers

Two earlier attempts at the iteration's guard surfaced real test failures and were discarded:

  1. First attempt unconditionally added implicit-use for every sibling sub-register — caused Invalid global physical register in MachineScheduler on the three X86 tests that hit LiveIntervals::computeRegUnitRange. The PhysRegDef[SubReg] guard fixed those.
  2. Second attempt computed IsDefinedHere via isSubRegister(SubReg, MO.getReg()) which only catches one direction of the sub-register relationship. Replaced with modifiesRegister, which handles both subset and superset and is the standard idiom elsewhere in this file.

The included AVR test is the third case from the bisect of those failures; it exercises both guard conditions in concert.

…al def

When a use of a super-register is fed by independent writes to its
sub-registers, LiveVariables routes the use through the LAST partial
def by adding an `implicit-def Reg` to it. Commit e8e3e6e
(llvm#119446) was meant to fix a separate issue where that implicit
operand iteration sometimes added `implicit $sub` for a sub-register
the same instruction already defines. The merged commit, however,
removed the iteration entirely instead of guarding it.

The unintended consequence: the earlier writers of OTHER sub-registers
appear dead. DeadMachineInstrElim deletes them, and downstream code
reads a now-undefined register. This is the AVR / Z80 / MSP430
miscompile from llvm#156428: a pair-half assignment vanishes before the
super-reg is consumed by a call.

Restore the iteration but guard it correctly:

- Snapshot which sub-registers of Reg are already covered by an
  existing explicit def of LastPartialDef BEFORE adding the
  implicit-def Reg (modifiesRegister would otherwise mistakenly include
  Reg as covering everything).
- For each remaining sub-register that has a prior partial def in this
  block, add an implicit-use to LastPartialDef. Skip sub-registers with
  no prior def — adding an implicit-use there would extend a live range
  into a region with no reaching def and crash the verifier.

New test llvm/test/CodeGen/AVR/livevars-implicitdef.mir covers three
shapes: the bug's reduction, the same pattern with an unrelated
instruction between the two partial defs, and the partial-def-of-subreg
case the suspect commit was originally trying to handle.

Fixes llvm#156428.

Assisted-by: Claude (Anthropic, claude-opus-4-7)
@github-actions

Copy link
Copy Markdown

Hello @ravn 👋

Thank you for submitting a Pull Request (PR) to the LLVM Project. Since this is your first PR, here are a few useful links covering our main contribution policies and review practices.

  • All contributions to LLVM must follow our LLVM AI Tool Use Policy. In particular, if you used AI while working on this PR, remember to add a note to the PR description.
  • The LLVM Code-Review Policy and Practices document contains practical information about the PR process, including how patches are reviewed and accepted, and who can review a PR.
  • Our LLVM Developer Policy describes our expectations for code quality, commit summaries and contains notes on our CI system.

Please reply to this message to confirm that you have read these policies, especially the LLVM AI Tool Use Policy, and that any AI tool usage has been noted in the PR description.


Frequently asked questions

How do I add reviewers?

This PR will be automatically labeled, and the relevant teams will be notified. For some parts of the project, reviewers may also be added automatically.

You can also add reviewers manually using the Reviewers section on this page. If you cannot use that section, it is probably because you do not have write permissions for the repository. In that case, you can request a review by tagging reviewers in a comment using @ followed by their GitHub username.

What if there are no comments?

If you have not received any comments on your PR after a week, you can request a review by pinging the PR with a comment such as “Ping”. The common courtesy ping rate is once a week. Please remember that you are asking for volunteer time from other developers.

Are any special GitHub settings required to contribute to LLVM?

We only require contributors to have a public email address associated with their GitHub commits, see this section of LLVM Developer Policy for details.


If you have questions, feel free to leave a comment on this PR, or ask on LLVM Discord or LLVM Discourse.

Thank you,
The LLVM Community

@llvmorg-github-actions

Copy link
Copy Markdown

@llvm/pr-subscribers-llvm-regalloc

Author: Thorbjørn Ravn Andersen (ravn)

Changes

Fixes #156428.

Summary

On targets with sub-register pairs like AVR (and downstream Z80 work)
byte-sized arithmetic typically writes the two halves of a 16-bit
register pair from independent instructions before passing the 16-bit register pair
to a function. For Z80, the D and E halves of the DE pair are written by separate instructions before DE is passed as an argument.

This PR restores a critical piece of bookkeeping in LiveVariables::HandlePhysRegUse allowing LiveVariables to handle this correctly, plus a safety check that prevents
extending a live range into a region with no reaching definition.

+55 / -0 in LiveVariables.cpp — most of which is explanatory
comments; the actual logic change is about a dozen lines — plus a new
MIR lit test on AVR with three cases: the bug's reduction, the same
shape with an intervening unrelated instruction, and the
partial-def-of-subreg case the 2026 commit was originally handling.

I have done this using Claude, but have tried to do this as clear and
simple from the issue as I could.

The text below explains things in more detail as written by Claude. I
have reviewed it but not rewritten it, as it was clear enough on its
own.

/Thorbjørn


> Attribution boundary. Everything below this line was drafted by
> Claude (Anthropic; model claude-opus-4-7) at my direction. I have
> reviewed it and confirm it matches the actual patch, but I did not
> rewrite the wording.
> — Thorbjørn


Detailed analysis (by Claude claude-opus-4-7, reviewed by Thorbjørn)

The bug

LiveVariables::HandlePhysRegUse handles a use of a super-register
whose value was assembled by independent writes to its
sub-registers — the canonical pattern on small-int targets where an
ABI parameter pair (e.g. AVR's $r25r24) is composed by two
independent 8-bit writes before being passed to a function.

When the super-register has no direct prior def, the pass routes the
use through the last partial def by adding implicit-def Reg to
it. That alone is fine.

What's missing in current main: the earlier partial def of the
other sub-register has no remaining user. Concretely, given

$r25 = MOVRdRr $r22
$r24 = LDIRdK 42
RCALLk @<!-- -->use, implicit $r25r24

LiveVariables tags the LDIRdK with implicit-def $r25r24. The first
MOVRdRr (which wrote $r25) is now seen by DeadMachineInstrElim as
unused — $r25 is "redefined" before any consumer — and gets
deleted. The downstream code reads an undefined $r25. Miscompile.

Where it came from

git bisect (single commit) points to
e8e3e6e893a2c944c8ce1878f290aa62843323e0 (#119446). That PR was
fixing a separate bug — the same iteration could add an
implicit $sub for a sub-register that the same instruction already
defines, which is invalid (you can't read a register that's defined
on the same instruction unless it's a tied operand).

The submitted fix in PR #119446 added an IsDefinedHere guard that
converted the would-be implicit-use into an implicit-def for
overlapping cases. The merged version reworked it differently — by
removing the iteration entirely. That over-correction silently
deleted the live-keeping bookkeeping for the case in this issue.

The fix

HandlePhysRegUse, after the existing implicit-def Reg line, gains
~30 lines of restored bookkeeping with two guards.

  1. Snapshot which sub-registers are already covered by
    LastPartialDef's existing explicit defs, before mutating the
    operand list with implicit-def Reg. This is necessary because
    modifiesRegister(Sub, TRI) looks at all defs including the
    just-added one, which would falsely report every sub-register as
    "covered" and skip the entire iteration.

  2. Add implicit-use for each remaining sub-register that has a
    prior partial def in this block.
    This is what keeps the earlier
    writer alive against DeadMachineInstrElim.

  3. Skip sub-registers with no prior def. Without this guard,
    adding an implicit-use for an undefined sub-register extends a
    live range into a region with no reaching def, which crashes
    LiveIntervals::computeRegUnitRange in MachineScheduler with
    LLVM ERROR: Invalid global physical register /
    Use not jointly dominated by defs. (Found by regression testing
    against three X86 tests on the first pass; this guard was the
    minimal fix.)

The 2026 PR's IsDefinedHere-becomes-implicit-def widening for
partially-overlapping sub-registers (e.g. promoting
implicit $sgpr2_sgpr3 to implicit-def $sgpr2_sgpr3 when $sgpr3 is
explicitly defined) is deliberately not part of this patch. The
super-register's existing implicit-def Reg already covers those bits
for live-tracking purposes; the operand list stays shorter and the
soundness story for this PR stays narrowly focused on the deletion
bug.

Test

llvm/test/CodeGen/AVR/livevars-implicitdef.mir adds three cases:

  • pair_two_halves_eqv_dist — the bug's reduction. Two
    consecutive independent writes followed by a super-reg use.
  • pair_two_halves_hole — same shape with an unrelated
    instruction between the two partial defs (so LastPartialDef is not
    immediately adjacent to the earlier partial def).
  • pair_partial_def_subreg_of_use — both halves written by MOV
    instructions, exercises the case PR #119446 was originally trying
    to make sound. Confirms my fix doesn't reintroduce the
    implicit $sub-where-sub-is-defined shape (it doesn't, because the
    AlreadyDefined snapshot via modifiesRegister catches the
    overlap and skips).

Each positive case fails on stock main and passes with the patch.

Regression sweep

Across Transforms/CodeGen/{AVR,AArch64,X86} after building all
required tools:

Total Discovered Tests: 9790
  Unsupported      :    3 (0.03%)
  Passed           : 9764 (99.73%)
  Expectedly Failed:   23 (0.23%)

Zero failures, zero regressions.

Note for reviewers

Two earlier attempts at the iteration's guard surfaced real test
failures and were discarded:

  1. First attempt unconditionally added implicit-use for every
    sibling sub-register — caused Invalid global physical register
    in MachineScheduler on the three X86 tests that hit
    LiveIntervals::computeRegUnitRange. The PhysRegDef[SubReg]
    guard fixed those.
  2. Second attempt computed IsDefinedHere via
    isSubRegister(SubReg, MO.getReg()) which only catches one
    direction of the sub-register relationship. Replaced with
    modifiesRegister, which handles both subset and superset and is
    the standard idiom elsewhere in this file.

The included AVR test is the third case from the bisect of those
failures; it exercises both guard conditions in concert.


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

2 Files Affected:

  • (modified) llvm/lib/CodeGen/LiveVariables.cpp (+55)
  • (added) llvm/test/CodeGen/AVR/livevars-implicitdef.mir (+87)
diff --git a/llvm/lib/CodeGen/LiveVariables.cpp b/llvm/lib/CodeGen/LiveVariables.cpp
index 246d538332af7..3ea0f34e3eaba 100644
--- a/llvm/lib/CodeGen/LiveVariables.cpp
+++ b/llvm/lib/CodeGen/LiveVariables.cpp
@@ -252,8 +252,63 @@ void LiveVariables::HandlePhysRegUse(Register Reg, MachineInstr &MI) {
     MachineInstr *LastPartialDef = FindLastPartialDef(Reg);
     // If LastPartialDef is NULL, it must be using a livein register.
     if (LastPartialDef) {
+      // Sub-registers that LastPartialDef already writes (explicitly or via
+      // an overlap) need no additional implicit operand from us — they're
+      // already covered. Record them here so the loop further down can skip
+      // them when deciding where to add implicit-uses.
+      //
+      // Example, Reg = $r25r24 on AVR:
+      //   $r25 = MOVRdRr $r22     ; earlier partial def of $r25
+      //   $r24 = LDIRdK 42        ; LastPartialDef
+      //   ... use $r25r24 ...
+      // LDIRdK already writes $r24 explicitly but not $r25, so
+      // AlreadyDefined = {$r24}. The loop below adds `implicit $r25`
+      // (kept-live via the MOVRdRr) and skips $r24 (which is already in
+      // the operand list).
+      //
+      // This snapshot must be taken *before* the `implicit-def Reg` added
+      // below: once Reg is in the operand list it overlaps every
+      // sub-register of Reg, so `modifiesRegister(SubReg, TRI)` would
+      // return true for all of them and the snapshot would collapse to
+      // "every sub-register".
+      SmallSet<MCPhysReg, 4> AlreadyDefined;
+      for (MCPhysReg SubReg : TRI->subregs(Reg))
+        if (LastPartialDef->modifiesRegister(SubReg, TRI))
+          AlreadyDefined.insert(SubReg);
+
       LastPartialDef->addOperand(
           MachineOperand::CreateReg(Reg, /*IsDef=*/true, /*IsImp=*/true));
+      PhysRegDef[Reg.id()] = LastPartialDef;
+
+      // For each remaining sub-register of Reg with a prior partial def in
+      // this block, add an implicit-use so live analysis (and
+      // DeadMachineInstrElim) sees that earlier def as still consumed at
+      // this point. Skip sub-registers with no prior def — adding an
+      // implicit-use there would extend a live range into a region with
+      // no reaching def.
+      //
+      // Continuing the example above (Reg = $r25r24, AlreadyDefined =
+      // {$r24}, PhysRegDef[$r25] = the earlier `$r25 = MOVRdRr`):
+      //   - $r24 → in AlreadyDefined → skip.
+      //   - $r25 → not in AlreadyDefined and has a prior def → add
+      //     `implicit $r25` to LDIRdK. The earlier `$r25 = MOVRdRr`
+      //     now has a consumer and won't be deleted as dead.
+      SmallSet<MCPhysReg, 8> Processed;
+      for (MCPhysReg SubReg : TRI->subregs(Reg)) {
+        // Already covered by a super-reg handled earlier in this loop.
+        if (!Processed.insert(SubReg).second)
+          continue;
+        // Already written by LastPartialDef's existing explicit defs.
+        if (AlreadyDefined.count(SubReg))
+          continue;
+        // No earlier writer in this block, so nothing to keep alive.
+        if (!PhysRegDef[SubReg])
+          continue;
+        LastPartialDef->addOperand(MachineOperand::CreateReg(
+            SubReg, /*IsDef=*/false, /*IsImp=*/true));
+        PhysRegDef[SubReg] = LastPartialDef;
+        Processed.insert_range(TRI->subregs(SubReg));
+      }
     }
   } else if (LastDef && !PhysRegUse[Reg.id()] &&
              !LastDef->findRegisterDefOperand(Reg, /*TRI=*/nullptr))
diff --git a/llvm/test/CodeGen/AVR/livevars-implicitdef.mir b/llvm/test/CodeGen/AVR/livevars-implicitdef.mir
new file mode 100644
index 0000000000000..e22074b776db9
--- /dev/null
+++ b/llvm/test/CodeGen/AVR/livevars-implicitdef.mir
@@ -0,0 +1,87 @@
+# RUN: llc -mtriple=avr --run-pass=livevars -o - %s | FileCheck %s
+
+# Regression test for #156428.
+# When a use of a register pair is fed by separate writes to each half
+# (typical of K&R-style 8-bit code on AVR/Z80-style targets), the previous
+# code dropped implicit operands that kept the earlier half's writer live.
+# DeadMachineInstrElim then deleted it; the resulting code read an
+# undefined register.
+#
+# After the fix, the LastPartialDef instruction must carry an
+# `implicit $r25` (the half written by the earlier instruction) so that
+# downstream passes see the earlier writer as consumed at this point.
+
+--- |
+  target triple = "avr"
+  declare void @use()
+  define void @pair_two_halves_eqv_dist() { unreachable }
+  define void @pair_two_halves_hole() { unreachable }
+  define void @pair_partial_def_subreg_of_use() { unreachable }
+...
+
+---
+# The repro from the bug. $r25 and $r24 are defined by independent
+# instructions, then $r25r24 is used. Pre-fix: LDIRdK $r24 gets only
+# implicit-def $r25r24, $r25 = MOVRdRr gets eliminated. Post-fix:
+# implicit $r25 is also added, keeping the prior MOVRdRr live.
+name: pair_two_halves_eqv_dist
+tracksRegLiveness: true
+body: |
+  bb.0:
+    liveins: $r22
+
+    ; CHECK-LABEL: name: pair_two_halves_eqv_dist
+    ; CHECK: $r25 = MOVRdRr killed $r22
+    ; CHECK-NEXT: $r24 = LDIRdK 42, implicit-def $r25r24, implicit $r25
+    ; CHECK-NEXT: $r20 = MOVRdRr $r25
+    ; CHECK-NEXT: RCALLk @use, implicit killed $r25r24, implicit killed $r20
+
+    $r25 = MOVRdRr $r22
+    $r24 = LDIRdK 42
+    $r20 = MOVRdRr $r25
+    RCALLk @use, implicit $r25r24, implicit $r20
+...
+
+---
+# Same shape with an unrelated instruction between the two partial defs.
+# The implicit-use must still cover $r25.
+name: pair_two_halves_hole
+tracksRegLiveness: true
+body: |
+  bb.0:
+    liveins: $r22
+
+    ; CHECK-LABEL: name: pair_two_halves_hole
+    ; CHECK: $r25 = MOVRdRr killed $r22
+    ; CHECK-NEXT: $r18 = LDIRdK 7
+    ; CHECK-NEXT: $r24 = LDIRdK 42, implicit-def $r25r24, implicit $r25
+    ; CHECK-NEXT: RCALLk @use, implicit killed $r25r24, implicit killed $r18
+
+    $r25 = MOVRdRr $r22
+    $r18 = LDIRdK 7
+    $r24 = LDIRdK 42
+    RCALLk @use, implicit $r25r24, implicit $r18
+...
+
+---
+# Guard test for PR #119446's original case. Here LastPartialDef's
+# EXPLICIT def is a subreg of the super-reg that the implicit-use
+# iteration is considering. That subreg must not be re-added as an
+# implicit operand (it's already in the operand list explicitly).
+# This case should already work pre-fix — included to guard against
+# regressing PR #119446's intent.
+name: pair_partial_def_subreg_of_use
+tracksRegLiveness: true
+body: |
+  bb.0:
+    liveins: $r22, $r23
+
+    ; CHECK-LABEL: name: pair_partial_def_subreg_of_use
+    ; CHECK: $r25 = MOVRdRr killed $r22
+    ; CHECK-NEXT: $r24 = MOVRdRr killed $r23, implicit-def $r25r24, implicit $r25
+    ; CHECK-NEXT: RCALLk @use, implicit killed $r25r24
+
+    $r25 = MOVRdRr $r22
+    $r24 = MOVRdRr $r23
+    RCALLk @use, implicit $r25r24
+...

@ravn

ravn commented Jun 21, 2026

Copy link
Copy Markdown
Author

I have read the policies and conform as well as I can.

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.

LiveVariables pass incorrectly add implicit-def registers

1 participant