[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