[llvm-branch-commits] [flang] [flang] Add a pass-pipeline config-augmentor hook for -load'ed plugins (PR #212195)
Valentin Churavy via llvm-branch-commits
llvm-branch-commits at lists.llvm.org
Tue Aug 4 23:50:47 PDT 2026
https://github.com/vchuravy updated https://github.com/llvm/llvm-project/pull/212195
>From 832eab1d2de0343c0085d203a283774b6ce96bd3 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] [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 a2eb73002d798..9a03b1f9b6ba0 100644
--- a/flang/docs/FlangDriver.md
+++ b/flang/docs/FlangDriver.md
@@ -546,6 +546,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 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/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 15a342e10fc7f..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.
///
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
More information about the llvm-branch-commits
mailing list