[llvm-branch-commits] [flang] [flang][OpenMP] Track reachable metadirective replacements (PR #219014)

via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Fri Sep 18 22:06:25 PDT 2026


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

>From 6dab091cdefa9734e954006caaf7bc4d12acd179 Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Wed, 26 Aug 2026 13:46:09 -0500
Subject: [PATCH 01/11] [flang][OpenMP] Track reachable metadirective
 replacement paths

The existing semantic checks can validate loop-associated directives in a
METADIRECTIVE against the following loop, but they do not model how the
METADIRECTIVE chooses among its replacements.

Today each WHEN is considered independently: if its selector can match, its
replacement is checked. Selection instead ranks all applicable candidates as
a set. An unguarded higher-ranked candidate makes lower-ranked candidates
unreachable, while a dynamically guarded candidate leaves them reachable
when its condition is false. Treating both cases alike can diagnose loop
requirements on a replacement that can never be selected.

The selected replacement can also affect later selection. Its directive
contributes to the construct context seen by a nested metadirective. The
checker currently retains only syntactic nesting, so nested construct
selectors cannot observe a directive selected by an enclosing
metadirective.

For example, consider an enclosing dynamic choice and a nested construct
selector:

    outer: when(user={condition(flag)}: target) default(parallel)
    inner: when(construct={parallel}: simd) default(nothing)

The enclosing selection creates two mutually exclusive contexts:

                         outer metadirective
                        /                     \
                    flag=true              flag=false
                       |                       |
                    TARGET                 PARALLEL
                       |                       |
            construct={parallel}   construct={parallel}
                   no match                 match
                                               |
                                              SIMD

Dropping selected directives misses the match on the PARALLEL path.
Flattening both paths would instead make the nested selector observe TARGET
and PARALLEL together, a construct context that can never occur.

This PR models each possible selection outcome as a reachable directive path.
For each enclosing path, it uses the existing candidate ranking to retain only
replacements that can win after static and dynamic conditions are considered.
It runs loop checks only for directive specifications present on at least one
reachable path, and propagates the same reachability through APPLY.

Each path is extended with the selected directive while walking its associated
loop or begin/end region. This lets nested construct selectors observe both
syntactic and selected enclosing constructs. The selected context is removed
at its association boundary so it cannot affect later constructs.

When a selector cannot be modeled, all replacements are conservatively
retained so error recovery does not suppress required checks. To control path
growth, this PR collects the construct selectors in the program unit and
merges only paths that none of those selectors can distinguish.

Add coverage for ranked static and dynamic candidates, implicit NOTHING,
nested standalone and begin/end metadirectives, non-NOTHING fallbacks,
context lifetime, and APPLY reachability.

Assisted with codex.
---
 flang/include/flang/Semantics/openmp-utils.h  |   4 +
 flang/lib/Semantics/check-omp-structure.cpp   |  81 +++-
 flang/lib/Semantics/check-omp-structure.h     |  50 +-
 flang/lib/Semantics/check-omp-variant.cpp     | 443 ++++++++++++++----
 flang/lib/Semantics/openmp-utils.cpp          |   2 +-
 ...metadirective-loop-applicability-apply.f90 |  23 +
 ...directive-loop-applicability-openmp-60.f90 |  13 +
 .../metadirective-loop-applicability.f90      | 186 ++++++++
 8 files changed, 668 insertions(+), 134 deletions(-)
 create mode 100644 flang/test/Semantics/OpenMP/metadirective-loop-applicability-apply.f90
 create mode 100644 flang/test/Semantics/OpenMP/metadirective-loop-applicability-openmp-60.f90

diff --git a/flang/include/flang/Semantics/openmp-utils.h b/flang/include/flang/Semantics/openmp-utils.h
index 769140a3147ed..f14820b1bda5f 100644
--- a/flang/include/flang/Semantics/openmp-utils.h
+++ b/flang/include/flang/Semantics/openmp-utils.h
@@ -242,6 +242,10 @@ class OmpVariantMatchContext : public llvm::omp::OMPContext {
   std::string features_;
 };
 
+/// Add the construct traits implied by an OpenMP directive to \p vmi.
+void AppendConstructTraitsForDirective(
+    llvm::omp::Directive, llvm::omp::VariantMatchInfo &vmi);
+
 struct MetadirectiveCandidate {
   MetadirectiveCandidate(const parser::OmpDirectiveSpecification *spec,
       llvm::omp::VariantMatchInfo vmi, bool isExplicit,
diff --git a/flang/lib/Semantics/check-omp-structure.cpp b/flang/lib/Semantics/check-omp-structure.cpp
index f282140e47b6b..bf1d3548edee4 100644
--- a/flang/lib/Semantics/check-omp-structure.cpp
+++ b/flang/lib/Semantics/check-omp-structure.cpp
@@ -168,18 +168,19 @@ OmpStructureChecker::OmpStructureChecker(SemanticsContext &context)
   scopeStack_.push_back(&context.globalScope());
 }
 
-void OmpStructureChecker::Enter(const parser::ProgramUnit &) { //
+void OmpStructureChecker::Enter(const parser::ProgramUnit &x) { //
   ClearLabels();
   declareVariantPairs_.clear();
+  CollectMetadirectiveConstructSelectors(x);
 }
 
 void OmpStructureChecker::Leave(const parser::ProgramUnit &) {
-  if (!metadirectiveLoopVariants_.empty()) {
+  if (!pendingLoopDirectiveGroups_.empty()) {
     // A declaration-only unit (module, submodule, or block data) has no
     // execution part to follow the metadirective, so its loop-associated
     // variants were never validated. A subprogram validates them while
     // scanning the execution part, leaving none pending here.
-    CheckMetadirectiveVariantsWithoutLoop();
+    CheckPendingLoopDirectivesWithoutLoop();
   }
 }
 
@@ -295,38 +296,39 @@ void OmpStructureChecker::Enter(const parser::EndMpSubprogramStmt &x) {
   scopeStack_.pop_back();
 }
 
-void OmpStructureChecker::BeginMetadirectiveVariantScope() {
-  metadirectiveVariantScopeStarts_.push_back(metadirectiveLoopVariants_.size());
+void OmpStructureChecker::BeginPendingLoopDirectiveScope() {
+  pendingLoopDirectiveScopeStarts_.push_back(
+      pendingLoopDirectiveGroups_.size());
 }
 
-void OmpStructureChecker::EndMetadirectiveVariantScope() {
-  CHECK(!metadirectiveVariantScopeStarts_.empty());
-  std::size_t firstVariant{metadirectiveVariantScopeStarts_.back()};
-  metadirectiveVariantScopeStarts_.pop_back();
-  if (firstVariant < metadirectiveLoopVariants_.size()) {
-    // Diagnose variants that were recorded in this scope but not consumed by
-    // one of its executable constructs, preserving variants from an enclosing
-    // scope.
-    CheckMetadirectiveVariantsWithoutLoop(firstVariant);
+void OmpStructureChecker::EndPendingLoopDirectiveScope() {
+  CHECK(!pendingLoopDirectiveScopeStarts_.empty());
+  std::size_t firstDirectiveGroup{pendingLoopDirectiveScopeStarts_.back()};
+  pendingLoopDirectiveScopeStarts_.pop_back();
+  if (firstDirectiveGroup < pendingLoopDirectiveGroups_.size()) {
+    // Diagnose directives that were recorded in this scope but not consumed
+    // by one of its executable constructs, preserving directives from an
+    // enclosing scope.
+    CheckPendingLoopDirectivesWithoutLoop(firstDirectiveGroup);
   }
 }
 
 void OmpStructureChecker::Enter(const parser::Block &) {
-  BeginMetadirectiveVariantScope();
+  BeginPendingLoopDirectiveScope();
 }
 
 void OmpStructureChecker::Leave(const parser::Block &) {
-  EndMetadirectiveVariantScope();
+  EndPendingLoopDirectiveScope();
 }
 
 void OmpStructureChecker::Enter(const parser::BlockConstruct &x) {
-  BeginMetadirectiveVariantScope();
+  BeginPendingLoopDirectiveScope();
   auto &endBlockStmt{std::get<parser::Statement<parser::EndBlockStmt>>(x.t)};
   scopeStack_.push_back(&context_.FindScope(endBlockStmt.source));
 }
 
 void OmpStructureChecker::Leave(const parser::BlockConstruct &x) {
-  EndMetadirectiveVariantScope();
+  EndPendingLoopDirectiveScope();
   scopeStack_.pop_back();
 }
 
@@ -339,20 +341,20 @@ void OmpStructureChecker::Enter(const parser::ModuleSubprogram &) {
 }
 
 void OmpStructureChecker::Enter(const parser::ModuleSubprogramPart &) {
-  if (!metadirectiveLoopVariants_.empty()) {
+  if (!pendingLoopDirectiveGroups_.empty()) {
     // The enclosing module or submodule has no execution part. Diagnose its
     // pending loop-associated variants before a contained procedure starts a
     // new specification part and resets the worklist.
-    CheckMetadirectiveVariantsWithoutLoop();
+    CheckPendingLoopDirectivesWithoutLoop();
   }
 }
 
 void OmpStructureChecker::Enter(const parser::InterfaceBody &) {
-  BeginMetadirectiveVariantScope();
+  BeginPendingLoopDirectiveScope();
 }
 
 void OmpStructureChecker::Leave(const parser::InterfaceBody &) {
-  EndMetadirectiveVariantScope();
+  EndPendingLoopDirectiveScope();
 }
 
 void OmpStructureChecker::Enter(const parser::SpecificationPart &) {
@@ -360,7 +362,7 @@ void OmpStructureChecker::Enter(const parser::SpecificationPart &) {
   // An empty partStack_ marks the unit's top-level specification part, so a
   // nested one such as an interface body does not reset them.
   if (partStack_.empty()) {
-    metadirectiveLoopVariants_.clear();
+    pendingLoopDirectiveGroups_.clear();
   }
   partStack_.push_back(PartKind::SpecificationPart);
 }
@@ -374,10 +376,10 @@ void OmpStructureChecker::Enter(const parser::ExecutionPart &) {
 }
 
 void OmpStructureChecker::Leave(const parser::ExecutionPart &) {
-  if (!metadirectiveLoopVariants_.empty()) {
+  if (!pendingLoopDirectiveGroups_.empty()) {
     // No loop nest followed the metadirective in this execution part, so its
     // loop-associated variants were never validated.
-    CheckMetadirectiveVariantsWithoutLoop();
+    CheckPendingLoopDirectivesWithoutLoop();
   }
   partStack_.pop_back();
 }
@@ -738,6 +740,35 @@ void OmpStructureChecker::ClearLabels() {
   targetLabels_.clear();
 }
 
+llvm::SmallVector<OmpStructureChecker::EffectiveDirectivePath, 4>
+OmpStructureChecker::GetEnclosingDirectivePaths() const {
+  // The current metadirective is already on dirContext_; start at its parent.
+  int actualIndex{static_cast<int>(dirContext_.size()) - 2};
+  std::size_t depth{0};
+  llvm::SmallVector<EffectiveDirectivePath, 4> paths(1);
+  if (!activeMetadirectiveReplacements_.empty()) {
+    const MetadirectiveReplacementContext &frame{
+        activeMetadirectiveReplacements_.back()};
+    depth = frame.directiveContextDepth;
+    paths = frame.paths;
+  }
+
+  EffectiveDirectivePath actualContexts;
+  while (actualIndex >= static_cast<int>(depth)) {
+    std::size_t index{static_cast<std::size_t>(actualIndex--)};
+    const DirectiveContext &context{dirContext_[index]};
+    // A metadirective is replaced, so it is transparent in an effective path.
+    if (context.directive == llvm::omp::Directive::OMPD_metadirective) {
+      continue;
+    }
+    actualContexts.push_back(context.directive);
+  }
+  for (EffectiveDirectivePath &path : paths) {
+    path.insert(path.begin(), actualContexts.begin(), actualContexts.end());
+  }
+  return GetUniqueEffectiveDirectivePaths(std::move(paths));
+}
+
 bool OmpStructureChecker::IsCloselyNestedRegion(
     const llvm::omp::Directives &set) {
   // Definition of close nesting:
diff --git a/flang/lib/Semantics/check-omp-structure.h b/flang/lib/Semantics/check-omp-structure.h
index e0a86b09a5d9a..2955e1322454b 100644
--- a/flang/lib/Semantics/check-omp-structure.h
+++ b/flang/lib/Semantics/check-omp-structure.h
@@ -23,6 +23,7 @@
 #include "llvm/ADT/SmallVector.h"
 #include "llvm/ADT/iterator_range.h"
 #include "llvm/Frontend/OpenMP/OMP.h"
+#include "llvm/Frontend/OpenMP/OMPContext.h"
 #include "llvm/Frontend/OpenMP/OMPDescriptors.h"
 
 #include <cstddef>
@@ -178,9 +179,11 @@ class OmpStructureChecker : public OmpStructureCheckerBase {
 
   void Enter(const parser::OmpMetadirectiveDirective &);
   void Leave(const parser::OmpMetadirectiveDirective &);
+  void Enter(const parser::OmpDelimitedMetadirectiveDirective &);
+  void Leave(const parser::OmpDelimitedMetadirectiveDirective &);
 
   void Enter(const parser::ExecutionPartConstruct &);
-  void Leave(const parser::OmpClause::When &);
+  void Leave(const parser::ExecutionPartConstruct &);
 
   void Enter(const parser::OmpContextSelector &);
   void Leave(const parser::OmpContextSelector &);
@@ -257,7 +260,6 @@ class OmpStructureChecker : public OmpStructureCheckerBase {
   void Enter(const parser::OmpClause::UseDeviceAddr &x);
   void Enter(const parser::OmpClause::UseDevicePtr &x);
   void Enter(const parser::OmpClause::UsesAllocators &x);
-  void Enter(const parser::OmpClause::When &x);
 
 private:
   using LoopOrConstruct = std::variant<const parser::DoConstruct *,
@@ -321,11 +323,12 @@ class OmpStructureChecker : public OmpStructureCheckerBase {
   void CheckDistLinear(const parser::OpenMPLoopConstruct &x);
   void CheckUnrollFullTripCount(const parser::OpenMPLoopConstruct &x);
 
-  void BeginMetadirectiveVariantScope();
-  void EndMetadirectiveVariantScope();
+  void BeginPendingLoopDirectiveScope();
+  void EndPendingLoopDirectiveScope();
 
   // check-omp-variant.cpp
-  void CheckMetadirectiveVariantsWithoutLoop(std::size_t firstVariant = 0);
+  void CheckPendingLoopDirectivesWithoutLoop(
+      std::size_t firstDirectiveGroup = 0);
   void CheckOmpDeclareVariantDirective(
       const parser::OmpDeclareVariantDirective &);
   void CheckDeclareVariantUserConditions(const parser::OmpContextSelector &);
@@ -410,6 +413,25 @@ class OmpStructureChecker : public OmpStructureCheckerBase {
   bool HasInvalidWorksharingNesting(
       const parser::OmpDirectiveName &name, const llvm::omp::Directives &);
 
+  using EffectiveDirectivePath = llvm::SmallVector<llvm::omp::Directive, 8>;
+  using ConstructTraitSequence = llvm::SmallVector<llvm::omp::TraitProperty, 8>;
+
+  struct MetadirectiveReplacementBranch {
+    EffectiveDirectivePath enclosingPath;
+    const parser::OmpDirectiveSpecification *spec{nullptr};
+  };
+  struct MetadirectiveReplacementContext {
+    std::size_t directiveContextDepth;
+    llvm::SmallVector<EffectiveDirectivePath, 4> paths;
+  };
+
+  llvm::SmallVector<EffectiveDirectivePath, 4>
+  GetEnclosingDirectivePaths() const;
+  llvm::SmallVector<EffectiveDirectivePath, 4> GetUniqueEffectiveDirectivePaths(
+      llvm::SmallVector<EffectiveDirectivePath, 4>) const;
+  void CollectMetadirectiveConstructSelectors(const parser::ProgramUnit &);
+  llvm::SmallVector<MetadirectiveReplacementBranch, 4>
+  GetReachableMetadirectiveReplacements(const parser::OmpClauseList &);
   bool IsCloselyNestedRegion(const llvm::omp::Directives &set);
   bool IsNestedInDirective(llvm::omp::Directive directive);
   bool IsCombinedParallelWorksharing(llvm::omp::Directive directive) const;
@@ -577,15 +599,17 @@ class OmpStructureChecker : public OmpStructureCheckerBase {
   };
   std::vector<PartKind> partStack_;
 
-  struct MetadirectiveLoopVariant {
-    const parser::traits::OmpContextSelectorSpecification *selector;
-    const parser::OmpDirectiveSpecification *spec;
-    bool checkDefaultNoneInAssociatedLoop;
+  struct PendingLoopDirectiveGroup {
+    llvm::SmallVector<MetadirectiveReplacementBranch, 4> branches;
+    bool activatesReplacementContext{false};
+    bool checkDefaultNoneInAssociatedLoop{false};
   };
-  std::vector<MetadirectiveLoopVariant> metadirectiveLoopVariants_;
-  std::vector<std::size_t> metadirectiveVariantScopeStarts_;
-  const parser::traits::OmpContextSelectorSpecification *currentWhenSelector_{
-      nullptr};
+  std::vector<PendingLoopDirectiveGroup> pendingLoopDirectiveGroups_;
+  std::vector<std::size_t> pendingLoopDirectiveScopeStarts_;
+  std::vector<ConstructTraitSequence> metadirectiveConstructSelectors_;
+  std::vector<bool> directiveSpecificationReachability_;
+  std::vector<MetadirectiveReplacementContext> activeMetadirectiveReplacements_;
+  std::vector<std::size_t> executionPartReplacementCounts_;
 
   std::multimap<const parser::Label,
       std::pair<parser::CharBlock, const parser::OpenMPConstruct *>>
diff --git a/flang/lib/Semantics/check-omp-variant.cpp b/flang/lib/Semantics/check-omp-variant.cpp
index 7d2dd8e6e0c3e..950936b9d6879 100644
--- a/flang/lib/Semantics/check-omp-variant.cpp
+++ b/flang/lib/Semantics/check-omp-variant.cpp
@@ -28,6 +28,7 @@
 #include "flang/Semantics/tools.h"
 
 #include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallPtrSet.h"
 #include "llvm/Frontend/OpenMP/OMP.h"
 
 #include <algorithm>
@@ -46,6 +47,67 @@ using namespace Fortran::semantics::omp;
 
 namespace {
 
+class MetadirectiveConstructSelectorCollector {
+public:
+  using ConstructTraitSequence = llvm::SmallVector<llvm::omp::TraitProperty, 8>;
+
+  explicit MetadirectiveConstructSelectorCollector(
+      std::vector<ConstructTraitSequence> &sequences)
+      : sequences_{sequences} {}
+
+  template <typename T> bool Pre(const T &) { return true; }
+  template <typename T> void Post(const T &) {}
+  bool Pre(const parser::CharBlock &) { return false; }
+
+  bool Pre(const parser::OmpClause::When &when) {
+    const auto &modifiers{std::get<0>(when.v.t)};
+    if (!modifiers || modifiers->size() != 1) {
+      return false;
+    }
+    const auto *contextSelector{
+        std::get_if<parser::modifier::OmpContextSelector>(
+            &modifiers->front().u)};
+    if (!contextSelector) {
+      return false;
+    }
+
+    ConstructTraitSequence sequence;
+    for (const parser::OmpTraitSetSelector &traitSet : contextSelector->v) {
+      using SetName = parser::OmpTraitSetSelectorName;
+      if (std::get<SetName>(traitSet.t).v != SetName::Value::Construct) {
+        continue;
+      }
+      for (const parser::OmpTraitSelector &selector :
+          std::get<std::list<parser::OmpTraitSelector>>(traitSet.t)) {
+        const auto &properties{
+            std::get<std::optional<parser::OmpTraitSelector::Properties>>(
+                selector.t)};
+        if (properties) {
+          // Construct properties are not modelled by variant matching yet.
+          // The recovery path retains every replacement independently of the
+          // construct context, so this selector does not distinguish paths.
+          return false;
+        }
+        const auto &name{std::get<parser::OmpTraitSelectorName>(selector.t)};
+        if (const auto *directive{std::get_if<llvm::omp::Directive>(&name.u)}) {
+          llvm::omp::VariantMatchInfo vmi;
+          AppendConstructTraitsForDirective(*directive, vmi);
+          sequence.append(
+              vmi.ConstructTraits.begin(), vmi.ConstructTraits.end());
+        }
+      }
+    }
+
+    if (!sequence.empty() && !llvm::is_contained(sequences_, sequence)) {
+      sequences_.push_back(std::move(sequence));
+    }
+    return false;
+  }
+
+private:
+  std::vector<ConstructTraitSequence> &sequences_;
+};
+
 bool HasDefaultNone(const parser::OmpDirectiveSpecification &spec) {
   using DataSharingAttribute = parser::OmpDefaultClause::DataSharingAttribute;
   const parser::OmpClause *clause{
@@ -186,23 +248,6 @@ void OmpStructureChecker::CheckDefaultNoneInAssociatedLoop(
   parser::Walk(rootLoop, checker);
 }
 
-void OmpStructureChecker::Enter(const parser::OmpClause::When &x) {
-  // Record this WHEN clause's context selector so the variant directive it
-  // controls can be paired with it for static-applicability matching. A
-  // well-formed WHEN clause has exactly one modifier, its context selector;
-  // pair it only in that case, which also makes front() safe. Any other count
-  // is malformed and already diagnosed by VerifyModifiers.
-  if (const auto &modifiers{std::get<0>(x.v.t)};
-      modifiers && modifiers->size() == 1) {
-    currentWhenSelector_ =
-        &std::get<parser::modifier::OmpContextSelector>(modifiers->front().u);
-  }
-}
-
-void OmpStructureChecker::Leave(const parser::OmpClause::When &) {
-  currentWhenSelector_ = nullptr;
-}
-
 void OmpStructureChecker::CheckContextSelectorSpecification(
     const parser::OmpContextSelector &ctx) {
   using SetName = parser::OmpTraitSetSelectorName;
@@ -702,6 +747,141 @@ void OmpStructureChecker::CheckTraitSimd(
   }
 }
 
+void OmpStructureChecker::CollectMetadirectiveConstructSelectors(
+    const parser::ProgramUnit &programUnit) {
+  metadirectiveConstructSelectors_.clear();
+  MetadirectiveConstructSelectorCollector collector{
+      metadirectiveConstructSelectors_};
+  parser::Walk(programUnit, collector);
+}
+
+llvm::SmallVector<OmpStructureChecker::EffectiveDirectivePath, 4>
+OmpStructureChecker::GetUniqueEffectiveDirectivePaths(
+    llvm::SmallVector<EffectiveDirectivePath, 4> paths) const {
+  if (paths.size() < 2) {
+    return paths;
+  }
+
+  // Selected directive paths are observed only by construct selectors on
+  // nested metadirectives. If there are none in this program unit, every path
+  // is equivalent for this analysis.
+  if (metadirectiveConstructSelectors_.empty()) {
+    paths.resize(1);
+    return paths;
+  }
+
+  auto getSignature = [&](const EffectiveDirectivePath &path) {
+    llvm::omp::VariantMatchInfo contextVMI;
+    for (auto directive{path.rbegin()}; directive != path.rend(); ++directive) {
+      AppendConstructTraitsForDirective(*directive, contextVMI);
+    }
+    const auto &contextTraits{contextVMI.ConstructTraits};
+
+    // Matching depends on trait presence and on the positions at which each
+    // ordered construct-selector prefix is matched. This is also enough to
+    // update the match when an inner directive is appended later.
+    std::vector<unsigned> signature;
+    signature.reserve(
+        1 + 2 * contextTraits.size() * metadirectiveConstructSelectors_.size());
+    signature.push_back(contextTraits.size());
+    for (const ConstructTraitSequence &selector :
+        metadirectiveConstructSelectors_) {
+      for (llvm::omp::TraitProperty property : selector) {
+        signature.push_back(llvm::is_contained(contextTraits, property));
+      }
+
+      std::size_t contextIndex{0};
+      for (llvm::omp::TraitProperty property : selector) {
+        while (contextIndex < contextTraits.size() &&
+            contextTraits[contextIndex] != property) {
+          ++contextIndex;
+        }
+        if (contextIndex == contextTraits.size()) {
+          signature.push_back(0);
+        } else {
+          // Reserve zero for an unmatched property.
+          signature.push_back(++contextIndex);
+        }
+      }
+    }
+    return signature;
+  };
+
+  std::set<std::vector<unsigned>> signatures;
+  llvm::SmallVector<EffectiveDirectivePath, 4> uniquePaths;
+  uniquePaths.reserve(paths.size());
+  for (EffectiveDirectivePath &path : paths) {
+    if (signatures.insert(getSignature(path)).second) {
+      uniquePaths.push_back(std::move(path));
+    }
+  }
+  return uniquePaths;
+}
+
+llvm::SmallVector<OmpStructureChecker::MetadirectiveReplacementBranch, 4>
+OmpStructureChecker::GetReachableMetadirectiveReplacements(
+    const parser::OmpClauseList &clauses) {
+  llvm::SmallVector<MetadirectiveReplacementBranch, 4> result;
+
+  for (const EffectiveDirectivePath &path : GetEnclosingDirectivePaths()) {
+    std::size_t firstBranch{result.size()};
+    llvm::omp::VariantMatchInfo constructVMI;
+    for (auto context{path.rbegin()}; context != path.rend(); ++context) {
+      AppendConstructTraitsForDirective(*context, constructVMI);
+    }
+    llvm::SmallVector<llvm::omp::TraitProperty, 8> constructTraits(
+        constructVMI.ConstructTraits.begin(),
+        constructVMI.ConstructTraits.end());
+    OmpVariantMatchContext matchContext{context_, constructTraits};
+    if (auto candidateSet{
+            BuildMetadirectiveCandidateSet(clauses, context_, matchContext)}) {
+      for (const parser::OmpDirectiveSpecification *spec :
+          GetReachableMetadirectiveVariants(
+              *candidateSet, matchContext, context_)) {
+        if (spec && spec->DirId() == llvm::omp::Directive::OMPD_nothing) {
+          spec = nullptr;
+        }
+        result.push_back({path, spec});
+      }
+      if (result.size() == firstBranch) {
+        result.push_back({path, nullptr});
+      }
+      continue;
+    }
+
+    // Unsupported selectors are diagnosed elsewhere. Retain every explicit
+    // replacement on each path so recovery cannot miss a loop constraint.
+    for (const parser::OmpClause &clause : clauses.v) {
+      const parser::OmpDirectiveSpecification *spec{nullptr};
+      if (const auto *when{std::get_if<parser::OmpClause::When>(&clause.u)}) {
+        const auto &optionalSpec{std::get<1>(when->v.t)};
+        if (optionalSpec) {
+          spec = &optionalSpec->value();
+        }
+      } else if (const auto *otherwise{
+                     std::get_if<parser::OmpClause::Otherwise>(&clause.u)}) {
+        if (otherwise->v && otherwise->v->v) {
+          spec = &otherwise->v->v->value();
+        }
+      } else if (const auto *defaultVariant{
+                     std::get_if<parser::OmpClause::DefaultVariant>(
+                         &clause.u)}) {
+        spec = &defaultVariant->v.v.value();
+      } else {
+        continue;
+      }
+      if (spec && spec->DirId() == llvm::omp::Directive::OMPD_nothing) {
+        spec = nullptr;
+      }
+      result.push_back({path, spec});
+    }
+    if (result.size() == firstBranch) {
+      result.push_back({path, nullptr});
+    }
+  }
+  return result;
+}
+
 void OmpStructureChecker::Enter(const parser::OmpDirectiveSpecification &x) {
   // OmpDirectiveSpecification exists on its own only in clauses on
   // METADIRECTIVE.
@@ -712,11 +892,8 @@ void OmpStructureChecker::Enter(const parser::OmpDirectiveSpecification &x) {
   }
 
   llvm::omp::Directive dirId{x.DirId()};
-  bool checkDefaultNoneInAssociatedLoop{
-      GetDirectiveNest(MetadirectiveNest) != 0};
   if (const parser::OpenMPConstruct *meta{GetCurrentConstruct()}) {
     if (parser::Unwrap<parser::OmpDelimitedMetadirectiveDirective>(meta->u)) {
-      checkDefaultNoneInAssociatedLoop = false;
       llvm::omp::Version version{context_.langOptions().getOpenMPVersion()};
       switch (llvm::omp::getDirectiveAssociation(dirId)) {
       case llvm::omp::Association::Block:
@@ -737,11 +914,32 @@ void OmpStructureChecker::Enter(const parser::OmpDirectiveSpecification &x) {
   PushContextAndClauseSets(
       std::get<parser::OmpDirectiveName>(x.t).source, dirId);
 
-  // Record each variant directive. A loop-associated one is later validated
-  // against the loop nest that follows the metadirective.
+  // Each metadirective group already contains its ranked reachable
+  // replacements. APPLY specifications are separate loop transformations;
+  // retain them only when their containing replacement is reachable.
+  bool reachable{true};
+  if (GetDirectiveNest(MetadirectiveNest)) {
+    reachable = llvm::any_of(pendingLoopDirectiveGroups_,
+        [&x](const PendingLoopDirectiveGroup &group) {
+          return llvm::any_of(group.branches,
+              [&x](const MetadirectiveReplacementBranch &branch) {
+                return branch.spec == &x;
+              });
+        });
+    if (!reachable && GetDirectiveNest(ApplyNest) &&
+        !directiveSpecificationReachability_.empty()) {
+      reachable = directiveSpecificationReachability_.back();
+    }
+  }
+  directiveSpecificationReachability_.push_back(reachable);
+
+  if (GetDirectiveNest(ApplyNest) && reachable &&
+      dirId != llvm::omp::Directive::OMPD_metadirective) {
+    pendingLoopDirectiveGroups_.push_back(
+        {{{EffectiveDirectivePath{}, &x}}, false, false});
+  }
+
   if (dirId != llvm::omp::Directive::OMPD_metadirective) {
-    metadirectiveLoopVariants_.push_back(
-        {currentWhenSelector_, &x, checkDefaultNoneInAssociatedLoop});
     // Metadirective is "pure", but its selected variant may not be.
     // Check the variant independently only when metadirective is legal;
     // otherwise, the outer metadirective check already reports the error.
@@ -763,11 +961,15 @@ void OmpStructureChecker::Enter(const parser::OmpDirectiveSpecification &x) {
 
 void OmpStructureChecker::Leave(const parser::OmpDirectiveSpecification &x) {
   if (GetDirectiveNest(MetadirectiveNest) || GetDirectiveNest(ApplyNest)) {
+    CHECK(!directiveSpecificationReachability_.empty());
+    directiveSpecificationReachability_.pop_back();
     dirContext_.pop_back();
   }
 }
 
 void OmpStructureChecker::Enter(const parser::OmpMetadirectiveDirective &x) {
+  auto branches{GetReachableMetadirectiveReplacements(x.v.Clauses())};
+  pendingLoopDirectiveGroups_.push_back({std::move(branches), true, true});
   EnterDirectiveNest(MetadirectiveNest);
 }
 
@@ -775,12 +977,36 @@ void OmpStructureChecker::Leave(const parser::OmpMetadirectiveDirective &) {
   ExitDirectiveNest(MetadirectiveNest);
 }
 
+void OmpStructureChecker::Enter(
+    const parser::OmpDelimitedMetadirectiveDirective &x) {
+  auto branches{GetReachableMetadirectiveReplacements(x.BeginDir().Clauses())};
+  llvm::SmallVector<EffectiveDirectivePath, 4> paths;
+  for (const MetadirectiveReplacementBranch &branch : branches) {
+    EffectiveDirectivePath path{branch.enclosingPath};
+    if (branch.spec) {
+      path.insert(path.begin(), branch.spec->DirId());
+    }
+    paths.push_back(std::move(path));
+  }
+  paths = GetUniqueEffectiveDirectivePaths(std::move(paths));
+  activeMetadirectiveReplacements_.push_back(
+      {dirContext_.size(), std::move(paths)});
+  pendingLoopDirectiveGroups_.push_back({std::move(branches), false, false});
+}
+
+void OmpStructureChecker::Leave(
+    const parser::OmpDelimitedMetadirectiveDirective &) {
+  CHECK(!activeMetadirectiveReplacements_.empty());
+  activeMetadirectiveReplacements_.pop_back();
+}
+
 // Check a loop-associated metadirective's variants against the loop nest they
 // apply to. The nest is not attached to the directive in the parse tree. It is
 // the next executable construct, either a following sibling or the first
 // execution-part construct for a declarative metadirective.
 void OmpStructureChecker::Enter(const parser::ExecutionPartConstruct &x) {
-  if (metadirectiveLoopVariants_.empty()) {
+  executionPartReplacementCounts_.push_back(0);
+  if (pendingLoopDirectiveGroups_.empty()) {
     return;
   }
   if (parser::Unwrap<parser::CompilerDirective>(x)) {
@@ -791,14 +1017,37 @@ void OmpStructureChecker::Enter(const parser::ExecutionPartConstruct &x) {
   if (!parser::Unwrap<parser::DoConstruct>(x)) {
     // A non-loop construct follows, so a loop-associated variant has no loop
     // nest to associate with.
-    CheckMetadirectiveVariantsWithoutLoop();
+    CheckPendingLoopDirectivesWithoutLoop();
     return;
   }
 
-  // A loop nest follows. Take the pending variants off the worklist and
-  // validate them against it.
-  std::vector<MetadirectiveLoopVariant> variants;
-  variants.swap(metadirectiveLoopVariants_);
+  // A loop nest follows. Take the pending groups off the worklist and validate
+  // their reachable directives against it.
+  std::vector<PendingLoopDirectiveGroup> pending;
+  pending.swap(pendingLoopDirectiveGroups_);
+  for (const PendingLoopDirectiveGroup &group : pending) {
+    if (!group.activatesReplacementContext) {
+      continue;
+    }
+    llvm::SmallVector<EffectiveDirectivePath, 4> paths;
+    for (const MetadirectiveReplacementBranch &branch : group.branches) {
+      EffectiveDirectivePath path{branch.enclosingPath};
+      const parser::OmpDirectiveSpecification *spec{branch.spec};
+      if (spec) {
+        llvm::omp::Association association{
+            llvm::omp::getDirectiveAssociation(spec->DirId())};
+        if (association == llvm::omp::Association::LoopNest ||
+            association == llvm::omp::Association::LoopSequence) {
+          path.insert(path.begin(), spec->DirId());
+        }
+      }
+      paths.push_back(std::move(path));
+    }
+    paths = GetUniqueEffectiveDirectivePaths(std::move(paths));
+    activeMetadirectiveReplacements_.push_back(
+        {dirContext_.size(), std::move(paths)});
+    ++executionPartReplacementCounts_.back();
+  }
 
   llvm::omp::Version version{context_.langOptions().getOpenMPVersion()};
   LoopSequence sequence(x, version, /*allowAllLoops=*/true, &context_);
@@ -833,87 +1082,91 @@ void OmpStructureChecker::Enter(const parser::ExecutionPartConstruct &x) {
         return false;
       };
 
-  // Build the matching context once for the static-applicability gate below.
-  OmpVariantMatchContext matchContext{context_};
   UnorderedSymbolSet defaultNoneDiagnosed;
+  llvm::SmallPtrSet<const parser::OmpDirectiveSpecification *, 8>
+      checkedSpecifications;
 
-  for (const MetadirectiveLoopVariant &variant : variants) {
-    const parser::OmpDirectiveSpecification *spec{variant.spec};
-    // Skip variants that can never be selected on this compilation target so
-    // that their associated loop is not diagnosed.
-    if (!MayVariantBeSelected(variant.selector, context_, matchContext)) {
-      continue;
-    }
-    auto assoc{llvm::omp::getDirectiveAssociation(spec->DirId())};
-    if (assoc == llvm::omp::Association::LoopNest) {
-      if (!checkRootLoopCanonical(*spec, /*isSequence=*/false)) {
+  for (const PendingLoopDirectiveGroup &group : pending) {
+    for (const MetadirectiveReplacementBranch &branch : group.branches) {
+      const parser::OmpDirectiveSpecification *spec{branch.spec};
+      if (!spec || !checkedSpecifications.insert(spec).second) {
         continue;
       }
+      auto assoc{llvm::omp::getDirectiveAssociation(spec->DirId())};
+      if (assoc == llvm::omp::Association::LoopNest) {
+        if (!checkRootLoopCanonical(*spec, /*isSequence=*/false)) {
+          continue;
+        }
 
-      // A standalone metadirective does not contain its associated loop in
-      // the parse tree, so name resolution cannot apply DEFAULT(NONE) to it.
-      if (variant.checkDefaultNoneInAssociatedLoop) {
-        CheckDefaultNoneInAssociatedLoop(*spec, rootLoop, defaultNoneDiagnosed);
-      }
+        // A standalone metadirective does not contain its associated loop in
+        // the parse tree, so name resolution cannot apply DEFAULT(NONE) to it.
+        if (group.checkDefaultNoneInAssociatedLoop) {
+          CheckDefaultNoneInAssociatedLoop(
+              *spec, rootLoop, defaultNoneDiagnosed);
+        }
 
-      auto [needDepth, needPerfect]{
-          GetAffectedNestDepthWithReason(*spec, version, &context_)};
-      auto haveDepth{needPerfect ? havePerfect : haveSemantic};
-      if (!needDepth || *needDepth.value <= 0 || !haveDepth ||
-          *haveDepth.value <= 0) {
-        continue;
-      }
-      if (*needDepth.value > *haveDepth.value) {
-        std::string_view perfectTxt{needPerfect ? " perfect" : ""};
-        auto &msg{context_.Say(spec->DirName().source,
-            "This construct requires a%s nest of depth %" PRId64
-            ", but the associated nest is a%s nest of depth %" PRId64
-            ""_err_en_US,
-            perfectTxt, *needDepth.value, perfectTxt, *haveDepth.value)};
-        haveDepth.reason.AttachTo(msg);
-        needDepth.reason.AttachTo(msg);
-      } else {
-        CheckRectangularNest(*spec, sequence);
+        auto [needDepth, needPerfect]{
+            GetAffectedNestDepthWithReason(*spec, version, &context_)};
+        auto haveDepth{needPerfect ? havePerfect : haveSemantic};
+        if (!needDepth || *needDepth.value <= 0 || !haveDepth ||
+            *haveDepth.value <= 0) {
+          continue;
+        }
+        if (*needDepth.value > *haveDepth.value) {
+          std::string_view perfectTxt{needPerfect ? " perfect" : ""};
+          auto &msg{context_.Say(spec->DirName().source,
+              "This construct requires a%s nest of depth %" PRId64
+              ", but the associated nest is a%s nest of depth %" PRId64
+              ""_err_en_US,
+              perfectTxt, *needDepth.value, perfectTxt, *haveDepth.value)};
+          haveDepth.reason.AttachTo(msg);
+          needDepth.reason.AttachTo(msg);
+        } else {
+          CheckRectangularNest(*spec, sequence);
+        }
+      } else if (assoc == llvm::omp::Association::LoopSequence) {
+        (void)checkRootLoopCanonical(*spec, /*isSequence=*/true);
       }
-    } else if (assoc == llvm::omp::Association::LoopSequence) {
-      (void)checkRootLoopCanonical(*spec, /*isSequence=*/true);
     }
   }
 }
 
-// Diagnose loop-associated metadirective variants that are not followed by a
-// loop nest, either because the metadirective is the last construct in the
-// execution part or because a non-loop construct follows it. Variants that
-// cannot be selected on this target are skipped.
-void OmpStructureChecker::CheckMetadirectiveVariantsWithoutLoop(
-    std::size_t firstVariant) {
-  CHECK(firstVariant <= metadirectiveLoopVariants_.size());
-  std::vector<MetadirectiveLoopVariant> variants;
-  if (firstVariant == 0) {
-    variants.swap(metadirectiveLoopVariants_);
-  } else {
-    auto first{metadirectiveLoopVariants_.begin() + firstVariant};
-    variants.assign(first, metadirectiveLoopVariants_.end());
-    metadirectiveLoopVariants_.erase(first, metadirectiveLoopVariants_.end());
-  }
+void OmpStructureChecker::Leave(const parser::ExecutionPartConstruct &) {
+  CHECK(!executionPartReplacementCounts_.empty());
+  std::size_t count{executionPartReplacementCounts_.back()};
+  executionPartReplacementCounts_.pop_back();
+  CHECK(count <= activeMetadirectiveReplacements_.size());
+  activeMetadirectiveReplacements_.resize(
+      activeMetadirectiveReplacements_.size() - count);
+}
 
-  OmpVariantMatchContext matchContext{context_};
+// Diagnose reachable loop-associated directives that are not followed by a
+// loop nest, either because the directive is last in the execution part or
+// because a non-loop construct follows it.
+void OmpStructureChecker::CheckPendingLoopDirectivesWithoutLoop(
+    std::size_t firstDirectiveGroup) {
+  CHECK(firstDirectiveGroup <= pendingLoopDirectiveGroups_.size());
   const auto MsgShouldContainDoOr{
       "This construct should contain a DO-loop or a loop-%s-generating construct"_err_en_US};
-
-  for (const MetadirectiveLoopVariant &variant : variants) {
-    if (!MayVariantBeSelected(variant.selector, context_, matchContext)) {
-      continue;
-    }
-    auto assoc{llvm::omp::getDirectiveAssociation(variant.spec->DirId())};
-    if (assoc == llvm::omp::Association::LoopNest) {
-      context_.Say(
-          variant.spec->DirName().source, MsgShouldContainDoOr, "nest");
-    } else if (assoc == llvm::omp::Association::LoopSequence) {
-      context_.Say(
-          variant.spec->DirName().source, MsgShouldContainDoOr, "sequence");
+  llvm::SmallPtrSet<const parser::OmpDirectiveSpecification *, 8>
+      checkedSpecifications;
+
+  auto first{pendingLoopDirectiveGroups_.begin() + firstDirectiveGroup};
+  for (auto group{first}; group != pendingLoopDirectiveGroups_.end(); ++group) {
+    for (const MetadirectiveReplacementBranch &branch : group->branches) {
+      const parser::OmpDirectiveSpecification *spec{branch.spec};
+      if (!spec || !checkedSpecifications.insert(spec).second) {
+        continue;
+      }
+      auto assoc{llvm::omp::getDirectiveAssociation(spec->DirId())};
+      if (assoc == llvm::omp::Association::LoopNest) {
+        context_.Say(spec->DirName().source, MsgShouldContainDoOr, "nest");
+      } else if (assoc == llvm::omp::Association::LoopSequence) {
+        context_.Say(spec->DirName().source, MsgShouldContainDoOr, "sequence");
+      }
     }
   }
+  pendingLoopDirectiveGroups_.erase(first, pendingLoopDirectiveGroups_.end());
 }
 
 static const parser::traits::OmpContextSelectorSpecification *
diff --git a/flang/lib/Semantics/openmp-utils.cpp b/flang/lib/Semantics/openmp-utils.cpp
index 136124664f4b0..09697da1c5b7f 100644
--- a/flang/lib/Semantics/openmp-utils.cpp
+++ b/flang/lib/Semantics/openmp-utils.cpp
@@ -2431,7 +2431,7 @@ UnsupportedSelectorFeature FindUnsupportedSelectorFeature(
 // `target` adds `construct_target_target`, `target teams` adds both
 // `construct_target_target` and `construct_teams_teams`) to \p vmi. This
 // decomposes combined/composite construct selectors into their leaf traits.
-static void AppendConstructTraitsForDirective(
+void AppendConstructTraitsForDirective(
     llvm::omp::Directive dir, llvm::omp::VariantMatchInfo &vmi) {
   auto add = [&](llvm::omp::TraitProperty prop) {
     vmi.addTrait(prop, llvm::omp::getOpenMPContextTraitPropertyName(prop, ""));
diff --git a/flang/test/Semantics/OpenMP/metadirective-loop-applicability-apply.f90 b/flang/test/Semantics/OpenMP/metadirective-loop-applicability-apply.f90
new file mode 100644
index 0000000000000..15b01f4375e16
--- /dev/null
+++ b/flang/test/Semantics/OpenMP/metadirective-loop-applicability-apply.f90
@@ -0,0 +1,23 @@
+!RUN: %python %S/../test_errors.py %s %flang -fopenmp -fopenmp-version=60
+
+! A loop transformation in APPLY inherits the reachability of the
+! metadirective replacement that contains it.
+
+subroutine f01()
+  !$omp metadirective &
+  !$omp& when(user={condition(score(10): .true.)}: nothing) &
+  !$omp& when(user={condition(score(5): .true.)}: &
+  !$omp& tile sizes(2) apply(grid: unroll)) &
+  !$omp& otherwise(nothing)
+end subroutine
+
+subroutine f02(flag)
+  logical :: flag
+  !$omp metadirective &
+  !$omp& when(user={condition(score(10): flag)}: nothing) &
+  !$omp& when(user={condition(score(5): .true.)}: &
+  !ERROR: This construct should contain a DO-loop or a loop-nest-generating construct
+  !ERROR: This construct should contain a DO-loop or a loop-nest-generating construct
+  !$omp& tile sizes(2) apply(grid: unroll)) &
+  !$omp& otherwise(nothing)
+end subroutine
diff --git a/flang/test/Semantics/OpenMP/metadirective-loop-applicability-openmp-60.f90 b/flang/test/Semantics/OpenMP/metadirective-loop-applicability-openmp-60.f90
new file mode 100644
index 0000000000000..6729a06fc4faa
--- /dev/null
+++ b/flang/test/Semantics/OpenMP/metadirective-loop-applicability-openmp-60.f90
@@ -0,0 +1,13 @@
+!RUN: %python %S/../test_errors.py %s %flang -fopenmp -fopenmp-version=60
+
+! An unsupported selector conservatively retains its OTHERWISE replacement.
+subroutine f01(n, a)
+  integer :: n, a(n), i
+  !$omp metadirective &
+  !ERROR: This construct requires a nest of depth 2, but the associated nest is a nest of depth 1
+  !BECAUSE: COLLAPSE clause was specified with argument 2
+  !$omp& when(target_device={kind(host)}: nothing) otherwise(do collapse(2))
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
diff --git a/flang/test/Semantics/OpenMP/metadirective-loop-applicability.f90 b/flang/test/Semantics/OpenMP/metadirective-loop-applicability.f90
index 0665ab304aea5..9c59d3ff1f5a7 100644
--- a/flang/test/Semantics/OpenMP/metadirective-loop-applicability.f90
+++ b/flang/test/Semantics/OpenMP/metadirective-loop-applicability.f90
@@ -131,3 +131,189 @@ subroutine f11()
   logical, parameter :: use_variant = .true.
   !$omp metadirective when(user={condition(use_variant)}, implementation={extension(match_none)}: do) default(nothing)
 end subroutine
+
+! A higher-scored static implicit NOTHING makes the lower-scored loop variant
+! unreachable, so the latter's loop requirements are not checked.
+subroutine f12(n, a)
+  integer :: n, a(n), i
+  !$omp metadirective &
+  !$omp& when(user={condition(score(10): .true.)}:) &
+  !$omp& when(user={condition(score(5): .true.)}: do collapse(2)) &
+  !$omp& default(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
+
+! A dynamic implicit NOTHING leaves the lower-scored loop variant reachable
+! when its condition is false, so that variant's requirements are checked.
+subroutine f13(flag, n, a)
+  logical :: flag
+  integer :: n, a(n), i
+  !$omp metadirective &
+  !$omp& when(user={condition(score(10): flag)}:) &
+  !ERROR: This construct requires a nest of depth 2, but the associated nest is a nest of depth 1
+  !BECAUSE: COLLAPSE clause was specified with argument 2
+  !$omp& when(user={condition(score(5): .true.)}: do collapse(2)) &
+  !$omp& default(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
+
+! A nested construct selector observes the loop directive selected by a
+! standalone metadirective.
+subroutine f14(n, a)
+  integer :: n, a(n, n), i, j
+  !$omp metadirective when(implementation={vendor(llvm)}: do) default(nothing)
+  do i = 1, n
+    !ERROR: This construct requires a nest of depth 2, but the associated nest is a nest of depth 1
+    !BECAUSE: COLLAPSE clause was specified with argument 2
+    !$omp metadirective when(construct={do}: simd collapse(2)) default(nothing)
+    do j = 1, n
+      a(j, i) = i
+    end do
+  end do
+end subroutine
+
+! Actual and selected contexts are both visible inside a begin/end
+! metadirective.
+subroutine f15(n, a)
+  integer :: n, a(n, n), i, j
+  !$omp target
+    !$omp begin metadirective &
+    !$omp& when(implementation={vendor(llvm)}: parallel) default(nothing)
+    do i = 1, n
+      !ERROR: This construct requires a nest of depth 2, but the associated nest is a nest of depth 1
+      !BECAUSE: COLLAPSE clause was specified with argument 2
+      !$omp metadirective when(construct={target, parallel}: simd collapse(2)) default(nothing)
+      do j = 1, n
+        a(j, i) = i
+      end do
+    end do
+    !$omp end metadirective
+  !$omp end target
+end subroutine
+
+! Mutually exclusive selected contexts are not flattened together. No path has
+! both TARGET and PARALLEL, so the first inner loop variant is unreachable.
+! The second inner variant verifies that the dynamic TARGET path is retained.
+subroutine f16(flag, n, a)
+  logical :: flag
+  integer :: n, a(n), i
+  !$omp begin metadirective &
+  !$omp& when(user={condition(flag)}: target) default(parallel)
+    !$omp metadirective &
+    !$omp& when(construct={target, parallel}: simd collapse(2)) &
+    !$omp& default(nothing)
+    do i = 1, n
+      a(i) = i
+    end do
+
+    !ERROR: This construct requires a nest of depth 2, but the associated nest is a nest of depth 1
+    !BECAUSE: COLLAPSE clause was specified with argument 2
+    !$omp metadirective when(construct={target}: simd collapse(2)) default(nothing)
+    do i = 1, n
+      a(i) = i
+    end do
+  !$omp end metadirective
+end subroutine
+
+! A non-NOTHING fallback contributes its selected context to nested matching.
+subroutine f17(flag, n, a)
+  logical :: flag
+  integer :: n, a(n), i
+  !$omp begin metadirective &
+  !$omp& when(user={condition(flag)}: target) default(parallel)
+    !ERROR: This construct requires a nest of depth 2, but the associated nest is a nest of depth 1
+    !BECAUSE: COLLAPSE clause was specified with argument 2
+    !$omp metadirective when(construct={parallel}: simd collapse(2)) default(nothing)
+    do i = 1, n
+      a(i) = i
+    end do
+  !$omp end metadirective
+end subroutine
+
+! A selected context ends with its associated loop and does not affect a
+! subsequent metadirective.
+subroutine f18(n, a)
+  integer :: n, a(n), i
+  !$omp metadirective when(implementation={vendor(llvm)}: do) default(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+
+  !$omp metadirective when(construct={do}: simd collapse(2)) default(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
+
+! A selected context ends at END METADIRECTIVE and does not affect a
+! subsequent metadirective.
+subroutine f19(n, a)
+  integer :: n, a(n), i
+  !$omp begin metadirective &
+  !$omp& when(implementation={vendor(llvm)}: parallel) default(nothing)
+    a(1) = 1
+  !$omp end metadirective
+
+  !$omp metadirective when(construct={parallel}: simd collapse(2)) default(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
+
+! Unsupported selectors conservatively retain their explicit replacement.
+subroutine f20(n, a)
+  integer :: n, a(n), i
+  !$omp metadirective &
+  !ERROR: This construct requires a nest of depth 2, but the associated nest is a nest of depth 1
+  !BECAUSE: COLLAPSE clause was specified with argument 2
+  !$omp& when(target_device={kind(host)}: do collapse(2)) default(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
+
+! Unsupported selectors also retain the DEFAULT replacement.
+subroutine f21(n, a)
+  integer :: n, a(n), i
+  !$omp metadirective &
+  !ERROR: This construct requires a nest of depth 2, but the associated nest is a nest of depth 1
+  !BECAUSE: COLLAPSE clause was specified with argument 2
+  !$omp& when(target_device={kind(host)}: nothing) default(do collapse(2))
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
+
+! Recovery retains replacements from siblings of an unsupported selector.
+subroutine f22(n, a)
+  integer :: n, a(n), i
+  !$omp metadirective &
+  !$omp& when(target_device={kind(host)}: nothing) &
+  !ERROR: This construct requires a nest of depth 2, but the associated nest is a nest of depth 1
+  !BECAUSE: COLLAPSE clause was specified with argument 2
+  !$omp& when(implementation={vendor(llvm)}: do collapse(2)) &
+  !$omp& default(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
+
+! A selected composite directive contributes each of its leaf constructs.
+subroutine f23(n, a)
+  integer :: n, a(n, n), i, j
+  !$omp metadirective &
+  !$omp& when(implementation={vendor(llvm)}: parallel do) default(nothing)
+  do i = 1, n
+    !$omp metadirective &
+    !ERROR: This construct requires a nest of depth 2, but the associated nest is a nest of depth 1
+    !BECAUSE: COLLAPSE clause was specified with argument 2
+    !$omp& when(construct={parallel, do}: simd collapse(2)) default(nothing)
+    do j = 1, n
+      a(j, i) = i
+    end do
+  end do
+end subroutine

>From 35d4cc455312a3c2137805fe569fadebd382612d Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Thu, 3 Sep 2026 16:25:26 -0500
Subject: [PATCH 02/11] Fix metadirective reachable path boundaries

Stop construct trait paths at the innermost target and retain the implicit
nothing fallback during unsupported-selector recovery.

Add coverage for actual and selected target boundaries and for nested
metadirectives reached through implicit fallback.
---
 flang/lib/Semantics/check-omp-structure.h     |  2 ++
 flang/lib/Semantics/check-omp-variant.cpp     | 36 ++++++++++++-------
 ...directive-loop-applicability-openmp-60.f90 | 16 +++++++++
 .../metadirective-loop-applicability.f90      | 32 +++++++++++++++++
 4 files changed, 73 insertions(+), 13 deletions(-)

diff --git a/flang/lib/Semantics/check-omp-structure.h b/flang/lib/Semantics/check-omp-structure.h
index 2955e1322454b..ecc3acea4efa4 100644
--- a/flang/lib/Semantics/check-omp-structure.h
+++ b/flang/lib/Semantics/check-omp-structure.h
@@ -427,6 +427,8 @@ class OmpStructureChecker : public OmpStructureCheckerBase {
 
   llvm::SmallVector<EffectiveDirectivePath, 4>
   GetEnclosingDirectivePaths() const;
+  ConstructTraitSequence GetConstructTraitsForPath(
+      const EffectiveDirectivePath &) const;
   llvm::SmallVector<EffectiveDirectivePath, 4> GetUniqueEffectiveDirectivePaths(
       llvm::SmallVector<EffectiveDirectivePath, 4>) const;
   void CollectMetadirectiveConstructSelectors(const parser::ProgramUnit &);
diff --git a/flang/lib/Semantics/check-omp-variant.cpp b/flang/lib/Semantics/check-omp-variant.cpp
index 950936b9d6879..8b01fc723f96e 100644
--- a/flang/lib/Semantics/check-omp-variant.cpp
+++ b/flang/lib/Semantics/check-omp-variant.cpp
@@ -755,6 +755,23 @@ void OmpStructureChecker::CollectMetadirectiveConstructSelectors(
   parser::Walk(programUnit, collector);
 }
 
+OmpStructureChecker::ConstructTraitSequence
+OmpStructureChecker::GetConstructTraitsForPath(
+    const EffectiveDirectivePath &path) const {
+  ConstructTraitSequence constructTraits;
+  for (auto directive{path.rbegin()}; directive != path.rend(); ++directive) {
+    // The construct trait set starts at the innermost target construct.
+    if (llvm::omp::allTargetSet.test(*directive)) {
+      constructTraits.clear();
+    }
+    llvm::omp::VariantMatchInfo directiveVMI;
+    AppendConstructTraitsForDirective(*directive, directiveVMI);
+    constructTraits.append(directiveVMI.ConstructTraits.begin(),
+        directiveVMI.ConstructTraits.end());
+  }
+  return constructTraits;
+}
+
 llvm::SmallVector<OmpStructureChecker::EffectiveDirectivePath, 4>
 OmpStructureChecker::GetUniqueEffectiveDirectivePaths(
     llvm::SmallVector<EffectiveDirectivePath, 4> paths) const {
@@ -771,11 +788,7 @@ OmpStructureChecker::GetUniqueEffectiveDirectivePaths(
   }
 
   auto getSignature = [&](const EffectiveDirectivePath &path) {
-    llvm::omp::VariantMatchInfo contextVMI;
-    for (auto directive{path.rbegin()}; directive != path.rend(); ++directive) {
-      AppendConstructTraitsForDirective(*directive, contextVMI);
-    }
-    const auto &contextTraits{contextVMI.ConstructTraits};
+    ConstructTraitSequence contextTraits{GetConstructTraitsForPath(path)};
 
     // Matching depends on trait presence and on the positions at which each
     // ordered construct-selector prefix is matched. This is also enough to
@@ -825,13 +838,7 @@ OmpStructureChecker::GetReachableMetadirectiveReplacements(
 
   for (const EffectiveDirectivePath &path : GetEnclosingDirectivePaths()) {
     std::size_t firstBranch{result.size()};
-    llvm::omp::VariantMatchInfo constructVMI;
-    for (auto context{path.rbegin()}; context != path.rend(); ++context) {
-      AppendConstructTraitsForDirective(*context, constructVMI);
-    }
-    llvm::SmallVector<llvm::omp::TraitProperty, 8> constructTraits(
-        constructVMI.ConstructTraits.begin(),
-        constructVMI.ConstructTraits.end());
+    ConstructTraitSequence constructTraits{GetConstructTraitsForPath(path)};
     OmpVariantMatchContext matchContext{context_, constructTraits};
     if (auto candidateSet{
             BuildMetadirectiveCandidateSet(clauses, context_, matchContext)}) {
@@ -851,6 +858,7 @@ OmpStructureChecker::GetReachableMetadirectiveReplacements(
 
     // Unsupported selectors are diagnosed elsewhere. Retain every explicit
     // replacement on each path so recovery cannot miss a loop constraint.
+    bool hasFallback{false};
     for (const parser::OmpClause &clause : clauses.v) {
       const parser::OmpDirectiveSpecification *spec{nullptr};
       if (const auto *when{std::get_if<parser::OmpClause::When>(&clause.u)}) {
@@ -860,12 +868,14 @@ OmpStructureChecker::GetReachableMetadirectiveReplacements(
         }
       } else if (const auto *otherwise{
                      std::get_if<parser::OmpClause::Otherwise>(&clause.u)}) {
+        hasFallback = true;
         if (otherwise->v && otherwise->v->v) {
           spec = &otherwise->v->v->value();
         }
       } else if (const auto *defaultVariant{
                      std::get_if<parser::OmpClause::DefaultVariant>(
                          &clause.u)}) {
+        hasFallback = true;
         spec = &defaultVariant->v.v.value();
       } else {
         continue;
@@ -875,7 +885,7 @@ OmpStructureChecker::GetReachableMetadirectiveReplacements(
       }
       result.push_back({path, spec});
     }
-    if (result.size() == firstBranch) {
+    if (!hasFallback) {
       result.push_back({path, nullptr});
     }
   }
diff --git a/flang/test/Semantics/OpenMP/metadirective-loop-applicability-openmp-60.f90 b/flang/test/Semantics/OpenMP/metadirective-loop-applicability-openmp-60.f90
index 6729a06fc4faa..0b6efa0617e35 100644
--- a/flang/test/Semantics/OpenMP/metadirective-loop-applicability-openmp-60.f90
+++ b/flang/test/Semantics/OpenMP/metadirective-loop-applicability-openmp-60.f90
@@ -11,3 +11,19 @@ subroutine f01(n, a)
     a(i) = i
   end do
 end subroutine
+
+! Unsupported-selector recovery retains an implicit NOTHING fallback.
+subroutine f02(n, a)
+  integer :: n, a(n), i
+  !$omp begin metadirective &
+  !$omp& when(construct={simd(simdlen(8))}: parallel)
+    !$omp metadirective &
+    !$omp& when(construct={parallel}: nothing) &
+    !ERROR: This construct requires a nest of depth 2, but the associated nest is a nest of depth 1
+    !BECAUSE: COLLAPSE clause was specified with argument 2
+    !$omp& otherwise(do collapse(2))
+    do i = 1, n
+      a(i) = i
+    end do
+  !$omp end metadirective
+end subroutine
diff --git a/flang/test/Semantics/OpenMP/metadirective-loop-applicability.f90 b/flang/test/Semantics/OpenMP/metadirective-loop-applicability.f90
index 9c59d3ff1f5a7..ccddf604708ea 100644
--- a/flang/test/Semantics/OpenMP/metadirective-loop-applicability.f90
+++ b/flang/test/Semantics/OpenMP/metadirective-loop-applicability.f90
@@ -317,3 +317,35 @@ subroutine f23(n, a)
     end do
   end do
 end subroutine
+
+! An actual TARGET hides enclosing constructs from a nested selector.
+subroutine f24(n, a)
+  integer :: n, a(n), i
+  !$omp parallel
+    !$omp target
+      !$omp metadirective &
+      !$omp& when(construct={parallel, target}: &
+      !$omp& simd collapse(2)) default(nothing)
+      do i = 1, n
+        a(i) = i
+      end do
+    !$omp end target
+  !$omp end parallel
+end subroutine
+
+! A selected TARGET also hides enclosing constructs from a nested selector.
+subroutine f25(n, a)
+  integer :: n, a(n), i
+  !$omp parallel
+    !$omp begin metadirective &
+    !$omp& when(implementation={vendor(llvm)}: target) &
+    !$omp& default(nothing)
+      !$omp metadirective &
+      !$omp& when(construct={parallel, target}: &
+      !$omp& simd collapse(2)) default(nothing)
+      do i = 1, n
+        a(i) = i
+      end do
+    !$omp end metadirective
+  !$omp end parallel
+end subroutine

>From e00df19b29d7195ccaf33ea33f58bb23fabd5cfe Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Tue, 8 Sep 2026 12:45:00 -0500
Subject: [PATCH 03/11] Fix extension matching and reachable path merging

Metadirective semantic checking relies on ranked selection to decide which
replacements need validation. With match_any and match_none, a selector can
remain applicable even when some construct traits are absent. The existing
matching and path-merging assumptions do not fully account for this, so
ranking can crash or discard a reachable replacement, allowing invalid loop
nests to pass unchecked.

Make applicability, scoring, and reachable-path merging agree on what these
matching extensions can observe. Preserve enough construct context to rank
partially matching selectors and distinguish paths whose nesting changes
the selected replacement. That context must also remain visible inside a
BLOCK associated with a selected directive, so nested metadirectives are
checked against the context in which they can execute.
---
 flang/lib/Semantics/check-omp-variant.cpp     | 87 +++++++++++--------
 .../metadirective-loop-applicability.f90      | 68 +++++++++++++++
 .../OpenMP/metadirective-match-any-paths.f90  | 71 +++++++++++++++
 .../OpenMP/metadirective-ordered-paths.f90    | 45 ++++++++++
 4 files changed, 237 insertions(+), 34 deletions(-)
 create mode 100644 flang/test/Semantics/OpenMP/metadirective-match-any-paths.f90
 create mode 100644 flang/test/Semantics/OpenMP/metadirective-ordered-paths.f90

diff --git a/flang/lib/Semantics/check-omp-variant.cpp b/flang/lib/Semantics/check-omp-variant.cpp
index 8b01fc723f96e..6f976f78a6f5b 100644
--- a/flang/lib/Semantics/check-omp-variant.cpp
+++ b/flang/lib/Semantics/check-omp-variant.cpp
@@ -790,9 +790,9 @@ OmpStructureChecker::GetUniqueEffectiveDirectivePaths(
   auto getSignature = [&](const EffectiveDirectivePath &path) {
     ConstructTraitSequence contextTraits{GetConstructTraitsForPath(path)};
 
-    // Matching depends on trait presence and on the positions at which each
-    // ordered construct-selector prefix is matched. This is also enough to
-    // update the match when an inner directive is appended later.
+    // Matching depends on trait presence and ordered match positions. Retain
+    // matches after a failure for match_any scoring, as well as successful
+    // prefixes for matching after inner directives are appended.
     std::vector<unsigned> signature;
     signature.reserve(
         1 + 2 * contextTraits.size() * metadirectiveConstructSelectors_.size());
@@ -805,12 +805,15 @@ OmpStructureChecker::GetUniqueEffectiveDirectivePaths(
 
       std::size_t contextIndex{0};
       for (llvm::omp::TraitProperty property : selector) {
+        std::size_t searchStart{contextIndex};
         while (contextIndex < contextTraits.size() &&
             contextTraits[contextIndex] != property) {
           ++contextIndex;
         }
         if (contextIndex == contextTraits.size()) {
           signature.push_back(0);
+          // Like match_any, skip absent traits without consuming the context.
+          contextIndex = searchStart;
         } else {
           // Reserve zero for an unmatched property.
           signature.push_back(++contextIndex);
@@ -1022,9 +1025,49 @@ void OmpStructureChecker::Enter(const parser::ExecutionPartConstruct &x) {
   if (parser::Unwrap<parser::CompilerDirective>(x)) {
     return;
   }
+
+  const parser::DoConstruct *rootLoop{parser::Unwrap<parser::DoConstruct>(x)};
+  bool isStrictlyStructuredBlock{
+      parser::Unwrap<parser::BlockConstruct>(x) != nullptr};
+
+  // A standalone metadirective's replacement applies to its following
+  // associated construct. Keep block-associated replacements active across a
+  // Fortran BLOCK, just as loop-associated replacements are active across
+  // their DO construct, so nested construct selectors see the selected path.
+  if (rootLoop || isStrictlyStructuredBlock) {
+    for (const PendingLoopDirectiveGroup &group : pendingLoopDirectiveGroups_) {
+      if (!group.activatesReplacementContext) {
+        continue;
+      }
+      llvm::SmallVector<EffectiveDirectivePath, 4> paths;
+      for (const MetadirectiveReplacementBranch &branch : group.branches) {
+        EffectiveDirectivePath path{branch.enclosingPath};
+        const parser::OmpDirectiveSpecification *spec{branch.spec};
+        if (spec) {
+          llvm::omp::Association association{
+              llvm::omp::getDirectiveAssociation(spec->DirId())};
+          bool appliesToAssociatedConstruct{
+              (rootLoop &&
+                  (association == llvm::omp::Association::LoopNest ||
+                      association == llvm::omp::Association::LoopSequence)) ||
+              (isStrictlyStructuredBlock &&
+                  association == llvm::omp::Association::Block)};
+          if (appliesToAssociatedConstruct) {
+            path.insert(path.begin(), spec->DirId());
+          }
+        }
+        paths.push_back(std::move(path));
+      }
+      paths = GetUniqueEffectiveDirectivePaths(std::move(paths));
+      activeMetadirectiveReplacements_.push_back(
+          {dirContext_.size(), std::move(paths)});
+      ++executionPartReplacementCounts_.back();
+    }
+  }
+
   // This is the first construct after the metadirective. It consumes the
   // pending variants, whether or not it is a loop nest.
-  if (!parser::Unwrap<parser::DoConstruct>(x)) {
+  if (!rootLoop) {
     // A non-loop construct follows, so a loop-associated variant has no loop
     // nest to associate with.
     CheckPendingLoopDirectivesWithoutLoop();
@@ -1035,33 +1078,8 @@ void OmpStructureChecker::Enter(const parser::ExecutionPartConstruct &x) {
   // their reachable directives against it.
   std::vector<PendingLoopDirectiveGroup> pending;
   pending.swap(pendingLoopDirectiveGroups_);
-  for (const PendingLoopDirectiveGroup &group : pending) {
-    if (!group.activatesReplacementContext) {
-      continue;
-    }
-    llvm::SmallVector<EffectiveDirectivePath, 4> paths;
-    for (const MetadirectiveReplacementBranch &branch : group.branches) {
-      EffectiveDirectivePath path{branch.enclosingPath};
-      const parser::OmpDirectiveSpecification *spec{branch.spec};
-      if (spec) {
-        llvm::omp::Association association{
-            llvm::omp::getDirectiveAssociation(spec->DirId())};
-        if (association == llvm::omp::Association::LoopNest ||
-            association == llvm::omp::Association::LoopSequence) {
-          path.insert(path.begin(), spec->DirId());
-        }
-      }
-      paths.push_back(std::move(path));
-    }
-    paths = GetUniqueEffectiveDirectivePaths(std::move(paths));
-    activeMetadirectiveReplacements_.push_back(
-        {dirContext_.size(), std::move(paths)});
-    ++executionPartReplacementCounts_.back();
-  }
-
   llvm::omp::Version version{context_.langOptions().getOpenMPVersion()};
   LoopSequence sequence(x, version, /*allowAllLoops=*/true, &context_);
-  const parser::DoConstruct &rootLoop{*parser::Unwrap<parser::DoConstruct>(x)};
   const auto &[haveSemantic, havePerfect]{sequence.depth()};
 
   const auto MsgRequiresCanonical{
@@ -1071,13 +1089,14 @@ void OmpStructureChecker::Enter(const parser::ExecutionPartConstruct &x) {
 
   auto checkRootLoopCanonical =
       [&](const parser::OmpDirectiveSpecification &spec, bool isSequence) {
-        parser::CharBlock source{*parser::GetSource(rootLoop)};
+        parser::CharBlock source{*parser::GetSource(*rootLoop)};
         Reason reason;
-        if (rootLoop.IsDoWhile()) {
+        if (rootLoop->IsDoWhile()) {
           reason.Say(source, MsgNotValidAffectedLoop, "DO WHILE loop");
-        } else if (rootLoop.IsDoConcurrent() && !IsDoConcurrentLegal(version)) {
+        } else if (rootLoop->IsDoConcurrent() &&
+            !IsDoConcurrentLegal(version)) {
           reason.Say(source, MsgNotValidAffectedLoop, "DO CONCURRENT loop");
-        } else if (!rootLoop.GetLoopControl()) {
+        } else if (!rootLoop->GetLoopControl()) {
           reason.Say(
               source, MsgNotValidAffectedLoop, "DO loop without loop control");
         }
@@ -1112,7 +1131,7 @@ void OmpStructureChecker::Enter(const parser::ExecutionPartConstruct &x) {
         // the parse tree, so name resolution cannot apply DEFAULT(NONE) to it.
         if (group.checkDefaultNoneInAssociatedLoop) {
           CheckDefaultNoneInAssociatedLoop(
-              *spec, rootLoop, defaultNoneDiagnosed);
+              *spec, *rootLoop, defaultNoneDiagnosed);
         }
 
         auto [needDepth, needPerfect]{
diff --git a/flang/test/Semantics/OpenMP/metadirective-loop-applicability.f90 b/flang/test/Semantics/OpenMP/metadirective-loop-applicability.f90
index ccddf604708ea..db544e9a6241a 100644
--- a/flang/test/Semantics/OpenMP/metadirective-loop-applicability.f90
+++ b/flang/test/Semantics/OpenMP/metadirective-loop-applicability.f90
@@ -349,3 +349,71 @@ subroutine f25(n, a)
     !$omp end metadirective
   !$omp end parallel
 end subroutine
+
+! MATCH_ANY with construct traits records only the construct traits that match
+! before ranking the reachable variants.
+subroutine f26(n, a)
+  integer :: n, a(n), i
+  !$omp parallel
+    !$omp metadirective &
+    !$omp& when(implementation={vendor(llvm)}: nothing) &
+    !$omp& when(construct={target, parallel}, &
+    !ERROR: This construct requires a nest of depth 2, but the associated nest is a nest of depth 1
+    !BECAUSE: COLLAPSE clause was specified with argument 2
+    !$omp& implementation={extension(match_any)}: simd collapse(2)) &
+    !$omp& default(nothing)
+    do i = 1, n
+      a(i) = i
+    end do
+  !$omp end parallel
+end subroutine
+
+! MATCH_NONE remains applicable when its construct trait is absent, and the
+! first equally ranked candidate is selected.
+subroutine f27(n, a)
+  integer :: n, a(n), i
+  !$omp metadirective &
+  !$omp& when(construct={parallel}, &
+  !ERROR: This construct requires a nest of depth 2, but the associated nest is a nest of depth 1
+  !BECAUSE: COLLAPSE clause was specified with argument 2
+  !$omp& implementation={extension(match_none)}: simd collapse(2)) &
+  !$omp& when(implementation={vendor(llvm)}: nothing) &
+  !$omp& default(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
+
+! A block-associated directive selected by a standalone metadirective remains
+! active throughout its following strictly structured BLOCK.
+subroutine f28(n, a)
+  integer :: n, a(n), i
+  !$omp metadirective &
+  !$omp& when(implementation={vendor(llvm)}: parallel) default(nothing)
+  block
+    !$omp metadirective &
+    !ERROR: This construct requires a nest of depth 2, but the associated nest is a nest of depth 1
+    !BECAUSE: COLLAPSE clause was specified with argument 2
+    !$omp& when(construct={parallel}: simd collapse(2)) default(nothing)
+    do i = 1, n
+      a(i) = i
+    end do
+  end block
+end subroutine
+
+! Precise ranking is required for MATCH_ANY with construct traits. A
+! conservative fallback would retain the lower-ranked invalid SIMD variant and
+! diagnose it even though the higher-scored NOTHING is always selected.
+subroutine f29(n, a)
+  integer :: n, a(n), i
+  !$omp parallel
+    !$omp metadirective &
+    !$omp& when(user={condition(score(100): .true.)}: nothing) &
+    !$omp& when(construct={target, parallel}, &
+    !$omp& implementation={extension(match_any)}: simd collapse(2)) &
+    !$omp& default(nothing)
+    do i = 1, n
+      a(i) = i
+    end do
+  !$omp end parallel
+end subroutine
diff --git a/flang/test/Semantics/OpenMP/metadirective-match-any-paths.f90 b/flang/test/Semantics/OpenMP/metadirective-match-any-paths.f90
new file mode 100644
index 0000000000000..5deeb73736842
--- /dev/null
+++ b/flang/test/Semantics/OpenMP/metadirective-match-any-paths.f90
@@ -0,0 +1,71 @@
+! RUN: not %flang_fc1 -fopenmp -fopenmp-version=51 -fsyntax-only %s 2>&1 | \
+! RUN:   FileCheck %s --implicit-check-not=error:
+
+! CHECK: error: Semantic errors in
+
+! Both paths lack TARGET and contain PARALLEL, but MATCH_ANY scores their
+! first PARALLEL at different positions. PARALLEL -> PARALLEL scores 2 and
+! loses the tie with NOTHING. TEAMS -> PARALLEL scores 3 and selects SIMD.
+! Merging these paths must not discard the reachable COLLAPSE diagnostic.
+subroutine parallel_first(flag, n, a)
+  logical :: flag
+  integer :: n, a(n), i
+  !$omp begin metadirective &
+  !$omp& when(user={condition(flag)}: parallel) default(teams)
+    !$omp parallel
+      !$omp metadirective &
+      !$omp& when(user={condition(score(1): .true.)}: nothing) &
+      !$omp& when(construct={target, parallel}, &
+! CHECK: :[[@LINE+3]]:{{[0-9]+}}: error: This construct requires
+! CHECK-SAME: a nest of depth 2, but the associated nest is a nest of depth 1
+! CHECK: because: COLLAPSE clause was specified with argument 2
+      !$omp& implementation={extension(match_any)}: simd collapse(2)) &
+      !$omp& default(nothing)
+      do i = 1, n
+        a(i) = i
+      end do
+    !$omp end parallel
+  !$omp end metadirective
+end subroutine
+
+! Reversing the path order must leave the same replacement reachable.
+subroutine teams_first(flag, n, a)
+  logical :: flag
+  integer :: n, a(n), i
+  !$omp begin metadirective &
+  !$omp& when(user={condition(flag)}: teams) default(parallel)
+    !$omp parallel
+      !$omp metadirective &
+      !$omp& when(user={condition(score(1): .true.)}: nothing) &
+      !$omp& when(construct={target, parallel}, &
+! CHECK: :[[@LINE+3]]:{{[0-9]+}}: error: This construct requires
+! CHECK-SAME: a nest of depth 2, but the associated nest is a nest of depth 1
+! CHECK: because: COLLAPSE clause was specified with argument 2
+      !$omp& implementation={extension(match_any)}: simd collapse(2)) &
+      !$omp& default(nothing)
+      do i = 1, n
+        a(i) = i
+      end do
+    !$omp end parallel
+  !$omp end metadirective
+end subroutine
+
+! A score-4 NOTHING wins on both paths, so retaining both contexts must not
+! make the lower-scored SIMD replacement reachable.
+subroutine nothing_wins(flag, n, a)
+  logical :: flag
+  integer :: n, a(n), i
+  !$omp begin metadirective &
+  !$omp& when(user={condition(flag)}: parallel) default(teams)
+    !$omp parallel
+      !$omp metadirective &
+      !$omp& when(user={condition(score(3): .true.)}: nothing) &
+      !$omp& when(construct={target, parallel}, &
+      !$omp& implementation={extension(match_any)}: simd collapse(2)) &
+      !$omp& default(nothing)
+      do i = 1, n
+        a(i) = i
+      end do
+    !$omp end parallel
+  !$omp end metadirective
+end subroutine
diff --git a/flang/test/Semantics/OpenMP/metadirective-ordered-paths.f90 b/flang/test/Semantics/OpenMP/metadirective-ordered-paths.f90
new file mode 100644
index 0000000000000..683bc72efff57
--- /dev/null
+++ b/flang/test/Semantics/OpenMP/metadirective-ordered-paths.f90
@@ -0,0 +1,45 @@
+! RUN: not %flang_fc1 -fopenmp -fopenmp-version=51 -fsyntax-only %s 2>&1 | \
+! RUN:   FileCheck %s --implicit-check-not=error:
+
+! CHECK: error: Semantic errors in
+
+! The outer choice and the inner selector produce PARALLEL -> DO first,
+! then DO -> PARALLEL. Both paths have the same length and trait presence,
+! but only the second matches the innermost ordered construct selector.
+! Merging by trait presence alone would discard its COLLAPSE diagnostic.
+subroutine ordered_paths(flag, n, a)
+  logical :: flag
+  integer :: n, a(n, n, n), i, j, k
+  !$omp begin metadirective &
+  !$omp& when(user={condition(flag)}: parallel) default(do)
+  do i = 1, n
+    !$omp begin metadirective &
+    !$omp& when(construct={do}: parallel) default(do)
+    do j = 1, n
+      !$omp metadirective &
+! CHECK: :[[@LINE+3]]:{{[0-9]+}}: error: This construct requires
+! CHECK-SAME: a nest of depth 2, but the associated nest is a nest of depth 1
+! CHECK: because: COLLAPSE clause was specified with argument 2
+      !$omp& when(construct={do, parallel}: simd collapse(2)) default(nothing)
+      do k = 1, n
+        a(k, j, i) = k
+      end do
+    end do
+    !$omp end metadirective
+  end do
+  !$omp end metadirective
+end subroutine
+
+! PARALLEL -> DO alone must not match the reversed selector.
+subroutine reversed_selector(n, a)
+  integer :: n, a(n, n), i, j
+  !$omp parallel do
+  do i = 1, n
+    !$omp metadirective &
+    !$omp& when(construct={do, parallel}: simd collapse(2)) default(nothing)
+    do j = 1, n
+      a(j, i) = j
+    end do
+  end do
+  !$omp end parallel do
+end subroutine

>From 82ae8f823441dcc56ccd4c9bfc5fe19db1389b4c Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Tue, 8 Sep 2026 13:54:11 -0500
Subject: [PATCH 04/11] Remove redundant handling, improve style, and add
 documentation

---
 flang/lib/Semantics/check-omp-structure.h     | 10 +++---
 flang/lib/Semantics/check-omp-variant.cpp     | 34 +++++++------------
 .../metadirective-loop-applicability.f90      | 12 ++++---
 3 files changed, 27 insertions(+), 29 deletions(-)

diff --git a/flang/lib/Semantics/check-omp-structure.h b/flang/lib/Semantics/check-omp-structure.h
index ecc3acea4efa4..b324d5d509502 100644
--- a/flang/lib/Semantics/check-omp-structure.h
+++ b/flang/lib/Semantics/check-omp-structure.h
@@ -413,11 +413,14 @@ class OmpStructureChecker : public OmpStructureCheckerBase {
   bool HasInvalidWorksharingNesting(
       const parser::OmpDirectiveName &name, const llvm::omp::Directives &);
 
+  // Directive paths run from the innermost construct to the outermost.
   using EffectiveDirectivePath = llvm::SmallVector<llvm::omp::Directive, 8>;
+  // Construct traits run from the outermost construct to the innermost.
   using ConstructTraitSequence = llvm::SmallVector<llvm::omp::TraitProperty, 8>;
 
   struct MetadirectiveReplacementBranch {
     EffectiveDirectivePath enclosingPath;
+    // Null represents an explicit or implicit NOTHING replacement.
     const parser::OmpDirectiveSpecification *spec{nullptr};
   };
   struct MetadirectiveReplacementContext {
@@ -603,15 +606,14 @@ class OmpStructureChecker : public OmpStructureCheckerBase {
 
   struct PendingLoopDirectiveGroup {
     llvm::SmallVector<MetadirectiveReplacementBranch, 4> branches;
-    bool activatesReplacementContext{false};
-    bool checkDefaultNoneInAssociatedLoop{false};
+    bool isStandaloneMetadirective{false};
   };
   std::vector<PendingLoopDirectiveGroup> pendingLoopDirectiveGroups_;
   std::vector<std::size_t> pendingLoopDirectiveScopeStarts_;
   std::vector<ConstructTraitSequence> metadirectiveConstructSelectors_;
-  std::vector<bool> directiveSpecificationReachability_;
+  llvm::SmallVector<bool> directiveSpecificationReachability_;
   std::vector<MetadirectiveReplacementContext> activeMetadirectiveReplacements_;
-  std::vector<std::size_t> executionPartReplacementCounts_;
+  std::vector<std::size_t> executionPartReplacementDepths_;
 
   std::multimap<const parser::Label,
       std::pair<parser::CharBlock, const parser::OpenMPConstruct *>>
diff --git a/flang/lib/Semantics/check-omp-variant.cpp b/flang/lib/Semantics/check-omp-variant.cpp
index 6f976f78a6f5b..fa6ac74963271 100644
--- a/flang/lib/Semantics/check-omp-variant.cpp
+++ b/flang/lib/Semantics/check-omp-variant.cpp
@@ -840,7 +840,6 @@ OmpStructureChecker::GetReachableMetadirectiveReplacements(
   llvm::SmallVector<MetadirectiveReplacementBranch, 4> result;
 
   for (const EffectiveDirectivePath &path : GetEnclosingDirectivePaths()) {
-    std::size_t firstBranch{result.size()};
     ConstructTraitSequence constructTraits{GetConstructTraitsForPath(path)};
     OmpVariantMatchContext matchContext{context_, constructTraits};
     if (auto candidateSet{
@@ -848,14 +847,8 @@ OmpStructureChecker::GetReachableMetadirectiveReplacements(
       for (const parser::OmpDirectiveSpecification *spec :
           GetReachableMetadirectiveVariants(
               *candidateSet, matchContext, context_)) {
-        if (spec && spec->DirId() == llvm::omp::Directive::OMPD_nothing) {
-          spec = nullptr;
-        }
         result.push_back({path, spec});
       }
-      if (result.size() == firstBranch) {
-        result.push_back({path, nullptr});
-      }
       continue;
     }
 
@@ -948,8 +941,7 @@ void OmpStructureChecker::Enter(const parser::OmpDirectiveSpecification &x) {
 
   if (GetDirectiveNest(ApplyNest) && reachable &&
       dirId != llvm::omp::Directive::OMPD_metadirective) {
-    pendingLoopDirectiveGroups_.push_back(
-        {{{EffectiveDirectivePath{}, &x}}, false, false});
+    pendingLoopDirectiveGroups_.push_back({{{EffectiveDirectivePath{}, &x}}});
   }
 
   if (dirId != llvm::omp::Directive::OMPD_metadirective) {
@@ -982,7 +974,8 @@ void OmpStructureChecker::Leave(const parser::OmpDirectiveSpecification &x) {
 
 void OmpStructureChecker::Enter(const parser::OmpMetadirectiveDirective &x) {
   auto branches{GetReachableMetadirectiveReplacements(x.v.Clauses())};
-  pendingLoopDirectiveGroups_.push_back({std::move(branches), true, true});
+  pendingLoopDirectiveGroups_.push_back(
+      {std::move(branches), /*isStandaloneMetadirective=*/true});
   EnterDirectiveNest(MetadirectiveNest);
 }
 
@@ -1004,7 +997,7 @@ void OmpStructureChecker::Enter(
   paths = GetUniqueEffectiveDirectivePaths(std::move(paths));
   activeMetadirectiveReplacements_.push_back(
       {dirContext_.size(), std::move(paths)});
-  pendingLoopDirectiveGroups_.push_back({std::move(branches), false, false});
+  pendingLoopDirectiveGroups_.push_back({std::move(branches)});
 }
 
 void OmpStructureChecker::Leave(
@@ -1018,7 +1011,8 @@ void OmpStructureChecker::Leave(
 // the next executable construct, either a following sibling or the first
 // execution-part construct for a declarative metadirective.
 void OmpStructureChecker::Enter(const parser::ExecutionPartConstruct &x) {
-  executionPartReplacementCounts_.push_back(0);
+  executionPartReplacementDepths_.push_back(
+      activeMetadirectiveReplacements_.size());
   if (pendingLoopDirectiveGroups_.empty()) {
     return;
   }
@@ -1036,7 +1030,7 @@ void OmpStructureChecker::Enter(const parser::ExecutionPartConstruct &x) {
   // their DO construct, so nested construct selectors see the selected path.
   if (rootLoop || isStrictlyStructuredBlock) {
     for (const PendingLoopDirectiveGroup &group : pendingLoopDirectiveGroups_) {
-      if (!group.activatesReplacementContext) {
+      if (!group.isStandaloneMetadirective) {
         continue;
       }
       llvm::SmallVector<EffectiveDirectivePath, 4> paths;
@@ -1061,7 +1055,6 @@ void OmpStructureChecker::Enter(const parser::ExecutionPartConstruct &x) {
       paths = GetUniqueEffectiveDirectivePaths(std::move(paths));
       activeMetadirectiveReplacements_.push_back(
           {dirContext_.size(), std::move(paths)});
-      ++executionPartReplacementCounts_.back();
     }
   }
 
@@ -1129,7 +1122,7 @@ void OmpStructureChecker::Enter(const parser::ExecutionPartConstruct &x) {
 
         // A standalone metadirective does not contain its associated loop in
         // the parse tree, so name resolution cannot apply DEFAULT(NONE) to it.
-        if (group.checkDefaultNoneInAssociatedLoop) {
+        if (group.isStandaloneMetadirective) {
           CheckDefaultNoneInAssociatedLoop(
               *spec, *rootLoop, defaultNoneDiagnosed);
         }
@@ -1161,12 +1154,11 @@ void OmpStructureChecker::Enter(const parser::ExecutionPartConstruct &x) {
 }
 
 void OmpStructureChecker::Leave(const parser::ExecutionPartConstruct &) {
-  CHECK(!executionPartReplacementCounts_.empty());
-  std::size_t count{executionPartReplacementCounts_.back()};
-  executionPartReplacementCounts_.pop_back();
-  CHECK(count <= activeMetadirectiveReplacements_.size());
-  activeMetadirectiveReplacements_.resize(
-      activeMetadirectiveReplacements_.size() - count);
+  CHECK(!executionPartReplacementDepths_.empty());
+  std::size_t depth{executionPartReplacementDepths_.back()};
+  executionPartReplacementDepths_.pop_back();
+  CHECK(depth <= activeMetadirectiveReplacements_.size());
+  activeMetadirectiveReplacements_.resize(depth);
 }
 
 // Diagnose reachable loop-associated directives that are not followed by a
diff --git a/flang/test/Semantics/OpenMP/metadirective-loop-applicability.f90 b/flang/test/Semantics/OpenMP/metadirective-loop-applicability.f90
index db544e9a6241a..169033fcfa9c4 100644
--- a/flang/test/Semantics/OpenMP/metadirective-loop-applicability.f90
+++ b/flang/test/Semantics/OpenMP/metadirective-loop-applicability.f90
@@ -186,7 +186,8 @@ subroutine f15(n, a)
     do i = 1, n
       !ERROR: This construct requires a nest of depth 2, but the associated nest is a nest of depth 1
       !BECAUSE: COLLAPSE clause was specified with argument 2
-      !$omp metadirective when(construct={target, parallel}: simd collapse(2)) default(nothing)
+      !$omp metadirective when(construct={target, parallel}: simd collapse(2)) &
+      !$omp& default(nothing)
       do j = 1, n
         a(j, i) = i
       end do
@@ -212,7 +213,8 @@ subroutine f16(flag, n, a)
 
     !ERROR: This construct requires a nest of depth 2, but the associated nest is a nest of depth 1
     !BECAUSE: COLLAPSE clause was specified with argument 2
-    !$omp metadirective when(construct={target}: simd collapse(2)) default(nothing)
+    !$omp metadirective when(construct={target}: simd collapse(2)) &
+    !$omp& default(nothing)
     do i = 1, n
       a(i) = i
     end do
@@ -227,7 +229,8 @@ subroutine f17(flag, n, a)
   !$omp& when(user={condition(flag)}: target) default(parallel)
     !ERROR: This construct requires a nest of depth 2, but the associated nest is a nest of depth 1
     !BECAUSE: COLLAPSE clause was specified with argument 2
-    !$omp metadirective when(construct={parallel}: simd collapse(2)) default(nothing)
+    !$omp metadirective when(construct={parallel}: simd collapse(2)) &
+    !$omp& default(nothing)
     do i = 1, n
       a(i) = i
     end do
@@ -258,7 +261,8 @@ subroutine f19(n, a)
     a(1) = 1
   !$omp end metadirective
 
-  !$omp metadirective when(construct={parallel}: simd collapse(2)) default(nothing)
+  !$omp metadirective when(construct={parallel}: simd collapse(2)) &
+  !$omp& default(nothing)
   do i = 1, n
     a(i) = i
   end do

>From 7b27b057a482bbce2b1060bff02853dc2a4972ca Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Wed, 9 Sep 2026 12:58:48 -0500
Subject: [PATCH 05/11] Preserve metadirective selection semantics

Reachability checks can miss required loop diagnostics when selection
drops SIMD traits or treats NOTHING with APPLY as a no-op.

Preserve those distinctions in the shared selection plan so semantic
checks retain reachable transformations and lowering diagnoses
unsupported cases.
---
 flang/include/flang/Semantics/openmp-utils.h  |   6 +-
 flang/lib/Lower/OpenMP/OpenMP.cpp             |   2 +
 flang/lib/Semantics/check-omp-structure.h     |   2 +-
 flang/lib/Semantics/check-omp-variant.cpp     |  12 +-
 flang/lib/Semantics/openmp-utils.cpp          |  49 +++--
 .../Todo/metadirective-nothing-apply.f90      |  23 +++
 .../Lower/OpenMP/metadirective-construct.f90  |  11 ++
 .../OpenMP/metadirective-construct-paths.f90  | 185 ++++++++++++++++++
 ...metadirective-loop-applicability-apply.f90 |  23 ---
 ...directive-loop-applicability-openmp-60.f90 | 111 ++++++++++-
 .../OpenMP/metadirective-match-any-paths.f90  |  71 -------
 .../OpenMP/metadirective-ordered-paths.f90    |  45 -----
 12 files changed, 366 insertions(+), 174 deletions(-)
 create mode 100644 flang/test/Lower/OpenMP/Todo/metadirective-nothing-apply.f90
 create mode 100644 flang/test/Semantics/OpenMP/metadirective-construct-paths.f90
 delete mode 100644 flang/test/Semantics/OpenMP/metadirective-loop-applicability-apply.f90
 delete mode 100644 flang/test/Semantics/OpenMP/metadirective-match-any-paths.f90
 delete mode 100644 flang/test/Semantics/OpenMP/metadirective-ordered-paths.f90

diff --git a/flang/include/flang/Semantics/openmp-utils.h b/flang/include/flang/Semantics/openmp-utils.h
index f14820b1bda5f..3f0b95c40d9b9 100644
--- a/flang/include/flang/Semantics/openmp-utils.h
+++ b/flang/include/flang/Semantics/openmp-utils.h
@@ -246,6 +246,10 @@ class OmpVariantMatchContext : public llvm::omp::OMPContext {
 void AppendConstructTraitsForDirective(
     llvm::omp::Directive, llvm::omp::VariantMatchInfo &vmi);
 
+/// Add the traits implied by a construct selector without properties.
+void AppendConstructTraitsForSelector(
+    const parser::OmpTraitSelectorName &, llvm::omp::VariantMatchInfo &vmi);
+
 struct MetadirectiveCandidate {
   MetadirectiveCandidate(const parser::OmpDirectiveSpecification *spec,
       llvm::omp::VariantMatchInfo vmi, bool isExplicit,
@@ -264,7 +268,7 @@ struct MetadirectiveCandidate {
 
 struct MetadirectiveCandidateSet {
   llvm::SmallVector<MetadirectiveCandidate, 4> candidates;
-  /// Null represents either an explicit NOTHING fallback or no fallback.
+  /// Null represents either a clause-free NOTHING fallback or no fallback.
   const parser::OmpDirectiveSpecification *fallback{nullptr};
 };
 
diff --git a/flang/lib/Lower/OpenMP/OpenMP.cpp b/flang/lib/Lower/OpenMP/OpenMP.cpp
index df14d13b76a4b..fd45617d93d6a 100644
--- a/flang/lib/Lower/OpenMP/OpenMP.cpp
+++ b/flang/lib/Lower/OpenMP/OpenMP.cpp
@@ -7647,6 +7647,8 @@ static void genMetadirective(lower::AbstractConverter &converter,
       return;
     }
     mlir::Location variantLoc = converter.genLocation(spec->source);
+    if (spec->DirId() == llvm::omp::Directive::OMPD_nothing)
+      TODO(variantLoc, "NOTHING with APPLY in METADIRECTIVE");
     List<Clause> variantClauses = makeClauses(spec->Clauses(), semaCtx);
     ConstructQueue queue{
         buildConstructQueue(converter.getFirOpBuilder().getModule(), semaCtx,
diff --git a/flang/lib/Semantics/check-omp-structure.h b/flang/lib/Semantics/check-omp-structure.h
index b324d5d509502..ab5971085187d 100644
--- a/flang/lib/Semantics/check-omp-structure.h
+++ b/flang/lib/Semantics/check-omp-structure.h
@@ -420,7 +420,7 @@ class OmpStructureChecker : public OmpStructureCheckerBase {
 
   struct MetadirectiveReplacementBranch {
     EffectiveDirectivePath enclosingPath;
-    // Null represents an explicit or implicit NOTHING replacement.
+    // Null represents a clause-free explicit or implicit NOTHING replacement.
     const parser::OmpDirectiveSpecification *spec{nullptr};
   };
   struct MetadirectiveReplacementContext {
diff --git a/flang/lib/Semantics/check-omp-variant.cpp b/flang/lib/Semantics/check-omp-variant.cpp
index fa6ac74963271..d8b83dae4e1f1 100644
--- a/flang/lib/Semantics/check-omp-variant.cpp
+++ b/flang/lib/Semantics/check-omp-variant.cpp
@@ -89,12 +89,9 @@ class MetadirectiveConstructSelectorCollector {
           return false;
         }
         const auto &name{std::get<parser::OmpTraitSelectorName>(selector.t)};
-        if (const auto *directive{std::get_if<llvm::omp::Directive>(&name.u)}) {
-          llvm::omp::VariantMatchInfo vmi;
-          AppendConstructTraitsForDirective(*directive, vmi);
-          sequence.append(
-              vmi.ConstructTraits.begin(), vmi.ConstructTraits.end());
-        }
+        llvm::omp::VariantMatchInfo vmi;
+        AppendConstructTraitsForSelector(name, vmi);
+        sequence.append(vmi.ConstructTraits.begin(), vmi.ConstructTraits.end());
       }
     }
 
@@ -876,7 +873,8 @@ OmpStructureChecker::GetReachableMetadirectiveReplacements(
       } else {
         continue;
       }
-      if (spec && spec->DirId() == llvm::omp::Directive::OMPD_nothing) {
+      if (spec && spec->DirId() == llvm::omp::Directive::OMPD_nothing &&
+          spec->Clauses().v.empty()) {
         spec = nullptr;
       }
       result.push_back({path, spec});
diff --git a/flang/lib/Semantics/openmp-utils.cpp b/flang/lib/Semantics/openmp-utils.cpp
index 09697da1c5b7f..115d0071ce6eb 100644
--- a/flang/lib/Semantics/openmp-utils.cpp
+++ b/flang/lib/Semantics/openmp-utils.cpp
@@ -2452,6 +2452,20 @@ void AppendConstructTraitsForDirective(
     add(llvm::omp::TraitProperty::construct_dispatch_dispatch);
 }
 
+void AppendConstructTraitsForSelector(const parser::OmpTraitSelectorName &name,
+    llvm::omp::VariantMatchInfo &vmi) {
+  if (const auto *dir{std::get_if<llvm::omp::Directive>(&name.u)}) {
+    AppendConstructTraitsForDirective(*dir, vmi);
+  } else if (const auto *value{
+                 std::get_if<parser::OmpTraitSelectorName::Value>(&name.u)}) {
+    // SIMD is parsed as a predefined selector name because it can also take
+    // clause properties, unlike the other construct selectors.
+    if (*value == parser::OmpTraitSelectorName::Value::Simd) {
+      AppendConstructTraitsForDirective(llvm::omp::Directive::OMPD_simd, vmi);
+    }
+  }
+}
+
 static void AddTraitPropertiesFromSelector(llvm::omp::TraitSet set,
     const parser::OmpTraitSelector &selector, llvm::omp::VariantMatchInfo &vmi,
     SemanticsContext &semaCtx,
@@ -2503,16 +2517,7 @@ static void AddTraitPropertiesFromSelector(llvm::omp::TraitSet set,
 
   // Construct trait selector with no properties (e.g. `construct={simd}`):
   // the selector itself implies the property.
-  if (const auto *dir{std::get_if<llvm::omp::Directive>(&traitName.u)}) {
-    AppendConstructTraitsForDirective(*dir, vmi);
-  } else if (const auto *value{std::get_if<parser::OmpTraitSelectorName::Value>(
-                 &traitName.u)}) {
-    // SIMD is a predefined selector name because it can take clause
-    // properties, unlike the other construct selectors.
-    if (*value == parser::OmpTraitSelectorName::Value::Simd) {
-      AppendConstructTraitsForDirective(llvm::omp::Directive::OMPD_simd, vmi);
-    }
-  }
+  AppendConstructTraitsForSelector(traitName, vmi);
 }
 
 std::optional<DynamicUserCondition> MakeVariantMatchInfo(
@@ -2550,20 +2555,22 @@ std::optional<MetadirectiveCandidateSet> BuildMetadirectiveCandidateSet(
         &modifiers->front().u);
   };
 
-  auto getDirectiveVariant = [](const parser::OmpClause::When &whenClause)
+  auto getVariant = [](const parser::OmpDirectiveSpecification &spec) {
+    // NOTHING with APPLY is a loop transformation, not a no-op. Preserve its
+    // specification so semantic checks can visit the reachable APPLY items.
+    return spec.DirId() == llvm::omp::Directive::OMPD_nothing &&
+            spec.Clauses().v.empty()
+        ? nullptr
+        : &spec;
+  };
+
+  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;
+    return {getVariant(optionalSpec->value()), true};
   };
 
   for (const parser::OmpClause &clause : clauses.v) {
@@ -2669,11 +2676,11 @@ std::optional<MetadirectiveCandidateSet> BuildMetadirectiveCandidateSet(
     } 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());
+        result.fallback = getVariant(otherwiseClause->v->v->value());
       }
     } else if (const auto *defaultVariantClause{
                    std::get_if<parser::OmpClause::DefaultVariant>(&clause.u)}) {
-      result.fallback = getFallbackVariant(defaultVariantClause->v.v.value());
+      result.fallback = getVariant(defaultVariantClause->v.v.value());
     }
   }
   return result;
diff --git a/flang/test/Lower/OpenMP/Todo/metadirective-nothing-apply.f90 b/flang/test/Lower/OpenMP/Todo/metadirective-nothing-apply.f90
new file mode 100644
index 0000000000000..a120d340d3e11
--- /dev/null
+++ b/flang/test/Lower/OpenMP/Todo/metadirective-nothing-apply.f90
@@ -0,0 +1,23 @@
+! RUN: %not_todo_cmd %flang_fc1 -cpp -emit-hlfir -fopenmp \
+! RUN:   -fopenmp-version=60 -o - %s 2>&1 | FileCheck %s
+! RUN: %not_todo_cmd %flang_fc1 -cpp -DFALLBACK -emit-hlfir -fopenmp \
+! RUN:   -fopenmp-version=60 -o - %s 2>&1 | FileCheck %s
+
+! NOTHING with APPLY must not be silently lowered as a no-op, whether it is
+! selected by a WHEN clause or as the fallback.
+! CHECK: not yet implemented: NOTHING with APPLY in METADIRECTIVE
+
+subroutine nothing_apply(n, a)
+  integer :: n, a(n), i
+#ifdef FALLBACK
+  !$omp metadirective when(user={condition(.false.)}: nothing) &
+  !$omp& otherwise(nothing apply(reverse))
+#else
+  !$omp metadirective &
+  !$omp& when(user={condition(.true.)}: nothing apply(reverse)) &
+  !$omp& otherwise(nothing)
+#endif
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
diff --git a/flang/test/Lower/OpenMP/metadirective-construct.f90 b/flang/test/Lower/OpenMP/metadirective-construct.f90
index c6b5e8188278a..339bda7009d4e 100644
--- a/flang/test/Lower/OpenMP/metadirective-construct.f90
+++ b/flang/test/Lower/OpenMP/metadirective-construct.f90
@@ -2,6 +2,17 @@
 
 ! RUN: %flang_fc1 -fopenmp -emit-hlfir -fopenmp-version=50 %s -o - | FileCheck %s
 
+! A bare SIMD selector is not an empty selector since it needs an enclosing
+! SIMD.
+! CHECK-LABEL: func.func @_QPtest_construct_simd_absent()
+! CHECK-NOT:     omp.barrier
+! CHECK:         omp.taskyield
+! CHECK-NOT:     omp.barrier
+! CHECK:         return
+subroutine test_construct_simd_absent()
+  !$omp metadirective when(construct={simd}: barrier) default(taskyield)
+end subroutine
+
 ! CHECK-LABEL: func.func @_QPtest_construct_parallel()
 ! CHECK:         omp.parallel
 ! CHECK:           omp.barrier
diff --git a/flang/test/Semantics/OpenMP/metadirective-construct-paths.f90 b/flang/test/Semantics/OpenMP/metadirective-construct-paths.f90
new file mode 100644
index 0000000000000..f6de4e54cd9d7
--- /dev/null
+++ b/flang/test/Semantics/OpenMP/metadirective-construct-paths.f90
@@ -0,0 +1,185 @@
+! RUN: not %flang_fc1 -fopenmp -fopenmp-version=51 -fsyntax-only %s 2>&1 | \
+! RUN:   FileCheck %s --implicit-check-not=error:
+
+! CHECK: error: Semantic errors in
+
+! The outer choice and the inner selector produce PARALLEL -> DO first,
+! then DO -> PARALLEL. Both paths have the same length and trait presence,
+! but only the second matches the innermost ordered construct selector.
+! Merging by trait presence alone would discard its COLLAPSE diagnostic.
+subroutine ordered_paths(flag, n, a)
+  logical :: flag
+  integer :: n, a(n, n, n), i, j, k
+  !$omp begin metadirective &
+  !$omp& when(user={condition(flag)}: parallel) default(do)
+  do i = 1, n
+    !$omp begin metadirective &
+    !$omp& when(construct={do}: parallel) default(do)
+    do j = 1, n
+      !$omp metadirective &
+! CHECK: :[[@LINE+3]]:{{[0-9]+}}: error: This construct requires
+! CHECK-SAME: a nest of depth 2, but the associated nest is a nest of depth 1
+! CHECK: because: COLLAPSE clause was specified with argument 2
+      !$omp& when(construct={do, parallel}: simd collapse(2)) default(nothing)
+      do k = 1, n
+        a(k, j, i) = k
+      end do
+    end do
+    !$omp end metadirective
+  end do
+  !$omp end metadirective
+end subroutine
+
+! PARALLEL -> DO alone must not match the reversed selector.
+subroutine reversed_selector(n, a)
+  integer :: n, a(n, n), i, j
+  !$omp parallel do
+  do i = 1, n
+    !$omp metadirective &
+    !$omp& when(construct={do, parallel}: simd collapse(2)) default(nothing)
+    do j = 1, n
+      a(j, i) = j
+    end do
+  end do
+  !$omp end parallel do
+end subroutine
+
+! Both paths lack TARGET and contain PARALLEL, but MATCH_ANY scores their
+! first PARALLEL at different positions. PARALLEL -> PARALLEL scores 2 and
+! loses the tie with NOTHING. TEAMS -> PARALLEL scores 3 and selects SIMD.
+! Merging these paths must not discard the reachable COLLAPSE diagnostic.
+subroutine parallel_first(flag, n, a)
+  logical :: flag
+  integer :: n, a(n), i
+  !$omp begin metadirective &
+  !$omp& when(user={condition(flag)}: parallel) default(teams)
+    !$omp parallel
+      !$omp metadirective &
+      !$omp& when(user={condition(score(1): .true.)}: nothing) &
+      !$omp& when(construct={target, parallel}, &
+! CHECK: :[[@LINE+3]]:{{[0-9]+}}: error: This construct requires
+! CHECK-SAME: a nest of depth 2, but the associated nest is a nest of depth 1
+! CHECK: because: COLLAPSE clause was specified with argument 2
+      !$omp& implementation={extension(match_any)}: simd collapse(2)) &
+      !$omp& default(nothing)
+      do i = 1, n
+        a(i) = i
+      end do
+    !$omp end parallel
+  !$omp end metadirective
+end subroutine
+
+! Reversing the path order must leave the same replacement reachable.
+subroutine teams_first(flag, n, a)
+  logical :: flag
+  integer :: n, a(n), i
+  !$omp begin metadirective &
+  !$omp& when(user={condition(flag)}: teams) default(parallel)
+    !$omp parallel
+      !$omp metadirective &
+      !$omp& when(user={condition(score(1): .true.)}: nothing) &
+      !$omp& when(construct={target, parallel}, &
+! CHECK: :[[@LINE+3]]:{{[0-9]+}}: error: This construct requires
+! CHECK-SAME: a nest of depth 2, but the associated nest is a nest of depth 1
+! CHECK: because: COLLAPSE clause was specified with argument 2
+      !$omp& implementation={extension(match_any)}: simd collapse(2)) &
+      !$omp& default(nothing)
+      do i = 1, n
+        a(i) = i
+      end do
+    !$omp end parallel
+  !$omp end metadirective
+end subroutine
+
+! A score-4 NOTHING wins on both paths, so retaining both contexts must not
+! make the lower-scored SIMD replacement reachable.
+subroutine nothing_wins(flag, n, a)
+  logical :: flag
+  integer :: n, a(n), i
+  !$omp begin metadirective &
+  !$omp& when(user={condition(flag)}: parallel) default(teams)
+    !$omp parallel
+      !$omp metadirective &
+      !$omp& when(user={condition(score(3): .true.)}: nothing) &
+      !$omp& when(construct={target, parallel}, &
+      !$omp& implementation={extension(match_any)}: simd collapse(2)) &
+      !$omp& default(nothing)
+      do i = 1, n
+        a(i) = i
+      end do
+    !$omp end parallel
+  !$omp end metadirective
+end subroutine
+
+! A bare SIMD selector must not match an empty construct context.
+subroutine absent_simd(n, a)
+  integer :: n, a(n), i
+  !$omp metadirective when(construct={simd}: nothing) &
+! CHECK: :[[@LINE+3]]:{{[0-9]+}}: error: This construct requires
+! CHECK-SAME: a nest of depth 2, but the associated nest is a nest of depth 1
+! CHECK: because: COLLAPSE clause was specified with argument 2
+  !$omp& default(simd collapse(2))
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
+
+! The same absent trait must keep an invalid WHEN replacement unreachable.
+subroutine unreachable_simd(n, a)
+  integer :: n, a(n), i
+  !$omp metadirective &
+  !$omp& when(construct={simd}: simd collapse(2)) default(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
+
+! A selected SIMD replacement supplies the trait to its associated loop.
+subroutine present_simd(n, a)
+  integer :: n, a(n, n), i, j
+  !$omp metadirective default(simd)
+  do i = 1, n
+    !$omp metadirective when(construct={simd}: nothing) &
+    !$omp& default(simd collapse(2))
+    do j = 1, n
+      a(i, j) = i + j
+    end do
+  end do
+end subroutine
+
+! DO and SIMD paths must remain distinct even when SIMD is the only selector
+! in this program unit. Keeping only the first path would lose the error.
+subroutine do_first(flag, n, a)
+  logical :: flag
+  integer :: n, a(n, n), i, j
+  !$omp metadirective &
+  !$omp& when(user={condition(flag)}: do) default(simd)
+  do i = 1, n
+    !$omp metadirective &
+! CHECK: :[[@LINE+3]]:{{[0-9]+}}: error: This construct requires
+! CHECK-SAME: a nest of depth 2, but the associated nest is a nest of depth 1
+! CHECK: because: COLLAPSE clause was specified with argument 2
+    !$omp& when(construct={simd}: simd collapse(2)) default(nothing)
+    do j = 1, n
+      a(i, j) = i + j
+    end do
+  end do
+end subroutine
+
+! Reversing the alternatives must also retain the path without SIMD.
+subroutine simd_first(flag, n, a)
+  logical :: flag
+  integer :: n, a(n, n), i, j
+  !$omp metadirective &
+  !$omp& when(user={condition(flag)}: simd) default(do)
+  do i = 1, n
+    !$omp metadirective when(construct={simd}: nothing) &
+! CHECK: :[[@LINE+3]]:{{[0-9]+}}: error: This construct requires
+! CHECK-SAME: a nest of depth 2, but the associated nest is a nest of depth 1
+! CHECK: because: COLLAPSE clause was specified with argument 2
+    !$omp& default(simd collapse(2))
+    do j = 1, n
+      a(i, j) = i + j
+    end do
+  end do
+end subroutine
diff --git a/flang/test/Semantics/OpenMP/metadirective-loop-applicability-apply.f90 b/flang/test/Semantics/OpenMP/metadirective-loop-applicability-apply.f90
deleted file mode 100644
index 15b01f4375e16..0000000000000
--- a/flang/test/Semantics/OpenMP/metadirective-loop-applicability-apply.f90
+++ /dev/null
@@ -1,23 +0,0 @@
-!RUN: %python %S/../test_errors.py %s %flang -fopenmp -fopenmp-version=60
-
-! A loop transformation in APPLY inherits the reachability of the
-! metadirective replacement that contains it.
-
-subroutine f01()
-  !$omp metadirective &
-  !$omp& when(user={condition(score(10): .true.)}: nothing) &
-  !$omp& when(user={condition(score(5): .true.)}: &
-  !$omp& tile sizes(2) apply(grid: unroll)) &
-  !$omp& otherwise(nothing)
-end subroutine
-
-subroutine f02(flag)
-  logical :: flag
-  !$omp metadirective &
-  !$omp& when(user={condition(score(10): flag)}: nothing) &
-  !$omp& when(user={condition(score(5): .true.)}: &
-  !ERROR: This construct should contain a DO-loop or a loop-nest-generating construct
-  !ERROR: This construct should contain a DO-loop or a loop-nest-generating construct
-  !$omp& tile sizes(2) apply(grid: unroll)) &
-  !$omp& otherwise(nothing)
-end subroutine
diff --git a/flang/test/Semantics/OpenMP/metadirective-loop-applicability-openmp-60.f90 b/flang/test/Semantics/OpenMP/metadirective-loop-applicability-openmp-60.f90
index 0b6efa0617e35..06d6f0bcc0ba7 100644
--- a/flang/test/Semantics/OpenMP/metadirective-loop-applicability-openmp-60.f90
+++ b/flang/test/Semantics/OpenMP/metadirective-loop-applicability-openmp-60.f90
@@ -1,11 +1,15 @@
-!RUN: %python %S/../test_errors.py %s %flang -fopenmp -fopenmp-version=60
+! RUN: not %flang_fc1 -fopenmp -fopenmp-version=60 -fsyntax-only %s 2>&1 | \
+! RUN:   FileCheck %s --implicit-check-not=error:
+
+! CHECK: error: Semantic errors in
 
 ! An unsupported selector conservatively retains its OTHERWISE replacement.
 subroutine f01(n, a)
   integer :: n, a(n), i
   !$omp metadirective &
-  !ERROR: This construct requires a nest of depth 2, but the associated nest is a nest of depth 1
-  !BECAUSE: COLLAPSE clause was specified with argument 2
+! CHECK: :[[@LINE+3]]:{{[0-9]+}}: error: This construct requires
+! CHECK-SAME: a nest of depth 2, but the associated nest is a nest of depth 1
+! CHECK: because: COLLAPSE clause was specified with argument 2
   !$omp& when(target_device={kind(host)}: nothing) otherwise(do collapse(2))
   do i = 1, n
     a(i) = i
@@ -19,11 +23,108 @@ subroutine f02(n, a)
   !$omp& when(construct={simd(simdlen(8))}: parallel)
     !$omp metadirective &
     !$omp& when(construct={parallel}: nothing) &
-    !ERROR: This construct requires a nest of depth 2, but the associated nest is a nest of depth 1
-    !BECAUSE: COLLAPSE clause was specified with argument 2
+! CHECK: :[[@LINE+3]]:{{[0-9]+}}: error: This construct requires
+! CHECK-SAME: a nest of depth 2, but the associated nest is a nest of depth 1
+! CHECK: because: COLLAPSE clause was specified with argument 2
     !$omp& otherwise(do collapse(2))
     do i = 1, n
       a(i) = i
     end do
   !$omp end metadirective
 end subroutine
+
+! A loop transformation in APPLY inherits the reachability of the
+! metadirective replacement that contains it.
+subroutine unreachable_transformation()
+  !$omp metadirective &
+  !$omp& when(user={condition(score(10): .true.)}: nothing) &
+  !$omp& when(user={condition(score(5): .true.)}: &
+  !$omp& tile sizes(2) apply(grid: unroll)) &
+  !$omp& otherwise(nothing)
+end subroutine
+
+subroutine reachable_transformation(flag)
+  logical :: flag
+  !$omp metadirective &
+  !$omp& when(user={condition(score(10): flag)}: nothing) &
+  !$omp& when(user={condition(score(5): .true.)}: &
+! CHECK: :[[@LINE+4]]:{{[0-9]+}}: error: This construct should contain
+! CHECK-SAME: a DO-loop or a loop-nest-generating construct
+! CHECK: :[[@LINE+2]]:{{[0-9]+}}: error: This construct should contain
+! CHECK-SAME: a DO-loop or a loop-nest-generating construct
+  !$omp& tile sizes(2) apply(grid: unroll)) &
+  !$omp& otherwise(nothing)
+end subroutine
+
+! NOTHING with APPLY must retain its specification when selected.
+subroutine selected_apply()
+  !$omp metadirective &
+! CHECK: :[[@LINE+2]]:{{[0-9]+}}: error: This construct should contain
+! CHECK-SAME: a DO-loop or a loop-nest-generating construct
+  !$omp& when(user={condition(.true.)}: nothing apply(reverse)) &
+  !$omp& otherwise(nothing)
+end subroutine
+
+! A selected fallback must also retain its APPLY specifications.
+subroutine fallback_apply()
+  !$omp metadirective when(user={condition(.false.)}: nothing) &
+! CHECK: :[[@LINE+2]]:{{[0-9]+}}: error: This construct should contain
+! CHECK-SAME: a DO-loop or a loop-nest-generating construct
+  !$omp& otherwise(nothing apply(reverse))
+end subroutine
+
+! A dynamic condition leaves the APPLY specification reachable.
+subroutine dynamic_apply(flag)
+  logical :: flag
+  !$omp metadirective &
+! CHECK: :[[@LINE+2]]:{{[0-9]+}}: error: This construct should contain
+! CHECK-SAME: a DO-loop or a loop-nest-generating construct
+  !$omp& when(user={condition(flag)}: nothing apply(reverse)) &
+  !$omp& otherwise(nothing)
+end subroutine
+
+! Unsupported-selector recovery must preserve NOTHING with APPLY as well.
+subroutine unsupported_apply()
+  !$omp metadirective when(target_device={kind(host)}: nothing) &
+! CHECK: :[[@LINE+2]]:{{[0-9]+}}: error: This construct should contain
+! CHECK-SAME: a DO-loop or a loop-nest-generating construct
+  !$omp& otherwise(nothing apply(reverse))
+end subroutine
+
+! The applied transformation must be checked against an associated loop.
+subroutine insufficient_depth(n, a)
+  integer :: n, a(n), i
+  !$omp metadirective &
+! CHECK: :[[@LINE+3]]:{{[0-9]+}}: error: This construct requires
+! CHECK-SAME: a perfect nest of depth 2
+! CHECK-SAME: but the associated nest is a perfect nest of depth 1
+  !$omp& otherwise(nothing apply(tile sizes(2, 2)))
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
+
+! Statically false and lower-ranked replacements must remain unchecked.
+subroutine unreachable_apply()
+  !$omp metadirective &
+  !$omp& when(user={condition(.false.)}: nothing apply(reverse)) &
+  !$omp& otherwise(nothing)
+end subroutine
+
+subroutine lower_ranked_apply()
+  !$omp metadirective &
+  !$omp& when(user={condition(score(1): .true.)}: nothing) &
+  !$omp& when(user={condition(.true.)}: nothing apply(reverse)) &
+  !$omp& otherwise(nothing apply(reverse))
+end subroutine
+
+! A reachable APPLY with a suitable associated loop is valid.
+subroutine valid_apply(n, a)
+  integer :: n, a(n), i
+  !$omp metadirective &
+  !$omp& when(user={condition(.true.)}: nothing apply(reverse)) &
+  !$omp& otherwise(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
diff --git a/flang/test/Semantics/OpenMP/metadirective-match-any-paths.f90 b/flang/test/Semantics/OpenMP/metadirective-match-any-paths.f90
deleted file mode 100644
index 5deeb73736842..0000000000000
--- a/flang/test/Semantics/OpenMP/metadirective-match-any-paths.f90
+++ /dev/null
@@ -1,71 +0,0 @@
-! RUN: not %flang_fc1 -fopenmp -fopenmp-version=51 -fsyntax-only %s 2>&1 | \
-! RUN:   FileCheck %s --implicit-check-not=error:
-
-! CHECK: error: Semantic errors in
-
-! Both paths lack TARGET and contain PARALLEL, but MATCH_ANY scores their
-! first PARALLEL at different positions. PARALLEL -> PARALLEL scores 2 and
-! loses the tie with NOTHING. TEAMS -> PARALLEL scores 3 and selects SIMD.
-! Merging these paths must not discard the reachable COLLAPSE diagnostic.
-subroutine parallel_first(flag, n, a)
-  logical :: flag
-  integer :: n, a(n), i
-  !$omp begin metadirective &
-  !$omp& when(user={condition(flag)}: parallel) default(teams)
-    !$omp parallel
-      !$omp metadirective &
-      !$omp& when(user={condition(score(1): .true.)}: nothing) &
-      !$omp& when(construct={target, parallel}, &
-! CHECK: :[[@LINE+3]]:{{[0-9]+}}: error: This construct requires
-! CHECK-SAME: a nest of depth 2, but the associated nest is a nest of depth 1
-! CHECK: because: COLLAPSE clause was specified with argument 2
-      !$omp& implementation={extension(match_any)}: simd collapse(2)) &
-      !$omp& default(nothing)
-      do i = 1, n
-        a(i) = i
-      end do
-    !$omp end parallel
-  !$omp end metadirective
-end subroutine
-
-! Reversing the path order must leave the same replacement reachable.
-subroutine teams_first(flag, n, a)
-  logical :: flag
-  integer :: n, a(n), i
-  !$omp begin metadirective &
-  !$omp& when(user={condition(flag)}: teams) default(parallel)
-    !$omp parallel
-      !$omp metadirective &
-      !$omp& when(user={condition(score(1): .true.)}: nothing) &
-      !$omp& when(construct={target, parallel}, &
-! CHECK: :[[@LINE+3]]:{{[0-9]+}}: error: This construct requires
-! CHECK-SAME: a nest of depth 2, but the associated nest is a nest of depth 1
-! CHECK: because: COLLAPSE clause was specified with argument 2
-      !$omp& implementation={extension(match_any)}: simd collapse(2)) &
-      !$omp& default(nothing)
-      do i = 1, n
-        a(i) = i
-      end do
-    !$omp end parallel
-  !$omp end metadirective
-end subroutine
-
-! A score-4 NOTHING wins on both paths, so retaining both contexts must not
-! make the lower-scored SIMD replacement reachable.
-subroutine nothing_wins(flag, n, a)
-  logical :: flag
-  integer :: n, a(n), i
-  !$omp begin metadirective &
-  !$omp& when(user={condition(flag)}: parallel) default(teams)
-    !$omp parallel
-      !$omp metadirective &
-      !$omp& when(user={condition(score(3): .true.)}: nothing) &
-      !$omp& when(construct={target, parallel}, &
-      !$omp& implementation={extension(match_any)}: simd collapse(2)) &
-      !$omp& default(nothing)
-      do i = 1, n
-        a(i) = i
-      end do
-    !$omp end parallel
-  !$omp end metadirective
-end subroutine
diff --git a/flang/test/Semantics/OpenMP/metadirective-ordered-paths.f90 b/flang/test/Semantics/OpenMP/metadirective-ordered-paths.f90
deleted file mode 100644
index 683bc72efff57..0000000000000
--- a/flang/test/Semantics/OpenMP/metadirective-ordered-paths.f90
+++ /dev/null
@@ -1,45 +0,0 @@
-! RUN: not %flang_fc1 -fopenmp -fopenmp-version=51 -fsyntax-only %s 2>&1 | \
-! RUN:   FileCheck %s --implicit-check-not=error:
-
-! CHECK: error: Semantic errors in
-
-! The outer choice and the inner selector produce PARALLEL -> DO first,
-! then DO -> PARALLEL. Both paths have the same length and trait presence,
-! but only the second matches the innermost ordered construct selector.
-! Merging by trait presence alone would discard its COLLAPSE diagnostic.
-subroutine ordered_paths(flag, n, a)
-  logical :: flag
-  integer :: n, a(n, n, n), i, j, k
-  !$omp begin metadirective &
-  !$omp& when(user={condition(flag)}: parallel) default(do)
-  do i = 1, n
-    !$omp begin metadirective &
-    !$omp& when(construct={do}: parallel) default(do)
-    do j = 1, n
-      !$omp metadirective &
-! CHECK: :[[@LINE+3]]:{{[0-9]+}}: error: This construct requires
-! CHECK-SAME: a nest of depth 2, but the associated nest is a nest of depth 1
-! CHECK: because: COLLAPSE clause was specified with argument 2
-      !$omp& when(construct={do, parallel}: simd collapse(2)) default(nothing)
-      do k = 1, n
-        a(k, j, i) = k
-      end do
-    end do
-    !$omp end metadirective
-  end do
-  !$omp end metadirective
-end subroutine
-
-! PARALLEL -> DO alone must not match the reversed selector.
-subroutine reversed_selector(n, a)
-  integer :: n, a(n, n), i, j
-  !$omp parallel do
-  do i = 1, n
-    !$omp metadirective &
-    !$omp& when(construct={do, parallel}: simd collapse(2)) default(nothing)
-    do j = 1, n
-      a(j, i) = j
-    end do
-  end do
-  !$omp end parallel do
-end subroutine

>From 22a9b0c1061ccb5d3aebc043a30fa1367a03c431 Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Thu, 10 Sep 2026 12:59:31 -0500
Subject: [PATCH 06/11] Preserve reachable metadirective variants across
 ranking and scopes

Incorrect candidate scores and stale scope boundaries can suppress required
loop diagnostics or let directives associate with loops outside their region.

Keep pending scopes consistent as directives are consumed, and preserve
candidate scores and highest-valued ordered matches when selecting and
merging reachable paths. Handle unknown properties without aborting ranking.
---
 flang/lib/Semantics/check-omp-structure.cpp   |  10 ++
 flang/lib/Semantics/check-omp-structure.h     |   1 +
 flang/lib/Semantics/check-omp-variant.cpp     |  22 ++-
 .../OpenMP/metadirective-construct-paths.f90  | 146 ++++++++++++++++++
 4 files changed, 177 insertions(+), 2 deletions(-)

diff --git a/flang/lib/Semantics/check-omp-structure.cpp b/flang/lib/Semantics/check-omp-structure.cpp
index bf1d3548edee4..284e97fa1669e 100644
--- a/flang/lib/Semantics/check-omp-structure.cpp
+++ b/flang/lib/Semantics/check-omp-structure.cpp
@@ -305,6 +305,7 @@ void OmpStructureChecker::EndPendingLoopDirectiveScope() {
   CHECK(!pendingLoopDirectiveScopeStarts_.empty());
   std::size_t firstDirectiveGroup{pendingLoopDirectiveScopeStarts_.back()};
   pendingLoopDirectiveScopeStarts_.pop_back();
+  CHECK(firstDirectiveGroup <= pendingLoopDirectiveGroups_.size());
   if (firstDirectiveGroup < pendingLoopDirectiveGroups_.size()) {
     // Diagnose directives that were recorded in this scope but not consumed
     // by one of its executable constructs, preserving directives from an
@@ -313,6 +314,14 @@ void OmpStructureChecker::EndPendingLoopDirectiveScope() {
   }
 }
 
+void OmpStructureChecker::UpdatePendingLoopDirectiveScopeStarts() {
+  // Removing a suffix can consume groups that preceded an active scope.
+  // Rebase its boundary so newly added groups still belong to that scope.
+  for (std::size_t &start : pendingLoopDirectiveScopeStarts_) {
+    start = std::min(start, pendingLoopDirectiveGroups_.size());
+  }
+}
+
 void OmpStructureChecker::Enter(const parser::Block &) {
   BeginPendingLoopDirectiveScope();
 }
@@ -363,6 +372,7 @@ void OmpStructureChecker::Enter(const parser::SpecificationPart &) {
   // nested one such as an interface body does not reset them.
   if (partStack_.empty()) {
     pendingLoopDirectiveGroups_.clear();
+    UpdatePendingLoopDirectiveScopeStarts();
   }
   partStack_.push_back(PartKind::SpecificationPart);
 }
diff --git a/flang/lib/Semantics/check-omp-structure.h b/flang/lib/Semantics/check-omp-structure.h
index ab5971085187d..8fa4940007cfe 100644
--- a/flang/lib/Semantics/check-omp-structure.h
+++ b/flang/lib/Semantics/check-omp-structure.h
@@ -325,6 +325,7 @@ class OmpStructureChecker : public OmpStructureCheckerBase {
 
   void BeginPendingLoopDirectiveScope();
   void EndPendingLoopDirectiveScope();
+  void UpdatePendingLoopDirectiveScopeStarts();
 
   // check-omp-variant.cpp
   void CheckPendingLoopDirectivesWithoutLoop(
diff --git a/flang/lib/Semantics/check-omp-variant.cpp b/flang/lib/Semantics/check-omp-variant.cpp
index d8b83dae4e1f1..7f226529ccefe 100644
--- a/flang/lib/Semantics/check-omp-variant.cpp
+++ b/flang/lib/Semantics/check-omp-variant.cpp
@@ -791,8 +791,6 @@ OmpStructureChecker::GetUniqueEffectiveDirectivePaths(
     // matches after a failure for match_any scoring, as well as successful
     // prefixes for matching after inner directives are appended.
     std::vector<unsigned> signature;
-    signature.reserve(
-        1 + 2 * contextTraits.size() * metadirectiveConstructSelectors_.size());
     signature.push_back(contextTraits.size());
     for (const ConstructTraitSequence &selector :
         metadirectiveConstructSelectors_) {
@@ -816,6 +814,24 @@ OmpStructureChecker::GetUniqueEffectiveDirectivePaths(
           signature.push_back(++contextIndex);
         }
       }
+
+      // Scoring uses the highest-valued complete ordered match. Retain that
+      // match for every selector prefix, since appended inner constructs can
+      // complete a selector that does not yet match the current context.
+      for (std::size_t prefixSize{1}; prefixSize <= selector.size();
+          ++prefixSize) {
+        contextIndex = contextTraits.size();
+        for (std::size_t i{prefixSize}; i > 0; --i) {
+          while (contextIndex > 0 &&
+              contextTraits[contextIndex - 1] != selector[i - 1]) {
+            --contextIndex;
+          }
+          signature.push_back(contextIndex);
+          if (contextIndex > 0) {
+            --contextIndex;
+          }
+        }
+      }
     }
     return signature;
   };
@@ -1069,6 +1085,7 @@ void OmpStructureChecker::Enter(const parser::ExecutionPartConstruct &x) {
   // their reachable directives against it.
   std::vector<PendingLoopDirectiveGroup> pending;
   pending.swap(pendingLoopDirectiveGroups_);
+  UpdatePendingLoopDirectiveScopeStarts();
   llvm::omp::Version version{context_.langOptions().getOpenMPVersion()};
   LoopSequence sequence(x, version, /*allowAllLoops=*/true, &context_);
   const auto &[haveSemantic, havePerfect]{sequence.depth()};
@@ -1186,6 +1203,7 @@ void OmpStructureChecker::CheckPendingLoopDirectivesWithoutLoop(
     }
   }
   pendingLoopDirectiveGroups_.erase(first, pendingLoopDirectiveGroups_.end());
+  UpdatePendingLoopDirectiveScopeStarts();
 }
 
 static const parser::traits::OmpContextSelectorSpecification *
diff --git a/flang/test/Semantics/OpenMP/metadirective-construct-paths.f90 b/flang/test/Semantics/OpenMP/metadirective-construct-paths.f90
index f6de4e54cd9d7..ef00af828b8f7 100644
--- a/flang/test/Semantics/OpenMP/metadirective-construct-paths.f90
+++ b/flang/test/Semantics/OpenMP/metadirective-construct-paths.f90
@@ -3,6 +3,99 @@
 
 ! CHECK: error: Semantic errors in
 
+! The inner PARALLEL gives the construct candidate score 3, not 2.
+subroutine repeated_parallel(n)
+  integer :: n, i
+  !$omp parallel
+    !$omp parallel
+      !$omp metadirective &
+      !$omp& when(user={condition(score(1): .true.)}: nothing) &
+! CHECK: :[[@LINE+3]]:{{[0-9]+}}: error: This construct requires
+! CHECK-SAME: a nest of depth 2, but the associated nest is a nest of depth 1
+! CHECK: because: COLLAPSE clause was specified with argument 2
+      !$omp& when(construct={parallel}: simd collapse(2)) default(nothing)
+      do i = 1, n
+      end do
+    !$omp end parallel
+  !$omp end parallel
+end subroutine
+
+! Both paths have the same first PARALLEL match, but different last matches.
+! Merging them must not lose the higher-scoring PARALLEL -> PARALLEL path.
+subroutine repeated_parallel_paths(flag, n)
+  logical :: flag
+  integer :: n, i
+  !$omp parallel
+    !$omp begin metadirective &
+    !$omp& when(user={condition(flag)}: teams) default(parallel)
+      !$omp metadirective &
+      !$omp& when(user={condition(score(1): .true.)}: nothing) &
+! CHECK: :[[@LINE+3]]:{{[0-9]+}}: error: This construct requires
+! CHECK-SAME: a nest of depth 2, but the associated nest is a nest of depth 1
+! CHECK: because: COLLAPSE clause was specified with argument 2
+      !$omp& when(construct={parallel}: simd collapse(2)) default(nothing)
+      do i = 1, n
+      end do
+    !$omp end metadirective
+  !$omp end parallel
+end subroutine
+
+! Before DO is appended, neither path fully matches {parallel, do}. The best
+! PARALLEL prefix must survive merging to obtain the correct score afterwards.
+subroutine repeated_parallel_prefix(flag, n)
+  logical :: flag
+  integer :: n, i, j
+  !$omp parallel
+    !$omp begin metadirective &
+    !$omp& when(user={condition(flag)}: teams) default(parallel)
+      !$omp do
+      do i = 1, n
+        !$omp metadirective &
+        !$omp& when(user={condition(score(5): .true.)}: nothing) &
+! CHECK: :[[@LINE+3]]:{{[0-9]+}}: error: This construct requires
+! CHECK-SAME: a nest of depth 2, but the associated nest is a nest of depth 1
+! CHECK: because: COLLAPSE clause was specified with argument 2
+        !$omp& when(construct={parallel, do}: simd collapse(2)) default(nothing)
+        do j = 1, n
+        end do
+      end do
+      !$omp end do
+    !$omp end metadirective
+  !$omp end parallel
+end subroutine
+
+! Ranking a matching unknown-vendor exclusion must not abort.
+subroutine match_none_unknown_vendor(n)
+  integer :: n, i
+  !$omp metadirective &
+  !$omp& when(implementation={vendor(bogus_vendor), &
+! CHECK: :[[@LINE+3]]:{{[0-9]+}}: error: This construct requires
+! CHECK-SAME: a nest of depth 2, but the associated nest is a nest of depth 1
+! CHECK: because: COLLAPSE clause was specified with argument 2
+  !$omp& extension(match_none)}: simd collapse(2)) &
+  !$omp& when(user={condition(.true.)}: nothing) default(nothing)
+  do i = 1, n
+  end do
+end subroutine
+
+! A higher-scored dynamic implicit NOTHING keeps the path without DO alive.
+subroutine omitted_score(flag, n)
+  logical :: flag
+  integer :: n, i, j
+  !$omp metadirective &
+  !$omp& when(user={condition(score(10): flag)}:) &
+  !$omp& when(user={condition(score(5): .true.)}: do) default(nothing)
+  do i = 1, n
+    !$omp metadirective when(construct={do}: nothing) &
+! CHECK: :[[@LINE+3]]:{{[0-9]+}}: error: This construct requires
+! CHECK-SAME: a nest of depth 2, but the associated nest is a nest of depth 1
+! CHECK: because: COLLAPSE clause was specified with argument 2
+    !$omp& default(simd collapse(2))
+    do j = 1, n
+    end do
+  end do
+end subroutine
+
 ! The outer choice and the inner selector produce PARALLEL -> DO first,
 ! then DO -> PARALLEL. Both paths have the same length and trait presence,
 ! but only the second matches the innermost ordered construct selector.
@@ -30,6 +123,59 @@ subroutine ordered_paths(flag, n, a)
   !$omp end metadirective
 end subroutine
 
+! Consuming the enclosing pending group must not invalidate scope boundaries.
+! Neither inner variant can associate with a loop outside the selected region.
+subroutine escaped_region(flag, n)
+  logical :: flag
+  integer :: n, i
+  !$omp begin metadirective default(parallel)
+    continue
+    !$omp metadirective &
+! CHECK: :[[@LINE+2]]:{{[0-9]+}}: error: This construct should contain
+! CHECK-SAME: a DO-loop or a loop-nest-generating construct
+    !$omp& when(user={condition(flag)}: simd) &
+! CHECK: :[[@LINE+2]]:{{[0-9]+}}: error: This construct should contain
+! CHECK-SAME: a DO-loop or a loop-nest-generating construct
+    !$omp& default(do)
+  !$omp end metadirective
+  do i = 1, n
+  end do
+end subroutine
+
+! A loop also consumes pending groups. Update every enclosing scope boundary,
+! including those of nested selected regions.
+subroutine escaped_nested_region_after_loop(flag, n)
+  logical :: flag
+  integer :: n, i
+  !$omp begin metadirective default(parallel)
+    !$omp begin metadirective default(parallel)
+      do i = 1, n
+      end do
+      !$omp metadirective &
+! CHECK: :[[@LINE+2]]:{{[0-9]+}}: error: This construct should contain
+! CHECK-SAME: a DO-loop or a loop-nest-generating construct
+      !$omp& when(user={condition(flag)}: simd) &
+! CHECK: :[[@LINE+2]]:{{[0-9]+}}: error: This construct should contain
+! CHECK-SAME: a DO-loop or a loop-nest-generating construct
+      !$omp& default(do)
+    !$omp end metadirective
+    do i = 1, n
+    end do
+  !$omp end metadirective
+end subroutine
+
+! A loop within the selected region still satisfies both inner variants.
+subroutine associated_loop_in_region(flag, n)
+  logical :: flag
+  integer :: n, i
+  !$omp begin metadirective default(parallel)
+    continue
+    !$omp metadirective when(user={condition(flag)}: simd) default(do)
+    do i = 1, n
+    end do
+  !$omp end metadirective
+end subroutine
+
 ! PARALLEL -> DO alone must not match the reversed selector.
 subroutine reversed_selector(n, a)
   integer :: n, a(n, n), i, j

>From 57a328a558352a9d5577a42395c5634b25cf0f8b Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Fri, 11 Sep 2026 00:45:28 -0500
Subject: [PATCH 07/11] Respect target boundaries during OpenMP variant
 lowering

Stop collecting construct traits at the innermost TARGET so lowering
agrees with semantic analysis when selecting metadirective replacements
and DECLARE VARIANT callees.
---
 flang/include/flang/Semantics/openmp-utils.h          |  3 ++-
 flang/lib/Semantics/check-omp-variant.cpp             | 11 +++++------
 flang/lib/Semantics/openmp-utils.cpp                  |  8 ++------
 flang/test/Lower/OpenMP/metadirective-construct.f90   |  3 +--
 .../Lower/OpenMP/metadirective-target-boundary.f90    |  7 ++++---
 .../OpenMP/metadirective-construct-paths.f90          |  8 ++++----
 6 files changed, 18 insertions(+), 22 deletions(-)

diff --git a/flang/include/flang/Semantics/openmp-utils.h b/flang/include/flang/Semantics/openmp-utils.h
index 3f0b95c40d9b9..057198bedac3a 100644
--- a/flang/include/flang/Semantics/openmp-utils.h
+++ b/flang/include/flang/Semantics/openmp-utils.h
@@ -243,6 +243,7 @@ class OmpVariantMatchContext : public llvm::omp::OMPContext {
 };
 
 /// Add the construct traits implied by an OpenMP directive to \p vmi.
+/// Combined and composite directives contribute each of their leaf traits.
 void AppendConstructTraitsForDirective(
     llvm::omp::Directive, llvm::omp::VariantMatchInfo &vmi);
 
@@ -302,7 +303,7 @@ llvm::SmallVector<unsigned, 4> GetMetadirectiveElsePathCandidates(
     const OmpVariantMatchContext &matchContext, SemanticsContext &context);
 
 /// Return every replacement that can be selected, retaining lower-ranked
-/// candidates after a dynamic condition. Null represents NOTHING.
+/// candidates after a dynamic condition. Null represents clause-free NOTHING.
 llvm::SmallVector<const parser::OmpDirectiveSpecification *, 4>
 GetReachableMetadirectiveVariants(const MetadirectiveCandidateSet &candidateSet,
     const OmpVariantMatchContext &matchContext, SemanticsContext &context);
diff --git a/flang/lib/Semantics/check-omp-variant.cpp b/flang/lib/Semantics/check-omp-variant.cpp
index 7f226529ccefe..6166995f32dcd 100644
--- a/flang/lib/Semantics/check-omp-variant.cpp
+++ b/flang/lib/Semantics/check-omp-variant.cpp
@@ -47,6 +47,7 @@ using namespace Fortran::semantics::omp;
 
 namespace {
 
+/// Collect construct selectors used to distinguish enclosing directive paths.
 class MetadirectiveConstructSelectorCollector {
 public:
   using ConstructTraitSequence = llvm::SmallVector<llvm::omp::TraitProperty, 8>;
@@ -865,8 +866,8 @@ OmpStructureChecker::GetReachableMetadirectiveReplacements(
       continue;
     }
 
-    // Unsupported selectors are diagnosed elsewhere. Retain every explicit
-    // replacement on each path so recovery cannot miss a loop constraint.
+    // If matching cannot model a selector, retain all replacements on each
+    // path so loop constraints are still checked.
     bool hasFallback{false};
     for (const parser::OmpClause &clause : clauses.v) {
       const parser::OmpDirectiveSpecification *spec{nullptr};
@@ -1038,10 +1039,8 @@ void OmpStructureChecker::Enter(const parser::ExecutionPartConstruct &x) {
   bool isStrictlyStructuredBlock{
       parser::Unwrap<parser::BlockConstruct>(x) != nullptr};
 
-  // A standalone metadirective's replacement applies to its following
-  // associated construct. Keep block-associated replacements active across a
-  // Fortran BLOCK, just as loop-associated replacements are active across
-  // their DO construct, so nested construct selectors see the selected path.
+  // Keep standalone replacements active throughout their associated DO or
+  // BLOCK construct so nested construct selectors see the selected path.
   if (rootLoop || isStrictlyStructuredBlock) {
     for (const PendingLoopDirectiveGroup &group : pendingLoopDirectiveGroups_) {
       if (!group.isStandaloneMetadirective) {
diff --git a/flang/lib/Semantics/openmp-utils.cpp b/flang/lib/Semantics/openmp-utils.cpp
index 115d0071ce6eb..2c0bc98fd2266 100644
--- a/flang/lib/Semantics/openmp-utils.cpp
+++ b/flang/lib/Semantics/openmp-utils.cpp
@@ -2427,10 +2427,6 @@ UnsupportedSelectorFeature FindUnsupportedSelectorFeature(
   return UnsupportedSelectorFeature::None;
 }
 
-// Add the construct trait properties implied by an OpenMP directive (e.g.
-// `target` adds `construct_target_target`, `target teams` adds both
-// `construct_target_target` and `construct_teams_teams`) to \p vmi. This
-// decomposes combined/composite construct selectors into their leaf traits.
 void AppendConstructTraitsForDirective(
     llvm::omp::Directive dir, llvm::omp::VariantMatchInfo &vmi) {
   auto add = [&](llvm::omp::TraitProperty prop) {
@@ -2556,8 +2552,8 @@ std::optional<MetadirectiveCandidateSet> BuildMetadirectiveCandidateSet(
   };
 
   auto getVariant = [](const parser::OmpDirectiveSpecification &spec) {
-    // NOTHING with APPLY is a loop transformation, not a no-op. Preserve its
-    // specification so semantic checks can visit the reachable APPLY items.
+    // Preserve NOTHING with APPLY so semantic checks can validate its loop
+    // transformations and lowering can diagnose unsupported APPLY clauses.
     return spec.DirId() == llvm::omp::Directive::OMPD_nothing &&
             spec.Clauses().v.empty()
         ? nullptr
diff --git a/flang/test/Lower/OpenMP/metadirective-construct.f90 b/flang/test/Lower/OpenMP/metadirective-construct.f90
index 339bda7009d4e..37382cab392fe 100644
--- a/flang/test/Lower/OpenMP/metadirective-construct.f90
+++ b/flang/test/Lower/OpenMP/metadirective-construct.f90
@@ -2,8 +2,7 @@
 
 ! RUN: %flang_fc1 -fopenmp -emit-hlfir -fopenmp-version=50 %s -o - | FileCheck %s
 
-! A bare SIMD selector is not an empty selector since it needs an enclosing
-! SIMD.
+! A SIMD selector without properties requires an enclosing SIMD construct.
 ! CHECK-LABEL: func.func @_QPtest_construct_simd_absent()
 ! CHECK-NOT:     omp.barrier
 ! CHECK:         omp.taskyield
diff --git a/flang/test/Lower/OpenMP/metadirective-target-boundary.f90 b/flang/test/Lower/OpenMP/metadirective-target-boundary.f90
index d7963bf7fe4ec..f80f4a8bec678 100644
--- a/flang/test/Lower/OpenMP/metadirective-target-boundary.f90
+++ b/flang/test/Lower/OpenMP/metadirective-target-boundary.f90
@@ -2,7 +2,8 @@
 ! RUN: %flang_fc1 -fopenmp -fopenmp-version=51 -emit-hlfir %s -o - | \
 ! RUN:   FileCheck %s
 
-! TARGET hides the outer PARALLEL, so the SIMD replacement is not lowered.
+! TARGET hides the outer PARALLEL. The unreachable SIMD replacement must
+! not be lowered, since its COLLAPSE clause is invalid for the single loop.
 ! CHECK-LABEL: func.func @_QPactual_target(
 ! CHECK: omp.parallel
 ! CHECK: omp.target
@@ -13,7 +14,7 @@ subroutine actual_target(n, a)
   !$omp parallel
     !$omp target
       !$omp metadirective &
-      !$omp& when(construct={parallel}: simd) default(nothing)
+      !$omp& when(construct={parallel}: simd collapse(2)) default(nothing)
       do i = 1, n
         a(i) = i
       end do
@@ -32,7 +33,7 @@ subroutine selected_target(n, a)
   !$omp parallel
     !$omp begin metadirective default(target)
       !$omp metadirective &
-      !$omp& when(construct={parallel, target}: simd) &
+      !$omp& when(construct={parallel, target}: simd collapse(2)) &
       !$omp& default(nothing)
       do i = 1, n
         a(i) = i
diff --git a/flang/test/Semantics/OpenMP/metadirective-construct-paths.f90 b/flang/test/Semantics/OpenMP/metadirective-construct-paths.f90
index ef00af828b8f7..5130f04e6fa82 100644
--- a/flang/test/Semantics/OpenMP/metadirective-construct-paths.f90
+++ b/flang/test/Semantics/OpenMP/metadirective-construct-paths.f90
@@ -3,7 +3,7 @@
 
 ! CHECK: error: Semantic errors in
 
-! The inner PARALLEL gives the construct candidate score 3, not 2.
+! Matching the inner PARALLEL gives the construct candidate a score of 3.
 subroutine repeated_parallel(n)
   integer :: n, i
   !$omp parallel
@@ -64,7 +64,7 @@ subroutine repeated_parallel_prefix(flag, n)
   !$omp end parallel
 end subroutine
 
-! Ranking a matching unknown-vendor exclusion must not abort.
+! MATCH_NONE with an unknown vendor remains applicable during ranking.
 subroutine match_none_unknown_vendor(n)
   integer :: n, i
   !$omp metadirective &
@@ -78,7 +78,7 @@ subroutine match_none_unknown_vendor(n)
   end do
 end subroutine
 
-! A higher-scored dynamic implicit NOTHING keeps the path without DO alive.
+! A higher-scored dynamic implicit NOTHING leaves a reachable path without DO.
 subroutine omitted_score(flag, n)
   logical :: flag
   integer :: n, i, j
@@ -142,7 +142,7 @@ subroutine escaped_region(flag, n)
   end do
 end subroutine
 
-! A loop also consumes pending groups. Update every enclosing scope boundary,
+! Consuming pending groups at a loop preserves every enclosing scope boundary,
 ! including those of nested selected regions.
 subroutine escaped_nested_region_after_loop(flag, n)
   logical :: flag

>From c951cdabe5ddb27799e33448473dd00fe2af7aa8 Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Fri, 11 Sep 2026 12:36:38 -0500
Subject: [PATCH 08/11] Reject negative trait scores during metadirective
 recovery

Reachability analysis can rank candidates before CheckTraitScore diagnoses
invalid scores. A score of -1 wraps the initial unsigned score to zero,
causing selection to dereference a null best candidate.

Treat negative scores as absent during recovery while preserving the
existing diagnostic.
---
 flang/lib/Semantics/openmp-utils.cpp          |  5 +++-
 .../Semantics/OpenMP/metadirective-common.f90 | 27 +++++++++++++++++++
 2 files changed, 31 insertions(+), 1 deletion(-)

diff --git a/flang/lib/Semantics/openmp-utils.cpp b/flang/lib/Semantics/openmp-utils.cpp
index 2c0bc98fd2266..cbd4452ca62cf 100644
--- a/flang/lib/Semantics/openmp-utils.cpp
+++ b/flang/lib/Semantics/openmp-utils.cpp
@@ -2355,7 +2355,10 @@ llvm::APInt *GetTraitScore(
     return nullptr;
 
   auto constVal = evaluate::ToInt64(*typedExpr);
-  if (!constVal)
+  // Reachability can request scores before CheckTraitScore diagnoses them.
+  // Treat an invalid score as absent during recovery rather than passing a
+  // negative value to the unsigned scorer.
+  if (!constVal || *constVal < 0)
     return nullptr;
 
   scoreStorage = llvm::APInt(64, *constVal);
diff --git a/flang/test/Semantics/OpenMP/metadirective-common.f90 b/flang/test/Semantics/OpenMP/metadirective-common.f90
index 3d219e9d9495a..16ae6e19dea13 100644
--- a/flang/test/Semantics/OpenMP/metadirective-common.f90
+++ b/flang/test/Semantics/OpenMP/metadirective-common.f90
@@ -26,6 +26,33 @@ subroutine f02_zero_score
   !$omp & when(user={condition(score(0): .true.)}: nothing)
 end
 
+! Competing candidates reach scoring before the SCORE diagnostic is emitted.
+! A score of -1 must not wrap the initial score to zero and crash selection.
+subroutine f02_ranked_negative_scores(flag)
+  logical :: flag
+  integer :: i
+  !$omp metadirective &
+!ERROR: SCORE expression must be a non-negative constant integer expression
+  !$omp & when(user={condition(score(-1): .true.)}: nothing) &
+  !$omp & when(implementation={vendor(llvm)}: simd) otherwise(nothing)
+  do i = 1, 4
+  end do
+
+  !$omp metadirective &
+!ERROR: SCORE expression must be a non-negative constant integer expression
+  !$omp & when(user={condition(score(-1): flag)}: nothing) &
+  !$omp & when(implementation={vendor(llvm)}: simd) otherwise(nothing)
+  do i = 1, 4
+  end do
+
+  !$omp metadirective &
+!ERROR: SCORE expression must be a non-negative constant integer expression
+  !$omp & when(implementation={vendor(score(-1): llvm)}: nothing) &
+  !$omp & when(user={condition(.true.)}: simd) otherwise(nothing)
+  do i = 1, 4
+  end do
+end
+
 subroutine f03(x)
   integer :: x
   !$omp metadirective &

>From 609668ca265a41836451640f6bb01e23ec2fd334 Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Mon, 14 Sep 2026 01:44:42 -0500
Subject: [PATCH 09/11] Preserve OpenMP variant ranking and reachability

Incorrect scores and premature candidate rejection can change lowering
selection or suppress required metadirective loop diagnostics.

Preserve full score values and context-dependent device weights, retain
paths distinguished by those weights, and let runtime conditions satisfy
match_any despite unknown static properties.
---
 flang/lib/Semantics/check-omp-variant.cpp     |  9 +--
 .../OpenMP/metadirective-construct-paths.f90  | 68 +++++++++++++++++++
 2 files changed, 69 insertions(+), 8 deletions(-)

diff --git a/flang/lib/Semantics/check-omp-variant.cpp b/flang/lib/Semantics/check-omp-variant.cpp
index 6166995f32dcd..1428dc1bf7d97 100644
--- a/flang/lib/Semantics/check-omp-variant.cpp
+++ b/flang/lib/Semantics/check-omp-variant.cpp
@@ -777,14 +777,6 @@ OmpStructureChecker::GetUniqueEffectiveDirectivePaths(
     return paths;
   }
 
-  // Selected directive paths are observed only by construct selectors on
-  // nested metadirectives. If there are none in this program unit, every path
-  // is equivalent for this analysis.
-  if (metadirectiveConstructSelectors_.empty()) {
-    paths.resize(1);
-    return paths;
-  }
-
   auto getSignature = [&](const EffectiveDirectivePath &path) {
     ConstructTraitSequence contextTraits{GetConstructTraitsForPath(path)};
 
@@ -792,6 +784,7 @@ OmpStructureChecker::GetUniqueEffectiveDirectivePaths(
     // matches after a failure for match_any scoring, as well as successful
     // prefixes for matching after inner directives are appended.
     std::vector<unsigned> signature;
+    // Device scores depend on context depth even without construct selectors.
     signature.push_back(contextTraits.size());
     for (const ConstructTraitSequence &selector :
         metadirectiveConstructSelectors_) {
diff --git a/flang/test/Semantics/OpenMP/metadirective-construct-paths.f90 b/flang/test/Semantics/OpenMP/metadirective-construct-paths.f90
index 5130f04e6fa82..519a5625adbd5 100644
--- a/flang/test/Semantics/OpenMP/metadirective-construct-paths.f90
+++ b/flang/test/Semantics/OpenMP/metadirective-construct-paths.f90
@@ -3,6 +3,74 @@
 
 ! CHECK: error: Semantic errors in
 
+! A runtime condition can satisfy MATCH_ANY despite an unknown static trait.
+subroutine dynamic_unknown_traits(flag, n)
+  logical :: flag
+  integer :: n, i
+  !$omp metadirective &
+  !$omp& when(implementation={vendor(bogus_vendor), extension(match_any)}, &
+! CHECK: :[[@LINE+3]]:{{[0-9]+}}: error: This construct requires
+! CHECK-SAME: a nest of depth 2, but the associated nest is a nest of depth 1
+! CHECK: because: COLLAPSE clause was specified with argument 2
+  !$omp& user={condition(flag)}: simd collapse(2)) default(nothing)
+  do i = 1, n
+  end do
+  !$omp metadirective &
+  !$omp& when(device={arch(bogus_arch)}, &
+  !$omp& implementation={extension(match_any)}, &
+! CHECK: :[[@LINE+3]]:{{[0-9]+}}: error: This construct requires
+! CHECK-SAME: a nest of depth 2, but the associated nest is a nest of depth 1
+! CHECK: because: COLLAPSE clause was specified with argument 2
+  !$omp& user={condition(flag)}: simd collapse(2)) default(nothing)
+  do i = 1, n
+  end do
+  !$omp metadirective &
+  !$omp& when(device={kind(bogus_kind)}, &
+  !$omp& implementation={extension(match_any)}, &
+! CHECK: :[[@LINE+3]]:{{[0-9]+}}: error: This construct requires
+! CHECK-SAME: a nest of depth 2, but the associated nest is a nest of depth 1
+! CHECK: because: COLLAPSE clause was specified with argument 2
+  !$omp& user={condition(flag)}: simd collapse(2)) default(nothing)
+  do i = 1, n
+  end do
+end subroutine
+
+! Neither a false condition nor MATCH_ALL can rescue an unknown static trait.
+subroutine unknown_traits_unreachable(flag, n)
+  logical :: flag
+  integer :: n, i
+  !$omp metadirective &
+  !$omp& when(implementation={vendor(bogus_vendor), extension(match_any)}, &
+  !$omp& user={condition(.false.)}: simd collapse(2)) default(nothing)
+  do i = 1, n
+  end do
+  !$omp metadirective &
+  !$omp& when(implementation={vendor(bogus_vendor), extension(match_all)}, &
+  !$omp& user={condition(flag)}: simd collapse(2)) default(nothing)
+  do i = 1, n
+  end do
+end subroutine
+
+! Even without construct selectors, path depth changes device scores. CPU
+! ties NOTHING at depth 1 but wins at depth 2, keeping the invalid loop live.
+subroutine device_weight_paths(flag, n)
+  logical :: flag
+  integer :: n, i
+  !$omp parallel
+    !$omp begin metadirective &
+    !$omp& when(user={condition(flag)}: nothing) default(parallel)
+      !$omp metadirective &
+      !$omp& when(user={condition(score(2): .true.)}: nothing) &
+! CHECK: :[[@LINE+3]]:{{[0-9]+}}: error: This construct requires
+! CHECK-SAME: a nest of depth 2, but the associated nest is a nest of depth 1
+! CHECK: because: COLLAPSE clause was specified with argument 2
+      !$omp& when(device={kind(cpu)}: simd collapse(2)) default(nothing)
+      do i = 1, n
+      end do
+    !$omp end metadirective
+  !$omp end parallel
+end subroutine
+
 ! Matching the inner PARALLEL gives the construct candidate a score of 3.
 subroutine repeated_parallel(n)
   integer :: n, i

>From 9dd52517d5ba6c41a9a206f2154a183937c7552a Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Thu, 17 Sep 2026 15:53:03 -0500
Subject: [PATCH 10/11] Count all enclosing leaves in reachable paths

Reachability must use the same construct positions and depth as lowering
when ranking metadirective replacements. Count source constructs without
selector traits, including leaves of combined directives, while keeping
NOTHING transparent.

Add regressions for actual and dynamically selected TASK regions and
DISTRIBUTE within TEAMS DISTRIBUTE PARALLEL DO.
---
 flang/lib/Semantics/check-omp-variant.cpp     | 21 ++++++--
 .../OpenMP/metadirective-construct-paths.f90  | 53 +++++++++++++++++++
 2 files changed, 70 insertions(+), 4 deletions(-)

diff --git a/flang/lib/Semantics/check-omp-variant.cpp b/flang/lib/Semantics/check-omp-variant.cpp
index 1428dc1bf7d97..a649830fa4685 100644
--- a/flang/lib/Semantics/check-omp-variant.cpp
+++ b/flang/lib/Semantics/check-omp-variant.cpp
@@ -762,10 +762,23 @@ OmpStructureChecker::GetConstructTraitsForPath(
     if (llvm::omp::allTargetSet.test(*directive)) {
       constructTraits.clear();
     }
-    llvm::omp::VariantMatchInfo directiveVMI;
-    AppendConstructTraitsForDirective(*directive, directiveVMI);
-    constructTraits.append(directiveVMI.ConstructTraits.begin(),
-        directiveVMI.ConstructTraits.end());
+    for (llvm::omp::Directive leaf :
+        llvm::omp::getLeafConstructsOrSelf(*directive)) {
+      if (leaf == llvm::omp::Directive::OMPD_nothing ||
+          leaf == llvm::omp::Directive::OMPD_unknown) {
+        continue;
+      }
+      llvm::omp::VariantMatchInfo leafVMI;
+      AppendConstructTraitsForDirective(leaf, leafVMI);
+      if (leafVMI.ConstructTraits.empty()) {
+        // Source constructs without selector traits still occupy positions
+        // in the scoring context, including leaves of combined directives.
+        constructTraits.push_back(llvm::omp::TraitProperty::invalid);
+      } else {
+        constructTraits.append(
+            leafVMI.ConstructTraits.begin(), leafVMI.ConstructTraits.end());
+      }
+    }
   }
   return constructTraits;
 }
diff --git a/flang/test/Semantics/OpenMP/metadirective-construct-paths.f90 b/flang/test/Semantics/OpenMP/metadirective-construct-paths.f90
index 519a5625adbd5..f482df62d5af0 100644
--- a/flang/test/Semantics/OpenMP/metadirective-construct-paths.f90
+++ b/flang/test/Semantics/OpenMP/metadirective-construct-paths.f90
@@ -71,6 +71,59 @@ subroutine device_weight_paths(flag, n)
   !$omp end parallel
 end subroutine
 
+! TASK has no selector trait, but still raises the CPU score from 3 to 5.
+subroutine task_weight(n)
+  integer :: n, i
+  !$omp parallel
+    !$omp task
+      !$omp metadirective &
+      !$omp& when(implementation={vendor(score(3): llvm)}: nothing) &
+! CHECK: :[[@LINE+3]]:{{[0-9]+}}: error: This construct requires
+! CHECK-SAME: a nest of depth 2, but the associated nest is a nest of depth 1
+! CHECK: because: COLLAPSE clause was specified with argument 2
+      !$omp& when(device={kind(cpu)}: simd collapse(2)) default(nothing)
+      do i = 1, n
+      end do
+    !$omp end task
+  !$omp end parallel
+end subroutine
+
+! A selected TASK also distinguishes paths through its context depth.
+subroutine selected_task_weight(flag, n)
+  logical :: flag
+  integer :: n, i
+  !$omp parallel
+    !$omp begin metadirective &
+    !$omp& when(user={condition(flag)}: nothing) default(task)
+      !$omp metadirective &
+      !$omp& when(implementation={vendor(score(3): llvm)}: nothing) &
+! CHECK: :[[@LINE+3]]:{{[0-9]+}}: error: This construct requires
+! CHECK-SAME: a nest of depth 2, but the associated nest is a nest of depth 1
+! CHECK: because: COLLAPSE clause was specified with argument 2
+      !$omp& when(device={kind(cpu)}: simd collapse(2)) default(nothing)
+      do i = 1, n
+      end do
+    !$omp end metadirective
+  !$omp end parallel
+end subroutine
+
+! DISTRIBUTE contributes a position within a combined construct's context.
+subroutine distribute_weight(n)
+  integer :: n, i, j
+  !$omp teams distribute parallel do
+  do i = 1, n
+    !$omp metadirective &
+    !$omp& when(implementation={vendor(score(9): llvm)}: nothing) &
+! CHECK: :[[@LINE+3]]:{{[0-9]+}}: error: This construct requires
+! CHECK-SAME: a nest of depth 2, but the associated nest is a nest of depth 1
+! CHECK: because: COLLAPSE clause was specified with argument 2
+    !$omp& when(device={kind(cpu)}: simd collapse(2)) default(nothing)
+    do j = 1, n
+    end do
+  end do
+  !$omp end teams distribute parallel do
+end subroutine
+
 ! Matching the inner PARALLEL gives the construct candidate a score of 3.
 subroutine repeated_parallel(n)
   integer :: n, i

>From 5d0595161f6d3eca9e844784acdb0c5fb46cd326 Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Sat, 19 Sep 2026 00:05:31 -0500
Subject: [PATCH 11/11] Set bbc target context for semantics

---
 flang/test/Driver/bbc-openmp-target-context.f90 | 17 +++++++++++++++++
 flang/tools/bbc/bbc.cpp                         |  2 ++
 2 files changed, 19 insertions(+)
 create mode 100644 flang/test/Driver/bbc-openmp-target-context.f90

diff --git a/flang/test/Driver/bbc-openmp-target-context.f90 b/flang/test/Driver/bbc-openmp-target-context.f90
new file mode 100644
index 0000000000000..479df5f7e70ed
--- /dev/null
+++ b/flang/test/Driver/bbc-openmp-target-context.f90
@@ -0,0 +1,17 @@
+! RUN: not bbc -target x86_64-unknown-linux-gnu -fopenmp \
+! RUN:   -fopenmp-version=52 -emit-hlfir %s -o /dev/null 2>&1 \
+! RUN:   | FileCheck %s
+
+! CHECK-NOT: not yet implemented
+! CHECK: error: {{.*}}This construct requires a nest of depth 2, but the
+! CHECK-SAME: associated nest is a nest of depth 1
+! CHECK: because: COLLAPSE clause was specified with argument 2
+
+subroutine cpu_collapse(n, a)
+  integer :: n, a(n), i
+  !$omp metadirective when(device={kind(cpu)}: simd collapse(2)) &
+  !$omp& otherwise(nothing)
+  do i = 1, n
+    a(i) = i
+  end do
+end subroutine
diff --git a/flang/tools/bbc/bbc.cpp b/flang/tools/bbc/bbc.cpp
index d6e0643c813c2..853844f9a32e8 100644
--- a/flang/tools/bbc/bbc.cpp
+++ b/flang/tools/bbc/bbc.cpp
@@ -753,6 +753,8 @@ int main(int argc, char **argv) {
     llvm::errs() << "failed to create target machine: " << error << "\n";
     return mlir::failed(mlir::failure());
   }
+  semanticsContext.set_targetTriple(targetMachine->getTargetTriple().str())
+      .set_targetFeatures(targetMachine->getTargetFeatureString().str());
   std::string compilerVersion = Fortran::common::getFlangToolFullVersion("bbc");
   std::string compilerOptions = "";
   Fortran::tools::setUpTargetCharacteristics(



More information about the llvm-branch-commits mailing list