[Mlir-commits] [flang] [mlir] [mlir][OpenMP] Add memory effects of single op (PR #237929)

llvmlistbot at llvm.org llvmlistbot at llvm.org
Mon Nov 2 03:51:24 PST 2026


https://github.com/JRP-arm created https://github.com/llvm/llvm-project/pull/237929

Added a new attribute for the copyprivate clause `copyprivate_assume_bounded_recipe_effects`. Applied when the copy private variables are of a specific type.

Add direct memory effect tests, CSE & canonicalisation tests.
Add tests for correct application of the `copyprivate_assume_bounded_recipe_effects` attribute.
AI-Assisted-by: Codex (OpenAI)

>From 2d2a7ef3528b04bd0af60ba76ae55701c195a40f Mon Sep 17 00:00:00 2001
From: "Jamie.Rowlinson-Patel at arm.com" <jamie.rowlinson-patel at arm.com>
Date: Mon, 26 Oct 2026 11:09:45 +0000
Subject: [PATCH] [mlir][OpenMP] Add memory effects of single op

---
 flang/lib/Lower/OpenMP/OpenMP.cpp             |  68 +++---
 flang/test/Lower/OpenMP/compound.f90          |  14 +-
 .../Lower/OpenMP/recipe-memory-effects.f90    | 199 ++++++++++++++++++
 .../mlir/Dialect/OpenMP/OpenMPClauses.td      |  20 +-
 mlir/include/mlir/Dialect/OpenMP/OpenMPOps.td |  10 +-
 mlir/lib/Dialect/OpenMP/IR/OpenMPDialect.cpp  |  49 ++++-
 mlir/test/Dialect/OpenMP/canonicalize.mlir    |  28 +++
 mlir/test/Dialect/OpenMP/cse.mlir             |  21 ++
 mlir/test/Dialect/OpenMP/memory-effects.mlir  |  84 ++++++++
 9 files changed, 445 insertions(+), 48 deletions(-)

diff --git a/flang/lib/Lower/OpenMP/OpenMP.cpp b/flang/lib/Lower/OpenMP/OpenMP.cpp
index 98a0a54ad2a6f2..63ebe6224bea4b 100644
--- a/flang/lib/Lower/OpenMP/OpenMP.cpp
+++ b/flang/lib/Lower/OpenMP/OpenMP.cpp
@@ -1858,6 +1858,30 @@ struct OpWithBodyGenInfo {
   int collapseValue = 0;
 };
 
+static bool isNonDerivedNonPointer(const semantics::Symbol &sym) {
+  const auto &ultimate = sym.GetUltimate();
+  const auto *type = ultimate.GetType();
+  return type && type->AsIntrinsic() && !semantics::IsAllocatable(ultimate) &&
+         !semantics::IsPointer(ultimate);
+}
+
+static bool isNonDerivedNonPointer(const mlir::Value &variable) {
+  return !fir::isAllocatableType(variable.getType()) &&
+         !fir::isa_derived(fir::getFortranElementType(variable.getType())) &&
+         !fir::isPointerType(variable.getType());
+}
+
+static bool isNonDerivedNonPointerItem(const semantics::Symbol &sym) {
+  const auto &ultimate = sym.GetUltimate();
+  if (const auto *common =
+          ultimate.detailsIf<semantics::CommonBlockDetails>()) {
+    return llvm::all_of(common->objects(), [](const auto &member) {
+      return isNonDerivedNonPointer(*member);
+    });
+  }
+  return isNonDerivedNonPointer(ultimate);
+}
+
 /// Create the body (block) for an OpenMP Operation.
 ///
 /// \param [in]   op  - the operation the body belongs to.
@@ -1926,6 +1950,17 @@ static void createBodyOfOp(mlir::Operation &op, const OpWithBodyGenInfo &info,
                     Fortran::lower::omp::isLastItemInQueue(item, queue),
                     /*useDelayedPrivatization=*/false, info.symTable);
     tempDsp->processStep1();
+
+    // TODO: omp.single still uses eager privatisation.
+    // Use clauseOps.privateVars inside genSingleOp once
+    // delayedPrivatization is turned on.
+    if (auto singleOp = llvm::dyn_cast<mlir::omp::SingleOp>(op)) {
+      const auto &privateSyms = tempDsp->getAllSymbolsToPrivatize();
+      if (llvm::all_of(privateSyms, [](const semantics::Symbol *sym) {
+            return isNonDerivedNonPointerItem(*sym);
+          }))
+        singleOp.setPrivateAssumeBoundedRecipeEffects(true);
+    }
   }
 
   if (info.dir == llvm::omp::Directive::OMPD_parallel) {
@@ -3564,30 +3599,6 @@ findEnclosingParallelOp(fir::FirOpBuilder &builder) {
   return {};
 }
 
-static bool isNonDerivedNonPointer(const semantics::Symbol &sym) {
-  const auto &ultimate = sym.GetUltimate();
-  const auto *type = ultimate.GetType();
-  return type && type->AsIntrinsic() && !semantics::IsAllocatable(ultimate) &&
-         !semantics::IsPointer(ultimate);
-}
-
-static bool isNonDerivedNonPointer(const mlir::Value &variable) {
-  return !fir::isAllocatableType(variable.getType()) &&
-         !fir::isa_derived(fir::getFortranElementType(variable.getType())) &&
-         !fir::isPointerType(variable.getType());
-}
-
-static bool isNonDerivedNonPointerItem(const semantics::Symbol &sym) {
-  const auto &ultimate = sym.GetUltimate();
-  if (const auto *common =
-          ultimate.detailsIf<semantics::CommonBlockDetails>()) {
-    return llvm::all_of(common->objects(), [](const auto &member) {
-      return isNonDerivedNonPointer(*member);
-    });
-  }
-  return isNonDerivedNonPointer(ultimate);
-}
-
 static mlir::omp::SectionsOp
 genSectionsOp(lower::AbstractConverter &converter, lower::SymMap &symTable,
               semantics::SemanticsContext &semaCtx,
@@ -3945,11 +3956,18 @@ genSingleOp(lower::AbstractConverter &converter, lower::SymMap &symTable,
   mlir::omp::SingleOperands clauseOps;
   genSingleClauses(converter, semaCtx, item->clauses, loc, clauseOps);
 
-  return genOpWithBody<mlir::omp::SingleOp>(
+  auto singleOp = genOpWithBody<mlir::omp::SingleOp>(
       OpWithBodyGenInfo(converter, symTable, semaCtx, loc, eval,
                         llvm::omp::Directive::OMPD_single)
           .setClauses(&item->clauses),
       queue, item, clauseOps);
+
+  if (llvm::all_of(clauseOps.copyprivateVars, [](mlir::Value variable) {
+        return isNonDerivedNonPointer(variable);
+      }))
+    singleOp.setCopyprivateAssumeBoundedRecipeEffects(true);
+
+  return singleOp;
 }
 
 static bool isDuplicateMappedSymbol(
diff --git a/flang/test/Lower/OpenMP/compound.f90 b/flang/test/Lower/OpenMP/compound.f90
index d61745345a6405..f06edf36244e5e 100644
--- a/flang/test/Lower/OpenMP/compound.f90
+++ b/flang/test/Lower/OpenMP/compound.f90
@@ -784,7 +784,7 @@ subroutine single_loop()
 ! CHECK-NEXT:      }
 ! CHECK-NOT:       omp.combined
 ! CHECK:           omp.terminator
-! CHECK-NEXT:    } {{{.*}}omp.combined{{.*}}}
+! CHECK-NEXT:    }{{.*}} {{{.*}}omp.combined{{.*}}}
 
 subroutine single_parallel()
   implicit none
@@ -803,7 +803,7 @@ subroutine single_parallel()
 ! CHECK-NEXT:      }
 ! CHECK-NOT:       omp.combined
 ! CHECK:           omp.terminator
-! CHECK-NEXT:    } {{{.*}}omp.combined{{.*}}}
+! CHECK-NEXT:    }{{.*}} {{{.*}}omp.combined{{.*}}}
 
 subroutine single_simd()
   implicit none
@@ -825,7 +825,7 @@ subroutine single_simd()
 ! CHECK-NEXT:      }
 ! CHECK-NOT:       omp.combined
 ! CHECK:           omp.terminator
-! CHECK-NEXT:    } {{{.*}}omp.combined{{.*}}}
+! CHECK-NEXT:    }{{.*}} {{{.*}}omp.combined{{.*}}}
 
 subroutine single_target()
   implicit none
@@ -844,7 +844,7 @@ subroutine single_target()
 ! CHECK-NEXT:      }
 ! CHECK-NOT:       omp.combined
 ! CHECK:           omp.terminator
-! CHECK-NEXT:    } {{{.*}}omp.combined{{.*}}}
+! CHECK-NEXT:    }{{.*}} {{{.*}}omp.combined{{.*}}}
 
 subroutine single_target_data()
   implicit none
@@ -864,7 +864,7 @@ subroutine single_target_data()
 ! CHECK-NEXT:      }
 ! CHECK-NOT:       omp.combined
 ! CHECK:           omp.terminator
-! CHECK-NEXT:    } {{{.*}}omp.combined{{.*}}}
+! CHECK-NEXT:    }{{.*}} {{{.*}}omp.combined{{.*}}}
 
 subroutine single_task()
   implicit none
@@ -883,7 +883,7 @@ subroutine single_task()
 ! CHECK-NEXT:      }
 ! CHECK-NOT:       omp.combined
 ! CHECK:           omp.terminator
-! CHECK-NEXT:    } {{{.*}}omp.combined{{.*}}}
+! CHECK-NEXT:    }{{.*}} {{{.*}}omp.combined{{.*}}}
 
 subroutine single_taskloop()
   implicit none
@@ -908,7 +908,7 @@ subroutine single_taskloop()
 ! CHECK:             omp.terminator
 ! CHECK-NEXT:      } {{{.*}}omp.combined{{.*}}}
 ! CHECK:           omp.terminator
-! CHECK-NEXT:    } {{{.*}}omp.combined{{.*}}}
+! CHECK-NEXT:    }{{.*}} {{{.*}}omp.combined{{.*}}}
 
 subroutine target_task()
   implicit none
diff --git a/flang/test/Lower/OpenMP/recipe-memory-effects.f90 b/flang/test/Lower/OpenMP/recipe-memory-effects.f90
index 631906ee7b45ba..1e4d748492e2ea 100644
--- a/flang/test/Lower/OpenMP/recipe-memory-effects.f90
+++ b/flang/test/Lower/OpenMP/recipe-memory-effects.f90
@@ -158,4 +158,203 @@ subroutine derived_common_private()
       z%value = x
     !$omp end sections
   end subroutine
+
+  ! Eager privatisation emits the privatisation recipe inside the single
+  ! region, preventing getEffects() from providing its memory effects.
+  ! Make sure these are emitted so recursive analyis records their memory
+  ! effects.
+  ! CHECK-LABEL: func.func @_QMrecipe_memory_effectsPsingle_eager_firstprivate(
+  ! CHECK: %[[ORIGINAL:.*]] = fir.alloca i32
+  ! CHECK: %[[ORIGINAL_DECL:.*]]:2 = hlfir.declare %[[ORIGINAL]]
+  ! CHECK: omp.parallel {
+  ! CHECK-NEXT: omp.single {
+  ! CHECK-NEXT: %[[PRIVATE:.*]] = fir.alloca i32
+  ! CHECK-NEXT: %[[PRIVATE_DECL:.*]]:2 = hlfir.declare %[[PRIVATE]]
+  ! CHECK-NEXT: %[[INITIAL:.*]] = fir.load %[[ORIGINAL_DECL]]#0 : !fir.ref<i32>
+  ! CHECK-NEXT: hlfir.assign %[[INITIAL]] to %[[PRIVATE_DECL]]#0 : i32, !fir.ref<i32>
+  ! CHECK-NEXT: %[[VALUE:.*]] = fir.load %[[PRIVATE_DECL]]#0 : !fir.ref<i32>
+  ! CHECK-NEXT: %[[ONE:.*]] = arith.constant 1 : i32
+  ! CHECK-NEXT: %[[UPDATED:.*]] = arith.addi %[[VALUE]], %[[ONE]] : i32
+  ! CHECK-NEXT: hlfir.assign %[[UPDATED]] to %[[PRIVATE_DECL]]#0 : i32, !fir.ref<i32>
+  ! CHECK-NEXT: omp.terminator
+  ! CHECK-NEXT: } <copyprivate_assume_bounded_recipe_effects, private_assume_bounded_recipe_effects>
+  ! CHECK-NEXT: omp.terminator
+  ! CHECK-NEXT: }
+  ! CHECK-NEXT: %[[AFTER:.*]] = fir.load %[[ORIGINAL_DECL]]#0 : !fir.ref<i32>
+  ! CHECK-NEXT: hlfir.assign %[[AFTER]] to %{{.*}} : i32, !fir.ref<i32>
+  ! CHECK-NEXT: return
+  subroutine single_eager_firstprivate(result)
+    integer :: result, x
+    x = 10
+    !$omp parallel shared(x)
+      !$omp single firstprivate(x)
+        x = x + 1
+      !$omp end single
+    !$omp end parallel
+    result = x
+  end subroutine
+
+  ! Eager single privatization maps COMMON members rather than the block.
+  ! CHECK-LABEL: func.func @_QMrecipe_memory_effectsPsingle_intrinsic_common_private(
+  ! CHECK: omp.single {
+  ! CHECK: } <copyprivate_assume_bounded_recipe_effects, private_assume_bounded_recipe_effects>
+  ! CHECK: return
+  subroutine single_intrinsic_common_private()
+    integer :: x
+    real :: y
+    common /single_private_block/ x, y
+    !$omp single private(/single_private_block/)
+      x = 1
+      y = 1.0
+    !$omp end single
+  end subroutine
+
+  ! Firstprivate COMMON blocks must also be checked member by member.
+  ! CHECK-LABEL: func.func @_QMrecipe_memory_effectsPsingle_intrinsic_common_firstprivate(
+  ! CHECK: omp.single {
+  ! CHECK: } <copyprivate_assume_bounded_recipe_effects, private_assume_bounded_recipe_effects>
+  ! CHECK: return
+  subroutine single_intrinsic_common_firstprivate()
+    integer :: x
+    real :: y
+    common /single_firstprivate_block/ x, y
+    !$omp single firstprivate(/single_firstprivate_block/)
+      x = x + 1
+      y = y + 1.0
+    !$omp end single
+  end subroutine
+
+  ! One derived member prevents the assumption for the entire COMMON block.
+  ! CHECK-LABEL: func.func @_QMrecipe_memory_effectsPsingle_derived_common_private(
+  ! CHECK: omp.single {
+  ! CHECK-NOT: private_assume_bounded_recipe_effects
+  ! CHECK: } <copyprivate_assume_bounded_recipe_effects>
+  ! CHECK: return
+  subroutine single_derived_common_private()
+    integer :: x
+    type(record) :: z
+    common /single_derived_block/ x, z
+    !$omp single private(/single_derived_block/)
+      x = 1
+      z%value = x
+    !$omp end single
+  end subroutine
+
+  ! Direct intrinsic scalars and fixed-size arrays have bounded copy functions.
+  ! CHECK-LABEL: func.func @_QMrecipe_memory_effectsPintrinsic_copyprivate(
+  ! CHECK: omp.single copyprivate(
+  ! CHECK: copyprivate_assume_bounded_recipe_effects
+  ! CHECK: return
+  subroutine intrinsic_copyprivate()
+    integer :: i, a(4)
+    real :: r
+    complex :: c
+    logical :: l
+    character(8) :: s
+    !$omp parallel private(i, a, r, c, l, s)
+      !$omp single
+      !$omp end single copyprivate(i, a, r, c, l, s)
+    !$omp end parallel
+  end subroutine
+
+  ! CHECK-LABEL: func.func @_QMrecipe_memory_effectsPno_copyprivate(
+  ! CHECK: omp.single {
+  ! CHECK: copyprivate_assume_bounded_recipe_effects
+  ! CHECK: return
+  subroutine no_copyprivate()
+    !$omp single
+    !$omp end single
+  end subroutine
+
+  ! Copyprivate and private make independent assumptions.
+  ! CHECK-LABEL: func.func @_QMrecipe_memory_effectsPbounded_copyprivate_derived_private(
+  ! CHECK: omp.single copyprivate(
+  ! CHECK: copyprivate_assume_bounded_recipe_effects
+  ! CHECK: return
+  subroutine bounded_copyprivate_derived_private()
+    type record
+      integer :: value
+    end type
+    type(record) :: z
+    integer :: i
+    !$omp parallel private(i)
+      !$omp single private(z)
+      !$omp end single copyprivate(i)
+    !$omp end parallel
+  end subroutine
+
+  ! One derived item prevents the assumption for the entire copyprivate clause.
+  ! CHECK-LABEL: func.func @_QMrecipe_memory_effectsPderived_copyprivate_bounded_private(
+  ! CHECK: omp.single copyprivate(
+  ! CHECK-NOT: copyprivate_assume_bounded_recipe_effects
+  ! CHECK: } <private_assume_bounded_recipe_effects>
+  ! CHECK: return
+  subroutine derived_copyprivate_bounded_private()
+    type record
+      integer :: value
+    end type
+    type(record) :: z
+    integer :: i, p
+    !$omp parallel private(i, z)
+      !$omp single private(p)
+      !$omp end single copyprivate(i, z)
+    !$omp end parallel
+  end subroutine
+
+  ! Allocatable copies may allocate, free, and access pointee storage.
+  ! CHECK-LABEL: func.func @_QMrecipe_memory_effectsPallocatable_scalar_copyprivate(
+  ! CHECK: omp.single copyprivate(
+  ! CHECK-NOT: copyprivate_assume_bounded_recipe_effects
+  ! CHECK: return
+  subroutine allocatable_scalar_copyprivate()
+    integer, allocatable :: a
+    !$omp parallel private(a)
+      !$omp single
+      !$omp end single copyprivate(a)
+    !$omp end parallel
+  end subroutine
+
+  ! Allocatables may access pointees of the copyprivate variable, violating the
+  ! copyprivate_assume_bounded_recipe_effects attribute contract.
+  ! CHECK-LABEL: func.func @_QMrecipe_memory_effectsPallocatable_array_copyprivate(
+  ! CHECK: omp.single copyprivate(
+  ! CHECK-NOT: copyprivate_assume_bounded_recipe_effects
+  ! CHECK: return
+  subroutine allocatable_array_copyprivate()
+    integer, allocatable :: a(:)
+    !$omp parallel private(a)
+      !$omp single
+      !$omp end single copyprivate(a)
+    !$omp end parallel
+  end subroutine
+
+  ! Pointer variables may access pointees of the copyprivate variable, violating
+  ! the copyprivate_assume_bounded_recipe_effects attribute contract.
+  ! CHECK-LABEL: func.func @_QMrecipe_memory_effectsPpointer_copyprivate(
+  ! CHECK: omp.single copyprivate(
+  ! CHECK-NOT: copyprivate_assume_bounded_recipe_effects
+  ! CHECK: return
+  subroutine pointer_copyprivate()
+    integer, pointer :: p, a(:)
+    !$omp parallel private(p, a)
+      !$omp single
+      !$omp end single copyprivate(p, a)
+    !$omp end parallel
+  end subroutine
+
+  ! Check the members of a threadprivate COMMON block.
+  ! CHECK-LABEL: func.func @_QMrecipe_memory_effectsPintrinsic_common_copyprivate(
+  ! CHECK: omp.single copyprivate(
+  ! CHECK: copyprivate_assume_bounded_recipe_effects
+  ! CHECK: return
+  subroutine intrinsic_common_copyprivate()
+    integer :: i
+    real :: r
+    common /intrinsic_copy_block/ i, r
+    !$omp threadprivate(/intrinsic_copy_block/)
+    !$omp parallel
+      !$omp single
+      !$omp end single copyprivate(/intrinsic_copy_block/)
+    !$omp end parallel
+  end subroutine
 end module
diff --git a/mlir/include/mlir/Dialect/OpenMP/OpenMPClauses.td b/mlir/include/mlir/Dialect/OpenMP/OpenMPClauses.td
index ab6e7178c9f2e9..eddd4fde2cd6bd 100644
--- a/mlir/include/mlir/Dialect/OpenMP/OpenMPClauses.td
+++ b/mlir/include/mlir/Dialect/OpenMP/OpenMPClauses.td
@@ -241,13 +241,16 @@ def OpenMP_CollapseClause : OpenMP_CollapseClauseSkip<>;
 
 class OpenMP_CopyprivateClauseSkip<
     bit traits = false, bit arguments = false, bit assemblyFormat = false,
-    bit description = false, bit extraClassDeclaration = false
+    bit description = false, bit extraClassDeclaration = false,
+    bit boundedRecipeEffects = false
   > : OpenMP_Clause<traits, arguments, assemblyFormat, description,
                     extraClassDeclaration> {
-  let arguments = (ins
+  let arguments = !con((ins
     Variadic<OpenMP_PointerLikeType>:$copyprivate_vars,
     OptionalAttr<SymbolRefArrayAttr>:$copyprivate_syms
-  );
+  ), !if(boundedRecipeEffects, (ins
+    UnitAttr:$copyprivate_assume_bounded_recipe_effects
+  ), (ins)));
 
   let optAssemblyFormat = [{
     `copyprivate` `(`
@@ -259,6 +262,15 @@ class OpenMP_CopyprivateClauseSkip<
     If `copyprivate` variables and functions are specified, then each thread
     variable is updated with the variable value of the thread that executed
     the single region, using the specified copy functions.
+
+    The optional `copyprivate_assume_bounded_recipe_effects` unit attribute
+    asserts that referenced copyprivatisation recipes may only read or write
+    `copyprivate` variables associated with the clause.
+    These effects apply only to storage directly designated by the `copyprivate`
+    variables. Recipes may not access other storage, including globals or
+    pointees reached by loading pointers from these variables. Without the
+    attribute, referenced copy functions are assumed to have arbitrary memory
+    effects.
   }];
 }
 
@@ -1515,7 +1527,6 @@ def OpenMP_PriorityClause : OpenMP_PriorityClauseSkip<>;
 class OpenMP_PrivateClauseSkip<
     bit traits = false, bit arguments = false, bit assemblyFormat = false,
     bit description = false, bit extraClassDeclaration = false,
-    // Only sections currently consumes the recipe-effect assumption.
     bit boundedRecipeEffects = false
   > : OpenMP_Clause<traits, arguments, assemblyFormat, description,
                     extraClassDeclaration> {
@@ -1584,7 +1595,6 @@ def OpenMP_ProcBindClause : OpenMP_ProcBindClauseSkip<>;
 class OpenMP_ReductionClauseSkip<
     bit traits = false, bit arguments = false, bit assemblyFormat = false,
     bit description = false, bit extraClassDeclaration = false,
-    // Only sections currently consumes the recipe-effect assumption.
     bit boundedRecipeEffects = false
   > : OpenMP_Clause<traits, arguments, assemblyFormat, description,
                     extraClassDeclaration> {
diff --git a/mlir/include/mlir/Dialect/OpenMP/OpenMPOps.td b/mlir/include/mlir/Dialect/OpenMP/OpenMPOps.td
index d08b27b90ede5c..021ed0ce7d35e8 100644
--- a/mlir/include/mlir/Dialect/OpenMP/OpenMPOps.td
+++ b/mlir/include/mlir/Dialect/OpenMP/OpenMPOps.td
@@ -370,10 +370,12 @@ def SectionsOp : OpenMP_Op<"sections", traits = [
 //===----------------------------------------------------------------------===//
 
 def SingleOp : OpenMP_Op<"single", traits = [
-    AttrSizedOperandSegments, DeclareOpInterfaceMethods<ComposableOpInterface>
+    AttrSizedOperandSegments, DeclareOpInterfaceMethods<ComposableOpInterface>,
+    DeclareOpInterfaceMethods<MemoryEffectsOpInterface>, RecursiveMemoryEffects
   ], clauses = [
-    OpenMP_AllocateClause, OpenMP_CopyprivateClause, OpenMP_NowaitClause,
-    OpenMP_PrivateClause
+    OpenMP_AllocateClause,
+    OpenMP_CopyprivateClauseSkip<boundedRecipeEffects = true>, OpenMP_NowaitClause,
+    OpenMP_PrivateClauseSkip<boundedRecipeEffects = true>
   ], singleRegion = true> {
   let summary = "single directive";
   let description = [{
@@ -390,7 +392,7 @@ def SingleOp : OpenMP_Op<"single", traits = [
 
   let assemblyFormat = clausesAssemblyFormat # [{
     custom<PrivateRegion>($region, $private_vars, type($private_vars),
-        $private_syms, $private_needs_barrier) attr-dict
+        $private_syms, $private_needs_barrier) prop-dict attr-dict
   }];
 
   let hasVerifier = 1;
diff --git a/mlir/lib/Dialect/OpenMP/IR/OpenMPDialect.cpp b/mlir/lib/Dialect/OpenMP/IR/OpenMPDialect.cpp
index 1c869ad04bbcf6..7e2d94e8f74c0b 100644
--- a/mlir/lib/Dialect/OpenMP/IR/OpenMPDialect.cpp
+++ b/mlir/lib/Dialect/OpenMP/IR/OpenMPDialect.cpp
@@ -3594,13 +3594,15 @@ void SingleOp::build(OpBuilder &builder, OperationState &state,
                      const SingleOperands &clauses) {
   MLIRContext *ctx = builder.getContext();
   // TODO Store clauses in op: privateVars, privateSyms, privateNeedsBarrier
-  SingleOp::build(builder, state, clauses.allocateVars, clauses.allocatorVars,
-                  makeDenseI64ArrayAttr(ctx, clauses.allocateAlignments),
-                  makeDenseI64ArrayAttr(ctx, clauses.allocatePrivateIndices),
-                  clauses.copyprivateVars,
-                  makeArrayAttr(ctx, clauses.copyprivateSyms), clauses.nowait,
-                  /*private_vars=*/{}, /*private_syms=*/nullptr,
-                  /*private_needs_barrier=*/nullptr);
+  SingleOp::build(
+      builder, state, clauses.allocateVars, clauses.allocatorVars,
+      makeDenseI64ArrayAttr(ctx, clauses.allocateAlignments),
+      makeDenseI64ArrayAttr(ctx, clauses.allocatePrivateIndices),
+      clauses.copyprivateVars, makeArrayAttr(ctx, clauses.copyprivateSyms),
+      /*copyprivate_assume_bounded_recipe_effects=*/nullptr, clauses.nowait,
+      /*private_vars=*/{}, /*private_syms=*/nullptr,
+      /*private_needs_barrier=*/nullptr,
+      /*private_assume_bounded_recipe_effects=*/nullptr);
 }
 
 LogicalResult SingleOp::verify() {
@@ -3614,6 +3616,39 @@ LogicalResult SingleOp::verify() {
                                   getCopyprivateSyms());
 }
 
+void SingleOp::getEffects(
+    llvm::SmallVectorImpl<SideEffects::EffectInstance<MemoryEffects::Effect>>
+        &effects) {
+  // The presence of the nowait clause eliminates the implicit barrier at the
+  // end of a single construct.
+  if (!getNowait()) {
+    getBarrierEffects(effects);
+  }
+
+  // Eager privatisation emits explicit operations and leaves privateVars
+  // empty. Since the privatisation operations are emitted explicitly, their
+  // memory effects are accounted for by recursive analysis.
+  // Once single uses delayed privatisation `!getPrivateVars().empty()` will no
+  // longer be redundant.
+  bool hasUnknownRecipeEffects =
+      (!getPrivateVars().empty() && !getPrivateAssumeBoundedRecipeEffects()) ||
+      (!getCopyprivateVars().empty() &&
+       !getCopyprivateAssumeBoundedRecipeEffects());
+  if (hasUnknownRecipeEffects) {
+    addUnknownMemoryEffects(effects);
+  } else {
+    getPrivatisationEffects(*this, effects);
+    // The unknown executing thread supplies the value and the other threads
+    // receive it, so each copyprivate operand may be both read and written.
+    OperandRange copyprivateVars = getCopyprivateVars();
+    for (OpOperand &variable :
+         MutableArrayRef(copyprivateVars.getBase(), copyprivateVars.size())) {
+      effects.emplace_back(MemoryEffects::Read::get(), &variable);
+      effects.emplace_back(MemoryEffects::Write::get(), &variable);
+    }
+  }
+}
+
 //===----------------------------------------------------------------------===//
 // WorkshareOp
 //===----------------------------------------------------------------------===//
diff --git a/mlir/test/Dialect/OpenMP/canonicalize.mlir b/mlir/test/Dialect/OpenMP/canonicalize.mlir
index 4b65b0d648f048..1483452e818a7e 100644
--- a/mlir/test/Dialect/OpenMP/canonicalize.mlir
+++ b/mlir/test/Dialect/OpenMP/canonicalize.mlir
@@ -223,3 +223,31 @@ func.func @sections_nowait_call() {
 }
 
 func.func private @opaque()
+
+// -----
+
+// A single with no side effects can be removed when it has no implicit barrier.
+// CHECK-LABEL: func.func @single_nowait_no_side_effects
+// CHECK-NEXT: return
+func.func @single_nowait_no_side_effects(%a: i32, %b: i32) {
+  omp.single nowait {
+    %unused = arith.addi %a, %b : i32
+    omp.terminator
+  }
+  return
+}
+
+// -----
+
+// The implicit barrier must survive even when the single's body is empty.
+// CHECK-LABEL: func.func @single_only_terminator
+// CHECK-NEXT: omp.single {
+// CHECK-NEXT: omp.terminator
+// CHECK-NEXT: }
+// CHECK-NEXT: return
+func.func @single_only_terminator() {
+  omp.single {
+    omp.terminator
+  }
+  return
+}
diff --git a/mlir/test/Dialect/OpenMP/cse.mlir b/mlir/test/Dialect/OpenMP/cse.mlir
index 919547efc2a092..11d78881e2a47a 100644
--- a/mlir/test/Dialect/OpenMP/cse.mlir
+++ b/mlir/test/Dialect/OpenMP/cse.mlir
@@ -18,3 +18,24 @@ func.func @sections_barrier_between_loads(%shared: memref<i32>) -> (i32, i32) {
   %after = memref.load %shared[] : memref<i32>
   return %before, %after : i32, i32
 }
+
+// -----
+
+// Without the private-clause assumption, arbitrary memory effects are assumed
+// so no CSE is possible.
+omp.private {type = private} @recipe : memref<i32>
+
+// CHECK-LABEL: func.func @single_unbounded_private(
+// CHECK-SAME: %[[X:[^:]+]]: memref<i32>
+// CHECK: %[[BEFORE:.*]] = memref.load %[[X]][] : memref<i32>
+// CHECK: omp.single nowait
+// CHECK: %[[AFTER:.*]] = memref.load %[[X]][] : memref<i32>
+// CHECK: return %[[BEFORE]], %[[AFTER]] : i32, i32
+func.func @single_unbounded_private(%x: memref<i32>, %original: memref<i32>) -> (i32, i32) {
+  %before = memref.load %x[] : memref<i32>
+  omp.single nowait private(@recipe %original -> %copy : memref<i32>) {
+    omp.terminator
+  }
+  %after = memref.load %x[] : memref<i32>
+  return %before, %after : i32, i32
+}
diff --git a/mlir/test/Dialect/OpenMP/memory-effects.mlir b/mlir/test/Dialect/OpenMP/memory-effects.mlir
index b52f815c5e05cb..f2372cd471c497 100644
--- a/mlir/test/Dialect/OpenMP/memory-effects.mlir
+++ b/mlir/test/Dialect/OpenMP/memory-effects.mlir
@@ -173,3 +173,87 @@ func.func @sections_bounded_private_bounded_reduction(%x: memref<i32>, %y: memre
   } <{private_assume_bounded_recipe_effects, reduction_assume_bounded_recipe_effects}>
   return
 }
+
+func.func private @copy_i32(memref<i32>, memref<i32>)
+func.func private @copy_ptr(!llvm.ptr, !llvm.ptr)
+
+func.func @single_unbounded_copyprivate_effects(%x: memref<i32>) {
+  // An unannotated copy function may access arbitrary memory, allocate or free.
+  // expected-remark at below {{recursive memory effects}}
+  // expected-remark at below {{found an instance of 'read' on resource '<Default>'}}
+  // expected-remark at below {{found an instance of 'write' on resource '<Default>'}}
+  // expected-remark at below {{found an instance of 'allocate' on resource '<Default>'}}
+  // expected-remark at below {{found an instance of 'free' on resource '<Default>'}}
+  omp.single copyprivate(%x -> @copy_i32 : memref<i32>) {
+    omp.terminator
+  }
+  return
+}
+
+func.func @single_bounded_copyprivate_effects(%x: memref<i32>, %y: !llvm.ptr) {
+  // expected-remark at below {{recursive memory effects}}
+  // expected-remark at below {{found an instance of 'read' on resource '<Default>'}}
+  // expected-remark at below {{found an instance of 'write' on resource '<Default>'}}
+  // expected-remark at below {{found an instance of 'read' on op operand 0, on resource '<Default>'}}
+  // expected-remark at below {{found an instance of 'write' on op operand 0, on resource '<Default>'}}
+  // expected-remark at below {{found an instance of 'read' on op operand 1, on resource '<Default>'}}
+  // expected-remark at below {{found an instance of 'write' on op operand 1, on resource '<Default>'}}
+  omp.single copyprivate(%x -> @copy_i32 : memref<i32>, %y -> @copy_ptr : !llvm.ptr) {
+    omp.terminator
+  } <{copyprivate_assume_bounded_recipe_effects}>
+  return
+}
+
+func.func @single_bounded_private_unbounded_copyprivate(%x: memref<i32>, %y: memref<i32>) {
+  // A bounded private clause does not constrain the copy function.
+  // expected-remark at below {{recursive memory effects}}
+  // expected-remark at below {{found an instance of 'read' on resource '<Default>'}}
+  // expected-remark at below {{found an instance of 'write' on resource '<Default>'}}
+  // expected-remark at below {{found an instance of 'allocate' on resource '<Default>'}}
+  // expected-remark at below {{found an instance of 'free' on resource '<Default>'}}
+  omp.single copyprivate(%x -> @copy_i32 : memref<i32>)
+      private(@private_i32 %y -> %py : memref<i32>) {
+    omp.terminator
+  } <{private_assume_bounded_recipe_effects}>
+  return
+}
+
+func.func @single_unbounded_private_bounded_copyprivate(%x: memref<i32>, %y: memref<i32>) {
+  // A bounded copy function does not constrain the private recipe.
+  // expected-remark at below {{recursive memory effects}}
+  // expected-remark at below {{found an instance of 'read' on resource '<Default>'}}
+  // expected-remark at below {{found an instance of 'write' on resource '<Default>'}}
+  // expected-remark at below {{found an instance of 'allocate' on resource '<Default>'}}
+  // expected-remark at below {{found an instance of 'free' on resource '<Default>'}}
+  omp.single copyprivate(%x -> @copy_i32 : memref<i32>)
+      private(@private_i32 %y -> %py : memref<i32>) {
+    omp.terminator
+  } <{copyprivate_assume_bounded_recipe_effects}>
+  return
+}
+
+func.func @single_bounded_private_bounded_copyprivate(%x: memref<i32>, %y: memref<i32>) {
+  // expected-remark at below {{recursive memory effects}}
+  // expected-remark at below {{found an instance of 'read' on resource '<Default>'}}
+  // expected-remark at below {{found an instance of 'write' on resource '<Default>'}}
+  // expected-remark at below {{found an instance of 'read' on op operand 1, on resource '<Default>'}}
+  // expected-remark at below {{found an instance of 'allocate' on block argument 0, on resource '<Default>'}}
+  // expected-remark at below {{found an instance of 'write' on block argument 0, on resource '<Default>'}}
+  // expected-remark at below {{found an instance of 'free' on block argument 0, on resource '<Default>'}}
+  // expected-remark at below {{found an instance of 'read' on op operand 0, on resource '<Default>'}}
+  // expected-remark at below {{found an instance of 'write' on op operand 0, on resource '<Default>'}}
+  omp.single copyprivate(%x -> @copy_i32 : memref<i32>)
+      private(@private_i32 %y -> %py : memref<i32>) {
+    omp.terminator
+  } <{copyprivate_assume_bounded_recipe_effects, private_assume_bounded_recipe_effects}>
+  return
+}
+
+func.func @single_empty_copyprivate_effects() {
+  // expected-remark at below {{recursive memory effects}}
+  // expected-remark at below {{operation has no memory effects}}
+  omp.single nowait {
+    omp.terminator
+  } <{copyprivate_assume_bounded_recipe_effects}>
+  return
+}



More information about the Mlir-commits mailing list