[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