[flang-commits] [clang] [flang] [llvm] [clang][flang][OpenMP] Fix context selector matching and scoring (PR #224431)

via flang-commits flang-commits at lists.llvm.org
Mon Oct 12 11:51:52 PDT 2026


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

>From f9edc33bc8819da5504c0243aaee146841ab3c46 Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Wed, 16 Sep 2026 16:48:23 -0500
Subject: [PATCH 01/23] [clang][flang][OpenMP] Fix context selector matching
 and scoring

Incorrect construct contexts and selector scoring can select the wrong
DECLARE VARIANT function or METADIRECTIVE replacement.

Build construct contexts in source order rather than emitted MLIR order:

  Source: teams -> distribute -> parallel -> do
  MLIR:   teams -> parallel -> distribute -> do

Count executable constructs without selectable traits, omit informational
directives, and start the context at the innermost TARGET.

Compute device weights from the enclosing context depth, choose the
highest-scoring complete ordered construct match, and count each selector's
score once using arbitrary-width arithmetic.

Inside a single PARALLEL region, device={kind(cpu)} previously scored 2,
tying construct={parallel}. Using the enclosing context depth gives the
device selector a score of 3, so it correctly wins.

Zero strict-subset scores before selecting the best applicable candidate.
For example, when both selectors match:

  A: implementation={vendor(score(100): llvm)}
  B: implementation={vendor(score(1): llvm)}, user={condition(1)}

A initially scores 101 and B scores 2. Since A is a strict subset of B,
A's final score is zero and B wins. Previously, subset checks applied
only to equal scores.

Preserve property and condition identity in subset comparisons, including
across Clang AST serialization. Keep Flang runtime and folded conditions
consistent: false conditions retain their identity, contribute no score
under match_any, and retain their score under match_none. Apply the same
ranking rules to implicit NOTHING replacements.

Add shared matcher unit tests and Clang/Flang regressions, including PCH
condition identity and static/runtime ranking.

This provides the shared matching and scoring prerequisite for #219014.

Assisted with Codex.
---
 clang/include/clang/AST/OpenMPClause.h        |   2 +-
 .../clang/Serialization/ASTRecordWriter.h     |   2 +
 clang/lib/AST/OpenMPClause.cpp                |  30 +-
 clang/lib/Parse/ParseOpenMP.cpp               |   6 +-
 clang/lib/Sema/SemaOpenMP.cpp                 |  50 +-
 clang/lib/Serialization/ASTReader.cpp         |   4 +-
 clang/lib/Serialization/ASTWriter.cpp         |   4 +-
 .../declare_variant_condition_identity_pch.c  |  57 ++
 .../declare_variant_device_kind_codegen.cpp   |  45 +-
 clang/test/OpenMP/declare_variant_scoring.c   | 289 ++++++++++
 clang/test/OpenMP/dispatch_variant_matching.c |  43 ++
 flang/include/flang/Lower/AbstractConverter.h |   3 +
 flang/include/flang/Semantics/openmp-utils.h  |   9 +
 flang/lib/Lower/Bridge.cpp                    |   6 +
 flang/lib/Lower/OpenMP/Atomic.cpp             |   3 +
 flang/lib/Lower/OpenMP/OpenMP.cpp             |  16 +-
 flang/lib/Lower/OpenMP/Utils.cpp              |  67 ++-
 flang/lib/Lower/OpenMP/Utils.h                |  23 +-
 flang/lib/Semantics/openmp-utils.cpp          | 103 +++-
 .../OpenMP/declare-variant-construct.f90      | 169 +++++-
 .../OpenMP/declare-variant-source-context.f90 | 113 ++++
 .../OpenMP/metadirective-device-kind.f90      |  17 +
 .../OpenMP/metadirective-implementation.f90   |  12 +
 .../OpenMP/metadirective-source-context.f90   |  68 +++
 .../OpenMP/metadirective-target-boundary.f90  |  62 ++
 .../OpenMP/metadirective-user-static.f90      |  81 +++
 .../test/Lower/OpenMP/metadirective-user.f90  | 306 +++++++---
 .../OpenMP/variant-scoring-any-sections.f90   |  81 +++
 .../include/llvm/Frontend/OpenMP/OMPContext.h |  60 +-
 llvm/lib/Frontend/OpenMP/OMPContext.cpp       | 304 +++++-----
 llvm/unittests/Frontend/OpenMPContextTest.cpp | 531 +++++++++++++++++-
 31 files changed, 2239 insertions(+), 327 deletions(-)
 create mode 100644 clang/test/OpenMP/declare_variant_condition_identity_pch.c
 create mode 100644 clang/test/OpenMP/declare_variant_scoring.c
 create mode 100644 clang/test/OpenMP/dispatch_variant_matching.c
 create mode 100644 flang/test/Lower/OpenMP/declare-variant-source-context.f90
 create mode 100644 flang/test/Lower/OpenMP/metadirective-source-context.f90
 create mode 100644 flang/test/Lower/OpenMP/metadirective-target-boundary.f90
 create mode 100644 flang/test/Lower/OpenMP/metadirective-user-static.f90
 create mode 100644 flang/test/Lower/OpenMP/variant-scoring-any-sections.f90

diff --git a/clang/include/clang/AST/OpenMPClause.h b/clang/include/clang/AST/OpenMPClause.h
index 8238b81e991ae..d8b6f769ff27f 100644
--- a/clang/include/clang/AST/OpenMPClause.h
+++ b/clang/include/clang/AST/OpenMPClause.h
@@ -10066,7 +10066,7 @@ struct OMPTraitProperty {
   llvm::omp::TraitProperty Kind = llvm::omp::TraitProperty::invalid;
 
   /// The raw string as we parsed it. This is needed for the `isa` trait set
-  /// (which accepts anything) and (later) extensions.
+  /// (which accepts anything), user-condition identity, and extensions.
   StringRef RawString;
 };
 
diff --git a/clang/include/clang/Serialization/ASTRecordWriter.h b/clang/include/clang/Serialization/ASTRecordWriter.h
index 8fc60338c4c72..a9060adc4949f 100644
--- a/clang/include/clang/Serialization/ASTRecordWriter.h
+++ b/clang/include/clang/Serialization/ASTRecordWriter.h
@@ -193,6 +193,8 @@ class ASTRecordWriter
     Record->push_back(llvm::to_underlying(Value));
   }
 
+  void writeString(StringRef Value) { Writer->AddString(Value, *Record); }
+
   void writeUnsignedOrNone(UnsignedOrNone Value) {
     Record->push_back(Value.toInternalRepresentation());
   }
diff --git a/clang/lib/AST/OpenMPClause.cpp b/clang/lib/AST/OpenMPClause.cpp
index 5e338c960878b..10d401cda880f 100644
--- a/clang/lib/AST/OpenMPClause.cpp
+++ b/clang/lib/AST/OpenMPClause.cpp
@@ -3057,9 +3057,10 @@ void OMPTraitInfo::getAsVariantMatchInfo(ASTContext &ASTCtx,
                 Selector.ScoreOrCondition->getIntegerConstantExpr(ASTCtx))
           VMI.addTrait(CondVal->isZero() ? TraitProperty::user_condition_false
                                          : TraitProperty::user_condition_true,
-                       "<condition>");
+                       Selector.Properties.front().RawString);
         else
-          VMI.addTrait(TraitProperty::user_condition_false, "<condition>");
+          VMI.addTrait(TraitProperty::user_condition_false,
+                       Selector.Properties.front().RawString);
         continue;
       }
 
@@ -3229,8 +3230,29 @@ TargetOMPContext::TargetOMPContext(
       DiagUnknownTrait(std::move(DiagUnknownTrait)) {
   ASTCtx.getFunctionFeatureMap(FeatureMap, CurrentFunctionDecl);
 
-  for (llvm::omp::TraitProperty Property : ConstructTraits)
-    addTrait(Property);
+  // The construct context starts at and includes the innermost target:
+  //
+  //   Enclosing stack: parallel -> target -> teams -> parallel
+  //   Matching context:            target -> teams -> parallel
+  //
+  // Constructs outside that target must not participate in matching or
+  // increase scoring depth. With no target, retain the entire stack.
+  auto Target = llvm::find(llvm::reverse(ConstructTraits),
+                           llvm::omp::TraitProperty::construct_target_target);
+  if (Target != ConstructTraits.rend())
+    ConstructTraits = ConstructTraits.take_back(
+        std::distance(ConstructTraits.rbegin(), Target) + 1);
+
+  // Constructs without selector properties still occupy scoring positions.
+  // For example, parallel -> task has depth two, with task represented by
+  // an invalid placeholder. Record its position without activating invalid
+  // as a matchable trait.
+  for (llvm::omp::TraitProperty Property : ConstructTraits) {
+    if (Property == llvm::omp::TraitProperty::invalid)
+      addUnknownConstruct();
+    else
+      addTrait(Property);
+  }
 }
 
 bool TargetOMPContext::matchesISATrait(StringRef RawString) const {
diff --git a/clang/lib/Parse/ParseOpenMP.cpp b/clang/lib/Parse/ParseOpenMP.cpp
index 95c26f35fff03..4fba0b43f65bd 100644
--- a/clang/lib/Parse/ParseOpenMP.cpp
+++ b/clang/lib/Parse/ParseOpenMP.cpp
@@ -1150,8 +1150,12 @@ void Parser::parseOMPContextSelector(
     if (!Condition.isUsable())
       return FinishSelector();
     TISelector.ScoreOrCondition = Condition.get();
+    StringRef ConditionText = Lexer::getSourceText(
+        CharSourceRange::getTokenRange(Condition.get()->getSourceRange()),
+        PP.getSourceManager(), PP.getLangOpts());
     TISelector.Properties.push_back(
-        {TraitProperty::user_condition_unknown, "<condition>"});
+        {TraitProperty::user_condition_unknown,
+         ConditionText.empty() ? "<condition>" : ConditionText});
     return;
   }
 
diff --git a/clang/lib/Sema/SemaOpenMP.cpp b/clang/lib/Sema/SemaOpenMP.cpp
index a3be416d2da24..67d3ba31a96d5 100644
--- a/clang/lib/Sema/SemaOpenMP.cpp
+++ b/clang/lib/Sema/SemaOpenMP.cpp
@@ -4781,16 +4781,46 @@ static void handleDeclareVariantConstructTrait(DSAStackTy *Stack,
                                                OpenMPDirectiveKind DKind,
                                                bool ScopeEntry) {
   SmallVector<llvm::omp::TraitProperty, 8> Traits;
-  if (isOpenMPTargetExecutionDirective(DKind))
-    Traits.emplace_back(llvm::omp::TraitProperty::construct_target_target);
-  if (isOpenMPTeamsDirective(DKind))
-    Traits.emplace_back(llvm::omp::TraitProperty::construct_teams_teams);
-  if (isOpenMPParallelDirective(DKind))
-    Traits.emplace_back(llvm::omp::TraitProperty::construct_parallel_parallel);
-  if (isOpenMPWorksharingDirective(DKind))
-    Traits.emplace_back(llvm::omp::TraitProperty::construct_for_for);
-  if (isOpenMPSimdDirective(DKind))
-    Traits.emplace_back(llvm::omp::TraitProperty::construct_simd_simd);
+  // Update the enclosing construct stack for declare variant matching and
+  // scoring on region entry or exit. Record each directive's constructs in
+  // nesting order, using placeholders for constructs without selector
+  // properties so they still contribute to scoring positions and depth.
+  for (OpenMPDirectiveKind Leaf : getLeafConstructsOrSelf(DKind)) {
+    if (llvm::omp::getDirectiveCategory(Leaf) ==
+        llvm::omp::Category::Informational)
+      continue;
+    switch (Leaf) {
+    case OMPD_target:
+      Traits.push_back(llvm::omp::TraitProperty::construct_target_target);
+      break;
+    case OMPD_teams:
+      Traits.push_back(llvm::omp::TraitProperty::construct_teams_teams);
+      break;
+    case OMPD_parallel:
+      Traits.push_back(llvm::omp::TraitProperty::construct_parallel_parallel);
+      break;
+    case OMPD_for:
+      Traits.push_back(llvm::omp::TraitProperty::construct_for_for);
+      break;
+    case OMPD_simd:
+      Traits.push_back(llvm::omp::TraitProperty::construct_simd_simd);
+      break;
+    case OMPD_section:
+      // SECTION separates blocks within SECTIONS and adds no construct level.
+      // Do not add a placeholder: spelling the optional first SECTION must
+      // not change variant scores.
+      break;
+    case OMPD_dispatch:
+      // OpenMP allows omitting DISPATCH from the construct context. Keep it
+      // omitted here: adding it for the whole region would also affect calls
+      // in arguments, but the trait may apply only to the target call.
+      break;
+    default:
+      // Constructs without a selector property still affect scoring depth.
+      Traits.push_back(llvm::omp::TraitProperty::invalid);
+      break;
+    }
+  }
   Stack->handleConstructTrait(Traits, ScopeEntry);
 }
 
diff --git a/clang/lib/Serialization/ASTReader.cpp b/clang/lib/Serialization/ASTReader.cpp
index 36b86afeeb32f..c3e564e7f6aad 100644
--- a/clang/lib/Serialization/ASTReader.cpp
+++ b/clang/lib/Serialization/ASTReader.cpp
@@ -13236,8 +13236,10 @@ OMPTraitInfo *ASTRecordReader::readOMPTraitInfo() {
       if (readBool())
         Selector.ScoreOrCondition = readExprRef();
       Selector.Properties.resize(readUInt32());
-      for (auto &Property : Selector.Properties)
+      for (auto &Property : Selector.Properties) {
         Property.Kind = readEnum<llvm::omp::TraitProperty>();
+        Property.RawString = getContext().backupStr(readString());
+      }
     }
   }
   return &TI;
diff --git a/clang/lib/Serialization/ASTWriter.cpp b/clang/lib/Serialization/ASTWriter.cpp
index 640047c2f9fb3..9e1b6c6329cfe 100644
--- a/clang/lib/Serialization/ASTWriter.cpp
+++ b/clang/lib/Serialization/ASTWriter.cpp
@@ -9049,8 +9049,10 @@ void ASTRecordWriter::writeOMPTraitInfo(const OMPTraitInfo *TI) {
       if (Selector.ScoreOrCondition)
         writeExprRef(Selector.ScoreOrCondition);
       writeUInt32(Selector.Properties.size());
-      for (const auto &Property : Selector.Properties)
+      for (const auto &Property : Selector.Properties) {
         writeEnum(Property.Kind);
+        writeString(Property.RawString);
+      }
     }
   }
 }
diff --git a/clang/test/OpenMP/declare_variant_condition_identity_pch.c b/clang/test/OpenMP/declare_variant_condition_identity_pch.c
new file mode 100644
index 0000000000000..3b0acd9f8f1f6
--- /dev/null
+++ b/clang/test/OpenMP/declare_variant_condition_identity_pch.c
@@ -0,0 +1,57 @@
+// RUN: split-file %s %t
+// RUN: %clang_cc1 -fopenmp -fopenmp-version=52 \
+// RUN:   -triple x86_64-unknown-linux -x c-header %t/conditions.h \
+// RUN:   -emit-pch -o %t/conditions-c.pch
+// RUN: %clang_cc1 -fopenmp -fopenmp-version=52 \
+// RUN:   -triple x86_64-unknown-linux -x c %t/use.c \
+// RUN:   -include-pch %t/conditions-c.pch -emit-llvm -o - | FileCheck %s
+// RUN: %clang_cc1 -fopenmp -fopenmp-version=52 \
+// RUN:   -triple x86_64-unknown-linux -x c++-header %t/conditions.h \
+// RUN:   -emit-pch -o %t/conditions-cxx.pch
+// RUN: %clang_cc1 -fopenmp -fopenmp-version=52 \
+// RUN:   -triple x86_64-unknown-linux -x c++ %t/use.c \
+// RUN:   -include-pch %t/conditions-cxx.pch -emit-llvm -o - | FileCheck %s
+
+// Verify that serialization preserves the source identity of folded user
+// conditions. Distinct conditions do not form a subset relationship, while
+// identical conditions do.
+
+// CHECK-LABEL: define{{.*}} void @test_conditions()
+// CHECK: call void @condition_high_variant()
+// CHECK-NEXT: call void @condition_low_variant()
+// CHECK: ret void
+
+//--- conditions.h
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void condition_high_variant(void);
+void condition_low_variant(void);
+
+#pragma omp declare variant(condition_high_variant)                       \
+    match(implementation = {vendor(score(100) : llvm)}, user = {condition(1)})
+#pragma omp declare variant(condition_low_variant)                         \
+    match(implementation = {vendor(score(1) : llvm)}, device = {kind(cpu)}, \
+          user = {condition(2)})
+void distinct_condition_base(void);
+
+#pragma omp declare variant(condition_high_variant)                       \
+    match(implementation = {vendor(score(100) : llvm)}, user = {condition(1)})
+#pragma omp declare variant(condition_low_variant)                         \
+    match(implementation = {vendor(score(1) : llvm)}, device = {kind(cpu)}, \
+          user = {condition(1)})
+void identical_condition_base(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+//--- use.c
+#ifdef __cplusplus
+extern "C"
+#endif
+void test_conditions(void) {
+  distinct_condition_base();
+  identical_condition_base();
+}
diff --git a/clang/test/OpenMP/declare_variant_device_kind_codegen.cpp b/clang/test/OpenMP/declare_variant_device_kind_codegen.cpp
index 9335df10f957c..d66d265caa8eb 100644
--- a/clang/test/OpenMP/declare_variant_device_kind_codegen.cpp
+++ b/clang/test/OpenMP/declare_variant_device_kind_codegen.cpp
@@ -82,21 +82,29 @@
 
 // Verify no unexpected global symbol aliasing
 // CHECK-NOT: @{{[^ ]+}} = {{.*}}alias
-
-// CHECK-NOT: ret i32 {{1|4|81|84}}
-// CHECK-DAG: declare {{.*}}i32 @_Z5bazzzv()
-// CHECK-DAG: define {{.*}}i32 @_Z3bazv()
-// CHECK-DAG: ret i32 2
-// CHECK-DAG: ret i32 3
-// CHECK-DAG: ret i32 5
-// CHECK-DAG: ret i32 6
-// CHECK-DAG: ret i32 7
-// CHECK-DAG: ret i32 82
-// CHECK-DAG: ret i32 83
-// CHECK-DAG: ret i32 85
-// CHECK-DAG: ret i32 86
-// CHECK-DAG: ret i32 87
-// CHECK-NOT: ret i32 {{4|81|84}}
+// CHECK-LABEL: define {{.*}}i32 @main()
+// CHECK: call {{.*}}i32 @_Z3foov()
+// CHECK: call {{.*}}i32 @_Z5bazzzv()
+// CHECK: call {{.*}}i32 @_Z4testv()
+// CHECK: call {{.*}}i32 @_ZL10stat_used_v()
+// CHECK: ret i32
+
+// CHECK-LABEL: define {{.*}}void @_Z3xxxv()
+// CHECK: call {{.*}}i32 @_ZN12SpecialFuncs7method_Ev(
+// CHECK: call {{.*}}i32 @_ZN16SpecSpecialFuncs7method_Ev(
+// CHECK: ret void
+
+// CHECK-LABEL: define {{.*}}i32 @_Z6int_fnv()
+// CHECK-DAG: call {{.*}}i32 @_Z5prio1v()
+// CHECK-DAG: call {{.*}}i32 @_ZL5prio2v()
+// CHECK: ret i32
+
+// CHECK-LABEL: define {{.*}}i32 @_Z18fn_linkage_variantv()
+// CHECK: ret i32 85
+// CHECK-LABEL: define {{.*}}i32 @fn_linkage_variant1()
+// CHECK: ret i32 86
+// CHECK-LABEL: define {{.*}}i32 @_Z3fn2v()
+// CHECK: ret i32 87
 
 #ifndef HEADER
 #define HEADER
@@ -108,8 +116,9 @@
 #define WRONG host, nohost
 #endif // HOST
 #ifdef CPU
-#define SUBSET cpu
-#define CORRECT cpu, any
+// kind(any) must appear alone and is equivalent to omitting kind.
+#define SUBSET any
+#define CORRECT cpu
 #define WRONG cpu, gpu
 #endif // CPU
 #ifdef NOHOST
@@ -199,7 +208,7 @@ static int prio4() { return 84; }
 #pragma omp declare variant(prio3) match(device = {kind(SUBSET)})
 static int prio1_() { return 1; }
 
-int int_fn() { return prio1_(); }
+int int_fn() { return prio_() + prio1_(); }
 
 int fn_linkage_variant() { return 85; }
 extern "C" {
diff --git a/clang/test/OpenMP/declare_variant_scoring.c b/clang/test/OpenMP/declare_variant_scoring.c
new file mode 100644
index 0000000000000..c924dbaf34624
--- /dev/null
+++ b/clang/test/OpenMP/declare_variant_scoring.c
@@ -0,0 +1,289 @@
+// RUN: %clang_cc1 -verify -fopenmp -fopenmp-version=52 \
+// RUN:   -triple x86_64-unknown-linux -target-feature +avx \
+// RUN:   -emit-llvm %s -o - | FileCheck %s
+// RUN: %clang_cc1 -x c++ -verify -fopenmp -fopenmp-version=52 \
+// RUN:   -triple x86_64-unknown-linux -target-feature +avx \
+// RUN:   -emit-llvm %s -o - | FileCheck %s
+// expected-no-diagnostics
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#pragma omp begin declare target
+void cpu_variant(void);
+void arch_variant(void);
+void scored_variant(void);
+void parallel_variant(void);
+
+#pragma omp declare variant(cpu_variant) match(device = {kind(cpu)})
+#pragma omp declare variant(scored_variant) \
+    match(implementation = {vendor(score(3) : llvm)})
+void target_base(void);
+
+#pragma omp declare variant(cpu_variant) match(device = {kind(cpu)})
+#pragma omp declare variant(parallel_variant) match(construct = {parallel})
+void depth_base(void);
+
+#pragma omp declare variant(parallel_variant) match(construct = {parallel})
+void construct_base(void);
+
+#pragma omp declare variant(cpu_variant) match(device = {kind(cpu)})
+#pragma omp declare variant(scored_variant) \
+    match(implementation = {vendor(score(3) : llvm)})
+void task_depth_base(void);
+
+#pragma omp declare variant(arch_variant) match(device = {arch(x86_64)})
+#pragma omp declare variant(scored_variant) \
+    match(implementation = {vendor(score(3) : llvm)})
+void source_depth_base(void);
+
+int arch_value(void);
+int scored_value(void);
+#pragma omp declare variant(arch_value) match(device = {arch(x86_64)})
+#pragma omp declare variant(scored_value) \
+    match(implementation = {vendor(score(3) : llvm)})
+int source_depth_value(void);
+
+void condition_high_variant(void);
+void condition_low_variant(void);
+#pragma omp declare variant(condition_high_variant)                       \
+    match(implementation = {vendor(score(100) : llvm)}, user = {condition(1)})
+#pragma omp declare variant(condition_low_variant)                         \
+    match(implementation = {vendor(score(1) : llvm)}, device = {kind(cpu)}, \
+          user = {condition(2)})
+void distinct_condition_base(void);
+
+#pragma omp declare variant(condition_high_variant)                       \
+    match(implementation = {vendor(score(100) : llvm)}, user = {condition(1)})
+#pragma omp declare variant(condition_low_variant)                         \
+    match(implementation = {vendor(score(1) : llvm)}, device = {kind(cpu)}, \
+          user = {condition(1)})
+void identical_condition_base(void);
+
+void subset_variant(void);
+void superset_variant(void);
+#pragma omp declare variant(subset_variant) \
+    match(implementation = {vendor(score(100) : llvm)})
+#pragma omp declare variant(superset_variant) \
+    match(implementation = {vendor(score(1) : llvm)}, user = {condition(1)})
+void subset_base(void);
+
+#pragma omp declare variant(subset_variant) \
+    match(implementation = {vendor(score(100) : llvm)})
+#pragma omp declare variant(superset_variant) \
+    match(implementation = {vendor(score(1) : llvm)}, device = {kind(any)})
+void any_base(void);
+
+#pragma omp declare variant(cpu_variant) match(device = {kind(cpu)})
+#pragma omp declare variant(scored_variant) \
+    match(implementation = {vendor(score(5) : llvm)})
+void sections_base(void);
+
+void ordered_high_variant(void);
+void ordered_low_variant(void);
+#pragma omp declare variant(ordered_high_variant)                            \
+    match(construct = {parallel, for},                                       \
+          implementation = {vendor(score(100) : llvm)})
+#pragma omp declare variant(ordered_low_variant)                             \
+    match(construct = {for, parallel, simd},                                 \
+          implementation = {vendor(score(1) : llvm)})
+void ordered_base(void);
+
+void isa_high_variant(void);
+void isa_low_variant(void);
+#pragma omp declare variant(isa_high_variant)                                \
+    match(device = {isa("sse2")},                                           \
+          implementation = {vendor(score(100) : llvm)})
+#pragma omp declare variant(isa_low_variant)                                 \
+    match(device = {isa("avx")}, implementation = {vendor(score(1) : llvm)}, \
+          user = {condition(1)})
+void isa_base(void);
+#pragma omp end declare target
+
+// With only TARGET in the context, CPU scores 3 and the vendor variant's
+// total score is 4.
+void target_only(void) {
+#pragma omp target
+  { target_base(); }
+}
+// CHECK-LABEL: define internal void @__omp_offloading_{{.*}}target_only
+// CHECK: call void @scored_variant()
+// CHECK: ret void
+
+// The outer PARALLEL must not increase the device score inside TARGET.
+void parallel_target(void) {
+#pragma omp parallel
+  {
+#pragma omp target
+    { target_base(); }
+  }
+}
+// CHECK-LABEL: define internal void @__omp_offloading_{{.*}}parallel_target
+// CHECK: call void @scored_variant()
+// CHECK: ret void
+
+// In PARALLEL, CPU scores 3 and construct={parallel} scores 2.
+void parallel_depth(void) {
+#pragma omp parallel
+  { depth_base(); }
+}
+// CHECK-LABEL: define internal void @parallel_depth.omp_outlined
+// CHECK: call void @cpu_variant()
+// CHECK: ret void
+
+// TARGET is retained, as are constructs nested inside it: CPU scores 5.
+void target_parallel(void) {
+#pragma omp target
+  {
+#pragma omp parallel
+    { target_base(); }
+  }
+}
+// CHECK-LABEL: define internal void @__omp_offloading_{{.*}}target_parallel
+// CHECK: define internal void @{{.*}}omp_outlined
+// CHECK: call void @cpu_variant()
+// CHECK: ret void
+
+// Leaving TARGET restores the enclosing PARALLEL context.
+void restore_context(void) {
+#pragma omp parallel
+  {
+#pragma omp target
+    { target_base(); }
+    depth_base();
+    construct_base();
+  }
+}
+// CHECK-LABEL: define internal void @restore_context.omp_outlined
+// CHECK: call void @cpu_variant()
+// CHECK: call void @parallel_variant()
+// CHECK: ret void
+// CHECK-LABEL: define internal void @__omp_offloading_{{.*}}restore_context
+// CHECK: call void @scored_variant()
+// CHECK: ret void
+
+// TASK has no construct-selector property, but contributes to the device
+// weight. In PARALLEL > TASK, CPU scores 5 and beats the vendor variant's
+// total score of 4.
+void task_depth(void) {
+#pragma omp parallel
+  {
+#pragma omp task
+    { task_depth_base(); }
+  }
+}
+// CHECK-LABEL: define internal {{.*}}i32 @.omp_task_entry.
+// CHECK: call void @cpu_variant()
+
+// Executable loop transformations contribute to the construct depth. At
+// depth one, ARCH scores 5 and beats the vendor variant's score of 4.
+void tile_depth(void) {
+#pragma omp tile sizes(2)
+  for (int i = 0; i < 4; ++i)
+    source_depth_base();
+}
+// CHECK-LABEL: define{{.*}} void @tile_depth
+// CHECK: call void @arch_variant()
+// CHECK: ret void
+
+void unroll_depth(void) {
+#pragma omp unroll partial(2)
+  for (int i = 0; i < 4; ++i)
+    source_depth_base();
+}
+// CHECK-LABEL: define{{.*}} void @unroll_depth
+// CHECK: call void @arch_variant()
+// CHECK: ret void
+
+// ATOMIC is an executable construct and contributes one context position.
+void atomic_depth(int *x) {
+#pragma omp atomic update
+  *x += source_depth_value();
+}
+// CHECK-LABEL: define{{.*}} void @atomic_depth
+// CHECK: call i32 @arch_value()
+// CHECK: ret void
+
+// ASSUME is informational and does not add a position. Inside PARALLEL, CPU
+// scores 3 and loses to the vendor variant's score of 4.
+void assume_context(void) {
+#pragma omp parallel
+  {
+#pragma omp assume no_openmp_routines
+    { task_depth_base(); }
+  }
+}
+// CHECK-LABEL: define internal void @assume_context.omp_outlined
+// CHECK: call void @scored_variant()
+// CHECK: ret void
+
+// Distinct folded user conditions do not create a subset relationship.
+void distinct_conditions(void) { distinct_condition_base(); }
+// CHECK-LABEL: define{{.*}} void @distinct_conditions
+// CHECK: call void @condition_high_variant()
+// CHECK: ret void
+
+// Identical conditions allow the first selector to be a strict subset.
+void identical_conditions(void) { identical_condition_base(); }
+// CHECK-LABEL: define{{.*}} void @identical_conditions
+// CHECK: call void @condition_low_variant()
+// CHECK: ret void
+
+// A strict subset has score zero before candidates are ranked, even when its
+// explicit score would otherwise be higher.
+void strict_subset(void) { subset_base(); }
+// CHECK-LABEL: define{{.*}} void @strict_subset
+// CHECK: call void @superset_variant()
+// CHECK: ret void
+
+// kind(any) does not make the lower-scored selector a strict superset.
+void kind_any(void) { any_base(); }
+// CHECK-LABEL: define{{.*}} void @kind_any
+// CHECK: call void @subset_variant()
+// CHECK: ret void
+
+// SECTION is a separator, so CPU scores 5 and the vendor scores 6 in both
+// spellings of the first section of PARALLEL SECTIONS.
+void explicit_section(void) {
+#pragma omp parallel sections
+  {
+#pragma omp section
+    { sections_base(); }
+  }
+}
+// CHECK-LABEL: define internal void @explicit_section.omp_outlined
+// CHECK: call void @scored_variant()
+// CHECK: ret void
+
+void implicit_section(void) {
+#pragma omp parallel sections
+  { sections_base(); }
+}
+// CHECK-LABEL: define internal void @implicit_section.omp_outlined
+// CHECK: call void @scored_variant()
+// CHECK: ret void
+
+// Different construct orders do not create a subset relationship.
+void different_construct_order(void) {
+#pragma omp parallel for
+  for (int i = 0; i < 2; ++i) {
+#pragma omp parallel for simd
+    for (int j = 0; j < 2; ++j)
+      ordered_base();
+  }
+}
+// CHECK-LABEL: define internal void @different_construct_order.omp_outlined
+// CHECK: define internal void @{{.*}}omp_outlined
+// CHECK: call void @ordered_high_variant()
+// CHECK: ret void
+
+// Different active ISA properties do not create a subset relationship.
+void different_isa(void) { isa_base(); }
+// CHECK-LABEL: define{{.*}} void @different_isa
+// CHECK: call void @isa_high_variant()
+// CHECK: ret void
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/clang/test/OpenMP/dispatch_variant_matching.c b/clang/test/OpenMP/dispatch_variant_matching.c
new file mode 100644
index 0000000000000..e61f719e77293
--- /dev/null
+++ b/clang/test/OpenMP/dispatch_variant_matching.c
@@ -0,0 +1,43 @@
+// RUN: %clang_cc1 -fopenmp -fopenmp-version=52 -verify -ast-dump %s \
+// RUN:   | FileCheck %s --implicit-check-not=PseudoObjectExpr
+// RUN: %clang_cc1 -x c++ -fopenmp -fopenmp-version=52 -verify -ast-dump %s \
+// RUN:   | FileCheck %s --implicit-check-not=PseudoObjectExpr
+// expected-no-diagnostics
+
+// Clang omits the implementation-defined DISPATCH construct trait. In
+// particular, it must not select dispatch variants in argument expressions.
+int g_variant(void);
+#pragma omp declare variant(g_variant) match(construct = {dispatch})
+int g(void);
+
+int f_variant(int);
+#pragma omp declare variant(f_variant) match(construct = {dispatch})
+int f(int);
+void plain(int);
+
+// CHECK-LABEL: FunctionDecl {{.*}} test_argument
+// CHECK: OMPDispatchDirective
+// CHECK: DeclRefExpr {{.*}} Function {{.*}} 'plain'
+// CHECK: DeclRefExpr {{.*}} Function {{.*}} 'g'
+void test_argument(void) {
+#pragma omp dispatch
+  plain(g());
+}
+
+// CHECK-LABEL: FunctionDecl {{.*}} test_target
+// CHECK: OMPDispatchDirective
+// CHECK: DeclRefExpr {{.*}} Function {{.*}} 'f'
+// CHECK: DeclRefExpr {{.*}} Function {{.*}} 'g'
+void test_target(void) {
+#pragma omp dispatch
+  f(g());
+}
+
+// CHECK-LABEL: FunctionDecl {{.*}} test_assignment
+// CHECK: OMPDispatchDirective
+// CHECK: DeclRefExpr {{.*}} Function {{.*}} 'f'
+// CHECK: DeclRefExpr {{.*}} Function {{.*}} 'g'
+void test_assignment(int *result) {
+#pragma omp dispatch
+  *result = f(g());
+}
diff --git a/flang/include/flang/Lower/AbstractConverter.h b/flang/include/flang/Lower/AbstractConverter.h
index 40d94dc9f5bfd..fd63f52f6aaf5 100644
--- a/flang/include/flang/Lower/AbstractConverter.h
+++ b/flang/include/flang/Lower/AbstractConverter.h
@@ -337,6 +337,9 @@ class AbstractConverter {
   /// Get the converter's current scope
   virtual const Fortran::semantics::Scope &getCurrentScope() = 0;
 
+  /// Get the PFT evaluation currently being lowered.
+  virtual pft::Evaluation &getCurrentEvaluation() = 0;
+
   //===--------------------------------------------------------------------===//
   // FIR/MLIR
   //===--------------------------------------------------------------------===//
diff --git a/flang/include/flang/Semantics/openmp-utils.h b/flang/include/flang/Semantics/openmp-utils.h
index c31459f256f62..326d97714f64f 100644
--- a/flang/include/flang/Semantics/openmp-utils.h
+++ b/flang/include/flang/Semantics/openmp-utils.h
@@ -261,6 +261,15 @@ class OmpVariantMatchContext : public llvm::omp::OMPContext {
   std::string features_;
 };
 
+/// Append the construct context contributed by \p directive.
+///
+/// Combined and composite directives contribute their leaf constructs in
+/// source order. Informational directives do not contribute. Entering a TARGET
+/// construct discards the outer context, as required by the OpenMP
+/// construct-set definition.
+void AppendDirectiveContextTraits(llvm::omp::Directive directive,
+    llvm::SmallVectorImpl<llvm::omp::TraitProperty> &constructTraits);
+
 struct MetadirectiveCandidate {
   MetadirectiveCandidate(const parser::OmpDirectiveSpecification *spec,
       llvm::omp::VariantMatchInfo vmi, bool isExplicit,
diff --git a/flang/lib/Lower/Bridge.cpp b/flang/lib/Lower/Bridge.cpp
index fc1e485436dcd..036c6fd110504 100644
--- a/flang/lib/Lower/Bridge.cpp
+++ b/flang/lib/Lower/Bridge.cpp
@@ -1165,6 +1165,8 @@ class FirConverter : public Fortran::lower::AbstractConverter {
 
   void genEval(Fortran::lower::pft::Evaluation &eval,
                bool unstructuredContext) override final {
+    Fortran::lower::pft::Evaluation *previousEval{evalPtr};
+    llvm::scope_exit restoreEval([&]() { evalPtr = previousEval; });
     genFIR(eval, unstructuredContext);
   }
 
@@ -1275,6 +1277,10 @@ class FirConverter : public Fortran::lower::AbstractConverter {
     return bridge.getSemanticsContext().FindScope(currentPosition);
   }
 
+  Fortran::lower::pft::Evaluation &getCurrentEvaluation() override final {
+    return getEval();
+  }
+
   fir::FirOpBuilder &getFirOpBuilder() override final {
     CHECK(builder && "builder is not set before calling getFirOpBuilder");
     return *builder;
diff --git a/flang/lib/Lower/OpenMP/Atomic.cpp b/flang/lib/Lower/OpenMP/Atomic.cpp
index 5a38c97ee77a2..4a73fae32b934 100644
--- a/flang/lib/Lower/OpenMP/Atomic.cpp
+++ b/flang/lib/Lower/OpenMP/Atomic.cpp
@@ -7,6 +7,7 @@
 //===----------------------------------------------------------------------===//
 
 #include "Atomic.h"
+#include "Utils.h"
 #include "flang/Evaluate/expression.h"
 #include "flang/Evaluate/fold.h"
 #include "flang/Evaluate/tools.h"
@@ -559,6 +560,8 @@ void Fortran::lower::omp::lowerAtomic(
 
   fir::FirOpBuilder &builder = converter.getFirOpBuilder();
   const parser::OmpDirectiveSpecification &dirSpec = construct.BeginDir();
+  mlir::SaveStateStack<OpenMPContextFrame> context{converter.getStateStack(),
+                                                   eval, dirSpec.DirId()};
   omp::List<omp::Clause> clauses = makeClauses(dirSpec.Clauses(), semaCtx);
   lower::StatementContext stmtCtx;
 
diff --git a/flang/lib/Lower/OpenMP/OpenMP.cpp b/flang/lib/Lower/OpenMP/OpenMP.cpp
index bdc6bb0073cff..c58bba6385f06 100644
--- a/flang/lib/Lower/OpenMP/OpenMP.cpp
+++ b/flang/lib/Lower/OpenMP/OpenMP.cpp
@@ -7427,8 +7427,7 @@ static void genMetadirective(lower::AbstractConverter &converter,
   fir::FirOpBuilder &builder = converter.getFirOpBuilder();
 
   llvm::SmallVector<llvm::omp::TraitProperty, 8> constructTraits;
-  collectEnclosingConstructTraits(builder.getInsertionBlock()->getParentOp(),
-                                  constructTraits);
+  collectEnclosingConstructTraits(converter, eval, constructTraits);
   semantics::omp::OmpVariantMatchContext ompCtx =
       makeVariantMatchContext(builder.getModule(), constructTraits);
 
@@ -7655,6 +7654,8 @@ static void genMetadirective(lower::AbstractConverter &converter,
       if (marking == MetadirectiveLoopIVMarking::ThreadprivateIV)
         TODO(variantLoc, "THREADPRIVATE loop iteration variable in "
                          "loop-associated METADIRECTIVE variant");
+      mlir::SaveStateStack<OpenMPContextFrame> context{
+          converter.getStateStack(), eval, spec->DirId()};
       genOMPDispatch(converter, symTable, semaCtx, eval, variantLoc, queue,
                      queue.begin(), dsaGuard.getMarkedSymbols());
       return;
@@ -7668,8 +7669,15 @@ static void genMetadirective(lower::AbstractConverter &converter,
       TODO(variantLoc,
            "METADIRECTIVE with both block- and loop-associated variants");
 
-    genOMPDispatch(converter, symTable, semaCtx, eval, variantLoc, queue,
-                   queue.begin());
+    if (consumesBody) {
+      mlir::SaveStateStack<OpenMPContextFrame> context{
+          converter.getStateStack(), eval, spec->DirId()};
+      genOMPDispatch(converter, symTable, semaCtx, eval, variantLoc, queue,
+                     queue.begin());
+    } else {
+      genOMPDispatch(converter, symTable, semaCtx, eval, variantLoc, queue,
+                     queue.begin());
+    }
     // A standalone variant (Association::None, e.g. barrier/taskwait/nothing)
     // does not consume the metadirective's nested block, so lower it here.
     if (!consumesBody && eval.hasNestedEvaluations())
diff --git a/flang/lib/Lower/OpenMP/Utils.cpp b/flang/lib/Lower/OpenMP/Utils.cpp
index e05a3eab6fad6..431697b3e10cc 100644
--- a/flang/lib/Lower/OpenMP/Utils.cpp
+++ b/flang/lib/Lower/OpenMP/Utils.cpp
@@ -1471,27 +1471,51 @@ semantics::omp::OmpVariantMatchContext makeVariantMatchContext(
 }
 
 void collectEnclosingConstructTraits(
-    mlir::Operation *op,
+    AbstractConverter &converter, const pft::Evaluation &evaluation,
     llvm::SmallVectorImpl<llvm::omp::TraitProperty> &constructTraits) {
-  // Collect enclosing OpenMP operations so variants chosen by an outer
-  // metadirective are part of this metadirective's context. For example, an
-  // inner metadirective inside `target` and an outer-selected `parallel` must
-  // 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::WsloopOp>(op))
-      constructTraits.push_back(llvm::omp::TraitProperty::construct_for_for);
-    if (mlir::isa<mlir::omp::ParallelOp>(op))
-      constructTraits.push_back(
-          llvm::omp::TraitProperty::construct_parallel_parallel);
-    if (mlir::isa<mlir::omp::TeamsOp>(op))
-      constructTraits.push_back(
-          llvm::omp::TraitProperty::construct_teams_teams);
-    if (mlir::isa<mlir::omp::TargetOp>(op))
-      constructTraits.push_back(
-          llvm::omp::TraitProperty::construct_target_target);
+  llvm::SmallVector<const OpenMPContextFrame *, 4> frames;
+  converter.getStateStack().stackWalk<OpenMPContextFrame>(
+      [&](OpenMPContextFrame &frame) {
+        frames.push_back(&frame);
+        return mlir::WalkResult::advance();
+      });
+  std::reverse(frames.begin(), frames.end());
+  llvm::SmallVector<bool, 4> usedFrames(frames.size(), false);
+
+  llvm::SmallVector<const pft::Evaluation *, 8> ancestors;
+  for (const pft::Evaluation *parent = evaluation.parentConstruct; parent;
+       parent = parent->parentConstruct) {
+    ancestors.push_back(parent);
+  }
+  std::reverse(ancestors.begin(), ancestors.end());
+
+  auto append = [&](llvm::omp::Directive directive) {
+    semantics::omp::AppendDirectiveContextTraits(directive, constructTraits);
+  };
+  for (const pft::Evaluation *ancestor : ancestors) {
+    const auto *omp = ancestor->getIf<parser::OpenMPConstruct>();
+    if (!omp)
+      continue;
+    llvm::omp::Directive directive{parser::omp::GetOmpDirectiveName(*omp).v};
+    if (directive != llvm::omp::Directive::OMPD_metadirective) {
+      append(directive);
+      continue;
+    }
+    for (auto [index, frame] : llvm::enumerate(frames)) {
+      if (&frame->evaluation == ancestor) {
+        append(frame->directive);
+        usedFrames[index] = true;
+        break;
+      }
+    }
   }
-  std::reverse(constructTraits.begin(), constructTraits.end());
+
+  // Active source contexts may not appear in the ancestor chain. This occurs
+  // for a loop-associated metadirective that owns a following sibling and for
+  // an atomic expression lowered from the atomic evaluation itself.
+  for (auto [index, frame] : llvm::enumerate(frames))
+    if (!usedFrames[index])
+      append(frame->directive);
 }
 
 const semantics::Symbol *
@@ -1535,9 +1559,8 @@ resolveDeclareVariantCallee(const semantics::Symbol &base,
   }
 
   llvm::SmallVector<llvm::omp::TraitProperty, 8> constructTraits;
-  collectEnclosingConstructTraits(
-      converter.getFirOpBuilder().getInsertionBlock()->getParentOp(),
-      constructTraits);
+  collectEnclosingConstructTraits(converter, converter.getCurrentEvaluation(),
+                                  constructTraits);
   semantics::omp::OmpVariantMatchContext ompCtx =
       makeVariantMatchContext(converter.getModuleOp(), constructTraits);
 
diff --git a/flang/lib/Lower/OpenMP/Utils.h b/flang/lib/Lower/OpenMP/Utils.h
index 4560c9df349b2..4e9095fc11a28 100644
--- a/flang/lib/Lower/OpenMP/Utils.h
+++ b/flang/lib/Lower/OpenMP/Utils.h
@@ -15,6 +15,7 @@
 #include "mlir/Dialect/OpenMP/OpenMPDialect.h"
 #include "mlir/IR/Location.h"
 #include "mlir/IR/Value.h"
+#include "mlir/Support/StateStack.h"
 #include "llvm/Frontend/OpenMP/OMPContext.h"
 #include "llvm/Support/CommandLine.h"
 #include <cstdint>
@@ -51,6 +52,19 @@ class AbstractConverter;
 
 namespace omp {
 
+class OpenMPContextFrame
+    : public mlir::StateStackFrameBase<OpenMPContextFrame> {
+public:
+  MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(OpenMPContextFrame)
+
+  OpenMPContextFrame(const pft::Evaluation &evaluation,
+                     llvm::omp::Directive directive)
+      : evaluation{evaluation}, directive{directive} {}
+
+  const pft::Evaluation &evaluation;
+  llvm::omp::Directive directive;
+};
+
 struct DeclareTargetCaptureInfo {
   mlir::omp::DeclareTargetCaptureClause clause;
   bool automap = false;
@@ -258,12 +272,11 @@ std::optional<llvm::SmallVector<mlir::Value>> getIteratorElementIndices(
     Fortran::lower::AbstractConverter &converter, const omp::Object &object,
     Fortran::lower::StatementContext &stmtCtx, mlir::Location loc);
 
-/// Walk the already-emitted MLIR parent operations starting from \p op and
-/// collect the implied OpenMP construct traits in outermost-to-innermost
-/// order. Used by metadirective lowering and declare-variant call resolution
-/// to build the `ConstructTraits` of an `OMPContext`.
+/// Collect the source OpenMP constructs enclosing \p evaluation in
+/// outermost-to-innermost order. Active metadirective replacements substitute
+/// for their METADIRECTIVE source constructs.
 void collectEnclosingConstructTraits(
-    mlir::Operation *op,
+    AbstractConverter &converter, const pft::Evaluation &evaluation,
     llvm::SmallVectorImpl<llvm::omp::TraitProperty> &constructTraits);
 
 /// Return true when \p module is being compiled for an AMDGPU device or all of
diff --git a/flang/lib/Semantics/openmp-utils.cpp b/flang/lib/Semantics/openmp-utils.cpp
index 16c265ce39690..b2545fa29c3b6 100644
--- a/flang/lib/Semantics/openmp-utils.cpp
+++ b/flang/lib/Semantics/openmp-utils.cpp
@@ -2410,10 +2410,9 @@ void ProcessTraitProperties(llvm::omp::VariantMatchInfo &vmi,
       vmi.addTrait(set, llvm::omp::TraitProperty::target_device_isa___ANY,
           name->v, scorePtr);
     } else {
-      // For non-ISA selectors (arch, kind, vendor, etc.), unknown properties
-      // mean the variant cannot match. Add an invalid trait to ensure it is
-      // not selected.
-      vmi.addTrait(llvm::omp::TraitProperty::invalid, name->v, scorePtr);
+      // Unknown properties remain inactive, but their selectors still
+      // contribute to scoring under match_any or match_none.
+      vmi.addUnknownTrait(selector, name->v, scorePtr);
     }
   }
 }
@@ -2474,6 +2473,36 @@ static void AppendConstructTraitsForDirective(
     add(llvm::omp::TraitProperty::construct_dispatch_dispatch);
 }
 
+void AppendDirectiveContextTraits(llvm::omp::Directive directive,
+    llvm::SmallVectorImpl<llvm::omp::TraitProperty> &constructTraits) {
+  using llvm::omp::Directive;
+  using llvm::omp::TraitProperty;
+
+  for (Directive leaf : llvm::omp::getLeafConstructsOrSelf(directive)) {
+    if (leaf == Directive::OMPD_nothing ||
+        leaf == Directive::OMPD_metadirective ||
+        leaf == Directive::OMPD_unknown || leaf == Directive::OMPD_section ||
+        leaf == Directive::OMPD_dispatch ||
+        llvm::omp::getDirectiveCategory(leaf) ==
+            llvm::omp::Category::Informational) {
+      continue;
+    }
+
+    llvm::omp::VariantMatchInfo vmi;
+    AppendConstructTraitsForDirective(leaf, vmi);
+    if (vmi.RequiredTraits.test(
+            static_cast<unsigned>(TraitProperty::construct_target_target))) {
+      constructTraits.clear();
+    }
+    if (vmi.ConstructTraits.empty()) {
+      constructTraits.push_back(TraitProperty::invalid);
+    } else {
+      constructTraits.append(
+          vmi.ConstructTraits.begin(), vmi.ConstructTraits.end());
+    }
+  }
+}
+
 static void AddTraitPropertiesFromSelector(llvm::omp::TraitSet set,
     const parser::OmpTraitSelector &selector, llvm::omp::VariantMatchInfo &vmi,
     SemanticsContext &semaCtx,
@@ -2501,17 +2530,18 @@ static void AddTraitPropertiesFromSelector(llvm::omp::TraitSet set,
         continue;
       }
       if (auto constValue{EvaluateUserCondition(semaCtx, *scalarExpr)}) {
+        llvm::StringRef condition{prop.source.begin(), prop.source.size()};
         vmi.addTrait(set,
             *constValue ? llvm::omp::TraitProperty::user_condition_true
                         : llvm::omp::TraitProperty::user_condition_false,
-            "<condition>", scorePtr);
+            condition, scorePtr);
         continue;
       }
       if (!dynamicCond) {
         dynamicCond = DynamicUserCondition{scalarExpr, prop.source};
       }
       vmi.addTrait(set, llvm::omp::TraitProperty::user_condition_unknown,
-          "<condition>", scorePtr);
+          llvm::StringRef{prop.source.begin(), prop.source.size()}, scorePtr);
     }
     return;
   }
@@ -2526,6 +2556,13 @@ static void AddTraitPropertiesFromSelector(llvm::omp::TraitSet set,
   // the selector itself implies the property.
   if (const auto *dir{std::get_if<llvm::omp::Directive>(&traitName.u)}) {
     AppendConstructTraitsForDirective(*dir, vmi);
+  } else if (const auto *value{std::get_if<parser::OmpTraitSelectorName::Value>(
+                 &traitName.u)}) {
+    // SIMD is a predefined selector name because it can take clause
+    // properties, unlike the other construct selectors.
+    if (*value == parser::OmpTraitSelectorName::Value::Simd) {
+      AppendConstructTraitsForDirective(llvm::omp::Directive::OMPD_simd, vmi);
+    }
   }
 }
 
@@ -2621,8 +2658,11 @@ std::optional<MetadirectiveCandidateSet> BuildMetadirectiveCandidateSet(
           staticVMI.ScoreMap.erase(scoreIt);
         }
         staticVMI.RequiredTraits.reset(unsigned(dynamicConditionTrait));
+        staticVMI.UserCondition = {};
         llvm::APInt *conditionScorePtr{
             conditionScore ? &*conditionScore : nullptr};
+        llvm::StringRef conditionSource{
+            dynamicCondition->source.begin(), dynamicCondition->source.size()};
 
         bool hasMatchAny{rawVMI.RequiredTraits.test(unsigned(matchAnyTrait))};
         bool hasMatchNone{rawVMI.RequiredTraits.test(unsigned(matchNoneTrait))};
@@ -2631,15 +2671,13 @@ std::optional<MetadirectiveCandidateSet> BuildMetadirectiveCandidateSet(
         // Only match_any can remain applicable when the static traits do not
         // match, because a true runtime condition may satisfy the selector.
         if (!isStaticVMIApplicable) {
-          if (!hasMatchAny ||
-              staticVMI.RequiredTraits.test(
-                  unsigned(llvm::omp::TraitProperty::invalid))) {
+          if (!hasMatchAny) {
             continue;
           }
 
           llvm::omp::VariantMatchInfo conditionTrueVMI{staticVMI};
           conditionTrueVMI.addTrait(
-              llvm::omp::TraitProperty::user_condition_true, "<condition>",
+              llvm::omp::TraitProperty::user_condition_true, conditionSource,
               conditionScorePtr);
           if (!llvm::omp::isVariantApplicableInContext(
                   conditionTrueVMI, matchContext)) {
@@ -2650,33 +2688,32 @@ std::optional<MetadirectiveCandidateSet> BuildMetadirectiveCandidateSet(
         auto addConditionTraitForRanking =
             [&](llvm::omp::VariantMatchInfo &rankingVMI) {
               rankingVMI.addTrait(hasMatchNone
-                      ? dynamicConditionTrait
+                      ? llvm::omp::TraitProperty::user_condition_false
                       : llvm::omp::TraitProperty::user_condition_true,
-                  "<condition>", conditionScorePtr);
+                  conditionSource, conditionScorePtr);
             };
 
         if (hasMatchAny && isStaticVMIApplicable) {
-          // Represent both outcomes: a guarded candidate with the condition's
-          // score and an unguarded candidate with only the static traits. If
-          // the WHEN clause omits its directive, only add the unguarded
-          // candidate.
-          if (isExplicit) {
-            llvm::omp::VariantMatchInfo conditionTrueVMI{staticVMI};
-            addConditionTraitForRanking(conditionTrueVMI);
-            result.candidates.push_back({spec, std::move(conditionTrueVMI),
-                isExplicit, dynamicCondition});
-          }
-          result.candidates.push_back({spec, std::move(staticVMI), isExplicit});
+          // Represent both outcomes. Keeping the false condition in the
+          // unguarded candidate preserves selector identity for subset
+          // comparisons; MATCH_ANY scoring omits its inactive score.
+          llvm::omp::VariantMatchInfo conditionTrueVMI{staticVMI};
+          addConditionTraitForRanking(conditionTrueVMI);
+          llvm::omp::VariantMatchInfo conditionFalseVMI{staticVMI};
+          conditionFalseVMI.addTrait(
+              llvm::omp::TraitProperty::user_condition_false, conditionSource,
+              conditionScorePtr);
+          result.candidates.push_back({spec, std::move(conditionTrueVMI),
+              isExplicit, dynamicCondition});
+          result.candidates.push_back(
+              {spec, std::move(conditionFalseVMI), 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;
@@ -2910,7 +2947,8 @@ bool MayVariantBeSelected(
   bool userTrue{required.test(unsigned(TP::user_condition_true))};
   bool userUnknown{required.test(unsigned(TP::user_condition_unknown))};
   bool userFalse{required.test(unsigned(TP::user_condition_false))};
-  bool invalid{required.test(unsigned(TP::invalid))};
+  bool invalid{
+      required.test(unsigned(TP::invalid)) || !vmi.UnknownTraits.empty()};
 
   // The target-only LLVM matcher below skips user and construct traits while
   // retaining the global match kind. Account for those skipped traits first;
@@ -2970,7 +3008,10 @@ OmpVariantMatchContext::OmpVariantMatchContext(bool isDeviceCompilation,
           std::move(targetOffloadTriple), /*DeviceNum=*/-1),
       features_(std::move(targetFeatures)) {
   for (llvm::omp::TraitProperty trait : constructTraits) {
-    addTrait(trait);
+    if (trait == llvm::omp::TraitProperty::invalid)
+      addUnknownConstruct();
+    else
+      addTrait(trait);
   }
 }
 
diff --git a/flang/test/Lower/OpenMP/declare-variant-construct.f90 b/flang/test/Lower/OpenMP/declare-variant-construct.f90
index ca72b5c8e47af..90fe7250b9bd6 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
@@ -10,6 +10,79 @@ module m
   subroutine base_tt
     !$omp declare variant (base_tt:vsub_tt) match (construct={target teams})
   end subroutine base_tt
+
+  subroutine base_device_weight
+    !$omp declare variant (vsub_cpu) match(device={kind(cpu)})
+    !$omp declare variant (vsub_parallel) match(construct={parallel})
+  end subroutine
+  subroutine vsub_cpu
+  end subroutine
+  subroutine vsub_parallel
+  end subroutine
+
+  ! CPU scores 5 in this context, beating PARALLEL's score of 3.
+  ! CHECK-LABEL: func.func @_QMmPtest_device_weight()
+  ! CHECK: omp.parallel
+  ! CHECK: omp.parallel
+  ! CHECK-NOT: fir.call @_QMmPvsub_parallel
+  ! CHECK: fir.call @_QMmPvsub_cpu()
+  ! CHECK-NOT: fir.call @_QMmPvsub_parallel
+  ! CHECK: return
+  subroutine test_device_weight
+    !$omp parallel
+      !$omp parallel
+        call base_device_weight()
+      !$omp end parallel
+    !$omp end parallel
+  end subroutine
+
+  subroutine base_task_weight
+    !$omp declare variant (vsub_cpu) match(device={kind(cpu)})
+    !$omp declare variant (vsub_score3) &
+    !$omp& match(implementation={vendor(score(3): llvm)})
+  end subroutine
+  subroutine vsub_score3
+  end subroutine
+
+  ! TASK has no construct-selector property, but contributes to the device
+  ! weight. In PARALLEL > TASK, CPU scores 5 and beats score(3)'s total of 4.
+  ! CHECK-LABEL: func.func @_QMmPtest_task_weight()
+  ! CHECK: omp.parallel
+  ! CHECK: omp.task
+  ! CHECK-NOT: fir.call @_QMmPvsub_score3
+  ! CHECK: fir.call @_QMmPvsub_cpu()
+  ! CHECK-NOT: fir.call @_QMmPvsub_score3
+  ! CHECK: return
+  subroutine test_task_weight
+    !$omp parallel
+      !$omp task
+        call base_task_weight()
+      !$omp end task
+    !$omp end parallel
+  end subroutine
+
+  subroutine base_subset
+    !$omp declare variant (vsub_subset) &
+    !$omp& match(implementation={vendor(score(100): llvm)})
+    !$omp declare variant (vsub_superset) &
+    !$omp& match(implementation={vendor(score(1): llvm)}, &
+    !$omp& user={condition(.true.)})
+  end subroutine
+  subroutine vsub_subset
+  end subroutine
+  subroutine vsub_superset
+  end subroutine
+
+  ! A strict subset has score zero before candidates are ranked, even when its
+  ! explicit score would otherwise be higher.
+  ! CHECK-LABEL: func.func @_QMmPtest_strict_subset()
+  ! CHECK-NOT: fir.call @_QMmPvsub_subset
+  ! CHECK: fir.call @_QMmPvsub_superset()
+  ! CHECK-NOT: fir.call @_QMmPvsub_subset
+  subroutine test_strict_subset
+    call base_subset()
+  end subroutine
+
   subroutine vsub_tt
   end subroutine vsub_tt
 
@@ -49,6 +122,38 @@ 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
+
+  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.
 
@@ -157,4 +262,64 @@ 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
+
+  ! 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/declare-variant-source-context.f90 b/flang/test/Lower/OpenMP/declare-variant-source-context.f90
new file mode 100644
index 0000000000000..420c649c44456
--- /dev/null
+++ b/flang/test/Lower/OpenMP/declare-variant-source-context.f90
@@ -0,0 +1,113 @@
+! RUN: %if x86-registered-target %{ %flang_fc1 -fopenmp \
+! RUN:   -fopenmp-version=52 -triple x86_64-unknown-linux-gnu \
+! RUN:   -emit-hlfir %s -o - | FileCheck %s %}
+
+module source_context
+contains
+  subroutine depth_base
+    !$omp declare variant(depth_vendor) &
+    !$omp& match(implementation={vendor(score(3): llvm)})
+    !$omp declare variant(depth_arch) match(device={arch(x86_64)})
+  end subroutine
+  subroutine depth_vendor
+  end subroutine
+  subroutine depth_arch
+  end subroutine
+
+  subroutine order_base
+    !$omp declare variant(order_vendor) &
+    !$omp& match(implementation={vendor(score(3): llvm)})
+    !$omp declare variant(order_parallel) match(construct={parallel})
+  end subroutine
+  subroutine order_vendor
+  end subroutine
+  subroutine order_parallel
+  end subroutine
+
+  integer function value_base()
+    !$omp declare variant(value_vendor) &
+    !$omp& match(implementation={vendor(score(3): llvm)})
+    !$omp declare variant(value_arch) match(device={arch(x86_64)})
+    value_base = 0
+  end function
+  integer function value_vendor()
+    value_vendor = 1
+  end function
+  integer function value_arch()
+    value_arch = 2
+  end function
+
+! CHECK-LABEL: func.func @_QMsource_contextPtile_context(
+! CHECK: fir.call @_QMsource_contextPdepth_arch()
+! CHECK-NOT: fir.call @_QMsource_contextPdepth_vendor
+! CHECK: return
+  subroutine tile_context(n)
+    integer :: n, i
+    !$omp tile sizes(2)
+    do i = 1, n
+      call depth_base()
+    end do
+  end subroutine
+
+! CHECK-LABEL: func.func @_QMsource_contextPunroll_context(
+! CHECK: fir.call @_QMsource_contextPdepth_arch()
+! CHECK-NOT: fir.call @_QMsource_contextPdepth_vendor
+! CHECK: return
+  subroutine unroll_context(n)
+    integer :: n, i
+    !$omp unroll partial(2)
+    do i = 1, n
+      call depth_base()
+    end do
+  end subroutine
+
+! CHECK-LABEL: func.func @_QMsource_contextPfuse_context(
+! CHECK-NOT: fir.call @_QMsource_contextPdepth_vendor
+! CHECK-COUNT-2: fir.call @_QMsource_contextPdepth_arch()
+! CHECK-NOT: fir.call @_QMsource_contextPdepth_vendor
+! CHECK: return
+  subroutine fuse_context(n)
+    integer :: n, i, j
+    !$omp fuse
+    do i = 1, n
+      call depth_base()
+    end do
+    do j = 1, n
+      call depth_base()
+    end do
+    !$omp end fuse
+  end subroutine
+
+! CHECK-LABEL: func.func @_QMsource_contextPatomic_context(
+! CHECK: fir.call @_QMsource_contextPvalue_arch()
+! CHECK-NOT: fir.call @_QMsource_contextPvalue_vendor
+! CHECK: omp.atomic.update
+! CHECK: return
+  subroutine atomic_context(x)
+    integer :: x
+    !$omp atomic update
+    x = x + value_base()
+  end subroutine
+
+! CHECK-LABEL: func.func @_QMsource_contextPassume_context()
+! CHECK: fir.call @_QMsource_contextPdepth_vendor()
+! CHECK-NOT: fir.call @_QMsource_contextPdepth_arch
+! CHECK: return
+  subroutine assume_context
+    !$omp assume holds(.true.)
+      call depth_base()
+    !$omp end assume
+  end subroutine
+
+! CHECK-LABEL: func.func @_QMsource_contextPcombined_context(
+! CHECK: fir.call @_QMsource_contextPorder_parallel()
+! CHECK-NOT: fir.call @_QMsource_contextPorder_vendor
+! CHECK: return
+  subroutine combined_context(n)
+    integer :: n, i
+    !$omp teams distribute parallel do
+    do i = 1, n
+      call order_base()
+    end do
+  end subroutine
+end module
diff --git a/flang/test/Lower/OpenMP/metadirective-device-kind.f90 b/flang/test/Lower/OpenMP/metadirective-device-kind.f90
index ae7613d40c5d9..d96be7739ac98 100644
--- a/flang/test/Lower/OpenMP/metadirective-device-kind.f90
+++ b/flang/test/Lower/OpenMP/metadirective-device-kind.f90
@@ -1,5 +1,22 @@
 ! RUN: %flang_fc1 -fopenmp -emit-hlfir -fopenmp-version=50 %s -o - | FileCheck %s
 
+! Device weights depend on context depth, not the candidate's selector count.
+! CHECK-LABEL: func.func @_QPtest_device_kind_nested_parallel()
+! CHECK: omp.parallel
+! CHECK: omp.parallel
+! CHECK-NOT: omp.barrier
+! CHECK: omp.taskyield
+! CHECK-NOT: omp.barrier
+! CHECK: return
+subroutine test_device_kind_nested_parallel()
+  !$omp parallel
+    !$omp parallel
+      !$omp metadirective when(device={kind(cpu)}: taskyield) &
+      !$omp& when(construct={parallel}: barrier)
+    !$omp end parallel
+  !$omp end parallel
+end subroutine
+
 ! CHECK-LABEL: func.func @_QPtest_device_kind_host()
 ! CHECK:         omp.taskyield
 ! CHECK:         return
diff --git a/flang/test/Lower/OpenMP/metadirective-implementation.f90 b/flang/test/Lower/OpenMP/metadirective-implementation.f90
index af8f2af938ca5..cd61b733cbb1d 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
 
+! 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
+! 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-source-context.f90 b/flang/test/Lower/OpenMP/metadirective-source-context.f90
new file mode 100644
index 0000000000000..9472dd6e92afc
--- /dev/null
+++ b/flang/test/Lower/OpenMP/metadirective-source-context.f90
@@ -0,0 +1,68 @@
+! RUN: %if x86-registered-target %{ %flang_fc1 -fopenmp \
+! RUN:   -fopenmp-version=52 -triple x86_64-unknown-linux-gnu \
+! RUN:   -emit-hlfir %s -o - | FileCheck %s %}
+
+! Executable loop transformations contribute to the construct context.
+! CHECK-LABEL: func.func @_QPtile_context(
+! CHECK: omp.barrier
+! CHECK-NOT: omp.taskyield
+! CHECK: return
+subroutine tile_context(n, a)
+  integer :: n, a(n, n), i, j
+  !$omp tile sizes(2)
+  do i = 1, n
+    !$omp metadirective &
+    !$omp& when(implementation={vendor(score(3): llvm)}: taskyield) &
+    !$omp& when(device={arch(x86_64)}: barrier)
+    do j = 1, n
+      a(j, i) = j
+    end do
+  end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPunroll_context(
+! CHECK: omp.barrier
+! CHECK-NOT: omp.taskyield
+! CHECK: return
+subroutine unroll_context(n, a)
+  integer :: n, a(n), i
+  !$omp unroll partial(2)
+  do i = 1, n
+    !$omp metadirective &
+    !$omp& when(implementation={vendor(score(3): llvm)}: taskyield) &
+    !$omp& when(device={arch(x86_64)}: barrier)
+    a(i) = i
+  end do
+end subroutine
+
+! Informational directives do not contribute to the construct context.
+! CHECK-LABEL: func.func @_QPassume_context()
+! CHECK: omp.taskyield
+! CHECK-NOT: omp.barrier
+! CHECK: return
+subroutine assume_context()
+  !$omp assume holds(.true.)
+    !$omp metadirective &
+    !$omp& when(implementation={vendor(score(3): llvm)}: taskyield) &
+    !$omp& when(device={arch(x86_64)}: barrier)
+  !$omp end assume
+end subroutine
+
+! Combined and composite constructs retain their source nesting order.
+! CHECK-LABEL: func.func @_QPcombined_context(
+! CHECK: omp.teams
+! CHECK: omp.parallel
+! CHECK: omp.distribute
+! CHECK: omp.wsloop
+! CHECK: omp.taskyield
+! CHECK-NOT: omp.barrier
+! CHECK: return
+subroutine combined_context(n)
+  integer :: n, i
+  !$omp teams distribute parallel do
+  do i = 1, n
+    !$omp metadirective &
+    !$omp& when(implementation={vendor(score(3): llvm)}: barrier) &
+    !$omp& when(construct={parallel}: taskyield)
+  end do
+end subroutine
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..d7963bf7fe4ec
--- /dev/null
+++ b/flang/test/Lower/OpenMP/metadirective-target-boundary.f90
@@ -0,0 +1,62 @@
+! 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, so the SIMD replacement is not lowered.
+! 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) 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) &
+      !$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-static.f90 b/flang/test/Lower/OpenMP/metadirective-user-static.f90
new file mode 100644
index 0000000000000..340b7c25293c8
--- /dev/null
+++ b/flang/test/Lower/OpenMP/metadirective-user-static.f90
@@ -0,0 +1,81 @@
+! Test lowering of OpenMP metadirective with static user={condition()}
+! selectors.
+
+! RUN: %flang_fc1 -fopenmp -emit-hlfir -fopenmp-version=50 %s -o - | \
+! RUN:   FileCheck %s
+! RUN: %flang_fc1 -fopenmp -emit-hlfir -fopenmp-version=51 %s -o - | \
+! RUN:   FileCheck %s
+! RUN: %flang_fc1 -fopenmp -emit-hlfir -fopenmp-version=52 %s -o - | \
+! RUN:   FileCheck %s
+
+! Large non-negative scores must not wrap their sum to zero.
+! CHECK-LABEL: func.func @_QPtest_wide_score()
+! CHECK: omp.parallel
+! CHECK-NOT: omp.taskyield
+! CHECK: omp.barrier
+! CHECK-NOT: omp.taskyield
+! CHECK: return
+subroutine test_wide_score()
+  !$omp parallel
+    !$omp metadirective &
+    !$omp& when(user={condition(score(9223372036854775807_8): .true.)}, &
+    !$omp& implementation={vendor(score(9223372036854775807_8): llvm)}, &
+    !$omp& construct={parallel}: barrier) &
+    !$omp& when(user={condition(.true.)}: taskyield)
+  !$omp end parallel
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_condition_true()
+! CHECK:         omp.taskyield
+! CHECK-NOT:     fir.if
+! CHECK:         return
+subroutine test_condition_true()
+  !$omp metadirective &
+  !$omp & when(user={condition(.true.)}: taskyield)
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_condition_false()
+! CHECK-NOT:     omp.taskwait
+! CHECK-NOT:     fir.if
+! CHECK:         return
+subroutine test_condition_false()
+  !$omp metadirective &
+  !$omp & when(user={condition(.false.)}: taskwait)
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_condition_score()
+! CHECK-NOT:     omp.taskyield
+! CHECK:         omp.taskwait
+! CHECK:         return
+subroutine test_condition_score()
+  !$omp metadirective &
+  !$omp & when(user={condition(.true.)}: taskyield) &
+  !$omp & when(user={condition(score(2): .true.)}: taskwait)
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_begin_condition_true()
+! CHECK:         omp.parallel
+! CHECK:           omp.terminator
+! CHECK-NOT:     fir.if
+! CHECK:         return
+subroutine test_begin_condition_true()
+  integer :: x
+  x = 0
+  !$omp begin metadirective &
+  !$omp & when(user={condition(.true.)}: parallel)
+  x = 1
+  !$omp end metadirective
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_begin_condition_false()
+! CHECK-NOT:     omp.parallel
+! CHECK-NOT:     fir.if
+! CHECK:         return
+subroutine test_begin_condition_false()
+  integer :: x
+  x = 0
+  !$omp begin metadirective &
+  !$omp & when(user={condition(.false.)}: parallel)
+  x = 1
+  !$omp end metadirective
+end subroutine
diff --git a/flang/test/Lower/OpenMP/metadirective-user.f90 b/flang/test/Lower/OpenMP/metadirective-user.f90
index cdfbddd4151a0..23c92954e7e75 100644
--- a/flang/test/Lower/OpenMP/metadirective-user.f90
+++ b/flang/test/Lower/OpenMP/metadirective-user.f90
@@ -1,93 +1,169 @@
-! Test lowering of OpenMP metadirective with user={condition()} selectors.
+! Test lowering of OpenMP metadirective with dynamic user={condition()}
+! selectors.
 
-! RUN: %flang_fc1 -fopenmp -emit-hlfir -fopenmp-version=50 %s -o - | FileCheck %s
 ! RUN: %flang_fc1 -fopenmp -emit-hlfir -fopenmp-version=51 %s -o - | FileCheck %s
 ! RUN: %flang_fc1 -fopenmp -emit-hlfir -fopenmp-version=52 -cpp -DOMP_52 %s -o - | FileCheck %s
 
 !===----------------------------------------------------------------------===!
-! Static (constant-folded) user conditions
+! Unknown ARCH retains its weight even when only the runtime condition matches.
+! CHECK-LABEL: func.func @_QPtest_unknown_arch_weight(
+! CHECK: fir.if
+! CHECK-NEXT: omp.barrier
+! CHECK-NEXT: } else {
+! CHECK-NEXT: omp.taskyield
+! CHECK: return
+subroutine test_unknown_arch_weight(flag)
+  logical :: flag
+  !$omp metadirective &
+  !$omp& when(device={arch(bogus_arch)}, &
+  !$omp& implementation={extension(match_any)}, &
+  !$omp& user={condition(flag)}: barrier) &
+  !$omp& when(user={condition(score(1): .true.)}: taskyield)
+end subroutine
+
+! Selectors with unknown properties retain their separate scores,
+! regardless of selector order.
+! CHECK-LABEL: func.func @_QPtest_unknown_selector_scores(
+! CHECK: fir.if
+! CHECK-NEXT: omp.barrier
+! CHECK-NEXT: } else {
+! CHECK-NEXT: omp.taskyield
+! CHECK: return
+subroutine test_unknown_selector_scores(flag)
+  logical :: flag
+  !$omp metadirective &
+  !$omp& when(implementation={vendor(score(10): bogus_vendor), &
+  !$omp& extension(score(1): match_any, bogus_extension)}, &
+  !$omp& user={condition(score(5): flag)}: barrier) &
+  !$omp& when(implementation={vendor(score(10): llvm)}: taskyield)
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_unknown_selector_scores_reversed(
+! CHECK: fir.if
+! CHECK-NEXT: omp.barrier
+! CHECK-NEXT: } else {
+! CHECK-NEXT: omp.taskyield
+! CHECK: return
+subroutine test_unknown_selector_scores_reversed(flag)
+  logical :: flag
+  !$omp metadirective &
+  !$omp& when(implementation={extension(score(1): match_any, bogus_extension), &
+  !$omp& vendor(score(10): bogus_vendor)}, &
+  !$omp& user={condition(score(5): flag)}: barrier) &
+  !$omp& when(implementation={vendor(score(10): llvm)}: taskyield)
+end subroutine
+
+! An unknown vendor does not veto a runtime MATCH_ANY condition or its score.
+! CHECK-LABEL: func.func @_QPtest_dynamic_unknown_vendor(
+! CHECK: fir.if
+! CHECK-NEXT: omp.barrier
+! CHECK-NEXT: } else {
+! CHECK-NEXT: omp.taskyield
+! CHECK: return
+subroutine test_dynamic_unknown_vendor(flag)
+  logical :: flag
+  !$omp metadirective &
+  !$omp& when(implementation={vendor(bogus_vendor), extension(match_any)}, &
+  !$omp& user={condition(score(5): flag)}: barrier) &
+  !$omp& when(user={condition(.true.)}: taskyield)
+end subroutine
+
+! The same rule applies to unknown device traits and implicit NOTHING.
+! CHECK-LABEL: func.func @_QPtest_dynamic_unknown_arch_implicit(
+! CHECK: fir.if
+! CHECK-NEXT: } else {
+! CHECK-NEXT: omp.barrier
+! CHECK: return
+subroutine test_dynamic_unknown_arch_implicit(flag)
+  logical :: flag
+  !$omp metadirective &
+  !$omp& when(device={arch(bogus_arch)}, &
+  !$omp& implementation={extension(match_any)}, &
+  !$omp& user={condition(score(10): flag)}:) &
+  !$omp& when(user={condition(score(5): .true.)}: barrier)
+end subroutine
+
+! Scored implicit NOTHING competes with explicit replacements by score.
 !===----------------------------------------------------------------------===!
 
-! CHECK-LABEL: func.func @_QPtest_condition_true()
-! CHECK:         omp.taskyield
-! CHECK-NOT:     fir.if
-! CHECK:         return
-subroutine test_condition_true()
+! 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(.true.)}: taskyield) &
-#ifdef OMP_52
-  !$omp & otherwise(nothing)
-#else
-  !$omp & default(nothing)
-#endif
+  !$omp& when(user={condition(score(10): flag)}:) &
+  !$omp& when(user={condition(score(5): .true.)}: barrier)
 end subroutine
 
-! CHECK-LABEL: func.func @_QPtest_condition_false()
-! CHECK-NOT:     omp.taskwait
-! CHECK-NOT:     fir.if
-! CHECK:         return
-subroutine test_condition_false()
+! 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(.false.)}: taskwait) &
-#ifdef OMP_52
-  !$omp & otherwise(nothing)
-#else
-  !$omp & default(nothing)
-#endif
+  !$omp& when(user={condition(score(10): flag)}: nothing) &
+  !$omp& when(user={condition(score(5): .true.)}: barrier)
 end subroutine
 
-! CHECK-LABEL: func.func @_QPtest_condition_score()
-! CHECK-NOT:     omp.taskyield
-! CHECK:         omp.taskwait
-! CHECK:         return
-subroutine test_condition_score()
+! 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(.true.)}: taskyield) &
-  !$omp & when(user={condition(score(2): .true.)}: taskwait) &
-#ifdef OMP_52
-  !$omp & otherwise(nothing)
-#else
-  !$omp & default(nothing)
-#endif
+  !$omp& when(user={condition(score(5): flag)}:) &
+  !$omp& when(user={condition(score(5): .true.)}: barrier)
 end subroutine
 
-! CHECK-LABEL: func.func @_QPtest_begin_condition_true()
-! CHECK:         omp.parallel
-! CHECK:           omp.terminator
-! CHECK-NOT:     fir.if
-! CHECK:         return
-subroutine test_begin_condition_true()
-  integer :: x
-  x = 0
-#ifdef OMP_52
-  !$omp begin metadirective &
-  !$omp & when(user={condition(.true.)}: parallel) &
-  !$omp & otherwise(nothing)
-#else
-  !$omp begin metadirective &
-  !$omp & when(user={condition(.true.)}: parallel)
-#endif
-  x = 1
-  !$omp end metadirective
+! 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
 
-! CHECK-LABEL: func.func @_QPtest_begin_condition_false()
-! CHECK-NOT:     omp.parallel
-! CHECK-NOT:     fir.if
-! CHECK:         return
-subroutine test_begin_condition_false()
-  integer :: x
-  x = 0
-#ifdef OMP_52
-  !$omp begin metadirective &
-  !$omp & when(user={condition(.false.)}: parallel) &
-  !$omp & otherwise(nothing)
-#else
-  !$omp begin metadirective &
-  !$omp & when(user={condition(.false.)}: parallel)
-#endif
-  x = 1
-  !$omp end metadirective
+! 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
 
 !===----------------------------------------------------------------------===!
@@ -331,8 +407,21 @@ subroutine test_dynamic_user_score_order(low, high)
   !$omp & when(device={kind(host)}: taskwait)
 end subroutine
 
-! Under extension(match_none), the runtime condition selects the variant when it
-! is false. Its score is still available for ranking that false path.
+! A statically false MATCH_NONE condition is the selector that makes the
+! candidate applicable, so its score participates in ranking.
+! CHECK-LABEL: func.func @_QPtest_static_user_score_match_none()
+! CHECK-NOT:     fir.if
+! CHECK:         omp.barrier
+! CHECK-NOT:     omp.taskwait
+! CHECK:         return
+subroutine test_static_user_score_match_none()
+  !$omp metadirective &
+  !$omp & when(implementation={extension(match_none)}, &
+  !$omp & user={condition(score(100): .false.)}: barrier) &
+  !$omp & when(user={condition(score(10): .true.)}: taskwait)
+end subroutine
+
+! The dynamic form must use the same score on its false path.
 ! CHECK-LABEL: func.func @_QPtest_dynamic_user_score_match_none(
 ! CHECK-SAME:    %[[ARG0:.*]]: !fir.ref<!fir.logical<4>>
 ! CHECK:         %[[DECL:.*]]:2 = hlfir.declare %[[ARG0]]
@@ -349,12 +438,9 @@ subroutine test_dynamic_user_score_order(low, high)
 subroutine test_dynamic_user_score_match_none(flag)
   logical, intent(in) :: flag
   !$omp metadirective &
-  !$omp & when(implementation={extension(match_none)}, user={condition(score(5): flag)}: barrier) &
-#ifdef OMP_52
-  !$omp & otherwise(taskwait)
-#else
-  !$omp & default(taskwait)
-#endif
+  !$omp & when(implementation={extension(match_none)}, &
+  !$omp & user={condition(score(100): flag)}: barrier) &
+  !$omp & when(user={condition(score(10): .true.)}: taskwait)
 end subroutine
 
 ! Under extension(match_any), a dynamic condition can be the selector that makes
@@ -425,13 +511,61 @@ 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
+! A statically false condition follows the dynamic false-path rule: its score
+! does not contribute when another selector satisfies MATCH_ANY.
+! CHECK-LABEL: func.func @_QPtest_static_user_match_any_false_score()
+! CHECK-NOT: fir.if
+! CHECK-NOT: omp.barrier
+! CHECK: omp.taskwait
+! CHECK: return
+subroutine test_static_user_match_any_false_score()
+  !$omp metadirective &
+  !$omp& when(implementation={extension(match_any), vendor(llvm)}, &
+  !$omp& user={condition(score(100): .false.)}: barrier) &
+  !$omp& when(user={condition(score(10): .true.)}: taskwait) &
+#ifdef OMP_52
+  !$omp& otherwise(nothing)
+#else
+  !$omp& default(nothing)
+#endif
+end subroutine
+
+! A false user condition remains part of the selector for specificity. The
+! more-specific MATCH_ANY candidate therefore wins over the vendor-only
+! candidate in both the static and dynamic forms.
+! CHECK-LABEL: func.func @_QPtest_static_user_match_any_false_subset()
+! CHECK-NOT: fir.if
+! CHECK: omp.barrier
+! CHECK-NOT: omp.taskwait
+! CHECK: return
+subroutine test_static_user_match_any_false_subset()
+  !$omp metadirective &
+  !$omp& when(implementation={extension(match_any), vendor(llvm)}, &
+  !$omp& user={condition(.false.)}: barrier) &
+  !$omp& when(implementation={vendor(llvm)}: taskwait)
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_dynamic_user_match_any_false_subset(
+! CHECK-NOT: fir.if
+! CHECK: omp.barrier
+! CHECK-NOT: omp.taskwait
+! CHECK: return
+subroutine test_dynamic_user_match_any_false_subset(flag)
+  logical, intent(in) :: flag
+  !$omp metadirective &
+  !$omp& when(implementation={extension(match_any), vendor(llvm)}, &
+  !$omp& user={condition(flag)}: barrier) &
+  !$omp& when(implementation={vendor(llvm)}: taskwait)
+end subroutine
+
+! The vendor-only selector is a strict subset of the implicit NOTHING's
+! 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 {
+! 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/Lower/OpenMP/variant-scoring-any-sections.f90 b/flang/test/Lower/OpenMP/variant-scoring-any-sections.f90
new file mode 100644
index 0000000000000..e51df9bc37746
--- /dev/null
+++ b/flang/test/Lower/OpenMP/variant-scoring-any-sections.f90
@@ -0,0 +1,81 @@
+! RUN: %flang_fc1 -emit-fir -fopenmp -fopenmp-version=52 %s -o - \
+! RUN:   | FileCheck %s
+! RUN: %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 %s -o - \
+! RUN:   | FileCheck %s
+
+module scoring
+contains
+  subroutine high()
+  end subroutine
+  subroutine low()
+  end subroutine
+  subroutine cpu()
+  end subroutine
+  subroutine vendor()
+  end subroutine
+
+  subroutine base_any()
+    !$omp declare variant(high) &
+    !$omp& match(implementation={vendor(score(100): llvm)})
+    !$omp declare variant(low) &
+    !$omp& match(implementation={vendor(score(1): llvm)}, device={kind(any)})
+  end subroutine
+
+  ! kind(any) does not make the lower-scored selector a strict superset.
+  ! CHECK-LABEL: func.func @_QMscoringPtest_any()
+  ! CHECK-NOT: fir.call @_QMscoringPlow
+  ! CHECK: fir.call @_QMscoringPhigh()
+  ! CHECK-NOT: fir.call @_QMscoringPlow
+  ! CHECK: return
+  subroutine test_any()
+    call base_any()
+  end subroutine
+
+  subroutine base_sections()
+    !$omp declare variant(cpu) match(device={kind(cpu)})
+    !$omp declare variant(vendor) &
+    !$omp& match(implementation={vendor(score(5): llvm)})
+  end subroutine
+
+  ! SECTION is a separator: the context depth is two, CPU scores 5, and the
+  ! vendor scores 6 whether the optional first SECTION is present or absent.
+  ! CHECK-LABEL: func.func @_QMscoringPtest_explicit_section()
+  ! CHECK: omp.parallel
+  ! CHECK: omp.sections
+  ! CHECK: omp.section
+  ! CHECK-NOT: fir.call @_QMscoringPcpu
+  ! CHECK: fir.call @_QMscoringPvendor()
+  ! CHECK-NOT: fir.call @_QMscoringPcpu
+  ! CHECK: return
+  subroutine test_explicit_section()
+    !$omp parallel sections
+      !$omp section
+        call base_sections()
+    !$omp end parallel sections
+  end subroutine
+
+  ! CHECK-LABEL: func.func @_QMscoringPtest_implicit_section()
+  ! CHECK: omp.parallel
+  ! CHECK: omp.sections
+  ! CHECK: omp.section
+  ! CHECK-NOT: fir.call @_QMscoringPcpu
+  ! CHECK: fir.call @_QMscoringPvendor()
+  ! CHECK-NOT: fir.call @_QMscoringPcpu
+  ! CHECK: return
+  subroutine test_implicit_section()
+    !$omp parallel sections
+      call base_sections()
+    !$omp end parallel sections
+  end subroutine
+end module
+
+! CHECK-LABEL: func.func @_QPtest_metadirective_any()
+! CHECK-NOT: omp.taskyield
+! CHECK: omp.barrier
+! CHECK-NEXT: return
+subroutine test_metadirective_any()
+  !$omp metadirective &
+  !$omp& when(implementation={vendor(score(100): llvm)}: barrier) &
+  !$omp& when(implementation={vendor(score(1): llvm)}, &
+  !$omp& device={kind(any)}: taskyield)
+end subroutine
diff --git a/llvm/include/llvm/Frontend/OpenMP/OMPContext.h b/llvm/include/llvm/Frontend/OpenMP/OMPContext.h
index 7849d32665994..f6f3f3a22eeff 100644
--- a/llvm/include/llvm/Frontend/OpenMP/OMPContext.h
+++ b/llvm/include/llvm/Frontend/OpenMP/OMPContext.h
@@ -21,6 +21,7 @@
 #include "llvm/ADT/DenseMapInfo.h"
 #include "llvm/Frontend/OpenMP/OMPConstants.h"
 #include "llvm/Support/Compiler.h"
+#include <optional>
 
 namespace llvm {
 class Triple;
@@ -118,10 +119,36 @@ LLVM_ABI bool isValidTraitPropertyForTraitSetAndSelector(TraitProperty Property,
                                                          TraitSelector Selector,
                                                          TraitSet Set);
 
-/// Variant match information describes the required traits and how they are
-/// scored (via the ScoresMap). In addition, the required consturct nesting is
-/// decribed as well.
+/// Variant match information describes the required property traits, how they
+/// are scored, and the required construct nesting.
 struct VariantMatchInfo {
+  struct ISATrait {
+    TraitProperty Property;
+    StringRef Name;
+
+    bool operator==(const ISATrait &Other) const {
+      return Property == Other.Property && Name == Other.Name;
+    }
+  };
+
+  struct UnknownTrait {
+    TraitSelector Selector;
+    StringRef Name;
+    std::optional<APInt> Score;
+
+    // Subset checks compare property identity, not its score.
+    bool operator==(const UnknownTrait &Other) const {
+      return Selector == Other.Selector && Name == Other.Name;
+    }
+  };
+
+  /// Keep unknown properties inactive without losing their selector or score.
+  void addUnknownTrait(TraitSelector Selector, StringRef Name,
+                       APInt *Score = nullptr) {
+    UnknownTraits.push_back(
+        {Selector, Name, Score ? std::optional<APInt>(*Score) : std::nullopt});
+  }
+
   /// Add the trait \p Property to the required trait set. \p RawString is the
   /// string we parsed and derived \p Property from. If \p Score is not null, it
   /// recorded as well. If \p Property is in the `construct` set it is recorded
@@ -142,20 +169,27 @@ struct VariantMatchInfo {
 
     // Special handling for `device={isa(...)}` as we do not match the enum but
     // the raw string.
-    if (Property == TraitProperty::device_isa___ANY)
-      ISATraits.push_back(RawString);
-    if (Property == TraitProperty::target_device_isa___ANY)
-      ISATraits.push_back(RawString);
+    if (Property == TraitProperty::device_isa___ANY ||
+        Property == TraitProperty::target_device_isa___ANY)
+      ISATraits.push_back({Property, RawString});
 
     RequiredTraits.set(unsigned(Property));
     if (Set == TraitSet::construct)
       ConstructTraits.push_back(Property);
+    if (getOpenMPContextTraitSelectorForProperty(Property) ==
+        TraitSelector::user_condition)
+      UserCondition = RawString;
   }
 
   BitVector RequiredTraits = BitVector(unsigned(TraitProperty::Last) + 1);
-  SmallVector<StringRef, 8> ISATraits;
+  SmallVector<ISATrait, 8> ISATraits;
   SmallVector<TraitProperty, 8> ConstructTraits;
   SmallDenseMap<TraitProperty, APInt> ScoreMap;
+  SmallVector<UnknownTrait, 2> UnknownTraits;
+  /// Identity of a user condition expression, when the producer can retain
+  /// it. Applicability uses the folded property above; subset checks use this
+  /// identity to avoid conflating distinct dynamic expressions.
+  StringRef UserCondition;
 };
 
 /// The context for a source location is made up of active property traits,
@@ -175,12 +209,22 @@ struct OMPContext {
       ConstructTraits.push_back(Property);
   }
 
+  /// Record an enclosing construct that has no corresponding construct
+  /// selector property. Such constructs still contribute to the positions
+  /// and device-selector weights used during variant scoring.
+  void addUnknownConstruct() {
+    ConstructTraits.push_back(TraitProperty::invalid);
+  }
+
   /// Hook for users to check if an ISA trait matches. The trait is described as
   /// the string that got parsed and it depends on the target and context if
   /// this matches or not.
   virtual bool matchesISATrait(StringRef) const { return false; }
 
   BitVector ActiveTraits = BitVector(unsigned(TraitProperty::Last) + 1);
+  /// Enclosing constructs in outermost-to-innermost order. An `invalid`
+  /// entry represents a construct that cannot itself appear in a construct
+  /// selector, but which still contributes to scoring depth.
   SmallVector<TraitProperty, 8> ConstructTraits;
 };
 
diff --git a/llvm/lib/Frontend/OpenMP/OMPContext.cpp b/llvm/lib/Frontend/OpenMP/OMPContext.cpp
index f2cdd9bbaf5e4..69d8c94cf98be 100644
--- a/llvm/lib/Frontend/OpenMP/OMPContext.cpp
+++ b/llvm/lib/Frontend/OpenMP/OMPContext.cpp
@@ -13,11 +13,13 @@
 //===----------------------------------------------------------------------===//
 
 #include "llvm/Frontend/OpenMP/OMPContext.h"
+#include "llvm/ADT/DenseSet.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/ADT/StringSwitch.h"
 #include "llvm/Support/Debug.h"
 #include "llvm/Support/raw_ostream.h"
 #include "llvm/TargetParser/Triple.h"
+#include <algorithm>
 
 #define DEBUG_TYPE "openmp-ir-builder"
 
@@ -138,13 +140,9 @@ OMPContext::OMPContext(bool IsDeviceCompilation, Triple TargetTriple,
   }
 }
 
-/// Return true if \p C0 is a subset of \p C1. Note that both arrays are
-/// expected to be sorted.
-template <typename T> static bool isSubset(ArrayRef<T> C0, ArrayRef<T> C1) {
-#ifdef EXPENSIVE_CHECKS
-  assert(llvm::is_sorted(C0) && llvm::is_sorted(C1) &&
-         "Expected sorted arrays!");
-#endif
+/// Return true if \p C0 is an ordered subsequence of \p C1.
+template <typename T>
+static bool isOrderedSubset(ArrayRef<T> C0, ArrayRef<T> C1) {
   if (C0.size() > C1.size())
     return false;
   auto It0 = C0.begin(), End0 = C0.end();
@@ -157,24 +155,46 @@ template <typename T> static bool isSubset(ArrayRef<T> C0, ArrayRef<T> C1) {
       ++It1;
       continue;
     }
-    ++It0;
+    ++It1;
   }
   return true;
 }
 
 static bool isStrictSubset(const VariantMatchInfo &VMI0,
                            const VariantMatchInfo &VMI1) {
-  // If all required traits are a strict subset and the ordered vectors storing
-  // the construct traits, we say it is a strict subset. Note that the latter
-  // relation is not required to be strict.
-  if (VMI0.RequiredTraits.count() >= VMI1.RequiredTraits.count())
+  // kind(any) is equivalent to omitting the kind selector.
+  BitVector Traits0 = VMI0.RequiredTraits, Traits1 = VMI1.RequiredTraits;
+  for (TraitProperty Property : {TraitProperty::device_kind_any,
+                                 TraitProperty::target_device_kind_any}) {
+    Traits0.reset(unsigned(Property));
+    Traits1.reset(unsigned(Property));
+  }
+  size_t TraitCount0 = Traits0.count() + VMI0.UnknownTraits.size();
+  size_t TraitCount1 = Traits1.count() + VMI1.UnknownTraits.size();
+  if (TraitCount0 > TraitCount1)
     return false;
-  for (unsigned Bit : VMI0.RequiredTraits.set_bits())
-    if (!VMI1.RequiredTraits.test(Bit))
+  for (unsigned Bit : Traits0.set_bits())
+    if (!Traits1.test(Bit))
+      return false;
+  for (const auto &Trait : VMI0.UnknownTraits)
+    if (!llvm::is_contained(VMI1.UnknownTraits, Trait))
       return false;
-  if (!isSubset<TraitProperty>(VMI0.ConstructTraits, VMI1.ConstructTraits))
+  for (const auto &Trait : VMI0.ISATraits)
+    if (!llvm::is_contained(VMI1.ISATraits, Trait))
+      return false;
+  bool HasAdditionalISATrait =
+      llvm::any_of(VMI1.ISATraits, [&](const auto &Trait) {
+        return !llvm::is_contained(VMI0.ISATraits, Trait);
+      });
+  if (!VMI0.UserCondition.empty() && VMI0.UserCondition != VMI1.UserCondition)
     return false;
-  return true;
+  if (!isOrderedSubset<TraitProperty>(VMI0.ConstructTraits,
+                                      VMI1.ConstructTraits))
+    return false;
+  // RequiredTraits is a bit vector, so repeated construct properties only
+  // make the ordered vector strict.
+  return TraitCount0 < TraitCount1 || HasAdditionalISATrait ||
+         VMI0.ConstructTraits.size() < VMI1.ConstructTraits.size();
 }
 
 static int
@@ -197,23 +217,21 @@ 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.
-    if (MK == MK_ANY) {
-      if (WasFound)
-        return true;
-      return std::nullopt;
-    }
+  bool AnyTraitMatched = false;
+
+  // 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;
+    if (MK == MK_ANY)
+      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({
@@ -229,6 +247,9 @@ isVariantApplicableInContextHelper(const VariantMatchInfo &VMI,
     return false;
   };
 
+  if (!VMI.UnknownTraits.empty() && !HandleTrait(TraitProperty::invalid, false))
+    return false;
+
   for (unsigned Bit : VMI.RequiredTraits.set_bits()) {
     TraitProperty Property = TraitProperty(Bit);
     if (DeviceOrImplementationSetOnly &&
@@ -247,39 +268,38 @@ isVariantApplicableInContextHelper(const VariantMatchInfo &VMI,
 
     // We overwrite the isa trait as it is actually up to the OMPContext hook to
     // check the raw string(s).
-    if (Property == TraitProperty::device_isa___ANY)
-      IsActiveTrait = llvm::all_of(VMI.ISATraits, [&](StringRef RawString) {
-        return Ctx.matchesISATrait(RawString);
-      });
-    if (Property == TraitProperty::target_device_isa___ANY)
-      IsActiveTrait = llvm::all_of(VMI.ISATraits, [&](StringRef RawString) {
-        return Ctx.matchesISATrait(RawString);
+    if (Property == TraitProperty::device_isa___ANY ||
+        Property == TraitProperty::target_device_isa___ANY)
+      IsActiveTrait = llvm::all_of(VMI.ISATraits, [&](const auto &Trait) {
+        return Trait.Property != Property || Ctx.matchesISATrait(Trait.Name);
       });
 
-    if (std::optional<bool> Result = HandleTrait(Property, IsActiveTrait))
-      return *Result;
+    if (!HandleTrait(Property, IsActiveTrait))
+      return false;
   }
 
   if (!DeviceOrImplementationSetOnly) {
-    // We could use isSubset here but we also want to record the match
-    // locations.
+    // Scan the construct sequence in order, recording matching context
+    // positions for scoring.
     unsigned ConstructIdx = 0, NoConstructTraits = Ctx.ConstructTraits.size();
     for (TraitProperty Property : VMI.ConstructTraits) {
       assert(getOpenMPContextTraitSetForProperty(Property) ==
                  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 (ConstructMatches)
+      if (!FoundInOrder && MK != MK_ALL)
+        ConstructIdx = SearchStart;
+      if (ConstructMatches && FoundInOrder)
         ConstructMatches->push_back(ConstructIdx - 1);
 
-      if (std::optional<bool> Result = HandleTrait(Property, FoundInOrder))
-        return *Result;
-
-      if (!FoundInOrder) {
+      if (!HandleTrait(Property, FoundInOrder)) {
         LLVM_DEBUG(dbgs() << "[" << DEBUG_TYPE << "] Construct property "
                           << getOpenMPContextTraitPropertyName(Property, "")
                           << " was not nested properly.\n");
@@ -289,11 +309,29 @@ isVariantApplicableInContextHelper(const VariantMatchInfo &VMI,
       // TODO: Verify SIMD
     }
 
-    assert(isSubset<TraitProperty>(VMI.ConstructTraits, Ctx.ConstructTraits) &&
-           "Broken invariant!");
+    // 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(isOrderedSubset<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");
@@ -313,118 +351,120 @@ bool llvm::omp::isVariantApplicableInContext(
 static APInt getVariantMatchScore(const VariantMatchInfo &VMI,
                                   const OMPContext &Ctx,
                                   SmallVectorImpl<unsigned> &ConstructMatches) {
-  APInt Score(64, 1);
-
-  unsigned NoConstructTraits = VMI.ConstructTraits.size();
-  for (unsigned Bit : VMI.RequiredTraits.set_bits()) {
-    TraitProperty Property = TraitProperty(Bit);
-    // If there is a user score attached, use it.
-    if (VMI.ScoreMap.count(Property)) {
-      const APInt &UserScore = VMI.ScoreMap.lookup(Property);
-      assert(UserScore.uge(0) && "Expect non-negative user scores!");
-      Score += UserScore.getZExtValue();
-      continue;
-    }
+  APInt Score(1, 1);
+
+  // A sum of valid scores can exceed the width of any individual score.
+  // Retain all active bits and allow one more bit for each addition's carry.
+  auto AddScore = [&](const APInt &Value) {
+    unsigned Width = std::max(Score.getActiveBits(), Value.getActiveBits()) + 1;
+    Score = Score.zextOrTrunc(Width);
+    Score += Value.zextOrTrunc(Width);
+  };
+  auto AddPowerOfTwo = [&](unsigned Exponent) {
+    AddScore(APInt::getOneBitSet(Exponent + 1, Exponent));
+  };
 
-    switch (getOpenMPContextTraitSetForProperty(Property)) {
-    case TraitSet::construct:
-      // We handle the construct traits later via the VMI.ConstructTraits
-      // container.
-      continue;
-    case TraitSet::implementation:
-      // No effect on the score (implementation defined).
-      continue;
-    case TraitSet::user:
-      // No effect on the score.
-      continue;
-    case TraitSet::device:
-      // Handled separately below.
-      break;
-    case TraitSet::target_device:
-      // TODO: Handling separately.
-      break;
-    case TraitSet::invalid:
-      llvm_unreachable("Unknown trait set is not to be used!");
+  unsigned NoConstructTraits = Ctx.ConstructTraits.size();
+  bool HasMatchAny = VMI.RequiredTraits.test(
+      unsigned(TraitProperty::implementation_extension_match_any));
+  SmallDenseSet<TraitSelector, 8> ScoredSelectors;
+  auto AddSelectorScore = [&](TraitSelector Selector, const APInt *UserScore) {
+    // Scores belong to selectors, not to individual properties. Unknown
+    // properties retain these scores even though they never match.
+    if (!ScoredSelectors.insert(Selector).second)
+      return;
+    if (UserScore) {
+      AddScore(*UserScore);
+      return;
     }
-
-    // device={kind(any)} is "as if" no kind selector was specified.
-    if (Property == TraitProperty::device_kind_any)
-      continue;
-    if (Property == TraitProperty::target_device_kind_any)
-      continue;
-
-    switch (getOpenMPContextTraitSelectorForProperty(Property)) {
+    switch (Selector) {
     case TraitSelector::device_kind:
-      Score += (1ULL << (NoConstructTraits + 0));
-      continue;
-    case TraitSelector::device_arch:
-      Score += (1ULL << (NoConstructTraits + 1));
-      continue;
-    case TraitSelector::device_isa:
-      Score += (1ULL << (NoConstructTraits + 2));
-      continue;
     case TraitSelector::target_device_kind:
-      Score += (1ULL << (NoConstructTraits + 0));
-      continue;
+      AddPowerOfTwo(NoConstructTraits);
+      break;
+    case TraitSelector::device_arch:
     case TraitSelector::target_device_arch:
-      Score += (1ULL << (NoConstructTraits + 1));
-      continue;
+      AddPowerOfTwo(NoConstructTraits + 1);
+      break;
+    case TraitSelector::device_isa:
     case TraitSelector::target_device_isa:
-      Score += (1ULL << (NoConstructTraits + 2));
-      continue;
+      AddPowerOfTwo(NoConstructTraits + 2);
+      break;
     default:
-      continue;
+      break;
     }
+  };
+  for (unsigned Bit : VMI.RequiredTraits.set_bits()) {
+    TraitProperty Property = TraitProperty(Bit);
+    // Construct scores use ordered positions below. kind(any) is treated as
+    // if no kind selector were specified. A false condition contributes no
+    // score when match_any is satisfied by another selector.
+    if (getOpenMPContextTraitSetForProperty(Property) == TraitSet::construct ||
+        Property == TraitProperty::device_kind_any ||
+        Property == TraitProperty::target_device_kind_any ||
+        (Property == TraitProperty::user_condition_false && HasMatchAny))
+      continue;
+    auto It = VMI.ScoreMap.find(Property);
+    AddSelectorScore(getOpenMPContextTraitSelectorForProperty(Property),
+                     It == VMI.ScoreMap.end() ? nullptr : &It->second);
   }
+  for (const auto &Trait : VMI.UnknownTraits)
+    AddSelectorScore(Trait.Selector, Trait.Score ? &*Trait.Score : nullptr);
 
-  unsigned ConstructIdx = 0;
-  assert(NoConstructTraits == ConstructMatches.size() &&
+  assert(VMI.ConstructTraits.size() >= ConstructMatches.size() &&
          "Mismatch in the construct traits!");
-  for (TraitProperty Property : VMI.ConstructTraits) {
-    assert(getOpenMPContextTraitSetForProperty(Property) ==
-               TraitSet::construct &&
-           "Ill-formed variant match info!");
-    (void)Property;
+  for (unsigned Match : ConstructMatches) {
     // ConstructMatches is the position p - 1 and we need 2^(p-1).
-    Score += (1ULL << ConstructMatches[ConstructIdx++]);
+    AddPowerOfTwo(Match);
   }
 
-  LLVM_DEBUG(dbgs() << "[" << DEBUG_TYPE << "] Variant has a score of " << Score
-                    << "\n");
+  LLVM_DEBUG({
+    dbgs() << "[" << DEBUG_TYPE << "] Variant has a score of ";
+    Score.print(dbgs(), /*isSigned=*/false);
+    dbgs() << "\n";
+  });
   return Score;
 }
 
 int llvm::omp::getBestVariantMatchForContext(
     const SmallVectorImpl<VariantMatchInfo> &VMIs, const OMPContext &Ctx) {
-
-  APInt BestScore(64, 0);
-  int BestVMIIdx = -1;
-  const VariantMatchInfo *BestVMI = nullptr;
-
+  SmallVector<std::optional<APInt>, 4> Scores(VMIs.size());
   for (unsigned u = 0, e = VMIs.size(); u < e; ++u) {
     const VariantMatchInfo &VMI = VMIs[u];
 
     SmallVector<unsigned, 8> ConstructMatches;
-    // If the variant is not applicable its not the best.
+    // Inapplicable variants do not participate in scoring or subset checks.
     if (!isVariantApplicableInContextHelper(
             VMI, Ctx, &ConstructMatches,
             /* DeviceOrImplementationSetOnly */ false))
       continue;
-    // Check if its clearly not the best.
-    APInt Score = getVariantMatchScore(VMI, Ctx, ConstructMatches);
-    if (Score.ult(BestScore))
+    Scores[u] = getVariantMatchScore(VMI, Ctx, ConstructMatches);
+  }
+
+  // A compatible selector that is a strict subset of another compatible
+  // selector has score zero, irrespective of their scores before this step.
+  // Apply this rule globally before choosing the maximum.
+  for (unsigned u = 0, e = VMIs.size(); u < e; ++u) {
+    if (!Scores[u])
       continue;
-    // Equal score need subset checks.
-    if (Score.eq(BestScore)) {
-      // Strict subset are never best.
-      if (isStrictSubset(VMI, *BestVMI))
-        continue;
-      // Same score and the current best is no strict subset so we keep it.
-      if (!isStrictSubset(*BestVMI, VMI))
-        continue;
+    for (unsigned v = 0; v < e; ++v) {
+      if (u != v && Scores[v] && isStrictSubset(VMIs[u], VMIs[v])) {
+        Scores[u] = APInt(1, 0);
+        break;
+      }
     }
-    // New best found.
-    BestVMI = &VMI;
+  }
+
+  APInt BestScore(1, 0);
+  int BestVMIIdx = -1;
+  for (unsigned u = 0, e = VMIs.size(); u < e; ++u) {
+    if (!Scores[u])
+      continue;
+    const APInt &Score = *Scores[u];
+    unsigned Width = std::max(Score.getBitWidth(), BestScore.getBitWidth());
+    if (BestVMIIdx >= 0 &&
+        !BestScore.zextOrTrunc(Width).ult(Score.zextOrTrunc(Width)))
+      continue;
     BestVMIIdx = u;
     BestScore = Score;
   }
diff --git a/llvm/unittests/Frontend/OpenMPContextTest.cpp b/llvm/unittests/Frontend/OpenMPContextTest.cpp
index f9683ae56e933..cf8e68f0a33a5 100644
--- a/llvm/unittests/Frontend/OpenMPContextTest.cpp
+++ b/llvm/unittests/Frontend/OpenMPContextTest.cpp
@@ -314,7 +314,536 @@ 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);
+
+  APInt Score(64, 2);
+  VariantMatchInfo Scored;
+  Scored.addTrait(TraitProperty::user_condition_true, "", &Score);
+
+  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 for MATCH_ANY beats the
+  // explicitly scored candidate's 1 + 2. Retaining only either construct
+  // match would not select MATCH_ANY.
+  SmallVector<VariantMatchInfo, 2> Candidates{Scored, 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);
+}
+
+TEST_F(OpenMPContextTest, ScoringMatchAnyFalseCondition) {
+  OMPContext Context(false, Triple("x86_64-unknown-linux"), Triple(), -1);
+  APInt HighScore(64, 100), LowScore(64, 10);
+
+  VariantMatchInfo MatchAnyFalse;
+  MatchAnyFalse.addTrait(TraitProperty::implementation_vendor_llvm, "");
+  MatchAnyFalse.addTrait(TraitProperty::implementation_extension_match_any, "");
+  MatchAnyFalse.addTrait(TraitProperty::user_condition_false, "condition",
+                         &HighScore);
+
+  VariantMatchInfo ScoredTrue;
+  ScoredTrue.addTrait(TraitProperty::user_condition_true, "other condition",
+                      &LowScore);
+
+  // A matching static trait keeps MATCH_ANY applicable, but a false condition
+  // does not contribute its score.
+  SmallVector<VariantMatchInfo, 2> Candidates{MatchAnyFalse, ScoredTrue};
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 1);
+
+  VariantMatchInfo MatchAnyTrue;
+  MatchAnyTrue.addTrait(TraitProperty::implementation_vendor_llvm, "");
+  MatchAnyTrue.addTrait(TraitProperty::implementation_extension_match_any, "");
+  MatchAnyTrue.addTrait(TraitProperty::user_condition_true, "condition",
+                        &HighScore);
+  Candidates[0] = MatchAnyTrue;
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 0);
+
+  VariantMatchInfo MatchNoneFalse;
+  MatchNoneFalse.addTrait(TraitProperty::implementation_extension_match_none,
+                          "");
+  MatchNoneFalse.addTrait(TraitProperty::user_condition_false, "condition",
+                          &HighScore);
+  // A false condition is the selector that makes MATCH_NONE applicable, so
+  // its score still participates in ranking.
+  Candidates[0] = MatchNoneFalse;
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 0);
+}
+
+TEST_F(OpenMPContextTest, ScoringUnknownProperty) {
+  OMPContext Context(false, Triple("x86_64-unknown-linux"), Triple(), -1);
+  VariantMatchInfo Unknown;
+  Unknown.addUnknownTrait(TraitSelector::implementation_vendor, "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 unscored vendor 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, ScoringUnknownDeviceProperties) {
+  const TraitSelector Selectors[] = {
+      TraitSelector::device_kind, TraitSelector::device_arch,
+      TraitSelector::target_device_kind, TraitSelector::target_device_arch};
+  for (unsigned Depth : {0u, 2u, 64u}) {
+    OMPContext Context(false, Triple("x86_64-unknown-linux"), Triple(), -1);
+    for (unsigned I = 0; I < Depth; ++I)
+      Context.addTrait(TraitProperty::construct_parallel_parallel);
+    for (unsigned I = 0; I < 4; ++I) {
+      SCOPED_TRACE(Depth);
+      SCOPED_TRACE(I);
+      VariantMatchInfo Unknown;
+      Unknown.addUnknownTrait(Selectors[I], "unknown");
+      EXPECT_FALSE(isVariantApplicableInContext(Unknown, Context));
+      Unknown.addTrait(TraitProperty::implementation_extension_match_any, "");
+      EXPECT_FALSE(isVariantApplicableInContext(Unknown, Context));
+      Unknown.addTrait(TraitProperty::user_condition_true, "");
+      EXPECT_TRUE(isVariantApplicableInContext(Unknown, Context));
+
+      APInt Weight = APInt::getOneBitSet(128, Depth + I % 2);
+      APInt Score = Weight - 1;
+      VariantMatchInfo Scored;
+      Scored.addTrait(TraitProperty::implementation_vendor_llvm, "", &Score);
+      SmallVector<VariantMatchInfo, 2> Candidates{Scored, Unknown};
+      EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 1);
+      Candidates[0].addTrait(TraitProperty::implementation_vendor_llvm, "",
+                             &Weight);
+      EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 0);
+
+      // match_none also retains the weight of an inactive selector.
+      Candidates[1] = VariantMatchInfo();
+      Candidates[1].addUnknownTrait(Selectors[I], "unknown");
+      Candidates[1].addTrait(TraitProperty::implementation_extension_match_none,
+                             "");
+      EXPECT_TRUE(isVariantApplicableInContext(Candidates[1], Context));
+      EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 0);
+      Candidates[0].addTrait(TraitProperty::implementation_vendor_llvm, "",
+                             &Score);
+      EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 1);
+    }
+  }
+}
+
+TEST_F(OpenMPContextTest, ScoringUnknownSelectors) {
+  OMPContext Context(false, Triple("x86_64-unknown-linux"), Triple(), -1);
+  APInt VendorScore(64, 10), ExtensionScore(64, 1), ConditionScore(64, 5);
+  for (bool Reverse : {false, true}) {
+    SCOPED_TRACE(Reverse);
+    VariantMatchInfo Unknown;
+    Unknown.addTrait(TraitProperty::implementation_extension_match_any, "",
+                     &ExtensionScore);
+    Unknown.addTrait(TraitProperty::user_condition_true, "", &ConditionScore);
+    Unknown.addUnknownTrait(TraitSelector::implementation_vendor, "vendor",
+                            &VendorScore);
+    Unknown.addUnknownTrait(TraitSelector::implementation_extension,
+                            "extension", &ExtensionScore);
+    if (Reverse)
+      std::swap(Unknown.UnknownTraits[0], Unknown.UnknownTraits[1]);
+
+    VariantMatchInfo Scored;
+    APInt Score(64, 15);
+    Scored.addTrait(TraitProperty::implementation_vendor_llvm, "", &Score);
+    SmallVector<VariantMatchInfo, 2> Candidates{Scored, Unknown};
+    // Each selector contributes once: 1 + 10 + 1 + 5 = 17.
+    EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 1);
+    Score = APInt(64, 16);
+    Candidates[0].addTrait(TraitProperty::implementation_vendor_llvm, "",
+                           &Score);
+    EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 0);
+  }
+}
+
+TEST_F(OpenMPContextTest, ScoringMultipleProperties) {
+  OMPContext Context(false, Triple("x86_64-unknown-linux"), Triple(), -1);
+  VariantMatchInfo Device;
+  Device.addTrait(TraitProperty::device_kind_cpu, "");
+  Device.addTrait(TraitProperty::device_kind_host, "");
+  APInt Score(64, 1);
+  VariantMatchInfo Scored;
+  Scored.addTrait(TraitProperty::implementation_vendor_llvm, "", &Score);
+  SmallVector<VariantMatchInfo, 2> Candidates{Scored, Device};
+  // KIND contributes one weight regardless of its number of properties.
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 0);
+
+  Candidates[1].addTrait(TraitProperty::implementation_extension_match_any, "");
+  Candidates[1].addUnknownTrait(TraitSelector::device_kind, "unknown");
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 0);
+}
+
+TEST_F(OpenMPContextTest, UnknownPropertySubsets) {
+  OMPContext Context(false, Triple("x86_64-unknown-linux"), Triple(), -1);
+  VariantMatchInfo First, Second;
+  First.addTrait(TraitProperty::implementation_extension_match_none, "");
+  First.addUnknownTrait(TraitSelector::implementation_vendor, "first");
+  Second.addTrait(TraitProperty::implementation_extension_match_none, "");
+  Second.addUnknownTrait(TraitSelector::implementation_vendor, "second");
+  Second.addUnknownTrait(TraitSelector::implementation_extension, "extension");
+  SmallVector<VariantMatchInfo, 2> Candidates{First, Second};
+  // Distinct unknown properties do not form a subset just because their
+  // selectors are the same. The scores tie, so the first candidate wins.
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 0);
+  Candidates[1].UnknownTraits[0].Name = "first";
+  // Now the first candidate is a strict subset of the second.
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 1);
+
+  APInt HighScore(64, 100);
+  First = VariantMatchInfo();
+  First.addTrait(TraitProperty::implementation_extension_match_none, "");
+  First.addUnknownTrait(TraitSelector::implementation_vendor, "same",
+                        &HighScore);
+  Second = VariantMatchInfo();
+  Second.addTrait(TraitProperty::implementation_extension_match_none, "");
+  Second.addUnknownTrait(TraitSelector::device_kind, "same");
+  Second.addUnknownTrait(TraitSelector::device_arch, "extra");
+  Candidates = {First, Second};
+  // The same property name under different selectors does not form a subset.
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 0);
+}
+
+TEST_F(OpenMPContextTest, StrictSubsetScoreIsZero) {
+  OMPContext Context(false, Triple("x86_64-unknown-linux"), Triple(), -1);
+  APInt HighScore(64, 100), LowScore(64, 1);
+  VariantMatchInfo Subset, Superset;
+  Subset.addTrait(TraitProperty::implementation_vendor_llvm, "", &HighScore);
+  Superset.addTrait(TraitProperty::implementation_vendor_llvm, "", &LowScore);
+  Superset.addTrait(TraitProperty::user_condition_true, "");
+
+  // The strict subset's score is zero even though its raw score is higher.
+  SmallVector<VariantMatchInfo, 2> Candidates{Subset, Superset};
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 1);
+  std::swap(Candidates[0], Candidates[1]);
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 0);
+}
+
+TEST_F(OpenMPContextTest, IncompatibleSupersetDoesNotZeroScore) {
+  OMPContext Context(false, Triple("x86_64-unknown-linux"), Triple(), -1);
+  APInt HighScore(64, 100), LowScore(64, 5);
+  VariantMatchInfo Subset, IncompatibleSuperset, Other;
+  Subset.addTrait(TraitProperty::implementation_vendor_llvm, "", &HighScore);
+  IncompatibleSuperset.addTrait(TraitProperty::implementation_vendor_llvm, "");
+  IncompatibleSuperset.addTrait(TraitProperty::user_condition_false, "false");
+  Other.addTrait(TraitProperty::user_condition_true, "true", &LowScore);
+
+  // Only compatible selectors participate in strict-subset zeroing.
+  SmallVector<VariantMatchInfo, 3> Candidates{Subset, IncompatibleSuperset,
+                                              Other};
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 0);
+}
+
+TEST_F(OpenMPContextTest, KindAnyDoesNotAffectSubsets) {
+  OMPContext Context(false, Triple("x86_64-unknown-linux"), Triple(), -1);
+  APInt HighScore(64, 100), LowScore(64, 1);
+  VariantMatchInfo High, Low;
+  High.addTrait(TraitProperty::implementation_vendor_llvm, "", &HighScore);
+  Low.addTrait(TraitProperty::implementation_vendor_llvm, "", &LowScore);
+  Low.addTrait(TraitProperty::device_kind_any, "");
+
+  SmallVector<VariantMatchInfo, 2> Candidates{High, Low};
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 0);
+  std::swap(Candidates[0], Candidates[1]);
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 1);
+
+  // Ignoring any must also work when it appears in the potential subset.
+  High.addTrait(TraitProperty::device_kind_any, "");
+  Low.RequiredTraits.reset(unsigned(TraitProperty::device_kind_any));
+  Low.addTrait(TraitProperty::device_kind_host, "");
+  Candidates = {High, Low};
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 1);
+}
+
+TEST_F(OpenMPContextTest, DistinctUserConditionsAreNotSubsets) {
+  OMPContext Context(false, Triple("x86_64-unknown-linux"), Triple(), -1);
+  APInt HighScore(64, 100);
+  VariantMatchInfo High, MoreTraits;
+  High.addTrait(TraitProperty::user_condition_true, "high", &HighScore);
+  MoreTraits.addTrait(TraitProperty::user_condition_true, "low");
+  MoreTraits.addTrait(TraitProperty::device_kind_host, "");
+
+  // The condition expressions are different properties. HIGH is not a strict
+  // subset of MORE_TRAITS and retains its explicit score.
+  SmallVector<VariantMatchInfo, 2> Candidates{MoreTraits, High};
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 1);
+}
+
+TEST_F(OpenMPContextTest, RepeatedConstructStrictSubsetScoreIsZero) {
+  OMPContext Context(false, Triple("x86_64-unknown-linux"), Triple(), -1);
+  Context.addTrait(TraitProperty::construct_parallel_parallel);
+  Context.addTrait(TraitProperty::construct_parallel_parallel);
+  APInt HighScore(64, 100), LowScore(64, 1);
+  VariantMatchInfo Subset, Superset;
+  Subset.addTrait(TraitProperty::implementation_vendor_llvm, "", &HighScore);
+  Subset.addTrait(TraitProperty::construct_parallel_parallel, "");
+  Superset.addTrait(TraitProperty::implementation_vendor_llvm, "", &LowScore);
+  Superset.addTrait(TraitProperty::construct_parallel_parallel, "");
+  Superset.addTrait(TraitProperty::construct_parallel_parallel, "");
+
+  // Repeated constructs are retained only in the ordered vector, not the bit
+  // vector. They must still make the second selector a strict superset.
+  SmallVector<VariantMatchInfo, 2> Candidates{Subset, Superset};
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 1);
+}
+
+TEST_F(OpenMPContextTest, DifferentConstructOrderIsNotSubset) {
+  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_simd_simd);
+
+  APInt HighScore(64, 100), LowScore(64, 1);
+  VariantMatchInfo High, Low;
+  High.addTrait(TraitProperty::implementation_vendor_llvm, "", &HighScore);
+  High.addTrait(TraitProperty::construct_parallel_parallel, "");
+  High.addTrait(TraitProperty::construct_for_for, "");
+  Low.addTrait(TraitProperty::implementation_vendor_llvm, "", &LowScore);
+  Low.addTrait(TraitProperty::user_condition_true, "");
+  Low.addTrait(TraitProperty::construct_for_for, "");
+  Low.addTrait(TraitProperty::construct_parallel_parallel, "");
+  Low.addTrait(TraitProperty::construct_simd_simd, "");
+
+  // HIGH's construct sequence is not a subsequence of LOW's, despite each
+  // distinct construct property being present in LOW.
+  SmallVector<VariantMatchInfo, 2> Candidates{High, Low};
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 0);
+  std::swap(Candidates[0], Candidates[1]);
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 1);
+}
+
+TEST_F(OpenMPContextTest, DifferentISAPropertiesAreNotSubsets) {
+  struct ISAContext : OMPContext {
+    using OMPContext::OMPContext;
+    bool matchesISATrait(StringRef) const override { return true; }
+  };
+  ISAContext Context(false, Triple("x86_64-unknown-linux"), Triple(), -1);
+  APInt HighScore(64, 100), LowScore(64, 1);
+  VariantMatchInfo High, Low;
+  High.addTrait(TraitProperty::implementation_vendor_llvm, "", &HighScore);
+  High.addTrait(TraitProperty::device_isa___ANY, "sse2");
+  High.addTrait(TraitProperty::target_device_isa___ANY, "avx");
+  Low.addTrait(TraitProperty::implementation_vendor_llvm, "", &LowScore);
+  Low.addTrait(TraitProperty::device_isa___ANY, "avx");
+  Low.addTrait(TraitProperty::target_device_isa___ANY, "sse2");
+  Low.addTrait(TraitProperty::user_condition_true, "");
+
+  SmallVector<VariantMatchInfo, 2> Candidates{High, Low};
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 0);
+  std::swap(Candidates[0], Candidates[1]);
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 1);
+
+  // Once the ISA properties and their selectors are identical, HIGH is a
+  // strict subset of LOW.
+  Candidates[0].ISATraits[0].Name = "sse2";
+  Candidates[0].ISATraits[1].Name = "avx";
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 0);
+}
+
+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);
+}
+
+TEST_F(OpenMPContextTest, ScoringDeviceWeights) {
+  struct ISAContext : OMPContext {
+    using OMPContext::OMPContext;
+    bool matchesISATrait(StringRef) const override { return true; }
+  };
+  const TraitProperty Properties[] = {TraitProperty::device_kind_cpu,
+                                      TraitProperty::device_arch_arm,
+                                      TraitProperty::device_isa___ANY,
+                                      TraitProperty::target_device_kind_cpu,
+                                      TraitProperty::target_device_arch_arm,
+                                      TraitProperty::target_device_isa___ANY};
+  for (unsigned Depth : {0u, 1u, 2u, 63u, 64u, 65u}) {
+    ISAContext Context(false, Triple("arm-unknown-linux"), Triple(), -1);
+    for (unsigned I = 0; I < Depth; ++I)
+      Context.addTrait(TraitProperty::construct_parallel_parallel);
+    for (unsigned I = 0; I < 6; ++I) {
+      SCOPED_TRACE(Depth);
+      SCOPED_TRACE(I);
+      APInt Weight = APInt::getOneBitSet(128, Depth + I % 3);
+      APInt Score = Weight - 1;
+      VariantMatchInfo Scored, Device;
+      Scored.addTrait(TraitProperty::user_condition_true, "", &Score);
+      Device.addTrait(Properties[I], "test-isa");
+      SmallVector<VariantMatchInfo, 2> Candidates{Scored, Device};
+      // The device score is exactly 1 + 2^(context depth + trait offset).
+      EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 1);
+      Candidates[0].addTrait(TraitProperty::user_condition_true, "", &Weight);
+      EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 0);
+    }
+  }
+}
+
+TEST_F(OpenMPContextTest, ScoringUnknownConstructs) {
+  OMPContext Context(false, Triple("x86_64-unknown-linux"), Triple(), -1);
+  Context.addTrait(TraitProperty::construct_parallel_parallel);
+  Context.addUnknownConstruct();
+
+  APInt Score(64, 3);
+  VariantMatchInfo Scored, Device;
+  Scored.addTrait(TraitProperty::implementation_vendor_llvm, "", &Score);
+  Device.addTrait(TraitProperty::device_kind_cpu, "");
+  SmallVector<VariantMatchInfo, 2> Candidates{Scored, Device};
+  // Both enclosing constructs contribute to the device weight: 1 + 2^2.
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 1);
+
+  OMPContext UnknownThenParallel(false, Triple("x86_64-unknown-linux"),
+                                 Triple(), -1);
+  UnknownThenParallel.addUnknownConstruct();
+  UnknownThenParallel.addTrait(TraitProperty::construct_parallel_parallel);
+  APInt LowerScore(64, 1);
+  VariantMatchInfo LowerScored, Parallel;
+  LowerScored.addTrait(TraitProperty::implementation_vendor_llvm, "",
+                       &LowerScore);
+  Parallel.addTrait(TraitProperty::construct_parallel_parallel, "");
+  // The unknown construct occupies position one, so PARALLEL scores 1 + 2.
+  EXPECT_TRUE(isVariantApplicableInContext(Parallel, UnknownThenParallel));
+  Candidates = {LowerScored, Parallel};
+  EXPECT_EQ(getBestVariantMatchForContext(Candidates, UnknownThenParallel), 1);
+}
+
+TEST_F(OpenMPContextTest, ScoringWideTotals) {
+  OMPContext Context(false, Triple("x86_64-unknown-linux"), Triple(), -1);
+  Context.addTrait(TraitProperty::construct_parallel_parallel);
+  for (unsigned Bits : {63u, 64u, 127u}) {
+    SCOPED_TRACE(Bits);
+    APInt Score = APInt::getLowBitsSet(Bits + 1, Bits);
+    VariantMatchInfo Large, Small;
+    Large.addTrait(TraitProperty::user_condition_true, "", &Score);
+    Large.addTrait(TraitProperty::implementation_vendor_llvm, "", &Score);
+    Large.addTrait(TraitProperty::construct_parallel_parallel, "");
+    APInt SmallScore(32, 20);
+    Small.addTrait(TraitProperty::user_condition_true, "", &SmallScore);
+    SmallVector<VariantMatchInfo, 2> Candidates{Large, Small};
+    // The sum is 2^(Bits + 1), not zero after fixed-width overflow.
+    EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 0);
+    std::swap(Candidates[0], Candidates[1]);
+    EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 1);
+    // Overflow to a nonzero value must not silently reverse the ranking.
+    Candidates[1].addTrait(TraitProperty::device_kind_cpu, "");
+    EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 1);
+  }
+}
+
+TEST_F(OpenMPContextTest, ScoringDeepConstructs) {
+  for (unsigned Depth : {63u, 64u, 65u}) {
+    SCOPED_TRACE(Depth);
+    OMPContext Context(false, Triple("x86_64-unknown-linux"), Triple(), -1);
+    for (unsigned I = 0; I < Depth; ++I)
+      Context.addTrait(TraitProperty::construct_parallel_parallel);
+    APInt Weight = APInt::getOneBitSet(128, Depth - 1);
+    APInt Score = Weight - 1;
+    VariantMatchInfo Scored, Parallel;
+    Scored.addTrait(TraitProperty::user_condition_true, "", &Score);
+    Parallel.addTrait(TraitProperty::construct_parallel_parallel, "");
+    SmallVector<VariantMatchInfo, 2> Candidates{Scored, Parallel};
+    EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 1);
+    Candidates[0].addTrait(TraitProperty::user_condition_true, "", &Weight);
+    EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 0);
+  }
 }
 
 } // namespace

>From d13311ef49cd51803fbfb3af8d51195dc633b8ec Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Thu, 24 Sep 2026 00:05:55 -0500
Subject: [PATCH 02/23] Fix variant matching in expressions

- Fix Flang crashes when selecting variants in specification expressions.
- Make variant selection in combined-loop bounds agree with explicit nesting.
- Fix Clang condition matching across macro redefinitions and parentheses.
- Strengthen tests for selected TARGET contexts and add bug regressions.
---
 clang/lib/Parse/ParseOpenMP.cpp               |  13 +-
 .../declare_variant_condition_identity_pch.c  |  20 ++-
 clang/test/OpenMP/declare_variant_scoring.c   |  44 +++++++
 flang/include/flang/Lower/AbstractConverter.h |   4 +-
 flang/lib/Lower/Bridge.cpp                    |   6 +-
 flang/lib/Lower/OpenMP/OpenMP.cpp             |  17 ++-
 flang/lib/Lower/OpenMP/Utils.cpp              |  37 ++++--
 flang/lib/Lower/OpenMP/Utils.h                |  12 +-
 .../OpenMP/declare-variant-loop-bounds.f90    | 120 ++++++++++++++++++
 .../OpenMP/declare-variant-spec-expr.f90      |  56 ++++++++
 .../OpenMP/metadirective-target-boundary.f90  |  34 +++--
 11 files changed, 323 insertions(+), 40 deletions(-)
 create mode 100644 flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
 create mode 100644 flang/test/Lower/OpenMP/declare-variant-spec-expr.f90

diff --git a/clang/lib/Parse/ParseOpenMP.cpp b/clang/lib/Parse/ParseOpenMP.cpp
index 4fba0b43f65bd..6684c6b69f8b3 100644
--- a/clang/lib/Parse/ParseOpenMP.cpp
+++ b/clang/lib/Parse/ParseOpenMP.cpp
@@ -28,6 +28,7 @@
 #include "llvm/Frontend/OpenMP/DirectiveNameParser.h"
 #include "llvm/Frontend/OpenMP/OMPAssume.h"
 #include "llvm/Frontend/OpenMP/OMPContext.h"
+#include "llvm/Support/raw_ostream.h"
 #include <climits>
 #include <optional>
 
@@ -1150,12 +1151,16 @@ void Parser::parseOMPContextSelector(
     if (!Condition.isUsable())
       return FinishSelector();
     TISelector.ScoreOrCondition = Condition.get();
-    StringRef ConditionText = Lexer::getSourceText(
-        CharSourceRange::getTokenRange(Condition.get()->getSourceRange()),
-        PP.getSourceManager(), PP.getLangOpts());
+    // Compare expanded expressions, since a macro can be redefined between
+    // selectors. Ignore outer parentheses when recording condition identity.
+    ASTContext &Context = Actions.getASTContext();
+    std::string ConditionText;
+    llvm::raw_string_ostream OS(ConditionText);
+    Condition.get()->IgnoreParenImpCasts()->printPretty(
+        OS, nullptr, Context.getPrintingPolicy());
     TISelector.Properties.push_back(
         {TraitProperty::user_condition_unknown,
-         ConditionText.empty() ? "<condition>" : ConditionText});
+         StringRef(ConditionText).copy(Context.getAllocator())});
     return;
   }
 
diff --git a/clang/test/OpenMP/declare_variant_condition_identity_pch.c b/clang/test/OpenMP/declare_variant_condition_identity_pch.c
index 3b0acd9f8f1f6..166612af3a933 100644
--- a/clang/test/OpenMP/declare_variant_condition_identity_pch.c
+++ b/clang/test/OpenMP/declare_variant_condition_identity_pch.c
@@ -12,7 +12,7 @@
 // RUN:   -triple x86_64-unknown-linux -x c++ %t/use.c \
 // RUN:   -include-pch %t/conditions-cxx.pch -emit-llvm -o - | FileCheck %s
 
-// Verify that serialization preserves the source identity of folded user
+// Verify that serialization preserves the expanded identity of folded user
 // conditions. Distinct conditions do not form a subset relationship, while
 // identical conditions do.
 
@@ -29,19 +29,29 @@ extern "C" {
 void condition_high_variant(void);
 void condition_low_variant(void);
 
+#define COND 1
 #pragma omp declare variant(condition_high_variant)                       \
-    match(implementation = {vendor(score(100) : llvm)}, user = {condition(1)})
+    match(implementation = {vendor(score(100) : llvm)},                    \
+          user = {condition(COND)})
+#undef COND
+#define COND 2
 #pragma omp declare variant(condition_low_variant)                         \
     match(implementation = {vendor(score(1) : llvm)}, device = {kind(cpu)}, \
-          user = {condition(2)})
+          user = {condition(COND)})
 void distinct_condition_base(void);
+#undef COND
 
+#define FIRST_COND 1
+#define SECOND_COND (1)
 #pragma omp declare variant(condition_high_variant)                       \
-    match(implementation = {vendor(score(100) : llvm)}, user = {condition(1)})
+    match(implementation = {vendor(score(100) : llvm)},                    \
+          user = {condition(FIRST_COND)})
 #pragma omp declare variant(condition_low_variant)                         \
     match(implementation = {vendor(score(1) : llvm)}, device = {kind(cpu)}, \
-          user = {condition(1)})
+          user = {condition(SECOND_COND)})
 void identical_condition_base(void);
+#undef FIRST_COND
+#undef SECOND_COND
 
 #ifdef __cplusplus
 }
diff --git a/clang/test/OpenMP/declare_variant_scoring.c b/clang/test/OpenMP/declare_variant_scoring.c
index c924dbaf34624..ea99df0fee2c8 100644
--- a/clang/test/OpenMP/declare_variant_scoring.c
+++ b/clang/test/OpenMP/declare_variant_scoring.c
@@ -4,6 +4,10 @@
 // RUN: %clang_cc1 -x c++ -verify -fopenmp -fopenmp-version=52 \
 // RUN:   -triple x86_64-unknown-linux -target-feature +avx \
 // RUN:   -emit-llvm %s -o - | FileCheck %s
+// RUN: %clang_cc1 -E -fopenmp -fopenmp-version=52 %s -o %t.i
+// RUN: %clang_cc1 -fopenmp -fopenmp-version=52 \
+// RUN:   -triple x86_64-unknown-linux -target-feature +avx \
+// RUN:   -emit-llvm %t.i -o - | FileCheck %s
 // expected-no-diagnostics
 
 #ifdef __cplusplus
@@ -61,6 +65,32 @@ void distinct_condition_base(void);
           user = {condition(1)})
 void identical_condition_base(void);
 
+// Macro spelling must not hide different expanded conditions.
+#define COND 1
+#pragma omp declare variant(condition_high_variant)                       \
+    match(implementation = {vendor(score(100) : llvm)},                    \
+          user = {condition(COND)})
+#undef COND
+#define COND 2
+#pragma omp declare variant(condition_low_variant)                        \
+    match(implementation = {vendor(score(1) : llvm)}, device = {kind(cpu)}, \
+          user = {condition(COND)})
+void redefined_condition_base(void);
+#undef COND
+
+// Different macro names and outer parentheses preserve condition identity.
+#define FIRST_COND 1
+#define SECOND_COND (1)
+#pragma omp declare variant(condition_high_variant)                       \
+    match(implementation = {vendor(score(100) : llvm)},                    \
+          user = {condition(FIRST_COND)})
+#pragma omp declare variant(condition_low_variant)                        \
+    match(implementation = {vendor(score(1) : llvm)}, device = {kind(cpu)}, \
+          user = {condition(SECOND_COND)})
+void equivalent_condition_base(void);
+#undef FIRST_COND
+#undef SECOND_COND
+
 void subset_variant(void);
 void superset_variant(void);
 #pragma omp declare variant(subset_variant) \
@@ -230,6 +260,20 @@ void identical_conditions(void) { identical_condition_base(); }
 // CHECK: call void @condition_low_variant()
 // CHECK: ret void
 
+void redefined_conditions(void) { redefined_condition_base(); }
+// CHECK-LABEL: define{{.*}} void @redefined_conditions
+// CHECK-NOT: call void @condition_low_variant()
+// CHECK: call void @condition_high_variant()
+// CHECK-NOT: call void @condition_low_variant()
+// CHECK: ret void
+
+void equivalent_conditions(void) { equivalent_condition_base(); }
+// CHECK-LABEL: define{{.*}} void @equivalent_conditions
+// CHECK-NOT: call void @condition_high_variant()
+// CHECK: call void @condition_low_variant()
+// CHECK-NOT: call void @condition_high_variant()
+// CHECK: ret void
+
 // A strict subset has score zero before candidates are ranked, even when its
 // explicit score would otherwise be higher.
 void strict_subset(void) { subset_base(); }
diff --git a/flang/include/flang/Lower/AbstractConverter.h b/flang/include/flang/Lower/AbstractConverter.h
index fd63f52f6aaf5..25337bdb9e35d 100644
--- a/flang/include/flang/Lower/AbstractConverter.h
+++ b/flang/include/flang/Lower/AbstractConverter.h
@@ -337,8 +337,8 @@ class AbstractConverter {
   /// Get the converter's current scope
   virtual const Fortran::semantics::Scope &getCurrentScope() = 0;
 
-  /// Get the PFT evaluation currently being lowered.
-  virtual pft::Evaluation &getCurrentEvaluation() = 0;
+  /// Get the current PFT evaluation, or null when lowering a function prologue.
+  virtual pft::Evaluation *getCurrentEvaluation() = 0;
 
   //===--------------------------------------------------------------------===//
   // FIR/MLIR
diff --git a/flang/lib/Lower/Bridge.cpp b/flang/lib/Lower/Bridge.cpp
index 036c6fd110504..618a69c76ee23 100644
--- a/flang/lib/Lower/Bridge.cpp
+++ b/flang/lib/Lower/Bridge.cpp
@@ -1277,8 +1277,8 @@ class FirConverter : public Fortran::lower::AbstractConverter {
     return bridge.getSemanticsContext().FindScope(currentPosition);
   }
 
-  Fortran::lower::pft::Evaluation &getCurrentEvaluation() override final {
-    return getEval();
+  Fortran::lower::pft::Evaluation *getCurrentEvaluation() override final {
+    return evalPtr;
   }
 
   fir::FirOpBuilder &getFirOpBuilder() override final {
@@ -6487,6 +6487,8 @@ class FirConverter : public Fortran::lower::AbstractConverter {
   /// Start translation of a function.
   void startNewFunction(Fortran::lower::pft::FunctionLikeUnit &funit) {
     assert(!builder && "expected nullptr");
+    // Specification expressions have no current executable evaluation.
+    evalPtr = nullptr;
     bridge.fctCtx().pushScope();
     bridge.cudaCleanupCtx().pushScope();
     bridge.openAccCtx().pushScope();
diff --git a/flang/lib/Lower/OpenMP/OpenMP.cpp b/flang/lib/Lower/OpenMP/OpenMP.cpp
index c58bba6385f06..c910496a57e37 100644
--- a/flang/lib/Lower/OpenMP/OpenMP.cpp
+++ b/flang/lib/Lower/OpenMP/OpenMP.cpp
@@ -1896,6 +1896,9 @@ static void createBodyOfOp(mlir::Operation &op, const OpWithBodyGenInfo &info,
     return {};
   }();
 
+  mlir::SaveStateStack<OpenMPContextFrame> context{
+      info.converter.getStateStack(), info.eval, info.dir, /*isPartial=*/true};
+
   // Mark the earliest insertion point.
   mlir::Operation *marker = insertMarker(firOpBuilder);
 
@@ -2046,6 +2049,9 @@ static void genBodyOfTargetDataOp(
   fir::FirOpBuilder &firOpBuilder = converter.getFirOpBuilder();
 
   genEntryBlock(firOpBuilder, args.asEntryBlockArgs(), dataOp.getRegion());
+  mlir::SaveStateStack<OpenMPContextFrame> context{
+      converter.getStateStack(), eval, llvm::omp::Directive::OMPD_target_data,
+      /*isPartial=*/true};
   bindEntryBlockArgs(converter, dataOp, args);
   auto argIface = llvm::cast<mlir::omp::BlockArgOpenMPOpInterface>(*dataOp);
   llvm::SmallVector<const semantics::Symbol *> sourceUseDeviceAddrSyms{
@@ -2132,6 +2138,9 @@ static void genBodyOfTargetOp(
 
   mlir::Region &region = targetOp.getRegion();
   genEntryBlock(firOpBuilder, args.asEntryBlockArgs(), region);
+  mlir::SaveStateStack<OpenMPContextFrame> context{
+      converter.getStateStack(), eval, llvm::omp::Directive::OMPD_target,
+      /*isPartial=*/true};
   bindEntryBlockArgs(converter, targetOp, args);
   if (HostEvalInfo *hostEvalInfo = getHostEvalInfoStackTop(converter))
     hostEvalInfo->bindOperands(argIface.getHostEvalBlockArgs());
@@ -5261,6 +5270,9 @@ static mlir::omp::DistributeOp genCompositeDistributeParallelDo(
   parallelArgs.reduction.vars = parallelClauseOps.reductionVars;
   genParallelOp(converter, symTable, semaCtx, eval, loc, queue, parallelItem,
                 parallelClauseOps, parallelArgs, &dsp, /*isComposite=*/true);
+  mlir::SaveStateStack<OpenMPContextFrame> context{
+      converter.getStateStack(), eval, llvm::omp::Directive::OMPD_parallel,
+      /*isPartial=*/true};
 
   // Clause processing.
   mlir::omp::DistributeOperands distributeClauseOps;
@@ -5331,6 +5343,9 @@ static mlir::omp::DistributeOp genCompositeDistributeParallelDoSimd(
   genParallelOp(converter, symTable, semaCtx, eval, loc, queue, parallelItem,
                 parallelClauseOps, parallelArgs, &parallelItemDSP,
                 /*isComposite=*/true);
+  mlir::SaveStateStack<OpenMPContextFrame> context{
+      converter.getStateStack(), eval, llvm::omp::Directive::OMPD_parallel,
+      /*isPartial=*/true};
 
   // Clause processing.
   // Use a shared cache so that both wsloop and simd produce the same SSA
@@ -7427,7 +7442,7 @@ static void genMetadirective(lower::AbstractConverter &converter,
   fir::FirOpBuilder &builder = converter.getFirOpBuilder();
 
   llvm::SmallVector<llvm::omp::TraitProperty, 8> constructTraits;
-  collectEnclosingConstructTraits(converter, eval, constructTraits);
+  collectEnclosingConstructTraits(converter, &eval, constructTraits);
   semantics::omp::OmpVariantMatchContext ompCtx =
       makeVariantMatchContext(builder.getModule(), constructTraits);
 
diff --git a/flang/lib/Lower/OpenMP/Utils.cpp b/flang/lib/Lower/OpenMP/Utils.cpp
index 431697b3e10cc..db424331b4042 100644
--- a/flang/lib/Lower/OpenMP/Utils.cpp
+++ b/flang/lib/Lower/OpenMP/Utils.cpp
@@ -1471,7 +1471,7 @@ semantics::omp::OmpVariantMatchContext makeVariantMatchContext(
 }
 
 void collectEnclosingConstructTraits(
-    AbstractConverter &converter, const pft::Evaluation &evaluation,
+    AbstractConverter &converter, const pft::Evaluation *evaluation,
     llvm::SmallVectorImpl<llvm::omp::TraitProperty> &constructTraits) {
   llvm::SmallVector<const OpenMPContextFrame *, 4> frames;
   converter.getStateStack().stackWalk<OpenMPContextFrame>(
@@ -1483,8 +1483,9 @@ void collectEnclosingConstructTraits(
   llvm::SmallVector<bool, 4> usedFrames(frames.size(), false);
 
   llvm::SmallVector<const pft::Evaluation *, 8> ancestors;
-  for (const pft::Evaluation *parent = evaluation.parentConstruct; parent;
-       parent = parent->parentConstruct) {
+  for (const pft::Evaluation *parent = evaluation ? evaluation->parentConstruct
+                                                  : nullptr;
+       parent; parent = parent->parentConstruct) {
     ancestors.push_back(parent);
   }
   std::reverse(ancestors.begin(), ancestors.end());
@@ -1497,25 +1498,37 @@ void collectEnclosingConstructTraits(
     if (!omp)
       continue;
     llvm::omp::Directive directive{parser::omp::GetOmpDirectiveName(*omp).v};
+    // An ancestor supplies the full source context, including constituents
+    // whose bodies also have active frames. Count each construct only once.
+    for (auto [index, frame] : llvm::enumerate(frames))
+      if (&frame->evaluation == ancestor)
+        usedFrames[index] = true;
     if (directive != llvm::omp::Directive::OMPD_metadirective) {
       append(directive);
       continue;
     }
-    for (auto [index, frame] : llvm::enumerate(frames)) {
-      if (&frame->evaluation == ancestor) {
+    for (const OpenMPContextFrame *frame : frames) {
+      if (&frame->evaluation == ancestor && !frame->isPartial) {
         append(frame->directive);
-        usedFrames[index] = true;
         break;
       }
     }
   }
 
-  // Active source contexts may not appear in the ancestor chain. This occurs
-  // for a loop-associated metadirective that owns a following sibling and for
-  // an atomic expression lowered from the atomic evaluation itself.
-  for (auto [index, frame] : llvm::enumerate(frames))
-    if (!usedFrames[index])
-      append(frame->directive);
+  // Include entered constituents while their own evaluation is current, e.g.
+  // PARALLEL when lowering the bounds of PARALLEL DO. Complete replacement
+  // frames already describe all constituents of a selected metadirective.
+  for (auto [index, frame] : llvm::enumerate(frames)) {
+    if (usedFrames[index])
+      continue;
+    if (frame->isPartial &&
+        llvm::any_of(
+            frames, [frameEval = &frame->evaluation](const auto *other) {
+              return !other->isPartial && &other->evaluation == frameEval;
+            }))
+      continue;
+    append(frame->directive);
+  }
 }
 
 const semantics::Symbol *
diff --git a/flang/lib/Lower/OpenMP/Utils.h b/flang/lib/Lower/OpenMP/Utils.h
index 4e9095fc11a28..e8d665e8600e4 100644
--- a/flang/lib/Lower/OpenMP/Utils.h
+++ b/flang/lib/Lower/OpenMP/Utils.h
@@ -58,11 +58,14 @@ class OpenMPContextFrame
   MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(OpenMPContextFrame)
 
   OpenMPContextFrame(const pft::Evaluation &evaluation,
-                     llvm::omp::Directive directive)
-      : evaluation{evaluation}, directive{directive} {}
+                     llvm::omp::Directive directive, bool isPartial = false)
+      : evaluation{evaluation}, directive{directive}, isPartial{isPartial} {}
 
   const pft::Evaluation &evaluation;
   llvm::omp::Directive directive;
+  // A partial frame records an entered constituent of a combined directive.
+  // A complete frame supplies the source context when it is absent from PFT.
+  bool isPartial;
 };
 
 struct DeclareTargetCaptureInfo {
@@ -274,9 +277,10 @@ std::optional<llvm::SmallVector<mlir::Value>> getIteratorElementIndices(
 
 /// Collect the source OpenMP constructs enclosing \p evaluation in
 /// outermost-to-innermost order. Active metadirective replacements substitute
-/// for their METADIRECTIVE source constructs.
+/// for their METADIRECTIVE source constructs. A null evaluation uses only
+/// active context frames.
 void collectEnclosingConstructTraits(
-    AbstractConverter &converter, const pft::Evaluation &evaluation,
+    AbstractConverter &converter, const pft::Evaluation *evaluation,
     llvm::SmallVectorImpl<llvm::omp::TraitProperty> &constructTraits);
 
 /// Return true when \p module is being compiled for an AMDGPU device or all of
diff --git a/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90 b/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
new file mode 100644
index 0000000000000..30cc4e7d55f4b
--- /dev/null
+++ b/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
@@ -0,0 +1,120 @@
+! RUN: %flang_fc1 -fopenmp -fopenmp-version=52 -emit-hlfir %s -o - | \
+! RUN:   FileCheck %s
+
+module loop_context
+contains
+  pure integer function parallel_bound(n)
+    integer, intent(in) :: n
+    parallel_bound = n
+  end function
+
+  pure integer function do_bound(n)
+    integer, intent(in) :: n
+    do_bound = n
+  end function
+
+  pure integer function bound(n)
+    integer, intent(in) :: n
+    !$omp declare variant(parallel_bound) match(construct={parallel})
+    !$omp declare variant(do_bound) match(construct={do})
+    bound = n
+  end function
+
+  ! NUM_THREADS is evaluated outside PARALLEL. The DO bound is inside
+  ! PARALLEL but outside DO, and the loop body is inside both constructs.
+  ! CHECK-LABEL: func.func @_QMloop_contextPcombined(
+  ! CHECK: fir.call @_QMloop_contextPbound(
+  ! CHECK: omp.parallel
+  ! CHECK-NOT: fir.call @_QMloop_contextPbound(
+  ! CHECK-NOT: fir.call @_QMloop_contextPdo_bound(
+  ! CHECK: fir.call @_QMloop_contextPparallel_bound(
+  ! CHECK: omp.wsloop
+  ! CHECK: fir.call @_QMloop_contextPdo_bound(
+  ! CHECK: return
+  subroutine combined(n, a)
+    integer, intent(in) :: n
+    integer :: a(n), i
+    !$omp parallel do num_threads(bound(n))
+    do i = 1, bound(n)
+      a(i) = bound(n)
+    end do
+  end subroutine
+
+  ! Explicit nesting must select the same variants as the combined spelling.
+  ! CHECK-LABEL: func.func @_QMloop_contextPexplicit_nest(
+  ! CHECK: fir.call @_QMloop_contextPbound(
+  ! CHECK: omp.parallel
+  ! CHECK-NOT: fir.call @_QMloop_contextPbound(
+  ! CHECK-NOT: fir.call @_QMloop_contextPdo_bound(
+  ! CHECK: fir.call @_QMloop_contextPparallel_bound(
+  ! CHECK: omp.wsloop
+  ! CHECK: fir.call @_QMloop_contextPdo_bound(
+  ! CHECK: return
+  subroutine explicit_nest(n, a)
+    integer, intent(in) :: n
+    integer :: a(n), i
+    !$omp parallel num_threads(bound(n))
+      !$omp do
+      do i = 1, bound(n)
+        a(i) = bound(n)
+      end do
+    !$omp end parallel
+  end subroutine
+
+  ! Composite lowering creates PARALLEL separately from the loop wrappers.
+  ! CHECK-LABEL: func.func @_QMloop_contextPcomposite(
+  ! CHECK: omp.teams
+  ! CHECK: omp.parallel
+  ! CHECK-NOT: fir.call @_QMloop_contextPbound(
+  ! CHECK-NOT: fir.call @_QMloop_contextPdo_bound(
+  ! CHECK: fir.call @_QMloop_contextPparallel_bound(
+  ! CHECK: omp.distribute
+  ! CHECK: omp.wsloop
+  ! CHECK: fir.call @_QMloop_contextPdo_bound(
+  ! CHECK: return
+  subroutine composite(n, a)
+    integer, intent(in) :: n
+    integer :: a(n), i
+    !$omp teams distribute parallel do
+    do i = 1, bound(n)
+      a(i) = bound(n)
+    end do
+  end subroutine
+
+  ! The SIMD composite follows a separate lowering path.
+  ! CHECK-LABEL: func.func @_QMloop_contextPcomposite_simd(
+  ! CHECK: omp.teams
+  ! CHECK: omp.parallel
+  ! CHECK-NOT: fir.call @_QMloop_contextPbound(
+  ! CHECK-NOT: fir.call @_QMloop_contextPdo_bound(
+  ! CHECK: fir.call @_QMloop_contextPparallel_bound(
+  ! CHECK: omp.distribute
+  ! CHECK: omp.wsloop
+  ! CHECK: omp.simd
+  ! CHECK: fir.call @_QMloop_contextPdo_bound(
+  ! CHECK: return
+  subroutine composite_simd(n, a)
+    integer, intent(in) :: n
+    integer :: a(n), i
+    !$omp teams distribute parallel do simd
+    do i = 1, bound(n)
+      a(i) = bound(n)
+    end do
+  end subroutine
+
+  ! A standalone DO does not contribute its own context to its bounds.
+  ! CHECK-LABEL: func.func @_QMloop_contextPstandalone_do(
+  ! CHECK-NOT: fir.call @_QMloop_contextPdo_bound(
+  ! CHECK: fir.call @_QMloop_contextPbound(
+  ! CHECK: omp.wsloop
+  ! CHECK: fir.call @_QMloop_contextPdo_bound(
+  ! CHECK: return
+  subroutine standalone_do(n, a)
+    integer, intent(in) :: n
+    integer :: a(n), i
+    !$omp do
+    do i = 1, bound(n)
+      a(i) = bound(n)
+    end do
+  end subroutine
+end module
diff --git a/flang/test/Lower/OpenMP/declare-variant-spec-expr.f90 b/flang/test/Lower/OpenMP/declare-variant-spec-expr.f90
new file mode 100644
index 0000000000000..704314e68696c
--- /dev/null
+++ b/flang/test/Lower/OpenMP/declare-variant-spec-expr.f90
@@ -0,0 +1,56 @@
+! RUN: %flang_fc1 -fopenmp -fopenmp-version=52 -emit-hlfir %s -o - | \
+! RUN:   FileCheck %s --implicit-check-not='fir.call @_QMspec_contextPparallel'
+
+module spec_context
+contains
+  ! Specification expressions are lowered before any current evaluation.
+  ! The caller comes first to avoid reusing a previous procedure's evaluation.
+  ! CHECK-LABEL: func.func @_QMspec_contextPfirst(
+  ! CHECK-NOT: fir.call @_QMspec_contextPbound(
+  ! CHECK: fir.call @_QMspec_contextPreplacement(
+  ! CHECK-NOT: fir.call @_QMspec_contextPreplacement(
+  ! CHECK: fir.call @_QMspec_contextPunmatched(
+  ! CHECK-NOT: fir.call @_QMspec_contextPbound(
+  ! CHECK: return
+  subroutine first(n)
+    integer, intent(in) :: n
+    integer :: a(bound(n)), b(unmatched(n))
+    print *, size(a), size(b)
+  end subroutine
+
+  pure integer function bound(n)
+    integer, intent(in) :: n
+    !$omp declare variant(replacement) match(implementation={vendor(llvm)})
+    bound = n
+  end function
+
+  pure integer function unmatched(n)
+    integer, intent(in) :: n
+    !$omp declare variant(parallel_replacement) match(construct={parallel})
+    unmatched = n
+  end function
+
+  pure integer function replacement(n)
+    integer, intent(in) :: n
+    replacement = n
+  end function
+
+  pure integer function parallel_replacement(n)
+    integer, intent(in) :: n
+    parallel_replacement = n
+  end function
+
+  ! A later procedure's prologue also starts with an empty construct context.
+  ! CHECK-LABEL: func.func @_QMspec_contextPlast(
+  ! CHECK-NOT: fir.call @_QMspec_contextPbound(
+  ! CHECK: fir.call @_QMspec_contextPreplacement(
+  ! CHECK-NOT: fir.call @_QMspec_contextPreplacement(
+  ! CHECK: fir.call @_QMspec_contextPunmatched(
+  ! CHECK-NOT: fir.call @_QMspec_contextPbound(
+  ! CHECK: return
+  subroutine last(n)
+    integer, intent(in) :: n
+    integer :: a(bound(n)), b(unmatched(n))
+    print *, size(a), size(b)
+  end subroutine
+end module
diff --git a/flang/test/Lower/OpenMP/metadirective-target-boundary.f90 b/flang/test/Lower/OpenMP/metadirective-target-boundary.f90
index d7963bf7fe4ec..00adbc0ffdba5 100644
--- a/flang/test/Lower/OpenMP/metadirective-target-boundary.f90
+++ b/flang/test/Lower/OpenMP/metadirective-target-boundary.f90
@@ -21,22 +21,36 @@ subroutine actual_target(n, a)
   !$omp end parallel
 end subroutine
 
-! A TARGET selected by a metadirective creates the same context boundary.
-! CHECK-LABEL: func.func @_QPselected_target(
+! A selected TARGET contributes its own construct trait.
+! CHECK-LABEL: func.func @_QPselected_target_present()
 ! CHECK: omp.parallel
 ! CHECK: omp.target
-! CHECK-NOT: omp.simd
+! CHECK-NOT: omp.taskyield
+! CHECK: omp.barrier
+! CHECK-NOT: omp.taskyield
 ! CHECK: return
-subroutine selected_target(n, a)
-  integer :: n, i, a(n)
+subroutine selected_target_present()
   !$omp parallel
     !$omp begin metadirective default(target)
       !$omp metadirective &
-      !$omp& when(construct={parallel, target}: simd) &
-      !$omp& default(nothing)
-      do i = 1, n
-        a(i) = i
-      end do
+      !$omp& when(construct={target}: barrier) default(taskyield)
+    !$omp end metadirective
+  !$omp end parallel
+end subroutine
+
+! The same selected TARGET hides the enclosing PARALLEL trait.
+! CHECK-LABEL: func.func @_QPselected_target_hides_parallel()
+! CHECK: omp.parallel
+! CHECK: omp.target
+! CHECK-NOT: omp.barrier
+! CHECK: omp.taskyield
+! CHECK-NOT: omp.barrier
+! CHECK: return
+subroutine selected_target_hides_parallel()
+  !$omp parallel
+    !$omp begin metadirective default(target)
+      !$omp metadirective &
+      !$omp& when(construct={parallel}: barrier) default(taskyield)
     !$omp end metadirective
   !$omp end parallel
 end subroutine

>From b4ebeaacbcfa48fb4db9649024800c23714e649c Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Thu, 24 Sep 2026 13:23:15 -0500
Subject: [PATCH 03/23] Fix Flang construct contexts during variant lowering

- Count each enclosing loop construct once, including collapsed nests.
- Match selected directive clauses and bounds in the same contexts as
  directly written directives.
- Evaluate composite clause expressions in source construct order.
- Add regressions for scoring and static/runtime variant selection.
---
 flang/lib/Lower/OpenMP/OpenMP.cpp             |  48 +++---
 flang/lib/Lower/OpenMP/Utils.cpp              |  15 +-
 flang/lib/Lower/OpenMP/Utils.h                |  11 +-
 .../OpenMP/declare-variant-loop-bounds.f90    |  60 +++++++
 .../declare-variant-metadirective-context.f90 | 149 ++++++++++++++++++
 .../OpenMP/variant-loop-context-depth.f90     | 118 ++++++++++++++
 6 files changed, 367 insertions(+), 34 deletions(-)
 create mode 100644 flang/test/Lower/OpenMP/declare-variant-metadirective-context.f90
 create mode 100644 flang/test/Lower/OpenMP/variant-loop-context-depth.f90

diff --git a/flang/lib/Lower/OpenMP/OpenMP.cpp b/flang/lib/Lower/OpenMP/OpenMP.cpp
index c910496a57e37..383f7a8335a10 100644
--- a/flang/lib/Lower/OpenMP/OpenMP.cpp
+++ b/flang/lib/Lower/OpenMP/OpenMP.cpp
@@ -1896,8 +1896,12 @@ static void createBodyOfOp(mlir::Operation &op, const OpWithBodyGenInfo &info,
     return {};
   }();
 
+  // A loop body uses the collapsed DO evaluation, but its context belongs to
+  // the owning directive. This also covers intervening code between loops.
+  auto &contextEval =
+      info.outerCollapseEval ? *info.outerCollapseEval : info.eval;
   mlir::SaveStateStack<OpenMPContextFrame> context{
-      info.converter.getStateStack(), info.eval, info.dir, /*isPartial=*/true};
+      info.converter.getStateStack(), contextEval, info.dir};
 
   // Mark the earliest insertion point.
   mlir::Operation *marker = insertMarker(firOpBuilder);
@@ -2050,8 +2054,7 @@ static void genBodyOfTargetDataOp(
 
   genEntryBlock(firOpBuilder, args.asEntryBlockArgs(), dataOp.getRegion());
   mlir::SaveStateStack<OpenMPContextFrame> context{
-      converter.getStateStack(), eval, llvm::omp::Directive::OMPD_target_data,
-      /*isPartial=*/true};
+      converter.getStateStack(), eval, llvm::omp::Directive::OMPD_target_data};
   bindEntryBlockArgs(converter, dataOp, args);
   auto argIface = llvm::cast<mlir::omp::BlockArgOpenMPOpInterface>(*dataOp);
   llvm::SmallVector<const semantics::Symbol *> sourceUseDeviceAddrSyms{
@@ -2139,8 +2142,7 @@ static void genBodyOfTargetOp(
   mlir::Region &region = targetOp.getRegion();
   genEntryBlock(firOpBuilder, args.asEntryBlockArgs(), region);
   mlir::SaveStateStack<OpenMPContextFrame> context{
-      converter.getStateStack(), eval, llvm::omp::Directive::OMPD_target,
-      /*isPartial=*/true};
+      converter.getStateStack(), eval, llvm::omp::Directive::OMPD_target};
   bindEntryBlockArgs(converter, targetOp, args);
   if (HostEvalInfo *hostEvalInfo = getHostEvalInfoStackTop(converter))
     hostEvalInfo->bindOperands(argIface.getHostEvalBlockArgs());
@@ -5251,6 +5253,14 @@ static mlir::omp::DistributeOp genCompositeDistributeParallelDo(
   ConstructQueue::const_iterator parallelItem = std::next(distributeItem);
   ConstructQueue::const_iterator doItem = std::next(parallelItem);
 
+  // Clause expressions follow source nesting even though the composite
+  // operations place PARALLEL outside DISTRIBUTE.
+  mlir::omp::DistributeOperands distributeClauseOps;
+  genDistributeClauses(converter, semaCtx, stmtCtx, distributeItem->clauses,
+                       loc, distributeClauseOps);
+  mlir::SaveStateStack<OpenMPContextFrame> distributeContext{
+      converter.getStateStack(), eval, llvm::omp::Directive::OMPD_distribute};
+
   // Create parent omp.parallel first.
   mlir::omp::ParallelOperands parallelClauseOps;
   llvm::SmallVector<Object> parallelReductionObjects;
@@ -5271,14 +5281,9 @@ static mlir::omp::DistributeOp genCompositeDistributeParallelDo(
   genParallelOp(converter, symTable, semaCtx, eval, loc, queue, parallelItem,
                 parallelClauseOps, parallelArgs, &dsp, /*isComposite=*/true);
   mlir::SaveStateStack<OpenMPContextFrame> context{
-      converter.getStateStack(), eval, llvm::omp::Directive::OMPD_parallel,
-      /*isPartial=*/true};
+      converter.getStateStack(), eval, llvm::omp::Directive::OMPD_parallel};
 
   // Clause processing.
-  mlir::omp::DistributeOperands distributeClauseOps;
-  genDistributeClauses(converter, semaCtx, stmtCtx, distributeItem->clauses,
-                       loc, distributeClauseOps);
-
   mlir::omp::WsloopOperands wsloopClauseOps;
   llvm::SmallVector<Object> wsloopReductionObjects;
   genWsloopClauses(converter, semaCtx, stmtCtx, doItem->clauses, loc,
@@ -5322,6 +5327,14 @@ static mlir::omp::DistributeOp genCompositeDistributeParallelDoSimd(
   ConstructQueue::const_iterator doItem = std::next(parallelItem);
   ConstructQueue::const_iterator simdItem = std::next(doItem);
 
+  // Evaluate DISTRIBUTE clauses before entering its source context and then
+  // PARALLEL, as in the explicit nesting.
+  mlir::omp::DistributeOperands distributeClauseOps;
+  genDistributeClauses(converter, semaCtx, stmtCtx, distributeItem->clauses,
+                       loc, distributeClauseOps);
+  mlir::SaveStateStack<OpenMPContextFrame> distributeContext{
+      converter.getStateStack(), eval, llvm::omp::Directive::OMPD_distribute};
+
   // Create parent omp.parallel first.
   mlir::omp::ParallelOperands parallelClauseOps;
   llvm::SmallVector<Object> parallelReductionObjects;
@@ -5344,18 +5357,13 @@ static mlir::omp::DistributeOp genCompositeDistributeParallelDoSimd(
                 parallelClauseOps, parallelArgs, &parallelItemDSP,
                 /*isComposite=*/true);
   mlir::SaveStateStack<OpenMPContextFrame> context{
-      converter.getStateStack(), eval, llvm::omp::Directive::OMPD_parallel,
-      /*isPartial=*/true};
+      converter.getStateStack(), eval, llvm::omp::Directive::OMPD_parallel};
 
   // Clause processing.
   // Use a shared cache so that both wsloop and simd produce the same SSA
   // values for array/box reduction variables. See genCompositeDoSimd.
   llvm::DenseMap<const semantics::Symbol *, mlir::Value> reductionVarCache;
 
-  mlir::omp::DistributeOperands distributeClauseOps;
-  genDistributeClauses(converter, semaCtx, stmtCtx, distributeItem->clauses,
-                       loc, distributeClauseOps);
-
   mlir::omp::WsloopOperands wsloopClauseOps;
   llvm::SmallVector<Object> wsloopReductionObjects;
   genWsloopClauses(converter, semaCtx, stmtCtx, doItem->clauses, loc,
@@ -7670,7 +7678,8 @@ static void genMetadirective(lower::AbstractConverter &converter,
         TODO(variantLoc, "THREADPRIVATE loop iteration variable in "
                          "loop-associated METADIRECTIVE variant");
       mlir::SaveStateStack<OpenMPContextFrame> context{
-          converter.getStateStack(), eval, spec->DirId()};
+          converter.getStateStack(), eval, spec->DirId(),
+          /*isReplacement=*/true};
       genOMPDispatch(converter, symTable, semaCtx, eval, variantLoc, queue,
                      queue.begin(), dsaGuard.getMarkedSymbols());
       return;
@@ -7686,7 +7695,8 @@ static void genMetadirective(lower::AbstractConverter &converter,
 
     if (consumesBody) {
       mlir::SaveStateStack<OpenMPContextFrame> context{
-          converter.getStateStack(), eval, spec->DirId()};
+          converter.getStateStack(), eval, spec->DirId(),
+          /*isReplacement=*/true};
       genOMPDispatch(converter, symTable, semaCtx, eval, variantLoc, queue,
                      queue.begin());
     } else {
diff --git a/flang/lib/Lower/OpenMP/Utils.cpp b/flang/lib/Lower/OpenMP/Utils.cpp
index db424331b4042..26eeaf0739393 100644
--- a/flang/lib/Lower/OpenMP/Utils.cpp
+++ b/flang/lib/Lower/OpenMP/Utils.cpp
@@ -1508,7 +1508,7 @@ void collectEnclosingConstructTraits(
       continue;
     }
     for (const OpenMPContextFrame *frame : frames) {
-      if (&frame->evaluation == ancestor && !frame->isPartial) {
+      if (&frame->evaluation == ancestor && frame->isReplacement) {
         append(frame->directive);
         break;
       }
@@ -1516,16 +1516,11 @@ void collectEnclosingConstructTraits(
   }
 
   // Include entered constituents while their own evaluation is current, e.g.
-  // PARALLEL when lowering the bounds of PARALLEL DO. Complete replacement
-  // frames already describe all constituents of a selected metadirective.
+  // PARALLEL when lowering the bounds of PARALLEL DO. A selected directive
+  // contributes here only through its entered constituents, so its own clause
+  // expressions have the same context as a directly written directive's.
   for (auto [index, frame] : llvm::enumerate(frames)) {
-    if (usedFrames[index])
-      continue;
-    if (frame->isPartial &&
-        llvm::any_of(
-            frames, [frameEval = &frame->evaluation](const auto *other) {
-              return !other->isPartial && &other->evaluation == frameEval;
-            }))
+    if (usedFrames[index] || frame->isReplacement)
       continue;
     append(frame->directive);
   }
diff --git a/flang/lib/Lower/OpenMP/Utils.h b/flang/lib/Lower/OpenMP/Utils.h
index e8d665e8600e4..2e0527c24e80b 100644
--- a/flang/lib/Lower/OpenMP/Utils.h
+++ b/flang/lib/Lower/OpenMP/Utils.h
@@ -58,14 +58,15 @@ class OpenMPContextFrame
   MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(OpenMPContextFrame)
 
   OpenMPContextFrame(const pft::Evaluation &evaluation,
-                     llvm::omp::Directive directive, bool isPartial = false)
-      : evaluation{evaluation}, directive{directive}, isPartial{isPartial} {}
+                     llvm::omp::Directive directive, bool isReplacement = false)
+      : evaluation{evaluation}, directive{directive},
+        isReplacement{isReplacement} {}
 
   const pft::Evaluation &evaluation;
   llvm::omp::Directive directive;
-  // A partial frame records an entered constituent of a combined directive.
-  // A complete frame supplies the source context when it is absent from PFT.
-  bool isPartial;
+  // A replacement records the selected directive for nested evaluations.
+  // Other frames record constructs whose regions have already been entered.
+  bool isReplacement;
 };
 
 struct DeclareTargetCaptureInfo {
diff --git a/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90 b/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
index 30cc4e7d55f4b..c3301c294c6ad 100644
--- a/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
+++ b/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
@@ -117,4 +117,64 @@ subroutine standalone_do(n, a)
       a(i) = bound(n)
     end do
   end subroutine
+
+  pure integer function cpu_count(n)
+    integer, intent(in) :: n
+    cpu_count = n
+  end function
+
+  pure integer function scored_count(n)
+    integer, intent(in) :: n
+    scored_count = n + 1
+  end function
+
+  pure integer function thread_count(n)
+    integer, intent(in) :: n
+    !$omp declare variant(cpu_count) match(device={kind(cpu)})
+    !$omp declare variant(scored_count) &
+    !$omp& match(implementation={vendor(score(3): llvm)})
+    thread_count = n
+  end function
+
+  ! DIST_SCHEDULE sees only TEAMS. NUM_THREADS sees TEAMS, DISTRIBUTE, so
+  ! CPU scores 5 and beats the vendor score of 4. The bound also sees PARALLEL.
+  ! CHECK-LABEL: func.func @_QMloop_contextPcomposite_clauses(
+  ! CHECK: omp.teams
+  ! CHECK: fir.call @_QMloop_contextPbound(
+  ! CHECK: fir.call @_QMloop_contextPcpu_count(
+  ! CHECK: omp.parallel
+  ! CHECK: fir.call @_QMloop_contextPparallel_bound(
+  ! CHECK: omp.distribute
+  ! CHECK: omp.wsloop
+  ! CHECK: fir.call @_QMloop_contextPdo_bound(
+  ! CHECK: return
+  subroutine composite_clauses(n, a)
+    integer :: n, a(n), i
+    !$omp teams distribute parallel do dist_schedule(static, bound(n)) &
+    !$omp& num_threads(thread_count(n))
+    do i = 1, bound(n)
+      a(i) = bound(n)
+    end do
+  end subroutine
+
+  ! The SIMD composite must evaluate its clauses in the same source contexts.
+  ! CHECK-LABEL: func.func @_QMloop_contextPcomposite_simd_clauses(
+  ! CHECK: omp.teams
+  ! CHECK: fir.call @_QMloop_contextPbound(
+  ! CHECK: fir.call @_QMloop_contextPcpu_count(
+  ! CHECK: omp.parallel
+  ! CHECK: fir.call @_QMloop_contextPparallel_bound(
+  ! CHECK: omp.distribute
+  ! CHECK: omp.wsloop
+  ! CHECK: omp.simd
+  ! CHECK: fir.call @_QMloop_contextPdo_bound(
+  ! CHECK: return
+  subroutine composite_simd_clauses(n, a)
+    integer :: n, a(n), i
+    !$omp teams distribute parallel do simd dist_schedule(static, bound(n)) &
+    !$omp& num_threads(thread_count(n))
+    do i = 1, bound(n)
+      a(i) = bound(n)
+    end do
+  end subroutine
 end module
diff --git a/flang/test/Lower/OpenMP/declare-variant-metadirective-context.f90 b/flang/test/Lower/OpenMP/declare-variant-metadirective-context.f90
new file mode 100644
index 0000000000000..2e5011167c9ee
--- /dev/null
+++ b/flang/test/Lower/OpenMP/declare-variant-metadirective-context.f90
@@ -0,0 +1,149 @@
+! RUN: %flang_fc1 -fopenmp -fopenmp-version=52 -emit-hlfir %s -o - | \
+! RUN:   FileCheck %s
+! RUN: %flang_fc1 -fopenmp -fopenmp-version=52 -emit-fir %s -o - | \
+! RUN:   FileCheck %s
+
+module selected_context
+contains
+  pure integer function in_parallel(n)
+    integer, intent(in) :: n
+    in_parallel = n + 1
+  end function
+
+  pure integer function in_do(n)
+    integer, intent(in) :: n
+    in_do = n + 2
+  end function
+
+  pure integer function value(n)
+    integer, intent(in) :: n
+    !$omp declare variant(in_parallel) match(construct={parallel})
+    !$omp declare variant(in_do) match(construct={do})
+    value = n
+  end function
+
+  ! NUM_THREADS is evaluated outside PARALLEL for either spelling. The body
+  ! sees PARALLEL, and leaving the region restores the enclosing context.
+  ! CHECK-LABEL: func.func @_QMselected_contextPdirect_parallel(
+  ! CHECK: fir.call @_QMselected_contextPvalue(
+  ! CHECK: omp.parallel
+  ! CHECK: fir.call @_QMselected_contextPin_parallel(
+  ! CHECK: fir.call @_QMselected_contextPvalue(
+  ! CHECK: return
+  subroutine direct_parallel(n, a)
+    integer :: n, a(n)
+    !$omp parallel num_threads(value(n))
+      a(1) = value(n)
+    !$omp end parallel
+    a(n) = value(n)
+  end subroutine
+
+  ! CHECK-LABEL: func.func @_QMselected_contextPstatic_parallel(
+  ! CHECK: fir.call @_QMselected_contextPvalue(
+  ! CHECK: omp.parallel
+  ! CHECK: fir.call @_QMselected_contextPin_parallel(
+  ! CHECK: fir.call @_QMselected_contextPvalue(
+  ! CHECK: return
+  subroutine static_parallel(n, a)
+    integer :: n, a(n)
+    !$omp begin metadirective &
+    !$omp& when(implementation={vendor(llvm)}: parallel num_threads(value(n))) &
+    !$omp& otherwise(nothing)
+      a(1) = value(n)
+    !$omp end metadirective
+    a(n) = value(n)
+  end subroutine
+
+  ! Each runtime arm uses its own context. NOTHING adds no construct.
+  ! CHECK-LABEL: func.func @_QMselected_contextPruntime_parallel(
+  ! CHECK: fir.if
+  ! CHECK: fir.call @_QMselected_contextPvalue(
+  ! CHECK: omp.parallel
+  ! CHECK: fir.call @_QMselected_contextPin_parallel(
+  ! CHECK: } else {
+  ! CHECK: fir.call @_QMselected_contextPvalue(
+  ! CHECK: fir.call @_QMselected_contextPvalue(
+  ! CHECK: return
+  subroutine runtime_parallel(flag, n, a)
+    logical :: flag
+    integer :: n, a(n)
+    !$omp begin metadirective &
+    !$omp& when(user={condition(flag)}: parallel num_threads(value(n))) &
+    !$omp& otherwise(nothing)
+      a(1) = value(n)
+    !$omp end metadirective
+    a(n) = value(n)
+  end subroutine
+
+  ! A loop's bounds exclude its own DO construct, while its body includes it.
+  ! CHECK-LABEL: func.func @_QMselected_contextPdirect_do(
+  ! CHECK: fir.call @_QMselected_contextPvalue(
+  ! CHECK: omp.wsloop
+  ! CHECK: fir.call @_QMselected_contextPin_do(
+  ! CHECK: fir.call @_QMselected_contextPvalue(
+  ! CHECK: return
+  subroutine direct_do(n, a)
+    integer :: n, a(n), i
+    !$omp do
+    do i = 1, value(n)
+      a(i) = value(n)
+    end do
+    a(n) = value(n)
+  end subroutine
+
+  ! The following DO remains a sibling of the standalone metadirective in
+  ! the source tree, but its body must still see the selected construct.
+  ! CHECK-LABEL: func.func @_QMselected_contextPstatic_do(
+  ! CHECK: fir.call @_QMselected_contextPvalue(
+  ! CHECK: omp.wsloop
+  ! CHECK: fir.call @_QMselected_contextPin_do(
+  ! CHECK: fir.call @_QMselected_contextPvalue(
+  ! CHECK: return
+  subroutine static_do(n, a)
+    integer :: n, a(n), i
+    !$omp metadirective when(implementation={vendor(llvm)}: do) &
+    !$omp& otherwise(nothing)
+    do i = 1, value(n)
+      a(i) = value(n)
+    end do
+    a(n) = value(n)
+  end subroutine
+
+  ! CHECK-LABEL: func.func @_QMselected_contextPruntime_do(
+  ! CHECK: fir.if
+  ! CHECK: fir.call @_QMselected_contextPvalue(
+  ! CHECK: omp.wsloop
+  ! CHECK: fir.call @_QMselected_contextPin_do(
+  ! CHECK: } else {
+  ! CHECK: fir.call @_QMselected_contextPvalue(
+  ! CHECK: fir.call @_QMselected_contextPvalue(
+  ! CHECK: fir.call @_QMselected_contextPvalue(
+  ! CHECK: return
+  subroutine runtime_do(flag, n, a)
+    logical :: flag
+    integer :: n, a(n), i
+    !$omp metadirective when(user={condition(flag)}: do) otherwise(nothing)
+    do i = 1, value(n)
+      a(i) = value(n)
+    end do
+    a(n) = value(n)
+  end subroutine
+
+  ! The delimited form owns the loop in the source tree.
+  ! CHECK-LABEL: func.func @_QMselected_contextPdelimited_do(
+  ! CHECK: fir.call @_QMselected_contextPvalue(
+  ! CHECK: omp.wsloop
+  ! CHECK: fir.call @_QMselected_contextPin_do(
+  ! CHECK: fir.call @_QMselected_contextPvalue(
+  ! CHECK: return
+  subroutine delimited_do(n, a)
+    integer :: n, a(n), i
+    !$omp begin metadirective &
+    !$omp& when(implementation={vendor(llvm)}: do) otherwise(nothing)
+    do i = 1, value(n)
+      a(i) = value(n)
+    end do
+    !$omp end metadirective
+    a(n) = value(n)
+  end subroutine
+end module
diff --git a/flang/test/Lower/OpenMP/variant-loop-context-depth.f90 b/flang/test/Lower/OpenMP/variant-loop-context-depth.f90
new file mode 100644
index 0000000000000..20a63d47f090d
--- /dev/null
+++ b/flang/test/Lower/OpenMP/variant-loop-context-depth.f90
@@ -0,0 +1,118 @@
+! RUN: %flang_fc1 -fopenmp -fopenmp-version=52 -emit-hlfir %s -o - | \
+! RUN:   FileCheck %s --implicit-check-not=omp.taskyield \
+! RUN:   --implicit-check-not='fir.call @_QMloop_depthPdo_value'
+! RUN: %flang_fc1 -fopenmp -fopenmp-version=52 -emit-fir %s -o - | \
+! RUN:   FileCheck %s --implicit-check-not=omp.taskyield \
+! RUN:   --implicit-check-not='fir.call @_QMloop_depthPdo_value'
+
+module loop_depth
+contains
+  integer function do_value()
+    do_value = 1
+  end function
+
+  integer function scored_value()
+    scored_value = 2
+  end function
+
+  integer function value()
+    !$omp declare variant(do_value) match(construct={do})
+    !$omp declare variant(scored_value) &
+    !$omp& match(implementation={vendor(score(3): llvm)})
+    value = 0
+  end function
+
+  ! At depth one CPU scores 3, below the user selector's score of 4.
+  ! Counting the DO twice would select TASKYIELD instead.
+  ! CHECK-LABEL: func.func @_QMloop_depthPbare_do(
+  ! CHECK: omp.wsloop
+  ! CHECK: omp.taskwait
+  ! CHECK: return
+  subroutine bare_do(n)
+    integer :: n, i
+    !$omp do
+    do i = 1, n
+      !$omp metadirective when(device={kind(cpu)}: taskyield) &
+      !$omp& when(user={condition(score(3): .true.)}: taskwait)
+    end do
+  end subroutine
+
+  ! DO at position two scores 3, below the vendor selector's score of 4.
+  ! The same context must be used for both kinds of variant selection.
+  ! CHECK-LABEL: func.func @_QMloop_depthPcombined(
+  ! CHECK: omp.parallel
+  ! CHECK: omp.wsloop
+  ! CHECK: omp.taskwait
+  ! CHECK: fir.call @_QMloop_depthPscored_value()
+  ! CHECK: return
+  subroutine combined(n, a)
+    integer :: n, i, a(n)
+    !$omp parallel do
+    do i = 1, n
+      !$omp metadirective when(construct={do}: taskyield) &
+      !$omp& when(implementation={vendor(score(3): llvm)}: taskwait)
+      a(i) = value()
+    end do
+  end subroutine
+
+  ! Intervening code is outside the innermost DO evaluation, but shares the
+  ! same OpenMP construct context as the collapsed loop body.
+  ! CHECK-LABEL: func.func @_QMloop_depthPcollapsed(
+  ! CHECK: omp.parallel
+  ! CHECK: omp.wsloop
+  ! CHECK: fir.call @_QMloop_depthPscored_value()
+  ! CHECK: fir.call @_QMloop_depthPscored_value()
+  ! CHECK: fir.call @_QMloop_depthPscored_value()
+  ! CHECK: return
+  subroutine collapsed(n, a)
+    integer :: n, i, j, a(n, n)
+    !$omp parallel do collapse(2)
+    do i = 1, n
+      a(i, 1) = value()
+      do j = 1, n
+        a(i, j) = value()
+      end do
+      a(i, n) = value()
+    end do
+  end subroutine
+
+  ! A nested TASK contributes a position, but must not move the enclosing DO.
+  ! CHECK-LABEL: func.func @_QMloop_depthPnested_task(
+  ! CHECK: omp.parallel
+  ! CHECK: omp.wsloop
+  ! CHECK: omp.task
+  ! CHECK: omp.taskwait
+  ! CHECK: return
+  subroutine nested_task(n)
+    integer :: n, i
+    !$omp parallel do
+    do i = 1, n
+      !$omp task
+        !$omp metadirective when(construct={do}: taskyield) &
+        !$omp& when(implementation={vendor(score(3): llvm)}: taskwait)
+      !$omp end task
+    end do
+  end subroutine
+
+  ! TARGET hides the outer PARALLEL. TARGET, PARALLEL, DO give depth three:
+  ! CPU scores 9 and loses to the user selector's score of 10.
+  ! CHECK-LABEL: func.func @_QMloop_depthPtarget_depth(
+  ! CHECK: omp.parallel
+  ! CHECK: omp.target
+  ! CHECK: omp.parallel
+  ! CHECK: omp.wsloop
+  ! CHECK: omp.taskwait
+  ! CHECK: return
+  subroutine target_depth(n)
+    integer :: n, i
+    !$omp parallel
+      !$omp target
+        !$omp parallel do
+        do i = 1, n
+          !$omp metadirective when(device={kind(cpu)}: taskyield) &
+          !$omp& when(user={condition(score(9): .true.)}: taskwait)
+        end do
+      !$omp end target
+    !$omp end parallel
+  end subroutine
+end module

>From 6d4d24c6b96e6b115797af6bb6f6a48dd07777a6 Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Thu, 24 Sep 2026 13:43:41 -0500
Subject: [PATCH 04/23] Preserve declaration identity in user conditions

Use canonical AST profiles instead of printed expressions for condition
subset checks. Preserve original expressions through template substitution
and PCH serialization so distinct declarations remain distinguishable.

Add regression coverage for template parameters, scoped declarations,
and PCH round trips.
---
 clang/include/clang/AST/OpenMPClause.h        |   4 +-
 clang/lib/AST/OpenMPClause.cpp                |  21 +++-
 clang/lib/Parse/ParseOpenMP.cpp               |  12 +-
 clang/lib/Serialization/ASTReader.cpp         |   2 +
 clang/lib/Serialization/ASTWriter.cpp         |   3 +
 ...declare_variant_condition_declarations.cpp | 107 ++++++++++++++++++
 .../declare_variant_condition_identity_pch.c  |   6 +-
 7 files changed, 138 insertions(+), 17 deletions(-)
 create mode 100644 clang/test/OpenMP/declare_variant_condition_declarations.cpp

diff --git a/clang/include/clang/AST/OpenMPClause.h b/clang/include/clang/AST/OpenMPClause.h
index d8b6f769ff27f..fb405b612cb1f 100644
--- a/clang/include/clang/AST/OpenMPClause.h
+++ b/clang/include/clang/AST/OpenMPClause.h
@@ -10066,12 +10066,14 @@ struct OMPTraitProperty {
   llvm::omp::TraitProperty Kind = llvm::omp::TraitProperty::invalid;
 
   /// The raw string as we parsed it. This is needed for the `isa` trait set
-  /// (which accepts anything), user-condition identity, and extensions.
+  /// (which accepts anything) and extensions.
   StringRef RawString;
 };
 
 struct OMPTraitSelector {
   Expr *ScoreOrCondition = nullptr;
+  /// Preserve the condition before template substitution for subset checks.
+  Expr *OriginalCondition = nullptr;
   llvm::omp::TraitSelector Kind = llvm::omp::TraitSelector::invalid;
   SmallVector<OMPTraitProperty, 1> Properties;
 };
diff --git a/clang/lib/AST/OpenMPClause.cpp b/clang/lib/AST/OpenMPClause.cpp
index 10d401cda880f..02251ec30c641 100644
--- a/clang/lib/AST/OpenMPClause.cpp
+++ b/clang/lib/AST/OpenMPClause.cpp
@@ -20,6 +20,7 @@
 #include "clang/Basic/LLVM.h"
 #include "clang/Basic/OpenMPKinds.h"
 #include "clang/Basic/TargetInfo.h"
+#include "llvm/ADT/FoldingSet.h"
 #include "llvm/ADT/Sequence.h"
 #include "llvm/ADT/SmallPtrSet.h"
 #include "llvm/Support/ErrorHandling.h"
@@ -3053,14 +3054,28 @@ void OMPTraitInfo::getAsVariantMatchInfo(ASTContext &ASTCtx,
                    TraitProperty::user_condition_unknown &&
                "Ill-formed user condition, expected unknown trait property!");
 
+        // Profile the original expression so equally valued template arguments
+        // do not erase distinctions between different parameters. Canonical
+        // profiles also distinguish declarations with the same printed name.
+        // Compute this after loading the AST: profiles contain local pointers
+        // and cannot be compared with profiles serialized by another process.
+        assert(Selector.OriginalCondition && "missing original condition");
+        llvm::FoldingSetNodeID ID;
+        Selector.OriginalCondition->IgnoreParenImpCasts()->Profile(
+            ID, ASTCtx, /*Canonical=*/true);
+        llvm::FoldingSetNodeIDRef Profile = ID.Intern(ASTCtx.getAllocator());
+        // Keep the full profile to avoid hash collisions in subset checks.
+        StringRef ConditionIdentity(
+            reinterpret_cast<const char *>(Profile.data()),
+            Profile.size() * sizeof(unsigned));
+
         if (std::optional<APSInt> CondVal =
                 Selector.ScoreOrCondition->getIntegerConstantExpr(ASTCtx))
           VMI.addTrait(CondVal->isZero() ? TraitProperty::user_condition_false
                                          : TraitProperty::user_condition_true,
-                       Selector.Properties.front().RawString);
+                       ConditionIdentity);
         else
-          VMI.addTrait(TraitProperty::user_condition_false,
-                       Selector.Properties.front().RawString);
+          VMI.addTrait(TraitProperty::user_condition_false, ConditionIdentity);
         continue;
       }
 
diff --git a/clang/lib/Parse/ParseOpenMP.cpp b/clang/lib/Parse/ParseOpenMP.cpp
index 6684c6b69f8b3..b623deaa77603 100644
--- a/clang/lib/Parse/ParseOpenMP.cpp
+++ b/clang/lib/Parse/ParseOpenMP.cpp
@@ -28,7 +28,6 @@
 #include "llvm/Frontend/OpenMP/DirectiveNameParser.h"
 #include "llvm/Frontend/OpenMP/OMPAssume.h"
 #include "llvm/Frontend/OpenMP/OMPContext.h"
-#include "llvm/Support/raw_ostream.h"
 #include <climits>
 #include <optional>
 
@@ -1151,16 +1150,9 @@ void Parser::parseOMPContextSelector(
     if (!Condition.isUsable())
       return FinishSelector();
     TISelector.ScoreOrCondition = Condition.get();
-    // Compare expanded expressions, since a macro can be redefined between
-    // selectors. Ignore outer parentheses when recording condition identity.
-    ASTContext &Context = Actions.getASTContext();
-    std::string ConditionText;
-    llvm::raw_string_ostream OS(ConditionText);
-    Condition.get()->IgnoreParenImpCasts()->printPretty(
-        OS, nullptr, Context.getPrintingPolicy());
+    TISelector.OriginalCondition = Condition.get();
     TISelector.Properties.push_back(
-        {TraitProperty::user_condition_unknown,
-         StringRef(ConditionText).copy(Context.getAllocator())});
+        {TraitProperty::user_condition_unknown, "<condition>"});
     return;
   }
 
diff --git a/clang/lib/Serialization/ASTReader.cpp b/clang/lib/Serialization/ASTReader.cpp
index c3e564e7f6aad..ac14cd3c30e77 100644
--- a/clang/lib/Serialization/ASTReader.cpp
+++ b/clang/lib/Serialization/ASTReader.cpp
@@ -13235,6 +13235,8 @@ OMPTraitInfo *ASTRecordReader::readOMPTraitInfo() {
       Selector.ScoreOrCondition = nullptr;
       if (readBool())
         Selector.ScoreOrCondition = readExprRef();
+      if (readBool())
+        Selector.OriginalCondition = readExprRef();
       Selector.Properties.resize(readUInt32());
       for (auto &Property : Selector.Properties) {
         Property.Kind = readEnum<llvm::omp::TraitProperty>();
diff --git a/clang/lib/Serialization/ASTWriter.cpp b/clang/lib/Serialization/ASTWriter.cpp
index 9e1b6c6329cfe..5a24a3cb63d22 100644
--- a/clang/lib/Serialization/ASTWriter.cpp
+++ b/clang/lib/Serialization/ASTWriter.cpp
@@ -9048,6 +9048,9 @@ void ASTRecordWriter::writeOMPTraitInfo(const OMPTraitInfo *TI) {
       writeBool(Selector.ScoreOrCondition);
       if (Selector.ScoreOrCondition)
         writeExprRef(Selector.ScoreOrCondition);
+      writeBool(Selector.OriginalCondition);
+      if (Selector.OriginalCondition)
+        writeExprRef(Selector.OriginalCondition);
       writeUInt32(Selector.Properties.size());
       for (const auto &Property : Selector.Properties) {
         writeEnum(Property.Kind);
diff --git a/clang/test/OpenMP/declare_variant_condition_declarations.cpp b/clang/test/OpenMP/declare_variant_condition_declarations.cpp
new file mode 100644
index 0000000000000..cbdbab8509e2b
--- /dev/null
+++ b/clang/test/OpenMP/declare_variant_condition_declarations.cpp
@@ -0,0 +1,107 @@
+// RUN: split-file %s %t
+// RUN: %clang_cc1 -fopenmp -fopenmp-version=52 \
+// RUN:   -triple x86_64-unknown-linux -include %t/conditions.h \
+// RUN:   %t/use.cpp -emit-llvm -o - | FileCheck %s
+// RUN: %clang_cc1 -fopenmp -fopenmp-version=52 \
+// RUN:   -triple x86_64-unknown-linux -x c++-header %t/conditions.h \
+// RUN:   -emit-pch -o %t/conditions.pch
+// RUN: %clang_cc1 -fopenmp -fopenmp-version=52 \
+// RUN:   -triple x86_64-unknown-linux -include-pch %t/conditions.pch \
+// RUN:   %t/use.cpp -emit-llvm -o - | FileCheck %s
+
+// Conditions naming different parameters remain distinct even when their
+// arguments have the same value. Exercise an attribute instantiated before
+// serialization as well as attributes instantiated after loading the PCH.
+// CHECK-LABEL: define{{.*}} i32 @instantiated_before_pch()
+// CHECK: call i32 @high()
+// CHECK: ret i32
+// CHECK-LABEL: define{{.*}} i32 @distinct_parameters()
+// CHECK: call i32 @high()
+// CHECK: call i32 @high()
+// CHECK: ret i32
+
+// Parameter names do not affect identity when their positions agree.
+// CHECK-LABEL: define{{.*}} i32 @renamed_parameters()
+// CHECK: call i32 @low()
+// CHECK: ret i32
+
+// Equal-valued enumerators in different scopes are different declarations.
+// CHECK-LABEL: define{{.*}} i32 @different_scopes()
+// CHECK: call i32 @high()
+// CHECK: ret i32
+
+// Compare a condition loaded from a PCH with one parsed in this process.
+// They name the same declaration and must still form a subset relationship.
+// CHECK-LABEL: define{{.*}} i32 @same_declaration()
+// CHECK: call i32 @low()
+// CHECK: ret i32
+
+//--- conditions.h
+extern "C" int high();
+extern "C" int low();
+
+#pragma omp declare variant(high) \
+    match(user = {condition(N)}, \
+          implementation = {vendor(score(100) : llvm)})
+template <int N, int M> int distinct();
+#pragma omp declare variant(low) \
+    match(user = {condition(N)}, device = {kind(cpu)}, \
+          implementation = {vendor(score(1) : llvm)})
+template <int M, int N> int distinct();
+
+extern "C" int instantiated_before_pch() { return distinct<1, 1>(); }
+
+#pragma omp declare variant(high) \
+    match(user = {condition(N)}, \
+          implementation = {vendor(score(100) : llvm)})
+template <int N> int renamed();
+#pragma omp declare variant(low) \
+    match(user = {condition(A)}, device = {kind(cpu)}, \
+          implementation = {vendor(score(1) : llvm)})
+template <int A> int renamed();
+
+namespace First {
+enum { A = 1 };
+extern "C" {
+#pragma omp declare variant(high) \
+    match(user = {condition(A)}, \
+          implementation = {vendor(score(100) : llvm)})
+int scoped_base();
+}
+}
+
+enum { Shared = 1 };
+#pragma omp declare variant(high) \
+    match(user = {condition(Shared)}, \
+          implementation = {vendor(score(100) : llvm)})
+int shared_base();
+
+//--- use.cpp
+namespace Second {
+enum { A = 1 };
+extern "C" {
+#pragma omp declare variant(low) \
+    match(user = {condition(A)}, device = {kind(cpu)}, \
+          implementation = {vendor(score(1) : llvm)})
+int scoped_base();
+}
+}
+
+#pragma omp declare variant(low) \
+    match(user = {condition(Shared)}, device = {kind(cpu)}, \
+          implementation = {vendor(score(1) : llvm)})
+int shared_base();
+
+extern "C" int distinct_parameters() {
+  return distinct<1, 2>() + distinct<1, 1>();
+}
+
+extern "C" int renamed_parameters() { return renamed<1>(); }
+
+extern "C" int different_scopes() {
+  return Second::scoped_base();
+}
+
+extern "C" int same_declaration() {
+  return shared_base();
+}
diff --git a/clang/test/OpenMP/declare_variant_condition_identity_pch.c b/clang/test/OpenMP/declare_variant_condition_identity_pch.c
index 166612af3a933..57d2c16ba8bf2 100644
--- a/clang/test/OpenMP/declare_variant_condition_identity_pch.c
+++ b/clang/test/OpenMP/declare_variant_condition_identity_pch.c
@@ -12,9 +12,9 @@
 // RUN:   -triple x86_64-unknown-linux -x c++ %t/use.c \
 // RUN:   -include-pch %t/conditions-cxx.pch -emit-llvm -o - | FileCheck %s
 
-// Verify that serialization preserves the expanded identity of folded user
-// conditions. Distinct conditions do not form a subset relationship, while
-// identical conditions do.
+// Verify that serialization preserves the expressions of folded user
+// conditions. Expanded expressions identify distinct conditions, while
+// ignoring outer parentheses preserves identical conditions.
 
 // CHECK-LABEL: define{{.*}} void @test_conditions()
 // CHECK: call void @condition_high_variant()

>From b70f65cdf5e434b94b4ef4a5c6e94b25170fc44c Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Fri, 25 Sep 2026 01:33:11 -0500
Subject: [PATCH 05/23] Fix construct contexts for SIMD clause expressions

Enter DO while lowering SIMD clauses in DO SIMD and DISTRIBUTE PARALLEL
DO SIMD, then restore the context before lowering loop bounds.

Check SIMD clause selection, TEAMS in DIST_SCHEDULE, and the absence of
PARALLEL in NUM_THREADS operands.
---
 flang/lib/Lower/OpenMP/OpenMP.cpp             | 20 ++++-
 .../OpenMP/declare-variant-loop-bounds.f90    | 74 +++++++++++++++++--
 2 files changed, 85 insertions(+), 9 deletions(-)

diff --git a/flang/lib/Lower/OpenMP/OpenMP.cpp b/flang/lib/Lower/OpenMP/OpenMP.cpp
index 383f7a8335a10..acb48dc54cf64 100644
--- a/flang/lib/Lower/OpenMP/OpenMP.cpp
+++ b/flang/lib/Lower/OpenMP/OpenMP.cpp
@@ -5371,8 +5371,14 @@ static mlir::omp::DistributeOp genCompositeDistributeParallelDoSimd(
 
   mlir::omp::SimdOperands simdClauseOps;
   llvm::SmallVector<Object> simdReductionObjects;
-  genSimdClauses(converter, semaCtx, simdItem->clauses, loc, simdClauseOps,
-                 simdReductionObjects, &reductionVarCache);
+  {
+    // SIMD clause expressions are inside DO, but loop control remains outside
+    // the loop constructs.
+    mlir::SaveStateStack<OpenMPContextFrame> doContext{
+        converter.getStateStack(), eval, llvm::omp::Directive::OMPD_do};
+    genSimdClauses(converter, semaCtx, simdItem->clauses, loc, simdClauseOps,
+                   simdReductionObjects, &reductionVarCache);
+  }
 
   // Same as genCompositeDoSimd.
   if (!simdClauseOps.linearVars.empty()) {
@@ -5519,8 +5525,14 @@ static mlir::omp::WsloopOp genCompositeDoSimd(
 
   mlir::omp::SimdOperands simdClauseOps;
   llvm::SmallVector<Object> simdReductionObjects;
-  genSimdClauses(converter, semaCtx, simdItem->clauses, loc, simdClauseOps,
-                 simdReductionObjects, &reductionVarCache);
+  {
+    // SIMD clause expressions are inside DO, but loop control remains outside
+    // the loop constructs.
+    mlir::SaveStateStack<OpenMPContextFrame> doContext{
+        converter.getStateStack(), eval, llvm::omp::Directive::OMPD_do};
+    genSimdClauses(converter, semaCtx, simdItem->clauses, loc, simdClauseOps,
+                   simdReductionObjects, &reductionVarCache);
+  }
 
   // omp.simd writes back linear vars unconditionally, causing a race when
   // inside a parallel region. Move them to wsloop which has proper last-iter
diff --git a/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90 b/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
index c3301c294c6ad..85a301bdfe3a8 100644
--- a/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
+++ b/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
@@ -1,8 +1,15 @@
 ! RUN: %flang_fc1 -fopenmp -fopenmp-version=52 -emit-hlfir %s -o - | \
-! RUN:   FileCheck %s
+! RUN:   FileCheck %s --implicit-check-not='fir.call @_QMloop_contextPparallel_count'
+! RUN: %flang_fc1 -fopenmp -fopenmp-version=52 -emit-fir %s -o - | \
+! RUN:   FileCheck %s --implicit-check-not='fir.call @_QMloop_contextPparallel_count'
 
 module loop_context
 contains
+  pure integer function teams_bound(n)
+    integer, intent(in) :: n
+    teams_bound = n
+  end function
+
   pure integer function parallel_bound(n)
     integer, intent(in) :: n
     parallel_bound = n
@@ -15,6 +22,7 @@ pure integer function do_bound(n)
 
   pure integer function bound(n)
     integer, intent(in) :: n
+    !$omp declare variant(teams_bound) match(construct={teams})
     !$omp declare variant(parallel_bound) match(construct={parallel})
     !$omp declare variant(do_bound) match(construct={do})
     bound = n
@@ -128,19 +136,38 @@ pure integer function scored_count(n)
     scored_count = n + 1
   end function
 
+  pure integer function parallel_count(n)
+    integer, intent(in) :: n
+    parallel_count = n + 2
+  end function
+
+  pure logical function do_pred(n)
+    integer, intent(in) :: n
+    do_pred = n > 0
+  end function
+
+  pure logical function pred(n)
+    integer, intent(in) :: n
+    !$omp declare variant(do_pred) match(construct={do})
+    pred = n > 0
+  end function
+
   pure integer function thread_count(n)
     integer, intent(in) :: n
     !$omp declare variant(cpu_count) match(device={kind(cpu)})
+    !$omp declare variant(parallel_count) &
+    !$omp& match(construct={parallel}, device={kind(cpu)})
     !$omp declare variant(scored_count) &
     !$omp& match(implementation={vendor(score(3): llvm)})
     thread_count = n
   end function
 
   ! DIST_SCHEDULE sees only TEAMS. NUM_THREADS sees TEAMS, DISTRIBUTE, so
-  ! CPU scores 5 and beats the vendor score of 4. The bound also sees PARALLEL.
+  ! CPU scores 5 and beats the vendor score of 4. A leaked PARALLEL would
+  ! make parallel_count a strict superset of cpu_count. The bound sees PARALLEL.
   ! CHECK-LABEL: func.func @_QMloop_contextPcomposite_clauses(
   ! CHECK: omp.teams
-  ! CHECK: fir.call @_QMloop_contextPbound(
+  ! CHECK: fir.call @_QMloop_contextPteams_bound(
   ! CHECK: fir.call @_QMloop_contextPcpu_count(
   ! CHECK: omp.parallel
   ! CHECK: fir.call @_QMloop_contextPparallel_bound(
@@ -160,9 +187,10 @@ subroutine composite_clauses(n, a)
   ! The SIMD composite must evaluate its clauses in the same source contexts.
   ! CHECK-LABEL: func.func @_QMloop_contextPcomposite_simd_clauses(
   ! CHECK: omp.teams
-  ! CHECK: fir.call @_QMloop_contextPbound(
+  ! CHECK: fir.call @_QMloop_contextPteams_bound(
   ! CHECK: fir.call @_QMloop_contextPcpu_count(
   ! CHECK: omp.parallel
+  ! CHECK: fir.call @_QMloop_contextPdo_pred(
   ! CHECK: fir.call @_QMloop_contextPparallel_bound(
   ! CHECK: omp.distribute
   ! CHECK: omp.wsloop
@@ -172,9 +200,45 @@ subroutine composite_clauses(n, a)
   subroutine composite_simd_clauses(n, a)
     integer :: n, a(n), i
     !$omp teams distribute parallel do simd dist_schedule(static, bound(n)) &
-    !$omp& num_threads(thread_count(n))
+    !$omp& num_threads(thread_count(n)) if(simd: pred(n))
+    do i = 1, bound(n)
+      a(i) = bound(n)
+    end do
+  end subroutine
+
+  ! SIMD clauses see DO, but the following loop bound and post-loop call do not.
+  ! CHECK-LABEL: func.func @_QMloop_contextPdo_simd_clauses(
+  ! CHECK: fir.call @_QMloop_contextPdo_pred(
+  ! CHECK: fir.call @_QMloop_contextPbound(
+  ! CHECK: omp.wsloop
+  ! CHECK: omp.simd
+  ! CHECK: fir.call @_QMloop_contextPdo_bound(
+  ! CHECK: fir.call @_QMloop_contextPpred(
+  ! CHECK: return
+  subroutine do_simd_clauses(n, a, flag)
+    integer :: n, a(n), i
+    logical :: flag
+    !$omp do simd if(simd: pred(n))
     do i = 1, bound(n)
       a(i) = bound(n)
     end do
+    flag = pred(n)
+  end subroutine
+
+  ! The explicit DO/SIMD nesting also exposes DO to the SIMD clause.
+  ! CHECK-LABEL: func.func @_QMloop_contextPnested_simd_clauses(
+  ! CHECK: omp.wsloop
+  ! CHECK: fir.call @_QMloop_contextPdo_pred(
+  ! CHECK: omp.simd
+  ! CHECK: return
+  subroutine nested_simd_clauses(n, a)
+    integer :: n, a(n, n), i, j
+    !$omp do
+    do i = 1, n
+      !$omp simd if(simd: pred(n))
+      do j = 1, n
+        a(i, j) = i + j
+      end do
+    end do
   end subroutine
 end module

>From 7e1f5e9aaaf0327f45773c7c038ff8aad40249bb Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Fri, 25 Sep 2026 01:33:11 -0500
Subject: [PATCH 06/23] Strengthen variant context and ISA serialization tests

Add DO-only calls at each collapsed-loop position so missing and duplicate
DO contexts are both detected. Resolve ISA variant calls after loading a
PCH, with and without the requested target feature.
---
 clang/test/OpenMP/declare_variant_isa_pch.c   | 30 +++++++++++++++++++
 .../OpenMP/variant-loop-context-depth.f90     | 17 ++++++++---
 2 files changed, 43 insertions(+), 4 deletions(-)
 create mode 100644 clang/test/OpenMP/declare_variant_isa_pch.c

diff --git a/clang/test/OpenMP/declare_variant_isa_pch.c b/clang/test/OpenMP/declare_variant_isa_pch.c
new file mode 100644
index 0000000000000..36364e96b4ef8
--- /dev/null
+++ b/clang/test/OpenMP/declare_variant_isa_pch.c
@@ -0,0 +1,30 @@
+// RUN: split-file %s %t
+// RUN: %clang_cc1 -fopenmp -triple x86_64-unknown-linux -target-feature +avx512f \
+// RUN:   -x c-header %t/variants.h -emit-pch -o %t/variants.pch
+// RUN: %clang_cc1 -fopenmp -triple x86_64-unknown-linux -target-feature +avx512f \
+// RUN:   -include-pch %t/variants.pch %t/use.c -emit-llvm -o - -verify | \
+// RUN:   FileCheck %s --check-prefix=AVX512
+// RUN: %clang_cc1 -fopenmp -triple x86_64-unknown-linux \
+// RUN:   -x c-header %t/variants.h -emit-pch -o %t/generic.pch
+// RUN: %clang_cc1 -fopenmp -triple x86_64-unknown-linux \
+// RUN:   -include-pch %t/generic.pch %t/use.c -emit-llvm -o - -verify | \
+// RUN:   FileCheck %s --check-prefix=GENERIC
+
+// Resolve the call after loading the selector, not while building the PCH.
+// AVX512-LABEL: define{{.*}} void @caller(
+// AVX512: call void @avx_variant()
+// AVX512-NOT: call void @base_isa()
+// AVX512: ret void
+// GENERIC-LABEL: define{{.*}} void @caller(
+// GENERIC: call void @base_isa()
+// GENERIC-NOT: call void @avx_variant()
+// GENERIC: ret void
+
+//--- variants.h
+void avx_variant(void);
+#pragma omp declare variant(avx_variant) match(device={isa(avx512f)})
+void base_isa(void);
+
+//--- use.c
+// expected-no-diagnostics
+void caller(void) { base_isa(); }
diff --git a/flang/test/Lower/OpenMP/variant-loop-context-depth.f90 b/flang/test/Lower/OpenMP/variant-loop-context-depth.f90
index 20a63d47f090d..87ca89d3b70f3 100644
--- a/flang/test/Lower/OpenMP/variant-loop-context-depth.f90
+++ b/flang/test/Lower/OpenMP/variant-loop-context-depth.f90
@@ -22,6 +22,11 @@ integer function value()
     value = 0
   end function
 
+  integer function requiring_do()
+    !$omp declare variant(do_value) match(construct={do})
+    requiring_do = 0
+  end function
+
   ! At depth one CPU scores 3, below the user selector's score of 4.
   ! Counting the DO twice would select TASKYIELD instead.
   ! CHECK-LABEL: func.func @_QMloop_depthPbare_do(
@@ -56,23 +61,27 @@ subroutine combined(n, a)
   end subroutine
 
   ! Intervening code is outside the innermost DO evaluation, but shares the
-  ! same OpenMP construct context as the collapsed loop body.
+  ! same OpenMP construct context as the collapsed loop body. The competing
+  ! call rejects a duplicated DO; the DO-only call rejects a missing DO.
   ! CHECK-LABEL: func.func @_QMloop_depthPcollapsed(
   ! CHECK: omp.parallel
   ! CHECK: omp.wsloop
   ! CHECK: fir.call @_QMloop_depthPscored_value()
+  ! CHECK: fir.call @_QMloop_depthPdo_value()
   ! CHECK: fir.call @_QMloop_depthPscored_value()
+  ! CHECK: fir.call @_QMloop_depthPdo_value()
   ! CHECK: fir.call @_QMloop_depthPscored_value()
+  ! CHECK: fir.call @_QMloop_depthPdo_value()
   ! CHECK: return
   subroutine collapsed(n, a)
     integer :: n, i, j, a(n, n)
     !$omp parallel do collapse(2)
     do i = 1, n
-      a(i, 1) = value()
+      a(i, 1) = value() + requiring_do()
       do j = 1, n
-        a(i, j) = value()
+        a(i, j) = value() + requiring_do()
       end do
-      a(i, n) = value()
+      a(i, n) = value() + requiring_do()
     end do
   end subroutine
 

>From 0e4b61016c84fd7b13d467663f376a4199b6bce2 Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Fri, 25 Sep 2026 01:33:12 -0500
Subject: [PATCH 07/23] Normalize Flang user condition identity

Ignore outer parentheses and resolve USE aliases to their ultimate
symbols, preserving original expressions and distinct declarations.
Carry the same identity through static and runtime candidate ranking.

Add coverage for aliases, equal-valued declarations, imported symbols,
and false-condition ranking.
---
 flang/lib/Semantics/openmp-utils.cpp          |  89 +++++++-
 .../declare-variant-condition-identity.f90    | 198 ++++++++++++++++++
 2 files changed, 280 insertions(+), 7 deletions(-)
 create mode 100644 flang/test/Lower/OpenMP/declare-variant-condition-identity.f90

diff --git a/flang/lib/Semantics/openmp-utils.cpp b/flang/lib/Semantics/openmp-utils.cpp
index b2545fa29c3b6..f08f9abfcdb98 100644
--- a/flang/lib/Semantics/openmp-utils.cpp
+++ b/flang/lib/Semantics/openmp-utils.cpp
@@ -28,6 +28,7 @@
 #include "flang/Evaluate/type.h"
 #include "flang/Evaluate/variable.h"
 #include "flang/Parser/openmp-utils.h"
+#include "flang/Parser/parse-tree-visitor.h"
 #include "flang/Parser/parse-tree.h"
 #include "flang/Semantics/expression.h"
 #include "flang/Semantics/openmp-directive-sets.h"
@@ -38,6 +39,7 @@
 #include "llvm/ADT/APInt.h"
 #include "llvm/ADT/ArrayRef.h"
 #include "llvm/ADT/DenseSet.h"
+#include "llvm/ADT/FoldingSet.h"
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/ADT/SmallPtrSet.h"
 #include "llvm/ADT/SmallVector.h"
@@ -2503,6 +2505,80 @@ void AppendDirectiveContextTraits(llvm::omp::Directive directive,
   }
 }
 
+namespace {
+struct ConditionNames {
+  template <typename A> bool Pre(const A &) { return true; }
+  template <typename A> void Post(const A &) {}
+  bool Pre(const parser::Name &name) {
+    if (name.symbol)
+      names.push_back(&name);
+    return false;
+  }
+  llvm::SmallVector<const parser::Name *> names;
+};
+
+// Profile the original expression, not its folded value. In particular, two
+// named constants with the same value still denote different declarations.
+// Retain the cooked expression spelling, except for outer parentheses, blanks
+// outside character literals, and names resolved to their ultimate symbols.
+llvm::StringRef GetConditionIdentity(
+    const parser::ScalarExpr &condition, SemanticsContext &context) {
+  const auto *expr{parser::Unwrap<parser::Expr>(condition)};
+  CHECK(expr);
+  while (
+      const auto *parentheses{std::get_if<parser::Expr::Parentheses>(&expr->u)})
+    expr = &parentheses->v.value();
+
+  ConditionNames visitor;
+  parser::Walk(*expr, visitor);
+  llvm::sort(visitor.names, [](const auto *a, const auto *b) {
+    return a->source.begin() < b->source.begin();
+  });
+
+  llvm::FoldingSetNodeID id;
+  auto addText = [&](parser::CharBlock source) {
+    std::string text;
+    char quote{0};
+    for (const char *p{source.begin()}; p != source.end(); ++p) {
+      char ch{*p};
+      if (quote) {
+        text += ch;
+        if (ch == quote)
+          quote = 0;
+        else if (ch == '\\' &&
+            context.IsEnabled(common::LanguageFeature::BackslashEscapes) &&
+            p + 1 != source.end())
+          text += *++p;
+      } else if (ch == '\'' || ch == '"') {
+        quote = ch;
+        text += ch;
+      } else if (!llvm::isSpace(ch)) {
+        text += ch;
+      }
+    }
+    id.AddString(text);
+  };
+
+  const char *next{expr->source.begin()};
+  for (const parser::Name *name : visitor.names) {
+    if (!expr->source.Contains(name->source) || name->source.begin() < next)
+      continue;
+    addText({next, name->source.begin()});
+    id.AddPointer(&name->symbol->GetUltimate());
+    next = name->source.end();
+  }
+  addText({next, expr->source.end()});
+
+  // Intern the full profile in context-owned storage. Recompute it after
+  // reading a module; symbol pointers are local to this compilation.
+  llvm::FoldingSetNodeIDRef profile{id.getRef()};
+  SourceName saved{context.SaveTempName(
+      std::string{reinterpret_cast<const char *>(profile.data()),
+          profile.size() * sizeof(unsigned)})};
+  return {saved.begin(), saved.size()};
+}
+} // namespace
+
 static void AddTraitPropertiesFromSelector(llvm::omp::TraitSet set,
     const parser::OmpTraitSelector &selector, llvm::omp::VariantMatchInfo &vmi,
     SemanticsContext &semaCtx,
@@ -2529,8 +2605,8 @@ static void AddTraitPropertiesFromSelector(llvm::omp::TraitSet set,
       if (!scalarExpr) {
         continue;
       }
+      llvm::StringRef condition{GetConditionIdentity(*scalarExpr, semaCtx)};
       if (auto constValue{EvaluateUserCondition(semaCtx, *scalarExpr)}) {
-        llvm::StringRef condition{prop.source.begin(), prop.source.size()};
         vmi.addTrait(set,
             *constValue ? llvm::omp::TraitProperty::user_condition_true
                         : llvm::omp::TraitProperty::user_condition_false,
@@ -2541,7 +2617,7 @@ static void AddTraitPropertiesFromSelector(llvm::omp::TraitSet set,
         dynamicCond = DynamicUserCondition{scalarExpr, prop.source};
       }
       vmi.addTrait(set, llvm::omp::TraitProperty::user_condition_unknown,
-          llvm::StringRef{prop.source.begin(), prop.source.size()}, scorePtr);
+          condition, scorePtr);
     }
     return;
   }
@@ -2661,8 +2737,7 @@ std::optional<MetadirectiveCandidateSet> BuildMetadirectiveCandidateSet(
         staticVMI.UserCondition = {};
         llvm::APInt *conditionScorePtr{
             conditionScore ? &*conditionScore : nullptr};
-        llvm::StringRef conditionSource{
-            dynamicCondition->source.begin(), dynamicCondition->source.size()};
+        llvm::StringRef conditionIdentity{rawVMI.UserCondition};
 
         bool hasMatchAny{rawVMI.RequiredTraits.test(unsigned(matchAnyTrait))};
         bool hasMatchNone{rawVMI.RequiredTraits.test(unsigned(matchNoneTrait))};
@@ -2677,7 +2752,7 @@ std::optional<MetadirectiveCandidateSet> BuildMetadirectiveCandidateSet(
 
           llvm::omp::VariantMatchInfo conditionTrueVMI{staticVMI};
           conditionTrueVMI.addTrait(
-              llvm::omp::TraitProperty::user_condition_true, conditionSource,
+              llvm::omp::TraitProperty::user_condition_true, conditionIdentity,
               conditionScorePtr);
           if (!llvm::omp::isVariantApplicableInContext(
                   conditionTrueVMI, matchContext)) {
@@ -2690,7 +2765,7 @@ std::optional<MetadirectiveCandidateSet> BuildMetadirectiveCandidateSet(
               rankingVMI.addTrait(hasMatchNone
                       ? llvm::omp::TraitProperty::user_condition_false
                       : llvm::omp::TraitProperty::user_condition_true,
-                  conditionSource, conditionScorePtr);
+                  conditionIdentity, conditionScorePtr);
             };
 
         if (hasMatchAny && isStaticVMIApplicable) {
@@ -2701,7 +2776,7 @@ std::optional<MetadirectiveCandidateSet> BuildMetadirectiveCandidateSet(
           addConditionTraitForRanking(conditionTrueVMI);
           llvm::omp::VariantMatchInfo conditionFalseVMI{staticVMI};
           conditionFalseVMI.addTrait(
-              llvm::omp::TraitProperty::user_condition_false, conditionSource,
+              llvm::omp::TraitProperty::user_condition_false, conditionIdentity,
               conditionScorePtr);
           result.candidates.push_back({spec, std::move(conditionTrueVMI),
               isExplicit, dynamicCondition});
diff --git a/flang/test/Lower/OpenMP/declare-variant-condition-identity.f90 b/flang/test/Lower/OpenMP/declare-variant-condition-identity.f90
new file mode 100644
index 0000000000000..93c77029df133
--- /dev/null
+++ b/flang/test/Lower/OpenMP/declare-variant-condition-identity.f90
@@ -0,0 +1,198 @@
+! RUN: split-file %s %t
+! RUN: cd %t && %flang_fc1 -fopenmp -fopenmp-version=52 -fsyntax-only settings.f90
+! RUN: cd %t && %flang_fc1 -fopenmp -fopenmp-version=52 -emit-hlfir variants.f90 -o - | FileCheck %s --check-prefix=LOCAL
+! RUN: cd %t && %flang_fc1 -fopenmp -fopenmp-version=52 -emit-hlfir use.f90 -o - | FileCheck %s --check-prefix=IMPORTED
+! RUN: cd %t && %flang_fc1 -fopenmp -fopenmp-version=52 -emit-fir use.f90 -o - | FileCheck %s --check-prefix=IMPORTED
+
+! Identical conditions make the CPU selector a strict superset, zeroing the
+! high-scoring variant. Distinct expressions and declarations must retain the
+! high score even when they fold to the same value.
+! LOCAL-LABEL: func.func @_QMconditionsPlocal_calls(
+! LOCAL: fir.call @_QMconditionsPlow()
+! LOCAL: fir.call @_QMconditionsPlow()
+! LOCAL: fir.call @_QMconditionsPlow()
+! LOCAL: fir.call @_QMconditionsPhigh()
+! LOCAL: fir.call @_QMconditionsPhigh()
+! LOCAL: fir.call @_QMconditionsPhigh()
+! LOCAL: fir.call @_QMconditionsPhigh()
+! LOCAL: return
+
+! Conditions referring to USE-associated names are rebuilt in the importing
+! compilation, using the loaded symbols rather than serialized pointers.
+! IMPORTED-LABEL: func.func @_QPimported_conditions(
+! IMPORTED: omp.taskwait
+! IMPORTED: omp.taskwait
+! IMPORTED: omp.taskyield
+! IMPORTED: return
+
+! Same runtime condition: the low-scoring strict superset wins when true.
+! IMPORTED-LABEL: func.func @_QPruntime_same(
+! IMPORTED: fir.if
+! IMPORTED: omp.taskwait
+! IMPORTED: } else {
+! IMPORTED: return
+
+! Different variables must not be merged even when their values might agree.
+! IMPORTED-LABEL: func.func @_QPruntime_distinct(
+! IMPORTED: fir.if
+! IMPORTED: omp.taskyield
+! IMPORTED: } else {
+! IMPORTED: fir.if
+! IMPORTED: omp.taskwait
+! IMPORTED: return
+
+! False conditions retain the same identity under match_none.
+! IMPORTED-LABEL: func.func @_QPfalse_conditions(
+! IMPORTED-NOT: omp.taskyield
+! IMPORTED: omp.taskwait
+! IMPORTED-NOT: omp.taskyield
+! IMPORTED: return
+
+! Both runtime outcomes under match_any retain condition identity, even when
+! the implementation selector makes each candidate applicable independently.
+! IMPORTED-LABEL: func.func @_QPruntime_any(
+! IMPORTED: fir.if
+! IMPORTED: omp.taskwait
+! IMPORTED: } else {
+! IMPORTED: omp.taskwait
+! IMPORTED: return
+
+!--- settings.f90
+module settings
+  logical, parameter :: on = .true., other = .true.
+  logical :: flag, other_flag
+end module
+
+!--- variants.f90
+module conditions
+  use settings, only: on, on2 => on, other
+contains
+  integer function high()
+    high = 100
+  end function
+  integer function low()
+    low = 1
+  end function
+
+  integer function parentheses()
+    !$omp declare variant(high) match(user={condition(.true.)}, &
+    !$omp& implementation={vendor(score(100): llvm)})
+    !$omp declare variant(low) match(user={condition(((.true.)))}, &
+    !$omp& implementation={vendor(score(1): llvm)}, device={kind(cpu)})
+    parentheses = 0
+  end function
+
+  integer function aliases()
+    !$omp declare variant(high) match(user={condition(on)}, &
+    !$omp& implementation={vendor(score(100): llvm)})
+    !$omp declare variant(low) match(user={condition(on2)}, &
+    !$omp& implementation={vendor(score(1): llvm)}, device={kind(cpu)})
+    aliases = 0
+  end function
+
+  integer function compound_aliases()
+    !$omp declare variant(high) match(user={condition(on .and. .true.)}, &
+    !$omp& implementation={vendor(score(100): llvm)})
+    !$omp declare variant(low) match(user={condition((on2.and..true.))}, &
+    !$omp& implementation={vendor(score(1): llvm)}, device={kind(cpu)})
+    compound_aliases = 0
+  end function
+
+  integer function distinct()
+    !$omp declare variant(high) match(user={condition(on)}, &
+    !$omp& implementation={vendor(score(100): llvm)})
+    !$omp declare variant(low) match(user={condition(other)}, &
+    !$omp& implementation={vendor(score(1): llvm)}, device={kind(cpu)})
+    distinct = 0
+  end function
+
+  integer function literal_and_name()
+    !$omp declare variant(high) match(user={condition(.true.)}, &
+    !$omp& implementation={vendor(score(100): llvm)})
+    !$omp declare variant(low) match(user={condition(on)}, &
+    !$omp& implementation={vendor(score(1): llvm)}, device={kind(cpu)})
+    literal_and_name = 0
+  end function
+
+  integer function ordered_expression()
+    !$omp declare variant(high) match(user={condition(on .and. other)}, &
+    !$omp& implementation={vendor(score(100): llvm)})
+    !$omp declare variant(low) match(user={condition(other .and. on)}, &
+    !$omp& implementation={vendor(score(1): llvm)}, device={kind(cpu)})
+    ordered_expression = 0
+  end function
+
+  integer function character_literals()
+    !$omp declare variant(high) match(user={condition('a b' == 'a b')}, &
+    !$omp& implementation={vendor(score(100): llvm)})
+    !$omp declare variant(low) match(user={condition('ab' == 'ab')}, &
+    !$omp& implementation={vendor(score(1): llvm)}, device={kind(cpu)})
+    character_literals = 0
+  end function
+
+  subroutine local_calls(a)
+    integer :: a(7)
+    a(1) = parentheses()
+    a(2) = aliases()
+    a(3) = compound_aliases()
+    a(4) = distinct()
+    a(5) = literal_and_name()
+    a(6) = ordered_expression()
+    a(7) = character_literals()
+  end subroutine
+end module
+
+!--- use.f90
+subroutine imported_conditions()
+  use settings, only: on, alias => on, other
+  !$omp metadirective &
+  !$omp& when(user={condition(on)}, &
+  !$omp& implementation={vendor(score(100): llvm)}: taskyield) &
+  !$omp& when(user={condition((alias))}, device={kind(cpu)}, &
+  !$omp& implementation={vendor(score(1): llvm)}: taskwait)
+  !$omp metadirective &
+  !$omp& when(user={condition(.true.)}, &
+  !$omp& implementation={vendor(score(100): llvm)}: taskyield) &
+  !$omp& when(user={condition((.true.))}, device={kind(cpu)}, &
+  !$omp& implementation={vendor(score(1): llvm)}: taskwait)
+  !$omp metadirective &
+  !$omp& when(user={condition(on)}, &
+  !$omp& implementation={vendor(score(100): llvm)}: taskyield) &
+  !$omp& when(user={condition(other)}, device={kind(cpu)}, &
+  !$omp& implementation={vendor(score(1): llvm)}: taskwait)
+end subroutine
+
+subroutine runtime_same()
+  use settings, only: flag, alias => flag
+  !$omp metadirective &
+  !$omp& when(user={condition(flag)}, &
+  !$omp& implementation={vendor(score(100): llvm)}: taskyield) &
+  !$omp& when(user={condition((alias))}, device={kind(cpu)}, &
+  !$omp& implementation={vendor(score(1): llvm)}: taskwait)
+end subroutine
+
+subroutine runtime_distinct()
+  use settings, only: flag, other_flag
+  !$omp metadirective &
+  !$omp& when(user={condition(flag)}, &
+  !$omp& implementation={vendor(score(100): llvm)}: taskyield) &
+  !$omp& when(user={condition(other_flag)}, device={kind(cpu)}, &
+  !$omp& implementation={vendor(score(1): llvm)}: taskwait)
+end subroutine
+
+subroutine false_conditions()
+  !$omp metadirective &
+  !$omp& when(user={condition(.false.)}, &
+  !$omp& implementation={extension(match_none), vendor(score(100): gnu)}: taskyield) &
+  !$omp& when(user={condition((.false.))}, device={kind(gpu)}, &
+  !$omp& implementation={extension(match_none), vendor(score(1): gnu)}: taskwait)
+end subroutine
+
+subroutine runtime_any()
+  use settings, only: flag, alias => flag
+  !$omp metadirective &
+  !$omp& when(user={condition(flag)}, &
+  !$omp& implementation={extension(match_any), vendor(score(100): llvm)}: taskyield) &
+  !$omp& when(user={condition((alias))}, device={kind(cpu)}, &
+  !$omp& implementation={extension(match_any), vendor(score(1): llvm)}: taskwait)
+end subroutine

>From a5f78eebac6fef0e141e29913ac6d93b68ba738c Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Sat, 26 Sep 2026 01:53:44 -0500
Subject: [PATCH 08/23] Fix DISTRIBUTE context for SIMD clauses

Enter DISTRIBUTE while evaluating SIMD clauses, then leave it before loop bounds. Cover clause selection, bound exclusion, and restoration.
---
 flang/lib/Lower/OpenMP/OpenMP.cpp             |  9 ++++-
 .../OpenMP/declare-variant-loop-bounds.f90    | 39 +++++++++++++++++++
 2 files changed, 46 insertions(+), 2 deletions(-)

diff --git a/flang/lib/Lower/OpenMP/OpenMP.cpp b/flang/lib/Lower/OpenMP/OpenMP.cpp
index acb48dc54cf64..4fbee2ebf1d71 100644
--- a/flang/lib/Lower/OpenMP/OpenMP.cpp
+++ b/flang/lib/Lower/OpenMP/OpenMP.cpp
@@ -5453,8 +5453,13 @@ static mlir::omp::DistributeOp genCompositeDistributeSimd(
 
   mlir::omp::SimdOperands simdClauseOps;
   llvm::SmallVector<Object> simdReductionObjects;
-  genSimdClauses(converter, semaCtx, simdItem->clauses, loc, simdClauseOps,
-                 simdReductionObjects);
+  {
+    // SIMD clauses see DISTRIBUTE, but loop bounds are evaluated before it.
+    mlir::SaveStateStack<OpenMPContextFrame> distributeContext{
+        converter.getStateStack(), eval, llvm::omp::Directive::OMPD_distribute};
+    genSimdClauses(converter, semaCtx, simdItem->clauses, loc, simdClauseOps,
+                   simdReductionObjects);
+  }
 
   DataSharingProcessor distributeItemDSP(
       converter, semaCtx, distributeItem->clauses, eval,
diff --git a/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90 b/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
index 85a301bdfe3a8..c543c6752a16e 100644
--- a/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
+++ b/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
@@ -241,4 +241,43 @@ subroutine nested_simd_clauses(n, a)
       end do
     end do
   end subroutine
+  ! SIMD clauses see TEAMS and DISTRIBUTE (CPU scores 5). The bound and
+  ! post-loop call see only TEAMS (CPU scores 3, losing to vendor score 4).
+  ! CHECK-LABEL: func.func @_QMloop_contextPdistribute_simd_clauses(
+  ! CHECK: omp.teams
+  ! CHECK: fir.call @_QMloop_contextPcpu_count(
+  ! CHECK: fir.call @_QMloop_contextPscored_count(
+  ! CHECK: omp.distribute
+  ! CHECK: omp.simd
+  ! CHECK: fir.call @_QMloop_contextPscored_count(
+  ! CHECK: return
+  subroutine distribute_simd_clauses(n, a)
+    integer :: n, a(n), i
+    !$omp teams
+    !$omp distribute simd if(simd: thread_count(n) > 0)
+    do i = 1, thread_count(n)
+      a(i) = i
+    end do
+    i = thread_count(n)
+    !$omp end teams
+  end subroutine
+
+  ! CHECK-LABEL: func.func @_QMloop_contextPnested_distribute_simd_clauses(
+  ! CHECK: omp.teams
+  ! CHECK: omp.distribute
+  ! CHECK: fir.call @_QMloop_contextPcpu_count(
+  ! CHECK: omp.simd
+  ! CHECK: return
+  subroutine nested_distribute_simd_clauses(n, a)
+    integer :: n, a(n, n), i, j
+    !$omp teams
+    !$omp distribute
+    do i = 1, n
+      !$omp simd if(simd: thread_count(n) > 0)
+      do j = 1, n
+        a(i, j) = i + j
+      end do
+    end do
+    !$omp end teams
+  end subroutine
 end module

>From 40f976c5cabfcf7bace16425ccc92f20f79446d7 Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Sat, 26 Sep 2026 01:53:54 -0500
Subject: [PATCH 09/23] Normalize Flang condition expression spelling

Profile original expression structure and declaration identity, normalizing intrinsic operators and literal values and kinds. Preserve signed integer boundaries and cover alternate spellings and nondefault kinds.
---
 flang/lib/Semantics/openmp-utils.cpp          | 138 ++++++++++-------
 .../declare-variant-condition-identity.f90    | 141 +++++++++++++++++-
 2 files changed, 220 insertions(+), 59 deletions(-)

diff --git a/flang/lib/Semantics/openmp-utils.cpp b/flang/lib/Semantics/openmp-utils.cpp
index f08f9abfcdb98..99f11cc13456f 100644
--- a/flang/lib/Semantics/openmp-utils.cpp
+++ b/flang/lib/Semantics/openmp-utils.cpp
@@ -2506,75 +2506,97 @@ void AppendDirectiveContextTraits(llvm::omp::Directive directive,
 }
 
 namespace {
-struct ConditionNames {
-  template <typename A> bool Pre(const A &) { return true; }
-  template <typename A> void Post(const A &) {}
+// Profile the original parse tree: folding a whole expression would lose
+// declaration identity. Parse-tree operators already unify alternate spellings;
+// analyze only literals to normalize their values and effective kinds.
+struct ConditionIdentity {
+  SemanticsContext &context;
+  llvm::FoldingSetNodeID id;
+
+  template <typename A> void AddNode() {
+    // Type tags and symbol addresses are local to this compilation. Profiles
+    // are rebuilt when importing symbols, never serialized as pointers.
+    static char tag;
+    id.AddPointer(&tag);
+  }
+  template <typename A> bool Pre(const A &x) {
+    AddNode<A>();
+    if constexpr (std::is_enum_v<A> || std::is_integral_v<A>)
+      id.AddInteger(static_cast<uint64_t>(x));
+    else if constexpr (std::is_same_v<A, std::string>)
+      id.AddString(x);
+    return true;
+  }
+  template <typename A> void Post(const A &) { id.AddInteger(0); }
+  // CharBlocks in expression nodes carry source locations, not structure.
+  bool Pre(const parser::CharBlock &) { return false; }
   bool Pre(const parser::Name &name) {
+    AddNode<parser::Name>();
     if (name.symbol)
-      names.push_back(&name);
+      id.AddPointer(&name.symbol->GetUltimate());
+    else
+      id.AddString(name.source.ToString());
+    return false;
+  }
+  bool Pre(const parser::SignedIntLiteralConstant &literal) {
+    AddNode<parser::SignedIntLiteralConstant>();
+    auto value{evaluate::ExpressionAnalyzer{context}.Analyze(literal)};
+    CHECK(value);
+    id.AddString(value->AsFortran());
     return false;
   }
-  llvm::SmallVector<const parser::Name *> names;
+  bool Pre(const parser::SignedRealLiteralConstant &literal) {
+    AddNode<parser::SignedRealLiteralConstant>();
+    auto value{evaluate::ExpressionAnalyzer{context}.Analyze(literal)};
+    CHECK(value);
+    id.AddString(value->AsFortran());
+    return false;
+  }
+  bool Pre(const parser::Expr &expr) {
+    if (const auto *parens{std::get_if<parser::Expr::Parentheses>(&expr.u)}) {
+      parser::Walk(parens->v.value(), *this);
+      return false;
+    }
+    AddNode<parser::Expr>();
+    if (const auto *negate{std::get_if<parser::Expr::Negate>(&expr.u)}) {
+      if (const auto *literal{
+              std::get_if<parser::LiteralConstant>(&negate->v.value().u)}) {
+        if (std::holds_alternative<parser::IntLiteralConstant>(literal->u)) {
+          // The magnitude of the most negative integer is not representable
+          // in its kind. Analyze the signed literal together, as semantics
+          // does, rather than reanalyzing its positive magnitude.
+          auto value{evaluate::ExpressionAnalyzer{context}.Analyze(expr)};
+          CHECK(value);
+          AddNode<parser::Expr::Negate>();
+          id.AddString(value->AsFortran());
+          return false;
+        }
+      }
+    }
+    if (const auto *literal{std::get_if<parser::LiteralConstant>(&expr.u)}) {
+      // Complex literal parts may name declarations: retain their structure.
+      if (!std::holds_alternative<parser::ComplexLiteralConstant>(literal->u)) {
+        if (auto value{evaluate::ExpressionAnalyzer{context}.Analyze(expr)}) {
+          AddNode<parser::LiteralConstant>();
+          id.AddString(value->AsFortran());
+          return false;
+        }
+      }
+    }
+    return true;
+  }
 };
 
-// Profile the original expression, not its folded value. In particular, two
-// named constants with the same value still denote different declarations.
-// Retain the cooked expression spelling, except for outer parentheses, blanks
-// outside character literals, and names resolved to their ultimate symbols.
 llvm::StringRef GetConditionIdentity(
     const parser::ScalarExpr &condition, SemanticsContext &context) {
   const auto *expr{parser::Unwrap<parser::Expr>(condition)};
   CHECK(expr);
-  while (
-      const auto *parentheses{std::get_if<parser::Expr::Parentheses>(&expr->u)})
-    expr = &parentheses->v.value();
-
-  ConditionNames visitor;
-  parser::Walk(*expr, visitor);
-  llvm::sort(visitor.names, [](const auto *a, const auto *b) {
-    return a->source.begin() < b->source.begin();
-  });
-
-  llvm::FoldingSetNodeID id;
-  auto addText = [&](parser::CharBlock source) {
-    std::string text;
-    char quote{0};
-    for (const char *p{source.begin()}; p != source.end(); ++p) {
-      char ch{*p};
-      if (quote) {
-        text += ch;
-        if (ch == quote)
-          quote = 0;
-        else if (ch == '\\' &&
-            context.IsEnabled(common::LanguageFeature::BackslashEscapes) &&
-            p + 1 != source.end())
-          text += *++p;
-      } else if (ch == '\'' || ch == '"') {
-        quote = ch;
-        text += ch;
-      } else if (!llvm::isSpace(ch)) {
-        text += ch;
-      }
-    }
-    id.AddString(text);
-  };
-
-  const char *next{expr->source.begin()};
-  for (const parser::Name *name : visitor.names) {
-    if (!expr->source.Contains(name->source) || name->source.begin() < next)
-      continue;
-    addText({next, name->source.begin()});
-    id.AddPointer(&name->symbol->GetUltimate());
-    next = name->source.end();
-  }
-  addText({next, expr->source.end()});
-
-  // Intern the full profile in context-owned storage. Recompute it after
-  // reading a module; symbol pointers are local to this compilation.
-  llvm::FoldingSetNodeIDRef profile{id.getRef()};
+  ConditionIdentity profile{context, {}};
+  parser::Walk(*expr, profile);
+  llvm::FoldingSetNodeIDRef data{profile.id.getRef()};
   SourceName saved{context.SaveTempName(
-      std::string{reinterpret_cast<const char *>(profile.data()),
-          profile.size() * sizeof(unsigned)})};
+      std::string{reinterpret_cast<const char *>(data.data()),
+          data.size() * sizeof(unsigned)})};
   return {saved.begin(), saved.size()};
 }
 } // namespace
diff --git a/flang/test/Lower/OpenMP/declare-variant-condition-identity.f90 b/flang/test/Lower/OpenMP/declare-variant-condition-identity.f90
index 93c77029df133..dd5ac38fe3415 100644
--- a/flang/test/Lower/OpenMP/declare-variant-condition-identity.f90
+++ b/flang/test/Lower/OpenMP/declare-variant-condition-identity.f90
@@ -4,6 +4,9 @@
 ! RUN: cd %t && %flang_fc1 -fopenmp -fopenmp-version=52 -emit-hlfir use.f90 -o - | FileCheck %s --check-prefix=IMPORTED
 ! RUN: cd %t && %flang_fc1 -fopenmp -fopenmp-version=52 -emit-fir use.f90 -o - | FileCheck %s --check-prefix=IMPORTED
 
+! RUN: cd %t && %flang_fc1 -fopenmp -fopenmp-version=52 -emit-hlfir kinds.f90 -o - | FileCheck %s --check-prefix=DEFAULT4
+! RUN: cd %t && %flang_fc1 -fopenmp -fopenmp-version=52 -fdefault-integer-8 -emit-hlfir kinds.f90 -o - | FileCheck %s --check-prefix=DEFAULT8
+
 ! Identical conditions make the CPU selector a strict superset, zeroing the
 ! high-scoring variant. Distinct expressions and declarations must retain the
 ! high score even when they fold to the same value.
@@ -15,6 +18,16 @@
 ! LOCAL: fir.call @_QMconditionsPhigh()
 ! LOCAL: fir.call @_QMconditionsPhigh()
 ! LOCAL: fir.call @_QMconditionsPhigh()
+! LOCAL: fir.call @_QMconditionsPlow()
+! LOCAL: fir.call @_QMconditionsPlow()
+! LOCAL: fir.call @_QMconditionsPlow()
+! LOCAL: fir.call @_QMconditionsPlow()
+! LOCAL: fir.call @_QMconditionsPlow()
+! LOCAL: fir.call @_QMconditionsPlow()
+! LOCAL: fir.call @_QMconditionsPlow()
+! LOCAL: fir.call @_QMconditionsPhigh()
+! LOCAL: fir.call @_QMconditionsPhigh()
+! LOCAL: fir.call @_QMconditionsPlow()
 ! LOCAL: return
 
 ! Conditions referring to USE-associated names are rebuilt in the importing
@@ -130,8 +143,91 @@ integer function character_literals()
     character_literals = 0
   end function
 
+  integer function spelling_eq()
+    !$omp declare variant(high) match(user={condition(1 .eq. 1)}, &
+    !$omp& implementation={vendor(score(100): llvm)})
+    !$omp declare variant(low) match(user={condition(1 == 1)}, &
+    !$omp& implementation={vendor(score(1): llvm)}, device={kind(cpu)})
+    spelling_eq = 0
+  end function
+
+  integer function spelling_ne()
+    !$omp declare variant(high) match(user={condition(1 .ne. 2)}, &
+    !$omp& implementation={vendor(score(100): llvm)})
+    !$omp declare variant(low) match(user={condition(1 /= 2)}, &
+    !$omp& implementation={vendor(score(1): llvm)}, device={kind(cpu)})
+    spelling_ne = 0
+  end function
+
+  integer function spelling_lt()
+    !$omp declare variant(high) match(user={condition(1 .lt. 2)}, &
+    !$omp& implementation={vendor(score(100): llvm)})
+    !$omp declare variant(low) match(user={condition(1 < 2)}, &
+    !$omp& implementation={vendor(score(1): llvm)}, device={kind(cpu)})
+    spelling_lt = 0
+  end function
+
+  integer function spelling_le()
+    !$omp declare variant(high) match(user={condition(1 .le. 1)}, &
+    !$omp& implementation={vendor(score(100): llvm)})
+    !$omp declare variant(low) match(user={condition(1 <= 1)}, &
+    !$omp& implementation={vendor(score(1): llvm)}, device={kind(cpu)})
+    spelling_le = 0
+  end function
+
+  integer function spelling_gt()
+    !$omp declare variant(high) match(user={condition(2 .gt. 1)}, &
+    !$omp& implementation={vendor(score(100): llvm)})
+    !$omp declare variant(low) match(user={condition(2 > 1)}, &
+    !$omp& implementation={vendor(score(1): llvm)}, device={kind(cpu)})
+    spelling_gt = 0
+  end function
+
+  integer function spelling_ge()
+    !$omp declare variant(high) match(user={condition(1 .ge. 1)}, &
+    !$omp& implementation={vendor(score(100): llvm)})
+    !$omp declare variant(low) match(user={condition(1 >= 1)}, &
+    !$omp& implementation={vendor(score(1): llvm)}, device={kind(cpu)})
+    spelling_ge = 0
+  end function
+
+  integer function spelling_integer_kind()
+    !$omp declare variant(high) match(user={condition(1 == 1)}, &
+    !$omp& implementation={vendor(score(100): llvm)})
+    !$omp declare variant(low) match(user={condition(1_4 == 1_4)}, &
+    !$omp& implementation={vendor(score(1): llvm)}, device={kind(cpu)})
+    spelling_integer_kind = 0
+  end function
+
+  integer function spelling_distinct_kind()
+    !$omp declare variant(high) match(user={condition(1_4 == 1_4)}, &
+    !$omp& implementation={vendor(score(100): llvm)})
+    !$omp declare variant(low) match(user={condition(1_8 == 1_8)}, &
+    !$omp& implementation={vendor(score(1): llvm)}, device={kind(cpu)})
+    spelling_distinct_kind = 0
+  end function
+
+  integer function spelling_complex_values()
+    !$omp declare variant(high) match(user={condition((1,2) == (1,2))}, &
+    !$omp& implementation={vendor(score(100): llvm)})
+    !$omp declare variant(low) match(user={condition((1,3) == (1,3))}, &
+    !$omp& implementation={vendor(score(1): llvm)}, device={kind(cpu)})
+    spelling_complex_values = 0
+  end function
+
+  ! The positive magnitude is out of range, but the signed literal is valid.
+  integer function spelling_integer_min()
+    !$omp declare variant(high) &
+    !$omp& match(user={condition(-2147483648 == -2147483648)}, &
+    !$omp& implementation={vendor(score(100): llvm)})
+    !$omp declare variant(low) &
+    !$omp& match(user={condition(-2147483648_4 == -2147483648_4)}, &
+    !$omp& implementation={vendor(score(1): llvm)}, device={kind(cpu)})
+    spelling_integer_min = 0
+  end function
+
   subroutine local_calls(a)
-    integer :: a(7)
+    integer :: a(17)
     a(1) = parentheses()
     a(2) = aliases()
     a(3) = compound_aliases()
@@ -139,6 +235,16 @@ subroutine local_calls(a)
     a(5) = literal_and_name()
     a(6) = ordered_expression()
     a(7) = character_literals()
+    a(8) = spelling_eq()
+    a(9) = spelling_ne()
+    a(10) = spelling_lt()
+    a(11) = spelling_le()
+    a(12) = spelling_gt()
+    a(13) = spelling_ge()
+    a(14) = spelling_integer_kind()
+    a(15) = spelling_distinct_kind()
+    a(16) = spelling_complex_values()
+    a(17) = spelling_integer_min()
   end subroutine
 end module
 
@@ -196,3 +302,36 @@ subroutine runtime_any()
   !$omp& when(user={condition((alias))}, device={kind(cpu)}, &
   !$omp& implementation={extension(match_any), vendor(score(1): llvm)}: taskwait)
 end subroutine
+
+! Imported-symbol runtime expressions also normalize relational operators.
+! IMPORTED-LABEL: func.func @_QPruntime_operators(
+! IMPORTED-NOT: omp.taskyield
+! IMPORTED: fir.if
+! IMPORTED: omp.taskwait
+! IMPORTED-NOT: omp.taskyield
+! IMPORTED: return
+subroutine runtime_operators(n)
+  integer :: n
+  !$omp metadirective &
+  !$omp& when(user={condition(n .eq. 1)}, &
+  !$omp& implementation={vendor(score(100): llvm)}: taskyield) &
+  !$omp& when(user={condition(n == 1)}, device={kind(cpu)}, &
+  !$omp& implementation={vendor(score(1): llvm)}: taskwait)
+end subroutine
+
+!--- kinds.f90
+! DEFAULT4-LABEL: func.func @_QPdefault_kind(
+! DEFAULT4-NOT: omp.taskyield
+! DEFAULT4: omp.taskwait
+! DEFAULT4: return
+! DEFAULT8-LABEL: func.func @_QPdefault_kind(
+! DEFAULT8-NOT: omp.taskwait
+! DEFAULT8: omp.taskyield
+! DEFAULT8: return
+subroutine default_kind()
+  !$omp metadirective &
+  !$omp& when(user={condition(1 == 1)}, &
+  !$omp& implementation={vendor(score(100): llvm)}: taskyield) &
+  !$omp& when(user={condition(1_4 == 1_4)}, device={kind(cpu)}, &
+  !$omp& implementation={vendor(score(1): llvm)}: taskwait)
+end subroutine

>From 5d478752e7d9ac40650ba7e081b06ebc8d841dd8 Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Sat, 26 Sep 2026 01:54:07 -0500
Subject: [PATCH 10/23] Preserve metadirective candidate order

Compute subset-adjusted scores once and retain the ordering across runtime guard failures. Share ranks between lowering and reachability so failed supersets cannot restore a subset score.
---
 flang/include/flang/Semantics/openmp-utils.h  | 12 +--
 flang/lib/Lower/OpenMP/OpenMP.cpp             |  8 +-
 flang/lib/Semantics/openmp-utils.cpp          | 78 ++++++++-----------
 .../declare-variant-condition-identity.f90    |  8 +-
 .../test/Lower/OpenMP/metadirective-user.f90  | 35 +++++++++
 .../include/llvm/Frontend/OpenMP/OMPContext.h |  7 ++
 llvm/lib/Frontend/OpenMP/OMPContext.cpp       | 11 ++-
 llvm/unittests/Frontend/OpenMPContextTest.cpp | 19 +++++
 8 files changed, 119 insertions(+), 59 deletions(-)

diff --git a/flang/include/flang/Semantics/openmp-utils.h b/flang/include/flang/Semantics/openmp-utils.h
index 326d97714f64f..ff31591eb71b1 100644
--- a/flang/include/flang/Semantics/openmp-utils.h
+++ b/flang/include/flang/Semantics/openmp-utils.h
@@ -284,6 +284,8 @@ struct MetadirectiveCandidate {
   bool isExplicit{false};
   std::optional<DynamicUserCondition> dynamicCondition;
   bool conditionShouldBeTrue{true};
+  /// Position in the original candidate ordering, including subset scores.
+  unsigned rank{0};
 };
 
 struct MetadirectiveCandidateSet {
@@ -300,10 +302,10 @@ std::optional<MetadirectiveCandidateSet> BuildMetadirectiveCandidateSet(
     const parser::OmpClauseList &clauses, SemanticsContext &context,
     const OmpVariantMatchContext &matchContext);
 
+/// Select the first remaining candidate in the original ranking.
 std::optional<unsigned> SelectBestMetadirectiveCandidate(
     llvm::ArrayRef<unsigned> candidateIndices,
-    llvm::ArrayRef<MetadirectiveCandidate> candidates,
-    const OmpVariantMatchContext &matchContext);
+    llvm::ArrayRef<MetadirectiveCandidate> candidates);
 
 /// Return true when repeated evaluation of \p condition cannot call a
 /// procedure or observe asynchronously changing state.
@@ -319,13 +321,13 @@ bool AreSameRepeatableMetadirectiveCondition(const parser::ScalarExpr &left,
 llvm::SmallVector<unsigned, 4> GetMetadirectiveElsePathCandidates(
     unsigned selectedIndex, llvm::ArrayRef<unsigned> candidateIndices,
     llvm::ArrayRef<MetadirectiveCandidate> candidates,
-    const OmpVariantMatchContext &matchContext, SemanticsContext &context);
+    SemanticsContext &context);
 
 /// Return every replacement that can be selected, retaining lower-ranked
 /// candidates after a dynamic condition. Null represents NOTHING.
 llvm::SmallVector<const parser::OmpDirectiveSpecification *, 4>
-GetReachableMetadirectiveVariants(const MetadirectiveCandidateSet &candidateSet,
-    const OmpVariantMatchContext &matchContext, SemanticsContext &context);
+GetReachableMetadirectiveVariants(
+    const MetadirectiveCandidateSet &candidateSet, SemanticsContext &context);
 
 /// True if a variant guarded by \p selector may be selected in the current
 /// compilation context.
diff --git a/flang/lib/Lower/OpenMP/OpenMP.cpp b/flang/lib/Lower/OpenMP/OpenMP.cpp
index 4fbee2ebf1d71..781dafa9f00e5 100644
--- a/flang/lib/Lower/OpenMP/OpenMP.cpp
+++ b/flang/lib/Lower/OpenMP/OpenMP.cpp
@@ -7511,7 +7511,7 @@ static void genMetadirective(lower::AbstractConverter &converter,
 
   llvm::SmallVector<const parser::OmpDirectiveSpecification *, 4>
       reachableVariantSpecs = semantics::omp::GetReachableMetadirectiveVariants(
-          *candidateSet, ompCtx, semaCtx);
+          *candidateSet, semaCtx);
 
   bool hasLoopAssociatedCandidate =
       llvm::any_of(reachableVariantSpecs, [](const auto *spec) {
@@ -7748,7 +7748,7 @@ static void genMetadirective(lower::AbstractConverter &converter,
   while (!remainingCandidates.empty()) {
     std::optional<unsigned> selected =
         semantics::omp::SelectBestMetadirectiveCandidate(remainingCandidates,
-                                                         candidates, ompCtx);
+                                                         candidates);
     if (!selected) {
       genVariant(fallback);
       return;
@@ -7763,7 +7763,7 @@ static void genMetadirective(lower::AbstractConverter &converter,
 
     llvm::SmallVector<unsigned, 4> elsePathCandidates =
         semantics::omp::GetMetadirectiveElsePathCandidates(
-            *selected, remainingCandidates, candidates, ompCtx, semaCtx);
+            *selected, remainingCandidates, candidates, semaCtx);
 
     // match_any may create a guarded condition-true candidate and an unguarded
     // static candidate for the same directive. If the else path picks the
@@ -7773,7 +7773,7 @@ static void genMetadirective(lower::AbstractConverter &converter,
     //   else barrier
     if (std::optional<unsigned> selectedInElse =
             semantics::omp::SelectBestMetadirectiveCandidate(
-                elsePathCandidates, candidates, ompCtx)) {
+                elsePathCandidates, candidates)) {
       const semantics::omp::MetadirectiveCandidate &candidateInElse =
           candidates[*selectedInElse];
       if (!candidateInElse.dynamicCondition &&
diff --git a/flang/lib/Semantics/openmp-utils.cpp b/flang/lib/Semantics/openmp-utils.cpp
index 99f11cc13456f..67b8ea979708f 100644
--- a/flang/lib/Semantics/openmp-utils.cpp
+++ b/flang/lib/Semantics/openmp-utils.cpp
@@ -2830,48 +2830,36 @@ std::optional<MetadirectiveCandidateSet> BuildMetadirectiveCandidateSet(
       result.fallback = getFallbackVariant(defaultVariantClause->v.v.value());
     }
   }
+  // Rank against the complete set once. Removing a failed runtime guard must
+  // not restore the raw score of a selector that was its strict subset.
+  llvm::SmallVector<llvm::omp::VariantMatchInfo, 4> vmis;
+  llvm::SmallVector<unsigned, 4> order;
+  for (const auto &[index, candidate] : llvm::enumerate(result.candidates)) {
+    vmis.push_back(candidate.vmi);
+    order.push_back(index);
+  }
+  auto scores{llvm::omp::getVariantMatchScores(vmis, matchContext)};
+  llvm::stable_sort(order, [&](unsigned a, unsigned b) {
+    CHECK(scores[a] && scores[b]);
+    const auto &left{*scores[a]}, &right{*scores[b]};
+    unsigned width{std::max(left.getBitWidth(), right.getBitWidth())};
+    if (left.zextOrTrunc(width) != right.zextOrTrunc(width))
+      return left.zextOrTrunc(width).ugt(right.zextOrTrunc(width));
+    return result.candidates[a].isExplicit && !result.candidates[b].isExplicit;
+  });
+  for (auto [rank, index] : llvm::enumerate(order))
+    result.candidates[index].rank = rank;
   return result;
 }
 
 std::optional<unsigned> SelectBestMetadirectiveCandidate(
     llvm::ArrayRef<unsigned> candidateIndices,
-    llvm::ArrayRef<MetadirectiveCandidate> candidates,
-    const OmpVariantMatchContext &matchContext) {
-  if (candidateIndices.empty()) {
-    return std::nullopt;
-  }
-  if (candidateIndices.size() == 1) {
-    return candidateIndices.front();
-  }
-
-  // The context scorer preserves input order for ties. Explicit replacements
-  // take precedence over an omitted directive's implicit NOTHING.
-  llvm::SmallVector<unsigned, 4> candidateOrder;
-  candidateOrder.reserve(candidateIndices.size());
-  for (unsigned index : candidateIndices) {
-    if (candidates[index].isExplicit) {
-      candidateOrder.push_back(index);
-    }
-  }
-  for (unsigned index : candidateIndices) {
-    if (!candidates[index].isExplicit) {
-      candidateOrder.push_back(index);
-    }
-  }
-
-  llvm::SmallVector<llvm::omp::VariantMatchInfo, 4> orderedVMIs;
-  orderedVMIs.reserve(candidateOrder.size());
-  for (unsigned index : candidateOrder) {
-    orderedVMIs.push_back(candidates[index].vmi);
-  }
-
-  int bestIndex{
-      llvm::omp::getBestVariantMatchForContext(orderedVMIs, matchContext)};
-  if (bestIndex < 0) {
+    llvm::ArrayRef<MetadirectiveCandidate> candidates) {
+  if (candidateIndices.empty())
     return std::nullopt;
-  }
-  CHECK(static_cast<std::size_t>(bestIndex) < candidateOrder.size());
-  return candidateOrder[bestIndex];
+  return *llvm::min_element(candidateIndices, [&](unsigned a, unsigned b) {
+    return candidates[a].rank < candidates[b].rank;
+  });
 }
 
 namespace {
@@ -2938,7 +2926,7 @@ bool AreSameRepeatableMetadirectiveCondition(const parser::ScalarExpr &left,
 llvm::SmallVector<unsigned, 4> GetMetadirectiveElsePathCandidates(
     unsigned selectedIndex, llvm::ArrayRef<unsigned> candidateIndices,
     llvm::ArrayRef<MetadirectiveCandidate> candidates,
-    const OmpVariantMatchContext &matchContext, SemanticsContext &context) {
+    SemanticsContext &context) {
   CHECK(selectedIndex < candidates.size());
   const MetadirectiveCandidate &selected{candidates[selectedIndex]};
   CHECK(selected.dynamicCondition);
@@ -2955,8 +2943,8 @@ llvm::SmallVector<unsigned, 4> GetMetadirectiveElsePathCandidates(
   // condition because it can change state before a lower-ranked occurrence is
   // evaluated.
   llvm::SmallVector<unsigned, 4> candidatesToInspect{result};
-  while (std::optional<unsigned> next{SelectBestMetadirectiveCandidate(
-      candidatesToInspect, candidates, matchContext)}) {
+  while (std::optional<unsigned> next{
+      SelectBestMetadirectiveCandidate(candidatesToInspect, candidates)}) {
     const MetadirectiveCandidate &candidate{candidates[*next]};
     if (!candidate.dynamicCondition ||
         !IsRepeatableMetadirectiveCondition(
@@ -2976,8 +2964,8 @@ llvm::SmallVector<unsigned, 4> GetMetadirectiveElsePathCandidates(
 }
 
 llvm::SmallVector<const parser::OmpDirectiveSpecification *, 4>
-GetReachableMetadirectiveVariants(const MetadirectiveCandidateSet &candidateSet,
-    const OmpVariantMatchContext &matchContext, SemanticsContext &context) {
+GetReachableMetadirectiveVariants(
+    const MetadirectiveCandidateSet &candidateSet, SemanticsContext &context) {
   llvm::SmallVector<unsigned, 4> candidates;
   candidates.reserve(candidateSet.candidates.size());
   for (unsigned index{0}; index < candidateSet.candidates.size(); ++index) {
@@ -2986,8 +2974,8 @@ GetReachableMetadirectiveVariants(const MetadirectiveCandidateSet &candidateSet,
 
   llvm::SmallVector<const parser::OmpDirectiveSpecification *, 4> reachable;
   while (true) {
-    std::optional<unsigned> selected{SelectBestMetadirectiveCandidate(
-        candidates, candidateSet.candidates, matchContext)};
+    std::optional<unsigned> selected{
+        SelectBestMetadirectiveCandidate(candidates, candidateSet.candidates)};
     if (!selected) {
       reachable.push_back(candidateSet.fallback);
       break;
@@ -3002,10 +2990,10 @@ GetReachableMetadirectiveVariants(const MetadirectiveCandidateSet &candidateSet,
     }
 
     candidates = GetMetadirectiveElsePathCandidates(
-        *selected, candidates, candidateSet.candidates, matchContext, context);
+        *selected, candidates, candidateSet.candidates, context);
 
     if (std::optional<unsigned> selectedInElse{SelectBestMetadirectiveCandidate(
-            candidates, candidateSet.candidates, matchContext)}) {
+            candidates, candidateSet.candidates)}) {
       const MetadirectiveCandidate &elseCandidate{
           candidateSet.candidates[*selectedInElse]};
       if (!elseCandidate.dynamicCondition &&
diff --git a/flang/test/Lower/OpenMP/declare-variant-condition-identity.f90 b/flang/test/Lower/OpenMP/declare-variant-condition-identity.f90
index dd5ac38fe3415..145002613f8d3 100644
--- a/flang/test/Lower/OpenMP/declare-variant-condition-identity.f90
+++ b/flang/test/Lower/OpenMP/declare-variant-condition-identity.f90
@@ -63,11 +63,13 @@
 
 ! Both runtime outcomes under match_any retain condition identity, even when
 ! the implementation selector makes each candidate applicable independently.
+! Both outcomes select TASKWAIT, so the runtime branch can be eliminated.
 ! IMPORTED-LABEL: func.func @_QPruntime_any(
-! IMPORTED: fir.if
-! IMPORTED: omp.taskwait
-! IMPORTED: } else {
+! IMPORTED-NOT: fir.if
+! IMPORTED-NOT: omp.taskyield
 ! IMPORTED: omp.taskwait
+! IMPORTED-NOT: fir.if
+! IMPORTED-NOT: omp.taskyield
 ! IMPORTED: return
 
 !--- settings.f90
diff --git a/flang/test/Lower/OpenMP/metadirective-user.f90 b/flang/test/Lower/OpenMP/metadirective-user.f90
index 23c92954e7e75..8c9967a2f2c8d 100644
--- a/flang/test/Lower/OpenMP/metadirective-user.f90
+++ b/flang/test/Lower/OpenMP/metadirective-user.f90
@@ -678,3 +678,38 @@ subroutine test_multi_dynamic_multi_static(a, b)
   !$omp & default(nothing)
 #endif
 end subroutine
+
+! Rank once: the vendor-only selector scores zero as a subset of the guarded
+! candidate. If that guard fails, its subset must not regain score 101 and
+! overtake the static candidate (score 11). The original order is 22, 11, 0.
+! CHECK-LABEL: func.func @_QPtest_dynamic_subset_order(
+! CHECK: fir.if
+! CHECK-NEXT: omp.barrier
+! CHECK-NEXT: } else {
+! CHECK-NEXT: omp.taskwait
+! CHECK-NOT: omp.taskyield
+! CHECK: return
+subroutine test_dynamic_subset_order(flag)
+  logical :: flag
+  !$omp metadirective &
+  !$omp& when(implementation={vendor(score(100): llvm)}: taskyield) &
+  !$omp& when(implementation={vendor(score(1): llvm)}, &
+  !$omp& user={condition(score(20): flag)}: barrier) &
+  !$omp& when(user={condition(score(10): .true.)}: taskwait)
+end subroutine
+
+! The same ordering applies when the guarded replacement is implicit NOTHING.
+! CHECK-LABEL: func.func @_QPtest_dynamic_subset_nothing_order(
+! CHECK: fir.if
+! CHECK-NEXT: } else {
+! CHECK-NEXT: omp.taskwait
+! CHECK-NOT: omp.taskyield
+! CHECK: return
+subroutine test_dynamic_subset_nothing_order(flag)
+  logical :: flag
+  !$omp metadirective &
+  !$omp& when(implementation={vendor(score(100): llvm)}: taskyield) &
+  !$omp& when(implementation={vendor(score(1): llvm)}, &
+  !$omp& user={condition(score(20): flag)}:) &
+  !$omp& when(user={condition(score(10): .true.)}: taskwait)
+end subroutine
diff --git a/llvm/include/llvm/Frontend/OpenMP/OMPContext.h b/llvm/include/llvm/Frontend/OpenMP/OMPContext.h
index f6f3f3a22eeff..acd0b1bda947d 100644
--- a/llvm/include/llvm/Frontend/OpenMP/OMPContext.h
+++ b/llvm/include/llvm/Frontend/OpenMP/OMPContext.h
@@ -237,6 +237,13 @@ LLVM_ABI bool
 isVariantApplicableInContext(const VariantMatchInfo &VMI, const OMPContext &Ctx,
                              bool DeviceOrImplementationSetOnly = false);
 
+/// Compute scores for all applicable variants, including strict-subset
+/// adjustments against the complete candidate set. Inapplicable entries have
+/// no score. Keep these scores when testing dynamic candidates in order.
+LLVM_ABI SmallVector<std::optional<APInt>, 4>
+getVariantMatchScores(const SmallVectorImpl<VariantMatchInfo> &VMIs,
+                      const OMPContext &Ctx);
+
 /// Return the index (into \p VMIs) of the variant with the highest score
 /// from the ones applicable in \p Ctx. See llvm::isVariantApplicableInContext.
 LLVM_ABI int
diff --git a/llvm/lib/Frontend/OpenMP/OMPContext.cpp b/llvm/lib/Frontend/OpenMP/OMPContext.cpp
index 69d8c94cf98be..ffa7b7f5e992e 100644
--- a/llvm/lib/Frontend/OpenMP/OMPContext.cpp
+++ b/llvm/lib/Frontend/OpenMP/OMPContext.cpp
@@ -426,8 +426,9 @@ static APInt getVariantMatchScore(const VariantMatchInfo &VMI,
   return Score;
 }
 
-int llvm::omp::getBestVariantMatchForContext(
-    const SmallVectorImpl<VariantMatchInfo> &VMIs, const OMPContext &Ctx) {
+SmallVector<std::optional<APInt>, 4>
+llvm::omp::getVariantMatchScores(const SmallVectorImpl<VariantMatchInfo> &VMIs,
+                                 const OMPContext &Ctx) {
   SmallVector<std::optional<APInt>, 4> Scores(VMIs.size());
   for (unsigned u = 0, e = VMIs.size(); u < e; ++u) {
     const VariantMatchInfo &VMI = VMIs[u];
@@ -455,6 +456,12 @@ int llvm::omp::getBestVariantMatchForContext(
     }
   }
 
+  return Scores;
+}
+
+int llvm::omp::getBestVariantMatchForContext(
+    const SmallVectorImpl<VariantMatchInfo> &VMIs, const OMPContext &Ctx) {
+  auto Scores = getVariantMatchScores(VMIs, Ctx);
   APInt BestScore(1, 0);
   int BestVMIIdx = -1;
   for (unsigned u = 0, e = VMIs.size(); u < e; ++u) {
diff --git a/llvm/unittests/Frontend/OpenMPContextTest.cpp b/llvm/unittests/Frontend/OpenMPContextTest.cpp
index cf8e68f0a33a5..7a9f95bf6c887 100644
--- a/llvm/unittests/Frontend/OpenMPContextTest.cpp
+++ b/llvm/unittests/Frontend/OpenMPContextTest.cpp
@@ -574,6 +574,25 @@ TEST_F(OpenMPContextTest, StrictSubsetScoreIsZero) {
   EXPECT_EQ(getBestVariantMatchForContext(Candidates, Context), 0);
 }
 
+TEST_F(OpenMPContextTest, PreserveSubsetAdjustedScores) {
+  OMPContext Context(false, Triple("x86_64-unknown-linux"), Triple(), -1);
+  APInt High(64, 100), Low(64, 1), Guard(64, 20), Medium(64, 10);
+  VariantMatchInfo Subset, Superset, Other;
+  Subset.addTrait(TraitProperty::implementation_vendor_llvm, "", &High);
+  Superset.addTrait(TraitProperty::implementation_vendor_llvm, "", &Low);
+  Superset.addTrait(TraitProperty::user_condition_true, "flag", &Guard);
+  Other.addTrait(TraitProperty::user_condition_true, "literal", &Medium);
+  SmallVector<VariantMatchInfo, 3> Candidates{Subset, Superset, Other};
+  auto Scores = getVariantMatchScores(Candidates, Context);
+  ASSERT_TRUE(Scores[0] && Scores[1] && Scores[2]);
+  EXPECT_EQ(Scores[0]->getZExtValue(), 0u);
+  EXPECT_EQ(Scores[1]->getZExtValue(), 22u);
+  EXPECT_EQ(Scores[2]->getZExtValue(), 11u);
+  // Consumers can retain these scores after the superset's runtime guard
+  // fails, so Other continues to precede Subset.
+  EXPECT_TRUE(Scores[0]->zext(64).ult(Scores[2]->zext(64)));
+}
+
 TEST_F(OpenMPContextTest, IncompatibleSupersetDoesNotZeroScore) {
   OMPContext Context(false, Triple("x86_64-unknown-linux"), Triple(), -1);
   APInt HighScore(64, 100), LowScore(64, 5);

>From 1a482d5345f70f765d27fe9a5ad9ac0ef365dafd Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Mon, 28 Sep 2026 11:41:25 -0500
Subject: [PATCH 11/23] Anchor imported condition checks to the selected branch

Require TASKWAIT in the first runtime arm so a nested fallback cannot
satisfy the same-condition expectation.
---
 .../test/Lower/OpenMP/declare-variant-condition-identity.f90  | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/flang/test/Lower/OpenMP/declare-variant-condition-identity.f90 b/flang/test/Lower/OpenMP/declare-variant-condition-identity.f90
index 145002613f8d3..be0a23d463236 100644
--- a/flang/test/Lower/OpenMP/declare-variant-condition-identity.f90
+++ b/flang/test/Lower/OpenMP/declare-variant-condition-identity.f90
@@ -41,8 +41,8 @@
 ! Same runtime condition: the low-scoring strict superset wins when true.
 ! IMPORTED-LABEL: func.func @_QPruntime_same(
 ! IMPORTED: fir.if
-! IMPORTED: omp.taskwait
-! IMPORTED: } else {
+! IMPORTED-NEXT: omp.taskwait
+! IMPORTED-NEXT: } else {
 ! IMPORTED: return
 
 ! Different variables must not be merged even when their values might agree.

>From bca38f25a2cfcc674e8d0684effb490f960bf788 Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Mon, 28 Sep 2026 11:41:26 -0500
Subject: [PATCH 12/23] Use the outer loop context for composite bounds

Resolve original bounds and strides before the outermost loop-associated
constituent. In particular, TEAMS DISTRIBUTE PARALLEL DO bounds see TEAMS,
even when lowering has already created the inner PARALLEL operation.

Scope the context to loop-control evaluation, preserving clause contexts,
genuinely enclosing loops, body contexts, and subsequent expressions.
Cover lower bounds, upper bounds, strides, composite scoring depth, and
static and runtime DO SIMD replacements.
---
 flang/lib/Lower/OpenMP/OpenMP.cpp             |  4 +-
 flang/lib/Lower/OpenMP/Utils.cpp              | 30 +++++++
 .../OpenMP/declare-variant-loop-bounds.f90    | 79 +++++++++++++++++--
 .../declare-variant-metadirective-context.f90 | 46 +++++++++++
 4 files changed, 149 insertions(+), 10 deletions(-)

diff --git a/flang/lib/Lower/OpenMP/OpenMP.cpp b/flang/lib/Lower/OpenMP/OpenMP.cpp
index 781dafa9f00e5..7b8a2eee0ecdd 100644
--- a/flang/lib/Lower/OpenMP/OpenMP.cpp
+++ b/flang/lib/Lower/OpenMP/OpenMP.cpp
@@ -7772,8 +7772,8 @@ static void genMetadirective(lower::AbstractConverter &converter,
     //   if (flag) barrier    into just    barrier
     //   else barrier
     if (std::optional<unsigned> selectedInElse =
-            semantics::omp::SelectBestMetadirectiveCandidate(
-                elsePathCandidates, candidates)) {
+            semantics::omp::SelectBestMetadirectiveCandidate(elsePathCandidates,
+                                                             candidates)) {
       const semantics::omp::MetadirectiveCandidate &candidateInElse =
           candidates[*selectedInElse];
       if (!candidateInElse.dynamicCondition &&
diff --git a/flang/lib/Lower/OpenMP/Utils.cpp b/flang/lib/Lower/OpenMP/Utils.cpp
index 26eeaf0739393..c173ab4a3e1bf 100644
--- a/flang/lib/Lower/OpenMP/Utils.cpp
+++ b/flang/lib/Lower/OpenMP/Utils.cpp
@@ -791,6 +791,21 @@ void collectTileSizesFromOpenMPConstruct(
       });
 }
 
+namespace {
+// Original loop control belongs outside the outermost loop-associated
+// constituent, including for composites that emit inner regions first.
+class LoopControlContext
+    : public mlir::StateStackFrameBase<LoopControlContext> {
+public:
+  MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(LoopControlContext)
+
+  explicit LoopControlContext(const pft::Evaluation &evaluation)
+      : evaluation(evaluation) {}
+
+  const pft::Evaluation &evaluation;
+};
+} // namespace
+
 int64_t collectLoopRelatedInfo(
     lower::AbstractConverter &converter, mlir::Location currentLocation,
     lower::pft::Evaluation &eval, lower::pft::Evaluation *nestedEval,
@@ -823,6 +838,8 @@ void collectLoopRelatedInfo(
     int64_t numCollapse, mlir::omp::LoopRelatedClauseOps &result,
     llvm::SmallVectorImpl<const semantics::Symbol *> &iv) {
 
+  mlir::SaveStateStack<LoopControlContext> context{converter.getStateStack(),
+                                                   eval};
   fir::FirOpBuilder &firOpBuilder = converter.getFirOpBuilder();
 
   // Collect the loops to collapse.
@@ -1519,9 +1536,22 @@ void collectEnclosingConstructTraits(
   // PARALLEL when lowering the bounds of PARALLEL DO. A selected directive
   // contributes here only through its entered constituents, so its own clause
   // expressions have the same context as a directly written directive's.
+  const auto *loopControl =
+      converter.getStateStack().getStackTop<LoopControlContext>();
+  bool insideLoop = false;
   for (auto [index, frame] : llvm::enumerate(frames)) {
     if (usedFrames[index] || frame->isReplacement)
       continue;
+    if (loopControl && &frame->evaluation == &loopControl->evaluation) {
+      // Keep the prefix before the first loop-associated constituent. For
+      // TEAMS DISTRIBUTE PARALLEL DO this is TEAMS, even though the emitted
+      // PARALLEL region already surrounds the bound calculation. Frames for
+      // genuinely enclosing directives are unaffected.
+      insideLoop |= llvm::omp::getDirectiveAssociation(frame->directive) ==
+                    llvm::omp::Association::LoopNest;
+      if (insideLoop)
+        continue;
+    }
     append(frame->directive);
   }
 }
diff --git a/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90 b/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
index c543c6752a16e..aaa5b4ba5914c 100644
--- a/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
+++ b/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
@@ -69,13 +69,14 @@ subroutine explicit_nest(n, a)
     !$omp end parallel
   end subroutine
 
-  ! Composite lowering creates PARALLEL separately from the loop wrappers.
+  ! Original bounds are evaluated in the context before DISTRIBUTE.
+  ! Emitting the composite PARALLEL region first must not affect selection.
   ! CHECK-LABEL: func.func @_QMloop_contextPcomposite(
   ! CHECK: omp.teams
   ! CHECK: omp.parallel
   ! CHECK-NOT: fir.call @_QMloop_contextPbound(
   ! CHECK-NOT: fir.call @_QMloop_contextPdo_bound(
-  ! CHECK: fir.call @_QMloop_contextPparallel_bound(
+  ! CHECK: fir.call @_QMloop_contextPteams_bound(
   ! CHECK: omp.distribute
   ! CHECK: omp.wsloop
   ! CHECK: fir.call @_QMloop_contextPdo_bound(
@@ -95,7 +96,7 @@ subroutine composite(n, a)
   ! CHECK: omp.parallel
   ! CHECK-NOT: fir.call @_QMloop_contextPbound(
   ! CHECK-NOT: fir.call @_QMloop_contextPdo_bound(
-  ! CHECK: fir.call @_QMloop_contextPparallel_bound(
+  ! CHECK: fir.call @_QMloop_contextPteams_bound(
   ! CHECK: omp.distribute
   ! CHECK: omp.wsloop
   ! CHECK: omp.simd
@@ -164,13 +165,15 @@ pure integer function thread_count(n)
 
   ! DIST_SCHEDULE sees only TEAMS. NUM_THREADS sees TEAMS, DISTRIBUTE, so
   ! CPU scores 5 and beats the vendor score of 4. A leaked PARALLEL would
-  ! make parallel_count a strict superset of cpu_count. The bound sees PARALLEL.
+  ! make parallel_count a strict superset of cpu_count. Bounds see only TEAMS.
   ! CHECK-LABEL: func.func @_QMloop_contextPcomposite_clauses(
   ! CHECK: omp.teams
   ! CHECK: fir.call @_QMloop_contextPteams_bound(
   ! CHECK: fir.call @_QMloop_contextPcpu_count(
   ! CHECK: omp.parallel
-  ! CHECK: fir.call @_QMloop_contextPparallel_bound(
+  ! CHECK: fir.call @_QMloop_contextPteams_bound(
+  ! CHECK: fir.call @_QMloop_contextPteams_bound(
+  ! CHECK: fir.call @_QMloop_contextPteams_bound(
   ! CHECK: omp.distribute
   ! CHECK: omp.wsloop
   ! CHECK: fir.call @_QMloop_contextPdo_bound(
@@ -179,7 +182,7 @@ subroutine composite_clauses(n, a)
     integer :: n, a(n), i
     !$omp teams distribute parallel do dist_schedule(static, bound(n)) &
     !$omp& num_threads(thread_count(n))
-    do i = 1, bound(n)
+    do i = bound(1), bound(n), bound(1)
       a(i) = bound(n)
     end do
   end subroutine
@@ -191,7 +194,7 @@ subroutine composite_clauses(n, a)
   ! CHECK: fir.call @_QMloop_contextPcpu_count(
   ! CHECK: omp.parallel
   ! CHECK: fir.call @_QMloop_contextPdo_pred(
-  ! CHECK: fir.call @_QMloop_contextPparallel_bound(
+  ! CHECK: fir.call @_QMloop_contextPscored_count(
   ! CHECK: omp.distribute
   ! CHECK: omp.wsloop
   ! CHECK: omp.simd
@@ -201,7 +204,7 @@ subroutine composite_simd_clauses(n, a)
     integer :: n, a(n), i
     !$omp teams distribute parallel do simd dist_schedule(static, bound(n)) &
     !$omp& num_threads(thread_count(n)) if(simd: pred(n))
-    do i = 1, bound(n)
+    do i = 1, thread_count(n)
       a(i) = bound(n)
     end do
   end subroutine
@@ -280,4 +283,64 @@ subroutine nested_distribute_simd_clauses(n, a)
     end do
     !$omp end teams
   end subroutine
+
+  ! The same original-bound rule applies without PARALLEL in the composite.
+  ! CHECK-LABEL: func.func @_QMloop_contextPteams_distribute(
+  ! CHECK: omp.teams
+  ! CHECK: fir.call @_QMloop_contextPteams_bound(
+  ! CHECK: fir.call @_QMloop_contextPscored_count(
+  ! CHECK: omp.distribute
+  ! CHECK: fir.call @_QMloop_contextPcpu_count(
+  ! CHECK: fir.call @_QMloop_contextPscored_count(
+  ! CHECK: return
+  subroutine teams_distribute(n, a)
+    integer :: n, a(n), i
+    !$omp teams
+    !$omp distribute
+    do i = bound(1), thread_count(n)
+      a(i) = thread_count(n)
+    end do
+    i = thread_count(n)
+    !$omp end teams
+  end subroutine
+
+  ! Retain PARALLEL preceding DO in the same combined directive.
+  ! CHECK-LABEL: func.func @_QMloop_contextPparallel_do_simd(
+  ! CHECK: omp.parallel
+  ! CHECK: fir.call @_QMloop_contextPdo_pred(
+  ! CHECK: fir.call @_QMloop_contextPparallel_bound(
+  ! CHECK: omp.wsloop
+  ! CHECK: omp.simd
+  ! CHECK: fir.call @_QMloop_contextPdo_bound(
+  ! CHECK: fir.call @_QMloop_contextPbound(
+  ! CHECK: return
+  subroutine parallel_do_simd(n, a)
+    integer :: n, a(n), i
+    !$omp parallel do simd if(simd: pred(n))
+    do i = 1, bound(n)
+      a(i) = bound(n)
+    end do
+    i = bound(n)
+  end subroutine
+
+  ! An enclosing DO belongs to a different source loop and must remain active
+  ! for the inner SIMD bounds. Only the inner directive's frames are filtered.
+  ! CHECK-LABEL: func.func @_QMloop_contextPenclosing_do(
+  ! CHECK: omp.wsloop
+  ! CHECK: fir.call @_QMloop_contextPdo_bound(
+  ! CHECK: omp.simd
+  ! CHECK: fir.call @_QMloop_contextPdo_bound(
+  ! CHECK: fir.call @_QMloop_contextPbound(
+  ! CHECK: return
+  subroutine enclosing_do(n, a)
+    integer :: n, a(n,n), i, j
+    !$omp do
+    do i = 1, n
+      !$omp simd
+      do j = 1, bound(n)
+        a(i,j) = bound(n)
+      end do
+    end do
+    i = bound(n)
+  end subroutine
 end module
diff --git a/flang/test/Lower/OpenMP/declare-variant-metadirective-context.f90 b/flang/test/Lower/OpenMP/declare-variant-metadirective-context.f90
index 2e5011167c9ee..b4a3002471296 100644
--- a/flang/test/Lower/OpenMP/declare-variant-metadirective-context.f90
+++ b/flang/test/Lower/OpenMP/declare-variant-metadirective-context.f90
@@ -146,4 +146,50 @@ subroutine delimited_do(n, a)
     !$omp end metadirective
     a(n) = value(n)
   end subroutine
+
+  ! SIMD clauses see DO; original bounds exclude both loop constituents.
+  ! CHECK-LABEL: func.func @_QMselected_contextPstatic_do_simd(
+  ! CHECK: fir.call @_QMselected_contextPin_do(
+  ! CHECK: fir.call @_QMselected_contextPvalue(
+  ! CHECK: omp.wsloop
+  ! CHECK: omp.simd
+  ! CHECK: fir.call @_QMselected_contextPin_do(
+  ! CHECK: fir.call @_QMselected_contextPvalue(
+  ! CHECK: return
+  subroutine static_do_simd(n, a)
+    integer :: n, a(n), i
+    !$omp begin metadirective &
+    !$omp& when(implementation={vendor(llvm)}: do simd if(simd: value(n) > 0)) &
+    !$omp& otherwise(nothing)
+    do i = 1, value(n)
+      a(i) = value(n)
+    end do
+    !$omp end metadirective
+    a(n) = value(n)
+  end subroutine
+
+  ! Both runtime arms and the subsequent call restore the outer context.
+  ! CHECK-LABEL: func.func @_QMselected_contextPruntime_do_simd(
+  ! CHECK: fir.if
+  ! CHECK: fir.call @_QMselected_contextPin_do(
+  ! CHECK: fir.call @_QMselected_contextPvalue(
+  ! CHECK: omp.wsloop
+  ! CHECK: omp.simd
+  ! CHECK: fir.call @_QMselected_contextPin_do(
+  ! CHECK: } else {
+  ! CHECK: fir.call @_QMselected_contextPvalue(
+  ! CHECK: fir.call @_QMselected_contextPvalue(
+  ! CHECK: fir.call @_QMselected_contextPvalue(
+  ! CHECK: return
+  subroutine runtime_do_simd(flag, n, a)
+    logical :: flag
+    integer :: n, a(n), i
+    !$omp metadirective &
+    !$omp& when(user={condition(flag)}: do simd if(simd: value(n) > 0)) &
+    !$omp& otherwise(nothing)
+    do i = 1, value(n)
+      a(i) = value(n)
+    end do
+    a(n) = value(n)
+  end subroutine
 end module

>From 9ed878696de3bd8415dec4903fc2ee0995d95971 Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Tue, 29 Sep 2026 12:09:02 -0500
Subject: [PATCH 13/23] Preserve inner condition parentheses and handle invalid
 literals

Strip only outer parentheses from condition identities and avoid assertions
when literal analysis fails. Strengthen runtime branch checks and cover
variant selection during lastprivate bound re-evaluation.
---
 flang/lib/Semantics/openmp-utils.cpp          | 39 ++++++++--------
 .../declare-variant-condition-identity.f90    | 44 ++++++++++++++++---
 .../OpenMP/declare-variant-loop-bounds.f90    | 20 +++++++++
 .../Semantics/OpenMP/metadirective-user.f90   | 20 +++++++++
 4 files changed, 100 insertions(+), 23 deletions(-)

diff --git a/flang/lib/Semantics/openmp-utils.cpp b/flang/lib/Semantics/openmp-utils.cpp
index 67b8ea979708f..adbdeec5f95c3 100644
--- a/flang/lib/Semantics/openmp-utils.cpp
+++ b/flang/lib/Semantics/openmp-utils.cpp
@@ -2508,7 +2508,8 @@ void AppendDirectiveContextTraits(llvm::omp::Directive directive,
 namespace {
 // Profile the original parse tree: folding a whole expression would lose
 // declaration identity. Parse-tree operators already unify alternate spellings;
-// analyze only literals to normalize their values and effective kinds.
+// analyze only literals to normalize their values and effective kinds. Failed
+// analysis during semantic error recovery falls back to the structural walk.
 struct ConditionIdentity {
   SemanticsContext &context;
   llvm::FoldingSetNodeID id;
@@ -2540,23 +2541,21 @@ struct ConditionIdentity {
   }
   bool Pre(const parser::SignedIntLiteralConstant &literal) {
     AddNode<parser::SignedIntLiteralConstant>();
-    auto value{evaluate::ExpressionAnalyzer{context}.Analyze(literal)};
-    CHECK(value);
-    id.AddString(value->AsFortran());
-    return false;
+    if (auto value{evaluate::ExpressionAnalyzer{context}.Analyze(literal)}) {
+      id.AddString(value->AsFortran());
+      return false;
+    }
+    return true;
   }
   bool Pre(const parser::SignedRealLiteralConstant &literal) {
     AddNode<parser::SignedRealLiteralConstant>();
-    auto value{evaluate::ExpressionAnalyzer{context}.Analyze(literal)};
-    CHECK(value);
-    id.AddString(value->AsFortran());
-    return false;
-  }
-  bool Pre(const parser::Expr &expr) {
-    if (const auto *parens{std::get_if<parser::Expr::Parentheses>(&expr.u)}) {
-      parser::Walk(parens->v.value(), *this);
+    if (auto value{evaluate::ExpressionAnalyzer{context}.Analyze(literal)}) {
+      id.AddString(value->AsFortran());
       return false;
     }
+    return true;
+  }
+  bool Pre(const parser::Expr &expr) {
     AddNode<parser::Expr>();
     if (const auto *negate{std::get_if<parser::Expr::Negate>(&expr.u)}) {
       if (const auto *literal{
@@ -2565,11 +2564,11 @@ struct ConditionIdentity {
           // The magnitude of the most negative integer is not representable
           // in its kind. Analyze the signed literal together, as semantics
           // does, rather than reanalyzing its positive magnitude.
-          auto value{evaluate::ExpressionAnalyzer{context}.Analyze(expr)};
-          CHECK(value);
-          AddNode<parser::Expr::Negate>();
-          id.AddString(value->AsFortran());
-          return false;
+          if (auto value{evaluate::ExpressionAnalyzer{context}.Analyze(expr)}) {
+            AddNode<parser::Expr::Negate>();
+            id.AddString(value->AsFortran());
+            return false;
+          }
         }
       }
     }
@@ -2591,6 +2590,10 @@ llvm::StringRef GetConditionIdentity(
     const parser::ScalarExpr &condition, SemanticsContext &context) {
   const auto *expr{parser::Unwrap<parser::Expr>(condition)};
   CHECK(expr);
+  // Only outer parentheses are insignificant. Inner parentheses can constrain
+  // reassociation and must remain part of the original expression's identity.
+  while (const auto *parens{std::get_if<parser::Expr::Parentheses>(&expr->u)})
+    expr = &parens->v.value();
   ConditionIdentity profile{context, {}};
   parser::Walk(*expr, profile);
   llvm::FoldingSetNodeIDRef data{profile.id.getRef()};
diff --git a/flang/test/Lower/OpenMP/declare-variant-condition-identity.f90 b/flang/test/Lower/OpenMP/declare-variant-condition-identity.f90
index be0a23d463236..d890786755f51 100644
--- a/flang/test/Lower/OpenMP/declare-variant-condition-identity.f90
+++ b/flang/test/Lower/OpenMP/declare-variant-condition-identity.f90
@@ -28,6 +28,7 @@
 ! LOCAL: fir.call @_QMconditionsPhigh()
 ! LOCAL: fir.call @_QMconditionsPhigh()
 ! LOCAL: fir.call @_QMconditionsPlow()
+! LOCAL: fir.call @_QMconditionsPhigh()
 ! LOCAL: return
 
 ! Conditions referring to USE-associated names are rebuilt in the importing
@@ -48,10 +49,10 @@
 ! Different variables must not be merged even when their values might agree.
 ! IMPORTED-LABEL: func.func @_QPruntime_distinct(
 ! IMPORTED: fir.if
-! IMPORTED: omp.taskyield
-! IMPORTED: } else {
+! IMPORTED-NEXT: omp.taskyield
+! IMPORTED-NEXT: } else {
 ! IMPORTED: fir.if
-! IMPORTED: omp.taskwait
+! IMPORTED-NEXT: omp.taskwait
 ! IMPORTED: return
 
 ! False conditions retain the same identity under match_none.
@@ -228,8 +229,17 @@ integer function spelling_integer_min()
     spelling_integer_min = 0
   end function
 
+  ! Inner parentheses remain significant even if both expressions fold alike.
+  integer function inner_parentheses()
+    !$omp declare variant(high) match(user={condition((1)+1 == 2)}, &
+    !$omp& implementation={vendor(score(100): llvm)})
+    !$omp declare variant(low) match(user={condition(1+1 == 2)}, &
+    !$omp& implementation={vendor(score(1): llvm)}, device={kind(cpu)})
+    inner_parentheses = 0
+  end function
+
   subroutine local_calls(a)
-    integer :: a(17)
+    integer :: a(18)
     a(1) = parentheses()
     a(2) = aliases()
     a(3) = compound_aliases()
@@ -247,6 +257,7 @@ subroutine local_calls(a)
     a(15) = spelling_distinct_kind()
     a(16) = spelling_complex_values()
     a(17) = spelling_integer_min()
+    a(18) = inner_parentheses()
   end subroutine
 end module
 
@@ -309,7 +320,8 @@ subroutine runtime_any()
 ! IMPORTED-LABEL: func.func @_QPruntime_operators(
 ! IMPORTED-NOT: omp.taskyield
 ! IMPORTED: fir.if
-! IMPORTED: omp.taskwait
+! IMPORTED-NEXT: omp.taskwait
+! IMPORTED-NEXT: } else {
 ! IMPORTED-NOT: omp.taskyield
 ! IMPORTED: return
 subroutine runtime_operators(n)
@@ -321,6 +333,28 @@ subroutine runtime_operators(n)
   !$omp& implementation={vendor(score(1): llvm)}: taskwait)
 end subroutine
 
+! Inner parentheses constrain reassociation. The higher-scoring condition
+! must be tested first, while an outer pair around it remains insignificant.
+! IMPORTED-LABEL: func.func @_QPruntime_inner_parentheses(
+! IMPORTED: %[[SUM:.*]] = arith.addf
+! IMPORTED: %[[GROUP:.*]] = {{(hlfir|fir)}}.no_reassoc %[[SUM]]
+! IMPORTED: arith.addf %[[GROUP]],
+! IMPORTED: fir.if
+! IMPORTED-NEXT: omp.taskyield
+! IMPORTED-NEXT: } else {
+! IMPORTED: fir.if
+! IMPORTED-NEXT: omp.taskwait
+! IMPORTED-NEXT: } else {
+! IMPORTED: return
+subroutine runtime_inner_parentheses(a, b, c)
+  real :: a, b, c
+  !$omp metadirective &
+  !$omp& when(user={condition(((a+b)+c > 0.0))}, &
+  !$omp& implementation={vendor(score(100): llvm)}: taskyield) &
+  !$omp& when(user={condition(a+b+c > 0.0)}, device={kind(cpu)}, &
+  !$omp& implementation={vendor(score(1): llvm)}: taskwait)
+end subroutine
+
 !--- kinds.f90
 ! DEFAULT4-LABEL: func.func @_QPdefault_kind(
 ! DEFAULT4-NOT: omp.taskyield
diff --git a/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90 b/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
index aaa5b4ba5914c..bff53d8ade266 100644
--- a/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
+++ b/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
@@ -343,4 +343,24 @@ subroutine enclosing_do(n, a)
     end do
     i = bound(n)
   end subroutine
+  ! Lastprivate re-evaluates the bound inside WSLOOP for the copy-back test.
+  ! Both evaluations belong to the original loop control, outside DO.
+  ! CHECK-LABEL: func.func @_QMloop_contextPlastprivate_bound(
+  ! CHECK: fir.call @_QMloop_contextPbound(
+  ! CHECK: omp.wsloop
+  ! CHECK-NOT: fir.call @_QMloop_contextPdo_bound(
+  ! CHECK: %[[COPY_UB:.*]] = fir.call @_QMloop_contextPbound(
+  ! CHECK-NOT: fir.call @_QMloop_contextPdo_bound(
+  ! CHECK: arith.cmpi sgt, %{{[^,]+}}, %[[COPY_UB]]
+  ! CHECK: fir.if
+  ! CHECK: {{hlfir.assign|fir.store}}
+  ! CHECK-NOT: fir.call @_QMloop_contextPdo_bound(
+  ! CHECK: return
+  subroutine lastprivate_bound(n, x)
+    integer :: n, x, i
+    !$omp do lastprivate(x)
+    do i = 1, bound(n)
+      x = i
+    end do
+  end subroutine
 end module
diff --git a/flang/test/Semantics/OpenMP/metadirective-user.f90 b/flang/test/Semantics/OpenMP/metadirective-user.f90
index 202d7781cd771..12e2dd0f028cf 100644
--- a/flang/test/Semantics/OpenMP/metadirective-user.f90
+++ b/flang/test/Semantics/OpenMP/metadirective-user.f90
@@ -34,3 +34,23 @@ subroutine f04(a, b)
 !ERROR: Repeated trait name CONDITION in a trait set
   !$omp & when(user={condition(a), condition(b)}: nothing)
 end
+
+! Profiling still runs after expression diagnostics have been recorded.
+subroutine invalid_negative_kind(n)
+  integer :: n
+!ERROR: CONDITION trait requires a single LOGICAL expression
+!ERROR: INTEGER(KIND=3) is not a supported type
+  !$omp metadirective when(user={condition(n == -1_3)}: taskyield) otherwise(taskwait)
+end
+
+subroutine invalid_complex_integer_kind()
+!ERROR: CONDITION trait requires a single LOGICAL expression
+!ERROR: INTEGER(KIND=3) is not a supported type
+  !$omp metadirective when(user={condition((1_3, 2) == (1, 2))}: taskyield) otherwise(taskwait)
+end
+
+subroutine invalid_complex_real_kind()
+!ERROR: CONDITION trait requires a single LOGICAL expression
+!ERROR: Unsupported REAL(KIND=7)
+  !$omp metadirective when(user={condition((1.0_7, 0.0) == (1.0, 0.0))}: taskyield) otherwise(taskwait)
+end

>From 8e1567ad75d5d9b3253862552cef8d4b19dc5789 Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Wed, 30 Sep 2026 16:55:57 -0500
Subject: [PATCH 14/23] Fix variant context for lastprivate bounds

Use the owning directive's evaluation when collecting construct ancestors
for loop-control expressions. Lastprivate can re-evaluate bounds after
loop-body lowering leaves a body evaluation current, which otherwise adds
the loop construct before the loop-control context filter runs.
---
 flang/lib/Lower/OpenMP/Utils.cpp              | 10 ++++-
 .../OpenMP/declare-variant-loop-bounds.f90    | 43 +++++++++++++++++++
 2 files changed, 51 insertions(+), 2 deletions(-)

diff --git a/flang/lib/Lower/OpenMP/Utils.cpp b/flang/lib/Lower/OpenMP/Utils.cpp
index c173ab4a3e1bf..cb1545bab4516 100644
--- a/flang/lib/Lower/OpenMP/Utils.cpp
+++ b/flang/lib/Lower/OpenMP/Utils.cpp
@@ -1490,6 +1490,14 @@ semantics::omp::OmpVariantMatchContext makeVariantMatchContext(
 void collectEnclosingConstructTraits(
     AbstractConverter &converter, const pft::Evaluation *evaluation,
     llvm::SmallVectorImpl<llvm::omp::TraitProperty> &constructTraits) {
+  const auto *loopControl =
+      converter.getStateStack().getStackTop<LoopControlContext>();
+  // Lastprivate can re-evaluate bounds after lowering the loop body, leaving
+  // a body evaluation current. Use the owning directive's ancestors so the
+  // loop itself is not added before filtering its entered constituents below.
+  if (loopControl)
+    evaluation = &loopControl->evaluation;
+
   llvm::SmallVector<const OpenMPContextFrame *, 4> frames;
   converter.getStateStack().stackWalk<OpenMPContextFrame>(
       [&](OpenMPContextFrame &frame) {
@@ -1536,8 +1544,6 @@ void collectEnclosingConstructTraits(
   // PARALLEL when lowering the bounds of PARALLEL DO. A selected directive
   // contributes here only through its entered constituents, so its own clause
   // expressions have the same context as a directly written directive's.
-  const auto *loopControl =
-      converter.getStateStack().getStackTop<LoopControlContext>();
   bool insideLoop = false;
   for (auto [index, frame] : llvm::enumerate(frames)) {
     if (usedFrames[index] || frame->isReplacement)
diff --git a/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90 b/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
index bff53d8ade266..de2e13d58973e 100644
--- a/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
+++ b/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
@@ -363,4 +363,47 @@ subroutine lastprivate_bound(n, x)
       x = i
     end do
   end subroutine
+
+  ! Keep the entered PARALLEL constituent when re-evaluating the bound.
+  ! CHECK-LABEL: func.func @_QMloop_contextPlastprivate_combined(
+  ! CHECK: omp.parallel
+  ! CHECK: fir.call @_QMloop_contextPparallel_bound(
+  ! CHECK: omp.wsloop
+  ! CHECK-NOT: fir.call @_QMloop_contextPdo_bound(
+  ! CHECK: %[[PAR_UB:.*]] = fir.call @_QMloop_contextPparallel_bound(
+  ! CHECK: arith.cmpi sgt, %{{[^,]+}}, %[[PAR_UB]]
+  ! CHECK: fir.if
+  ! CHECK: {{hlfir.assign|fir.store}}
+  ! CHECK-NOT: fir.call @_QMloop_contextPdo_bound(
+  ! CHECK: return
+  subroutine lastprivate_combined(n, x)
+    integer :: n, x, i
+    !$omp parallel do lastprivate(x)
+    do i = 1, bound(n)
+      x = i
+    end do
+  end subroutine
+
+  ! A separately enclosing PARALLEL must also remain in the bound context.
+  ! CHECK-LABEL: func.func @_QMloop_contextPlastprivate_nested(
+  ! CHECK: omp.parallel
+  ! CHECK: fir.call @_QMloop_contextPparallel_bound(
+  ! CHECK: omp.wsloop
+  ! CHECK-NOT: fir.call @_QMloop_contextPdo_bound(
+  ! CHECK: %[[NESTED_UB:.*]] = fir.call @_QMloop_contextPparallel_bound(
+  ! CHECK: arith.cmpi sgt, %{{[^,]+}}, %[[NESTED_UB]]
+  ! CHECK: fir.if
+  ! CHECK: {{hlfir.assign|fir.store}}
+  ! CHECK-NOT: fir.call @_QMloop_contextPdo_bound(
+  ! CHECK: return
+  subroutine lastprivate_nested(n, x)
+    integer :: n, x, i
+    !$omp parallel
+    !$omp do lastprivate(x)
+    do i = 1, bound(n)
+      x = i
+    end do
+    !$omp end parallel
+  end subroutine
+
 end module

>From b29c97e6c4030c7bec4838ac5d86bc3f99604ab0 Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Thu, 1 Oct 2026 01:46:04 -0500
Subject: [PATCH 15/23] Fix host-evaluated loop-bound contexts

Host-evaluated bounds can select a different variant from lastprivate
copy-back, leaving the lastprivate value unchanged.

This fix build the loop-bound context from source constituents before
the first loop-associated construct, even when their frames have not
been entered.
---
 flang/lib/Lower/OpenMP/Utils.cpp              |  48 +++--
 .../OpenMP/declare-variant-loop-bounds.f90    | 171 +++++++++++++++++-
 2 files changed, 199 insertions(+), 20 deletions(-)

diff --git a/flang/lib/Lower/OpenMP/Utils.cpp b/flang/lib/Lower/OpenMP/Utils.cpp
index cb1545bab4516..d349ee232b722 100644
--- a/flang/lib/Lower/OpenMP/Utils.cpp
+++ b/flang/lib/Lower/OpenMP/Utils.cpp
@@ -1492,9 +1492,8 @@ void collectEnclosingConstructTraits(
     llvm::SmallVectorImpl<llvm::omp::TraitProperty> &constructTraits) {
   const auto *loopControl =
       converter.getStateStack().getStackTop<LoopControlContext>();
-  // Lastprivate can re-evaluate bounds after lowering the loop body, leaving
-  // a body evaluation current. Use the owning directive's ancestors so the
-  // loop itself is not added before filtering its entered constituents below.
+  // Use the loop owner's ancestors: host evaluation may have TARGET current
+  // while evaluating a nested loop's bounds.
   if (loopControl)
     evaluation = &loopControl->evaluation;
 
@@ -1540,26 +1539,39 @@ void collectEnclosingConstructTraits(
     }
   }
 
-  // Include entered constituents while their own evaluation is current, e.g.
-  // PARALLEL when lowering the bounds of PARALLEL DO. A selected directive
-  // contributes here only through its entered constituents, so its own clause
-  // expressions have the same context as a directly written directive's.
-  bool insideLoop = false;
+  // Use entered frames for clauses. Loop bounds use the source prefix below,
+  // which is available even before the owner's frames are entered.
   for (auto [index, frame] : llvm::enumerate(frames)) {
     if (usedFrames[index] || frame->isReplacement)
       continue;
-    if (loopControl && &frame->evaluation == &loopControl->evaluation) {
-      // Keep the prefix before the first loop-associated constituent. For
-      // TEAMS DISTRIBUTE PARALLEL DO this is TEAMS, even though the emitted
-      // PARALLEL region already surrounds the bound calculation. Frames for
-      // genuinely enclosing directives are unaffected.
-      insideLoop |= llvm::omp::getDirectiveAssociation(frame->directive) ==
-                    llvm::omp::Association::LoopNest;
-      if (insideLoop)
-        continue;
-    }
+    if (loopControl && &frame->evaluation == evaluation)
+      continue;
     append(frame->directive);
   }
+
+  if (!loopControl)
+    return;
+  const auto *omp = evaluation->getIf<parser::OpenMPConstruct>();
+  if (!omp)
+    return;
+  llvm::omp::Directive directive = parser::omp::GetOmpDirectiveName(*omp).v;
+  if (directive == llvm::omp::Directive::OMPD_metadirective) {
+    for (const OpenMPContextFrame *frame : frames) {
+      if (&frame->evaluation == evaluation && frame->isReplacement) {
+        directive = frame->directive;
+        break;
+      }
+    }
+  }
+  // Bounds precede the first loop-associated constituent in source order.
+  // TARGET TEAMS DISTRIBUTE PARALLEL DO therefore retains TARGET and TEAMS.
+  for (llvm::omp::Directive leaf :
+       llvm::omp::getLeafConstructsOrSelf(directive)) {
+    if (llvm::omp::getDirectiveAssociation(leaf) ==
+        llvm::omp::Association::LoopNest)
+      break;
+    append(leaf);
+  }
 }
 
 const semantics::Symbol *
diff --git a/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90 b/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
index de2e13d58973e..002b8f9143b80 100644
--- a/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
+++ b/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
@@ -1,7 +1,15 @@
 ! RUN: %flang_fc1 -fopenmp -fopenmp-version=52 -emit-hlfir %s -o - | \
-! RUN:   FileCheck %s --implicit-check-not='fir.call @_QMloop_contextPparallel_count'
+! RUN:   FileCheck %s --check-prefixes=CHECK,TARGET,HOST \
+! RUN:     --implicit-check-not='fir.call @_QMloop_contextPparallel_count'
 ! RUN: %flang_fc1 -fopenmp -fopenmp-version=52 -emit-fir %s -o - | \
-! RUN:   FileCheck %s --implicit-check-not='fir.call @_QMloop_contextPparallel_count'
+! RUN:   FileCheck %s --check-prefixes=CHECK,TARGET,HOST \
+! RUN:     --implicit-check-not='fir.call @_QMloop_contextPparallel_count'
+! RUN: %flang_fc1 -fopenmp -fopenmp-version=52 -emit-hlfir \
+! RUN:   -fopenmp-is-target-device %s -o - | \
+! RUN:   FileCheck %s --check-prefixes=TARGET,DEVICE
+! RUN: %flang_fc1 -fopenmp -fopenmp-version=52 -emit-fir \
+! RUN:   -fopenmp-is-target-device %s -o - | \
+! RUN:   FileCheck %s --check-prefixes=TARGET,DEVICE
 
 module loop_context
 contains
@@ -407,3 +415,162 @@ subroutine lastprivate_nested(n, x)
   end subroutine
 
 end module
+
+! Host-evaluated bounds and lastprivate copy-back must use the same source
+! context, before the first loop-associated constituent.
+module host_bounds
+contains
+  pure integer function target_bound(n)
+    integer, intent(in) :: n
+    target_bound = n + 2
+  end function
+
+  pure integer function parallel_bound(n)
+    integer, intent(in) :: n
+    parallel_bound = n + 1
+  end function
+
+  pure integer function teams_bound(n)
+    integer, intent(in) :: n
+    teams_bound = n + 3
+  end function
+
+  pure integer function do_bound(n)
+    integer, intent(in) :: n
+    do_bound = n + 10
+  end function
+
+  pure integer function bound(n)
+    integer, intent(in) :: n
+    !$omp declare variant(target_bound) match(construct={target})
+    !$omp declare variant(parallel_bound) match(construct={target, parallel})
+    !$omp declare variant(teams_bound) match(construct={target, teams})
+    !$omp declare variant(do_bound) match(construct={target, parallel, do})
+    bound = n
+  end function
+
+  pure integer function parallel_depth(n)
+    integer, intent(in) :: n
+    parallel_depth = n + 1
+  end function
+
+  pure integer function scored_depth(n)
+    integer, intent(in) :: n
+    scored_depth = n
+  end function
+
+  pure integer function depth_bound(n)
+    integer, intent(in) :: n
+    !$omp declare variant(parallel_depth) match(construct={parallel})
+    !$omp declare variant(scored_depth) &
+    !$omp& match(implementation={vendor(score(6): llvm)})
+    depth_bound = n
+  end function
+
+  ! Bounds see TARGET and PARALLEL before DO.
+  ! TARGET-LABEL: func.func @_QMhost_boundsPcombined(
+  ! HOST: fir.call @_QMhost_boundsPparallel_bound(
+  ! TARGET: omp.target
+  ! TARGET: omp.parallel
+  ! DEVICE: fir.call @_QMhost_boundsPparallel_bound(
+  ! TARGET: omp.wsloop
+  ! TARGET: %[[UB:.*]] = fir.call @_QMhost_boundsPparallel_bound(
+  ! TARGET: arith.cmpi sgt, %{{[^,]+}}, %[[UB]]
+  ! TARGET: fir.if
+  ! TARGET: {{hlfir.assign|fir.store}}
+  ! TARGET: return
+  subroutine combined(n, x)
+    integer :: n, x, i
+    !$omp target parallel do lastprivate(x) map(tofrom: x)
+    do i = 1, bound(n)
+      x = i
+    end do
+  end subroutine
+
+  ! Bounds see the enclosing TARGET and the combined directive's PARALLEL.
+  ! TARGET-LABEL: func.func @_QMhost_boundsPpartial_nest(
+  ! HOST: fir.call @_QMhost_boundsPparallel_bound(
+  ! TARGET: omp.target
+  ! TARGET: omp.parallel
+  ! DEVICE: fir.call @_QMhost_boundsPparallel_bound(
+  ! TARGET: omp.wsloop
+  ! TARGET: %[[UB:.*]] = fir.call @_QMhost_boundsPparallel_bound(
+  ! TARGET: arith.cmpi sgt, %{{[^,]+}}, %[[UB]]
+  ! TARGET: fir.if
+  ! TARGET: {{hlfir.assign|fir.store}}
+  ! TARGET: return
+  subroutine partial_nest(n, x)
+    integer :: n, x, i
+    !$omp target map(tofrom: x)
+      !$omp parallel do lastprivate(x)
+      do i = 1, bound(n)
+        x = i
+      end do
+    !$omp end target
+  end subroutine
+
+  ! Bounds see both source ancestors while host evaluation has TARGET current.
+  ! TARGET-LABEL: func.func @_QMhost_boundsPexplicit_nest(
+  ! HOST: fir.call @_QMhost_boundsPparallel_bound(
+  ! TARGET: omp.target
+  ! TARGET: omp.parallel
+  ! DEVICE: fir.call @_QMhost_boundsPparallel_bound(
+  ! TARGET: omp.wsloop
+  ! TARGET: %[[UB:.*]] = fir.call @_QMhost_boundsPparallel_bound(
+  ! TARGET: arith.cmpi sgt, %{{[^,]+}}, %[[UB]]
+  ! TARGET: fir.if
+  ! TARGET: {{hlfir.assign|fir.store}}
+  ! TARGET: return
+  subroutine explicit_nest(n, x)
+    integer :: n, x, i
+    !$omp target map(tofrom: x)
+      !$omp parallel
+        !$omp do lastprivate(x)
+        do i = 1, bound(n)
+          x = i
+        end do
+      !$omp end parallel
+    !$omp end target
+  end subroutine
+
+  ! Bounds see TARGET and TEAMS before DISTRIBUTE.
+  ! TARGET-LABEL: func.func @_QMhost_boundsPcomposite(
+  ! HOST: fir.call @_QMhost_boundsPteams_bound(
+  ! TARGET: omp.target
+  ! TARGET: omp.teams
+  ! TARGET: omp.parallel
+  ! DEVICE: fir.call @_QMhost_boundsPteams_bound(
+  ! TARGET: omp.distribute
+  ! TARGET: omp.wsloop
+  ! TARGET: return
+  subroutine composite(n, a)
+    integer :: n, a(:), i
+    !$omp target teams distribute parallel do map(tofrom: a)
+    do i = 1, bound(n)
+      a(i) = i
+    end do
+  end subroutine
+
+  ! PARALLEL scores 5 and the vendor scores 7 in TARGET, PARALLEL.
+  ! Counting PARALLEL twice raises its score to 9 and selects the wrong variant.
+  ! TARGET-LABEL: func.func @_QMhost_boundsPprefix_depth(
+  ! HOST: fir.call @_QMhost_boundsPscored_depth(
+  ! TARGET: omp.target
+  ! TARGET: omp.parallel
+  ! DEVICE: fir.call @_QMhost_boundsPscored_depth(
+  ! TARGET: omp.wsloop
+  ! TARGET: %[[UB:.*]] = fir.call @_QMhost_boundsPscored_depth(
+  ! TARGET: arith.cmpi sgt, %{{[^,]+}}, %[[UB]]
+  ! TARGET: fir.if
+  ! TARGET: {{hlfir.assign|fir.store}}
+  ! TARGET: return
+  subroutine prefix_depth(n, x)
+    integer :: n, x, i
+    !$omp target map(tofrom: x)
+      !$omp parallel do lastprivate(x)
+      do i = 1, depth_bound(n)
+        x = i
+      end do
+    !$omp end target
+  end subroutine
+end module

>From cdf438b87a548ba473b5cf3e5a45b2128abc08a6 Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Fri, 2 Oct 2026 12:46:12 -0500
Subject: [PATCH 16/23] Fix variant contexts for associated loops and blocks

Associated bounds and BLOCK bodies must use the selected source context.
Stop FUSE bounds before the loop construct, attach standalone BLOCK
bodies during metadirective lowering, and strengthen scoring regressions.
---
 flang/lib/Lower/OpenMP/OpenMP.cpp             | 60 ++++++++++-
 flang/lib/Lower/OpenMP/Utils.cpp              |  6 +-
 .../Todo/metadirective-block-unstructured.f90 | 25 +++++
 .../OpenMP/declare-variant-loop-bounds.f90    |  6 +-
 .../OpenMP/declare-variant-source-context.f90 | 37 ++++++-
 .../OpenMP/metadirective-source-context.f90   | 99 +++++++++++++++++++
 6 files changed, 221 insertions(+), 12 deletions(-)
 create mode 100644 flang/test/Lower/OpenMP/Todo/metadirective-block-unstructured.f90

diff --git a/flang/lib/Lower/OpenMP/OpenMP.cpp b/flang/lib/Lower/OpenMP/OpenMP.cpp
index 7b8a2eee0ecdd..c74f9bf9387ac 100644
--- a/flang/lib/Lower/OpenMP/OpenMP.cpp
+++ b/flang/lib/Lower/OpenMP/OpenMP.cpp
@@ -7092,8 +7092,8 @@ struct SplicedAssociatedEvaluations {
   void suppressEntryBlock(lower::pft::Evaluation &evaluation) {
     assert(!entryEvaluation && evaluation.isNewBlock && evaluation.block &&
            "invalid associated entry evaluation");
-    // Do not let either cloned loop arm enter a function-region block. The
-    // metadirective selection will be placed in this block for an active ENTRY.
+    // Keep selected bodies out of function-region blocks. Place the
+    // metadirective selection in this block for an active ENTRY.
     entryEvaluation = &evaluation;
     entryBlock = evaluation.block;
     evaluation.isNewBlock = false;
@@ -7250,6 +7250,40 @@ static lower::pft::Evaluation *spliceAssociatedDoEval(
   return &eval.getNestedEvaluations().back();
 }
 
+// A standalone metadirective's strictly structured BLOCK is a sibling in the
+// PFT. Attach it while lowering the selected replacement, as for associated DO.
+static lower::pft::Evaluation *
+spliceAssociatedBlockEval(lower::pft::Evaluation &eval,
+                          SplicedAssociatedEvaluations &splicedEvaluations) {
+  const auto *omp = eval.getIf<parser::OpenMPConstruct>();
+  if (omp && std::holds_alternative<parser::OmpDelimitedMetadirectiveDirective>(
+                 omp->u))
+    return nullptr;
+
+  lower::pft::FunctionLikeUnit *owningProc = eval.getOwningProcedure();
+  if (!eval.parentConstruct && !owningProc)
+    return nullptr;
+  auto &parent = eval.parentConstruct
+                     ? eval.parentConstruct->getNestedEvaluations()
+                     : owningProc->evaluationList;
+  auto metaIt = llvm::find_if(
+      parent, [&](lower::pft::Evaluation &e) { return &e == &eval; });
+  assert(metaIt != parent.end() &&
+         "metadirective eval not found in parent list");
+  auto blockIt = std::next(metaIt);
+  if (blockIt == parent.end() || !blockIt->getIf<parser::BlockConstruct>())
+    return nullptr;
+
+  auto &blockStmt = blockIt->getFirstNestedEvaluation();
+  if (blockStmt.isNewBlock && blockStmt.block)
+    splicedEvaluations.suppressEntryBlock(blockStmt);
+  splicedEvaluations.record(parent, blockIt);
+  // Preserve source nesting for construct selectors in the BLOCK.
+  blockIt->parentConstruct = &eval;
+  eval.evaluationList->splice(eval.evaluationList->end(), parent, blockIt);
+  return &eval.getNestedEvaluations().back();
+}
+
 static bool hasContentFollowingAssociatedDo(lower::pft::Evaluation &eval,
                                             lower::pft::Evaluation &loopEval) {
   auto &nested = eval.getNestedEvaluations();
@@ -7520,7 +7554,10 @@ static void genMetadirective(lower::AbstractConverter &converter,
       });
   SplicedAssociatedEvaluations splicedAssociatedEvaluations;
   lower::pft::Evaluation *associatedLoopEval = nullptr;
+  lower::pft::Evaluation *associatedBlockEval = nullptr;
   llvm::scope_exit restoreEvaluationOwnership([&]() {
+    if (associatedBlockEval)
+      associatedBlockEval->parentConstruct = eval.parentConstruct;
     if (eval.hasNestedEvaluations())
       splicedAssociatedEvaluations.restore(eval.getNestedEvaluations());
   });
@@ -7597,6 +7634,18 @@ static void genMetadirective(lower::AbstractConverter &converter,
     }
   }
 
+  if (!hasLoopAssociatedCandidate &&
+      llvm::any_of(reachableVariantSpecs, [](const auto *spec) {
+        return spec && hasDirectiveAssociation(spec->DirId(),
+                                               llvm::omp::Association::Block);
+      }))
+    associatedBlockEval =
+        spliceAssociatedBlockEval(eval, splicedAssociatedEvaluations);
+
+  if (associatedBlockEval && splicedAssociatedEvaluations.getEntryBlock())
+    builder.setInsertionPointToStart(
+        splicedAssociatedEvaluations.getEntryBlock());
+
   auto genMetadirectiveBody = [&]() {
     for (lower::pft::Evaluation &nested : eval.getNestedEvaluations())
       if (!hasLoopAssociatedCandidate ||
@@ -7711,6 +7760,9 @@ static void genMetadirective(lower::AbstractConverter &converter,
            "METADIRECTIVE with both block- and loop-associated variants");
 
     if (consumesBody) {
+      if (associatedBlockEval && associatedBlockEval->lowerAsUnstructured())
+        TODO(variantLoc,
+             "unstructured associated BLOCK in METADIRECTIVE variant");
       mlir::SaveStateStack<OpenMPContextFrame> context{
           converter.getStateStack(), eval, spec->DirId(),
           /*isReplacement=*/true};
@@ -7791,6 +7843,10 @@ static void genMetadirective(lower::AbstractConverter &converter,
            "unstructured associated DO in loop-associated METADIRECTIVE "
            "variant");
 
+    if (associatedBlockEval && associatedBlockEval->lowerAsUnstructured())
+      TODO(converter.genLocation(candidate.dynamicCondition->source),
+           "unstructured associated BLOCK in METADIRECTIVE variant");
+
     mlir::Location condLoc =
         converter.genLocation(candidate.dynamicCondition->source);
     const auto *condExpr =
diff --git a/flang/lib/Lower/OpenMP/Utils.cpp b/flang/lib/Lower/OpenMP/Utils.cpp
index d349ee232b722..7dcd166975639 100644
--- a/flang/lib/Lower/OpenMP/Utils.cpp
+++ b/flang/lib/Lower/OpenMP/Utils.cpp
@@ -1567,8 +1567,10 @@ void collectEnclosingConstructTraits(
   // TARGET TEAMS DISTRIBUTE PARALLEL DO therefore retains TARGET and TEAMS.
   for (llvm::omp::Directive leaf :
        llvm::omp::getLeafConstructsOrSelf(directive)) {
-    if (llvm::omp::getDirectiveAssociation(leaf) ==
-        llvm::omp::Association::LoopNest)
+    llvm::omp::Association association =
+        llvm::omp::getDirectiveAssociation(leaf);
+    if (association == llvm::omp::Association::LoopNest ||
+        association == llvm::omp::Association::LoopSequence)
       break;
     append(leaf);
   }
diff --git a/flang/test/Lower/OpenMP/Todo/metadirective-block-unstructured.f90 b/flang/test/Lower/OpenMP/Todo/metadirective-block-unstructured.f90
new file mode 100644
index 0000000000000..af771eec93297
--- /dev/null
+++ b/flang/test/Lower/OpenMP/Todo/metadirective-block-unstructured.f90
@@ -0,0 +1,25 @@
+! Unstructured BLOCKs need independent PFT block mappings in each region.
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
+! RUN:   -cpp -DSTATIC %s -o - 2>&1 | FileCheck %s
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
+! RUN:   -cpp %s -o - 2>&1 | FileCheck %s
+
+! CHECK: not yet implemented: unstructured associated BLOCK
+! CHECK-SAME: in METADIRECTIVE variant
+subroutine unstructured_block(flag, selector)
+  logical :: flag
+  integer :: selector
+#ifdef STATIC
+  !$omp metadirective when(implementation={vendor(llvm)}: parallel)
+#else
+  !$omp metadirective when(user={condition(flag)}: parallel) &
+  !$omp& otherwise(nothing)
+#endif
+  block
+    go to (10, 20), selector
+10  call first_path()
+    go to 30
+20  call second_path()
+30  continue
+  end block
+end subroutine
diff --git a/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90 b/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
index 002b8f9143b80..696014170f3ff 100644
--- a/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
+++ b/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
@@ -463,7 +463,7 @@ pure integer function depth_bound(n)
     integer, intent(in) :: n
     !$omp declare variant(parallel_depth) match(construct={parallel})
     !$omp declare variant(scored_depth) &
-    !$omp& match(implementation={vendor(score(6): llvm)})
+    !$omp& match(implementation={vendor(score(3): llvm)})
     depth_bound = n
   end function
 
@@ -551,8 +551,8 @@ subroutine composite(n, a)
     end do
   end subroutine
 
-  ! PARALLEL scores 5 and the vendor scores 7 in TARGET, PARALLEL.
-  ! Counting PARALLEL twice raises its score to 9 and selects the wrong variant.
+  ! PARALLEL scores 3 and the vendor scores 4 in TARGET, PARALLEL.
+  ! Counting PARALLEL twice raises its score to at least 5, so it wins.
   ! TARGET-LABEL: func.func @_QMhost_boundsPprefix_depth(
   ! HOST: fir.call @_QMhost_boundsPscored_depth(
   ! TARGET: omp.target
diff --git a/flang/test/Lower/OpenMP/declare-variant-source-context.f90 b/flang/test/Lower/OpenMP/declare-variant-source-context.f90
index 420c649c44456..88f45dda30c7c 100644
--- a/flang/test/Lower/OpenMP/declare-variant-source-context.f90
+++ b/flang/test/Lower/OpenMP/declare-variant-source-context.f90
@@ -1,6 +1,11 @@
 ! RUN: %if x86-registered-target %{ %flang_fc1 -fopenmp \
 ! RUN:   -fopenmp-version=52 -triple x86_64-unknown-linux-gnu \
-! RUN:   -emit-hlfir %s -o - | FileCheck %s %}
+! RUN:   -emit-hlfir %s -o - | FileCheck %s \
+! RUN:   --implicit-check-not='fir.call @_QMsource_contextPbound_arch' %}
+! RUN: %if x86-registered-target %{ %flang_fc1 -fopenmp \
+! RUN:   -fopenmp-version=60 -triple x86_64-unknown-linux-gnu \
+! RUN:   -emit-hlfir %s -o - | FileCheck %s \
+! RUN:   --implicit-check-not='fir.call @_QMsource_contextPbound_arch' %}
 
 module source_context
 contains
@@ -37,14 +42,32 @@ integer function value_arch()
     value_arch = 2
   end function
 
+  integer function bound_base(n)
+    integer, intent(in) :: n
+    !$omp declare variant(bound_vendor) &
+    !$omp& match(implementation={vendor(score(3): llvm)})
+    !$omp declare variant(bound_arch) match(device={arch(x86_64)})
+    bound_base = n
+  end function
+  integer function bound_vendor(n)
+    integer, intent(in) :: n
+    bound_vendor = n
+  end function
+  integer function bound_arch(n)
+    integer, intent(in) :: n
+    bound_arch = n
+  end function
+
+! Bounds exclude TILE in both evaluations; the body includes it.
 ! CHECK-LABEL: func.func @_QMsource_contextPtile_context(
+! CHECK-COUNT-2: fir.call @_QMsource_contextPbound_vendor(
 ! CHECK: fir.call @_QMsource_contextPdepth_arch()
 ! CHECK-NOT: fir.call @_QMsource_contextPdepth_vendor
 ! CHECK: return
   subroutine tile_context(n)
     integer :: n, i
     !$omp tile sizes(2)
-    do i = 1, n
+    do i = 1, bound_base(n)
       call depth_base()
     end do
   end subroutine
@@ -61,18 +84,22 @@ subroutine unroll_context(n)
     end do
   end subroutine
 
+! Bounds exclude FUSE in both evaluations of each loop; the bodies include it.
 ! CHECK-LABEL: func.func @_QMsource_contextPfuse_context(
 ! CHECK-NOT: fir.call @_QMsource_contextPdepth_vendor
-! CHECK-COUNT-2: fir.call @_QMsource_contextPdepth_arch()
+! CHECK-COUNT-2: fir.call @_QMsource_contextPbound_vendor(
+! CHECK: fir.call @_QMsource_contextPdepth_arch()
+! CHECK-COUNT-2: fir.call @_QMsource_contextPbound_vendor(
+! CHECK: fir.call @_QMsource_contextPdepth_arch()
 ! CHECK-NOT: fir.call @_QMsource_contextPdepth_vendor
 ! CHECK: return
   subroutine fuse_context(n)
     integer :: n, i, j
     !$omp fuse
-    do i = 1, n
+    do i = 1, bound_base(n)
       call depth_base()
     end do
-    do j = 1, n
+    do j = 1, bound_base(n)
       call depth_base()
     end do
     !$omp end fuse
diff --git a/flang/test/Lower/OpenMP/metadirective-source-context.f90 b/flang/test/Lower/OpenMP/metadirective-source-context.f90
index 9472dd6e92afc..e0804337f4295 100644
--- a/flang/test/Lower/OpenMP/metadirective-source-context.f90
+++ b/flang/test/Lower/OpenMP/metadirective-source-context.f90
@@ -1,6 +1,9 @@
 ! RUN: %if x86-registered-target %{ %flang_fc1 -fopenmp \
 ! RUN:   -fopenmp-version=52 -triple x86_64-unknown-linux-gnu \
 ! RUN:   -emit-hlfir %s -o - | FileCheck %s %}
+! RUN: %if x86-registered-target %{ %flang_fc1 -fopenmp \
+! RUN:   -fopenmp-version=52 -triple x86_64-unknown-linux-gnu \
+! RUN:   -emit-fir %s -o - | FileCheck %s %}
 
 ! Executable loop transformations contribute to the construct context.
 ! CHECK-LABEL: func.func @_QPtile_context(
@@ -66,3 +69,99 @@ subroutine combined_context(n)
     !$omp& when(construct={parallel}: taskyield)
   end do
 end subroutine
+
+! A standalone replacement owns the following BLOCK and its nested context.
+! CHECK-LABEL: func.func @_QPstandalone_block()
+! CHECK: fir.call @_QPbefore_block
+! CHECK: omp.parallel
+! CHECK: omp.barrier
+! CHECK: omp.single
+! CHECK: omp.taskyield
+! CHECK: omp.terminator
+! CHECK: omp.terminator
+! CHECK-NOT: omp.parallel
+! CHECK-NOT: omp.barrier
+! CHECK-NOT: omp.taskyield
+! CHECK: return
+subroutine standalone_block()
+  call before_block()
+  !$omp metadirective when(implementation={vendor(llvm)}: parallel)
+  block
+    !$omp metadirective when(construct={parallel}: barrier)
+    !$omp single
+      !$omp metadirective &
+      !$omp& when(construct={parallel, single}: taskyield)
+    !$omp end single
+  end block
+  !$omp metadirective when(construct={parallel}: taskyield)
+end subroutine
+
+! Each runtime alternative lowers the BLOCK once, with its own context.
+! CHECK-LABEL: func.func @_QPruntime_block(
+! CHECK: fir.if
+! CHECK: omp.parallel
+! CHECK: fir.call @_QPuse_block_local
+! CHECK-NOT: fir.call @_QPuse_block_local
+! CHECK: omp.taskyield
+! CHECK: omp.terminator
+! CHECK-NOT: fir.call @_QPuse_block_local
+! CHECK: } else {
+! CHECK-NOT: omp.parallel
+! CHECK-NOT: omp.taskyield
+! CHECK: fir.call @_QPuse_block_local
+! CHECK-NOT: fir.call @_QPuse_block_local
+! CHECK-NOT: omp.parallel
+! CHECK-NOT: omp.taskyield
+! CHECK: return
+subroutine runtime_block(flag)
+  logical :: flag
+  !$omp metadirective when(user={condition(flag)}: parallel) &
+  !$omp& otherwise(nothing)
+  block
+    integer :: local
+    local = 1
+    call use_block_local(local)
+    !$omp metadirective when(construct={parallel}: taskyield)
+  end block
+  !$omp metadirective when(construct={parallel}: taskyield)
+end subroutine
+
+! An empty delimited replacement must not capture the following BLOCK.
+! CHECK-LABEL: func.func @_QPempty_delimited_block()
+! CHECK-NOT: omp.parallel
+! CHECK: fir.call @_QPafter_delimited
+! CHECK-NOT: omp.taskyield
+! CHECK-NOT: fir.call @_QPafter_delimited
+! CHECK: return
+subroutine empty_delimited_block()
+  !$omp begin metadirective &
+  !$omp& when(implementation={vendor(llvm)}: parallel)
+  !$omp end metadirective
+  block
+    call after_delimited()
+    !$omp metadirective when(construct={parallel}: taskyield)
+  end block
+end subroutine
+
+! The selected PARALLEL makes the inner SIMD variant lose to NOTHING.
+! CHECK-LABEL: func.func @_QPstandalone_block_loop(
+! CHECK: omp.parallel
+! CHECK-NOT: omp.simd
+! CHECK: fir.do_loop
+! CHECK: omp.terminator
+! CHECK-NOT: fir.do_loop
+! CHECK-NOT: omp.simd
+! CHECK: return
+subroutine standalone_block_loop(n)
+  integer :: n, i
+  !$omp metadirective when(implementation={vendor(llvm)}: parallel) &
+  !$omp& otherwise(nothing)
+  block
+    !$omp metadirective &
+    !$omp& when(construct={parallel}: nothing) &
+    !$omp& when(implementation={vendor(score(0): llvm)}: simd collapse(1)) &
+    !$omp& otherwise(nothing)
+    do i = 1, n
+    end do
+  end block
+end subroutine

>From 439156e99e012cb8e0ce89da7c16ceed3e34008c Mon Sep 17 00:00:00 2001
From: "Chi-Chun, Chen" <chichun.chen at hpe.com>
Date: Fri, 2 Oct 2026 12:58:55 -0500
Subject: [PATCH 17/23] Simplify variant context, candidate handling and
 consolidate variant selection tests

---
 clang/lib/Sema/SemaOpenMP.cpp                 |  43 +-----
 clang/test/OpenMP/declare_variant_scoring.c   |   9 +-
 flang/include/flang/Semantics/openmp-utils.h  |  17 +--
 flang/lib/Lower/OpenMP/OpenMP.cpp             | 129 ++++++----------
 flang/lib/Lower/OpenMP/Utils.cpp              |  32 ++--
 flang/lib/Semantics/openmp-utils.cpp          |  87 +++--------
 .../OpenMP/declare-variant-loop-bounds.f90    |   6 +-
 .../declare-variant-metadirective-context.f90 | 144 +++++++++++++++---
 .../OpenMP/declare-variant-source-context.f90 | 140 -----------------
 .../OpenMP/variant-loop-context-depth.f90     | 127 ---------------
 ...context.f90 => variant-source-context.f90} | 144 +++++++++++++++++-
 .../include/llvm/Frontend/OpenMP/OMPContext.h |   5 +
 llvm/lib/Frontend/OpenMP/OMPContext.cpp       |  38 +++++
 13 files changed, 401 insertions(+), 520 deletions(-)
 delete mode 100644 flang/test/Lower/OpenMP/declare-variant-source-context.f90
 delete mode 100644 flang/test/Lower/OpenMP/variant-loop-context-depth.f90
 rename flang/test/Lower/OpenMP/{metadirective-source-context.f90 => variant-source-context.f90} (52%)

diff --git a/clang/lib/Sema/SemaOpenMP.cpp b/clang/lib/Sema/SemaOpenMP.cpp
index 67d3ba31a96d5..901525cdfe9e7 100644
--- a/clang/lib/Sema/SemaOpenMP.cpp
+++ b/clang/lib/Sema/SemaOpenMP.cpp
@@ -4780,48 +4780,7 @@ class DSAAttrChecker final : public StmtVisitor<DSAAttrChecker, void> {
 static void handleDeclareVariantConstructTrait(DSAStackTy *Stack,
                                                OpenMPDirectiveKind DKind,
                                                bool ScopeEntry) {
-  SmallVector<llvm::omp::TraitProperty, 8> Traits;
-  // Update the enclosing construct stack for declare variant matching and
-  // scoring on region entry or exit. Record each directive's constructs in
-  // nesting order, using placeholders for constructs without selector
-  // properties so they still contribute to scoring positions and depth.
-  for (OpenMPDirectiveKind Leaf : getLeafConstructsOrSelf(DKind)) {
-    if (llvm::omp::getDirectiveCategory(Leaf) ==
-        llvm::omp::Category::Informational)
-      continue;
-    switch (Leaf) {
-    case OMPD_target:
-      Traits.push_back(llvm::omp::TraitProperty::construct_target_target);
-      break;
-    case OMPD_teams:
-      Traits.push_back(llvm::omp::TraitProperty::construct_teams_teams);
-      break;
-    case OMPD_parallel:
-      Traits.push_back(llvm::omp::TraitProperty::construct_parallel_parallel);
-      break;
-    case OMPD_for:
-      Traits.push_back(llvm::omp::TraitProperty::construct_for_for);
-      break;
-    case OMPD_simd:
-      Traits.push_back(llvm::omp::TraitProperty::construct_simd_simd);
-      break;
-    case OMPD_section:
-      // SECTION separates blocks within SECTIONS and adds no construct level.
-      // Do not add a placeholder: spelling the optional first SECTION must
-      // not change variant scores.
-      break;
-    case OMPD_dispatch:
-      // OpenMP allows omitting DISPATCH from the construct context. Keep it
-      // omitted here: adding it for the whole region would also affect calls
-      // in arguments, but the trait may apply only to the target call.
-      break;
-    default:
-      // Constructs without a selector property still affect scoring depth.
-      Traits.push_back(llvm::omp::TraitProperty::invalid);
-      break;
-    }
-  }
-  Stack->handleConstructTrait(Traits, ScopeEntry);
+  Stack->handleConstructTrait(llvm::omp::getConstructTraits(DKind), ScopeEntry);
 }
 
 static SmallVector<SemaOpenMP::CapturedParamNameType>
diff --git a/clang/test/OpenMP/declare_variant_scoring.c b/clang/test/OpenMP/declare_variant_scoring.c
index ea99df0fee2c8..1f8ca37cfa359 100644
--- a/clang/test/OpenMP/declare_variant_scoring.c
+++ b/clang/test/OpenMP/declare_variant_scoring.c
@@ -32,11 +32,6 @@ void depth_base(void);
 #pragma omp declare variant(parallel_variant) match(construct = {parallel})
 void construct_base(void);
 
-#pragma omp declare variant(cpu_variant) match(device = {kind(cpu)})
-#pragma omp declare variant(scored_variant) \
-    match(implementation = {vendor(score(3) : llvm)})
-void task_depth_base(void);
-
 #pragma omp declare variant(arch_variant) match(device = {arch(x86_64)})
 #pragma omp declare variant(scored_variant) \
     match(implementation = {vendor(score(3) : llvm)})
@@ -200,7 +195,7 @@ void task_depth(void) {
 #pragma omp parallel
   {
 #pragma omp task
-    { task_depth_base(); }
+    { target_base(); }
   }
 }
 // CHECK-LABEL: define internal {{.*}}i32 @.omp_task_entry.
@@ -241,7 +236,7 @@ void assume_context(void) {
 #pragma omp parallel
   {
 #pragma omp assume no_openmp_routines
-    { task_depth_base(); }
+    { target_base(); }
   }
 }
 // CHECK-LABEL: define internal void @assume_context.omp_outlined
diff --git a/flang/include/flang/Semantics/openmp-utils.h b/flang/include/flang/Semantics/openmp-utils.h
index ff31591eb71b1..ecf617664ff0c 100644
--- a/flang/include/flang/Semantics/openmp-utils.h
+++ b/flang/include/flang/Semantics/openmp-utils.h
@@ -284,12 +284,13 @@ struct MetadirectiveCandidate {
   bool isExplicit{false};
   std::optional<DynamicUserCondition> dynamicCondition;
   bool conditionShouldBeTrue{true};
-  /// Position in the original candidate ordering, including subset scores.
-  unsigned rank{0};
 };
 
 struct MetadirectiveCandidateSet {
   llvm::SmallVector<MetadirectiveCandidate, 4> candidates;
+  /// Candidate indices in selection order, including global subset scores.
+  /// Filtering this list must preserve its order without recomputing scores.
+  llvm::SmallVector<unsigned, 4> order;
   /// Null represents either an explicit NOTHING fallback or no fallback.
   const parser::OmpDirectiveSpecification *fallback{nullptr};
 };
@@ -302,11 +303,6 @@ std::optional<MetadirectiveCandidateSet> BuildMetadirectiveCandidateSet(
     const parser::OmpClauseList &clauses, SemanticsContext &context,
     const OmpVariantMatchContext &matchContext);
 
-/// Select the first remaining candidate in the original ranking.
-std::optional<unsigned> SelectBestMetadirectiveCandidate(
-    llvm::ArrayRef<unsigned> candidateIndices,
-    llvm::ArrayRef<MetadirectiveCandidate> candidates);
-
 /// Return true when repeated evaluation of \p condition cannot call a
 /// procedure or observe asynchronously changing state.
 bool IsRepeatableMetadirectiveCondition(
@@ -316,10 +312,11 @@ bool IsRepeatableMetadirectiveCondition(
 bool AreSameRepeatableMetadirectiveCondition(const parser::ScalarExpr &left,
     const parser::ScalarExpr &right, SemanticsContext &context);
 
-/// Return candidates reachable after \p selectedIndex fails. Equal repeatable
-/// guards are pruned until a non-repeatable guard is encountered.
+/// Return candidates reachable after the first candidate fails. The nonempty
+/// \p candidateIndices list must be in selection order. Equal repeatable guards
+/// are pruned until an unguarded or non-repeatable candidate is encountered.
 llvm::SmallVector<unsigned, 4> GetMetadirectiveElsePathCandidates(
-    unsigned selectedIndex, llvm::ArrayRef<unsigned> candidateIndices,
+    llvm::ArrayRef<unsigned> candidateIndices,
     llvm::ArrayRef<MetadirectiveCandidate> candidates,
     SemanticsContext &context);
 
diff --git a/flang/lib/Lower/OpenMP/OpenMP.cpp b/flang/lib/Lower/OpenMP/OpenMP.cpp
index c74f9bf9387ac..f22aca859cd49 100644
--- a/flang/lib/Lower/OpenMP/OpenMP.cpp
+++ b/flang/lib/Lower/OpenMP/OpenMP.cpp
@@ -7155,25 +7155,24 @@ isUnsupportedMetadirectiveLoopAssociationEval(lower::pft::Evaluation &eval) {
          (eval.isDirective() && !eval.isExecutableDirective());
 }
 
-/// A loop-associated metadirective is lowered like a real loop construct, but
-/// the PFT leaves its associated loop nest as the following sibling instead of
-/// nesting it underneath. Splice that sibling into the metadirective's own
-/// nested evaluations so the shared loop-lowering path can find it. Return
-/// nullptr if no associated DO loop follows.
-static lower::pft::Evaluation *spliceAssociatedDoEval(
+// The PFT leaves associated DO and BLOCK constructs as siblings. Attach them
+// while lowering the selected replacement and restore them afterwards.
+template <typename Construct>
+static lower::pft::Evaluation *spliceAssociatedEval(
     lower::pft::Evaluation &eval,
     SplicedAssociatedEvaluations *splicedEvaluations = nullptr,
     lower::pft::Evaluation **unsupportedInterveningEval = nullptr) {
+  constexpr bool isLoop = std::is_same_v<Construct, parser::DoConstruct>;
   if (unsupportedInterveningEval)
     *unsupportedInterveningEval = nullptr;
 
-  if (eval.hasNestedEvaluations()) {
+  if (isLoop && eval.hasNestedEvaluations()) {
     auto nestedIt =
         llvm::find_if(eval.getNestedEvaluations(), [](auto &nested) {
           return !isIgnorableMetadirectiveLoopAssociationEval(nested);
         });
     if (nestedIt != eval.getNestedEvaluations().end()) {
-      if (nestedIt->getIf<parser::DoConstruct>())
+      if (nestedIt->template getIf<Construct>())
         return &*nestedIt;
       if (unsupportedInterveningEval &&
           isUnsupportedMetadirectiveLoopAssociationEval(*nestedIt))
@@ -7183,7 +7182,7 @@ static lower::pft::Evaluation *spliceAssociatedDoEval(
   }
 
   // A delimited metadirective owns only its nested evaluations. An empty body
-  // must not capture a following sibling loop as its associated DO.
+  // must not capture a following sibling construct.
   if (const auto *omp = eval.getIf<parser::OpenMPConstruct>();
       omp && std::holds_alternative<parser::OmpDelimitedMetadirectiveDirective>(
                  omp->u))
@@ -7203,41 +7202,41 @@ static lower::pft::Evaluation *spliceAssociatedDoEval(
          "metadirective eval not found in parent list");
 
   auto firstAssociatedIt = std::next(metaIt);
-  auto loopIt = firstAssociatedIt;
-  while (loopIt != parentList->end() &&
-         isIgnorableMetadirectiveLoopAssociationEval(*loopIt))
-    ++loopIt;
+  auto associatedIt = firstAssociatedIt;
+  while (isLoop && associatedIt != parentList->end() &&
+         isIgnorableMetadirectiveLoopAssociationEval(*associatedIt))
+    ++associatedIt;
 
-  if (loopIt == parentList->end())
+  if (associatedIt == parentList->end())
     return nullptr;
-  if (!loopIt->getIf<parser::DoConstruct>()) {
+  if (!associatedIt->template getIf<Construct>()) {
     if (unsupportedInterveningEval &&
-        isUnsupportedMetadirectiveLoopAssociationEval(*loopIt))
-      *unsupportedInterveningEval = &*loopIt;
+        isUnsupportedMetadirectiveLoopAssociationEval(*associatedIt))
+      *unsupportedInterveningEval = &*associatedIt;
     return nullptr;
   }
 
   if (splicedEvaluations) {
     auto entryIt =
-        llvm::find_if(llvm::make_range(firstAssociatedIt, loopIt),
+        llvm::find_if(llvm::make_range(firstAssociatedIt, associatedIt),
                       [](lower::pft::Evaluation &candidate) {
                         return candidate.isNewBlock && candidate.block;
                       });
-    if (entryIt != loopIt) {
+    if (entryIt != associatedIt) {
       splicedEvaluations->suppressEntryBlock(*entryIt);
     } else {
-      lower::pft::Evaluation &doStmt = loopIt->getFirstNestedEvaluation();
-      if (doStmt.isNewBlock && doStmt.block)
-        splicedEvaluations->suppressEntryBlock(doStmt);
+      auto &stmt = associatedIt->getFirstNestedEvaluation();
+      if (stmt.isNewBlock && stmt.block)
+        splicedEvaluations->suppressEntryBlock(stmt);
     }
   }
 
   // Compiler directives between the metadirective and its associated loop
   // must be processed before the loop is lowered. Move them with the loop so
   // they are not visited later as siblings of the metadirective.
-  for (auto it = firstAssociatedIt; it != loopIt;) {
+  for (auto it = firstAssociatedIt; it != associatedIt;) {
     auto current = it++;
-    if (current->getIf<parser::CompilerDirective>()) {
+    if (current->template getIf<parser::CompilerDirective>()) {
       if (splicedEvaluations)
         splicedEvaluations->record(*parentList, current);
       eval.evaluationList->splice(eval.evaluationList->end(), *parentList,
@@ -7245,42 +7244,12 @@ static lower::pft::Evaluation *spliceAssociatedDoEval(
     }
   }
   if (splicedEvaluations)
-    splicedEvaluations->record(*parentList, loopIt);
-  eval.evaluationList->splice(eval.evaluationList->end(), *parentList, loopIt);
-  return &eval.getNestedEvaluations().back();
-}
-
-// A standalone metadirective's strictly structured BLOCK is a sibling in the
-// PFT. Attach it while lowering the selected replacement, as for associated DO.
-static lower::pft::Evaluation *
-spliceAssociatedBlockEval(lower::pft::Evaluation &eval,
-                          SplicedAssociatedEvaluations &splicedEvaluations) {
-  const auto *omp = eval.getIf<parser::OpenMPConstruct>();
-  if (omp && std::holds_alternative<parser::OmpDelimitedMetadirectiveDirective>(
-                 omp->u))
-    return nullptr;
-
-  lower::pft::FunctionLikeUnit *owningProc = eval.getOwningProcedure();
-  if (!eval.parentConstruct && !owningProc)
-    return nullptr;
-  auto &parent = eval.parentConstruct
-                     ? eval.parentConstruct->getNestedEvaluations()
-                     : owningProc->evaluationList;
-  auto metaIt = llvm::find_if(
-      parent, [&](lower::pft::Evaluation &e) { return &e == &eval; });
-  assert(metaIt != parent.end() &&
-         "metadirective eval not found in parent list");
-  auto blockIt = std::next(metaIt);
-  if (blockIt == parent.end() || !blockIt->getIf<parser::BlockConstruct>())
-    return nullptr;
-
-  auto &blockStmt = blockIt->getFirstNestedEvaluation();
-  if (blockStmt.isNewBlock && blockStmt.block)
-    splicedEvaluations.suppressEntryBlock(blockStmt);
-  splicedEvaluations.record(parent, blockIt);
-  // Preserve source nesting for construct selectors in the BLOCK.
-  blockIt->parentConstruct = &eval;
-  eval.evaluationList->splice(eval.evaluationList->end(), parent, blockIt);
+    splicedEvaluations->record(*parentList, associatedIt);
+  // BLOCK bodies need source ancestry for nested construct selectors.
+  if constexpr (!isLoop)
+    associatedIt->parentConstruct = &eval;
+  eval.evaluationList->splice(eval.evaluationList->end(), *parentList,
+                              associatedIt);
   return &eval.getNestedEvaluations().back();
 }
 
@@ -7538,11 +7507,6 @@ static void genMetadirective(lower::AbstractConverter &converter,
   auto &candidates = candidateSet->candidates;
   const parser::OmpDirectiveSpecification *fallback = candidateSet->fallback;
 
-  llvm::SmallVector<unsigned, 4> allCandidateIndices;
-  allCandidateIndices.reserve(candidates.size());
-  for (unsigned idx = 0, end = candidates.size(); idx < end; ++idx)
-    allCandidateIndices.push_back(idx);
-
   llvm::SmallVector<const parser::OmpDirectiveSpecification *, 4>
       reachableVariantSpecs = semantics::omp::GetReachableMetadirectiveVariants(
           *candidateSet, semaCtx);
@@ -7563,8 +7527,10 @@ static void genMetadirective(lower::AbstractConverter &converter,
   });
   if (hasLoopAssociatedCandidate) {
     lower::pft::Evaluation *unsupportedInterveningEval = nullptr;
-    if (lower::pft::Evaluation *loopEval = spliceAssociatedDoEval(
-            eval, &splicedAssociatedEvaluations, &unsupportedInterveningEval)) {
+    if (lower::pft::Evaluation *loopEval =
+            spliceAssociatedEval<parser::DoConstruct>(
+                eval, &splicedAssociatedEvaluations,
+                &unsupportedInterveningEval)) {
       associatedLoopEval = loopEval;
       if (lower::pft::FunctionLikeUnit *owningProc =
               eval.getOwningProcedure()) {
@@ -7639,8 +7605,8 @@ static void genMetadirective(lower::AbstractConverter &converter,
         return spec && hasDirectiveAssociation(spec->DirId(),
                                                llvm::omp::Association::Block);
       }))
-    associatedBlockEval =
-        spliceAssociatedBlockEval(eval, splicedAssociatedEvaluations);
+    associatedBlockEval = spliceAssociatedEval<parser::BlockConstruct>(
+        eval, &splicedAssociatedEvaluations);
 
   if (associatedBlockEval && splicedAssociatedEvaluations.getEntryBlock())
     builder.setInsertionPointToStart(
@@ -7708,7 +7674,8 @@ static void genMetadirective(lower::AbstractConverter &converter,
       if (!enableDelayedPrivatization)
         TODO(variantLoc,
              "loop-associated METADIRECTIVE with eager privatization");
-      lower::pft::Evaluation *loopEval = spliceAssociatedDoEval(eval);
+      lower::pft::Evaluation *loopEval =
+          spliceAssociatedEval<parser::DoConstruct>(eval);
       if (!loopEval)
         TODO(variantLoc, "loop-associated METADIRECTIVE without associated DO");
       if (hasContentFollowingAssociatedDo(eval, *loopEval))
@@ -7778,7 +7745,7 @@ static void genMetadirective(lower::AbstractConverter &converter,
       genMetadirectiveBody();
   };
 
-  llvm::SmallVector<unsigned, 4> remainingCandidates{allCandidateIndices};
+  llvm::SmallVector<unsigned, 4> remainingCandidates{candidateSet->order};
 
   lower::StatementContext stmtCtx;
 
@@ -7798,24 +7765,16 @@ static void genMetadirective(lower::AbstractConverter &converter,
   // If the else path selects the same unguarded directive, lower it directly.
   // Stop when selection reaches an unguarded candidate or the fallback.
   while (!remainingCandidates.empty()) {
-    std::optional<unsigned> selected =
-        semantics::omp::SelectBestMetadirectiveCandidate(remainingCandidates,
-                                                         candidates);
-    if (!selected) {
-      genVariant(fallback);
-      return;
-    }
-
     const semantics::omp::MetadirectiveCandidate &candidate =
-        candidates[*selected];
+        candidates[remainingCandidates.front()];
     if (!candidate.dynamicCondition) {
       genVariant(candidate.spec);
       return;
     }
 
     llvm::SmallVector<unsigned, 4> elsePathCandidates =
-        semantics::omp::GetMetadirectiveElsePathCandidates(
-            *selected, remainingCandidates, candidates, semaCtx);
+        semantics::omp::GetMetadirectiveElsePathCandidates(remainingCandidates,
+                                                           candidates, semaCtx);
 
     // match_any may create a guarded condition-true candidate and an unguarded
     // static candidate for the same directive. If the else path picks the
@@ -7823,11 +7782,9 @@ static void genMetadirective(lower::AbstractConverter &converter,
     //
     //   if (flag) barrier    into just    barrier
     //   else barrier
-    if (std::optional<unsigned> selectedInElse =
-            semantics::omp::SelectBestMetadirectiveCandidate(elsePathCandidates,
-                                                             candidates)) {
+    if (!elsePathCandidates.empty()) {
       const semantics::omp::MetadirectiveCandidate &candidateInElse =
-          candidates[*selectedInElse];
+          candidates[elsePathCandidates.front()];
       if (!candidateInElse.dynamicCondition &&
           candidateInElse.spec == candidate.spec) {
         genVariant(candidate.spec);
diff --git a/flang/lib/Lower/OpenMP/Utils.cpp b/flang/lib/Lower/OpenMP/Utils.cpp
index 7dcd166975639..ac158ebc02b92 100644
--- a/flang/lib/Lower/OpenMP/Utils.cpp
+++ b/flang/lib/Lower/OpenMP/Utils.cpp
@@ -1517,26 +1517,25 @@ void collectEnclosingConstructTraits(
   auto append = [&](llvm::omp::Directive directive) {
     semantics::omp::AppendDirectiveContextTraits(directive, constructTraits);
   };
+  auto getDirective = [&](const pft::Evaluation &eval,
+                          const parser::OpenMPConstruct &omp) {
+    llvm::omp::Directive directive = parser::omp::GetOmpDirectiveName(omp).v;
+    if (directive == llvm::omp::Directive::OMPD_metadirective)
+      for (const OpenMPContextFrame *frame : frames)
+        if (&frame->evaluation == &eval && frame->isReplacement)
+          return frame->directive;
+    return directive;
+  };
   for (const pft::Evaluation *ancestor : ancestors) {
     const auto *omp = ancestor->getIf<parser::OpenMPConstruct>();
     if (!omp)
       continue;
-    llvm::omp::Directive directive{parser::omp::GetOmpDirectiveName(*omp).v};
     // An ancestor supplies the full source context, including constituents
     // whose bodies also have active frames. Count each construct only once.
     for (auto [index, frame] : llvm::enumerate(frames))
       if (&frame->evaluation == ancestor)
         usedFrames[index] = true;
-    if (directive != llvm::omp::Directive::OMPD_metadirective) {
-      append(directive);
-      continue;
-    }
-    for (const OpenMPContextFrame *frame : frames) {
-      if (&frame->evaluation == ancestor && frame->isReplacement) {
-        append(frame->directive);
-        break;
-      }
-    }
+    append(getDirective(*ancestor, *omp));
   }
 
   // Use entered frames for clauses. Loop bounds use the source prefix below,
@@ -1554,19 +1553,10 @@ void collectEnclosingConstructTraits(
   const auto *omp = evaluation->getIf<parser::OpenMPConstruct>();
   if (!omp)
     return;
-  llvm::omp::Directive directive = parser::omp::GetOmpDirectiveName(*omp).v;
-  if (directive == llvm::omp::Directive::OMPD_metadirective) {
-    for (const OpenMPContextFrame *frame : frames) {
-      if (&frame->evaluation == evaluation && frame->isReplacement) {
-        directive = frame->directive;
-        break;
-      }
-    }
-  }
   // Bounds precede the first loop-associated constituent in source order.
   // TARGET TEAMS DISTRIBUTE PARALLEL DO therefore retains TARGET and TEAMS.
   for (llvm::omp::Directive leaf :
-       llvm::omp::getLeafConstructsOrSelf(directive)) {
+       llvm::omp::getLeafConstructsOrSelf(getDirective(*evaluation, *omp))) {
     llvm::omp::Association association =
         llvm::omp::getDirectiveAssociation(leaf);
     if (association == llvm::omp::Association::LoopNest ||
diff --git a/flang/lib/Semantics/openmp-utils.cpp b/flang/lib/Semantics/openmp-utils.cpp
index adbdeec5f95c3..106beed7b98a2 100644
--- a/flang/lib/Semantics/openmp-utils.cpp
+++ b/flang/lib/Semantics/openmp-utils.cpp
@@ -2477,31 +2477,11 @@ static void AppendConstructTraitsForDirective(
 
 void AppendDirectiveContextTraits(llvm::omp::Directive directive,
     llvm::SmallVectorImpl<llvm::omp::TraitProperty> &constructTraits) {
-  using llvm::omp::Directive;
-  using llvm::omp::TraitProperty;
-
-  for (Directive leaf : llvm::omp::getLeafConstructsOrSelf(directive)) {
-    if (leaf == Directive::OMPD_nothing ||
-        leaf == Directive::OMPD_metadirective ||
-        leaf == Directive::OMPD_unknown || leaf == Directive::OMPD_section ||
-        leaf == Directive::OMPD_dispatch ||
-        llvm::omp::getDirectiveCategory(leaf) ==
-            llvm::omp::Category::Informational) {
-      continue;
-    }
-
-    llvm::omp::VariantMatchInfo vmi;
-    AppendConstructTraitsForDirective(leaf, vmi);
-    if (vmi.RequiredTraits.test(
-            static_cast<unsigned>(TraitProperty::construct_target_target))) {
+  for (llvm::omp::TraitProperty trait :
+      llvm::omp::getConstructTraits(directive)) {
+    if (trait == llvm::omp::TraitProperty::construct_target_target)
       constructTraits.clear();
-    }
-    if (vmi.ConstructTraits.empty()) {
-      constructTraits.push_back(TraitProperty::invalid);
-    } else {
-      constructTraits.append(
-          vmi.ConstructTraits.begin(), vmi.ConstructTraits.end());
-    }
+    constructTraits.push_back(trait);
   }
 }
 
@@ -2836,13 +2816,12 @@ std::optional<MetadirectiveCandidateSet> BuildMetadirectiveCandidateSet(
   // Rank against the complete set once. Removing a failed runtime guard must
   // not restore the raw score of a selector that was its strict subset.
   llvm::SmallVector<llvm::omp::VariantMatchInfo, 4> vmis;
-  llvm::SmallVector<unsigned, 4> order;
   for (const auto &[index, candidate] : llvm::enumerate(result.candidates)) {
     vmis.push_back(candidate.vmi);
-    order.push_back(index);
+    result.order.push_back(index);
   }
   auto scores{llvm::omp::getVariantMatchScores(vmis, matchContext)};
-  llvm::stable_sort(order, [&](unsigned a, unsigned b) {
+  llvm::stable_sort(result.order, [&](unsigned a, unsigned b) {
     CHECK(scores[a] && scores[b]);
     const auto &left{*scores[a]}, &right{*scores[b]};
     unsigned width{std::max(left.getBitWidth(), right.getBitWidth())};
@@ -2850,21 +2829,9 @@ std::optional<MetadirectiveCandidateSet> BuildMetadirectiveCandidateSet(
       return left.zextOrTrunc(width).ugt(right.zextOrTrunc(width));
     return result.candidates[a].isExplicit && !result.candidates[b].isExplicit;
   });
-  for (auto [rank, index] : llvm::enumerate(order))
-    result.candidates[index].rank = rank;
   return result;
 }
 
-std::optional<unsigned> SelectBestMetadirectiveCandidate(
-    llvm::ArrayRef<unsigned> candidateIndices,
-    llvm::ArrayRef<MetadirectiveCandidate> candidates) {
-  if (candidateIndices.empty())
-    return std::nullopt;
-  return *llvm::min_element(candidateIndices, [&](unsigned a, unsigned b) {
-    return candidates[a].rank < candidates[b].rank;
-  });
-}
-
 namespace {
 struct MetadirectiveConditionNormalizer : evaluate::rewrite::Identity {
   using evaluate::rewrite::Identity::operator();
@@ -2927,28 +2894,23 @@ bool AreSameRepeatableMetadirectiveCondition(const parser::ScalarExpr &left,
 }
 
 llvm::SmallVector<unsigned, 4> GetMetadirectiveElsePathCandidates(
-    unsigned selectedIndex, llvm::ArrayRef<unsigned> candidateIndices,
+    llvm::ArrayRef<unsigned> candidateIndices,
     llvm::ArrayRef<MetadirectiveCandidate> candidates,
     SemanticsContext &context) {
-  CHECK(selectedIndex < candidates.size());
-  const MetadirectiveCandidate &selected{candidates[selectedIndex]};
+  CHECK(!candidateIndices.empty() &&
+      candidateIndices.front() < candidates.size());
+  const MetadirectiveCandidate &selected{candidates[candidateIndices.front()]};
   CHECK(selected.dynamicCondition);
 
-  llvm::SmallVector<unsigned, 4> result;
-  result.reserve(candidateIndices.size());
-  for (unsigned index : candidateIndices)
-    if (index != selectedIndex)
-      result.push_back(index);
+  llvm::SmallVector<unsigned, 4> result{candidateIndices.drop_front()};
 
   // Inspect candidates in the order in which selection would evaluate them.
   // A distinct repeatable condition cannot modify the selected condition, so
   // the failed value remains usable past it. Stop at the first non-repeatable
   // condition because it can change state before a lower-ranked occurrence is
   // evaluated.
-  llvm::SmallVector<unsigned, 4> candidatesToInspect{result};
-  while (std::optional<unsigned> next{
-      SelectBestMetadirectiveCandidate(candidatesToInspect, candidates)}) {
-    const MetadirectiveCandidate &candidate{candidates[*next]};
+  for (unsigned index : candidateIndices.drop_front()) {
+    const MetadirectiveCandidate &candidate{candidates[index]};
     if (!candidate.dynamicCondition ||
         !IsRepeatableMetadirectiveCondition(
             *candidate.dynamicCondition->expr, context))
@@ -2960,8 +2922,7 @@ llvm::SmallVector<unsigned, 4> GetMetadirectiveElsePathCandidates(
             *selected.dynamicCondition->expr, *candidate.dynamicCondition->expr,
             context)};
     if (hasSameFailedCondition)
-      llvm::erase(result, *next);
-    llvm::erase(candidatesToInspect, *next);
+      llvm::erase(result, index);
   }
   return result;
 }
@@ -2969,22 +2930,17 @@ llvm::SmallVector<unsigned, 4> GetMetadirectiveElsePathCandidates(
 llvm::SmallVector<const parser::OmpDirectiveSpecification *, 4>
 GetReachableMetadirectiveVariants(
     const MetadirectiveCandidateSet &candidateSet, SemanticsContext &context) {
-  llvm::SmallVector<unsigned, 4> candidates;
-  candidates.reserve(candidateSet.candidates.size());
-  for (unsigned index{0}; index < candidateSet.candidates.size(); ++index) {
-    candidates.push_back(index);
-  }
+  llvm::SmallVector<unsigned, 4> candidates{candidateSet.order};
 
   llvm::SmallVector<const parser::OmpDirectiveSpecification *, 4> reachable;
   while (true) {
-    std::optional<unsigned> selected{
-        SelectBestMetadirectiveCandidate(candidates, candidateSet.candidates)};
-    if (!selected) {
+    if (candidates.empty()) {
       reachable.push_back(candidateSet.fallback);
       break;
     }
 
-    const MetadirectiveCandidate &candidate{candidateSet.candidates[*selected]};
+    const MetadirectiveCandidate &candidate{
+        candidateSet.candidates[candidates.front()]};
     reachable.push_back(candidate.spec);
     // An unguarded winner ends selection. A dynamic winner leaves the
     // remaining candidates reachable through its false path.
@@ -2993,12 +2949,11 @@ GetReachableMetadirectiveVariants(
     }
 
     candidates = GetMetadirectiveElsePathCandidates(
-        *selected, candidates, candidateSet.candidates, context);
+        candidates, candidateSet.candidates, context);
 
-    if (std::optional<unsigned> selectedInElse{SelectBestMetadirectiveCandidate(
-            candidates, candidateSet.candidates)}) {
+    if (!candidates.empty()) {
       const MetadirectiveCandidate &elseCandidate{
-          candidateSet.candidates[*selectedInElse]};
+          candidateSet.candidates[candidates.front()]};
       if (!elseCandidate.dynamicCondition &&
           elseCandidate.spec == candidate.spec) {
         break;
diff --git a/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90 b/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
index 696014170f3ff..5f80a3815cf6e 100644
--- a/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
+++ b/flang/test/Lower/OpenMP/declare-variant-loop-bounds.f90
@@ -119,12 +119,15 @@ subroutine composite_simd(n, a)
     end do
   end subroutine
 
-  ! A standalone DO does not contribute its own context to its bounds.
+  ! A standalone DO contributes to its body, but not its bounds or later calls.
   ! CHECK-LABEL: func.func @_QMloop_contextPstandalone_do(
   ! CHECK-NOT: fir.call @_QMloop_contextPdo_bound(
   ! CHECK: fir.call @_QMloop_contextPbound(
   ! CHECK: omp.wsloop
   ! CHECK: fir.call @_QMloop_contextPdo_bound(
+  ! CHECK-NOT: fir.call @_QMloop_contextPdo_bound(
+  ! CHECK: fir.call @_QMloop_contextPbound(
+  ! CHECK-NOT: fir.call @_QMloop_contextPdo_bound(
   ! CHECK: return
   subroutine standalone_do(n, a)
     integer, intent(in) :: n
@@ -133,6 +136,7 @@ subroutine standalone_do(n, a)
     do i = 1, bound(n)
       a(i) = bound(n)
     end do
+    a(n) = bound(n)
   end subroutine
 
   pure integer function cpu_count(n)
diff --git a/flang/test/Lower/OpenMP/declare-variant-metadirective-context.f90 b/flang/test/Lower/OpenMP/declare-variant-metadirective-context.f90
index b4a3002471296..030aa1cd1ac4d 100644
--- a/flang/test/Lower/OpenMP/declare-variant-metadirective-context.f90
+++ b/flang/test/Lower/OpenMP/declare-variant-metadirective-context.f90
@@ -1,7 +1,9 @@
 ! RUN: %flang_fc1 -fopenmp -fopenmp-version=52 -emit-hlfir %s -o - | \
-! RUN:   FileCheck %s
+! RUN:   FileCheck %s --implicit-check-not=omp.taskyield \
+! RUN:   --implicit-check-not='fir.call @_QMloop_depthPdo_value'
 ! RUN: %flang_fc1 -fopenmp -fopenmp-version=52 -emit-fir %s -o - | \
-! RUN:   FileCheck %s
+! RUN:   FileCheck %s --implicit-check-not=omp.taskyield \
+! RUN:   --implicit-check-not='fir.call @_QMloop_depthPdo_value'
 
 module selected_context
 contains
@@ -75,22 +77,6 @@ subroutine runtime_parallel(flag, n, a)
     a(n) = value(n)
   end subroutine
 
-  ! A loop's bounds exclude its own DO construct, while its body includes it.
-  ! CHECK-LABEL: func.func @_QMselected_contextPdirect_do(
-  ! CHECK: fir.call @_QMselected_contextPvalue(
-  ! CHECK: omp.wsloop
-  ! CHECK: fir.call @_QMselected_contextPin_do(
-  ! CHECK: fir.call @_QMselected_contextPvalue(
-  ! CHECK: return
-  subroutine direct_do(n, a)
-    integer :: n, a(n), i
-    !$omp do
-    do i = 1, value(n)
-      a(i) = value(n)
-    end do
-    a(n) = value(n)
-  end subroutine
-
   ! The following DO remains a sibling of the standalone metadirective in
   ! the source tree, but its body must still see the selected construct.
   ! CHECK-LABEL: func.func @_QMselected_contextPstatic_do(
@@ -193,3 +179,125 @@ subroutine runtime_do_simd(flag, n, a)
     a(n) = value(n)
   end subroutine
 end module
+
+! Construct depth must agree for calls and metadirective replacements.
+module loop_depth
+contains
+  integer function do_value()
+    do_value = 1
+  end function
+
+  integer function scored_value()
+    scored_value = 2
+  end function
+
+  integer function value()
+    !$omp declare variant(do_value) match(construct={do})
+    !$omp declare variant(scored_value) &
+    !$omp& match(implementation={vendor(score(3): llvm)})
+    value = 0
+  end function
+
+  integer function requiring_do()
+    !$omp declare variant(do_value) match(construct={do})
+    requiring_do = 0
+  end function
+
+  ! At depth one CPU scores 3, below the user selector's score of 4.
+  ! Counting the DO twice would select TASKYIELD instead.
+  ! CHECK-LABEL: func.func @_QMloop_depthPbare_do(
+  ! CHECK: omp.wsloop
+  ! CHECK: omp.taskwait
+  ! CHECK: return
+  subroutine bare_do(n)
+    integer :: n, i
+    !$omp do
+    do i = 1, n
+      !$omp metadirective when(device={kind(cpu)}: taskyield) &
+      !$omp& when(user={condition(score(3): .true.)}: taskwait)
+    end do
+  end subroutine
+
+  ! DO at position two scores 3, below the vendor selector's score of 4.
+  ! The same context must be used for both kinds of variant selection.
+  ! CHECK-LABEL: func.func @_QMloop_depthPcombined(
+  ! CHECK: omp.parallel
+  ! CHECK: omp.wsloop
+  ! CHECK: omp.taskwait
+  ! CHECK: fir.call @_QMloop_depthPscored_value()
+  ! CHECK: return
+  subroutine combined(n, a)
+    integer :: n, i, a(n)
+    !$omp parallel do
+    do i = 1, n
+      !$omp metadirective when(construct={do}: taskyield) &
+      !$omp& when(implementation={vendor(score(3): llvm)}: taskwait)
+      a(i) = value()
+    end do
+  end subroutine
+
+  ! Intervening code is outside the innermost DO evaluation, but shares the
+  ! same OpenMP construct context as the collapsed loop body. The competing
+  ! call rejects a duplicated DO; the DO-only call rejects a missing DO.
+  ! CHECK-LABEL: func.func @_QMloop_depthPcollapsed(
+  ! CHECK: omp.parallel
+  ! CHECK: omp.wsloop
+  ! CHECK: fir.call @_QMloop_depthPscored_value()
+  ! CHECK: fir.call @_QMloop_depthPdo_value()
+  ! CHECK: fir.call @_QMloop_depthPscored_value()
+  ! CHECK: fir.call @_QMloop_depthPdo_value()
+  ! CHECK: fir.call @_QMloop_depthPscored_value()
+  ! CHECK: fir.call @_QMloop_depthPdo_value()
+  ! CHECK: return
+  subroutine collapsed(n, a)
+    integer :: n, i, j, a(n, n)
+    !$omp parallel do collapse(2)
+    do i = 1, n
+      a(i, 1) = value() + requiring_do()
+      do j = 1, n
+        a(i, j) = value() + requiring_do()
+      end do
+      a(i, n) = value() + requiring_do()
+    end do
+  end subroutine
+
+  ! A nested TASK contributes a position, but must not move the enclosing DO.
+  ! CHECK-LABEL: func.func @_QMloop_depthPnested_task(
+  ! CHECK: omp.parallel
+  ! CHECK: omp.wsloop
+  ! CHECK: omp.task
+  ! CHECK: omp.taskwait
+  ! CHECK: return
+  subroutine nested_task(n)
+    integer :: n, i
+    !$omp parallel do
+    do i = 1, n
+      !$omp task
+        !$omp metadirective when(construct={do}: taskyield) &
+        !$omp& when(implementation={vendor(score(3): llvm)}: taskwait)
+      !$omp end task
+    end do
+  end subroutine
+
+  ! TARGET hides the outer PARALLEL. TARGET, PARALLEL, DO give depth three:
+  ! CPU scores 9 and loses to the user selector's score of 10.
+  ! CHECK-LABEL: func.func @_QMloop_depthPtarget_depth(
+  ! CHECK: omp.parallel
+  ! CHECK: omp.target
+  ! CHECK: omp.parallel
+  ! CHECK: omp.wsloop
+  ! CHECK: omp.taskwait
+  ! CHECK: return
+  subroutine target_depth(n)
+    integer :: n, i
+    !$omp parallel
+      !$omp target
+        !$omp parallel do
+        do i = 1, n
+          !$omp metadirective when(device={kind(cpu)}: taskyield) &
+          !$omp& when(user={condition(score(9): .true.)}: taskwait)
+        end do
+      !$omp end target
+    !$omp end parallel
+  end subroutine
+end module
diff --git a/flang/test/Lower/OpenMP/declare-variant-source-context.f90 b/flang/test/Lower/OpenMP/declare-variant-source-context.f90
deleted file mode 100644
index 88f45dda30c7c..0000000000000
--- a/flang/test/Lower/OpenMP/declare-variant-source-context.f90
+++ /dev/null
@@ -1,140 +0,0 @@
-! RUN: %if x86-registered-target %{ %flang_fc1 -fopenmp \
-! RUN:   -fopenmp-version=52 -triple x86_64-unknown-linux-gnu \
-! RUN:   -emit-hlfir %s -o - | FileCheck %s \
-! RUN:   --implicit-check-not='fir.call @_QMsource_contextPbound_arch' %}
-! RUN: %if x86-registered-target %{ %flang_fc1 -fopenmp \
-! RUN:   -fopenmp-version=60 -triple x86_64-unknown-linux-gnu \
-! RUN:   -emit-hlfir %s -o - | FileCheck %s \
-! RUN:   --implicit-check-not='fir.call @_QMsource_contextPbound_arch' %}
-
-module source_context
-contains
-  subroutine depth_base
-    !$omp declare variant(depth_vendor) &
-    !$omp& match(implementation={vendor(score(3): llvm)})
-    !$omp declare variant(depth_arch) match(device={arch(x86_64)})
-  end subroutine
-  subroutine depth_vendor
-  end subroutine
-  subroutine depth_arch
-  end subroutine
-
-  subroutine order_base
-    !$omp declare variant(order_vendor) &
-    !$omp& match(implementation={vendor(score(3): llvm)})
-    !$omp declare variant(order_parallel) match(construct={parallel})
-  end subroutine
-  subroutine order_vendor
-  end subroutine
-  subroutine order_parallel
-  end subroutine
-
-  integer function value_base()
-    !$omp declare variant(value_vendor) &
-    !$omp& match(implementation={vendor(score(3): llvm)})
-    !$omp declare variant(value_arch) match(device={arch(x86_64)})
-    value_base = 0
-  end function
-  integer function value_vendor()
-    value_vendor = 1
-  end function
-  integer function value_arch()
-    value_arch = 2
-  end function
-
-  integer function bound_base(n)
-    integer, intent(in) :: n
-    !$omp declare variant(bound_vendor) &
-    !$omp& match(implementation={vendor(score(3): llvm)})
-    !$omp declare variant(bound_arch) match(device={arch(x86_64)})
-    bound_base = n
-  end function
-  integer function bound_vendor(n)
-    integer, intent(in) :: n
-    bound_vendor = n
-  end function
-  integer function bound_arch(n)
-    integer, intent(in) :: n
-    bound_arch = n
-  end function
-
-! Bounds exclude TILE in both evaluations; the body includes it.
-! CHECK-LABEL: func.func @_QMsource_contextPtile_context(
-! CHECK-COUNT-2: fir.call @_QMsource_contextPbound_vendor(
-! CHECK: fir.call @_QMsource_contextPdepth_arch()
-! CHECK-NOT: fir.call @_QMsource_contextPdepth_vendor
-! CHECK: return
-  subroutine tile_context(n)
-    integer :: n, i
-    !$omp tile sizes(2)
-    do i = 1, bound_base(n)
-      call depth_base()
-    end do
-  end subroutine
-
-! CHECK-LABEL: func.func @_QMsource_contextPunroll_context(
-! CHECK: fir.call @_QMsource_contextPdepth_arch()
-! CHECK-NOT: fir.call @_QMsource_contextPdepth_vendor
-! CHECK: return
-  subroutine unroll_context(n)
-    integer :: n, i
-    !$omp unroll partial(2)
-    do i = 1, n
-      call depth_base()
-    end do
-  end subroutine
-
-! Bounds exclude FUSE in both evaluations of each loop; the bodies include it.
-! CHECK-LABEL: func.func @_QMsource_contextPfuse_context(
-! CHECK-NOT: fir.call @_QMsource_contextPdepth_vendor
-! CHECK-COUNT-2: fir.call @_QMsource_contextPbound_vendor(
-! CHECK: fir.call @_QMsource_contextPdepth_arch()
-! CHECK-COUNT-2: fir.call @_QMsource_contextPbound_vendor(
-! CHECK: fir.call @_QMsource_contextPdepth_arch()
-! CHECK-NOT: fir.call @_QMsource_contextPdepth_vendor
-! CHECK: return
-  subroutine fuse_context(n)
-    integer :: n, i, j
-    !$omp fuse
-    do i = 1, bound_base(n)
-      call depth_base()
-    end do
-    do j = 1, bound_base(n)
-      call depth_base()
-    end do
-    !$omp end fuse
-  end subroutine
-
-! CHECK-LABEL: func.func @_QMsource_contextPatomic_context(
-! CHECK: fir.call @_QMsource_contextPvalue_arch()
-! CHECK-NOT: fir.call @_QMsource_contextPvalue_vendor
-! CHECK: omp.atomic.update
-! CHECK: return
-  subroutine atomic_context(x)
-    integer :: x
-    !$omp atomic update
-    x = x + value_base()
-  end subroutine
-
-! CHECK-LABEL: func.func @_QMsource_contextPassume_context()
-! CHECK: fir.call @_QMsource_contextPdepth_vendor()
-! CHECK-NOT: fir.call @_QMsource_contextPdepth_arch
-! CHECK: return
-  subroutine assume_context
-    !$omp assume holds(.true.)
-      call depth_base()
-    !$omp end assume
-  end subroutine
-
-! CHECK-LABEL: func.func @_QMsource_contextPcombined_context(
-! CHECK: fir.call @_QMsource_contextPorder_parallel()
-! CHECK-NOT: fir.call @_QMsource_contextPorder_vendor
-! CHECK: return
-  subroutine combined_context(n)
-    integer :: n, i
-    !$omp teams distribute parallel do
-    do i = 1, n
-      call order_base()
-    end do
-  end subroutine
-end module
diff --git a/flang/test/Lower/OpenMP/variant-loop-context-depth.f90 b/flang/test/Lower/OpenMP/variant-loop-context-depth.f90
deleted file mode 100644
index 87ca89d3b70f3..0000000000000
--- a/flang/test/Lower/OpenMP/variant-loop-context-depth.f90
+++ /dev/null
@@ -1,127 +0,0 @@
-! RUN: %flang_fc1 -fopenmp -fopenmp-version=52 -emit-hlfir %s -o - | \
-! RUN:   FileCheck %s --implicit-check-not=omp.taskyield \
-! RUN:   --implicit-check-not='fir.call @_QMloop_depthPdo_value'
-! RUN: %flang_fc1 -fopenmp -fopenmp-version=52 -emit-fir %s -o - | \
-! RUN:   FileCheck %s --implicit-check-not=omp.taskyield \
-! RUN:   --implicit-check-not='fir.call @_QMloop_depthPdo_value'
-
-module loop_depth
-contains
-  integer function do_value()
-    do_value = 1
-  end function
-
-  integer function scored_value()
-    scored_value = 2
-  end function
-
-  integer function value()
-    !$omp declare variant(do_value) match(construct={do})
-    !$omp declare variant(scored_value) &
-    !$omp& match(implementation={vendor(score(3): llvm)})
-    value = 0
-  end function
-
-  integer function requiring_do()
-    !$omp declare variant(do_value) match(construct={do})
-    requiring_do = 0
-  end function
-
-  ! At depth one CPU scores 3, below the user selector's score of 4.
-  ! Counting the DO twice would select TASKYIELD instead.
-  ! CHECK-LABEL: func.func @_QMloop_depthPbare_do(
-  ! CHECK: omp.wsloop
-  ! CHECK: omp.taskwait
-  ! CHECK: return
-  subroutine bare_do(n)
-    integer :: n, i
-    !$omp do
-    do i = 1, n
-      !$omp metadirective when(device={kind(cpu)}: taskyield) &
-      !$omp& when(user={condition(score(3): .true.)}: taskwait)
-    end do
-  end subroutine
-
-  ! DO at position two scores 3, below the vendor selector's score of 4.
-  ! The same context must be used for both kinds of variant selection.
-  ! CHECK-LABEL: func.func @_QMloop_depthPcombined(
-  ! CHECK: omp.parallel
-  ! CHECK: omp.wsloop
-  ! CHECK: omp.taskwait
-  ! CHECK: fir.call @_QMloop_depthPscored_value()
-  ! CHECK: return
-  subroutine combined(n, a)
-    integer :: n, i, a(n)
-    !$omp parallel do
-    do i = 1, n
-      !$omp metadirective when(construct={do}: taskyield) &
-      !$omp& when(implementation={vendor(score(3): llvm)}: taskwait)
-      a(i) = value()
-    end do
-  end subroutine
-
-  ! Intervening code is outside the innermost DO evaluation, but shares the
-  ! same OpenMP construct context as the collapsed loop body. The competing
-  ! call rejects a duplicated DO; the DO-only call rejects a missing DO.
-  ! CHECK-LABEL: func.func @_QMloop_depthPcollapsed(
-  ! CHECK: omp.parallel
-  ! CHECK: omp.wsloop
-  ! CHECK: fir.call @_QMloop_depthPscored_value()
-  ! CHECK: fir.call @_QMloop_depthPdo_value()
-  ! CHECK: fir.call @_QMloop_depthPscored_value()
-  ! CHECK: fir.call @_QMloop_depthPdo_value()
-  ! CHECK: fir.call @_QMloop_depthPscored_value()
-  ! CHECK: fir.call @_QMloop_depthPdo_value()
-  ! CHECK: return
-  subroutine collapsed(n, a)
-    integer :: n, i, j, a(n, n)
-    !$omp parallel do collapse(2)
-    do i = 1, n
-      a(i, 1) = value() + requiring_do()
-      do j = 1, n
-        a(i, j) = value() + requiring_do()
-      end do
-      a(i, n) = value() + requiring_do()
-    end do
-  end subroutine
-
-  ! A nested TASK contributes a position, but must not move the enclosing DO.
-  ! CHECK-LABEL: func.func @_QMloop_depthPnested_task(
-  ! CHECK: omp.parallel
-  ! CHECK: omp.wsloop
-  ! CHECK: omp.task
-  ! CHECK: omp.taskwait
-  ! CHECK: return
-  subroutine nested_task(n)
-    integer :: n, i
-    !$omp parallel do
-    do i = 1, n
-      !$omp task
-        !$omp metadirective when(construct={do}: taskyield) &
-        !$omp& when(implementation={vendor(score(3): llvm)}: taskwait)
-      !$omp end task
-    end do
-  end subroutine
-
-  ! TARGET hides the outer PARALLEL. TARGET, PARALLEL, DO give depth three:
-  ! CPU scores 9 and loses to the user selector's score of 10.
-  ! CHECK-LABEL: func.func @_QMloop_depthPtarget_depth(
-  ! CHECK: omp.parallel
-  ! CHECK: omp.target
-  ! CHECK: omp.parallel
-  ! CHECK: omp.wsloop
-  ! CHECK: omp.taskwait
-  ! CHECK: return
-  subroutine target_depth(n)
-    integer :: n, i
-    !$omp parallel
-      !$omp target
-        !$omp parallel do
-        do i = 1, n
-          !$omp metadirective when(device={kind(cpu)}: taskyield) &
-          !$omp& when(user={condition(score(9): .true.)}: taskwait)
-        end do
-      !$omp end target
-    !$omp end parallel
-  end subroutine
-end module
diff --git a/flang/test/Lower/OpenMP/metadirective-source-context.f90 b/flang/test/Lower/OpenMP/variant-source-context.f90
similarity index 52%
rename from flang/test/Lower/OpenMP/metadirective-source-context.f90
rename to flang/test/Lower/OpenMP/variant-source-context.f90
index e0804337f4295..937c8392fb494 100644
--- a/flang/test/Lower/OpenMP/metadirective-source-context.f90
+++ b/flang/test/Lower/OpenMP/variant-source-context.f90
@@ -1,9 +1,149 @@
 ! RUN: %if x86-registered-target %{ %flang_fc1 -fopenmp \
 ! RUN:   -fopenmp-version=52 -triple x86_64-unknown-linux-gnu \
-! RUN:   -emit-hlfir %s -o - | FileCheck %s %}
+! RUN:   -emit-hlfir %s -o - | FileCheck %s \
+! RUN:   --implicit-check-not='fir.call @_QMsource_contextPbound_arch' %}
+! RUN: %if x86-registered-target %{ %flang_fc1 -fopenmp \
+! RUN:   -fopenmp-version=60 -triple x86_64-unknown-linux-gnu \
+! RUN:   -emit-hlfir %s -o - | FileCheck %s \
+! RUN:   --implicit-check-not='fir.call @_QMsource_contextPbound_arch' %}
 ! RUN: %if x86-registered-target %{ %flang_fc1 -fopenmp \
 ! RUN:   -fopenmp-version=52 -triple x86_64-unknown-linux-gnu \
-! RUN:   -emit-fir %s -o - | FileCheck %s %}
+! RUN:   -emit-fir %s -o - | FileCheck %s \
+! RUN:   --implicit-check-not='fir.call @_QMsource_contextPbound_arch' %}
+
+! DECLARE VARIANT and METADIRECTIVE use the same source construct context.
+
+module source_context
+contains
+  subroutine depth_base
+    !$omp declare variant(depth_vendor) &
+    !$omp& match(implementation={vendor(score(3): llvm)})
+    !$omp declare variant(depth_arch) match(device={arch(x86_64)})
+  end subroutine
+  subroutine depth_vendor
+  end subroutine
+  subroutine depth_arch
+  end subroutine
+
+  subroutine order_base
+    !$omp declare variant(order_vendor) &
+    !$omp& match(implementation={vendor(score(3): llvm)})
+    !$omp declare variant(order_parallel) match(construct={parallel})
+  end subroutine
+  subroutine order_vendor
+  end subroutine
+  subroutine order_parallel
+  end subroutine
+
+  integer function value_base()
+    !$omp declare variant(value_vendor) &
+    !$omp& match(implementation={vendor(score(3): llvm)})
+    !$omp declare variant(value_arch) match(device={arch(x86_64)})
+    value_base = 0
+  end function
+  integer function value_vendor()
+    value_vendor = 1
+  end function
+  integer function value_arch()
+    value_arch = 2
+  end function
+
+  integer function bound_base(n)
+    integer, intent(in) :: n
+    !$omp declare variant(bound_vendor) &
+    !$omp& match(implementation={vendor(score(3): llvm)})
+    !$omp declare variant(bound_arch) match(device={arch(x86_64)})
+    bound_base = n
+  end function
+  integer function bound_vendor(n)
+    integer, intent(in) :: n
+    bound_vendor = n
+  end function
+  integer function bound_arch(n)
+    integer, intent(in) :: n
+    bound_arch = n
+  end function
+
+! Bounds exclude TILE in both evaluations; the body includes it.
+! CHECK-LABEL: func.func @_QMsource_contextPtile_context(
+! CHECK-COUNT-2: fir.call @_QMsource_contextPbound_vendor(
+! CHECK: fir.call @_QMsource_contextPdepth_arch()
+! CHECK-NOT: fir.call @_QMsource_contextPdepth_vendor
+! CHECK: return
+  subroutine tile_context(n)
+    integer :: n, i
+    !$omp tile sizes(2)
+    do i = 1, bound_base(n)
+      call depth_base()
+    end do
+  end subroutine
+
+! CHECK-LABEL: func.func @_QMsource_contextPunroll_context(
+! CHECK: fir.call @_QMsource_contextPdepth_arch()
+! CHECK-NOT: fir.call @_QMsource_contextPdepth_vendor
+! CHECK: return
+  subroutine unroll_context(n)
+    integer :: n, i
+    !$omp unroll partial(2)
+    do i = 1, n
+      call depth_base()
+    end do
+  end subroutine
+
+! Bounds exclude FUSE in both evaluations of each loop; the bodies include it.
+! CHECK-LABEL: func.func @_QMsource_contextPfuse_context(
+! CHECK-NOT: fir.call @_QMsource_contextPdepth_vendor
+! CHECK-COUNT-2: fir.call @_QMsource_contextPbound_vendor(
+! CHECK: fir.call @_QMsource_contextPdepth_arch()
+! CHECK-COUNT-2: fir.call @_QMsource_contextPbound_vendor(
+! CHECK: fir.call @_QMsource_contextPdepth_arch()
+! CHECK-NOT: fir.call @_QMsource_contextPdepth_vendor
+! CHECK: return
+  subroutine fuse_context(n)
+    integer :: n, i, j
+    !$omp fuse
+    do i = 1, bound_base(n)
+      call depth_base()
+    end do
+    do j = 1, bound_base(n)
+      call depth_base()
+    end do
+    !$omp end fuse
+  end subroutine
+
+! CHECK-LABEL: func.func @_QMsource_contextPatomic_context(
+! CHECK: fir.call @_QMsource_contextPvalue_arch()
+! CHECK-NOT: fir.call @_QMsource_contextPvalue_vendor
+! CHECK: omp.atomic.update
+! CHECK: return
+  subroutine atomic_context(x)
+    integer :: x
+    !$omp atomic update
+    x = x + value_base()
+  end subroutine
+
+! CHECK-LABEL: func.func @_QMsource_contextPassume_context()
+! CHECK: fir.call @_QMsource_contextPdepth_vendor()
+! CHECK-NOT: fir.call @_QMsource_contextPdepth_arch
+! CHECK: return
+  subroutine assume_context
+    !$omp assume holds(.true.)
+      call depth_base()
+    !$omp end assume
+  end subroutine
+
+! CHECK-LABEL: func.func @_QMsource_contextPcombined_context(
+! CHECK: fir.call @_QMsource_contextPorder_parallel()
+! CHECK-NOT: fir.call @_QMsource_contextPorder_vendor
+! CHECK: return
+  subroutine combined_context(n)
+    integer :: n, i
+    !$omp teams distribute parallel do
+    do i = 1, n
+      call order_base()
+    end do
+  end subroutine
+end module
 
 ! Executable loop transformations contribute to the construct context.
 ! CHECK-LABEL: func.func @_QPtile_context(
diff --git a/llvm/include/llvm/Frontend/OpenMP/OMPContext.h b/llvm/include/llvm/Frontend/OpenMP/OMPContext.h
index acd0b1bda947d..c829479f1d298 100644
--- a/llvm/include/llvm/Frontend/OpenMP/OMPContext.h
+++ b/llvm/include/llvm/Frontend/OpenMP/OMPContext.h
@@ -192,6 +192,11 @@ struct VariantMatchInfo {
   StringRef UserCondition;
 };
 
+/// Return a directive's enclosing construct traits in source order. Constructs
+/// without selector properties use invalid placeholders to retain scoring
+/// depth. DISPATCH is omitted; callers handle its target call separately.
+LLVM_ABI SmallVector<TraitProperty, 8> getConstructTraits(Directive Kind);
+
 /// The context for a source location is made up of active property traits,
 /// e.g., device={kind(host)}, and constructs traits which describe the nesting
 /// in OpenMP constructs at the location.
diff --git a/llvm/lib/Frontend/OpenMP/OMPContext.cpp b/llvm/lib/Frontend/OpenMP/OMPContext.cpp
index ffa7b7f5e992e..97c31271b2ab8 100644
--- a/llvm/lib/Frontend/OpenMP/OMPContext.cpp
+++ b/llvm/lib/Frontend/OpenMP/OMPContext.cpp
@@ -16,6 +16,7 @@
 #include "llvm/ADT/DenseSet.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/ADT/StringSwitch.h"
+#include "llvm/Frontend/OpenMP/OMP.h"
 #include "llvm/Support/Debug.h"
 #include "llvm/Support/raw_ostream.h"
 #include "llvm/TargetParser/Triple.h"
@@ -26,6 +27,43 @@
 using namespace llvm;
 using namespace omp;
 
+SmallVector<TraitProperty, 8> llvm::omp::getConstructTraits(Directive Kind) {
+  SmallVector<TraitProperty, 8> Traits;
+  for (Directive Leaf : getLeafConstructsOrSelf(Kind)) {
+    if (getDirectiveCategory(Leaf) == Category::Informational)
+      continue;
+    switch (Leaf) {
+    case OMPD_target:
+      Traits.push_back(TraitProperty::construct_target_target);
+      break;
+    case OMPD_teams:
+      Traits.push_back(TraitProperty::construct_teams_teams);
+      break;
+    case OMPD_parallel:
+      Traits.push_back(TraitProperty::construct_parallel_parallel);
+      break;
+    case OMPD_for:
+    case OMPD_do:
+      Traits.push_back(TraitProperty::construct_for_for);
+      break;
+    case OMPD_simd:
+      Traits.push_back(TraitProperty::construct_simd_simd);
+      break;
+    case OMPD_section:
+      // SECTION separates blocks within SECTIONS without adding a level.
+    case OMPD_dispatch:
+    case OMPD_nothing:
+    case OMPD_metadirective:
+    case OMPD_unknown:
+      break;
+    default:
+      Traits.push_back(TraitProperty::invalid);
+      break;
+    }
+  }
+  return Traits;
+}
+
 OMPContext::OMPContext(bool IsDeviceCompilation, Triple TargetTriple,
                        Triple TargetOffloadTriple, int DeviceNum) {
   // Add the appropriate target device kind trait based on the target triple

>From ac4d7bb33247bf89a15fc46bbab6175ca2ebafd7 Mon Sep 17 00:00:00 2001
From: "Chi Chun, Chen" <chichun.chen at hpe.com>
Date: Thu, 8 Oct 2026 13:05:42 -0500
Subject: [PATCH 18/23] Fix variant construct contexts and selected TARGET
 BLOCK maps

Evaluate each canonical loop's bounds under the loop-control context.
A later FUSE loop's bounds otherwise resolved variants with the previous
loop's body as the current evaluation, which included FUSE itself.

Treat a METADIRECTIVE that precedes every executable statement, which is
a declarative construct, like an executable one when collecting the
enclosing constructs. Its selected replacement keeps its source
position, so an inner TARGET still hides it.

A BLOCK spliced into a body-less METADIRECTIVE is not part of the
METADIRECTIVE's parse tree. Walk it too when collecting TARGET implicit
maps and dynamic-substring constants, so a selected TARGET maps the
BLOCK's variables like a directly written TARGET.
---
 flang/lib/Lower/OpenMP/OpenMP.cpp             | 32 +++++++++++-
 flang/lib/Lower/OpenMP/Utils.cpp              | 41 +++++++--------
 flang/lib/Lower/OpenMP/Utils.h                | 13 +++++
 .../OpenMP/metadirective-target-boundary.f90  | 50 +++++++++++++++++++
 4 files changed, 110 insertions(+), 26 deletions(-)

diff --git a/flang/lib/Lower/OpenMP/OpenMP.cpp b/flang/lib/Lower/OpenMP/OpenMP.cpp
index f22aca859cd49..1f44500960b86 100644
--- a/flang/lib/Lower/OpenMP/OpenMP.cpp
+++ b/flang/lib/Lower/OpenMP/OpenMP.cpp
@@ -3030,6 +3030,9 @@ static void genCanonicalLoopNest(
   // Computing the trip count must happen before entering the outermost loop
   lower::pft::Evaluation *innermostEval = nestedEval;
   for (std::size_t i = 0; i < ivs.size(); ++i) {
+    // Otherwise a later loop's bounds would see the previous loop's body.
+    mlir::SaveStateStack<LoopControlContext> loopControlContext{
+        converter.getStateStack(), eval};
     if (innermostEval->getIf<parser::DoConstruct>()->IsDoConcurrent()) {
       // OpenMP specifies DO CONCURRENT only with the `!omp loop` construct.
       // Will need to add special cases for this combination.
@@ -3975,13 +3978,36 @@ struct DynamicSubstringVisitor {
   }
 };
 
+static bool isBodylessMetadirective(const lower::pft::Evaluation &eval) {
+  if (const auto *decl = eval.getIf<parser::OpenMPDeclarativeConstruct>())
+    return std::holds_alternative<parser::OmpMetadirectiveDirective>(decl->u);
+  const auto *omp = eval.getIf<parser::OpenMPConstruct>();
+  const auto *standalone =
+      omp ? std::get_if<parser::OpenMPStandaloneConstruct>(&omp->u) : nullptr;
+  return standalone &&
+         std::holds_alternative<parser::OmpMetadirectiveDirective>(
+             standalone->u);
+}
+
+/// Also visit evaluations spliced into a body-less METADIRECTIVE's PFT node.
+static void
+forEachParseTreeEval(lower::pft::Evaluation &eval,
+                     llvm::function_ref<void(lower::pft::Evaluation &)> fn) {
+  fn(eval);
+  if (isBodylessMetadirective(eval) && eval.hasNestedEvaluations())
+    for (lower::pft::Evaluation &nested : eval.getNestedEvaluations())
+      fn(nested);
+}
+
 // Collect symbols that have dynamic substring accesses in the target region
 static void collectSymbolsWithDynamicSubstring(
     semantics::SemanticsContext &semaCtx, lower::pft::Evaluation &eval,
     llvm::SmallPtrSet<const semantics::Symbol *, 8>
         &symbolsWithDynamicSubstring) {
   DynamicSubstringVisitor visitor(semaCtx);
-  eval.visit([&](const auto &node) { parser::Walk(node, visitor); });
+  forEachParseTreeEval(eval, [&](lower::pft::Evaluation &e) {
+    e.visit([&](const auto &node) { parser::Walk(node, visitor); });
+  });
   symbolsWithDynamicSubstring = visitor.symbolsWithDynamicSubstring;
 }
 
@@ -4273,7 +4299,9 @@ genTargetOp(lower::AbstractConverter &converter, lower::SymMap &symTable,
     if (const semantics::Symbol *sym = object.sym())
       captureImplicitMap(*sym, /*forceAddressPreserving=*/true);
 
-  lower::pft::visitAllSymbols(eval, captureImplicitMap);
+  forEachParseTreeEval(eval, [&](lower::pft::Evaluation &e) {
+    lower::pft::visitAllSymbols(e, captureImplicitMap);
+  });
 
   auto targetOp = mlir::omp::TargetOp::create(firOpBuilder, loc, clauseOps);
 
diff --git a/flang/lib/Lower/OpenMP/Utils.cpp b/flang/lib/Lower/OpenMP/Utils.cpp
index ac158ebc02b92..ebfcf5ef158a6 100644
--- a/flang/lib/Lower/OpenMP/Utils.cpp
+++ b/flang/lib/Lower/OpenMP/Utils.cpp
@@ -791,21 +791,6 @@ void collectTileSizesFromOpenMPConstruct(
       });
 }
 
-namespace {
-// Original loop control belongs outside the outermost loop-associated
-// constituent, including for composites that emit inner regions first.
-class LoopControlContext
-    : public mlir::StateStackFrameBase<LoopControlContext> {
-public:
-  MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(LoopControlContext)
-
-  explicit LoopControlContext(const pft::Evaluation &evaluation)
-      : evaluation(evaluation) {}
-
-  const pft::Evaluation &evaluation;
-};
-} // namespace
-
 int64_t collectLoopRelatedInfo(
     lower::AbstractConverter &converter, mlir::Location currentLocation,
     lower::pft::Evaluation &eval, lower::pft::Evaluation *nestedEval,
@@ -1517,9 +1502,17 @@ void collectEnclosingConstructTraits(
   auto append = [&](llvm::omp::Directive directive) {
     semantics::omp::AppendDirectiveContextTraits(directive, constructTraits);
   };
-  auto getDirective = [&](const pft::Evaluation &eval,
-                          const parser::OpenMPConstruct &omp) {
-    llvm::omp::Directive directive = parser::omp::GetOmpDirectiveName(omp).v;
+  auto getDirective =
+      [&](const pft::Evaluation &eval) -> std::optional<llvm::omp::Directive> {
+    std::optional<llvm::omp::Directive> directive;
+    if (const auto *omp = eval.getIf<parser::OpenMPConstruct>()) {
+      directive = parser::omp::GetOmpDirectiveName(*omp).v;
+    } else if (const auto *decl =
+                   eval.getIf<parser::OpenMPDeclarativeConstruct>()) {
+      // A metadirective before the first executable statement is declarative.
+      if (std::holds_alternative<parser::OmpMetadirectiveDirective>(decl->u))
+        directive = llvm::omp::Directive::OMPD_metadirective;
+    }
     if (directive == llvm::omp::Directive::OMPD_metadirective)
       for (const OpenMPContextFrame *frame : frames)
         if (&frame->evaluation == &eval && frame->isReplacement)
@@ -1527,15 +1520,15 @@ void collectEnclosingConstructTraits(
     return directive;
   };
   for (const pft::Evaluation *ancestor : ancestors) {
-    const auto *omp = ancestor->getIf<parser::OpenMPConstruct>();
-    if (!omp)
+    std::optional<llvm::omp::Directive> directive = getDirective(*ancestor);
+    if (!directive)
       continue;
     // An ancestor supplies the full source context, including constituents
     // whose bodies also have active frames. Count each construct only once.
     for (auto [index, frame] : llvm::enumerate(frames))
       if (&frame->evaluation == ancestor)
         usedFrames[index] = true;
-    append(getDirective(*ancestor, *omp));
+    append(*directive);
   }
 
   // Use entered frames for clauses. Loop bounds use the source prefix below,
@@ -1550,13 +1543,13 @@ void collectEnclosingConstructTraits(
 
   if (!loopControl)
     return;
-  const auto *omp = evaluation->getIf<parser::OpenMPConstruct>();
-  if (!omp)
+  std::optional<llvm::omp::Directive> directive = getDirective(*evaluation);
+  if (!directive)
     return;
   // Bounds precede the first loop-associated constituent in source order.
   // TARGET TEAMS DISTRIBUTE PARALLEL DO therefore retains TARGET and TEAMS.
   for (llvm::omp::Directive leaf :
-       llvm::omp::getLeafConstructsOrSelf(getDirective(*evaluation, *omp))) {
+       llvm::omp::getLeafConstructsOrSelf(*directive)) {
     llvm::omp::Association association =
         llvm::omp::getDirectiveAssociation(leaf);
     if (association == llvm::omp::Association::LoopNest ||
diff --git a/flang/lib/Lower/OpenMP/Utils.h b/flang/lib/Lower/OpenMP/Utils.h
index 2e0527c24e80b..36ea143c5e133 100644
--- a/flang/lib/Lower/OpenMP/Utils.h
+++ b/flang/lib/Lower/OpenMP/Utils.h
@@ -69,6 +69,19 @@ class OpenMPContextFrame
   bool isReplacement;
 };
 
+// Original loop control belongs outside the outermost loop-associated
+// constituent, including for composites that emit inner regions first.
+class LoopControlContext
+    : public mlir::StateStackFrameBase<LoopControlContext> {
+public:
+  MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(LoopControlContext)
+
+  explicit LoopControlContext(const pft::Evaluation &evaluation)
+      : evaluation(evaluation) {}
+
+  const pft::Evaluation &evaluation;
+};
+
 struct DeclareTargetCaptureInfo {
   mlir::omp::DeclareTargetCaptureClause clause;
   bool automap = false;
diff --git a/flang/test/Lower/OpenMP/metadirective-target-boundary.f90 b/flang/test/Lower/OpenMP/metadirective-target-boundary.f90
index 00adbc0ffdba5..0c16fbce032b6 100644
--- a/flang/test/Lower/OpenMP/metadirective-target-boundary.f90
+++ b/flang/test/Lower/OpenMP/metadirective-target-boundary.f90
@@ -74,3 +74,53 @@ subroutine target_inner_parallel()
     !$omp end target
   !$omp end parallel
 end subroutine
+
+! A selected PARALLEL from a metadirective that precedes every executable
+! statement is still hidden by an inner TARGET.
+! CHECK-LABEL: func.func @_QPdeclarative_parallel_target()
+! CHECK: omp.parallel
+! CHECK: omp.barrier
+! CHECK: omp.target
+! CHECK-NOT: omp.barrier
+! CHECK: omp.taskyield
+! CHECK: return
+subroutine declarative_parallel_target()
+  !$omp metadirective when(implementation={vendor(llvm)}: parallel)
+  block
+    !$omp metadirective when(construct={parallel}: barrier) default(taskyield)
+    !$omp target
+      !$omp metadirective when(construct={parallel}: barrier) &
+      !$omp& default(taskyield)
+    !$omp end target
+  end block
+end subroutine
+
+! A BLOCK associated with a selected TARGET is mapped like a direct TARGET body.
+! CHECK-LABEL: func.func @_QPselected_target_block(
+! CHECK: %[[MAP:.*]] = omp.map.info {{.*}}name("x")
+! CHECK: omp.target {{.*}}map_entries(%[[MAP]] -> %[[ARG:.*]] : !fir.ref<i32>) {
+! CHECK: {{(hlfir|fir)}}.declare %[[ARG]] uniq_name("_QFselected_target_blockEx")
+! CHECK: omp.terminator
+subroutine selected_target_block(x)
+  integer :: x
+  !$omp metadirective when(implementation={vendor(llvm)}: target)
+  block
+    x = x + 1
+  end block
+end subroutine
+
+! A named constant with a dynamic substring in the BLOCK is mapped.
+! CHECK-LABEL: func.func @_QPselected_target_block_substring(
+! CHECK: %[[P:.*]] = omp.map.info {{.*}}name("p")
+! CHECK: omp.target {{.*}}map_entries({{.*}}%[[P]] -> %[[ARG:[^ ,]*]]
+! CHECK: {{(hlfir|fir)}}.declare %[[ARG]] {{.*}}uniq_name("_QFselected_target_block_substringECp")
+! CHECK: omp.terminator
+subroutine selected_target_block_substring(n, c)
+  integer :: n
+  character(1) :: c
+  character(4), parameter :: p = "abcd"
+  !$omp metadirective when(implementation={vendor(llvm)}: target)
+  block
+    c = p(n:n)
+  end block
+end subroutine

>From 939af72ba962afe796df9f7f9a399e6cba55f723 Mon Sep 17 00:00:00 2001
From: "Chi Chun, Chen" <chichun.chen at hpe.com>
Date: Thu, 8 Oct 2026 13:53:25 -0500
Subject: [PATCH 19/23] Diagnose data environments selected for an associated
 BLOCK

Name resolution computes no variant-specific data-sharing attributes for
a BLOCK associated with a standalone METADIRECTIVE, so a selected
PARALLEL, TEAMS, TASK, or TASKLOOP left sequential DO variables shared.
Report these replacements as not yet implemented, as for loop-associated
replacements.

Tests that used a selected PARALLEL only to give a BLOCK an owning
construct now select TARGET. TARGET is not diagnosed because lowering
computes its implicit captures, and it still contributes a construct
trait, so the tests keep checking BLOCK ownership, per-branch lowering,
and source-order contexts. The new TODO test covers PARALLEL, TASK, and
TEAMS.
---
 flang/lib/Lower/OpenMP/OpenMP.cpp             |  4 +++
 .../metadirective-block-data-environment.f90  | 27 ++++++++++++++++
 .../OpenMP/metadirective-target-boundary.f90  | 23 ++++++-------
 .../Lower/OpenMP/variant-source-context.f90   | 32 +++++++++----------
 4 files changed, 57 insertions(+), 29 deletions(-)
 create mode 100644 flang/test/Lower/OpenMP/Todo/metadirective-block-data-environment.f90

diff --git a/flang/lib/Lower/OpenMP/OpenMP.cpp b/flang/lib/Lower/OpenMP/OpenMP.cpp
index 1f44500960b86..04d8def8ffd4e 100644
--- a/flang/lib/Lower/OpenMP/OpenMP.cpp
+++ b/flang/lib/Lower/OpenMP/OpenMP.cpp
@@ -7758,6 +7758,10 @@ static void genMetadirective(lower::AbstractConverter &converter,
       if (associatedBlockEval && associatedBlockEval->lowerAsUnstructured())
         TODO(variantLoc,
              "unstructured associated BLOCK in METADIRECTIVE variant");
+      // Semantics computed no variant-specific data-sharing for the BLOCK.
+      if (associatedBlockEval && hasUnsupportedDataEnvironmentDirective(queue))
+        TODO(variantLoc, "data-environment construct with associated BLOCK in "
+                         "METADIRECTIVE variant");
       mlir::SaveStateStack<OpenMPContextFrame> context{
           converter.getStateStack(), eval, spec->DirId(),
           /*isReplacement=*/true};
diff --git a/flang/test/Lower/OpenMP/Todo/metadirective-block-data-environment.f90 b/flang/test/Lower/OpenMP/Todo/metadirective-block-data-environment.f90
new file mode 100644
index 0000000000000..b7890276a43e6
--- /dev/null
+++ b/flang/test/Lower/OpenMP/Todo/metadirective-block-data-environment.f90
@@ -0,0 +1,27 @@
+! A BLOCK has no data-sharing attributes for a metadirective-selected data
+! environment, so its sequential DO variable would not be privatized.
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
+! RUN:   -cpp -DPARALLEL %s -o - 2>&1 | FileCheck %s
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
+! RUN:   -cpp -DTASK %s -o - 2>&1 | FileCheck %s
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=52 \
+! RUN:   -cpp -DTEAMS %s -o - 2>&1 | FileCheck %s
+
+! CHECK: not yet implemented: data-environment construct with associated BLOCK
+! CHECK-SAME: in METADIRECTIVE variant
+subroutine block_data_environment(flag)
+  logical :: flag
+  integer :: i
+#if defined(PARALLEL)
+  !$omp metadirective when(implementation={vendor(llvm)}: parallel)
+#elif defined(TASK)
+  !$omp metadirective when(user={condition(flag)}: task) otherwise(nothing)
+#else
+  !$omp metadirective when(implementation={vendor(llvm)}: teams)
+#endif
+  block
+    do i = 1, 2
+      call observe(i)
+    end do
+  end block
+end subroutine
diff --git a/flang/test/Lower/OpenMP/metadirective-target-boundary.f90 b/flang/test/Lower/OpenMP/metadirective-target-boundary.f90
index 0c16fbce032b6..507e3b5d1163d 100644
--- a/flang/test/Lower/OpenMP/metadirective-target-boundary.f90
+++ b/flang/test/Lower/OpenMP/metadirective-target-boundary.f90
@@ -75,23 +75,20 @@ subroutine target_inner_parallel()
   !$omp end parallel
 end subroutine
 
-! A selected PARALLEL from a metadirective that precedes every executable
-! statement is still hidden by an inner TARGET.
-! CHECK-LABEL: func.func @_QPdeclarative_parallel_target()
-! CHECK: omp.parallel
-! CHECK: omp.barrier
+! A selected TARGET from a metadirective that precedes every executable
+! statement keeps its source position before an inner TEAMS.
+! CHECK-LABEL: func.func @_QPdeclarative_target_teams()
 ! CHECK: omp.target
-! CHECK-NOT: omp.barrier
+! CHECK: omp.teams
 ! CHECK: omp.taskyield
 ! CHECK: return
-subroutine declarative_parallel_target()
-  !$omp metadirective when(implementation={vendor(llvm)}: parallel)
+subroutine declarative_target_teams()
+  !$omp metadirective when(implementation={vendor(llvm)}: target)
   block
-    !$omp metadirective when(construct={parallel}: barrier) default(taskyield)
-    !$omp target
-      !$omp metadirective when(construct={parallel}: barrier) &
-      !$omp& default(taskyield)
-    !$omp end target
+    !$omp teams
+      !$omp metadirective when(construct={target, teams}: taskyield) &
+      !$omp& default(nothing)
+    !$omp end teams
   end block
 end subroutine
 
diff --git a/flang/test/Lower/OpenMP/variant-source-context.f90 b/flang/test/Lower/OpenMP/variant-source-context.f90
index 937c8392fb494..4d17fa969bd64 100644
--- a/flang/test/Lower/OpenMP/variant-source-context.f90
+++ b/flang/test/Lower/OpenMP/variant-source-context.f90
@@ -213,57 +213,57 @@ subroutine combined_context(n)
 ! A standalone replacement owns the following BLOCK and its nested context.
 ! CHECK-LABEL: func.func @_QPstandalone_block()
 ! CHECK: fir.call @_QPbefore_block
-! CHECK: omp.parallel
+! CHECK: omp.target
 ! CHECK: omp.barrier
 ! CHECK: omp.single
 ! CHECK: omp.taskyield
 ! CHECK: omp.terminator
 ! CHECK: omp.terminator
-! CHECK-NOT: omp.parallel
+! CHECK-NOT: omp.target
 ! CHECK-NOT: omp.barrier
 ! CHECK-NOT: omp.taskyield
 ! CHECK: return
 subroutine standalone_block()
   call before_block()
-  !$omp metadirective when(implementation={vendor(llvm)}: parallel)
+  !$omp metadirective when(implementation={vendor(llvm)}: target)
   block
-    !$omp metadirective when(construct={parallel}: barrier)
+    !$omp metadirective when(construct={target}: barrier)
     !$omp single
       !$omp metadirective &
-      !$omp& when(construct={parallel, single}: taskyield)
+      !$omp& when(construct={target, single}: taskyield)
     !$omp end single
   end block
-  !$omp metadirective when(construct={parallel}: taskyield)
+  !$omp metadirective when(construct={target}: taskyield)
 end subroutine
 
 ! Each runtime alternative lowers the BLOCK once, with its own context.
 ! CHECK-LABEL: func.func @_QPruntime_block(
 ! CHECK: fir.if
-! CHECK: omp.parallel
+! CHECK: omp.target
 ! CHECK: fir.call @_QPuse_block_local
 ! CHECK-NOT: fir.call @_QPuse_block_local
 ! CHECK: omp.taskyield
 ! CHECK: omp.terminator
 ! CHECK-NOT: fir.call @_QPuse_block_local
 ! CHECK: } else {
-! CHECK-NOT: omp.parallel
+! CHECK-NOT: omp.target
 ! CHECK-NOT: omp.taskyield
 ! CHECK: fir.call @_QPuse_block_local
 ! CHECK-NOT: fir.call @_QPuse_block_local
-! CHECK-NOT: omp.parallel
+! CHECK-NOT: omp.target
 ! CHECK-NOT: omp.taskyield
 ! CHECK: return
 subroutine runtime_block(flag)
   logical :: flag
-  !$omp metadirective when(user={condition(flag)}: parallel) &
+  !$omp metadirective when(user={condition(flag)}: target) &
   !$omp& otherwise(nothing)
   block
     integer :: local
     local = 1
     call use_block_local(local)
-    !$omp metadirective when(construct={parallel}: taskyield)
+    !$omp metadirective when(construct={target}: taskyield)
   end block
-  !$omp metadirective when(construct={parallel}: taskyield)
+  !$omp metadirective when(construct={target}: taskyield)
 end subroutine
 
 ! An empty delimited replacement must not capture the following BLOCK.
@@ -283,9 +283,9 @@ subroutine empty_delimited_block()
   end block
 end subroutine
 
-! The selected PARALLEL makes the inner SIMD variant lose to NOTHING.
+! The selected TARGET makes the inner SIMD variant lose to NOTHING.
 ! CHECK-LABEL: func.func @_QPstandalone_block_loop(
-! CHECK: omp.parallel
+! CHECK: omp.target
 ! CHECK-NOT: omp.simd
 ! CHECK: fir.do_loop
 ! CHECK: omp.terminator
@@ -294,11 +294,11 @@ subroutine empty_delimited_block()
 ! CHECK: return
 subroutine standalone_block_loop(n)
   integer :: n, i
-  !$omp metadirective when(implementation={vendor(llvm)}: parallel) &
+  !$omp metadirective when(implementation={vendor(llvm)}: target) &
   !$omp& otherwise(nothing)
   block
     !$omp metadirective &
-    !$omp& when(construct={parallel}: nothing) &
+    !$omp& when(construct={target}: nothing) &
     !$omp& when(implementation={vendor(score(0): llvm)}: simd collapse(1)) &
     !$omp& otherwise(nothing)
     do i = 1, n

>From 9c59c0429f72cf7b6d86113235bf281fd92bcee9 Mon Sep 17 00:00:00 2001
From: "Chi Chun, Chen" <chichun.chen at hpe.com>
Date: Thu, 8 Oct 2026 14:14:29 -0500
Subject: [PATCH 20/23] Restore OpenMP 5.0 runtime metadirective coverage

The OpenMP 5.0 RUN line was dropped because the remaining cases use
NOTHING, which OpenMP 5.0 lacks. Add 5.0-only runtime cases for strict
subset ranking and the DEFAULT fallback.
---
 .../test/Lower/OpenMP/metadirective-user.f90  | 36 +++++++++++++++++++
 1 file changed, 36 insertions(+)

diff --git a/flang/test/Lower/OpenMP/metadirective-user.f90 b/flang/test/Lower/OpenMP/metadirective-user.f90
index 8c9967a2f2c8d..b9bf21510093e 100644
--- a/flang/test/Lower/OpenMP/metadirective-user.f90
+++ b/flang/test/Lower/OpenMP/metadirective-user.f90
@@ -3,6 +3,41 @@
 
 ! 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
+! RUN: %flang_fc1 -fopenmp -emit-hlfir -fopenmp-version=50 -cpp -DOMP_50 %s \
+! RUN:   -o - | FileCheck --check-prefix=OMP50 %s
+
+#ifdef OMP_50
+! OpenMP 5.0 has no NOTHING directive, so it gets its own runtime cases.
+
+! A strict subset loses to its dynamic superset despite a higher score.
+! OMP50-LABEL: func.func @_QPtest_omp50_dynamic_subset_order(
+! OMP50: fir.if
+! OMP50-NEXT: omp.barrier
+! OMP50-NEXT: } else {
+! OMP50-NEXT: omp.taskwait
+! OMP50-NOT: omp.taskyield
+! OMP50: return
+subroutine test_omp50_dynamic_subset_order(flag)
+  logical :: flag
+  !$omp metadirective &
+  !$omp& when(implementation={vendor(score(100): llvm)}: taskyield) &
+  !$omp& when(implementation={vendor(score(1): llvm)}, &
+  !$omp& user={condition(score(20): flag)}: barrier) &
+  !$omp& when(user={condition(score(10): .true.)}: taskwait)
+end subroutine
+
+! DEFAULT supplies the fallback when the runtime condition is false.
+! OMP50-LABEL: func.func @_QPtest_omp50_default(
+! OMP50: fir.if
+! OMP50-NEXT: omp.barrier
+! OMP50-NEXT: } else {
+! OMP50-NEXT: omp.taskwait
+! OMP50: return
+subroutine test_omp50_default(flag)
+  logical :: flag
+  !$omp metadirective when(user={condition(flag)}: barrier) default(taskwait)
+end subroutine
+#else
 
 !===----------------------------------------------------------------------===!
 ! Unknown ARCH retains its weight even when only the runtime condition matches.
@@ -713,3 +748,4 @@ subroutine test_dynamic_subset_nothing_order(flag)
   !$omp& user={condition(score(20): flag)}:) &
   !$omp& when(user={condition(score(10): .true.)}: taskwait)
 end subroutine
+#endif

>From 363c78ec02446be97c5c4f4c690b2e67a2aaadd2 Mon Sep 17 00:00:00 2001
From: "Chi Chun, Chen" <chichun.chen at hpe.com>
Date: Fri, 9 Oct 2026 11:52:08 -0500
Subject: [PATCH 21/23] Destroy composite clause temporaries once, after
 PARALLEL

DISTRIBUTE PARALLEL DO and DISTRIBUTE PARALLEL DO SIMD evaluate the
DISTRIBUTE and PARALLEL clause operands before creating omp.parallel,
but their cleanup ran after the returned omp.distribute, inside the
parallel region. Each thread then destroyed the same temporary, so
dist_schedule(static, size(f())) or num_threads(size(f())) with an
allocatable result freed it more than once.

Evaluate these operands in a separate statement context and emit its
cleanup after omp.parallel. WSLOOP and SIMD clause temporaries stay
inside the parallel region. This also fixes num_threads, which already
had this problem before DISTRIBUTE clause evaluation moved.
---
 flang/lib/Lower/OpenMP/OpenMP.cpp             | 44 +++++++++++++------
 .../OpenMP/distribute-parallel-do-simd.f90    | 34 ++++++++++++++
 .../Lower/OpenMP/distribute-parallel-do.f90   | 34 ++++++++++++++
 3 files changed, 99 insertions(+), 13 deletions(-)

diff --git a/flang/lib/Lower/OpenMP/OpenMP.cpp b/flang/lib/Lower/OpenMP/OpenMP.cpp
index 04d8def8ffd4e..c9363a7aba3a5 100644
--- a/flang/lib/Lower/OpenMP/OpenMP.cpp
+++ b/flang/lib/Lower/OpenMP/OpenMP.cpp
@@ -5281,19 +5281,22 @@ static mlir::omp::DistributeOp genCompositeDistributeParallelDo(
   ConstructQueue::const_iterator parallelItem = std::next(distributeItem);
   ConstructQueue::const_iterator doItem = std::next(parallelItem);
 
+  // Operands evaluated outside omp.parallel are destroyed once, after it.
+  lower::StatementContext outerStmtCtx;
+
   // Clause expressions follow source nesting even though the composite
   // operations place PARALLEL outside DISTRIBUTE.
   mlir::omp::DistributeOperands distributeClauseOps;
-  genDistributeClauses(converter, semaCtx, stmtCtx, distributeItem->clauses,
-                       loc, distributeClauseOps);
+  genDistributeClauses(converter, semaCtx, outerStmtCtx,
+                       distributeItem->clauses, loc, distributeClauseOps);
   mlir::SaveStateStack<OpenMPContextFrame> distributeContext{
       converter.getStateStack(), eval, llvm::omp::Directive::OMPD_distribute};
 
   // Create parent omp.parallel first.
   mlir::omp::ParallelOperands parallelClauseOps;
   llvm::SmallVector<Object> parallelReductionObjects;
-  genParallelClauses(converter, semaCtx, stmtCtx, parallelItem->clauses, loc,
-                     parallelClauseOps, parallelReductionObjects);
+  genParallelClauses(converter, semaCtx, outerStmtCtx, parallelItem->clauses,
+                     loc, parallelClauseOps, parallelReductionObjects);
 
   DataSharingProcessor dsp(converter, semaCtx, doItem->clauses, eval,
                            /*shouldCollectPreDeterminedSymbols=*/true,
@@ -5306,8 +5309,10 @@ static mlir::omp::DistributeOp genCompositeDistributeParallelDo(
   parallelArgs.priv.vars = parallelClauseOps.privateVars;
   parallelArgs.reduction.objects = parallelReductionObjects;
   parallelArgs.reduction.vars = parallelClauseOps.reductionVars;
-  genParallelOp(converter, symTable, semaCtx, eval, loc, queue, parallelItem,
-                parallelClauseOps, parallelArgs, &dsp, /*isComposite=*/true);
+  mlir::omp::ParallelOp parallelOp =
+      genParallelOp(converter, symTable, semaCtx, eval, loc, queue,
+                    parallelItem, parallelClauseOps, parallelArgs, &dsp,
+                    /*isComposite=*/true);
   mlir::SaveStateStack<OpenMPContextFrame> context{
       converter.getStateStack(), eval, llvm::omp::Directive::OMPD_parallel};
 
@@ -5341,6 +5346,11 @@ static mlir::omp::DistributeOp genCompositeDistributeParallelDo(
                 loopNestClauseOps, iv,
                 {{distributeOp, distributeArgs}, {wsloopOp, wsloopArgs}},
                 llvm::omp::Directive::OMPD_distribute_parallel_do, dsp);
+
+  fir::FirOpBuilder &builder = converter.getFirOpBuilder();
+  fir::FirOpBuilder::InsertionGuard guard(builder);
+  builder.setInsertionPointAfter(parallelOp);
+  outerStmtCtx.finalizeAndPop();
   return distributeOp;
 }
 
@@ -5355,19 +5365,22 @@ static mlir::omp::DistributeOp genCompositeDistributeParallelDoSimd(
   ConstructQueue::const_iterator doItem = std::next(parallelItem);
   ConstructQueue::const_iterator simdItem = std::next(doItem);
 
+  // Operands evaluated outside omp.parallel are destroyed once, after it.
+  lower::StatementContext outerStmtCtx;
+
   // Evaluate DISTRIBUTE clauses before entering its source context and then
   // PARALLEL, as in the explicit nesting.
   mlir::omp::DistributeOperands distributeClauseOps;
-  genDistributeClauses(converter, semaCtx, stmtCtx, distributeItem->clauses,
-                       loc, distributeClauseOps);
+  genDistributeClauses(converter, semaCtx, outerStmtCtx,
+                       distributeItem->clauses, loc, distributeClauseOps);
   mlir::SaveStateStack<OpenMPContextFrame> distributeContext{
       converter.getStateStack(), eval, llvm::omp::Directive::OMPD_distribute};
 
   // Create parent omp.parallel first.
   mlir::omp::ParallelOperands parallelClauseOps;
   llvm::SmallVector<Object> parallelReductionObjects;
-  genParallelClauses(converter, semaCtx, stmtCtx, parallelItem->clauses, loc,
-                     parallelClauseOps, parallelReductionObjects);
+  genParallelClauses(converter, semaCtx, outerStmtCtx, parallelItem->clauses,
+                     loc, parallelClauseOps, parallelReductionObjects);
 
   DataSharingProcessor parallelItemDSP(
       converter, semaCtx, parallelItem->clauses, eval,
@@ -5381,9 +5394,9 @@ static mlir::omp::DistributeOp genCompositeDistributeParallelDoSimd(
   parallelArgs.priv.vars = parallelClauseOps.privateVars;
   parallelArgs.reduction.objects = parallelReductionObjects;
   parallelArgs.reduction.vars = parallelClauseOps.reductionVars;
-  genParallelOp(converter, symTable, semaCtx, eval, loc, queue, parallelItem,
-                parallelClauseOps, parallelArgs, &parallelItemDSP,
-                /*isComposite=*/true);
+  mlir::omp::ParallelOp parallelOp = genParallelOp(
+      converter, symTable, semaCtx, eval, loc, queue, parallelItem,
+      parallelClauseOps, parallelArgs, &parallelItemDSP, /*isComposite=*/true);
   mlir::SaveStateStack<OpenMPContextFrame> context{
       converter.getStateStack(), eval, llvm::omp::Directive::OMPD_parallel};
 
@@ -5462,6 +5475,11 @@ static mlir::omp::DistributeOp genCompositeDistributeParallelDoSimd(
                  {simdOp, simdArgs}},
                 llvm::omp::Directive::OMPD_distribute_parallel_do_simd,
                 simdItemDSP);
+
+  fir::FirOpBuilder &builder = converter.getFirOpBuilder();
+  fir::FirOpBuilder::InsertionGuard guard(builder);
+  builder.setInsertionPointAfter(parallelOp);
+  outerStmtCtx.finalizeAndPop();
   return distributeOp;
 }
 
diff --git a/flang/test/Lower/OpenMP/distribute-parallel-do-simd.f90 b/flang/test/Lower/OpenMP/distribute-parallel-do-simd.f90
index 266e5482e9756..0506781d2a650 100644
--- a/flang/test/Lower/OpenMP/distribute-parallel-do-simd.f90
+++ b/flang/test/Lower/OpenMP/distribute-parallel-do-simd.f90
@@ -42,6 +42,40 @@ subroutine distribute_parallel_do_simd_dist_schedule()
   !$omp end teams
 end subroutine distribute_parallel_do_simd_dist_schedule
 
+! Temporaries evaluated outside omp.parallel must be destroyed once, after it.
+! CHECK-LABEL: func.func @_QPdistribute_parallel_do_simd_temp_cleanup(
+subroutine distribute_parallel_do_simd_temp_cleanup(n)
+  integer :: n
+  interface
+    function get(n) result(r)
+      integer, intent(in) :: n
+      integer, allocatable :: r(:)
+    end function
+  end interface
+  !$omp teams
+
+  ! CHECK:      omp.teams {
+  ! CHECK:      fir.call @_QPget(
+  ! CHECK:      %[[DIST_TMP:.*]] = hlfir.as_expr
+  ! CHECK:      fir.call @_QPget(
+  ! CHECK:      %[[NT_TMP:.*]] = hlfir.as_expr
+  ! CHECK:      omp.parallel num_threads({{.*}}) {
+  ! CHECK-NOT:  hlfir.destroy
+  ! CHECK:      omp.distribute dist_schedule_static dist_schedule_chunk_size({{.*}}) {
+  ! CHECK-NOT:  hlfir.destroy
+  ! CHECK:      omp.terminator
+  ! CHECK-NEXT: } {omp.composite}
+  ! CHECK:      hlfir.destroy %[[NT_TMP]] :
+  ! CHECK:      hlfir.destroy %[[DIST_TMP]] :
+  ! CHECK-NEXT: omp.terminator
+  !$omp distribute parallel do simd num_threads(size(get(n))) dist_schedule(static, size(get(n)))
+  do index_ = 1, 10
+  end do
+  !$omp end distribute parallel do simd
+
+  !$omp end teams
+end subroutine distribute_parallel_do_simd_temp_cleanup
+
 ! CHECK-LABEL: func.func @_QPdistribute_parallel_do_simd_schedule(
 subroutine distribute_parallel_do_simd_schedule()
   !$omp teams
diff --git a/flang/test/Lower/OpenMP/distribute-parallel-do.f90 b/flang/test/Lower/OpenMP/distribute-parallel-do.f90
index cddf61647ead3..b69a45086f6a1 100644
--- a/flang/test/Lower/OpenMP/distribute-parallel-do.f90
+++ b/flang/test/Lower/OpenMP/distribute-parallel-do.f90
@@ -36,6 +36,40 @@ subroutine distribute_parallel_do_dist_schedule()
   !$omp end teams
 end subroutine distribute_parallel_do_dist_schedule
 
+! Temporaries evaluated outside omp.parallel must be destroyed once, after it.
+! CHECK-LABEL: func.func @_QPdistribute_parallel_do_temp_cleanup(
+subroutine distribute_parallel_do_temp_cleanup(n)
+  integer :: n
+  interface
+    function get(n) result(r)
+      integer, intent(in) :: n
+      integer, allocatable :: r(:)
+    end function
+  end interface
+  !$omp teams
+
+  ! CHECK:      omp.teams {
+  ! CHECK:      fir.call @_QPget(
+  ! CHECK:      %[[DIST_TMP:.*]] = hlfir.as_expr
+  ! CHECK:      fir.call @_QPget(
+  ! CHECK:      %[[NT_TMP:.*]] = hlfir.as_expr
+  ! CHECK:      omp.parallel num_threads({{.*}}) private({{.*}}) {
+  ! CHECK-NOT:  hlfir.destroy
+  ! CHECK:      omp.distribute dist_schedule_static dist_schedule_chunk_size({{.*}}) {
+  ! CHECK-NOT:  hlfir.destroy
+  ! CHECK:      omp.terminator
+  ! CHECK-NEXT: } {omp.composite}
+  ! CHECK:      hlfir.destroy %[[NT_TMP]] :
+  ! CHECK:      hlfir.destroy %[[DIST_TMP]] :
+  ! CHECK-NEXT: omp.terminator
+  !$omp distribute parallel do num_threads(size(get(n))) dist_schedule(static, size(get(n)))
+  do index_ = 1, 10
+  end do
+  !$omp end distribute parallel do
+
+  !$omp end teams
+end subroutine distribute_parallel_do_temp_cleanup
+
 ! CHECK-LABEL: func.func @_QPdistribute_parallel_do_schedule(
 subroutine distribute_parallel_do_schedule()
   !$omp teams

>From 0e411b24ee7fe3b802c344de2c5d502459d17f8f Mon Sep 17 00:00:00 2001
From: "Chi Chun, Chen" <chichun.chen at hpe.com>
Date: Fri, 9 Oct 2026 12:45:51 -0500
Subject: [PATCH 22/23] Associate a BLOCK across ignored compiler directives

A standalone METADIRECTIVE that selects a block-associated directive
takes the BLOCK that follows it. An ignored compiler directive between
them, such as !dir$ ignored_comment, ended the search, so the selected
TARGET was empty and the BLOCK was lowered outside it.

Skip the compiler directives that semantics reports as ignored (an
unrecognized directive, a bare name/value list, LOOP COUNT, and
ASSUME_ALIGNED) and move them with the BLOCK as the DO path does. Other
compiler directives, such as PREFETCH, still end the search like any
statement.
---
 flang/lib/Lower/OpenMP/OpenMP.cpp             | 26 +++++++++++--
 .../Lower/OpenMP/variant-source-context.f90   | 38 +++++++++++++++++++
 2 files changed, 60 insertions(+), 4 deletions(-)

diff --git a/flang/lib/Lower/OpenMP/OpenMP.cpp b/flang/lib/Lower/OpenMP/OpenMP.cpp
index c9363a7aba3a5..92e5e99ad550d 100644
--- a/flang/lib/Lower/OpenMP/OpenMP.cpp
+++ b/flang/lib/Lower/OpenMP/OpenMP.cpp
@@ -7249,8 +7249,26 @@ static lower::pft::Evaluation *spliceAssociatedEval(
 
   auto firstAssociatedIt = std::next(metaIt);
   auto associatedIt = firstAssociatedIt;
-  while (isLoop && associatedIt != parentList->end() &&
-         isIgnorableMetadirectiveLoopAssociationEval(*associatedIt))
+  // Directives that semantics reports as ignored.
+  auto isIgnoredDirective = [](lower::pft::Evaluation &e) {
+    const auto *directive = e.getIf<parser::CompilerDirective>();
+    if (!directive)
+      return false;
+    using IgnoredDirectives =
+        std::tuple<parser::CompilerDirective::Unrecognized,
+                   std::list<parser::CompilerDirective::NameValue>,
+                   parser::CompilerDirective::LoopCount,
+                   std::list<parser::CompilerDirective::AssumeAligned>>;
+    return common::visit(
+        [](const auto &value) {
+          return common::HasMember<std::decay_t<decltype(value)>,
+                                   IgnoredDirectives>;
+        },
+        directive->u);
+  };
+  while (associatedIt != parentList->end() &&
+         (isLoop ? isIgnorableMetadirectiveLoopAssociationEval(*associatedIt)
+                 : isIgnoredDirective(*associatedIt)))
     ++associatedIt;
 
   if (associatedIt == parentList->end())
@@ -7277,8 +7295,8 @@ static lower::pft::Evaluation *spliceAssociatedEval(
     }
   }
 
-  // Compiler directives between the metadirective and its associated loop
-  // must be processed before the loop is lowered. Move them with the loop so
+  // Compiler directives between the metadirective and its associated construct
+  // must be processed before the construct is lowered. Move them with it so
   // they are not visited later as siblings of the metadirective.
   for (auto it = firstAssociatedIt; it != associatedIt;) {
     auto current = it++;
diff --git a/flang/test/Lower/OpenMP/variant-source-context.f90 b/flang/test/Lower/OpenMP/variant-source-context.f90
index 4d17fa969bd64..b1d0eb07c1cc9 100644
--- a/flang/test/Lower/OpenMP/variant-source-context.f90
+++ b/flang/test/Lower/OpenMP/variant-source-context.f90
@@ -266,6 +266,44 @@ subroutine runtime_block(flag)
   !$omp metadirective when(construct={target}: taskyield)
 end subroutine
 
+! An ignored compiler directive does not separate the BLOCK from its
+! replacement.
+! CHECK-LABEL: func.func @_QPstandalone_block_ignored_directive(
+! CHECK: omp.target
+! CHECK: omp.taskyield
+! CHECK: omp.terminator
+! CHECK-NOT: omp.taskwait
+! CHECK: return
+subroutine standalone_block_ignored_directive(a)
+  integer :: a(10)
+  !$omp metadirective when(implementation={vendor(llvm)}: target)
+  !dir$ ignored_comment
+  !dir$ ignored(1)
+  !dir$ loop count(10)
+  !dir$ assume_aligned a:64
+  block
+    !$omp metadirective when(construct={target}: taskyield) otherwise(taskwait)
+  end block
+end subroutine
+
+! A compiler directive with an effect still ends the search for the BLOCK.
+! CHECK-LABEL: func.func @_QPstandalone_block_prefetch(
+! CHECK: omp.target
+! CHECK-NOT: fir.prefetch
+! CHECK: omp.terminator
+! CHECK: fir.prefetch
+! CHECK-NOT: omp.taskyield
+! CHECK: omp.taskwait
+! CHECK: return
+subroutine standalone_block_prefetch(a)
+  integer :: a(10)
+  !$omp metadirective when(implementation={vendor(llvm)}: target)
+  !dir$ prefetch a
+  block
+    !$omp metadirective when(construct={target}: taskyield) otherwise(taskwait)
+  end block
+end subroutine
+
 ! An empty delimited replacement must not capture the following BLOCK.
 ! CHECK-LABEL: func.func @_QPempty_delimited_block()
 ! CHECK-NOT: omp.parallel

>From 384d1bb5dffd356e3e916c464cfa7b0c9f0ea771 Mon Sep 17 00:00:00 2001
From: "Chi Chun, Chen" <chichun.chen at hpe.com>
Date: Mon, 12 Oct 2026 13:33:43 -0500
Subject: [PATCH 23/23] Associate a BLOCK across compiler directives that emit
 no code

A named INLINEALWAYS, unnamed inlining directives, and loop directives
between a standalone METADIRECTIVE and its BLOCK still ended the search,
so a selected TARGET was empty and the BLOCK was lowered outside it.

Use the DO path's classification of compiler directives that emit no
executable operations for the BLOCK path too, and rename it accordingly.
The directives move with the BLOCK and are still lowered, so a matching
INLINEALWAYS still marks the procedure always-inline, a mismatched one
stays ignored, and loop and inlining directives see the same following
statement as before. PREFETCH still ends the search.
---
 flang/lib/Lower/OpenMP/OpenMP.cpp             | 38 ++++++-------------
 .../Lower/OpenMP/variant-source-context.f90   | 26 ++++++++++++-
 2 files changed, 37 insertions(+), 27 deletions(-)

diff --git a/flang/lib/Lower/OpenMP/OpenMP.cpp b/flang/lib/Lower/OpenMP/OpenMP.cpp
index 92e5e99ad550d..ef54d0837f57d 100644
--- a/flang/lib/Lower/OpenMP/OpenMP.cpp
+++ b/flang/lib/Lower/OpenMP/OpenMP.cpp
@@ -7156,11 +7156,11 @@ struct SplicedAssociatedEvaluations {
 };
 } // namespace
 
-static bool isSupportedMetadirectiveLoopCompilerDirective(
-    const parser::CompilerDirective &directive) {
-  // Only directives known not to emit executable operations may be processed
-  // before runtime selection. New variants remain unsupported until classified.
-  using SupportedDirectives = std::tuple<
+static bool
+isNonExecutableCompilerDirective(const parser::CompilerDirective &directive) {
+  // Directives known not to emit executable operations. New variants remain
+  // unsupported until classified.
+  using NonExecutableDirectives = std::tuple<
       std::list<parser::CompilerDirective::IgnoreTKR>,
       parser::CompilerDirective::LoopCount,
       std::list<parser::CompilerDirective::AssumeAligned>,
@@ -7180,7 +7180,7 @@ static bool isSupportedMetadirectiveLoopCompilerDirective(
   return common::visit(
       [](const auto &value) {
         using T = std::decay_t<decltype(value)>;
-        return common::HasMember<T, SupportedDirectives>;
+        return common::HasMember<T, NonExecutableDirectives>;
       },
       directive.u);
 }
@@ -7189,14 +7189,13 @@ static bool
 isIgnorableMetadirectiveLoopAssociationEval(lower::pft::Evaluation &eval) {
   const auto *directive = eval.getIf<parser::CompilerDirective>();
   return eval.isEndStmt() ||
-         (directive &&
-          isSupportedMetadirectiveLoopCompilerDirective(*directive));
+         (directive && isNonExecutableCompilerDirective(*directive));
 }
 
 static bool
 isUnsupportedMetadirectiveLoopAssociationEval(lower::pft::Evaluation &eval) {
   if (const auto *directive = eval.getIf<parser::CompilerDirective>())
-    return !isSupportedMetadirectiveLoopCompilerDirective(*directive);
+    return !isNonExecutableCompilerDirective(*directive);
   return eval.isOtherStmt() ||
          (eval.isDirective() && !eval.isExecutableDirective());
 }
@@ -7249,26 +7248,13 @@ static lower::pft::Evaluation *spliceAssociatedEval(
 
   auto firstAssociatedIt = std::next(metaIt);
   auto associatedIt = firstAssociatedIt;
-  // Directives that semantics reports as ignored.
-  auto isIgnoredDirective = [](lower::pft::Evaluation &e) {
+  auto isNonExecutableDirective = [](lower::pft::Evaluation &e) {
     const auto *directive = e.getIf<parser::CompilerDirective>();
-    if (!directive)
-      return false;
-    using IgnoredDirectives =
-        std::tuple<parser::CompilerDirective::Unrecognized,
-                   std::list<parser::CompilerDirective::NameValue>,
-                   parser::CompilerDirective::LoopCount,
-                   std::list<parser::CompilerDirective::AssumeAligned>>;
-    return common::visit(
-        [](const auto &value) {
-          return common::HasMember<std::decay_t<decltype(value)>,
-                                   IgnoredDirectives>;
-        },
-        directive->u);
+    return directive && isNonExecutableCompilerDirective(*directive);
   };
   while (associatedIt != parentList->end() &&
          (isLoop ? isIgnorableMetadirectiveLoopAssociationEval(*associatedIt)
-                 : isIgnoredDirective(*associatedIt)))
+                 : isNonExecutableDirective(*associatedIt)))
     ++associatedIt;
 
   if (associatedIt == parentList->end())
@@ -7630,7 +7616,7 @@ static void genMetadirective(lower::AbstractConverter &converter,
       else if (const auto *directive =
                    unsupportedInterveningEval
                        ->getIf<parser::CompilerDirective>()) {
-        assert(!isSupportedMetadirectiveLoopCompilerDirective(*directive) &&
+        assert(!isNonExecutableCompilerDirective(*directive) &&
                "unexpected compiler directive");
         evalName = std::holds_alternative<parser::CompilerDirective::Prefetch>(
                        directive->u)
diff --git a/flang/test/Lower/OpenMP/variant-source-context.f90 b/flang/test/Lower/OpenMP/variant-source-context.f90
index b1d0eb07c1cc9..98f348d09e70c 100644
--- a/flang/test/Lower/OpenMP/variant-source-context.f90
+++ b/flang/test/Lower/OpenMP/variant-source-context.f90
@@ -266,9 +266,10 @@ subroutine runtime_block(flag)
   !$omp metadirective when(construct={target}: taskyield)
 end subroutine
 
-! An ignored compiler directive does not separate the BLOCK from its
+! A compiler directive that emits no code does not separate the BLOCK from its
 ! replacement.
 ! CHECK-LABEL: func.func @_QPstandalone_block_ignored_directive(
+! CHECK-NOT: llvm.always_inline
 ! CHECK: omp.target
 ! CHECK: omp.taskyield
 ! CHECK: omp.terminator
@@ -281,6 +282,29 @@ subroutine standalone_block_ignored_directive(a)
   !dir$ ignored(1)
   !dir$ loop count(10)
   !dir$ assume_aligned a:64
+  !dir$ inlinealways different
+  !dir$ forceinline
+  !dir$ unroll 4
+  block
+    !$omp metadirective when(construct={target}: taskyield) otherwise(taskwait)
+  end block
+end subroutine
+
+! A matching INLINEALWAYS moves with the BLOCK and still takes effect.
+! CHECK-LABEL: func.func @_QPstandalone_block_inlinealways(
+! CHECK-SAME: llvm.always_inline
+! CHECK: fir.if
+! CHECK: omp.target
+! CHECK: omp.taskyield
+! CHECK: omp.terminator
+! CHECK: } else {
+! CHECK-NOT: omp.taskyield
+! CHECK: omp.taskwait
+! CHECK: return
+subroutine standalone_block_inlinealways(flag)
+  logical :: flag
+  !$omp metadirective when(user={condition(flag)}: target) otherwise(nothing)
+  !dir$ inlinealways standalone_block_inlinealways
   block
     !$omp metadirective when(construct={target}: taskyield) otherwise(taskwait)
   end block



More information about the flang-commits mailing list