Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
0166f89
[RISCV] Fix musttail with indirect arguments by forwarding incoming p…
xroche Mar 6, 2026
d94c5a0
[RISCV] Address review: use DenseMap for indirect args, add tests
xroche Mar 7, 2026
ecb7952
[RISCV] Fix musttail indirect arg forwarding when arguments are reord…
xroche Mar 7, 2026
568648b
[RISCV] Handle computed values in musttail indirect arg forwarding
xroche Mar 9, 2026
484f135
[RISCV][NFC] Apply clang-format to musttail indirect arg forwarding
xroche Mar 9, 2026
0caa871
Merge remote-tracking branch 'origin/main' into riscv-musttail-indire…
xroche Apr 19, 2026
76df154
[RISCV] Fix SDValue lifetime for musttail indirect arg forwarding
xroche Apr 19, 2026
782cd6b
[RISCV] Reject byval in musttail to avoid silent miscompile
xroche Apr 21, 2026
3c3433f
[RISCV] Address musttail review: thread CopyFromReg chain, use getUnk…
xroche Apr 21, 2026
b2a04bb
[RISCV] Add musttail tests for stack-spilled, sret, and mixed indirec…
xroche Apr 21, 2026
960401f
[RISCV] Sequence musttail computed-arg stores after CopyFromReg
xroche Apr 28, 2026
d9c9be7
[RISCV][NFC] Regenerate CHECK lines for musttail computed-args test
xroche Apr 28, 2026
f888fe4
[RISCV] Handle scalable vectors in musttail computed-arg parts loop
xroche May 6, 2026
718f9cf
[RISCV][NFC] Clarify why musttail computed-arg stores chain off CopyF…
xroche May 6, 2026
63260ea
[RISCV][NFC] Keep isEligibleForTailCallOptimization on one line in te…
xroche May 6, 2026
b88aa6f
[RISCV] Add musttail tests for scalable vector indirect args
xroche May 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 152 additions & 50 deletions llvm/lib/Target/RISCV/RISCVISelLowering.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24352,6 +24352,18 @@ SDValue RISCVTargetLowering::LowerFormalArguments(
EVT PtrVT = getPointerTy(DAG.getDataLayout());
MVT XLenVT = Subtarget.getXLenVT();
unsigned XLenInBytes = Subtarget.getXLen() / 8;

// Check if this function has any musttail calls. If so, incoming indirect
// arg pointers must be saved in virtual registers so they survive across
// basic blocks (the SelectionDAG is cleared between BBs). Only do this
// when needed to avoid adding register pressure to non-musttail functions.
bool HasMusttail = llvm::any_of(Func, [](const BasicBlock &BB) {
return llvm::any_of(BB, [](const Instruction &I) {
if (const auto *CI = dyn_cast<CallInst>(&I))
return CI->isMustTailCall();
return false;
});
});
// Used with vargs to accumulate store chains.
std::vector<SDValue> OutChains;

Expand Down Expand Up @@ -24383,6 +24395,13 @@ SDValue RISCVTargetLowering::LowerFormalArguments(
InVals.push_back(DAG.getLoad(VA.getValVT(), DL, Chain, ArgValue,
MachinePointerInfo()));
unsigned ArgIndex = Ins[InsIdx].OrigArgIndex;
if (HasMusttail) {
RISCVMachineFunctionInfo *RVFI = MF.getInfo<RISCVMachineFunctionInfo>();
Register VReg =
MF.getRegInfo().createVirtualRegister(&RISCV::GPRRegClass);
Chain = DAG.getCopyToReg(Chain, DL, VReg, ArgValue);
RVFI->setIncomingIndirectArg(ArgIndex, VReg);
}
unsigned ArgPartOffset = Ins[InsIdx].PartOffset;
assert(VA.getValVT().isVector() || ArgPartOffset == 0);
while (i + 1 != e && Ins[InsIdx + 1].OrigArgIndex == ArgIndex) {
Expand Down Expand Up @@ -24491,18 +24510,20 @@ bool RISCVTargetLowering::isEligibleForTailCallOptimization(
if (Caller.hasFnAttribute("interrupt"))
return false;

// musttail calls must always be tail-called. The remaining checks are
// for optional tail call optimization of regular (non-musttail) calls.
if (CLI.CB && CLI.CB->isMustTailCall())
return true;

// Do not tail call opt if the stack is used to pass parameters.
if (CCInfo.getStackSize() != 0)
return false;

// Do not tail call opt if any parameters need to be passed indirectly.
// Since long doubles (fp128) and i128 are larger than 2*XLEN, they are
// passed indirectly. So the address of the value will be passed in a
// register, or if not available, then the address is put on the stack. In
// order to pass indirectly, space on the stack often needs to be allocated
// in order to store the value. In this case the CCInfo.getNextStackOffset()
// != 0 check is not enough and we need to check if any CCValAssign ArgsLocs
// are passed CCValAssign::Indirect.
// passed indirectly. The caller allocates stack space for the value and
// passes a pointer. On a tail call the caller's frame is deallocated
// before the callee executes, leaving the pointer dangling.
for (auto &VA : ArgLocs)
if (VA.getLocInfo() == CCValAssign::Indirect)
return false;
Expand Down Expand Up @@ -24665,51 +24686,132 @@ SDValue RISCVTargetLowering::LowerCall(CallLoweringInfo &CLI,
// Promote the value if needed.
// For now, only handle fully promoted and indirect arguments.
if (VA.getLocInfo() == CCValAssign::Indirect) {
// Store the argument in a stack slot and pass its address.
Align StackAlign =
std::max(getPrefTypeAlign(Outs[OutIdx].ArgVT, DAG),
getPrefTypeAlign(ArgValue.getValueType(), DAG));
TypeSize StoredSize = ArgValue.getValueType().getStoreSize();
// If the original argument was split (e.g. i128), we need
// to store the required parts of it here (and pass just one address).
// Vectors may be partly split to registers and partly to the stack, in
// which case the base address is partly offset and subsequent stores are
// relative to that.
unsigned ArgIndex = Outs[OutIdx].OrigArgIndex;
unsigned ArgPartOffset = Outs[OutIdx].PartOffset;
assert(VA.getValVT().isVector() || ArgPartOffset == 0);
// Calculate the total size to store. We don't have access to what we're
// actually storing other than performing the loop and collecting the
// info.
SmallVector<std::pair<SDValue, SDValue>> Parts;
while (i + 1 != e && Outs[OutIdx + 1].OrigArgIndex == ArgIndex) {
SDValue PartValue = OutVals[OutIdx + 1];
unsigned PartOffset = Outs[OutIdx + 1].PartOffset - ArgPartOffset;
SDValue Offset = DAG.getIntPtrConstant(PartOffset, DL);
EVT PartVT = PartValue.getValueType();
if (PartVT.isScalableVector())
Offset = DAG.getNode(ISD::VSCALE, DL, XLenVT, Offset);
StoredSize += PartVT.getStoreSize();
StackAlign = std::max(StackAlign, getPrefTypeAlign(PartVT, DAG));
Parts.push_back(std::make_pair(PartValue, Offset));
++i;
++OutIdx;
}
SDValue SpillSlot = DAG.CreateStackTemporary(StoredSize, StackAlign);
int FI = cast<FrameIndexSDNode>(SpillSlot)->getIndex();
MemOpChains.push_back(
DAG.getStore(Chain, DL, ArgValue, SpillSlot,
MachinePointerInfo::getFixedStack(MF, FI)));
for (const auto &Part : Parts) {
SDValue PartValue = Part.first;
SDValue PartOffset = Part.second;
SDValue Address =
DAG.getNode(ISD::ADD, DL, PtrVT, SpillSlot, PartOffset);
// For musttail calls, reuse incoming indirect pointers instead of
// creating new stack temporaries. The incoming pointers point to the
// caller's caller's frame, which remains valid after a tail call.
if (IsTailCall && CLI.CB && CLI.CB->isMustTailCall()) {
Comment thread
lenary marked this conversation as resolved.
RISCVMachineFunctionInfo *RVFI = MF.getInfo<RISCVMachineFunctionInfo>();
unsigned CallArgIdx = Outs[OutIdx].OrigArgIndex;

// Resolve which formal parameter is being passed at this call
// position. Outs[].OrigArgIndex indexes the filtered arg list
// (empty types removed), but IncomingIndirectArgs is keyed by
// Argument::getArgNo() (unfiltered position). We need to:
// 1. Find the call operand at filtered position CallArgIdx
// 2. Check if it's a forwarded formal param (dyn_cast<Argument>)
// 3. Resolve the unfiltered formal param index for the map lookup
Comment thread
xroche marked this conversation as resolved.
Outdated
const Argument *FormalArg = nullptr;
unsigned FilteredIdx = 0;
for (const auto &CallArg : CLI.CB->args()) {
if (CallArg->getType()->isEmptyTy())
continue;
if (FilteredIdx == CallArgIdx) {
FormalArg = dyn_cast<Argument>(CallArg);
break;
}
++FilteredIdx;
}

// For forwarded args, getArgNo() gives the unfiltered index directly.
// For computed args, walk the caller's formals to resolve it.
unsigned FormalArgIdx = CallArgIdx;
if (FormalArg) {
FormalArgIdx = FormalArg->getArgNo();
} else {
const Function *CallerFn = CLI.CB->getFunction();
Comment thread
lenary marked this conversation as resolved.
Outdated
FilteredIdx = 0;
for (const auto &Arg : CallerFn->args()) {
if (Arg.getType()->isEmptyTy())
continue;
if (FilteredIdx == CallArgIdx) {
FormalArgIdx = Arg.getArgNo();
break;
}
++FilteredIdx;
}
}

if (FormalArg) {
// Forwarded formal parameter: reuse its incoming indirect pointer.
Register VReg = RVFI->getIncomingIndirectArg(FormalArgIdx);
ArgValue = DAG.getCopyFromReg(Chain, DL, VReg, PtrVT);
Comment thread
xroche marked this conversation as resolved.
Outdated
} else {
// Computed value: store into the incoming indirect pointer for the
// same-position formal parameter (musttail guarantees matching
// prototypes, so types match). The pointer survives the tail call
// since it points to the caller's caller's frame.
Register VReg = RVFI->getIncomingIndirectArg(FormalArgIdx);
SDValue IncomingPtr = DAG.getCopyFromReg(Chain, DL, VReg, PtrVT);
MemOpChains.push_back(DAG.getStore(Chain, DL, ArgValue, IncomingPtr,
MachinePointerInfo()));
Comment thread
xroche marked this conversation as resolved.
Outdated
// Store any split parts at their respective offsets.
unsigned ArgPartOffset = Outs[OutIdx].PartOffset;
while (i + 1 != e && Outs[OutIdx + 1].OrigArgIndex == CallArgIdx) {
SDValue PartValue = OutVals[OutIdx + 1];
unsigned PartOffset = Outs[OutIdx + 1].PartOffset - ArgPartOffset;
SDValue Addr = DAG.getNode(ISD::ADD, DL, PtrVT, IncomingPtr,
DAG.getIntPtrConstant(PartOffset, DL));
MemOpChains.push_back(
DAG.getStore(Chain, DL, PartValue, Addr, MachinePointerInfo()));
++i;
++OutIdx;
}
ArgValue = IncomingPtr;
}

// Skip any remaining split parts (for forwarded args, they are
// covered by the forwarded pointer).
while (i + 1 != e && Outs[OutIdx + 1].OrigArgIndex == CallArgIdx) {
++i;
++OutIdx;
Comment thread
xroche marked this conversation as resolved.
Outdated
}
Comment thread
xroche marked this conversation as resolved.
Outdated
} else {
Comment thread
xroche marked this conversation as resolved.
// Store the argument in a stack slot and pass its address.
Align StackAlign =
std::max(getPrefTypeAlign(Outs[OutIdx].ArgVT, DAG),
getPrefTypeAlign(ArgValue.getValueType(), DAG));
TypeSize StoredSize = ArgValue.getValueType().getStoreSize();
// If the original argument was split (e.g. i128), we need
Comment thread
xroche marked this conversation as resolved.
// to store the required parts of it here (and pass just one address).
// Vectors may be partly split to registers and partly to the stack, in
// which case the base address is partly offset and subsequent stores
// are relative to that.
unsigned ArgIndex = Outs[OutIdx].OrigArgIndex;
unsigned ArgPartOffset = Outs[OutIdx].PartOffset;
assert(VA.getValVT().isVector() || ArgPartOffset == 0);
// Calculate the total size to store. We don't have access to what
// we're actually storing other than performing the loop and collecting
// the info.
SmallVector<std::pair<SDValue, SDValue>> Parts;
while (i + 1 != e && Outs[OutIdx + 1].OrigArgIndex == ArgIndex) {
SDValue PartValue = OutVals[OutIdx + 1];
unsigned PartOffset = Outs[OutIdx + 1].PartOffset - ArgPartOffset;
SDValue Offset = DAG.getIntPtrConstant(PartOffset, DL);
EVT PartVT = PartValue.getValueType();
if (PartVT.isScalableVector())
Offset = DAG.getNode(ISD::VSCALE, DL, XLenVT, Offset);
StoredSize += PartVT.getStoreSize();
StackAlign = std::max(StackAlign, getPrefTypeAlign(PartVT, DAG));
Parts.push_back(std::make_pair(PartValue, Offset));
++i;
++OutIdx;
}
SDValue SpillSlot = DAG.CreateStackTemporary(StoredSize, StackAlign);
int FI = cast<FrameIndexSDNode>(SpillSlot)->getIndex();
MemOpChains.push_back(
DAG.getStore(Chain, DL, PartValue, Address,
DAG.getStore(Chain, DL, ArgValue, SpillSlot,
MachinePointerInfo::getFixedStack(MF, FI)));
for (const auto &Part : Parts) {
SDValue PartValue = Part.first;
SDValue PartOffset = Part.second;
SDValue Address =
DAG.getNode(ISD::ADD, DL, PtrVT, SpillSlot, PartOffset);
MemOpChains.push_back(
DAG.getStore(Chain, DL, PartValue, Address,
MachinePointerInfo::getFixedStack(MF, FI)));
}
ArgValue = SpillSlot;
}
ArgValue = SpillSlot;
} else {
ArgValue = convertValVTToLocVT(DAG, ArgValue, VA, DL, Subtarget);
}
Expand All @@ -24727,8 +24829,8 @@ SDValue RISCVTargetLowering::LowerCall(CallLoweringInfo &CLI,
CSInfo.ArgRegPairs.emplace_back(VA.getLocReg(), i);
} else {
assert(VA.isMemLoc() && "Argument not register or memory");
assert(!IsTailCall && "Tail call not allowed if stack is used "
"for passing parameters");
assert((!IsTailCall || (CLI.CB && CLI.CB->isMustTailCall())) &&
"Tail call not allowed if stack is used for passing parameters");

// Work out the address of the stack slot.
if (!StackPtr.getNode())
Expand Down
18 changes: 18 additions & 0 deletions llvm/lib/Target/RISCV/RISCVMachineFunctionInfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#define LLVM_LIB_TARGET_RISCV_RISCVMACHINEFUNCTIONINFO_H

#include "RISCVSubtarget.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/CodeGen/MIRYamlMapping.h"
#include "llvm/CodeGen/MachineFrameInfo.h"
#include "llvm/CodeGen/MachineFunction.h"
Expand Down Expand Up @@ -65,6 +66,14 @@ class RISCVMachineFunctionInfo : public MachineFunctionInfo {
uint64_t RVVPadding = 0;
/// Size of stack frame to save callee saved registers
unsigned CalleeSavedStackSize = 0;

/// Incoming indirect argument pointers saved as virtual registers, keyed by
/// formal parameter index. Used for musttail forwarding of indirect args.
/// Virtual registers (not SDValues) are used because the SelectionDAG is
/// cleared between basic blocks, and musttail calls may be in non-entry
/// blocks.
DenseMap<unsigned, Register> IncomingIndirectArgs;

/// Is there any vector argument or return?
bool IsVectorCall = false;

Expand Down Expand Up @@ -145,6 +154,15 @@ class RISCVMachineFunctionInfo : public MachineFunctionInfo {
unsigned getCalleeSavedStackSize() const { return CalleeSavedStackSize; }
void setCalleeSavedStackSize(unsigned Size) { CalleeSavedStackSize = Size; }

void setIncomingIndirectArg(unsigned ArgIndex, Register Reg) {
IncomingIndirectArgs[ArgIndex] = Reg;
}
Register getIncomingIndirectArg(unsigned ArgIndex) const {
auto It = IncomingIndirectArgs.find(ArgIndex);
assert(It != IncomingIndirectArgs.end() && "No incoming indirect arg");
return It->second;
}

enum class PushPopKind { None = 0, StdExtZcmp, VendorXqccmp };

PushPopKind getPushPopKind(const MachineFunction &MF) const;
Expand Down
17 changes: 7 additions & 10 deletions llvm/test/CodeGen/RISCV/musttail-call.ll
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
; Check that we error out if tail is not possible but call is marked as mustail.
; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py UTC_ARGS: --version 5
; Check that musttail with sret generates a tail call (not an error).

; RUN: not --crash llc -mtriple riscv32-unknown-linux-gnu -o - %s \
; RUN: 2>&1 | FileCheck %s
; RUN: not --crash llc -mtriple riscv32-unknown-elf -o - %s \
; RUN: 2>&1 | FileCheck %s
; RUN: not --crash llc -mtriple riscv64-unknown-linux-gnu -o - %s \
; RUN: 2>&1 | FileCheck %s
; RUN: not --crash llc -mtriple riscv64-unknown-elf -o - %s \
; RUN: 2>&1 | FileCheck %s
; RUN: llc -mtriple riscv32-unknown-linux-gnu -o - %s | FileCheck %s
; RUN: llc -mtriple riscv64-unknown-linux-gnu -o - %s | FileCheck %s

%struct.A = type { i32 }

declare void @callee_musttail(ptr sret(%struct.A) %a)
define void @caller_musttail(ptr sret(%struct.A) %a) {
; CHECK: LLVM ERROR: failed to perform tail call elimination on a call site marked musttail
; CHECK-LABEL: caller_musttail:
; CHECK: # %bb.0: # %entry
; CHECK-NEXT: tail callee_musttail
entry:
musttail call void @callee_musttail(ptr sret(%struct.A) %a)
ret void
Expand Down
Loading