[clang] [CIR] Support array coercions and flattened direct structs in CallConvLowering (PR #220207)
via cfe-commits
cfe-commits at lists.llvm.org
Thu Oct 22 01:57:54 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-clangir
Author: Chaitanya (skc7)
<details>
<summary>Changes</summary>
**Summary:**
- The AMDGPU classifier can pass or return an aggregate directly in registers and can pack a 33-64 bit aggregate
into a [2 x i32] register pair. The CallConvLowering bridge handled neither, so such signatures hit the NYI path. Handle both in convertABIArgInfo/abiTypeToCIR.
**Changes:**
- `abiTypeToCIR` gets an `ArrayType` case
- A Direct struct argument with a null coerce type and `CanBeFlattened` is split into one argument per field, as classic CGCall does. Returns, unions and non-flattenable structs pass through unchanged.
- Added cir-opt coverage for 64-bit struct coerced to [2 x i32] and larger struct passed directly in registers.
Addresses review feedback from @<!-- -->koparasy on #<!-- -->220197.
Related to issue: https://github.com/llvm/llvm-project/issues/220471
Assisted by: Claude Opus 5.5
---
Full diff: https://github.com/llvm/llvm-project/pull/220207.diff
2 Files Affected:
- (modified) clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp (+19-2)
- (modified) clang/test/CIR/Transforms/abi-lowering/amdgpu-calling-conv.cir (+126-2)
``````````diff
diff --git a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
index a978d3a899c2a8..69c69392d21c94 100644
--- a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
@@ -322,6 +322,12 @@ static mlir::Type abiTypeToCIR(const llvm::abi::Type *ty, MLIRContext *ctx) {
return cir::VectorType::get(elemCIR,
vecTy->getNumElements().getFixedValue());
})
+ .Case([&](const llvm::abi::ArrayType *arrTy) -> mlir::Type {
+ mlir::Type elemCIR = abiTypeToCIR(arrTy->getElementType(), ctx);
+ if (!elemCIR)
+ return nullptr;
+ return cir::ArrayType::get(elemCIR, arrTy->getNumElements());
+ })
.Case([&](const llvm::abi::RecordType *recTy) -> mlir::Type {
SmallVector<mlir::Type> fieldTypes;
fieldTypes.reserve(recTy->getFields().size());
@@ -567,7 +573,9 @@ static const llvm::abi::Type *mapCIRType(mlir::Type type,
/// follows the classifier's CanBeFlattened, so the rewriter splits a
/// multi-field coerced struct into individual wire arguments unless the
/// classifier asked to keep it intact. Any other scalar passes in its
-/// natural CIR type, which a null coercion denotes. A coercion this bridge
+/// natural CIR type, which a null coercion denotes. A multi-field struct with
+/// a null coercion is its own coerced type, so the rewriter splits it into one
+/// wire argument per field as classic codegen does. A coercion this bridge
/// cannot represent yields std::nullopt so the caller reports NYI rather than
/// silently passing the value unchanged.
///
@@ -589,11 +597,20 @@ convertABIArgInfo(const llvm::abi::ArgInfo &info, MLIRContext *ctx,
// The classifier names a coerce type even where it matches the natural
// type, so a non-null coerce does not by itself mean a rewrite is needed.
const llvm::abi::Type *coerceAbi = info.getCoerceToType();
+ if (!coerceAbi) {
+ auto recTy = dyn_cast_if_present<cir::RecordType>(origTy);
+ if (!recTy || !recTy.isStruct() || !info.getCanBeFlattened())
+ return ArgClassification::getDirect();
+ ArgClassification classified =
+ ArgClassification::getDirect(recTy, /*offset=*/0);
+ classified.canFlatten = true;
+ return classified;
+ }
bool isAggregate = isa_and_present<cir::RecordType, cir::ArrayType>(origTy);
// For a _Complex or a vector the classifier's coerce is only sometimes the
// natural type, so it has to be read rather than assumed.
bool comparesAgainstCoerce =
- coerceAbi && isa_and_present<cir::ComplexType, cir::VectorType>(origTy);
+ isa_and_present<cir::ComplexType, cir::VectorType>(origTy);
bool coerceIsRegisterTuple =
isa_and_present<llvm::abi::RecordType>(coerceAbi);
// A tuple coercion cannot be read at an offset, so refuse the pair rather
diff --git a/clang/test/CIR/Transforms/abi-lowering/amdgpu-calling-conv.cir b/clang/test/CIR/Transforms/abi-lowering/amdgpu-calling-conv.cir
index 4043621d054bca..04d95708419a06 100644
--- a/clang/test/CIR/Transforms/abi-lowering/amdgpu-calling-conv.cir
+++ b/clang/test/CIR/Transforms/abi-lowering/amdgpu-calling-conv.cir
@@ -1,13 +1,18 @@
// RUN: cir-opt %s -cir-call-conv-lowering=target=amdgpu | FileCheck %s
// Checks that CallConvLowering under target=amdgpu classifies a function by
-// its calling convention. Default functions get extension and Ignore rewrites
-// applied to definitions and call sites. Kernels go to the kernel classifier.
+// its calling convention. Default functions get extension, Ignore and
+// aggregate rewrites applied to definitions and call sites. Kernels go to the
+// kernel classifier.
!s8i = !cir.int<s, 8>
!s32i = !cir.int<s, 32>
+!u32i = !cir.int<u, 32>
!rec_E0 = !cir.struct<"E0" {}>
+!rec_Pair32 = !cir.struct<"Pair32" {data !s32i, data !s32i}>
+!rec_Triple = !cir.struct<"Triple" {data !s32i, data !s32i, data !s32i}>
+!rec_Outer = !cir.struct<"Outer" {data !s32i, data !rec_Triple}>
module attributes {
dlti.dl_spec = #dlti.dl_spec<
@@ -42,6 +47,125 @@ module attributes {
// CHECK: cir.func{{.*}} @take_empty(%arg0: !s32i) -> !s32i
// CHECK-NEXT: cir.return %arg0 : !s32i
+ // A 33-64 bit aggregate is packed into a pair of i32 registers, coerced on
+ // the wire to a [2 x i32] array.
+ cir.func @takes_pair32(%arg0: !rec_Pair32) {
+ %0 = cir.alloca "p" align(4) : !cir.ptr<!rec_Pair32>
+ cir.store %arg0, %0 : !rec_Pair32, !cir.ptr<!rec_Pair32>
+ cir.return
+ }
+
+ // CHECK: cir.func{{.*}} @takes_pair32(%[[ARR:.*]]: !cir.array<!u32i x 2>) {
+ // CHECK-NEXT: %[[TMP:.*]] = cir.alloca "coerce" align(4) : !cir.ptr<!cir.array<!u32i x 2>>
+ // CHECK-NEXT: cir.store %[[ARR]], %[[TMP]] : !cir.array<!u32i x 2>, !cir.ptr<!cir.array<!u32i x 2>>
+ // CHECK-NEXT: %[[TMP_REC:.*]] = cir.cast bitcast %[[TMP]] : !cir.ptr<!cir.array<!u32i x 2>> -> !cir.ptr<!rec_Pair32>
+ // CHECK-NEXT: %[[P:.*]] = cir.alloca "p" align(4) : !cir.ptr<!rec_Pair32>
+ // CHECK-NEXT: cir.copy %[[TMP_REC]] align(4) to %[[P]] align(4) : !cir.ptr<!rec_Pair32>
+
+ // A 33-64 bit aggregate return is coerced to [2 x i32] as well.
+ cir.func @ret_pair32(%arg0: !cir.ptr<!rec_Pair32>) -> !rec_Pair32 {
+ %0 = cir.load %arg0 : !cir.ptr<!rec_Pair32>, !rec_Pair32
+ cir.return %0 : !rec_Pair32
+ }
+
+ // CHECK: cir.func{{.*}} @ret_pair32(%[[SRC:.*]]: !cir.ptr<!rec_Pair32>) -> !cir.array<!u32i x 2> {
+ // CHECK-NEXT: %[[TMP:.*]] = cir.alloca "coerce" align(4) : !cir.ptr<!rec_Pair32>
+ // CHECK-NEXT: cir.copy %[[SRC]] align(4) to %[[TMP]] align(4) : !cir.ptr<!rec_Pair32>
+ // CHECK-NEXT: %[[TMP_ARR:.*]] = cir.cast bitcast %[[TMP]] : !cir.ptr<!rec_Pair32> -> !cir.ptr<!cir.array<!u32i x 2>>
+ // CHECK-NEXT: %[[RET:.*]] = cir.load align(4) %[[TMP_ARR]] : !cir.ptr<!cir.array<!u32i x 2>>, !cir.array<!u32i x 2>
+ // CHECK-NEXT: cir.return %[[RET]] : !cir.array<!u32i x 2>
+
+ // The call site coerces both the operand and the result.
+ cir.func @call_pair32() {
+ %0 = cir.alloca "p" align(4) : !cir.ptr<!rec_Pair32>
+ %1 = cir.load %0 : !cir.ptr<!rec_Pair32>, !rec_Pair32
+ cir.call @takes_pair32(%1) : (!rec_Pair32) -> ()
+ %2 = cir.call @ret_pair32(%0) : (!cir.ptr<!rec_Pair32>) -> !rec_Pair32
+ cir.store %2, %0 : !rec_Pair32, !cir.ptr<!rec_Pair32>
+ cir.return
+ }
+
+ // CHECK: cir.func{{.*}} @call_pair32() {
+ // CHECK-NEXT: %[[RES_TMP:.*]] = cir.alloca "coerce" align(4) : !cir.ptr<!cir.array<!u32i x 2>>
+ // CHECK-NEXT: %[[P:.*]] = cir.alloca "p" align(4) : !cir.ptr<!rec_Pair32>
+ // CHECK-NEXT: %[[P_ARR:.*]] = cir.cast bitcast %[[P]] : !cir.ptr<!rec_Pair32> -> !cir.ptr<!cir.array<!u32i x 2>>
+ // CHECK-NEXT: %[[ARG:.*]] = cir.load align(4) %[[P_ARR]] : !cir.ptr<!cir.array<!u32i x 2>>, !cir.array<!u32i x 2>
+ // CHECK-NEXT: cir.call @takes_pair32(%[[ARG]]) : (!cir.array<!u32i x 2>) -> ()
+ // CHECK-NEXT: %[[RES:.*]] = cir.call @ret_pair32(%[[P]]) : (!cir.ptr<!rec_Pair32>) -> !cir.array<!u32i x 2>
+ // CHECK-NEXT: cir.store %[[RES]], %[[RES_TMP]] : !cir.array<!u32i x 2>, !cir.ptr<!cir.array<!u32i x 2>>
+ // CHECK-NEXT: %[[RES_REC:.*]] = cir.cast bitcast %[[RES_TMP]] : !cir.ptr<!cir.array<!u32i x 2>> -> !cir.ptr<!rec_Pair32>
+ // CHECK-NEXT: cir.copy %[[RES_REC]] align(4) to %[[P]] align(4) : !cir.ptr<!rec_Pair32>
+
+ // A larger aggregate that still fits the register budget is passed directly,
+ // one argument per field, as classic codegen does.
+ cir.func @takes_triple(%arg0: !rec_Triple) -> !s32i {
+ %0 = cir.alloca "t" align(4) : !cir.ptr<!rec_Triple>
+ cir.store %arg0, %0 : !rec_Triple, !cir.ptr<!rec_Triple>
+ %1 = cir.get_member %0[1] {name = "b"} : !cir.ptr<!rec_Triple> -> !cir.ptr<!s32i>
+ %2 = cir.load %1 : !cir.ptr<!s32i>, !s32i
+ cir.return %2 : !s32i
+ }
+
+ // CHECK: cir.func{{.*}} @takes_triple(%[[ARG0:[^:]*]]: !s32i, %[[ARG1:[^:]*]]: !s32i, %[[ARG2:[^:]*]]: !s32i) -> !s32i {
+ // CHECK-NEXT: %[[TMP:.*]] = cir.alloca "coerce" align(4) : !cir.ptr<!rec_Triple>
+ // CHECK-NEXT: %[[SLOT0:.*]] = cir.get_member %[[TMP]][0] {name = ""} : !cir.ptr<!rec_Triple> -> !cir.ptr<!s32i>
+ // CHECK-NEXT: cir.store %[[ARG0]], %[[SLOT0]] : !s32i, !cir.ptr<!s32i>
+ // CHECK-NEXT: %[[SLOT1:.*]] = cir.get_member %[[TMP]][1] {name = ""} : !cir.ptr<!rec_Triple> -> !cir.ptr<!s32i>
+ // CHECK-NEXT: cir.store %[[ARG1]], %[[SLOT1]] : !s32i, !cir.ptr<!s32i>
+ // CHECK-NEXT: %[[SLOT2:.*]] = cir.get_member %[[TMP]][2] {name = ""} : !cir.ptr<!rec_Triple> -> !cir.ptr<!s32i>
+ // CHECK-NEXT: cir.store %[[ARG2]], %[[SLOT2]] : !s32i, !cir.ptr<!s32i>
+ // CHECK-NEXT: %[[T:.*]] = cir.alloca "t" align(4) : !cir.ptr<!rec_Triple>
+ // CHECK-NEXT: cir.copy %[[TMP]] align(4) to %[[T]] align(4) : !cir.ptr<!rec_Triple>
+ // CHECK-NEXT: %[[B_PTR:.*]] = cir.get_member %[[T]][1] {name = "b"} : !cir.ptr<!rec_Triple> -> !cir.ptr<!s32i>
+ // CHECK-NEXT: %[[B:.*]] = cir.load %[[B_PTR]] : !cir.ptr<!s32i>, !s32i
+ // CHECK-NEXT: cir.return %[[B]] : !s32i
+
+ // A direct aggregate return is not split.
+ cir.func @ret_triple(%arg0: !cir.ptr<!rec_Triple>) -> !rec_Triple {
+ %0 = cir.load %arg0 : !cir.ptr<!rec_Triple>, !rec_Triple
+ cir.return %0 : !rec_Triple
+ }
+
+ // CHECK: cir.func{{.*}} @ret_triple(%[[SRC:.*]]: !cir.ptr<!rec_Triple>) -> !rec_Triple {
+ // CHECK-NEXT: %[[V:.*]] = cir.load %[[SRC]] : !cir.ptr<!rec_Triple>, !rec_Triple
+ // CHECK-NEXT: cir.return %[[V]] : !rec_Triple
+
+ // The call site passes each field of the operand as its own argument.
+ cir.func @call_triple() -> !s32i {
+ %0 = cir.alloca "t" align(4) : !cir.ptr<!rec_Triple>
+ %1 = cir.load %0 : !cir.ptr<!rec_Triple>, !rec_Triple
+ %2 = cir.call @takes_triple(%1) : (!rec_Triple) -> !s32i
+ %3 = cir.call @ret_triple(%0) : (!cir.ptr<!rec_Triple>) -> !rec_Triple
+ cir.store %3, %0 : !rec_Triple, !cir.ptr<!rec_Triple>
+ cir.return %2 : !s32i
+ }
+
+ // CHECK: cir.func{{.*}} @call_triple() -> !s32i {
+ // CHECK-NEXT: %[[T:.*]] = cir.alloca "t" align(4) : !cir.ptr<!rec_Triple>
+ // CHECK-NEXT: %[[SLOT0:.*]] = cir.get_member %[[T]][0] {name = ""} : !cir.ptr<!rec_Triple> -> !cir.ptr<!s32i>
+ // CHECK-NEXT: %[[ARG0:.*]] = cir.load %[[SLOT0]] : !cir.ptr<!s32i>, !s32i
+ // CHECK-NEXT: %[[SLOT1:.*]] = cir.get_member %[[T]][1] {name = ""} : !cir.ptr<!rec_Triple> -> !cir.ptr<!s32i>
+ // CHECK-NEXT: %[[ARG1:.*]] = cir.load %[[SLOT1]] : !cir.ptr<!s32i>, !s32i
+ // CHECK-NEXT: %[[SLOT2:.*]] = cir.get_member %[[T]][2] {name = ""} : !cir.ptr<!rec_Triple> -> !cir.ptr<!s32i>
+ // CHECK-NEXT: %[[ARG2:.*]] = cir.load %[[SLOT2]] : !cir.ptr<!s32i>, !s32i
+ // CHECK-NEXT: %[[R:.*]] = cir.call @takes_triple(%[[ARG0]], %[[ARG1]], %[[ARG2]]) : (!s32i, !s32i, !s32i) -> !s32i
+ // CHECK-NEXT: %[[RET:.*]] = cir.call @ret_triple(%[[T]]) : (!cir.ptr<!rec_Triple>) -> !rec_Triple
+ // CHECK-NEXT: cir.store %[[RET]], %[[T]] : !rec_Triple, !cir.ptr<!rec_Triple>
+ // CHECK-NEXT: cir.return %[[R]] : !s32i
+
+ // Only the top level is split, so a nested struct stays one argument.
+ cir.func @takes_outer(%arg0: !rec_Outer) {
+ cir.return
+ }
+
+ // CHECK: cir.func{{.*}} @takes_outer(%[[I:[^:]*]]: !s32i, %[[INNER:[^:]*]]: !rec_Triple) {
+ // CHECK-NEXT: %[[TMP:.*]] = cir.alloca "coerce" align(4) : !cir.ptr<!rec_Outer>
+ // CHECK-NEXT: %[[SLOT0:.*]] = cir.get_member %[[TMP]][0] {name = ""} : !cir.ptr<!rec_Outer> -> !cir.ptr<!s32i>
+ // CHECK-NEXT: cir.store %[[I]], %[[SLOT0]] : !s32i, !cir.ptr<!s32i>
+ // CHECK-NEXT: %[[SLOT1:.*]] = cir.get_member %[[TMP]][1] {name = ""} : !cir.ptr<!rec_Outer> -> !cir.ptr<!rec_Triple>
+ // CHECK-NEXT: cir.store %[[INNER]], %[[SLOT1]] : !rec_Triple, !cir.ptr<!rec_Triple>
+ // CHECK-NEXT: cir.return
+
// A kernel scalar argument stays Direct: the AMDGPU_KERNEL convention is
// routed to the kernel classifier and the signature is unchanged.
cir.func @kernel_scalar(%arg0: !s32i) cc(amdgpu_kernel) {
``````````
</details>
https://github.com/llvm/llvm-project/pull/220207
More information about the cfe-commits
mailing list