[Mlir-commits] [flang] [mlir] [acc] Support for Optionals in firstprivate recipes. (PR #185764)

llvmlistbot at llvm.org llvmlistbot at llvm.org
Tue Mar 10 14:59:34 PDT 2026


https://github.com/nvptm created https://github.com/llvm/llvm-project/pull/185764

Add support for explicit of implicit firstprivates that are Fortran Optional arguments.

>From 8b870c8b9fd58f9501ce57c1e0afa9810d37aa83 Mon Sep 17 00:00:00 2001
From: nvpm <pmathew at nvidia.com>
Date: Fri, 6 Mar 2026 09:42:00 -0800
Subject: [PATCH 1/2] Support firstprivate optionals

---
 .../Support/FIROpenACCTypeInterfaces.h        |  6 +-
 .../OpenACC/Support/FIROpenACCUtils.h         |  5 +-
 flang/lib/Lower/OpenACC.cpp                   |  3 +-
 .../Support/FIROpenACCTypeInterfaces.cpp      | 80 ++++++++++++++----
 .../OpenACC/Support/FIROpenACCUtils.cpp       | 84 ++++++++++++++++---
 .../OpenACC/acc-implicit-firstprivate.fir     | 37 ++++++++
 .../Dialect/OpenACC/OpenACCTypeInterfaces.td  |  6 +-
 mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp       | 43 ++++++----
 8 files changed, 217 insertions(+), 47 deletions(-)

diff --git a/flang/include/flang/Optimizer/OpenACC/Support/FIROpenACCTypeInterfaces.h b/flang/include/flang/Optimizer/OpenACC/Support/FIROpenACCTypeInterfaces.h
index 9d952a6130f0e..3c4339298f5d2 100644
--- a/flang/include/flang/Optimizer/OpenACC/Support/FIROpenACCTypeInterfaces.h
+++ b/flang/include/flang/Optimizer/OpenACC/Support/FIROpenACCTypeInterfaces.h
@@ -85,7 +85,8 @@ struct OpenACCMappableModel
                                   mlir::TypedValue<mlir::acc::MappableType> var,
                                   llvm::StringRef varName,
                                   mlir::ValueRange extents, mlir::Value initVal,
-                                  bool &needsDestroy) const;
+                                  bool &needsDestroy,
+                                  bool isOptional = false) const;
 
   bool generatePrivateDestroy(mlir::Type type, mlir::OpBuilder &builder,
                               mlir::Location loc, mlir::Value privatized,
@@ -95,7 +96,8 @@ struct OpenACCMappableModel
                     mlir::Location loc,
                     mlir::TypedValue<mlir::acc::MappableType> source,
                     mlir::TypedValue<mlir::acc::MappableType> dest,
-                    mlir::ValueRange bounds) const;
+                    mlir::ValueRange bounds,
+                    bool isOptional = false) const;
 
   bool generateCombiner(mlir::Type type, mlir::OpBuilder &mlirBuilder,
                         mlir::Location loc,
diff --git a/flang/include/flang/Optimizer/OpenACC/Support/FIROpenACCUtils.h b/flang/include/flang/Optimizer/OpenACC/Support/FIROpenACCUtils.h
index 360f4eb000a9e..290db5adf981f 100644
--- a/flang/include/flang/Optimizer/OpenACC/Support/FIROpenACCUtils.h
+++ b/flang/include/flang/Optimizer/OpenACC/Support/FIROpenACCUtils.h
@@ -68,11 +68,14 @@ createOrGetPrivateRecipe(mlir::OpBuilder &builder, mlir::Location loc,
 /// \param loc The location
 /// \param ty The type of the variable
 /// \param dataBoundOps Optional bounds for the variable
+/// \param isOptional Whether the variable is Fortran optional (enables null-check
+///        in init/copy; uses a separate recipe with _optional suffix)
 /// \return The existing or created FirstprivateRecipeOp symbol
 mlir::SymbolRefAttr
 createOrGetFirstprivateRecipe(mlir::OpBuilder &builder, mlir::Location loc,
                               mlir::Type ty,
-                              llvm::SmallVector<mlir::Value> &dataBoundOps);
+                              llvm::SmallVector<mlir::Value> &dataBoundOps,
+                              bool isOptional = false);
 
 /// Create or get a reduction recipe for the given type, name and operator.
 /// \param builder The FIR builder
diff --git a/flang/lib/Lower/OpenACC.cpp b/flang/lib/Lower/OpenACC.cpp
index f44e5b2a28f67..184244d5c817c 100644
--- a/flang/lib/Lower/OpenACC.cpp
+++ b/flang/lib/Lower/OpenACC.cpp
@@ -792,8 +792,9 @@ genDataOperandOperations(const Fortran::parser::AccObjectList &objectList,
           builder, operandLocation, baseAddr.getType(), bounds);
       op.setRecipeAttr(recipeAttr);
     } else if constexpr (std::is_same_v<Op, mlir::acc::FirstprivateOp>) {
+      bool isOptional = Fortran::semantics::IsOptional(symbol);
       mlir::SymbolRefAttr recipeAttr = fir::acc::createOrGetFirstprivateRecipe(
-          builder, operandLocation, baseAddr.getType(), bounds);
+          builder, operandLocation, baseAddr.getType(), bounds, isOptional);
       op.setRecipeAttr(recipeAttr);
     }
   }
diff --git a/flang/lib/Optimizer/OpenACC/Support/FIROpenACCTypeInterfaces.cpp b/flang/lib/Optimizer/OpenACC/Support/FIROpenACCTypeInterfaces.cpp
index 5d427c9aab8ab..f5f8b7ad64e90 100644
--- a/flang/lib/Optimizer/OpenACC/Support/FIROpenACCTypeInterfaces.cpp
+++ b/flang/lib/Optimizer/OpenACC/Support/FIROpenACCTypeInterfaces.cpp
@@ -713,17 +713,40 @@ static bool boundsAreAllConstants(mlir::ValueRange bounds) {
   return true;
 }
 
+/// Get the address value used for a null check. Language-agnostic: works for
+/// optional (null when absent) and implicitly firstprivate cases.
+static mlir::Value getAddrForNullCheck(fir::FirOpBuilder &builder,
+                                       mlir::Location loc, mlir::Type type,
+                                       mlir::Value var) {
+  if (mlir::isa<fir::BaseBoxType>(type))
+    return fir::BoxAddrOp::create(builder, loc, var);
+  // For ref, ptr, heap the value itself represents the address
+  return var;
+}
+
 template <typename Ty>
 mlir::Value OpenACCMappableModel<Ty>::generatePrivateInit(
     mlir::Type type, mlir::OpBuilder &mlirBuilder, mlir::Location loc,
     mlir::TypedValue<mlir::acc::MappableType> var, llvm::StringRef varName,
-    mlir::ValueRange bounds, mlir::Value initVal, bool &needsDestroy) const {
+    mlir::ValueRange bounds, mlir::Value initVal, bool &needsDestroy,
+    bool isOptional) const {
   mlir::ModuleOp mod = mlirBuilder.getInsertionBlock()
                            ->getParent()
                            ->getParentOfType<mlir::ModuleOp>();
   assert(mod && "failed to retrieve ModuleOp");
   fir::FirOpBuilder builder(mlirBuilder, mod);
 
+  // When variable is optional: null check (e.g. non-present optional). When
+  // non-optional, skip the conditional to avoid unnecessary branches.
+  std::optional<fir::IfOp> optIfOp;
+  if (isOptional) {
+    mlir::Value addr = getAddrForNullCheck(builder, loc, type, var);
+    mlir::Value cond = builder.genIsNotNullAddr(loc, addr);
+    optIfOp = fir::IfOp::create(builder, loc, mlir::TypeRange{type}, cond,
+                                /*withElseRegion=*/true);
+    builder.setInsertionPointToStart(&optIfOp->getThenRegion().front());
+  }
+
   hlfir::Entity inputVar = hlfir::Entity{var};
   if (inputVar.isPolymorphic())
     TODO(loc, "OpenACC: polymorphic variable privatization");
@@ -819,9 +842,10 @@ mlir::Value OpenACCMappableModel<Ty>::generatePrivateInit(
          "dynamic allocation of the whole base in case of section is not "
          "expected");
 
-  if (inputVar.getType() == alloc.getType() && !allocateSection)
-    return alloc;
-
+  mlir::Value retVal;
+  if (inputVar.getType() == alloc.getType() && !allocateSection) {
+    retVal = alloc;
+  } else {
   // Step4: reconstruct the input variable from the privatized part:
   // - get a mock base address if the privatized part is a section (so that any
   // addressing of the input variable can be replaced by the same addressing of
@@ -875,7 +899,7 @@ mlir::Value OpenACCMappableModel<Ty>::generatePrivateInit(
         builder.createConvert(loc, inputBaseAddrType, mockBase);
   }
 
-  mlir::Value retVal = privateVarBaseAddr;
+  retVal = privateVarBaseAddr;
   if (inputVar.isBoxAddressOrValue()) {
     // Recreate descriptor with same bounds as the input variable.
     mlir::Value shape;
@@ -893,6 +917,15 @@ mlir::Value OpenACCMappableModel<Ty>::generatePrivateInit(
       retVal = box;
     }
   }
+  }
+
+  if (isOptional) {
+    fir::ResultOp::create(builder, loc, retVal);
+    builder.setInsertionPointToStart(&optIfOp->getElseRegion().front());
+    mlir::Value zero = fir::ZeroOp::create(builder, loc, type);
+    fir::ResultOp::create(builder, loc, zero);
+    return optIfOp->getResult(0);
+  }
   return retVal;
 }
 
@@ -900,35 +933,52 @@ template mlir::Value
 OpenACCMappableModel<fir::BaseBoxType>::generatePrivateInit(
     mlir::Type type, mlir::OpBuilder &builder, mlir::Location loc,
     mlir::TypedValue<mlir::acc::MappableType> var, llvm::StringRef varName,
-    mlir::ValueRange extents, mlir::Value initVal, bool &needsDestroy) const;
+    mlir::ValueRange extents, mlir::Value initVal, bool &needsDestroy,
+    bool isOptional) const;
 
 template mlir::Value
 OpenACCMappableModel<fir::ReferenceType>::generatePrivateInit(
     mlir::Type type, mlir::OpBuilder &builder, mlir::Location loc,
     mlir::TypedValue<mlir::acc::MappableType> var, llvm::StringRef varName,
-    mlir::ValueRange extents, mlir::Value initVal, bool &needsDestroy) const;
+    mlir::ValueRange extents, mlir::Value initVal, bool &needsDestroy,
+    bool isOptional) const;
 
 template mlir::Value OpenACCMappableModel<fir::HeapType>::generatePrivateInit(
     mlir::Type type, mlir::OpBuilder &builder, mlir::Location loc,
     mlir::TypedValue<mlir::acc::MappableType> var, llvm::StringRef varName,
-    mlir::ValueRange extents, mlir::Value initVal, bool &needsDestroy) const;
+    mlir::ValueRange extents, mlir::Value initVal, bool &needsDestroy,
+    bool isOptional) const;
 
 template mlir::Value
 OpenACCMappableModel<fir::PointerType>::generatePrivateInit(
     mlir::Type type, mlir::OpBuilder &builder, mlir::Location loc,
     mlir::TypedValue<mlir::acc::MappableType> var, llvm::StringRef varName,
-    mlir::ValueRange extents, mlir::Value initVal, bool &needsDestroy) const;
+    mlir::ValueRange extents, mlir::Value initVal, bool &needsDestroy,
+    bool isOptional) const;
 
 template <typename Ty>
 bool OpenACCMappableModel<Ty>::generateCopy(
     mlir::Type type, mlir::OpBuilder &mlirBuilder, mlir::Location loc,
     mlir::TypedValue<mlir::acc::MappableType> src,
-    mlir::TypedValue<mlir::acc::MappableType> dest,
-    mlir::ValueRange bounds) const {
+    mlir::TypedValue<mlir::acc::MappableType> dest, mlir::ValueRange bounds,
+    bool isOptional) const {
   mlir::ModuleOp mod =
       mlirBuilder.getBlock()->getParent()->getParentOfType<mlir::ModuleOp>();
   assert(mod && "failed to retrieve parent module");
   fir::FirOpBuilder builder(mlirBuilder, mod);
+
+  // When optional: only copy when source is non-null (e.g. present). When
+  // null, destination is already null from init. When non-optional, copy
+  // directly without the conditional.
+  std::optional<fir::IfOp> optIfOp;
+  if (isOptional) {
+    mlir::Value addr = getAddrForNullCheck(builder, loc, type, src);
+    mlir::Value cond = builder.genIsNotNullAddr(loc, addr);
+    optIfOp = fir::IfOp::create(builder, loc, mlir::TypeRange{}, cond,
+                                /*withElseRegion=*/false);
+    builder.setInsertionPointToStart(&optIfOp->getThenRegion().front());
+  }
+
   hlfir::Entity source{src};
   hlfir::Entity destination{dest};
 
@@ -961,19 +1011,19 @@ bool OpenACCMappableModel<Ty>::generateCopy(
 template bool OpenACCMappableModel<fir::BaseBoxType>::generateCopy(
     mlir::Type, mlir::OpBuilder &, mlir::Location,
     mlir::TypedValue<mlir::acc::MappableType>,
-    mlir::TypedValue<mlir::acc::MappableType>, mlir::ValueRange) const;
+    mlir::TypedValue<mlir::acc::MappableType>, mlir::ValueRange, bool) const;
 template bool OpenACCMappableModel<fir::ReferenceType>::generateCopy(
     mlir::Type, mlir::OpBuilder &, mlir::Location,
     mlir::TypedValue<mlir::acc::MappableType>,
-    mlir::TypedValue<mlir::acc::MappableType>, mlir::ValueRange) const;
+    mlir::TypedValue<mlir::acc::MappableType>, mlir::ValueRange, bool) const;
 template bool OpenACCMappableModel<fir::PointerType>::generateCopy(
     mlir::Type, mlir::OpBuilder &, mlir::Location,
     mlir::TypedValue<mlir::acc::MappableType>,
-    mlir::TypedValue<mlir::acc::MappableType>, mlir::ValueRange) const;
+    mlir::TypedValue<mlir::acc::MappableType>, mlir::ValueRange, bool) const;
 template bool OpenACCMappableModel<fir::HeapType>::generateCopy(
     mlir::Type, mlir::OpBuilder &, mlir::Location,
     mlir::TypedValue<mlir::acc::MappableType>,
-    mlir::TypedValue<mlir::acc::MappableType>, mlir::ValueRange) const;
+    mlir::TypedValue<mlir::acc::MappableType>, mlir::ValueRange, bool) const;
 
 template <typename Op>
 static mlir::Value genLogicalCombiner(fir::FirOpBuilder &builder,
diff --git a/flang/lib/Optimizer/OpenACC/Support/FIROpenACCUtils.cpp b/flang/lib/Optimizer/OpenACC/Support/FIROpenACCUtils.cpp
index a53ea9216f7ab..0d02d4e66d06f 100644
--- a/flang/lib/Optimizer/OpenACC/Support/FIROpenACCUtils.cpp
+++ b/flang/lib/Optimizer/OpenACC/Support/FIROpenACCUtils.cpp
@@ -12,6 +12,7 @@
 
 #include "flang/Optimizer/OpenACC/Support/FIROpenACCUtils.h"
 #include "flang/Optimizer/Builder/BoxValue.h"
+#include "flang/Optimizer/Dialect/FortranVariableInterface.h"
 #include "flang/Optimizer/Builder/Complex.h"
 #include "flang/Optimizer/Builder/FIRBuilder.h"
 #include "flang/Optimizer/Dialect/FIROps.h"
@@ -232,7 +233,8 @@ static std::string getRecipeName(mlir::acc::RecipeKind kind, Type type,
                                  const fir::KindMapping &kindMap,
                                  llvm::ArrayRef<Value> bounds,
                                  mlir::acc::ReductionOperator reductionOp =
-                                     mlir::acc::ReductionOperator::AccNone) {
+                                     mlir::acc::ReductionOperator::AccNone,
+                                 bool firstprivateOptional = false) {
   assert(fir::isa_fir_type(type) && "getRecipeName expects a FIR type");
 
   // Build the complete prefix with all components before calling
@@ -259,16 +261,68 @@ static std::string getRecipeName(mlir::acc::RecipeKind kind, Type type,
   if (!bounds.empty())
     prefixOS << getBoundsString(bounds);
 
+  if (firstprivateOptional)
+    prefixOS << "_optional";
+
   return fir::getTypeAsString(type, kindMap, prefixOS.str());
 }
 
+/// Returns true if the variable is optional (e.g. Fortran OPTIONAL dummy).
+/// Used for implicit firstprivate recipe naming so optional variables get
+/// the _optional suffix and null-check in init/copy.
+/// Walks the def-use chain but stops at declare ops to check their optional
+/// attribute (getOriginalDef would follow ViewLike through declare to memref,
+/// losing the optional info).
+static bool isOptionalVariable(Value var) {
+  if (!var)
+    return false;
+  Value current = var;
+  while (current) {
+    Operation *defOp = current.getDefiningOp();
+    if (!defOp)
+      break;
+    // Trace through ACC data clause ops (CopyinOp, etc.) to get the original
+    // var - e.g. when parallel is inside enter data, live-ins are data op
+    // results; the original var carries the optional attribute.
+    if (Value accVar = acc::getVar(defOp)) {
+      current = accVar;
+      continue;
+    }
+    // Check declare ops - they carry the optional attribute.
+    if (auto varIface = dyn_cast<fir::FortranVariableOpInterface>(defOp))
+      return varIface.isOptional();
+    // Local alloca/allocmem are not optional.
+    if (isa<fir::AllocaOp, fir::AllocMemOp>(defOp))
+      return false;
+    // Follow through convert and view-like, but not through declare.
+    if (auto convertOp = dyn_cast<fir::ConvertOp>(defOp)) {
+      current = convertOp.getValue();
+      continue;
+    }
+    if (auto viewLike = dyn_cast<mlir::ViewLikeOpInterface>(defOp)) {
+      current = viewLike.getViewSource();
+      continue;
+    }
+    break;
+  }
+  // Fallback: value may trace to a block arg (function parameter) with
+  // fir.optional; valueHasFirAttribute checks function arg attributes.
+  if (isa<BlockArgument>(current))
+    return fir::valueHasFirAttribute(current, fir::getOptionalAttrName());
+  return false;
+}
+
 std::string fir::acc::getRecipeName(mlir::acc::RecipeKind kind, Type type,
                                     Value var, llvm::ArrayRef<Value> bounds,
                                     mlir::acc::ReductionOperator reductionOp) {
   auto kindMap = var && var.getDefiningOp()
                      ? fir::getKindMapping(var.getDefiningOp())
                      : fir::KindMapping(type.getContext());
-  return ::getRecipeName(kind, type, kindMap, bounds, reductionOp);
+  bool firstprivateOptional =
+      (kind == mlir::acc::RecipeKind::firstprivate_recipe && var &&
+       isOptionalVariable(var));
+  return ::getRecipeName(kind, type, kindMap, bounds, reductionOp,
+                         firstprivateOptional);
 }
 
 /// Get the initial value for reduction operator.
@@ -455,7 +509,8 @@ static RecipeOp genRecipeOp(
     fir::FirOpBuilder &builder, mlir::ModuleOp mod, llvm::StringRef recipeName,
     mlir::Location loc, mlir::Type ty,
     llvm::SmallVector<mlir::Value> &dataOperationBounds, bool allConstantBound,
-    mlir::acc::ReductionOperator op = mlir::acc::ReductionOperator::AccNone) {
+    mlir::acc::ReductionOperator op = mlir::acc::ReductionOperator::AccNone,
+    bool isOptional = false) {
   mlir::OpBuilder modBuilder(mod.getBodyRegion());
   RecipeOp recipe;
   if constexpr (std::is_same_v<RecipeOp, mlir::acc::ReductionRecipeOp>) {
@@ -496,9 +551,14 @@ static RecipeOp genRecipeOp(
   llvm::SmallVector<mlir::Value> initBounds =
       getRecipeBounds(builder, loc, dataOperationBounds,
                       initBlock->getArguments().drop_front(1));
-  mlir::Value retVal = mappableTy.generatePrivateInit(
+  mlir::Value retVal;
+
+  // Optional handling (fir.if + null check) is in generatePrivateInit only
+  // when isOptional is true.
+  retVal = mappableTy.generatePrivateInit(
       builder, loc, mlir::cast<MappableValue>(initBlock->getArgument(0)),
-      initName, initBounds, initValue, needsDestroy);
+      initName, initBounds, initValue, needsDestroy, isOptional);
+
   mlir::acc::YieldOp::create(builder, loc, retVal);
   // Create destroy region and generate destruction if requested.
   if (needsDestroy) {
@@ -552,13 +612,14 @@ fir::acc::createOrGetPrivateRecipe(mlir::OpBuilder &mlirBuilder,
 
 mlir::SymbolRefAttr fir::acc::createOrGetFirstprivateRecipe(
     mlir::OpBuilder &mlirBuilder, mlir::Location loc, mlir::Type ty,
-    llvm::SmallVector<mlir::Value> &dataBoundOps) {
+    llvm::SmallVector<mlir::Value> &dataBoundOps, bool isOptional) {
   mlir::ModuleOp mod =
       mlirBuilder.getBlock()->getParent()->getParentOfType<mlir::ModuleOp>();
   fir::FirOpBuilder builder(mlirBuilder, mod);
   std::string recipeName =
       ::getRecipeName(mlir::acc::RecipeKind::firstprivate_recipe, ty,
-                      builder.getKindMap(), dataBoundOps);
+                      builder.getKindMap(), dataBoundOps,
+                      mlir::acc::ReductionOperator::AccNone, isOptional);
   if (auto recipe =
           mod.lookupSymbol<mlir::acc::FirstprivateRecipeOp>(recipeName))
     return mlir::SymbolRefAttr::get(builder.getContext(), recipe.getSymName());
@@ -566,7 +627,8 @@ mlir::SymbolRefAttr fir::acc::createOrGetFirstprivateRecipe(
   mlir::OpBuilder::InsertionGuard guard(builder);
   bool allConstantBound = fir::acc::areAllBoundsConstant(dataBoundOps);
   auto recipe = genRecipeOp<mlir::acc::FirstprivateRecipeOp>(
-      builder, mod, recipeName, loc, ty, dataBoundOps, allConstantBound);
+      builder, mod, recipeName, loc, ty, dataBoundOps, allConstantBound,
+      mlir::acc::ReductionOperator::AccNone, isOptional);
   auto [source, destination] = genRecipeCombinerOrCopyRegion(
       builder, loc, ty, recipe.getCopyRegion(), dataBoundOps, allConstantBound);
   llvm::SmallVector<mlir::Value> copyBounds =
@@ -576,8 +638,10 @@ mlir::SymbolRefAttr fir::acc::createOrGetFirstprivateRecipe(
   auto mappableTy = mlir::dyn_cast<mlir::acc::MappableType>(ty);
   assert(mappableTy &&
          "Expected that all variable types are considered mappable");
-  [[maybe_unused]] bool success =
-      mappableTy.generateCopy(builder, loc, source, destination, copyBounds);
+  // Optional handling (fir.if + null check) is in generateCopy only when
+  // isOptional is true.
+  [[maybe_unused]] bool success = mappableTy.generateCopy(
+      builder, loc, source, destination, copyBounds, isOptional);
   assert(success && "failed to generate copy");
   mlir::acc::TerminatorOp::create(builder, loc);
   return mlir::SymbolRefAttr::get(builder.getContext(), recipe.getSymName());
diff --git a/flang/test/Transforms/OpenACC/acc-implicit-firstprivate.fir b/flang/test/Transforms/OpenACC/acc-implicit-firstprivate.fir
index d1033d1b580ff..cf7b818318803 100644
--- a/flang/test/Transforms/OpenACC/acc-implicit-firstprivate.fir
+++ b/flang/test/Transforms/OpenACC/acc-implicit-firstprivate.fir
@@ -273,3 +273,40 @@ func.func @test_i16_scalar_in_parallel() {
 // CHECK: %[[FIRSTPRIV:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<i16>) recipe(@firstprivatization_ref_i16) -> !fir.ref<i16> {implicit = true, name = "i16_var"}
 // CHECK: acc.parallel firstprivate(%[[FIRSTPRIV]] : !fir.ref<i16>)
 
+// -----
+
+// Implicit firstprivate for optional dummy: uses _optional recipe with null-check.
+
+// CHECK-LABEL: acc.firstprivate.recipe @firstprivatization_optional_ref_i32 : !fir.ref<i32> init {
+// CHECK:       ^bb0(%{{.*}}: !fir.ref<i32>):
+// CHECK:         fir.if {{.*}} -> (!fir.ref<i32>) {
+// CHECK:           %[[ALLOC:.*]] = fir.alloca i32
+// CHECK:           fir.result %[[ALLOC]] : !fir.ref<i32>
+// CHECK:         } else {
+// CHECK:           fir.zero_bits !fir.ref<i32>
+// CHECK:           fir.result
+// CHECK:         }
+// CHECK:         acc.yield {{.*}} : !fir.ref<i32>
+// CHECK:       } copy {
+// CHECK:       ^bb0(%[[SRC:.*]]: !fir.ref<i32>, %[[DST:.*]]: !fir.ref<i32>):
+// CHECK:         fir.if {{.*}} {
+// CHECK:           {{.*}} = fir.load %[[SRC]] : !fir.ref<i32>
+// CHECK:           hlfir.assign {{.*}} to %[[DST]] temporary_lhs : i32, !fir.ref<i32>
+// CHECK:         }
+// CHECK:         acc.terminator
+// CHECK:       }
+
+// CHECK-LABEL: func.func @test_optional_dummy_in_parallel
+func.func @test_optional_dummy_in_parallel(%arg0: !fir.ref<i32>) {
+  %0 = fir.dummy_scope : !fir.dscope
+  %decl = fir.declare %arg0 dummy_scope %0 arg 1 {fortran_attrs = #fir.var_attrs<optional>, uniq_name = "x"} : (!fir.ref<i32>, !fir.dscope) -> !fir.ref<i32>
+  acc.parallel {
+    %load = fir.load %decl : !fir.ref<i32>
+    acc.yield
+  }
+  return
+}
+
+// CHECK: %[[FIRSTPRIV:.*]] = acc.firstprivate varPtr(%{{.*}} : !fir.ref<i32>) recipe(@firstprivatization_optional_ref_i32) -> !fir.ref<i32> {implicit = true, name = "x"}
+// CHECK: acc.parallel firstprivate(%[[FIRSTPRIV]] : !fir.ref<i32>)
+
diff --git a/mlir/include/mlir/Dialect/OpenACC/OpenACCTypeInterfaces.td b/mlir/include/mlir/Dialect/OpenACC/OpenACCTypeInterfaces.td
index 97def012990c0..fd8e648463c77 100644
--- a/mlir/include/mlir/Dialect/OpenACC/OpenACCTypeInterfaces.td
+++ b/mlir/include/mlir/Dialect/OpenACC/OpenACCTypeInterfaces.td
@@ -387,7 +387,8 @@ def OpenACC_MappableTypeInterface : TypeInterface<"MappableType"> {
                     "::llvm::StringRef":$varName,
                     "::mlir::ValueRange":$extents,
                     "::mlir::Value":$initVal,
-                    "bool &":$needsDestroy),
+                    "bool &":$needsDestroy,
+                    "bool":$isOptional),
       /*methodBody=*/"",
       /*defaultImplementation=*/[{
         return {};
@@ -402,7 +403,8 @@ def OpenACC_MappableTypeInterface : TypeInterface<"MappableType"> {
                     "::mlir::Location":$loc,
                     "::mlir::TypedValue<::mlir::acc::MappableType>":$from,
                     "::mlir::TypedValue<::mlir::acc::MappableType>":$to,
-                    "::mlir::ValueRange":$bounds),
+                    "::mlir::ValueRange":$bounds,
+                    "bool":$isOptional),
       /*methodBody=*/"",
       /*defaultImplementation=*/[{
         return false;
diff --git a/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp b/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp
index 800305ffb36c5..1dd3826d25915 100644
--- a/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp
+++ b/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp
@@ -1543,10 +1543,12 @@ struct RemoveConstantIfConditionWithRegion : public OpRewritePattern<OpTy> {
 /// Create and populate an init region for privatization recipes.
 /// Returns success if the region is populated, failure otherwise.
 /// Sets needsFree to indicate if the allocated memory requires deallocation.
+/// When isOptional is true (e.g. recipe name ends with _optional), init uses
+/// null-check for Fortran optional variables.
 static LogicalResult createInitRegion(OpBuilder &builder, Location loc,
                                       Region &initRegion, Type varType,
                                       StringRef varName, ValueRange bounds,
-                                      bool &needsFree) {
+                                      bool &needsFree, bool isOptional = false) {
   // Create init block with arguments: original value + bounds
   SmallVector<Type> argTypes{varType};
   SmallVector<Location> argLocs{loc};
@@ -1569,7 +1571,7 @@ static LogicalResult createInitRegion(OpBuilder &builder, Location loc,
     auto mappableTy = cast<MappableType>(varType);
     auto typedVar = cast<TypedValue<MappableType>>(blockArgVar);
     privatizedValue = mappableTy.generatePrivateInit(
-        builder, loc, typedVar, varName, bounds, {}, needsFree);
+        builder, loc, typedVar, varName, bounds, {}, needsFree, isOptional);
     if (!privatizedValue)
       return failure();
   } else {
@@ -1590,10 +1592,12 @@ static LogicalResult createInitRegion(OpBuilder &builder, Location loc,
 
 /// Create and populate a copy region for firstprivate recipes.
 /// Returns success if the region is populated, failure otherwise.
-/// TODO: Handle MappableType - it does not yet have a copy API.
+/// When isOptional is true (e.g. recipe name ends with _optional), copy uses
+/// null-check for Fortran optional variables via MappableType::generateCopy.
 static LogicalResult createCopyRegion(OpBuilder &builder, Location loc,
                                       Region &copyRegion, Type varType,
-                                      ValueRange bounds) {
+                                      ValueRange bounds,
+                                      bool isOptional = false) {
   // Create copy block with arguments: original value + privatized value +
   // bounds
   SmallVector<Type> copyArgTypes{varType, varType};
@@ -1607,24 +1611,28 @@ static LogicalResult createCopyRegion(OpBuilder &builder, Location loc,
   copyBlock->addArguments(copyArgTypes, copyArgLocs);
   builder.setInsertionPointToStart(copyBlock);
 
+  Value originalArg = copyBlock->getArgument(0);
+  Value privatizedArg = copyBlock->getArgument(1);
+
   bool isMappable = isa<MappableType>(varType);
   bool isPointerLike = isa<PointerLikeType>(varType);
-  // TODO: Handle MappableType - it does not yet have a copy API.
-  // Otherwise, for now just fallback to pointer-like behavior.
-  if (isMappable && !isPointerLike)
-    return failure();
 
-  // Generate copy region body based on variable type
-  if (isPointerLike) {
+  // When optional, use MappableType::generateCopy for null-check handling.
+  if (isMappable && isOptional) {
+    auto mappableTy = cast<MappableType>(varType);
+    if (!mappableTy.generateCopy(
+            builder, loc, cast<TypedValue<MappableType>>(originalArg),
+            cast<TypedValue<MappableType>>(privatizedArg), bounds, isOptional))
+      return failure();
+  } else if (isPointerLike) {
     auto pointerLikeTy = cast<PointerLikeType>(varType);
-    Value originalArg = copyBlock->getArgument(0);
-    Value privatizedArg = copyBlock->getArgument(1);
-
-    // Generate copy operation using PointerLikeType interface
     if (!pointerLikeTy.genCopy(
             builder, loc, cast<TypedValue<PointerLikeType>>(privatizedArg),
             cast<TypedValue<PointerLikeType>>(originalArg), varType))
       return failure();
+  } else if (isMappable && !isPointerLike) {
+    // MappableType without optional handling - not yet supported for copy
+    return failure();
   }
 
   // Add terminator to copy block
@@ -1821,6 +1829,9 @@ FirstprivateRecipeOp::createAndPopulate(OpBuilder &builder, Location loc,
   if (!isMappable && !isPointerLike)
     return std::nullopt;
 
+  // Derive isOptional from recipe name (e.g. firstprivatization_optional_ref_i32)
+  bool isOptional = recipeName.contains("_optional");
+
   OpBuilder::InsertionGuard guard(builder);
 
   // Create the recipe operation first so regions have proper parent context
@@ -1829,14 +1840,14 @@ FirstprivateRecipeOp::createAndPopulate(OpBuilder &builder, Location loc,
   // Populate the init region
   bool needsFree = false;
   if (failed(createInitRegion(builder, loc, recipe.getInitRegion(), varType,
-                              varName, bounds, needsFree))) {
+                              varName, bounds, needsFree, isOptional))) {
     recipe.erase();
     return std::nullopt;
   }
 
   // Populate the copy region
   if (failed(createCopyRegion(builder, loc, recipe.getCopyRegion(), varType,
-                              bounds))) {
+                              bounds, isOptional))) {
     recipe.erase();
     return std::nullopt;
   }

>From 8e081148c2fea75ccacfa79a91ce49ec34af8dab Mon Sep 17 00:00:00 2001
From: nvpm <pmathew at nvidia.com>
Date: Tue, 10 Mar 2026 14:20:03 -0700
Subject: [PATCH 2/2] f90 tests

---
 .../OpenACC/acc-firstprivate-optional.f90     | 31 ++++++++++++++
 .../acc-implicit-firstprivate-optional.f90    | 42 +++++++++++++++++++
 2 files changed, 73 insertions(+)
 create mode 100644 flang/test/Transforms/OpenACC/acc-firstprivate-optional.f90
 create mode 100644 flang/test/Transforms/OpenACC/acc-implicit-firstprivate-optional.f90

diff --git a/flang/test/Transforms/OpenACC/acc-firstprivate-optional.f90 b/flang/test/Transforms/OpenACC/acc-firstprivate-optional.f90
new file mode 100644
index 0000000000000..644fd8e1ca945
--- /dev/null
+++ b/flang/test/Transforms/OpenACC/acc-firstprivate-optional.f90
@@ -0,0 +1,31 @@
+! Test lowering of firstprivate on optional dummy arguments.
+! Optional variables use a separate recipe with _optional suffix and null-check
+! in init/copy regions.
+
+! RUN: bbc -fopenacc -emit-hlfir %s -o - | FileCheck %s
+
+subroutine test_optional_firstprivate(x)
+  integer, optional :: x
+
+  !$acc parallel firstprivate(x)
+  !$acc end parallel
+end subroutine
+
+! CHECK-LABEL: acc.firstprivate.recipe @firstprivatization_optional_ref_i32 : !fir.ref<i32> init {
+! CHECK:       ^bb0(%{{.*}}: !fir.ref<i32>):
+! CHECK:         fir.if {{.*}} -> (!fir.ref<i32>) {
+! CHECK:           %[[ALLOC:.*]] = fir.alloca i32
+! CHECK:           fir.result %[[ALLOC]] : !fir.ref<i32>
+! CHECK:         } else {
+! CHECK:           fir.zero_bits !fir.ref<i32>
+! CHECK:           fir.result
+! CHECK:         }
+! CHECK:         acc.yield {{.*}} : !fir.ref<i32>
+! CHECK:       } copy {
+! CHECK:       ^bb0(%[[SRC:.*]]: !fir.ref<i32>, %[[DST:.*]]: !fir.ref<i32>):
+! CHECK:         fir.if {{.*}} {
+! CHECK:           {{.*}} = fir.load %[[SRC]] : !fir.ref<i32>
+! CHECK:           hlfir.assign {{.*}} to %[[DST]] {{.*}}
+! CHECK:         }
+! CHECK:         acc.terminator
+! CHECK:       }
diff --git a/flang/test/Transforms/OpenACC/acc-implicit-firstprivate-optional.f90 b/flang/test/Transforms/OpenACC/acc-implicit-firstprivate-optional.f90
new file mode 100644
index 0000000000000..80a29af783261
--- /dev/null
+++ b/flang/test/Transforms/OpenACC/acc-implicit-firstprivate-optional.f90
@@ -0,0 +1,42 @@
+! Test implicit firstprivate for optional dummy arguments.
+! An optional scalar used in acc parallel without an explicit data clause gets
+! implicit firstprivate. The recipe must use _optional suffix and emit fir.if
+! null-check in init/copy regions.
+
+! RUN: bbc -fopenacc -emit-hlfir %s -o - \
+! RUN:   | fir-opt --pass-pipeline="builtin.module(acc-initialize-fir-analyses,acc-implicit-data)" \
+! RUN:   -o - | FileCheck %s
+
+subroutine test_optional_implicit_firstprivate(x)
+  integer, optional :: x
+  integer :: val
+
+  ! No explicit firstprivate - x gets implicit firstprivate as a scalar in parallel
+  ! x must be used in the region; assign to val so x becomes a live-in
+  !$acc parallel
+    val = 0
+    if (present(x)) val = x
+  !$acc end parallel
+end subroutine
+
+! CHECK-LABEL: acc.firstprivate.recipe @firstprivatization_optional_ref_i32
+! CHECK:       ^bb0(%{{.*}}: !fir.ref<i32>):
+! CHECK:         fir.if {{.*}} -> (!fir.ref<i32>) {
+! CHECK:           {{.*}} = fir.alloca i32
+! CHECK:           fir.result {{.*}} : !fir.ref<i32>
+! CHECK:         } else {
+! CHECK:           {{.*}} = fir.zero_bits !fir.ref<i32>
+! CHECK:           fir.result
+! CHECK:         }
+! CHECK:         acc.yield {{.*}} : !fir.ref<i32>
+! CHECK:       } copy {
+! CHECK:       ^bb0(%{{.*}}: !fir.ref<i32>, %{{.*}}: !fir.ref<i32>):
+! CHECK:         fir.if {{.*}} {
+! CHECK:           {{.*}} = fir.load {{.*}} : !fir.ref<i32>
+! CHECK:           hlfir.assign {{.*}} to {{.*}} {{.*}}
+! CHECK:         }
+! CHECK:         acc.terminator
+! CHECK:       }
+
+! CHECK: acc.firstprivate varPtr(%{{.*}} : !fir.ref<i32>) recipe(@firstprivatization_optional_ref_i32) -> !fir.ref<i32> {implicit = true, name = "x"}
+! CHECK: acc.parallel firstprivate(%{{.*}}, %{{.*}} : !fir.ref<i32>, !fir.ref<i32>)



More information about the Mlir-commits mailing list