https://github.com/skc7 updated https://github.com/llvm/llvm-project/pull/220207
>From 9758dc2c95253bd01de6f88fa12ef2e15b1e8c2c Mon Sep 17 00:00:00 2001 From: skc7 <[email protected]> Date: Tue, 1 Sep 2026 15:01:27 +0530 Subject: [PATCH] [CIR] Support direct-in-registers aggregates and array coercions in call-conv lowering --- .../Transforms/CallConvLoweringPass.cpp | 21 ++- .../abi-lowering/amdgpu-calling-conv.cir | 128 +++++++++++++++++- 2 files changed, 145 insertions(+), 4 deletions(-) diff --git a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp index 6981fc2c23d198..e18a2cb5d07d76 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::getFixed( 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()); @@ -566,7 +572,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. /// @@ -588,11 +596,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) { _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
