[flang-commits] [flang] [flang][OpenMP] Lower cross-module operator declare reduction (PR #207493)
via flang-commits
flang-commits at lists.llvm.org
Sat Jul 4 00:22:31 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-flang-semantics
Author: Matt (MattPD)
<details>
<summary>Changes</summary>
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.
---
Patch is 71.21 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/207493.diff
22 Files Affected:
- (modified) flang/include/flang/Lower/OpenMP.h (+8)
- (modified) flang/include/flang/Semantics/openmp-utils.h (+18)
- (modified) flang/lib/Lower/Bridge.cpp (+9)
- (modified) flang/lib/Lower/OpenMP/OpenMP.cpp (+135-15)
- (modified) flang/lib/Lower/Support/ReductionProcessor.cpp (+26-19)
- (modified) flang/lib/Semantics/check-omp-structure.cpp (+4-129)
- (modified) flang/lib/Semantics/mod-file.cpp (+21-1)
- (modified) flang/lib/Semantics/openmp-utils.cpp (+139)
- (modified) flang/lib/Semantics/resolve-names-utils.cpp (+5)
- (modified) flang/lib/Semantics/resolve-names-utils.h (+11)
- (removed) flang/test/Lower/OpenMP/Todo/declare-reduction-operator-use-assoc.f90 (-36)
- (added) flang/test/Lower/OpenMP/declare-mapper-hermetic.f90 (+38)
- (added) flang/test/Lower/OpenMP/declare-reduction-operator-collide.f90 (+79)
- (added) flang/test/Lower/OpenMP/declare-reduction-operator-merged.f90 (+75)
- (added) flang/test/Lower/OpenMP/declare-reduction-operator-renamed.f90 (+45)
- (added) flang/test/Lower/OpenMP/declare-reduction-operator-separate-combiners.f90 (+106)
- (added) flang/test/Lower/OpenMP/declare-reduction-operator-separate-hermetic.f90 (+56)
- (added) flang/test/Lower/OpenMP/declare-reduction-operator-separate-reexport.f90 (+93)
- (added) flang/test/Lower/OpenMP/declare-reduction-operator-separate-unused-unsupported.f90 (+86)
- (added) flang/test/Lower/OpenMP/declare-reduction-operator-separate.f90 (+162)
- (added) flang/test/Lower/OpenMP/declare-reduction-operator-use-assoc.f90 (+47)
- (added) flang/test/Semantics/OpenMP/declare-reduction-operator-modfile-reexport.f90 (+86)
``````````diff
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 ...
[truncated]
``````````
</details>
https://github.com/llvm/llvm-project/pull/207493
More information about the flang-commits
mailing list