[flang-commits] [flang] [flang] Add a MLIRToLLVMPassPipeline callbacks for plugins (PR #212195)
via flang-commits
flang-commits at lists.llvm.org
Tue Aug 4 23:56:48 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-flang-driver
@llvm/pr-subscribers-flang-fir-hlfir
Author: Valentin Churavy (vchuravy)
<details>
<summary>Changes</summary>
The HLFIR-to-FIR pass pipeline exposes extension points on
MLIRToLLVMPassPipelineConfig, but that config is built inside the frontend, so
plugins has no way to reach it and register passes.
Follow-up to #<!-- -->212194
Co-Authored-By: Claude Opus 5 <noreply@<!-- -->anthropic.com>
---
Full diff: https://github.com/llvm/llvm-project/pull/212195.diff
7 Files Affected:
- (modified) flang/docs/FlangDriver.md (+51)
- (modified) flang/include/flang/Optimizer/Passes/Pipelines.h (+11)
- (modified) flang/include/flang/Tools/CrossToolHelpers.h (+32)
- (modified) flang/lib/Frontend/FrontendActions.cpp (+5)
- (modified) flang/lib/Optimizer/Passes/Pipelines.cpp (+23)
- (modified) flang/unittests/Optimizer/CMakeLists.txt (+2)
- (added) flang/unittests/Optimizer/HLFIRExtensionPointsTest.cpp (+208)
``````````diff
diff --git a/flang/docs/FlangDriver.md b/flang/docs/FlangDriver.md
index 4edc99944ad44..9a03b1f9b6ba0 100644
--- a/flang/docs/FlangDriver.md
+++ b/flang/docs/FlangDriver.md
@@ -525,6 +525,57 @@ passes at different points of the default pass pipeline. An example use of these
extension point callbacks is shown in `registerDefaultInlinerPass` to invoke the
default inliner pass in `flang`.
+These extension points all run after HLFIR has been lowered to FIR, so the HLFIR
+intrinsic operations (`hlfir.sum`, `hlfir.matmul`, ...) are already gone. For
+transformations that need to see them, `createHLFIRToFIRPassPipeline` provides
+two more extension points:
+
+* `invokeHLFIROptEarlyEPCallbacks` runs at the start of the pipeline, before any
+ HLFIR simplification or inlining.
+* `invokeHLFIROptLastEPCallbacks` runs just before `createLowerHLFIRIntrinsics`,
+ the last point at which HLFIR intrinsic operations still exist.
+
+Drivers register passes with `registerHLFIROptEarlyEPCallbacks` and
+`registerHLFIROptLastEPCallbacks` on the `MLIRToLLVMPassPipelineConfig` (defined
+in `flang/include/flang/Tools/CrossToolHelpers.h`), for example:
+
+```c++
+config.registerHLFIROptEarlyEPCallbacks(
+ [](mlir::PassManager &pm, llvm::OptimizationLevel) {
+ pm.addPass(createMyHLFIRPass());
+ });
+```
+
+### Registering Extension Point Passes from a Plugin
+
+To add passes at these extension points from a
+[plugin](#frontend-driver-plugins), register a *pipeline config callback* with
+`fir::registerPassPipelineConfigCallback`
+(`flang/include/flang/Optimizer/Passes/Pipelines.h`). The frontend driver runs
+every registered callback on its `MLIRToLLVMPassPipelineConfig` before it builds
+the pipeline. Register from a static initializer, so the callback is in place as
+soon as the plugin is loaded and before any compilation begins:
+
+```c++
+struct MyPluginRegistration {
+ MyPluginRegistration() {
+ fir::registerPassPipelineConfigCallback(
+ [](MLIRToLLVMPassPipelineConfig &config) {
+ config.registerHLFIROptEarlyEPCallbacks(
+ [](mlir::PassManager &pm, llvm::OptimizationLevel) {
+ pm.addPass(createMyHLFIRPass());
+ });
+ });
+ }
+};
+static MyPluginRegistration myPluginRegistration;
+```
+
+These callbacks run on both the `-emit-fir` path
+(`CodeGenAction::lowerHLFIRToFIR`) and the `-emit-llvm`/`-emit-obj` path
+(`CodeGenAction::generateLLVMIR`), so registering once is enough. The registry
+is append-only and runs callbacks in registration order.
+
## LLVM Pass Plugins
Pass plugins are dynamic shared objects that consist of one or more LLVM IR
diff --git a/flang/include/flang/Optimizer/Passes/Pipelines.h b/flang/include/flang/Optimizer/Passes/Pipelines.h
index c50d41844941e..8d269f162d0ea 100644
--- a/flang/include/flang/Optimizer/Passes/Pipelines.h
+++ b/flang/include/flang/Optimizer/Passes/Pipelines.h
@@ -132,6 +132,17 @@ void addLLVMDialectToLLVMPass(mlir::PassManager &pm, llvm::raw_ostream &output);
/// Use inliner extension point callback to register the default inliner pass.
void registerDefaultInlinerPass(MLIRToLLVMPassPipelineConfig &config);
+/// Register a callback that augments the MLIRToLLVMPassPipelineConfig before
+/// the frontend builds the pipeline. Use this to add passes at the pipeline
+/// extension points from a plugin. Call from a static initializer; callbacks
+/// run in registration order.
+void registerPassPipelineConfigCallback(
+ std::function<void(MLIRToLLVMPassPipelineConfig &)> callback);
+
+/// Run the callbacks registered via registerPassPipelineConfigCallback on
+/// \p config.
+void invokePassPipelineConfigCallbacks(MLIRToLLVMPassPipelineConfig &config);
+
/// Register the passes used in Flang's MLIR pass pipeline
/// e.g. --mlir-print-ir-before=<pass> and similar.
void registerFlangPipelinePasses();
diff --git a/flang/include/flang/Tools/CrossToolHelpers.h b/flang/include/flang/Tools/CrossToolHelpers.h
index 7bae6e9824795..6569d34e0f255 100644
--- a/flang/include/flang/Tools/CrossToolHelpers.h
+++ b/flang/include/flang/Tools/CrossToolHelpers.h
@@ -46,6 +46,18 @@ class FlangEPCallBacks {
FIROptLastEPCallbacks.push_back(C);
}
+ void registerHLFIROptEarlyEPCallbacks(
+ const std::function<void(mlir::PassManager &, llvm::OptimizationLevel)>
+ &C) {
+ HLFIROptEarlyEPCallbacks.push_back(C);
+ }
+
+ void registerHLFIROptLastEPCallbacks(
+ const std::function<void(mlir::PassManager &, llvm::OptimizationLevel)>
+ &C) {
+ HLFIROptLastEPCallbacks.push_back(C);
+ }
+
void invokeFIROptEarlyEPCallbacks(
mlir::PassManager &pm, llvm::OptimizationLevel optLevel) {
for (auto &C : FIROptEarlyEPCallbacks)
@@ -64,6 +76,18 @@ class FlangEPCallBacks {
C(pm, optLevel);
};
+ void invokeHLFIROptEarlyEPCallbacks(
+ mlir::PassManager &pm, llvm::OptimizationLevel optLevel) const {
+ for (auto &C : HLFIROptEarlyEPCallbacks)
+ C(pm, optLevel);
+ };
+
+ void invokeHLFIROptLastEPCallbacks(
+ mlir::PassManager &pm, llvm::OptimizationLevel optLevel) const {
+ for (auto &C : HLFIROptLastEPCallbacks)
+ C(pm, optLevel);
+ };
+
private:
llvm::SmallVector<
std::function<void(mlir::PassManager &, llvm::OptimizationLevel)>, 1>
@@ -76,6 +100,14 @@ class FlangEPCallBacks {
llvm::SmallVector<
std::function<void(mlir::PassManager &, llvm::OptimizationLevel)>, 1>
FIROptLastEPCallbacks;
+
+ llvm::SmallVector<
+ std::function<void(mlir::PassManager &, llvm::OptimizationLevel)>, 1>
+ HLFIROptEarlyEPCallbacks;
+
+ llvm::SmallVector<
+ std::function<void(mlir::PassManager &, llvm::OptimizationLevel)>, 1>
+ HLFIROptLastEPCallbacks;
};
/// Configuriation for the MLIR to LLVM pass pipeline.
diff --git a/flang/lib/Frontend/FrontendActions.cpp b/flang/lib/Frontend/FrontendActions.cpp
index 8955a8f61e513..adbbfaa69a0cf 100644
--- a/flang/lib/Frontend/FrontendActions.cpp
+++ b/flang/lib/Frontend/FrontendActions.cpp
@@ -633,6 +633,8 @@ void CodeGenAction::lowerHLFIRToFIR() {
ci.getInvocation().getLoweringOpts().getFPMaxminBehavior();
if (ci.getInvocation().getLangOpts().OpenMPIsTargetDevice)
config.EnableOpenMPIsTargetDevice = true;
+ // Give plugins a chance to register passes at the extension points.
+ fir::invokePassPipelineConfigCallbacks(config);
// Create the pass pipeline
fir::createHLFIRToFIRPassPipeline(pm, enableOpenMP, config);
(void)mlir::applyPassManagerCLOptions(pm);
@@ -751,6 +753,9 @@ void CodeGenAction::generateLLVMIR() {
config.SkipConvertComplexPow = pipelineTriple.isAMDGCN();
fir::registerDefaultInlinerPass(config);
+ // Give plugins a chance to register passes at the extension points.
+ fir::invokePassPipelineConfigCallbacks(config);
+
if (auto vsr = getVScaleRange(ci)) {
config.VScaleMin = vsr->first;
config.VScaleMax = vsr->second;
diff --git a/flang/lib/Optimizer/Passes/Pipelines.cpp b/flang/lib/Optimizer/Passes/Pipelines.cpp
index ba346bfb62111..46c801ede5eb4 100644
--- a/flang/lib/Optimizer/Passes/Pipelines.cpp
+++ b/flang/lib/Optimizer/Passes/Pipelines.cpp
@@ -171,6 +171,23 @@ void registerDefaultInlinerPass(MLIRToLLVMPassPipelineConfig &config) {
});
}
+static std::vector<std::function<void(MLIRToLLVMPassPipelineConfig &)>> &
+getPassPipelineConfigCallbacks() {
+ static std::vector<std::function<void(MLIRToLLVMPassPipelineConfig &)>>
+ callbacks;
+ return callbacks;
+}
+
+void registerPassPipelineConfigCallback(
+ std::function<void(MLIRToLLVMPassPipelineConfig &)> callback) {
+ getPassPipelineConfigCallbacks().push_back(std::move(callback));
+}
+
+void invokePassPipelineConfigCallbacks(MLIRToLLVMPassPipelineConfig &config) {
+ for (auto &callback : getPassPipelineConfigCallbacks())
+ callback(config);
+}
+
/// Create a pass pipeline for running default optimization passes for
/// incremental conversion of FIR.
///
@@ -263,6 +280,9 @@ void createHLFIRToFIRPassPipeline(mlir::PassManager &pm,
EnableOpenMP enableOpenMP,
const MLIRToLLVMPassPipelineConfig &config) {
llvm::OptimizationLevel optLevel = config.OptLevel;
+
+ config.invokeHLFIROptEarlyEPCallbacks(pm, optLevel);
+
if (optLevel != llvm::OptimizationLevel::O0) {
addNestedPassToAllTopLevelOperations<PassConstructor>(
pm, hlfir::createExpressionSimplification);
@@ -310,6 +330,9 @@ void createHLFIRToFIRPassPipeline(mlir::PassManager &pm,
}
pm.addPass(hlfir::createLowerHLFIROrderedAssignments(
{/*tryFusingAssignments=*/optLevel != llvm::OptimizationLevel::O0}));
+
+ config.invokeHLFIROptLastEPCallbacks(pm, optLevel);
+
pm.addPass(hlfir::createLowerHLFIRIntrinsics());
hlfir::BufferizeHLFIROptions bufferizeOptions;
diff --git a/flang/unittests/Optimizer/CMakeLists.txt b/flang/unittests/Optimizer/CMakeLists.txt
index 6f83b2ac268da..0e0c12b21bb56 100644
--- a/flang/unittests/Optimizer/CMakeLists.txt
+++ b/flang/unittests/Optimizer/CMakeLists.txt
@@ -19,6 +19,7 @@ set(LIBS
FIRTransforms
HLFIRDialect
MIFDialect
+ flangPasses
)
add_flang_unittest(FlangOptimizerTests
@@ -44,6 +45,7 @@ add_flang_unittest(FlangOptimizerTests
FIRContextTest.cpp
FIRTypesTest.cpp
FortranVariableTest.cpp
+ HLFIRExtensionPointsTest.cpp
InternalNamesTest.cpp
KindMappingTest.cpp
RTBuilder.cpp
diff --git a/flang/unittests/Optimizer/HLFIRExtensionPointsTest.cpp b/flang/unittests/Optimizer/HLFIRExtensionPointsTest.cpp
new file mode 100644
index 0000000000000..9af355b686e63
--- /dev/null
+++ b/flang/unittests/Optimizer/HLFIRExtensionPointsTest.cpp
@@ -0,0 +1,208 @@
+//===- HLFIRExtensionPointsTest.cpp ---------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Tests for the HLFIR extension points of the HLFIR-to-FIR pass pipeline, and
+// for the pipeline config callback registry plugins use to reach them.
+//
+// The callbacks run when the pipeline is built, so no IR is needed: building
+// the pipeline is enough to observe them.
+//
+//===----------------------------------------------------------------------===//
+
+#include "gtest/gtest.h"
+#include "mlir/IR/BuiltinOps.h"
+#include "mlir/IR/MLIRContext.h"
+#include "mlir/Pass/Pass.h"
+#include "mlir/Pass/PassManager.h"
+#include "flang/Optimizer/Passes/Pipelines.h"
+#include "flang/Tools/CrossToolHelpers.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/raw_ostream.h"
+#include <string>
+#include <vector>
+
+namespace {
+
+struct MarkerPass : public mlir::PassWrapper<MarkerPass,
+ mlir::OperationPass<mlir::ModuleOp>> {
+ MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(MarkerPass)
+
+ llvm::StringRef getArgument() const override { return "ep-marker"; }
+ llvm::StringRef getDescription() const override {
+ return "No-op pass used to locate an extension point in a pipeline";
+ }
+ void runOnOperation() override {}
+};
+
+TEST(HLFIRExtensionPoint, CallbacksAreInvokedInOrder) {
+ mlir::MLIRContext context;
+ mlir::PassManager pm(&context, mlir::ModuleOp::getOperationName());
+ MLIRToLLVMPassPipelineConfig config(llvm::OptimizationLevel::O2);
+
+ std::vector<std::string> order;
+ size_t earlySizeAtCall = ~size_t{0};
+
+ config.registerHLFIROptEarlyEPCallbacks(
+ [&](mlir::PassManager &nestedPm, llvm::OptimizationLevel) {
+ order.push_back("early");
+ earlySizeAtCall = nestedPm.size();
+ });
+ config.registerHLFIROptLastEPCallbacks(
+ [&](mlir::PassManager &, llvm::OptimizationLevel) {
+ order.push_back("last");
+ });
+
+ fir::createHLFIRToFIRPassPipeline(pm, fir::EnableOpenMP::None, config);
+
+ ASSERT_EQ(order.size(), 2u);
+ EXPECT_EQ(order[0], "early");
+ EXPECT_EQ(order[1], "last");
+ EXPECT_EQ(earlySizeAtCall, 0u);
+ EXPECT_GT(pm.size(), 0u);
+}
+
+TEST(HLFIRExtensionPoint, CallbacksAreInvokedAtEveryOptLevel) {
+ for (llvm::OptimizationLevel level :
+ {llvm::OptimizationLevel::O0, llvm::OptimizationLevel::O1,
+ llvm::OptimizationLevel::O2, llvm::OptimizationLevel::O3}) {
+ mlir::MLIRContext context;
+ mlir::PassManager pm(&context, mlir::ModuleOp::getOperationName());
+ MLIRToLLVMPassPipelineConfig config(level);
+
+ int earlyCount = 0;
+ int lastCount = 0;
+ llvm::OptimizationLevel seenLevel = llvm::OptimizationLevel::O0;
+ config.registerHLFIROptEarlyEPCallbacks(
+ [&](mlir::PassManager &, llvm::OptimizationLevel cbLevel) {
+ ++earlyCount;
+ seenLevel = cbLevel;
+ });
+ config.registerHLFIROptLastEPCallbacks(
+ [&](mlir::PassManager &, llvm::OptimizationLevel) { ++lastCount; });
+
+ fir::createHLFIRToFIRPassPipeline(pm, fir::EnableOpenMP::None, config);
+
+ EXPECT_EQ(earlyCount, 1);
+ EXPECT_EQ(lastCount, 1);
+ EXPECT_EQ(seenLevel, level);
+ }
+}
+
+TEST(HLFIRExtensionPoint, MarkersAreAtTheDocumentedPositions) {
+ mlir::MLIRContext context;
+ mlir::PassManager pm(&context, mlir::ModuleOp::getOperationName());
+ MLIRToLLVMPassPipelineConfig config(llvm::OptimizationLevel::O2);
+
+ config.registerHLFIROptEarlyEPCallbacks(
+ [](mlir::PassManager &nestedPm, llvm::OptimizationLevel) {
+ nestedPm.addPass(std::make_unique<MarkerPass>());
+ });
+ config.registerHLFIROptLastEPCallbacks(
+ [](mlir::PassManager &nestedPm, llvm::OptimizationLevel) {
+ nestedPm.addPass(std::make_unique<MarkerPass>());
+ });
+
+ fir::createHLFIRToFIRPassPipeline(pm, fir::EnableOpenMP::None, config);
+
+ std::string pipeline;
+ llvm::raw_string_ostream os(pipeline);
+ pm.printAsTextualPipeline(os);
+
+ size_t earlyMarker = pipeline.find("ep-marker");
+ ASSERT_NE(earlyMarker, std::string::npos) << pipeline;
+ size_t lastMarker = pipeline.find("ep-marker", earlyMarker + 1);
+ ASSERT_NE(lastMarker, std::string::npos) << pipeline;
+
+ size_t simplify = pipeline.find("simplify-hlfir-intrinsics");
+ ASSERT_NE(simplify, std::string::npos) << pipeline;
+ size_t lowerIntrinsics = pipeline.find("lower-hlfir-intrinsics");
+ ASSERT_NE(lowerIntrinsics, std::string::npos) << pipeline;
+
+ EXPECT_LT(earlyMarker, simplify) << pipeline;
+ EXPECT_GT(lastMarker, simplify) << pipeline;
+ EXPECT_LT(lastMarker, lowerIntrinsics) << pipeline;
+}
+
+// The registry is process-global and append-only, so a callback capturing a
+// local by reference would be re-invoked by a later test with the referent
+// destroyed. Tests record into this process-lifetime recorder instead, and each
+// asserts only on the markers it wrote, so they do not depend on test order.
+struct ConfigCallbackRecorder {
+ std::vector<std::string> order;
+ MLIRToLLVMPassPipelineConfig *seenConfig = nullptr;
+ bool epRan = false;
+
+ void reset() {
+ order.clear();
+ seenConfig = nullptr;
+ epRan = false;
+ }
+ /// Index of \p marker in `order`, or npos.
+ size_t indexOf(llvm::StringRef marker) const {
+ for (size_t i = 0, e = order.size(); i != e; ++i)
+ if (order[i] == marker)
+ return i;
+ return std::string::npos;
+ }
+};
+
+ConfigCallbackRecorder &recorder() {
+ static ConfigCallbackRecorder r;
+ return r;
+}
+
+TEST(PassPipelineConfigCallback, CallbacksRunInRegistrationOrderOnTheConfig) {
+ fir::registerPassPipelineConfigCallback(
+ [](MLIRToLLVMPassPipelineConfig &config) {
+ recorder().order.push_back("order-first");
+ recorder().seenConfig = &config;
+ });
+ fir::registerPassPipelineConfigCallback([](MLIRToLLVMPassPipelineConfig &) {
+ recorder().order.push_back("order-second");
+ });
+
+ recorder().reset();
+ MLIRToLLVMPassPipelineConfig config(llvm::OptimizationLevel::O2);
+ fir::invokePassPipelineConfigCallbacks(config);
+
+ size_t first = recorder().indexOf("order-first");
+ size_t second = recorder().indexOf("order-second");
+ ASSERT_NE(first, std::string::npos);
+ ASSERT_NE(second, std::string::npos);
+ EXPECT_LT(first, second);
+ EXPECT_EQ(recorder().seenConfig, &config);
+}
+
+// The plugin shape: the config callback registers an extension point
+// callback, which then contributes a pass when the pipeline is built.
+TEST(PassPipelineConfigCallback, CanRegisterHLFIRExtensionPoints) {
+ fir::registerPassPipelineConfigCallback(
+ [](MLIRToLLVMPassPipelineConfig &config) {
+ config.registerHLFIROptEarlyEPCallbacks(
+ [](mlir::PassManager &pm, llvm::OptimizationLevel) {
+ recorder().epRan = true;
+ pm.addPass(std::make_unique<MarkerPass>());
+ });
+ });
+
+ recorder().reset();
+ mlir::MLIRContext context;
+ mlir::PassManager pm(&context, mlir::ModuleOp::getOperationName());
+ MLIRToLLVMPassPipelineConfig config(llvm::OptimizationLevel::O2);
+ fir::invokePassPipelineConfigCallbacks(config);
+ fir::createHLFIRToFIRPassPipeline(pm, fir::EnableOpenMP::None, config);
+
+ std::string pipeline;
+ llvm::raw_string_ostream os(pipeline);
+ pm.printAsTextualPipeline(os);
+
+ EXPECT_TRUE(recorder().epRan);
+ EXPECT_NE(pipeline.find("ep-marker"), std::string::npos) << pipeline;
+}
+
+} // namespace
``````````
</details>
https://github.com/llvm/llvm-project/pull/212195
More information about the flang-commits
mailing list