[flang-commits] [flang] [DRAFT] [flang][OpenMP] Support loop-associated metadirective variants (part 3) (PR #195344)
via flang-commits
flang-commits at lists.llvm.org
Fri Jun 26 11:46:00 PDT 2026
https://github.com/chichunchen updated https://github.com/llvm/llvm-project/pull/195344
>From a5e03cf157a7f68f5c923a71b889590884439f33 Mon Sep 17 00:00:00 2001
From: "Chi Chun, Chen" <chichun.chen at hpe.com>
Date: Wed, 24 Jun 2026 14:48:09 -0500
Subject: [PATCH 1/3] [flang][OpenMP] Support lowering of metadirective (part
3)
Enable metadirective lowering for loop-associated variants such as
`do`, `simd`, `parallel do`, and `do simd`.
When a metadirective resolves to a loop-associated directive, the
associated DO construct is made available to the selected variant so
existing OpenMP loop lowering can process it. Standalone metadirectives
use the following sibling DO evaluation, while begin/end metadirectives
use the first substantive nested evaluation from the metadirective block.
Loop induction-variable data-sharing attributes are marked during
lowering because semantic analysis cannot know which variant will be
selected. The temporary attributes are scoped to the selected variant so
DSA state does not leak between alternatives.
Dynamic metadirective selection also preserves the associated DO
evaluation when a runtime condition may choose either a loop-associated
variant or a standalone fallback. This keeps fallback lowering from
dropping the original loop body after another candidate has consumed the
loop.
This patch also uses the normal affected-loop-depth calculation for
selected variants, so `collapse(n)` and `ordered(n)` mark all affected
induction variables. SIMD variants are identified from their leaf
constructs so standalone `simd` preserves its implicit linear
induction-variable semantics.
This patch is part of the feature work for #188820 and builds on
[#194424](https://github.com/llvm/llvm-project/pull/194424).
Assisted with Copilot and GPT-5.
---
.../lib/Lower/OpenMP/DataSharingProcessor.cpp | 90 ++++-
flang/lib/Lower/OpenMP/OpenMP.cpp | 180 +++++++++-
flang/lib/Lower/OpenMP/Utils.cpp | 17 +
flang/lib/Lower/OpenMP/Utils.h | 2 +
.../Lower/OpenMP/Todo/metadirective-loop.f90 | 12 -
.../OpenMP/Todo/metadirective-module-loop.f90 | 7 +
.../OpenMP/Todo/metadirective-no-loop.f90 | 12 +
.../OpenMP/Todo/metadirective-target-loop.f90 | 15 +
.../test/Lower/OpenMP/metadirective-loop.f90 | 339 ++++++++++++++++++
9 files changed, 652 insertions(+), 22 deletions(-)
delete mode 100644 flang/test/Lower/OpenMP/Todo/metadirective-loop.f90
create mode 100644 flang/test/Lower/OpenMP/Todo/metadirective-module-loop.f90
create mode 100644 flang/test/Lower/OpenMP/Todo/metadirective-no-loop.f90
create mode 100644 flang/test/Lower/OpenMP/Todo/metadirective-target-loop.f90
create mode 100644 flang/test/Lower/OpenMP/metadirective-loop.f90
diff --git a/flang/lib/Lower/OpenMP/DataSharingProcessor.cpp b/flang/lib/Lower/OpenMP/DataSharingProcessor.cpp
index c97d054e1210b..7a28986c7feb0 100644
--- a/flang/lib/Lower/OpenMP/DataSharingProcessor.cpp
+++ b/flang/lib/Lower/OpenMP/DataSharingProcessor.cpp
@@ -41,8 +41,46 @@ bool DataSharingProcessor::OMPConstructSymbolVisitor::isSymbolDefineBy(
const semantics::Symbol *symbol, lower::pft::Evaluation &eval) const {
return eval.visit(common::visitors{
[&](const parser::OpenMPConstruct &functionParserNode) {
+ if (symDefMap.count(symbol) &&
+ symDefMap.at(symbol) == ConstructPtr(&functionParserNode))
+ return true;
+
+ // For metadirectives on standalone or delimited constructs, the
+ // construct itself does not introduce privatization. As a result,
+ // symbols from nested (spliced or block-owned) DO evaluations may be
+ // associated with a null OpenMPConstruct pointer. This is expected and
+ // should be accepted.
+ bool isMetadirective =
+ std::holds_alternative<parser::OmpDelimitedMetadirectiveDirective>(
+ functionParserNode.u);
+ if (const auto *standalone =
+ std::get_if<parser::OpenMPStandaloneConstruct>(
+ &functionParserNode.u)) {
+ isMetadirective =
+ isMetadirective ||
+ std::holds_alternative<parser::OmpMetadirectiveDirective>(
+ standalone->u);
+ }
+ if (isMetadirective)
+ return symDefMap.count(symbol) &&
+ symDefMap.at(symbol) ==
+ ConstructPtr(
+ static_cast<const parser::OpenMPConstruct *>(nullptr));
+ return false;
+ },
+ [&](const parser::OpenMPDeclarativeConstruct &functionParserNode) {
+ // Metadirective variants are resolved during lowering, so the eval
+ // is an OpenMPDeclarativeConstruct rather than an OpenMPConstruct.
+ // The visitor does not track declarative constructs, so symbols
+ // referenced at the top level of the eval (e.g. loop IVs from a
+ // spliced DO) are mapped to a null OpenMPConstruct pointer.
+ if (!std::holds_alternative<parser::OmpMetadirectiveDirective>(
+ functionParserNode.u))
+ return false;
return symDefMap.count(symbol) &&
- symDefMap.at(symbol) == ConstructPtr(&functionParserNode);
+ symDefMap.at(symbol) ==
+ ConstructPtr(
+ static_cast<const parser::OpenMPConstruct *>(nullptr));
},
[](const auto &functionParserNode) { return false; }});
}
@@ -77,6 +115,14 @@ DataSharingProcessor::DataSharingProcessor(
eval.visit([&](const auto &functionParserNode) {
parser::Walk(functionParserNode, visitor);
});
+ // For metadirective evaluations, the associated DO loop is spliced into the
+ // evaluation tree but is not part of the metadirective's parse tree. Walk
+ // nested evaluations' parse trees so the visitor can track their symbols
+ // (e.g. loop iteration variables).
+ if (isMetadirectiveEval(eval) && eval.hasNestedEvaluations()) {
+ for (auto &nestedEval : eval.getNestedEvaluations())
+ nestedEval.visit([&](const auto &node) { parser::Walk(node, visitor); });
+ }
}
DataSharingProcessor::DataSharingProcessor(lower::AbstractConverter &converter,
@@ -220,8 +266,27 @@ void DataSharingProcessor::copyFirstPrivateSymbol(
void DataSharingProcessor::copyLastPrivateSymbol(
const semantics::Symbol *sym, mlir::OpBuilder::InsertPoint *lastPrivIP) {
- if (sym->test(semantics::Symbol::Flag::OmpLastPrivate))
+ if (!sym->test(semantics::Symbol::Flag::OmpLastPrivate))
+ return;
+
+ if (sym->has<semantics::HostAssocDetails>()) {
converter.copyHostAssociateVar(*sym, lastPrivIP, /*hostIsSource=*/false);
+ return;
+ }
+
+ // Metadirective loop IVs can be marked lastprivate during lowering, after
+ // semantic host-association symbols would normally be created. Copy from the
+ // private binding back to the one-level-up binding directly.
+ mlir::OpBuilder::InsertionGuard guard(firOpBuilder);
+ if (lastPrivIP)
+ firOpBuilder.restoreInsertionPoint(*lastPrivIP);
+ lower::SymbolBox hostBox = converter.lookupOneLevelUpSymbol(*sym);
+ lower::SymbolBox privBox = converter.shallowLookupSymbol(*sym);
+ assert(hostBox && privBox &&
+ "expected symbol bindings for lastprivate loop IV");
+ if (hostBox.getAddr() != privBox.getAddr())
+ converter.copyVar(converter.getCurrentLocation(), hostBox.getAddr(),
+ privBox.getAddr(), fir::FortranVariableFlagsEnum::None);
}
void DataSharingProcessor::collectOmpObjectListSymbol(
@@ -542,6 +607,19 @@ void DataSharingProcessor::collectPrivatizedSymbols(
llvm::SetVector<const semantics::Scope *> clauseScopes;
(void)collectScopes(semaCtx, eval, clauseScopes);
+ // For metadirective evaluations, the source range only covers the directive
+ // clauses, not the spliced DO loop. The scope found from that narrow range
+ // may not include parent scopes where the loop IV is declared (e.g. the
+ // function scope when the metadirective is inside a target region). Walk up
+ // the scope chain to include all ancestor scopes.
+ if (isMetadirectiveEval(eval) && !clauseScopes.empty()) {
+ const semantics::Scope *scope = *clauseScopes.begin();
+ while (scope->kind() != semantics::Scope::Kind::Global) {
+ clauseScopes.insert(scope);
+ scope = &scope->parent();
+ }
+ }
+
for (const auto *sym : allSymbols) {
if (semantics::omp::IsPrivatizable(*sym) &&
!symbolsInNestedRegions.contains(sym) &&
@@ -571,6 +649,14 @@ void DataSharingProcessor::collectSymbols(
/*collectSymbols=*/true,
/*collectHostAssociatedSymbols=*/true);
+ // Collect symbols from spliced nested evaluations for metadirectives.
+ if (isMetadirectiveEval(eval) && eval.hasNestedEvaluations()) {
+ for (auto &nestedEval : eval.getNestedEvaluations())
+ converter.collectSymbolSet(nestedEval, allSymbols, flag,
+ /*collectSymbols=*/true,
+ /*collectHostAssociatedSymbols=*/true);
+ }
+
llvm::SetVector<const semantics::Symbol *> symbolsInNestedRegions;
collectSymbolsInNestedRegions(eval, flag, symbolsInNestedRegions);
diff --git a/flang/lib/Lower/OpenMP/OpenMP.cpp b/flang/lib/Lower/OpenMP/OpenMP.cpp
index 4b83ac68ebf44..a9b0bb31ecd52 100644
--- a/flang/lib/Lower/OpenMP/OpenMP.cpp
+++ b/flang/lib/Lower/OpenMP/OpenMP.cpp
@@ -4802,6 +4802,135 @@ struct MetadirectiveCandidate {
};
} // namespace
+/// A loop-associated metadirective is lowered like a real loop construct, but
+/// the PFT leaves its associated loop nest as the following sibling instead of
+/// nesting it underneath. Splice that sibling into the metadirective's own
+/// nested evaluations so the shared loop-lowering path can find it. Return a
+/// nullptr if no associated DO loop follows.
+static lower::pft::Evaluation *
+spliceAssociatedDoEval(lower::pft::Evaluation &eval) {
+ if (eval.hasNestedEvaluations()) {
+ auto nestedIt =
+ llvm::find_if(eval.getNestedEvaluations(), [](auto &nested) {
+ return !nested.isEndStmt() &&
+ !nested.template getIf<parser::CompilerDirective>();
+ });
+ if (nestedIt != eval.getNestedEvaluations().end())
+ return nestedIt->getIf<parser::DoConstruct>() ? &*nestedIt : nullptr;
+ return nullptr;
+ }
+
+ // A metadirective in a specification part (e.g. at module scope) has no
+ // parent construct and no owning procedure, so there is no sibling list.
+ lower::pft::FunctionLikeUnit *owningProc = eval.getOwningProcedure();
+ if (!eval.parentConstruct && !owningProc)
+ return nullptr;
+ auto *parentList = eval.parentConstruct
+ ? eval.parentConstruct->evaluationList.get()
+ : &owningProc->evaluationList;
+ auto metaIt = llvm::find_if(
+ *parentList, [&](lower::pft::Evaluation &e) { return &e == &eval; });
+ assert(metaIt != parentList->end() &&
+ "metadirective eval not found in parent list");
+
+ auto loopIt = std::next(metaIt);
+ while (loopIt != parentList->end() &&
+ (loopIt->isEndStmt() || loopIt->getIf<parser::CompilerDirective>()))
+ ++loopIt;
+
+ if (loopIt == parentList->end() || !loopIt->getIf<parser::DoConstruct>())
+ return nullptr;
+
+ eval.evaluationList->splice(eval.evaluationList->end(), *parentList, loopIt);
+ return &eval.getNestedEvaluations().back();
+}
+
+static bool hasLoopAssociatedDirective(const ConstructQueue &queue) {
+ return llvm::any_of(queue, [](const auto &item) {
+ return llvm::omp::getDirectiveAssociation(item.id) ==
+ llvm::omp::Association::LoopNest;
+ });
+}
+
+static bool consumesAssociatedEvaluation(const ConstructQueue &queue) {
+ return llvm::any_of(queue, [](const auto &item) {
+ return llvm::omp::getDirectiveAssociation(item.id) !=
+ llvm::omp::Association::None;
+ });
+}
+
+/// Clear all existing DSA flags on \p sym, then set PreDetermined + \p dsa.
+static void applySymbolDSA(semantics::Symbol &sym,
+ semantics::Symbol::Flag dsa) {
+ using Symbol = semantics::Symbol;
+ Symbol::Flags dataSharingAttributeFlags{
+ Symbol::Flag::OmpShared, Symbol::Flag::OmpPrivate,
+ Symbol::Flag::OmpFirstPrivate, Symbol::Flag::OmpLastPrivate,
+ Symbol::Flag::OmpReduction, Symbol::Flag::OmpLinear};
+ sym.flags() &=
+ ~(dataSharingAttributeFlags |
+ Symbol::Flags{Symbol::Flag::OmpExplicit, Symbol::Flag::OmpImplicit,
+ Symbol::Flag::OmpPreDetermined});
+ sym.flags() |= Symbol::Flags{Symbol::Flag::OmpPreDetermined, dsa};
+}
+
+class SymbolDSAGuard {
+public:
+ ~SymbolDSAGuard() {
+ for (auto &[sym, flags] : llvm::reverse(savedFlags))
+ sym->flags() = flags;
+ }
+
+ void setSymbolDSA(semantics::Symbol &sym, semantics::Symbol::Flag dsa) {
+ if (!llvm::any_of(savedFlags,
+ [&](const auto &entry) { return entry.first == &sym; }))
+ savedFlags.emplace_back(&sym, sym.flags());
+ applySymbolDSA(sym, dsa);
+ }
+
+private:
+ llvm::SmallVector<std::pair<semantics::Symbol *, semantics::Symbol::Flags>, 4>
+ savedFlags;
+};
+
+/// Mark loop induction variable data-sharing attributes for a
+/// metadirective-selected loop variant. Semantic analysis cannot mark these
+/// because the variant is resolved at lowering time.
+static void
+markMetadirectiveLoopIVs(semantics::SemanticsContext &semaCtx,
+ const parser::OmpDirectiveSpecification &spec,
+ lower::pft::Evaluation &loopEval,
+ SymbolDSAGuard &dsaGuard) {
+ using Symbol = semantics::Symbol;
+
+ auto [depth, _] = semantics::omp::GetAffectedNestDepthWithReason(
+ spec, semaCtx.langOptions().OpenMPVersion, &semaCtx);
+ if (!depth || !depth.value || *depth.value <= 0)
+ return;
+
+ int64_t affectedDepth = *depth.value;
+ bool isSimdVariant =
+ llvm::is_contained(llvm::omp::getLeafConstructsOrSelf(spec.DirId()),
+ llvm::omp::Directive::OMPD_simd);
+ Symbol::Flag ivDSA;
+ if (!isSimdVariant)
+ ivDSA = Symbol::Flag::OmpPrivate;
+ else if (affectedDepth == 1 && semaCtx.langOptions().OpenMPVersion < 60)
+ ivDSA = Symbol::Flag::OmpLinear;
+ else
+ ivDSA = Symbol::Flag::OmpLastPrivate;
+
+ lower::pft::Evaluation *doEval = &loopEval;
+ for (int64_t level = 0; level < affectedDepth; ++level) {
+ assert(doEval->getIf<parser::DoConstruct>() &&
+ "expected associated DO construct");
+ if (semantics::Symbol *sym = getIterationVariableSymbol(*doEval))
+ dsaGuard.setSymbolDSA(*sym, ivDSA);
+ if (level + 1 < affectedDepth)
+ doEval = getNestedDoConstruct(*doEval);
+ }
+}
+
static void genMetadirective(lower::AbstractConverter &converter,
lower::SymMap &symTable,
semantics::SemanticsContext &semaCtx,
@@ -4974,23 +5103,41 @@ static void genMetadirective(lower::AbstractConverter &converter,
}
}
+ auto makeVariantQueue = [&](const parser::OmpDirectiveSpecification &spec) {
+ List<Clause> variantClauses = makeClauses(spec.Clauses(), semaCtx);
+ return ConstructQueue{
+ buildConstructQueue(converter.getFirOpBuilder().getModule(), semaCtx,
+ eval, spec.source, spec.DirId(), variantClauses)};
+ };
+
+ bool hasLoopAssociatedCandidate = false;
+ for (const MetadirectiveCandidate &candidate : candidates) {
+ if (candidate.spec &&
+ hasLoopAssociatedDirective(makeVariantQueue(*candidate.spec))) {
+ hasLoopAssociatedCandidate = true;
+ break;
+ }
+ }
+ if (!hasLoopAssociatedCandidate && fallback)
+ hasLoopAssociatedCandidate =
+ hasLoopAssociatedDirective(makeVariantQueue(*fallback));
+ if (hasLoopAssociatedCandidate)
+ (void)spliceAssociatedDoEval(eval);
+
// Lower a single resolved candidate.
auto genVariant = [&](const parser::OmpDirectiveSpecification *spec) {
if (!spec) {
genNestedEvaluations(converter, eval);
return;
}
- List<Clause> variantClauses = makeClauses(spec->Clauses(), semaCtx);
mlir::Location variantLoc = converter.genLocation(spec->source);
- ConstructQueue queue{
- buildConstructQueue(converter.getFirOpBuilder().getModule(), semaCtx,
- eval, spec->source, spec->DirId(), variantClauses)};
+ ConstructQueue queue = makeVariantQueue(*spec);
if (llvm::any_of(queue, [](const auto &item) {
- return llvm::omp::getDirectiveAssociation(item.id) ==
- llvm::omp::Association::LoopNest;
+ return llvm::omp::allTargetSet.test(item.id);
})) {
- TODO(variantLoc, "loop-associated METADIRECTIVE variant");
+ TODO(variantLoc,
+ "TARGET construct selected by METADIRECTIVE (host-eval)");
}
if (llvm::any_of(queue, [](const auto &item) {
@@ -5002,8 +5149,22 @@ static void genMetadirective(lower::AbstractConverter &converter,
TODO(variantLoc, "declarative METADIRECTIVE variant");
}
+ bool hasLoopAssociation = hasLoopAssociatedDirective(queue);
+ if (hasLoopAssociation) {
+ lower::pft::Evaluation *loopEval = spliceAssociatedDoEval(eval);
+ if (!loopEval)
+ TODO(variantLoc, "loop-associated METADIRECTIVE without associated DO");
+ SymbolDSAGuard dsaGuard;
+ markMetadirectiveLoopIVs(semaCtx, *spec, *loopEval, dsaGuard);
+ genOMPDispatch(converter, symTable, semaCtx, eval, variantLoc, queue,
+ queue.begin());
+ return;
+ }
+
genOMPDispatch(converter, symTable, semaCtx, eval, variantLoc, queue,
queue.begin());
+ if (!consumesAssociatedEvaluation(queue) && eval.hasNestedEvaluations())
+ genNestedEvaluations(converter, eval);
};
auto selectBestCandidate =
@@ -5518,7 +5679,10 @@ void Fortran::lower::genOpenMPDeclarativeConstruct(
semantics::SemanticsContext &semaCtx, lower::pft::Evaluation &eval,
const parser::OpenMPDeclarativeConstruct &omp) {
genOMP(converter, symTable, semaCtx, eval, omp);
- genNestedEvaluations(converter, eval);
+ // Metadirective lowering selects a variant and consumes its associated
+ // evaluations itself.
+ if (!isMetadirectiveEval(eval))
+ genNestedEvaluations(converter, eval);
}
void Fortran::lower::genOpenMPSymbolProperties(
diff --git a/flang/lib/Lower/OpenMP/Utils.cpp b/flang/lib/Lower/OpenMP/Utils.cpp
index 382292b6c6c13..c145612e6e91c 100644
--- a/flang/lib/Lower/OpenMP/Utils.cpp
+++ b/flang/lib/Lower/OpenMP/Utils.cpp
@@ -725,6 +725,23 @@ pft::Evaluation *getNestedDoConstruct(pft::Evaluation &eval) {
llvm_unreachable("Expected do loop to be in the nested evaluations");
}
+/// Return true if \p eval holds a metadirective.
+bool isMetadirectiveEval(lower::pft::Evaluation &eval) {
+ if (const auto *decl = eval.getIf<parser::OpenMPDeclarativeConstruct>())
+ return std::holds_alternative<parser::OmpMetadirectiveDirective>(decl->u);
+ if (const auto *ompConstruct = eval.getIf<parser::OpenMPConstruct>()) {
+ if (std::holds_alternative<parser::OmpDelimitedMetadirectiveDirective>(
+ ompConstruct->u))
+ return true;
+ if (const auto *standalone =
+ std::get_if<parser::OpenMPStandaloneConstruct>(&ompConstruct->u)) {
+ return std::holds_alternative<parser::OmpMetadirectiveDirective>(
+ standalone->u);
+ }
+ }
+ return false;
+}
+
/// Populates the sizes vector with values if the given OpenMPConstruct
/// contains a loop construct with an inner tiling construct.
void collectTileSizesFromOpenMPConstruct(
diff --git a/flang/lib/Lower/OpenMP/Utils.h b/flang/lib/Lower/OpenMP/Utils.h
index efe6c963a3778..4b268d7df4272 100644
--- a/flang/lib/Lower/OpenMP/Utils.h
+++ b/flang/lib/Lower/OpenMP/Utils.h
@@ -277,6 +277,8 @@ class FlangOMPContext final : public llvm::omp::OMPContext {
mlir::LLVM::TargetFeaturesAttr targetFeatures;
};
+bool isMetadirectiveEval(lower::pft::Evaluation &eval);
+
} // namespace omp
} // namespace lower
} // namespace Fortran
diff --git a/flang/test/Lower/OpenMP/Todo/metadirective-loop.f90 b/flang/test/Lower/OpenMP/Todo/metadirective-loop.f90
deleted file mode 100644
index 999a8c0839d15..0000000000000
--- a/flang/test/Lower/OpenMP/Todo/metadirective-loop.f90
+++ /dev/null
@@ -1,12 +0,0 @@
-! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=50 -o - %s 2>&1 | FileCheck %s
-
-! CHECK: not yet implemented: loop-associated METADIRECTIVE variant
-
-subroutine test_loop_variant()
- integer :: i
- !$omp metadirective &
- !$omp & when(implementation={vendor(llvm)}: parallel do) &
- !$omp & default(nothing)
- do i = 1, 100
- end do
-end subroutine
diff --git a/flang/test/Lower/OpenMP/Todo/metadirective-module-loop.f90 b/flang/test/Lower/OpenMP/Todo/metadirective-module-loop.f90
new file mode 100644
index 0000000000000..be08d91a95c01
--- /dev/null
+++ b/flang/test/Lower/OpenMP/Todo/metadirective-module-loop.f90
@@ -0,0 +1,7 @@
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=51 -o - %s 2>&1 | FileCheck %s
+
+! CHECK: not yet implemented: loop-associated METADIRECTIVE without associated DO
+
+module m
+ !$omp metadirective when(implementation={vendor(llvm)}: do) default(nothing)
+end module
diff --git a/flang/test/Lower/OpenMP/Todo/metadirective-no-loop.f90 b/flang/test/Lower/OpenMP/Todo/metadirective-no-loop.f90
new file mode 100644
index 0000000000000..d10b57c493b6d
--- /dev/null
+++ b/flang/test/Lower/OpenMP/Todo/metadirective-no-loop.f90
@@ -0,0 +1,12 @@
+! Test that a metadirective resolving to a loop-associated variant
+! without an associated DO loop correctly reports a TODO.
+
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=51 -o - %s 2>&1 | FileCheck %s
+
+! CHECK: not yet implemented: loop-associated METADIRECTIVE without associated DO
+
+subroutine test_no_associated_do()
+ !$omp metadirective &
+ !$omp & when(implementation={vendor(llvm)}: parallel do) &
+ !$omp & default(nothing)
+end subroutine
diff --git a/flang/test/Lower/OpenMP/Todo/metadirective-target-loop.f90 b/flang/test/Lower/OpenMP/Todo/metadirective-target-loop.f90
new file mode 100644
index 0000000000000..f271cbeffb21f
--- /dev/null
+++ b/flang/test/Lower/OpenMP/Todo/metadirective-target-loop.f90
@@ -0,0 +1,15 @@
+! Test that a metadirective variant resolving to a target construct
+! correctly reports a TODO (host-eval support needed).
+
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=51 -o - %s 2>&1 | FileCheck %s
+
+! CHECK: not yet implemented: TARGET construct selected by METADIRECTIVE (host-eval)
+
+subroutine test_target_loop()
+ integer :: i
+ !$omp metadirective &
+ !$omp & when(implementation={vendor(llvm)}: target teams distribute parallel do) &
+ !$omp & default(nothing)
+ do i = 1, 100
+ end do
+end subroutine
diff --git a/flang/test/Lower/OpenMP/metadirective-loop.f90 b/flang/test/Lower/OpenMP/metadirective-loop.f90
new file mode 100644
index 0000000000000..6d429c3e3f2f2
--- /dev/null
+++ b/flang/test/Lower/OpenMP/metadirective-loop.f90
@@ -0,0 +1,339 @@
+! Test lowering of OpenMP metadirective with loop-associated variants.
+
+! RUN: %flang_fc1 -fopenmp -emit-hlfir -fopenmp-version=51 %s -o - | FileCheck %s
+
+!===----------------------------------------------------------------------===!
+! Basic loop-associated variants via static selection
+!===----------------------------------------------------------------------===!
+
+! CHECK-LABEL: func.func @_QPtest_parallel_do()
+! CHECK: omp.parallel {
+! CHECK: omp.wsloop
+! CHECK: omp.loop_nest
+! CHECK: omp.yield
+! CHECK: omp.terminator
+! CHECK: return
+subroutine test_parallel_do()
+ integer :: i
+ !$omp metadirective &
+ !$omp & when(implementation={vendor(llvm)}: parallel do) &
+ !$omp & default(nothing)
+ do i = 1, 100
+ end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_do()
+! CHECK-NOT: omp.parallel
+! CHECK: omp.wsloop
+! CHECK: omp.loop_nest
+! CHECK: omp.yield
+! CHECK: return
+subroutine test_do()
+ integer :: i
+ !$omp metadirective &
+ !$omp & when(implementation={vendor(llvm)}: do) &
+ !$omp & default(nothing)
+ do i = 1, 100
+ end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_simd()
+! CHECK: %[[I:.*]]:2 = hlfir.declare {{.*}} {uniq_name = "_QFtest_simdEi"}
+! CHECK-NOT: omp.wsloop
+! CHECK: omp.simd linear(%[[I]]#0
+! CHECK: omp.loop_nest
+! CHECK: omp.yield
+! CHECK-NOT: fir.do_loop
+! CHECK: fir.load %[[I]]#0
+! CHECK: return
+subroutine test_simd()
+ integer :: i, i_after
+ !$omp metadirective &
+ !$omp & when(implementation={vendor(llvm)}: simd) &
+ !$omp & default(nothing)
+ do i = 1, 100
+ i_after = i
+ end do
+ i_after = i
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_do_simd()
+! CHECK-NOT: omp.parallel
+! CHECK: omp.wsloop
+! CHECK: omp.simd
+! CHECK: omp.loop_nest
+! CHECK: omp.yield
+! CHECK: return
+subroutine test_do_simd()
+ integer :: i
+ !$omp metadirective &
+ !$omp & when(implementation={vendor(llvm)}: do simd) &
+ !$omp & default(nothing)
+ do i = 1, 100
+ end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_begin_do()
+! CHECK-NOT: omp.parallel
+! CHECK: omp.wsloop
+! CHECK: omp.loop_nest
+! CHECK: omp.yield
+! CHECK: return
+subroutine test_begin_do()
+ integer :: i
+ !$omp begin metadirective &
+ !$omp & when(implementation={vendor(llvm)}: do) &
+ !$omp & default(nothing)
+ do i = 1, 100
+ end do
+ !$omp end metadirective
+end subroutine
+
+!===----------------------------------------------------------------------===!
+! Static mismatch falls through to standalone fallback
+!===----------------------------------------------------------------------===!
+
+! CHECK-LABEL: func.func @_QPtest_loop_static_mismatch()
+! CHECK-NOT: omp.wsloop
+! CHECK-NOT: omp.loop_nest
+! CHECK: omp.barrier
+! CHECK: return
+subroutine test_loop_static_mismatch()
+ integer :: i
+ !$omp metadirective &
+ !$omp & when(implementation={vendor("unknown")}: parallel do) &
+ !$omp & default(barrier)
+ do i = 1, 100
+ end do
+end subroutine
+
+!===----------------------------------------------------------------------===!
+! Dynamic user condition with loop-associated variant
+!===----------------------------------------------------------------------===!
+
+! CHECK-LABEL: func.func @_QPtest_dynamic_loop(
+! CHECK-SAME: %[[ARG0:.*]]: !fir.ref<!fir.logical<4>>
+! CHECK: %[[DECL:.*]]:2 = hlfir.declare %[[ARG0]]
+! CHECK: %[[LOAD:.*]] = fir.load %[[DECL]]#0
+! CHECK: %[[COND:.*]] = fir.convert %[[LOAD]] : (!fir.logical<4>) -> i1
+! CHECK: fir.if %[[COND]] {
+! CHECK: omp.parallel {
+! CHECK: omp.wsloop
+! CHECK: omp.loop_nest
+! CHECK: } else {
+! CHECK: omp.simd
+! CHECK: omp.loop_nest
+! CHECK: }
+! CHECK: return
+subroutine test_dynamic_loop(flag)
+ logical, intent(in) :: flag
+ integer :: i
+ !$omp metadirective &
+ !$omp & when(user={condition(flag)}: parallel do) &
+ !$omp & default(simd)
+ do i = 1, 100
+ end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_dynamic_loop_standalone_fallback(
+! CHECK: fir.if {{.*}} {
+! CHECK: omp.parallel {
+! CHECK: omp.wsloop
+! CHECK: omp.loop_nest
+! CHECK: } else {
+! CHECK: omp.barrier
+! CHECK: fir.do_loop
+! CHECK: }
+! CHECK: return
+subroutine test_dynamic_loop_standalone_fallback(flag, a)
+ logical, intent(in) :: flag
+ integer :: i, a
+ a = 0
+ !$omp metadirective &
+ !$omp & when(user={condition(flag)}: parallel do) &
+ !$omp & default(barrier)
+ do i = 1, 100
+ a = a + i
+ end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_dynamic_loop_dsa_isolation(
+! CHECK: fir.if {{.*}} {
+! CHECK: omp.simd {{.*}}private(@{{[^,]*}}Ei_private_i32
+! CHECK-SAME: @_QFtest_dynamic_loop_dsa_isolationEj_private_i32
+! CHECK: omp.loop_nest ({{.*}}, {{.*}}) : i32 {{.*}} collapse(2)
+! CHECK: } else {
+! CHECK: omp.simd linear({{[^)]*}}) {
+! CHECK: omp.loop_nest ({{.*}}) : i32
+! CHECK: }
+! CHECK: return
+subroutine test_dynamic_loop_dsa_isolation(flag, n, sink)
+ logical, intent(in) :: flag
+ integer :: n, sink
+ integer :: i, j
+ sink = 0
+ !$omp metadirective &
+ !$omp & when(user={condition(flag)}: simd collapse(2)) &
+ !$omp & default(simd)
+ do i = 1, n
+ do j = 1, n
+ sink = sink + i + j
+ end do
+ end do
+ sink = sink + j
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_ordered_depth()
+! CHECK: omp.parallel {
+! CHECK: omp.wsloop {{.*}}private(@{{[^,]*}}Ei_private_i32
+! CHECK-SAME: @_QFtest_ordered_depthEj_private_i32
+! CHECK: omp.loop_nest ({{.*}}) : i32
+! CHECK: return
+subroutine test_ordered_depth()
+ integer :: i, j
+ !$omp metadirective &
+ !$omp & when(implementation={vendor(llvm)}: parallel do ordered(2)) &
+ !$omp & default(nothing)
+ do i = 1, 100
+ do j = 1, 100
+ end do
+ end do
+end subroutine
+
+!===----------------------------------------------------------------------===!
+! Loop-associated variants with clauses
+!===----------------------------------------------------------------------===!
+
+! CHECK-LABEL: func.func @_QPtest_schedule()
+! CHECK: omp.wsloop schedule(static)
+! CHECK: omp.loop_nest
+! CHECK: return
+subroutine test_schedule()
+ integer :: i
+ !$omp metadirective &
+ !$omp & when(implementation={vendor(llvm)}: do schedule(static)) &
+ !$omp & default(nothing)
+ do i = 1, 100
+ end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_reduction()
+! CHECK: omp.wsloop {{.*}} reduction(@add_reduction_i32
+! CHECK: omp.loop_nest
+! CHECK: return
+subroutine test_reduction()
+ integer :: i, s
+ s = 0
+ !$omp metadirective &
+ !$omp & when(implementation={vendor(llvm)}: do reduction(+:s)) &
+ !$omp & default(nothing)
+ do i = 1, 100
+ s = s + i
+ end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_private()
+! CHECK: omp.wsloop private(
+! CHECK: omp.loop_nest
+! CHECK: return
+subroutine test_private()
+ integer :: i, x
+ !$omp metadirective &
+ !$omp & when(implementation={vendor(llvm)}: do private(x)) &
+ !$omp & default(nothing)
+ do i = 1, 100
+ x = i
+ end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_collapse()
+! CHECK: omp.wsloop
+! CHECK: omp.loop_nest ({{.*}}, {{.*}}) : i32 {{.*}} collapse(2)
+! CHECK: return
+subroutine test_collapse()
+ integer :: i, j
+ !$omp metadirective &
+ !$omp & when(implementation={vendor(llvm)}: do collapse(2)) &
+ !$omp & default(nothing)
+ do i = 1, 100
+ do j = 1, 100
+ end do
+ end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_safelen()
+! CHECK: omp.simd {{.*}}safelen(4)
+! CHECK: omp.loop_nest
+! CHECK: return
+subroutine test_safelen()
+ integer :: i
+ !$omp metadirective &
+ !$omp & when(implementation={vendor(llvm)}: simd safelen(4)) &
+ !$omp & default(nothing)
+ do i = 1, 100
+ end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_num_threads()
+! CHECK: omp.parallel num_threads({{.*}}) {
+! CHECK: omp.wsloop
+! CHECK: omp.loop_nest
+! CHECK: return
+subroutine test_num_threads()
+ integer :: i
+ !$omp metadirective &
+ !$omp & when(implementation={vendor(llvm)}: parallel do num_threads(4)) &
+ !$omp & default(nothing)
+ do i = 1, 100
+ end do
+end subroutine
+
+!===----------------------------------------------------------------------===!
+! Lastprivate copy-back for metadirective-selected loop induction variables
+!===----------------------------------------------------------------------===!
+
+! CHECK-LABEL: func.func @_QPtest_simd_collapse_lastprivate(
+! CHECK: %[[I_ORIG:.*]]:2 = hlfir.declare %{{.*}} {uniq_name = "_QFtest_simd_collapse_lastprivateEi"}
+! CHECK: %[[J_ORIG:.*]]:2 = hlfir.declare %{{.*}} {uniq_name = "_QFtest_simd_collapse_lastprivateEj"}
+! CHECK: omp.simd {{.*}}private({{.*}}_QFtest_simd_collapse_lastprivateEi_private_i32{{.*}}_QFtest_simd_collapse_lastprivateEj_private_i32{{.*}}) {
+! CHECK: omp.loop_nest ({{.*}}, {{.*}}) : i32 {{.*}} collapse(2) {
+! CHECK: fir.if
+! CHECK: hlfir.assign %{{.*}} to %[[I_ORIG]]#0
+! CHECK: hlfir.assign %{{.*}} to %[[J_ORIG]]#0
+! CHECK: omp.yield
+subroutine test_simd_collapse_lastprivate(n, sink)
+ integer :: n, sink
+ integer :: i, j
+ sink = 0
+ !$omp metadirective &
+ !$omp & when(implementation={vendor(llvm)}: simd collapse(2)) &
+ !$omp & default(nothing)
+ do i = 1, n
+ do j = 1, n
+ sink = sink + i + j
+ end do
+ end do
+ sink = sink + j
+end subroutine
+
+!===----------------------------------------------------------------------===!
+! Metadirective nested inside a BLOCK construct
+!===----------------------------------------------------------------------===!
+
+! CHECK-LABEL: func.func @_QPtest_block_nested_parallel_do(
+! CHECK: omp.parallel {
+! CHECK: omp.wsloop private(@_QFtest_block_nested_parallel_doEi_private_i32 {{.*}}) {
+! CHECK: omp.loop_nest
+subroutine test_block_nested_parallel_do(n, a)
+ integer :: n
+ integer :: a(n)
+ integer :: i
+ block
+ !$omp metadirective &
+ !$omp & when(implementation={vendor(llvm)}: parallel do) &
+ !$omp & default(nothing)
+ do i = 1, n
+ a(i) = i
+ end do
+ end block
+end subroutine
>From 614565bdb9dde75db5007e68547b928406b37218 Mon Sep 17 00:00:00 2001
From: "Chi Chun, Chen" <chichun.chen at hpe.com>
Date: Wed, 24 Jun 2026 21:47:57 -0500
Subject: [PATCH 2/3] Privatize sequential loop indices in metadirective
variants
When a metadirective resolves to a parallel-, task-, or teams-generating
loop variant (e.g. `parallel do`), the iteration variable of every
sequential loop nested in the generated region must be private, matching
OmpAttributeVisitor::ResolveSeqLoopIndexInParallelOrTaskConstruct.
Semantic analysis cannot apply this rule because the variant is resolved
only at lowering time, so an inner non-associated loop index was left
shared across threads, causing a data race.
Mark such indices during lowering by walking the selected variant's loop
body. The walk stops at nested constructs that establish their own data
environment (parallel/task/teams/target), and leaves alone indices that
are already predetermined or named in the variant's own clauses, so the
result matches the corresponding real directive.
---
flang/lib/Lower/OpenMP/OpenMP.cpp | 58 ++++++++
.../test/Lower/OpenMP/metadirective-loop.f90 | 137 ++++++++++++++++++
2 files changed, 195 insertions(+)
diff --git a/flang/lib/Lower/OpenMP/OpenMP.cpp b/flang/lib/Lower/OpenMP/OpenMP.cpp
index a9b0bb31ecd52..737ee3760172c 100644
--- a/flang/lib/Lower/OpenMP/OpenMP.cpp
+++ b/flang/lib/Lower/OpenMP/OpenMP.cpp
@@ -4893,6 +4893,42 @@ class SymbolDSAGuard {
savedFlags;
};
+/// Mark the iteration variable of every sequential DO loop nested in a
+/// metadirective variant's generated region as private, mirroring
+/// OmpAttributeVisitor::ResolveSeqLoopIndexInParallelOrTaskConstruct, which
+/// cannot run because the variant is only resolved during lowering.
+static void markSequentialLoopIVs(
+ lower::pft::Evaluation &eval, SymbolDSAGuard &dsaGuard,
+ const llvm::SmallPtrSetImpl<const semantics::Symbol *> &clauseSyms,
+ unsigned version) {
+ using Symbol = semantics::Symbol;
+ if (!eval.hasNestedEvaluations())
+ return;
+ for (lower::pft::Evaluation &nested : eval.getNestedEvaluations()) {
+ // A nested construct with its own data environment (parallel, task, teams,
+ // or target) owns the loops it contains, so do not descend into it. A
+ // nested unresolved metadirective is not special-cased: privatizing its
+ // sibling loop index here too is benign for a loop index.
+ if (const auto *ompConstruct = nested.getIf<parser::OpenMPConstruct>()) {
+ llvm::omp::Directive dir =
+ parser::omp::GetOmpDirectiveName(*ompConstruct).v;
+ if (llvm::omp::allParallelSet.test(dir) ||
+ llvm::omp::taskGeneratingSet.test(dir) ||
+ llvm::omp::allTargetSet.test(dir) ||
+ (version >= 52 && llvm::omp::allTeamsSet.test(dir)))
+ continue;
+ }
+ // Skip an index that already has a DSA: predetermined (the variant's own
+ // associated loops and nested loop-construct indices) or clause-named.
+ if (nested.getIf<parser::DoConstruct>())
+ if (Symbol *sym = getIterationVariableSymbol(nested))
+ if (!sym->test(Symbol::Flag::OmpPreDetermined) &&
+ !clauseSyms.contains(sym))
+ dsaGuard.setSymbolDSA(*sym, Symbol::Flag::OmpPrivate);
+ markSequentialLoopIVs(nested, dsaGuard, clauseSyms, version);
+ }
+}
+
/// Mark loop induction variable data-sharing attributes for a
/// metadirective-selected loop variant. Semantic analysis cannot mark these
/// because the variant is resolved at lowering time.
@@ -4929,6 +4965,28 @@ markMetadirectiveLoopIVs(semantics::SemanticsContext &semaCtx,
if (level + 1 < affectedDepth)
doEval = getNestedDoConstruct(*doEval);
}
+
+ // A variant that generates a parallel, task, or (>= 5.2) teams region also
+ // privatizes the index of every nested sequential loop. `doEval` is the
+ // innermost associated loop, so its body holds those loops.
+ bool variantGeneratesParallelRegion =
+ llvm::omp::allParallelSet.test(spec.DirId()) ||
+ llvm::omp::taskGeneratingSet.test(spec.DirId()) ||
+ (semaCtx.langOptions().OpenMPVersion >= 52 &&
+ llvm::omp::allTeamsSet.test(spec.DirId()));
+ if (variantGeneratesParallelRegion) {
+ // Indices named in the variant's own clauses already have an explicit DSA;
+ // exclude them so the marking below does not override it.
+ llvm::SmallPtrSet<const semantics::Symbol *, 4> clauseSyms;
+ for (const parser::OmpClause &clause : spec.Clauses().v)
+ if (const parser::OmpObjectList *objects =
+ parser::omp::GetOmpObjectList(clause))
+ for (const parser::OmpObject &object : objects->v)
+ if (const semantics::Symbol *sym = makeObject(object, semaCtx).sym())
+ clauseSyms.insert(sym);
+ markSequentialLoopIVs(*doEval, dsaGuard, clauseSyms,
+ semaCtx.langOptions().OpenMPVersion);
+ }
}
static void genMetadirective(lower::AbstractConverter &converter,
diff --git a/flang/test/Lower/OpenMP/metadirective-loop.f90 b/flang/test/Lower/OpenMP/metadirective-loop.f90
index 6d429c3e3f2f2..559949d2ef7bf 100644
--- a/flang/test/Lower/OpenMP/metadirective-loop.f90
+++ b/flang/test/Lower/OpenMP/metadirective-loop.f90
@@ -337,3 +337,140 @@ subroutine test_block_nested_parallel_do(n, a)
end do
end block
end subroutine
+
+!===----------------------------------------------------------------------===!
+! Inner sequential loop induction-variable privatization
+!===----------------------------------------------------------------------===!
+
+! CHECK-LABEL: func.func @_QPtest_parallel_do_seq_inner(
+! CHECK: omp.parallel {
+! CHECK: omp.wsloop private({{.*}}_QFtest_parallel_do_seq_innerEi_private_i32{{.*}}_QFtest_parallel_do_seq_innerEk_private_i32{{.*}}) {
+! CHECK: omp.loop_nest
+subroutine test_parallel_do_seq_inner(n, a)
+ integer :: n
+ integer :: a(n, n)
+ integer :: i, k
+ !$omp metadirective &
+ !$omp & when(implementation={vendor(llvm)}: parallel do) &
+ !$omp & default(nothing)
+ do i = 1, n
+ do k = 1, n
+ a(k, i) = k
+ end do
+ end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_do_seq_inner_shared(
+! CHECK: omp.wsloop private(
+! CHECK-NOT: _QFtest_do_seq_inner_sharedEk_private
+! CHECK: omp.loop_nest
+subroutine test_do_seq_inner_shared(n, a)
+ integer :: n
+ integer :: a(n, n)
+ integer :: i, k
+ !$omp metadirective &
+ !$omp & when(implementation={vendor(llvm)}: do) &
+ !$omp & default(nothing)
+ do i = 1, n
+ do k = 1, n
+ a(k, i) = k
+ end do
+ end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_parallel_do_critical_inner(
+! CHECK: omp.parallel {
+! CHECK: omp.wsloop private({{.*}}_QFtest_parallel_do_critical_innerEi_private_i32{{.*}}_QFtest_parallel_do_critical_innerEk_private_i32{{.*}}) {
+! CHECK: omp.loop_nest
+! CHECK: omp.critical
+subroutine test_parallel_do_critical_inner(n, a)
+ integer :: n
+ integer :: a(n)
+ integer :: i, k
+ !$omp metadirective &
+ !$omp & when(implementation={vendor(llvm)}: parallel do) &
+ !$omp & default(nothing)
+ do i = 1, n
+ !$omp critical
+ do k = 1, n
+ a(k) = k
+ end do
+ !$omp end critical
+ end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_parallel_do_inner_shared(
+! CHECK: omp.wsloop private(
+! CHECK: omp.loop_nest
+! CHECK: omp.parallel {
+! CHECK-NOT: _QFtest_parallel_do_inner_sharedEk_private
+subroutine test_parallel_do_inner_shared(n, a)
+ integer :: n
+ integer :: a(n)
+ integer :: i, k
+ !$omp metadirective &
+ !$omp & when(implementation={vendor(llvm)}: parallel do) &
+ !$omp & default(nothing)
+ do i = 1, n
+ !$omp parallel shared(k)
+ do k = 1, n
+ a(k) = k
+ end do
+ !$omp end parallel
+ end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_parallel_do_reduction_iv(
+! CHECK: omp.wsloop private(@_QFtest_parallel_do_reduction_ivEi_private_i32 {{[^,]*}}) reduction(@add_reduction_i32 {{.*}}) {
+subroutine test_parallel_do_reduction_iv(n, a)
+ integer :: n
+ integer :: a(n)
+ integer :: i, k
+ !$omp metadirective &
+ !$omp & when(implementation={vendor(llvm)}: parallel do reduction(+:k)) &
+ !$omp & default(nothing)
+ do i = 1, n
+ do k = 1, n
+ a(k) = k
+ end do
+ end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_parallel_do_enclosing_shared(
+! CHECK: omp.wsloop private({{.*}}_QFtest_parallel_do_enclosing_sharedEi_private_i32{{.*}}_QFtest_parallel_do_enclosing_sharedEk_private_i32{{.*}}) {
+! CHECK: omp.loop_nest
+subroutine test_parallel_do_enclosing_shared(n, a)
+ integer :: n
+ integer :: a(n)
+ integer :: i, k
+ !$omp parallel shared(k)
+ !$omp metadirective &
+ !$omp & when(implementation={vendor(llvm)}: parallel do) &
+ !$omp & default(nothing)
+ do i = 1, n
+ do k = 1, n
+ a(k) = k
+ end do
+ end do
+ !$omp end parallel
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_parallel_do_target_inner(
+! CHECK: omp.wsloop private(@_QFtest_parallel_do_target_innerEi_private_i32 {{[^,]*}}) {
+! CHECK: omp.target
+! CHECK-NOT: _QFtest_parallel_do_target_innerEk_private_i32
+subroutine test_parallel_do_target_inner(n, a)
+ integer :: n
+ integer :: a(n)
+ integer :: i, k
+ !$omp metadirective &
+ !$omp & when(implementation={vendor(llvm)}: parallel do) &
+ !$omp & default(nothing)
+ do i = 1, n
+ !$omp target map(tofrom: a)
+ do k = 1, n
+ a(k) = k
+ end do
+ !$omp end target
+ end do
+end subroutine
>From 10db363fab2599130850c9b04afe2cc9689a3b84 Mon Sep 17 00:00:00 2001
From: "Chi Chun, Chen" <chichun.chen at hpe.com>
Date: Thu, 25 Jun 2026 17:30:27 -0500
Subject: [PATCH 3/3] Diagnose oversized COLLAPSE/ORDERED on metadirective loop
variants
A loop-associated METADIRECTIVE variant is only resolved during lowering, so
its COLLAPSE/ORDERED was never range-checked; an oversized argument (e.g.
`do collapse(3)` over a two-deep nest) reached lowering and crashed on an
assertion.
Check it in OmpStructureChecker against the loop nest that follows the
metadirective, emitting the same error a real loop-associated construct
produces. The lowering path keeps a non-asserting fallback so it bails
cleanly instead of asserting if the semantic check is bypassed.
---
flang/lib/Lower/OpenMP/OpenMP.cpp | 24 +++--
flang/lib/Lower/OpenMP/Utils.cpp | 15 ++-
flang/lib/Lower/OpenMP/Utils.h | 4 +
flang/lib/Semantics/check-omp-structure.cpp | 3 +
flang/lib/Semantics/check-omp-structure.h | 6 ++
flang/lib/Semantics/check-omp-variant.cpp | 49 +++++++++
.../OpenMP/metadirective-loop-nest.f90 | 101 ++++++++++++++++++
7 files changed, 191 insertions(+), 11 deletions(-)
create mode 100644 flang/test/Semantics/OpenMP/metadirective-loop-nest.f90
diff --git a/flang/lib/Lower/OpenMP/OpenMP.cpp b/flang/lib/Lower/OpenMP/OpenMP.cpp
index 737ee3760172c..3fedf9056ade7 100644
--- a/flang/lib/Lower/OpenMP/OpenMP.cpp
+++ b/flang/lib/Lower/OpenMP/OpenMP.cpp
@@ -4931,8 +4931,10 @@ static void markSequentialLoopIVs(
/// Mark loop induction variable data-sharing attributes for a
/// metadirective-selected loop variant. Semantic analysis cannot mark these
-/// because the variant is resolved at lowering time.
-static void
+/// because the variant is resolved at lowering time. Returns false if the
+/// associated loop nest is shallower than the variant's COLLAPSE/ORDERED
+/// requires, leaving the diagnostic to the caller.
+static bool
markMetadirectiveLoopIVs(semantics::SemanticsContext &semaCtx,
const parser::OmpDirectiveSpecification &spec,
lower::pft::Evaluation &loopEval,
@@ -4942,7 +4944,7 @@ markMetadirectiveLoopIVs(semantics::SemanticsContext &semaCtx,
auto [depth, _] = semantics::omp::GetAffectedNestDepthWithReason(
spec, semaCtx.langOptions().OpenMPVersion, &semaCtx);
if (!depth || !depth.value || *depth.value <= 0)
- return;
+ return true;
int64_t affectedDepth = *depth.value;
bool isSimdVariant =
@@ -4958,12 +4960,16 @@ markMetadirectiveLoopIVs(semantics::SemanticsContext &semaCtx,
lower::pft::Evaluation *doEval = &loopEval;
for (int64_t level = 0; level < affectedDepth; ++level) {
- assert(doEval->getIf<parser::DoConstruct>() &&
- "expected associated DO construct");
+ // A nest shallower than COLLAPSE/ORDERED requires is diagnosed during
+ // semantic analysis in check-omp-variant. Guard against it here too,
+ // returning false so the caller handles it instead of asserting in
+ // `getNestedDoConstruct`.
+ if (!doEval || !doEval->getIf<parser::DoConstruct>())
+ return false;
if (semantics::Symbol *sym = getIterationVariableSymbol(*doEval))
dsaGuard.setSymbolDSA(*sym, ivDSA);
if (level + 1 < affectedDepth)
- doEval = getNestedDoConstruct(*doEval);
+ doEval = tryGetNestedDoConstruct(*doEval);
}
// A variant that generates a parallel, task, or (>= 5.2) teams region also
@@ -4987,6 +4993,7 @@ markMetadirectiveLoopIVs(semantics::SemanticsContext &semaCtx,
markSequentialLoopIVs(*doEval, dsaGuard, clauseSyms,
semaCtx.langOptions().OpenMPVersion);
}
+ return true;
}
static void genMetadirective(lower::AbstractConverter &converter,
@@ -5213,7 +5220,10 @@ static void genMetadirective(lower::AbstractConverter &converter,
if (!loopEval)
TODO(variantLoc, "loop-associated METADIRECTIVE without associated DO");
SymbolDSAGuard dsaGuard;
- markMetadirectiveLoopIVs(semaCtx, *spec, *loopEval, dsaGuard);
+ if (!markMetadirectiveLoopIVs(semaCtx, *spec, *loopEval, dsaGuard))
+ TODO(variantLoc, "METADIRECTIVE variant with COLLAPSE or ORDERED "
+ "requires a deeper perfectly-nested loop nest than "
+ "is present");
genOMPDispatch(converter, symTable, semaCtx, eval, variantLoc, queue,
queue.begin());
return;
diff --git a/flang/lib/Lower/OpenMP/Utils.cpp b/flang/lib/Lower/OpenMP/Utils.cpp
index c145612e6e91c..1d6e868ce8c4b 100644
--- a/flang/lib/Lower/OpenMP/Utils.cpp
+++ b/flang/lib/Lower/OpenMP/Utils.cpp
@@ -699,7 +699,7 @@ static void processTileSizesFromOpenMPConstruct(
}
}
-pft::Evaluation *getNestedDoConstruct(pft::Evaluation &eval) {
+pft::Evaluation *tryGetNestedDoConstruct(pft::Evaluation &eval) {
for (pft::Evaluation &nested : eval.getNestedEvaluations()) {
// In an OpenMPConstruct there can be compiler directives:
// 1 <<OpenMPConstruct>>
@@ -719,10 +719,17 @@ pft::Evaluation *getNestedDoConstruct(pft::Evaluation &eval) {
// Loop transformations can introduce nested OpenMP
// constructs between the directive and the actual do-loop nest.
if (nested.getIf<parser::OpenMPConstruct>())
- return getNestedDoConstruct(nested);
- assert(false && "Unexpected construct in the nested evaluations");
+ return tryGetNestedDoConstruct(nested);
+ // Anything else means the perfectly-nested do-loop nest ends here.
+ return nullptr;
}
- llvm_unreachable("Expected do loop to be in the nested evaluations");
+ return nullptr;
+}
+
+pft::Evaluation *getNestedDoConstruct(pft::Evaluation &eval) {
+ pft::Evaluation *doEval = tryGetNestedDoConstruct(eval);
+ assert(doEval && "Expected do loop to be in the nested evaluations");
+ return doEval;
}
/// Return true if \p eval holds a metadirective.
diff --git a/flang/lib/Lower/OpenMP/Utils.h b/flang/lib/Lower/OpenMP/Utils.h
index 4b268d7df4272..7772cc0736594 100644
--- a/flang/lib/Lower/OpenMP/Utils.h
+++ b/flang/lib/Lower/OpenMP/Utils.h
@@ -167,6 +167,10 @@ void genObjectList(const ObjectList &objects,
void lastprivateModifierNotSupported(const omp::clause::Lastprivate &lastp,
mlir::Location loc);
+/// Return the perfectly-nested DO construct directly contained in \p eval, or
+/// null when there is none (e.g. the loop nest is shallower than required).
+pft::Evaluation *tryGetNestedDoConstruct(pft::Evaluation &eval);
+
pft::Evaluation *getNestedDoConstruct(pft::Evaluation &eval);
int64_t collectLoopRelatedInfo(
diff --git a/flang/lib/Semantics/check-omp-structure.cpp b/flang/lib/Semantics/check-omp-structure.cpp
index d4572ec685e61..27b69d14f69c4 100644
--- a/flang/lib/Semantics/check-omp-structure.cpp
+++ b/flang/lib/Semantics/check-omp-structure.cpp
@@ -204,6 +204,9 @@ void OmpStructureChecker::Enter(const parser::ModuleSubprogram &) {
}
void OmpStructureChecker::Enter(const parser::SpecificationPart &) {
+ // The pending metadirective requirement is reset once per program unit.
+ if (partStack_.empty())
+ metadirectiveLoopVariants_.clear();
partStack_.push_back(PartKind::SpecificationPart);
}
diff --git a/flang/lib/Semantics/check-omp-structure.h b/flang/lib/Semantics/check-omp-structure.h
index 256383d890cab..4bd0fdb71e6db 100644
--- a/flang/lib/Semantics/check-omp-structure.h
+++ b/flang/lib/Semantics/check-omp-structure.h
@@ -145,6 +145,8 @@ class OmpStructureChecker : public OmpStructureCheckerBase {
void Enter(const parser::OmpMetadirectiveDirective &);
void Leave(const parser::OmpMetadirectiveDirective &);
+ void Enter(const parser::ExecutionPartConstruct &);
+
void Enter(const parser::OmpContextSelector &);
void Leave(const parser::OmpContextSelector &);
@@ -421,6 +423,10 @@ class OmpStructureChecker : public OmpStructureCheckerBase {
};
std::vector<PartKind> partStack_;
+ // Loop variants pending COLLAPSE/ORDERED validation against their loop nest.
+ std::vector<const parser::OmpDirectiveSpecification *>
+ metadirectiveLoopVariants_;
+
std::multimap<const parser::Label,
std::pair<parser::CharBlock, const parser::OpenMPConstruct *>>
sourceLabels_;
diff --git a/flang/lib/Semantics/check-omp-variant.cpp b/flang/lib/Semantics/check-omp-variant.cpp
index 8b782030c37e3..0369983702166 100644
--- a/flang/lib/Semantics/check-omp-variant.cpp
+++ b/flang/lib/Semantics/check-omp-variant.cpp
@@ -575,6 +575,11 @@ void OmpStructureChecker::Enter(const parser::OmpDirectiveSpecification &x) {
PushContextAndClauseSets(
std::get<parser::OmpDirectiveName>(x.t).source, dirId);
+
+ // Record each variant directive so its COLLAPSE/ORDERED can be checked against
+ // the associated loop nest.
+ if (dirId != llvm::omp::Directive::OMPD_metadirective)
+ metadirectiveLoopVariants_.push_back(&x);
}
void OmpStructureChecker::Leave(const parser::OmpDirectiveSpecification &x) {
@@ -585,12 +590,56 @@ void OmpStructureChecker::Leave(const parser::OmpDirectiveSpecification &x) {
void OmpStructureChecker::Enter(const parser::OmpMetadirectiveDirective &x) {
EnterDirectiveNest(MetadirectiveNest);
+ metadirectiveLoopVariants_.clear();
}
void OmpStructureChecker::Leave(const parser::OmpMetadirectiveDirective &) {
ExitDirectiveNest(MetadirectiveNest);
}
+// Check a loop-associated metadirective's variants against the loop nest they
+// apply to. The nest is not attached to the directive in the parse tree. It is
+// the next executable construct, either a following sibling or the first
+// execution-part construct for a declarative metadirective.
+void OmpStructureChecker::Enter(const parser::ExecutionPartConstruct &x) {
+ if (metadirectiveLoopVariants_.empty())
+ return;
+ if (parser::Unwrap<parser::CompilerDirective>(x))
+ return;
+ // Take the pending variants off the worklist so they are validated only
+ // here, at this first construct, loop nest or not.
+ std::vector<const parser::OmpDirectiveSpecification *> variants;
+ variants.swap(metadirectiveLoopVariants_);
+
+ if (!parser::Unwrap<parser::DoConstruct>(x))
+ return;
+
+ unsigned version{context_.langOptions().OpenMPVersion};
+ LoopSequence sequence(x, version, /*allowAllLoops=*/true, &context_);
+ const auto &[haveSemantic, havePerfect]{sequence.depth()};
+
+ for (const parser::OmpDirectiveSpecification *spec : variants) {
+ auto [needDepth, needPerfect]{
+ GetAffectedNestDepthWithReason(*spec, version, &context_)};
+ if (!needDepth || *needDepth.value <= 0)
+ continue;
+ auto haveDepth{needPerfect ? havePerfect : haveSemantic};
+ if (!haveDepth || *haveDepth.value <= 0)
+ continue;
+ if (*needDepth.value > *haveDepth.value) {
+ std::string_view perfectTxt{needPerfect ? " perfect" : ""};
+ auto &msg{context_.Say(spec->DirName().source,
+ "This construct requires a%s nest of depth %" PRId64
+ ", but the associated nest is a%s nest of depth %" PRId64 ""_err_en_US,
+ perfectTxt, *needDepth.value, perfectTxt, *haveDepth.value)};
+ haveDepth.reason.AttachTo(msg);
+ needDepth.reason.AttachTo(msg);
+ } else {
+ CheckRectangularNest(*spec, sequence);
+ }
+ }
+}
+
static const parser::traits::OmpContextSelectorSpecification *
getMatchClauseContextSelector(const parser::OmpDirectiveSpecification &spec) {
for (const parser::OmpClause &clause : spec.Clauses().v) {
diff --git a/flang/test/Semantics/OpenMP/metadirective-loop-nest.f90 b/flang/test/Semantics/OpenMP/metadirective-loop-nest.f90
new file mode 100644
index 0000000000000..4342e4f406d0d
--- /dev/null
+++ b/flang/test/Semantics/OpenMP/metadirective-loop-nest.f90
@@ -0,0 +1,101 @@
+!RUN: %python %S/../test_errors.py %s %flang -fopenmp -fopenmp-version=51
+
+subroutine collapse_too_deep(n, a)
+ integer :: n, a(n, n), i, j
+ !ERROR: This construct requires a perfect nest of depth 3, but the associated nest is a perfect nest of depth 2
+ !BECAUSE: COLLAPSE clause was specified with argument 3
+ !$omp metadirective when(implementation={vendor(llvm)}: do collapse(3)) default(nothing)
+ do i = 1, n
+ do j = 1, n
+ a(j, i) = i
+ end do
+ end do
+end subroutine
+
+subroutine ordered_too_deep(n, a)
+ integer :: n, a(n, n), i, j
+ !ERROR: This construct requires a perfect nest of depth 3, but the associated nest is a perfect nest of depth 2
+ !BECAUSE: ORDERED clause was specified with argument 3
+ !$omp metadirective when(implementation={vendor(llvm)}: do ordered(3)) default(nothing)
+ do i = 1, n
+ do j = 1, n
+ a(j, i) = i
+ end do
+ end do
+end subroutine
+
+subroutine collapse_too_deep_exec(n, a)
+ integer :: n, a(n, n), i, j
+ a = 0
+ !ERROR: This construct requires a perfect nest of depth 3, but the associated nest is a perfect nest of depth 2
+ !BECAUSE: COLLAPSE clause was specified with argument 3
+ !$omp metadirective when(implementation={vendor(llvm)}: do collapse(3)) default(nothing)
+ do i = 1, n
+ do j = 1, n
+ a(j, i) = i
+ end do
+ end do
+end subroutine
+
+subroutine collapse_too_deep_compiler_directive(n, a)
+ integer :: n, a(n, n), i, j
+ a = 0
+ !ERROR: This construct requires a perfect nest of depth 3, but the associated nest is a perfect nest of depth 2
+ !BECAUSE: COLLAPSE clause was specified with argument 3
+ !$omp metadirective when(implementation={vendor(llvm)}: do collapse(3)) default(nothing)
+ !dir$ ivdep
+ do i = 1, n
+ do j = 1, n
+ a(j, i) = i
+ end do
+ end do
+end subroutine
+
+subroutine collapse_too_deep_interface(n, a)
+ integer :: n, a(n, n), i, j
+ !ERROR: This construct requires a perfect nest of depth 3, but the associated nest is a perfect nest of depth 2
+ !BECAUSE: COLLAPSE clause was specified with argument 3
+ !$omp metadirective when(implementation={vendor(llvm)}: do collapse(3)) default(nothing)
+ interface
+ subroutine ext()
+ end subroutine
+ end interface
+ do i = 1, n
+ do j = 1, n
+ a(j, i) = i
+ end do
+ end do
+end subroutine
+
+subroutine tile_non_rectangular(n, a)
+ integer :: n, a(n, n), i, j
+ !ERROR: This construct requires a rectangular loop nest, but the associated nest is not
+ !BECAUSE: None of the loops affected by TILE can be non-rectangular
+ !$omp metadirective when(implementation={vendor(llvm)}: tile sizes(2, 2)) default(nothing)
+ do i = 1, n
+ !BECAUSE: The upper bound of the affected loop uses iteration variables of enclosing loops: 'i'
+ do j = 1, i
+ a(j, i) = i
+ end do
+ end do
+end subroutine
+
+subroutine collapse_ok(n, a)
+ integer :: n, a(n, n), i, j
+ !$omp metadirective when(implementation={vendor(llvm)}: do collapse(2)) default(nothing)
+ do i = 1, n
+ do j = 1, n
+ a(j, i) = i
+ end do
+ end do
+end subroutine
+
+subroutine collapse_nonrectangular_ok(n, a)
+ integer :: n, a(n, n), i, j
+ !$omp metadirective when(implementation={vendor(llvm)}: do collapse(2)) default(nothing)
+ do i = 1, n
+ do j = 1, i
+ a(j, i) = i
+ end do
+ end do
+end subroutine
More information about the flang-commits
mailing list