[flang-commits] [flang] [flang] Add a MLIRToLLVMPassPipeline callbacks for plugins (PR #212195)
Valentin Churavy via flang-commits
flang-commits at lists.llvm.org
Wed Sep 30 03:12:33 PDT 2026
https://github.com/vchuravy updated https://github.com/llvm/llvm-project/pull/212195
>From 7b63359b37488ba76396f1db0959f74c9941d5ed Mon Sep 17 00:00:00 2001
From: Valentin Churavy <v.churavy at gmail.com>
Date: Wed, 22 Jul 2026 17:44:14 +0200
Subject: [PATCH 1/2] [flang] Add a pass-pipeline config callback hook for
plugins
The HLFIR-to-FIR pipeline extension points live on MLIRToLLVMPassPipelineConfig,
which the frontend builds as a local of CodeGenAction, out of reach of a plugin.
Add a process-global registry of callbacks that run on the config before the
pipeline is built. A plugin registers one from a static initializer, so it is in
place before any compilation begins, as FrontendPluginRegistry does for plugin
actions. Both code generation entry points invoke the callbacks, lowerHLFIRToFIR
for -emit-fir and generateLLVMIR for -emit-llvm/-emit-obj, and are mutually
exclusive for a given compilation, so a plugin sees the same behaviour whichever
output was asked for.
Co-Authored-By: Claude Opus 5 <noreply at anthropic.com>
---
flang/docs/FlangDriver.md | 30 +++++++
.../flang/Optimizer/Passes/Pipelines.h | 11 +++
flang/lib/Frontend/FrontendActions.cpp | 5 ++
flang/lib/Optimizer/Passes/Pipelines.cpp | 17 ++++
.../Optimizer/HLFIRExtensionPointsTest.cpp | 80 ++++++++++++++++++-
5 files changed, 142 insertions(+), 1 deletion(-)
diff --git a/flang/docs/FlangDriver.md b/flang/docs/FlangDriver.md
index c9df05ded7f49..d9ecbbc1372bf 100644
--- a/flang/docs/FlangDriver.md
+++ b/flang/docs/FlangDriver.md
@@ -560,6 +560,36 @@ config.registerHLFIROptEarlyEPCallbacks(
});
```
+### 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 ce4af1b6a1f36..5f1e5ac5e85d8 100644
--- a/flang/include/flang/Optimizer/Passes/Pipelines.h
+++ b/flang/include/flang/Optimizer/Passes/Pipelines.h
@@ -130,6 +130,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/lib/Frontend/FrontendActions.cpp b/flang/lib/Frontend/FrontendActions.cpp
index 1e29f4f430a1e..a49e68de5af94 100644
--- a/flang/lib/Frontend/FrontendActions.cpp
+++ b/flang/lib/Frontend/FrontendActions.cpp
@@ -653,6 +653,8 @@ void CodeGenAction::lowerHLFIRToFIR() {
if (ci.getInvocation().getFortranOpts().features.IsEnabled(
Fortran::common::LanguageFeature::CUDA))
config.EnableCUDA = 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);
@@ -771,6 +773,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 ef18ff1cde945..b3d850a6737be 100644
--- a/flang/lib/Optimizer/Passes/Pipelines.cpp
+++ b/flang/lib/Optimizer/Passes/Pipelines.cpp
@@ -165,6 +165,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);
+}
+
void createDefaultFIRPreCFGOptimizerPassPipeline(
mlir::PassManager &pm, MLIRToLLVMPassPipelineConfig &pc) {
// simplify the IR
diff --git a/flang/unittests/Optimizer/HLFIRExtensionPointsTest.cpp b/flang/unittests/Optimizer/HLFIRExtensionPointsTest.cpp
index 035efc1606a3b..9af355b686e63 100644
--- a/flang/unittests/Optimizer/HLFIRExtensionPointsTest.cpp
+++ b/flang/unittests/Optimizer/HLFIRExtensionPointsTest.cpp
@@ -6,7 +6,8 @@
//
//===----------------------------------------------------------------------===//
//
-// Tests for the HLFIR extension points of the HLFIR-to-FIR pass pipeline.
+// 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.
@@ -127,4 +128,81 @@ TEST(HLFIRExtensionPoint, MarkersAreAtTheDocumentedPositions) {
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
>From f04c8d96179e4c732f1cbe2ca16a8f91948a45d0 Mon Sep 17 00:00:00 2001
From: Valentin Churavy <v.churavy at gmail.com>
Date: Wed, 30 Sep 2026 12:08:34 +0200
Subject: [PATCH 2/2] [flang] Address review comments on the pipeline config
callbacks
- Adopt the suggested doc comment wording and name the callback type
(PassPipelineConfigCallback) instead of spelling std::function / auto.
- Run the callbacks in generateLLVMIR once the config is fully set up, so a
plugin sees the OpenMP/OpenACC, vscale and other settings as on -emit-fir.
- Document that -emit-fir only reaches the HLFIR extension points, that
callbacks must not register callbacks, and that bbc/tco/fir-opt do not run
the registry.
- Tests: drop the redundant epRan flag and use a plain global recorder.
Assisted-by: Claude Code (Opus 5.5)
---
flang/docs/FlangDriver.md | 12 ++++++---
.../flang/Optimizer/Passes/Pipelines.h | 15 +++++++----
flang/lib/Frontend/FrontendActions.cpp | 7 ++---
flang/lib/Optimizer/Passes/Pipelines.cpp | 11 ++++----
.../Optimizer/HLFIRExtensionPointsTest.cpp | 27 ++++++++-----------
5 files changed, 38 insertions(+), 34 deletions(-)
diff --git a/flang/docs/FlangDriver.md b/flang/docs/FlangDriver.md
index d9ecbbc1372bf..cfed20ebe60e9 100644
--- a/flang/docs/FlangDriver.md
+++ b/flang/docs/FlangDriver.md
@@ -566,8 +566,8 @@ 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
+every registered callback on its `MLIRToLLVMPassPipelineConfig` once the config
+is fully set up, just 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++
@@ -587,8 +587,12 @@ 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.
+(`CodeGenAction::generateLLVMIR`). `-emit-fir` only builds the HLFIR-to-FIR
+pipeline, so passes registered at the HLFIR extension points run on both paths,
+while those registered at the FIR optimizer extension points run only when
+generating LLVM IR. The registry is append-only and runs callbacks in
+registration order; a callback must not register further callbacks. Only the
+frontend driver runs the registry: `bbc`, `tco` and `fir-opt` do not.
## LLVM Pass Plugins
diff --git a/flang/include/flang/Optimizer/Passes/Pipelines.h b/flang/include/flang/Optimizer/Passes/Pipelines.h
index 5f1e5ac5e85d8..f071f52c1477e 100644
--- a/flang/include/flang/Optimizer/Passes/Pipelines.h
+++ b/flang/include/flang/Optimizer/Passes/Pipelines.h
@@ -28,6 +28,7 @@
#include "llvm/Frontend/Debug/Options.h"
#include "llvm/Passes/OptimizationLevel.h"
#include "llvm/Support/CommandLine.h"
+#include <functional>
namespace fir {
@@ -130,12 +131,16 @@ void addLLVMDialectToLLVMPass(mlir::PassManager &pm, llvm::raw_ostream &output);
/// Use inliner extension point callback to register the default inliner pass.
void registerDefaultInlinerPass(MLIRToLLVMPassPipelineConfig &config);
+/// A callback run on the MLIRToLLVMPassPipelineConfig before the frontend
+/// builds the pipeline.
+using PassPipelineConfigCallback =
+ std::function<void(MLIRToLLVMPassPipelineConfig &)>;
+
/// 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);
+/// the frontend builds the pipeline. Call this from a static initializer in a
+/// plugin to add passes at the pipeline extension points. The callbacks are run
+/// in registration order, and must not themselves register callbacks.
+void registerPassPipelineConfigCallback(PassPipelineConfigCallback callback);
/// Run the callbacks registered via registerPassPipelineConfigCallback on
/// \p config.
diff --git a/flang/lib/Frontend/FrontendActions.cpp b/flang/lib/Frontend/FrontendActions.cpp
index a49e68de5af94..06d67e9417bc4 100644
--- a/flang/lib/Frontend/FrontendActions.cpp
+++ b/flang/lib/Frontend/FrontendActions.cpp
@@ -773,9 +773,6 @@ 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;
@@ -803,6 +800,10 @@ void CodeGenAction::generateLLVMIR() {
config.ComplexRange = opts.getComplexRange();
+ // Give plugins a chance to register passes at the extension points, once the
+ // config is fully set up.
+ fir::invokePassPipelineConfigCallbacks(config);
+
// Create the pass pipeline
fir::createMLIRToLLVMPassPipeline(pm, config, getCurrentFile());
(void)mlir::applyPassManagerCLOptions(pm);
diff --git a/flang/lib/Optimizer/Passes/Pipelines.cpp b/flang/lib/Optimizer/Passes/Pipelines.cpp
index b3d850a6737be..0602340d04ae5 100644
--- a/flang/lib/Optimizer/Passes/Pipelines.cpp
+++ b/flang/lib/Optimizer/Passes/Pipelines.cpp
@@ -17,6 +17,7 @@
#include "mlir/Dialect/LLVMIR/Transforms/Passes.h"
#include "mlir/Dialect/OpenMP/Transforms/Passes.h"
#include "llvm/Support/CommandLine.h"
+#include <vector>
/// Force setting the no-alias attribute on fuction arguments when possible.
static llvm::cl::opt<bool> forceNoAlias("force-no-alias", llvm::cl::Hidden,
@@ -165,20 +166,18 @@ void registerDefaultInlinerPass(MLIRToLLVMPassPipelineConfig &config) {
});
}
-static std::vector<std::function<void(MLIRToLLVMPassPipelineConfig &)>> &
+static std::vector<PassPipelineConfigCallback> &
getPassPipelineConfigCallbacks() {
- static std::vector<std::function<void(MLIRToLLVMPassPipelineConfig &)>>
- callbacks;
+ static std::vector<PassPipelineConfigCallback> callbacks;
return callbacks;
}
-void registerPassPipelineConfigCallback(
- std::function<void(MLIRToLLVMPassPipelineConfig &)> callback) {
+void registerPassPipelineConfigCallback(PassPipelineConfigCallback callback) {
getPassPipelineConfigCallbacks().push_back(std::move(callback));
}
void invokePassPipelineConfigCallbacks(MLIRToLLVMPassPipelineConfig &config) {
- for (auto &callback : getPassPipelineConfigCallbacks())
+ for (PassPipelineConfigCallback &callback : getPassPipelineConfigCallbacks())
callback(config);
}
diff --git a/flang/unittests/Optimizer/HLFIRExtensionPointsTest.cpp b/flang/unittests/Optimizer/HLFIRExtensionPointsTest.cpp
index 9af355b686e63..4ca3a6ecbfb2f 100644
--- a/flang/unittests/Optimizer/HLFIRExtensionPointsTest.cpp
+++ b/flang/unittests/Optimizer/HLFIRExtensionPointsTest.cpp
@@ -135,12 +135,10 @@ TEST(HLFIRExtensionPoint, MarkersAreAtTheDocumentedPositions) {
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 {
@@ -151,31 +149,30 @@ struct ConfigCallbackRecorder {
}
};
-ConfigCallbackRecorder &recorder() {
- static ConfigCallbackRecorder r;
- return r;
-}
+// Callbacks are registered and run from test bodies, never during static
+// initialization, so a plain global is safe.
+ConfigCallbackRecorder recorder;
TEST(PassPipelineConfigCallback, CallbacksRunInRegistrationOrderOnTheConfig) {
fir::registerPassPipelineConfigCallback(
[](MLIRToLLVMPassPipelineConfig &config) {
- recorder().order.push_back("order-first");
- recorder().seenConfig = &config;
+ recorder.order.push_back("order-first");
+ recorder.seenConfig = &config;
});
fir::registerPassPipelineConfigCallback([](MLIRToLLVMPassPipelineConfig &) {
- recorder().order.push_back("order-second");
+ recorder.order.push_back("order-second");
});
- recorder().reset();
+ recorder.reset();
MLIRToLLVMPassPipelineConfig config(llvm::OptimizationLevel::O2);
fir::invokePassPipelineConfigCallbacks(config);
- size_t first = recorder().indexOf("order-first");
- size_t second = recorder().indexOf("order-second");
+ 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);
+ EXPECT_EQ(recorder.seenConfig, &config);
}
// The plugin shape: the config callback registers an extension point
@@ -185,12 +182,11 @@ TEST(PassPipelineConfigCallback, CanRegisterHLFIRExtensionPoints) {
[](MLIRToLLVMPassPipelineConfig &config) {
config.registerHLFIROptEarlyEPCallbacks(
[](mlir::PassManager &pm, llvm::OptimizationLevel) {
- recorder().epRan = true;
pm.addPass(std::make_unique<MarkerPass>());
});
});
- recorder().reset();
+ recorder.reset();
mlir::MLIRContext context;
mlir::PassManager pm(&context, mlir::ModuleOp::getOperationName());
MLIRToLLVMPassPipelineConfig config(llvm::OptimizationLevel::O2);
@@ -201,7 +197,6 @@ TEST(PassPipelineConfigCallback, CanRegisterHLFIRExtensionPoints) {
llvm::raw_string_ostream os(pipeline);
pm.printAsTextualPipeline(os);
- EXPECT_TRUE(recorder().epRan);
EXPECT_NE(pipeline.find("ep-marker"), std::string::npos) << pipeline;
}
More information about the flang-commits
mailing list