[clang] [CIR] Widen memory effects for ABI-introduced argument memory (PR #221067)
Adam Smith via cfe-commits
cfe-commits at lists.llvm.org
Fri Sep 11 12:59:06 PDT 2026
https://github.com/adams381 updated https://github.com/llvm/llvm-project/pull/221067
>From 708017d2380314bb8bfec88e0fd2aac2a865bc2f Mon Sep 17 00:00:00 2001
From: Adam Smith <adams at nvidia.com>
Date: Thu, 3 Sep 2026 15:13:17 -0700
Subject: [PATCH] [CIR] Widen memory effects for ABI-introduced argument memory
Both memory-effect sites in LowerToLLVM now check `arg_attrs` and add
argument memory, as classic does in `AddPotentialArgAccess`. Variadic
definitions widen too, since their parameters do not cover every
argument. CallConvLowering also carries `side_effect` across when it
rebuilds a call.
Assisted-by: Cursor / claude-opus-5
---
.../TargetLowering/CIRABIRewriteContext.cpp | 23 ++-
.../CIR/Lowering/DirectToLLVM/LowerToLLVM.cpp | 118 +++++++--------
clang/test/CIR/CodeGen/side-effect.cpp | 139 +++++++++++++++---
.../abi-lowering/side-effect-argmem.cir | 132 +++++++++++++++++
4 files changed, 326 insertions(+), 86 deletions(-)
create mode 100644 clang/test/CIR/Transforms/abi-lowering/side-effect-argmem.cir
diff --git a/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp b/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
index 918f3f17b9933..4e8a04cb8ffbf 100644
--- a/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
@@ -811,7 +811,8 @@ SmallVector<mlir::NamedAttribute> buildSretSlotAttrs(mlir::OpBuilder &builder,
// it, and once the CIR `!cir.ptr<retTy>` lowers to an opaque LLVM `ptr` the
// pointee type can no longer be recovered from the pointer.
attrs.push_back(
- builder.getNamedAttr("llvm.sret", mlir::TypeAttr::get(retTy)));
+ builder.getNamedAttr(mlir::LLVM::LLVMDialect::getStructRetAttrName(),
+ mlir::TypeAttr::get(retTy)));
attrs.push_back(
builder.getNamedAttr("llvm.align", builder.getI64IntegerAttr(align)));
if (withNoalias)
@@ -823,6 +824,18 @@ SmallVector<mlir::NamedAttribute> buildSretSlotAttrs(mlir::OpBuilder &builder,
return attrs;
}
+/// Carry \p call's attributes over to the rewritten \p newCall, leaving
+/// attributes already on \p newCall, such as its callee, alone.
+///
+/// side_effect is set explicitly because it is a DefaultValuedAttr: a fresh
+/// call already carries `all`, so the copy sees it as present and skips it.
+void carryCallAttrs(cir::CallOp call, cir::CallOp newCall) {
+ for (mlir::NamedAttribute attr : call->getAttrs())
+ if (!newCall->hasAttr(attr.getName()))
+ newCall->setAttr(attr.getName(), attr.getValue());
+ newCall.setSideEffect(call.getSideEffect());
+}
+
/// Prepend the sret slot's attrs at position 0 of newCall's arg_attrs.
/// Called after the call has been rewritten with the sret pointer at
/// operand 0, so the operand count now includes the sret slot. \p argAttrs
@@ -937,9 +950,7 @@ void rewriteIndirectReturnCall(cir::CallOp call,
prependIndirectCallee(call, sretArgs, sretVoidTy, builder);
auto newCall = cir::CallOp::create(
builder, call.getLoc(), call.getCalleeAttr(), sretVoidTy, sretArgs);
- for (mlir::NamedAttribute attr : call->getAttrs())
- if (!newCall->hasAttr(attr.getName()))
- newCall->setAttr(attr.getName(), attr.getValue());
+ carryCallAttrs(call, newCall);
// Shape the per-argument attrs exactly as the non-sret path does
// (signext / zeroext for Extend, drop Ignore slots, byval / align for
@@ -1300,9 +1311,7 @@ CIRABIRewriteContext::rewriteCallSite(mlir::Operation *callOp,
prependIndirectCallee(call, newArgs, callRetTy, builder);
auto newCall = cir::CallOp::create(builder, call.getLoc(),
call.getCalleeAttr(), callRetTy, newArgs);
- for (mlir::NamedAttribute attr : call->getAttrs())
- if (!newCall->hasAttr(attr.getName()))
- newCall->setAttr(attr.getName(), attr.getValue());
+ carryCallAttrs(call, newCall);
// Direct return with coercion: the new call returns the coerced type;
// emit a coercion back to the original type for the call's existing uses.
diff --git a/clang/lib/CIR/Lowering/DirectToLLVM/LowerToLLVM.cpp b/clang/lib/CIR/Lowering/DirectToLLVM/LowerToLLVM.cpp
index 233e6aaa15adb..2cc8bc31a8176 100644
--- a/clang/lib/CIR/Lowering/DirectToLLVM/LowerToLLVM.cpp
+++ b/clang/lib/CIR/Lowering/DirectToLLVM/LowerToLLVM.cpp
@@ -410,45 +410,66 @@ mlir::Value lowerCirAttrAsValue(mlir::Operation *parentOp,
return value;
}
-void convertSideEffectForCall(mlir::Operation *callOp, bool isNothrow,
- cir::SideEffect sideEffect,
- mlir::LLVM::MemoryEffectsAttr &memoryEffect,
- bool &noUnwind, bool &willReturn,
- bool &noReturn) {
+/// Whether the ABI hands the callee memory through a pointer argument: an
+/// sret slot for an indirect return, or byval / byref for an indirect
+/// argument. Such a function reaches argument memory no matter what its
+/// source-level attributes say, so `const` and `pure` cannot lower to a
+/// memory effect that excludes it.
+static bool hasABIIndirectMemoryArg(mlir::ArrayAttr argAttrs) {
+ if (!argAttrs)
+ return false;
+ return llvm::any_of(argAttrs, [](mlir::Attribute a) {
+ auto dict = mlir::cast<mlir::DictionaryAttr>(a);
+ return dict.contains(mlir::LLVM::LLVMDialect::getStructRetAttrName()) ||
+ dict.contains(mlir::LLVM::LLVMDialect::getByValAttrName()) ||
+ dict.contains(mlir::LLVM::LLVMDialect::getByRefAttrName());
+ });
+}
+
+/// Lower a CIR `side_effect` to an LLVM memory effect, widening argument
+/// memory to ModRef when \p accessesArgMemory, since a widened slot may be
+/// either written or read. A null result means unknown effects, which is
+/// how `All` is represented.
+static mlir::LLVM::MemoryEffectsAttr
+buildMemoryEffects(mlir::MLIRContext *ctx, cir::SideEffect sideEffect,
+ bool accessesArgMemory) {
using mlir::LLVM::ModRefInfo;
+ ModRefInfo other;
switch (sideEffect) {
case cir::SideEffect::All:
- memoryEffect = {};
- noUnwind = isNothrow;
- willReturn = false;
- break;
-
+ return {};
case cir::SideEffect::Pure:
- memoryEffect = mlir::LLVM::MemoryEffectsAttr::get(
- callOp->getContext(), /*other=*/ModRefInfo::Ref,
- /*argMem=*/ModRefInfo::Ref,
- /*inaccessibleMem=*/ModRefInfo::Ref,
- /*errnoMem=*/ModRefInfo::Ref,
- /*targetMem0=*/ModRefInfo::Ref,
- /*targetMem1=*/ModRefInfo::Ref);
- noUnwind = true;
- willReturn = true;
+ other = ModRefInfo::Ref;
break;
-
case cir::SideEffect::Const:
- memoryEffect = mlir::LLVM::MemoryEffectsAttr::get(
- callOp->getContext(), /*other=*/ModRefInfo::NoModRef,
- /*argMem=*/ModRefInfo::NoModRef,
- /*inaccessibleMem=*/ModRefInfo::NoModRef,
- /*errnoMem=*/ModRefInfo::NoModRef,
- /*targetMem0=*/ModRefInfo::NoModRef,
- /*targetMem1=*/ModRefInfo::NoModRef);
- noUnwind = true;
- willReturn = true;
+ other = ModRefInfo::NoModRef;
break;
}
+ ModRefInfo argMem = accessesArgMemory ? ModRefInfo::ModRef : other;
+ return mlir::LLVM::MemoryEffectsAttr::get(ctx, /*other=*/other,
+ /*argMem=*/argMem,
+ /*inaccessibleMem=*/other,
+ /*errnoMem=*/other,
+ /*targetMem0=*/other,
+ /*targetMem1=*/other);
+}
+
+void convertSideEffectForCall(mlir::Operation *callOp, bool isNothrow,
+ cir::SideEffect sideEffect,
+ mlir::LLVM::MemoryEffectsAttr &memoryEffect,
+ bool &noUnwind, bool &willReturn,
+ bool &noReturn) {
+ bool accessesArgMemory =
+ hasABIIndirectMemoryArg(callOp->getAttrOfType<mlir::ArrayAttr>(
+ CIRDialect::getArgAttrsAttrName()));
+ memoryEffect =
+ buildMemoryEffects(callOp->getContext(), sideEffect, accessesArgMemory);
+
+ bool isConstOrPure = sideEffect != cir::SideEffect::All;
+ noUnwind = isConstOrPure || isNothrow;
+ willReturn = isConstOrPure;
noReturn = callOp->hasAttr(CIRDialect::getNoReturnAttrName());
}
@@ -2752,35 +2773,16 @@ mlir::LogicalResult CIRToLLVMFuncOpLowering::matchAndRewrite(
assert(!cir::MissingFeatures::opFuncMultipleReturnVals());
- if (std::optional<cir::SideEffect> sideEffectKind = op.getSideEffect()) {
- switch (*sideEffectKind) {
- case cir::SideEffect::All:
- break;
- case cir::SideEffect::Pure:
- fn.setMemoryEffectsAttr(mlir::LLVM::MemoryEffectsAttr::get(
- fn.getContext(),
- /*other=*/mlir::LLVM::ModRefInfo::Ref,
- /*argMem=*/mlir::LLVM::ModRefInfo::Ref,
- /*inaccessibleMem=*/mlir::LLVM::ModRefInfo::Ref,
- /*errnoMem=*/mlir::LLVM::ModRefInfo::Ref,
- /*targetMem0=*/mlir::LLVM::ModRefInfo::Ref,
- /*targetMem1=*/mlir::LLVM::ModRefInfo::Ref));
- fn.setNoUnwind(true);
- fn.setWillReturn(true);
- break;
- case cir::SideEffect::Const:
- fn.setMemoryEffectsAttr(mlir::LLVM::MemoryEffectsAttr::get(
- fn.getContext(),
- /*other=*/mlir::LLVM::ModRefInfo::NoModRef,
- /*argMem=*/mlir::LLVM::ModRefInfo::NoModRef,
- /*inaccessibleMem=*/mlir::LLVM::ModRefInfo::NoModRef,
- /*errnoMem=*/mlir::LLVM::ModRefInfo::NoModRef,
- /*targetMem0=*/mlir::LLVM::ModRefInfo::NoModRef,
- /*targetMem1=*/mlir::LLVM::ModRefInfo::NoModRef));
- fn.setNoUnwind(true);
- fn.setWillReturn(true);
- break;
- }
+ if (std::optional<cir::SideEffect> sideEffectKind = op.getSideEffect();
+ sideEffectKind && *sideEffectKind != cir::SideEffect::All) {
+ // A variadic callee's declared parameters do not cover every argument, so
+ // its argument memory widens too.
+ bool accessesArgMemory = hasABIIndirectMemoryArg(op.getArgAttrsAttr()) ||
+ op.getFunctionType().isVarArg();
+ fn.setMemoryEffectsAttr(buildMemoryEffects(fn.getContext(), *sideEffectKind,
+ accessesArgMemory));
+ fn.setNoUnwind(true);
+ fn.setWillReturn(true);
}
if (op->hasAttr(CIRDialect::getNoReturnAttrName()))
diff --git a/clang/test/CIR/CodeGen/side-effect.cpp b/clang/test/CIR/CodeGen/side-effect.cpp
index a1e0fbbeb0e2c..a53f5b3376e86 100644
--- a/clang/test/CIR/CodeGen/side-effect.cpp
+++ b/clang/test/CIR/CodeGen/side-effect.cpp
@@ -1,43 +1,140 @@
// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-cir %s -o %t.cir
// RUN: FileCheck --input-file=%t.cir %s -check-prefix=CIR
-// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-llvm %s -o %t.ll
-// RUN: FileCheck --input-file=%t.ll %s -check-prefix=LLVM
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-llvm %s -o %t-cir.ll
+// RUN: FileCheck --input-file=%t-cir.ll %s -check-prefix=LLVM
// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm %s -o %t.ll
-// RUN: FileCheck --input-file=%t.ll %s -check-prefix=OGCG
+// RUN: FileCheck --input-file=%t.ll %s -check-prefix=LLVM
+
+struct Big { long a, b, c, d; };
+struct Small { int x; };
+
extern "C" {
+__attribute__((const)) Big const_sret();
+__attribute__((pure)) Big pure_sret();
+__attribute__((const)) int const_byval(Big b);
+__attribute__((pure)) int pure_byval(Big b);
+__attribute__((const)) Small const_small();
+__attribute__((const)) int const_ptr(const int *p);
+__attribute__((const)) int const_variadic_decl(int n, ...);
+__attribute__((const)) int const_byval2(int n, Big b);
+__attribute__((const)) int const_bigref(const Big &b);
+
// FIXME: We should figure out how to better print this on functions in the
// future.
// CIR: cir.func{{.*}}@pure_func() -> !s32i side_effect(pure) attributes {{{.*}}nothrow} {
-// LLVM: Function Attrs: {{.*}}nounwind{{.*}}willreturn{{.*}}memory(read)
-// LLVM: define{{.*}} @pure_func() #{{.*}} {
-// OGCG: Function Attrs: {{.*}}nounwind{{.*}}willreturn{{.*}}memory(read)
-// OGCG: define{{.*}} @pure_func() #{{.*}} {
__attribute__((pure))
int pure_func() { return 2;}
// CIR: cir.func{{.*}}@const_func() -> !s32i side_effect(const) attributes {{{.*}}nothrow} {
-// LLVM: Function Attrs: {{.*}}nounwind{{.*}}willreturn{{.*}}memory(none)
-// LLVM: define{{.*}} @const_func() #{{.*}} {
-// OGCG: Function Attrs: {{.*}}nounwind{{.*}}willreturn{{.*}}memory(none)
-// OGCG: define{{.*}} @const_func() #{{.*}} {
__attribute__((const))
int const_func() { return 1;}
+// Variadic definition: widened with no indirect slot in the signature.
+// CIR: cir.func{{.*}}@const_variadic(%arg0: !s32i {{.*}}, ...) -> !s32i side_effect(const)
+__attribute__((const))
+int const_variadic(int n, ...) { return n; }
+
+// A definition also gets llvm.noalias on the sret slot, so this is where
+// noalias, writable and the widened effect have to coexist.
+// CIR: cir.func{{.*}}@const_sret_def(%arg0: !cir.ptr<!rec_Big> {{{.*}}llvm.noalias{{.*}}llvm.sret = !rec_Big{{.*}}llvm.writable{{.*}}) side_effect(const)
+__attribute__((const))
+Big const_sret_def() { Big r{}; return r; }
+
void use() {
- // CIR: cir.call @pure_func() side_effect(pure) : () -> !s32i
- // LLVM: call i32 @pure_func() #[[PURE_ATTR:.*]]
- // OGCG: call i32 @pure_func() #[[PURE_ATTR:.*]]
+ // Unwidened at a call site: neither takes an indirect slot.
+ // CIR: cir.call @pure_func() side_effect(pure)
pure_func();
- // CIR: cir.call @const_func() side_effect(const) : () -> !s32i
- // LLVM: call i32 @const_func() #[[CONST_ATTR:.*]]
- // OGCG: call i32 @const_func() #[[CONST_ATTR:.*]]
+ // CIR: cir.call @const_func() side_effect(const)
const_func();
+
+ // The pass has already given these calls their sret operand.
+ // CIR: cir.call @const_sret(%{{.+}}) side_effect(const)
+ const_sret();
+ // CIR: cir.call @pure_sret(%{{.+}}) side_effect(pure)
+ pure_sret();
+
+ Big b{};
+ int i = 0;
+ // CIR: cir.call @const_byval({{.*}}) side_effect(const)
+ const_byval(b);
+ // CIR: cir.call @pure_byval({{.*}}) side_effect(pure)
+ pure_byval(b);
+
+ // CIR: cir.call @const_small() side_effect(const)
+ const_small();
+ // A source-level pointer is not ABI-introduced memory, so the effect stays
+ // memory(none).
+ // CIR: cir.call @const_ptr({{.*}}) side_effect(const)
+ const_ptr(&i);
+
+ // A call site sees the arguments it passes, so it is not widened for being
+ // variadic.
+ // CIR: cir.call @const_variadic({{.*}}) side_effect(const)
+ const_variadic(1);
+ // CIR: cir.call @const_variadic_decl({{.*}}) side_effect(const)
+ const_variadic_decl(1);
+
+ // CIR: cir.call @const_byval2({{.*}}) side_effect(const) : (!s32i{{.*}}, !cir.ptr<!rec_Big> {{{.*}}llvm.byval{{.*}}}) -> !s32i
+ const_byval2(1, b);
+ // CIR: cir.call @const_sret_def(%{{.+}}) side_effect(const)
+ const_sret_def();
+
+ // A reference is a direct pointer slot that still carries align, so neither
+ // align nor a record pointee can stand in for an indirect slot.
+ // CIR: cir.call @const_bigref({{.*}}) side_effect(const)
+ const_bigref(b);
}
-// LLVM: attributes #[[PURE_ATTR]] = {{{.*}}nounwind{{.*}}willreturn{{.*}}memory(read) }
-// OGCG: attributes #[[PURE_ATTR]] = {{{.*}}nounwind{{.*}}willreturn{{.*}}memory(read) }
-// LLVM: attributes #[[CONST_ATTR]] = {{{.*}}nounwind{{.*}}willreturn{{.*}}memory(none) }
-// OGCG: attributes #[[CONST_ATTR]] = {{{.*}}nounwind{{.*}}willreturn{{.*}}memory(none) }
}
+// The named functions appear in the same relative order in both emits, so
+// these checks are ordered. The intrinsic declarations do not, so each
+// attribute group is captured off its own define or declare line instead of
+// matched adjacent to it.
+
+// Definitions.
+// LLVM: define{{.*}} i32 @pure_func() #[[READ_DEF:[0-9]+]] {
+// LLVM: define{{.*}} i32 @const_func() #[[NONE_DEF:[0-9]+]] {
+// LLVM: define{{.*}} i32 @const_variadic(i32 noundef %{{[^,)]+}}, ...) #[[ARGMEM_DEF:[0-9]+]] {
+// LLVM: define{{.*}} void @const_sret_def(ptr dead_on_unwind noalias writable sret(%struct.Big) align 8 %{{[^,)]+}}) #[[ARGMEM_DEF]] {
+
+// Call sites.
+// LLVM: define{{.*}} void @use()
+// LLVM: call i32 @pure_func() #[[READ_CALL:[0-9]+]]
+// LLVM: call i32 @const_func() #[[NONE_CALL:[0-9]+]]
+// LLVM: call void @const_sret(ptr dead_on_unwind writable sret(%struct.Big) align 8 %{{.+}}) #[[ARGMEM_CALL:[0-9]+]]
+// LLVM: call void @pure_sret(ptr dead_on_unwind writable sret(%struct.Big) align 8 %{{.+}}) #[[READ_ARGMEM_CALL:[0-9]+]]
+// LLVM: call i32 @const_byval(ptr noundef byval(%struct.Big) align 8 %{{.+}}) #[[ARGMEM_CALL]]
+// LLVM: call i32 @pure_byval(ptr noundef byval(%struct.Big) align 8 %{{.+}}) #[[READ_ARGMEM_CALL]]
+// LLVM: call i32 @const_small() #[[NONE_CALL]]
+// LLVM: call i32 @const_ptr(ptr noundef %{{.+}}) #[[NONE_CALL]]
+// LLVM: call i32 (i32, ...) @const_variadic(i32 noundef 1) #[[NONE_CALL]]
+// LLVM: call i32 (i32, ...) @const_variadic_decl(i32 noundef 1) #[[NONE_CALL]]
+// LLVM: call i32 @const_byval2(i32 noundef 1, ptr noundef byval(%struct.Big) align 8 %{{.+}}) #[[ARGMEM_CALL]]
+// LLVM: call void @const_sret_def(ptr dead_on_unwind writable sret(%struct.Big) align 8 %{{.+}}) #[[ARGMEM_CALL]]
+// LLVM: call i32 @const_bigref(ptr noundef nonnull align 8 dereferenceable(32) %{{.+}}) #[[NONE_CALL]]
+
+// Declarations.
+// LLVM: declare void @const_sret(ptr dead_on_unwind writable sret(%struct.Big) align 8) #[[ARGMEM_DECL:[0-9]+]]
+// LLVM: declare void @pure_sret(ptr dead_on_unwind writable sret(%struct.Big) align 8) #[[READ_ARGMEM_DECL:[0-9]+]]
+// LLVM: declare i32 @const_byval(ptr noundef byval(%struct.Big) align 8) #[[ARGMEM_DECL]]
+// LLVM: declare i32 @pure_byval(ptr noundef byval(%struct.Big) align 8) #[[READ_ARGMEM_DECL]]
+// LLVM: declare i32 @const_small() #[[NONE_DECL:[0-9]+]]
+// LLVM: declare i32 @const_ptr(ptr noundef) #[[NONE_DECL]]
+// LLVM: declare i32 @const_variadic_decl(i32 noundef, ...) #[[ARGMEM_DECL]]
+// LLVM: declare i32 @const_byval2(i32 noundef, ptr noundef byval(%struct.Big) align 8) #[[ARGMEM_DECL]]
+// LLVM: declare i32 @const_bigref(ptr noundef nonnull align 8 dereferenceable(32)) #[[NONE_DECL]]
+
+// The trailing wildcard covers target-features and the other codegen-option
+// strings, which differ between the emits.
+// LLVM-DAG: attributes #[[READ_DEF]] = { {{.*}}nounwind{{.*}}willreturn memory(read) {{.*}}}
+// LLVM-DAG: attributes #[[NONE_DEF]] = { {{.*}}nounwind{{.*}}willreturn memory(none) {{.*}}}
+// LLVM-DAG: attributes #[[ARGMEM_DEF]] = { {{.*}}nounwind{{.*}}willreturn memory(argmem: readwrite) {{.*}}}
+// LLVM-DAG: attributes #[[ARGMEM_DECL]] = { nounwind willreturn memory(argmem: readwrite) {{.*}}}
+// LLVM-DAG: attributes #[[READ_ARGMEM_DECL]] = { nounwind willreturn memory(read, argmem: readwrite) {{.*}}}
+// LLVM-DAG: attributes #[[NONE_DECL]] = { nounwind willreturn memory(none) {{.*}}}
+// LLVM-DAG: attributes #[[READ_CALL]] = { nounwind willreturn memory(read) }
+// LLVM-DAG: attributes #[[NONE_CALL]] = { nounwind willreturn memory(none) }
+// LLVM-DAG: attributes #[[ARGMEM_CALL]] = { nounwind willreturn memory(argmem: readwrite) }
+// LLVM-DAG: attributes #[[READ_ARGMEM_CALL]] = { nounwind willreturn memory(read, argmem: readwrite) }
diff --git a/clang/test/CIR/Transforms/abi-lowering/side-effect-argmem.cir b/clang/test/CIR/Transforms/abi-lowering/side-effect-argmem.cir
new file mode 100644
index 0000000000000..b213dbf13f9db
--- /dev/null
+++ b/clang/test/CIR/Transforms/abi-lowering/side-effect-argmem.cir
@@ -0,0 +1,132 @@
+// RUN: cir-opt %s -cir-call-conv-lowering="classification-attr=test_classify" \
+// RUN: | FileCheck %s
+// The pass leaves test_classify on the lowered func, and its coerced_type is a
+// CIR type, so the alias outlives every cir op and mlir-translate has to
+// accept it.
+// RUN: cir-opt %s -cir-call-conv-lowering="classification-attr=test_classify" \
+// RUN: -cir-to-llvm -o - \
+// RUN: | mlir-translate -mlir-to-llvmir --allow-unregistered-dialect \
+// RUN: | FileCheck %s --check-prefix=LLVM
+
+!s8i = !cir.int<s, 8>
+!s32i = !cir.int<s, 32>
+!s64i = !cir.int<s, 64>
+!rec_Big = !cir.struct<"Big" {data !s64i, data !s64i, data !s64i}>
+
+// The leading direct argument puts the byval slot at index 1.
+#byval_arg = {
+ return = { kind = "direct" },
+ args = [ { kind = "direct" },
+ { kind = "indirect", indirect_align = 8 } ]
+}
+
+#byref_arg = {
+ return = { kind = "direct" },
+ args = [ { kind = "indirect", indirect_align = 8, byval = false } ]
+}
+
+#sret_ret = {
+ return = { kind = "indirect", indirect_align = 8 },
+ args = [ ]
+}
+
+#extend_arg = {
+ return = { kind = "direct" },
+ args = [ { kind = "extend",
+ coerced_type = !cir.int<s, 32>,
+ sign_extend = true } ]
+}
+
+#passthrough = {
+ return = { kind = "direct" },
+ args = [ ]
+}
+
+module attributes {
+ cir.triple = "x86_64-unknown-linux-gnu",
+ dlti.dl_spec = #dlti.dl_spec<
+ #dlti.dl_entry<i8, dense<8>: vector<2xi64>>,
+ #dlti.dl_entry<i32, dense<32>: vector<2xi64>>,
+ #dlti.dl_entry<i64, dense<64>: vector<2xi64>>>
+} {
+
+ cir.func private @const_sret() -> !rec_Big side_effect(const)
+ attributes { test_classify = #sret_ret }
+
+ // CHECK: cir.func{{.*}} @const_sret(!cir.ptr<!rec_Big> {{{.*}}llvm.sret = !rec_Big{{.*}}}) side_effect(const)
+ // LLVM: ; Function Attrs: nounwind willreturn memory(argmem: readwrite)
+ // LLVM-NEXT: declare void @const_sret(ptr dead_on_unwind writable sret(%struct.Big) align 8)
+
+ cir.func private @pure_sret() -> !rec_Big side_effect(pure)
+ attributes { test_classify = #sret_ret }
+
+ // CHECK: cir.func{{.*}} @pure_sret(!cir.ptr<!rec_Big> {{{.*}}llvm.sret = !rec_Big{{.*}}}) side_effect(pure)
+ // LLVM: ; Function Attrs: nounwind willreturn memory(read, argmem: readwrite)
+ // LLVM-NEXT: declare void @pure_sret(ptr dead_on_unwind writable sret(%struct.Big) align 8)
+
+ cir.func private @const_byval(!s32i, !rec_Big) side_effect(const)
+ attributes { test_classify = #byval_arg }
+
+ // CHECK: cir.func{{.*}} @const_byval(!s32i, !cir.ptr<!rec_Big> {{{.*}}llvm.byval = !rec_Big{{.*}}}) side_effect(const)
+ // LLVM: ; Function Attrs: nounwind willreturn memory(argmem: readwrite)
+ // LLVM-NEXT: declare void @const_byval(i32, ptr noundef byval(%struct.Big) align 8)
+
+ cir.func private @const_byref(!rec_Big) side_effect(const)
+ attributes { test_classify = #byref_arg }
+
+ // CHECK: cir.func{{.*}} @const_byref(!cir.ptr<!rec_Big> {{{.*}}llvm.byref = !rec_Big{{.*}}}) side_effect(const)
+ // LLVM: ; Function Attrs: nounwind willreturn memory(argmem: readwrite)
+ // LLVM-NEXT: declare void @const_byref(ptr byref(%struct.Big) align 8)
+
+ cir.func private @const_variadic(!s8i, ...) -> !s32i side_effect(const)
+ attributes { test_classify = #extend_arg }
+
+ // CHECK: cir.func{{.*}} @const_variadic(!s8i {{{.*}}llvm.signext{{.*}}}, ...) -> !s32i side_effect(const)
+ // LLVM: ; Function Attrs: nounwind willreturn memory(argmem: readwrite)
+ // LLVM-NEXT: declare i32 @const_variadic(i8 signext, ...)
+
+ // Negative: an Extend argument carries a non-empty attribute dictionary but
+ // no ABI-introduced memory, so the effect stays memory(none).
+ cir.func private @const_extend(!s8i) -> !s32i side_effect(const)
+ attributes { test_classify = #extend_arg }
+
+ // CHECK: cir.func{{.*}} @const_extend(!s8i {{{.*}}llvm.signext{{.*}}}) -> !s32i side_effect(const)
+ // LLVM: ; Function Attrs: nounwind willreturn memory(none)
+ // LLVM-NEXT: declare i32 @const_extend(i8 signext)
+
+ // A callee with no side_effect must still come out with none, so that
+ // forwarding the original is distinguishable from hard-coding a value.
+ cir.func private @plain_sret() -> !rec_Big
+ attributes { test_classify = #sret_ret }
+
+ cir.func @call_plain_sret() attributes { test_classify = #passthrough } {
+ %r = cir.call @plain_sret() : () -> !rec_Big
+ cir.return
+ }
+
+ // CHECK: cir.call @plain_sret(%{{[^)]+}}) : (!cir.ptr<!rec_Big>{{.*}}) -> ()
+ // LLVM: call void @plain_sret(ptr dead_on_unwind writable sret(%struct.Big) align 8 %{{.+}}){{$}}
+
+ // nothrow without const or pure still reaches nounwind, and still gets no
+ // memory effect.
+ cir.func @call_plain_sret_nothrow() attributes { test_classify = #passthrough } {
+ %r = cir.call @plain_sret() nothrow : () -> !rec_Big
+ cir.return
+ }
+
+ // CHECK: cir.call @plain_sret(%{{[^)]+}}) nothrow : (!cir.ptr<!rec_Big>{{.*}}) -> ()
+ // LLVM: call void @plain_sret(ptr dead_on_unwind writable sret(%struct.Big) align 8 %{{.+}}) #[[NOTHROW_CALL:[0-9]+]]
+
+ // The call site keeps its side_effect across the rewrite.
+ cir.func @call_const_sret() attributes { test_classify = #passthrough } {
+ %r = cir.call @const_sret() side_effect(const) : () -> !rec_Big
+ cir.return
+ }
+
+ // CHECK: cir.func{{.*}} @call_const_sret()
+ // CHECK: cir.call @const_sret(%{{.+}}) side_effect(const)
+ // LLVM: define void @call_const_sret()
+ // LLVM: call void @const_sret(ptr dead_on_unwind writable sret(%struct.Big) align 8 %{{.+}}) #[[CALL_ATTRS:[0-9]+]]
+ // LLVM-DAG: attributes #[[CALL_ATTRS]] = { nounwind willreturn memory(argmem: readwrite) }
+ // LLVM-DAG: attributes #[[NOTHROW_CALL]] = { nounwind }
+}
More information about the cfe-commits
mailing list