[llvm] [RISCV] Support tail calls for functions with sret parameters (PR #223999)

via llvm-commits llvm-commits at lists.llvm.org
Wed Sep 23 18:21:53 PDT 2026


https://github.com/renndong updated https://github.com/llvm/llvm-project/pull/223999

>From 321b04323e310ec0f605851f1ade4bf4e948c6cc Mon Sep 17 00:00:00 2001
From: Mingliang Liu <liumingliang.dev at bytedance.com>
Date: Wed, 16 Sep 2026 19:50:01 +0800
Subject: [PATCH 1/6] [RISCV] Add tail call test

---
 llvm/test/CodeGen/RISCV/tail-calls-sret.ll | 192 +++++++++++++++++++++
 1 file changed, 192 insertions(+)
 create mode 100644 llvm/test/CodeGen/RISCV/tail-calls-sret.ll

diff --git a/llvm/test/CodeGen/RISCV/tail-calls-sret.ll b/llvm/test/CodeGen/RISCV/tail-calls-sret.ll
new file mode 100644
index 0000000000000..f09c2fdf6d2ad
--- /dev/null
+++ b/llvm/test/CodeGen/RISCV/tail-calls-sret.ll
@@ -0,0 +1,192 @@
+; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py UTC_ARGS: --version 5
+; RUN: llc -mtriple=riscv32 -verify-machineinstrs < %s | FileCheck %s --check-prefix=RV32
+; RUN: llc -mtriple=riscv64 -verify-machineinstrs < %s | FileCheck %s --check-prefix=RV64
+
+%struct.Buffer = type { [7 x i64] }
+
+declare void @forward_sret(ptr sret(%struct.Buffer), i64)
+declare void @use_pointer(ptr)
+declare void @use_as_second_arg(i32, ptr)
+
+; Matching caller and callee sret semantics can reuse the incoming
+; return buffer.
+define void @forward_result(ptr noalias sret(%struct.Buffer) %result, i64 %tag) {
+; RV32-LABEL: forward_result:
+; RV32:       # %bb.0: # %entry
+; RV32-NEXT:    addi sp, sp, -16
+; RV32-NEXT:    .cfi_def_cfa_offset 16
+; RV32-NEXT:    sw ra, 12(sp) # 4-byte Folded Spill
+; RV32-NEXT:    .cfi_offset ra, -4
+; RV32-NEXT:    call forward_sret
+; RV32-NEXT:    lw ra, 12(sp) # 4-byte Folded Reload
+; RV32-NEXT:    .cfi_restore ra
+; RV32-NEXT:    addi sp, sp, 16
+; RV32-NEXT:    .cfi_def_cfa_offset 0
+; RV32-NEXT:    ret
+;
+; RV64-LABEL: forward_result:
+; RV64:       # %bb.0: # %entry
+; RV64-NEXT:    addi sp, sp, -16
+; RV64-NEXT:    .cfi_def_cfa_offset 16
+; RV64-NEXT:    sd ra, 8(sp) # 8-byte Folded Spill
+; RV64-NEXT:    .cfi_offset ra, -8
+; RV64-NEXT:    call forward_sret
+; RV64-NEXT:    ld ra, 8(sp) # 8-byte Folded Reload
+; RV64-NEXT:    .cfi_restore ra
+; RV64-NEXT:    addi sp, sp, 16
+; RV64-NEXT:    .cfi_def_cfa_offset 0
+; RV64-NEXT:    ret
+entry:
+  tail call void @forward_sret(ptr sret(%struct.Buffer) %result, i64 %tag)
+  ret void
+}
+
+; A caller's sret pointer does not need to be forwarded to a callee without
+; sret semantics.
+define void @caller_sret_unused(ptr noalias sret(%struct.Buffer) %result, ptr %other) {
+; RV32-LABEL: caller_sret_unused:
+; RV32:       # %bb.0: # %entry
+; RV32-NEXT:    addi sp, sp, -16
+; RV32-NEXT:    .cfi_def_cfa_offset 16
+; RV32-NEXT:    sw ra, 12(sp) # 4-byte Folded Spill
+; RV32-NEXT:    .cfi_offset ra, -4
+; RV32-NEXT:    mv a0, a1
+; RV32-NEXT:    call use_pointer
+; RV32-NEXT:    lw ra, 12(sp) # 4-byte Folded Reload
+; RV32-NEXT:    .cfi_restore ra
+; RV32-NEXT:    addi sp, sp, 16
+; RV32-NEXT:    .cfi_def_cfa_offset 0
+; RV32-NEXT:    ret
+;
+; RV64-LABEL: caller_sret_unused:
+; RV64:       # %bb.0: # %entry
+; RV64-NEXT:    addi sp, sp, -16
+; RV64-NEXT:    .cfi_def_cfa_offset 16
+; RV64-NEXT:    sd ra, 8(sp) # 8-byte Folded Spill
+; RV64-NEXT:    .cfi_offset ra, -8
+; RV64-NEXT:    mv a0, a1
+; RV64-NEXT:    call use_pointer
+; RV64-NEXT:    ld ra, 8(sp) # 8-byte Folded Reload
+; RV64-NEXT:    .cfi_restore ra
+; RV64-NEXT:    addi sp, sp, 16
+; RV64-NEXT:    .cfi_def_cfa_offset 0
+; RV64-NEXT:    ret
+entry:
+  tail call void @use_pointer(ptr %other)
+  ret void
+}
+
+; The caller's sret pointer can also be passed as an ordinary non-first
+; argument.
+define void @caller_sret_as_second_arg(ptr noalias sret(%struct.Buffer) %result, i32 %tag) {
+; RV32-LABEL: caller_sret_as_second_arg:
+; RV32:       # %bb.0: # %entry
+; RV32-NEXT:    addi sp, sp, -16
+; RV32-NEXT:    .cfi_def_cfa_offset 16
+; RV32-NEXT:    sw ra, 12(sp) # 4-byte Folded Spill
+; RV32-NEXT:    .cfi_offset ra, -4
+; RV32-NEXT:    mv a2, a0
+; RV32-NEXT:    mv a0, a1
+; RV32-NEXT:    mv a1, a2
+; RV32-NEXT:    call use_as_second_arg
+; RV32-NEXT:    lw ra, 12(sp) # 4-byte Folded Reload
+; RV32-NEXT:    .cfi_restore ra
+; RV32-NEXT:    addi sp, sp, 16
+; RV32-NEXT:    .cfi_def_cfa_offset 0
+; RV32-NEXT:    ret
+;
+; RV64-LABEL: caller_sret_as_second_arg:
+; RV64:       # %bb.0: # %entry
+; RV64-NEXT:    addi sp, sp, -16
+; RV64-NEXT:    .cfi_def_cfa_offset 16
+; RV64-NEXT:    sd ra, 8(sp) # 8-byte Folded Spill
+; RV64-NEXT:    .cfi_offset ra, -8
+; RV64-NEXT:    mv a2, a0
+; RV64-NEXT:    mv a0, a1
+; RV64-NEXT:    mv a1, a2
+; RV64-NEXT:    call use_as_second_arg
+; RV64-NEXT:    ld ra, 8(sp) # 8-byte Folded Reload
+; RV64-NEXT:    .cfi_restore ra
+; RV64-NEXT:    addi sp, sp, 16
+; RV64-NEXT:    .cfi_def_cfa_offset 0
+; RV64-NEXT:    ret
+entry:
+  tail call void @use_as_second_arg(i32 %tag, ptr %result)
+  ret void
+}
+
+; Do not tail call when caller and callee use different sret buffers.
+define void @sret_not_forwarded(ptr noalias sret(%struct.Buffer) %result, ptr %other, i64 %tag) {
+; RV32-LABEL: sret_not_forwarded:
+; RV32:       # %bb.0: # %entry
+; RV32-NEXT:    addi sp, sp, -16
+; RV32-NEXT:    .cfi_def_cfa_offset 16
+; RV32-NEXT:    sw ra, 12(sp) # 4-byte Folded Spill
+; RV32-NEXT:    .cfi_offset ra, -4
+; RV32-NEXT:    mv a0, a1
+; RV32-NEXT:    mv a1, a2
+; RV32-NEXT:    mv a2, a3
+; RV32-NEXT:    call forward_sret
+; RV32-NEXT:    lw ra, 12(sp) # 4-byte Folded Reload
+; RV32-NEXT:    .cfi_restore ra
+; RV32-NEXT:    addi sp, sp, 16
+; RV32-NEXT:    .cfi_def_cfa_offset 0
+; RV32-NEXT:    ret
+;
+; RV64-LABEL: sret_not_forwarded:
+; RV64:       # %bb.0: # %entry
+; RV64-NEXT:    addi sp, sp, -16
+; RV64-NEXT:    .cfi_def_cfa_offset 16
+; RV64-NEXT:    sd ra, 8(sp) # 8-byte Folded Spill
+; RV64-NEXT:    .cfi_offset ra, -8
+; RV64-NEXT:    mv a0, a1
+; RV64-NEXT:    mv a1, a2
+; RV64-NEXT:    call forward_sret
+; RV64-NEXT:    ld ra, 8(sp) # 8-byte Folded Reload
+; RV64-NEXT:    .cfi_restore ra
+; RV64-NEXT:    addi sp, sp, 16
+; RV64-NEXT:    .cfi_def_cfa_offset 0
+; RV64-NEXT:    ret
+entry:
+  tail call void @forward_sret(ptr sret(%struct.Buffer) %other, i64 %tag)
+  ret void
+}
+
+; Do not tail call when the callee's sret pointer refers to the caller's local
+; stack frame.
+define void @local_sret_buffer(i64 %tag) {
+; RV32-LABEL: local_sret_buffer:
+; RV32:       # %bb.0: # %entry
+; RV32-NEXT:    addi sp, sp, -64
+; RV32-NEXT:    .cfi_def_cfa_offset 64
+; RV32-NEXT:    sw ra, 60(sp) # 4-byte Folded Spill
+; RV32-NEXT:    .cfi_offset ra, -4
+; RV32-NEXT:    mv a2, a1
+; RV32-NEXT:    mv a1, a0
+; RV32-NEXT:    mv a0, sp
+; RV32-NEXT:    call forward_sret
+; RV32-NEXT:    lw ra, 60(sp) # 4-byte Folded Reload
+; RV32-NEXT:    .cfi_restore ra
+; RV32-NEXT:    addi sp, sp, 64
+; RV32-NEXT:    .cfi_def_cfa_offset 0
+; RV32-NEXT:    ret
+;
+; RV64-LABEL: local_sret_buffer:
+; RV64:       # %bb.0: # %entry
+; RV64-NEXT:    addi sp, sp, -64
+; RV64-NEXT:    .cfi_def_cfa_offset 64
+; RV64-NEXT:    sd ra, 56(sp) # 8-byte Folded Spill
+; RV64-NEXT:    .cfi_offset ra, -8
+; RV64-NEXT:    mv a1, a0
+; RV64-NEXT:    mv a0, sp
+; RV64-NEXT:    call forward_sret
+; RV64-NEXT:    ld ra, 56(sp) # 8-byte Folded Reload
+; RV64-NEXT:    .cfi_restore ra
+; RV64-NEXT:    addi sp, sp, 64
+; RV64-NEXT:    .cfi_def_cfa_offset 0
+; RV64-NEXT:    ret
+entry:
+  %local = alloca %struct.Buffer, align 8
+  tail call void @forward_sret(ptr sret(%struct.Buffer) %local, i64 %tag)
+  ret void
+}

>From 6550d780391a15dd722d610392dd8c147a109e55 Mon Sep 17 00:00:00 2001
From: Mingliang Liu <liumingliang.dev at bytedance.com>
Date: Wed, 16 Sep 2026 20:06:57 +0800
Subject: [PATCH 2/6] [RISCV] Support tail calls with sret parameters

---
 llvm/lib/Target/RISCV/RISCVISelLowering.cpp | 21 ++++--
 llvm/test/CodeGen/RISCV/tail-calls-sret.ll  | 81 ++++++---------------
 llvm/test/CodeGen/RISCV/tail-calls.ll       | 37 ++--------
 3 files changed, 43 insertions(+), 96 deletions(-)

diff --git a/llvm/lib/Target/RISCV/RISCVISelLowering.cpp b/llvm/lib/Target/RISCV/RISCVISelLowering.cpp
index d05e1f85afb69..75c96b704b45e 100644
--- a/llvm/lib/Target/RISCV/RISCVISelLowering.cpp
+++ b/llvm/lib/Target/RISCV/RISCVISelLowering.cpp
@@ -27587,12 +27587,23 @@ bool RISCVTargetLowering::isEligibleForTailCallOptimization(
     if (VA.getLocInfo() == CCValAssign::Indirect)
       return false;
 
-  // Do not tail call opt if either caller or callee uses struct return
-  // semantics.
-  auto IsCallerStructRet = Caller.hasStructRetAttr();
+  // If the callee has an sret parameter, conservatively require it to receive
+  // the caller's sret pointer. If only the caller has an sret parameter, treat
+  // that pointer like an ordinary pointer when passing call arguments.
+  // TODO: Support other sret buffers that outlive the caller, such as globals.
+  auto IsCallerStructRet =
+      !Caller.arg_empty() && Caller.getArg(0)->hasStructRetAttr();
   auto IsCalleeStructRet = Outs.empty() ? false : Outs[0].Flags.isSRet();
-  if (IsCallerStructRet || IsCalleeStructRet)
-    return false;
+  if (IsCalleeStructRet) {
+    // Do not allow the tail call if the caller has no sret parameter.
+    if (!IsCallerStructRet)
+      return false;
+    // RISC-V passes the sret pointer as the first argument in a0. Require the
+    // callee's sret argument to be the caller's incoming sret pointer.
+    if (!CLI.CB || CLI.CB->arg_empty() ||
+        CLI.CB->getArgOperand(0) != Caller.getArg(0))
+      return false;
+  }
 
   // The callee has to preserve all registers the caller needs to preserve.
   const RISCVRegisterInfo *TRI = Subtarget.getRegisterInfo();
diff --git a/llvm/test/CodeGen/RISCV/tail-calls-sret.ll b/llvm/test/CodeGen/RISCV/tail-calls-sret.ll
index f09c2fdf6d2ad..8256916f3c293 100644
--- a/llvm/test/CodeGen/RISCV/tail-calls-sret.ll
+++ b/llvm/test/CodeGen/RISCV/tail-calls-sret.ll
@@ -8,34 +8,31 @@ declare void @forward_sret(ptr sret(%struct.Buffer), i64)
 declare void @use_pointer(ptr)
 declare void @use_as_second_arg(i32, ptr)
 
+; The caller's sret pointer can be passed as an ordinary first argument, such
+; as the `this` pointer of a C++ constructor.
+define void @caller_sret_as_first_arg(ptr noalias sret(%struct.Buffer) %result) {
+; RV32-LABEL: caller_sret_as_first_arg:
+; RV32:       # %bb.0: # %entry
+; RV32-NEXT:    tail use_pointer
+;
+; RV64-LABEL: caller_sret_as_first_arg:
+; RV64:       # %bb.0: # %entry
+; RV64-NEXT:    tail use_pointer
+entry:
+  tail call void @use_pointer(ptr %result)
+  ret void
+}
+
 ; Matching caller and callee sret semantics can reuse the incoming
 ; return buffer.
 define void @forward_result(ptr noalias sret(%struct.Buffer) %result, i64 %tag) {
 ; RV32-LABEL: forward_result:
 ; RV32:       # %bb.0: # %entry
-; RV32-NEXT:    addi sp, sp, -16
-; RV32-NEXT:    .cfi_def_cfa_offset 16
-; RV32-NEXT:    sw ra, 12(sp) # 4-byte Folded Spill
-; RV32-NEXT:    .cfi_offset ra, -4
-; RV32-NEXT:    call forward_sret
-; RV32-NEXT:    lw ra, 12(sp) # 4-byte Folded Reload
-; RV32-NEXT:    .cfi_restore ra
-; RV32-NEXT:    addi sp, sp, 16
-; RV32-NEXT:    .cfi_def_cfa_offset 0
-; RV32-NEXT:    ret
+; RV32-NEXT:    tail forward_sret
 ;
 ; RV64-LABEL: forward_result:
 ; RV64:       # %bb.0: # %entry
-; RV64-NEXT:    addi sp, sp, -16
-; RV64-NEXT:    .cfi_def_cfa_offset 16
-; RV64-NEXT:    sd ra, 8(sp) # 8-byte Folded Spill
-; RV64-NEXT:    .cfi_offset ra, -8
-; RV64-NEXT:    call forward_sret
-; RV64-NEXT:    ld ra, 8(sp) # 8-byte Folded Reload
-; RV64-NEXT:    .cfi_restore ra
-; RV64-NEXT:    addi sp, sp, 16
-; RV64-NEXT:    .cfi_def_cfa_offset 0
-; RV64-NEXT:    ret
+; RV64-NEXT:    tail forward_sret
 entry:
   tail call void @forward_sret(ptr sret(%struct.Buffer) %result, i64 %tag)
   ret void
@@ -46,31 +43,13 @@ entry:
 define void @caller_sret_unused(ptr noalias sret(%struct.Buffer) %result, ptr %other) {
 ; RV32-LABEL: caller_sret_unused:
 ; RV32:       # %bb.0: # %entry
-; RV32-NEXT:    addi sp, sp, -16
-; RV32-NEXT:    .cfi_def_cfa_offset 16
-; RV32-NEXT:    sw ra, 12(sp) # 4-byte Folded Spill
-; RV32-NEXT:    .cfi_offset ra, -4
 ; RV32-NEXT:    mv a0, a1
-; RV32-NEXT:    call use_pointer
-; RV32-NEXT:    lw ra, 12(sp) # 4-byte Folded Reload
-; RV32-NEXT:    .cfi_restore ra
-; RV32-NEXT:    addi sp, sp, 16
-; RV32-NEXT:    .cfi_def_cfa_offset 0
-; RV32-NEXT:    ret
+; RV32-NEXT:    tail use_pointer
 ;
 ; RV64-LABEL: caller_sret_unused:
 ; RV64:       # %bb.0: # %entry
-; RV64-NEXT:    addi sp, sp, -16
-; RV64-NEXT:    .cfi_def_cfa_offset 16
-; RV64-NEXT:    sd ra, 8(sp) # 8-byte Folded Spill
-; RV64-NEXT:    .cfi_offset ra, -8
 ; RV64-NEXT:    mv a0, a1
-; RV64-NEXT:    call use_pointer
-; RV64-NEXT:    ld ra, 8(sp) # 8-byte Folded Reload
-; RV64-NEXT:    .cfi_restore ra
-; RV64-NEXT:    addi sp, sp, 16
-; RV64-NEXT:    .cfi_def_cfa_offset 0
-; RV64-NEXT:    ret
+; RV64-NEXT:    tail use_pointer
 entry:
   tail call void @use_pointer(ptr %other)
   ret void
@@ -81,35 +60,17 @@ entry:
 define void @caller_sret_as_second_arg(ptr noalias sret(%struct.Buffer) %result, i32 %tag) {
 ; RV32-LABEL: caller_sret_as_second_arg:
 ; RV32:       # %bb.0: # %entry
-; RV32-NEXT:    addi sp, sp, -16
-; RV32-NEXT:    .cfi_def_cfa_offset 16
-; RV32-NEXT:    sw ra, 12(sp) # 4-byte Folded Spill
-; RV32-NEXT:    .cfi_offset ra, -4
 ; RV32-NEXT:    mv a2, a0
 ; RV32-NEXT:    mv a0, a1
 ; RV32-NEXT:    mv a1, a2
-; RV32-NEXT:    call use_as_second_arg
-; RV32-NEXT:    lw ra, 12(sp) # 4-byte Folded Reload
-; RV32-NEXT:    .cfi_restore ra
-; RV32-NEXT:    addi sp, sp, 16
-; RV32-NEXT:    .cfi_def_cfa_offset 0
-; RV32-NEXT:    ret
+; RV32-NEXT:    tail use_as_second_arg
 ;
 ; RV64-LABEL: caller_sret_as_second_arg:
 ; RV64:       # %bb.0: # %entry
-; RV64-NEXT:    addi sp, sp, -16
-; RV64-NEXT:    .cfi_def_cfa_offset 16
-; RV64-NEXT:    sd ra, 8(sp) # 8-byte Folded Spill
-; RV64-NEXT:    .cfi_offset ra, -8
 ; RV64-NEXT:    mv a2, a0
 ; RV64-NEXT:    mv a0, a1
 ; RV64-NEXT:    mv a1, a2
-; RV64-NEXT:    call use_as_second_arg
-; RV64-NEXT:    ld ra, 8(sp) # 8-byte Folded Reload
-; RV64-NEXT:    .cfi_restore ra
-; RV64-NEXT:    addi sp, sp, 16
-; RV64-NEXT:    .cfi_def_cfa_offset 0
-; RV64-NEXT:    ret
+; RV64-NEXT:    tail use_as_second_arg
 entry:
   tail call void @use_as_second_arg(i32 %tag, ptr %result)
   ret void
diff --git a/llvm/test/CodeGen/RISCV/tail-calls.ll b/llvm/test/CodeGen/RISCV/tail-calls.ll
index e43acfa56f3fd..bd12a9498d22e 100644
--- a/llvm/test/CodeGen/RISCV/tail-calls.ll
+++ b/llvm/test/CodeGen/RISCV/tail-calls.ll
@@ -1110,63 +1110,38 @@ entry:
   ret void
 }
 
-; Do not tail call optimize if caller uses structret semantics.
+; A caller using structret semantics does not prevent tail call optimization.
 declare void @callee_nostruct()
 define void @caller_struct(ptr sret(%struct.A) %a) nounwind {
 ; CHECK-LABEL: caller_struct:
 ; CHECK:       # %bb.0: # %entry
-; CHECK-NEXT:    addi sp, sp, -16
-; CHECK-NEXT:    sw ra, 12(sp) # 4-byte Folded Spill
-; CHECK-NEXT:    call callee_nostruct
-; CHECK-NEXT:    lw ra, 12(sp) # 4-byte Folded Reload
-; CHECK-NEXT:    addi sp, sp, 16
-; CHECK-NEXT:    ret
+; CHECK-NEXT:    tail callee_nostruct
 ;
 ; CHECK-CF-RV32-LABEL: caller_struct:
 ; CHECK-CF-RV32:       # %bb.0: # %entry
 ; CHECK-CF-RV32-NEXT:    lpad 0
-; CHECK-CF-RV32-NEXT:    addi sp, sp, -16
-; CHECK-CF-RV32-NEXT:    sw ra, 12(sp) # 4-byte Folded Spill
-; CHECK-CF-RV32-NEXT:    call callee_nostruct
-; CHECK-CF-RV32-NEXT:    lw ra, 12(sp) # 4-byte Folded Reload
-; CHECK-CF-RV32-NEXT:    addi sp, sp, 16
-; CHECK-CF-RV32-NEXT:    ret
+; CHECK-CF-RV32-NEXT:    tail callee_nostruct, t2
 ;
 ; CHECK-CF-RV64-LABEL: caller_struct:
 ; CHECK-CF-RV64:       # %bb.0: # %entry
 ; CHECK-CF-RV64-NEXT:    lpad 0
-; CHECK-CF-RV64-NEXT:    addi sp, sp, -16
-; CHECK-CF-RV64-NEXT:    sd ra, 8(sp) # 8-byte Folded Spill
-; CHECK-CF-RV64-NEXT:    call callee_nostruct
-; CHECK-CF-RV64-NEXT:    ld ra, 8(sp) # 8-byte Folded Reload
-; CHECK-CF-RV64-NEXT:    addi sp, sp, 16
-; CHECK-CF-RV64-NEXT:    ret
+; CHECK-CF-RV64-NEXT:    tail callee_nostruct, t2
 ;
 ; CHECK-CF-RV32-LARGE-LABEL: caller_struct:
 ; CHECK-CF-RV32-LARGE:       # %bb.0: # %entry
 ; CHECK-CF-RV32-LARGE-NEXT:    lpad 0
-; CHECK-CF-RV32-LARGE-NEXT:    addi sp, sp, -16
-; CHECK-CF-RV32-LARGE-NEXT:    sw ra, 12(sp) # 4-byte Folded Spill
 ; CHECK-CF-RV32-LARGE-NEXT:  .Lpcrel_hi15:
 ; CHECK-CF-RV32-LARGE-NEXT:    auipc a0, %pcrel_hi(.LCPI12_0)
 ; CHECK-CF-RV32-LARGE-NEXT:    lw t2, %pcrel_lo(.Lpcrel_hi15)(a0)
-; CHECK-CF-RV32-LARGE-NEXT:    jalr t2
-; CHECK-CF-RV32-LARGE-NEXT:    lw ra, 12(sp) # 4-byte Folded Reload
-; CHECK-CF-RV32-LARGE-NEXT:    addi sp, sp, 16
-; CHECK-CF-RV32-LARGE-NEXT:    ret
+; CHECK-CF-RV32-LARGE-NEXT:    jr t2
 ;
 ; CHECK-CF-RV64-LARGE-LABEL: caller_struct:
 ; CHECK-CF-RV64-LARGE:       # %bb.0: # %entry
 ; CHECK-CF-RV64-LARGE-NEXT:    lpad 0
-; CHECK-CF-RV64-LARGE-NEXT:    addi sp, sp, -16
-; CHECK-CF-RV64-LARGE-NEXT:    sd ra, 8(sp) # 8-byte Folded Spill
 ; CHECK-CF-RV64-LARGE-NEXT:  .Lpcrel_hi15:
 ; CHECK-CF-RV64-LARGE-NEXT:    auipc a0, %pcrel_hi(.LCPI12_0)
 ; CHECK-CF-RV64-LARGE-NEXT:    ld t2, %pcrel_lo(.Lpcrel_hi15)(a0)
-; CHECK-CF-RV64-LARGE-NEXT:    jalr t2
-; CHECK-CF-RV64-LARGE-NEXT:    ld ra, 8(sp) # 8-byte Folded Reload
-; CHECK-CF-RV64-LARGE-NEXT:    addi sp, sp, 16
-; CHECK-CF-RV64-LARGE-NEXT:    ret
+; CHECK-CF-RV64-LARGE-NEXT:    jr t2
 entry:
   tail call void @callee_nostruct()
   ret void

>From 07609d645fd6078f207efd307eaf27476a2eca98 Mon Sep 17 00:00:00 2001
From: Mingliang Liu <liumingliang.dev at bytedance.com>
Date: Wed, 16 Sep 2026 20:48:14 +0800
Subject: [PATCH 3/6] [RISCV] Remove duplicate test

---
 llvm/test/CodeGen/RISCV/tail-calls-sret.ll | 15 ---------------
 1 file changed, 15 deletions(-)

diff --git a/llvm/test/CodeGen/RISCV/tail-calls-sret.ll b/llvm/test/CodeGen/RISCV/tail-calls-sret.ll
index 8256916f3c293..9d91f5e8f52f5 100644
--- a/llvm/test/CodeGen/RISCV/tail-calls-sret.ll
+++ b/llvm/test/CodeGen/RISCV/tail-calls-sret.ll
@@ -8,21 +8,6 @@ declare void @forward_sret(ptr sret(%struct.Buffer), i64)
 declare void @use_pointer(ptr)
 declare void @use_as_second_arg(i32, ptr)
 
-; The caller's sret pointer can be passed as an ordinary first argument, such
-; as the `this` pointer of a C++ constructor.
-define void @caller_sret_as_first_arg(ptr noalias sret(%struct.Buffer) %result) {
-; RV32-LABEL: caller_sret_as_first_arg:
-; RV32:       # %bb.0: # %entry
-; RV32-NEXT:    tail use_pointer
-;
-; RV64-LABEL: caller_sret_as_first_arg:
-; RV64:       # %bb.0: # %entry
-; RV64-NEXT:    tail use_pointer
-entry:
-  tail call void @use_pointer(ptr %result)
-  ret void
-}
-
 ; Matching caller and callee sret semantics can reuse the incoming
 ; return buffer.
 define void @forward_result(ptr noalias sret(%struct.Buffer) %result, i64 %tag) {

>From c36278c6e435a170c55e119f5be7c468a37dff79 Mon Sep 17 00:00:00 2001
From: Mingliang Liu <liumingliang.dev at bytedance.com>
Date: Thu, 17 Sep 2026 13:08:55 +0800
Subject: [PATCH 4/6] fixup! don't use auto

---
 llvm/lib/Target/RISCV/RISCVISelLowering.cpp | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/llvm/lib/Target/RISCV/RISCVISelLowering.cpp b/llvm/lib/Target/RISCV/RISCVISelLowering.cpp
index 75c96b704b45e..dc1301ca6eefd 100644
--- a/llvm/lib/Target/RISCV/RISCVISelLowering.cpp
+++ b/llvm/lib/Target/RISCV/RISCVISelLowering.cpp
@@ -27591,9 +27591,9 @@ bool RISCVTargetLowering::isEligibleForTailCallOptimization(
   // the caller's sret pointer. If only the caller has an sret parameter, treat
   // that pointer like an ordinary pointer when passing call arguments.
   // TODO: Support other sret buffers that outlive the caller, such as globals.
-  auto IsCallerStructRet =
+  bool IsCallerStructRet =
       !Caller.arg_empty() && Caller.getArg(0)->hasStructRetAttr();
-  auto IsCalleeStructRet = Outs.empty() ? false : Outs[0].Flags.isSRet();
+  bool IsCalleeStructRet = Outs.empty() ? false : Outs[0].Flags.isSRet();
   if (IsCalleeStructRet) {
     // Do not allow the tail call if the caller has no sret parameter.
     if (!IsCallerStructRet)

>From 062c28ae1b21a5e5d084d4cfb373b9f1c4d10c01 Mon Sep 17 00:00:00 2001
From: Mingliang Liu <liumingliang.dev at bytedance.com>
Date: Tue, 22 Sep 2026 23:37:01 +0800
Subject: [PATCH 5/6] [RISCV] Allow sret tail calls for Microsoft C++ ABI

---
 llvm/lib/Target/RISCV/RISCVISelLowering.cpp | 24 ++++++++++++++-------
 llvm/test/CodeGen/RISCV/tail-calls-sret.ll  | 22 +++++++++++++++++++
 2 files changed, 38 insertions(+), 8 deletions(-)

diff --git a/llvm/lib/Target/RISCV/RISCVISelLowering.cpp b/llvm/lib/Target/RISCV/RISCVISelLowering.cpp
index dc1301ca6eefd..f052ed9fb489b 100644
--- a/llvm/lib/Target/RISCV/RISCVISelLowering.cpp
+++ b/llvm/lib/Target/RISCV/RISCVISelLowering.cpp
@@ -27591,17 +27591,25 @@ bool RISCVTargetLowering::isEligibleForTailCallOptimization(
   // the caller's sret pointer. If only the caller has an sret parameter, treat
   // that pointer like an ordinary pointer when passing call arguments.
   // TODO: Support other sret buffers that outlive the caller, such as globals.
-  bool IsCallerStructRet =
-      !Caller.arg_empty() && Caller.getArg(0)->hasStructRetAttr();
-  bool IsCalleeStructRet = Outs.empty() ? false : Outs[0].Flags.isSRet();
+  bool IsCalleeStructRet = llvm::any_of(
+      Outs, [](const ISD::OutputArg &Out) { return Out.Flags.isSRet(); });
   if (IsCalleeStructRet) {
     // Do not allow the tail call if the caller has no sret parameter.
-    if (!IsCallerStructRet)
+    if (!Caller.hasStructRetAttr() || !CLI.CB || CLI.CB->arg_empty())
       return false;
-    // RISC-V passes the sret pointer as the first argument in a0. Require the
-    // callee's sret argument to be the caller's incoming sret pointer.
-    if (!CLI.CB || CLI.CB->arg_empty() ||
-        CLI.CB->getArgOperand(0) != Caller.getArg(0))
+
+    // RISC-V psABI passes the sret pointer as the first argument. But under
+    // the Microsoft C++ ABI on Windows, the sret pointer is allowed as the
+    // second pointer after `this` pointer.
+    if (Subtarget.getTargetTriple().isKnownWindowsMSVCEnvironment()) {
+      auto *CallerSRetArg = Caller.getArg(0)->hasStructRetAttr()
+                                ? Caller.getArg(0)
+                                : Caller.getArg(1);
+      for (unsigned Idx = 0; Idx < 2 && Idx < CLI.CB->arg_size(); Idx++)
+        if (CLI.CB->paramHasAttr(Idx, Attribute::StructRet) &&
+            CLI.CB->getArgOperand(Idx) != CallerSRetArg)
+          return false;
+    } else if (CLI.CB->getArgOperand(0) != Caller.getArg(0))
       return false;
   }
 
diff --git a/llvm/test/CodeGen/RISCV/tail-calls-sret.ll b/llvm/test/CodeGen/RISCV/tail-calls-sret.ll
index 9d91f5e8f52f5..818b67c5d894d 100644
--- a/llvm/test/CodeGen/RISCV/tail-calls-sret.ll
+++ b/llvm/test/CodeGen/RISCV/tail-calls-sret.ll
@@ -1,12 +1,14 @@
 ; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py UTC_ARGS: --version 5
 ; RUN: llc -mtriple=riscv32 -verify-machineinstrs < %s | FileCheck %s --check-prefix=RV32
 ; RUN: llc -mtriple=riscv64 -verify-machineinstrs < %s | FileCheck %s --check-prefix=RV64
+; RUN: llc -mtriple=riscv64-pc-windows-msvc -verify-machineinstrs < %s | FileCheck %s --check-prefix=WIN64
 
 %struct.Buffer = type { [7 x i64] }
 
 declare void @forward_sret(ptr sret(%struct.Buffer), i64)
 declare void @use_pointer(ptr)
 declare void @use_as_second_arg(i32, ptr)
+declare void @forward_windows_sret(ptr, ptr sret(%struct.Buffer))
 
 ; Matching caller and callee sret semantics can reuse the incoming
 ; return buffer.
@@ -136,3 +138,23 @@ entry:
   tail call void @forward_sret(ptr sret(%struct.Buffer) %local, i64 %tag)
   ret void
 }
+
+define void @windows_sret_after_this(ptr %this, ptr noalias sret(%struct.Buffer) %result) {
+; WIN64-LABEL: windows_sret_after_this:
+; WIN64:       # %bb.0: # %entry
+; WIN64-NEXT:    tail forward_windows_sret
+entry:
+  tail call void @forward_windows_sret(ptr %this, ptr sret(%struct.Buffer) %result)
+  ret void
+}
+
+define void @windows_sret(ptr %this, ptr noalias sret(%struct.Buffer) %result, i64 %tag) {
+; WIN64-LABEL: windows_sret:
+; WIN64:       # %bb.0: # %entry
+; WIN64-NEXT:  mv	a0, a1
+; WIN64-NEXT:	mv	a1, a2
+; WIN64-NEXT:	tail	forward_sret
+entry:
+  tail call void @forward_sret(ptr sret(%struct.Buffer) %result, i64 %tag)
+  ret void
+}

>From 410df60537ced42fde74e90a837c9f2ebdbb911f Mon Sep 17 00:00:00 2001
From: Mingliang Liu <liumingliang.dev at bytedance.com>
Date: Thu, 24 Sep 2026 01:15:04 +0800
Subject: [PATCH 6/6] [RISCV] Assert that the sret pointer is the first
 argument

---
 llvm/lib/Target/RISCV/RISCVISelLowering.cpp | 18 ++++++-----------
 llvm/test/CodeGen/RISCV/tail-calls-sret.ll  | 22 ---------------------
 2 files changed, 6 insertions(+), 34 deletions(-)

diff --git a/llvm/lib/Target/RISCV/RISCVISelLowering.cpp b/llvm/lib/Target/RISCV/RISCVISelLowering.cpp
index f052ed9fb489b..f717c74bf3cbb 100644
--- a/llvm/lib/Target/RISCV/RISCVISelLowering.cpp
+++ b/llvm/lib/Target/RISCV/RISCVISelLowering.cpp
@@ -27598,18 +27598,12 @@ bool RISCVTargetLowering::isEligibleForTailCallOptimization(
     if (!Caller.hasStructRetAttr() || !CLI.CB || CLI.CB->arg_empty())
       return false;
 
-    // RISC-V psABI passes the sret pointer as the first argument. But under
-    // the Microsoft C++ ABI on Windows, the sret pointer is allowed as the
-    // second pointer after `this` pointer.
-    if (Subtarget.getTargetTriple().isKnownWindowsMSVCEnvironment()) {
-      auto *CallerSRetArg = Caller.getArg(0)->hasStructRetAttr()
-                                ? Caller.getArg(0)
-                                : Caller.getArg(1);
-      for (unsigned Idx = 0; Idx < 2 && Idx < CLI.CB->arg_size(); Idx++)
-        if (CLI.CB->paramHasAttr(Idx, Attribute::StructRet) &&
-            CLI.CB->getArgOperand(Idx) != CallerSRetArg)
-          return false;
-    } else if (CLI.CB->getArgOperand(0) != Caller.getArg(0))
+    // RISC-V psABI passes the sret pointer as the first argument. The Microsoft
+    // C++ ABI may instead pass it as the second argument after `this`, but that
+    // ABI is rarely used on RISC-V and is not supported here.
+    assert(Caller.getArg(0)->hasStructRetAttr() && Outs[0].Flags.isSRet() &&
+           "sret pointer must be argument 0");
+    if (CLI.CB->getArgOperand(0) != Caller.getArg(0))
       return false;
   }
 
diff --git a/llvm/test/CodeGen/RISCV/tail-calls-sret.ll b/llvm/test/CodeGen/RISCV/tail-calls-sret.ll
index 818b67c5d894d..9d91f5e8f52f5 100644
--- a/llvm/test/CodeGen/RISCV/tail-calls-sret.ll
+++ b/llvm/test/CodeGen/RISCV/tail-calls-sret.ll
@@ -1,14 +1,12 @@
 ; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py UTC_ARGS: --version 5
 ; RUN: llc -mtriple=riscv32 -verify-machineinstrs < %s | FileCheck %s --check-prefix=RV32
 ; RUN: llc -mtriple=riscv64 -verify-machineinstrs < %s | FileCheck %s --check-prefix=RV64
-; RUN: llc -mtriple=riscv64-pc-windows-msvc -verify-machineinstrs < %s | FileCheck %s --check-prefix=WIN64
 
 %struct.Buffer = type { [7 x i64] }
 
 declare void @forward_sret(ptr sret(%struct.Buffer), i64)
 declare void @use_pointer(ptr)
 declare void @use_as_second_arg(i32, ptr)
-declare void @forward_windows_sret(ptr, ptr sret(%struct.Buffer))
 
 ; Matching caller and callee sret semantics can reuse the incoming
 ; return buffer.
@@ -138,23 +136,3 @@ entry:
   tail call void @forward_sret(ptr sret(%struct.Buffer) %local, i64 %tag)
   ret void
 }
-
-define void @windows_sret_after_this(ptr %this, ptr noalias sret(%struct.Buffer) %result) {
-; WIN64-LABEL: windows_sret_after_this:
-; WIN64:       # %bb.0: # %entry
-; WIN64-NEXT:    tail forward_windows_sret
-entry:
-  tail call void @forward_windows_sret(ptr %this, ptr sret(%struct.Buffer) %result)
-  ret void
-}
-
-define void @windows_sret(ptr %this, ptr noalias sret(%struct.Buffer) %result, i64 %tag) {
-; WIN64-LABEL: windows_sret:
-; WIN64:       # %bb.0: # %entry
-; WIN64-NEXT:  mv	a0, a1
-; WIN64-NEXT:	mv	a1, a2
-; WIN64-NEXT:	tail	forward_sret
-entry:
-  tail call void @forward_sret(ptr sret(%struct.Buffer) %result, i64 %tag)
-  ret void
-}



More information about the llvm-commits mailing list