[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 ©Region, 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