[clang] [CIR] Add opt-in flag to run x86_64 call-conv lowering (PR #211335)
via cfe-commits
cfe-commits at lists.llvm.org
Wed Jul 22 10:55:26 PDT 2026
https://github.com/adams381 created https://github.com/llvm/llvm-project/pull/211335
The x86_64 System V classifier and the CallConvLowering pass that drives it are already in the tree, but nothing runs them: the -fclangir pipeline never adds the pass, so CIR still emits uncoerced aggregate signatures on x86_64 and its LLVM output diverges from classic CodeGen for anything passed or returned in registers.
This adds the cc1 flag -clangir-enable-call-conv-lowering, off by default like -clangir-enable-idiom-recognizer, and runs the pass in runCIRToCIRPasses right after CXXABILowering, which has already reduced C++ ABI types to plain records the classifier can handle. The flag is gated on x86_64. On other targets it is a silent no-op, so a driver can set it unconditionally without breaking non-x86_64 builds. The AVX level is fixed at None because the classifier's current supported subset has no >128-bit vector aggregate where the level would change the result.
With the flag on, supported scalars and small aggregates are coerced to match classic CodeGen: sign/zero-extended narrow integers, SSE scalars, one- and two-eightbyte INTEGER/SSE struct coercion, ignored empty structs, sret returns, and byval arguments. Types the classifier does not yet handle — unions, long double, and float aggregates that classify to an SSE vector — still errorNYI when the flag is on. CIR does emit noalias on byval parameters where classic CodeGen does not, which the test documents with split prefixes.
>From 6ce3427b8348be653547e89875dfd5ab11462c1f Mon Sep 17 00:00:00 2001
From: Adam Smith <adams at nvidia.com>
Date: Wed, 22 Jul 2026 10:52:44 -0700
Subject: [PATCH] [CIR] Add opt-in flag to run x86_64 call-conv lowering
The CallConvLowering pass and its x86_64 System V classifier are in the
tree but nothing runs them in the -fclangir pipeline, so CIR still emits
uncoerced aggregate signatures for x86_64.
Add the cc1 flag -clangir-enable-call-conv-lowering (off by default,
mirroring -clangir-enable-idiom-recognizer) and run the pass in
runCIRToCIRPasses right after CXXABILowering, which has already reduced
C++ ABI types to plain records the classifier understands. The pass is
gated on x86_64. On other targets the flag is a silent no-op so it can be
set unconditionally without breaking non-x86_64 compilations.
AVX level is fixed at None: the classifier's supported type subset has no
>128-bit vector aggregate where the level matters, so deriving it now
would be untested. Types the classifier does not yet handle (e.g.
unions) still errorNYI when the flag is on.
---
clang/include/clang/CIR/CIRToCIRPasses.h | 3 +-
.../include/clang/Frontend/FrontendOptions.h | 8 +-
clang/include/clang/Options/Options.td | 5 ++
clang/lib/CIR/FrontendAction/CIRGenAction.cpp | 3 +-
clang/lib/CIR/Lowering/CIRPasses.cpp | 28 ++++++-
.../CIR/CodeGen/call-conv-lowering-x86_64.c | 81 +++++++++++++++++++
6 files changed, 122 insertions(+), 6 deletions(-)
create mode 100644 clang/test/CIR/CodeGen/call-conv-lowering-x86_64.c
diff --git a/clang/include/clang/CIR/CIRToCIRPasses.h b/clang/include/clang/CIR/CIRToCIRPasses.h
index de0d4fab8597d..6e60614f8e9de 100644
--- a/clang/include/clang/CIR/CIRToCIRPasses.h
+++ b/clang/include/clang/CIR/CIRToCIRPasses.h
@@ -33,7 +33,8 @@ mlir::LogicalResult
runCIRToCIRPasses(mlir::ModuleOp theModule, mlir::MLIRContext &mlirCtx,
clang::ASTContext &astCtx, bool enableVerifier,
bool enableIdiomRecognizer, bool enableCIRSimplify,
- bool enableLibOpt, llvm::StringRef libOptOptions);
+ bool enableLibOpt, llvm::StringRef libOptOptions,
+ bool enableCallConvLowering);
} // namespace cir
diff --git a/clang/include/clang/Frontend/FrontendOptions.h b/clang/include/clang/Frontend/FrontendOptions.h
index 73c422998a217..3d410c0d7deef 100644
--- a/clang/include/clang/Frontend/FrontendOptions.h
+++ b/clang/include/clang/Frontend/FrontendOptions.h
@@ -423,6 +423,10 @@ class FrontendOptions {
LLVM_PREFERRED_TYPE(bool)
unsigned ClangIREnableIdiomRecognizer : 1;
+ /// Enable Clang IR (CIR) calling-convention lowering
+ LLVM_PREFERRED_TYPE(bool)
+ unsigned ClangIREnableCallConvLowering : 1;
+
/// Enable ClangIR library optimization.
/// Set when -fclangir-lib-opt or -fclangir-lib-opt= was passed.
LLVM_PREFERRED_TYPE(bool)
@@ -565,8 +569,8 @@ class FrontendOptions {
EmitPrettySymbolGraphs(false), GenReducedBMI(false),
UseClangIRPipeline(false), ClangIRDisablePasses(false),
ClangIRDisableCIRVerifier(false), ClangIREnableIdiomRecognizer(false),
- ClangIRLibOptEnabled(false), TimeTraceGranularity(500),
- TimeTraceVerbose(false) {}
+ ClangIREnableCallConvLowering(false), ClangIRLibOptEnabled(false),
+ TimeTraceGranularity(500), TimeTraceVerbose(false) {}
/// getInputKindForExtension - Return the appropriate input kind for a file
/// extension. For example, "c" would return Language::C.
diff --git a/clang/include/clang/Options/Options.td b/clang/include/clang/Options/Options.td
index 669b6e64725c9..f5264d5ea12fa 100644
--- a/clang/include/clang/Options/Options.td
+++ b/clang/include/clang/Options/Options.td
@@ -3459,6 +3459,11 @@ def clangir_enable_idiom_recognizer : Flag<["-"], "clangir-enable-idiom-recogniz
HelpText<"ClangIR: Enable Idiom Recognizer pass">,
MarshallingInfoFlag<FrontendOpts<"ClangIREnableIdiomRecognizer">>;
+def clangir_enable_call_conv_lowering : Flag<["-"], "clangir-enable-call-conv-lowering">,
+ Visibility<[ClangOption, CC1Option]>,
+ HelpText<"ClangIR: Enable calling-convention lowering pass (x86_64 only)">,
+ MarshallingInfoFlag<FrontendOpts<"ClangIREnableCallConvLowering">>;
+
def clangir_lib_opt_EQ : Joined<["-"], "clangir-lib-opt=">,
Visibility<[ClangOption, CC1Option]>, Group<f_Group>, Values<"all">,
HelpText<"Enable C/C++ library based optimizations (with options)">,
diff --git a/clang/lib/CIR/FrontendAction/CIRGenAction.cpp b/clang/lib/CIR/FrontendAction/CIRGenAction.cpp
index 28229ac8c58fd..42baa948d9698 100644
--- a/clang/lib/CIR/FrontendAction/CIRGenAction.cpp
+++ b/clang/lib/CIR/FrontendAction/CIRGenAction.cpp
@@ -144,7 +144,8 @@ class CIRGenConsumer : public clang::ASTConsumer {
if (runCIRToCIRPasses(
MlirModule, MlirCtx, C, !FEOptions.ClangIRDisableCIRVerifier,
FEOptions.ClangIREnableIdiomRecognizer, CGO.OptimizationLevel > 0,
- EnableLibOpt, LibOptOptions)
+ EnableLibOpt, LibOptOptions,
+ FEOptions.ClangIREnableCallConvLowering)
.failed()) {
CI.getDiagnostics().Report(diag::err_cir_to_cir_transform_failed);
return;
diff --git a/clang/lib/CIR/Lowering/CIRPasses.cpp b/clang/lib/CIR/Lowering/CIRPasses.cpp
index 2866e5f15379c..0a683b8c8a498 100644
--- a/clang/lib/CIR/Lowering/CIRPasses.cpp
+++ b/clang/lib/CIR/Lowering/CIRPasses.cpp
@@ -10,19 +10,30 @@
//
//===----------------------------------------------------------------------===//
-// #include "clang/AST/ASTContext.h"
#include "mlir/IR/BuiltinOps.h"
#include "mlir/Pass/PassManager.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/Basic/TargetInfo.h"
#include "clang/CIR/Dialect/Passes.h"
#include "llvm/Support/TimeProfiler.h"
+#include "llvm/TargetParser/Triple.h"
namespace cir {
+/// Map a target triple to the ABI target that drives CallConvLowering.
+/// Returns None for targets whose calling convention is not yet implemented.
+static CallConvTarget getCallConvTarget(const llvm::Triple &triple) {
+ if (triple.getArch() == llvm::Triple::x86_64)
+ return CallConvTarget::X86_64;
+ return CallConvTarget::None;
+}
+
mlir::LogicalResult
runCIRToCIRPasses(mlir::ModuleOp theModule, mlir::MLIRContext &mlirContext,
clang::ASTContext &astContext, bool enableVerifier,
bool enableIdiomRecognizer, bool enableCIRSimplify,
- bool enableLibOpt, llvm::StringRef libOptOptions) {
+ bool enableLibOpt, llvm::StringRef libOptOptions,
+ bool enableCallConvLowering) {
llvm::TimeTraceScope scope("CIR To CIR Passes");
@@ -49,6 +60,19 @@ runCIRToCIRPasses(mlir::ModuleOp theModule, mlir::MLIRContext &mlirContext,
pm.addPass(mlir::createTargetLoweringPass());
pm.addPass(mlir::createCXXABILoweringPass());
+
+ if (enableCallConvLowering) {
+ // CallConvLowering rewrites signatures and call sites using the classifier,
+ // so it must run after CXXABILowering has lowered C++ ABI types to plain
+ // records the classifier can handle. Only the x86_64 System V classifier
+ // is implemented; other targets are left unchanged.
+ CallConvTarget target =
+ getCallConvTarget(astContext.getTargetInfo().getTriple());
+ if (target != CallConvTarget::None)
+ pm.addPass(mlir::createCallConvLoweringPass(
+ target, llvm::abi::X86AVXABILevel::None));
+ }
+
pm.addPass(mlir::createLoweringPreparePass(&astContext));
pm.enableVerifier(enableVerifier);
diff --git a/clang/test/CIR/CodeGen/call-conv-lowering-x86_64.c b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64.c
new file mode 100644
index 0000000000000..6931198a90c3d
--- /dev/null
+++ b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64.c
@@ -0,0 +1,81 @@
+// 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 { int x; int y; } Pair2;
+typedef struct { long a; long b; } Pair16;
+typedef struct { long a, b, c, d; } Big;
+typedef struct { long a; double b; } IntSSE;
+typedef struct { double a; double b; } SSE2;
+typedef struct { } Empty;
+
+// Narrow signed integer sign-extended in a register.
+signed char ext_schar(signed char c) { return c; }
+
+// CIR: cir.func {{.*}}@ext_schar(%arg0: !s8i {{.*}}llvm.signext{{.*}}) -> (!s8i {{.*}}llvm.signext
+// LLVM: define dso_local signext i8 @ext_schar(i8 noundef signext %{{.+}})
+
+// Narrow unsigned integer zero-extended in a register.
+unsigned char ext_uchar(unsigned char c) { return c; }
+
+// CIR: cir.func {{.*}}@ext_uchar(%arg0: !u8i {{.*}}llvm.zeroext{{.*}}) -> (!u8i {{.*}}llvm.zeroext
+// LLVM: define dso_local zeroext i8 @ext_uchar(i8 noundef zeroext %{{.+}})
+
+// Floating-point scalar passed/returned in an SSE register.
+double sse_double(double d) { return d; }
+
+// CIR: cir.func {{.*}}@sse_double(%arg0: !cir.double {{.*}}) -> !cir.double
+// LLVM: define dso_local double @sse_double(double noundef %{{.+}})
+
+// Two-int struct returned in a single INTEGER eightbyte -> i64.
+Pair2 ret_pair2(int a) { Pair2 p = {a, a}; return p; }
+
+// CIR: cir.func {{.*}}@ret_pair2(%arg0: !s32i {{.*}}) -> !u64i
+// LLVM: define dso_local i64 @ret_pair2(i32 noundef %{{.+}})
+
+// 16-byte struct flattened into two integer registers.
+void take_pair16(Pair16 p) { (void)p; }
+
+// CIR: cir.func {{.*}}@take_pair16(%arg0: !s64i{{.*}}, %arg1: !s64i{{.*}})
+// LLVM: define dso_local void @take_pair16(i64 %{{.+}}, i64 %{{.+}})
+
+// Struct split into one INTEGER and one SSE eightbyte.
+void take_int_sse(IntSSE s) { (void)s; }
+
+// CIR: cir.func {{.*}}@take_int_sse(%arg0: !s64i{{.*}}, %arg1: !cir.double{{.*}})
+// LLVM: define dso_local void @take_int_sse(i64 %{{.+}}, double %{{.+}})
+
+// Struct split into two SSE eightbytes.
+void take_sse2(SSE2 s) { (void)s; }
+
+// CIR: cir.func {{.*}}@take_sse2(%arg0: !cir.double{{.*}}, %arg1: !cir.double{{.*}})
+// LLVM: define dso_local void @take_sse2(double %{{.+}}, double %{{.+}})
+
+// Empty struct argument is ignored -- dropped from the signature entirely.
+void take_empty(Empty e) { (void)e; }
+
+// CIR: cir.func {{.*}}@take_empty()
+// LLVM: define dso_local void @take_empty()
+
+// Empty struct return is ignored -- the function returns void.
+Empty ret_empty(void) { Empty e; return e; }
+
+// CIR: cir.func {{.*}}@ret_empty()
+// LLVM: define dso_local void @ret_empty()
+
+// Large struct returned indirectly via sret.
+Big ret_big(void) { Big b = {1, 2, 3, 4}; return b; }
+
+// CIR: cir.func {{.*}}@ret_big(%arg0: !cir.ptr<!rec_Big> {{.*}}llvm.sret = !rec_Big{{.*}})
+// LLVM: define dso_local void @ret_big(ptr dead_on_unwind noalias writable sret(%struct.Big) align 8 %{{.+}})
+
+// Large struct passed byval. CIR also emits noalias on byval; OGCG only does
+// so under -fpass-by-value-is-noalias.
+void take_big(Big b) { (void)b; }
+
+// CIR: cir.func {{.*}}@take_big(%arg0: !cir.ptr<!rec_Big> {{.*}}llvm.byval = !rec_Big{{.*}})
+// LLVM-CIR: define dso_local void @take_big(ptr noalias noundef byval(%struct.Big) align 8 %{{.+}})
+// LLVM-OGCG: define dso_local void @take_big(ptr noundef byval(%struct.Big) align 8 %{{.+}})
More information about the cfe-commits
mailing list