[flang-commits] [flang] [flang][OpenMP] Lower DO, SIMD, and DO SIMD metadirective variants (PR #218555)

via flang-commits flang-commits at lists.llvm.org
Wed Aug 26 08:57:45 PDT 2026


https://github.com/chichunchen updated https://github.com/llvm/llvm-project/pull/218555

>From 456b1195691f0d62295787974254644fbacb843e Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Mon, 24 Aug 2026 17:43:03 -0500
Subject: [PATCH 1/3] [flang][OpenMP] Lower DO, SIMD, and DO SIMD metadirective
 variants

A standalone metadirective and its associated loop are represented as
sibling PFT evaluations. For example:

```
  !$omp metadirective &
  !$omp& when(user={condition(flag)}: do) &
  !$omp& otherwise(nothing)
  do i = 1, n
    a(i) = i
  end do
```

has the following evaluation shape:

```
  METADIRECTIVE
  DO
  `-- a(i) = i
```

Lowering temporarily associates the DO evaluation with the metadirective so
the selected replacement can reuse the existing OpenMP loop-lowering path.
For the example above, runtime selection lowers conceptually as:

```
  if (flag)
    omp.wsloop
      omp.loop_nest
  else
    fir.do_loop
```

The evaluation ownership is restored after lowering so that the PFT remains
available to other lowering paths.

This patch lowers only canonical structured DO loop nests selected as DO,
SIMD, or DO SIMD. It supports standalone metadirectives and begin/end forms
whose structured block is the associated loop nest. Static selections and
runtime user conditions are supported. NOTHING retains ordinary Fortran loop
lowering. Other loop-associated cases are diagnosed.

Assisted with codex.
---
 flang/include/flang/Lower/PFTBuilder.h        |   3 +
 flang/include/flang/Semantics/openmp-utils.h  |  57 ++
 flang/lib/Lower/Bridge.cpp                    |   5 +
 .../lib/Lower/OpenMP/DataSharingProcessor.cpp |  89 +-
 flang/lib/Lower/OpenMP/DataSharingProcessor.h |  32 +-
 flang/lib/Lower/OpenMP/OpenMP.cpp             | 802 ++++++++++++------
 flang/lib/Lower/OpenMP/Utils.cpp              |   2 +-
 flang/lib/Lower/OpenMP/Utils.h                |   2 +
 flang/lib/Semantics/openmp-utils.cpp          | 335 ++++++++
 .../metadirective-loop-data-environment.f90   |  94 ++
 .../metadirective-loop-iteration-variable.f90 |  57 ++
 .../Todo/metadirective-loop-unstructured.f90  |  39 +
 ...irective-loop-unsupported-replacements.f90 | 111 +++
 .../Lower/OpenMP/Todo/metadirective-loop.f90  |  12 -
 .../OpenMP/metadirective-implementation.f90   |  51 +-
 .../test/Lower/OpenMP/metadirective-loop.f90  | 562 ++++++++++++
 16 files changed, 1979 insertions(+), 274 deletions(-)
 create mode 100644 flang/test/Lower/OpenMP/Todo/metadirective-loop-data-environment.f90
 create mode 100644 flang/test/Lower/OpenMP/Todo/metadirective-loop-iteration-variable.f90
 create mode 100644 flang/test/Lower/OpenMP/Todo/metadirective-loop-unstructured.f90
 create mode 100644 flang/test/Lower/OpenMP/Todo/metadirective-loop-unsupported-replacements.f90
 delete mode 100644 flang/test/Lower/OpenMP/Todo/metadirective-loop.f90
 create mode 100644 flang/test/Lower/OpenMP/metadirective-loop.f90

diff --git a/flang/include/flang/Lower/PFTBuilder.h b/flang/include/flang/Lower/PFTBuilder.h
index 8a59dd6638b71..f495046c66a32 100644
--- a/flang/include/flang/Lower/PFTBuilder.h
+++ b/flang/include/flang/Lower/PFTBuilder.h
@@ -377,6 +377,9 @@ struct Evaluation : EvaluationVariant {
   bool isUnstructured{false};  // evaluation has unstructured control flow
   bool negateCondition{false}; // If[Then]Stmt condition must be negated
   bool activeConstruct{false}; // temporarily set for some constructs
+  // The enclosing evaluation-list traversal should skip this evaluation once
+  // because directive lowering already consumed it.
+  bool skipNextLowering{false};
   mlir::Block *block{nullptr}; // isNewBlock block (ActionStmt, ConstructStmt)
   int printIndex{0}; // (ActionStmt, ConstructStmt) evaluation index for dumps
 };
diff --git a/flang/include/flang/Semantics/openmp-utils.h b/flang/include/flang/Semantics/openmp-utils.h
index 031926d4a7cc2..be6cff0bb4f4e 100644
--- a/flang/include/flang/Semantics/openmp-utils.h
+++ b/flang/include/flang/Semantics/openmp-utils.h
@@ -257,6 +257,63 @@ class OmpVariantMatchContext : public llvm::omp::OMPContext {
   std::string features_;
 };
 
+struct MetadirectiveCandidate {
+  MetadirectiveCandidate(const parser::OmpDirectiveSpecification *spec,
+      llvm::omp::VariantMatchInfo vmi, bool isExplicit,
+      std::optional<DynamicUserCondition> dynamicCondition = std::nullopt,
+      bool conditionShouldBeTrue = true)
+      : spec{spec}, vmi{std::move(vmi)}, isExplicit{isExplicit},
+        dynamicCondition{dynamicCondition},
+        conditionShouldBeTrue{conditionShouldBeTrue} {}
+
+  const parser::OmpDirectiveSpecification *spec{nullptr};
+  llvm::omp::VariantMatchInfo vmi;
+  bool isExplicit{false};
+  std::optional<DynamicUserCondition> dynamicCondition;
+  bool conditionShouldBeTrue{true};
+};
+
+struct MetadirectiveCandidateSet {
+  llvm::SmallVector<MetadirectiveCandidate, 4> candidates;
+  /// Null represents either an explicit NOTHING fallback or no fallback.
+  const parser::OmpDirectiveSpecification *fallback{nullptr};
+};
+
+/// Build the statically applicable candidates for a METADIRECTIVE.
+///
+/// Returns std::nullopt when a selector is malformed or uses a feature that
+/// variant matching cannot yet model.
+std::optional<MetadirectiveCandidateSet> BuildMetadirectiveCandidateSet(
+    const parser::OmpClauseList &clauses, SemanticsContext &context,
+    const OmpVariantMatchContext &matchContext);
+
+std::optional<unsigned> SelectBestMetadirectiveCandidate(
+    llvm::ArrayRef<unsigned> candidateIndices,
+    llvm::ArrayRef<MetadirectiveCandidate> candidates,
+    const OmpVariantMatchContext &matchContext);
+
+/// Return true when repeated evaluation of \p condition cannot call a
+/// procedure or observe asynchronously changing state.
+bool IsRepeatableMetadirectiveCondition(
+    const parser::ScalarExpr &condition, SemanticsContext &context);
+
+/// Return true when two repeatable conditions normalize to the same expression.
+bool AreSameRepeatableMetadirectiveCondition(const parser::ScalarExpr &left,
+    const parser::ScalarExpr &right, SemanticsContext &context);
+
+/// Return candidates reachable after \p selectedIndex fails. Equal repeatable
+/// guards are pruned until a non-repeatable guard is encountered.
+llvm::SmallVector<unsigned, 4> GetMetadirectiveElsePathCandidates(
+    unsigned selectedIndex, llvm::ArrayRef<unsigned> candidateIndices,
+    llvm::ArrayRef<MetadirectiveCandidate> candidates,
+    const OmpVariantMatchContext &matchContext, SemanticsContext &context);
+
+/// Return every replacement that can be selected, retaining lower-ranked
+/// candidates after a dynamic condition. Null represents NOTHING.
+llvm::SmallVector<const parser::OmpDirectiveSpecification *, 4>
+GetReachableMetadirectiveVariants(const MetadirectiveCandidateSet &candidateSet,
+    const OmpVariantMatchContext &matchContext, SemanticsContext &context);
+
 /// True if a variant guarded by \p selector may be selected in the current
 /// compilation context.
 ///
diff --git a/flang/lib/Lower/Bridge.cpp b/flang/lib/Lower/Bridge.cpp
index 3d71a4dbf9b1b..e6201c50c26d0 100644
--- a/flang/lib/Lower/Bridge.cpp
+++ b/flang/lib/Lower/Bridge.cpp
@@ -5952,6 +5952,11 @@ class FirConverter : public Fortran::lower::AbstractConverter {
                           ? eval.getFirstNestedEvaluation().block
                           : eval.block);
 
+    if (eval.skipNextLowering) {
+      eval.skipNextLowering = false;
+      return;
+    }
+
     // Add scope for constructs inside acc.loop to properly contain symbol
     // bindings (e.g., from cache directive) within the construct.
     bool needsAccScope =
diff --git a/flang/lib/Lower/OpenMP/DataSharingProcessor.cpp b/flang/lib/Lower/OpenMP/DataSharingProcessor.cpp
index 1d39c1a8d4b77..5cfc69f86e158 100644
--- a/flang/lib/Lower/OpenMP/DataSharingProcessor.cpp
+++ b/flang/lib/Lower/OpenMP/DataSharingProcessor.cpp
@@ -37,19 +37,32 @@ namespace lower {
 namespace omp {
 bool DataSharingProcessor::OMPConstructSymbolVisitor::isSymbolDefineBy(
     const semantics::Symbol *symbol, lower::pft::Evaluation &eval) const {
+  auto definition = symDefMap.find(symbol);
+  if (definition == symDefMap.end())
+    return false;
+
+  // The selected variant is not an enclosing parse-tree construct, so its
+  // IVs map to a null construct. Admit only IVs owned by this variant because
+  // enclosing IVs can map to null too.
+  if (isMetadirectiveLoop)
+    return metadirectiveLoopIVs.contains(symbol) &&
+           definition->second ==
+               ConstructPtr(
+                   static_cast<const parser::OpenMPConstruct *>(nullptr));
+
   return eval.visit(common::visitors{
-      [&](const parser::OpenMPConstruct &functionParserNode) {
-        return symDefMap.count(symbol) &&
-               symDefMap.at(symbol) == ConstructPtr(&functionParserNode);
+      [&](const parser::OpenMPConstruct &ompConstruct) {
+        return definition->second == ConstructPtr(&ompConstruct);
       },
-      [](const auto &functionParserNode) { return false; }});
+      [](const auto &) { return false; }});
 }
 
 bool DataSharingProcessor::OMPConstructSymbolVisitor::
     isSymbolDefineByNestedDeclaration(const semantics::Symbol *symbol) const {
-  return symDefMap.count(symbol) &&
+  auto definition = symDefMap.find(symbol);
+  return definition != symDefMap.end() &&
          std::holds_alternative<const parser::DeclarationConstruct *>(
-             symDefMap.at(symbol));
+             definition->second);
 }
 
 static bool isConstructWithTopLevelTarget(lower::pft::Evaluation &eval) {
@@ -66,15 +79,26 @@ DataSharingProcessor::DataSharingProcessor(
     lower::AbstractConverter &converter, semantics::SemanticsContext &semaCtx,
     const List<Clause> &clauses, lower::pft::Evaluation &eval,
     bool shouldCollectPreDeterminedSymbols, bool useDelayedPrivatization,
-    lower::SymMap &symTable, bool isTargetPrivatization)
+    lower::SymMap &symTable, bool isTargetPrivatization,
+    llvm::ArrayRef<const semantics::Symbol *> metadirectiveLoopIVs)
     : converter(converter), semaCtx(semaCtx),
       firOpBuilder(converter.getFirOpBuilder()), clauses(clauses), eval(eval),
       shouldCollectPreDeterminedSymbols(shouldCollectPreDeterminedSymbols),
       useDelayedPrivatization(useDelayedPrivatization), symTable(symTable),
-      isTargetPrivatization(isTargetPrivatization), visitor(semaCtx) {
+      isTargetPrivatization(isTargetPrivatization),
+      isMetadirectiveLoop(!metadirectiveLoopIVs.empty()),
+      visitor(semaCtx, metadirectiveLoopIVs) {
   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 (isMetadirectiveLoop && eval.hasNestedEvaluations()) {
+    for (auto &nestedEval : eval.getNestedEvaluations())
+      nestedEval.visit([&](const auto &node) { parser::Walk(node, visitor); });
+  }
 }
 
 DataSharingProcessor::DataSharingProcessor(lower::AbstractConverter &converter,
@@ -220,9 +244,31 @@ void DataSharingProcessor::copyLastPrivateSymbol(
     const semantics::Symbol *sym, mlir::OpBuilder::InsertPoint *lastPrivIP) {
   // Conditional-lastprivate symbols use their own guarded copy-back (from the
   // reduction accumulator), not the standard "last iteration wins" copy-back.
-  if (sym->test(semantics::Symbol::Flag::OmpLastPrivate) &&
-      !conditionalLastPrivatizedSymbols.contains(sym))
+  if (!sym->test(semantics::Symbol::Flag::OmpLastPrivate) ||
+      conditionalLastPrivatizedSymbols.contains(sym))
+    return;
+
+  if (sym->has<semantics::HostAssocDetails>()) {
     converter.copyHostAssociateVar(*sym, lastPrivIP, /*hostIsSource=*/false);
+    return;
+  }
+
+  assert(isMetadirectiveLoop &&
+         "unexpected lastprivate symbol without host association");
+
+  // 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(
@@ -574,7 +620,20 @@ void DataSharingProcessor::collectPrivatizedSymbols(
   };
 
   llvm::SetVector<const semantics::Scope *> clauseScopes;
-  (void)collectScopes(semaCtx, eval, clauseScopes);
+  const semantics::Scope *curScope = 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 (isMetadirectiveLoop && curScope) {
+    const semantics::Scope *scope = curScope;
+    while (scope->kind() != semantics::Scope::Kind::Global) {
+      clauseScopes.insert(scope);
+      scope = &scope->parent();
+    }
+  }
 
   for (const auto *sym : allSymbols) {
     if (semantics::omp::IsPrivatizable(*sym) &&
@@ -605,6 +664,14 @@ void DataSharingProcessor::collectSymbols(
                              /*collectSymbols=*/true,
                              /*collectHostAssociatedSymbols=*/true);
 
+  // Collect symbols from spliced nested evaluations for metadirectives.
+  if (isMetadirectiveLoop && 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/DataSharingProcessor.h b/flang/lib/Lower/OpenMP/DataSharingProcessor.h
index 557584c21123f..9f430780c49d8 100644
--- a/flang/lib/Lower/OpenMP/DataSharingProcessor.h
+++ b/flang/lib/Lower/OpenMP/DataSharingProcessor.h
@@ -37,8 +37,13 @@ class DataSharingProcessor {
   /// at any point in time. This is used to track Symbol definition scopes in
   /// order to tell which OMP scope defined vs. references a certain Symbol.
   struct OMPConstructSymbolVisitor {
-    OMPConstructSymbolVisitor(semantics::SemanticsContext &ctx)
-        : version(ctx.langOptions().OpenMPVersion) {}
+    OMPConstructSymbolVisitor(
+        semantics::SemanticsContext &ctx,
+        llvm::ArrayRef<const semantics::Symbol *> metadirectiveLoopIVs)
+        : metadirectiveLoopIVs(metadirectiveLoopIVs.begin(),
+                               metadirectiveLoopIVs.end()),
+          isMetadirectiveLoop(!metadirectiveLoopIVs.empty()),
+          version(ctx.langOptions().OpenMPVersion) {}
     template <typename T>
     bool Pre(const T &) {
       return true;
@@ -46,6 +51,13 @@ class DataSharingProcessor {
     template <typename T>
     void Post(const T &) {}
 
+    bool Pre(const parser::LoopControl::Bounds &bounds) {
+      if (isMetadirectiveLoop)
+        if (const semantics::Symbol *symbol = bounds.Name().thing.symbol)
+          metadirectiveLoopIVs.insert(symbol);
+      return true;
+    }
+
     bool Pre(const parser::OpenMPConstruct &omp) {
       // Skip constructs that may not have privatizations.
       if (isOpenMPPrivatizingConstruct(omp, version))
@@ -87,6 +99,8 @@ class DataSharingProcessor {
                                       const parser::DeclarationConstruct *>;
     llvm::SmallVector<ConstructPtr> constructs;
     llvm::DenseMap<semantics::Symbol *, ConstructPtr> symDefMap;
+    llvm::SmallPtrSet<const semantics::Symbol *, 4> metadirectiveLoopIVs;
+    bool isMetadirectiveLoop;
 
     unsigned version;
   };
@@ -117,6 +131,7 @@ class DataSharingProcessor {
   llvm::SmallPtrSet<const semantics::Symbol *, 16> mightHaveReadHostSym;
   lower::SymMap &symTable;
   bool isTargetPrivatization;
+  bool isMetadirectiveLoop;
   OMPConstructSymbolVisitor visitor;
 
   bool needBarrier();
@@ -157,13 +172,12 @@ class DataSharingProcessor {
   bool isOpenMPPrivatizingEvaluation(const pft::Evaluation &eval) const;
 
 public:
-  DataSharingProcessor(lower::AbstractConverter &converter,
-                       semantics::SemanticsContext &semaCtx,
-                       const List<Clause> &clauses,
-                       lower::pft::Evaluation &eval,
-                       bool shouldCollectPreDeterminedSymbols,
-                       bool useDelayedPrivatization, lower::SymMap &symTable,
-                       bool isTargetPrivatization = false);
+  DataSharingProcessor(
+      lower::AbstractConverter &converter, semantics::SemanticsContext &semaCtx,
+      const List<Clause> &clauses, lower::pft::Evaluation &eval,
+      bool shouldCollectPreDeterminedSymbols, bool useDelayedPrivatization,
+      lower::SymMap &symTable, bool isTargetPrivatization = false,
+      llvm::ArrayRef<const semantics::Symbol *> metadirectiveLoopIVs = {});
 
   DataSharingProcessor(lower::AbstractConverter &converter,
                        semantics::SemanticsContext &semaCtx,
diff --git a/flang/lib/Lower/OpenMP/OpenMP.cpp b/flang/lib/Lower/OpenMP/OpenMP.cpp
index 7503d33c8df38..165f7c63f238d 100644
--- a/flang/lib/Lower/OpenMP/OpenMP.cpp
+++ b/flang/lib/Lower/OpenMP/OpenMP.cpp
@@ -46,6 +46,7 @@
 #include "flang/Parser/tools.h"
 #include "flang/Semantics/expression.h"
 #include "flang/Semantics/openmp-directive-sets.h"
+#include "flang/Semantics/openmp-dsa.h"
 #include "flang/Semantics/openmp-utils.h"
 #include "flang/Semantics/tools.h"
 #include "flang/Support/Flags.h"
@@ -61,6 +62,7 @@
 #include "llvm/ADT/DenseSet.h"
 #include "llvm/ADT/MapVector.h"
 #include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/ScopeExit.h"
 #include "llvm/ADT/SmallPtrSet.h"
 #include "llvm/ADT/SmallSet.h"
 #include "llvm/ADT/StringSwitch.h"
@@ -119,12 +121,12 @@ emitNestedParallelGuardForCondLp(lower::AbstractConverter &converter,
 // Code generation helper functions
 //===----------------------------------------------------------------------===//
 
-static void genOMPDispatch(lower::AbstractConverter &converter,
-                           lower::SymMap &symTable,
-                           semantics::SemanticsContext &semaCtx,
-                           lower::pft::Evaluation &eval, mlir::Location loc,
-                           const ConstructQueue &queue,
-                           ConstructQueue::const_iterator item);
+static void genOMPDispatch(
+    lower::AbstractConverter &converter, lower::SymMap &symTable,
+    semantics::SemanticsContext &semaCtx, lower::pft::Evaluation &eval,
+    mlir::Location loc, const ConstructQueue &queue,
+    ConstructQueue::const_iterator item,
+    llvm::ArrayRef<const semantics::Symbol *> metadirectiveLoopIVs = {});
 
 /// Return the directive that is immediately nested inside of the given
 /// \c parent evaluation, if it is its only non-end-statement nested evaluation
@@ -4702,7 +4704,8 @@ static mlir::omp::WsloopOp genStandaloneDo(
     lower::AbstractConverter &converter, lower::SymMap &symTable,
     lower::StatementContext &stmtCtx, semantics::SemanticsContext &semaCtx,
     lower::pft::Evaluation &eval, mlir::Location loc,
-    const ConstructQueue &queue, ConstructQueue::const_iterator item) {
+    const ConstructQueue &queue, ConstructQueue::const_iterator item,
+    llvm::ArrayRef<const semantics::Symbol *> metadirectiveLoopIVs) {
   mlir::omp::WsloopOperands wsloopClauseOps;
   llvm::SmallVector<Object> wsloopReductionObjects;
   genWsloopClauses(converter, semaCtx, stmtCtx, item->clauses, loc,
@@ -4710,7 +4713,9 @@ static mlir::omp::WsloopOp genStandaloneDo(
 
   DataSharingProcessor dsp(converter, semaCtx, item->clauses, eval,
                            /*shouldCollectPreDeterminedSymbols=*/true,
-                           enableDelayedPrivatization, symTable);
+                           enableDelayedPrivatization, symTable,
+                           /*isTargetPrivatization=*/false,
+                           metadirectiveLoopIVs);
   // Worksharing loops use the private-copy lowering for conditional lastprivate
   // (each list item gets an ordinary private copy + a reduction accumulator),
   // which is correct under any schedule including nonmonotonic.
@@ -4919,12 +4924,12 @@ static mlir::omp::ParallelOp genStandaloneParallel(
                        enableDelayedPrivatization ? &dsp.value() : nullptr);
 }
 
-static mlir::omp::SimdOp
-genStandaloneSimd(lower::AbstractConverter &converter, lower::SymMap &symTable,
-                  semantics::SemanticsContext &semaCtx,
-                  lower::pft::Evaluation &eval, mlir::Location loc,
-                  const ConstructQueue &queue,
-                  ConstructQueue::const_iterator item) {
+static mlir::omp::SimdOp genStandaloneSimd(
+    lower::AbstractConverter &converter, lower::SymMap &symTable,
+    semantics::SemanticsContext &semaCtx, lower::pft::Evaluation &eval,
+    mlir::Location loc, const ConstructQueue &queue,
+    ConstructQueue::const_iterator item,
+    llvm::ArrayRef<const semantics::Symbol *> metadirectiveLoopIVs) {
   mlir::omp::SimdOperands simdClauseOps;
   llvm::SmallVector<Object> simdReductionObjects;
   genSimdClauses(converter, semaCtx, item->clauses, loc, simdClauseOps,
@@ -4932,7 +4937,9 @@ genStandaloneSimd(lower::AbstractConverter &converter, lower::SymMap &symTable,
 
   DataSharingProcessor dsp(converter, semaCtx, item->clauses, eval,
                            /*shouldCollectPreDeterminedSymbols=*/true,
-                           enableDelayedPrivatization, symTable);
+                           enableDelayedPrivatization, symTable,
+                           /*isTargetPrivatization=*/false,
+                           metadirectiveLoopIVs);
   dsp.processStep1(&simdClauseOps);
 
   if (!dsp.getConditionalLastprivateSymbols().empty())
@@ -5266,7 +5273,8 @@ static mlir::omp::WsloopOp genCompositeDoSimd(
     lower::AbstractConverter &converter, lower::SymMap &symTable,
     lower::StatementContext &stmtCtx, semantics::SemanticsContext &semaCtx,
     lower::pft::Evaluation &eval, mlir::Location loc,
-    const ConstructQueue &queue, ConstructQueue::const_iterator item) {
+    const ConstructQueue &queue, ConstructQueue::const_iterator item,
+    llvm::ArrayRef<const semantics::Symbol *> metadirectiveLoopIVs) {
   assert(std::distance(item, queue.end()) == 2 && "Invalid leaf constructs");
   ConstructQueue::const_iterator doItem = item;
   ConstructQueue::const_iterator simdItem = std::next(doItem);
@@ -5311,7 +5319,9 @@ static mlir::omp::WsloopOp genCompositeDoSimd(
 
   DataSharingProcessor simdItemDSP(converter, semaCtx, simdItem->clauses, eval,
                                    /*shouldCollectPreDeterminedSymbols=*/true,
-                                   /*useDelayedPrivatization=*/true, symTable);
+                                   /*useDelayedPrivatization=*/true, symTable,
+                                   /*isTargetPrivatization=*/false,
+                                   metadirectiveLoopIVs);
   simdItemDSP.processStep1(&simdClauseOps, simdItem->id);
 
   // Pass the innermost leaf construct's clauses because that's where COLLAPSE
@@ -5369,7 +5379,8 @@ static bool genOMPCompositeDispatch(
     lower::StatementContext &stmtCtx, semantics::SemanticsContext &semaCtx,
     lower::pft::Evaluation &eval, mlir::Location loc,
     const ConstructQueue &queue, ConstructQueue::const_iterator item,
-    mlir::Operation *&newOp) {
+    mlir::Operation *&newOp,
+    llvm::ArrayRef<const semantics::Symbol *> metadirectiveLoopIVs) {
   using llvm::omp::Directive;
   using lower::omp::matchLeafSequence;
 
@@ -5389,7 +5400,7 @@ static bool genOMPCompositeDispatch(
                                        eval, loc, queue, item);
   else if (matchLeafSequence(item, queue, Directive::OMPD_do_simd))
     newOp = genCompositeDoSimd(converter, symTable, stmtCtx, semaCtx, eval, loc,
-                               queue, item);
+                               queue, item, metadirectiveLoopIVs);
   else if (matchLeafSequence(item, queue, Directive::OMPD_taskloop_simd))
     newOp = genCompositeTaskloopSimd(converter, symTable, stmtCtx, semaCtx,
                                      eval, loc, queue, item);
@@ -5399,12 +5410,12 @@ static bool genOMPCompositeDispatch(
   return true;
 }
 
-static void genOMPDispatch(lower::AbstractConverter &converter,
-                           lower::SymMap &symTable,
-                           semantics::SemanticsContext &semaCtx,
-                           lower::pft::Evaluation &eval, mlir::Location loc,
-                           const ConstructQueue &queue,
-                           ConstructQueue::const_iterator item) {
+static void
+genOMPDispatch(lower::AbstractConverter &converter, lower::SymMap &symTable,
+               semantics::SemanticsContext &semaCtx,
+               lower::pft::Evaluation &eval, mlir::Location loc,
+               const ConstructQueue &queue, ConstructQueue::const_iterator item,
+               llvm::ArrayRef<const semantics::Symbol *> metadirectiveLoopIVs) {
   assert(item != queue.end());
 
   lower::StatementContext stmtCtx;
@@ -5425,7 +5436,8 @@ static void genOMPDispatch(lower::AbstractConverter &converter,
   if (loopLeaf) {
     symTable.pushScope();
     if (genOMPCompositeDispatch(converter, symTable, stmtCtx, semaCtx, eval,
-                                loc, queue, item, newOp)) {
+                                loc, queue, item, newOp,
+                                metadirectiveLoopIVs)) {
       symTable.popScope();
       finalizeStmtCtx();
       return;
@@ -5443,7 +5455,7 @@ static void genOMPDispatch(lower::AbstractConverter &converter,
     break;
   case llvm::omp::Directive::OMPD_do:
     newOp = genStandaloneDo(converter, symTable, stmtCtx, semaCtx, eval, loc,
-                            queue, item);
+                            queue, item, metadirectiveLoopIVs);
     break;
   case llvm::omp::Directive::OMPD_loop:
     newOp = genLoopOp(converter, symTable, semaCtx, eval, loc, queue, item);
@@ -5475,8 +5487,8 @@ static void genOMPDispatch(lower::AbstractConverter &converter,
     newOp = genSectionsOp(converter, symTable, semaCtx, eval, loc, queue, item);
     break;
   case llvm::omp::Directive::OMPD_simd:
-    newOp =
-        genStandaloneSimd(converter, symTable, semaCtx, eval, loc, queue, item);
+    newOp = genStandaloneSimd(converter, symTable, semaCtx, eval, loc, queue,
+                              item, metadirectiveLoopIVs);
     break;
   case llvm::omp::Directive::OMPD_scope:
     newOp = genScopeOp(converter, symTable, semaCtx, eval, loc, queue, item);
@@ -6822,23 +6834,347 @@ static void genOMP(lower::AbstractConverter &converter, lower::SymMap &symTable,
 }
 
 namespace {
-struct MetadirectiveCandidate {
-  MetadirectiveCandidate(const parser::OmpDirectiveSpecification *spec,
-                         llvm::omp::VariantMatchInfo vmi, bool isExplicit,
-                         std::optional<semantics::omp::DynamicUserCondition>
-                             dynamicCond = std::nullopt,
-                         bool conditionShouldBeTrue = true)
-      : spec(spec), vmi(vmi), isExplicit(isExplicit), dynamicCond(dynamicCond),
-        conditionShouldBeTrue(conditionShouldBeTrue) {}
-
-  const parser::OmpDirectiveSpecification *spec = nullptr;
-  llvm::omp::VariantMatchInfo vmi;
-  bool isExplicit = false;
-  std::optional<semantics::omp::DynamicUserCondition> dynamicCond;
-  bool conditionShouldBeTrue = true;
+struct SplicedAssociatedEvaluations {
+  using Iterator = lower::pft::EvaluationList::iterator;
+
+  void record(lower::pft::EvaluationList &parent, Iterator evaluation) {
+    assert((!parentList || parentList == &parent) &&
+           "associated evaluations have different parents");
+    parentList = &parent;
+    evaluations.emplace_back(evaluation, std::next(evaluation));
+  }
+
+  void restore(lower::pft::EvaluationList &nested) {
+    if (evaluations.empty())
+      return;
+    assert(parentList && "missing parent evaluation list");
+    // A saved successor may also have been spliced. Restore in reverse order
+    // so every insertion point is back in the parent list before it is used.
+    for (auto &entry : llvm::reverse(evaluations)) {
+      entry.first->skipNextLowering = true;
+      parentList->splice(entry.second, nested, entry.first);
+    }
+    if (entryEvaluation) {
+      entryEvaluation->isNewBlock = true;
+      entryEvaluation->block = entryBlock;
+    }
+  }
+
+  void suppressEntryBlock(lower::pft::Evaluation &evaluation) {
+    assert(!entryEvaluation && evaluation.isNewBlock && evaluation.block &&
+           "invalid associated entry evaluation");
+    // Do not let either cloned loop arm enter a function-region block. The
+    // metadirective selection will be placed in this block for an active ENTRY.
+    entryEvaluation = &evaluation;
+    entryBlock = evaluation.block;
+    evaluation.isNewBlock = false;
+    evaluation.block = nullptr;
+  }
+
+  mlir::Block *getEntryBlock() const { return entryBlock; }
+
+private:
+  lower::pft::EvaluationList *parentList = nullptr;
+  llvm::SmallVector<std::pair<Iterator, Iterator>, 4> evaluations;
+  lower::pft::Evaluation *entryEvaluation = nullptr;
+  mlir::Block *entryBlock = nullptr;
 };
 } // 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
+/// nullptr if no associated DO loop follows.
+static bool
+isIgnorableMetadirectiveLoopAssociationEval(lower::pft::Evaluation &eval) {
+  return eval.isEndStmt() || eval.getIf<parser::CompilerDirective>();
+}
+
+static lower::pft::Evaluation *spliceAssociatedDoEval(
+    lower::pft::Evaluation &eval,
+    SplicedAssociatedEvaluations *splicedEvaluations = nullptr) {
+  if (eval.hasNestedEvaluations()) {
+    auto nestedIt =
+        llvm::find_if(eval.getNestedEvaluations(), [](auto &nested) {
+          return !isIgnorableMetadirectiveLoopAssociationEval(nested);
+        });
+    if (nestedIt != eval.getNestedEvaluations().end())
+      return nestedIt->getIf<parser::DoConstruct>() ? &*nestedIt : nullptr;
+    return nullptr;
+  }
+
+  // A delimited metadirective owns only its nested evaluations. An empty body
+  // must not capture a following sibling loop as its associated DO.
+  if (const auto *omp = eval.getIf<parser::OpenMPConstruct>();
+      omp && std::holds_alternative<parser::OmpDelimitedMetadirectiveDirective>(
+                 omp->u))
+    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 firstAssociatedIt = std::next(metaIt);
+  auto loopIt = firstAssociatedIt;
+  while (loopIt != parentList->end() &&
+         isIgnorableMetadirectiveLoopAssociationEval(*loopIt))
+    ++loopIt;
+
+  if (loopIt == parentList->end() || !loopIt->getIf<parser::DoConstruct>())
+    return nullptr;
+
+  if (splicedEvaluations) {
+    auto entryIt =
+        llvm::find_if(llvm::make_range(firstAssociatedIt, loopIt),
+                      [](lower::pft::Evaluation &candidate) {
+                        return candidate.isNewBlock && candidate.block;
+                      });
+    if (entryIt != loopIt) {
+      splicedEvaluations->suppressEntryBlock(*entryIt);
+    } else {
+      lower::pft::Evaluation &doStmt = loopIt->getFirstNestedEvaluation();
+      if (doStmt.isNewBlock && doStmt.block)
+        splicedEvaluations->suppressEntryBlock(doStmt);
+    }
+  }
+
+  // Compiler directives between the metadirective and its associated loop
+  // must be processed before the loop is lowered. Move them with the loop so
+  // they are not visited later as siblings of the metadirective.
+  for (auto it = firstAssociatedIt; it != loopIt;) {
+    auto current = it++;
+    if (current->getIf<parser::CompilerDirective>()) {
+      if (splicedEvaluations)
+        splicedEvaluations->record(*parentList, current);
+      eval.evaluationList->splice(eval.evaluationList->end(), *parentList,
+                                  current);
+    }
+  }
+  if (splicedEvaluations)
+    splicedEvaluations->record(*parentList, loopIt);
+  eval.evaluationList->splice(eval.evaluationList->end(), *parentList, loopIt);
+  return &eval.getNestedEvaluations().back();
+}
+
+static bool hasContentFollowingAssociatedDo(lower::pft::Evaluation &eval,
+                                            lower::pft::Evaluation &loopEval) {
+  auto &nested = eval.getNestedEvaluations();
+  auto loopIt = llvm::find_if(
+      nested, [&](lower::pft::Evaluation &e) { return &e == &loopEval; });
+  assert(loopIt != nested.end() && "associated loop not nested");
+  return llvm::any_of(llvm::make_range(std::next(loopIt), nested.end()),
+                      [](lower::pft::Evaluation &e) {
+                        // PFTBuilder adds a source-less CONTINUE as the exit
+                        // target when an executable directive's region ends
+                        // with a construct.
+                        bool isSyntheticExit =
+                            e.getIf<parser::ContinueStmt>() &&
+                            e.position.empty();
+                        return !e.isEndStmt() && !isSyntheticExit;
+                      });
+}
+
+static bool hasNestedOpenMPConstruct(lower::pft::Evaluation &eval) {
+  if (!eval.hasNestedEvaluations())
+    return false;
+  for (lower::pft::Evaluation &nested : eval.getNestedEvaluations()) {
+    if (nested.getIf<parser::OpenMPConstruct>() ||
+        hasNestedOpenMPConstruct(nested))
+      return true;
+  }
+  return false;
+}
+
+static bool hasDirectiveAssociation(llvm::omp::Directive directive,
+                                    llvm::omp::Association association) {
+  return llvm::any_of(llvm::omp::getLeafConstructsOrSelf(directive),
+                      [association](llvm::omp::Directive leaf) {
+                        return llvm::omp::getDirectiveAssociation(leaf) ==
+                               association;
+                      });
+}
+
+static bool hasDirectiveAssociation(const ConstructQueue &queue,
+                                    llvm::omp::Association association) {
+  return llvm::any_of(queue, [association](const auto &item) {
+    return llvm::omp::getDirectiveAssociation(item.id) == association;
+  });
+}
+
+static bool isSupportedMetadirectiveLoopQueue(const ConstructQueue &queue) {
+  using llvm::omp::Directive;
+  using lower::omp::matchLeafSequence;
+  return matchLeafSequence(queue.begin(), queue, Directive::OMPD_do) ||
+         matchLeafSequence(queue.begin(), queue, Directive::OMPD_simd) ||
+         matchLeafSequence(queue.begin(), queue, Directive::OMPD_do_simd);
+}
+
+static bool isNestedInOpenMPDataEnvironment(lower::pft::Evaluation &eval,
+                                            mlir::Operation *currentOp) {
+  for (lower::pft::Evaluation *parent = eval.parentConstruct; parent;
+       parent = parent->parentConstruct) {
+    if (const auto *omp = parent->getIf<parser::OpenMPConstruct>()) {
+      llvm::omp::Directive directive = parser::omp::GetOmpDirectiveName(*omp).v;
+      if (semantics::omp::HasDataEnvironment(directive))
+        return true;
+    }
+  }
+
+  // A PFT ancestor can itself be a metadirective, so its source directive does
+  // not reveal the data environment selected during lowering. Check the
+  // already-emitted OpenMP operation ancestry as well.
+  for (mlir::Operation *op = currentOp; op; op = op->getParentOp()) {
+    if (mlir::isa<mlir::omp::DistributeOp, mlir::omp::LoopNestOp,
+                  mlir::omp::ParallelOp, mlir::omp::ScopeOp,
+                  mlir::omp::SectionsOp, mlir::omp::SimdOp, mlir::omp::SingleOp,
+                  mlir::omp::TargetDataOp, mlir::omp::TargetOp,
+                  mlir::omp::TaskgroupOp, mlir::omp::TaskloopContextOp,
+                  mlir::omp::TaskOp, mlir::omp::TeamsOp, mlir::omp::WsloopOp>(
+            op))
+      return true;
+  }
+  return false;
+}
+
+static bool
+hasUnsupportedDataEnvironmentDirective(const ConstructQueue &queue) {
+  return llvm::any_of(queue, [](const auto &item) {
+    return llvm::omp::allParallelSet.test(item.id) ||
+           llvm::omp::taskGeneratingSet.test(item.id) ||
+           llvm::omp::allTeamsSet.test(item.id);
+  });
+}
+
+static bool hasUnsupportedDataSharingClause(const ConstructQueue &queue,
+                                            unsigned version) {
+  return llvm::any_of(queue, [version](const auto &item) {
+    return llvm::any_of(item.clauses, [version](const Clause &ompClause) {
+      return std::holds_alternative<clause::Default>(ompClause.u) ||
+             llvm::omp::isDataSharingAttributeClause(ompClause.id, version);
+    });
+  });
+}
+
+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());
+      markedSymbols.push_back(&sym);
+    }
+    using Symbol = semantics::Symbol;
+    semantics::SetSymbolDSA(sym, {Symbol::Flag::OmpPreDetermined, dsa});
+  }
+
+  llvm::ArrayRef<const semantics::Symbol *> getMarkedSymbols() const {
+    return markedSymbols;
+  }
+
+private:
+  llvm::SmallVector<std::pair<semantics::Symbol *, semantics::Symbol::Flags>, 4>
+      savedFlags;
+  llvm::SmallVector<const semantics::Symbol *, 4> markedSymbols;
+};
+
+enum class MetadirectiveLoopIVMarking {
+  Marked,           // Induction variables marked (or there was nothing to do).
+  NestTooShallow,   // Fewer DO loops than the variant's COLLAPSE/ORDERED needs.
+  NonCanonicalLoop, // An affected loop is a DO WHILE or has no loop control.
+  IndirectIV,       // An affected induction variable is POINTER or ALLOCATABLE.
+  AssociateIV,      // An affected induction variable is an ASSOCIATE name.
+  ThreadprivateIV,  // An affected induction variable is THREADPRIVATE.
+};
+
+/// 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. Return a non-`Marked`
+/// result, leaving the diagnostic to the caller, when the associated loop nest
+/// is shallower than the variant's COLLAPSE/ORDERED requires or an affected
+/// loop is not a canonical DO loop or an affected induction variable requires
+/// construct-scoped name resolution that metadirective lowering cannot yet
+/// reproduce.
+static MetadirectiveLoopIVMarking
+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 MetadirectiveLoopIVMarking::Marked;
+
+  int64_t affectedDepth = *depth.value;
+  bool isSimdVariant = llvm::omp::allSimdSet.test(spec.DirId());
+  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) {
+    // A nest shallower than COLLAPSE/ORDERED requires is diagnosed during
+    // semantic analysis in check-omp-variant. Guard against it here too so the
+    // caller handles it instead of descending into a missing loop.
+    const parser::DoConstruct *doConstruct =
+        doEval ? doEval->getIf<parser::DoConstruct>() : nullptr;
+    if (!doConstruct)
+      return MetadirectiveLoopIVMarking::NestTooShallow;
+    // The affected loop must be a canonical DO loop (or a DO CONCURRENT, which
+    // lowering rejects further down). A DO WHILE or a loop without loop control
+    // is rejected earlier by the merged metadirective loop-nest semantic checks
+    // (check-omp-variant.cpp), so it should not reach lowering. This guard is
+    // defense-in-depth: bail out for the caller to emit a TODO rather than
+    // crash if that invariant is ever violated.
+    if (!doConstruct->IsDoNormal() && !doConstruct->IsDoConcurrent())
+      return MetadirectiveLoopIVMarking::NonCanonicalLoop;
+    if (semantics::Symbol *sym = getIterationVariableSymbol(*doEval)) {
+      // Ordinary OpenMP name resolution creates a construct-scoped symbol for
+      // an ASSOCIATE-name induction variable. Marking the associate name after
+      // name resolution cannot create the private or lastprivate binding that
+      // loop lowering requires.
+      if (sym->GetUltimate().has<semantics::AssocEntityDetails>())
+        return MetadirectiveLoopIVMarking::AssociateIV;
+      // Ordinary OpenMP semantic resolution creates a construct-scoped symbol
+      // for a POINTER or ALLOCATABLE induction variable. A metadirective
+      // variant is selected too late for that name-resolution step, and marking
+      // the descriptor-backed source symbol cannot recreate it.
+      if (semantics::IsAllocatableOrObjectPointer(sym))
+        return MetadirectiveLoopIVMarking::IndirectIV;
+      // A canonical OpenMP loop iteration variable may not be THREADPRIVATE.
+      // Semantic analysis does not apply the selected variant's loop checks,
+      // so reject it here rather than also marking it with the variant's DSA.
+      if (sym->GetUltimate().test(Symbol::Flag::OmpThreadprivate))
+        return MetadirectiveLoopIVMarking::ThreadprivateIV;
+      dsaGuard.setSymbolDSA(*sym, ivDSA);
+    }
+    if (level + 1 < affectedDepth)
+      doEval = tryGetNestedDoConstruct(*doEval);
+  }
+
+  return MetadirectiveLoopIVMarking::Marked;
+}
+
 static void genMetadirective(lower::AbstractConverter &converter,
                              lower::SymMap &symTable,
                              semantics::SemanticsContext &semaCtx,
@@ -6852,49 +7188,16 @@ static void genMetadirective(lower::AbstractConverter &converter,
   semantics::omp::OmpVariantMatchContext ompCtx =
       makeVariantMatchContext(builder.getModule(), constructTraits);
 
-  llvm::SmallVector<MetadirectiveCandidate, 4> candidates;
-  // A null directive specification represents either the implicit `nothing`
-  // variant or the absence of an explicit otherwise/default clause.
-  const parser::OmpDirectiveSpecification *fallback = nullptr;
-
-  // Extract the context-selector that controls whether a WHEN variant is
-  // applicable. Modifier validation requires exactly one selector per clause.
-  auto getContextSelector = [](const parser::OmpClause::When &whenClause)
-      -> const parser::modifier::OmpContextSelector & {
-    const auto &modifiers = std::get<0>(whenClause.v.t);
-    assert(modifiers && modifiers->size() == 1 &&
-           "WHEN clause should contain one context-selector");
-    return std::get<parser::modifier::OmpContextSelector>(modifiers->front().u);
-  };
-
-  // Extract the directive variant spec from a when clause.
-  // Returns {spec_ptr, isExplicit}. A null spec means "nothing".
-  auto getDirectiveVariant = [](const parser::OmpClause::When &whenClause)
-      -> std::pair<const parser::OmpDirectiveSpecification *, bool> {
-    const auto &opt = std::get<1>(whenClause.v.t);
-    if (!opt)
-      return {nullptr, false};
-    if (opt->value().DirId() == llvm::omp::Directive::OMPD_nothing)
-      return {nullptr, true};
-    return {&opt->value(), true};
-  };
-
-  // Return the directive spec pointer, or nullptr for "nothing".
-  auto getFallbackVariant = [](const parser::OmpDirectiveSpecification &spec)
-      -> const parser::OmpDirectiveSpecification * {
-    if (spec.DirId() == llvm::omp::Directive::OMPD_nothing)
-      return nullptr;
-    return &spec;
-  };
-
+  // Lowering does not yet support every selector feature accepted by
+  // semantics. Diagnose those before building the shared selection plan.
   for (const auto &clause : clauseList.v) {
     if (const auto *whenClause =
             std::get_if<parser::OmpClause::When>(&clause.u)) {
-      const auto &ctxSel = getContextSelector(*whenClause);
-      auto [spec, isExplicit] = getDirectiveVariant(*whenClause);
-
-      // METADIRECTIVE cannot yet honour some selector features that are
-      // otherwise accepted; reject them before building the match info.
+      const auto &modifiers = std::get<0>(whenClause->v.t);
+      assert(modifiers && modifiers->size() == 1 &&
+             "WHEN clause should contain one context-selector");
+      const auto &ctxSel =
+          std::get<parser::modifier::OmpContextSelector>(modifiers->front().u);
       switch (semantics::omp::FindUnsupportedSelectorFeature(ctxSel, semaCtx)) {
       case semantics::omp::UnsupportedSelectorFeature::TargetDevice:
         TODO(converter.genLocation(clause.source),
@@ -6908,126 +7211,87 @@ static void genMetadirective(lower::AbstractConverter &converter,
       case semantics::omp::UnsupportedSelectorFeature::None:
         break;
       }
+    }
+  }
 
-      llvm::omp::VariantMatchInfo rawVMI;
-      std::optional<semantics::omp::DynamicUserCondition> dynamicCond =
-          semantics::omp::MakeVariantMatchInfo(rawVMI, ctxSel, semaCtx);
-
-      if (dynamicCond) {
-        constexpr llvm::omp::TraitProperty dynamicConditionTrait =
-            llvm::omp::TraitProperty::user_condition_unknown;
-        constexpr llvm::omp::TraitProperty matchAnyTrait =
-            llvm::omp::TraitProperty::implementation_extension_match_any;
-        constexpr llvm::omp::TraitProperty matchNoneTrait =
-            llvm::omp::TraitProperty::implementation_extension_match_none;
-
-        // Static applicability must only use traits known at lowering time.
-        // For example, in
-        //   when(implementation={vendor(llvm)},
-        //        user={condition(score(5): flag)}: barrier)
-        // vendor(llvm) can be checked now, but flag cannot. Drop the
-        // runtime-only user_condition_unknown for applicability, while keeping
-        // score(5) so ranking can still honor the user-condition selector.
-        llvm::omp::VariantMatchInfo staticVMI = rawVMI;
-        std::optional<llvm::APInt> conditionScore;
-        auto scoreIt = staticVMI.ScoreMap.find(dynamicConditionTrait);
-        if (scoreIt != staticVMI.ScoreMap.end()) {
-          conditionScore = scoreIt->second;
-          staticVMI.ScoreMap.erase(scoreIt);
-        }
-        staticVMI.RequiredTraits.reset(unsigned(dynamicConditionTrait));
-        llvm::APInt *conditionScorePtr =
-            conditionScore ? &*conditionScore : nullptr;
-
-        bool hasMatchAny = rawVMI.RequiredTraits.test(unsigned(matchAnyTrait));
-        bool hasMatchNone =
-            rawVMI.RequiredTraits.test(unsigned(matchNoneTrait));
-        bool isStaticVMIApplicable =
-            llvm::omp::isVariantApplicableInContext(staticVMI, ompCtx);
-        // If staticVMI does not match, only match_any can still apply. Check
-        // conditionTrueVMI because the runtime condition may satisfy match_any.
-        if (!isStaticVMIApplicable) {
-          if (!hasMatchAny || staticVMI.RequiredTraits.test(
-                                  unsigned(llvm::omp::TraitProperty::invalid)))
-            continue;
-
-          llvm::omp::VariantMatchInfo conditionTrueVMI = staticVMI;
-          conditionTrueVMI.addTrait(
-              llvm::omp::TraitProperty::user_condition_true, "<condition>",
-              conditionScorePtr);
-          if (!llvm::omp::isVariantApplicableInContext(conditionTrueVMI,
-                                                       ompCtx))
-            continue;
-        }
+  std::optional<semantics::omp::MetadirectiveCandidateSet> candidateSet =
+      semantics::omp::BuildMetadirectiveCandidateSet(clauseList, semaCtx,
+                                                     ompCtx);
+  assert(candidateSet && "unsupported selector reached candidate planning");
+  auto &candidates = candidateSet->candidates;
+  const parser::OmpDirectiveSpecification *fallback = candidateSet->fallback;
 
-        auto addConditionTraitForRanking =
-            [&](llvm::omp::VariantMatchInfo &rankingVMI) {
-              rankingVMI.addTrait(
-                  hasMatchNone ? dynamicConditionTrait
-                               : llvm::omp::TraitProperty::user_condition_true,
-                  "<condition>", conditionScorePtr);
-            };
-
-        if (hasMatchAny && isStaticVMIApplicable) {
-          // A statically matched match_any selector needs two candidates: a
-          // guarded candidate with the user condition and score, and an
-          // unguarded candidate with only the statically matched traits. If the
-          // when clause omits its directive, only add the unguarded candidate.
-          if (isExplicit) {
-            llvm::omp::VariantMatchInfo conditionTrueVMI = staticVMI;
-            addConditionTraitForRanking(conditionTrueVMI);
-            candidates.emplace_back(spec, conditionTrueVMI, isExplicit,
-                                    dynamicCond);
-          }
-          candidates.emplace_back(spec, staticVMI, isExplicit);
-          continue;
-        }
+  llvm::SmallVector<unsigned, 4> allCandidateIndices;
+  allCandidateIndices.reserve(candidates.size());
+  for (unsigned idx = 0, end = candidates.size(); idx < end; ++idx)
+    allCandidateIndices.push_back(idx);
 
-        llvm::omp::VariantMatchInfo rankingVMI = staticVMI;
-        // An omitted directive is implicit nothing, so do not let the runtime
-        // condition raise its rank. Explicit `nothing` is still a variant.
-        if (!isExplicit && hasMatchAny && !isStaticVMIApplicable)
-          rankingVMI = llvm::omp::VariantMatchInfo();
-        else if (isExplicit)
-          addConditionTraitForRanking(rankingVMI);
-        candidates.emplace_back(spec, rankingVMI, isExplicit, dynamicCond,
-                                /*conditionShouldBeTrue=*/!hasMatchNone);
-        continue;
-      }
+  llvm::SmallVector<const parser::OmpDirectiveSpecification *, 4>
+      reachableVariantSpecs = semantics::omp::GetReachableMetadirectiveVariants(
+          *candidateSet, ompCtx, semaCtx);
 
-      if (!llvm::omp::isVariantApplicableInContext(rawVMI, ompCtx))
-        continue;
+  bool hasLoopAssociatedCandidate =
+      llvm::any_of(reachableVariantSpecs, [](const auto *spec) {
+        return spec && hasDirectiveAssociation(
+                           spec->DirId(), llvm::omp::Association::LoopNest);
+      });
+  SplicedAssociatedEvaluations splicedAssociatedEvaluations;
+  lower::pft::Evaluation *associatedLoopEval = nullptr;
+  llvm::scope_exit restoreEvaluationOwnership([&]() {
+    if (eval.hasNestedEvaluations())
+      splicedAssociatedEvaluations.restore(eval.getNestedEvaluations());
+  });
+  if (hasLoopAssociatedCandidate) {
+    if (lower::pft::Evaluation *loopEval =
+            spliceAssociatedDoEval(eval, &splicedAssociatedEvaluations)) {
+      associatedLoopEval = loopEval;
+      if (lower::pft::FunctionLikeUnit *owningProc =
+              eval.getOwningProcedure()) {
+        if (owningProc->getEntryEval() &&
+            splicedAssociatedEvaluations.getEntryBlock()) {
+          // Alternate ENTRY lowering starts with a branch. Emit selection in
+          // the detached associated block, which is either that branch's
+          // destination or unreachable for an ENTRY after the metadirective.
+          builder.setInsertionPointToStart(
+              splicedAssociatedEvaluations.getEntryBlock());
+        }
+      }
 
-      candidates.emplace_back(spec, rawVMI, isExplicit);
-    } else if (const auto *otherwiseClause =
-                   std::get_if<parser::OmpClause::Otherwise>(&clause.u)) {
-      if (otherwiseClause->v && otherwiseClause->v->v)
-        fallback = getFallbackVariant(otherwiseClause->v->v->value());
-    } else if (const auto *defaultVariantClause =
-                   std::get_if<parser::OmpClause::DefaultVariant>(&clause.u)) {
-      const auto &dirSpec = defaultVariantClause->v.v;
-      fallback = getFallbackVariant(dirSpec.value());
+      auto &nested = eval.getNestedEvaluations();
+      auto loopIt =
+          llvm::find_if(nested, [loopEval](lower::pft::Evaluation &e) {
+            return &e == loopEval;
+          });
+      assert(loopIt != nested.end() && "associated loop not nested");
+
+      // Attach compiler directives to the loop before any selected variant
+      // lowers it. Variant bodies skip them below to avoid processing them a
+      // second time.
+      for (auto it = nested.begin(); it != loopIt; ++it)
+        if (it->getIf<parser::CompilerDirective>())
+          converter.genEval(*it);
     }
   }
 
+  auto genMetadirectiveBody = [&]() {
+    for (lower::pft::Evaluation &nested : eval.getNestedEvaluations())
+      if (!hasLoopAssociatedCandidate ||
+          !nested.getIf<parser::CompilerDirective>())
+        converter.genEval(nested);
+  };
+
   // Lower a single resolved candidate.
   auto genVariant = [&](const parser::OmpDirectiveSpecification *spec) {
     if (!spec) {
-      genNestedEvaluations(converter, eval);
+      genMetadirectiveBody();
       return;
     }
-    List<Clause> variantClauses = makeClauses(spec->Clauses(), semaCtx);
     mlir::Location variantLoc = converter.genLocation(spec->source);
+    List<Clause> variantClauses = makeClauses(spec->Clauses(), semaCtx);
     ConstructQueue queue{
         buildConstructQueue(converter.getFirOpBuilder().getModule(), semaCtx,
                             eval, spec->source, spec->DirId(), variantClauses)};
-
-    if (llvm::any_of(queue, [](const auto &item) {
-          return llvm::omp::getDirectiveAssociation(item.id) ==
-                 llvm::omp::Association::LoopNest;
-        })) {
-      TODO(variantLoc, "loop-associated METADIRECTIVE variant");
-    }
+    unsigned ompVersion{semaCtx.langOptions().OpenMPVersion};
 
     if (llvm::any_of(queue, [](const auto &item) {
           return llvm::omp::getDirectiveAssociation(item.id) ==
@@ -7038,50 +7302,96 @@ static void genMetadirective(lower::AbstractConverter &converter,
       TODO(variantLoc, "declarative METADIRECTIVE variant");
     }
 
+    bool hasLoopAssociation =
+        hasDirectiveAssociation(queue, llvm::omp::Association::LoopNest);
+    if (hasLoopAssociation && llvm::any_of(queue, [](const auto &item) {
+          return llvm::omp::allTargetSet.test(item.id);
+        }))
+      TODO(variantLoc,
+           "TARGET construct selected by METADIRECTIVE (host-eval)");
+    if (hasLoopAssociation && hasUnsupportedDataEnvironmentDirective(queue))
+      TODO(variantLoc,
+           "data-environment construct in loop-associated METADIRECTIVE "
+           "variant");
+
+    if (hasLoopAssociation) {
+      // Name resolution cannot give a metadirective variant its own DSA
+      // scope, so marking its loop IV can otherwise contaminate an enclosing
+      // data environment.
+      if (isNestedInOpenMPDataEnvironment(
+              eval, builder.getInsertionBlock()->getParentOp()))
+        TODO(variantLoc, "loop-associated METADIRECTIVE nested in an OpenMP "
+                         "data environment");
+      if (hasUnsupportedDataSharingClause(queue, ompVersion))
+        TODO(variantLoc,
+             "data-sharing clause in loop-associated METADIRECTIVE variant");
+      if (!isSupportedMetadirectiveLoopQueue(queue))
+        TODO(variantLoc,
+             "loop-associated METADIRECTIVE variant other than DO, SIMD, or "
+             "DO SIMD");
+      // Eager privatization requires a construct-scoped IV symbol with a host
+      // association, which name resolution cannot create for a
+      // metadirective-selected loop.
+      if (!enableDelayedPrivatization)
+        TODO(variantLoc,
+             "loop-associated METADIRECTIVE with eager privatization");
+      lower::pft::Evaluation *loopEval = spliceAssociatedDoEval(eval);
+      if (!loopEval)
+        TODO(variantLoc, "loop-associated METADIRECTIVE without associated DO");
+      if (hasContentFollowingAssociatedDo(eval, *loopEval))
+        TODO(variantLoc,
+             "loop-associated METADIRECTIVE with content following the "
+             "associated DO");
+      // Unstructured loops own PFT blocks that cannot be reused by begin/end
+      // metadirectives or alternate ENTRY lowering without independent block
+      // mappings. Keep Part 2 conservative for all such loops.
+      if (loopEval->lowerAsUnstructured())
+        TODO(variantLoc, "unstructured associated DO in loop-associated "
+                         "METADIRECTIVE variant");
+      if (hasNestedOpenMPConstruct(*loopEval))
+        TODO(variantLoc, "nested OpenMP construct in loop-associated "
+                         "METADIRECTIVE loop region");
+      SymbolDSAGuard dsaGuard;
+      MetadirectiveLoopIVMarking marking =
+          markMetadirectiveLoopIVs(semaCtx, *spec, *loopEval, dsaGuard);
+      if (marking == MetadirectiveLoopIVMarking::NestTooShallow)
+        TODO(variantLoc, "METADIRECTIVE variant with COLLAPSE or ORDERED "
+                         "requires a deeper perfectly-nested loop nest than "
+                         "is present");
+      if (marking == MetadirectiveLoopIVMarking::NonCanonicalLoop)
+        TODO(variantLoc, "METADIRECTIVE variant with a non-canonical affected "
+                         "loop (a DO WHILE or a DO without loop control)");
+      if (marking == MetadirectiveLoopIVMarking::IndirectIV)
+        TODO(variantLoc, "POINTER or ALLOCATABLE loop iteration variable in "
+                         "loop-associated METADIRECTIVE variant");
+      if (marking == MetadirectiveLoopIVMarking::AssociateIV)
+        TODO(variantLoc, "ASSOCIATE name loop iteration variable in "
+                         "loop-associated METADIRECTIVE variant");
+      if (marking == MetadirectiveLoopIVMarking::ThreadprivateIV)
+        TODO(variantLoc, "THREADPRIVATE loop iteration variable in "
+                         "loop-associated METADIRECTIVE variant");
+      genOMPDispatch(converter, symTable, semaCtx, eval, variantLoc, queue,
+                     queue.begin(), dsaGuard.getMarkedSymbols());
+      return;
+    }
+
+    bool consumesBody = llvm::any_of(queue, [](const auto &item) {
+      return llvm::omp::getDirectiveAssociation(item.id) !=
+             llvm::omp::Association::None;
+    });
+    if (hasLoopAssociatedCandidate && consumesBody)
+      TODO(variantLoc,
+           "METADIRECTIVE with both block- and loop-associated variants");
+
     genOMPDispatch(converter, symTable, semaCtx, eval, variantLoc, queue,
                    queue.begin());
+    // A standalone variant (Association::None, e.g. barrier/taskwait/nothing)
+    // does not consume the metadirective's nested block, so lower it here.
+    if (!consumesBody && eval.hasNestedEvaluations())
+      genMetadirectiveBody();
   };
 
-  auto selectBestCandidate =
-      [](llvm::ArrayRef<unsigned> candidateIndices,
-         llvm::ArrayRef<MetadirectiveCandidate> candidates,
-         const semantics::omp::OmpVariantMatchContext &ompCtx)
-      -> std::optional<unsigned> {
-    if (candidateIndices.empty())
-      return std::nullopt;
-    if (candidateIndices.size() == 1)
-      return candidateIndices.front();
-
-    // The OpenMP context scorer preserves input order for tied candidates.
-    // Put explicit variants first so they take precedence over implicit
-    // `nothing`, as required by metadirective selection.
-    llvm::SmallVector<unsigned, 4> candidateOrder;
-    candidateOrder.reserve(candidateIndices.size());
-    for (unsigned idx : candidateIndices)
-      if (candidates[idx].isExplicit)
-        candidateOrder.push_back(idx);
-    for (unsigned idx : candidateIndices)
-      if (!candidates[idx].isExplicit)
-        candidateOrder.push_back(idx);
-
-    llvm::SmallVector<llvm::omp::VariantMatchInfo, 4> orderedVMIs;
-    orderedVMIs.reserve(candidateOrder.size());
-    for (unsigned idx : candidateOrder)
-      orderedVMIs.push_back(candidates[idx].vmi);
-
-    int bestIdx = llvm::omp::getBestVariantMatchForContext(orderedVMIs, ompCtx);
-    if (bestIdx >= 0) {
-      assert(static_cast<size_t>(bestIdx) < candidateOrder.size() &&
-             "best variant index out of range");
-      return candidateOrder[bestIdx];
-    }
-    return std::nullopt;
-  };
-
-  llvm::SmallVector<unsigned, 4> remainingCandidates;
-  remainingCandidates.reserve(candidates.size());
-  for (unsigned idx = 0, end = candidates.size(); idx < end; ++idx)
-    remainingCandidates.push_back(idx);
+  llvm::SmallVector<unsigned, 4> remainingCandidates{allCandidateIndices};
 
   lower::StatementContext stmtCtx;
 
@@ -7102,23 +7412,23 @@ static void genMetadirective(lower::AbstractConverter &converter,
   // Stop when selection reaches an unguarded candidate or the fallback.
   while (!remainingCandidates.empty()) {
     std::optional<unsigned> selected =
-        selectBestCandidate(remainingCandidates, candidates, ompCtx);
+        semantics::omp::SelectBestMetadirectiveCandidate(remainingCandidates,
+                                                         candidates, ompCtx);
     if (!selected) {
       genVariant(fallback);
       return;
     }
 
-    const MetadirectiveCandidate &candidate = candidates[*selected];
-    if (!candidate.dynamicCond) {
+    const semantics::omp::MetadirectiveCandidate &candidate =
+        candidates[*selected];
+    if (!candidate.dynamicCondition) {
       genVariant(candidate.spec);
       return;
     }
 
-    llvm::SmallVector<unsigned, 4> elsePathCandidates(remainingCandidates);
-    auto *remainingIt = llvm::find(elsePathCandidates, *selected);
-    assert(remainingIt != elsePathCandidates.end() &&
-           "selected candidate missing from remaining candidates");
-    elsePathCandidates.erase(remainingIt);
+    llvm::SmallVector<unsigned, 4> elsePathCandidates =
+        semantics::omp::GetMetadirectiveElsePathCandidates(
+            *selected, remainingCandidates, candidates, ompCtx, semaCtx);
 
     // match_any may create a guarded condition-true candidate and an unguarded
     // static candidate for the same directive. If the else path picks the
@@ -7127,20 +7437,29 @@ static void genMetadirective(lower::AbstractConverter &converter,
     //   if (flag) barrier    into just    barrier
     //   else barrier
     if (std::optional<unsigned> selectedInElse =
-            selectBestCandidate(elsePathCandidates, candidates, ompCtx)) {
-      const MetadirectiveCandidate &candidateInElse =
+            semantics::omp::SelectBestMetadirectiveCandidate(
+                elsePathCandidates, candidates, ompCtx)) {
+      const semantics::omp::MetadirectiveCandidate &candidateInElse =
           candidates[*selectedInElse];
-      if (!candidateInElse.dynamicCond &&
+      if (!candidateInElse.dynamicCondition &&
           candidateInElse.spec == candidate.spec) {
         genVariant(candidate.spec);
         return;
       }
     }
 
+    // Unstructured evaluations own PFT blocks that lowering reparents into the
+    // generated region. They cannot be reused for both sides of a runtime
+    // selection until each arm can receive an independent block mapping.
+    if (associatedLoopEval && associatedLoopEval->lowerAsUnstructured())
+      TODO(converter.genLocation(candidate.dynamicCondition->source),
+           "unstructured associated DO in loop-associated METADIRECTIVE "
+           "variant");
+
     mlir::Location condLoc =
-        converter.genLocation(candidate.dynamicCond->source);
+        converter.genLocation(candidate.dynamicCondition->source);
     const auto *condExpr =
-        semantics::GetExpr(semaCtx, *candidate.dynamicCond->expr);
+        semantics::GetExpr(semaCtx, *candidate.dynamicCondition->expr);
     assert(condExpr && "missing expression for user condition");
     mlir::Value condVal =
         fir::getBase(converter.genExprValue(*condExpr, stmtCtx, &condLoc));
@@ -7816,7 +8135,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 (!std::holds_alternative<parser::OmpMetadirectiveDirective>(omp.u))
+    genNestedEvaluations(converter, eval);
 }
 
 void Fortran::lower::genOpenMPSymbolProperties(
diff --git a/flang/lib/Lower/OpenMP/Utils.cpp b/flang/lib/Lower/OpenMP/Utils.cpp
index 4e7cf5ca34cc3..a5c7875cd2f80 100644
--- a/flang/lib/Lower/OpenMP/Utils.cpp
+++ b/flang/lib/Lower/OpenMP/Utils.cpp
@@ -709,7 +709,7 @@ static void processTileSizesFromOpenMPConstruct(
 // can happen when COLLAPSE counts loops that a transforming construct such as
 // TILE generates from the source DO loops. getNestedDoConstruct wraps this for
 // callers that require a DO construct and asserts when none is found.
-static pft::Evaluation *tryGetNestedDoConstruct(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>>
diff --git a/flang/lib/Lower/OpenMP/Utils.h b/flang/lib/Lower/OpenMP/Utils.h
index 94f85c43f7033..256e928eb0f6c 100644
--- a/flang/lib/Lower/OpenMP/Utils.h
+++ b/flang/lib/Lower/OpenMP/Utils.h
@@ -170,6 +170,8 @@ void genObjectList(const ObjectList &objects,
 void lastprivateModifierNotSupported(const omp::clause::Lastprivate &lastp,
                                      mlir::Location loc);
 
+pft::Evaluation *tryGetNestedDoConstruct(pft::Evaluation &eval);
+
 pft::Evaluation *getNestedDoConstruct(pft::Evaluation &eval);
 
 int64_t collectLoopRelatedInfo(
diff --git a/flang/lib/Semantics/openmp-utils.cpp b/flang/lib/Semantics/openmp-utils.cpp
index dc3d5a302e841..fa4b8f4f26987 100644
--- a/flang/lib/Semantics/openmp-utils.cpp
+++ b/flang/lib/Semantics/openmp-utils.cpp
@@ -22,6 +22,7 @@
 #include "flang/Evaluate/check-expression.h"
 #include "flang/Evaluate/expression.h"
 #include "flang/Evaluate/match.h"
+#include "flang/Evaluate/rewrite.h"
 #include "flang/Evaluate/tools.h"
 #include "flang/Evaluate/traverse.h"
 #include "flang/Evaluate/type.h"
@@ -2521,6 +2522,340 @@ std::optional<DynamicUserCondition> MakeVariantMatchInfo(
   return dynamicCond;
 }
 
+std::optional<MetadirectiveCandidateSet> BuildMetadirectiveCandidateSet(
+    const parser::OmpClauseList &clauses, SemanticsContext &context,
+    const OmpVariantMatchContext &matchContext) {
+  MetadirectiveCandidateSet result;
+
+  auto getContextSelector = [](const parser::OmpClause::When &whenClause)
+      -> const parser::modifier::OmpContextSelector * {
+    const auto &modifiers{std::get<0>(whenClause.v.t)};
+    if (!modifiers || modifiers->size() != 1) {
+      return nullptr;
+    }
+    return std::get_if<parser::modifier::OmpContextSelector>(
+        &modifiers->front().u);
+  };
+
+  auto getDirectiveVariant = [](const parser::OmpClause::When &whenClause)
+      -> std::pair<const parser::OmpDirectiveSpecification *, bool> {
+    const auto &optionalSpec{std::get<1>(whenClause.v.t)};
+    if (!optionalSpec) {
+      return {nullptr, false};
+    }
+    if (optionalSpec->value().DirId() == llvm::omp::Directive::OMPD_nothing) {
+      return {nullptr, true};
+    }
+    return {&optionalSpec->value(), true};
+  };
+
+  auto getFallbackVariant = [](const parser::OmpDirectiveSpecification &spec) {
+    return spec.DirId() == llvm::omp::Directive::OMPD_nothing ? nullptr : &spec;
+  };
+
+  for (const parser::OmpClause &clause : clauses.v) {
+    if (const auto *whenClause{
+            std::get_if<parser::OmpClause::When>(&clause.u)}) {
+      const auto *ctxSel{getContextSelector(*whenClause)};
+      if (!ctxSel ||
+          FindUnsupportedSelectorFeature(*ctxSel, context) !=
+              UnsupportedSelectorFeature::None) {
+        return std::nullopt;
+      }
+
+      auto [spec, isExplicit]{getDirectiveVariant(*whenClause)};
+      llvm::omp::VariantMatchInfo rawVMI;
+      std::optional<DynamicUserCondition> dynamicCondition{
+          MakeVariantMatchInfo(rawVMI, *ctxSel, context)};
+      if (llvm::any_of(
+              rawVMI.ConstructTraits, [](llvm::omp::TraitProperty property) {
+                return llvm::omp::getOpenMPContextTraitSetForProperty(
+                           property) != llvm::omp::TraitSet::construct;
+              })) {
+        return std::nullopt;
+      }
+
+      if (dynamicCondition) {
+        constexpr llvm::omp::TraitProperty dynamicConditionTrait{
+            llvm::omp::TraitProperty::user_condition_unknown};
+        constexpr llvm::omp::TraitProperty matchAnyTrait{
+            llvm::omp::TraitProperty::implementation_extension_match_any};
+        constexpr llvm::omp::TraitProperty matchNoneTrait{
+            llvm::omp::TraitProperty::implementation_extension_match_none};
+
+        // Static applicability uses only traits known at compile time. Keep
+        // the condition's score so a true runtime condition is still ranked
+        // correctly.
+        llvm::omp::VariantMatchInfo staticVMI{rawVMI};
+        std::optional<llvm::APInt> conditionScore;
+        auto scoreIt{staticVMI.ScoreMap.find(dynamicConditionTrait)};
+        if (scoreIt != staticVMI.ScoreMap.end()) {
+          conditionScore = scoreIt->second;
+          staticVMI.ScoreMap.erase(scoreIt);
+        }
+        staticVMI.RequiredTraits.reset(unsigned(dynamicConditionTrait));
+        llvm::APInt *conditionScorePtr{
+            conditionScore ? &*conditionScore : nullptr};
+
+        bool hasMatchAny{rawVMI.RequiredTraits.test(unsigned(matchAnyTrait))};
+        bool hasMatchNone{rawVMI.RequiredTraits.test(unsigned(matchNoneTrait))};
+        bool isStaticVMIApplicable{
+            llvm::omp::isVariantApplicableInContext(staticVMI, matchContext)};
+        // Only match_any can remain applicable when the static traits do not
+        // match, because a true runtime condition may satisfy the selector.
+        if (!isStaticVMIApplicable) {
+          if (!hasMatchAny ||
+              staticVMI.RequiredTraits.test(
+                  unsigned(llvm::omp::TraitProperty::invalid))) {
+            continue;
+          }
+
+          llvm::omp::VariantMatchInfo conditionTrueVMI{staticVMI};
+          conditionTrueVMI.addTrait(
+              llvm::omp::TraitProperty::user_condition_true, "<condition>",
+              conditionScorePtr);
+          if (!llvm::omp::isVariantApplicableInContext(
+                  conditionTrueVMI, matchContext)) {
+            continue;
+          }
+        }
+
+        auto addConditionTraitForRanking =
+            [&](llvm::omp::VariantMatchInfo &rankingVMI) {
+              rankingVMI.addTrait(hasMatchNone
+                      ? dynamicConditionTrait
+                      : llvm::omp::TraitProperty::user_condition_true,
+                  "<condition>", conditionScorePtr);
+            };
+
+        if (hasMatchAny && isStaticVMIApplicable) {
+          // Represent both outcomes: a guarded candidate with the condition's
+          // score and an unguarded candidate with only the static traits. If
+          // the WHEN clause omits its directive, only add the unguarded
+          // candidate.
+          if (isExplicit) {
+            llvm::omp::VariantMatchInfo conditionTrueVMI{staticVMI};
+            addConditionTraitForRanking(conditionTrueVMI);
+            result.candidates.push_back({spec, std::move(conditionTrueVMI),
+                isExplicit, dynamicCondition});
+          }
+          result.candidates.push_back({spec, std::move(staticVMI), isExplicit});
+          continue;
+        }
+
+        llvm::omp::VariantMatchInfo rankingVMI{staticVMI};
+        // Preserve the existing lowering behavior for an omitted directive:
+        // do not let its runtime condition raise the implicit NOTHING rank.
+        if (!isExplicit && hasMatchAny && !isStaticVMIApplicable)
+          rankingVMI = llvm::omp::VariantMatchInfo();
+        else if (isExplicit)
+          addConditionTraitForRanking(rankingVMI);
+        result.candidates.push_back({spec, std::move(rankingVMI), isExplicit,
+            dynamicCondition, /*conditionShouldBeTrue=*/!hasMatchNone});
+        continue;
+      }
+
+      if (!llvm::omp::isVariantApplicableInContext(rawVMI, matchContext)) {
+        continue;
+      }
+      result.candidates.push_back({spec, std::move(rawVMI), isExplicit});
+    } else if (const auto *otherwiseClause{
+                   std::get_if<parser::OmpClause::Otherwise>(&clause.u)}) {
+      if (otherwiseClause->v && otherwiseClause->v->v) {
+        result.fallback = getFallbackVariant(otherwiseClause->v->v->value());
+      }
+    } else if (const auto *defaultVariantClause{
+                   std::get_if<parser::OmpClause::DefaultVariant>(&clause.u)}) {
+      result.fallback = getFallbackVariant(defaultVariantClause->v.v.value());
+    }
+  }
+  return result;
+}
+
+std::optional<unsigned> SelectBestMetadirectiveCandidate(
+    llvm::ArrayRef<unsigned> candidateIndices,
+    llvm::ArrayRef<MetadirectiveCandidate> candidates,
+    const OmpVariantMatchContext &matchContext) {
+  if (candidateIndices.empty()) {
+    return std::nullopt;
+  }
+  if (candidateIndices.size() == 1) {
+    return candidateIndices.front();
+  }
+
+  // The context scorer preserves input order for ties. Explicit replacements
+  // take precedence over an omitted directive's implicit NOTHING.
+  llvm::SmallVector<unsigned, 4> candidateOrder;
+  candidateOrder.reserve(candidateIndices.size());
+  for (unsigned index : candidateIndices) {
+    if (candidates[index].isExplicit) {
+      candidateOrder.push_back(index);
+    }
+  }
+  for (unsigned index : candidateIndices) {
+    if (!candidates[index].isExplicit) {
+      candidateOrder.push_back(index);
+    }
+  }
+
+  llvm::SmallVector<llvm::omp::VariantMatchInfo, 4> orderedVMIs;
+  orderedVMIs.reserve(candidateOrder.size());
+  for (unsigned index : candidateOrder) {
+    orderedVMIs.push_back(candidates[index].vmi);
+  }
+
+  int bestIndex{
+      llvm::omp::getBestVariantMatchForContext(orderedVMIs, matchContext)};
+  if (bestIndex < 0) {
+    return std::nullopt;
+  }
+  CHECK(static_cast<std::size_t>(bestIndex) < candidateOrder.size());
+  return candidateOrder[bestIndex];
+}
+
+namespace {
+struct MetadirectiveConditionNormalizer : evaluate::rewrite::Identity {
+  using evaluate::rewrite::Identity::operator();
+
+  template <typename T>
+  evaluate::Expr<T> operator()(
+      evaluate::Expr<T> &&, const evaluate::Parentheses<T> &parentheses) {
+    return common::Clone(parentheses.left());
+  }
+
+  template <int KIND>
+  evaluate::Expr<evaluate::Type<common::TypeCategory::Logical, KIND>>
+  operator()(evaluate::Expr<evaluate::Type<common::TypeCategory::Logical, KIND>>
+                 &&expr,
+      const evaluate::LogicalOperation<KIND> &operation) {
+    if ((operation.logicalOperator == evaluate::LogicalOperator::And ||
+            operation.logicalOperator == evaluate::LogicalOperator::Or) &&
+        operation.left() == operation.right())
+      return common::Clone(operation.left());
+    return std::move(expr);
+  }
+};
+
+bool isRepeatableMetadirectiveCondition(const SomeExpr &expr) {
+  // A procedure call can depend on state that is not represented in the
+  // expression tree, so conservatively do not correlate calls, even if the
+  // procedure is pure. This also rejects coarray references and other
+  // expression nodes that are unsafe to copy.
+  if (!evaluate::IsSafelyCopyable(expr))
+    return false;
+
+  for (const Symbol &symbol : evaluate::CollectSymbols(expr)) {
+    const Symbol &ultimate{symbol.GetUltimate()};
+    if (ultimate.attrs().HasAny({Attr::ASYNCHRONOUS, Attr::VOLATILE}) ||
+        evaluate::IsCoarray(ultimate))
+      return false;
+  }
+  return true;
+}
+} // namespace
+
+bool IsRepeatableMetadirectiveCondition(
+    const parser::ScalarExpr &condition, SemanticsContext &context) {
+  const SomeExpr *expr{GetExpr(context, condition)};
+  return expr && isRepeatableMetadirectiveCondition(*expr);
+}
+
+bool AreSameRepeatableMetadirectiveCondition(const parser::ScalarExpr &left,
+    const parser::ScalarExpr &right, SemanticsContext &context) {
+  const SomeExpr *leftExpr{GetExpr(context, left)};
+  const SomeExpr *rightExpr{GetExpr(context, right)};
+  if (!leftExpr || !rightExpr ||
+      !isRepeatableMetadirectiveCondition(*leftExpr) ||
+      !isRepeatableMetadirectiveCondition(*rightExpr))
+    return false;
+
+  MetadirectiveConditionNormalizer normalizer;
+  evaluate::rewrite::Mutator normalize{normalizer};
+  return normalize(*leftExpr) == normalize(*rightExpr);
+}
+
+llvm::SmallVector<unsigned, 4> GetMetadirectiveElsePathCandidates(
+    unsigned selectedIndex, llvm::ArrayRef<unsigned> candidateIndices,
+    llvm::ArrayRef<MetadirectiveCandidate> candidates,
+    const OmpVariantMatchContext &matchContext, SemanticsContext &context) {
+  CHECK(selectedIndex < candidates.size());
+  const MetadirectiveCandidate &selected{candidates[selectedIndex]};
+  CHECK(selected.dynamicCondition);
+
+  llvm::SmallVector<unsigned, 4> result;
+  result.reserve(candidateIndices.size());
+  for (unsigned index : candidateIndices)
+    if (index != selectedIndex)
+      result.push_back(index);
+
+  // Inspect candidates in the order in which selection would evaluate them.
+  // A distinct repeatable condition cannot modify the selected condition, so
+  // the failed value remains usable past it. Stop at the first non-repeatable
+  // condition because it can change state before a lower-ranked occurrence is
+  // evaluated.
+  llvm::SmallVector<unsigned, 4> candidatesToInspect{result};
+  while (std::optional<unsigned> next{SelectBestMetadirectiveCandidate(
+      candidatesToInspect, candidates, matchContext)}) {
+    const MetadirectiveCandidate &candidate{candidates[*next]};
+    if (!candidate.dynamicCondition ||
+        !IsRepeatableMetadirectiveCondition(
+            *candidate.dynamicCondition->expr, context))
+      break;
+
+    bool hasSameFailedCondition{
+        candidate.conditionShouldBeTrue == selected.conditionShouldBeTrue &&
+        AreSameRepeatableMetadirectiveCondition(
+            *selected.dynamicCondition->expr, *candidate.dynamicCondition->expr,
+            context)};
+    if (hasSameFailedCondition)
+      llvm::erase(result, *next);
+    llvm::erase(candidatesToInspect, *next);
+  }
+  return result;
+}
+
+llvm::SmallVector<const parser::OmpDirectiveSpecification *, 4>
+GetReachableMetadirectiveVariants(const MetadirectiveCandidateSet &candidateSet,
+    const OmpVariantMatchContext &matchContext, SemanticsContext &context) {
+  llvm::SmallVector<unsigned, 4> candidates;
+  candidates.reserve(candidateSet.candidates.size());
+  for (unsigned index{0}; index < candidateSet.candidates.size(); ++index) {
+    candidates.push_back(index);
+  }
+
+  llvm::SmallVector<const parser::OmpDirectiveSpecification *, 4> reachable;
+  while (true) {
+    std::optional<unsigned> selected{SelectBestMetadirectiveCandidate(
+        candidates, candidateSet.candidates, matchContext)};
+    if (!selected) {
+      reachable.push_back(candidateSet.fallback);
+      break;
+    }
+
+    const MetadirectiveCandidate &candidate{candidateSet.candidates[*selected]};
+    reachable.push_back(candidate.spec);
+    // An unguarded winner ends selection. A dynamic winner leaves the
+    // remaining candidates reachable through its false path.
+    if (!candidate.dynamicCondition) {
+      break;
+    }
+
+    candidates = GetMetadirectiveElsePathCandidates(
+        *selected, candidates, candidateSet.candidates, matchContext, context);
+
+    if (std::optional<unsigned> selectedInElse{SelectBestMetadirectiveCandidate(
+            candidates, candidateSet.candidates, matchContext)}) {
+      const MetadirectiveCandidate &elseCandidate{
+          candidateSet.candidates[*selectedInElse]};
+      if (!elseCandidate.dynamicCondition &&
+          elseCandidate.spec == candidate.spec) {
+        break;
+      }
+    }
+  }
+  return reachable;
+}
+
 bool MayVariantBeSelected(
     const parser::traits::OmpContextSelectorSpecification *selector,
     SemanticsContext &context, OmpVariantMatchContext &matchContext) {
diff --git a/flang/test/Lower/OpenMP/Todo/metadirective-loop-data-environment.f90 b/flang/test/Lower/OpenMP/Todo/metadirective-loop-data-environment.f90
new file mode 100644
index 0000000000000..8b641e77ccfe9
--- /dev/null
+++ b/flang/test/Lower/OpenMP/Todo/metadirective-loop-data-environment.f90
@@ -0,0 +1,94 @@
+! Loop-associated metadirective variants cannot yet reconstruct variant-local
+! data-sharing relationships. Cover selected data environments, explicit
+! data-sharing clauses, enclosing data environments, and eager privatization.
+
+! RUN: split-file %s %t
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
+! RUN:   -o - %t/parallel-do.f90 2>&1 \
+! RUN:   | FileCheck --check-prefix=DATA-ENV %s
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
+! RUN:   -o - %t/private.f90 2>&1 \
+! RUN:   | FileCheck --check-prefix=DATA-SHARING %s
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
+! RUN:   -mmlir --enable-delayed-privatization=false \
+! RUN:   -o - %t/eager.f90 2>&1 | FileCheck --check-prefix=EAGER %s
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
+! RUN:   -o - %t/enclosing-static.f90 2>&1 \
+! RUN:   | FileCheck --check-prefix=ENCLOSING %s
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
+! RUN:   -o - %t/selected-metadirective.f90 2>&1 \
+! RUN:   | FileCheck --check-prefix=SELECTED %s
+
+! DATA-ENV: not yet implemented: data-environment construct in loop-associated METADIRECTIVE variant
+! DATA-SHARING: not yet implemented: data-sharing clause in loop-associated METADIRECTIVE variant
+! EAGER: not yet implemented: loop-associated METADIRECTIVE with eager privatization
+! ENCLOSING: not yet implemented: loop-associated METADIRECTIVE nested in an OpenMP data environment
+! SELECTED: not yet implemented: loop-associated METADIRECTIVE nested in an OpenMP data environment
+
+!--- parallel-do.f90
+subroutine test_parallel_do(n, a)
+  integer :: n, a(n), i
+  !$omp metadirective &
+  !$omp & when(implementation={vendor(llvm)}: parallel do) &
+  !$omp & otherwise(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
+
+!--- private.f90
+subroutine test_private(n, a)
+  integer :: n, a(n), i, x
+  !$omp metadirective &
+  !$omp & when(implementation={vendor(llvm)}: do private(x)) &
+  !$omp & otherwise(nothing)
+  do i = 1, n
+    x = i
+    a(i) = x
+  end do
+end subroutine
+
+!--- eager.f90
+subroutine test_eager_privatization(n, a)
+  integer :: n, a(n), i
+  !$omp metadirective &
+  !$omp & when(implementation={vendor(llvm)}: do) &
+  !$omp & otherwise(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
+
+!--- enclosing-static.f90
+subroutine test_static_do_in_parallel(n, a, after)
+  integer :: n, a(n), after, i
+  i = 0
+  !$omp parallel num_threads(1) shared(n, a, after, i)
+  !$omp metadirective &
+  !$omp & when(implementation={vendor(llvm)}: do) &
+  !$omp & otherwise(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+  after = i
+  !$omp end parallel
+end subroutine
+
+!--- selected-metadirective.f90
+subroutine test_do_in_selected_parallel(flag, n, a, after)
+  logical, intent(in) :: flag
+  integer :: n, a(n), after, i
+  i = 0
+  !$omp begin metadirective &
+  !$omp & when(implementation={vendor(llvm)}: &
+  !$omp &   parallel num_threads(1) shared(flag, n, a, after, i)) &
+  !$omp & otherwise(nothing)
+  !$omp metadirective &
+  !$omp & when(user={condition(flag)}: do) &
+  !$omp & otherwise(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+  after = i
+  !$omp end metadirective
+end subroutine
diff --git a/flang/test/Lower/OpenMP/Todo/metadirective-loop-iteration-variable.f90 b/flang/test/Lower/OpenMP/Todo/metadirective-loop-iteration-variable.f90
new file mode 100644
index 0000000000000..6c011fe333177
--- /dev/null
+++ b/flang/test/Lower/OpenMP/Todo/metadirective-loop-iteration-variable.f90
@@ -0,0 +1,57 @@
+! Some loop iteration variables require construct-scoped name resolution for
+! the private, linear, or lastprivate bindings of a selected loop variant.
+
+! RUN: split-file %s %t
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
+! RUN:   -o - %t/associate-do.f90 2>&1 \
+! RUN:   | FileCheck --check-prefix=ASSOCIATE %s
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
+! RUN:   -o - %t/pointer.f90 2>&1 \
+! RUN:   | FileCheck --check-prefix=INDIRECT %s
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
+! RUN:   -o - %t/threadprivate.f90 2>&1 \
+! RUN:   | FileCheck --check-prefix=THREADPRIVATE %s
+
+! ASSOCIATE: not yet implemented: ASSOCIATE name loop iteration variable in loop-associated METADIRECTIVE variant
+! INDIRECT: not yet implemented: POINTER or ALLOCATABLE loop iteration variable in loop-associated METADIRECTIVE variant
+! THREADPRIVATE: not yet implemented: THREADPRIVATE loop iteration variable in loop-associated METADIRECTIVE variant
+
+!--- associate-do.f90
+subroutine test_do(n, a)
+  integer :: n, a(n), source_i
+  associate(i => source_i)
+    !$omp metadirective &
+    !$omp & when(implementation={vendor(llvm)}: do) &
+    !$omp & otherwise(nothing)
+    do i = 1, n
+      a(i) = i
+    end do
+  end associate
+end subroutine
+
+!--- pointer.f90
+subroutine test_pointer_iv(n, a)
+  integer :: n, a(n)
+  integer, target :: target
+  integer, pointer :: i
+  i => target
+  !$omp metadirective &
+  !$omp & when(implementation={vendor(llvm)}: do) &
+  !$omp & otherwise(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
+
+!--- threadprivate.f90
+subroutine test_threadprivate_iv(n, a)
+  integer :: n, a(n)
+  integer, save :: i
+  !$omp threadprivate(i)
+  !$omp metadirective &
+  !$omp & when(implementation={vendor(llvm)}: do) &
+  !$omp & otherwise(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
diff --git a/flang/test/Lower/OpenMP/Todo/metadirective-loop-unstructured.f90 b/flang/test/Lower/OpenMP/Todo/metadirective-loop-unstructured.f90
new file mode 100644
index 0000000000000..08ebcc7b4c8ef
--- /dev/null
+++ b/flang/test/Lower/OpenMP/Todo/metadirective-loop-unstructured.f90
@@ -0,0 +1,39 @@
+! Defer unstructured associated loops until every selection path can give its
+! PFT blocks an independent mapping.
+
+! RUN: split-file %s %t
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 -o - %t/static.f90 2>&1 | FileCheck %s
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 -o - %t/runtime.f90 2>&1 | FileCheck %s
+
+! CHECK: not yet implemented: unstructured associated DO in loop-associated METADIRECTIVE variant
+
+!--- static.f90
+subroutine test_static(n, a, selector)
+  integer :: n, a(n), selector, i
+  !$omp metadirective &
+  !$omp & when(implementation={vendor(llvm)}: do) &
+  !$omp & otherwise(nothing)
+  do i = 1, n
+    go to (10, 20), selector
+10  a(i) = 1
+    go to 30
+20  a(i) = 2
+30  continue
+  end do
+end subroutine
+
+!--- runtime.f90
+subroutine test_runtime(flag, n, a, selector)
+  logical :: flag
+  integer :: n, a(n), selector, i
+  !$omp metadirective &
+  !$omp & when(user={condition(flag)}: do) &
+  !$omp & otherwise(nothing)
+  do i = 1, n
+    go to (10, 20), selector
+10  a(i) = 1
+    go to 30
+20  a(i) = 2
+30  continue
+  end do
+end subroutine
diff --git a/flang/test/Lower/OpenMP/Todo/metadirective-loop-unsupported-replacements.f90 b/flang/test/Lower/OpenMP/Todo/metadirective-loop-unsupported-replacements.f90
new file mode 100644
index 0000000000000..12ef39dac6edb
--- /dev/null
+++ b/flang/test/Lower/OpenMP/Todo/metadirective-loop-unsupported-replacements.f90
@@ -0,0 +1,111 @@
+! Part 2 supports ordinary DO, SIMD, and DO SIMD loop replacement arms. Keep
+! unsupported association mixes, directives, body shapes, and target
+! host-evaluation paths diagnosed until their lowering is implemented.
+
+! RUN: split-file %s %t
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
+! RUN:   -o - %t/mixed-association.f90 2>&1 \
+! RUN:   | FileCheck --check-prefix=MIXED %s
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
+! RUN:   -o - %t/loop-directive.f90 2>&1 \
+! RUN:   | FileCheck --check-prefix=DIRECTIVE %s
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
+! RUN:   -o - %t/target-loop.f90 2>&1 \
+! RUN:   | FileCheck --check-prefix=TARGET %s
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
+! RUN:   -o - %t/empty-delimited-body.f90 2>&1 \
+! RUN:   | FileCheck --check-prefix=EMPTY %s
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
+! RUN:   -cpp -DOMP_52 -o - %t/trailing-static.f90 2>&1 \
+! RUN:   | FileCheck --check-prefix=TRAILING %s
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
+! RUN:   -cpp -DOMP_52 -o - %t/nested-openmp.f90 2>&1 \
+! RUN:   | FileCheck --check-prefix=NESTED %s
+
+! MIXED: not yet implemented: METADIRECTIVE with both block- and loop-associated variants
+! DIRECTIVE: not yet implemented: loop-associated METADIRECTIVE variant other than DO, SIMD, or DO SIMD
+! TARGET: not yet implemented: TARGET construct selected by METADIRECTIVE (host-eval)
+! EMPTY: not yet implemented: loop-associated METADIRECTIVE without associated
+! EMPTY-SAME: DO
+! TRAILING: not yet implemented: loop-associated METADIRECTIVE with content following the associated DO
+! NESTED: not yet implemented: nested OpenMP construct in loop-associated METADIRECTIVE loop region
+
+!--- mixed-association.f90
+subroutine test_single_fallback(flag, n, a)
+  logical, intent(in) :: flag
+  integer :: n, a(n), i
+  !$omp begin metadirective &
+  !$omp & when(user={condition(flag)}: do) &
+  !$omp & otherwise(single)
+  do i = 1, n
+    a(i) = i
+  end do
+  !$omp end metadirective
+end subroutine
+
+!--- loop-directive.f90
+subroutine test_loop(n, a)
+  integer :: n, a(n), i
+  !$omp metadirective &
+  !$omp & when(implementation={vendor(llvm)}: loop bind(thread)) &
+  !$omp & otherwise(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
+
+!--- target-loop.f90
+subroutine test_target_loop()
+  integer :: i
+  !$omp metadirective &
+  !$omp & when(implementation={vendor(llvm)}: target teams distribute parallel do) &
+  !$omp & otherwise(nothing)
+  do i = 1, 100
+  end do
+end subroutine
+
+!--- empty-delimited-body.f90
+subroutine test_empty_delimited_body(n, a)
+  integer :: n, a(n), i
+  !$omp begin metadirective &
+  !$omp & when(implementation={vendor(llvm)}: do) &
+  !$omp & otherwise(nothing)
+  !$omp end metadirective
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
+
+!--- trailing-static.f90
+subroutine test_trailing_static(n, a, x)
+  integer :: n, a(n), x, i
+  !$omp begin metadirective &
+  !$omp & when(implementation={vendor(llvm)}: do) &
+#ifdef OMP_52
+  !$omp & otherwise(nothing)
+#else
+  !$omp & default(nothing)
+#endif
+  do i = 1, n
+    a(i) = i
+  end do
+  x = 42
+  !$omp end metadirective
+end subroutine
+
+!--- nested-openmp.f90
+subroutine test_nested_openmp(n, a)
+  integer :: n, a(n), i
+  !$omp metadirective &
+  !$omp & when(implementation={vendor(llvm)}: do) &
+#ifdef OMP_52
+  !$omp & otherwise(nothing)
+#else
+  !$omp & default(nothing)
+#endif
+  do i = 1, n
+    !$omp parallel
+    a(i) = i
+    !$omp end parallel
+  end do
+end subroutine
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/metadirective-implementation.f90 b/flang/test/Lower/OpenMP/metadirective-implementation.f90
index d85a8ecda2466..af8f2af938ca5 100644
--- a/flang/test/Lower/OpenMP/metadirective-implementation.f90
+++ b/flang/test/Lower/OpenMP/metadirective-implementation.f90
@@ -1,6 +1,7 @@
 ! Test lowering of OpenMP metadirective with implementation selectors.
 
-! RUN: %flang_fc1 -fopenmp -emit-hlfir -fopenmp-version=50 %s -o - | FileCheck %s
+! RUN: %flang_fc1 -fopenmp -emit-hlfir -fopenmp-version=50 %s -o - \
+! RUN:   | FileCheck %s
 ! RUN: %flang_fc1 -fopenmp -emit-hlfir -fopenmp-version=51 %s -o - | FileCheck %s
 ! RUN: %flang_fc1 -fopenmp -emit-hlfir -fopenmp-version=52 -cpp -DOMP_52 %s -o - | FileCheck %s
 
@@ -30,6 +31,54 @@ subroutine test_vendor_no_match()
 #endif
 end subroutine
 
+! A selected variant provides a positive control: its clause expression is
+! lowered and attached to the replacement directive.
+! CHECK-LABEL: func.func @_QPtest_selected_clause(
+! CHECK:         %[[NUM_THREADS:.*]] = fir.call @_QPmetadirective_num_threads()
+! CHECK:         omp.parallel num_threads(%[[NUM_THREADS]] : i32)
+! CHECK:           hlfir.assign
+! CHECK:         return
+subroutine test_selected_clause(x)
+  integer :: x, metadirective_num_threads
+  external :: metadirective_num_threads
+  !$omp begin metadirective &
+  !$omp & when(implementation={vendor(llvm)}: &
+  !$omp &   parallel num_threads(metadirective_num_threads())) &
+#ifdef OMP_52
+  !$omp & otherwise(nothing)
+#else
+  !$omp & default(nothing)
+#endif
+  x = 1
+  !$omp end metadirective
+end subroutine
+
+! A statically applicable but lower-ranked candidate must not have its clauses
+! lowered either.
+! CHECK-LABEL: func.func @_QPtest_unselected_ranked_clause(
+! CHECK-NOT:     fir.call @_QPmetadirective_num_threads
+! CHECK:         omp.parallel
+! CHECK-NOT:     num_threads
+! CHECK-NOT:     fir.call @_QPmetadirective_num_threads
+! CHECK:           hlfir.assign
+! CHECK-NOT:     fir.call @_QPmetadirective_num_threads
+! CHECK:         return
+subroutine test_unselected_ranked_clause(x)
+  integer :: x, metadirective_num_threads
+  external :: metadirective_num_threads
+  !$omp begin metadirective &
+  !$omp & when(user={condition(score(1): .true.)}: &
+  !$omp &   parallel num_threads(metadirective_num_threads())) &
+  !$omp & when(user={condition(score(2): .true.)}: parallel) &
+#ifdef OMP_52
+  !$omp & otherwise(nothing)
+#else
+  !$omp & default(nothing)
+#endif
+  x = 1
+  !$omp end metadirective
+end subroutine
+
 ! CHECK-LABEL: func.func @_QPtest_standalone_barrier_match()
 ! CHECK:         omp.barrier
 ! CHECK:         return
diff --git a/flang/test/Lower/OpenMP/metadirective-loop.f90 b/flang/test/Lower/OpenMP/metadirective-loop.f90
new file mode 100644
index 0000000000000..adee0b6128d5e
--- /dev/null
+++ b/flang/test/Lower/OpenMP/metadirective-loop.f90
@@ -0,0 +1,562 @@
+! Test lowering of metadirectives with ordinary loop-associated variants.
+
+! RUN: %flang_fc1 -fopenmp -emit-hlfir -fopenmp-version=52 %s -o - | FileCheck %s
+
+! CHECK: #loop_unroll = #llvm.loop_unroll<disable = false, count = 4 : i64>
+! CHECK: #loop_annotation = #llvm.loop_annotation<unroll = #loop_unroll>
+
+! CHECK-LABEL: func.func @_QPtest_do(
+! CHECK-NOT:     omp.parallel
+! CHECK:         omp.wsloop
+! CHECK:           omp.loop_nest
+! CHECK:             hlfir.assign
+! CHECK:             omp.yield
+! CHECK:         return
+subroutine test_do(n, a)
+  integer :: n, a(n), i
+  !$omp metadirective &
+  !$omp & when(implementation={vendor(llvm)}: do) &
+  !$omp & otherwise(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_simd(
+! CHECK-NOT:     omp.wsloop
+! CHECK:         omp.simd linear(
+! CHECK:           omp.loop_nest
+! CHECK:             hlfir.assign
+! CHECK:             omp.yield
+! CHECK-NOT:     fir.do_loop
+! CHECK:         fir.load
+! CHECK:         hlfir.assign
+! CHECK:         return
+subroutine test_simd(n, a, after)
+  integer :: n, a(n), after, i
+  !$omp metadirective &
+  !$omp & when(implementation={vendor(llvm)}: simd) &
+  !$omp & otherwise(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+  after = i
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_do_simd(
+! CHECK-NOT:     omp.parallel
+! CHECK:         omp.wsloop
+! CHECK:           omp.simd linear(
+! CHECK:             omp.loop_nest
+! CHECK:               hlfir.assign
+! CHECK:               omp.yield
+! CHECK:         return
+subroutine test_do_simd(n, a)
+  integer :: n, a(n), i
+  !$omp metadirective &
+  !$omp & when(implementation={vendor(llvm)}: do simd) &
+  !$omp & otherwise(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_begin_do(
+! CHECK:         fir.if {{.*}} {
+! CHECK:           omp.wsloop
+! CHECK:             omp.loop_nest
+! CHECK:               hlfir.assign
+! CHECK:         } else {
+! CHECK-NOT:       omp.
+! CHECK:           fir.do_loop
+! CHECK:             hlfir.assign
+! CHECK:         }
+! CHECK:         return
+subroutine test_begin_do(flag, n, a)
+  logical, intent(in) :: flag
+  integer :: n, a(n), i
+  !$omp begin metadirective &
+  !$omp & when(user={condition(flag)}: do) &
+  !$omp & otherwise(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+  !$omp end metadirective
+end subroutine
+
+! The following loop must remain available when the PFT is reused for ENTRY.
+! CHECK-LABEL: func.func @_QPtest_standalone_entry_no_directive(
+! CHECK:         fir.if {{.*}} {
+! CHECK:           omp.wsloop
+! CHECK:         } else {
+! CHECK:           fir.do_loop
+! CHECK:         }
+! CHECK-NOT:     fir.do_loop
+! CHECK:         return
+! CHECK-LABEL: func.func @_QPtest_alt_standalone_entry_no_directive(
+! CHECK:         fir.if {{.*}} {
+! CHECK:           omp.wsloop
+! CHECK:         } else {
+! CHECK:           fir.do_loop
+! CHECK:         }
+! CHECK-NOT:     fir.do_loop
+! CHECK:         return
+! CHECK-LABEL: func.func @_QPtest_after_standalone_entry_no_directive(
+! CHECK-NOT:     fir.if
+! CHECK-NOT:     omp.
+! CHECK-NOT:     fir.do_loop
+! CHECK:         %[[AFTER_ENTRY_C77:.*]] = arith.constant 77 : i32
+! CHECK:         hlfir.assign %[[AFTER_ENTRY_C77]]
+! CHECK:         return
+subroutine test_standalone_entry_no_directive(flag, n, a)
+  logical, intent(in) :: flag
+  integer :: n, a(n), i
+  entry test_alt_standalone_entry_no_directive(flag, n, a)
+  !$omp metadirective &
+  !$omp & when(user={condition(flag)}: do) &
+  !$omp & otherwise(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+  entry test_after_standalone_entry_no_directive(n, a)
+  a(1) = 77
+end subroutine
+
+! A statically inapplicable loop variant nested in a parallel region leaves the
+! following loop sequential.
+! CHECK-LABEL: func.func @_QPtest_inapplicable_do_in_parallel(
+! CHECK:         omp.parallel
+! CHECK-NOT:       omp.wsloop
+! CHECK-NOT:       omp.loop_nest
+! CHECK:           fir.do_loop
+! CHECK:             hlfir.assign
+! CHECK-NOT:       fir.do_loop
+! CHECK:           omp.terminator
+! CHECK:         return
+subroutine test_inapplicable_do_in_parallel(n, a, after)
+  integer :: n, a(n), after, i
+  !$omp parallel num_threads(1) shared(n, a, after)
+  !$omp metadirective &
+  !$omp & when(implementation={vendor("unknown")}: do) &
+  !$omp & otherwise(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+  after = i
+  !$omp end parallel
+end subroutine
+
+! An unreachable loop variant likewise does not turn a statically selected
+! block variant into a mixed-association metadirective.
+! CHECK-LABEL: func.func @_QPtest_unselected_do_with_block_variant(
+! CHECK-NOT:     omp.wsloop
+! CHECK-NOT:     omp.loop_nest
+! CHECK:         omp.masked
+! CHECK-NOT:       omp.wsloop
+! CHECK-NOT:       omp.loop_nest
+! CHECK:           fir.do_loop
+! CHECK:             hlfir.assign
+! CHECK-NOT:       fir.do_loop
+! CHECK:           omp.terminator
+! CHECK:         return
+subroutine test_unselected_do_with_block_variant(n, a)
+  integer :: n, a(n), i
+  !$omp begin metadirective &
+  !$omp & when(user={condition(score(2): .true.)}: masked) &
+  !$omp & when(user={condition(score(1): .true.)}: do) &
+  !$omp & otherwise(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+  !$omp end metadirective
+end subroutine
+
+! A lower-ranked candidate guarded by the same runtime expression is
+! unreachable: when FLAG is true the higher-ranked BARRIER wins, and when it
+! is false neither guarded candidate matches. Do not emit a dead OpenMP loop.
+! CHECK-LABEL: func.func @_QPtest_unreachable_same_runtime_condition(
+! CHECK-NOT:     omp.wsloop
+! CHECK-NOT:     omp.loop_nest
+! CHECK:         fir.if {{.*}} {
+! CHECK:           omp.barrier
+! CHECK-NOT:       omp.wsloop
+! CHECK-NOT:       omp.loop_nest
+! CHECK:         } else {
+! CHECK-NOT:       omp.wsloop
+! CHECK-NOT:       omp.loop_nest
+! CHECK:         }
+! CHECK:         fir.do_loop
+! CHECK:           hlfir.assign
+! CHECK-NOT:     omp.wsloop
+! CHECK-NOT:     omp.loop_nest
+! CHECK:         return
+subroutine test_unreachable_same_runtime_condition(flag, n, a)
+  logical, intent(in) :: flag
+  integer :: n, a(n), i
+  !$omp metadirective &
+  !$omp & when(user={condition(score(2): flag)}: barrier) &
+  !$omp & when(user={condition(score(1): flag)}: do) &
+  !$omp & otherwise(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
+
+! Parentheses do not make a repeatable condition distinct. The lower-ranked
+! loop remains unreachable and must not be emitted.
+! CHECK-LABEL: func.func @_QPtest_unreachable_parenthesized_condition(
+! CHECK-NOT:     omp.wsloop
+! CHECK-NOT:     omp.loop_nest
+! CHECK:         fir.if {{.*}} {
+! CHECK:           omp.barrier
+! CHECK:         } else {
+! CHECK-NOT:       omp.wsloop
+! CHECK-NOT:       omp.loop_nest
+! CHECK:         }
+! CHECK:         fir.do_loop
+! CHECK-NOT:     omp.wsloop
+! CHECK-NOT:     omp.loop_nest
+! CHECK:         return
+subroutine test_unreachable_parenthesized_condition(flag, n, a)
+  logical, intent(in) :: flag
+  integer :: n, a(n), i
+  !$omp metadirective &
+  !$omp & when(user={condition(score(2): flag)}: barrier) &
+  !$omp & when(user={condition(score(1): (flag))}: do) &
+  !$omp & otherwise(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
+
+! Idempotent AND/OR spelling is normalized after proving that the condition is
+! repeatable.
+! CHECK-LABEL: func.func @_QPtest_unreachable_idempotent_condition(
+! CHECK-NOT:     omp.wsloop
+! CHECK-NOT:     omp.loop_nest
+! CHECK:         fir.if {{.*}} {
+! CHECK:           omp.barrier
+! CHECK:         } else {
+! CHECK-NOT:       omp.wsloop
+! CHECK-NOT:       omp.loop_nest
+! CHECK:         }
+! CHECK:         fir.do_loop
+! CHECK-NOT:     omp.wsloop
+! CHECK-NOT:     omp.loop_nest
+! CHECK:         return
+subroutine test_unreachable_idempotent_condition(flag, n, a)
+  logical, intent(in) :: flag
+  integer :: n, a(n), i
+  !$omp metadirective &
+  !$omp & when(user={condition(score(2): flag)}: barrier) &
+  !$omp & when(user={condition(score(1): flag .or. flag)}: do) &
+  !$omp & otherwise(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
+
+! Calls to an opaque procedure are independent runtime conditions even when
+! their source expressions are identical. Preserve both candidates without
+! relying on clause-expression side effects.
+! CHECK-LABEL: func.func @_QPtest_opaque_runtime_conditions(
+! CHECK:         fir.call @_QPmetadirective_runtime_condition
+! CHECK:         fir.if {{.*}} {
+! CHECK:           omp.barrier
+! CHECK:         } else {
+! CHECK:           fir.call @_QPmetadirective_runtime_condition
+! CHECK:           fir.if {{.*}} {
+! CHECK:             omp.wsloop
+! CHECK:               omp.loop_nest
+! CHECK:           } else {
+! CHECK:             fir.do_loop
+! CHECK:           }
+! CHECK:         }
+! CHECK:         return
+subroutine test_opaque_runtime_conditions(n, a)
+  integer :: n, a(n), i
+  logical :: metadirective_runtime_condition
+  external :: metadirective_runtime_condition
+  !$omp metadirective &
+  !$omp & when(user={condition(score(2): metadirective_runtime_condition())}: barrier) &
+  !$omp & when(user={condition(score(1): metadirective_runtime_condition())}: do) &
+  !$omp & otherwise(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
+
+module metadirective_condition_helpers
+contains
+  pure logical function metadirective_identity(value)
+    logical, intent(in) :: value
+    metadirective_identity = value
+  end function
+end module
+
+! Procedure calls are conservatively kept as independent runtime conditions
+! because the expression tree does not describe the callee's state.
+! CHECK-LABEL: func.func @_QPtest_pure_runtime_conditions(
+! CHECK:         fir.call @_QMmetadirective_condition_helpersPmetadirective_identity
+! CHECK:         fir.if {{.*}} {
+! CHECK:           omp.wsloop
+! CHECK:             omp.loop_nest
+! CHECK:         } else {
+! CHECK:           fir.call @_QMmetadirective_condition_helpersPmetadirective_identity
+! CHECK:           fir.if {{.*}} {
+! CHECK:             omp.simd
+! CHECK:               omp.loop_nest
+! CHECK:           } else {
+! CHECK:             fir.do_loop
+! CHECK:           }
+! CHECK:         }
+! CHECK:         return
+subroutine test_pure_runtime_conditions(flag, n, a)
+  use metadirective_condition_helpers, only : metadirective_identity
+  logical, intent(in) :: flag
+  integer :: n, a(n), i
+  !$omp metadirective &
+  !$omp & when(user={condition(score(2): &
+  !$omp &   metadirective_identity(flag))}: do) &
+  !$omp & when(user={condition(score(1): &
+  !$omp &   metadirective_identity(flag))}: simd) &
+  !$omp & otherwise(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_dynamic_loop(
+! CHECK:         fir.if {{.*}} {
+! CHECK:           omp.wsloop
+! CHECK:             omp.loop_nest
+! CHECK:               hlfir.assign
+! CHECK:         } else {
+! CHECK:           omp.simd
+! CHECK:             omp.loop_nest
+! CHECK:               hlfir.assign
+! CHECK:         }
+! CHECK:         return
+subroutine test_dynamic_loop(flag, n, a)
+  logical, intent(in) :: flag
+  integer :: n, a(n), i
+  !$omp metadirective &
+  !$omp & when(user={condition(flag)}: do) &
+  !$omp & otherwise(simd)
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
+
+! When the standalone fallback is selected at runtime, the following loop is
+! lowered sequentially in that arm.
+! CHECK-LABEL: func.func @_QPtest_dynamic_standalone_fallback(
+! CHECK:         fir.if {{.*}} {
+! CHECK:           omp.wsloop
+! CHECK:             omp.loop_nest
+! CHECK:               hlfir.assign
+! CHECK:         } else {
+! CHECK:           omp.barrier
+! CHECK:           fir.do_loop
+! CHECK:             hlfir.assign
+! CHECK:         }
+! CHECK:         return
+subroutine test_dynamic_standalone_fallback(flag, n, a)
+  logical, intent(in) :: flag
+  integer :: n, a(n), i
+  !$omp metadirective &
+  !$omp & when(user={condition(flag)}: do) &
+  !$omp & otherwise(barrier)
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
+
+! When NOTHING is selected, the following loop is lowered normally.
+! CHECK-LABEL: func.func @_QPtest_dynamic_nothing_fallback(
+! CHECK:         fir.if {{.*}} {
+! CHECK:           omp.wsloop
+! CHECK:             omp.loop_nest
+! CHECK:               hlfir.assign
+! CHECK:         } else {
+! CHECK-NOT:       omp.
+! CHECK:           fir.do_loop
+! CHECK:             hlfir.assign
+! CHECK:         }
+! CHECK:         return
+subroutine test_dynamic_nothing_fallback(flag, n, a)
+  logical, intent(in) :: flag
+  integer :: n, a(n), i
+  !$omp metadirective &
+  !$omp & when(user={condition(flag)}: do) &
+  !$omp & otherwise(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
+
+! Compiler directives preceding the associated loop are processed before it.
+! CHECK-LABEL: func.func @_QPtest_dynamic_unroll_fallback(
+! CHECK:         fir.if {{.*}} {
+! CHECK:           omp.wsloop
+! CHECK:         } else {
+! CHECK:           fir.do_loop {{.*}} attributes {loopAnnotation = #loop_annotation}
+! CHECK:         }
+! CHECK:         return
+subroutine test_dynamic_unroll_fallback(flag, n, a)
+  logical, intent(in) :: flag
+  integer :: n, a(n), i
+  !$omp metadirective &
+  !$omp & when(user={condition(flag)}: do) &
+  !$omp & otherwise(nothing)
+  !dir$ unroll 4
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
+
+! Each runtime arm must compute its own affected depth and restore temporary
+! loop-index attributes before lowering the next arm.
+! CHECK-LABEL: func.func @_QPtest_dynamic_collapse(
+! CHECK:         fir.if {{.*}} {
+! CHECK:           omp.simd {{.*}}private({{.*}}Ei_private_i32{{.*}}Ej_private_i32
+! CHECK:             omp.loop_nest ({{.*}}, {{.*}}) : i32 {{.*}} collapse(2)
+! CHECK:               hlfir.assign
+! CHECK:         } else {
+! CHECK:           omp.simd linear(
+! CHECK:             omp.loop_nest ({{.*}}) : i32
+! CHECK:               fir.do_loop
+! CHECK:                 hlfir.assign
+! CHECK:         }
+! CHECK:         return
+subroutine test_dynamic_collapse(flag, n, a)
+  logical, intent(in) :: flag
+  integer :: n, a(n, n), i, j
+  !$omp metadirective &
+  !$omp & when(user={condition(flag)}: simd collapse(2)) &
+  !$omp & otherwise(simd)
+  do i = 1, n
+    do j = 1, n
+      a(j, i) = i + j
+    end do
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_ordered_depth(
+! CHECK-NOT:     omp.parallel
+! CHECK:         omp.wsloop {{.*}}private({{.*}}Ei_private_i32{{.*}}Ej_private_i32
+! CHECK:           omp.loop_nest ({{.*}}) : i32
+! CHECK:             fir.do_loop
+! CHECK:               hlfir.assign
+! CHECK:         return
+subroutine test_ordered_depth(n, a)
+  integer :: n, a(n, n), i, j
+  !$omp metadirective &
+  !$omp & when(implementation={vendor(llvm)}: do ordered(2)) &
+  !$omp & otherwise(nothing)
+  do i = 1, n
+    do j = 1, n
+      a(j, i) = i + j
+    end do
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_collapse(
+! CHECK:         omp.wsloop
+! CHECK:           omp.loop_nest ({{.*}}, {{.*}}) : i32 {{.*}} collapse(2)
+! CHECK:             hlfir.assign
+! CHECK:         return
+subroutine test_collapse(n, a)
+  integer :: n, a(n, n), i, j
+  !$omp metadirective &
+  !$omp & when(implementation={vendor(llvm)}: do collapse(2)) &
+  !$omp & otherwise(nothing)
+  do i = 1, n
+    do j = 1, n
+      a(j, i) = i + j
+    end do
+  end do
+end subroutine
+
+! SIMD collapse makes every affected index lastprivate in OpenMP 5.2. Check
+! that lowering copies both private values back to their source bindings.
+! CHECK-LABEL: func.func @_QPtest_simd_collapse_lastprivate(
+! CHECK:         %[[I:.*]]:2 = hlfir.declare {{.*}} {uniq_name = "_QFtest_simd_collapse_lastprivateEi"}
+! CHECK:         %[[J:.*]]:2 = hlfir.declare {{.*}} {uniq_name = "_QFtest_simd_collapse_lastprivateEj"}
+! CHECK:         omp.simd {{.*}}private({{.*}}Ei_private_i32{{.*}}Ej_private_i32
+! CHECK:           omp.loop_nest ({{.*}}, {{.*}}) : i32 {{.*}} collapse(2)
+! CHECK:             fir.if
+! CHECK:               hlfir.assign {{.*}} to %[[I]]#0
+! CHECK:               hlfir.assign {{.*}} to %[[J]]#0
+! CHECK:             omp.yield
+! CHECK:         return
+subroutine test_simd_collapse_lastprivate(n, a)
+  integer :: n, a(n, n), i, j
+  !$omp metadirective &
+  !$omp & when(implementation={vendor(llvm)}: simd collapse(2)) &
+  !$omp & otherwise(nothing)
+  do i = 1, n
+    do j = 1, n
+      a(j, i) = i + j
+    end do
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_block_nested_do(
+! CHECK-NOT:     omp.parallel
+! CHECK:         omp.wsloop {{.*}}private({{.*}}Ei_private_i32
+! CHECK:           omp.loop_nest
+! CHECK:             hlfir.assign
+! CHECK:         return
+subroutine test_block_nested_do(n, a)
+  integer :: n, a(n), i
+  block
+    !$omp metadirective &
+    !$omp & when(implementation={vendor(llvm)}: do) &
+    !$omp & otherwise(nothing)
+    do i = 1, n
+      a(i) = i
+    end do
+  end block
+end subroutine
+
+! The selected-loop IV exception must not claim an IV predetermined by an
+! enclosing construct when lowering an existing block-associated replacement.
+! CHECK-LABEL: func.func @_QPtest_enclosing_do_iv(
+! CHECK:         omp.wsloop {{.*}}private({{.*}}Ei_private_i32
+! CHECK:           omp.loop_nest
+! CHECK:             %[[I:.*]]:2 = hlfir.declare
+! CHECK:             omp.parallel {
+! CHECK:               %{{.*}} = fir.load %[[I]]#0 : !fir.ref<i32>
+! CHECK:         return
+subroutine test_enclosing_do_iv(n, a)
+  integer :: n, a(n), i
+  !$omp do
+  do i = 1, n
+    !$omp begin metadirective &
+    !$omp& when(implementation={vendor(llvm)}: parallel) &
+    !$omp& otherwise(nothing)
+    a(i) = i
+    !$omp end metadirective
+  end do
+end subroutine
+
+! A predetermined flag left by a sibling transform must likewise remain
+! outside an existing non-loop replacement.
+! CHECK-LABEL: func.func @_QPtest_sibling_transform_single(
+! CHECK:         %[[I:.*]]:2 = hlfir.declare {{.*}}Ei"
+! CHECK:         omp.single {
+! CHECK:           %{{.*}} = fir.load %[[I]]#0 : !fir.ref<i32>
+! CHECK:         return
+subroutine test_sibling_transform_single(n, a)
+  integer :: n, a(n), i
+  !$omp unroll partial(2)
+  do i = 1, n
+    a(i) = i
+  end do
+  !$omp begin metadirective &
+  !$omp& when(user={condition(.true.)}: single) &
+  !$omp& otherwise(nothing)
+  a(1) = i
+  !$omp end metadirective
+end subroutine

>From ae17530dbe16a8bef1a757cb456548c4e7514871 Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Tue, 25 Aug 2026 14:57:56 -0500
Subject: [PATCH 2/3] Resolve metadirective loop lowering feedback

Strengthen tests to require loop-IV privatization and verify that the associated
loop is emitted only once.

Diagnose unsupported statements between a loop-associated metadirective and its
DO. Before runtime selection, process only compiler directives known not to emit
executable IR and diagnose the others.
---
 flang/lib/Lower/OpenMP/OpenMP.cpp             | 118 +++++++++++++++--
 ...tadirective-loop-intervening-statement.f90 | 123 ++++++++++++++++++
 .../test/Lower/OpenMP/metadirective-loop.f90  |  36 ++++-
 3 files changed, 264 insertions(+), 13 deletions(-)
 create mode 100644 flang/test/Lower/OpenMP/Todo/metadirective-loop-intervening-statement.f90

diff --git a/flang/lib/Lower/OpenMP/OpenMP.cpp b/flang/lib/Lower/OpenMP/OpenMP.cpp
index 165f7c63f238d..3e2a14b470f1b 100644
--- a/flang/lib/Lower/OpenMP/OpenMP.cpp
+++ b/flang/lib/Lower/OpenMP/OpenMP.cpp
@@ -6881,26 +6881,75 @@ struct SplicedAssociatedEvaluations {
 };
 } // namespace
 
+static bool isSupportedMetadirectiveLoopCompilerDirective(
+    const parser::CompilerDirective &directive) {
+  // Only directives known not to emit executable operations may be processed
+  // before runtime selection. New variants remain unsupported until classified.
+  using SupportedDirectives = std::tuple<
+      std::list<parser::CompilerDirective::IgnoreTKR>,
+      parser::CompilerDirective::LoopCount,
+      std::list<parser::CompilerDirective::AssumeAligned>,
+      parser::CompilerDirective::VectorAlways,
+      parser::CompilerDirective::VectorLength,
+      std::list<parser::CompilerDirective::NameValue>,
+      parser::CompilerDirective::Unroll,
+      parser::CompilerDirective::UnrollAndJam,
+      parser::CompilerDirective::Unrecognized,
+      parser::CompilerDirective::NoVector, parser::CompilerDirective::NoUnroll,
+      parser::CompilerDirective::NoUnrollAndJam,
+      parser::CompilerDirective::ForceInline, parser::CompilerDirective::Inline,
+      parser::CompilerDirective::NoInline,
+      parser::CompilerDirective::InlineAlways, parser::CompilerDirective::IVDep,
+      parser::CompilerDirective::Simd>;
+
+  return common::visit(
+      [](const auto &value) {
+        using T = std::decay_t<decltype(value)>;
+        return common::HasMember<T, SupportedDirectives>;
+      },
+      directive.u);
+}
+
+static bool
+isIgnorableMetadirectiveLoopAssociationEval(lower::pft::Evaluation &eval) {
+  const auto *directive = eval.getIf<parser::CompilerDirective>();
+  return eval.isEndStmt() ||
+         (directive &&
+          isSupportedMetadirectiveLoopCompilerDirective(*directive));
+}
+
+static bool
+isUnsupportedMetadirectiveLoopAssociationEval(lower::pft::Evaluation &eval) {
+  if (const auto *directive = eval.getIf<parser::CompilerDirective>())
+    return !isSupportedMetadirectiveLoopCompilerDirective(*directive);
+  return eval.isOtherStmt() ||
+         (eval.isDirective() && !eval.isExecutableDirective());
+}
+
 /// 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
 /// nullptr if no associated DO loop follows.
-static bool
-isIgnorableMetadirectiveLoopAssociationEval(lower::pft::Evaluation &eval) {
-  return eval.isEndStmt() || eval.getIf<parser::CompilerDirective>();
-}
-
 static lower::pft::Evaluation *spliceAssociatedDoEval(
     lower::pft::Evaluation &eval,
-    SplicedAssociatedEvaluations *splicedEvaluations = nullptr) {
+    SplicedAssociatedEvaluations *splicedEvaluations = nullptr,
+    lower::pft::Evaluation **unsupportedInterveningEval = nullptr) {
+  if (unsupportedInterveningEval)
+    *unsupportedInterveningEval = nullptr;
+
   if (eval.hasNestedEvaluations()) {
     auto nestedIt =
         llvm::find_if(eval.getNestedEvaluations(), [](auto &nested) {
           return !isIgnorableMetadirectiveLoopAssociationEval(nested);
         });
-    if (nestedIt != eval.getNestedEvaluations().end())
-      return nestedIt->getIf<parser::DoConstruct>() ? &*nestedIt : nullptr;
+    if (nestedIt != eval.getNestedEvaluations().end()) {
+      if (nestedIt->getIf<parser::DoConstruct>())
+        return &*nestedIt;
+      if (unsupportedInterveningEval &&
+          isUnsupportedMetadirectiveLoopAssociationEval(*nestedIt))
+        *unsupportedInterveningEval = &*nestedIt;
+    }
     return nullptr;
   }
 
@@ -6930,8 +6979,14 @@ static lower::pft::Evaluation *spliceAssociatedDoEval(
          isIgnorableMetadirectiveLoopAssociationEval(*loopIt))
     ++loopIt;
 
-  if (loopIt == parentList->end() || !loopIt->getIf<parser::DoConstruct>())
+  if (loopIt == parentList->end())
     return nullptr;
+  if (!loopIt->getIf<parser::DoConstruct>()) {
+    if (unsupportedInterveningEval &&
+        isUnsupportedMetadirectiveLoopAssociationEval(*loopIt))
+      *unsupportedInterveningEval = &*loopIt;
+    return nullptr;
+  }
 
   if (splicedEvaluations) {
     auto entryIt =
@@ -7242,8 +7297,9 @@ static void genMetadirective(lower::AbstractConverter &converter,
       splicedAssociatedEvaluations.restore(eval.getNestedEvaluations());
   });
   if (hasLoopAssociatedCandidate) {
-    if (lower::pft::Evaluation *loopEval =
-            spliceAssociatedDoEval(eval, &splicedAssociatedEvaluations)) {
+    lower::pft::Evaluation *unsupportedInterveningEval = nullptr;
+    if (lower::pft::Evaluation *loopEval = spliceAssociatedDoEval(
+            eval, &splicedAssociatedEvaluations, &unsupportedInterveningEval)) {
       associatedLoopEval = loopEval;
       if (lower::pft::FunctionLikeUnit *owningProc =
               eval.getOwningProcedure()) {
@@ -7270,6 +7326,46 @@ static void genMetadirective(lower::AbstractConverter &converter,
       for (auto it = nested.begin(); it != loopIt; ++it)
         if (it->getIf<parser::CompilerDirective>())
           converter.genEval(*it);
+    } else if (unsupportedInterveningEval) {
+      std::string evalName;
+      if (unsupportedInterveningEval->getIf<parser::EntryStmt>())
+        evalName = "ENTRY statement";
+      else if (unsupportedInterveningEval->getIf<parser::FormatStmt>())
+        evalName = "FORMAT statement";
+      else if (const auto *directive =
+                   unsupportedInterveningEval
+                       ->getIf<parser::CompilerDirective>()) {
+        assert(!isSupportedMetadirectiveLoopCompilerDirective(*directive) &&
+               "unexpected compiler directive");
+        evalName = std::holds_alternative<parser::CompilerDirective::Prefetch>(
+                       directive->u)
+                       ? "PREFETCH compiler directive"
+                       : "unsupported compiler directive";
+      } else if (const auto *omp =
+                     unsupportedInterveningEval
+                         ->getIf<parser::OpenMPDeclarativeConstruct>()) {
+        evalName =
+            parser::omp::GetUpperName(parser::omp::GetOmpDirectiveName(*omp).v,
+                                      semaCtx.langOptions().OpenMPVersion);
+        evalName += " directive";
+      } else if (const auto *acc =
+                     unsupportedInterveningEval
+                         ->getIf<parser::OpenACCDeclarativeConstruct>()) {
+        evalName = std::holds_alternative<
+                       parser::OpenACCStandaloneDeclarativeConstruct>(acc->u)
+                       ? "OpenACC DECLARE directive"
+                       : "OpenACC ROUTINE directive";
+      } else if (unsupportedInterveningEval
+                     ->getIf<parser::OpenACCRoutineConstruct>()) {
+        evalName = "OpenACC ROUTINE directive";
+      } else {
+        evalName = "non-executable directive";
+      }
+
+      TODO(converter.getCurrentLocation(),
+           llvm::Twine(evalName) +
+               " between loop-associated METADIRECTIVE and its associated "
+               "DO");
     }
   }
 
diff --git a/flang/test/Lower/OpenMP/Todo/metadirective-loop-intervening-statement.f90 b/flang/test/Lower/OpenMP/Todo/metadirective-loop-intervening-statement.f90
new file mode 100644
index 0000000000000..1673c63d319c5
--- /dev/null
+++ b/flang/test/Lower/OpenMP/Todo/metadirective-loop-intervening-statement.f90
@@ -0,0 +1,123 @@
+! Test diagnostics for unsupported statements between a loop-associated
+! metadirective and its associated DO.
+
+! RUN: split-file %s %t
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
+! RUN:   -o - %t/threadprivate.f90 2>&1 | FileCheck --check-prefix=TP %s
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
+! RUN:   -o - %t/requires.f90 2>&1 | FileCheck --check-prefix=REQ %s
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
+! RUN:   -o - %t/prefetch.f90 2>&1 | FileCheck --check-prefix=PREFETCH %s
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
+! RUN:   -o - %t/entry.f90 2>&1 | FileCheck --check-prefix=ENTRY %s
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
+! RUN:   -o - %t/format.f90 2>&1 | FileCheck --check-prefix=FORMAT %s
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
+! RUN:   -fopenacc -o - %t/acc-declare.f90 2>&1 \
+! RUN:   | FileCheck --check-prefix=ACC %s
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
+! RUN:   -fopenacc -o - %t/acc-routine.f90 2>&1 \
+! RUN:   | FileCheck --check-prefix=ROUTINE %s
+
+! TP: not yet implemented: THREADPRIVATE directive between loop-associated
+! TP-SAME: METADIRECTIVE and its associated DO
+! REQ: not yet implemented: REQUIRES directive between loop-associated
+! REQ-SAME: METADIRECTIVE and its associated DO
+! PREFETCH: not yet implemented: PREFETCH compiler directive between
+! PREFETCH-SAME: loop-associated METADIRECTIVE and its associated DO
+! ENTRY: not yet implemented: ENTRY statement between loop-associated
+! ENTRY-SAME: METADIRECTIVE and its associated DO
+! FORMAT: not yet implemented: FORMAT statement between loop-associated
+! FORMAT-SAME: METADIRECTIVE and its associated DO
+! ACC: not yet implemented: OpenACC DECLARE directive between loop-associated
+! ACC-SAME: METADIRECTIVE and its associated DO
+! ROUTINE: not yet implemented: OpenACC ROUTINE directive between
+! ROUTINE-SAME: loop-associated METADIRECTIVE and its associated DO
+
+!--- threadprivate.f90
+subroutine threadprivate_between(n, a)
+  integer :: n, a(n), i
+  real, save :: p
+  !$omp metadirective &
+  !$omp & when(implementation={vendor(llvm)}: do) &
+  !$omp & otherwise(nothing)
+  !$omp threadprivate(p)
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
+
+!--- requires.f90
+subroutine requires_between()
+  integer :: a(10), i
+  !$omp metadirective &
+  !$omp & when(implementation={vendor(llvm)}: do) &
+  !$omp & otherwise(nothing)
+  !$omp requires unified_address
+  do i = 1, 10
+    a(i) = i
+  end do
+end subroutine
+
+!--- prefetch.f90
+subroutine prefetch_between(a, n)
+  integer :: a(n), n, i, idx
+  logical :: choose
+  external :: idx, choose
+  !$omp metadirective &
+  !$omp & when(user={condition(choose())}: do) &
+  !$omp & otherwise(nothing)
+  !dir$ prefetch a(idx())
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
+
+!--- entry.f90
+subroutine entry_between()
+  integer :: a(10), i
+  !$omp metadirective &
+  !$omp & when(implementation={vendor(llvm)}: do) &
+  !$omp & otherwise(nothing)
+  entry alternate_entry()
+  do i = 1, 10
+    a(i) = i
+  end do
+end subroutine
+
+!--- acc-routine.f90
+subroutine acc_routine_between(a)
+  integer :: a(10), i
+  !$omp metadirective &
+  !$omp & when(implementation={vendor(llvm)}: do) &
+  !$omp & otherwise(nothing)
+  !$acc routine seq
+  do i = 1, 10
+    a(i) = i
+  end do
+end subroutine
+
+!--- format.f90
+subroutine format_between()
+  integer :: a(10), i
+  !$omp metadirective &
+  !$omp & when(implementation={vendor(llvm)}: do) &
+  !$omp & otherwise(nothing)
+100 format(i0)
+  do i = 1, 10
+    a(i) = i
+  end do
+end subroutine
+
+!--- acc-declare.f90
+subroutine acc_declare_between()
+  integer, save :: a(10)
+  integer :: i
+  !$omp metadirective &
+  !$omp & when(implementation={vendor(llvm)}: do) &
+  !$omp & otherwise(nothing)
+  !$acc declare create(a)
+  do i = 1, 10
+    a(i) = i
+  end do
+end subroutine
diff --git a/flang/test/Lower/OpenMP/metadirective-loop.f90 b/flang/test/Lower/OpenMP/metadirective-loop.f90
index adee0b6128d5e..62eaa38150068 100644
--- a/flang/test/Lower/OpenMP/metadirective-loop.f90
+++ b/flang/test/Lower/OpenMP/metadirective-loop.f90
@@ -7,7 +7,7 @@
 
 ! CHECK-LABEL: func.func @_QPtest_do(
 ! CHECK-NOT:     omp.parallel
-! CHECK:         omp.wsloop
+! CHECK:         omp.wsloop private({{.*}}Ei_private_i32
 ! CHECK:           omp.loop_nest
 ! CHECK:             hlfir.assign
 ! CHECK:             omp.yield
@@ -63,7 +63,7 @@ subroutine test_do_simd(n, a)
 
 ! CHECK-LABEL: func.func @_QPtest_begin_do(
 ! CHECK:         fir.if {{.*}} {
-! CHECK:           omp.wsloop
+! CHECK:           omp.wsloop private({{.*}}Ei_private_i32
 ! CHECK:             omp.loop_nest
 ! CHECK:               hlfir.assign
 ! CHECK:         } else {
@@ -71,6 +71,8 @@ subroutine test_do_simd(n, a)
 ! CHECK:           fir.do_loop
 ! CHECK:             hlfir.assign
 ! CHECK:         }
+! CHECK-NOT:     fir.do_loop
+! CHECK-NOT:     omp.
 ! CHECK:         return
 subroutine test_begin_do(flag, n, a)
   logical, intent(in) :: flag
@@ -415,6 +417,36 @@ subroutine test_dynamic_unroll_fallback(flag, n, a)
   end do
 end subroutine
 
+! Other compiler directives that do not emit executable operations may also
+! appear between the metadirective and its associated loop. Check a loop
+! annotation, an inlining annotation, and an unrecognized no-op directive.
+! CHECK-LABEL: func.func @_QPtest_dynamic_intervening_compiler_directives(
+! CHECK:         fir.if {{.*}} {
+! CHECK:           omp.wsloop
+! CHECK:             omp.loop_nest
+! CHECK:               fir.call @_QPconsume
+! CHECK:         } else {
+! CHECK:           fir.do_loop
+! CHECK-SAME:        attributes {loopAnnotation = #loop_annotation{{[0-9]*}}}
+! CHECK:             fir.call @_QPconsume
+! CHECK-SAME:          inline_attr = #fir.inline_attrs<always_inline>
+! CHECK:         }
+! CHECK:         return
+subroutine test_dynamic_intervening_compiler_directives(flag, n, a)
+  logical, intent(in) :: flag
+  integer :: n, a(n), i
+  external :: consume
+  !$omp metadirective &
+  !$omp & when(user={condition(flag)}: do) &
+  !$omp & otherwise(nothing)
+  !dir$ vector always
+  !dir$ forceinline
+  !dir$ unknown
+  do i = 1, n
+    call consume(a(i))
+  end do
+end subroutine
+
 ! Each runtime arm must compute its own affected depth and restore temporary
 ! loop-index attributes before lowering the next arm.
 ! CHECK-LABEL: func.func @_QPtest_dynamic_collapse(

>From db093a7b010523c39b0595a1b6a48ec92bd51939 Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Wed, 26 Aug 2026 10:55:16 -0500
Subject: [PATCH 3/3] Improve metadirective loop tests

- Cover supported and unsupported compiler directives in begin/end forms.
- Add vectorization checks to the enabled loop annotation.
---
 ...tadirective-loop-intervening-statement.f90 | 20 ++++++++++
 .../test/Lower/OpenMP/metadirective-loop.f90  | 38 +++++++++++++++++--
 2 files changed, 54 insertions(+), 4 deletions(-)

diff --git a/flang/test/Lower/OpenMP/Todo/metadirective-loop-intervening-statement.f90 b/flang/test/Lower/OpenMP/Todo/metadirective-loop-intervening-statement.f90
index 1673c63d319c5..2b2ebd35b71b8 100644
--- a/flang/test/Lower/OpenMP/Todo/metadirective-loop-intervening-statement.f90
+++ b/flang/test/Lower/OpenMP/Todo/metadirective-loop-intervening-statement.f90
@@ -9,6 +9,9 @@
 ! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
 ! RUN:   -o - %t/prefetch.f90 2>&1 | FileCheck --check-prefix=PREFETCH %s
 ! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
+! RUN:   -o - %t/begin-prefetch.f90 2>&1 \
+! RUN:   | FileCheck --check-prefix=BEGIN-PREFETCH %s
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
 ! RUN:   -o - %t/entry.f90 2>&1 | FileCheck --check-prefix=ENTRY %s
 ! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
 ! RUN:   -o - %t/format.f90 2>&1 | FileCheck --check-prefix=FORMAT %s
@@ -25,6 +28,8 @@
 ! REQ-SAME: METADIRECTIVE and its associated DO
 ! PREFETCH: not yet implemented: PREFETCH compiler directive between
 ! PREFETCH-SAME: loop-associated METADIRECTIVE and its associated DO
+! BEGIN-PREFETCH: not yet implemented: PREFETCH compiler directive between
+! BEGIN-PREFETCH-SAME: loop-associated METADIRECTIVE and its associated DO
 ! ENTRY: not yet implemented: ENTRY statement between loop-associated
 ! ENTRY-SAME: METADIRECTIVE and its associated DO
 ! FORMAT: not yet implemented: FORMAT statement between loop-associated
@@ -73,6 +78,21 @@ subroutine prefetch_between(a, n)
   end do
 end subroutine
 
+!--- begin-prefetch.f90
+subroutine begin_prefetch_between(a, n)
+  integer :: a(n), n, i, idx
+  logical :: choose
+  external :: idx, choose
+  !$omp begin metadirective &
+  !$omp & when(user={condition(choose())}: do) &
+  !$omp & otherwise(nothing)
+  !dir$ prefetch a(idx())
+  do i = 1, n
+    a(i) = i
+  end do
+  !$omp end metadirective
+end subroutine
+
 !--- entry.f90
 subroutine entry_between()
   integer :: a(10), i
diff --git a/flang/test/Lower/OpenMP/metadirective-loop.f90 b/flang/test/Lower/OpenMP/metadirective-loop.f90
index 62eaa38150068..7af3ffebcd9c0 100644
--- a/flang/test/Lower/OpenMP/metadirective-loop.f90
+++ b/flang/test/Lower/OpenMP/metadirective-loop.f90
@@ -2,8 +2,14 @@
 
 ! RUN: %flang_fc1 -fopenmp -emit-hlfir -fopenmp-version=52 %s -o - | FileCheck %s
 
-! CHECK: #loop_unroll = #llvm.loop_unroll<disable = false, count = 4 : i64>
-! CHECK: #loop_annotation = #llvm.loop_annotation<unroll = #loop_unroll>
+! CHECK: #[[UNROLL:loop_unroll[0-9]*]] =
+! CHECK-SAME: #llvm.loop_unroll<disable = false, count = 4 : i64>
+! CHECK: #[[VECTORIZE:loop_vectorize[0-9]*]] =
+! CHECK-SAME: #llvm.loop_vectorize<disable = false>
+! CHECK: #[[UNROLL_ANNOTATION:loop_annotation[0-9]*]] =
+! CHECK-SAME: #llvm.loop_annotation<unroll = #[[UNROLL]]>
+! CHECK: #[[VECTOR_ANNOTATION:loop_annotation[0-9]*]] =
+! CHECK-SAME: #llvm.loop_annotation<vectorize = #[[VECTORIZE]]>
 
 ! CHECK-LABEL: func.func @_QPtest_do(
 ! CHECK-NOT:     omp.parallel
@@ -402,7 +408,8 @@ subroutine test_dynamic_nothing_fallback(flag, n, a)
 ! CHECK:         fir.if {{.*}} {
 ! CHECK:           omp.wsloop
 ! CHECK:         } else {
-! CHECK:           fir.do_loop {{.*}} attributes {loopAnnotation = #loop_annotation}
+! CHECK:           fir.do_loop
+! CHECK-SAME:        attributes {loopAnnotation = #[[UNROLL_ANNOTATION]]}
 ! CHECK:         }
 ! CHECK:         return
 subroutine test_dynamic_unroll_fallback(flag, n, a)
@@ -417,6 +424,29 @@ subroutine test_dynamic_unroll_fallback(flag, n, a)
   end do
 end subroutine
 
+! A supported compiler directive nested inside a begin/end metadirective is
+! attached to the associated loop before runtime selection.
+! CHECK-LABEL: func.func @_QPtest_begin_unroll_fallback(
+! CHECK:         fir.if {{.*}} {
+! CHECK:           omp.wsloop
+! CHECK:         } else {
+! CHECK:           fir.do_loop
+! CHECK-SAME:        attributes {loopAnnotation = #[[UNROLL_ANNOTATION]]}
+! CHECK:         }
+! CHECK:         return
+subroutine test_begin_unroll_fallback(flag, n, a)
+  logical, intent(in) :: flag
+  integer :: n, a(n), i
+  !$omp begin metadirective &
+  !$omp & when(user={condition(flag)}: do) &
+  !$omp & otherwise(nothing)
+  !dir$ unroll 4
+  do i = 1, n
+    a(i) = i
+  end do
+  !$omp end metadirective
+end subroutine
+
 ! Other compiler directives that do not emit executable operations may also
 ! appear between the metadirective and its associated loop. Check a loop
 ! annotation, an inlining annotation, and an unrecognized no-op directive.
@@ -427,7 +457,7 @@ subroutine test_dynamic_unroll_fallback(flag, n, a)
 ! CHECK:               fir.call @_QPconsume
 ! CHECK:         } else {
 ! CHECK:           fir.do_loop
-! CHECK-SAME:        attributes {loopAnnotation = #loop_annotation{{[0-9]*}}}
+! CHECK-SAME:        attributes {loopAnnotation = #[[VECTOR_ANNOTATION]]}
 ! CHECK:             fir.call @_QPconsume
 ! CHECK-SAME:          inline_attr = #fir.inline_attrs<always_inline>
 ! CHECK:         }



More information about the flang-commits mailing list