https://github.com/adams381 updated https://github.com/llvm/llvm-project/pull/215118
>From 77536bbe2747a41713fe2638f75fc4519a456968 Mon Sep 17 00:00:00 2001 From: Adam Smith <[email protected]> Date: Sun, 9 Aug 2026 10:05:12 -0700 Subject: [PATCH 1/3] [CIR] Accept fixed-width vectors in x86_64 callconv lowering The CallConvLowering bridge rejects a vector in a parameter or return position, so a function taking one fails the pass. It also never reads the AVX level, which is what decides whether a vector wider than 128 bits reaches a register. A vector is accepted now where the classifier and clang size it the same way, which means a whole-byte element and a power-of-two width. Scalable vectors and the other widths stay rejected. The module's AVX level comes from the target ABI name, as CodeGenModule does. A classifier per level lets a target attribute raise it for one function. An ABI older than the rule pins every function back to the module's level. A direct call takes its callee's level, and an indirect call the level of the function containing it. CIRGen records target features on a definition but not on a declaration, so a declaration carrying the attribute is classified at the module's level until Assisted-by: Cursor / claude-opus-5 --- clang/include/clang/CIR/Dialect/Passes.h | 7 +- clang/include/clang/CIR/Dialect/Passes.td | 4 + .../Transforms/CallConvLoweringPass.cpp | 164 +++++++++++++---- clang/lib/CIR/Lowering/CIRPasses.cpp | 28 ++- .../call-conv-lowering-x86_64-abi-compat.c | 62 ++++++- .../CodeGen/call-conv-lowering-x86_64-avx.c | 165 ++++++++++++++++++ .../CIR/CodeGen/call-conv-lowering-x86_64.c | 77 ++++++++ .../abi-lowering/x86_64-aggregate-nyi.cir | 43 +++++ .../abi-lowering/x86_64-variadic-call.cir | 71 +++++--- .../Transforms/abi-lowering/x86_64-vector.cir | 75 +++++++- 10 files changed, 615 insertions(+), 81 deletions(-) create mode 100644 clang/test/CIR/CodeGen/call-conv-lowering-x86_64-avx.c diff --git a/clang/include/clang/CIR/Dialect/Passes.h b/clang/include/clang/CIR/Dialect/Passes.h index 888e7b833b1cf..6c8bec15c3838 100644 --- a/clang/include/clang/CIR/Dialect/Passes.h +++ b/clang/include/clang/CIR/Dialect/Passes.h @@ -36,10 +36,9 @@ std::unique_ptr<Pass> createCIREHABILoweringPass(); std::unique_ptr<Pass> createCXXABILoweringPass(); std::unique_ptr<Pass> createTargetLoweringPass(); std::unique_ptr<Pass> createCallConvLoweringPass(); -std::unique_ptr<Pass> -createCallConvLoweringPass(cir::CallConvTarget target, - llvm::abi::X86AVXABILevel x86AvxAbiLevel, - const llvm::abi::ABICompatInfo &x86AbiCompat); +std::unique_ptr<Pass> createCallConvLoweringPass( + cir::CallConvTarget target, llvm::abi::X86AVXABILevel x86AvxAbiLevel, + bool x86TargetAttrAvx, const llvm::abi::ABICompatInfo &x86AbiCompat); std::unique_ptr<Pass> createHoistAllocasPass(); std::unique_ptr<Pass> createLoweringPreparePass(); std::unique_ptr<Pass> createLoweringPreparePass(clang::ASTContext *astCtx); diff --git a/clang/include/clang/CIR/Dialect/Passes.td b/clang/include/clang/CIR/Dialect/Passes.td index 04ff1c92cb7f7..bc1b99da01440 100644 --- a/clang/include/clang/CIR/Dialect/Passes.td +++ b/clang/include/clang/CIR/Dialect/Passes.td @@ -260,6 +260,10 @@ def CallConvLowering : Pass<"cir-call-conv-lowering", "mlir::ModuleOp"> { clEnumValN(llvm::abi::X86AVXABILevel::AVX, "avx", "AVX"), clEnumValN(llvm::abi::X86AVXABILevel::AVX512, "avx512", "AVX512") )}]>, + Option<"x86TargetAttrAvx", "x86-target-attr-avx", "bool", + /*default=*/"true", + "Let a function's own target features raise its AVX ABI level " + "above x86-avx-abi-level">, ]; } diff --git a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp index 4e3c169f86d9d..5f25e827b9cd2 100644 --- a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp +++ b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp @@ -47,8 +47,13 @@ #include "llvm/ABI/FunctionInfo.h" #include "llvm/ABI/TargetInfo.h" #include "llvm/ABI/Types.h" +#include "llvm/ADT/StringExtras.h" #include "llvm/ADT/TypeSwitch.h" #include "llvm/IR/CallingConv.h" +#include "llvm/Support/MathExtras.h" + +#include <algorithm> +#include <array> using namespace mlir; using namespace mlir::abi; @@ -68,10 +73,11 @@ namespace { // SysV x86_64 classifier, and converts the result back into the // dialect-agnostic mlir::abi::FunctionClassification that CIRABIRewriteContext // consumes. Integer (including `_BitInt` up to 128 bits) / pointer / bool / -// floating-point scalars are handled, as are struct / union / array aggregates -// and `_Complex`. Vectors, packed or padded records, and a union no member of -// which spans its declared size are reported NYI by classifyX86_64Function so -// an unsupported signature fails the pass instead of being misclassified. +// floating-point scalars are handled, as are struct / union / array aggregates, +// `_Complex`, and a fixed-width vector whose width is a power of two. Other +// vectors, packed or padded records, and a union no member of which spans its +// declared size are reported NYI by classifyX86_64Function so an unsupported +// signature fails the pass instead of being misclassified. //===----------------------------------------------------------------------===// /// Whether a struct's declared argument-passing kind (from the module's @@ -103,8 +109,9 @@ static llvm::Align recordDeclaredAlign(ModuleOp modOp, cir::RecordType recTy, /// bits (including `_BitInt` and `__int128`), pointer, bool, void, or any /// floating-point type. Aggregates: a complete struct or union whose members /// are all themselves supported, or an array of a supported element type. -/// Also a `_Complex` of a supported element type. Everything else is reported -/// NYI at the reject() choke point in classifyX86_64Function. +/// Also a `_Complex`, or a fixed-width vector, of a supported element type. +/// Everything else is reported NYI at the reject() choke point in +/// classifyX86_64Function. static bool isSupportedType(mlir::Type ty, const DataLayout &dl) { // A pointer is only handled in the default address space (null) or an // already-lowered target address space. A LangAddressSpaceAttr must be @@ -135,6 +142,34 @@ static bool isSupportedType(mlir::Type ty, const DataLayout &dl) { } if (auto complexTy = dyn_cast<cir::ComplexType>(ty)) return isSupportedType(complexTy.getElementType(), dl); + if (auto vecTy = dyn_cast<cir::VectorType>(ty)) { + // A scalable vector has no size the SysV eightbyte rules can read, and + // x86_64 has no calling convention for one. + if (vecTy.getIsScalable()) + return false; + // The classifier sizes a vector as element count times element width, so + // an element is only usable where that width is the one clang gives it. + // It is not for bool (a bit to clang, a byte here), for a _BitInt narrower + // than a byte (clang rounds to the storage container), or for x87 long + // double (80 bits here against clang's 128). A pointer is excluded for a + // different reason, its pointee being what abiTypeToCIR drops. + mlir::Type elemTy = vecTy.getElementType(); + if (auto elemInt = dyn_cast<cir::IntType>(elemTy)) { + if (elemInt.getWidth() % 8) + return false; + } else if (auto elemFp = dyn_cast<cir::FPTypeInterface>(elemTy)) { + if (&elemFp.getFloatSemantics() == &llvm::APFloat::x87DoubleExtended()) + return false; + } else { + return false; + } + // Clang also rounds the vector's own width up to a power of two, and the + // classifier branches on the exact width, so a three-char vector would be + // classified at 24 bits where clang uses 32. + if (!llvm::isPowerOf2_64(dl.getTypeSizeInBits(ty).getFixedValue())) + return false; + return isSupportedType(elemTy, dl); + } if (auto arrTy = dyn_cast<cir::ArrayType>(ty)) return isSupportedType(arrTy.getElementType(), dl); if (auto recTy = dyn_cast<cir::RecordType>(ty)) { @@ -256,6 +291,14 @@ static const llvm::abi::Type *mapCIRType(mlir::Type type, mapCIRType(complexTy.getElementType(), typeMapper, dl, modOp), llvm::Align(dl.getTypeABIAlignment(type))); }) + .Case([&](cir::VectorType vecTy) { + // isSupportedType rejects a scalable vector, so the element count is + // always fixed here. + return tb.getVectorType( + mapCIRType(vecTy.getElementType(), typeMapper, dl, modOp), + llvm::ElementCount::getFixed(vecTy.getSize()), + llvm::Align(dl.getTypeABIAlignment(type))); + }) .Case([&](cir::ArrayType arrTy) { const llvm::abi::Type *elemAbi = mapCIRType(arrTy.getElementType(), typeMapper, dl, modOp); @@ -334,10 +377,10 @@ convertABIArgInfo(const llvm::abi::ArgInfo &info, MLIRContext *ctx, // type, so a non-null coerce does not by itself mean a rewrite is needed. const llvm::abi::Type *coerceAbi = info.getCoerceToType(); bool isAggregate = isa_and_present<cir::RecordType, cir::ArrayType>(origTy); - // For a _Complex the classifier's coerce is only sometimes the natural - // type, so it has to be read rather than assumed. + // 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>(origTy); + coerceAbi && isa_and_present<cir::ComplexType, cir::VectorType>(origTy); bool coerceIsRegisterTuple = isa_and_present<llvm::abi::RecordType>(coerceAbi); // Compare widths rather than identity: a coerce no wider than the natural @@ -381,8 +424,7 @@ convertABIArgInfo(const llvm::abi::ArgInfo &info, MLIRContext *ctx, /// Where \p fnTy's declared parameters end and its ellipsis arguments begin. /// /// The only x86_64 rule that reads this boundary sends an unnamed vector wider -/// than 128 bits to memory, and isSupportedType rejects every vector, so no -/// input this bridge accepts can observe the difference. +/// than 128 bits to memory. static llvm::abi::RequiredArgs requiredArgs(cir::FuncType fnTy) { if (!fnTy.isVarArg()) return llvm::abi::RequiredArgs::All; @@ -465,6 +507,34 @@ static std::optional<FunctionClassification> classifyX86_64Signature( return fc; } +/// The AVX level in force at \p func, following getEffectiveX86AVXABILevel in +/// clang/lib/CodeGen/Targets/X86.cpp. Classic reads the target attribute +/// itself, where this reads the feature list CIRGen recorded from it. Two +/// consequences: a declaration gets \p base, CIRGen recording features only on +/// definitions, and target_clones and cpu_specific will need excluding once +/// multiversioning is implemented, classic leaving those at the module level. +static llvm::abi::X86AVXABILevel funcAvxLevel(cir::FuncOp func, + llvm::abi::X86AVXABILevel base) { + auto features = func->getAttrOfType<mlir::StringAttr>("cir.target-features"); + if (!features) + return base; + // A '-' entry disables the feature, so match a whole '+' entry rather than + // searching for the name. + auto enabled = [&](llvm::StringRef name) { + for (llvm::StringRef feature : llvm::split(features.getValue(), ',')) + if (feature.consume_front("+") && feature == name) + return true; + return false; + }; + // avx512f implies avx, so both entries are present. Test the wider name + // first or the AVX512 branch is unreachable. + if (enabled("avx512f")) + return std::max(base, llvm::abi::X86AVXABILevel::AVX512); + if (enabled("avx")) + return std::max(base, llvm::abi::X86AVXABILevel::AVX); + return base; +} + /// 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. @@ -634,16 +704,33 @@ void CallConvLoweringPass::runOnOperation() { CIRABIRewriteContext rewriteCtx(moduleOp, dl); SymbolTable symbolTable(moduleOp); - // For the x86_64 target, build the LLVM ABI library classifier once and - // reuse it (and its type mapper) across every function. + // A per-function target attribute can raise the AVX level, so one classifier + // per module would misclassify a wide vector in such a function. + static constexpr unsigned numAvxLevels = + static_cast<unsigned>(llvm::abi::X86AVXABILevel::AVX512) + 1; + bool isX86 = target == cir::CallConvTarget::X86_64; std::optional<mlir::abi::ABITypeMapper> x86TypeMapper; - std::unique_ptr<llvm::abi::TargetInfo> x86Target; - if (target == cir::CallConvTarget::X86_64) { + std::array<std::unique_ptr<llvm::abi::TargetInfo>, numAvxLevels> x86Targets; + if (isX86) x86TypeMapper.emplace(dl); - x86Target = llvm::abi::createX86_64TargetInfo( - x86TypeMapper->getTypeBuilder(), x86AvxAbiLevel.getValue(), - /*Has64BitPointers=*/true, x86AbiCompat); - } + auto x86TargetFor = + [&](llvm::abi::X86AVXABILevel level) -> const llvm::abi::TargetInfo & { + assert(static_cast<unsigned>(level) < numAvxLevels && + "a new X86AVXABILevel needs a slot in x86Targets"); + std::unique_ptr<llvm::abi::TargetInfo> &slot = + x86Targets[static_cast<unsigned>(level)]; + if (!slot) + slot = llvm::abi::createX86_64TargetInfo( + x86TypeMapper->getTypeBuilder(), level, + /*Has64BitPointers=*/true, x86AbiCompat); + return *slot; + }; + llvm::abi::X86AVXABILevel baseAvxLevel = x86AvxAbiLevel.getValue(); + auto avxLevelFor = [&](cir::FuncOp func) -> llvm::abi::X86AVXABILevel { + if (!x86TargetAttrAvx || !func) + return baseAvxLevel; + return funcAvxLevel(func, baseAvxLevel); + }; // Classify every cir.func up front. No IR mutation happens here, so // later walks can consult any function's classification regardless of @@ -652,8 +739,9 @@ void CallConvLoweringPass::runOnOperation() { bool anyFailed = false; moduleOp.walk([&](cir::FuncOp f) { std::optional<FunctionClassification> fc; - if (x86Target) - fc = classifyX86_64Function(f, dl, *x86TypeMapper, *x86Target, moduleOp); + if (isX86) + fc = classifyX86_64Function(f, dl, *x86TypeMapper, + x86TargetFor(avxLevelFor(f)), moduleOp); else fc = classifyFunction(f, dl, target, classificationAttr); if (!fc) { @@ -694,7 +782,7 @@ void CallConvLoweringPass::runOnOperation() { // drivers the classification comes from a fixed per-function source, so // such a call stays short a classification and rewriteCallSite reports it. cir::FuncType calleeTy = callee.getFunctionType(); - if (!x86Target || call.getNumArgOperands() <= calleeTy.getNumInputs()) + if (!isX86 || call.getNumArgOperands() <= calleeTy.getNumInputs()) return; // A callee declared without a prototype also takes more operands than it // declares, and the verifier allows it. Those extra arguments are named @@ -706,8 +794,14 @@ void CallConvLoweringPass::runOnOperation() { anyFailed = true; return; } - std::optional<FunctionClassification> fc = classifyX86_64VariadicCall( - call, calleeTy, dl, *x86TypeMapper, *x86Target, moduleOp); + // The callee's level, not the caller's: this pass rewrites the definition + // and its call sites from one classification, so they have to agree. + // Classic instead arranges every call site from the caller and reports a + // caller whose level disagrees with its callee in checkFunctionCallABI, + // which has no equivalent here yet. + std::optional<FunctionClassification> fc = + classifyX86_64VariadicCall(call, calleeTy, dl, *x86TypeMapper, + x86TargetFor(avxLevelFor(callee)), moduleOp); if (!fc) { anyFailed = true; return; @@ -807,11 +901,15 @@ void CallConvLoweringPass::runOnOperation() { cir::FuncType funcTy = indirectCalleeType(c); auto classifySignature = [&](mlir::TypeRange argTypes) -> std::optional<FunctionClassification> { - if (x86Target) - return classifyX86_64Signature(funcTy.getReturnType(), argTypes, - requiredArgs(funcTy), ctx, dl, - *x86TypeMapper, *x86Target, moduleOp, - [&]() { return c->emitOpError(); }); + // A callee resolved at run time carries no features of its own, so the + // level comes from the function containing the call, which is the + // declaration classic arranges every call site from. + if (isX86) + return classifyX86_64Signature( + funcTy.getReturnType(), argTypes, requiredArgs(funcTy), ctx, dl, + *x86TypeMapper, + x86TargetFor(avxLevelFor(c->getParentOfType<cir::FuncOp>())), + moduleOp, [&]() { return c->emitOpError(); }); return withReturnVoidness( mlir::abi::test::classify(argTypes, funcTy.getReturnType(), dl), funcTy.getReturnType()); @@ -857,12 +955,12 @@ std::unique_ptr<Pass> mlir::createCallConvLoweringPass() { return std::make_unique<CallConvLoweringPass>(); } -std::unique_ptr<Pass> -mlir::createCallConvLoweringPass(cir::CallConvTarget target, - llvm::abi::X86AVXABILevel x86AvxAbiLevel, - const llvm::abi::ABICompatInfo &x86AbiCompat) { +std::unique_ptr<Pass> mlir::createCallConvLoweringPass( + cir::CallConvTarget target, llvm::abi::X86AVXABILevel x86AvxAbiLevel, + bool x86TargetAttrAvx, const llvm::abi::ABICompatInfo &x86AbiCompat) { CallConvLoweringOptions options; options.target = target; options.x86AvxAbiLevel = x86AvxAbiLevel; + options.x86TargetAttrAvx = x86TargetAttrAvx; return std::make_unique<CallConvLoweringPass>(options, x86AbiCompat); } diff --git a/clang/lib/CIR/Lowering/CIRPasses.cpp b/clang/lib/CIR/Lowering/CIRPasses.cpp index dad17f2ef8659..972dc6d1a4a8b 100644 --- a/clang/lib/CIR/Lowering/CIRPasses.cpp +++ b/clang/lib/CIR/Lowering/CIRPasses.cpp @@ -29,6 +29,26 @@ static CallConvTarget getCallConvTarget(const llvm::Triple &triple) { return CallConvTarget::None; } +/// The AVX level the classifier uses to size a native vector. Read from the +/// target ABI name, as CodeGenModule does. +static llvm::abi::X86AVXABILevel getX86AVXABILevel(llvm::StringRef abi) { + if (abi == "avx512") + return llvm::abi::X86AVXABILevel::AVX512; + if (abi == "avx") + return llvm::abi::X86AVXABILevel::AVX; + return llvm::abi::X86AVXABILevel::None; +} + +/// Whether `__attribute__((target(...)))` on a function may raise its AVX ABI +/// level above the command line's. Mirrors getEffectiveX86AVXABILevel in +/// clang/lib/CodeGen/Targets/X86.cpp, which keeps a target that opts out, and +/// any ABI older than the rule, at the module level. +static bool allowsX86TargetAttrAvx(const clang::ASTContext &astContext) { + return !astContext.getTargetInfo().getTriple().isPS() && + astContext.getLangOpts().getClangABICompat() > + clang::LangOptions::ClangABI::Ver23; +} + /// The x86_64 ABI-compatibility flags, derived from the target and the /// requested compatibility version. Every flag defaults to true in the ABI /// library, which is not what any target computes: Clang11Compat is false for a @@ -95,12 +115,12 @@ runCIRToCIRPasses(mlir::ModuleOp theModule, mlir::MLIRContext &mlirContext, // 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()); + const clang::TargetInfo &targetInfo = astContext.getTargetInfo(); + CallConvTarget target = getCallConvTarget(targetInfo.getTriple()); if (target != CallConvTarget::None) pm.addPass(mlir::createCallConvLoweringPass( - target, llvm::abi::X86AVXABILevel::None, - getX86ABICompatInfo(astContext))); + target, getX86AVXABILevel(targetInfo.getABI()), + allowsX86TargetAttrAvx(astContext), getX86ABICompatInfo(astContext))); } pm.addPass(mlir::createLoweringPreparePass(&astContext)); diff --git a/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-abi-compat.c b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-abi-compat.c index f5cf3c1d49da1..8eb03df7fa5f9 100644 --- a/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-abi-compat.c +++ b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-abi-compat.c @@ -1,13 +1,32 @@ -// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-llvm %s -o %t-cir.ll -// RUN: FileCheck --check-prefix=LINUX-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-prefix=LINUX-OGCG --input-file=%t.ll %s +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -target-feature +avx -fclangir -emit-llvm %s -o %t-cir.ll +// RUN: FileCheck --check-prefixes=LINUX,LINUX-CIR --input-file=%t-cir.ll %s +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -target-feature +avx -emit-llvm %s -o %t.ll +// RUN: FileCheck --check-prefixes=LINUX,LINUX-OGCG --input-file=%t.ll %s -// RUN: %clang_cc1 -triple x86_64-apple-darwin -fclangir -emit-llvm %s -o %t-darwin-cir.ll +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -target-feature +avx -fclang-abi-compat=3.8 -fclangir -emit-llvm %s -o %t-38-cir.ll +// RUN: FileCheck --check-prefix=LINUX38 --input-file=%t-38-cir.ll %s +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -target-feature +avx -fclang-abi-compat=3.8 -emit-llvm %s -o %t-38.ll +// RUN: FileCheck --check-prefix=LINUX38 --input-file=%t-38.ll %s + +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -target-feature +avx -fclang-abi-compat=9 -fclangir -emit-llvm %s -o %t-9-cir.ll +// RUN: FileCheck --check-prefix=LINUX9 --input-file=%t-9-cir.ll %s +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -target-feature +avx -fclang-abi-compat=9 -emit-llvm %s -o %t-9.ll +// RUN: FileCheck --check-prefix=LINUX9 --input-file=%t-9.ll %s + +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -target-feature +avx -fclang-abi-compat=11 -fclangir -emit-llvm %s -o %t-11-cir.ll +// RUN: FileCheck --check-prefixes=LINUX11-CIR --input-file=%t-11-cir.ll %s +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -target-feature +avx -fclang-abi-compat=11 -emit-llvm %s -o %t-11.ll +// RUN: FileCheck --check-prefixes=LINUX11-OGCG --input-file=%t-11.ll %s + +// RUN: %clang_cc1 -triple x86_64-apple-darwin -target-feature +avx -fclangir -emit-llvm %s -o %t-darwin-cir.ll // RUN: FileCheck --check-prefix=DARWIN --input-file=%t-darwin-cir.ll %s -// RUN: %clang_cc1 -triple x86_64-apple-darwin -emit-llvm %s -o %t-darwin.ll +// RUN: %clang_cc1 -triple x86_64-apple-darwin -target-feature +avx -emit-llvm %s -o %t-darwin.ll // RUN: FileCheck --check-prefix=DARWIN --input-file=%t-darwin.ll %s +typedef long long v1ll __attribute__((vector_size(8))); +typedef __int128 v2i128 __attribute__((vector_size(32))); +typedef float v8f __attribute__((vector_size(32))); + // The 0.98 ABI revision sends an eightbyte pair to memory when the high half is // X87UP and the low half is not X87. Darwin exempts itself for binary // compatibility with older GCC, so the same union passes in registers there. @@ -18,3 +37,34 @@ void rev98(ULongDouble u) { (void)u; } // LINUX-CIR: define dso_local void @rev98(ptr noalias noundef byval(%union.ULongDouble) align 16 %{{[^,)]+}}) // LINUX-OGCG: define dso_local void @rev98(ptr noundef byval(%union.ULongDouble) align 16 %{{[^,)]+}}) // DARWIN: define void @rev98(i64 %{{[^,)]+}}, double %{{[^,)]+}}) + +// GCC classifies a 64-bit vector of a 64-bit integer as SSE. Clang 3.8 and +// older did not, and Darwin, FreeBSD and PlayStation still do not. +void mmx(v1ll v) { (void)v; } + +// LINUX: define dso_local void @mmx(double noundef %{{[^,)]+}}) +// LINUX38: define dso_local void @mmx(i64 noundef %{{[^,)]+}}) +// LINUX9: define dso_local void @mmx(double noundef %{{[^,)]+}}) +// DARWIN: define void @mmx(i64 noundef %{{[^,)]+}}) + +// GCC classifies a vector of __int128 as memory. Clang 9 and older did not, +// and only Linux and NetBSD follow it. AVX is on above so that this vector +// would otherwise reach a register, which is what makes the rule observable. +void wide_int128(v2i128 v) { (void)v; } + +// LINUX-CIR: define dso_local void @wide_int128(ptr noalias noundef byval(<2 x i128>) align 32 %{{[^,)]+}}) +// LINUX-OGCG: define dso_local void @wide_int128(ptr noundef byval(<2 x i128>) align 32 %{{[^,)]+}}) +// LINUX38: define dso_local void @wide_int128(<2 x i128> noundef %{{[^,)]+}}) +// LINUX9: define dso_local void @wide_int128(<2 x i128> noundef %{{[^,)]+}}) +// DARWIN: define void @wide_int128(<2 x i128> noundef %{{[^,)]+}}) + +// A union larger than an eightbyte is classified from the member spanning its +// size, so it reaches registers once the level admits the vector. Clang 11 and +// older instead treated every member as spanning, which sends this to memory +// because the float member does not. +union UnionWideVector { v8f v; float f; }; +void take_union_wide_vector(union UnionWideVector u) { (void)u; } + +// LINUX: define dso_local void @take_union_wide_vector(<4 x double> %{{[^,)]+}}) +// LINUX11-CIR: define dso_local void @take_union_wide_vector(ptr noalias noundef byval(%union.UnionWideVector) align 32 %{{[^,)]+}}) +// LINUX11-OGCG: define dso_local void @take_union_wide_vector(ptr noundef byval(%union.UnionWideVector) align 32 %{{[^,)]+}}) diff --git a/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-avx.c b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-avx.c new file mode 100644 index 0000000000000..0c733c34f4d9d --- /dev/null +++ b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-avx.c @@ -0,0 +1,165 @@ +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -clangir-enable-call-conv-lowering -emit-cir %s -o %t.cir +// RUN: FileCheck --check-prefixes=CIR,CIR-SSE --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-SSE --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-SSE --input-file=%t.ll %s + +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -target-feature +avx -fclangir -clangir-enable-call-conv-lowering -emit-cir %s -o %t-avx.cir +// RUN: FileCheck --check-prefixes=CIR,CIR-AVX --input-file=%t-avx.cir %s +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -target-feature +avx -fclangir -clangir-enable-call-conv-lowering -emit-llvm %s -o %t-avx-cir.ll +// RUN: FileCheck --check-prefixes=LLVM,LLVM-AVX,LLVM-CIR-AVX --input-file=%t-avx-cir.ll %s +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -target-feature +avx -emit-llvm %s -o %t-avx.ll +// RUN: FileCheck --check-prefixes=LLVM,LLVM-AVX,LLVM-OGCG-AVX --input-file=%t-avx.ll %s + +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -target-feature +avx512f -fclangir -clangir-enable-call-conv-lowering -emit-llvm %s -o %t-avx512-cir.ll +// RUN: FileCheck --check-prefixes=LLVM,LLVM-AVX512 --input-file=%t-avx512-cir.ll %s +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -target-feature +avx512f -emit-llvm %s -o %t-avx512.ll +// RUN: FileCheck --check-prefixes=LLVM,LLVM-AVX512 --input-file=%t-avx512.ll %s + +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclang-abi-compat=23 -fclangir -clangir-enable-call-conv-lowering -emit-llvm %s -o %t-compat23-cir.ll +// RUN: FileCheck --check-prefix=LLVM-CIR-PINNED --input-file=%t-compat23-cir.ll %s +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclang-abi-compat=23 -emit-llvm %s -o %t-compat23.ll +// RUN: FileCheck --check-prefix=LLVM-OGCG-PINNED --input-file=%t-compat23.ll %s + +// RUN: %clang_cc1 -triple x86_64-scei-ps4 -fclangir -clangir-enable-call-conv-lowering -emit-llvm %s -o %t-ps4-cir.ll +// RUN: FileCheck --check-prefix=LLVM-CIR-PINNED --input-file=%t-ps4-cir.ll %s +// RUN: %clang_cc1 -triple x86_64-scei-ps4 -emit-llvm %s -o %t-ps4.ll +// RUN: FileCheck --check-prefix=LLVM-OGCG-PINNED --input-file=%t-ps4.ll %s + +typedef float v4f __attribute__((vector_size(16))); +typedef float v8f __attribute__((vector_size(32))); +typedef float v16f __attribute__((vector_size(64))); + +// A 128-bit vector is at or below the native vector size at every AVX level, +// so it always passes in a register. +void take_v128(v4f v) { (void)v; } + +// CIR: cir.func {{.*}}@take_v128(%arg0: !cir.vector<4 x !cir.float>{{.*}}) +// LLVM: define dso_local void @take_v128(<4 x float> noundef %{{[^,)]+}}) + +// A 256-bit vector only reaches a register once the ABI level is AVX. Below +// that it is passed byval, aligned to its size. +void take_v256(v8f v) { (void)v; } + +// CIR-SSE: cir.func {{.*}}@take_v256(%arg0: !cir.ptr<!cir.vector<8 x !cir.float>> {{.*}}llvm.align = 32 : i64{{.*}}llvm.byval = !cir.vector<8 x !cir.float>{{.*}}) +// CIR-AVX: cir.func {{.*}}@take_v256(%arg0: !cir.vector<8 x !cir.float>{{.*}}) +// LLVM-CIR-SSE: define dso_local void @take_v256(ptr noalias noundef byval(<8 x float>) align 32 %{{[^,)]+}}) +// LLVM-OGCG-SSE: define dso_local void @take_v256(ptr noundef byval(<8 x float>) align 32 %{{[^,)]+}}) +// LLVM-AVX: define dso_local void @take_v256(<8 x float> noundef %{{[^,)]+}}) +// LLVM-AVX512: define dso_local void @take_v256(<8 x float> noundef %{{[^,)]+}}) + +// The register-versus-memory split applies to arguments only. A 256-bit +// vector comes back in registers at every level. +v8f ret_v256(void) { v8f z = {0}; return z; } + +// LLVM: define dso_local <8 x float> @ret_v256() + +// Below AVX the caller has to build the byval slot, which is the memory copy a +// vector argument needs on a non-variadic call. +void call_v256(v8f v) { take_v256(v); } + +// LLVM-CIR-SSE: define dso_local void @call_v256(ptr noalias noundef byval(<8 x float>) align 32 %{{[^,)]+}}) +// LLVM-CIR-SSE: call void @take_v256(ptr noalias noundef byval(<8 x float>) align 32 %{{[^,)]+}}) +// LLVM-OGCG-SSE: define dso_local void @call_v256(ptr noundef byval(<8 x float>) align 32 %{{[^,)]+}}) +// LLVM-OGCG-SSE: call void @take_v256(ptr noundef byval(<8 x float>) align 32 %{{[^,)]+}}) +// LLVM-AVX: define dso_local void @call_v256(<8 x float> noundef %{{[^,)]+}}) +// LLVM-AVX: call void @take_v256(<8 x float> noundef %{{[^,)]+}}) +// LLVM-AVX512: define dso_local void @call_v256(<8 x float> noundef %{{[^,)]+}}) +// LLVM-AVX512: call void @take_v256(<8 x float> noundef %{{[^,)]+}}) + +// A 512-bit vector needs AVX512 for the same treatment. +void take_v512(v16f v) { (void)v; } + +// CIR-SSE: cir.func {{.*}}@take_v512(%arg0: !cir.ptr<!cir.vector<16 x !cir.float>> {{.*}}llvm.align = 64 : i64{{.*}}llvm.byval = !cir.vector<16 x !cir.float>{{.*}}) +// CIR-AVX: cir.func {{.*}}@take_v512(%arg0: !cir.ptr<!cir.vector<16 x !cir.float>> {{.*}}llvm.align = 64 : i64{{.*}}llvm.byval = !cir.vector<16 x !cir.float>{{.*}}) +// LLVM-CIR-SSE: define dso_local void @take_v512(ptr noalias noundef byval(<16 x float>) align 64 %{{[^,)]+}}) +// LLVM-OGCG-SSE: define dso_local void @take_v512(ptr noundef byval(<16 x float>) align 64 %{{[^,)]+}}) +// LLVM-CIR-AVX: define dso_local void @take_v512(ptr noalias noundef byval(<16 x float>) align 64 %{{[^,)]+}}) +// LLVM-OGCG-AVX: define dso_local void @take_v512(ptr noundef byval(<16 x float>) align 64 %{{[^,)]+}}) +// LLVM-AVX512: define dso_local void @take_v512(<16 x float> noundef %{{[^,)]+}}) + +// Disabling a feature the command line enabled is the only way a function's +// feature list carries a '-' entry, and the level still cannot fall below the +// module's, so this is byval only where the module itself lacks AVX512. +__attribute__((target("no-avx512f"))) void take_v512_no_avx512(v16f v) { (void)v; } + +// LLVM-CIR-SSE: define dso_local void @take_v512_no_avx512(ptr noalias noundef byval(<16 x float>) align 64 %{{[^,)]+}}) +// LLVM-OGCG-SSE: define dso_local void @take_v512_no_avx512(ptr noundef byval(<16 x float>) align 64 %{{[^,)]+}}) +// LLVM-CIR-AVX: define dso_local void @take_v512_no_avx512(ptr noalias noundef byval(<16 x float>) align 64 %{{[^,)]+}}) +// LLVM-OGCG-AVX: define dso_local void @take_v512_no_avx512(ptr noundef byval(<16 x float>) align 64 %{{[^,)]+}}) +// LLVM-AVX512: define dso_local void @take_v512_no_avx512(<16 x float> noundef %{{[^,)]+}}) + +// A target attribute raises the level for one function, so these two classify +// their vector in a register at every configuration above. An ABI older than +// the rule pins them back to the module's level, as does a target that opts out +// of the per-function rule entirely. +__attribute__((target("avx"))) void take_v256_tgt(v8f v) { (void)v; } + +// CIR: cir.func {{.*}}@take_v256_tgt(%arg0: !cir.vector<8 x !cir.float>{{.*}}) +// LLVM: define dso_local void @take_v256_tgt(<8 x float> noundef %{{[^,)]+}}) +// LLVM-CIR-PINNED: define dso_local void @take_v256_tgt(ptr noalias noundef byval(<8 x float>) align 32 %{{[^,)]+}}) +// LLVM-OGCG-PINNED: define dso_local void @take_v256_tgt(ptr noundef byval(<8 x float>) align 32 %{{[^,)]+}}) + +__attribute__((target("avx512f"))) void take_v512_tgt(v16f v) { (void)v; } + +// CIR: cir.func {{.*}}@take_v512_tgt(%arg0: !cir.vector<16 x !cir.float>{{.*}}) +// LLVM: define dso_local void @take_v512_tgt(<16 x float> noundef %{{[^,)]+}}) +// LLVM-CIR-PINNED: define dso_local void @take_v512_tgt(ptr noalias noundef byval(<16 x float>) align 64 %{{[^,)]+}}) +// LLVM-OGCG-PINNED: define dso_local void @take_v512_tgt(ptr noundef byval(<16 x float>) align 64 %{{[^,)]+}}) + +// A call site has to agree with the callee it resolves to. Both sides carry +// the attribute here, which is the case classic accepts. A caller whose level +// disagrees with its callee is diagnosed by checkFunctionCallABI in classic +// CodeGen, and nothing diagnoses it here yet. +__attribute__((target("avx"))) void call_v256_tgt(v8f v) { take_v256_tgt(v); } + +// LLVM: define dso_local void @call_v256_tgt(<8 x float> noundef %{{[^,)]+}}) +// LLVM: call void @take_v256_tgt(<8 x float> noundef %{{[^,)]+}}) +// LLVM-CIR-PINNED: define dso_local void @call_v256_tgt(ptr noalias noundef byval(<8 x float>) align 32 %{{[^,)]+}}) +// LLVM-CIR-PINNED: call void @take_v256_tgt(ptr noalias noundef byval(<8 x float>) align 32 %{{[^,)]+}}) +// LLVM-OGCG-PINNED: define dso_local void @call_v256_tgt(ptr noundef byval(<8 x float>) align 32 %{{[^,)]+}}) +// LLVM-OGCG-PINNED: call void @take_v256_tgt(ptr noundef byval(<8 x float>) align 32 %{{[^,)]+}}) + +// A callee resolved at run time has no features of its own, so the level comes +// from the function containing the call. The pair differs only in the +// attribute, so it is the attribute that has to move the argument. +typedef void (*v8f_fn)(v8f); +__attribute__((target("avx"))) void call_indirect(v8f_fn p, v8f v) { p(v); } + +// LLVM: define dso_local void @call_indirect(ptr noundef %{{[^,)]+}}, <8 x float> noundef %{{[^,)]+}}) +// LLVM: call void %{{[0-9]+}}(<8 x float> noundef %{{[^,)]+}}) +// LLVM-CIR-PINNED: define dso_local void @call_indirect(ptr noundef %{{[^,)]+}}, ptr noalias noundef byval(<8 x float>) align 32 %{{[^,)]+}}) +// LLVM-OGCG-PINNED: define dso_local void @call_indirect(ptr noundef %{{[^,)]+}}, ptr noundef byval(<8 x float>) align 32 %{{[^,)]+}}) + +void call_indirect_plain(v8f_fn p, v8f v) { p(v); } + +// LLVM-CIR-SSE: define dso_local void @call_indirect_plain(ptr noundef %{{[^,)]+}}, ptr noalias noundef byval(<8 x float>) align 32 %{{[^,)]+}}) +// LLVM-CIR-SSE: call void %{{[0-9]+}}(ptr noalias noundef byval(<8 x float>) align 32 %{{[^,)]+}}) +// LLVM-OGCG-SSE: define dso_local void @call_indirect_plain(ptr noundef %{{[^,)]+}}, ptr noundef byval(<8 x float>) align 32 %{{[^,)]+}}) +// LLVM-AVX: define dso_local void @call_indirect_plain(ptr noundef %{{[^,)]+}}, <8 x float> noundef %{{[^,)]+}}) +// LLVM-AVX: call void %{{[0-9]+}}(<8 x float> noundef %{{[^,)]+}}) + +// Only a named argument can reach a register, so the struct that passes +// directly as a declared parameter goes to memory at the ellipsis. This is +// the rule that reads the declared-parameter boundary, and it is observable +// only where the level admits the vector. +typedef struct { v8f v; } StructWideVector; +int variadic(const char *f, ...); +void named_swv(StructWideVector s) { (void)s; } +void pass_swv(StructWideVector s) { variadic("x", s); } + +// LLVM-AVX: define dso_local void @named_swv(<8 x float> %{{[^,)]+}}) +// LLVM-AVX: define dso_local void @pass_swv(<8 x float> %{{[^,)]+}}) +// LLVM-CIR-AVX: call i32 (ptr, ...) @variadic(ptr noundef @.str, ptr noalias noundef byval(%struct.StructWideVector) align 32 %{{[^,)]+}}) +// LLVM-OGCG-AVX: call i32 (ptr, ...) @variadic(ptr noundef @.str, ptr noundef byval(%struct.StructWideVector) align 32 %{{[^,)]+}}) + +// A union whose widest member is a 256-bit vector is classified from that +// member, so it reaches registers once the level admits the vector. +union UnionWideVector { v8f v; float f; }; +void take_union_wide_vector(union UnionWideVector u) { (void)u; } + +// LLVM-CIR-SSE: define dso_local void @take_union_wide_vector(ptr noalias noundef byval(%union.UnionWideVector) align 32 %{{[^,)]+}}) +// LLVM-OGCG-SSE: define dso_local void @take_union_wide_vector(ptr noundef byval(%union.UnionWideVector) align 32 %{{[^,)]+}}) +// LLVM-AVX: define dso_local void @take_union_wide_vector(<4 x double> %{{[^,)]+}}) +// LLVM-AVX512: define dso_local void @take_union_wide_vector(<4 x double> %{{[^,)]+}}) diff --git a/clang/test/CIR/CodeGen/call-conv-lowering-x86_64.c b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64.c index 50c9eedfa9086..d3ba0ebb2a9b0 100644 --- a/clang/test/CIR/CodeGen/call-conv-lowering-x86_64.c +++ b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64.c @@ -264,6 +264,83 @@ _Complex long long complex_longlong(_Complex long long c) { return c; } // LLVM-CIR: define dso_local { i64, i64 } @complex_longlong(i64 %{{[^,)]+}}, i64 %{{[^,)]+}}) // LLVM-OGCG: define dso_local { i64, i64 } @complex_longlong(i64 noundef %{{[^,)]+}}, i64 noundef %{{[^,)]+}}) +// A 128-bit vector fills one xmm register and passes in its own type. +typedef float v4f __attribute__((vector_size(16))); +v4f vector128(v4f v) { return v; } + +// CIR: cir.func {{.*}}@vector128(%arg0: !cir.vector<4 x !cir.float> {{.*}}) -> !cir.vector<4 x !cir.float> +// LLVM: define dso_local <4 x float> @vector128(<4 x float> noundef %{{[^,)]+}}) + +// The classifier does not look inside a vector when picking the type for an SSE +// eightbyte, so a 64-bit vector coerces to double rather than to a two-element +// vector. +typedef float v2f __attribute__((vector_size(8))); +v2f vector64(v2f v) { return v; } + +// CIR: cir.func {{.*}}@vector64(%arg0: !cir.double {{.*}}) -> !cir.double +// LLVM: define dso_local double @vector64(double noundef %{{[^,)]+}}) + +// A vector at or below 32 bits classifies INTEGER, so it passes as the integer +// covering it rather than in an xmm register. +typedef int v1i __attribute__((vector_size(4))); +v1i vector32(v1i v) { return v; } + +// CIR: cir.func {{.*}}@vector32(%arg0: !u32i {{.*}}) -> !u32i +// LLVM: define dso_local i32 @vector32(i32 noundef %{{[^,)]+}}) + +// An eightbyte holding a 16-bit float vector coerces to double, the same as +// any other all-float eightbyte, but at 128 bits the format is preserved. +typedef _Float16 v4h __attribute__((ext_vector_type(4))); +typedef _Float16 v8h __attribute__((ext_vector_type(8))); +void take_v4h(v4h v) { (void)v; } +void take_v8h(v8h v) { (void)v; } + +// CIR: cir.func {{.*}}@take_v4h(%arg0: !cir.double{{.*}}) +// CIR: cir.func {{.*}}@take_v8h(%arg0: !cir.vector<8 x !cir.f16>{{.*}}) +// LLVM: define dso_local void @take_v4h(double noundef %{{[^,)]+}}) +// LLVM: define dso_local void @take_v8h(<8 x half> noundef %{{[^,)]+}}) + +typedef __bf16 v4bf __attribute__((ext_vector_type(4))); +typedef __bf16 v8bf __attribute__((ext_vector_type(8))); +void take_v4bf(v4bf v) { (void)v; } +void take_v8bf(v8bf v) { (void)v; } + +// CIR: cir.func {{.*}}@take_v4bf(%arg0: !cir.double{{.*}}) +// CIR: cir.func {{.*}}@take_v8bf(%arg0: !cir.vector<8 x !cir.bf16>{{.*}}) +// LLVM: define dso_local void @take_v4bf(double noundef %{{[^,)]+}}) +// LLVM: define dso_local void @take_v8bf(<8 x bfloat> noundef %{{[^,)]+}}) + +// An integer element narrower than an eightbyte does not change the rule: the +// eightbyte is what gets classified, and here it is SSE. +typedef char v8c __attribute__((ext_vector_type(8))); +void take_v8c(v8c v) { (void)v; } + +// CIR: cir.func {{.*}}@take_v8c(%arg0: !cir.double{{.*}}) +// LLVM: define dso_local void @take_v8c(double noundef %{{[^,)]+}}) + +// A vector of a 64-bit element keeps its own type, and a one-element vector of +// __int128 coerces to the pair of eightbytes it occupies. +typedef double v2d __attribute__((vector_size(16))); +void take_v2d(v2d v) { (void)v; } + +// CIR: cir.func {{.*}}@take_v2d(%arg0: !cir.vector<2 x !cir.double>{{.*}}) +// LLVM: define dso_local void @take_v2d(<2 x double> noundef %{{[^,)]+}}) + +typedef __int128 v1i128 __attribute__((vector_size(16))); +void take_v1i128(v1i128 v) { (void)v; } + +// CIR: cir.func {{.*}}@take_v1i128(%arg0: !cir.vector<2 x !u64i>{{.*}}) +// LLVM: define dso_local void @take_v1i128(<2 x i64> noundef %{{[^,)]+}}) + +// gcc passes a one-element vector of a 64-bit float in memory. Its byval +// alignment is the greater of the vector's own alignment and 8. +typedef double v1d __attribute__((vector_size(8))); +void take_v1d(v1d v) { (void)v; } + +// CIR: cir.func {{.*}}@take_v1d(%arg0: !cir.ptr<!cir.vector<1 x !cir.double>> {{.*}}llvm.align = 8 : i64{{.*}}llvm.byval = !cir.vector<1 x !cir.double>{{.*}}) +// LLVM-CIR: define dso_local void @take_v1d(ptr noalias noundef byval(<1 x double>) align 8 %{{[^,)]+}}) +// LLVM-OGCG: define dso_local void @take_v1d(ptr noundef byval(<1 x double>) align 8 %{{[^,)]+}}) + // A _Complex reaches the classifier as a record member too, not just on its // own, so these cover the field walk rather than the top-level mapping. typedef struct { _Complex float c; } WrapComplexFloat; diff --git a/clang/test/CIR/Transforms/abi-lowering/x86_64-aggregate-nyi.cir b/clang/test/CIR/Transforms/abi-lowering/x86_64-aggregate-nyi.cir index 89bc54433d586..ff7eacbe3f608 100644 --- a/clang/test/CIR/Transforms/abi-lowering/x86_64-aggregate-nyi.cir +++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-aggregate-nyi.cir @@ -112,4 +112,47 @@ module attributes { // CHECK: not yet implemented for type '!cir.struct<padded {{.*}}pad !cir.array<!cir.int<s, 8> x 2>}>' + // A scalable vector has no size the eightbyte rules can read. + cir.func @take_scalable(%arg0: !cir.vector<[4] x !cir.float>) { + cir.return + } + + // CHECK: not yet implemented for type '!cir.vector<[4] x !cir.float> + + // Clang pads a vector's type width up to a power of two and the classifier + // branches on the exact width, so a three-byte vector would be classified at + // 24 bits where clang uses 32. The ABI library's vector type cannot carry + // the padded width, so this is rejected rather than misclassified. + cir.func @take_odd_width(%arg0: !cir.vector<3 x !s8i>) { + cir.return + } + + // CHECK: not yet implemented for type '!cir.vector<3 x !cir.int<s, 8>> + + // A pointer element loses its pointee on the way back from the classifier, + // which would rewrite the signature to a vector of void pointers. No C + // vector has a pointer element, so this is rejected instead. + cir.func @take_ptr_vector(%arg0: !cir.vector<2 x !cir.ptr<!s32i>>) { + cir.return + } + + // CHECK: not yet implemented for type '!cir.vector<2 x !cir.ptr<!cir.int<s, 32>>> + + // An element narrower than a byte is rounded up to its storage container by + // clang but not by the classifier, so the two would size the vector + // differently. Classic passes this one in an xmm register from its 128-bit + // width, where the classifier would read 32 bits and coerce to an integer. + cir.func @take_sub_byte_element(%arg0: !cir.vector<16 x !cir.int<s, 2, bitint>>) { + cir.return + } + + // CHECK: not yet implemented for type '!cir.vector<16 x !cir.int<s, 2, bitint>> + + // An x87 long double is 80 bits to the classifier and 128 to clang, so the + // two would size this vector differently for the same reason. + cir.func @take_x87_element(%arg0: !cir.vector<2 x !cir.long_double<!cir.f80>>) { + cir.return + } + + // CHECK: not yet implemented for type '!cir.vector<2 x !cir.long_double<!cir.f80>> } diff --git a/clang/test/CIR/Transforms/abi-lowering/x86_64-variadic-call.cir b/clang/test/CIR/Transforms/abi-lowering/x86_64-variadic-call.cir index b8f6422cd5c8a..f0cb16463c8d5 100644 --- a/clang/test/CIR/Transforms/abi-lowering/x86_64-variadic-call.cir +++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-variadic-call.cir @@ -120,6 +120,18 @@ module attributes { // CHECK: cir.func{{.*}} @pass_all_float(%arg0: !cir.ptr<!s8i>, %arg1: !cir.vector<2 x !cir.float>) // CHECK: cir.call @variadic(%arg0, %{{.*}}) : (!cir.ptr<!s8i>, !cir.vector<2 x !cir.float>) -> !s32i + // At this level a 256-bit vector is too wide for a register whether it is + // named or not, so both the declared parameter and the ellipsis argument + // carry the same byval slot. + cir.func @pass_wide_vector(%arg0: !cir.ptr<!s8i>, %arg1: !cir.vector<8 x !cir.float>) { + %0 = cir.call @variadic(%arg0, %arg1) + : (!cir.ptr<!s8i>, !cir.vector<8 x !cir.float>) -> !s32i + cir.return + } + + // CHECK: cir.func{{.*}} @pass_wide_vector(%arg0: !cir.ptr<!s8i>, %arg1: !cir.ptr<!cir.vector<8 x !cir.float>> {llvm.align = 32 : i64, llvm.byval = !cir.vector<8 x !cir.float>, llvm.noalias, llvm.noundef}) + // CHECK: cir.call @variadic(%arg0, %{{.*}}) : (!cir.ptr<!s8i>, !cir.ptr<!cir.vector<8 x !cir.float>> {llvm.align = 32 : i64, llvm.byval = !cir.vector<8 x !cir.float>, llvm.noalias, llvm.noundef}) -> !s32i + // A declared parameter is coerced the same way whether or not the call also // passes ellipsis arguments. cir.func @pass_declared_coerced(%arg0: !rec_Pair, %arg1: !s32i) { @@ -181,44 +193,47 @@ module attributes { // LLVM: declare i32 @variadic_pair(i64, ...) // LLVM: declare void @variadic_big(ptr dead_on_unwind writable sret(%struct.Big) align 8, ptr, ...) -// LLVM: define void @pass_none(ptr %{{.+}}) -// LLVM: call i32 (ptr, ...) @variadic(ptr %{{.+}}) +// LLVM: define void @pass_none(ptr %{{[^,)]+}}) +// LLVM: call i32 (ptr, ...) @variadic(ptr %{{[^,)]+}}) + +// LLVM: define void @pass_scalar(ptr %{{[^,)]+}}, i32 %{{[^,)]+}}) +// LLVM: call i32 (ptr, ...) @variadic(ptr %{{[^,)]+}}, i32 %{{[^,)]+}}) -// LLVM: define void @pass_scalar(ptr %{{.+}}, i32 %{{.+}}) -// LLVM: call i32 (ptr, ...) @variadic(ptr %{{.+}}, i32 %{{.+}}) +// LLVM: define void @pass_narrow(ptr %{{[^,)]+}}, i8 signext %{{[^,)]+}}) +// LLVM: call i32 (ptr, ...) @variadic(ptr %{{[^,)]+}}, i8 signext %{{[^,)]+}}) -// LLVM: define void @pass_narrow(ptr %{{.+}}, i8 signext %{{.+}}) -// LLVM: call i32 (ptr, ...) @variadic(ptr %{{.+}}, i8 signext %{{.+}}) +// LLVM: define void @pass_double(ptr %{{[^,)]+}}, double %{{[^,)]+}}) +// LLVM: call i32 (ptr, ...) @variadic(ptr %{{[^,)]+}}, double %{{[^,)]+}}) -// LLVM: define void @pass_double(ptr %{{.+}}, double %{{.+}}) -// LLVM: call i32 (ptr, ...) @variadic(ptr %{{.+}}, double %{{.+}}) +// LLVM: define void @pass_two_early(ptr %{{[^,)]+}}, i64 %{{[^,)]+}}, i64 %{{[^,)]+}}) +// LLVM: call i32 (ptr, ...) @variadic(ptr %{{[^,)]+}}, i64 %{{[^,)]+}}, i64 %{{[^,)]+}}) -// LLVM: define void @pass_two_early(ptr %{{.+}}, i64 %{{.+}}, i64 %{{.+}}) -// LLVM: call i32 (ptr, ...) @variadic(ptr %{{.+}}, i64 %{{.+}}, i64 %{{.+}}) +// LLVM: define void @pass_two_exhausted(ptr %{{[^,)]+}}, i32 %{{[^,)]+}}, i64 %{{[^,)]+}}, i64 %{{[^,)]+}}) +// LLVM: call i32 (ptr, ...) @variadic(ptr %{{[^,)]+}}, i32 %{{[^,)]+}}, i32 %{{[^,)]+}}, i32 %{{[^,)]+}}, i32 %{{[^,)]+}}, i32 %{{[^,)]+}}, ptr noalias noundef byval(%struct.Two) align 8 %{{[^,)]+}}) -// LLVM: define void @pass_two_exhausted(ptr %{{.+}}, i32 %{{.+}}, i64 %{{.+}}, i64 %{{.+}}) -// LLVM: call i32 (ptr, ...) @variadic(ptr %{{.+}}, i32 %{{.+}}, i32 %{{.+}}, i32 %{{.+}}, i32 %{{.+}}, i32 %{{.+}}, ptr noalias noundef byval(%struct.Two) align 8 %{{.+}}) +// LLVM: define void @pass_big(ptr %{{[^,)]+}}, ptr noalias noundef byval(%struct.Big) align 8 %{{[^,)]+}}) +// LLVM: call i32 (ptr, ...) @variadic(ptr %{{[^,)]+}}, ptr noalias noundef byval(%struct.Big) align 8 %{{[^,)]+}}) -// LLVM: define void @pass_big(ptr %{{.+}}, ptr noalias noundef byval(%struct.Big) align 8 %{{.+}}) -// LLVM: call i32 (ptr, ...) @variadic(ptr %{{.+}}, ptr noalias noundef byval(%struct.Big) align 8 %{{.+}}) +// LLVM: define void @pass_empty(ptr %{{[^,)]+}}, i32 %{{[^,)]+}}) +// LLVM: call i32 (ptr, ...) @variadic(ptr %{{[^,)]+}}, i32 %{{[^,)]+}}) -// LLVM: define void @pass_empty(ptr %{{.+}}, i32 %{{.+}}) -// LLVM: call i32 (ptr, ...) @variadic(ptr %{{.+}}, i32 %{{.+}}) +// LLVM: define void @pass_all_float(ptr %{{[^,)]+}}, <2 x float> %{{[^,)]+}}) +// LLVM: call i32 (ptr, ...) @variadic(ptr %{{[^,)]+}}, <2 x float> %{{[^,)]+}}) -// LLVM: define void @pass_all_float(ptr %{{.+}}, <2 x float> %{{.+}}) -// LLVM: call i32 (ptr, ...) @variadic(ptr %{{.+}}, <2 x float> %{{.+}}) +// LLVM: define void @pass_wide_vector(ptr %{{[^,)]+}}, ptr noalias noundef byval(<8 x float>) align 32 %{{[^,)]+}}) +// LLVM: call i32 (ptr, ...) @variadic(ptr %{{[^,)]+}}, ptr noalias noundef byval(<8 x float>) align 32 %{{[^,)]+}}) -// LLVM: define void @pass_declared_coerced(i64 %{{.+}}, i32 %{{.+}}) -// LLVM: call i32 (i64, ...) @variadic_pair(i64 %{{.+}}, i32 %{{.+}}) +// LLVM: define void @pass_declared_coerced(i64 %{{[^,)]+}}, i32 %{{[^,)]+}}) +// LLVM: call i32 (i64, ...) @variadic_pair(i64 %{{[^,)]+}}, i32 %{{[^,)]+}}) -// LLVM: define void @pass_sret_return(ptr dead_on_unwind noalias writable sret(%struct.Big) align 8 %{{.+}}, ptr %{{.+}}, i32 %{{.+}}) -// LLVM: call void (ptr, ptr, ...) @variadic_big(ptr dead_on_unwind writable sret(%struct.Big) align 8 %{{.+}}, ptr %{{.+}}, i32 %{{.+}}) +// LLVM: define void @pass_sret_return(ptr dead_on_unwind noalias writable sret(%struct.Big) align 8 %{{[^,)]+}}, ptr %{{[^,)]+}}, i32 %{{[^,)]+}}) +// LLVM: call void (ptr, ptr, ...) @variadic_big(ptr dead_on_unwind writable sret(%struct.Big) align 8 %{{[^,)]+}}, ptr %{{[^,)]+}}, i32 %{{[^,)]+}}) -// LLVM: define void @indirect_scalar(ptr %{{.+}}, ptr %{{.+}}, i32 %{{.+}}) -// LLVM: call i32 (ptr, ...) %{{.+}}(ptr %{{.+}}, i32 %{{.+}}) +// LLVM: define void @indirect_scalar(ptr %{{[^,)]+}}, ptr %{{[^,)]+}}, i32 %{{[^,)]+}}) +// LLVM: call i32 (ptr, ...) %{{[^,)]+}}(ptr %{{[^,)]+}}, i32 %{{[^,)]+}}) -// LLVM: define void @indirect_void(ptr %{{.+}}, ptr %{{.+}}, i32 %{{.+}}) -// LLVM: call void (ptr, ...) %{{.+}}(ptr %{{.+}}, i32 %{{.+}}) +// LLVM: define void @indirect_void(ptr %{{[^,)]+}}, ptr %{{[^,)]+}}, i32 %{{[^,)]+}}) +// LLVM: call void (ptr, ...) %{{[^,)]+}}(ptr %{{[^,)]+}}, i32 %{{[^,)]+}}) -// LLVM: define void @indirect_coerced(ptr %{{.+}}, i64 %{{.+}}) -// LLVM: call i32 (i64, ...) %{{.+}}(i64 %{{.+}}) +// LLVM: define void @indirect_coerced(ptr %{{[^,)]+}}, i64 %{{[^,)]+}}) +// LLVM: call i32 (i64, ...) %{{[^,)]+}}(i64 %{{[^,)]+}}) diff --git a/clang/test/CIR/Transforms/abi-lowering/x86_64-vector.cir b/clang/test/CIR/Transforms/abi-lowering/x86_64-vector.cir index 1c8d705f6d9ac..b1aa989585e10 100644 --- a/clang/test/CIR/Transforms/abi-lowering/x86_64-vector.cir +++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-vector.cir @@ -1,8 +1,13 @@ // RUN: cir-opt %s -cir-call-conv-lowering=target=x86_64 | FileCheck %s +// RUN: cir-opt %s -cir-call-conv-lowering='target=x86_64 x86-avx-abi-level=avx' \ +// RUN: | FileCheck %s --check-prefix=AVX +// RUN: cir-opt %s -cir-call-conv-lowering='target=x86_64 x86-avx-abi-level=avx512' \ +// RUN: | FileCheck %s --check-prefix=AVX512 // RUN: cir-opt %s -cir-call-conv-lowering=target=x86_64 -cir-to-llvm -o - 2>/dev/null \ // RUN: | mlir-translate -mlir-to-llvmir --allow-unregistered-dialect \ // RUN: | FileCheck %s --check-prefix=LLVM +!s32i = !cir.int<s, 32> !rec_FF = !cir.struct<"FF" {data !cir.float, data !cir.float}> !rec_F3 = !cir.struct<"F3" {data !cir.float, data !cir.float, data !cir.float}> !rec_UFloats = !cir.union<"UFloats" {data !cir.array<!cir.float x 2>, data !cir.array<!cir.float x 2>}> @@ -15,6 +20,52 @@ module attributes { #dlti.dl_entry<f64, dense<64>: vector<2xi64>>> } { + // A 128-bit vector fills one xmm register and passes in its own type, so no + // coercion is inserted. + cir.func @v128(%arg0: !cir.vector<4 x !cir.float>) -> !cir.vector<4 x !cir.float> { + cir.return %arg0 : !cir.vector<4 x !cir.float> + } + + // CHECK: cir.func{{.*}} @v128(%arg0: !cir.vector<4 x !cir.float>) -> !cir.vector<4 x !cir.float> + // CHECK-NEXT: cir.return %arg0 + + // The classifier does not look inside a vector when picking the type for an + // SSE eightbyte, so it falls back to double rather than to the two-element + // vector that take_ff below gets for the same eightbyte. + cir.func @v64(%arg0: !cir.vector<2 x !cir.float>) -> !cir.vector<2 x !cir.float> { + cir.return %arg0 : !cir.vector<2 x !cir.float> + } + + // CHECK: cir.func{{.*}} @v64(%arg0: !cir.double) -> !cir.double + + // A 32-bit vector is passed as an integer, matching what gcc does. + cir.func @v32(%arg0: !cir.vector<1 x !s32i>) -> !cir.vector<1 x !s32i> { + cir.return %arg0 : !cir.vector<1 x !s32i> + } + + // CHECK: cir.func{{.*}} @v32(%arg0: !u32i) -> !u32i + + // Without AVX a 256-bit vector exceeds the native vector size and goes to + // memory, and with AVX it stays in a register. The byval alignment is the + // greater of the vector's own alignment and 8. + cir.func @v256(%arg0: !cir.vector<8 x !cir.float>) { + cir.return + } + + // CHECK: cir.func{{.*}} @v256(%arg0: !cir.ptr<!cir.vector<8 x !cir.float>> {{.*}}llvm.align = 32 : i64{{.*}}llvm.byval = !cir.vector<8 x !cir.float>{{.*}}) + // AVX: cir.func{{.*}} @v256(%arg0: !cir.vector<8 x !cir.float>) + // AVX512: cir.func{{.*}} @v256(%arg0: !cir.vector<8 x !cir.float>) + + // A 512-bit vector needs the top level before it reaches a register, so this + // is the case that distinguishes avx from avx512. + cir.func @v512(%arg0: !cir.vector<16 x !cir.float>) { + cir.return + } + + // CHECK: cir.func{{.*}} @v512(%arg0: !cir.ptr<!cir.vector<16 x !cir.float>> {{.*}}llvm.align = 64 : i64{{.*}}llvm.byval = !cir.vector<16 x !cir.float>{{.*}}) + // AVX: cir.func{{.*}} @v512(%arg0: !cir.ptr<!cir.vector<16 x !cir.float>> {{.*}}llvm.align = 64 : i64{{.*}}llvm.byval = !cir.vector<16 x !cir.float>{{.*}}) + // AVX512: cir.func{{.*}} @v512(%arg0: !cir.vector<16 x !cir.float>) + // Two floats in one eightbyte classify SSE, and the coercion type the // classifier picks for that eightbyte is a vector. cir.func @take_ff(%arg0: !rec_FF) { @@ -46,17 +97,29 @@ module attributes { // CHECK: cir.func{{.*}} @take_f3(%arg0: !cir.vector<2 x !cir.float>, %arg1: !cir.float) - // A union whose highest-aligned member is an all-float array reaches the same - // vector coercion through the union path. + // A union whose widest member is an all-float array reaches the same vector + // coercion through the union path. cir.func @take_union_floats(%arg0: !rec_UFloats) { cir.return } // CHECK: cir.func{{.*}} @take_union_floats(%arg0: !cir.vector<2 x !cir.float>) + + cir.func @call_v128(%arg0: !cir.vector<4 x !cir.float>) -> !cir.vector<4 x !cir.float> { + %0 = cir.call @v128(%arg0) : (!cir.vector<4 x !cir.float>) -> !cir.vector<4 x !cir.float> + cir.return %0 : !cir.vector<4 x !cir.float> + } + + // CHECK: cir.func{{.*}} @call_v128(%arg0: !cir.vector<4 x !cir.float>) -> !cir.vector<4 x !cir.float> + // CHECK: cir.call @v128(%arg0) : (!cir.vector<4 x !cir.float>) -> !cir.vector<4 x !cir.float> } -// LLVM: define void @take_ff(<2 x float> %{{.+}}) +// LLVM: define <4 x float> @v128(<4 x float> %{{[^,)]+}}) +// LLVM: define double @v64(double %{{[^,)]+}}) +// LLVM: define i32 @v32(i32 %{{[^,)]+}}) +// LLVM: define void @v256(ptr noalias noundef byval(<8 x float>) align 32 %{{[^,)]+}}) +// LLVM: define void @take_ff(<2 x float> %{{[^,)]+}}) // LLVM: define <2 x float> @ret_ff() -// LLVM: define void @take_farr(<2 x float> %{{.+}}) -// LLVM: define void @take_f3(<2 x float> %{{.+}}, float %{{.+}}) -// LLVM: define void @take_union_floats(<2 x float> %{{.+}}) +// LLVM: define void @take_farr(<2 x float> %{{[^,)]+}}) +// LLVM: define void @take_f3(<2 x float> %{{[^,)]+}}, float %{{[^,)]+}}) +// LLVM: define void @take_union_floats(<2 x float> %{{[^,)]+}}) >From 5d8843194e589a08e1712a53bceed31171df0509 Mon Sep 17 00:00:00 2001 From: Adam Smith <[email protected]> Date: Wed, 12 Aug 2026 13:44:14 -0700 Subject: [PATCH 2/3] [CIR] Drop the call-conv lowering flag from the AVX test The AVX test this branch adds asks for the pass with `-clangir-enable-call-conv-lowering`, which no longer exists now that this branch has picked up #215026 and #215117. The pass runs by default, so the RUN lines do not need a flag at all and get the same lowering without one. Assisted-by: Cursor / claude-opus-5 --- .../CIR/CodeGen/call-conv-lowering-x86_64-avx.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-avx.c b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-avx.c index 0c733c34f4d9d..d632744e9c647 100644 --- a/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-avx.c +++ b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-avx.c @@ -1,28 +1,28 @@ -// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -clangir-enable-call-conv-lowering -emit-cir %s -o %t.cir +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-cir %s -o %t.cir // RUN: FileCheck --check-prefixes=CIR,CIR-SSE --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: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-llvm %s -o %t-cir.ll // RUN: FileCheck --check-prefixes=LLVM,LLVM-CIR-SSE --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-SSE --input-file=%t.ll %s -// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -target-feature +avx -fclangir -clangir-enable-call-conv-lowering -emit-cir %s -o %t-avx.cir +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -target-feature +avx -fclangir -emit-cir %s -o %t-avx.cir // RUN: FileCheck --check-prefixes=CIR,CIR-AVX --input-file=%t-avx.cir %s -// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -target-feature +avx -fclangir -clangir-enable-call-conv-lowering -emit-llvm %s -o %t-avx-cir.ll +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -target-feature +avx -fclangir -emit-llvm %s -o %t-avx-cir.ll // RUN: FileCheck --check-prefixes=LLVM,LLVM-AVX,LLVM-CIR-AVX --input-file=%t-avx-cir.ll %s // RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -target-feature +avx -emit-llvm %s -o %t-avx.ll // RUN: FileCheck --check-prefixes=LLVM,LLVM-AVX,LLVM-OGCG-AVX --input-file=%t-avx.ll %s -// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -target-feature +avx512f -fclangir -clangir-enable-call-conv-lowering -emit-llvm %s -o %t-avx512-cir.ll +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -target-feature +avx512f -fclangir -emit-llvm %s -o %t-avx512-cir.ll // RUN: FileCheck --check-prefixes=LLVM,LLVM-AVX512 --input-file=%t-avx512-cir.ll %s // RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -target-feature +avx512f -emit-llvm %s -o %t-avx512.ll // RUN: FileCheck --check-prefixes=LLVM,LLVM-AVX512 --input-file=%t-avx512.ll %s -// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclang-abi-compat=23 -fclangir -clangir-enable-call-conv-lowering -emit-llvm %s -o %t-compat23-cir.ll +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclang-abi-compat=23 -fclangir -emit-llvm %s -o %t-compat23-cir.ll // RUN: FileCheck --check-prefix=LLVM-CIR-PINNED --input-file=%t-compat23-cir.ll %s // RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclang-abi-compat=23 -emit-llvm %s -o %t-compat23.ll // RUN: FileCheck --check-prefix=LLVM-OGCG-PINNED --input-file=%t-compat23.ll %s -// RUN: %clang_cc1 -triple x86_64-scei-ps4 -fclangir -clangir-enable-call-conv-lowering -emit-llvm %s -o %t-ps4-cir.ll +// RUN: %clang_cc1 -triple x86_64-scei-ps4 -fclangir -emit-llvm %s -o %t-ps4-cir.ll // RUN: FileCheck --check-prefix=LLVM-CIR-PINNED --input-file=%t-ps4-cir.ll %s // RUN: %clang_cc1 -triple x86_64-scei-ps4 -emit-llvm %s -o %t-ps4.ll // RUN: FileCheck --check-prefix=LLVM-OGCG-PINNED --input-file=%t-ps4.ll %s >From 394b3b5dc71903ea63fd4a07dce6c5a5f49240a2 Mon Sep 17 00:00:00 2001 From: Adam Smith <[email protected]> Date: Wed, 12 Aug 2026 15:14:54 -0700 Subject: [PATCH 3/3] [CIR] Search the feature list once and assert on multiversioning Review feedback on #215118. funcAvxLevel walked the whole feature list once per name and was called twice, so it now makes a single pass. It also has no equivalent of the `hasAttr<TargetAttr>()` gate classic uses. A multiversioned function's feature list carries the same `+avx` entry a plain `target` attribute produces, so this would take a raised level where classic keeps the module's. CIRGen rejects multiversioning today, so an assert on opFuncMultiVersioning marks the gate that has to go in when that changes. Comments naming a function or file in classic CodeGen came out. They go stale when that code moves, and one already had, claiming a declaration carries no features. x86TargetAttrAvx is now allowsX86TargetAttrAvx, after the function that produces it. Assisted-by: Cursor / claude-opus-5 --- clang/include/clang/CIR/Dialect/Passes.h | 2 +- clang/include/clang/CIR/Dialect/Passes.td | 2 +- .../Transforms/CallConvLoweringPass.cpp | 44 +++++++++---------- clang/lib/CIR/Lowering/CIRPasses.cpp | 14 +++--- 4 files changed, 28 insertions(+), 34 deletions(-) diff --git a/clang/include/clang/CIR/Dialect/Passes.h b/clang/include/clang/CIR/Dialect/Passes.h index 6c8bec15c3838..674f2180c27ab 100644 --- a/clang/include/clang/CIR/Dialect/Passes.h +++ b/clang/include/clang/CIR/Dialect/Passes.h @@ -38,7 +38,7 @@ std::unique_ptr<Pass> createTargetLoweringPass(); std::unique_ptr<Pass> createCallConvLoweringPass(); std::unique_ptr<Pass> createCallConvLoweringPass( cir::CallConvTarget target, llvm::abi::X86AVXABILevel x86AvxAbiLevel, - bool x86TargetAttrAvx, const llvm::abi::ABICompatInfo &x86AbiCompat); + bool allowsX86TargetAttrAvx, const llvm::abi::ABICompatInfo &x86AbiCompat); std::unique_ptr<Pass> createHoistAllocasPass(); std::unique_ptr<Pass> createLoweringPreparePass(); std::unique_ptr<Pass> createLoweringPreparePass(clang::ASTContext *astCtx); diff --git a/clang/include/clang/CIR/Dialect/Passes.td b/clang/include/clang/CIR/Dialect/Passes.td index bc1b99da01440..b983cfe59112a 100644 --- a/clang/include/clang/CIR/Dialect/Passes.td +++ b/clang/include/clang/CIR/Dialect/Passes.td @@ -260,7 +260,7 @@ def CallConvLowering : Pass<"cir-call-conv-lowering", "mlir::ModuleOp"> { clEnumValN(llvm::abi::X86AVXABILevel::AVX, "avx", "AVX"), clEnumValN(llvm::abi::X86AVXABILevel::AVX512, "avx512", "AVX512") )}]>, - Option<"x86TargetAttrAvx", "x86-target-attr-avx", "bool", + Option<"allowsX86TargetAttrAvx", "x86-target-attr-avx", "bool", /*default=*/"true", "Let a function's own target features raise its AVX ABI level " "above x86-avx-abi-level">, diff --git a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp index 5f25e827b9cd2..6fcd149f61cd4 100644 --- a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp +++ b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp @@ -44,6 +44,7 @@ #include "mlir/Pass/Pass.h" #include "clang/CIR/Dialect/IR/CIRDialect.h" #include "clang/CIR/Dialect/Passes.h" +#include "clang/CIR/MissingFeatures.h" #include "llvm/ABI/FunctionInfo.h" #include "llvm/ABI/TargetInfo.h" #include "llvm/ABI/Types.h" @@ -507,32 +508,29 @@ static std::optional<FunctionClassification> classifyX86_64Signature( return fc; } -/// The AVX level in force at \p func, following getEffectiveX86AVXABILevel in -/// clang/lib/CodeGen/Targets/X86.cpp. Classic reads the target attribute -/// itself, where this reads the feature list CIRGen recorded from it. Two -/// consequences: a declaration gets \p base, CIRGen recording features only on -/// definitions, and target_clones and cpu_specific will need excluding once -/// multiversioning is implemented, classic leaving those at the module level. +/// The AVX level to classify \p func at: \p base, raised if the function's own +/// recorded feature list enables a wider vector. static llvm::abi::X86AVXABILevel funcAvxLevel(cir::FuncOp func, llvm::abi::X86AVXABILevel base) { + // Only a `target` attribute may raise the level. A multiversioned function + // carries a raised feature list too, and must stay at the module's level. + assert(!cir::MissingFeatures::opFuncMultiVersioning()); + auto features = func->getAttrOfType<mlir::StringAttr>("cir.target-features"); if (!features) return base; // A '-' entry disables the feature, so match a whole '+' entry rather than - // searching for the name. - auto enabled = [&](llvm::StringRef name) { - for (llvm::StringRef feature : llvm::split(features.getValue(), ',')) - if (feature.consume_front("+") && feature == name) - return true; - return false; - }; - // avx512f implies avx, so both entries are present. Test the wider name - // first or the AVX512 branch is unreachable. - if (enabled("avx512f")) - return std::max(base, llvm::abi::X86AVXABILevel::AVX512); - if (enabled("avx")) - return std::max(base, llvm::abi::X86AVXABILevel::AVX); - return base; + // searching for the name. avx512f implies avx, so both entries are present: + // return on the wider name so one pass suffices. + bool avx = false; + for (llvm::StringRef feature : llvm::split(features.getValue(), ',')) { + if (!feature.consume_front("+")) + continue; + if (feature == "avx512f") + return std::max(base, llvm::abi::X86AVXABILevel::AVX512); + avx |= feature == "avx"; + } + return avx ? std::max(base, llvm::abi::X86AVXABILevel::AVX) : base; } /// Classify a cir.func for x86_64 SysV using the LLVM ABI library. Returns @@ -727,7 +725,7 @@ void CallConvLoweringPass::runOnOperation() { }; llvm::abi::X86AVXABILevel baseAvxLevel = x86AvxAbiLevel.getValue(); auto avxLevelFor = [&](cir::FuncOp func) -> llvm::abi::X86AVXABILevel { - if (!x86TargetAttrAvx || !func) + if (!allowsX86TargetAttrAvx || !func) return baseAvxLevel; return funcAvxLevel(func, baseAvxLevel); }; @@ -957,10 +955,10 @@ std::unique_ptr<Pass> mlir::createCallConvLoweringPass() { std::unique_ptr<Pass> mlir::createCallConvLoweringPass( cir::CallConvTarget target, llvm::abi::X86AVXABILevel x86AvxAbiLevel, - bool x86TargetAttrAvx, const llvm::abi::ABICompatInfo &x86AbiCompat) { + bool allowsX86TargetAttrAvx, const llvm::abi::ABICompatInfo &x86AbiCompat) { CallConvLoweringOptions options; options.target = target; options.x86AvxAbiLevel = x86AvxAbiLevel; - options.x86TargetAttrAvx = x86TargetAttrAvx; + options.allowsX86TargetAttrAvx = allowsX86TargetAttrAvx; return std::make_unique<CallConvLoweringPass>(options, x86AbiCompat); } diff --git a/clang/lib/CIR/Lowering/CIRPasses.cpp b/clang/lib/CIR/Lowering/CIRPasses.cpp index 972dc6d1a4a8b..1d1fdaf42aaa4 100644 --- a/clang/lib/CIR/Lowering/CIRPasses.cpp +++ b/clang/lib/CIR/Lowering/CIRPasses.cpp @@ -29,8 +29,8 @@ static CallConvTarget getCallConvTarget(const llvm::Triple &triple) { return CallConvTarget::None; } -/// The AVX level the classifier uses to size a native vector. Read from the -/// target ABI name, as CodeGenModule does. +/// The AVX level the classifier uses to size a native vector, read from the +/// target ABI name. static llvm::abi::X86AVXABILevel getX86AVXABILevel(llvm::StringRef abi) { if (abi == "avx512") return llvm::abi::X86AVXABILevel::AVX512; @@ -40,9 +40,8 @@ static llvm::abi::X86AVXABILevel getX86AVXABILevel(llvm::StringRef abi) { } /// Whether `__attribute__((target(...)))` on a function may raise its AVX ABI -/// level above the command line's. Mirrors getEffectiveX86AVXABILevel in -/// clang/lib/CodeGen/Targets/X86.cpp, which keeps a target that opts out, and -/// any ABI older than the rule, at the module level. +/// level above the command line's. A target that opts out, and any ABI older +/// than the rule, stay at the module level. static bool allowsX86TargetAttrAvx(const clang::ASTContext &astContext) { return !astContext.getTargetInfo().getTriple().isPS() && astContext.getLangOpts().getClangABICompat() > @@ -53,10 +52,7 @@ static bool allowsX86TargetAttrAvx(const clang::ASTContext &astContext) { /// requested compatibility version. Every flag defaults to true in the ABI /// library, which is not what any target computes: Clang11Compat is false for a /// modern Linux target, so leaving it at the default classifies a union larger -/// than an eightbyte as though every member spanned its size. Mirrors the -/// predicates in clang/lib/CodeGen/Targets/X86.cpp and the derivation in -/// CodeGenModule::getLLVMABITargetInfo, which computes the same five flags for -/// the classic path. +/// than an eightbyte as though every member spanned its size. static llvm::abi::ABICompatInfo getX86ABICompatInfo(const clang::ASTContext &astContext) { const llvm::Triple &triple = astContext.getTargetInfo().getTriple(); _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
