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

via flang-commits flang-commits at lists.llvm.org
Fri Sep 11 11:06:21 PDT 2026


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

>From 5a3571228a7f108874b082ae3adfde5abb41c393 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/10] [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 dffece95796cc..b503da7d47f5e 100644
--- a/flang/lib/Semantics/check-omp-structure.cpp
+++ b/flang/lib/Semantics/check-omp-structure.cpp
@@ -80,18 +80,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();
   }
 }
 
@@ -207,38 +208,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();
 }
 
@@ -251,20 +253,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 &) {
@@ -272,7 +274,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);
 }
@@ -286,10 +288,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();
 }
@@ -650,6 +652,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 41b866337c4f1..e2dc55c43ef0b 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;
@@ -576,15 +598,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 5526b5cc3d08a..b6ff6e3e6b793 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::LoopSeq) {
+          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::LoopSeq) {
+        (void)checkRootLoopCanonical(*spec, /*isSequence=*/true);
       }
-    } else if (assoc == llvm::omp::Association::LoopSeq) {
-      (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::LoopSeq) {
-      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::LoopSeq) {
+        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 a341cf4fa1706..621db8e164914 100644
--- a/flang/lib/Semantics/openmp-utils.cpp
+++ b/flang/lib/Semantics/openmp-utils.cpp
@@ -2432,7 +2432,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 e3d0307b4e999c80ce6fee12b0deeca177fe2fe9 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/10] 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 e2dc55c43ef0b..f6fb8b001fc71 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 b6ff6e3e6b793..e54b2cbb27ed6 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 29d468efdac4f4b0193eb77dcd113703bf769603 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/10] 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     | 86 ++++++++++++-------
 .../metadirective-loop-applicability.f90      | 68 +++++++++++++++
 .../OpenMP/metadirective-match-any-paths.f90  | 71 +++++++++++++++
 .../OpenMP/metadirective-ordered-paths.f90    | 45 ++++++++++
 llvm/lib/Frontend/OpenMP/OMPContext.cpp       | 81 +++++++++--------
 llvm/unittests/Frontend/OpenMPContextTest.cpp | 66 +++++++++++++-
 6 files changed, 349 insertions(+), 68 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 e54b2cbb27ed6..9b56dd2d0bd2a 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::LoopSeq)) ||
+              (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,9 @@ 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::LoopSeq) {
-          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 +1090,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 +1132,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
diff --git a/llvm/lib/Frontend/OpenMP/OMPContext.cpp b/llvm/lib/Frontend/OpenMP/OMPContext.cpp
index f2cdd9bbaf5e4..ff65c866730dc 100644
--- a/llvm/lib/Frontend/OpenMP/OMPContext.cpp
+++ b/llvm/lib/Frontend/OpenMP/OMPContext.cpp
@@ -13,6 +13,7 @@
 //===----------------------------------------------------------------------===//
 
 #include "llvm/Frontend/OpenMP/OMPContext.h"
+#include "llvm/ADT/STLExtras.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/ADT/StringSwitch.h"
 #include "llvm/Support/Debug.h"
@@ -177,11 +178,10 @@ static bool isStrictSubset(const VariantMatchInfo &VMI0,
   return true;
 }
 
-static int
-isVariantApplicableInContextHelper(const VariantMatchInfo &VMI,
-                                   const OMPContext &Ctx,
-                                   SmallVectorImpl<unsigned> *ConstructMatches,
-                                   bool DeviceOrImplementationSetOnly) {
+static int isVariantApplicableInContextHelper(
+    const VariantMatchInfo &VMI, const OMPContext &Ctx,
+    SmallVectorImpl<std::optional<unsigned>> *ConstructMatches,
+    bool DeviceOrImplementationSetOnly) {
 
   // The match kind determines if we need to match all traits, any of the
   // traits, or none of the traits for it to be an applicable context.
@@ -197,23 +197,24 @@ isVariantApplicableInContextHelper(const VariantMatchInfo &VMI,
           unsigned(TraitProperty::implementation_extension_match_none)))
     MK = MK_NONE;
 
-  // Helper to deal with a single property that was (not) found in the OpenMP
-  // context based on the match kind selected by the user via
-  // `implementation={extensions(match_[all,any,none])}'
-  auto HandleTrait = [MK](TraitProperty Property,
-                          bool WasFound) -> std::optional<bool> /* Result */ {
-    // For kind "any" a single match is enough but we ignore non-matched
-    // properties.
+  bool AnyTraitMatched = false;
+
+  // Handle a single property that was (not) found in the OpenMP context based
+  // on the match kind selected by the user via
+  // `implementation={extensions(match_[all,any,none])}'. Keep inspecting
+  // traits after match_any succeeds so construct match positions needed for
+  // scoring are still recorded.
+  auto HandleTrait = [MK, &AnyTraitMatched](TraitProperty Property,
+                                            bool WasFound) -> bool {
+    AnyTraitMatched |= WasFound;
     if (MK == MK_ANY) {
-      if (WasFound)
-        return true;
-      return std::nullopt;
+      return true;
     }
 
     // In "all" or "none" mode we accept a matching or non-matching property
     // respectively and move on. We are not done yet!
     if ((WasFound && MK == MK_ALL) || (!WasFound && MK == MK_NONE))
-      return std::nullopt;
+      return true;
 
     // We missed a property, provide some debug output and indicate failure.
     LLVM_DEBUG({
@@ -243,6 +244,10 @@ isVariantApplicableInContextHelper(const VariantMatchInfo &VMI,
         TraitSelector::implementation_extension)
       continue;
 
+    // Construct traits require ordered matching and are handled below.
+    if (getOpenMPContextTraitSetForProperty(Property) == TraitSet::construct)
+      continue;
+
     bool IsActiveTrait = Ctx.ActiveTraits.test(unsigned(Property));
 
     // We overwrite the isa trait as it is actually up to the OMPContext hook to
@@ -256,8 +261,8 @@ isVariantApplicableInContextHelper(const VariantMatchInfo &VMI,
         return Ctx.matchesISATrait(RawString);
       });
 
-    if (std::optional<bool> Result = HandleTrait(Property, IsActiveTrait))
-      return *Result;
+    if (!HandleTrait(Property, IsActiveTrait))
+      return false;
   }
 
   if (!DeviceOrImplementationSetOnly) {
@@ -269,17 +274,21 @@ isVariantApplicableInContextHelper(const VariantMatchInfo &VMI,
                  TraitSet::construct &&
              "Variant context is ill-formed!");
 
-      // Verify the nesting.
+      // Verify the nesting. A failed match in match_any or match_none must not
+      // consume the remaining context, since a later selector property can
+      // still match.
+      unsigned SearchStart = ConstructIdx;
       bool FoundInOrder = false;
       while (!FoundInOrder && ConstructIdx != NoConstructTraits)
         FoundInOrder = (Ctx.ConstructTraits[ConstructIdx++] == Property);
+      if (!FoundInOrder && MK != MK_ALL)
+        ConstructIdx = SearchStart;
       if (ConstructMatches)
-        ConstructMatches->push_back(ConstructIdx - 1);
-
-      if (std::optional<bool> Result = HandleTrait(Property, FoundInOrder))
-        return *Result;
+        ConstructMatches->push_back(
+            FoundInOrder ? std::optional<unsigned>{ConstructIdx - 1}
+                         : std::nullopt);
 
-      if (!FoundInOrder) {
+      if (!HandleTrait(Property, FoundInOrder)) {
         LLVM_DEBUG(dbgs() << "[" << DEBUG_TYPE << "] Construct property "
                           << getOpenMPContextTraitPropertyName(Property, "")
                           << " was not nested properly.\n");
@@ -289,11 +298,13 @@ isVariantApplicableInContextHelper(const VariantMatchInfo &VMI,
       // TODO: Verify SIMD
     }
 
-    assert(isSubset<TraitProperty>(VMI.ConstructTraits, Ctx.ConstructTraits) &&
-           "Broken invariant!");
+    if (MK == MK_ALL)
+      assert(
+          isSubset<TraitProperty>(VMI.ConstructTraits, Ctx.ConstructTraits) &&
+          "Broken invariant!");
   }
 
-  if (MK == MK_ANY) {
+  if (MK == MK_ANY && !AnyTraitMatched) {
     LLVM_DEBUG(dbgs() << "[" << DEBUG_TYPE
                       << "] None of the properties was in the OpenMP context "
                          "but match kind is any.\n");
@@ -310,9 +321,9 @@ bool llvm::omp::isVariantApplicableInContext(
       VMI, Ctx, /* ConstructMatches */ nullptr, DeviceOrImplementationSetOnly);
 }
 
-static APInt getVariantMatchScore(const VariantMatchInfo &VMI,
-                                  const OMPContext &Ctx,
-                                  SmallVectorImpl<unsigned> &ConstructMatches) {
+static APInt getVariantMatchScore(
+    const VariantMatchInfo &VMI, const OMPContext &Ctx,
+    SmallVectorImpl<std::optional<unsigned>> &ConstructMatches) {
   APInt Score(64, 1);
 
   unsigned NoConstructTraits = VMI.ConstructTraits.size();
@@ -377,16 +388,18 @@ static APInt getVariantMatchScore(const VariantMatchInfo &VMI,
     }
   }
 
-  unsigned ConstructIdx = 0;
   assert(NoConstructTraits == ConstructMatches.size() &&
          "Mismatch in the construct traits!");
-  for (TraitProperty Property : VMI.ConstructTraits) {
+  for (auto [Property, Match] :
+       llvm::zip_equal(VMI.ConstructTraits, ConstructMatches)) {
     assert(getOpenMPContextTraitSetForProperty(Property) ==
                TraitSet::construct &&
            "Ill-formed variant match info!");
     (void)Property;
+    if (!Match)
+      continue;
     // ConstructMatches is the position p - 1 and we need 2^(p-1).
-    Score += (1ULL << ConstructMatches[ConstructIdx++]);
+    Score += (1ULL << *Match);
   }
 
   LLVM_DEBUG(dbgs() << "[" << DEBUG_TYPE << "] Variant has a score of " << Score
@@ -404,7 +417,7 @@ int llvm::omp::getBestVariantMatchForContext(
   for (unsigned u = 0, e = VMIs.size(); u < e; ++u) {
     const VariantMatchInfo &VMI = VMIs[u];
 
-    SmallVector<unsigned, 8> ConstructMatches;
+    SmallVector<std::optional<unsigned>, 8> ConstructMatches;
     // If the variant is not applicable its not the best.
     if (!isVariantApplicableInContextHelper(
             VMI, Ctx, &ConstructMatches,
diff --git a/llvm/unittests/Frontend/OpenMPContextTest.cpp b/llvm/unittests/Frontend/OpenMPContextTest.cpp
index f9683ae56e933..77a29a756c3e0 100644
--- a/llvm/unittests/Frontend/OpenMPContextTest.cpp
+++ b/llvm/unittests/Frontend/OpenMPContextTest.cpp
@@ -314,7 +314,71 @@ TEST_F(OpenMPContextTest, ApplicabilityAllTraits) {
 }
 
 TEST_F(OpenMPContextTest, ScoringSimple) {
-  // TODO: Add scoring tests (via getBestVariantMatchForContext).
+  OMPContext Parallel(false, Triple("x86_64-unknown-linux"), Triple(), -1);
+  Parallel.addTrait(TraitProperty::construct_parallel_parallel);
+  OMPContext NoConstruct(false, Triple("x86_64-unknown-linux"), Triple(), -1);
+
+  VariantMatchInfo MatchAny;
+  MatchAny.addTrait(TraitProperty::construct_target_target, "");
+  MatchAny.addTrait(TraitProperty::construct_parallel_parallel, "");
+  MatchAny.addTrait(TraitProperty::implementation_extension_match_any, "");
+  EXPECT_TRUE(isVariantApplicableInContext(MatchAny, Parallel));
+  EXPECT_FALSE(isVariantApplicableInContext(MatchAny, NoConstruct));
+
+  VariantMatchInfo VendorLLVM;
+  VendorLLVM.addTrait(TraitProperty::implementation_vendor_llvm, "");
+  // The matching construct must raise the score, not just win a tie by order.
+  SmallVector<VariantMatchInfo, 2> MatchAnyCandidates{VendorLLVM, MatchAny};
+  EXPECT_EQ(getBestVariantMatchForContext(MatchAnyCandidates, Parallel), 1);
+
+  VariantMatchInfo MatchNone;
+  MatchNone.addTrait(TraitProperty::construct_parallel_parallel, "");
+  MatchNone.addTrait(TraitProperty::implementation_extension_match_none, "");
+  EXPECT_TRUE(isVariantApplicableInContext(MatchNone, NoConstruct));
+  EXPECT_FALSE(isVariantApplicableInContext(MatchNone, Parallel));
+
+  VariantMatchInfo Empty;
+  SmallVector<VariantMatchInfo, 2> MatchNoneCandidates{MatchNone, Empty};
+  EXPECT_EQ(getBestVariantMatchForContext(MatchNoneCandidates, NoConstruct), 0);
+}
+
+TEST_F(OpenMPContextTest, ScoringMatchAnyConstructs) {
+  OMPContext TargetParallel(false, Triple("x86_64-unknown-linux"), Triple(),
+                            -1);
+  TargetParallel.addTrait(TraitProperty::construct_target_target);
+  TargetParallel.addTrait(TraitProperty::construct_parallel_parallel);
+
+  VariantMatchInfo Parallel;
+  Parallel.addTrait(TraitProperty::construct_parallel_parallel, "");
+
+  VariantMatchInfo MatchAny;
+  MatchAny.addTrait(TraitProperty::construct_target_target, "");
+  MatchAny.addTrait(TraitProperty::construct_parallel_parallel, "");
+  MatchAny.addTrait(TraitProperty::implementation_extension_match_any, "");
+
+  // Both construct matches contribute: 1 + 1 + 2 beats PARALLEL's 1 + 2.
+  // Stopping after the first match must not omit the later match's score.
+  SmallVector<VariantMatchInfo, 2> Candidates{Parallel, MatchAny};
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, TargetParallel), 1);
+}
+
+TEST_F(OpenMPContextTest, ScoringMatchAnyWithoutMatchingConstructs) {
+  OMPContext NoConstruct(false, Triple("x86_64-unknown-linux"), Triple(), -1);
+
+  VariantMatchInfo MatchAny;
+  MatchAny.addTrait(TraitProperty::construct_parallel_parallel, "");
+  MatchAny.addTrait(TraitProperty::implementation_vendor_llvm, "");
+  MatchAny.addTrait(TraitProperty::implementation_extension_match_any, "");
+  EXPECT_TRUE(isVariantApplicableInContext(MatchAny, NoConstruct));
+
+  APInt Score(64, 1);
+  VariantMatchInfo Scored;
+  Scored.addTrait(TraitProperty::user_condition_true, "", &Score);
+
+  // The vendor match makes MATCH_ANY applicable, but the absent construct
+  // must not add to its score. The scored candidate wins by 2 to 1.
+  SmallVector<VariantMatchInfo, 2> Candidates{MatchAny, Scored};
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, NoConstruct), 1);
 }
 
 } // namespace

>From b7b55b09eeea27455c37e2e358779f4e6541122c 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/10] 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 ++++---
 llvm/lib/Frontend/OpenMP/OMPContext.cpp       |  7 ++--
 4 files changed, 31 insertions(+), 32 deletions(-)

diff --git a/flang/lib/Semantics/check-omp-structure.h b/flang/lib/Semantics/check-omp-structure.h
index f6fb8b001fc71..aafd65fc76134 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 {
@@ -602,15 +605,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 9b56dd2d0bd2a..670be6804076c 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();
     }
   }
 
@@ -1130,7 +1123,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);
         }
@@ -1162,12 +1155,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
diff --git a/llvm/lib/Frontend/OpenMP/OMPContext.cpp b/llvm/lib/Frontend/OpenMP/OMPContext.cpp
index ff65c866730dc..80191bce2aec2 100644
--- a/llvm/lib/Frontend/OpenMP/OMPContext.cpp
+++ b/llvm/lib/Frontend/OpenMP/OMPContext.cpp
@@ -13,6 +13,7 @@
 //===----------------------------------------------------------------------===//
 
 #include "llvm/Frontend/OpenMP/OMPContext.h"
+#include "llvm/ADT/ArrayRef.h"
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/ADT/StringSwitch.h"
@@ -321,9 +322,9 @@ bool llvm::omp::isVariantApplicableInContext(
       VMI, Ctx, /* ConstructMatches */ nullptr, DeviceOrImplementationSetOnly);
 }
 
-static APInt getVariantMatchScore(
-    const VariantMatchInfo &VMI, const OMPContext &Ctx,
-    SmallVectorImpl<std::optional<unsigned>> &ConstructMatches) {
+static APInt
+getVariantMatchScore(const VariantMatchInfo &VMI, const OMPContext &Ctx,
+                     ArrayRef<std::optional<unsigned>> ConstructMatches) {
   APInt Score(64, 1);
 
   unsigned NoConstructTraits = VMI.ConstructTraits.size();

>From 7b1ee06bf87a8b01016b732be3a9df78efe50e60 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/10] 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          |  42 ++--
 .../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(+), 167 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 e0ee9d8249ee7..efb08e235d0b0 100644
--- a/flang/lib/Lower/OpenMP/OpenMP.cpp
+++ b/flang/lib/Lower/OpenMP/OpenMP.cpp
@@ -7654,6 +7654,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 aafd65fc76134..3b51bd6f9955b 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 670be6804076c..48ec6cf019ec1 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 621db8e164914..b60c7c2b5983e 100644
--- a/flang/lib/Semantics/openmp-utils.cpp
+++ b/flang/lib/Semantics/openmp-utils.cpp
@@ -2453,6 +2453,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,9 +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);
-  }
+  AppendConstructTraitsForSelector(traitName, vmi);
 }
 
 std::optional<DynamicUserCondition> MakeVariantMatchInfo(
@@ -2543,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) {
@@ -2668,11 +2682,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 2e29ccbbd0c35327ff015995b53b4077620b5cc3 Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Wed, 9 Sep 2026 13:25:26 -0500
Subject: [PATCH 06/10] Simplify construct match scoring

---
 llvm/lib/Frontend/OpenMP/OMPContext.cpp | 42 +++++++++----------------
 1 file changed, 14 insertions(+), 28 deletions(-)

diff --git a/llvm/lib/Frontend/OpenMP/OMPContext.cpp b/llvm/lib/Frontend/OpenMP/OMPContext.cpp
index 80191bce2aec2..fa2025f251502 100644
--- a/llvm/lib/Frontend/OpenMP/OMPContext.cpp
+++ b/llvm/lib/Frontend/OpenMP/OMPContext.cpp
@@ -13,8 +13,6 @@
 //===----------------------------------------------------------------------===//
 
 #include "llvm/Frontend/OpenMP/OMPContext.h"
-#include "llvm/ADT/ArrayRef.h"
-#include "llvm/ADT/STLExtras.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/ADT/StringSwitch.h"
 #include "llvm/Support/Debug.h"
@@ -179,10 +177,11 @@ static bool isStrictSubset(const VariantMatchInfo &VMI0,
   return true;
 }
 
-static int isVariantApplicableInContextHelper(
-    const VariantMatchInfo &VMI, const OMPContext &Ctx,
-    SmallVectorImpl<std::optional<unsigned>> *ConstructMatches,
-    bool DeviceOrImplementationSetOnly) {
+static int
+isVariantApplicableInContextHelper(const VariantMatchInfo &VMI,
+                                   const OMPContext &Ctx,
+                                   SmallVectorImpl<unsigned> *ConstructMatches,
+                                   bool DeviceOrImplementationSetOnly) {
 
   // The match kind determines if we need to match all traits, any of the
   // traits, or none of the traits for it to be an applicable context.
@@ -245,10 +244,6 @@ static int isVariantApplicableInContextHelper(
         TraitSelector::implementation_extension)
       continue;
 
-    // Construct traits require ordered matching and are handled below.
-    if (getOpenMPContextTraitSetForProperty(Property) == TraitSet::construct)
-      continue;
-
     bool IsActiveTrait = Ctx.ActiveTraits.test(unsigned(Property));
 
     // We overwrite the isa trait as it is actually up to the OMPContext hook to
@@ -284,10 +279,8 @@ static int isVariantApplicableInContextHelper(
         FoundInOrder = (Ctx.ConstructTraits[ConstructIdx++] == Property);
       if (!FoundInOrder && MK != MK_ALL)
         ConstructIdx = SearchStart;
-      if (ConstructMatches)
-        ConstructMatches->push_back(
-            FoundInOrder ? std::optional<unsigned>{ConstructIdx - 1}
-                         : std::nullopt);
+      if (ConstructMatches && FoundInOrder)
+        ConstructMatches->push_back(ConstructIdx - 1);
 
       if (!HandleTrait(Property, FoundInOrder)) {
         LLVM_DEBUG(dbgs() << "[" << DEBUG_TYPE << "] Construct property "
@@ -322,9 +315,9 @@ bool llvm::omp::isVariantApplicableInContext(
       VMI, Ctx, /* ConstructMatches */ nullptr, DeviceOrImplementationSetOnly);
 }
 
-static APInt
-getVariantMatchScore(const VariantMatchInfo &VMI, const OMPContext &Ctx,
-                     ArrayRef<std::optional<unsigned>> ConstructMatches) {
+static APInt getVariantMatchScore(const VariantMatchInfo &VMI,
+                                  const OMPContext &Ctx,
+                                  SmallVectorImpl<unsigned> &ConstructMatches) {
   APInt Score(64, 1);
 
   unsigned NoConstructTraits = VMI.ConstructTraits.size();
@@ -389,18 +382,11 @@ getVariantMatchScore(const VariantMatchInfo &VMI, const OMPContext &Ctx,
     }
   }
 
-  assert(NoConstructTraits == ConstructMatches.size() &&
+  assert(NoConstructTraits >= ConstructMatches.size() &&
          "Mismatch in the construct traits!");
-  for (auto [Property, Match] :
-       llvm::zip_equal(VMI.ConstructTraits, ConstructMatches)) {
-    assert(getOpenMPContextTraitSetForProperty(Property) ==
-               TraitSet::construct &&
-           "Ill-formed variant match info!");
-    (void)Property;
-    if (!Match)
-      continue;
+  for (unsigned Match : ConstructMatches) {
     // ConstructMatches is the position p - 1 and we need 2^(p-1).
-    Score += (1ULL << *Match);
+    Score += (1ULL << Match);
   }
 
   LLVM_DEBUG(dbgs() << "[" << DEBUG_TYPE << "] Variant has a score of " << Score
@@ -418,7 +404,7 @@ int llvm::omp::getBestVariantMatchForContext(
   for (unsigned u = 0, e = VMIs.size(); u < e; ++u) {
     const VariantMatchInfo &VMI = VMIs[u];
 
-    SmallVector<std::optional<unsigned>, 8> ConstructMatches;
+    SmallVector<unsigned, 8> ConstructMatches;
     // If the variant is not applicable its not the best.
     if (!isVariantApplicableInContextHelper(
             VMI, Ctx, &ConstructMatches,

>From 17755921c1ee27a42346d29faa2b8567598c225e Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Thu, 10 Sep 2026 11:58:17 -0500
Subject: [PATCH 07/10] Preserve SIMD context for variant selection

SIMD selectors cannot match when lowering omits the enclosing SIMD trait.
Collect it so calls inside SIMD and DO SIMD select the correct variant.
---
 flang/lib/Lower/OpenMP/Utils.cpp              |  2 +
 .../OpenMP/declare-variant-construct.f90      | 59 ++++++++++++++++++-
 2 files changed, 59 insertions(+), 2 deletions(-)

diff --git a/flang/lib/Lower/OpenMP/Utils.cpp b/flang/lib/Lower/OpenMP/Utils.cpp
index 4244662ad4290..20db217e90c5f 100644
--- a/flang/lib/Lower/OpenMP/Utils.cpp
+++ b/flang/lib/Lower/OpenMP/Utils.cpp
@@ -1442,6 +1442,8 @@ void collectEnclosingConstructTraits(
   // be able to match construct={target, parallel}. The final reverse yields
   // outermost-to-innermost order as required by OMPContext.
   for (; op; op = op->getParentOp()) {
+    if (mlir::isa<mlir::omp::SimdOp>(op))
+      constructTraits.push_back(llvm::omp::TraitProperty::construct_simd_simd);
     if (mlir::isa<mlir::omp::WsloopOp>(op))
       constructTraits.push_back(llvm::omp::TraitProperty::construct_for_for);
     if (mlir::isa<mlir::omp::ParallelOp>(op))
diff --git a/flang/test/Lower/OpenMP/declare-variant-construct.f90 b/flang/test/Lower/OpenMP/declare-variant-construct.f90
index ca72b5c8e47af..da5897378787a 100644
--- a/flang/test/Lower/OpenMP/declare-variant-construct.f90
+++ b/flang/test/Lower/OpenMP/declare-variant-construct.f90
@@ -1,8 +1,8 @@
 ! RUN: %flang_fc1 -emit-fir -fopenmp -fopenmp-version=51 %s -o - | FileCheck %s
 ! RUN: %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=51 %s -o - | FileCheck %s
 
-! DECLARE VARIANT callee resolution with combined/composite construct
-! selectors. The bases and their variants are sibling module procedures, so
+! DECLARE VARIANT callee resolution with construct selectors, including
+! combined/composite selectors. Bases and variants are sibling procedures, so
 ! each variant is accessible at every reference to its base.
 
 module m
@@ -49,6 +49,18 @@ end subroutine vsub_lo
   subroutine vsub_hi
   end subroutine vsub_hi
 
+  subroutine base_simd
+    !$omp declare variant (vsub_simd) match (construct={simd})
+  end subroutine base_simd
+  subroutine vsub_simd
+  end subroutine vsub_simd
+
+  subroutine base_do_simd
+    !$omp declare variant (vsub_do_simd) match (construct={do, simd})
+  end subroutine base_do_simd
+  subroutine vsub_do_simd
+  end subroutine vsub_do_simd
+
   ! The combined directive selector decomposes to {target, teams}; it matches
   ! only when both constructs enclose the call.
 
@@ -157,4 +169,47 @@ end subroutine test_rank_parallel_only
   subroutine test_score_ranking
     call base_score()
   end subroutine test_score_ranking
+
+  ! Without an enclosing SIMD construct, neither selector matches.
+
+  ! CHECK-LABEL: func.func @_QMmPtest_outside_simd(
+  ! CHECK: fir.call @_QMmPbase_simd()
+  ! CHECK: fir.call @_QMmPbase_do_simd()
+  subroutine test_outside_simd
+    call base_simd()
+    call base_do_simd()
+  end subroutine test_outside_simd
+
+  ! SIMD supplies its construct trait, but not the DO trait.
+
+  ! CHECK-LABEL: func.func @_QMmPtest_inside_simd(
+  ! CHECK: omp.simd
+  ! CHECK: omp.loop_nest
+  ! CHECK: fir.call @_QMmPvsub_simd()
+  ! CHECK: fir.call @_QMmPbase_do_simd()
+  subroutine test_inside_simd(n)
+    integer :: n, i
+    !$omp simd
+    do i = 1, n
+      call base_simd()
+      call base_do_simd()
+    end do
+  end subroutine test_inside_simd
+
+  ! DO SIMD supplies both traits in DO -> SIMD order.
+
+  ! CHECK-LABEL: func.func @_QMmPtest_inside_do_simd(
+  ! CHECK: omp.wsloop
+  ! CHECK: omp.simd
+  ! CHECK: omp.loop_nest
+  ! CHECK: fir.call @_QMmPvsub_simd()
+  ! CHECK: fir.call @_QMmPvsub_do_simd()
+  subroutine test_inside_do_simd(n)
+    integer :: n, i
+    !$omp do simd
+    do i = 1, n
+      call base_simd()
+      call base_do_simd()
+    end do
+  end subroutine test_inside_do_simd
 end module m

>From f1e90900fa1cfd9157cda3cfb1b022b1bf63c5e3 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 08/10] 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 ++-
 flang/lib/Semantics/openmp-utils.cpp          |  23 +--
 .../OpenMP/declare-variant-construct.f90      |  20 +++
 .../OpenMP/metadirective-implementation.f90   |  12 ++
 .../test/Lower/OpenMP/metadirective-user.f90  |  97 +++++++++++-
 .../OpenMP/metadirective-construct-paths.f90  | 146 ++++++++++++++++++
 llvm/lib/Frontend/OpenMP/OMPContext.cpp       |  20 ++-
 llvm/unittests/Frontend/OpenMPContextTest.cpp |  67 ++++++++
 10 files changed, 394 insertions(+), 24 deletions(-)

diff --git a/flang/lib/Semantics/check-omp-structure.cpp b/flang/lib/Semantics/check-omp-structure.cpp
index b503da7d47f5e..771a77598a872 100644
--- a/flang/lib/Semantics/check-omp-structure.cpp
+++ b/flang/lib/Semantics/check-omp-structure.cpp
@@ -217,6 +217,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
@@ -225,6 +226,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();
 }
@@ -275,6 +284,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 3b51bd6f9955b..c076ff2cdb370 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 48ec6cf019ec1..d46f57df5ccb4 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_);
@@ -1187,6 +1204,7 @@ void OmpStructureChecker::CheckPendingLoopDirectivesWithoutLoop(
     }
   }
   pendingLoopDirectiveGroups_.erase(first, pendingLoopDirectiveGroups_.end());
+  UpdatePendingLoopDirectiveScopeStarts();
 }
 
 static const parser::traits::OmpContextSelectorSpecification *
diff --git a/flang/lib/Semantics/openmp-utils.cpp b/flang/lib/Semantics/openmp-utils.cpp
index b60c7c2b5983e..f8a0eec4c8c24 100644
--- a/flang/lib/Semantics/openmp-utils.cpp
+++ b/flang/lib/Semantics/openmp-utils.cpp
@@ -2650,26 +2650,19 @@ std::optional<MetadirectiveCandidateSet> BuildMetadirectiveCandidateSet(
 
         if (hasMatchAny && isStaticVMIApplicable) {
           // Represent both outcomes: a guarded candidate with the condition's
-          // score and an unguarded candidate with only the static traits. If
-          // the WHEN clause omits its directive, only add the unguarded
-          // candidate.
-          if (isExplicit) {
-            llvm::omp::VariantMatchInfo conditionTrueVMI{staticVMI};
-            addConditionTraitForRanking(conditionTrueVMI);
-            result.candidates.push_back({spec, std::move(conditionTrueVMI),
-                isExplicit, dynamicCondition});
-          }
+          // score and an unguarded candidate with only the static traits.
+          llvm::omp::VariantMatchInfo conditionTrueVMI{staticVMI};
+          addConditionTraitForRanking(conditionTrueVMI);
+          result.candidates.push_back({spec, std::move(conditionTrueVMI),
+              isExplicit, dynamicCondition});
           result.candidates.push_back({spec, std::move(staticVMI), isExplicit});
           continue;
         }
 
         llvm::omp::VariantMatchInfo rankingVMI{staticVMI};
-        // Preserve the existing lowering behavior for an omitted directive:
-        // do not let its runtime condition raise the implicit NOTHING rank.
-        if (!isExplicit && hasMatchAny && !isStaticVMIApplicable)
-          rankingVMI = llvm::omp::VariantMatchInfo();
-        else if (isExplicit)
-          addConditionTraitForRanking(rankingVMI);
+        // Implicit NOTHING participates in scoring just like an explicit
+        // replacement; explicitness only breaks ties between equal scores.
+        addConditionTraitForRanking(rankingVMI);
         result.candidates.push_back({spec, std::move(rankingVMI), isExplicit,
             dynamicCondition, /*conditionShouldBeTrue=*/!hasMatchNone});
         continue;
diff --git a/flang/test/Lower/OpenMP/declare-variant-construct.f90 b/flang/test/Lower/OpenMP/declare-variant-construct.f90
index da5897378787a..6b3bfe82c4b69 100644
--- a/flang/test/Lower/OpenMP/declare-variant-construct.f90
+++ b/flang/test/Lower/OpenMP/declare-variant-construct.f90
@@ -61,6 +61,26 @@ end subroutine base_do_simd
   subroutine vsub_do_simd
   end subroutine vsub_do_simd
 
+  subroutine base_repeated
+    !$omp declare variant (vsub_lo) &
+    !$omp& match(user={condition(score(1): .true.)})
+    !$omp declare variant (vsub_par) match(construct={parallel})
+  end subroutine base_repeated
+
+  ! The inner PARALLEL raises the construct score above the user score.
+  ! CHECK-LABEL: func.func @_QMmPtest_repeated_parallel(
+  ! CHECK: omp.parallel
+  ! CHECK: omp.parallel
+  ! CHECK-NOT: fir.call @_QMmPvsub_lo
+  ! CHECK: fir.call @_QMmPvsub_par()
+  subroutine test_repeated_parallel
+    !$omp parallel
+      !$omp parallel
+        call base_repeated()
+      !$omp end parallel
+    !$omp end parallel
+  end subroutine test_repeated_parallel
+
   ! The combined directive selector decomposes to {target, teams}; it matches
   ! only when both constructs enclose the call.
 
diff --git a/flang/test/Lower/OpenMP/metadirective-implementation.f90 b/flang/test/Lower/OpenMP/metadirective-implementation.f90
index af8f2af938ca5..6ad4c93483116 100644
--- a/flang/test/Lower/OpenMP/metadirective-implementation.f90
+++ b/flang/test/Lower/OpenMP/metadirective-implementation.f90
@@ -5,6 +5,18 @@
 ! RUN: %flang_fc1 -fopenmp -emit-hlfir -fopenmp-version=51 %s -o - | FileCheck %s
 ! RUN: %flang_fc1 -fopenmp -emit-hlfir -fopenmp-version=52 -cpp -DOMP_52 %s -o - | FileCheck %s
 
+! An unknown vendor matches MATCH_NONE and can win a ranking tie.
+! CHECK-LABEL: func.func @_QPtest_unknown_vendor_match_none()
+! CHECK-NOT: omp.barrier
+! CHECK: omp.taskyield
+! CHECK-NEXT: return
+subroutine test_unknown_vendor_match_none()
+  !$omp metadirective &
+  !$omp& when(implementation={vendor(bogus_vendor), extension(match_none)}: &
+  !$omp& taskyield) &
+  !$omp& when(user={condition(.true.)}: barrier)
+end subroutine
+
 ! CHECK-LABEL: func.func @_QPtest_vendor_llvm()
 ! CHECK:         omp.taskwait
 ! CHECK:         return
diff --git a/flang/test/Lower/OpenMP/metadirective-user.f90 b/flang/test/Lower/OpenMP/metadirective-user.f90
index cdfbddd4151a0..eefa48315d6fa 100644
--- a/flang/test/Lower/OpenMP/metadirective-user.f90
+++ b/flang/test/Lower/OpenMP/metadirective-user.f90
@@ -4,6 +4,90 @@
 ! RUN: %flang_fc1 -fopenmp -emit-hlfir -fopenmp-version=51 %s -o - | FileCheck %s
 ! RUN: %flang_fc1 -fopenmp -emit-hlfir -fopenmp-version=52 -cpp -DOMP_52 %s -o - | FileCheck %s
 
+!===----------------------------------------------------------------------===!
+! Scored implicit NOTHING competes with explicit replacements by score.
+!===----------------------------------------------------------------------===!
+
+! CHECK-LABEL: func.func @_QPtest_implicit_nothing_score(
+! CHECK: fir.if
+! CHECK-NEXT: } else {
+! CHECK-NEXT: omp.barrier
+! CHECK: return
+subroutine test_implicit_nothing_score(flag)
+  logical :: flag
+  !$omp metadirective &
+  !$omp& when(user={condition(score(10): flag)}:) &
+  !$omp& when(user={condition(score(5): .true.)}: barrier)
+end subroutine
+
+! Explicit NOTHING with the same score must produce the same selection.
+! CHECK-LABEL: func.func @_QPtest_explicit_nothing_score(
+! CHECK: fir.if
+! CHECK-NEXT: } else {
+! CHECK-NEXT: omp.barrier
+! CHECK: return
+subroutine test_explicit_nothing_score(flag)
+  logical :: flag
+  !$omp metadirective &
+  !$omp& when(user={condition(score(10): flag)}: nothing) &
+  !$omp& when(user={condition(score(5): .true.)}: barrier)
+end subroutine
+
+! Equal scores favor the explicit replacement without a runtime branch.
+! CHECK-LABEL: func.func @_QPtest_implicit_nothing_equal_score(
+! CHECK-NOT: fir.if
+! CHECK: omp.barrier
+! CHECK-NEXT: return
+subroutine test_implicit_nothing_equal_score(flag)
+  logical :: flag
+  !$omp metadirective &
+  !$omp& when(user={condition(score(5): flag)}:) &
+  !$omp& when(user={condition(score(5): .true.)}: barrier)
+end subroutine
+
+! MATCH_ANY still needs a scored runtime candidate when a static trait matches.
+! CHECK-LABEL: func.func @_QPtest_implicit_nothing_match_any_static(
+! CHECK: fir.if
+! CHECK-NEXT: } else {
+! CHECK-NEXT: omp.barrier
+! CHECK: return
+subroutine test_implicit_nothing_match_any_static(flag)
+  logical :: flag
+  !$omp metadirective &
+  !$omp& when(implementation={vendor(llvm), extension(match_any)}, &
+  !$omp& user={condition(score(10): flag)}:) &
+  !$omp& when(user={condition(score(5): .true.)}: barrier)
+end subroutine
+
+! MATCH_ANY can also depend entirely on the runtime condition.
+! CHECK-LABEL: func.func @_QPtest_implicit_nothing_match_any_runtime(
+! CHECK: fir.if
+! CHECK-NEXT: } else {
+! CHECK-NEXT: omp.barrier
+! CHECK: return
+subroutine test_implicit_nothing_match_any_runtime(flag)
+  logical :: flag
+  !$omp metadirective &
+  !$omp& when(implementation={vendor(gnu), extension(match_any)}, &
+  !$omp& user={condition(score(10): flag)}:) &
+  !$omp& when(user={condition(score(5): .true.)}: barrier)
+end subroutine
+
+! MATCH_NONE retains the score but selects NOTHING when the condition is false.
+! CHECK-LABEL: func.func @_QPtest_implicit_nothing_match_none(
+! CHECK: arith.xori
+! CHECK: fir.if
+! CHECK-NEXT: } else {
+! CHECK-NEXT: omp.barrier
+! CHECK: return
+subroutine test_implicit_nothing_match_none(flag)
+  logical :: flag
+  !$omp metadirective &
+  !$omp& when(implementation={extension(match_none)}, &
+  !$omp& user={condition(score(10): flag)}:) &
+  !$omp& when(user={condition(score(5): .true.)}: barrier)
+end subroutine
+
 !===----------------------------------------------------------------------===!
 ! Static (constant-folded) user conditions
 !===----------------------------------------------------------------------===!
@@ -425,13 +509,14 @@ subroutine test_dynamic_user_match_any_static_score(flag)
 #endif
 end subroutine
 
-! The explicit directive variant wins this tie over the earlier implicit
-! nothing candidate.
-! CHECK-LABEL: func.func @_QPtest_dynamic_implicit_nothing_tie_break(
-! CHECK-NOT:     fir.if
-! CHECK:         omp.barrier
+! The vendor-only selector is a strict subset of the implicit NOTHING's
+! selector. Retaining the user condition makes this a runtime choice, not a tie.
+! CHECK-LABEL: func.func @_QPtest_dynamic_implicit_nothing_more_specific(
+! CHECK:         fir.if
+! CHECK-NEXT:    } else {
+! CHECK-NEXT:      omp.barrier
 ! CHECK:         return
-subroutine test_dynamic_implicit_nothing_tie_break(flag)
+subroutine test_dynamic_implicit_nothing_more_specific(flag)
   logical, intent(in) :: flag
   !$omp metadirective &
   !$omp & when(implementation={vendor(llvm)}, user={condition(flag)}:) &
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
diff --git a/llvm/lib/Frontend/OpenMP/OMPContext.cpp b/llvm/lib/Frontend/OpenMP/OMPContext.cpp
index fa2025f251502..edf05dc758c71 100644
--- a/llvm/lib/Frontend/OpenMP/OMPContext.cpp
+++ b/llvm/lib/Frontend/OpenMP/OMPContext.cpp
@@ -292,6 +292,22 @@ isVariantApplicableInContextHelper(const VariantMatchInfo &VMI,
       // TODO: Verify SIMD
     }
 
+    // A complete ordered match can have several embeddings in the context.
+    // Match backwards to choose the highest-valued one for scoring. Keep the
+    // forward scan's partial matches for the match_any extension.
+    if (ConstructMatches &&
+        ConstructMatches->size() == VMI.ConstructTraits.size()) {
+      ConstructIdx = NoConstructTraits;
+      for (unsigned I = VMI.ConstructTraits.size(); I > 0; --I) {
+        TraitProperty Property = VMI.ConstructTraits[I - 1];
+        while (ConstructIdx > 0 &&
+               Ctx.ConstructTraits[ConstructIdx - 1] != Property)
+          --ConstructIdx;
+        assert(ConstructIdx > 0 && "Previously matched construct not found!");
+        (*ConstructMatches)[I - 1] = --ConstructIdx;
+      }
+    }
+
     if (MK == MK_ALL)
       assert(
           isSubset<TraitProperty>(VMI.ConstructTraits, Ctx.ConstructTraits) &&
@@ -349,7 +365,9 @@ static APInt getVariantMatchScore(const VariantMatchInfo &VMI,
       // TODO: Handling separately.
       break;
     case TraitSet::invalid:
-      llvm_unreachable("Unknown trait set is not to be used!");
+      // An unknown property can be applicable under match_any or match_none,
+      // but contributes no score of its own.
+      continue;
     }
 
     // device={kind(any)} is "as if" no kind selector was specified.
diff --git a/llvm/unittests/Frontend/OpenMPContextTest.cpp b/llvm/unittests/Frontend/OpenMPContextTest.cpp
index 77a29a756c3e0..f04b2dd45bc94 100644
--- a/llvm/unittests/Frontend/OpenMPContextTest.cpp
+++ b/llvm/unittests/Frontend/OpenMPContextTest.cpp
@@ -381,4 +381,71 @@ TEST_F(OpenMPContextTest, ScoringMatchAnyWithoutMatchingConstructs) {
   EXPECT_EQ(getBestVariantMatchForContext(Candidates, NoConstruct), 1);
 }
 
+TEST_F(OpenMPContextTest, ScoringUnknownProperty) {
+  OMPContext Context(false, Triple("x86_64-unknown-linux"), Triple(), -1);
+  VariantMatchInfo Unknown;
+  Unknown.addTrait(TraitProperty::invalid, "bogus_vendor");
+  EXPECT_FALSE(isVariantApplicableInContext(Unknown, Context));
+  Unknown.addTrait(TraitProperty::implementation_extension_match_none, "");
+  EXPECT_TRUE(isVariantApplicableInContext(Unknown, Context));
+
+  VariantMatchInfo UserTrue;
+  UserTrue.addTrait(TraitProperty::user_condition_true, "");
+  SmallVector<VariantMatchInfo, 2> Candidates{Unknown, UserTrue};
+  // An unknown property contributes zero, so lexical order breaks the tie.
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 0);
+
+  APInt Score(64, 1);
+  Candidates[1].addTrait(TraitProperty::user_condition_true, "", &Score);
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 1);
+}
+
+TEST_F(OpenMPContextTest, ScoringRepeatedConstructs) {
+  OMPContext Context(false, Triple("x86_64-unknown-linux"), Triple(), -1);
+  Context.addTrait(TraitProperty::construct_parallel_parallel);
+  Context.addTrait(TraitProperty::construct_parallel_parallel);
+
+  APInt Score(64, 1);
+  VariantMatchInfo Scored;
+  Scored.addTrait(TraitProperty::user_condition_true, "", &Score);
+  VariantMatchInfo Parallel;
+  Parallel.addTrait(TraitProperty::construct_parallel_parallel, "");
+  SmallVector<VariantMatchInfo, 2> Candidates{Scored, Parallel};
+  // The inner PARALLEL scores 3, beating the explicit score's 2.
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 1);
+  Candidates[1].addTrait(TraitProperty::implementation_extension_match_any, "");
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 1);
+
+  // An incomplete match_any selector retains its forward partial match.
+  VariantMatchInfo Partial;
+  Partial.addTrait(TraitProperty::construct_target_target, "");
+  Partial.addTrait(TraitProperty::construct_parallel_parallel, "");
+  Partial.addTrait(TraitProperty::implementation_extension_match_any, "");
+  Candidates[1] = Partial;
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 0);
+}
+
+TEST_F(OpenMPContextTest, ScoringHighestOrderedMatch) {
+  OMPContext Context(false, Triple("x86_64-unknown-linux"), Triple(), -1);
+  Context.addTrait(TraitProperty::construct_parallel_parallel);
+  Context.addTrait(TraitProperty::construct_for_for);
+  Context.addTrait(TraitProperty::construct_parallel_parallel);
+  Context.addTrait(TraitProperty::construct_for_for);
+  Context.addTrait(TraitProperty::construct_parallel_parallel);
+
+  APInt Score(64, 11);
+  VariantMatchInfo Scored;
+  Scored.addTrait(TraitProperty::user_condition_true, "", &Score);
+  VariantMatchInfo ParallelFor;
+  ParallelFor.addTrait(TraitProperty::construct_parallel_parallel, "");
+  ParallelFor.addTrait(TraitProperty::construct_for_for, "");
+  SmallVector<VariantMatchInfo, 2> Candidates{Scored, ParallelFor};
+  // Positions 3 and 4 score 1 + 4 + 8 = 13. The final PARALLEL cannot be
+  // chosen because it follows every FOR in the context.
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 1);
+  Score = APInt(64, 12);
+  Candidates[0].addTrait(TraitProperty::user_condition_true, "", &Score);
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 0);
+}
+
 } // namespace

>From 839abb70937289212bd0acb5d7662d056ed78aef 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 09/10] 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/Lower/OpenMP/Utils.cpp              |  6 +-
 flang/lib/Semantics/check-omp-variant.cpp     | 11 ++--
 flang/lib/Semantics/openmp-utils.cpp          |  8 +--
 .../OpenMP/declare-variant-construct.f90      | 17 +++++
 .../Lower/OpenMP/metadirective-construct.f90  |  3 +-
 .../OpenMP/metadirective-implementation.f90   |  2 +-
 .../OpenMP/metadirective-target-boundary.f90  | 63 +++++++++++++++++++
 .../test/Lower/OpenMP/metadirective-user.f90  |  2 +-
 .../OpenMP/metadirective-construct-paths.f90  |  8 +--
 llvm/lib/Frontend/OpenMP/OMPContext.cpp       | 12 ++--
 11 files changed, 106 insertions(+), 29 deletions(-)
 create mode 100644 flang/test/Lower/OpenMP/metadirective-target-boundary.f90

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/Lower/OpenMP/Utils.cpp b/flang/lib/Lower/OpenMP/Utils.cpp
index 20db217e90c5f..0b8575a5ef724 100644
--- a/flang/lib/Lower/OpenMP/Utils.cpp
+++ b/flang/lib/Lower/OpenMP/Utils.cpp
@@ -1452,9 +1452,13 @@ void collectEnclosingConstructTraits(
     if (mlir::isa<mlir::omp::TeamsOp>(op))
       constructTraits.push_back(
           llvm::omp::TraitProperty::construct_teams_teams);
-    if (mlir::isa<mlir::omp::TargetOp>(op))
+    if (mlir::isa<mlir::omp::TargetOp>(op)) {
       constructTraits.push_back(
           llvm::omp::TraitProperty::construct_target_target);
+      // The construct context starts at the innermost TARGET, as in
+      // semantic analysis.
+      break;
+    }
   }
   std::reverse(constructTraits.begin(), constructTraits.end());
 }
diff --git a/flang/lib/Semantics/check-omp-variant.cpp b/flang/lib/Semantics/check-omp-variant.cpp
index d46f57df5ccb4..088b6e6b414f3 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 f8a0eec4c8c24..588c9fe01a1a4 100644
--- a/flang/lib/Semantics/openmp-utils.cpp
+++ b/flang/lib/Semantics/openmp-utils.cpp
@@ -2428,10 +2428,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/declare-variant-construct.f90 b/flang/test/Lower/OpenMP/declare-variant-construct.f90
index 6b3bfe82c4b69..f62d0dcd2e8ea 100644
--- a/flang/test/Lower/OpenMP/declare-variant-construct.f90
+++ b/flang/test/Lower/OpenMP/declare-variant-construct.f90
@@ -232,4 +232,21 @@ subroutine test_inside_do_simd(n)
       call base_do_simd()
     end do
   end subroutine test_inside_do_simd
+
+  ! TARGET hides the outer PARALLEL during callee selection as well.
+
+  ! CHECK-LABEL: func.func @_QMmPtest_target_boundary(
+  ! CHECK: omp.parallel
+  ! CHECK: omp.target
+  ! CHECK: fir.call @_QMmPbase_tp2()
+  ! CHECK-NEXT: fir.call @_QMmPbase_rank()
+  ! CHECK: return
+  subroutine test_target_boundary
+    !$omp parallel
+      !$omp target
+        call base_tp2()
+        call base_rank()
+      !$omp end target
+    !$omp end parallel
+  end subroutine test_target_boundary
 end module m
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-implementation.f90 b/flang/test/Lower/OpenMP/metadirective-implementation.f90
index 6ad4c93483116..cd61b733cbb1d 100644
--- a/flang/test/Lower/OpenMP/metadirective-implementation.f90
+++ b/flang/test/Lower/OpenMP/metadirective-implementation.f90
@@ -5,7 +5,7 @@
 ! RUN: %flang_fc1 -fopenmp -emit-hlfir -fopenmp-version=51 %s -o - | FileCheck %s
 ! RUN: %flang_fc1 -fopenmp -emit-hlfir -fopenmp-version=52 -cpp -DOMP_52 %s -o - | FileCheck %s
 
-! An unknown vendor matches MATCH_NONE and can win a ranking tie.
+! MATCH_NONE accepts an unknown vendor; clause order breaks the scoring tie.
 ! CHECK-LABEL: func.func @_QPtest_unknown_vendor_match_none()
 ! 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
new file mode 100644
index 0000000000000..f80f4a8bec678
--- /dev/null
+++ b/flang/test/Lower/OpenMP/metadirective-target-boundary.f90
@@ -0,0 +1,63 @@
+! RUN: %flang_fc1 -fopenmp -fopenmp-version=51 -emit-fir %s -o - | FileCheck %s
+! RUN: %flang_fc1 -fopenmp -fopenmp-version=51 -emit-hlfir %s -o - | \
+! RUN:   FileCheck %s
+
+! 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
+! CHECK-NOT: omp.simd
+! CHECK: return
+subroutine actual_target(n, a)
+  integer :: n, i, a(n)
+  !$omp parallel
+    !$omp target
+      !$omp metadirective &
+      !$omp& when(construct={parallel}: simd collapse(2)) default(nothing)
+      do i = 1, n
+        a(i) = i
+      end do
+    !$omp end target
+  !$omp end parallel
+end subroutine
+
+! A TARGET selected by a metadirective creates the same context boundary.
+! CHECK-LABEL: func.func @_QPselected_target(
+! CHECK: omp.parallel
+! CHECK: omp.target
+! CHECK-NOT: omp.simd
+! CHECK: return
+subroutine selected_target(n, a)
+  integer :: n, i, a(n)
+  !$omp parallel
+    !$omp begin metadirective default(target)
+      !$omp metadirective &
+      !$omp& when(construct={parallel, target}: simd collapse(2)) &
+      !$omp& default(nothing)
+      do i = 1, n
+        a(i) = i
+      end do
+    !$omp end metadirective
+  !$omp end parallel
+end subroutine
+
+! The boundary includes TARGET itself and constructs nested inside it.
+! CHECK-LABEL: func.func @_QPtarget_inner_parallel()
+! CHECK: omp.parallel
+! CHECK: omp.target
+! CHECK: omp.parallel
+! CHECK-NOT: omp.taskyield
+! CHECK: omp.barrier
+! CHECK-NOT: omp.taskyield
+! CHECK: return
+subroutine target_inner_parallel()
+  !$omp parallel
+    !$omp target
+      !$omp parallel
+        !$omp metadirective when(construct={target, parallel}: barrier) &
+        !$omp& default(taskyield)
+      !$omp end parallel
+    !$omp end target
+  !$omp end parallel
+end subroutine
diff --git a/flang/test/Lower/OpenMP/metadirective-user.f90 b/flang/test/Lower/OpenMP/metadirective-user.f90
index eefa48315d6fa..ce2d79b1ee688 100644
--- a/flang/test/Lower/OpenMP/metadirective-user.f90
+++ b/flang/test/Lower/OpenMP/metadirective-user.f90
@@ -510,7 +510,7 @@ subroutine test_dynamic_user_match_any_static_score(flag)
 end subroutine
 
 ! The vendor-only selector is a strict subset of the implicit NOTHING's
-! selector. Retaining the user condition makes this a runtime choice, not a tie.
+! selector. The user condition determines which replacement is selected.
 ! CHECK-LABEL: func.func @_QPtest_dynamic_implicit_nothing_more_specific(
 ! CHECK:         fir.if
 ! CHECK-NEXT:    } else {
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
diff --git a/llvm/lib/Frontend/OpenMP/OMPContext.cpp b/llvm/lib/Frontend/OpenMP/OMPContext.cpp
index edf05dc758c71..3d6a2e582781f 100644
--- a/llvm/lib/Frontend/OpenMP/OMPContext.cpp
+++ b/llvm/lib/Frontend/OpenMP/OMPContext.cpp
@@ -199,11 +199,9 @@ isVariantApplicableInContextHelper(const VariantMatchInfo &VMI,
 
   bool AnyTraitMatched = false;
 
-  // Handle a single property that was (not) found in the OpenMP context based
-  // on the match kind selected by the user via
-  // `implementation={extensions(match_[all,any,none])}'. Keep inspecting
-  // traits after match_any succeeds so construct match positions needed for
-  // scoring are still recorded.
+  // Apply the match kind selected by implementation={extension(...)} to
+  // each property. Continue after match_any succeeds to record all construct
+  // match positions needed for scoring.
   auto HandleTrait = [MK, &AnyTraitMatched](TraitProperty Property,
                                             bool WasFound) -> bool {
     AnyTraitMatched |= WasFound;
@@ -365,8 +363,8 @@ static APInt getVariantMatchScore(const VariantMatchInfo &VMI,
       // TODO: Handling separately.
       break;
     case TraitSet::invalid:
-      // An unknown property can be applicable under match_any or match_none,
-      // but contributes no score of its own.
+      // Unknown properties can be applicable under match_any or match_none.
+      // They contribute no score unless an explicit score was handled above.
       continue;
     }
 

>From 398137ee50c7291a24752e7411b081c6250382f2 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 10/10] 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 588c9fe01a1a4..94669466368dd 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 &



More information about the flang-commits mailing list