[llvm-branch-commits] [flang] [llvm] [flang][OpenMP] Don't treat different runtime conditions as subsets (PR #233493)

via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Tue Oct 20 21:07:35 PDT 2026


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

>From 6b4f9d9db1490168c08e2c27ca71aaa5068b16c9 Mon Sep 17 00:00:00 2001
From: "Chi Chun, Chen" <chichun.chen at hpe.com>
Date: Sat, 17 Oct 2026 22:32:07 -0500
Subject: [PATCH 1/6] [flang][OpenMP] Don't treat different runtime conditions
 as subsets

When two METADIRECTIVE candidates have the same score and one selector is
a strict subset of the other, the superset wins. Flang ranks every
runtime condition with the same property, so isStrictSubset saw
different conditions as the same:

  !$omp metadirective &
  !$omp& when(user={condition(a)}, &
  !$omp&      implementation={vendor(score(4): llvm)}: taskyield) &
  !$omp& when(user={condition(b)}, device={kind(host)}, &
  !$omp&      implementation={vendor(score(3): llvm)}: taskwait)

Both candidates score 5, and `b` was tested first, so TASKWAIT ran when
both conditions were true. Neither selector is a subset of the other, so
clause order should decide.

This patch adds a UserCondition key to VariantMatchInfo, and
isStrictSubset requires equal keys when the first selector has a
condition. Flang gives each distinct runtime condition of a METADIRECTIVE
its own key. It compares the conditions the same way as the existing
check for repeated conditions, so condition(a) and condition((a)) still
form a strict subset. Like that check, it treats different names for the
same entity as different conditions. Ranking also sees conditions with
function calls, which that check rejects, so the comparison now also
collects the symbols in the length of a character array constructor.
Conditions that fold to a constant of any kind, including all of Clang's,
leave the key empty and are still compared by truth value. A constant
condition and a runtime condition are now different.

Fixes #233038.

Assisted-by: GitHub Copilot (Claude Opus 5.5)
---
 flang/lib/Semantics/openmp-utils.cpp          |  45 +++-
 .../test/Lower/OpenMP/metadirective-user.f90  | 224 ++++++++++++++++++
 .../include/llvm/Frontend/OpenMP/OMPContext.h |   4 +
 llvm/lib/Frontend/OpenMP/OMPContext.cpp       |   7 +
 llvm/unittests/Frontend/OpenMPContextTest.cpp |  37 +++
 5 files changed, 314 insertions(+), 3 deletions(-)

diff --git a/flang/lib/Semantics/openmp-utils.cpp b/flang/lib/Semantics/openmp-utils.cpp
index 0c86239136553c..9a116c886f1a79 100644
--- a/flang/lib/Semantics/openmp-utils.cpp
+++ b/flang/lib/Semantics/openmp-utils.cpp
@@ -2473,6 +2473,12 @@ static void AppendConstructTraitsForDirective(
     add(llvm::omp::TraitProperty::construct_dispatch_dispatch);
 }
 
+static SomeExpr NormalizeMetadirectiveCondition(const SomeExpr &expr);
+namespace {
+template <typename T>
+bool AreSameMetadirectiveCondition(const T &left, const T &right);
+} // namespace
+
 static void AddTraitPropertiesFromSelector(llvm::omp::TraitSet set,
     const parser::OmpTraitSelector &selector, llvm::omp::VariantMatchInfo &vmi,
     SemanticsContext &semaCtx,
@@ -2579,6 +2585,24 @@ std::optional<MetadirectiveCandidateSet> BuildMetadirectiveCandidateSet(
     return spec.DirId() == llvm::omp::Directive::OMPD_nothing ? nullptr : &spec;
   };
 
+  // Identify each distinct runtime condition, so that different conditions
+  // are not strict subsets of each other.
+  llvm::SmallVector<SomeExpr, 4> runtimeConditions;
+  auto getConditionKey = [&](const DynamicUserCondition &condition) {
+    const SomeExpr *expr{GetExpr(context, *condition.expr)};
+    if (!expr ||
+        evaluate::IsActuallyConstant(
+            evaluate::Fold(context.foldingContext(), common::Clone(*expr))))
+      return std::string{};
+    SomeExpr normalized{NormalizeMetadirectiveCondition(*expr)};
+    auto *it{llvm::find_if(runtimeConditions, [&](const SomeExpr &condition) {
+      return AreSameMetadirectiveCondition(condition, normalized);
+    })};
+    if (it == runtimeConditions.end())
+      it = &runtimeConditions.emplace_back(std::move(normalized));
+    return std::to_string(it - runtimeConditions.begin() + 1);
+  };
+
   for (const parser::OmpClause &clause : clauses.v) {
     if (const auto *whenClause{
             std::get_if<parser::OmpClause::When>(&clause.u)}) {
@@ -2593,6 +2617,8 @@ std::optional<MetadirectiveCandidateSet> BuildMetadirectiveCandidateSet(
       llvm::omp::VariantMatchInfo rawVMI;
       std::optional<DynamicUserCondition> dynamicCondition{
           MakeVariantMatchInfo(rawVMI, *ctxSel, context)};
+      if (dynamicCondition)
+        rawVMI.UserCondition = getConditionKey(*dynamicCondition);
       if (llvm::any_of(
               rawVMI.ConstructTraits, [](llvm::omp::TraitProperty property) {
                 return llvm::omp::getOpenMPContextTraitSetForProperty(
@@ -2751,6 +2777,14 @@ struct ConditionSymbols : evaluate::Traverse<ConditionSymbols, bool, false> {
     symbols.push_back(&symbol);
     return true;
   }
+  // Expr equality compares the length, but Traverse doesn't visit it.
+  template <int KIND>
+  bool operator()(const evaluate::ArrayConstructor<
+      evaluate::Type<common::TypeCategory::Character, KIND>> &x) const {
+    if (const auto *len{x.LEN()})
+      (*this)(*len);
+    return Base::operator()(x);
+  }
   std::vector<const Symbol *> &symbols;
 };
 
@@ -2805,6 +2839,12 @@ bool isRepeatableMetadirectiveCondition(const SomeExpr &expr) {
 }
 } // namespace
 
+static SomeExpr NormalizeMetadirectiveCondition(const SomeExpr &expr) {
+  MetadirectiveConditionNormalizer normalizer;
+  evaluate::rewrite::Mutator normalize{normalizer};
+  return normalize(expr);
+}
+
 bool IsRepeatableMetadirectiveCondition(
     const parser::ScalarExpr &condition, SemanticsContext &context) {
   const SomeExpr *expr{GetExpr(context, condition)};
@@ -2820,10 +2860,9 @@ bool AreSameRepeatableMetadirectiveCondition(const parser::ScalarExpr &left,
       !isRepeatableMetadirectiveCondition(*rightExpr))
     return false;
 
-  MetadirectiveConditionNormalizer normalizer;
-  evaluate::rewrite::Mutator normalize{normalizer};
   return AreSameMetadirectiveCondition(
-      normalize(*leftExpr), normalize(*rightExpr));
+      NormalizeMetadirectiveCondition(*leftExpr),
+      NormalizeMetadirectiveCondition(*rightExpr));
 }
 
 llvm::SmallVector<unsigned, 4> GetMetadirectiveElsePathCandidates(
diff --git a/flang/test/Lower/OpenMP/metadirective-user.f90 b/flang/test/Lower/OpenMP/metadirective-user.f90
index 2f201cc10e982a..fb12ba8739ea83 100644
--- a/flang/test/Lower/OpenMP/metadirective-user.f90
+++ b/flang/test/Lower/OpenMP/metadirective-user.f90
@@ -559,3 +559,227 @@ subroutine test_multi_dynamic_multi_static(a, b)
   !$omp & default(nothing)
 #endif
 end subroutine
+
+! Both candidates score 5. The conditions differ, so neither selector is a
+! subset of the other and `a` is tested first.
+! CHECK-LABEL: func.func @_QPtest_distinct_conditions(
+! CHECK-SAME:    %[[ARG0:[^,]*]]: !fir.ref<!fir.logical<4>>
+! CHECK-SAME:    %[[ARG1:.*]]: !fir.ref<!fir.logical<4>>
+! CHECK:         %[[DECLA:.*]]:2 = hlfir.declare %[[ARG0]]
+! CHECK:         %[[DECLB:.*]]:2 = hlfir.declare %[[ARG1]]
+! CHECK:         %[[LOADA:.*]] = fir.load %[[DECLA]]#0
+! CHECK:         %[[CONDA:.*]] = fir.convert %[[LOADA]] : (!fir.logical<4>) -> i1
+! CHECK:         fir.if %[[CONDA]] {
+! CHECK-NEXT:      omp.taskyield
+! CHECK-NEXT:    } else {
+! CHECK:           %[[LOADB:.*]] = fir.load %[[DECLB]]#0
+! CHECK:           %[[CONDB:.*]] = fir.convert %[[LOADB]] : (!fir.logical<4>) -> i1
+! CHECK:           fir.if %[[CONDB]] {
+! CHECK-NEXT:        omp.taskwait
+! CHECK:         return
+subroutine test_distinct_conditions(a, b)
+  logical, intent(in) :: a, b
+  !$omp metadirective &
+  !$omp & when(user={condition(a)}, &
+  !$omp &      implementation={vendor(score(4): llvm)}: taskyield) &
+  !$omp & when(user={condition(b)}, device={kind(host)}, &
+  !$omp &      implementation={vendor(score(3): llvm)}: taskwait)
+end subroutine
+
+! With the same condition, the first selector is a strict subset of the second.
+! CHECK-LABEL: func.func @_QPtest_same_condition(
+! CHECK:         fir.if
+! CHECK-NEXT:      omp.taskwait
+! CHECK-NOT:     omp.taskyield
+! CHECK:         return
+subroutine test_same_condition(a)
+  logical, intent(in) :: a
+  !$omp metadirective &
+  !$omp & when(user={condition(a)}, &
+  !$omp &      implementation={vendor(score(4): llvm)}: taskyield) &
+  !$omp & when(user={condition(a)}, device={kind(host)}, &
+  !$omp &      implementation={vendor(score(3): llvm)}: taskwait)
+end subroutine
+
+! Outer parentheses don't change the condition.
+! CHECK-LABEL: func.func @_QPtest_parenthesized_condition(
+! CHECK:         fir.if
+! CHECK-NEXT:      omp.taskwait
+! CHECK-NOT:     omp.taskyield
+! CHECK:         return
+subroutine test_parenthesized_condition(a)
+  logical, intent(in) :: a
+  !$omp metadirective &
+  !$omp & when(user={condition((a))}, &
+  !$omp &      implementation={vendor(score(4): llvm)}: taskyield) &
+  !$omp & when(user={condition(a)}, device={kind(host)}, &
+  !$omp &      implementation={vendor(score(3): llvm)}: taskwait)
+end subroutine
+
+! Under match_none, both candidates score 3 and `a` is tested first.
+! CHECK-LABEL: func.func @_QPtest_distinct_conditions_match_none(
+! CHECK-SAME:    %[[ARG0:[^,]*]]: !fir.ref<!fir.logical<4>>
+! CHECK:         %[[DECLA:.*]]:2 = hlfir.declare %[[ARG0]]
+! CHECK:         %[[LOADA:.*]] = fir.load %[[DECLA]]#0
+! CHECK:         %[[CONDA:.*]] = fir.convert %[[LOADA]] : (!fir.logical<4>) -> i1
+! CHECK:         %[[NOTA:.*]] = arith.xori %[[CONDA]]
+! CHECK:         fir.if %[[NOTA]] {
+! CHECK-NEXT:      omp.taskyield
+! CHECK-NEXT:    } else {
+! CHECK:           omp.taskwait
+! CHECK:         return
+subroutine test_distinct_conditions_match_none(a, b)
+  logical, intent(in) :: a, b
+  !$omp metadirective &
+  !$omp & when(user={condition(score(2): a)}, &
+  !$omp &      implementation={extension(match_none)}: taskyield) &
+  !$omp & when(user={condition(b)}, device={arch(nvptx)}, &
+  !$omp &      implementation={extension(match_none)}: taskwait)
+end subroutine
+
+! Constant conditions are compared by truth value, whatever their kind.
+! CHECK-LABEL: func.func @_QPtest_constant_condition_kinds(
+! CHECK-NOT:     omp.taskyield
+! CHECK:         omp.taskwait
+! CHECK-NOT:     omp.taskyield
+! CHECK:         return
+subroutine test_constant_condition_kinds()
+  !$omp metadirective &
+  !$omp & when(user={condition(.true._8)}, &
+  !$omp &      implementation={vendor(score(4): llvm)}: taskyield) &
+  !$omp & when(user={condition(.true._4)}, device={kind(host)}, &
+  !$omp &      implementation={vendor(score(3): llvm)}: taskwait)
+end subroutine
+
+! Calls to intrinsics without arguments are evaluated at runtime.
+! CHECK-LABEL: func.func @_QPtest_intrinsic_call_conditions(
+! CHECK:         arith.cmpi sgt, %{{.*}}, %c0
+! CHECK:         fir.if
+! CHECK-NEXT:      omp.taskyield
+! CHECK-NEXT:    } else {
+! CHECK:           arith.cmpi sgt, %{{.*}}, %c1
+! CHECK:           fir.if
+! CHECK-NEXT:        omp.taskwait
+! CHECK:         return
+subroutine test_intrinsic_call_conditions()
+  !$omp metadirective &
+  !$omp & when(user={condition(command_argument_count() > 0)}, &
+  !$omp &      implementation={vendor(score(4): llvm)}: taskyield) &
+  !$omp & when(user={condition(command_argument_count() > 1)}, &
+  !$omp &      device={kind(host)}, &
+  !$omp &      implementation={vendor(score(3): llvm)}: taskwait)
+end subroutine
+
+module metadirective_user_a
+contains
+  logical function predicate()
+    predicate = .true.
+  end function
+  logical function invoke(p)
+    interface
+      logical function p()
+      end function
+    end interface
+    invoke = p()
+  end function
+end module
+
+module metadirective_user_b
+contains
+  logical function predicate()
+    predicate = .false.
+  end function
+end module
+
+! The conditions pass different procedures with the same name, so `a` is
+! tested first.
+! CHECK-LABEL: func.func @_QPtest_same_name_conditions(
+! CHECK:         fir.address_of(@_QMmetadirective_user_aPpredicate)
+! CHECK:         fir.if
+! CHECK-NEXT:      omp.taskyield
+! CHECK-NEXT:    } else {
+! CHECK:           fir.address_of(@_QMmetadirective_user_bPpredicate)
+! CHECK:           fir.if
+! CHECK-NEXT:        omp.taskwait
+! CHECK:         return
+subroutine test_same_name_conditions()
+  use metadirective_user_a, only: a => predicate, invoke
+  use metadirective_user_b, only: b => predicate
+  !$omp metadirective &
+  !$omp & when(user={condition(invoke(a))}, &
+  !$omp &      implementation={vendor(score(4): llvm)}: taskyield) &
+  !$omp & when(user={condition(invoke(b))}, device={kind(host)}, &
+  !$omp &      implementation={vendor(score(3): llvm)}: taskwait)
+end subroutine
+
+! `a` and `b` are the first dummy arguments of different procedures, but they
+! are different variables, so `a` is tested first.
+! CHECK-LABEL: func.func private @_QFtest_host_dummy_conditionsPinner(
+! CHECK-SAME:    %[[ARG0:[^,]*]]: !fir.ref<!fir.logical<4>>
+! CHECK:         %[[A:.*]]:2 = hlfir.declare {{.*}}"_QFtest_host_dummy_conditionsEa"
+! CHECK:         %[[B:.*]]:2 = hlfir.declare %[[ARG0]] {{.*}}"_QFtest_host_dummy_conditionsFinnerEb"
+! CHECK:         %[[LOADA:.*]] = fir.load %[[A]]#0
+! CHECK:         %[[CONDA:.*]] = fir.convert %[[LOADA]]
+! CHECK:         fir.if %[[CONDA]] {
+! CHECK-NEXT:      omp.taskyield
+! CHECK-NEXT:    } else {
+! CHECK:           fir.load %[[B]]#0
+! CHECK:           omp.taskwait
+! CHECK:         return
+subroutine test_host_dummy_conditions(a)
+  logical, intent(in) :: a
+  call inner(a)
+contains
+  subroutine inner(b)
+    logical, intent(in) :: b
+    !$omp metadirective &
+    !$omp & when(user={condition(a)}, &
+    !$omp &      implementation={vendor(score(4): llvm)}: taskyield) &
+    !$omp & when(user={condition(b)}, device={kind(host)}, &
+    !$omp &      implementation={vendor(score(3): llvm)}: taskwait)
+  end subroutine
+end subroutine
+
+! `a .and. b` is not the same condition as `a`, so it is tested first.
+! CHECK-LABEL: func.func private @_QFtest_host_dummy_and_conditionPinner(
+! CHECK:         fir.logical_and
+! CHECK:         fir.if
+! CHECK-NEXT:      omp.taskyield
+! CHECK-NEXT:    } else {
+! CHECK:           omp.taskwait
+! CHECK:         return
+subroutine test_host_dummy_and_condition(a)
+  logical, intent(in) :: a
+  call inner(a)
+contains
+  subroutine inner(b)
+    logical, intent(in) :: b
+    !$omp metadirective &
+    !$omp & when(user={condition(a .and. b)}, &
+    !$omp &      implementation={vendor(score(4): llvm)}: taskyield) &
+    !$omp & when(user={condition(a)}, device={kind(host)}, &
+    !$omp &      implementation={vendor(score(3): llvm)}: taskwait)
+  end subroutine
+end subroutine
+
+! The array constructors have lengths from different dummy arguments.
+! CHECK-LABEL: func.func private @_QFtest_host_dummy_lengthsPinner(
+! CHECK:         fir.if
+! CHECK-NEXT:      omp.taskyield
+! CHECK-NEXT:    } else {
+! CHECK:           omp.taskwait
+! CHECK:         return
+subroutine test_host_dummy_lengths(n)
+  integer, intent(in) :: n
+  call inner(n)
+contains
+  subroutine inner(m)
+    integer, intent(in) :: m
+    !$omp metadirective &
+    !$omp & when(user={condition(maxval([character(len=n)::]) == '')}, &
+    !$omp &      implementation={vendor(score(4): llvm)}: taskyield) &
+    !$omp & when(user={condition(maxval([character(len=m)::]) == '')}, &
+    !$omp &      device={kind(host)}, &
+    !$omp &      implementation={vendor(score(3): llvm)}: taskwait)
+  end subroutine
+end subroutine
diff --git a/llvm/include/llvm/Frontend/OpenMP/OMPContext.h b/llvm/include/llvm/Frontend/OpenMP/OMPContext.h
index 7849d326659944..c8c69823d59edd 100644
--- a/llvm/include/llvm/Frontend/OpenMP/OMPContext.h
+++ b/llvm/include/llvm/Frontend/OpenMP/OMPContext.h
@@ -22,6 +22,8 @@
 #include "llvm/Frontend/OpenMP/OMPConstants.h"
 #include "llvm/Support/Compiler.h"
 
+#include <string>
+
 namespace llvm {
 class Triple;
 namespace omp {
@@ -156,6 +158,8 @@ struct VariantMatchInfo {
   SmallVector<StringRef, 8> ISATraits;
   SmallVector<TraitProperty, 8> ConstructTraits;
   SmallDenseMap<TraitProperty, APInt> ScoreMap;
+  /// Identifies a runtime user condition; empty for a constant condition.
+  std::string UserCondition;
 };
 
 /// The context for a source location is made up of active property traits,
diff --git a/llvm/lib/Frontend/OpenMP/OMPContext.cpp b/llvm/lib/Frontend/OpenMP/OMPContext.cpp
index 268f067031dd89..880e26a440a216 100644
--- a/llvm/lib/Frontend/OpenMP/OMPContext.cpp
+++ b/llvm/lib/Frontend/OpenMP/OMPContext.cpp
@@ -172,6 +172,13 @@ static bool isStrictSubset(const VariantMatchInfo &VMI0,
   for (unsigned Bit : VMI0.RequiredTraits.set_bits())
     if (!VMI1.RequiredTraits.test(Bit))
       return false;
+  // Different runtime conditions share a trait, so compare them as well.
+  for (TraitProperty Property :
+       {TraitProperty::user_condition_true, TraitProperty::user_condition_false,
+        TraitProperty::user_condition_unknown})
+    if (VMI0.RequiredTraits.test(unsigned(Property)) &&
+        VMI0.UserCondition != VMI1.UserCondition)
+      return false;
   if (!isSubset<TraitProperty>(VMI0.ConstructTraits, VMI1.ConstructTraits))
     return false;
   return true;
diff --git a/llvm/unittests/Frontend/OpenMPContextTest.cpp b/llvm/unittests/Frontend/OpenMPContextTest.cpp
index 7f6ac92c6259c8..8e03969dffbd5f 100644
--- a/llvm/unittests/Frontend/OpenMPContextTest.cpp
+++ b/llvm/unittests/Frontend/OpenMPContextTest.cpp
@@ -340,4 +340,41 @@ TEST_F(OpenMPContextTest, StrictSubsetOfLaterTiedVariant) {
   EXPECT_EQ(Best({Subset, Superset, Other}), 1);
 }
 
+TEST_F(OpenMPContextTest, UserConditionInStrictSubset) {
+  OMPContext Context(false, Triple("x86_64-unknown-linux"), Triple(), -1);
+  APInt Three(64, 3), Four(64, 4), Five(64, 5);
+  VariantMatchInfo A, B;
+  A.addTrait(TraitProperty::user_condition_true, "");
+  A.addTrait(TraitProperty::implementation_vendor_llvm, "", &Four);
+  B.addTrait(TraitProperty::user_condition_true, "");
+  B.addTrait(TraitProperty::implementation_vendor_llvm, "", &Three);
+  B.addTrait(TraitProperty::device_kind_host, "");
+  auto Best = [&](const VariantMatchInfo &First,
+                  const VariantMatchInfo &Second) {
+    return getBestVariantMatchForContext(
+        SmallVector<VariantMatchInfo, 2>{First, Second}, Context);
+  };
+
+  // Both score 5. Constant conditions are compared by their trait alone.
+  EXPECT_EQ(Best(A, B), 1);
+  EXPECT_EQ(Best(B, A), 0);
+
+  // Different runtime conditions keep clause order.
+  A.UserCondition = "a";
+  B.UserCondition = "b";
+  EXPECT_EQ(Best(A, B), 0);
+  EXPECT_EQ(Best(B, A), 0);
+
+  // The same runtime condition still makes A a strict subset of B.
+  B.UserCondition = "a";
+  EXPECT_EQ(Best(A, B), 1);
+  EXPECT_EQ(Best(B, A), 0);
+
+  // A higher score wins regardless of the conditions.
+  B.UserCondition = "b";
+  A.ScoreMap[TraitProperty::implementation_vendor_llvm] = Five;
+  EXPECT_EQ(Best(A, B), 0);
+  EXPECT_EQ(Best(B, A), 1);
+}
+
 } // namespace

>From fde2d289cebbf062c32cf9ab2277107e79b7e5b6 Mon Sep 17 00:00:00 2001
From: "Chi Chun, Chen" <chichun.chen at hpe.com>
Date: Mon, 19 Oct 2026 23:49:38 -0500
Subject: [PATCH 2/6] Compare the condition of an unguarded match_any candidate

A match_any WHEN clause with a runtime condition also gets a candidate
without the condition, since its other traits can still match. That
candidate has no condition trait, so isStrictSubset skipped the key
check, and it became a subset of a WHEN clause with a different
condition. The key is now compared whenever it is set.
---
 .../test/Lower/OpenMP/metadirective-user.f90  | 35 +++++++++++++++++++
 llvm/lib/Frontend/OpenMP/OMPContext.cpp       |  3 ++
 llvm/unittests/Frontend/OpenMPContextTest.cpp | 14 ++++++++
 3 files changed, 52 insertions(+)

diff --git a/flang/test/Lower/OpenMP/metadirective-user.f90 b/flang/test/Lower/OpenMP/metadirective-user.f90
index fb12ba8739ea83..a27d70755544a7 100644
--- a/flang/test/Lower/OpenMP/metadirective-user.f90
+++ b/flang/test/Lower/OpenMP/metadirective-user.f90
@@ -637,6 +637,41 @@ subroutine test_distinct_conditions_match_none(a, b)
   !$omp &      implementation={extension(match_none)}: taskwait)
 end subroutine
 
+! Under match_any, the first WHEN also matches through `vendor(llvm)` when `a` is
+! false. Both score 3 and the conditions differ, so TASKYIELD is always selected.
+! CHECK-LABEL: func.func @_QPtest_distinct_conditions_match_any(
+! CHECK-NOT:     omp.taskwait
+! CHECK:         omp.taskyield
+! CHECK-NOT:     omp.taskwait
+! CHECK:         return
+subroutine test_distinct_conditions_match_any(a, b)
+  logical, intent(in) :: a, b
+  !$omp metadirective &
+  !$omp & when(user={condition(a)}, &
+  !$omp &      implementation={vendor(score(2): llvm), extension(match_any)}: &
+  !$omp &      taskyield) &
+  !$omp & when(user={condition(b)}, device={kind(host)}, &
+  !$omp &      implementation={vendor(score(1): llvm), extension(match_any)}: &
+  !$omp &      taskwait)
+end subroutine
+
+! A constant condition is different from a runtime condition.
+! CHECK-LABEL: func.func @_QPtest_constant_and_runtime_conditions_match_any(
+! CHECK-NOT:     omp.taskwait
+! CHECK:         omp.taskyield
+! CHECK-NOT:     omp.taskwait
+! CHECK:         return
+subroutine test_constant_and_runtime_conditions_match_any(b)
+  logical, intent(in) :: b
+  !$omp metadirective &
+  !$omp & when(user={condition(.true.)}, &
+  !$omp &      implementation={vendor(score(2): llvm), extension(match_any)}: &
+  !$omp &      taskyield) &
+  !$omp & when(user={condition(b)}, device={kind(host)}, &
+  !$omp &      implementation={vendor(score(1): llvm), extension(match_any)}: &
+  !$omp &      taskwait)
+end subroutine
+
 ! Constant conditions are compared by truth value, whatever their kind.
 ! CHECK-LABEL: func.func @_QPtest_constant_condition_kinds(
 ! CHECK-NOT:     omp.taskyield
diff --git a/llvm/lib/Frontend/OpenMP/OMPContext.cpp b/llvm/lib/Frontend/OpenMP/OMPContext.cpp
index 880e26a440a216..f0108ae05451c3 100644
--- a/llvm/lib/Frontend/OpenMP/OMPContext.cpp
+++ b/llvm/lib/Frontend/OpenMP/OMPContext.cpp
@@ -173,6 +173,9 @@ static bool isStrictSubset(const VariantMatchInfo &VMI0,
     if (!VMI1.RequiredTraits.test(Bit))
       return false;
   // Different runtime conditions share a trait, so compare them as well.
+  // A runtime condition can be left out of the traits but keep its key.
+  if (!VMI0.UserCondition.empty() && VMI0.UserCondition != VMI1.UserCondition)
+    return false;
   for (TraitProperty Property :
        {TraitProperty::user_condition_true, TraitProperty::user_condition_false,
         TraitProperty::user_condition_unknown})
diff --git a/llvm/unittests/Frontend/OpenMPContextTest.cpp b/llvm/unittests/Frontend/OpenMPContextTest.cpp
index 8e03969dffbd5f..8446156ae6aa61 100644
--- a/llvm/unittests/Frontend/OpenMPContextTest.cpp
+++ b/llvm/unittests/Frontend/OpenMPContextTest.cpp
@@ -375,6 +375,20 @@ TEST_F(OpenMPContextTest, UserConditionInStrictSubset) {
   A.ScoreMap[TraitProperty::implementation_vendor_llvm] = Five;
   EXPECT_EQ(Best(A, B), 0);
   EXPECT_EQ(Best(B, A), 1);
+
+  // A runtime condition left out of the traits is still compared by its key.
+  VariantMatchInfo C;
+  C.addTrait(TraitProperty::implementation_vendor_llvm, "", &Four);
+  C.UserCondition = "a";
+  EXPECT_EQ(Best(C, B), 0);
+  EXPECT_EQ(Best(B, C), 0);
+  C.UserCondition = "b";
+  EXPECT_EQ(Best(C, B), 1);
+
+  // A constant condition is different from a runtime condition.
+  C.UserCondition = "";
+  C.addTrait(TraitProperty::user_condition_true, "");
+  EXPECT_EQ(Best(C, B), 0);
 }
 
 } // namespace

>From cddd17261b12c7e362847177ab454ff21f9aec49 Mon Sep 17 00:00:00 2001
From: "Chi Chun, Chen" <chichun.chen at hpe.com>
Date: Tue, 20 Oct 2026 12:36:55 -0500
Subject: [PATCH 3/6] Add a three-way runtime condition tie to the tests

The first WHEN clause is a strict subset of the third, which ties with the
second. Clause order then tests `b` first, and TASKYIELD is never
selected.
---
 .../test/Lower/OpenMP/metadirective-user.f90  | 29 +++++++++++++++++++
 1 file changed, 29 insertions(+)

diff --git a/flang/test/Lower/OpenMP/metadirective-user.f90 b/flang/test/Lower/OpenMP/metadirective-user.f90
index a27d70755544a7..44545487cae31c 100644
--- a/flang/test/Lower/OpenMP/metadirective-user.f90
+++ b/flang/test/Lower/OpenMP/metadirective-user.f90
@@ -586,6 +586,35 @@ subroutine test_distinct_conditions(a, b)
   !$omp &      implementation={vendor(score(3): llvm)}: taskwait)
 end subroutine
 
+! All three score 5. The first is a strict subset of the third, which ties with
+! the second, so `b` is tested first and TASKYIELD is never selected.
+! CHECK-LABEL: func.func @_QPtest_distinct_conditions_three_way(
+! CHECK-SAME:    %[[ARG0:[^,]*]]: !fir.ref<!fir.logical<4>>
+! CHECK-SAME:    %[[ARG1:.*]]: !fir.ref<!fir.logical<4>>
+! CHECK:         %[[DECLA:.*]]:2 = hlfir.declare %[[ARG0]]
+! CHECK:         %[[DECLB:.*]]:2 = hlfir.declare %[[ARG1]]
+! CHECK:         %[[LOADB:.*]] = fir.load %[[DECLB]]#0
+! CHECK:         %[[CONDB:.*]] = fir.convert %[[LOADB]] : (!fir.logical<4>) -> i1
+! CHECK:         fir.if %[[CONDB]] {
+! CHECK-NEXT:      omp.taskwait
+! CHECK-NEXT:    } else {
+! CHECK:           %[[LOADA:.*]] = fir.load %[[DECLA]]#0
+! CHECK:           %[[CONDA:.*]] = fir.convert %[[LOADA]] : (!fir.logical<4>) -> i1
+! CHECK:           fir.if %[[CONDA]] {
+! CHECK-NEXT:        omp.barrier
+! CHECK-NOT:     omp.taskyield
+! CHECK:         return
+subroutine test_distinct_conditions_three_way(a, b)
+  logical, intent(in) :: a, b
+  !$omp metadirective &
+  !$omp & when(user={condition(a)}, &
+  !$omp &      implementation={vendor(score(4): llvm)}: taskyield) &
+  !$omp & when(user={condition(b)}, device={kind(host)}, &
+  !$omp &      implementation={vendor(score(3): llvm)}: taskwait) &
+  !$omp & when(user={condition(a)}, device={kind(host)}, &
+  !$omp &      implementation={vendor(score(3): llvm)}: barrier)
+end subroutine
+
 ! With the same condition, the first selector is a strict subset of the second.
 ! CHECK-LABEL: func.func @_QPtest_same_condition(
 ! CHECK:         fir.if

>From 360ea0be1f02659cb308f923546724f023c66a30 Mon Sep 17 00:00:00 2001
From: "Chi Chun, Chen" <chichun.chen at hpe.com>
Date: Tue, 20 Oct 2026 22:15:43 -0500
Subject: [PATCH 4/6] Fold logical user conditions of any kind

EvaluateUserCondition only folded default-kind logicals, so a condition
like .false._8 stayed a runtime check. getConditionKey folded it on its
own and gave it the empty key of a constant, so the WHEN clause was
ranked like a true constant although it can never apply.
EvaluateUserCondition now folds logical constants of any kind, and
getConditionKey only sees runtime conditions.
---
 flang/lib/Semantics/openmp-utils.cpp           | 18 ++++++++++++------
 flang/test/Lower/OpenMP/metadirective-user.f90 | 18 ++++++++++++++++++
 2 files changed, 30 insertions(+), 6 deletions(-)

diff --git a/flang/lib/Semantics/openmp-utils.cpp b/flang/lib/Semantics/openmp-utils.cpp
index 9a116c886f1a79..987db8c2c36f82 100644
--- a/flang/lib/Semantics/openmp-utils.cpp
+++ b/flang/lib/Semantics/openmp-utils.cpp
@@ -2352,9 +2352,17 @@ std::optional<bool> EvaluateUserCondition(
   if (auto constVal = evaluate::ToInt64(foldedExpr))
     return *constVal != 0;
 
-  if (auto logicalVal =
-          evaluate::GetScalarConstantValue<evaluate::LogicalResult>(foldedExpr))
-    return logicalVal->IsTrue();
+  if (const auto *logical{
+          evaluate::UnwrapExpr<evaluate::Expr<evaluate::SomeLogical>>(
+              foldedExpr)})
+    return common::visit(
+        [](const auto &x) -> std::optional<bool> {
+          using T = typename std::decay_t<decltype(x)>::Result;
+          if (auto value{evaluate::GetScalarConstantValue<T>(x)})
+            return value->IsTrue();
+          return std::nullopt;
+        },
+        logical->u);
 
   return std::nullopt;
 }
@@ -2590,9 +2598,7 @@ std::optional<MetadirectiveCandidateSet> BuildMetadirectiveCandidateSet(
   llvm::SmallVector<SomeExpr, 4> runtimeConditions;
   auto getConditionKey = [&](const DynamicUserCondition &condition) {
     const SomeExpr *expr{GetExpr(context, *condition.expr)};
-    if (!expr ||
-        evaluate::IsActuallyConstant(
-            evaluate::Fold(context.foldingContext(), common::Clone(*expr))))
+    if (!expr)
       return std::string{};
     SomeExpr normalized{NormalizeMetadirectiveCondition(*expr)};
     auto *it{llvm::find_if(runtimeConditions, [&](const SomeExpr &condition) {
diff --git a/flang/test/Lower/OpenMP/metadirective-user.f90 b/flang/test/Lower/OpenMP/metadirective-user.f90
index 44545487cae31c..1c8f6dca5866c8 100644
--- a/flang/test/Lower/OpenMP/metadirective-user.f90
+++ b/flang/test/Lower/OpenMP/metadirective-user.f90
@@ -715,6 +715,24 @@ subroutine test_constant_condition_kinds()
   !$omp &      implementation={vendor(score(3): llvm)}: taskwait)
 end subroutine
 
+! A false constant of any kind makes the third WHEN clause inapplicable, so the
+! first two tie and TASKYIELD is selected.
+! CHECK-LABEL: func.func @_QPtest_false_constant_condition_kind(
+! CHECK-NOT:     {{fir.if|omp.taskwait|omp.barrier}}
+! CHECK:         omp.taskyield
+! CHECK-NOT:     {{fir.if|omp.taskwait|omp.barrier}}
+! CHECK:         return
+subroutine test_false_constant_condition_kind(b)
+  logical, intent(in) :: b
+  !$omp metadirective &
+  !$omp & when(device={kind(host)}, implementation={vendor(score(3): llvm)}: &
+  !$omp &      taskyield) &
+  !$omp & when(user={condition(b)}, implementation={vendor(score(4): llvm)}: &
+  !$omp &      taskwait) &
+  !$omp & when(user={condition(.false._8)}, device={kind(host)}, &
+  !$omp &      implementation={vendor(score(3): llvm)}: barrier)
+end subroutine
+
 ! Calls to intrinsics without arguments are evaluated at runtime.
 ! CHECK-LABEL: func.func @_QPtest_intrinsic_call_conditions(
 ! CHECK:         arith.cmpi sgt, %{{.*}}, %c0

>From ffe882fe8d6452d25fe8b941c70855cac17da65f Mon Sep 17 00:00:00 2001
From: "Chi Chun, Chen" <chichun.chen at hpe.com>
Date: Tue, 20 Oct 2026 22:23:18 -0500
Subject: [PATCH 5/6] Count a retained runtime condition in the strict-subset
 check

The unguarded candidate of a match_any WHEN clause keeps its condition
key but not the condition trait, so it had one trait fewer than an
identical guarded candidate and counted as its strict subset. A key
without a trait now counts as a trait. A WHEN clause with an omitted
directive is ranked without its condition, so it no longer gets a key.
---
 flang/lib/Semantics/openmp-utils.cpp          |  3 +-
 .../test/Lower/OpenMP/metadirective-user.f90  | 20 +++++++++++++
 llvm/lib/Frontend/OpenMP/OMPContext.cpp       | 28 +++++++++++++------
 llvm/unittests/Frontend/OpenMPContextTest.cpp | 11 ++++++++
 4 files changed, 52 insertions(+), 10 deletions(-)

diff --git a/flang/lib/Semantics/openmp-utils.cpp b/flang/lib/Semantics/openmp-utils.cpp
index 987db8c2c36f82..59c220579ba5c0 100644
--- a/flang/lib/Semantics/openmp-utils.cpp
+++ b/flang/lib/Semantics/openmp-utils.cpp
@@ -2623,7 +2623,8 @@ std::optional<MetadirectiveCandidateSet> BuildMetadirectiveCandidateSet(
       llvm::omp::VariantMatchInfo rawVMI;
       std::optional<DynamicUserCondition> dynamicCondition{
           MakeVariantMatchInfo(rawVMI, *ctxSel, context)};
-      if (dynamicCondition)
+      // An omitted directive is ranked without its condition.
+      if (dynamicCondition && isExplicit)
         rawVMI.UserCondition = getConditionKey(*dynamicCondition);
       if (llvm::any_of(
               rawVMI.ConstructTraits, [](llvm::omp::TraitProperty property) {
diff --git a/flang/test/Lower/OpenMP/metadirective-user.f90 b/flang/test/Lower/OpenMP/metadirective-user.f90
index 1c8f6dca5866c8..d1ba840790c2e9 100644
--- a/flang/test/Lower/OpenMP/metadirective-user.f90
+++ b/flang/test/Lower/OpenMP/metadirective-user.f90
@@ -701,6 +701,26 @@ subroutine test_constant_and_runtime_conditions_match_any(b)
   !$omp &      taskwait)
 end subroutine
 
+! The first and third WHEN clauses are identical, so the first wins the tie, also
+! through `vendor(llvm)` when `a` is false.
+! CHECK-LABEL: func.func @_QPtest_same_condition_match_any(
+! CHECK-NOT:     {{fir.if|omp.taskwait|omp.barrier}}
+! CHECK:         omp.taskyield
+! CHECK-NOT:     {{fir.if|omp.taskwait|omp.barrier}}
+! CHECK:         return
+subroutine test_same_condition_match_any(a)
+  logical, intent(in) :: a
+  !$omp metadirective &
+  !$omp & when(user={condition(a)}, &
+  !$omp &      implementation={vendor(score(2): llvm), extension(match_any)}: &
+  !$omp &      taskyield) &
+  !$omp & when(user={condition(command_argument_count() > 0)}, &
+  !$omp &      implementation={vendor(score(2): llvm)}: taskwait) &
+  !$omp & when(user={condition(a)}, &
+  !$omp &      implementation={vendor(score(2): llvm), extension(match_any)}: &
+  !$omp &      barrier)
+end subroutine
+
 ! Constant conditions are compared by truth value, whatever their kind.
 ! CHECK-LABEL: func.func @_QPtest_constant_condition_kinds(
 ! CHECK-NOT:     omp.taskyield
diff --git a/llvm/lib/Frontend/OpenMP/OMPContext.cpp b/llvm/lib/Frontend/OpenMP/OMPContext.cpp
index f0108ae05451c3..401bba5c9f35b8 100644
--- a/llvm/lib/Frontend/OpenMP/OMPContext.cpp
+++ b/llvm/lib/Frontend/OpenMP/OMPContext.cpp
@@ -162,26 +162,36 @@ template <typename T> static bool isSubset(ArrayRef<T> C0, ArrayRef<T> C1) {
   return true;
 }
 
+static bool hasUserConditionTrait(const VariantMatchInfo &VMI) {
+  for (TraitProperty Property :
+       {TraitProperty::user_condition_true, TraitProperty::user_condition_false,
+        TraitProperty::user_condition_unknown})
+    if (VMI.RequiredTraits.test(unsigned(Property)))
+      return true;
+  return false;
+}
+
+// A runtime condition can be left out of the traits but keep its key, so count
+// it as a trait.
+static unsigned countTraits(const VariantMatchInfo &VMI) {
+  return VMI.RequiredTraits.count() +
+         (!VMI.UserCondition.empty() && !hasUserConditionTrait(VMI));
+}
+
 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())
+  if (countTraits(VMI0) >= countTraits(VMI1))
     return false;
   for (unsigned Bit : VMI0.RequiredTraits.set_bits())
     if (!VMI1.RequiredTraits.test(Bit))
       return false;
   // Different runtime conditions share a trait, so compare them as well.
-  // A runtime condition can be left out of the traits but keep its key.
-  if (!VMI0.UserCondition.empty() && VMI0.UserCondition != VMI1.UserCondition)
+  if ((!VMI0.UserCondition.empty() || hasUserConditionTrait(VMI0)) &&
+      VMI0.UserCondition != VMI1.UserCondition)
     return false;
-  for (TraitProperty Property :
-       {TraitProperty::user_condition_true, TraitProperty::user_condition_false,
-        TraitProperty::user_condition_unknown})
-    if (VMI0.RequiredTraits.test(unsigned(Property)) &&
-        VMI0.UserCondition != VMI1.UserCondition)
-      return false;
   if (!isSubset<TraitProperty>(VMI0.ConstructTraits, VMI1.ConstructTraits))
     return false;
   return true;
diff --git a/llvm/unittests/Frontend/OpenMPContextTest.cpp b/llvm/unittests/Frontend/OpenMPContextTest.cpp
index 8446156ae6aa61..fc7b28ae72c406 100644
--- a/llvm/unittests/Frontend/OpenMPContextTest.cpp
+++ b/llvm/unittests/Frontend/OpenMPContextTest.cpp
@@ -389,6 +389,17 @@ TEST_F(OpenMPContextTest, UserConditionInStrictSubset) {
   C.UserCondition = "";
   C.addTrait(TraitProperty::user_condition_true, "");
   EXPECT_EQ(Best(C, B), 0);
+
+  // A condition left out of the traits still counts as one, so D, which only
+  // has the key, is not a strict subset of E, which also has the trait.
+  VariantMatchInfo D, E;
+  D.addTrait(TraitProperty::implementation_vendor_llvm, "", &Four);
+  D.UserCondition = "a";
+  E.addTrait(TraitProperty::user_condition_true, "");
+  E.addTrait(TraitProperty::implementation_vendor_llvm, "", &Four);
+  E.UserCondition = "a";
+  EXPECT_EQ(Best(D, E), 0);
+  EXPECT_EQ(Best(E, D), 0);
 }
 
 } // namespace

>From 17561c62066f9f0f1878c4f2cb03ba2c0738efb4 Mon Sep 17 00:00:00 2001
From: "Chi Chun, Chen" <chichun.chen at hpe.com>
Date: Tue, 20 Oct 2026 22:25:10 -0500
Subject: [PATCH 6/6] Check that TASKYIELD is unconditional in the match_any
 tests

---
 flang/test/Lower/OpenMP/metadirective-user.f90 | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/flang/test/Lower/OpenMP/metadirective-user.f90 b/flang/test/Lower/OpenMP/metadirective-user.f90
index d1ba840790c2e9..46a45639650b88 100644
--- a/flang/test/Lower/OpenMP/metadirective-user.f90
+++ b/flang/test/Lower/OpenMP/metadirective-user.f90
@@ -669,9 +669,9 @@ subroutine test_distinct_conditions_match_none(a, b)
 ! Under match_any, the first WHEN also matches through `vendor(llvm)` when `a` is
 ! false. Both score 3 and the conditions differ, so TASKYIELD is always selected.
 ! CHECK-LABEL: func.func @_QPtest_distinct_conditions_match_any(
-! CHECK-NOT:     omp.taskwait
+! CHECK-NOT:     {{fir.if|omp.taskwait}}
 ! CHECK:         omp.taskyield
-! CHECK-NOT:     omp.taskwait
+! CHECK-NOT:     {{fir.if|omp.taskwait}}
 ! CHECK:         return
 subroutine test_distinct_conditions_match_any(a, b)
   logical, intent(in) :: a, b
@@ -686,9 +686,9 @@ subroutine test_distinct_conditions_match_any(a, b)
 
 ! A constant condition is different from a runtime condition.
 ! CHECK-LABEL: func.func @_QPtest_constant_and_runtime_conditions_match_any(
-! CHECK-NOT:     omp.taskwait
+! CHECK-NOT:     {{fir.if|omp.taskwait}}
 ! CHECK:         omp.taskyield
-! CHECK-NOT:     omp.taskwait
+! CHECK-NOT:     {{fir.if|omp.taskwait}}
 ! CHECK:         return
 subroutine test_constant_and_runtime_conditions_match_any(b)
   logical, intent(in) :: b



More information about the llvm-branch-commits mailing list