From 048ec1d5aa7261c8db6cd0eb310f2a22a4511d4d Mon Sep 17 00:00:00 2001 From: AkshayK Date: Mon, 14 Sep 2026 17:54:52 -0400 Subject: [PATCH 1/2] [ARM] Enable and validate call-used register clearing Advertise register-clearing support, retaining core-only modes on Thumb-1 targets with VFP hardware while diagnosing requests that may require inaccessible VFP instructions. Allow alias-preserving emitters to bypass the legacy sibling exclusion so an ARM return in r0 does not leave r1 uncleared. Preserve the return-address register independently of the callee-saved list and keep LR out of ARM zero-source selection. Cover all modes, FP aliases, multiple exits, cleanup calls, live flags, scratch exhaustion, return-address preservation, and both Clang request paths. The ARM/Thumb/Thumb2 suites pass 2501 tests with five expected failures; all 20 focused, compatibility, and Clang checks pass. Four regressions were verified to fail with the protections disabled. --- clang/test/CodeGen/arm-zero-call-used-regs.c | 27 ++++++ .../llvm/CodeGen/TargetFrameLowering.h | 6 ++ llvm/lib/CodeGen/PrologEpilogInserter.cpp | 18 ++-- llvm/lib/Target/ARM/ARMFrameLowering.cpp | 18 +++- llvm/lib/Target/ARM/ARMFrameLowering.h | 6 ++ .../ARM/zero-call-used-regs-capabilities.ll | 30 +++++++ .../ARM/zero-call-used-regs-cleanup.ll | 50 +++++++++++ .../zero-call-used-regs-exit-diagnostics.mir | 61 ++++++++++++++ .../CodeGen/ARM/zero-call-used-regs-exits.mir | 84 +++++++++++++++++++ .../ARM/zero-call-used-regs-fp-returns.ll | 32 +++++++ .../CodeGen/ARM/zero-call-used-regs-fp.ll | 8 +- .../ARM/zero-call-used-regs-thumb1-vfp.ll | 18 ++++ .../ARM/zero-call-used-regs-unsupported.ll | 23 ----- llvm/test/CodeGen/ARM/zero-call-used-regs.ll | 67 ++++++++++++--- .../CodeGen/ARM/zeroize-clearing-sequence.ll | 14 ++-- llvm/test/CodeGen/ARM/zeroize-naked.ll | 4 - .../test/CodeGen/ARM/zeroize-per-exit-regs.ll | 16 ++-- 17 files changed, 415 insertions(+), 67 deletions(-) create mode 100644 clang/test/CodeGen/arm-zero-call-used-regs.c create mode 100644 llvm/test/CodeGen/ARM/zero-call-used-regs-capabilities.ll create mode 100644 llvm/test/CodeGen/ARM/zero-call-used-regs-cleanup.ll create mode 100644 llvm/test/CodeGen/ARM/zero-call-used-regs-exit-diagnostics.mir create mode 100644 llvm/test/CodeGen/ARM/zero-call-used-regs-exits.mir create mode 100644 llvm/test/CodeGen/ARM/zero-call-used-regs-fp-returns.ll create mode 100644 llvm/test/CodeGen/ARM/zero-call-used-regs-thumb1-vfp.ll delete mode 100644 llvm/test/CodeGen/ARM/zero-call-used-regs-unsupported.ll diff --git a/clang/test/CodeGen/arm-zero-call-used-regs.c b/clang/test/CodeGen/arm-zero-call-used-regs.c new file mode 100644 index 0000000000000..e2051a2dc579d --- /dev/null +++ b/clang/test/CodeGen/arm-zero-call-used-regs.c @@ -0,0 +1,27 @@ +// REQUIRES: arm-registered-target +// RUN: %clang --target=armv7-none-eabi -O2 -S %s -o - | FileCheck %s --check-prefix=ATTR +// RUN: %clang --target=thumbv6m-none-eabi -O2 -S %s -o - | FileCheck %s --check-prefix=ATTR +// RUN: %clang --target=thumbv7m-none-eabi -O2 -S %s -o - | FileCheck %s --check-prefix=ATTR +// RUN: %clang --target=armv7-none-eabi -O2 -fzero-call-used-regs=all-gpr -S %s -o - | FileCheck %s --check-prefix=FLAG +// RUN: %clang --target=thumbv6m-none-eabi -O2 -fzero-call-used-regs=all-gpr -S %s -o - | FileCheck %s --check-prefix=FLAG +// RUN: %clang --target=thumbv7m-none-eabi -O2 -fzero-call-used-regs=all-gpr -S %s -o - | FileCheck %s --check-prefix=FLAG + +// Exercise code generation, not just the driver's -### output. Both request +// paths must reach the target emitter, while an explicit skip still opts out. + +// ATTR-LABEL: attributed: +// ATTR: mov{{.*}} r12, +// ATTR: bx lr +__attribute__((zero_call_used_regs("all-gpr"))) +int attributed(int x) { return x; } + +// FLAG-LABEL: flag_enabled: +// FLAG: mov{{.*}} r12, +// FLAG: bx lr +int flag_enabled(int x) { return x; } + +// FLAG-LABEL: skipped: +// FLAG-NOT: mov +// FLAG: bx lr +__attribute__((zero_call_used_regs("skip"))) +int skipped(int x) { return x; } diff --git a/llvm/include/llvm/CodeGen/TargetFrameLowering.h b/llvm/include/llvm/CodeGen/TargetFrameLowering.h index 7da579736bf08..d3611db1234e7 100644 --- a/llvm/include/llvm/CodeGen/TargetFrameLowering.h +++ b/llvm/include/llvm/CodeGen/TargetFrameLowering.h @@ -219,6 +219,12 @@ class LLVM_ABI TargetFrameLowering { return false; } + /// Whether emitZeroCallUsedRegs avoids widening a clear into an unrequested + /// sibling register. Such targets do not need PEI's legacy sibling exclusion. + virtual bool zeroCallUsedRegsPreservesUnrequestedSiblings() const { + return false; + } + /// emitZeroCallUsedRegs - Zeros out call used registers. Only called on /// targets whose supportsZeroCallUsedRegs returns true. /// diff --git a/llvm/lib/CodeGen/PrologEpilogInserter.cpp b/llvm/lib/CodeGen/PrologEpilogInserter.cpp index 7c5b4d25a9b86..f6b3f136ee136 100644 --- a/llvm/lib/CodeGen/PrologEpilogInserter.cpp +++ b/llvm/lib/CodeGen/PrologEpilogInserter.cpp @@ -1513,7 +1513,8 @@ getClearingInsertPoint(MachineBasicBlock &MBB, MachineInstr &ExitMI) { /// registers uncleared at every other return, dead and holding a value. static BitVector computeRegsToClearAtExit( const BitVector &Candidates, const MachineBasicBlock &MBB, - MachineBasicBlock::const_iterator InsertPt, const TargetRegisterInfo &TRI) { + MachineBasicBlock::const_iterator InsertPt, const TargetRegisterInfo &TRI, + const TargetFrameLowering &TFI) { // Only the rest of the block runs after the sequence, and only because the // block does not continue in the function; getEnforceableExit() ensures that. assert(MBB.succ_empty() && "exit block continues in the function"); @@ -1535,7 +1536,7 @@ static BitVector computeRegsToClearAtExit( // the same: it also spares siblings a clear would widen into a live // register (%ah into %al on x86), and no target-agnostic rule keeps both // that and AArch64's independently-cleared register tuples correct. - if (MI.isReturn()) + if (MI.isReturn() && !TFI.zeroCallUsedRegsPreservesUnrequestedSiblings()) for (MCRegUnit Unit : TRI.regunits(Reg)) RegsToZero.reset(static_cast(Unit)); @@ -1627,9 +1628,9 @@ void PEIImpl::emitClearingStep(ClearingStep Step, const ExitClearingPlan &Plan, case ClearingStep::ClearRegisters: // What to clear is settled here rather than in the plan, because it is the // exit that decides it: see computeRegsToClearAtExit. - TFI.emitZeroCallUsedRegs( - computeRegsToClearAtExit(Plan.CandidateRegsToZero, MBB, InsertPt, TRI), - MBB, InsertPt, RS); + TFI.emitZeroCallUsedRegs(computeRegsToClearAtExit(Plan.CandidateRegsToZero, + MBB, InsertPt, TRI, TFI), + MBB, InsertPt, RS); break; case ClearingStep::ClearFlags: @@ -1783,6 +1784,13 @@ PEIImpl::planClearRegisters(MachineFunction &MF, for (MCRegister Reg : TRI.sub_and_superregs_inclusive(CSReg)) CandidateRegsToZero.reset(Reg.id()); + // Some return instructions read the return address without an explicit + // operand, and conventions such as GHC leave it out of the callee-saved + // list. It must survive clearing regardless of either representation. + if (MCRegister RAReg = TRI.getRARegister()) + for (MCRegister Reg : TRI.sub_and_superregs_inclusive(RAReg)) + CandidateRegsToZero.reset(Reg.id()); + return ClearingDisposition::Emit; } diff --git a/llvm/lib/Target/ARM/ARMFrameLowering.cpp b/llvm/lib/Target/ARM/ARMFrameLowering.cpp index 5a41077860ee5..7724086e2cee9 100644 --- a/llvm/lib/Target/ARM/ARMFrameLowering.cpp +++ b/llvm/lib/Target/ARM/ARMFrameLowering.cpp @@ -1594,6 +1594,20 @@ void ARMFrameLowering::emitEpilogue(MachineFunction &MF, // register is only used when every part of it was asked for. //===----------------------------------------------------------------------===// +bool ARMFrameLowering::supportsZeroCallUsedRegs( + const MachineFunction &MF) const { + // VFP registers may exist on a Thumb-1 target even though Thumb-1 has no + // instructions that can clear them. Keep core-only modes available, but do + // not advertise a request that may need floating-point clearing there. + if (!STI.isThumb1Only() || !STI.hasFPRegs()) + return true; + + StringRef Mode = + MF.getFunction().getFnAttribute("zero-call-used-regs").getValueAsString(); + return Mode == "used-gpr-arg" || Mode == "used-gpr" || + Mode == "all-gpr-arg" || Mode == "all-gpr"; +} + /// Whether \p Reg is one of the registers this step sorts into the /// floating-point half of the work. /// @@ -1754,7 +1768,9 @@ void ARMFrameLowering::emitZeroCallUsedRegs(BitVector RegsToZero, LiveRegUnits Used(TRI); computeLiveUnitsAt(Used, MBB, MBBI); for (MCRegister Reg : ZeroSrcRC) - if (!MRI.isReserved(Reg) && Used.available(Reg)) { + // A return can read LR without naming it as a machine operand, and + // not every calling convention lists it as callee-saved. + if (Reg != ARM::LR && !MRI.isReserved(Reg) && Used.available(Reg)) { ZeroSrc = Reg; break; } diff --git a/llvm/lib/Target/ARM/ARMFrameLowering.h b/llvm/lib/Target/ARM/ARMFrameLowering.h index 16950d54d8329..4094114c6296f 100644 --- a/llvm/lib/Target/ARM/ARMFrameLowering.h +++ b/llvm/lib/Target/ARM/ARMFrameLowering.h @@ -90,6 +90,12 @@ class ARMFrameLowering : public TargetFrameLowering { const SpillSlot * getCalleeSavedSpillSlots(unsigned &NumEntries) const override; + bool supportsZeroCallUsedRegs(const MachineFunction &MF) const override; + + bool zeroCallUsedRegsPreservesUnrequestedSiblings() const override { + return true; + } + protected: bool hasFPImpl(const MachineFunction &MF) const override; diff --git a/llvm/test/CodeGen/ARM/zero-call-used-regs-capabilities.ll b/llvm/test/CodeGen/ARM/zero-call-used-regs-capabilities.ll new file mode 100644 index 0000000000000..63b602943c4a1 --- /dev/null +++ b/llvm/test/CodeGen/ARM/zero-call-used-regs-capabilities.ll @@ -0,0 +1,30 @@ +; RUN: llc -mtriple=armv7-unknown-linux-gnueabi -pei-print-clearing-sequence %s -o /dev/null 2>&1 | FileCheck %s --implicit-check-not=error: +; RUN: llc -mtriple=armebv7-unknown-linux-gnueabi -pei-print-clearing-sequence %s -o /dev/null 2>&1 | FileCheck %s --implicit-check-not=error: +; RUN: llc -mtriple=thumbv6m-none-eabi -pei-print-clearing-sequence %s -o /dev/null 2>&1 | FileCheck %s --implicit-check-not=error: +; RUN: llc -mtriple=thumbv8m.base-none-eabi -pei-print-clearing-sequence %s -o /dev/null 2>&1 | FileCheck %s --implicit-check-not=error: +; RUN: llc -mtriple=thumbv7m-none-eabi -pei-print-clearing-sequence %s -o /dev/null 2>&1 | FileCheck %s --implicit-check-not=error: +; RUN: llc -mtriple=thumbv8m.main-none-eabi -mattr=+fp-armv8d16sp -pei-print-clearing-sequence %s -o /dev/null 2>&1 | FileCheck %s --implicit-check-not=error: +; RUN: llc -mtriple=thumbv7-unknown-linux-gnueabihf -mattr=+neon -pei-print-clearing-sequence %s -o /dev/null 2>&1 | FileCheck %s --implicit-check-not=error: +; RUN: llc -mtriple=thumbv8.1m.main-none-eabi -mattr=+mve -pei-print-clearing-sequence %s -o /dev/null 2>&1 | FileCheck %s --implicit-check-not=error: + +; Register clearing is supported across instruction modes and FP/vector +; configurations. Stack clearing remains an independent, unsupported capability. + +; CHECK-LABEL: clearing sequence for function 'used_gpr': +; CHECK: return: clear-stack=not-requested clear-registers=emitted clear-flags=unimplemented +define i32 @used_gpr(i32 %x) "zero-call-used-regs"="used-gpr" { + ret i32 %x +} + +; CHECK-LABEL: clearing sequence for function 'all': +; CHECK: return: clear-stack=not-requested clear-registers=emitted clear-flags=unimplemented +define i32 @all(i32 %x) "zero-call-used-regs"="all" { + ret i32 %x +} + +; "skip" asks for nothing, so there is nothing to report. +; CHECK-LABEL: clearing sequence for function 'skip': +; CHECK: return: clear-stack=not-requested clear-registers=not-requested clear-flags=unimplemented +define i32 @skip(i32 %x) "zero-call-used-regs"="skip" { + ret i32 %x +} diff --git a/llvm/test/CodeGen/ARM/zero-call-used-regs-cleanup.ll b/llvm/test/CodeGen/ARM/zero-call-used-regs-cleanup.ll new file mode 100644 index 0000000000000..28b125b9685c0 --- /dev/null +++ b/llvm/test/CodeGen/ARM/zero-call-used-regs-cleanup.ll @@ -0,0 +1,50 @@ +; RUN: llc -mtriple=armv7-none-eabi -verify-machineinstrs %s -o - | FileCheck %s +; RUN: llc -mtriple=thumbv7m-none-eabi -verify-machineinstrs %s -o - | FileCheck %s +; RUN: llc -mtriple=thumbv6m-none-eabi -verify-machineinstrs %s -o - | FileCheck %s + +declare void @sink() +declare void @cleanup() +declare i32 @__gxx_personality_v0(...) +declare void @_Unwind_Resume(ptr) noreturn + +; Both the ordinary return and ARM EHABI's cleanup exit need clearing. The +; latter must run before __cxa_end_cleanup rather than at the end of its block. +; CHECK-LABEL: cleanup_and_return: +; CHECK: mov{{.*}} r12, +; CHECK: {{(pop|ldm)}} +; CHECK: bl cleanup +; CHECK: mov{{.*}} r12, +; CHECK-NEXT: bl __cxa_end_cleanup +define i32 @cleanup_and_return(i32 %secret) "zero-call-used-regs"="all-gpr" personality ptr @__gxx_personality_v0 { +entry: + invoke void @sink() to label %cont unwind label %lpad +cont: + ret i32 %secret +lpad: + %l = landingpad { ptr, i32 } cleanup + call void @cleanup() + resume { ptr, i32 } %l +} + +; _Unwind_Resume consumes the exception object in r0. This direct call also +; tests the other cleanup routine understood by the shared coordinator. +; CHECK-LABEL: resume_call: +; CHECK-NOT: mov{{.*}} r0, +; CHECK: mov{{.*}} r12, +; CHECK-NOT: mov{{.*}} r0, +; CHECK-NEXT: bl _Unwind_Resume +define void @resume_call(ptr %exception) "zero-call-used-regs"="all-gpr" { + call void @_Unwind_Resume(ptr %exception) + unreachable +} + +; LR can be absent from the callee-saved list and from the return's explicit +; operands. It is still the return address and must never be cleared. +; CHECK-LABEL: ghc_return: +; CHECK-NOT: mov{{.*}} lr, +; CHECK: mov{{.*}} r12, +; CHECK-NOT: mov{{.*}} lr, +; CHECK: bx lr +define ghccc void @ghc_return() "zero-call-used-regs"="all-gpr" { + ret void +} diff --git a/llvm/test/CodeGen/ARM/zero-call-used-regs-exit-diagnostics.mir b/llvm/test/CodeGen/ARM/zero-call-used-regs-exit-diagnostics.mir new file mode 100644 index 0000000000000..99280aaa8bf27 --- /dev/null +++ b/llvm/test/CodeGen/ARM/zero-call-used-regs-exit-diagnostics.mir @@ -0,0 +1,61 @@ +# RUN: split-file %s %t +# RUN: not llc -mtriple=thumbv6m-none-eabi -run-pass=prolog-epilog -verify-machineinstrs %t/flags.mir -o /dev/null 2>&1 | FileCheck %s --check-prefix=FLAGS +# RUN: llc -mtriple=thumbv7m-none-eabi -run-pass=prolog-epilog -verify-machineinstrs %t/flags.mir -o - | FileCheck %s --check-prefix=THUMB2 +# RUN: llc -mtriple=thumbv8m.base-none-eabi -run-pass=prolog-epilog -verify-machineinstrs %t/flags.mir -o - | FileCheck %s --check-prefix=BASELINE +# RUN: not llc -mtriple=thumbv6m-none-eabi -run-pass=prolog-epilog -verify-machineinstrs %t/scratch.mir -o /dev/null 2>&1 | FileCheck %s --check-prefix=SCRATCH +# RUN: not llc -mtriple=armv7-none-eabihf -run-pass=prolog-epilog -verify-machineinstrs %t/return-address.mir -o /dev/null 2>&1 | FileCheck %s --check-prefix=SCRATCH + +# Classic Thumb-1 needs a flag-setting instruction to materialize zero. A +# predicated exit must retain its flags. Thumb-2 and v8-M Baseline can use a +# flag-preserving move instead. +# FLAGS: error: {{.*}}clearing the call-used registers writes the condition flags on this subtarget, and they are live at this exit +# THUMB2-LABEL: name: live_flags +# THUMB2: $r12 = t2MOVi 0, 14{{.*}}$noreg, $noreg +# THUMB2: tBX_RET 0{{.*}}$cpsr, implicit $r0 +# BASELINE-LABEL: name: live_flags +# BASELINE: $r12 = t2MOVi16 0, 14{{.*}}$noreg +# BASELINE: tBX_RET 0{{.*}}$cpsr, implicit $r0 + +# All caller-saved low registers are live. Clearing r12 must not borrow a +# callee-saved register or overwrite a live result to obtain a low zero source. +# SCRATCH: error: {{.*}}clearing the call-used registers needs a register to hold zero and none is free at this exit + +#--- flags.mir +--- | + define void @live_flags() "zero-call-used-regs"="all-gpr" { ret void } +... +--- +name: live_flags +tracksRegLiveness: true +body: | + bb.0: + liveins: $r0, $cpsr + tBX_RET 0, $cpsr, implicit $r0 +... + +#--- scratch.mir +--- | + define void @no_scratch() "zero-call-used-regs"="all-gpr" { ret void } +... +--- +name: no_scratch +tracksRegLiveness: true +body: | + bb.0: + liveins: $r0, $r1, $r2, $r3 + tBX_RET 14, $noreg, implicit $r0, implicit $r1, implicit $r2, implicit $r3 +... + +#--- return-address.mir +--- | + define ghccc void @no_scratch_lr() "zero-call-used-regs"="used" { ret void } +... +--- +name: no_scratch_lr +tracksRegLiveness: true +body: | + bb.0: + liveins: $r0, $r1, $r2, $r3, $r4, $r5, $r6, $r7, $r8, $r9, $r10, $r11, $r12 + $s1 = VMOVS undef $s2, 14, $noreg + BX_RET 14, $noreg, implicit $r0, implicit $r1, implicit $r2, implicit $r3, implicit $r4, implicit $r5, implicit $r6, implicit $r7, implicit $r8, implicit $r9, implicit $r10, implicit $r11, implicit $r12 +... diff --git a/llvm/test/CodeGen/ARM/zero-call-used-regs-exits.mir b/llvm/test/CodeGen/ARM/zero-call-used-regs-exits.mir new file mode 100644 index 0000000000000..cb40cb3fa7485 --- /dev/null +++ b/llvm/test/CodeGen/ARM/zero-call-used-regs-exits.mir @@ -0,0 +1,84 @@ +# RUN: llc -mtriple=armv7-none-eabi -run-pass=prolog-epilog -verify-machineinstrs %s -o - | FileCheck %s + +# The two returns deliberately have different physical live-outs. A clear at +# one must not inherit the other exit's exclusions. +# CHECK-LABEL: name: different_liveouts +# CHECK: bb.1: +# CHECK-NOT: $r0 = MOVi +# CHECK: $r1 = MOVi 0, +# CHECK: $r2 = MOVi 0, +# CHECK-NOT: $r0 = MOVi +# CHECK: BX_RET 14{{.*}}implicit $r0 +# CHECK: bb.2: +# CHECK: $r0 = MOVi 0, +# CHECK-NOT: $r2 = MOVi +# CHECK: $r3 = MOVi 0, +# CHECK-NOT: $r2 = MOVi +# CHECK: BX_RET 14{{.*}}implicit $r2 + +# Clearing belongs before the non-returning cleanup call, while r0 still +# carries the exception object. Nothing emitted after the call can protect it. +# CHECK-LABEL: name: cleanup_exit +# CHECK-NOT: $r0 = MOVi +# CHECK: $r1 = MOVi 0, +# CHECK-NEXT: $r2 = MOVi 0, +# CHECK-NEXT: $r3 = MOVi 0, +# CHECK-NEXT: $r12 = MOVi 0, +# CHECK-NEXT: BL @_Unwind_Resume, {{.*}}implicit $r0 +# CHECK-NOT: MOVi +# CHECK: ... + +# r4 is modified, saved and restored. Register clearing must preserve the +# restored value as well as the return address and result. +# CHECK-LABEL: name: callee_saved +# CHECK: frame-setup +# CHECK: $r4 = MOVr $r0, +# CHECK: $r1 = MOVi 0, +# CHECK-NOT: $r4 = MOVi +# CHECK-NOT: $lr = MOVi +# CHECK: frame-destroy LDMIA_RET {{.*}}def $r4, def $pc, implicit $r0 + +--- | + declare void @_Unwind_Resume(ptr) + define void @different_liveouts() "zero-call-used-regs"="all-gpr" { ret void } + define void @cleanup_exit(ptr %exception) "zero-call-used-regs"="all-gpr" { + call void @_Unwind_Resume(ptr %exception) + unreachable + } + define void @callee_saved() "zero-call-used-regs"="all-gpr" { ret void } +... +--- +name: different_liveouts +tracksRegLiveness: true +body: | + bb.0: + successors: %bb.1, %bb.2 + liveins: $r0, $r2, $r3 + CMPri $r3, 0, 14, $noreg, implicit-def $cpsr + Bcc %bb.2, 0, killed $cpsr + bb.1: + liveins: $r0 + BX_RET 14, $noreg, implicit $r0 + bb.2: + liveins: $r2 + BX_RET 14, $noreg, implicit $r2 +... +--- +name: cleanup_exit +tracksRegLiveness: true +frameInfo: + hasCalls: true +body: | + bb.0: + liveins: $r0 + BL @_Unwind_Resume, csr_aapcs, implicit-def $lr, implicit $sp, implicit $r0 +... +--- +name: callee_saved +tracksRegLiveness: true +body: | + bb.0: + liveins: $r0 + $r4 = MOVr $r0, 14, $noreg, $noreg + BX_RET 14, $noreg, implicit $r0 +... diff --git a/llvm/test/CodeGen/ARM/zero-call-used-regs-fp-returns.ll b/llvm/test/CodeGen/ARM/zero-call-used-regs-fp-returns.ll new file mode 100644 index 0000000000000..d6c10791df893 --- /dev/null +++ b/llvm/test/CodeGen/ARM/zero-call-used-regs-fp-returns.ll @@ -0,0 +1,32 @@ +; RUN: llc -mtriple=armv7-none-eabihf -mattr=+neon -verify-machineinstrs %s -o - | FileCheck %s --check-prefixes=CHECK,NEON +; RUN: llc -mtriple=armv7-none-eabihf -mattr=+vfp2,-neon -verify-machineinstrs %s -o - | FileCheck %s --check-prefixes=CHECK,VFP +; RUN: llc -mtriple=thumbv8m.main-none-eabihf -mattr=+fp-armv8d16sp -verify-machineinstrs %s -o - | FileCheck %s --check-prefixes=CHECK,VFP +; RUN: llc -mtriple=thumbv8.1m.main-none-eabihf -mattr=+mve.fp -verify-machineinstrs %s -o - | FileCheck %s --check-prefixes=CHECK,MVE +; RUN: llc -mtriple=armv7-none-eabihf -mattr=+neon -filetype=obj %s -o /dev/null +; RUN: llc -mtriple=armv7-none-eabihf -mattr=+vfp3,-neon -filetype=obj %s -o /dev/null +; RUN: llc -mtriple=thumbv8m.main-none-eabihf -mattr=+fp-armv8d16sp -filetype=obj %s -o /dev/null +; RUN: llc -mtriple=thumbv8.1m.main-none-eabihf -mattr=+mve.fp -filetype=obj %s -o /dev/null + +; The unselected half of d0 is a return value. Clearing s1 must not widen to +; d0 or q0, even though vector immediate instructions would be cheaper. +; CHECK-LABEL: float_result: +; CHECK: mov{{.*}} r0, #0 +; CHECK-NOT: {{vmov.*(s0|d0|q0),}} +; CHECK: vmov s1, r0 +; CHECK-NOT: {{vmov.*(s0|d0|q0),}} +; CHECK: bx lr +define arm_aapcs_vfpcc float @float_result(float %x) "zero-call-used-regs"="all-arg" { + ret float %x +} + +; With a whole d0 return value, d1 is still clearable but q0 is not. +; CHECK-LABEL: double_result: +; CHECK-NOT: {{vmov.*(s0|s1|d0|q0),}} +; NEON: vmov.i32 d1, #0x0 +; VFP: vmov d1, r0, r0 +; MVE: vmov d1, r0, r0 +; CHECK-NOT: {{vmov.*(s0|s1|d0|q0),}} +; CHECK: bx lr +define arm_aapcs_vfpcc double @double_result(double %x) "zero-call-used-regs"="all" { + ret double %x +} diff --git a/llvm/test/CodeGen/ARM/zero-call-used-regs-fp.ll b/llvm/test/CodeGen/ARM/zero-call-used-regs-fp.ll index b1b54b869a789..28e3b46144d64 100644 --- a/llvm/test/CodeGen/ARM/zero-call-used-regs-fp.ll +++ b/llvm/test/CodeGen/ARM/zero-call-used-regs-fp.ll @@ -4,10 +4,10 @@ ; register when there is neither, and nothing at all when the registers do not ; exist. -; RUN: llc -mtriple=armv7-unknown-linux-gnueabihf %s -o - | FileCheck %s --check-prefix=NEON -; RUN: llc -mtriple=thumbv8m.main -mattr=+fp-armv8d16sp %s -o - | FileCheck %s --check-prefix=VFP -; RUN: llc -mtriple=thumbv8.1m.main -mattr=+mve %s -o - | FileCheck %s --check-prefix=MVE -; RUN: llc -mtriple=thumbv7m-none-eabi %s -o - | FileCheck %s --check-prefix=NOFP +; RUN: llc -verify-machineinstrs -mtriple=armv7-unknown-linux-gnueabihf %s -o - | FileCheck %s --check-prefix=NEON +; RUN: llc -verify-machineinstrs -mtriple=thumbv8m.main -mattr=+fp-armv8d16sp %s -o - | FileCheck %s --check-prefix=VFP +; RUN: llc -verify-machineinstrs -mtriple=thumbv8.1m.main -mattr=+mve %s -o - | FileCheck %s --check-prefix=MVE +; RUN: llc -verify-machineinstrs -mtriple=thumbv7m-none-eabi %s -o - | FileCheck %s --check-prefix=NOFP ; D8-D15 are callee-saved, so the vector registers built out of them are the ; caller's and are not cleared: on NEON that leaves q0-q3 and q8-q15, and the diff --git a/llvm/test/CodeGen/ARM/zero-call-used-regs-thumb1-vfp.ll b/llvm/test/CodeGen/ARM/zero-call-used-regs-thumb1-vfp.ll new file mode 100644 index 0000000000000..b3fbab34e6438 --- /dev/null +++ b/llvm/test/CodeGen/ARM/zero-call-used-regs-thumb1-vfp.ll @@ -0,0 +1,18 @@ +; RUN: split-file %s %t +; RUN: not llc -mtriple=thumbv6-none-eabi -mattr=+vfp2 %t/fp.ll -o /dev/null 2>&1 | FileCheck %s --check-prefix=REFUSED +; RUN: llc -mtriple=thumbv6-none-eabi -mattr=+vfp2 -verify-machineinstrs %t/gpr.ll -o - | FileCheck %s --check-prefix=GPR + +; The hardware may have a VFP register file, but Thumb-1 cannot emit VFP +; instructions. Reject modes that could require clearing it, while retaining +; the core-register modes on the same configuration. +; REFUSED: error: {{.*}}in function all_regs void (): "zero-call-used-regs" is not supported by this target +; GPR-LABEL: core_regs: +; GPR: movs r0, #0 +; GPR: mov r12, r0 +; GPR-NEXT: bx lr + +;--- fp.ll +define void @all_regs() "zero-call-used-regs"="all" { ret void } + +;--- gpr.ll +define void @core_regs() "zero-call-used-regs"="all-gpr" { ret void } diff --git a/llvm/test/CodeGen/ARM/zero-call-used-regs-unsupported.ll b/llvm/test/CodeGen/ARM/zero-call-used-regs-unsupported.ll deleted file mode 100644 index 31037fcad2bc3..0000000000000 --- a/llvm/test/CodeGen/ARM/zero-call-used-regs-unsupported.ll +++ /dev/null @@ -1,23 +0,0 @@ -; RUN: not llc -mtriple=armv7-unknown-linux-gnueabi < %s -o /dev/null 2>&1 | FileCheck %s - -; ARM does not implement emitZeroCallUsedRegs, so supportsZeroCallUsedRegs is -; false and the request is reported. Before the query it was dropped silently. - -; CHECK: error: {{.*}}in function used_gpr i32 (i32): "zero-call-used-regs" is not supported by this target -define i32 @used_gpr(i32 %x) "zero-call-used-regs"="used-gpr" { - ret i32 %x -} - -; CHECK: error: {{.*}}in function all i32 (i32): "zero-call-used-regs" is not supported by this target -define i32 @all(i32 %x) "zero-call-used-regs"="all" { - ret i32 %x -} - -; The naked case is reported against the function rather than the target, so it -; lives in zeroize-naked.ll. This file's message is about the target. - -; "skip" asks for nothing, so there is nothing to report. -; CHECK-NOT: in function skip -define i32 @skip(i32 %x) "zero-call-used-regs"="skip" { - ret i32 %x -} diff --git a/llvm/test/CodeGen/ARM/zero-call-used-regs.ll b/llvm/test/CodeGen/ARM/zero-call-used-regs.ll index e962ef4c92875..f08ce6c0f2d5b 100644 --- a/llvm/test/CodeGen/ARM/zero-call-used-regs.ll +++ b/llvm/test/CodeGen/ARM/zero-call-used-regs.ll @@ -32,6 +32,7 @@ define dso_local i32 @used_gpr_arg(i32 noundef %a, i32 noundef %b, i32 noundef % ; ARM: @ %bb.0: @ %entry ; ARM-NEXT: mul r0, r1, r0 ; ARM-NEXT: orr r0, r0, r2 +; ARM-NEXT: mov r1, #0 ; ARM-NEXT: mov r2, #0 ; ARM-NEXT: bx lr ; @@ -39,6 +40,7 @@ define dso_local i32 @used_gpr_arg(i32 noundef %a, i32 noundef %b, i32 noundef % ; THUMB2: @ %bb.0: @ %entry ; THUMB2-NEXT: muls r0, r1, r0 ; THUMB2-NEXT: orrs r0, r2 +; THUMB2-NEXT: movs r1, #0 ; THUMB2-NEXT: movs r2, #0 ; THUMB2-NEXT: bx lr ; @@ -46,7 +48,8 @@ define dso_local i32 @used_gpr_arg(i32 noundef %a, i32 noundef %b, i32 noundef % ; THUMB1: @ %bb.0: @ %entry ; THUMB1-NEXT: muls r0, r1, r0 ; THUMB1-NEXT: orrs r0, r2 -; THUMB1-NEXT: movs r2, #0 +; THUMB1-NEXT: movs r1, #0 +; THUMB1-NEXT: mov r2, r1 ; THUMB1-NEXT: bx lr entry: %mul = mul nsw i32 %b, %a @@ -59,6 +62,7 @@ define dso_local i32 @used_gpr(i32 noundef %a, i32 noundef %b, i32 noundef %c) l ; ARM: @ %bb.0: @ %entry ; ARM-NEXT: mul r0, r1, r0 ; ARM-NEXT: orr r0, r0, r2 +; ARM-NEXT: mov r1, #0 ; ARM-NEXT: mov r2, #0 ; ARM-NEXT: bx lr ; @@ -66,6 +70,7 @@ define dso_local i32 @used_gpr(i32 noundef %a, i32 noundef %b, i32 noundef %c) l ; THUMB2: @ %bb.0: @ %entry ; THUMB2-NEXT: muls r0, r1, r0 ; THUMB2-NEXT: orrs r0, r2 +; THUMB2-NEXT: movs r1, #0 ; THUMB2-NEXT: movs r2, #0 ; THUMB2-NEXT: bx lr ; @@ -73,7 +78,8 @@ define dso_local i32 @used_gpr(i32 noundef %a, i32 noundef %b, i32 noundef %c) l ; THUMB1: @ %bb.0: @ %entry ; THUMB1-NEXT: muls r0, r1, r0 ; THUMB1-NEXT: orrs r0, r2 -; THUMB1-NEXT: movs r2, #0 +; THUMB1-NEXT: movs r1, #0 +; THUMB1-NEXT: mov r2, r1 ; THUMB1-NEXT: bx lr entry: %mul = mul nsw i32 %b, %a @@ -86,6 +92,7 @@ define dso_local i32 @used_arg(i32 noundef %a, i32 noundef %b, i32 noundef %c) l ; ARM: @ %bb.0: @ %entry ; ARM-NEXT: mul r0, r1, r0 ; ARM-NEXT: orr r0, r0, r2 +; ARM-NEXT: mov r1, #0 ; ARM-NEXT: mov r2, #0 ; ARM-NEXT: bx lr ; @@ -93,6 +100,7 @@ define dso_local i32 @used_arg(i32 noundef %a, i32 noundef %b, i32 noundef %c) l ; THUMB2: @ %bb.0: @ %entry ; THUMB2-NEXT: muls r0, r1, r0 ; THUMB2-NEXT: orrs r0, r2 +; THUMB2-NEXT: movs r1, #0 ; THUMB2-NEXT: movs r2, #0 ; THUMB2-NEXT: bx lr ; @@ -100,7 +108,8 @@ define dso_local i32 @used_arg(i32 noundef %a, i32 noundef %b, i32 noundef %c) l ; THUMB1: @ %bb.0: @ %entry ; THUMB1-NEXT: muls r0, r1, r0 ; THUMB1-NEXT: orrs r0, r2 -; THUMB1-NEXT: movs r2, #0 +; THUMB1-NEXT: movs r1, #0 +; THUMB1-NEXT: mov r2, r1 ; THUMB1-NEXT: bx lr entry: %mul = mul nsw i32 %b, %a @@ -113,6 +122,7 @@ define dso_local i32 @used(i32 noundef %a, i32 noundef %b, i32 noundef %c) local ; ARM: @ %bb.0: @ %entry ; ARM-NEXT: mul r0, r1, r0 ; ARM-NEXT: orr r0, r0, r2 +; ARM-NEXT: mov r1, #0 ; ARM-NEXT: mov r2, #0 ; ARM-NEXT: bx lr ; @@ -120,6 +130,7 @@ define dso_local i32 @used(i32 noundef %a, i32 noundef %b, i32 noundef %c) local ; THUMB2: @ %bb.0: @ %entry ; THUMB2-NEXT: muls r0, r1, r0 ; THUMB2-NEXT: orrs r0, r2 +; THUMB2-NEXT: movs r1, #0 ; THUMB2-NEXT: movs r2, #0 ; THUMB2-NEXT: bx lr ; @@ -127,7 +138,8 @@ define dso_local i32 @used(i32 noundef %a, i32 noundef %b, i32 noundef %c) local ; THUMB1: @ %bb.0: @ %entry ; THUMB1-NEXT: muls r0, r1, r0 ; THUMB1-NEXT: orrs r0, r2 -; THUMB1-NEXT: movs r2, #0 +; THUMB1-NEXT: movs r1, #0 +; THUMB1-NEXT: mov r2, r1 ; THUMB1-NEXT: bx lr entry: %mul = mul nsw i32 %b, %a @@ -139,6 +151,7 @@ define dso_local i32 @all_gpr_arg(i32 noundef %a, i32 noundef %b, i32 noundef %c ; ARM-LABEL: all_gpr_arg: ; ARM: @ %bb.0: @ %entry ; ARM-NEXT: mul r0, r1, r0 +; ARM-NEXT: mov r1, #0 ; ARM-NEXT: mov r3, #0 ; ARM-NEXT: mov r12, #0 ; ARM-NEXT: orr r0, r0, r2 @@ -148,6 +161,7 @@ define dso_local i32 @all_gpr_arg(i32 noundef %a, i32 noundef %b, i32 noundef %c ; THUMB2-LABEL: all_gpr_arg: ; THUMB2: @ %bb.0: @ %entry ; THUMB2-NEXT: muls r0, r1, r0 +; THUMB2-NEXT: movs r1, #0 ; THUMB2-NEXT: movs r3, #0 ; THUMB2-NEXT: mov.w r12, #0 ; THUMB2-NEXT: orrs r0, r2 @@ -158,9 +172,10 @@ define dso_local i32 @all_gpr_arg(i32 noundef %a, i32 noundef %b, i32 noundef %c ; THUMB1: @ %bb.0: @ %entry ; THUMB1-NEXT: muls r0, r1, r0 ; THUMB1-NEXT: orrs r0, r2 -; THUMB1-NEXT: movs r2, #0 -; THUMB1-NEXT: mov r3, r2 -; THUMB1-NEXT: mov r12, r2 +; THUMB1-NEXT: movs r1, #0 +; THUMB1-NEXT: mov r2, r1 +; THUMB1-NEXT: mov r3, r1 +; THUMB1-NEXT: mov r12, r1 ; THUMB1-NEXT: bx lr entry: %mul = mul nsw i32 %b, %a @@ -172,6 +187,7 @@ define dso_local i32 @all_gpr(i32 noundef %a, i32 noundef %b, i32 noundef %c) lo ; ARM-LABEL: all_gpr: ; ARM: @ %bb.0: @ %entry ; ARM-NEXT: mul r0, r1, r0 +; ARM-NEXT: mov r1, #0 ; ARM-NEXT: mov r3, #0 ; ARM-NEXT: mov r12, #0 ; ARM-NEXT: orr r0, r0, r2 @@ -181,6 +197,7 @@ define dso_local i32 @all_gpr(i32 noundef %a, i32 noundef %b, i32 noundef %c) lo ; THUMB2-LABEL: all_gpr: ; THUMB2: @ %bb.0: @ %entry ; THUMB2-NEXT: muls r0, r1, r0 +; THUMB2-NEXT: movs r1, #0 ; THUMB2-NEXT: movs r3, #0 ; THUMB2-NEXT: mov.w r12, #0 ; THUMB2-NEXT: orrs r0, r2 @@ -191,12 +208,42 @@ define dso_local i32 @all_gpr(i32 noundef %a, i32 noundef %b, i32 noundef %c) lo ; THUMB1: @ %bb.0: @ %entry ; THUMB1-NEXT: muls r0, r1, r0 ; THUMB1-NEXT: orrs r0, r2 -; THUMB1-NEXT: movs r2, #0 -; THUMB1-NEXT: mov r3, r2 -; THUMB1-NEXT: mov r12, r2 +; THUMB1-NEXT: movs r1, #0 +; THUMB1-NEXT: mov r2, r1 +; THUMB1-NEXT: mov r3, r1 +; THUMB1-NEXT: mov r12, r1 ; THUMB1-NEXT: bx lr entry: %mul = mul nsw i32 %b, %a %or = or i32 %mul, %c ret i32 %or } + +; The remaining argument-only mode includes every eligible argument register, +; not just registers used by this function. +define i32 @all_arg(i32 %x) "zero-call-used-regs"="all-arg" { +; ARM-LABEL: all_arg: +; ARM: @ %bb.0: +; ARM-NEXT: mov r1, #0 +; ARM-NEXT: mov r2, #0 +; ARM-NEXT: mov r3, #0 +; ARM-NEXT: mov r12, #0 +; ARM-NEXT: bx lr +; +; THUMB2-LABEL: all_arg: +; THUMB2: @ %bb.0: +; THUMB2-NEXT: movs r1, #0 +; THUMB2-NEXT: movs r2, #0 +; THUMB2-NEXT: movs r3, #0 +; THUMB2-NEXT: mov.w r12, #0 +; THUMB2-NEXT: bx lr +; +; THUMB1-LABEL: all_arg: +; THUMB1: @ %bb.0: +; THUMB1-NEXT: movs r1, #0 +; THUMB1-NEXT: mov r2, r1 +; THUMB1-NEXT: mov r3, r1 +; THUMB1-NEXT: mov r12, r1 +; THUMB1-NEXT: bx lr + ret i32 %x +} diff --git a/llvm/test/CodeGen/ARM/zeroize-clearing-sequence.ll b/llvm/test/CodeGen/ARM/zeroize-clearing-sequence.ll index 85bb6c29f222e..44d42dda6c202 100644 --- a/llvm/test/CodeGen/ARM/zeroize-clearing-sequence.ll +++ b/llvm/test/CodeGen/ARM/zeroize-clearing-sequence.ll @@ -1,17 +1,13 @@ -; The sequence is planned for every protected function, on every target, and a -; step the target cannot discharge is reported and then keeps its place in the -; order rather than collapsing it. ARM implements neither clearing the stack -; nor clearing the call-used registers, so both steps are refused here and the -; order is all that is left of them. +; ARM supports the register-clearing step independently of stack clearing. +; An unsupported stack request must not suppress the register clear. -; RUN: not llc -mtriple=armv7-unknown-linux-gnueabi -pei-print-clearing-sequence %s -o /dev/null 2>&1 | FileCheck %s +; RUN: llc -mtriple=armv7-unknown-linux-gnueabi -pei-print-clearing-sequence %s -o /dev/null 2>&1 | FileCheck %s declare i32 @callee(i32) ; CHECK: warning: {{.*}}in function both i32 (i32): "zeroize-stack" is not supported by this target -; CHECK: error: {{.*}}in function both i32 (i32): "zero-call-used-regs" is not supported by this target ; CHECK-LABEL: clearing sequence for function 'both': -; CHECK-NEXT: %bb.0 return: clear-stack=unsupported clear-registers=unsupported clear-flags=unimplemented +; CHECK-NEXT: %bb.0 return: clear-stack=unsupported clear-registers=emitted clear-flags=unimplemented ; CHECK-NEXT: end clearing sequence for function 'both' define i32 @both(i32 %x) "zeroize-stack"="used" "zero-call-used-regs"="used-gpr" { ret i32 %x @@ -20,7 +16,7 @@ define i32 @both(i32 %x) "zeroize-stack"="used" "zero-call-used-regs"="used-gpr" ; The exits are classified the same way whatever the target can do with them: ; nothing about which blocks are in scope is x86's. ; CHECK-LABEL: clearing sequence for function 'exits': -; CHECK-NEXT: %bb.0 tail-call: clear-stack=not-requested clear-registers=unsupported clear-flags=unimplemented +; CHECK-NEXT: %bb.0 tail-call: clear-stack=not-requested clear-registers=emitted clear-flags=unimplemented ; CHECK-NEXT: end clearing sequence for function 'exits' define i32 @exits(i32 %x) "zero-call-used-regs"="used-gpr" { %r = tail call i32 @callee(i32 %x) diff --git a/llvm/test/CodeGen/ARM/zeroize-naked.ll b/llvm/test/CodeGen/ARM/zeroize-naked.ll index 20afb58da1c20..c40c43ec0af0f 100644 --- a/llvm/test/CodeGen/ARM/zeroize-naked.ll +++ b/llvm/test/CodeGen/ARM/zeroize-naked.ll @@ -50,7 +50,3 @@ define void @naked_skip() naked "zero-call-used-regs"="skip" { call void asm sideeffect "nop", ""() ret void } - -; The non-naked control for this attribute lives in -; zero-call-used-regs-unsupported.ll: ARM errors there, which would make llc -; exit non-zero and destroy what these RUN lines pin. diff --git a/llvm/test/CodeGen/ARM/zeroize-per-exit-regs.ll b/llvm/test/CodeGen/ARM/zeroize-per-exit-regs.ll index 2343fe2935bf9..eede505b1dc05 100644 --- a/llvm/test/CodeGen/ARM/zeroize-per-exit-regs.ll +++ b/llvm/test/CodeGen/ARM/zeroize-per-exit-regs.ll @@ -1,20 +1,14 @@ -; Which registers the clear covers is now decided at each exit, but whether the -; step runs at all is still decided for the function. The two are easy to -; confuse once one of them has moved, and a target that refuses the request is -; where the difference shows: ARM cannot clear call-used registers, so a -; function that asks for it has to be told once, however many exits it has, and -; every exit has to report the same refusal rather than some of them planning -; their own. +; Every supported exit runs the register clear once capability dispatch is +; enabled, including a tail call whose outgoing arguments must be preserved. -; RUN: not llc -mtriple=armv7-unknown-linux-gnueabi -pei-print-clearing-sequence %s -o /dev/null 2>&1 | FileCheck %s +; RUN: llc -mtriple=armv7-unknown-linux-gnueabi -pei-print-clearing-sequence %s -o /dev/null 2>&1 | FileCheck %s declare i32 @callee(i32, i32) -; CHECK: error: {{.*}}in function two_exits i32 (i1, i32, i32): "zero-call-used-regs" is not supported by this target ; CHECK-NOT: error: ; CHECK-LABEL: clearing sequence for function 'two_exits': -; CHECK-NEXT: %bb.1 tail-call: clear-stack=not-requested clear-registers=unsupported clear-flags=unimplemented -; CHECK-NEXT: %bb.2 return: clear-stack=not-requested clear-registers=unsupported clear-flags=unimplemented +; CHECK-NEXT: %bb.1 tail-call: clear-stack=not-requested clear-registers=emitted clear-flags=unimplemented +; CHECK-NEXT: %bb.2 return: clear-stack=not-requested clear-registers=emitted clear-flags=unimplemented ; CHECK-NEXT: end clearing sequence for function 'two_exits' define i32 @two_exits(i1 %c, i32 %a, i32 %b) "zero-call-used-regs"="used-gpr" { entry: From 7920fc5086ed7d5b4adb65b9971a8b79588ce87a Mon Sep 17 00:00:00 2001 From: AkshayK Date: Mon, 14 Sep 2026 18:41:16 -0400 Subject: [PATCH 2/2] fix: resolve code review findings for PR #24 Fix P1 vector-return corruption by removing live FP leaves after expanding overlapping register tuples and before selecting clearing widths. This preserves live Q-register return values while retaining clears of dead leaves. Add NEON and MVE vector-return regressions with machine verification and object emission. The regression fails before the fix; all 21 focused LLVM and Clang checks pass afterward. Changed-line formatting and diff checks pass; independent Codex review found no actionable regressions. --- llvm/lib/Target/ARM/ARMFrameLowering.cpp | 10 ++++++++++ .../ARM/zero-call-used-regs-vector-returns.ll | 16 ++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 llvm/test/CodeGen/ARM/zero-call-used-regs-vector-returns.ll diff --git a/llvm/lib/Target/ARM/ARMFrameLowering.cpp b/llvm/lib/Target/ARM/ARMFrameLowering.cpp index 7724086e2cee9..fe0285952025b 100644 --- a/llvm/lib/Target/ARM/ARMFrameLowering.cpp +++ b/llvm/lib/Target/ARM/ARMFrameLowering.cpp @@ -1690,6 +1690,16 @@ void ARMFrameLowering::emitZeroCallUsedRegs(BitVector RegsToZero, } const bool ClearVPR = RegsToZero.test(ARM::VPR) && STI.hasMVEIntegerOps(); + // PEI excludes subregisters and superregisters of live exit operands, but a + // partially overlapping tuple can survive that exclusion. Expanding it above + // can therefore reintroduce live leaves (for example, D0_D2 overlaps a Q0 + // return value). Remove those leaves before choosing the clearing widths. + LiveRegUnits LiveUnits(TRI); + computeLiveUnitsAt(LiveUnits, MBB, MBBI); + for (MCRegister Reg : FPLeaves.set_bits()) + if (!LiveUnits.available(Reg)) + FPLeaves.reset(Reg); + // Reduce the leaves to the widest register that covers only leaves that were // asked for. Q first, then D, and whatever is left stays an S. auto coversOnlyRequested = [&](MCRegister Reg) { diff --git a/llvm/test/CodeGen/ARM/zero-call-used-regs-vector-returns.ll b/llvm/test/CodeGen/ARM/zero-call-used-regs-vector-returns.ll new file mode 100644 index 0000000000000..5a1e9307587c2 --- /dev/null +++ b/llvm/test/CodeGen/ARM/zero-call-used-regs-vector-returns.ll @@ -0,0 +1,16 @@ +; RUN: llc -mtriple=armv7-none-eabihf -mattr=+neon -verify-machineinstrs %s -o - | FileCheck %s +; RUN: llc -mtriple=thumbv8.1m.main-none-eabihf -mattr=+mve -verify-machineinstrs %s -o - | FileCheck %s +; RUN: llc -mtriple=thumbv8.1m.main-none-eabihf -mattr=+mve.fp -verify-machineinstrs %s -o - | FileCheck %s +; RUN: llc -mtriple=armv7-none-eabihf -mattr=+neon -filetype=obj %s -o /dev/null +; RUN: llc -mtriple=thumbv8.1m.main-none-eabihf -mattr=+mve -filetype=obj %s -o /dev/null + +; A tuple may overlap q0 without being its subregister or superregister. +; Expanding such a candidate must not reintroduce live return-value leaves. +; CHECK-LABEL: vector_result: +; CHECK-NOT: {{vmov.*(q0|d0|d1|s0|s1|s2|s3),}} +; CHECK: vmov.i32 q1, #0x0 +; CHECK-NOT: {{vmov.*(q0|d0|d1|s0|s1|s2|s3),}} +; CHECK: bx lr +define arm_aapcs_vfpcc <4 x i32> @vector_result(<4 x i32> %x) "zero-call-used-regs"="all" { + ret <4 x i32> %x +}