[clang] [CIR] Add CallConvLowering pass + Direct/Ignore ABI rewriting (PR #195737)
Andy Kaylor via cfe-commits
cfe-commits at lists.llvm.org
Mon May 18 10:50:29 PDT 2026
================
@@ -0,0 +1,319 @@
+//===- CIRABIRewriteContext.cpp - CIR ABI rewrite context ----------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "CIRABIRewriteContext.h"
+#include "mlir/IR/Builders.h"
+#include "clang/CIR/Dialect/IR/CIRAttrs.h"
+#include "clang/CIR/Dialect/IR/CIRTypes.h"
+
+using namespace cir;
+using namespace mlir;
+using namespace mlir::abi;
+
+// This rewrite context currently supports only the Direct (no coercion) and
+// Ignore classifications. All other ArgKinds emit an errorNYI here rather
+// than silently passing through, because the IR they would produce is wrong
+// (e.g. Expand should flatten an aggregate into multiple primitives, not
+// pass it through as a single value). Subsequent PRs in the
+// CallConvLowering split series add the remaining kinds and the
+// signature-shaping behavior that goes with them (sret / byval insert
+// extra arguments, struct coercion replaces one argument with several).
+
+namespace {
+
+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;
+}
+
+SmallVector<unsigned> ignoredArgIndices(const FunctionClassification &fc) {
+ SmallVector<unsigned> v;
+ for (auto [idx, ac] : llvm::enumerate(fc.argInfos))
+ if (ac.kind == ArgKind::Ignore)
+ v.push_back(idx);
+ return v;
+}
+
+/// Build the new argument-type list for a function whose ABI classification
+/// is \p fc. This currently handles only Direct (no coercion) and Ignore;
+/// other kinds emit an error. Classifications that add arguments (e.g.
+/// Indirect-sret would prepend a return-pointer arg) are not yet
+/// implemented and will arrive in a subsequent PR.
+LogicalResult buildNewArgTypes(ArrayRef<Type> oldArgTypes,
+ const FunctionClassification &fc,
+ SmallVectorImpl<Type> &newArgTypes,
+ function_ref<InFlightDiagnostic()> emitError) {
+ assert(newArgTypes.empty() && "expected an empty output vector");
+ newArgTypes.reserve(oldArgTypes.size());
+ for (auto [idx, ac] : llvm::enumerate(fc.argInfos)) {
+ Type origTy = oldArgTypes[idx];
+ switch (ac.kind) {
+ case ArgKind::Direct:
+ if (ac.coercedType) {
+ emitError() << "Direct with coerced type at arg " << idx
+ << " not yet implemented in CallConvLowering";
+ return failure();
+ }
+ newArgTypes.push_back(origTy);
+ break;
+ case ArgKind::Ignore:
+ break;
+ case ArgKind::Expand:
+ emitError() << "Expand at arg " << idx
+ << " not yet implemented in CallConvLowering";
+ return failure();
+ case ArgKind::Extend:
+ emitError() << "Extend at arg " << idx
+ << " not yet implemented in CallConvLowering";
+ return failure();
+ case ArgKind::Indirect:
+ emitError() << "Indirect at arg " << idx
+ << " not yet implemented in CallConvLowering";
+ return failure();
+ }
+ }
+ return success();
+}
+
+/// Compute the new return type for a function whose return classification
+/// is \p retInfo. As with `buildNewArgTypes`, only Direct (no coercion)
+/// and Ignore are implemented here; the remaining kinds emit an error.
+Type computeNewReturnType(Type origRetTy, const ArgClassification &retInfo,
+ MLIRContext *ctx,
+ function_ref<InFlightDiagnostic()> emitError) {
+ switch (retInfo.kind) {
+ case ArgKind::Direct:
+ if (retInfo.coercedType) {
+ emitError() << "Direct return with coerced type not yet implemented "
+ << "in CallConvLowering";
+ return nullptr;
+ }
+ return origRetTy;
+ case ArgKind::Ignore:
+ return cir::VoidType::get(ctx);
+ case ArgKind::Expand:
+ emitError() << "Expand return is not allowed (classic codegen rejects "
+ << "it in EmitFunctionEpilog)";
+ return nullptr;
+ case ArgKind::Extend:
+ emitError() << "Extend return not yet implemented in CallConvLowering";
+ return nullptr;
+ case ArgKind::Indirect:
+ emitError() << "Indirect return (sret) not yet implemented in "
+ << "CallConvLowering";
+ return nullptr;
+ }
+ llvm_unreachable("all ArgKind cases handled");
+}
+
+/// Create a typed poison constant to stand in for a value the body of a
+/// function (or the result of a call) still references but whose ABI
+/// classification is Ignore. Using poison is honest -- the value is
+/// genuinely unused at the ABI boundary -- and avoids a fake alloca+load
+/// pattern that would suggest we have a value when we don't.
+Value createIgnoredValue(OpBuilder &builder, Location loc, Type ty) {
+ return cir::ConstantOp::create(builder, loc, ty, cir::PoisonAttr::get(ty));
+}
+
+} // namespace
+
+LogicalResult CIRABIRewriteContext::rewriteFunctionDefinition(
+ FunctionOpInterface funcOpInterface, const FunctionClassification &fc,
+ OpBuilder &rewriter) {
+ // The pass driver (CallConvLoweringPass) only ever hands us cir.func ops,
+ // and the body of this routine is end-to-end CIR (it creates cir.constant,
+ // cir.return, etc.). Cast once at the top so the rest of the function
+ // reads in CIR's own vocabulary, and so we can dispatch to the
+ // CIRGlobalValueInterface for isDefinition() (FunctionOpInterface alone
+ // does not inherit from CIRGlobalValueInterface).
+ cir::FuncOp funcOp = cast<cir::FuncOp>(funcOpInterface);
+
+ if (!needsRewrite(fc))
+ return success();
+
+ ArrayRef<Type> oldArgTypes = funcOp.getArgumentTypes();
+ ArrayRef<Type> oldResultTypes = funcOp.getResultTypes();
+ MLIRContext *ctx = funcOp->getContext();
+
+ // CIR follows LLVM IR's single-result rule: a function returns either
+ // zero or one value. Document the invariant so a future multi-result
+ // change forces us to revisit the return-handling below.
+ assert(oldResultTypes.size() <= 1 &&
+ "CIR functions return zero or one value");
+
+ SmallVector<Type> newArgTypes;
+ if (failed(buildNewArgTypes(oldArgTypes, fc, newArgTypes,
+ [&]() { return funcOp.emitOpError(); })))
+ return failure();
+
+ Type voidTy = cir::VoidType::get(ctx);
+ Type origRetTy = oldResultTypes.empty() ? voidTy : oldResultTypes[0];
+ Type newRetTy = computeNewReturnType(origRetTy, fc.returnInfo, ctx,
+ [&]() { return funcOp.emitOpError(); });
+ if (!newRetTy)
+ return failure();
+ SmallVector<Type> newResultTypes = {newRetTy};
+
+ if (funcOp.isDefinition()) {
+ Region &body = funcOp->getRegion(0);
+ if (!body.empty()) {
+ Block &entry = body.front();
+
+ // For each Ignored argument: drop the block argument and, if the
+ // body still references it, replace those uses with a poison
+ // constant. Ignore classifications mean the value is empty / not
+ // passed at the ABI level, so any remaining uses are vacuous;
+ // poison says exactly that.
+ SmallVector<unsigned> ignored = ignoredArgIndices(fc);
+ for (unsigned blockIdx : llvm::reverse(ignored)) {
+ if (blockIdx >= entry.getNumArguments())
+ continue;
+ BlockArgument arg = entry.getArgument(blockIdx);
+ if (!arg.use_empty()) {
+ rewriter.setInsertionPointToStart(&entry);
+ Value poison =
+ createIgnoredValue(rewriter, funcOp.getLoc(), arg.getType());
+ arg.replaceAllUsesWith(poison);
+ }
+ entry.eraseArgument(blockIdx);
+ }
+ }
+
+ // When the return is classified Ignore but the original function had
+ // a non-void return type, every cir.return becomes a naked return.
+ // This relies on the invariant that computeNewReturnType has set
+ // newRetTy = void for Ignore above, and that the function type is
+ // updated below to match. Asserting this keeps the dependency
+ // explicit.
+ if (fc.returnInfo.kind == ArgKind::Ignore && !oldResultTypes.empty()) {
+ assert(isa<cir::VoidType>(newRetTy) &&
+ "Ignore-return path requires the new return type to be void");
+ SmallVector<cir::ReturnOp> returns;
+ funcOp.walk([&](cir::ReturnOp r) { returns.push_back(r); });
+ for (cir::ReturnOp r : returns) {
+ if (r.getNumOperands() == 0)
+ continue;
+ rewriter.setInsertionPoint(r);
+ cir::ReturnOp::create(rewriter, r.getLoc());
+ r.erase();
+ }
+ }
+ }
+
+ Type newFnTy = funcOp.cloneTypeWith(newArgTypes, newResultTypes);
+ funcOp.setFunctionTypeAttr(TypeAttr::get(newFnTy));
+
+ // Keep the arg_attrs array in sync with the new argument count by
+ // dropping entries for every Ignored argument. Without this the
+ // attribute array would have stale entries that no longer match any
+ // block argument.
+ SmallVector<unsigned> ignored = ignoredArgIndices(fc);
+ if (!ignored.empty())
+ if (auto existing = funcOp->getAttrOfType<ArrayAttr>("arg_attrs")) {
+ SmallVector<Attribute> kept;
+ kept.reserve(newArgTypes.size());
+ for (auto [oldIdx, attr] : llvm::enumerate(existing.getValue()))
+ if (oldIdx >= fc.argInfos.size() ||
+ fc.argInfos[oldIdx].kind != ArgKind::Ignore)
+ kept.push_back(attr);
+ funcOp->setAttr("arg_attrs", ArrayAttr::get(ctx, kept));
+ }
+
+ return success();
+}
+
+LogicalResult CIRABIRewriteContext::rewriteCallSite(
+ Operation *callOp, const FunctionClassification &fc, OpBuilder &rewriter) {
+ if (!needsRewrite(fc))
+ return success();
+
+ // TODO: also handle cir::TryCallOp (the throwing-call form used by C++
+ // exceptions) -- templating this routine on the call op type lets the
+ // rewriting logic stay shared. Will land with the EH-aware follow-up
+ // since the EH paths also need their own ABI considerations.
+ auto call = cast<cir::CallOp>(callOp);
----------------
andykaylor wrote:
Can you detect indirect calls here and emit an error?
https://github.com/llvm/llvm-project/pull/195737
More information about the cfe-commits
mailing list