[clang] b979a11 - [CIR] Lower indirect calls in CallConvLowering (#211636)

via cfe-commits cfe-commits at lists.llvm.org
Wed Jul 29 10:51:45 PDT 2026


Author: Adam Smith
Date: 2026-07-29T12:51:38-05:00
New Revision: b979a119274a13fdee769478702148006219fc5b

URL: https://github.com/llvm/llvm-project/commit/b979a119274a13fdee769478702148006219fc5b
DIFF: https://github.com/llvm/llvm-project/commit/b979a119274a13fdee769478702148006219fc5b.diff

LOG: [CIR] Lower indirect calls in CallConvLowering (#211636)

Indirect calls were unsupported by CallConvLowering.  `rewriteCallSite` bailed
with "indirect call not yet implemented", and the pass driver rejected any
module that both needed call-site rewriting and contained an indirect call.  A
function pointer whose signature required ABI reshaping (a by-value struct
argument, or an sret return) could not be compiled at all, even though a
direct call to the same signature lowered fine.

The callee is opaque, but its type is a pointer to a `FuncType` that carries the
full signature.  Classification is now driven from that pointee signature
through the same LLVM ABI path as the direct case: `classifyX86_64Function` is
split into `classifyX86_64Signature`, taking a return type, an argument
`TypeRange`, and an `emitError` callback.  The driver collects indirect calls,
classifies each callee signature (x86_64, or the test target used by the pass
tests), and runs `rewriteCallSite`.  `prependIndirectCallee` re-adds the callee
as operand 0, bitcasting the function pointer to the coerced signature when it
differs so the rebuilt call matches its reshaped return.

The `classification-attr` driver mode injects a per-function classification
that cannot describe a callee resolved at run time, so an indirect call is a
diagnosed error there rather than being left silently unrewritten while direct
calls are coerced.

Added: 
    clang/test/CIR/CodeGen/call-conv-lowering-x86_64-indirect.c
    clang/test/CIR/Transforms/abi-lowering/indirect-call-classification-attr.cir
    clang/test/CIR/Transforms/abi-lowering/indirect-call-test-target.cir
    clang/test/CIR/Transforms/abi-lowering/indirect-call.cir

Modified: 
    clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
    clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp

Removed: 
    clang/test/CIR/Transforms/abi-lowering/indirect-call-nyi.cir


################################################################################
diff  --git a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
index 2e385d480f4f1..f3e42ac5e73b7 100644
--- a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
@@ -299,31 +299,30 @@ convertABIArgInfo(const llvm::abi::ArgInfo &info, MLIRContext *ctx,
   return ArgClassification::getIgnore();
 }
 
-/// Classify a cir.func for x86_64 SysV using the LLVM ABI library.  Returns
-/// std::nullopt and emits an NYI error if the signature uses a type the bridge
-/// does not handle yet.
-static std::optional<FunctionClassification>
-classifyX86_64Function(cir::FuncOp func, const DataLayout &dl,
-                       mlir::abi::ABITypeMapper &typeMapper,
-                       const llvm::abi::TargetInfo &targetInfo,
-                       ModuleOp modOp) {
-  MLIRContext *ctx = func->getContext();
-  cir::FuncType fnTy = func.getFunctionType();
-  mlir::Type retCIR = fnTy.getReturnType();
-  assert(retCIR && "FuncType::getReturnType() never returns null");
+/// Classify an x86_64 SysV signature (return type + argument types) using the
+/// LLVM ABI library.  Shared by the cir.func path and the indirect-call path
+/// (which classifies from the callee function pointer's pointee FuncType).
+/// Returns std::nullopt and emits an NYI error via \p emitError if the
+/// signature uses a type the bridge does not handle yet.
+static std::optional<FunctionClassification> classifyX86_64Signature(
+    mlir::Type retCIR, mlir::TypeRange inputs, MLIRContext *ctx,
+    const DataLayout &dl, mlir::abi::ABITypeMapper &typeMapper,
+    const llvm::abi::TargetInfo &targetInfo, ModuleOp modOp,
+    llvm::function_ref<mlir::InFlightDiagnostic()> emitError) {
+  assert(retCIR && "signature return type must be non-null");
   bool voidRet = isa<cir::VoidType>(retCIR);
 
   auto reject = [&](mlir::Type t) -> bool {
     if (isSupportedType(t))
       return false;
-    func.emitOpError()
+    emitError()
         << "x86_64 calling-convention lowering not yet implemented for type "
         << t;
     return true;
   };
   if (!voidRet && reject(retCIR))
     return std::nullopt;
-  for (mlir::Type a : fnTy.getInputs())
+  for (mlir::Type a : inputs)
     if (reject(a))
       return std::nullopt;
 
@@ -331,7 +330,7 @@ classifyX86_64Function(cir::FuncOp func, const DataLayout &dl,
       voidRet ? typeMapper.getTypeBuilder().getVoidType()
               : mapCIRType(retCIR, typeMapper, dl, modOp);
   SmallVector<const llvm::abi::Type *> argAbi;
-  for (mlir::Type a : fnTy.getInputs())
+  for (mlir::Type a : inputs)
     argAbi.push_back(mapCIRType(a, typeMapper, dl, modOp));
 
   std::unique_ptr<llvm::abi::FunctionInfo> fi =
@@ -342,9 +341,9 @@ classifyX86_64Function(cir::FuncOp func, const DataLayout &dl,
   // this bridge cannot represent (e.g. an SSE vector coerce for an all-float
   // aggregate).  Report it as NYI rather than emitting a wrong signature.
   auto nyiCoercion = [&](mlir::Type t) {
-    func.emitOpError() << "x86_64 calling-convention lowering not yet "
-                          "implemented for the ABI coercion of type "
-                       << t;
+    emitError() << "x86_64 calling-convention lowering not yet "
+                   "implemented for the ABI coercion of type "
+                << t;
   };
 
   FunctionClassification fc;
@@ -356,7 +355,6 @@ classifyX86_64Function(cir::FuncOp func, const DataLayout &dl,
     return std::nullopt;
   }
   fc.returnInfo = *retAc;
-  auto inputs = fnTy.getInputs();
   for (unsigned i = 0, e = fi->arg_size(); i < e; ++i) {
     mlir::Type origArg = i < inputs.size() ? inputs[i] : mlir::Type();
     std::optional<ArgClassification> ac =
@@ -370,13 +368,18 @@ classifyX86_64Function(cir::FuncOp func, const DataLayout &dl,
   return fc;
 }
 
-bool needsRewrite(const FunctionClassification &fc) {
-  if ((fc.returnInfo.kind != ArgKind::Direct) || fc.returnInfo.coercedType)
-    return true;
-  for (const ArgClassification &ac : fc.argInfos)
-    if ((ac.kind != ArgKind::Direct) || ac.coercedType)
-      return true;
-  return false;
+/// Classify a cir.func for x86_64 SysV using the LLVM ABI library.  Returns
+/// std::nullopt and emits an NYI error if the signature uses a type the bridge
+/// does not handle yet.
+static std::optional<FunctionClassification>
+classifyX86_64Function(cir::FuncOp func, const DataLayout &dl,
+                       mlir::abi::ABITypeMapper &typeMapper,
+                       const llvm::abi::TargetInfo &targetInfo,
+                       ModuleOp modOp) {
+  cir::FuncType fnTy = func.getFunctionType();
+  return classifyX86_64Signature(fnTy.getReturnType(), fnTy.getInputs(),
+                                 func->getContext(), dl, typeMapper, targetInfo,
+                                 modOp, [&]() { return func.emitOpError(); });
 }
 
 struct CallConvLoweringPass
@@ -529,24 +532,44 @@ void CallConvLoweringPass::runOnOperation() {
     }
   }
 
-  // Reject indirect calls when the module contains any ABI rewrite that
-  // would need call-site lowering.  We cannot strip or coerce operands
-  // without a resolved callee symbol.
-  const FunctionClassification *rewriteFc = nullptr;
-  for (auto &kv : classifications) {
-    if (needsRewrite(kv.second)) {
-      rewriteFc = &kv.second;
-      break;
+  // Rewrite indirect call sites.  The callee is opaque, so classify from the
+  // function pointer's pointee FuncType and let rewriteCallSite retype the
+  // callee pointer to match the coerced signature.  Collect the calls first:
+  // when an sret rewrite reuses a single-use store's destination as the return
+  // slot it erases that store, which is the operation a live walk has already
+  // cached as the next one to visit.
+  SmallVector<cir::CallOp> indirectCalls;
+  moduleOp.walk([&](cir::CallOp c) {
+    if (c.isIndirect())
+      indirectCalls.push_back(c);
+  });
+  for (cir::CallOp c : indirectCalls) {
+    // classification-attr mode injects a per-function classification, which
+    // cannot describe a callee resolved at run time.  Report it rather than
+    // leave the indirect call unrewritten while direct calls are coerced.
+    if (!classificationAttr.empty()) {
+      c.emitOpError() << "indirect call cannot be classified in the "
+                         "'classification-attr' driver mode";
+      signalPassFailure();
+      return;
     }
-  }
-  if (rewriteFc) {
-    moduleOp.walk([&](cir::CallOp c) {
-      if (!c.isIndirect())
-        return;
-      if (failed(rewriteCtx.rewriteCallSite(c, *rewriteFc, builder)))
-        anyFailed = true;
-    });
-    if (anyFailed) {
+    // The CallOp verifier guarantees an indirect callee is a pointer to a
+    // function type.
+    auto ptrTy = cast<cir::PointerType>(c.getIndirectCall().getType());
+    auto funcTy = cast<cir::FuncType>(ptrTy.getPointee());
+    std::optional<FunctionClassification> fc;
+    if (x86Target)
+      fc = classifyX86_64Signature(funcTy.getReturnType(), funcTy.getInputs(),
+                                   ctx, dl, *x86TypeMapper, *x86Target,
+                                   moduleOp, [&]() { return c.emitOpError(); });
+    else
+      fc = mlir::abi::test::classify(funcTy.getInputs(), funcTy.getReturnType(),
+                                     dl);
+    if (!fc) {
+      signalPassFailure();
+      return;
+    }
+    if (failed(rewriteCtx.rewriteCallSite(c, *fc, builder))) {
       signalPassFailure();
       return;
     }

diff  --git a/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp b/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
index d4c76fd266a22..e87ae2326d524 100644
--- a/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
@@ -799,6 +799,32 @@ void applySretSlotAttrs(cir::CallOp newCall, mlir::ArrayAttr argAttrs,
   newCall->setAttr("arg_attrs", mlir::ArrayAttr::get(ctx, newArgAttrs));
 }
 
+/// For an indirect call, prepend the callee function pointer as operand 0 so
+/// CallOp::create rebuilds it as an indirect call, bitcasting it to a function
+/// pointer whose signature matches the rewritten operands and return type.
+/// No-op for direct calls.
+static void prependIndirectCallee(cir::CallOp call,
+                                  SmallVectorImpl<mlir::Value> &args,
+                                  mlir::Type retTy, mlir::OpBuilder &builder) {
+  if (!call.isIndirect())
+    return;
+  mlir::Value calleePtr = call.getIndirectCall();
+  SmallVector<mlir::Type> paramTypes;
+  paramTypes.reserve(args.size());
+  llvm::transform(args, std::back_inserter(paramTypes),
+                  [](mlir::Value v) { return v.getType(); });
+  // Lowering builds an indirect call's LLVM function type from the callee
+  // pointer's pointee and takes the call's result from that type, so the
+  // pointee's return type has to track the rewrite: an sret return would
+  // leave a result the call no longer produces, and a coerced return one of
+  // the wrong type.
+  auto newPtrTy = cir::PointerType::get(cir::FuncType::get(paramTypes, retTy));
+  if (calleePtr.getType() != newPtrTy)
+    calleePtr = cir::CastOp::create(builder, call.getLoc(), newPtrTy,
+                                    cir::CastKind::bitcast, calleePtr);
+  args.insert(args.begin(), calleePtr);
+}
+
 /// Rewrite an indirect-return (sret) call site: prepend a return-slot
 /// pointer as operand 0, make the call return void, and either reuse a
 /// dominating single-use store destination as the slot (so construction
@@ -853,6 +879,7 @@ void rewriteIndirectReturnCall(cir::CallOp call,
   sretArgs.append(newArgs.begin(), newArgs.end());
 
   mlir::Type sretVoidTy = cir::VoidType::get(ctx);
+  prependIndirectCallee(call, sretArgs, sretVoidTy, builder);
   auto newCall = cir::CallOp::create(
       builder, call.getLoc(), call.getCalleeAttr(), sretVoidTy, sretArgs);
   for (mlir::NamedAttribute attr : call->getAttrs())
@@ -1082,10 +1109,6 @@ CIRABIRewriteContext::rewriteCallSite(mlir::Operation *callOp,
            << "TryCallOp not yet implemented in CallConvLowering";
 
   auto call = mlir::cast<cir::CallOp>(callOp);
-  if (call.isIndirect())
-    return call.emitOpError()
-           << "indirect call not yet implemented in CallConvLowering";
-
   mlir::MLIRContext *ctx = callOp->getContext();
   auto enclosingFunc = call->getParentOfType<mlir::FunctionOpInterface>();
 
@@ -1196,6 +1219,7 @@ CIRABIRewriteContext::rewriteCallSite(mlir::Operation *callOp,
     callRetTy = fc.returnInfo.coercedType;
 
   builder.setInsertionPoint(call);
+  prependIndirectCallee(call, newArgs, callRetTy, builder);
   auto newCall = cir::CallOp::create(builder, call.getLoc(),
                                      call.getCalleeAttr(), callRetTy, newArgs);
   for (mlir::NamedAttribute attr : call->getAttrs())

diff  --git a/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-indirect.c b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-indirect.c
new file mode 100644
index 0000000000000..b6c22f0f0077d
--- /dev/null
+++ b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-indirect.c
@@ -0,0 +1,49 @@
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -clangir-enable-call-conv-lowering -emit-cir %s -o %t.cir
+// RUN: FileCheck --check-prefix=CIR --input-file=%t.cir %s
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -clangir-enable-call-conv-lowering -emit-llvm %s -o %t-cir.ll
+// RUN: FileCheck --check-prefixes=LLVM,LLVM-CIR --input-file=%t-cir.ll %s
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm %s -o %t.ll
+// RUN: FileCheck --check-prefixes=LLVM,LLVM-OGCG --input-file=%t.ll %s
+
+typedef struct { long a, b, c, d; } Big;
+typedef struct { int a, b; } Pair;
+
+// Scalar indirect call: no ABI reshaping, the callee pointer is called as-is.
+int call_scalar(int (*fp)(int), int x) { return fp(x); }
+
+// CIR: cir.func {{.*}}@call_scalar(%arg0: !cir.ptr<!cir.func<(!s32i) -> !s32i>> {{.*}}, %arg1: !s32i {{.*}}) -> !s32i
+// CIR:   %{{.+}} = cir.call %{{.+}}(%{{.+}}) : (!cir.ptr<!cir.func<(!s32i) -> !s32i>>, !s32i {llvm.noundef}) -> !s32i
+// LLVM: define dso_local i32 @call_scalar(ptr noundef %{{.+}}, i32 noundef %{{.+}})
+// LLVM:   call i32 %{{.+}}(i32 noundef %{{.+}})
+
+// Indirect call whose small-struct argument and return are coerced to a
+// register: the callee pointer is bitcast so its pointee tracks both.
+Pair call_coerce(Pair (*fp)(Pair), Pair p) { return fp(p); }
+
+// CIR: cir.func {{.*}}@call_coerce(%arg0: !cir.ptr<!cir.func<(!rec_Pair) -> !rec_Pair>> {{.*}}, %arg1: !u64i{{.*}}) -> !u64i
+// CIR:   %[[PCAST:.*]] = cir.cast bitcast %{{.+}} : !cir.ptr<!cir.func<(!rec_Pair) -> !rec_Pair>> -> !cir.ptr<!cir.func<(!u64i) -> !u64i>>
+// CIR:   %{{.+}} = cir.call %[[PCAST]](%{{.+}}) : (!cir.ptr<!cir.func<(!u64i) -> !u64i>>, !u64i) -> !u64i
+// LLVM: define dso_local i64 @call_coerce(ptr noundef %{{.+}}, i64 %{{.+}})
+// LLVM:   call i64 %{{.+}}(i64 %{{.+}})
+
+// Indirect call with a byval struct argument: the argument is spilled to a
+// stack slot and the callee pointer is bitcast to the coerced signature.
+long call_byval(long (*fp)(Big), Big b) { return fp(b); }
+
+// CIR: cir.func {{.*}}@call_byval(%arg0: !cir.ptr<!cir.func<(!rec_Big) -> !s64i>> {{.*}}, %arg1: !cir.ptr<!rec_Big> {{.*}}llvm.byval = !rec_Big{{.*}}) -> !s64i
+// CIR:   %[[CAST:.*]] = cir.cast bitcast %{{.+}} : !cir.ptr<!cir.func<(!rec_Big) -> !s64i>> -> !cir.ptr<!cir.func<(!cir.ptr<!rec_Big>) -> !s64i>>
+// CIR:   %{{.+}} = cir.call %[[CAST]](%{{.+}}) : (!cir.ptr<!cir.func<(!cir.ptr<!rec_Big>) -> !s64i>>, !cir.ptr<!rec_Big> {llvm.align = 8 : i64, llvm.byval = !rec_Big, llvm.noalias, llvm.noundef}) -> !s64i
+// LLVM-CIR: define dso_local i64 @call_byval(ptr noundef %{{.+}}, ptr noalias noundef byval(%struct.Big) align 8 %{{.+}})
+// LLVM-OGCG: define dso_local i64 @call_byval(ptr noundef %{{.+}}, ptr noundef byval(%struct.Big) align 8 %{{.+}})
+// LLVM-CIR:   call i64 %{{.+}}(ptr noalias noundef byval(%struct.Big) align 8 %{{.+}})
+// LLVM-OGCG:  call i64 %{{.+}}(ptr noundef byval(%struct.Big) align 8 %{{.+}})
+
+// Indirect call returning a large struct: an sret pointer slot is prepended
+// and the callee is bitcast to the void-returning sret signature.
+Big call_sret(Big (*fp)(void)) { return fp(); }
+
+// CIR: cir.func {{.*}}@call_sret(%arg0: !cir.ptr<!rec_Big> {{.*}}llvm.sret = !rec_Big{{.*}}, %arg1: !cir.ptr<!cir.func<() -> !rec_Big>> {{.*}})
+// CIR:   %[[SCAST:.*]] = cir.cast bitcast %{{.+}} : !cir.ptr<!cir.func<() -> !rec_Big>> -> !cir.ptr<!cir.func<(!cir.ptr<!rec_Big>)>>
+// CIR:   cir.call %[[SCAST]](%{{.+}}) : (!cir.ptr<!cir.func<(!cir.ptr<!rec_Big>)>>, !cir.ptr<!rec_Big> {llvm.align = 8 : i64, llvm.dead_on_unwind, llvm.sret = !rec_Big, llvm.writable}) -> ()
+// LLVM: define dso_local void @call_sret(ptr dead_on_unwind noalias writable sret(%struct.Big) align 8 %{{.+}}, ptr noundef %{{.+}})
+// LLVM:   call void %{{.+}}(ptr dead_on_unwind writable sret(%struct.Big) align 8 %{{.+}})

diff  --git a/clang/test/CIR/Transforms/abi-lowering/indirect-call-nyi.cir b/clang/test/CIR/Transforms/abi-lowering/indirect-call-classification-attr.cir
similarity index 59%
rename from clang/test/CIR/Transforms/abi-lowering/indirect-call-nyi.cir
rename to clang/test/CIR/Transforms/abi-lowering/indirect-call-classification-attr.cir
index e4bd17588bd57..590e9eb21b1a7 100644
--- a/clang/test/CIR/Transforms/abi-lowering/indirect-call-nyi.cir
+++ b/clang/test/CIR/Transforms/abi-lowering/indirect-call-classification-attr.cir
@@ -3,25 +3,18 @@
 
 !s32i = !cir.int<s, 32>
 
-#ignore_first_arg = {
-  return = { kind = "direct" },
-  args   = [ { kind = "ignore" }, { kind = "direct" } ]
-}
-
 #passthrough = {
   return = { kind = "direct" },
-  args   = [ { kind = "direct" }, { kind = "direct" }, { kind = "direct" } ]
+  args   = [ { kind = "direct" }, { kind = "direct" } ]
 }
 
 module attributes {
   dlti.dl_spec = #dlti.dl_spec<#dlti.dl_entry<i32, dense<32>: vector<2xi64>>>
 } {
 
-  cir.func @callee(%arg0: !s32i, %arg1: !s32i) -> !s32i
-      attributes { test_classify = #ignore_first_arg } {
-    cir.return %arg1 : !s32i
-  }
-
+  // classification-attr injects a per-function classification, which cannot
+  // describe a callee resolved at run time, so an indirect call is a hard
+  // error in this driver mode rather than being left silently unrewritten.
   cir.func @caller(%fp: !cir.ptr<!cir.func<(!s32i, !s32i) -> !s32i>>,
                    %arg0: !s32i, %arg1: !s32i) -> !s32i
       attributes { test_classify = #passthrough } {
@@ -32,4 +25,4 @@ module attributes {
 
 }
 
-// CHECK: error: 'cir.call' op indirect call not yet implemented in CallConvLowering
+// CHECK: error: 'cir.call' op indirect call cannot be classified in the 'classification-attr' driver mode

diff  --git a/clang/test/CIR/Transforms/abi-lowering/indirect-call-test-target.cir b/clang/test/CIR/Transforms/abi-lowering/indirect-call-test-target.cir
new file mode 100644
index 0000000000000..794487e756ffe
--- /dev/null
+++ b/clang/test/CIR/Transforms/abi-lowering/indirect-call-test-target.cir
@@ -0,0 +1,41 @@
+// RUN: cir-opt %s -cir-call-conv-lowering=target=test | FileCheck %s
+
+!s32i = !cir.int<s, 32>
+!s64i = !cir.int<s, 64>
+!rec_Big = !cir.struct<"Big" {!s64i, !s64i, !s64i, !s64i}>
+
+module attributes {
+  dlti.dl_spec = #dlti.dl_spec<
+    #dlti.dl_entry<i32, dense<32>: vector<2xi64>>,
+    #dlti.dl_entry<i64, dense<64>: vector<2xi64>>>
+} {
+
+  // Scalar indirect call under the test target: the callee's pointee FuncType
+  // classifies to Direct passthrough, so signature and call site are unchanged.
+  cir.func @call_scalar(%fp: !cir.ptr<!cir.func<(!s32i) -> !s32i>>, %x: !s32i) -> !s32i {
+    %0 = cir.call %fp(%x) : (!cir.ptr<!cir.func<(!s32i) -> !s32i>>, !s32i) -> !s32i
+    cir.return %0 : !s32i
+  }
+
+  // CHECK:      cir.func{{.*}} @call_scalar(%[[FP:.*]]: !cir.ptr<!cir.func<(!s32i) -> !s32i>>, %[[X:.*]]: !s32i) -> !s32i
+  // CHECK-NEXT:   %[[RES:.*]] = cir.call %[[FP]](%[[X]]) : (!cir.ptr<!cir.func<(!s32i) -> !s32i>>, !s32i) -> !s32i
+  // CHECK-NEXT:   cir.return %[[RES]] : !s32i
+
+  // Indirect call whose struct argument classifies to Indirect (byval) under
+  // the test target: the argument spills to a stack slot and the callee
+  // pointer is bitcast to the coerced signature before the call.
+  cir.func @call_byval(%fp: !cir.ptr<!cir.func<(!rec_Big) -> !s64i>>, %b: !rec_Big) -> !s64i {
+    %0 = cir.call %fp(%b) : (!cir.ptr<!cir.func<(!rec_Big) -> !s64i>>, !rec_Big) -> !s64i
+    cir.return %0 : !s64i
+  }
+
+  // CHECK:      cir.func{{.*}} @call_byval(%[[FP:.*]]: !cir.ptr<!cir.func<(!rec_Big) -> !s64i>>, %{{.*}}: !cir.ptr<!rec_Big>
+  // CHECK-SAME:     llvm.byval = !rec_Big
+  // CHECK:        %[[SLOT:.*]] = cir.alloca "byval" align(8) : !cir.ptr<!rec_Big>
+  // CHECK:        %[[CAST:.*]] = cir.cast bitcast %[[FP]] : !cir.ptr<!cir.func<(!rec_Big) -> !s64i>> -> !cir.ptr<!cir.func<(!cir.ptr<!rec_Big>) -> !s64i>>
+  // CHECK:        %{{.*}} = cir.call %[[CAST]](%[[SLOT]]) : (!cir.ptr<!cir.func<(!cir.ptr<!rec_Big>) -> !s64i>>, !cir.ptr<!rec_Big>
+  // CHECK-SAME:     llvm.byval = !rec_Big
+  // CHECK-SAME:     llvm.noalias
+  // CHECK-SAME:     llvm.noundef
+
+}

diff  --git a/clang/test/CIR/Transforms/abi-lowering/indirect-call.cir b/clang/test/CIR/Transforms/abi-lowering/indirect-call.cir
new file mode 100644
index 0000000000000..6a6f7fe7a5a0b
--- /dev/null
+++ b/clang/test/CIR/Transforms/abi-lowering/indirect-call.cir
@@ -0,0 +1,105 @@
+// RUN: cir-opt %s -cir-call-conv-lowering=target=x86_64 | FileCheck %s
+
+!s16i = !cir.int<s, 16>
+!s32i = !cir.int<s, 32>
+!s64i = !cir.int<s, 64>
+!rec_Big = !cir.struct<"Big" {!s64i, !s64i, !s64i}>
+!rec_Pair = !cir.struct<"Pair" {!s32i, !s32i}>
+!rec_Empty = !cir.struct<"Empty" {}>
+
+module attributes {
+  dlti.dl_spec = #dlti.dl_spec<
+    #dlti.dl_entry<i16, dense<16>: vector<2xi64>>,
+    #dlti.dl_entry<i32, dense<32>: vector<2xi64>>,
+    #dlti.dl_entry<i64, dense<64>: vector<2xi64>>>
+} {
+
+  // Scalar indirect call: no ABI reshaping, so it passes through unchanged
+  // (and is no longer rejected as NYI).
+  cir.func @call_scalar(%fp: !cir.ptr<!cir.func<(!s32i) -> !s32i>>,
+                        %arg0: !s32i) -> !s32i {
+    %0 = cir.call %fp(%arg0)
+        : (!cir.ptr<!cir.func<(!s32i) -> !s32i>>, !s32i) -> !s32i
+    cir.return %0 : !s32i
+  }
+
+  // CHECK: cir.func{{.*}} @call_scalar
+  // CHECK:   cir.call %{{.+}}(%{{.+}}) : (!cir.ptr<!cir.func<(!s32i) -> !s32i>>, !s32i) -> !s32i
+
+  // Indirect call whose sub-32-bit int arg/return is Extend: the signext
+  // attribute is applied at the call site and the callee pointer keeps its
+  // signature (Extend does not change the value type, so no bitcast).
+  cir.func @call_ext(%fp: !cir.ptr<!cir.func<(!s16i) -> !s16i>>,
+                     %arg0: !s16i) -> !s16i {
+    %0 = cir.call %fp(%arg0)
+        : (!cir.ptr<!cir.func<(!s16i) -> !s16i>>, !s16i) -> !s16i
+    cir.return %0 : !s16i
+  }
+
+  // CHECK: cir.func{{.*}} @call_ext
+  // CHECK-NOT: cir.cast bitcast
+  // CHECK:   cir.call %{{.+}}(%{{.+}}) : (!cir.ptr<!cir.func<(!s16i) -> !s16i>>, !s16i {llvm.signext}) -> (!s16i {llvm.signext})
+
+  // Indirect call whose small-struct argument and return are Direct with a
+  // coerce type: the callee pointer is bitcast so its pointee tracks both the
+  // coerced parameter and the coerced result.
+  cir.func @call_coerce(%fp: !cir.ptr<!cir.func<(!rec_Pair) -> !rec_Pair>>,
+                        %out: !cir.ptr<!rec_Pair>) {
+    %0 = cir.alloca "p" align(4) : !cir.ptr<!rec_Pair>
+    %1 = cir.load %0 : !cir.ptr<!rec_Pair>, !rec_Pair
+    %2 = cir.call %fp(%1)
+        : (!cir.ptr<!cir.func<(!rec_Pair) -> !rec_Pair>>,
+           !rec_Pair) -> !rec_Pair
+    cir.store %2, %out : !rec_Pair, !cir.ptr<!rec_Pair>
+    cir.return
+  }
+
+  // CHECK: cir.func{{.*}} @call_coerce(%arg0: !cir.ptr<!cir.func<(!rec_Pair) -> !rec_Pair>>
+  // CHECK:   %[[CCAST:.*]] = cir.cast bitcast %arg0 : !cir.ptr<!cir.func<(!rec_Pair) -> !rec_Pair>> -> !cir.ptr<!cir.func<(!u64i) -> !u64i>>
+  // CHECK:   cir.call %[[CCAST]](%{{.+}}) : (!cir.ptr<!cir.func<(!u64i) -> !u64i>>, !u64i) -> !u64i
+
+  // Indirect call with an Ignore-classified empty-record argument: the operand
+  // is dropped and the callee pointer loses that parameter.
+  cir.func @call_ignore(%fp: !cir.ptr<!cir.func<(!rec_Empty, !s32i) -> !s32i>>,
+                        %x: !s32i) -> !s32i {
+    %0 = cir.alloca "e" align(1) : !cir.ptr<!rec_Empty>
+    %1 = cir.load %0 : !cir.ptr<!rec_Empty>, !rec_Empty
+    %2 = cir.call %fp(%1, %x)
+        : (!cir.ptr<!cir.func<(!rec_Empty, !s32i) -> !s32i>>,
+           !rec_Empty, !s32i) -> !s32i
+    cir.return %2 : !s32i
+  }
+
+  // CHECK: cir.func{{.*}} @call_ignore(%arg0: !cir.ptr<!cir.func<(!rec_Empty, !s32i) -> !s32i>>
+  // CHECK:   %[[ICAST:.*]] = cir.cast bitcast %arg0 : !cir.ptr<!cir.func<(!rec_Empty, !s32i) -> !s32i>> -> !cir.ptr<!cir.func<(!s32i) -> !s32i>>
+  // CHECK:   cir.call %[[ICAST]](%arg1) : (!cir.ptr<!cir.func<(!s32i) -> !s32i>>, !s32i) -> !s32i
+
+  // Indirect call with a by-value (byval) struct argument: the argument is
+  // spilled to a stack slot and the callee pointer is bitcast to the coerced
+  // signature before the call is rebuilt.
+  cir.func @call_byval(%fp: !cir.ptr<!cir.func<(!rec_Big) -> !s64i>>) -> !s64i {
+    %0 = cir.alloca "b" align(8) : !cir.ptr<!rec_Big>
+    %1 = cir.load %0 : !cir.ptr<!rec_Big>, !rec_Big
+    %2 = cir.call %fp(%1)
+        : (!cir.ptr<!cir.func<(!rec_Big) -> !s64i>>, !rec_Big) -> !s64i
+    cir.return %2 : !s64i
+  }
+
+  // CHECK: cir.func{{.*}} @call_byval(%arg0: !cir.ptr<!cir.func<(!rec_Big) -> !s64i>>)
+  // CHECK:   %[[SLOT:.*]] = cir.alloca "byval" align(8) : !cir.ptr<!rec_Big>
+  // CHECK:   cir.store %{{.+}}, %[[SLOT]] : !rec_Big, !cir.ptr<!rec_Big>
+  // CHECK:   %[[CAST:.*]] = cir.cast bitcast %arg0 : !cir.ptr<!cir.func<(!rec_Big) -> !s64i>> -> !cir.ptr<!cir.func<(!cir.ptr<!rec_Big>) -> !s64i>>
+  // CHECK:   cir.call %[[CAST]](%[[SLOT]]) : (!cir.ptr<!cir.func<(!cir.ptr<!rec_Big>) -> !s64i>>, !cir.ptr<!rec_Big> {llvm.align = 8 : i64, llvm.byval = !rec_Big, llvm.noalias, llvm.noundef}) -> !s64i
+
+  // Indirect call returning a large struct: an sret pointer slot is prepended
+  // and the callee is bitcast to the void-returning sret signature.
+  cir.func @call_sret(%fp: !cir.ptr<!cir.func<() -> !rec_Big>>) {
+    %0 = cir.call %fp() : (!cir.ptr<!cir.func<() -> !rec_Big>>) -> !rec_Big
+    cir.return
+  }
+
+  // CHECK: cir.func{{.*}} @call_sret(%arg0: !cir.ptr<!cir.func<() -> !rec_Big>>)
+  // CHECK:   %[[SRET:.*]] = cir.alloca "sret" align(8) : !cir.ptr<!rec_Big>
+  // CHECK:   %[[SCAST:.*]] = cir.cast bitcast %arg0 : !cir.ptr<!cir.func<() -> !rec_Big>> -> !cir.ptr<!cir.func<(!cir.ptr<!rec_Big>)>>
+  // CHECK:   cir.call %[[SCAST]](%[[SRET]]) : (!cir.ptr<!cir.func<(!cir.ptr<!rec_Big>)>>, !cir.ptr<!rec_Big> {llvm.align = 8 : i64, llvm.dead_on_unwind, llvm.sret = !rec_Big, llvm.writable}) -> ()
+}


        


More information about the cfe-commits mailing list