[flang-commits] [flang] [flang][OpenMP] Lower multi-type user-defined declare reduction (PR #207494)

via flang-commits flang-commits at lists.llvm.org
Sat Jul 4 00:22:10 PDT 2026


https://github.com/MattPD created https://github.com/llvm/llvm-project/pull/207494

A user-defined declare reduction covering several types silently miscompiled: every listed type bound one type-less omp.declare_reduction op, so a variable of any but the first type ran through an op built for the wrong type (an !fir.ref<f32> reduction on an i32 op, a mismatch the verifier does not catch). This affects all three forms: operator (.op.), named (myred), and a name that shadows an intrinsic (max/min/iand/ior/ieor, the arm added by llvm-project#205893).

Fixes #207489.

Emit one op per listed type and bind each variable to the op for its type. The directive loop threads a per-type instance index across the parallel type, combiner, and initializer lists so each op gets its own element type, combiner, and initializer.

Name every user-reduction op from one canonical source, getScopedUserReductionName: the reduction symbol's ultimate name, qualified with its owning scope, plus a type and by-ref suffix. Both the directive and clause sides call it, so their names cannot drift, and the use-associated cases converge on the source declaration's name. Because the name also carries the owning scope, a same-spelling same-type operator reduction imported from two modules yields two distinct ops rather than colliding on one.

Suffix the type unconditionally, including for a single-type reduction: a conditional suffix would let a multi-type myred's per-type op @_QQFmyred_i32 collide with a single-type reduction literally named myred_i32, silently binding one reduction's clause to the other's combiner.

An allocatable or pointer reduction of a trivial element type is a clean TODO, deferred to the separate boxed-type reduction work in llvm-project#186765; a mode-independent guard (a boxed operand of a trivial element type) keeps it a clean TODO even under -mmlir --force-byref-reduction. Allocatable character and derived-type reductions still lower.

The same helper also lowers use-associated named reductions, including a rename, and use-associated intrinsic-shadowing reductions.

Assisted-by: Claude Opus 4.8, GPT-5.5.


>From 7cf4240e36485a7bc82e3ba9b3b9c01acc075eed Mon Sep 17 00:00:00 2001
From: "Matt P. Dziubinski" <matt-p.dziubinski at hpe.com>
Date: Thu, 2 Jul 2026 22:25:14 -0500
Subject: [PATCH 1/4] [flang][OpenMP] Keep hermetic embedded module parse tree
 alive

Fix a use-after-free when lowering materializes an imported declare mapper or
declare reduction owned by a module embedded in a hermetic module file
(-fhermetic-module-files).

ModFileReader::Read resolved the embedded modules from a local parser::Program
that was destroyed when Read returned, leaving the symbols resolved from it
dangling. MapperDetails and UserReductionDetails hold raw parse-tree pointers, so
a consumer that later materializes such an imported declaration dereferenced
freed memory.

Retain the embedded parse tree in the SemanticsContext with SaveParseTree, as the
primary module's tree already is, so those pointers stay valid through lowering.
The new test covers materializing an imported declare mapper from an embedded
module.

Assisted-by: Claude Opus 4.8, GPT-5.5.
---
 flang/lib/Semantics/mod-file.cpp              |  7 +++-
 .../Lower/OpenMP/declare-mapper-hermetic.f90  | 38 +++++++++++++++++++
 2 files changed, 44 insertions(+), 1 deletion(-)
 create mode 100644 flang/test/Lower/OpenMP/declare-mapper-hermetic.f90

diff --git a/flang/lib/Semantics/mod-file.cpp b/flang/lib/Semantics/mod-file.cpp
index fd1b1caa7fce1..96d749a571342 100644
--- a/flang/lib/Semantics/mod-file.cpp
+++ b/flang/lib/Semantics/mod-file.cpp
@@ -1727,7 +1727,12 @@ Scope *ModFileReader::Read(SourceName name, std::optional<bool> isIntrinsic,
   // within the module file.
   Scope *previousHermetic{context_.currentHermeticModuleFileScope()};
   if (parseTree.v.size() > 1) {
-    parser::Program hermeticModules{std::move(parseTree.v)};
+    // Retain the embedded modules' parse tree for the lifetime of the
+    // SemanticsContext: symbols resolved from it (e.g. UserReductionDetails and
+    // MapperDetails, which hold raw parse-tree pointers) outlive this function
+    // and are dereferenced later, including at lowering time.
+    parser::Program &hermeticModules{
+        context_.SaveParseTree(parser::Program{std::move(parseTree.v)})};
     parseTree.v.emplace_back(std::move(hermeticModules.v.front()));
     hermeticModules.v.pop_front();
     Scope &hermeticScope{topScope.MakeScope(Scope::Kind::Global)};
diff --git a/flang/test/Lower/OpenMP/declare-mapper-hermetic.f90 b/flang/test/Lower/OpenMP/declare-mapper-hermetic.f90
new file mode 100644
index 0000000000000..334c8d25a8189
--- /dev/null
+++ b/flang/test/Lower/OpenMP/declare-mapper-hermetic.f90
@@ -0,0 +1,38 @@
+! Hermetic module files embed the modules a module uses into its own .mod. A
+! consumer that materializes an imported declare mapper owned by such an embedded
+! module dereferences parse-tree pointers (MapperDetails) into it, so the embedded
+! module's parse tree must stay live past module-file reading. map_wrap re-exports
+! map_base (which owns the mapper) and is compiled hermetically; map_base.mod is
+! then removed so the consumer can only reach the mapper through the embedding.
+
+! RUN: rm -rf %t && split-file %s %t && cd %t
+! RUN: %flang_fc1 -fsyntax-only -fopenmp -fopenmp-version=50 map_base.f90
+! RUN: %flang_fc1 -fhermetic-module-files -fsyntax-only -fopenmp -fopenmp-version=50 map_wrap.f90
+! RUN: rm map_base.mod
+! RUN: %flang_fc1 -emit-hlfir -fopenmp -fopenmp-version=50 map.use.f90 -o - | FileCheck map.use.f90
+
+!--- map_base.f90
+module map_base
+  type :: t
+    integer :: x = 0
+  end type
+  !$omp declare mapper(mymapper : t :: v) map(v%x)
+end module
+
+!--- map_wrap.f90
+module map_wrap
+  use map_base
+end module
+
+!--- map.use.f90
+! CHECK: omp.declare_mapper @[[MAPPER:_QQMmap_base[A-Za-z0-9_.]*mymapper]]
+! CHECK: mapper(@[[MAPPER]])
+program main
+  use map_wrap
+  type(t) :: obj
+  obj%x = 0
+  !$omp target map(mapper(mymapper), tofrom: obj)
+  obj%x = obj%x + 1
+  !$omp end target
+  print *, obj%x
+end program
\ No newline at end of file

>From da5e8cd21fb5f251e6f12b65d594f15051ff3424 Mon Sep 17 00:00:00 2001
From: "Matt P. Dziubinski" <matt-p.dziubinski at hpe.com>
Date: Thu, 2 Jul 2026 22:29:33 -0500
Subject: [PATCH 2/4] [flang][OpenMP] Do not emit a use-only item for a
 reduction symbol in a module file

Stop writing a module file that cannot be re-parsed (reading it crashes) when a
module re-exports a USE-associated user-defined operator declare reduction.

Such a reduction is represented by an internal symbol whose name is the mangled
operator (e.g. "op.remote."), which is not valid Fortran. The module-file writer
emitted it as a `use MODULE, only: op.remote.` item, which the reader cannot
parse. This affected both a plain re-export facade and an embedded module in a
hermetic module file.

Skip use-only emission for a use-associated reduction symbol. The reduction is
re-exported implicitly with its operator, which is emitted, and the reduction
resolver recovers it from there (FindUserReductionSymbol / SearchOperatorReduction).

The new test checks the re-exporting module file round-trips and that a consumer
reading it resolves the reduction through the operator.

Assisted-by: Claude Opus 4.8, GPT-5.5.
---
 flang/lib/Semantics/mod-file.cpp              | 15 ++++
 flang/lib/Semantics/resolve-names-utils.cpp   |  5 ++
 flang/lib/Semantics/resolve-names-utils.h     | 11 +++
 ...re-reduction-operator-modfile-reexport.f90 | 86 +++++++++++++++++++
 4 files changed, 117 insertions(+)
 create mode 100644 flang/test/Semantics/OpenMP/declare-reduction-operator-modfile-reexport.f90

diff --git a/flang/lib/Semantics/mod-file.cpp b/flang/lib/Semantics/mod-file.cpp
index 96d749a571342..3bec5836ac08f 100644
--- a/flang/lib/Semantics/mod-file.cpp
+++ b/flang/lib/Semantics/mod-file.cpp
@@ -890,6 +890,21 @@ void ModFileWriter::PutGeneric(const Symbol &symbol) {
 }
 
 void ModFileWriter::PutUse(const Symbol &symbol) {
+  // A user-defined operator declare reduction has an internal mangled
+  // symbol name ("op.<operator>.") that is not valid Fortran and cannot be
+  // emitted as a "use,only:" item: doing so produces a module file that fails
+  // to re-parse (both for a plain re-export and for an embedded module in a
+  // hermetic module file). Such a reduction is re-exported implicitly with its
+  // operator, which is emitted, and the reduction resolver recovers it from
+  // there (FindUserReductionSymbol / SearchOperatorReduction), so skip its use
+  // item. A reduction named by a plain identifier ("myred") has a valid name
+  // and is emitted normally; an intrinsic-operator or special-intrinsic
+  // reduction has no re-exported operator to recover through and is left to the
+  // normal path.
+  if (symbol.GetUltimate().has<UserReductionDetails>() &&
+      IsMangledDefinedOperatorReductionName(symbol.GetUltimate().name())) {
+    return;
+  }
   auto &details{symbol.get<UseDetails>()};
   auto &use{details.symbol()};
   const Symbol &module{GetUsedModule(details)};
diff --git a/flang/lib/Semantics/resolve-names-utils.cpp b/flang/lib/Semantics/resolve-names-utils.cpp
index 03d5df73a9d79..93a53515dabf9 100644
--- a/flang/lib/Semantics/resolve-names-utils.cpp
+++ b/flang/lib/Semantics/resolve-names-utils.cpp
@@ -987,4 +987,9 @@ std::string GetReductionFortranId(const parser::CharBlock &mangledName) {
   return suffix.str();
 }
 
+bool IsMangledDefinedOperatorReductionName(const parser::CharBlock &name) {
+  llvm::StringRef ref{name.begin(), name.size()};
+  return ref.starts_with("op.") && ref.ends_with(".");
+}
+
 } // namespace Fortran::semantics
diff --git a/flang/lib/Semantics/resolve-names-utils.h b/flang/lib/Semantics/resolve-names-utils.h
index a1bac72e45ca1..e31056da7b855 100644
--- a/flang/lib/Semantics/resolve-names-utils.h
+++ b/flang/lib/Semantics/resolve-names-utils.h
@@ -152,6 +152,17 @@ parser::CharBlock MakeNameFromOperator(
 parser::CharBlock MangleSpecialFunctions(const parser::CharBlock &name);
 std::string MangleDefinedOperator(const parser::CharBlock &name);
 
+// True iff `name` is the mangled name of a user-defined-operator declare
+// reduction (as produced by MangleDefinedOperator, e.g. "op.remote."): it
+// starts with "op." and ends with ".". This is the only reduction kind whose
+// symbol name is not valid Fortran yet is recoverable through its re-exported
+// operator interface, so the module-file writer omits its (unwritable) use-only
+// item and relies on the operator to carry it. Intrinsic-operator ("op.+") and
+// special-intrinsic ("op.max") reductions do not match (no trailing dot); plain
+// named reductions ("myred") do not match (no "op." prefix, since a valid
+// identifier cannot contain '.').
+bool IsMangledDefinedOperatorReductionName(const parser::CharBlock &name);
+
 // Map a mangled declare reduction name (e.g., "op.+", "op.combine.",
 // "op.max") back to the Fortran identifier used as the scope key for the
 // corresponding operator or procedure (e.g., "operator(+)", ".combine.",
diff --git a/flang/test/Semantics/OpenMP/declare-reduction-operator-modfile-reexport.f90 b/flang/test/Semantics/OpenMP/declare-reduction-operator-modfile-reexport.f90
new file mode 100644
index 0000000000000..dd42760781464
--- /dev/null
+++ b/flang/test/Semantics/OpenMP/declare-reduction-operator-modfile-reexport.f90
@@ -0,0 +1,86 @@
+! RUN: rm -rf %t && split-file %s %t && cd %t
+! A module that uses another module's user-defined declare reduction re-exports
+! it. A reduction named by an operator has an internal mangled symbol name
+! ("op.remote.") that is not valid Fortran, so it must not be written as a
+! "use,only:" item (that module file could not be re-parsed and reading it
+! crashed); it is recovered through the re-exported operator instead. A reduction
+! named by a plain identifier ("myred") has a valid name and is re-exported as a
+! normal "use,only:" item.
+
+! RUN: %flang_fc1 -fsyntax-only -fopenmp rx_base.f90
+! RUN: %flang_fc1 -fsyntax-only -fopenmp rx_wrap.f90
+! RUN: FileCheck --check-prefix=OPMOD --input-file=rx_wrap.mod %s
+! RUN: %flang_fc1 -fsyntax-only -fopenmp rx_use.f90
+
+! RUN: %flang_fc1 -fsyntax-only -fopenmp nm_base.f90
+! RUN: %flang_fc1 -fsyntax-only -fopenmp nm_wrap.f90
+! RUN: FileCheck --check-prefix=NMMOD --input-file=nm_wrap.mod %s
+! RUN: %flang_fc1 -fsyntax-only -fopenmp nm_use.f90
+
+! The operator is re-exported; the mangled reduction symbol is not.
+! OPMOD: use rx_base,only:operator(.remote.)
+! OPMOD-NOT: only:op.remote.
+! The plainly-named reduction is re-exported as a normal use item.
+! NMMOD: use nm_base,only:myred
+
+!--- rx_base.f90
+module rx_base
+  type :: t
+    integer :: val = 0
+  end type
+  interface operator(.remote.)
+    module procedure add_t
+  end interface
+  !$omp declare reduction(.remote.:t:omp_out%val=omp_out%val+omp_in%val) &
+  !$omp   initializer(omp_priv=t(0))
+contains
+  type(t) function add_t(a, b)
+    type(t), intent(in) :: a, b
+    add_t%val = a%val + b%val
+  end function
+end module
+
+!--- rx_wrap.f90
+module rx_wrap
+  use rx_base
+end module
+
+!--- rx_use.f90
+program main
+  use rx_wrap, only: t, operator(.local.) => operator(.remote.)
+  type(t) :: x
+  integer :: i
+  x = t(0)
+  !$omp parallel do reduction(.local.:x)
+  do i = 1, 100
+    x%val = x%val + 1
+  end do
+  print *, x%val
+end program
+
+!--- nm_base.f90
+module nm_base
+  type :: t
+    integer :: val = 0
+  end type
+  !$omp declare reduction(myred:t:omp_out%val=omp_out%val+omp_in%val) &
+  !$omp   initializer(omp_priv=t(0))
+end module
+
+!--- nm_wrap.f90
+module nm_wrap
+  use nm_base
+end module
+
+!--- nm_use.f90
+program main
+  use nm_wrap
+  type(t) :: x
+  integer :: i
+  x = t(0)
+  !$omp parallel do reduction(myred:x)
+  do i = 1, 100
+    x%val = x%val + 1
+  end do
+  print *, x%val
+end program

>From 38dbf588efb6964594f8f34c96298eac1f34bc16 Mon Sep 17 00:00:00 2001
From: "Matt P. Dziubinski" <matt-p.dziubinski at hpe.com>
Date: Fri, 3 Jul 2026 22:24:56 -0500
Subject: [PATCH 3/4] [flang][OpenMP] Lower cross-module operator declare
 reduction

Lower a reduction clause naming a USE-associated, renamed, or merged-generic
user-defined operator, which currently aborts lowering with a "not yet
implemented" TODO. Semantics already accepts these programs (since #200329).

Fixes #207255.

Resolve the use-site operator to its source reduction symbol through the shared
semantic resolver (Fortran::semantics::omp::FindOperatorUserReductionSymbol),
extracted here from check-omp-structure.cpp into Semantics/openmp-utils so the
lowering and the semantic checks share one resolution authority. Name the
omp.declare_reduction op from the resolved symbol's (ultimate name,
owning-module) via getScopedUserReductionName, on both the clause and directive
sides so the two cannot drift. Because the name carries the owning module, a
same-spelling same-type operator reduction imported from two modules yields two
distinct ops, each clause binding its own combiner instead of colliding onto one.

For separate compilation, materializeOpenMPDeclareReductions walks module scopes
read from .mod files and lowers each imported operator reduction through
genOpenMPDeclareReductionImpl, mirroring imported declare-mapper materialization.
Mod-file reading runs only ResolveNames, so an imported combiner and initializer
carry bound names but no typed expressions; the materializer runs the
expression-analysis pass over each imported directive before lowering. An unused
import that lowering does not yet support is skipped rather than aborting the
consumer.

Single-declaration, single-type operator reductions. Multiple declarations or
types are the stacked follow-up.

Assisted-by: Claude Opus 4.8, GPT-5.5.
---
 flang/include/flang/Lower/OpenMP.h            |   8 +
 flang/include/flang/Semantics/openmp-utils.h  |  18 ++
 flang/lib/Lower/Bridge.cpp                    |   9 +
 flang/lib/Lower/OpenMP/OpenMP.cpp             | 150 ++++++++++++++--
 .../lib/Lower/Support/ReductionProcessor.cpp  |  45 +++--
 flang/lib/Semantics/check-omp-structure.cpp   | 133 +-------------
 flang/lib/Semantics/openmp-utils.cpp          | 139 +++++++++++++++
 .../declare-reduction-operator-use-assoc.f90  |  36 ----
 .../declare-reduction-operator-collide.f90    |  79 +++++++++
 .../declare-reduction-operator-merged.f90     |  75 ++++++++
 .../declare-reduction-operator-renamed.f90    |  45 +++++
 ...-reduction-operator-separate-combiners.f90 | 106 ++++++++++++
 ...e-reduction-operator-separate-hermetic.f90 |  56 ++++++
 ...e-reduction-operator-separate-reexport.f90 |  93 ++++++++++
 ...n-operator-separate-unused-unsupported.f90 |  86 ++++++++++
 .../declare-reduction-operator-separate.f90   | 162 ++++++++++++++++++
 .../declare-reduction-operator-use-assoc.f90  |  47 +++++
 17 files changed, 1088 insertions(+), 199 deletions(-)
 delete mode 100644 flang/test/Lower/OpenMP/Todo/declare-reduction-operator-use-assoc.f90
 create mode 100644 flang/test/Lower/OpenMP/declare-reduction-operator-collide.f90
 create mode 100644 flang/test/Lower/OpenMP/declare-reduction-operator-merged.f90
 create mode 100644 flang/test/Lower/OpenMP/declare-reduction-operator-renamed.f90
 create mode 100644 flang/test/Lower/OpenMP/declare-reduction-operator-separate-combiners.f90
 create mode 100644 flang/test/Lower/OpenMP/declare-reduction-operator-separate-hermetic.f90
 create mode 100644 flang/test/Lower/OpenMP/declare-reduction-operator-separate-reexport.f90
 create mode 100644 flang/test/Lower/OpenMP/declare-reduction-operator-separate-unused-unsupported.f90
 create mode 100644 flang/test/Lower/OpenMP/declare-reduction-operator-separate.f90
 create mode 100644 flang/test/Lower/OpenMP/declare-reduction-operator-use-assoc.f90

diff --git a/flang/include/flang/Lower/OpenMP.h b/flang/include/flang/Lower/OpenMP.h
index 882ebad9071bc..90d0e7010c033 100644
--- a/flang/include/flang/Lower/OpenMP.h
+++ b/flang/include/flang/Lower/OpenMP.h
@@ -105,6 +105,14 @@ void materializeOpenMPDeclareMappers(
     Fortran::lower::AbstractConverter &, Fortran::semantics::SemanticsContext &,
     const Fortran::semantics::Scope *scope = nullptr);
 
+// Materialize omp.declare_reduction ops for user-defined operator reduction
+// declarations found in imported modules (separate compilation). If scope
+// is null, materialize for the whole semantics global scope; otherwise, operate
+// recursively starting at scope.
+void materializeOpenMPDeclareReductions(
+    Fortran::lower::AbstractConverter &, Fortran::semantics::SemanticsContext &,
+    const Fortran::semantics::Scope *scope = nullptr);
+
 } // namespace lower
 } // namespace Fortran
 
diff --git a/flang/include/flang/Semantics/openmp-utils.h b/flang/include/flang/Semantics/openmp-utils.h
index 75bab5f6cb8ec..6e91ed4019b3d 100644
--- a/flang/include/flang/Semantics/openmp-utils.h
+++ b/flang/include/flang/Semantics/openmp-utils.h
@@ -35,6 +35,7 @@
 #include <vector>
 
 namespace Fortran::semantics {
+class DeclTypeSpec;
 class Scope;
 class SemanticsContext;
 class Symbol;
@@ -116,6 +117,23 @@ bool IsArrayElement(const parser::OmpObject &object, SemanticsContext *semaCtx);
 
 const Symbol *GetHostSymbol(const Symbol &sym);
 
+// Resolve a user-defined reduction visible in scope under the mangled name
+// mangledName (e.g. "op.myop." for operator(.myop.), or a named reduction).
+// Follows USE associations, operator renames, private visibility, and merged
+// generics exactly as the OpenMP semantic checks do, returning the found
+// (non-ultimate) reduction symbol, or null if none is visible. When type is
+// non-null, only a reduction that supports that type is accepted (used to
+// disambiguate an operator that carries reductions for several types).
+const Symbol *FindUserReductionSymbol(const Scope &scope,
+    const parser::CharBlock &mangledName, const DeclTypeSpec *type = nullptr);
+
+// Resolve the user-defined reduction associated with the defined-operator
+// symbol operatorSym. Delegates to FindUserReductionSymbol starting from the
+// operator's owning scope with the operator's mangled ("op...") name; type
+// filters by supported type as above.
+const Symbol *FindOperatorUserReductionSymbol(
+    const Symbol &operatorSym, const DeclTypeSpec *type = nullptr);
+
 bool IsMapEnteringType(parser::OmpMapType::Value type);
 bool IsMapExitingType(parser::OmpMapType::Value type);
 
diff --git a/flang/lib/Lower/Bridge.cpp b/flang/lib/Lower/Bridge.cpp
index 27fa06362a1a8..1bd92049211c7 100644
--- a/flang/lib/Lower/Bridge.cpp
+++ b/flang/lib/Lower/Bridge.cpp
@@ -580,6 +580,15 @@ class FirConverter : public Fortran::lower::AbstractConverter {
           *this, bridge.getSemanticsContext());
     });
 
+    // Ensure imported OpenMP user-defined operator declare reductions are
+    // materialized at module scope before lowering any reduction clauses that
+    // may reference them (the separate-compilation counterpart of same-file
+    // module reduction lowering).
+    createBuilderOutsideOfFuncOpAndDo([&]() {
+      Fortran::lower::materializeOpenMPDeclareReductions(
+          *this, bridge.getSemanticsContext());
+    });
+
     // Create definitions of intrinsic module constants.
     createBuilderOutsideOfFuncOpAndDo(
         [&]() { createIntrinsicModuleDefinitions(pft); });
diff --git a/flang/lib/Lower/OpenMP/OpenMP.cpp b/flang/lib/Lower/OpenMP/OpenMP.cpp
index d630d825062b7..a45b52b53212f 100644
--- a/flang/lib/Lower/OpenMP/OpenMP.cpp
+++ b/flang/lib/Lower/OpenMP/OpenMP.cpp
@@ -42,6 +42,7 @@
 #include "flang/Parser/openmp-utils.h"
 #include "flang/Parser/parse-tree.h"
 #include "flang/Parser/tools.h"
+#include "flang/Semantics/expression.h"
 #include "flang/Semantics/openmp-directive-sets.h"
 #include "flang/Semantics/openmp-utils.h"
 #include "flang/Semantics/tools.h"
@@ -4680,12 +4681,17 @@ static bool isSimpleReductionType(mlir::Type reductionType) {
   return false;
 }
 
-// Getting the type from a symbol compared to a DeclSpec is simpler since we do
-// not need to consider derived vs intrinsic types. Semantics is guaranteed to
-// generate these symbols.
+// Compute the reduction's element type from the combiner's stylized declaration
+// symbol, without checking whether lowering supports it. Shared by
+// getReductionType (same-file, enforces support with a TODO) and
+// materializeOpenMPDeclareReductions (skips an unsupported unused import via
+// isSimpleReductionType instead of aborting the consumer). Getting the type
+// from that symbol compared to the declared type-list (a DeclarationTypeSpec)
+// is simpler since we do not need to consider derived vs intrinsic types.
+// Semantics is guaranteed to generate these symbols.
 static mlir::Type
-getReductionType(lower::AbstractConverter &converter,
-                 const parser::OmpReductionSpecifier &specifier) {
+computeReductionType(lower::AbstractConverter &converter,
+                     const parser::OmpReductionSpecifier &specifier) {
   const auto &combinerExpression =
       std::get<std::optional<parser::OmpCombinerExpression>>(specifier.t)
           .value();
@@ -4696,8 +4702,15 @@ getReductionType(lower::AbstractConverter &converter,
   const parser::OmpStylizedDeclaration &decl = declList.front();
   const auto &name = std::get<parser::ObjectName>(decl.var.t);
   const auto &symbol = semantics::SymbolRef(*name.symbol);
-  mlir::Type reductionType = converter.genType(symbol);
+  return converter.genType(symbol);
+}
 
+// Return the reduction's element type, emitting a TODO if lowering does not
+// support it.
+static mlir::Type
+getReductionType(lower::AbstractConverter &converter,
+                 const parser::OmpReductionSpecifier &specifier) {
+  mlir::Type reductionType = computeReductionType(converter, specifier);
   if (!isSimpleReductionType(reductionType))
     TODO(converter.getCurrentLocation(),
          "declare reduction currently only supports trivial types, "
@@ -4733,10 +4746,16 @@ appendCombiner(const parser::OmpDeclareReductionDirective &construct,
   llvm_unreachable("Expecting reduction combiner");
 }
 
-static void genOMP(lower::AbstractConverter &converter, lower::SymMap &symTable,
-                   semantics::SemanticsContext &semaCtx,
-                   lower::pft::Evaluation &eval,
-                   const parser::OmpDeclareReductionDirective &construct) {
+// Lower a single declare-reduction directive. Serves both same-file lowering
+// (symOpt null) and separate-compilation materialization of an imported
+// reduction (symOpt is the source symbol from
+// materializeOpenMPDeclareReductions, with a fresh SymMap). Mirrors
+// genOpenMPDeclareMapperImpl.
+static void genOpenMPDeclareReductionImpl(
+    lower::AbstractConverter &converter, lower::SymMap &symTable,
+    semantics::SemanticsContext &semaCtx,
+    const parser::OmpDeclareReductionDirective &construct,
+    const semantics::Symbol *symOpt = nullptr) {
   if (semaCtx.langOptions().OpenMPSimd)
     return;
 
@@ -4802,13 +4821,17 @@ static void genOMP(lower::AbstractConverter &converter, lower::SymMap &symTable,
                           -> std::string {
                         // Directive side of the user-defined operator reduction
                         // naming contract (the clause side is in
-                        // ReductionProcessor::processReductionArguments).
-                        // opName.v.sym() is the reduction symbol
-                        // "op<spelling>". Only single-declaration, single-type
-                        // reductions are supported; otherwise emit a clean
+                        // ReductionProcessor::processReductionArguments). Name
+                        // the op via getScopedUserReductionName from the
+                        // symbol's ultimate (name, owner), byte-identical to
+                        // the clause reference. symOpt supplies the source
+                        // symbol for separate compilation, else opName.v.sym().
+                        // Single-declaration, single-type only; otherwise a
+                        // clean
                         // TODO.
                         const semantics::Symbol &redSym =
-                            opName.v.sym()->GetUltimate();
+                            symOpt ? symOpt->GetUltimate()
+                                   : opName.v.sym()->GetUltimate();
                         const auto *userDetails =
                             redSym.detailsIf<semantics::UserReductionDetails>();
                         if (!userDetails || typeNameList.v.size() != 1 ||
@@ -4916,6 +4939,18 @@ static void genOMP(lower::AbstractConverter &converter, lower::SymMap &symTable,
   }
 }
 
+// Same-file delegator for a declare-reduction directive. Mirrors the
+// declare-mapper delegator: it forwards the enclosing SymMap so the combiner/
+// initializer callbacks share the current scope. Separate-compilation
+// materialization instead calls genOpenMPDeclareReductionImpl directly with a
+// fresh SymMap and the source reduction symbol.
+static void genOMP(lower::AbstractConverter &converter, lower::SymMap &symTable,
+                   semantics::SemanticsContext &semaCtx,
+                   lower::pft::Evaluation &eval,
+                   const parser::OmpDeclareReductionDirective &construct) {
+  genOpenMPDeclareReductionImpl(converter, symTable, semaCtx, construct);
+}
+
 static void
 genOMP(lower::AbstractConverter &converter, lower::SymMap &symTable,
        semantics::SemanticsContext &semaCtx, lower::pft::Evaluation &eval,
@@ -6077,3 +6112,88 @@ void Fortran::lower::materializeOpenMPDeclareMappers(
     }
   }
 }
+
+// Walk scopes and materialize omp.declare_reduction ops for user-defined
+// operator reductions imported from modules. If scope is null, start from the
+// global scope. Mirrors materializeOpenMPDeclareMappers: gating on mod-file
+// module scopes restricts this to separate compilation (a same-file module's op
+// is emitted by the primary translation pass).
+void Fortran::lower::materializeOpenMPDeclareReductions(
+    Fortran::lower::AbstractConverter &converter,
+    semantics::SemanticsContext &semaCtx, const semantics::Scope *scope) {
+  const semantics::Scope &root = scope ? *scope : semaCtx.globalScope();
+
+  // Recurse into child scopes first (modules, submodules, etc.).
+  for (const semantics::Scope &child : root.children())
+    materializeOpenMPDeclareReductions(converter, semaCtx, &child);
+
+  // Only consider module scopes to avoid duplicating local constructs.
+  if (!root.IsModule())
+    return;
+
+  // Only materialize for modules coming from mod files to avoid duplicates.
+  if (!root.symbol() || !root.symbol()->test(semantics::Symbol::Flag::ModFile))
+    return;
+
+  // Scan symbols in this module scope for UserReductionDetails.
+  for (auto &it : root) {
+    const semantics::Symbol &sym = *it.second;
+    const semantics::Symbol &ultimate = sym.GetUltimate();
+    const auto *userDetails =
+        ultimate.detailsIf<semantics::UserReductionDetails>();
+    if (!userDetails)
+      continue;
+    // Skip without calling the impl: a consumer that merely uses this module
+    // must compile even when the reduction is out of the clause-side resolver's
+    // reach. Eagerly lowering a private-ultimate or unsupported-shape (multiple
+    // declarations/types) reduction would trip the impl's TODO and abort an
+    // unused import; a program that references it still gets the clause-side
+    // TODO.
+    if (ultimate.attrs().test(semantics::Attr::PRIVATE) ||
+        userDetails->GetDeclList().size() != 1 ||
+        userDetails->GetTypeList().size() != 1)
+      continue;
+    for (const auto *decl : userDetails->GetDeclList()) {
+      if (const auto *reductionDecl =
+              std::get_if<parser::OmpDeclareReductionDirective>(&decl->u)) {
+        // An unused import must not abort the consumer, so skip a shape the
+        // impl cannot lower (a program that references it still gets the
+        // clause-side
+        // TODO), completing the mandatory skip above (which covers only private
+        // / multiple declarations / types).
+        const auto &specifier =
+            DEREF(parser::omp::GetFirstArgument<parser::OmpReductionSpecifier>(
+                reductionDecl->v));
+        // The combiner-in-clause form (declare reduction(id:type)
+        // combiner(...), OpenMP 6.0) is not yet lowered even in the same-file
+        // path; computeReductionType and the impl both read the combiner from
+        // the specifier.
+        if (!std::get<std::optional<parser::OmpCombinerExpression>>(specifier.t)
+                 .has_value())
+          continue;
+        // A reduction type lowering does not yet support: mirror the impl's
+        // getReductionType check with the shared predicate.
+        if (!isSimpleReductionType(computeReductionType(converter, specifier)))
+          continue;
+        // Mod-file reading runs only ResolveNames, so an imported combiner and
+        // initializer carry bound names but null
+        // typedExpr/typedCall/typedAssignment (the same-file path fills these
+        // later in PerformStatementSemantics, which mod-file reading skips).
+        // Lowering below reads the typed forms, so run expression analysis over
+        // this imported directive now. Doing it here rather than in the
+        // mod-file reader scopes the analysis to the reductions actually
+        // materialized and leaves mod-file reading unchanged for other
+        // consumers.
+        semantics::ExprChecker checker{semaCtx};
+        parser::Walk(*reductionDecl, checker);
+        // Fresh, materialization-local SymMap: the combiner/initializer
+        // callbacks created inside the impl capture it by reference and run
+        // synchronously during createDeclareReductionHelper, so it must
+        // outlive the impl call (do not reuse Bridge's localSymbols).
+        lower::SymMap materializeSymTable;
+        genOpenMPDeclareReductionImpl(converter, materializeSymTable, semaCtx,
+                                      *reductionDecl, &sym);
+      }
+    }
+  }
+}
diff --git a/flang/lib/Lower/Support/ReductionProcessor.cpp b/flang/lib/Lower/Support/ReductionProcessor.cpp
index e1b4684bd8871..54a9ffe642613 100644
--- a/flang/lib/Lower/Support/ReductionProcessor.cpp
+++ b/flang/lib/Lower/Support/ReductionProcessor.cpp
@@ -22,6 +22,7 @@
 #include "flang/Optimizer/Builder/Todo.h"
 #include "flang/Optimizer/Dialect/FIRType.h"
 #include "flang/Optimizer/HLFIR/HLFIROps.h"
+#include "flang/Semantics/openmp-utils.h"
 #include "mlir/Dialect/OpenMP/OpenMPDialect.h"
 #include "llvm/Support/CommandLine.h"
 #include <type_traits>
@@ -825,37 +826,43 @@ bool ReductionProcessor::processReductionArguments(
                 std::get_if<omp::clause::DefinedOperator::DefinedOpName>(
                     &redDefinedOp->u)) {
           // User-defined operator reduction (e.g. reduction(.myop.:x)). Resolve
-          // the use-site operator to its reduction symbol, which semantics
-          // names "op<spelling>" (MangleDefinedOperator in resolve-names), in
-          // the current scope, then reference the omp.declare_reduction op the
-          // directive materialized for it. Only a locally-declared,
-          // single-declaration, single-type reduction whose type the variable
-          // supports is handled here; anything else (imported, renamed, merged,
-          // or multiple declarations/types) is a clean TODO rather than a crash
-          // or a wrong binding.
+          // the use-site operator to the source reduction symbol as the OpenMP
+          // semantic checks do (following USE association, operator renames,
+          // private visibility, and merged generics), then reference the
+          // omp.declare_reduction op named from that symbol's scoped (name,
+          // owner). Reductions in different source modules have distinct owner
+          // scopes, so a same-spelling/same-type operator reduction imported
+          // from two modules yields two distinct ops, each clause binding its
+          // own combiner instead of colliding onto one. Single-declaration,
+          // single-type only; anything else, or an operator with no reduction
+          // for the variable's type, is a clean TODO rather than a crash or a
+          // wrong binding.
           const semantics::Symbol *opSym = definedOpName->v.sym();
-          std::string mangledName = "op" + opSym->name().ToString();
-          const semantics::Symbol *redSym =
-              converter.getCurrentScope().FindSymbol(
-                  parser::CharBlock{mangledName});
+          const semantics::DeclTypeSpec *varType =
+              reductionSymbols[idx]->GetUltimate().GetType();
+          // The variable's type disambiguates an operator that carries
+          // reductions for several types (e.g. a generic merged from multiple
+          // single-type modules): the resolver returns only a reduction that
+          // supports varType.
+          const semantics::Symbol *resolvedSym =
+              semantics::omp::FindOperatorUserReductionSymbol(*opSym, varType);
+          // resolvedSym is the found symbol, which may be a USE-associated
+          // wrapper; take its ultimate before reading the reduction details.
           const semantics::Symbol *ultimate =
-              redSym ? &redSym->GetUltimate() : nullptr;
+              resolvedSym ? &resolvedSym->GetUltimate() : nullptr;
           const semantics::UserReductionDetails *userDetails =
               ultimate ? ultimate->detailsIf<semantics::UserReductionDetails>()
                        : nullptr;
-          const semantics::DeclTypeSpec *varType =
-              reductionSymbols[idx]->GetUltimate().GetType();
-          if (!redSym || ultimate != redSym || !userDetails ||
+          if (!varType || !resolvedSym || !userDetails ||
               userDetails->GetDeclList().size() != 1 ||
-              userDetails->GetTypeList().size() != 1 || !varType ||
-              !userDetails->SupportsType(*varType)) {
+              userDetails->GetTypeList().size() != 1) {
             TODO(currentLocation,
                  "OpenMP user-defined operator reduction is not yet supported "
                  "for imported, renamed, or multiple-declaration/type "
                  "reductions");
           }
           std::string opName = ReductionProcessor::getScopedUserReductionName(
-              converter, *redSym);
+              converter, *resolvedSym);
           mlir::ModuleOp module = builder.getModule();
           auto existingDecl = module.lookupSymbol<OpType>(opName);
           // The MLIR verifier does not type-check these ops (they have no
diff --git a/flang/lib/Semantics/check-omp-structure.cpp b/flang/lib/Semantics/check-omp-structure.cpp
index 4e514223b388a..7ee83236b32f4 100644
--- a/flang/lib/Semantics/check-omp-structure.cpp
+++ b/flang/lib/Semantics/check-omp-structure.cpp
@@ -3820,131 +3820,6 @@ void OmpStructureChecker::Enter(const parser::OmpClause::TaskReduction &x) {
   CheckReductionObjects(objects, llvm::omp::Clause::OMPC_task_reduction);
 }
 
-// Compute the mangled reduction name to look up in a reduction's source module.
-// If the operator was renamed on import (e.g. USE m, ONLY: operator(.local.) =>
-// operator(.remote.)), the local mangled name will not match in the source
-// module; re-derive the lookup name from the source operator's ultimate name.
-// Only defined operators can be renamed (intrinsic operators and named
-// reductions cannot), so a detected rename always has a ".op." source name.
-// For non-renamed lookups the original mangled name is returned unchanged.
-static std::string SourceReductionName(const parser::CharBlock &mangledName,
-    const parser::CharBlock &localName, const parser::CharBlock &sourceName) {
-  if (sourceName != localName && sourceName.size() >= 3 &&
-      sourceName.front() == '.' && sourceName.back() == '.') {
-    return MangleDefinedOperator(sourceName);
-  }
-  return mangledName.ToString();
-}
-
-// Return the reduction details of `symbol` if it is a user reduction that
-// supports `type` (any type when `type` is null).
-static const UserReductionDetails *AcceptReduction(
-    const Symbol &symbol, const DeclTypeSpec *type) {
-  const auto *details{symbol.GetUltimate().detailsIf<UserReductionDetails>()};
-  if (details && (!type || details->SupportsType(*type))) {
-    return details;
-  }
-  return nullptr;
-}
-
-// A reduction symbol is locally declared (authoritative) when it is not reached
-// through any USE association, even via host association. Such a reduction
-// shadows reductions imported or reachable through its operator.
-static bool IsLocalReduction(const Symbol &symbol) {
-  const Symbol *s{&symbol};
-  while (const auto *host{s->detailsIf<HostAssocDetails>()}) {
-    s = &host->symbol();
-  }
-  return !s->detailsIf<UseDetails>();
-}
-
-// Search for a user reduction supporting `type` by following the operator/
-// procedure symbol `opSym` through its USE associations and merged generic
-// sources. Each module the operator passes through is checked for a (possibly
-// renamed) reduction; `localName` is the operator name written at the use site,
-// used to detect renames. A locally declared reduction in a module is
-// authoritative: it is returned if it supports the type, otherwise it shadows
-// reductions reachable further along that branch.
-static const UserReductionDetails *SearchOperatorReduction(const Symbol &opSym,
-    const parser::CharBlock &mangledName, const parser::CharBlock &localName,
-    const DeclTypeSpec *type, llvm::SmallPtrSetImpl<const Symbol *> &visited) {
-  if (!visited.insert(&opSym).second) {
-    return nullptr;
-  }
-  const Scope &scope{opSym.owner()};
-  if (scope.kind() == Scope::Kind::Module) {
-    std::string lookupName{
-        SourceReductionName(mangledName, localName, opSym.name())};
-    auto it{scope.find(parser::CharBlock{lookupName})};
-    if (it != scope.end()) {
-      const Symbol &reductionSym{*it->second};
-      const Symbol &reductionUltimate{reductionSym.GetUltimate()};
-      if (!reductionUltimate.attrs().test(Attr::PRIVATE)) {
-        if (const auto *details{AcceptReduction(reductionUltimate, type)}) {
-          return details;
-        }
-        // A locally declared reduction here shadows reductions reachable
-        // further along this branch.
-        if (reductionUltimate.detailsIf<UserReductionDetails>() &&
-            IsLocalReduction(reductionSym)) {
-          return nullptr;
-        }
-      }
-    }
-  }
-  // Follow a USE-associated operator to the module it was imported from.
-  if (const auto *use{opSym.detailsIf<UseDetails>()}) {
-    return SearchOperatorReduction(
-        use->symbol(), mangledName, localName, type, visited);
-  }
-  // Search each module merged into a generic operator (recursing through
-  // re-exporting facade modules).
-  if (const auto *generic{opSym.detailsIf<GenericDetails>()}) {
-    for (const Symbol &useSym : generic->uses()) {
-      if (const auto *details{SearchOperatorReduction(
-              useSym, mangledName, localName, type, visited)}) {
-        return details;
-      }
-    }
-  }
-  return nullptr;
-}
-
-// Find user reduction details for a mangled name, following USE associations
-// when the reduction is not directly visible in the scope. A type may be
-// supplied to disambiguate an operator that carries reductions for several
-// types (e.g. a generic merged from multiple modules); a candidate is accepted
-// only if it supports that type. A locally declared reduction is authoritative
-// for its operator in its scope and shadows USE-associated reductions.
-static const UserReductionDetails *FindUserReduction(const Scope &scope,
-    const parser::CharBlock &mangledName, const DeclTypeSpec *type = nullptr) {
-  // Direct lookup: a reduction directly visible via bare USE or a local
-  // declaration.
-  const Symbol *directSymbol{scope.FindSymbol(mangledName)};
-  if (directSymbol) {
-    if (const auto *details{AcceptReduction(*directSymbol, type)}) {
-      return details;
-    }
-    // A locally declared reduction that does not support the requested type is
-    // authoritative: it shadows USE-associated reductions (ProcessReduction-
-    // Specifier erases the latter), so do not resurrect them via the operator.
-    if (directSymbol->GetUltimate().detailsIf<UserReductionDetails>() &&
-        IsLocalReduction(*directSymbol)) {
-      return nullptr;
-    }
-  }
-  // Trace the operator/procedure to the modules that declare its reduction.
-  std::string fortranName{GetReductionFortranId(mangledName)};
-  const Symbol *opSymbol{
-      fortranName.empty() ? nullptr : scope.FindSymbol(fortranName)};
-  if (!opSymbol) {
-    return nullptr;
-  }
-  llvm::SmallPtrSet<const Symbol *, 8> visited;
-  return SearchOperatorReduction(
-      *opSymbol, mangledName, opSymbol->name(), type, visited);
-}
-
 bool OmpStructureChecker::CheckReductionOperator(
     const parser::OmpReductionIdentifier &ident, parser::CharBlock source,
     llvm::omp::Clause clauseId) {
@@ -3975,7 +3850,7 @@ bool OmpStructureChecker::CheckReductionOperator(
     if (const auto *definedOp{std::get_if<parser::DefinedOpName>(&dOpr.u)}) {
       std::string mangled{MangleDefinedOperator(definedOp->v.symbol->name())};
       const Scope &scope{definedOp->v.symbol->owner()};
-      if (FindUserReduction(scope, mangled)) {
+      if (omp::FindUserReductionSymbol(scope, mangled)) {
         return true;
       }
     }
@@ -4063,9 +3938,9 @@ void OmpStructureChecker::CheckReductionObjects(
 
 static bool CheckSymbolSupportsType(const Scope &scope,
     const parser::CharBlock &name, const DeclTypeSpec &type) {
-  // FindUserReduction only returns a reduction that supports the requested
-  // type.
-  return FindUserReduction(scope, name, &type) != nullptr;
+  // FindUserReductionSymbol only returns a reduction that supports the
+  // requested type.
+  return omp::FindUserReductionSymbol(scope, name, &type) != nullptr;
 }
 
 static bool IsReductionAllowedForType(
diff --git a/flang/lib/Semantics/openmp-utils.cpp b/flang/lib/Semantics/openmp-utils.cpp
index 8cf174a0139af..46efa3b3e5336 100644
--- a/flang/lib/Semantics/openmp-utils.cpp
+++ b/flang/lib/Semantics/openmp-utils.cpp
@@ -12,6 +12,8 @@
 
 #include "flang/Semantics/openmp-utils.h"
 
+#include "resolve-names-utils.h"
+
 #include "flang/Common/Fortran-consts.h"
 #include "flang/Common/idioms.h"
 #include "flang/Common/indirection.h"
@@ -36,6 +38,7 @@
 #include "llvm/ADT/ArrayRef.h"
 #include "llvm/ADT/DenseSet.h"
 #include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallPtrSet.h"
 #include "llvm/ADT/StringExtras.h"
 #include "llvm/Frontend/OpenMP/OMPContext.h"
 
@@ -2419,4 +2422,140 @@ std::optional<DynamicUserCondition> MakeVariantMatchInfo(
   }
   return dynamicCond;
 }
+
+// User-defined reduction resolution, shared between the OpenMP semantic checks
+// and lowering. The two public entry points (FindUserReductionSymbol and
+// FindOperatorUserReductionSymbol) return the resolved (non-ultimate) reduction
+// symbol; the caller reads its UserReductionDetails.
+
+// Compute the mangled reduction name to look up in a reduction's source module.
+// If the operator was renamed on import (e.g. USE m, ONLY: operator(.local.) =>
+// operator(.remote.)), the local mangled name will not match in the source
+// module; re-derive the lookup name from the source operator's ultimate name.
+// Only defined operators can be renamed (intrinsic operators and named
+// reductions cannot), so a detected rename always has a ".op." source name.
+// For non-renamed lookups the original mangled name is returned unchanged.
+static std::string SourceReductionName(const parser::CharBlock &mangledName,
+    const parser::CharBlock &localName, const parser::CharBlock &sourceName) {
+  if (sourceName != localName && sourceName.size() >= 3 &&
+      sourceName.front() == '.' && sourceName.back() == '.') {
+    return MangleDefinedOperator(sourceName);
+  }
+  return mangledName.ToString();
+}
+
+// Return the reduction details of `symbol` if it is a user reduction that
+// supports `type` (any type when `type` is null).
+static const UserReductionDetails *AcceptReduction(
+    const Symbol &symbol, const DeclTypeSpec *type) {
+  const auto *details{symbol.GetUltimate().detailsIf<UserReductionDetails>()};
+  if (details && (!type || details->SupportsType(*type))) {
+    return details;
+  }
+  return nullptr;
+}
+
+// A reduction symbol is locally declared (authoritative) when it is not reached
+// through any USE association, even via host association. Such a reduction
+// shadows reductions imported or reachable through its operator.
+static bool IsLocalReduction(const Symbol &symbol) {
+  const Symbol *s{&symbol};
+  while (const auto *host{s->detailsIf<HostAssocDetails>()}) {
+    s = &host->symbol();
+  }
+  return !s->detailsIf<UseDetails>();
+}
+
+// Search for a user reduction supporting `type` by following the operator/
+// procedure symbol `opSym` through its USE associations and merged generic
+// sources. Each module the operator passes through is checked for a (possibly
+// renamed) reduction; `localName` is the operator name written at the use site,
+// used to detect renames. A locally declared reduction in a module is
+// authoritative: it is returned if it supports the type, otherwise it shadows
+// reductions reachable further along that branch.
+static const Symbol *SearchOperatorReduction(const Symbol &opSym,
+    const parser::CharBlock &mangledName, const parser::CharBlock &localName,
+    const DeclTypeSpec *type, llvm::SmallPtrSetImpl<const Symbol *> &visited) {
+  if (!visited.insert(&opSym).second) {
+    return nullptr;
+  }
+  const Scope &scope{opSym.owner()};
+  if (scope.kind() == Scope::Kind::Module) {
+    std::string lookupName{
+        SourceReductionName(mangledName, localName, opSym.name())};
+    auto it{scope.find(parser::CharBlock{lookupName})};
+    if (it != scope.end()) {
+      const Symbol &reductionSym{*it->second};
+      const Symbol &reductionUltimate{reductionSym.GetUltimate()};
+      if (!reductionUltimate.attrs().test(Attr::PRIVATE)) {
+        if (AcceptReduction(reductionUltimate, type)) {
+          return &reductionSym;
+        }
+        // A locally declared reduction here shadows reductions reachable
+        // further along this branch.
+        if (reductionUltimate.detailsIf<UserReductionDetails>() &&
+            IsLocalReduction(reductionSym)) {
+          return nullptr;
+        }
+      }
+    }
+  }
+  // Follow a USE-associated operator to the module it was imported from.
+  if (const auto *use{opSym.detailsIf<UseDetails>()}) {
+    return SearchOperatorReduction(
+        use->symbol(), mangledName, localName, type, visited);
+  }
+  // Search each module merged into a generic operator (recursing through
+  // re-exporting facade modules).
+  if (const auto *generic{opSym.detailsIf<GenericDetails>()}) {
+    for (const Symbol &useSym : generic->uses()) {
+      if (const Symbol *result{SearchOperatorReduction(
+              useSym, mangledName, localName, type, visited)}) {
+        return result;
+      }
+    }
+  }
+  return nullptr;
+}
+
+// Find user reduction details for a mangled name, following USE associations
+// when the reduction is not directly visible in the scope. A type may be
+// supplied to disambiguate an operator that carries reductions for several
+// types (e.g. a generic merged from multiple modules); a candidate is accepted
+// only if it supports that type. A locally declared reduction is authoritative
+// for its operator in its scope and shadows USE-associated reductions.
+const Symbol *FindUserReductionSymbol(const Scope &scope,
+    const parser::CharBlock &mangledName, const DeclTypeSpec *type) {
+  // Direct lookup: a reduction directly visible via bare USE or a local
+  // declaration.
+  const Symbol *directSymbol{scope.FindSymbol(mangledName)};
+  if (directSymbol) {
+    if (AcceptReduction(*directSymbol, type)) {
+      return directSymbol;
+    }
+    // A locally declared reduction that does not support the requested type is
+    // authoritative: it shadows USE-associated reductions (ProcessReduction-
+    // Specifier erases the latter), so do not resurrect them via the operator.
+    if (directSymbol->GetUltimate().detailsIf<UserReductionDetails>() &&
+        IsLocalReduction(*directSymbol)) {
+      return nullptr;
+    }
+  }
+  // Trace the operator/procedure to the modules that declare its reduction.
+  std::string fortranName{GetReductionFortranId(mangledName)};
+  const Symbol *opSymbol{
+      fortranName.empty() ? nullptr : scope.FindSymbol(fortranName)};
+  if (!opSymbol) {
+    return nullptr;
+  }
+  llvm::SmallPtrSet<const Symbol *, 8> visited;
+  return SearchOperatorReduction(
+      *opSymbol, mangledName, opSymbol->name(), type, visited);
+}
+
+const Symbol *FindOperatorUserReductionSymbol(
+    const Symbol &operatorSym, const DeclTypeSpec *type) {
+  return FindUserReductionSymbol(
+      operatorSym.owner(), MangleDefinedOperator(operatorSym.name()), type);
+}
 } // namespace Fortran::semantics::omp
diff --git a/flang/test/Lower/OpenMP/Todo/declare-reduction-operator-use-assoc.f90 b/flang/test/Lower/OpenMP/Todo/declare-reduction-operator-use-assoc.f90
deleted file mode 100644
index 68be5781ce5cf..0000000000000
--- a/flang/test/Lower/OpenMP/Todo/declare-reduction-operator-use-assoc.f90
+++ /dev/null
@@ -1,36 +0,0 @@
-! A user-defined operator reduction whose declaration is USE-associated from a
-! module (plain USE, so the "op<spelling>" reduction symbol is imported) reaches
-! lowering but is not yet supported: lowering does not materialize imported
-! declare reductions. It must emit a clean TODO rather than ICE (#204299).
-
-! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -o - %s 2>&1 | FileCheck %s
-
-! CHECK: not yet implemented: OpenMP user-defined operator reduction is not yet supported for imported, renamed, or multiple-declaration/type reductions
-
-module m_use_op
-  type :: t
-    integer :: val = 0
-  end type
-  interface operator(.plus.)
-    module procedure add_t
-  end interface
-  !$omp declare reduction(.plus.:t:omp_out%val=omp_out%val+omp_in%val) &
-  !$omp   initializer(omp_priv=t(0))
-contains
-  type(t) function add_t(a, b)
-    type(t), intent(in) :: a, b
-    add_t%val = a%val + b%val
-  end function add_t
-end module m_use_op
-
-program p
-  use m_use_op
-  type(t) :: x
-  integer :: i
-  x = t(0)
-  !$omp parallel do reduction(.plus.:x)
-  do i = 1, 100
-    x = x .plus. t(1)
-  end do
-  !$omp end parallel do
-end program p
diff --git a/flang/test/Lower/OpenMP/declare-reduction-operator-collide.f90 b/flang/test/Lower/OpenMP/declare-reduction-operator-collide.f90
new file mode 100644
index 0000000000000..45ac1ba7ee495
--- /dev/null
+++ b/flang/test/Lower/OpenMP/declare-reduction-operator-collide.f90
@@ -0,0 +1,79 @@
+! Cross-module user-defined operator declare reduction, a regression against a
+! silent miscompile. Two modules (mod_add, mod_mul) declare operator(.remote.)
+! and a declare reduction for the same derived type t (from tmod), but with
+! different combiners (sum vs product) and matching identity initializers
+! (0 vs 1). The program imports both, each renamed to a distinct use-site
+! spelling (.addop., .mulop.), and reduces one variable with each.
+!
+! The reduction op name keys on the resolved source symbol's (name, owner), so
+! the two same-spelling same-type reductions get distinct module-scoped op names
+! and each clause binds its own combiner. Binding both clauses to one op would
+! run one variable through the other's combiner. The test asserts the two ops
+! are distinct (different owning-module qualifier) and that each clause binds its
+! own; a collision would emit a single op or cross-bind, which FileCheck catches.
+! https://github.com/llvm/llvm-project/issues/207255
+
+! RUN: %flang_fc1 -emit-hlfir -fopenmp %s -o - | FileCheck %s
+
+module tmod
+  type :: t
+    integer :: val = 0
+  end type
+end module
+
+module mod_add
+  use tmod
+  interface operator(.remote.)
+    module procedure add_t
+  end interface
+  !$omp declare reduction(.remote.:t:omp_out%val=omp_out%val+omp_in%val) &
+  !$omp   initializer(omp_priv=t(0))
+contains
+  type(t) function add_t(a, b)
+    type(t), intent(in) :: a, b
+    add_t%val = a%val + b%val
+  end function
+end module
+
+module mod_mul
+  use tmod
+  interface operator(.remote.)
+    module procedure mul_t
+  end interface
+  !$omp declare reduction(.remote.:t:omp_out%val=omp_out%val*omp_in%val) &
+  !$omp   initializer(omp_priv=t(1))
+contains
+  type(t) function mul_t(a, b)
+    type(t), intent(in) :: a, b
+    mul_t%val = a%val * b%val
+  end function
+end module
+
+program main
+  use tmod, only: t
+  use mod_add, only: operator(.addop.) => operator(.remote.)
+  use mod_mul, only: operator(.mulop.) => operator(.remote.)
+  type(t) :: x, y
+  integer :: i
+  x = t(0)
+  !$omp parallel do reduction(.addop.:x)
+  do i = 1, 5
+    x%val = x%val + i
+  end do
+  y = t(1)
+  !$omp parallel do reduction(.mulop.:y)
+  do i = 1, 5
+    y%val = y%val * i
+  end do
+  print '(I0,1X,I0)', x%val, y%val
+end program
+
+! Two distinct module-scoped ops (mod_add vs mod_mul owner qualifier), each bound
+! by its own clause (the addop loop is emitted first, then the mulop loop).
+! loose captures (R1) keyed on the owning module name; the two qualifiers differ.
+! CHECK-DAG: omp.declare_reduction @[[REDADD:_QQ[A-Za-z0-9_.]*mod_add[A-Za-z0-9_.]*op\.remote\.]] : !fir.ref
+! CHECK-DAG: omp.declare_reduction @[[REDMUL:_QQ[A-Za-z0-9_.]*mod_mul[A-Za-z0-9_.]*op\.remote\.]] : !fir.ref
+! CHECK: omp.wsloop
+! CHECK-SAME: reduction(byref @[[REDADD]]
+! CHECK: omp.wsloop
+! CHECK-SAME: reduction(byref @[[REDMUL]]
diff --git a/flang/test/Lower/OpenMP/declare-reduction-operator-merged.f90 b/flang/test/Lower/OpenMP/declare-reduction-operator-merged.f90
new file mode 100644
index 0000000000000..2ef1c2d6cc231
--- /dev/null
+++ b/flang/test/Lower/OpenMP/declare-reduction-operator-merged.f90
@@ -0,0 +1,75 @@
+! Cross-module user-defined operator declare reduction via a merged generic. Two
+! modules each declare operator(.remote.) and a single-type declare reduction:
+! m_int for t_int, m_real for t_real. The program imports both with USE, ONLY,
+! renaming operator(.remote.) to a common operator(.local.), so .local. is one
+! merged generic; each reduction clause is disambiguated by the variable's type
+! back to its own source module's reduction op (named from the source spelling
+! op.remote., not the use-site rename op.local.). Each source declare is
+! single-type. Lowering counterpart of the semantics test
+! Semantics/OpenMP/declare-reduction-use-only-merged.f90.
+! https://github.com/llvm/llvm-project/issues/207255
+
+! RUN: %flang_fc1 -emit-hlfir -fopenmp %s -o - | FileCheck %s
+
+module m_int
+  type :: t_int
+    integer :: v = 0
+  end type
+  interface operator(.remote.)
+    module procedure add_int
+  end interface
+  !$omp declare reduction(.remote.:t_int:omp_out%v=omp_out%v+omp_in%v) &
+  !$omp   initializer(omp_priv=t_int(0))
+contains
+  type(t_int) function add_int(a, b)
+    type(t_int), intent(in) :: a, b
+    add_int%v = a%v + b%v
+  end function
+end module
+
+module m_real
+  type :: t_real
+    real :: v = 0.0
+  end type
+  interface operator(.remote.)
+    module procedure add_real
+  end interface
+  !$omp declare reduction(.remote.:t_real:omp_out%v=omp_out%v+omp_in%v) &
+  !$omp   initializer(omp_priv=t_real(0.0))
+contains
+  type(t_real) function add_real(a, b)
+    type(t_real), intent(in) :: a, b
+    add_real%v = a%v + b%v
+  end function
+end module
+
+program main
+  use m_int, only: t_int, operator(.local.) => operator(.remote.)
+  use m_real, only: t_real, operator(.local.) => operator(.remote.)
+  type(t_int) :: xi
+  type(t_real) :: xr
+  integer :: i
+  xi = t_int(0)
+  !$omp parallel do reduction(.local.:xi)
+  do i = 1, 5
+    xi%v = xi%v + i
+  end do
+  xr = t_real(0.0)
+  !$omp parallel do reduction(.local.:xr)
+  do i = 1, 4
+    xr%v = xr%v + real(i)
+  end do
+  print '(I0,1X,F0.1)', xi%v, xr%v
+end program
+
+! Two source-scoped ops (m_int vs m_real owner), named from the source spelling
+! op.remote. and disambiguated by type; the t_int loop is emitted first, then the
+! t_real loop. Because the CHECK-DAG names require op.remote., an op wrongly keyed
+! on the use-site rename op.local. would fail these checks.
+! loose captures (R1) keyed on the owning module name; the two qualifiers differ.
+! CHECK-DAG: omp.declare_reduction @[[REDINT:_QQ[A-Za-z0-9_.]*m_int[A-Za-z0-9_.]*op\.remote\.]] : !fir.ref
+! CHECK-DAG: omp.declare_reduction @[[REDREAL:_QQ[A-Za-z0-9_.]*m_real[A-Za-z0-9_.]*op\.remote\.]] : !fir.ref
+! CHECK: omp.wsloop
+! CHECK-SAME: reduction(byref @[[REDINT]]
+! CHECK: omp.wsloop
+! CHECK-SAME: reduction(byref @[[REDREAL]]
diff --git a/flang/test/Lower/OpenMP/declare-reduction-operator-renamed.f90 b/flang/test/Lower/OpenMP/declare-reduction-operator-renamed.f90
new file mode 100644
index 0000000000000..3bc153bba84a3
--- /dev/null
+++ b/flang/test/Lower/OpenMP/declare-reduction-operator-renamed.f90
@@ -0,0 +1,45 @@
+! Cross-module user-defined operator declare reduction, renamed on import
+! (USE m, ONLY: operator(.local.) => operator(.remote.)). The declare-reduction
+! op must be named from the source operator spelling (.remote.), never the
+! use-site rename (.local.): lowering resolves the renamed clause operator back
+! to the module's source reduction symbol. Reproducer prints 100 at run time
+! (repro/B_renamed.f90). https://github.com/llvm/llvm-project/issues/207255
+
+! RUN: %flang_fc1 -emit-hlfir -fopenmp %s -o - | FileCheck %s
+
+module m
+  type :: t
+    integer :: val = 0
+  end type
+  interface operator(.remote.)
+    module procedure add_t
+  end interface
+  !$omp declare reduction(.remote.:t:omp_out%val=omp_out%val+omp_in%val) &
+  !$omp   initializer(omp_priv=t(0))
+contains
+  type(t) function add_t(a, b)
+    type(t), intent(in) :: a, b
+    add_t%val = a%val + b%val
+  end function
+end module
+
+program main
+  use m, only: t, operator(.local.) => operator(.remote.)
+  type(t) :: x
+  integer :: i
+  x = t(0)
+  !$omp parallel do reduction(.local.:x)
+  do i = 1, 100
+    x%val = x%val + 1
+  end do
+  print *, x%val
+end program
+
+! The op keys on the source spelling op.remote. (module-scoped); the use-site
+! rename op.local. must never appear as an op name.
+! loose capture (R1): the exact mangled qualifier is pinned after building.
+! CHECK-NOT: op.local.
+! CHECK: omp.declare_reduction @[[RED:_QQ[A-Za-z0-9_.]*op\.remote\.]] : !fir.ref
+! CHECK: omp.wsloop
+! CHECK-SAME: reduction(byref @[[RED]]
+! CHECK-NOT: op.local.
diff --git a/flang/test/Lower/OpenMP/declare-reduction-operator-separate-combiners.f90 b/flang/test/Lower/OpenMP/declare-reduction-operator-separate-combiners.f90
new file mode 100644
index 0000000000000..14454701204e7
--- /dev/null
+++ b/flang/test/Lower/OpenMP/declare-reduction-operator-separate-combiners.f90
@@ -0,0 +1,106 @@
+! Separate-compilation user-defined operator declare reductions whose combiner is
+! not a plain intrinsic-assignment expression. These exercise the two typed forms
+! (CallStmt::typedCall and AssignmentStmt::typedAssignment) that mod-file reading
+! leaves null and the materializer must repopulate before lowering, in addition
+! to the parser::Expr::typedExpr form covered by the plain cases. Both run to 100
+! out of tree. https://github.com/llvm/llvm-project/issues/207255
+
+! RUN: rm -rf %t && split-file %s %t && cd %t
+
+! Case 1: the combiner is a subroutine call (combine_sub), so lowering reads the
+! reduction combiner's CallStmt::typedCall. A null typedCall trips an assertion.
+! RUN: %flang_fc1 -emit-hlfir -fopenmp call.mod.f90 -o - > /dev/null
+! RUN: %flang_fc1 -emit-hlfir -fopenmp call.use.f90 -o - | FileCheck call.use.f90
+
+! Case 2: the combiner assigns a whole derived-type result through a defined
+! assignment, so lowering reads AssignmentStmt::typedAssignment and takes the
+! user-defined-assignment path.
+! RUN: %flang_fc1 -emit-hlfir -fopenmp asgn.mod.f90 -o - > /dev/null
+! RUN: %flang_fc1 -emit-hlfir -fopenmp asgn.use.f90 -o - | FileCheck asgn.use.f90
+
+!--- call.mod.f90
+module red_call
+  type :: t
+    integer :: val = 0
+  end type
+  interface operator(.remote.)
+    module procedure add_t
+  end interface
+  !$omp declare reduction(.remote.:t:combine_sub(omp_out, omp_in)) &
+  !$omp   initializer(omp_priv=t(0))
+contains
+  type(t) function add_t(a, b)
+    type(t), intent(in) :: a, b
+    add_t%val = a%val + b%val
+  end function
+  subroutine combine_sub(x, y)
+    type(t), intent(inout) :: x
+    type(t), intent(in) :: y
+    x%val = x%val + y%val
+  end subroutine
+end module
+
+!--- call.use.f90
+! The combiner region must lower to a call of the imported subroutine.
+! CHECK: omp.declare_reduction @[[RED:_QQ[A-Za-z0-9_.]*op\.remote\.]] : !fir.ref
+! CHECK: combiner
+! CHECK: fir.call @_QMred_callPcombine_sub
+! CHECK: omp.wsloop
+! CHECK-SAME: reduction(byref @[[RED]]
+! CHECK-NOT: not yet implemented
+program main
+  use red_call, only: t, operator(.local.) => operator(.remote.)
+  type(t) :: x
+  integer :: i
+  x = t(0)
+  !$omp parallel do reduction(.local.:x)
+  do i = 1, 100
+    x%val = x%val + 1
+  end do
+  print *, x%val
+end program
+
+!--- asgn.mod.f90
+module red_asgn
+  type :: t
+    integer :: val = 0
+  end type
+  interface operator(.remote.)
+    module procedure add_t
+  end interface
+  interface assignment(=)
+    module procedure assign_t
+  end interface
+  !$omp declare reduction(.remote.:t:omp_out = add_t(omp_out, omp_in)) &
+  !$omp   initializer(omp_priv=t(0))
+contains
+  type(t) function add_t(a, b)
+    type(t), intent(in) :: a, b
+    add_t%val = a%val + b%val
+  end function
+  subroutine assign_t(lhs, rhs)
+    type(t), intent(out) :: lhs
+    type(t), intent(in) :: rhs
+    lhs%val = rhs%val
+  end subroutine
+end module
+
+!--- asgn.use.f90
+! The combiner region must lower a call to the imported defined assignment.
+! CHECK: omp.declare_reduction @[[RED:_QQ[A-Za-z0-9_.]*op\.remote\.]] : !fir.ref
+! CHECK: combiner
+! CHECK: fir.call @_QMred_asgnPassign_t
+! CHECK: omp.wsloop
+! CHECK-SAME: reduction(byref @[[RED]]
+! CHECK-NOT: not yet implemented
+program main
+  use red_asgn, only: t, operator(.local.) => operator(.remote.)
+  type(t) :: x
+  integer :: i
+  x = t(0)
+  !$omp parallel do reduction(.local.:x)
+  do i = 1, 100
+    x%val = x%val + 1
+  end do
+  print *, x%val
+end program
diff --git a/flang/test/Lower/OpenMP/declare-reduction-operator-separate-hermetic.f90 b/flang/test/Lower/OpenMP/declare-reduction-operator-separate-hermetic.f90
new file mode 100644
index 0000000000000..ffe61e27d1e34
--- /dev/null
+++ b/flang/test/Lower/OpenMP/declare-reduction-operator-separate-hermetic.f90
@@ -0,0 +1,56 @@
+! Hermetic module files (-fhermetic-module-files) embed a module's USEd modules
+! into its own .mod. This exercises an imported operator declare reduction that
+! lives in such an embedded module: the wrapper herm_wrap re-exports herm_base
+! (which owns the reduction) and is compiled hermetically, then herm_base.mod is
+! removed so the consumer can only reach the reduction through the embedding.
+! Materializing it requires (a) the embedded module's parse tree to stay live at
+! lowering time, and (b) the reduction's module file to round-trip (its internal
+! symbol is not written as an invalid use-only item). Runtime verified out of
+! tree (runs to 100). https://github.com/llvm/llvm-project/issues/207255
+
+! RUN: rm -rf %t && split-file %s %t && cd %t
+! RUN: %flang_fc1 -fsyntax-only -fopenmp herm_base.f90
+! RUN: %flang_fc1 -fhermetic-module-files -fsyntax-only -fopenmp herm_wrap.f90
+! RUN: rm herm_base.mod
+! RUN: %flang_fc1 -emit-hlfir -fopenmp herm.use.f90 -o - | FileCheck herm.use.f90
+
+!--- herm_base.f90
+module herm_base
+  type :: t
+    integer :: val = 0
+  end type
+  interface operator(.remote.)
+    module procedure add_t
+  end interface
+  !$omp declare reduction(.remote.:t:omp_out%val=omp_out%val+omp_in%val) &
+  !$omp   initializer(omp_priv=t(0))
+contains
+  type(t) function add_t(a, b)
+    type(t), intent(in) :: a, b
+    add_t%val = a%val + b%val
+  end function
+end module
+
+!--- herm_wrap.f90
+module herm_wrap
+  use herm_base
+end module
+
+!--- herm.use.f90
+! The reduction lives in the embedded herm_base; materialization must reach it
+! through the hermetic herm_wrap.mod and name it from its source module.
+! CHECK: omp.declare_reduction @[[RED:_QQ[A-Za-z0-9_.]*base[A-Za-z0-9_.]*op\.remote\.]] : !fir.ref
+! CHECK: omp.wsloop
+! CHECK-SAME: reduction(byref @[[RED]]
+! CHECK-NOT: not yet implemented
+program main
+  use herm_wrap, only: t, operator(.local.) => operator(.remote.)
+  type(t) :: x
+  integer :: i
+  x = t(0)
+  !$omp parallel do reduction(.local.:x)
+  do i = 1, 100
+    x%val = x%val + 1
+  end do
+  print *, x%val
+end program
diff --git a/flang/test/Lower/OpenMP/declare-reduction-operator-separate-reexport.f90 b/flang/test/Lower/OpenMP/declare-reduction-operator-separate-reexport.f90
new file mode 100644
index 0000000000000..6e1019863e09b
--- /dev/null
+++ b/flang/test/Lower/OpenMP/declare-reduction-operator-separate-reexport.f90
@@ -0,0 +1,93 @@
+! Facade re-export: a module (rx_wrap) merely uses another module (rx_base) that
+! declares an operator reduction, and a consumer uses the wrapper. rx_wrap must
+! re-export the reduction so the consumer's clause binds. This also pins the
+! module-file round-trip: the reduction's internal symbol (mangled "op.remote.")
+! must not be written into rx_wrap.mod as an invalid use-only item that would
+! make rx_wrap.mod fail to re-parse. The reduction is recovered through the
+! re-exported operator.
+! https://github.com/llvm/llvm-project/issues/207255
+
+! RUN: rm -rf %t && split-file %s %t && cd %t
+! RUN: %flang_fc1 -fsyntax-only -fopenmp rx_base.f90
+! RUN: %flang_fc1 -fsyntax-only -fopenmp rx_wrap.f90
+! The wrapper module file must round-trip: no invalid "op.remote." use-only item.
+! RUN: FileCheck --check-prefix=MODFILE --input-file=rx_wrap.mod rx_wrap.f90
+! RUN: %flang_fc1 -emit-hlfir -fopenmp rx.use.f90 -o - | FileCheck rx.use.f90
+
+! A plainly-named reduction ("myred") has a valid name, so it is re-exported as a
+! normal use item and must still lower through the facade.
+! RUN: %flang_fc1 -fsyntax-only -fopenmp nm_base.f90
+! RUN: %flang_fc1 -fsyntax-only -fopenmp nm_wrap.f90
+! RUN: %flang_fc1 -emit-hlfir -fopenmp nm.use.f90 -o - | FileCheck nm.use.f90
+
+!--- rx_base.f90
+module rx_base
+  type :: t
+    integer :: val = 0
+  end type
+  interface operator(.remote.)
+    module procedure add_t
+  end interface
+  !$omp declare reduction(.remote.:t:omp_out%val=omp_out%val+omp_in%val) &
+  !$omp   initializer(omp_priv=t(0))
+contains
+  type(t) function add_t(a, b)
+    type(t), intent(in) :: a, b
+    add_t%val = a%val + b%val
+  end function
+end module
+
+!--- rx_wrap.f90
+! MODFILE: use rx_base,only:operator(.remote.)
+! MODFILE-NOT: only:op.remote.
+module rx_wrap
+  use rx_base
+end module
+
+!--- rx.use.f90
+! CHECK: omp.declare_reduction @[[RED:_QQ[A-Za-z0-9_.]*base[A-Za-z0-9_.]*op\.remote\.]] : !fir.ref
+! CHECK: omp.wsloop
+! CHECK-SAME: reduction(byref @[[RED]]
+! CHECK-NOT: not yet implemented
+program main
+  use rx_wrap, only: t, operator(.local.) => operator(.remote.)
+  type(t) :: x
+  integer :: i
+  x = t(0)
+  !$omp parallel do reduction(.local.:x)
+  do i = 1, 100
+    x%val = x%val + 1
+  end do
+  print *, x%val
+end program
+
+!--- nm_base.f90
+module nm_base
+  type :: t
+    integer :: val = 0
+  end type
+  !$omp declare reduction(myred:t:omp_out%val=omp_out%val+omp_in%val) &
+  !$omp   initializer(omp_priv=t(0))
+end module
+
+!--- nm_wrap.f90
+module nm_wrap
+  use nm_base
+end module
+
+!--- nm.use.f90
+! CHECK: omp.declare_reduction @[[RED:_QQ[A-Za-z0-9_.]*nm_base[A-Za-z0-9_.]*myred]] : !fir.ref
+! CHECK: omp.wsloop
+! CHECK-SAME: reduction(byref @[[RED]]
+! CHECK-NOT: not yet implemented
+program main
+  use nm_wrap
+  type(t) :: x
+  integer :: i
+  x = t(0)
+  !$omp parallel do reduction(myred:x)
+  do i = 1, 100
+    x%val = x%val + 1
+  end do
+  print *, x%val
+end program
\ No newline at end of file
diff --git a/flang/test/Lower/OpenMP/declare-reduction-operator-separate-unused-unsupported.f90 b/flang/test/Lower/OpenMP/declare-reduction-operator-separate-unused-unsupported.f90
new file mode 100644
index 0000000000000..5c1ef78339820
--- /dev/null
+++ b/flang/test/Lower/OpenMP/declare-reduction-operator-separate-unused-unsupported.f90
@@ -0,0 +1,86 @@
+! An unused import of an operator declare reduction over a type lowering does not
+! yet support must not abort the consumer. The materializer eagerly walks every
+! imported module reduction; without a support prefilter it would call the
+! lowering path for this reduction and hit getReductionType's "not yet
+! implemented" TODO, aborting a program that merely USEs the module. The
+! materializer's isSimpleReductionType prefilter skips it, so nothing is emitted
+! and the program compiles. A program that actually named this reduction in a
+! clause would still get the clean clause-side TODO.
+! https://github.com/llvm/llvm-project/issues/207255
+
+! RUN: rm -rf %t && split-file %s %t && cd %t
+
+! Produce the .mod with -fsyntax-only: lowering the module's own reduction would
+! hit the same-file TODO (expected for the unsupported type), but semantics and
+! the .mod write succeed.
+! RUN: %flang_fc1 -fsyntax-only -fopenmp unsup.mod.f90
+! The consumer USEs the type but never the reduction; it must lower cleanly.
+! RUN: %flang_fc1 -emit-hlfir -fopenmp unsup.use.f90 -o - 2>&1 | FileCheck unsup.use.f90
+
+! An unused import of the OpenMP 6.0 combiner-in-clause form (the combiner is in
+! a clause, not the reduction specifier) must also compile: the materializer
+! reads the combiner from the specifier (computeReductionType and the impl both
+! do), so it cannot lower this form and skips it instead of crashing on the empty
+! specifier combiner.
+! RUN: %flang_fc1 -fsyntax-only -fopenmp -fopenmp-version=60 clause.mod.f90
+! RUN: %flang_fc1 -emit-hlfir -fopenmp clause.use.f90 -o - 2>&1 | FileCheck clause.use.f90
+
+!--- unsup.mod.f90
+module red_unsup
+  type :: tarr
+    integer :: a(4) = 0
+  end type
+  interface operator(.remote.)
+    module procedure add_tarr
+  end interface
+  !$omp declare reduction(.remote.:tarr:omp_out%a=omp_out%a+omp_in%a) &
+  !$omp   initializer(omp_priv=tarr())
+contains
+  type(tarr) function add_tarr(a, b)
+    type(tarr), intent(in) :: a, b
+    add_tarr%a = a%a + b%a
+  end function
+end module
+
+!--- unsup.use.f90
+! No reduction op is materialized for the unused unsupported import, and no TODO
+! aborts the compile; the program lowers normally.
+! CHECK-NOT: omp.declare_reduction
+! CHECK-NOT: not yet implemented
+! CHECK: func.func @_QQmain
+program main
+  use red_unsup, only: tarr
+  type(tarr) :: x
+  x%a = 0
+  print *, sum(x%a)
+end program
+
+!--- clause.mod.f90
+module red_clause
+  type :: t
+    integer :: val = 0
+  end type
+  interface operator(.remote.)
+    module procedure add_t
+  end interface
+  !$omp declare reduction(.remote.:t) combiner(omp_out%val=omp_out%val+omp_in%val) &
+  !$omp   initializer(omp_priv=t(0))
+contains
+  type(t) function add_t(a, b)
+    type(t), intent(in) :: a, b
+    add_t%val = a%val + b%val
+  end function
+end module
+
+!--- clause.use.f90
+! No op is materialized for the unused combiner-in-clause import, and no TODO or
+! crash aborts the compile.
+! CHECK-NOT: omp.declare_reduction
+! CHECK-NOT: not yet implemented
+! CHECK: func.func @_QQmain
+program main
+  use red_clause, only: t
+  type(t) :: x
+  x%val = 0
+  print *, x%val
+end program
diff --git a/flang/test/Lower/OpenMP/declare-reduction-operator-separate.f90 b/flang/test/Lower/OpenMP/declare-reduction-operator-separate.f90
new file mode 100644
index 0000000000000..6864c8249d8b6
--- /dev/null
+++ b/flang/test/Lower/OpenMP/declare-reduction-operator-separate.f90
@@ -0,0 +1,162 @@
+! Separate-compilation user-defined operator declare reduction: the declare
+! reduction lives in a module compiled on its own to a .mod, and a different
+! translation unit uses the module and names the operator in a reduction clause.
+! The module is not a unit of the consumer TU, so the primary lowering pass never
+! emits its omp.declare_reduction op; lowering the consumer must materialize the
+! imported reduction (mirroring imported declare mappers) for the clause to bind,
+! rather than hit a clean "not yet implemented" TODO.
+! https://github.com/llvm/llvm-project/issues/207255
+
+! RUN: rm -rf %t && split-file %s %t && cd %t
+
+! Case 1 (plain USE). The materialized op name must be byte-identical to the
+! clause reference; the shared [[RED1]] capture pins that. A .mod re-parse that
+! resolved to the operator generic instead of the reduction symbol would name a
+! different op and fall through to the CHECK-NOT TODO.
+! RUN: %flang_fc1 -emit-hlfir -fopenmp plain.mod.f90 -o - > /dev/null
+! RUN: %flang_fc1 -emit-hlfir -fopenmp plain.use.f90 -o - | FileCheck plain.use.f90
+
+! Case 2 (renamed operator, .local. => .remote.). The reduction is still named
+! from the source module operator, so the clause reference and the materialized
+! op agree.
+! RUN: %flang_fc1 -emit-hlfir -fopenmp rename.mod.f90 -o - > /dev/null
+! RUN: %flang_fc1 -emit-hlfir -fopenmp rename.use.f90 -o - | FileCheck rename.use.f90
+
+! Case 3 (collision). Two modules define operator(.remote.) with a reduction on
+! the same type. Imported under different local names, they must lower to two
+! distinct ops keyed by their source module, not a single shared op that would
+! run one variable through the other's combiner.
+! RUN: %flang_fc1 -emit-hlfir -fopenmp collide_ty.mod.f90 -o - > /dev/null
+! RUN: %flang_fc1 -emit-hlfir -fopenmp collide_add.mod.f90 -o - > /dev/null
+! RUN: %flang_fc1 -emit-hlfir -fopenmp collide_mul.mod.f90 -o - > /dev/null
+! RUN: %flang_fc1 -emit-hlfir -fopenmp collide.use.f90 -o - | FileCheck collide.use.f90
+
+!--- plain.mod.f90
+module red_plain
+  type :: t
+    integer :: val = 0
+  end type
+  interface operator(.plus.)
+    module procedure add_t
+  end interface
+  !$omp declare reduction(.plus.:t:omp_out%val=omp_out%val+omp_in%val) &
+  !$omp   initializer(omp_priv=t(0))
+contains
+  type(t) function add_t(a, b)
+    type(t), intent(in) :: a, b
+    add_t%val = a%val + b%val
+  end function
+end module
+
+!--- plain.use.f90
+! CHECK: omp.declare_reduction @[[RED1:_QQ[A-Za-z0-9_.]*op\.plus\.]] : !fir.ref
+! CHECK: omp.wsloop
+! CHECK-SAME: reduction(byref @[[RED1]]
+! CHECK-NOT: not yet implemented
+program main
+  use red_plain
+  type(t) :: x
+  integer :: i
+  x = t(0)
+  !$omp parallel do reduction(.plus.:x)
+  do i = 1, 100
+    x%val = x%val + 1
+  end do
+  print *, x%val
+end program
+
+!--- rename.mod.f90
+module red_rename
+  type :: t
+    integer :: val = 0
+  end type
+  interface operator(.remote.)
+    module procedure add_t
+  end interface
+  !$omp declare reduction(.remote.:t:omp_out%val=omp_out%val+omp_in%val) &
+  !$omp   initializer(omp_priv=t(0))
+contains
+  type(t) function add_t(a, b)
+    type(t), intent(in) :: a, b
+    add_t%val = a%val + b%val
+  end function
+end module
+
+!--- rename.use.f90
+! CHECK: omp.declare_reduction @[[RED2:_QQ[A-Za-z0-9_.]*op\.remote\.]] : !fir.ref
+! CHECK: omp.wsloop
+! CHECK-SAME: reduction(byref @[[RED2]]
+! CHECK-NOT: not yet implemented
+program main
+  use red_rename, only: t, operator(.local.) => operator(.remote.)
+  type(t) :: x
+  integer :: i
+  x = t(0)
+  !$omp parallel do reduction(.local.:x)
+  do i = 1, 100
+    x%val = x%val + 1
+  end do
+  print *, x%val
+end program
+
+!--- collide_ty.mod.f90
+module red_ty
+  type :: t
+    integer :: val = 0
+  end type
+end module
+
+!--- collide_add.mod.f90
+module red_addmod
+  use red_ty
+  interface operator(.remote.)
+    module procedure add_t
+  end interface
+  !$omp declare reduction(.remote.:t:omp_out%val=omp_out%val+omp_in%val) &
+  !$omp   initializer(omp_priv=t(0))
+contains
+  type(t) function add_t(a, b)
+    type(t), intent(in) :: a, b
+    add_t%val = a%val + b%val
+  end function
+end module
+
+!--- collide_mul.mod.f90
+module red_mulmod
+  use red_ty
+  interface operator(.remote.)
+    module procedure mul_t
+  end interface
+  !$omp declare reduction(.remote.:t:omp_out%val=omp_out%val*omp_in%val) &
+  !$omp   initializer(omp_priv=t(1))
+contains
+  type(t) function mul_t(a, b)
+    type(t), intent(in) :: a, b
+    mul_t%val = a%val * b%val
+  end function
+end module
+
+!--- collide.use.f90
+! The two ops are distinguished by their source module (addmod vs mulmod) in the
+! mangled name, so pinning one op to each module proves they are distinct.
+! CHECK-DAG: omp.declare_reduction @{{_QQ[A-Za-z0-9_.]*addmod[A-Za-z0-9_.]*op\.remote\.}} : !fir.ref
+! CHECK-DAG: omp.declare_reduction @{{_QQ[A-Za-z0-9_.]*mulmod[A-Za-z0-9_.]*op\.remote\.}} : !fir.ref
+! CHECK-NOT: not yet implemented
+program main
+  use red_ty, only: t
+  use red_addmod, only: operator(.addop.) => operator(.remote.)
+  use red_mulmod, only: operator(.mulop.) => operator(.remote.)
+  type(t) :: x, y
+  integer :: i
+  x = t(0)
+  !$omp parallel do reduction(.addop.:x)
+  do i = 1, 5
+    x%val = x%val + i
+  end do
+  y = t(1)
+  !$omp parallel do reduction(.mulop.:y)
+  do i = 1, 5
+    y%val = y%val * i
+  end do
+  print *, x%val, y%val
+end program
diff --git a/flang/test/Lower/OpenMP/declare-reduction-operator-use-assoc.f90 b/flang/test/Lower/OpenMP/declare-reduction-operator-use-assoc.f90
new file mode 100644
index 0000000000000..78f370518e0c1
--- /dev/null
+++ b/flang/test/Lower/OpenMP/declare-reduction-operator-use-assoc.f90
@@ -0,0 +1,47 @@
+! Cross-module USE-associated user-defined operator declare reduction (plain USE).
+! The reduction clause names an operator (.plus.) whose declare reduction is
+! imported from a module; lowering resolves it to the module's source reduction
+! op and binds the clause to it. This used to be a clean TODO and was asserted by
+! Todo/declare-reduction-operator-use-assoc.f90 (now removed / flipped to this
+! positive test). Reproducer prints 100 at run time (repro/A_use_assoc.f90).
+! https://github.com/llvm/llvm-project/issues/207255
+
+! RUN: %flang_fc1 -emit-hlfir -fopenmp %s -o - | FileCheck %s
+
+module m
+  type :: t
+    integer :: val = 0
+  end type
+  interface operator(.plus.)
+    module procedure add_t
+  end interface
+  !$omp declare reduction(.plus.:t:omp_out%val=omp_out%val+omp_in%val) &
+  !$omp   initializer(omp_priv=t(0))
+contains
+  type(t) function add_t(a, b)
+    type(t), intent(in) :: a, b
+    add_t%val = a%val + b%val
+  end function
+end module
+
+program main
+  use m
+  type(t) :: x
+  integer :: i
+  x = t(0)
+  !$omp parallel do reduction(.plus.:x)
+  do i = 1, 100
+    x%val = x%val + 1
+  end do
+  print *, x%val
+end program
+
+! The op is named from the source module reduction symbol (module-scoped mangled
+! "_QQ...op.plus.", not the bare use-site spelling), and the wsloop clause binds
+! that same op.
+! loose captures (R1): the exact mangled qualifier is pinned after building.
+! CHECK: omp.declare_reduction @[[RED:_QQ[A-Za-z0-9_.]*op\.plus\.]] : !fir.ref
+! CHECK-NOT: omp.declare_reduction @op.plus.
+! CHECK: omp.wsloop
+! CHECK-SAME: reduction(byref @[[RED]]
+! CHECK-NOT: not yet implemented

>From c4519dd01052ee3a346989fa6fb6bf6dc1964c1a Mon Sep 17 00:00:00 2001
From: "Matt P. Dziubinski" <matt-p.dziubinski at hpe.com>
Date: Fri, 3 Jul 2026 15:44:40 -0500
Subject: [PATCH 4/4] [flang][OpenMP] Lower multi-type user-defined declare
 reduction

A user-defined declare reduction covering several types silently miscompiled:
every listed type bound one type-less omp.declare_reduction op, so a variable of
any but the first type ran through an op built for the wrong type (an
!fir.ref<f32> reduction on an i32 op, a mismatch the verifier does not catch).
This affects all three forms: operator (.op.), named (myred), and a name that
shadows an intrinsic (max/min/iand/ior/ieor, the arm added by llvm-project#205893).

Fixes #207489.

Emit one op per listed type and bind each variable to the op for its type. The
directive loop threads a per-type instance index across the parallel type,
combiner, and initializer lists so each op gets its own element type, combiner,
and initializer.

Name every user-reduction op from one canonical source,
getScopedUserReductionName: the reduction symbol's ultimate name, qualified with
its owning scope, plus a type and by-ref suffix. Both the directive and clause
sides call it, so their names cannot drift, and the use-associated cases converge
on the source declaration's name. Because the name also carries the owning scope,
a same-spelling same-type operator reduction imported from two modules yields two
distinct ops rather than colliding on one.

Suffix the type unconditionally, including for a single-type reduction: a
conditional suffix would let a multi-type myred's per-type op @_QQFmyred_i32
collide with a single-type reduction literally named myred_i32, silently binding
one reduction's clause to the other's combiner.

An allocatable or pointer reduction of a trivial element type is a clean TODO,
deferred to the separate boxed-type reduction work in llvm-project#186765; a
mode-independent guard (a boxed operand of a trivial element type) keeps it a
clean TODO even under -mmlir --force-byref-reduction. Allocatable character and
derived-type reductions still lower.

The same helper also lowers use-associated named reductions, including a rename,
and use-associated intrinsic-shadowing reductions.

Assisted-by: Claude Opus 4.8, GPT-5.5.
---
 .../flang/Lower/Support/ReductionProcessor.h  |  18 +-
 flang/lib/Lower/OpenMP/ClauseProcessor.cpp    |  21 +-
 flang/lib/Lower/OpenMP/ClauseProcessor.h      |   3 +-
 flang/lib/Lower/OpenMP/OpenMP.cpp             | 133 ++++++++-----
 .../lib/Lower/Support/ReductionProcessor.cpp  | 184 ++++++++++++------
 .../declare-reduction-allocatable-trivial.f90 |  33 ++++
 ...lare-reduction-operator-multiple-types.f90 |  38 ----
 .../Todo/multiple-types-declare_reduction.f90 |  51 -----
 ...eclare-reduction-character-allocatable.f90 |   4 +-
 ...eclare-reduction-initializer-component.f90 |   2 +-
 ...e-reduction-initializer-defined-assign.f90 |   2 +-
 ...declare-reduction-initializer-rhs-call.f90 |   2 +-
 .../OpenMP/declare-reduction-logical-init.f90 |   2 +-
 ...re-reduction-name-type-token-collision.f90 |  43 ++++
 .../declare-reduction-named-renamed.f90       |  38 ++++
 .../declare-reduction-named-use-assoc.f90     |  37 ++++
 ...are-reduction-no-initializer-intrinsic.f90 |   4 +-
 .../declare-reduction-operator-collide.f90    |   4 +-
 .../declare-reduction-operator-derived.f90    |   2 +-
 .../declare-reduction-operator-merged.f90     |   9 +-
 ...lare-reduction-operator-multiple-types.f90 |  66 +++++++
 .../declare-reduction-operator-renamed.f90    |   2 +-
 ...-reduction-operator-separate-combiners.f90 |   4 +-
 ...e-reduction-operator-separate-hermetic.f90 |   2 +-
 ...e-reduction-operator-separate-reexport.f90 |   4 +-
 .../declare-reduction-operator-separate.f90   |   8 +-
 .../declare-reduction-operator-use-assoc.f90  |   2 +-
 .../OpenMP/declare-reduction-operator.f90     |   2 +-
 ...re-reduction-same-name-different-scope.f90 |   4 +-
 ...ction-shadows-intrinsic-multiple-types.f90 |  45 +++++
 ...-reduction-shadows-intrinsic-use-assoc.f90 |  37 ++++
 .../declare-reduction-shadows-intrinsic.f90   |   2 +-
 .../declare-reduction-target-intrinsic.f90    |  18 +-
 .../multiple-types-declare_reduction.f90      |  55 ++++++
 .../OpenMP/omp-declare-reduction-combsub.f90  |   3 +-
 .../omp-declare-reduction-derivedtype.f90     |   2 +-
 .../OpenMP/omp-declare-reduction-initsub.f90  |   2 +-
 .../Lower/OpenMP/omp-declare-reduction.f90    |   2 +-
 38 files changed, 635 insertions(+), 255 deletions(-)
 create mode 100644 flang/test/Lower/OpenMP/Todo/declare-reduction-allocatable-trivial.f90
 delete mode 100644 flang/test/Lower/OpenMP/Todo/declare-reduction-operator-multiple-types.f90
 delete mode 100644 flang/test/Lower/OpenMP/Todo/multiple-types-declare_reduction.f90
 create mode 100644 flang/test/Lower/OpenMP/declare-reduction-name-type-token-collision.f90
 create mode 100644 flang/test/Lower/OpenMP/declare-reduction-named-renamed.f90
 create mode 100644 flang/test/Lower/OpenMP/declare-reduction-named-use-assoc.f90
 create mode 100644 flang/test/Lower/OpenMP/declare-reduction-operator-multiple-types.f90
 create mode 100644 flang/test/Lower/OpenMP/declare-reduction-shadows-intrinsic-multiple-types.f90
 create mode 100644 flang/test/Lower/OpenMP/declare-reduction-shadows-intrinsic-use-assoc.f90
 create mode 100644 flang/test/Lower/OpenMP/multiple-types-declare_reduction.f90

diff --git a/flang/include/flang/Lower/Support/ReductionProcessor.h b/flang/include/flang/Lower/Support/ReductionProcessor.h
index a949da875b3a2..e500d24f52bd0 100644
--- a/flang/include/flang/Lower/Support/ReductionProcessor.h
+++ b/flang/include/flang/Lower/Support/ReductionProcessor.h
@@ -96,15 +96,19 @@ class ReductionProcessor {
                                       const fir::KindMapping &kindMap,
                                       mlir::Type ty, bool isByRef);
 
-  /// Returns the module-unique name of the omp.declare_reduction op that
-  /// materializes a user-defined reduction (named or operator). The name is
-  /// derived from the reduction symbol's ultimate name, qualified with its
-  /// owning scope via AbstractConverter::mangleName, so that reductions with
-  /// the same spelling in different modules do not collide. The directive and
-  /// clause lowering must both use this to agree on the op's symbol name.
+  /// Returns the module-unique name of the omp.declare_reduction op for a
+  /// user-defined reduction (named or operator). Derived from the reduction
+  /// symbol's ultimate name, qualified with its owning scope via
+  /// AbstractConverter::mangleName so same-spelling reductions in different
+  /// modules do not collide, then suffixed with the reduction type (via
+  /// getReductionName) so a reduction listing several types produces one op per
+  /// type (single-type is N=1). The directive and clause sides must both call
+  /// this with the same unwrapped type and isByRef, since both tokens are part
+  /// of the name, or the names diverge.
   static std::string
   getScopedUserReductionName(AbstractConverter &converter,
-                             const semantics::Symbol &reductionSymbol);
+                             const semantics::Symbol &reductionSymbol,
+                             mlir::Type reductionType, bool isByRef);
 
   /// This function returns the identity value of the operator \p
   /// reductionOpName. For example:
diff --git a/flang/lib/Lower/OpenMP/ClauseProcessor.cpp b/flang/lib/Lower/OpenMP/ClauseProcessor.cpp
index ee88a24f97263..0d203afc4a168 100644
--- a/flang/lib/Lower/OpenMP/ClauseProcessor.cpp
+++ b/flang/lib/Lower/OpenMP/ClauseProcessor.cpp
@@ -538,7 +538,8 @@ bool ClauseProcessor::processInclusive(
 
 bool ClauseProcessor::processInitializer(
     lower::SymMap &symMap, ReductionProcessor::GenInitValueCBTy &genInitValueCB,
-    const parser::OmpStylizedInstance *parserInitInstance) const {
+    const parser::OmpStylizedInstance *parserInitInstance,
+    unsigned instanceIdx) const {
   if (auto *clause = findUniqueClause<omp::clause::Initializer>()) {
     // Extract the typed assignment from the parser-level instance, if
     // the initializer is an assignment statement (as opposed to a call).
@@ -553,13 +554,21 @@ bool ClauseProcessor::processInitializer(
             assign = &*wrapper->v;
       }
     }
-    genInitValueCB = [&, clause, assign](fir::FirOpBuilder &builder,
-                                         mlir::Location loc, mlir::Type type,
-                                         mlir::Value moldArg,
-                                         mlir::Value privArg) {
+    // A multi-type declare reduction carries one initializer instance per
+    // listed type (parallel to the combiner instances and typeNameList); select
+    // the one for the type being lowered (index instanceIdx). Single-type is
+    // index 0 of one. Capture instanceIdx by value: the callback runs later
+    // (during createDeclareReductionHelper), after this parameter's lifetime
+    // ends.
+    assert(instanceIdx < clause->v.size() &&
+           "initializer instance index out of range");
+    genInitValueCB = [&, clause, assign,
+                      instanceIdx](fir::FirOpBuilder &builder,
+                                   mlir::Location loc, mlir::Type type,
+                                   mlir::Value moldArg, mlir::Value privArg) {
       lower::SymMapScope scope(symMap);
       mlir::Value ompPrivVar;
-      const StylizedInstance &inst = clause->v.front();
+      const StylizedInstance &inst = clause->v[instanceIdx];
 
       for (const Object &object :
            std::get<StylizedInstance::Variables>(inst.t)) {
diff --git a/flang/lib/Lower/OpenMP/ClauseProcessor.h b/flang/lib/Lower/OpenMP/ClauseProcessor.h
index 1435af3e844e9..61c4db49c3877 100644
--- a/flang/lib/Lower/OpenMP/ClauseProcessor.h
+++ b/flang/lib/Lower/OpenMP/ClauseProcessor.h
@@ -97,7 +97,8 @@ class ClauseProcessor {
   bool processInitializer(
       lower::SymMap &symMap,
       ReductionProcessor::GenInitValueCBTy &genInitValueCB,
-      const parser::OmpStylizedInstance *parserInitInstance = nullptr) const;
+      const parser::OmpStylizedInstance *parserInitInstance = nullptr,
+      unsigned instanceIdx = 0) const;
   bool processMergeable(mlir::omp::MergeableClauseOps &result) const;
   bool processNogroup(mlir::omp::NogroupClauseOps &result) const;
   bool processNotinbranch(mlir::omp::NotinbranchClauseOps &result) const;
diff --git a/flang/lib/Lower/OpenMP/OpenMP.cpp b/flang/lib/Lower/OpenMP/OpenMP.cpp
index a45b52b53212f..b18a4f5b34e63 100644
--- a/flang/lib/Lower/OpenMP/OpenMP.cpp
+++ b/flang/lib/Lower/OpenMP/OpenMP.cpp
@@ -4506,7 +4506,7 @@ genOMP(lower::AbstractConverter &converter, lower::SymMap &symTable,
 
 static ReductionProcessor::GenCombinerCBTy processReductionCombiner(
     lower::AbstractConverter &converter, lower::SymMap &symTable,
-    semantics::SemanticsContext &semaCtx, const clause::Combiner &combiner,
+    semantics::SemanticsContext &semaCtx, const StylizedInstance &combinerInst,
     const parser::OmpStylizedInstance &parserInst) {
   // Extract the typed assignment from the parser-level instance, if
   // the combiner is an assignment statement (as opposed to a call).
@@ -4520,7 +4520,11 @@ static ReductionProcessor::GenCombinerCBTy processReductionCombiner(
         assign = &*wrapper->v;
   }
   ReductionProcessor::GenCombinerCBTy genCombinerCB;
-  const StylizedInstance &inst = combiner.v.front();
+  // Use the combiner instance for the type currently being lowered (one per
+  // listed type, in typeNameList order): a multi-type declare reduction carries
+  // a distinct stylized instance per type so omp_out/omp_in are re-typed for
+  // each. Single-type is index 0 of one.
+  const StylizedInstance &inst = combinerInst;
   semantics::SomeExpr evalExpr = std::get<StylizedInstance::Instance>(inst.t);
 
   genCombinerCB = [&, evalExpr, assign](fir::FirOpBuilder &builder,
@@ -4681,22 +4685,19 @@ static bool isSimpleReductionType(mlir::Type reductionType) {
   return false;
 }
 
-// Compute the reduction's element type from the combiner's stylized declaration
-// symbol, without checking whether lowering supports it. Shared by
-// getReductionType (same-file, enforces support with a TODO) and
-// materializeOpenMPDeclareReductions (skips an unsupported unused import via
-// isSimpleReductionType instead of aborting the consumer). Getting the type
-// from that symbol compared to the declared type-list (a DeclarationTypeSpec)
-// is simpler since we do not need to consider derived vs intrinsic types.
-// Semantics is guaranteed to generate these symbols.
+// Compute the reduction's element type from the given combiner instance's
+// stylized declaration symbol (omp_out), without checking whether lowering
+// supports it. A multi-type declare reduction carries one combiner instance per
+// listed type (typeNameList order), so the caller passes the instance for the
+// type being lowered. Shared by getReductionType (same-file, enforces support
+// with a TODO) and materializeOpenMPDeclareReductions (skips an unsupported
+// unused import via isSimpleReductionType instead of aborting the consumer).
+// Getting the type from that symbol compared to the declared type-list (a
+// DeclarationTypeSpec) is simpler since we do not need to consider derived vs
+// intrinsic types. Semantics is guaranteed to generate these symbols.
 static mlir::Type
 computeReductionType(lower::AbstractConverter &converter,
-                     const parser::OmpReductionSpecifier &specifier) {
-  const auto &combinerExpression =
-      std::get<std::optional<parser::OmpCombinerExpression>>(specifier.t)
-          .value();
-  const parser::OmpStylizedInstance &combinerInstance =
-      combinerExpression.v.front();
+                     const parser::OmpStylizedInstance &combinerInstance) {
   const std::list<parser::OmpStylizedDeclaration> &declList =
       std::get<std::list<parser::OmpStylizedDeclaration>>(combinerInstance.t);
   const parser::OmpStylizedDeclaration &decl = declList.front();
@@ -4709,8 +4710,8 @@ computeReductionType(lower::AbstractConverter &converter,
 // support it.
 static mlir::Type
 getReductionType(lower::AbstractConverter &converter,
-                 const parser::OmpReductionSpecifier &specifier) {
-  mlir::Type reductionType = computeReductionType(converter, specifier);
+                 const parser::OmpStylizedInstance &combinerInstance) {
+  mlir::Type reductionType = computeReductionType(converter, combinerInstance);
   if (!isSimpleReductionType(reductionType))
     TODO(converter.getCurrentLocation(),
          "declare reduction currently only supports trivial types, "
@@ -4793,6 +4794,10 @@ static void genOpenMPDeclareReductionImpl(
       initExpr ? initExpr->v.begin()
                : std::list<parser::OmpStylizedInstance>::const_iterator{};
 
+  // Index of the type currently being lowered within the parallel per-type
+  // lists: typeNameList.v, the clause-level combiner instances (combiner.v),
+  // and the clause-level initializer instances. Single-type is index 0 of one.
+  unsigned instanceIdx = 0;
   for (const auto &typeSpec : typeNameList.v) {
     (void)typeSpec; // Currently unused
 
@@ -4800,7 +4805,7 @@ static void genOpenMPDeclareReductionImpl(
            "Mismatched combiner instance count");
     const parser::OmpStylizedInstance &parserInst = *parserInstIt++;
 
-    mlir::Type reductionType = getReductionType(converter, specifier);
+    mlir::Type reductionType = getReductionType(converter, parserInst);
     bool isByRef = ReductionProcessor::doReductionByRef(reductionType);
     // Compute the canonical reduction name the same way
     // processReductionArguments does.
@@ -4823,43 +4828,52 @@ static void genOpenMPDeclareReductionImpl(
                         // naming contract (the clause side is in
                         // ReductionProcessor::processReductionArguments). Name
                         // the op via getScopedUserReductionName from the
-                        // symbol's ultimate (name, owner), byte-identical to
-                        // the clause reference. symOpt supplies the source
-                        // symbol for separate compilation, else opName.v.sym().
-                        // Single-declaration, single-type only; otherwise a
-                        // clean
-                        // TODO.
+                        // symbol's ultimate (name, owner) plus the per-type
+                        // suffix, byte-identical to the clause reference.
+                        // symOpt supplies the source symbol for separate
+                        // compilation, else opName.v.sym(). Runs once per
+                        // listed type (the enclosing loop walks typeNameList in
+                        // lockstep with the per-type combiner/initializer
+                        // instances), emitting one op per (operator, type);
+                        // single-type is N=1.
                         const semantics::Symbol &redSym =
                             symOpt ? symOpt->GetUltimate()
                                    : opName.v.sym()->GetUltimate();
                         const auto *userDetails =
                             redSym.detailsIf<semantics::UserReductionDetails>();
-                        if (!userDetails || typeNameList.v.size() != 1 ||
-                            userDetails->GetDeclList().size() != 1 ||
-                            userDetails->GetTypeList().size() != 1)
+                        if (!userDetails)
                           TODO(converter.getCurrentLocation(),
                                "OpenMP user-defined operator declare reduction "
-                               "with multiple declarations or multiple types");
+                               "without user reduction details");
                         return ReductionProcessor::getScopedUserReductionName(
-                            converter, redSym);
+                            converter, redSym, reductionType, isByRef);
                       },
                   },
                   defOp.u);
             },
             [&](const clause::ProcedureDesignator &pd) -> std::string {
-              // Qualify the name with the scope in which the user-defined
-              // reduction is declared so that reductions with the same name
-              // in different scopes produce distinct omp.declare_reduction ops.
-              const semantics::Symbol *sym = pd.v.sym();
-              std::string name = sym->name().ToString();
-              return converter.mangleName(name, sym->GetUltimate().owner());
+              // Directive side of the named-reduction naming contract (clause
+              // side in ReductionProcessor::processReductionArguments). Name
+              // the op via getScopedUserReductionName from the source symbol's
+              // ultimate (name, owner) plus the per-type suffix, byte-identical
+              // to the clause reference. symOpt supplies the source symbol for
+              // separate compilation, else pd.v.sym(); GetUltimate keeps the
+              // name stable across a plain `use`. reductionType is the declared
+              // element type (already canonical), so unlike the clause side it
+              // needs no namingType normalization.
+              const semantics::Symbol &redSym =
+                  (symOpt ? *symOpt : *pd.v.sym()).GetUltimate();
+              return ReductionProcessor::getScopedUserReductionName(
+                  converter, redSym, reductionType, isByRef);
             },
         },
         redOp.u);
 
+    assert(instanceIdx < combiner.v.size() &&
+           "Mismatched combiner instance count");
     ReductionProcessor::GenCombinerCBTy genCombinerCB =
-        processReductionCombiner(converter, symTable, semaCtx, combiner,
-                                 parserInst);
+        processReductionCombiner(converter, symTable, semaCtx,
+                                 combiner.v[instanceIdx], parserInst);
     const parser::OmpStylizedInstance *parserInitInst = nullptr;
     if (initExpr) {
       assert(parserInitInstIt != initExpr->v.end() &&
@@ -4883,7 +4897,8 @@ static void genOpenMPDeclareReductionImpl(
 
     ReductionProcessor::GenInitValueCBTy genInitValueCB;
     ClauseProcessor cp(converter, semaCtx, clauses);
-    if (!cp.processInitializer(symTable, genInitValueCB, parserInitInst)) {
+    if (!cp.processInitializer(symTable, genInitValueCB, parserInitInst,
+                               instanceIdx)) {
       // No initializer clause provided. Per OpenMP, initialize as
       // default-initialized using the shared inline init helper.
       const semantics::DerivedTypeSpec *derivedTypeSpec = nullptr;
@@ -4936,6 +4951,7 @@ static void genOpenMPDeclareReductionImpl(
         mlir::omp::DeclareReductionOp>(
         converter, reductionNameStr, redType, converter.getCurrentLocation(),
         isByRef, genCombinerCB, genInitValueCB, reductionSym);
+    ++instanceIdx;
   }
 }
 
@@ -6145,13 +6161,14 @@ void Fortran::lower::materializeOpenMPDeclareReductions(
       continue;
     // Skip without calling the impl: a consumer that merely uses this module
     // must compile even when the reduction is out of the clause-side resolver's
-    // reach. Eagerly lowering a private-ultimate or unsupported-shape (multiple
-    // declarations/types) reduction would trip the impl's TODO and abort an
-    // unused import; a program that references it still gets the clause-side
-    // TODO.
-    if (ultimate.attrs().test(semantics::Attr::PRIVATE) ||
-        userDetails->GetDeclList().size() != 1 ||
-        userDetails->GetTypeList().size() != 1)
+    // reach. Eagerly lowering a private-ultimate reduction would trip the
+    // impl's
+    // TODO and abort an unused import; a program that references it still gets
+    // the clause-side TODO. The impl's typeNameList loop materializes multiple
+    // declarations and types (one op per type); the per-declaration skips below
+    // guard the remaining unsupported shapes (combiner-in-clause and
+    // unsupported element types).
+    if (ultimate.attrs().test(semantics::Attr::PRIVATE))
       continue;
     for (const auto *decl : userDetails->GetDeclList()) {
       if (const auto *reductionDecl =
@@ -6159,8 +6176,8 @@ void Fortran::lower::materializeOpenMPDeclareReductions(
         // An unused import must not abort the consumer, so skip a shape the
         // impl cannot lower (a program that references it still gets the
         // clause-side
-        // TODO), completing the mandatory skip above (which covers only private
-        // / multiple declarations / types).
+        // TODO), completing the mandatory skip above (which covers only
+        // private).
         const auto &specifier =
             DEREF(parser::omp::GetFirstArgument<parser::OmpReductionSpecifier>(
                 reductionDecl->v));
@@ -6172,8 +6189,24 @@ void Fortran::lower::materializeOpenMPDeclareReductions(
                  .has_value())
           continue;
         // A reduction type lowering does not yet support: mirror the impl's
-        // getReductionType check with the shared predicate.
-        if (!isSimpleReductionType(computeReductionType(converter, specifier)))
+        // getReductionType check with the shared predicate. A single
+        // declaration may list several types (Form A), each with its own
+        // combiner instance; the impl emits one op per type in a single call,
+        // so skip the whole declaration if any listed type is unsupported
+        // (materializing only a subset is not expressible through the impl's
+        // all-types loop).
+        const parser::OmpCombinerExpression &combinerExpr =
+            std::get<std::optional<parser::OmpCombinerExpression>>(specifier.t)
+                .value();
+        bool anyUnsupportedType = false;
+        for (const parser::OmpStylizedInstance &combinerInst : combinerExpr.v) {
+          if (!isSimpleReductionType(
+                  computeReductionType(converter, combinerInst))) {
+            anyUnsupportedType = true;
+            break;
+          }
+        }
+        if (anyUnsupportedType)
           continue;
         // Mod-file reading runs only ResolveNames, so an imported combiner and
         // initializer carry bound names but null
diff --git a/flang/lib/Lower/Support/ReductionProcessor.cpp b/flang/lib/Lower/Support/ReductionProcessor.cpp
index 54a9ffe642613..381269c60bce3 100644
--- a/flang/lib/Lower/Support/ReductionProcessor.cpp
+++ b/flang/lib/Lower/Support/ReductionProcessor.cpp
@@ -218,14 +218,27 @@ ReductionProcessor::getReductionName(ReductionIdentifier redId,
 }
 
 std::string ReductionProcessor::getScopedUserReductionName(
-    AbstractConverter &converter, const semantics::Symbol &reductionSymbol) {
+    AbstractConverter &converter, const semantics::Symbol &reductionSymbol,
+    mlir::Type reductionType, bool isByRef) {
   // Qualify the reduction symbol's ultimate name with its owning scope so that
   // user-defined reductions with the same spelling in different modules get
   // distinct op names. Use the (name, scope) mangleName overload: the
   // (symbol) overload does not handle UserReductionDetails.
   const semantics::Symbol &ultimate = reductionSymbol.GetUltimate();
   std::string name = ultimate.name().ToString();
-  return converter.mangleName(name, ultimate.owner());
+  std::string scopedName = converter.mangleName(name, ultimate.owner());
+  // Append the type and by-ref suffix, as the intrinsic reductions do, so a
+  // declare reduction listing several types produces one op per type and every
+  // name ends in a type-grammar token. Suffix unconditionally, even for one
+  // type: otherwise a multi-type myred's per-type op "myred_i32" would collide
+  // with a single-type reduction named "myred_i32", and the by-name dedup would
+  // bind one reduction's clause to the other's combiner (a silent miscompile).
+  // The by-ref token also gives an allocatable/pointer trivial reduction (a
+  // boxed by-ref operand of a by-value declared type) a name the directive
+  // never emitted, so it reaches a clean TODO instead of binding a by-value op
+  // to a box (handled by llvm-project#186765).
+  return getReductionName(scopedName, converter.getFirOpBuilder().getKindMap(),
+                          reductionType, isByRef);
 }
 
 mlir::Value
@@ -699,25 +712,17 @@ bool ReductionProcessor::processReductionArguments(
         redOperatorList.front();
 
     if (!std::holds_alternative<omp::clause::DefinedOperator>(redOperator.u)) {
-      if (const auto *reductionIntrinsic =
-              std::get_if<omp::clause::ProcedureDesignator>(&redOperator.u)) {
-        if (!ReductionProcessor::supportedIntrinsicProcReduction(
-                *reductionIntrinsic)) {
-          // If not an intrinsic is has to be a custom reduction op, and should
-          // be available in the module. The op is named using the scope in
-          // which the user-defined reduction was declared, so qualify the
-          // lookup name the same way the declaration and use sides do.
-          semantics::Symbol *sym = reductionIntrinsic->v.sym();
-          mlir::ModuleOp module = builder.getModule();
-          std::string declName = getRealName(sym).ToString();
-          declName = converter.mangleName(declName, sym->GetUltimate().owner());
-          auto decl = module.lookupSymbol<OpType>(declName);
-          if (!decl)
-            return false;
-        }
-      } else {
-        return false;
-      }
+      // A named (procedure-designator) reduction, the only other alternative.
+      // Defer validation to the per-variable loop below: a named reduction's op
+      // is named per the variable's type (getScopedUserReductionName appends
+      // the type suffix), so its existence and type can only be checked once
+      // the variable type is known. The loop resolves the symbol, confirms its
+      // ultimate has UserReductionDetails, looks the op up by the type-specific
+      // name, and emits a clean TODO if missing or type-mismatched.
+      assert(std::holds_alternative<omp::clause::ProcedureDesignator>(
+                 redOperator.u) &&
+             "ReductionIdentifier variant has only DefinedOperator and "
+             "ProcedureDesignator");
     }
   }
 
@@ -820,6 +825,32 @@ bool ReductionProcessor::processReductionArguments(
                                  omp::clause::ReductionOperatorList>) {
       const Fortran::lower::omp::clause::ReductionOperator &redOperator =
           redOperatorList.front();
+      // Name user-defined reduction ops from the canonical element type, not
+      // the raw lowered variable type. An allocatable/pointer variable lowers
+      // to a boxed reference (!fir.ref<!fir.box<!fir.heap<!fir.char<1>>>>), but
+      // the directive names its op from the declared element type
+      // (!fir.char<1>); getReductionName unwraps only one reference level, so
+      // without stripping the ref/box/heap/pointer wrappers the by-ref suffix
+      // diverges and a valid allocatable-character reduction is wrongly
+      // rejected. Keep any array fir::SequenceType. Only naming and the type
+      // check use namingType; redType stays intact for op binding.
+      mlir::Type namingType =
+          fir::unwrapPassByRefType(fir::unwrapRefType(redType));
+      // An allocatable/pointer reduction of a trivial element type is a case
+      // the directive does not materialize: it emits the scalar by-value op,
+      // and binding the boxed by-ref operand to it is invalid IR. In the
+      // default mode the by-ref name suffix diverges (boxed clause by-ref,
+      // scalar op by-value), the lookup misses, and it is a clean TODO. Under
+      // -mmlir
+      // --force-byref-reduction both sides are forced by-ref, the names match,
+      // and the element-only type check (i32 == i32) would bind the box to the
+      // scalar op. Guard on the inherent triviality of the element type, not
+      // doReductionByRef (which the flag forces), so it is a clean TODO in
+      // every mode; boxed character and derived reductions (genuinely by-ref)
+      // still bind. Deferred to flang PR #186765.
+      const bool isBoxedTrivialReduction =
+          mlir::isa<fir::BaseBoxType>(fir::unwrapRefType(redType)) &&
+          fir::isa_trivial(namingType);
       if (const auto &redDefinedOp =
               std::get_if<omp::clause::DefinedOperator>(&redOperator.u)) {
         if (const auto *definedOpName =
@@ -833,10 +864,12 @@ bool ReductionProcessor::processReductionArguments(
           // owner). Reductions in different source modules have distinct owner
           // scopes, so a same-spelling/same-type operator reduction imported
           // from two modules yields two distinct ops, each clause binding its
-          // own combiner instead of colliding onto one. Single-declaration,
-          // single-type only; anything else, or an operator with no reduction
-          // for the variable's type, is a clean TODO rather than a crash or a
-          // wrong binding.
+          // own combiner instead of colliding onto one. The variable's type
+          // selects the matching per-type op name (getScopedUserReductionName
+          // appends the type), so a multiple-declaration/multiple-type operator
+          // is handled (one op per type). An operator with no reduction for the
+          // variable's type, or a variable with no type, is a clean TODO rather
+          // than a crash or a wrong binding.
           const semantics::Symbol *opSym = definedOpName->v.sym();
           const semantics::DeclTypeSpec *varType =
               reductionSymbols[idx]->GetUltimate().GetType();
@@ -853,25 +886,26 @@ bool ReductionProcessor::processReductionArguments(
           const semantics::UserReductionDetails *userDetails =
               ultimate ? ultimate->detailsIf<semantics::UserReductionDetails>()
                        : nullptr;
-          if (!varType || !resolvedSym || !userDetails ||
-              userDetails->GetDeclList().size() != 1 ||
-              userDetails->GetTypeList().size() != 1) {
+          if (!varType || !resolvedSym || !userDetails) {
             TODO(currentLocation,
                  "OpenMP user-defined operator reduction is not yet supported "
-                 "for imported, renamed, or multiple-declaration/type "
-                 "reductions");
+                 "when the variable has no type or no matching reduction is "
+                 "visible for its type");
           }
           std::string opName = ReductionProcessor::getScopedUserReductionName(
-              converter, *resolvedSym);
+              converter, *resolvedSym, namingType, isByRef);
           mlir::ModuleOp module = builder.getModule();
           auto existingDecl = module.lookupSymbol<OpType>(opName);
           // The MLIR verifier does not type-check these ops (they have no
           // atomic region), so this is the only guard against binding a
-          // mismatched declaration. Compare unwrapped types: the clause redType
-          // is always a reference type, while the op stores the unwrapped type
-          // for by-value reductions.
-          if (!existingDecl || fir::unwrapRefType(existingDecl.getType()) !=
-                                   fir::unwrapRefType(redType)) {
+          // mismatched declaration. Compare unwrapped element types: namingType
+          // is the canonical reduction element type (allocatable/pointer
+          // storage wrappers stripped), matching the type the op was named and
+          // created from on the directive side.
+          if (!existingDecl ||
+              fir::unwrapRefType(existingDecl.getType()) !=
+                  fir::unwrapRefType(namingType) ||
+              isBoxedTrivialReduction) {
             TODO(currentLocation,
                  "OpenMP user-defined operator reduction declaration was not "
                  "materialized for this type");
@@ -915,17 +949,47 @@ bool ReductionProcessor::processReductionArguments(
                          &redOperator.u)) {
         if (!ReductionProcessor::supportedIntrinsicProcReduction(
                 *reductionIntrinsic)) {
-          // Custom reductions we can just add to the symbols without
-          // generating the declare reduction op.
+          // A user-defined named reduction (declare reduction(myred: ...)). The
+          // clause references the (possibly USE-associated) reduction symbol;
+          // bind its pre-materialized omp.declare_reduction op instead of
+          // generating a new one.
           semantics::Symbol *sym = reductionIntrinsic->v.sym();
-          // Qualify the name with the scope in which the user-defined
-          // reduction was declared so that reductions with the same name in
-          // different scopes refer to distinct omp.declare_reduction ops.
-          std::string reductionName = getRealName(sym).ToString();
-          reductionName =
-              converter.mangleName(reductionName, sym->GetUltimate().owner());
-          reductionDeclSymbols.push_back(
-              mlir::SymbolRefAttr::get(builder.getContext(), reductionName));
+          if (!sym->GetUltimate()
+                   .detailsIf<semantics::UserReductionDetails>()) {
+            // Not a supported intrinsic proc (checked just above) and not a
+            // user-defined reduction: a clean TODO, never a wrong binding. (A
+            // named reduction reusing an intrinsic spelling such as `max` is
+            // handled by the supportedIntrinsicProcReduction branch, not here.)
+            TODO(currentLocation, "Lowering unrecognised reduction type");
+          }
+          // Name the op from the resolved symbol's scoped ultimate (name,
+          // owner) plus the per-type suffix, byte-identical to the
+          // directive/materializer side, via getScopedUserReductionName.
+          // Reductions of the same spelling in different scopes refer to
+          // distinct ops, and a named reduction listing several types binds the
+          // op for the variable's type instead of colliding on the first type's
+          // op. namingType is the canonical element type (storage wrappers
+          // stripped) so the by-ref suffix matches the directive.
+          std::string reductionName =
+              ReductionProcessor::getScopedUserReductionName(
+                  converter, *sym, namingType, isByRef);
+          mlir::ModuleOp module = builder.getModule();
+          auto existingDecl = module.lookupSymbol<OpType>(reductionName);
+          // The MLIR verifier does not type-check these ops, so this is the
+          // only guard against binding a missing or mismatched declaration
+          // (e.g. a named declare reduction that does not list the variable's
+          // type). Compare unwrapped element types against namingType, the type
+          // the op was named and created from.
+          if (!existingDecl ||
+              fir::unwrapRefType(existingDecl.getType()) !=
+                  fir::unwrapRefType(namingType) ||
+              isBoxedTrivialReduction) {
+            TODO(currentLocation,
+                 "OpenMP user-defined named reduction declaration was not "
+                 "materialized for the variable's type");
+          }
+          reductionDeclSymbols.push_back(mlir::SymbolRefAttr::get(
+              builder.getContext(), existingDecl.getSymName()));
           ++idx;
           continue;
         }
@@ -953,21 +1017,25 @@ bool ReductionProcessor::processReductionArguments(
           // implicit intrinsic reduction still applies, so fall through to it.
           if (userDetails && varType && userDetails->SupportsType(*varType)) {
             // The user declaration takes precedence over the intrinsic for this
-            // type. Only a locally-declared, single-declaration, single-type
-            // reduction is currently supported.
-            if (&ultimate != redSym || userDetails->GetDeclList().size() != 1 ||
-                userDetails->GetTypeList().size() != 1) {
-              TODO(currentLocation,
-                   "OpenMP user-defined reduction shadowing an intrinsic "
-                   "reduction is not yet supported for imported, renamed, or "
-                   "multiple-declaration/type reductions.");
-            }
+            // type. A declaration listing several types (or several merged
+            // declarations) is handled the same way as the operator and named
+            // paths: the directive emits one op per type and the variable's
+            // type selects the matching per-type name below. A USE-associated
+            // shadowing reduction is found by FindSymbol as a use wrapper;
+            // naming from its ultimate (name, owner) below binds the source
+            // module's op, which materializeOpenMPDeclareReductions emits,
+            // exactly as the named path does. A renamed shadowing intrinsic
+            // does not reach here: the renamed name resolves to the intrinsic
+            // rather than the user reduction, so semantics rejects the clause
+            // with a type-incompatibility error before lowering.
             std::string opName = ReductionProcessor::getScopedUserReductionName(
-                converter, ultimate);
+                converter, ultimate, namingType, isByRef);
             mlir::ModuleOp module = builder.getModule();
             auto existingDecl = module.lookupSymbol<OpType>(opName);
-            if (!existingDecl || fir::unwrapRefType(existingDecl.getType()) !=
-                                     fir::unwrapRefType(redType)) {
+            if (!existingDecl ||
+                fir::unwrapRefType(existingDecl.getType()) !=
+                    fir::unwrapRefType(namingType) ||
+                isBoxedTrivialReduction) {
               TODO(currentLocation,
                    "OpenMP user-defined reduction declaration was not "
                    "materialized for this type");
diff --git a/flang/test/Lower/OpenMP/Todo/declare-reduction-allocatable-trivial.f90 b/flang/test/Lower/OpenMP/Todo/declare-reduction-allocatable-trivial.f90
new file mode 100644
index 0000000000000..0adfb07ddde3e
--- /dev/null
+++ b/flang/test/Lower/OpenMP/Todo/declare-reduction-allocatable-trivial.f90
@@ -0,0 +1,33 @@
+! A user-defined reduction on an allocatable (or pointer) variable of a trivial
+! by-value type (integer/real) is not yet lowered: the directive materializes a
+! by-value op (e.g. @..._i32) from the declared element type, but the reduction
+! operand is a boxed, by-ref allocatable, so the clause looks up a by-ref name
+! (@..._byref_i32) the directive never emitted and reaches a clean TODO rather
+! than binding a by-value op to a boxed operand (which produced invalid IR
+! before). Creating the boxed-type op for such operands is tracked by
+! https://github.com/llvm/llvm-project/pull/186765. Character and derived-type
+! allocatable reductions (by-ref op and by-ref operand) do lower and are covered
+! by declare-reduction-character-allocatable.f90.
+
+! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -o - %s 2>&1 | FileCheck %s
+
+! CHECK: not yet implemented: OpenMP user-defined named reduction declaration was not materialized for the variable's type
+
+program p
+  integer, allocatable :: a
+  real, allocatable :: b
+  integer :: i
+  !$omp declare reduction(myadd: integer, real : omp_out = omp_out + omp_in) &
+  !$omp   initializer(omp_priv = 0)
+  allocate(a); a = 0
+  allocate(b); b = 0.0
+  !$omp parallel do reduction(myadd: a)
+  do i = 1, 5
+    a = a + i
+  end do
+  !$omp parallel do reduction(myadd: b)
+  do i = 1, 5
+    b = b + real(i)
+  end do
+  print *, a, b
+end program
diff --git a/flang/test/Lower/OpenMP/Todo/declare-reduction-operator-multiple-types.f90 b/flang/test/Lower/OpenMP/Todo/declare-reduction-operator-multiple-types.f90
deleted file mode 100644
index 626316221af1b..0000000000000
--- a/flang/test/Lower/OpenMP/Todo/declare-reduction-operator-multiple-types.f90
+++ /dev/null
@@ -1,38 +0,0 @@
-! A user-defined operator declare reduction listing multiple types reaches
-! lowering but is not yet supported (the op name is not type-specific, so the
-! per-type ops would collide). It must emit a clean TODO rather than ICE or
-! miscompile (#204299). The guard is on the directive side, which is lowered
-! before any reduction clause.
-
-! RUN: %not_todo_cmd %flang_fc1 -emit-hlfir -fopenmp -o - %s 2>&1 | FileCheck %s
-
-! CHECK: not yet implemented: OpenMP user-defined operator declare reduction with multiple declarations or multiple types
-
-program p
-  type :: t1
-    integer :: v = 0
-  end type
-  type :: t2
-    integer :: w = 0
-  end type
-  interface operator(.mt.)
-    function f1(a, b)
-      import :: t1
-      type(t1), intent(in) :: a, b
-      type(t1) :: f1
-    end function f1
-    function f2(a, b)
-      import :: t2
-      type(t2), intent(in) :: a, b
-      type(t2) :: f2
-    end function f2
-  end interface
-  !$omp declare reduction(.mt.:t1,t2:omp_out=omp_in)
-  type(t1) :: x1
-  integer :: i
-  !$omp parallel do reduction(.mt.:x1)
-  do i = 1, 5
-    x1 = x1 .mt. t1(1)
-  end do
-  !$omp end parallel do
-end program p
diff --git a/flang/test/Lower/OpenMP/Todo/multiple-types-declare_reduction.f90 b/flang/test/Lower/OpenMP/Todo/multiple-types-declare_reduction.f90
deleted file mode 100644
index aa83d7f832c9b..0000000000000
--- a/flang/test/Lower/OpenMP/Todo/multiple-types-declare_reduction.f90
+++ /dev/null
@@ -1,51 +0,0 @@
-! Test OpenMP declare reduction with integer and real types.
-! This test verifies correct lowering of user-defined reductions
-! to HLFIR and their use in OpenMP parallel loops.
-
-! RUN: %flang_fc1 -emit-hlfir -fopenmp %s -o - | FileCheck %s
-
-program main
-  implicit none
-  integer :: i, isum
-  real    :: rsum
-
-  !$omp declare reduction(myred: integer, real : &
-  !$omp  omp_out = omp_out + omp_in) initializer(omp_priv = 0)
-
-  isum = 0
-  rsum = 0.0
-
-  !$omp parallel do reduction(myred:isum)
-  do i = 1, 3
-     isum = isum + i
-  end do
-
-  !$omp parallel do reduction(myred:rsum)
-  do i = 1, 3
-     rsum = rsum + real(i)
-  end do
-
-  print *, isum, rsum
-end program main
-
-! Verify declare reduction is created for integer
-! CHECK-LABEL: omp.declare_reduction @_QQFmyred : i32
-! CHECK: init {
-! CHECK: arith.constant 0 : i32
-! CHECK: omp.yield
-
-! Verify integer combiner uses addi
-! CHECK: combiner {
-! CHECK: arith.addi
-! CHECK: omp.yield
-
-! Verify reduction is used in first parallel loop (integer)
-! CHECK: omp.parallel
-! CHECK: omp.wsloop
-! CHECK-SAME: reduction(@_QQFmyred
-
-! Verify reduction is used in second parallel loop (real)
-! CHECK: omp.parallel
-! CHECK: omp.wsloop
-! CHECK-SAME: reduction(@_QQFmyred
-! CHECK: arith.addf
diff --git a/flang/test/Lower/OpenMP/declare-reduction-character-allocatable.f90 b/flang/test/Lower/OpenMP/declare-reduction-character-allocatable.f90
index e4af5818ecb71..8220452213cc9 100644
--- a/flang/test/Lower/OpenMP/declare-reduction-character-allocatable.f90
+++ b/flang/test/Lower/OpenMP/declare-reduction-character-allocatable.f90
@@ -19,7 +19,7 @@ program test_character_reduction
 end program test_character_reduction
 
 ! Verify the declare_reduction is generated with reference type for character
-! CHECK-LABEL: omp.declare_reduction @_QQFchar_max : !fir.ref<!fir.char<1>>
+! CHECK-LABEL: omp.declare_reduction @_QQFchar_max_byref_c8 : !fir.ref<!fir.char<1>>
 ! CHECK: init {
 ! CHECK: omp.yield
 
@@ -30,4 +30,4 @@ end program test_character_reduction
 
 ! Verify the reduction is used in the parallel sections
 ! CHECK: omp.parallel
-! CHECK:   omp.sections reduction(byref @_QQFchar_max
+! CHECK:   omp.sections reduction(byref @_QQFchar_max_byref_c8
diff --git a/flang/test/Lower/OpenMP/declare-reduction-initializer-component.f90 b/flang/test/Lower/OpenMP/declare-reduction-initializer-component.f90
index 39f1eb6e71f44..afd4ab7db29af 100644
--- a/flang/test/Lower/OpenMP/declare-reduction-initializer-component.f90
+++ b/flang/test/Lower/OpenMP/declare-reduction-initializer-component.f90
@@ -25,7 +25,7 @@ subroutine test_component_init()
   !$omp end parallel do
 end subroutine
 
-!CHECK: omp.declare_reduction @_QQFtest_component_initadd_member : !fir.ref<!fir.type<_QFtest_component_initTt{member:i32}>>
+!CHECK: omp.declare_reduction @_QQFtest_component_initadd_member_byref_rec__QFtest_component_initTt : !fir.ref<!fir.type<_QFtest_component_initTt{member:i32}>>
 !CHECK-SAME: alloc {
 !CHECK:   %[[ALLOCA:.*]] = fir.alloca !fir.type<_QFtest_component_initTt{member:i32}>
 !CHECK:   omp.yield(%[[ALLOCA]] : !fir.ref<!fir.type<_QFtest_component_initTt{member:i32}>>)
diff --git a/flang/test/Lower/OpenMP/declare-reduction-initializer-defined-assign.f90 b/flang/test/Lower/OpenMP/declare-reduction-initializer-defined-assign.f90
index 366d28c47b706..40571beb20b81 100644
--- a/flang/test/Lower/OpenMP/declare-reduction-initializer-defined-assign.f90
+++ b/flang/test/Lower/OpenMP/declare-reduction-initializer-defined-assign.f90
@@ -41,7 +41,7 @@ subroutine test_defined_assign_init()
   !$omp end parallel do
 end subroutine
 
-!CHECK: omp.declare_reduction @_QQFtest_defined_assign_initadd_t :
+!CHECK: omp.declare_reduction @_QQFtest_defined_assign_initadd_t_byref_rec__QMm_defined_assignTt :
 !CHECK-SAME: alloc {
 !CHECK:   %[[ALLOCA:.*]] = fir.alloca
 !CHECK:   omp.yield(%[[ALLOCA]] :
diff --git a/flang/test/Lower/OpenMP/declare-reduction-initializer-rhs-call.f90 b/flang/test/Lower/OpenMP/declare-reduction-initializer-rhs-call.f90
index 988219d65b1a6..e1c67f61dd8a5 100644
--- a/flang/test/Lower/OpenMP/declare-reduction-initializer-rhs-call.f90
+++ b/flang/test/Lower/OpenMP/declare-reduction-initializer-rhs-call.f90
@@ -36,7 +36,7 @@ subroutine test_rhs_call()
   !$omp end parallel do
 end subroutine
 
-!CHECK: omp.declare_reduction @_QQFtest_rhs_calladd_t :
+!CHECK: omp.declare_reduction @_QQFtest_rhs_calladd_t_byref_rec__QMmTt :
 !CHECK-SAME: alloc {
 !CHECK:   %[[ALLOCA:.*]] = fir.alloca !fir.type<_QMmTt{member:i32}>
 !CHECK:   omp.yield(%[[ALLOCA]] :
diff --git a/flang/test/Lower/OpenMP/declare-reduction-logical-init.f90 b/flang/test/Lower/OpenMP/declare-reduction-logical-init.f90
index c8c7bdaa48f13..9720cf8f3d523 100644
--- a/flang/test/Lower/OpenMP/declare-reduction-logical-init.f90
+++ b/flang/test/Lower/OpenMP/declare-reduction-logical-init.f90
@@ -14,7 +14,7 @@ subroutine test_logical(r)
   end do
 end subroutine
 
-! CHECK: omp.declare_reduction @[[RED:_QQFtest_logicalmy_and]] : !fir.logical<4> init {
+! CHECK: omp.declare_reduction @[[RED:_QQFtest_logicalmy_and_l32]] : !fir.logical<4> init {
 ! CHECK: %[[FALSE:.*]] = arith.constant false
 ! CHECK: %[[CONV:.*]] = fir.convert %[[FALSE]] : (i1) -> !fir.logical<4>
 ! CHECK: omp.yield(%[[CONV]] : !fir.logical<4>)
diff --git a/flang/test/Lower/OpenMP/declare-reduction-name-type-token-collision.f90 b/flang/test/Lower/OpenMP/declare-reduction-name-type-token-collision.f90
new file mode 100644
index 0000000000000..70524aa803b1e
--- /dev/null
+++ b/flang/test/Lower/OpenMP/declare-reduction-name-type-token-collision.f90
@@ -0,0 +1,43 @@
+! A single-type user reduction whose name happens to end in a type token
+! (myred_i32) must not collide with a multi-type reduction (myred: integer, real)
+! whose per-type integer op is also spelled with an _i32 suffix. Because the
+! per-type suffix is appended unconditionally, the single-type reduction becomes
+! @_QQFmyred_i32_i32, distinct from the multi-type reduction's @_QQFmyred_i32, so
+! the by-name op dedup cannot silently fold one reduction onto the other. A
+! conditional suffix (bare single-type name) would collide here and silently bind
+! myred_i32's clause to myred's combiner (a miscompile).
+! See https://github.com/llvm/llvm-project/issues/207255.
+
+! RUN: %flang_fc1 -emit-hlfir -fopenmp %s -o - | FileCheck %s
+
+program p
+  integer :: a, b, i
+  !$omp declare reduction(myred: integer, real : omp_out = omp_out + omp_in) &
+  !$omp   initializer(omp_priv = 0)
+  !$omp declare reduction(myred_i32: integer : omp_out = omp_out * omp_in) &
+  !$omp   initializer(omp_priv = 1)
+  a = 0
+  b = 1
+  !$omp parallel do reduction(myred:a)
+  do i = 1, 4
+    a = a + i
+  end do
+  !$omp parallel do reduction(myred_i32:b)
+  do i = 1, 4
+    b = b * i
+  end do
+  print *, a, b
+end program
+
+! Three distinct ops: the multi-type reduction's two per-type ops, and the
+! single-type reduction's own (double-suffixed) op. Under a conditional suffix
+! only two ops would exist and the single-type reduction would silently reuse
+! the multi-type add op; three distinct ops here proves there is no collision.
+! CHECK-DAG: omp.declare_reduction @[[MYRED_I32:_QQFmyred_i32]] : i32
+! CHECK-DAG: omp.declare_reduction @[[MYRED_F32:_QQFmyred_f32]] : f32
+! CHECK-DAG: omp.declare_reduction @[[MYREDI32_I32:_QQFmyred_i32_i32]] : i32
+
+! Each loop binds its own reduction's op, by full name. Loop 2 (myred_i32) binds
+! its own @_QQFmyred_i32_i32, not the multi-type reduction's @_QQFmyred_i32.
+! CHECK-DAG: reduction(@[[MYRED_I32]] %{{[^)]*}}!fir.ref<i32>
+! CHECK-DAG: reduction(@[[MYREDI32_I32]] %{{[^)]*}}!fir.ref<i32>
diff --git a/flang/test/Lower/OpenMP/declare-reduction-named-renamed.f90 b/flang/test/Lower/OpenMP/declare-reduction-named-renamed.f90
new file mode 100644
index 0000000000000..3fe9ae0cb6385
--- /dev/null
+++ b/flang/test/Lower/OpenMP/declare-reduction-named-renamed.f90
@@ -0,0 +1,38 @@
+! A named user-defined declare reduction imported under a rename
+! (myadd_alias => myadd) must lower to the source module's op, not the local
+! alias spelling. The reduction op is named from the module scope and the source
+! ultimate name (myadd), and the use-site clause (which spells the alias) binds
+! that same source op. This is the named analogue of
+! declare-reduction-operator-renamed.f90; the shared owner-qualified naming
+! helper resolves the alias to the source by taking the symbol's ultimate.
+! Reproducer runs to 55 (repro/xmod-named/xmn.f90).
+
+! RUN: %flang_fc1 -emit-hlfir -fopenmp %s -o - | FileCheck %s
+
+module m
+  type :: t
+    integer :: v = 0
+  end type
+  !$omp declare reduction(myadd : t : omp_out%v = omp_out%v + omp_in%v) &
+  !$omp   initializer(omp_priv = t(0))
+end module
+
+program p
+  use m, only : t, myadd_alias => myadd
+  type(t) :: x
+  integer :: i
+  x = t(0)
+  !$omp parallel do reduction(myadd_alias : x)
+  do i = 1, 10
+    x%v = x%v + i
+  end do
+  print *, x%v   ! expected 55
+end program
+
+! The op is named from the module owner (_QQMm) and the source name (myadd),
+! never the alias (myadd_alias). The clause spelled with the alias binds the
+! same source op.
+! CHECK: omp.declare_reduction @[[RED:_QQMmmyadd_byref_rec__QMmTt]] : !fir.ref<!fir.type<{{[^>]*}}t{v:i32}>>
+! CHECK-NOT: myadd_alias
+! CHECK: omp.wsloop
+! CHECK-SAME: reduction(byref @[[RED]]
diff --git a/flang/test/Lower/OpenMP/declare-reduction-named-use-assoc.f90 b/flang/test/Lower/OpenMP/declare-reduction-named-use-assoc.f90
new file mode 100644
index 0000000000000..844fa5a732977
--- /dev/null
+++ b/flang/test/Lower/OpenMP/declare-reduction-named-use-assoc.f90
@@ -0,0 +1,37 @@
+! A named user-defined declare reduction declared in a module and used in a
+! separate program unit must lower correctly cross-scope: the reduction op is
+! named from the module scope (owner-qualified) and the use-site clause binds
+! that same op. The materializer (materializeOpenMPDeclareReductions) emits the
+! imported op for separate compilation; here the module and program share a file,
+! but the naming contract is the same. Single-type reductions are named without a
+! type suffix, so this locks the un-suffixed cross-scope spelling.
+! https://github.com/llvm/llvm-project/issues/207255
+
+! RUN: %flang_fc1 -emit-hlfir -fopenmp %s -o - | FileCheck %s
+
+module m
+  type :: t
+    integer :: v = 0
+  end type
+  !$omp declare reduction(myadd: t: omp_out%v = omp_out%v + omp_in%v) &
+  !$omp   initializer(omp_priv=t(0))
+end module
+
+program main
+  use m
+  type(t) :: x
+  integer :: i
+  x = t(0)
+  !$omp parallel do reduction(myadd:x)
+  do i = 1, 5
+    x%v = x%v + i
+  end do
+  print *, x%v
+end program
+
+! The op is owner-qualified to the module scope (_QQMm...) and carries the
+! type/byref suffix. The use-site clause binds the same op.
+! CHECK: omp.declare_reduction @[[RED:_QQMmmyadd_byref_rec__QMmTt]] : !fir.ref<!fir.type<{{[^>]*}}t{v:i32}>>
+! CHECK-NOT: omp.declare_reduction @{{.*}}myadd{{.*}}myadd
+! CHECK: omp.wsloop
+! CHECK-SAME: reduction(byref @[[RED]]
diff --git a/flang/test/Lower/OpenMP/declare-reduction-no-initializer-intrinsic.f90 b/flang/test/Lower/OpenMP/declare-reduction-no-initializer-intrinsic.f90
index cb768d3b92744..5bb0a57b0dedd 100644
--- a/flang/test/Lower/OpenMP/declare-reduction-no-initializer-intrinsic.f90
+++ b/flang/test/Lower/OpenMP/declare-reduction-no-initializer-intrinsic.f90
@@ -3,7 +3,7 @@
 ! Test declare reduction without initializer clause for intrinsic types.
 ! Without an initializer, the private variable should be zero-initialized.
 
-! CHECK-DAG: omp.declare_reduction @_QQFchar_max : !fir.ref<!fir.char<1,10>>
+! CHECK-DAG: omp.declare_reduction @_QQFchar_max_byref_c8x10 : !fir.ref<!fir.char<1,10>>
 ! CHECK:       init {
 ! CHECK:       %[[CHZERO:.*]] = fir.zero_bits !fir.char<1,10>
 ! CHECK:       fir.store %[[CHZERO]]
@@ -72,7 +72,7 @@ program test_no_init_intrinsic
   !$omp end parallel do
 
   ! Test fixed-length character reduction without initializer
-  ! CHECK: omp.wsloop {{.*}} reduction(byref @_QQFchar_max
+  ! CHECK: omp.wsloop {{.*}} reduction(byref @_QQFchar_max_byref_c8x10
   !$omp parallel do reduction(char_max: s)
   do i = 1, 10
     continue
diff --git a/flang/test/Lower/OpenMP/declare-reduction-operator-collide.f90 b/flang/test/Lower/OpenMP/declare-reduction-operator-collide.f90
index 45ac1ba7ee495..899689174faa8 100644
--- a/flang/test/Lower/OpenMP/declare-reduction-operator-collide.f90
+++ b/flang/test/Lower/OpenMP/declare-reduction-operator-collide.f90
@@ -71,8 +71,8 @@ program main
 ! Two distinct module-scoped ops (mod_add vs mod_mul owner qualifier), each bound
 ! by its own clause (the addop loop is emitted first, then the mulop loop).
 ! loose captures (R1) keyed on the owning module name; the two qualifiers differ.
-! CHECK-DAG: omp.declare_reduction @[[REDADD:_QQ[A-Za-z0-9_.]*mod_add[A-Za-z0-9_.]*op\.remote\.]] : !fir.ref
-! CHECK-DAG: omp.declare_reduction @[[REDMUL:_QQ[A-Za-z0-9_.]*mod_mul[A-Za-z0-9_.]*op\.remote\.]] : !fir.ref
+! CHECK-DAG: omp.declare_reduction @[[REDADD:_QQ[A-Za-z0-9_.]*mod_add[A-Za-z0-9_.]*op\.remote\.[A-Za-z0-9_.]*]] : !fir.ref
+! CHECK-DAG: omp.declare_reduction @[[REDMUL:_QQ[A-Za-z0-9_.]*mod_mul[A-Za-z0-9_.]*op\.remote\.[A-Za-z0-9_.]*]] : !fir.ref
 ! CHECK: omp.wsloop
 ! CHECK-SAME: reduction(byref @[[REDADD]]
 ! CHECK: omp.wsloop
diff --git a/flang/test/Lower/OpenMP/declare-reduction-operator-derived.f90 b/flang/test/Lower/OpenMP/declare-reduction-operator-derived.f90
index 62b30cb5d4542..4a40af8aac629 100644
--- a/flang/test/Lower/OpenMP/declare-reduction-operator-derived.f90
+++ b/flang/test/Lower/OpenMP/declare-reduction-operator-derived.f90
@@ -30,7 +30,7 @@ end subroutine red_derived
 
 ! The op name must be module-scoped (a mangled "_QQ..." generated name), NOT the
 ! bare operator spelling. The directive and clause must reference the same name.
-! CHECK: omp.declare_reduction @[[RED:_QQ[A-Za-z0-9_.]*op\.myop\.]] : !fir.ref
+! CHECK: omp.declare_reduction @[[RED:_QQ[A-Za-z0-9_.]*op\.myop\.[A-Za-z0-9_.]*]] : !fir.ref
 ! CHECK-NOT: omp.declare_reduction @op.myop.
 ! CHECK: omp.wsloop
 ! CHECK-SAME: reduction(byref @[[RED]]
diff --git a/flang/test/Lower/OpenMP/declare-reduction-operator-merged.f90 b/flang/test/Lower/OpenMP/declare-reduction-operator-merged.f90
index 2ef1c2d6cc231..93f148fda8b69 100644
--- a/flang/test/Lower/OpenMP/declare-reduction-operator-merged.f90
+++ b/flang/test/Lower/OpenMP/declare-reduction-operator-merged.f90
@@ -5,8 +5,9 @@
 ! merged generic; each reduction clause is disambiguated by the variable's type
 ! back to its own source module's reduction op (named from the source spelling
 ! op.remote., not the use-site rename op.local.). Each source declare is
-! single-type. Lowering counterpart of the semantics test
-! Semantics/OpenMP/declare-reduction-use-only-merged.f90.
+! single-type; the multi-type-in-one-declare case (Form A) is covered by
+! declare-reduction-operator-multiple-types.f90. Lowering counterpart of the
+! semantics test Semantics/OpenMP/declare-reduction-use-only-merged.f90.
 ! https://github.com/llvm/llvm-project/issues/207255
 
 ! RUN: %flang_fc1 -emit-hlfir -fopenmp %s -o - | FileCheck %s
@@ -67,8 +68,8 @@ program main
 ! t_real loop. Because the CHECK-DAG names require op.remote., an op wrongly keyed
 ! on the use-site rename op.local. would fail these checks.
 ! loose captures (R1) keyed on the owning module name; the two qualifiers differ.
-! CHECK-DAG: omp.declare_reduction @[[REDINT:_QQ[A-Za-z0-9_.]*m_int[A-Za-z0-9_.]*op\.remote\.]] : !fir.ref
-! CHECK-DAG: omp.declare_reduction @[[REDREAL:_QQ[A-Za-z0-9_.]*m_real[A-Za-z0-9_.]*op\.remote\.]] : !fir.ref
+! CHECK-DAG: omp.declare_reduction @[[REDINT:_QQ[A-Za-z0-9_.]*m_int[A-Za-z0-9_.]*op\.remote\.[A-Za-z0-9_.]*]] : !fir.ref
+! CHECK-DAG: omp.declare_reduction @[[REDREAL:_QQ[A-Za-z0-9_.]*m_real[A-Za-z0-9_.]*op\.remote\.[A-Za-z0-9_.]*]] : !fir.ref
 ! CHECK: omp.wsloop
 ! CHECK-SAME: reduction(byref @[[REDINT]]
 ! CHECK: omp.wsloop
diff --git a/flang/test/Lower/OpenMP/declare-reduction-operator-multiple-types.f90 b/flang/test/Lower/OpenMP/declare-reduction-operator-multiple-types.f90
new file mode 100644
index 0000000000000..142b218b3da40
--- /dev/null
+++ b/flang/test/Lower/OpenMP/declare-reduction-operator-multiple-types.f90
@@ -0,0 +1,66 @@
+! A user-defined operator declare reduction that lists several types in a single
+! declaration (Form A: `declare reduction(.op.:t1,t2:...)`) must lower to one
+! omp.declare_reduction op per listed type, each with its own element type,
+! combiner and initializer. Folding every type onto the first type's op is a
+! silent miscompile (a real reduction lowered through an integer combiner). See
+! https://github.com/llvm/llvm-project/issues/207255.
+!
+! t1 is integer-valued and t2 is real-valued, so a fold onto one op is glaring:
+! the two ops must carry distinct element types (i32 vs f32) and each loop must
+! bind its own type's op.
+
+! RUN: %flang_fc1 -emit-hlfir -fopenmp %s -o - | FileCheck %s
+
+module m
+  type :: t1
+    integer :: v = 0
+  end type
+  type :: t2
+    real :: v = 0.0
+  end type
+  interface operator(.op.)
+    module procedure a1, a2
+  end interface
+  !$omp declare reduction(.op.:t1,t2:omp_out%v=omp_out%v+omp_in%v)
+contains
+  type(t1) function a1(a,b)
+    type(t1), intent(in) :: a,b
+    a1%v=a%v+b%v
+  end function
+  type(t2) function a2(a,b)
+    type(t2), intent(in) :: a,b
+    a2%v=a%v+b%v
+  end function
+end module
+
+program main
+  use m
+  type(t1) :: x
+  type(t2) :: y
+  integer :: i
+  x = t1(0)
+  !$omp parallel do reduction(.op.:x)
+  do i=1,5
+    x%v = x%v + i
+  end do
+  y = t2(0.0)
+  !$omp parallel do reduction(.op.:y)
+  do i=1,4
+    y%v = y%v + real(i)
+  end do
+  print *, x%v, y%v
+end program
+
+! One distinct op per listed type, each with its own element type. The op name
+! carries the owning scope (mangled "_QQ...") plus a per-type suffix, so the two
+! types get two ops rather than colliding.
+! CHECK-DAG: omp.declare_reduction @[[REDT1:_QQ[A-Za-z0-9_.]*op\.op\.[A-Za-z0-9_.]*t1]] : !fir.ref<!fir.type<{{[^>]*}}t1{v:i32}>>
+! CHECK-DAG: omp.declare_reduction @[[REDT2:_QQ[A-Za-z0-9_.]*op\.op\.[A-Za-z0-9_.]*t2]] : !fir.ref<!fir.type<{{[^>]*}}t2{v:f32}>>
+
+! The bare operator spelling must never be a declare_reduction name (it would
+! collide across scopes/types).
+! CHECK-NOT: omp.declare_reduction @op.op.
+
+! Each parallel loop binds the op for its own variable's type.
+! CHECK-DAG: reduction(byref @[[REDT1]] {{[^)]*}}t1{v:i32}
+! CHECK-DAG: reduction(byref @[[REDT2]] {{[^)]*}}t2{v:f32}
diff --git a/flang/test/Lower/OpenMP/declare-reduction-operator-renamed.f90 b/flang/test/Lower/OpenMP/declare-reduction-operator-renamed.f90
index 3bc153bba84a3..57cb88dfda917 100644
--- a/flang/test/Lower/OpenMP/declare-reduction-operator-renamed.f90
+++ b/flang/test/Lower/OpenMP/declare-reduction-operator-renamed.f90
@@ -39,7 +39,7 @@ program main
 ! rename op.local. must never appear as an op name.
 ! loose capture (R1): the exact mangled qualifier is pinned after building.
 ! CHECK-NOT: op.local.
-! CHECK: omp.declare_reduction @[[RED:_QQ[A-Za-z0-9_.]*op\.remote\.]] : !fir.ref
+! CHECK: omp.declare_reduction @[[RED:_QQ[A-Za-z0-9_.]*op\.remote\.[A-Za-z0-9_.]*]] : !fir.ref
 ! CHECK: omp.wsloop
 ! CHECK-SAME: reduction(byref @[[RED]]
 ! CHECK-NOT: op.local.
diff --git a/flang/test/Lower/OpenMP/declare-reduction-operator-separate-combiners.f90 b/flang/test/Lower/OpenMP/declare-reduction-operator-separate-combiners.f90
index 14454701204e7..a698f97ce631a 100644
--- a/flang/test/Lower/OpenMP/declare-reduction-operator-separate-combiners.f90
+++ b/flang/test/Lower/OpenMP/declare-reduction-operator-separate-combiners.f90
@@ -42,7 +42,7 @@ subroutine combine_sub(x, y)
 
 !--- call.use.f90
 ! The combiner region must lower to a call of the imported subroutine.
-! CHECK: omp.declare_reduction @[[RED:_QQ[A-Za-z0-9_.]*op\.remote\.]] : !fir.ref
+! CHECK: omp.declare_reduction @[[RED:_QQ[A-Za-z0-9_.]*op\.remote\.[A-Za-z0-9_.]*]] : !fir.ref
 ! CHECK: combiner
 ! CHECK: fir.call @_QMred_callPcombine_sub
 ! CHECK: omp.wsloop
@@ -87,7 +87,7 @@ subroutine assign_t(lhs, rhs)
 
 !--- asgn.use.f90
 ! The combiner region must lower a call to the imported defined assignment.
-! CHECK: omp.declare_reduction @[[RED:_QQ[A-Za-z0-9_.]*op\.remote\.]] : !fir.ref
+! CHECK: omp.declare_reduction @[[RED:_QQ[A-Za-z0-9_.]*op\.remote\.[A-Za-z0-9_.]*]] : !fir.ref
 ! CHECK: combiner
 ! CHECK: fir.call @_QMred_asgnPassign_t
 ! CHECK: omp.wsloop
diff --git a/flang/test/Lower/OpenMP/declare-reduction-operator-separate-hermetic.f90 b/flang/test/Lower/OpenMP/declare-reduction-operator-separate-hermetic.f90
index ffe61e27d1e34..856e52fca6a70 100644
--- a/flang/test/Lower/OpenMP/declare-reduction-operator-separate-hermetic.f90
+++ b/flang/test/Lower/OpenMP/declare-reduction-operator-separate-hermetic.f90
@@ -39,7 +39,7 @@ module herm_wrap
 !--- herm.use.f90
 ! The reduction lives in the embedded herm_base; materialization must reach it
 ! through the hermetic herm_wrap.mod and name it from its source module.
-! CHECK: omp.declare_reduction @[[RED:_QQ[A-Za-z0-9_.]*base[A-Za-z0-9_.]*op\.remote\.]] : !fir.ref
+! CHECK: omp.declare_reduction @[[RED:_QQ[A-Za-z0-9_.]*base[A-Za-z0-9_.]*op\.remote\.[A-Za-z0-9_.]*]] : !fir.ref
 ! CHECK: omp.wsloop
 ! CHECK-SAME: reduction(byref @[[RED]]
 ! CHECK-NOT: not yet implemented
diff --git a/flang/test/Lower/OpenMP/declare-reduction-operator-separate-reexport.f90 b/flang/test/Lower/OpenMP/declare-reduction-operator-separate-reexport.f90
index 6e1019863e09b..2dfd5b6451c05 100644
--- a/flang/test/Lower/OpenMP/declare-reduction-operator-separate-reexport.f90
+++ b/flang/test/Lower/OpenMP/declare-reduction-operator-separate-reexport.f90
@@ -45,7 +45,7 @@ module rx_wrap
 end module
 
 !--- rx.use.f90
-! CHECK: omp.declare_reduction @[[RED:_QQ[A-Za-z0-9_.]*base[A-Za-z0-9_.]*op\.remote\.]] : !fir.ref
+! CHECK: omp.declare_reduction @[[RED:_QQMrx_baseop\.remote\._byref_rec__QMrx_baseTt]] : !fir.ref
 ! CHECK: omp.wsloop
 ! CHECK-SAME: reduction(byref @[[RED]]
 ! CHECK-NOT: not yet implemented
@@ -76,7 +76,7 @@ module nm_wrap
 end module
 
 !--- nm.use.f90
-! CHECK: omp.declare_reduction @[[RED:_QQ[A-Za-z0-9_.]*nm_base[A-Za-z0-9_.]*myred]] : !fir.ref
+! CHECK: omp.declare_reduction @[[RED:_QQMnm_basemyred_byref_rec__QMnm_baseTt]] : !fir.ref
 ! CHECK: omp.wsloop
 ! CHECK-SAME: reduction(byref @[[RED]]
 ! CHECK-NOT: not yet implemented
diff --git a/flang/test/Lower/OpenMP/declare-reduction-operator-separate.f90 b/flang/test/Lower/OpenMP/declare-reduction-operator-separate.f90
index 6864c8249d8b6..38665f864fa60 100644
--- a/flang/test/Lower/OpenMP/declare-reduction-operator-separate.f90
+++ b/flang/test/Lower/OpenMP/declare-reduction-operator-separate.f90
@@ -49,7 +49,7 @@ type(t) function add_t(a, b)
 end module
 
 !--- plain.use.f90
-! CHECK: omp.declare_reduction @[[RED1:_QQ[A-Za-z0-9_.]*op\.plus\.]] : !fir.ref
+! CHECK: omp.declare_reduction @[[RED1:_QQ[A-Za-z0-9_.]*op\.plus\.[A-Za-z0-9_.]*]] : !fir.ref
 ! CHECK: omp.wsloop
 ! CHECK-SAME: reduction(byref @[[RED1]]
 ! CHECK-NOT: not yet implemented
@@ -83,7 +83,7 @@ type(t) function add_t(a, b)
 end module
 
 !--- rename.use.f90
-! CHECK: omp.declare_reduction @[[RED2:_QQ[A-Za-z0-9_.]*op\.remote\.]] : !fir.ref
+! CHECK: omp.declare_reduction @[[RED2:_QQ[A-Za-z0-9_.]*op\.remote\.[A-Za-z0-9_.]*]] : !fir.ref
 ! CHECK: omp.wsloop
 ! CHECK-SAME: reduction(byref @[[RED2]]
 ! CHECK-NOT: not yet implemented
@@ -139,8 +139,8 @@ type(t) function mul_t(a, b)
 !--- collide.use.f90
 ! The two ops are distinguished by their source module (addmod vs mulmod) in the
 ! mangled name, so pinning one op to each module proves they are distinct.
-! CHECK-DAG: omp.declare_reduction @{{_QQ[A-Za-z0-9_.]*addmod[A-Za-z0-9_.]*op\.remote\.}} : !fir.ref
-! CHECK-DAG: omp.declare_reduction @{{_QQ[A-Za-z0-9_.]*mulmod[A-Za-z0-9_.]*op\.remote\.}} : !fir.ref
+! CHECK-DAG: omp.declare_reduction @{{_QQ[A-Za-z0-9_.]*addmod[A-Za-z0-9_.]*op\.remote\.[A-Za-z0-9_.]*}} : !fir.ref
+! CHECK-DAG: omp.declare_reduction @{{_QQ[A-Za-z0-9_.]*mulmod[A-Za-z0-9_.]*op\.remote\.[A-Za-z0-9_.]*}} : !fir.ref
 ! CHECK-NOT: not yet implemented
 program main
   use red_ty, only: t
diff --git a/flang/test/Lower/OpenMP/declare-reduction-operator-use-assoc.f90 b/flang/test/Lower/OpenMP/declare-reduction-operator-use-assoc.f90
index 78f370518e0c1..e34d02d057083 100644
--- a/flang/test/Lower/OpenMP/declare-reduction-operator-use-assoc.f90
+++ b/flang/test/Lower/OpenMP/declare-reduction-operator-use-assoc.f90
@@ -40,7 +40,7 @@ program main
 ! "_QQ...op.plus.", not the bare use-site spelling), and the wsloop clause binds
 ! that same op.
 ! loose captures (R1): the exact mangled qualifier is pinned after building.
-! CHECK: omp.declare_reduction @[[RED:_QQ[A-Za-z0-9_.]*op\.plus\.]] : !fir.ref
+! CHECK: omp.declare_reduction @[[RED:_QQ[A-Za-z0-9_.]*op\.plus\.[A-Za-z0-9_.]*]] : !fir.ref
 ! CHECK-NOT: omp.declare_reduction @op.plus.
 ! CHECK: omp.wsloop
 ! CHECK-SAME: reduction(byref @[[RED]]
diff --git a/flang/test/Lower/OpenMP/declare-reduction-operator.f90 b/flang/test/Lower/OpenMP/declare-reduction-operator.f90
index 11897212078eb..1f18693bc399b 100644
--- a/flang/test/Lower/OpenMP/declare-reduction-operator.f90
+++ b/flang/test/Lower/OpenMP/declare-reduction-operator.f90
@@ -25,7 +25,7 @@ end subroutine red_integer
 ! The op name must be module-scoped (a mangled "_QQ..." generated name), NOT the
 ! bare operator spelling, so reductions with the same spelling in different
 ! modules do not collide. The directive and clause must reference the same name.
-! CHECK: omp.declare_reduction @[[RED:_QQ[A-Za-z0-9_.]*op\.zzzz\.]] : i32
+! CHECK: omp.declare_reduction @[[RED:_QQ[A-Za-z0-9_.]*op\.zzzz\.[A-Za-z0-9_.]*]] : i32
 ! CHECK-NOT: omp.declare_reduction @op.zzzz.
 ! CHECK: omp.parallel
 ! CHECK-SAME: reduction(@[[RED]]
diff --git a/flang/test/Lower/OpenMP/declare-reduction-same-name-different-scope.f90 b/flang/test/Lower/OpenMP/declare-reduction-same-name-different-scope.f90
index 066758ae746e9..84b5f65f658f6 100644
--- a/flang/test/Lower/OpenMP/declare-reduction-same-name-different-scope.f90
+++ b/flang/test/Lower/OpenMP/declare-reduction-same-name-different-scope.f90
@@ -23,11 +23,11 @@ subroutine test
   end subroutine test
 end module m
 
-! CHECK: omp.declare_reduction @[[TEST_RED:_QQMmFtesta]] : i32 init {
+! CHECK: omp.declare_reduction @[[TEST_RED:_QQMmFtesta_i32]] : i32 init {
 ! CHECK: %[[C0:.*]] = arith.constant 0 : i32
 ! CHECK: omp.yield(%[[C0]] : i32)
 
-! CHECK: omp.declare_reduction @[[DUMMY_RED:_QQMmFdummya]] : i32 init {
+! CHECK: omp.declare_reduction @[[DUMMY_RED:_QQMmFdummya_i32]] : i32 init {
 ! CHECK: %[[C10000:.*]] = arith.constant 10000 : i32
 ! CHECK: omp.yield(%[[C10000]] : i32)
 
diff --git a/flang/test/Lower/OpenMP/declare-reduction-shadows-intrinsic-multiple-types.f90 b/flang/test/Lower/OpenMP/declare-reduction-shadows-intrinsic-multiple-types.f90
new file mode 100644
index 0000000000000..aa346cf8eb261
--- /dev/null
+++ b/flang/test/Lower/OpenMP/declare-reduction-shadows-intrinsic-multiple-types.f90
@@ -0,0 +1,45 @@
+! A user-defined declare reduction whose identifier shadows an intrinsic
+! reduction name (max/min/iand/ior/ieor) and lists several types must lower to
+! one omp.declare_reduction op per listed type, and each reduction clause must
+! bind the op for its variable's type (rather than falling back to the built-in
+! intrinsic reduction). This is the intrinsic-shadowing counterpart of the
+! operator and named multi-type paths. Single-type shadowing is handled by
+! declare-reduction-shadows-intrinsic.f90; here the declaration lists both
+! integer and real. See https://github.com/llvm/llvm-project/issues/207255.
+
+! RUN: %flang_fc1 -emit-hlfir -fopenmp %s -o - | FileCheck %s
+
+program p
+  integer :: i, isum
+  real :: rsum
+
+  !$omp declare reduction(max: integer, real : omp_out = max(omp_out, omp_in)) &
+  !$omp   initializer(omp_priv = omp_orig)
+
+  isum = 0
+  rsum = 0.0
+
+  !$omp parallel do reduction(max: isum)
+  do i = 1, 5
+    isum = max(isum, i)
+  end do
+
+  !$omp parallel do reduction(max: rsum)
+  do i = 1, 5
+    rsum = max(rsum, real(i))
+  end do
+
+  print *, isum, rsum
+end program
+
+! The shadowing user reduction is named "op.max" (MangleSpecialFunctions), and
+! being multi-type it gets one type-suffixed op per listed type. There must be
+! no single type-less op that both types fold onto.
+! CHECK-NOT: omp.declare_reduction @_QQFop.max :
+! CHECK-DAG: omp.declare_reduction @[[MAXI:_QQFop.max_i32]] : i32
+! CHECK-DAG: omp.declare_reduction @[[MAXR:_QQFop.max_f32]] : f32
+
+! Each loop binds the op for its own variable's type, by full name (not the
+! shared @_QQFop.max_ prefix, and not the built-in max reduction).
+! CHECK-DAG: reduction(@[[MAXI]] {{[^)]*}}!fir.ref<i32>
+! CHECK-DAG: reduction(@[[MAXR]] {{[^)]*}}!fir.ref<f32>
diff --git a/flang/test/Lower/OpenMP/declare-reduction-shadows-intrinsic-use-assoc.f90 b/flang/test/Lower/OpenMP/declare-reduction-shadows-intrinsic-use-assoc.f90
new file mode 100644
index 0000000000000..b39219f25e413
--- /dev/null
+++ b/flang/test/Lower/OpenMP/declare-reduction-shadows-intrinsic-use-assoc.f90
@@ -0,0 +1,37 @@
+! A user-defined declare reduction whose name shadows an intrinsic
+! (max/min/iand/ior/ieor), declared in a module and used through USE
+! association, must lower cross-scope like the named path: the op is named from
+! the module scope and the source ultimate (op.max), and the use-site clause
+! binds that same op. This used to be a clean TODO ("not yet supported for
+! imported or renamed reductions"); the imported case is now handled by naming
+! from the found symbol's ultimate, and the renamed case is rejected by
+! semantics before lowering (the renamed name resolves to the intrinsic, not the
+! user reduction). Reproducer runs to 10 (repro/xmod-shadow/xsh.f90).
+
+! RUN: %flang_fc1 -emit-hlfir -fopenmp %s -o - | FileCheck %s
+
+module m
+  type :: t
+    integer :: v = 0
+  end type
+  !$omp declare reduction(max : t : omp_out%v = max(omp_out%v, omp_in%v)) &
+  !$omp   initializer(omp_priv = t(-2147483647))
+end module
+
+program p
+  use m
+  type(t) :: x
+  integer :: i
+  x = t(-2147483647)
+  !$omp parallel do reduction(max : x)
+  do i = 1, 10
+    x%v = max(x%v, i)
+  end do
+  print *, x%v   ! expected 10
+end program
+
+! The op is owner-qualified to the module (_QQMm) and carries the mangled
+! shadowing name (op.max). The USE-associated clause binds the same op.
+! CHECK: omp.declare_reduction @[[RED:_QQMmop.max_byref_rec__QMmTt]] : !fir.ref<!fir.type<{{[^>]*}}t{v:i32}>>
+! CHECK: omp.wsloop
+! CHECK-SAME: reduction(byref @[[RED]]
diff --git a/flang/test/Lower/OpenMP/declare-reduction-shadows-intrinsic.f90 b/flang/test/Lower/OpenMP/declare-reduction-shadows-intrinsic.f90
index 212909c242222..509a96343ceb4 100644
--- a/flang/test/Lower/OpenMP/declare-reduction-shadows-intrinsic.f90
+++ b/flang/test/Lower/OpenMP/declare-reduction-shadows-intrinsic.f90
@@ -20,7 +20,7 @@ subroutine test_max(x)
 ! @max_reduction / @max_i32 op should be generated.
 
 ! CHECK-NOT: omp.declare_reduction @max
-! CHECK: omp.declare_reduction @[[RED:_QQFtest_maxop.max]] : i32 init {
+! CHECK: omp.declare_reduction @[[RED:_QQFtest_maxop\.max_i32]] : i32 init {
 ! CHECK: combiner {
 ! CHECK: arith.addi
 ! CHECK: omp.yield
diff --git a/flang/test/Lower/OpenMP/declare-reduction-target-intrinsic.f90 b/flang/test/Lower/OpenMP/declare-reduction-target-intrinsic.f90
index 43caea5e2dfd4..d7f3d20a7a04a 100644
--- a/flang/test/Lower/OpenMP/declare-reduction-target-intrinsic.f90
+++ b/flang/test/Lower/OpenMP/declare-reduction-target-intrinsic.f90
@@ -4,21 +4,21 @@
 ! These should generate inline constant initialization (no runtime calls),
 ! so they work on GPU targets without requiring the device Fortran runtime.
 
-! CHECK-LABEL: omp.declare_reduction @_QQFaddc : complex<f32> init {
+! CHECK-LABEL: omp.declare_reduction @_QQFaddc_z32 : complex<f32> init {
 ! CHECK:         %[[CZERO:.*]] = fir.zero_bits complex<f32>
 ! CHECK:         omp.yield(%[[CZERO]] : complex<f32>)
 ! CHECK:       } combiner {
 ! CHECK:         fir.addc
 ! CHECK:       }
 
-! CHECK-LABEL: omp.declare_reduction @_QQFaddr : f32 init {
+! CHECK-LABEL: omp.declare_reduction @_QQFaddr_f32 : f32 init {
 ! CHECK:         %[[FZERO:.*]] = fir.zero_bits f32
 ! CHECK:         omp.yield(%[[FZERO]] : f32)
 ! CHECK:       } combiner {
 ! CHECK:         arith.addf
 ! CHECK:       }
 
-! CHECK-LABEL: omp.declare_reduction @_QQFaddi : i32 init {
+! CHECK-LABEL: omp.declare_reduction @_QQFaddi_i32 : i32 init {
 ! CHECK:         %[[IZERO:.*]] = fir.zero_bits i32
 ! CHECK:         omp.yield(%[[IZERO]] : i32)
 ! CHECK:       } combiner {
@@ -26,16 +26,16 @@
 ! CHECK:       }
 
 ! CHECK: omp.target
-! CHECK:   omp.teams reduction(@_QQFaddi
-! CHECK:     omp.wsloop reduction(@_QQFaddi
+! CHECK:   omp.teams reduction(@_QQFaddi_i32
+! CHECK:     omp.wsloop reduction(@_QQFaddi_i32
 
 ! CHECK: omp.target
-! CHECK:   omp.teams reduction(@_QQFaddr
-! CHECK:     omp.wsloop reduction(@_QQFaddr
+! CHECK:   omp.teams reduction(@_QQFaddr_f32
+! CHECK:     omp.wsloop reduction(@_QQFaddr_f32
 
 ! CHECK: omp.target
-! CHECK:   omp.teams reduction(@_QQFaddc
-! CHECK:     omp.wsloop reduction(@_QQFaddc
+! CHECK:   omp.teams reduction(@_QQFaddc_z32
+! CHECK:     omp.wsloop reduction(@_QQFaddc_z32
 
 program test_target_named_reduction
   implicit none
diff --git a/flang/test/Lower/OpenMP/multiple-types-declare_reduction.f90 b/flang/test/Lower/OpenMP/multiple-types-declare_reduction.f90
new file mode 100644
index 0000000000000..3d614e9d07027
--- /dev/null
+++ b/flang/test/Lower/OpenMP/multiple-types-declare_reduction.f90
@@ -0,0 +1,55 @@
+! A named user-defined declare reduction that lists several types in a single
+! declaration (declare reduction(myred: integer, real: ...)) must lower to one
+! omp.declare_reduction op per listed type, each with its own element type and
+! combiner, and each reduction clause must bind the op for its variable's type.
+! Folding the whole declaration onto a single type-less op would bind a real
+! reduction to an integer-typed op (an !fir.ref<f32> reduction on an i32 op, an
+! IR type mismatch the verifier does not catch) and silently miscompile. See https://github.com/llvm/llvm-project/issues/207255.
+!
+! This test uses a strong oracle: it pins the full type-suffixed op names, checks
+! each op's combiner arithmetic inside the combiner region (so a matching
+! arith.addf/addi in the loop body cannot satisfy it), and binds each loop to the
+! full type-specific name (the shared @_QQFmyred_ prefix must not let a substring
+! match accept the wrong op).
+
+! RUN: %flang_fc1 -emit-hlfir -fopenmp %s -o - | FileCheck %s
+
+program main
+  integer :: i, isum
+  real    :: rsum
+
+  !$omp declare reduction(myred: integer, real : omp_out = omp_out + omp_in)
+
+  isum = 0
+  rsum = 0.0
+
+  !$omp parallel do reduction(myred:isum)
+  do i = 1, 3
+     isum = isum + i
+  end do
+
+  !$omp parallel do reduction(myred:rsum)
+  do i = 1, 3
+     rsum = rsum + real(i)
+  end do
+
+  print *, isum, rsum
+end program main
+
+! There must be no single type-less op that both types fold onto.
+! CHECK-NOT: omp.declare_reduction @_QQFmyred :
+
+! One op per listed type, each with its own combiner arithmetic anchored inside
+! the combiner region (the real op adds f32, the integer op adds i32).
+! CHECK-LABEL: omp.declare_reduction @_QQFmyred_f32 : f32
+! CHECK: combiner {
+! CHECK: arith.addf
+! CHECK-LABEL: omp.declare_reduction @_QQFmyred_i32 : i32
+! CHECK: combiner {
+! CHECK: arith.addi
+
+! Each loop binds the op for its own variable's type, by full name.
+! CHECK: omp.wsloop
+! CHECK-SAME: reduction(@_QQFmyred_i32
+! CHECK: omp.wsloop
+! CHECK-SAME: reduction(@_QQFmyred_f32
diff --git a/flang/test/Lower/OpenMP/omp-declare-reduction-combsub.f90 b/flang/test/Lower/OpenMP/omp-declare-reduction-combsub.f90
index ae1eb9747bd40..ad7fe60f138c8 100644
--- a/flang/test/Lower/OpenMP/omp-declare-reduction-combsub.f90
+++ b/flang/test/Lower/OpenMP/omp-declare-reduction-combsub.f90
@@ -17,7 +17,7 @@ subroutine combine_me(out, in)
        integer out, in
      end subroutine combine_me
   end interface
-!CHECK:  omp.declare_reduction @_QQFfuncred_add : i32 init {
+!CHECK:  omp.declare_reduction @_QQFfuncred_add_i32 : i32 init {
 !CHECK: ^bb0(%[[OMP_ORIG_ARG_I:.*]]: i32):
 !CHECK:    %[[OMP_PRIV:.*]] = fir.alloca i32
 !CHECK:    %[[OMP_ORIG:.*]] = fir.alloca i32
@@ -57,4 +57,3 @@ end subroutine combine_me
   enddo
   func=res
 end function func
-
diff --git a/flang/test/Lower/OpenMP/omp-declare-reduction-derivedtype.f90 b/flang/test/Lower/OpenMP/omp-declare-reduction-derivedtype.f90
index b92f42e2e25de..34412f87ac569 100644
--- a/flang/test/Lower/OpenMP/omp-declare-reduction-derivedtype.f90
+++ b/flang/test/Lower/OpenMP/omp-declare-reduction-derivedtype.f90
@@ -41,7 +41,7 @@ function func(x, n, init)
   end function func
 
 end module maxtype_mod
-!CHECK:  omp.declare_reduction @_QQMmaxtype_modFfuncred_add_max : !fir.ref<[[MAXTYPE:.*]]> {{.*}} alloc {
+!CHECK:  omp.declare_reduction @_QQMmaxtype_modFfuncred_add_max_byref_rec__QMmaxtype_modTmaxtype : !fir.ref<[[MAXTYPE:.*]]> {{.*}} alloc {
 !CHECK:  %[[ALLOCA:.*]] = fir.alloca [[MAXTYPE:.*]]
 !CHECK:  omp.yield(%[[ALLOCA]] : !fir.ref<[[MAXTYPE]]>)
 !CHECK:  } init {
diff --git a/flang/test/Lower/OpenMP/omp-declare-reduction-initsub.f90 b/flang/test/Lower/OpenMP/omp-declare-reduction-initsub.f90
index 2f6e432a72b13..333df70c5e989 100644
--- a/flang/test/Lower/OpenMP/omp-declare-reduction-initsub.f90
+++ b/flang/test/Lower/OpenMP/omp-declare-reduction-initsub.f90
@@ -17,7 +17,7 @@ subroutine initme(x,n)
        integer x,n
      end subroutine initme
   end interface
-!CHECK:  omp.declare_reduction @_QQFfuncred_add : i32 init {
+!CHECK:  omp.declare_reduction @_QQFfuncred_add_i32 : i32 init {
 !CHECK: ^bb0(%[[OMP_ORIG_ARG_I:.*]]: i32):
 !CHECK:    %[[OMP_PRIV:.*]] = fir.alloca i32
 !CHECK:    %[[OMP_ORIG:.*]] = fir.alloca i32
diff --git a/flang/test/Lower/OpenMP/omp-declare-reduction.f90 b/flang/test/Lower/OpenMP/omp-declare-reduction.f90
index 73e3c28622a58..5257560e51443 100644
--- a/flang/test/Lower/OpenMP/omp-declare-reduction.f90
+++ b/flang/test/Lower/OpenMP/omp-declare-reduction.f90
@@ -4,7 +4,7 @@
 
 subroutine declare_red()
   integer :: my_var
-!CHECK: omp.declare_reduction @_QQFdeclare_redmy_red : i32 init {
+!CHECK: omp.declare_reduction @_QQFdeclare_redmy_red_i32 : i32 init {
 !CHECK: ^bb0(%[[OMP_ORIG_ARG_I:.*]]: i32):
 !CHECK:    %[[OMP_PRIV:.*]] = fir.alloca i32
 !CHECK:    %[[OMP_ORIG:.*]] = fir.alloca i32



More information about the flang-commits mailing list