https://github.com/vchuravy created
https://github.com/llvm/llvm-project/pull/212195
The HLFIR-to-FIR pass pipeline exposes extension points on
MLIRToLLVMPassPipelineConfig, but that config is built inside the frontend, so
a -load'ed plugin has no way to reach it and register passes.
registerDefaultInlinerPass is the only augmentor today, and it is wired in by
hand.
Add a global registry of config augmentors:
* fir::registerPassPipelineConfigCallback(cb) appends a callback, to be
called from a plugin's static initializer at -load time.
* fir::invokePassPipelineConfigCallbacks(config) runs them on the config.
This mirrors what flang already does for -load'ed plugin actions via
FrontendPluginRegistry: a process-global, append-only registry populated from
static initializers, which run before any compilation begins.
Both code generation entry points invoke the callbacks after building their
config and before constructing the pipeline: CodeGenAction::lowerHLFIRToFIR,
which serves -emit-fir, and CodeGenAction::generateLLVMIR, which serves
-emit-llvm/-emit-obj and reaches createHLFIRToFIRPassPipeline through
createMLIRToLLVMPassPipeline. The two are mutually exclusive for a given
compilation, so the callbacks run exactly once either way and a plugin gets the
same behaviour whichever output the user asked for.
No-op unless a callback is registered; tools that build the same pipelines
without consulting the registry (bbc, tco) are unaffected.
Co-Authored-By: Claude Opus 5 <[email protected]>
>From c827a8ca5a22dbf6bd1753eaddf728dc7b01559f Mon Sep 17 00:00:00 2001
From: Valentin Churavy <[email protected]>
Date: Wed, 22 Jul 2026 17:44:14 +0200
Subject: [PATCH] [flang] Add a pass-pipeline config-augmentor hook for
-load'ed plugins
The HLFIR-to-FIR pass pipeline exposes extension points on
MLIRToLLVMPassPipelineConfig, but that config is built inside the frontend, so
a -load'ed plugin has no way to reach it and register passes.
registerDefaultInlinerPass is the only augmentor today, and it is wired in by
hand.
Add a global registry of config augmentors:
* fir::registerPassPipelineConfigCallback(cb) appends a callback, to be
called from a plugin's static initializer at -load time.
* fir::invokePassPipelineConfigCallbacks(config) runs them on the config.
This mirrors what flang already does for -load'ed plugin actions via
FrontendPluginRegistry: a process-global, append-only registry populated from
static initializers, which run before any compilation begins.
Both code generation entry points invoke the callbacks after building their
config and before constructing the pipeline: CodeGenAction::lowerHLFIRToFIR,
which serves -emit-fir, and CodeGenAction::generateLLVMIR, which serves
-emit-llvm/-emit-obj and reaches createHLFIRToFIRPassPipeline through
createMLIRToLLVMPassPipeline. The two are mutually exclusive for a given
compilation, so the callbacks run exactly once either way and a plugin gets the
same behaviour whichever output the user asked for.
No-op unless a callback is registered; tools that build the same pipelines
without consulting the registry (bbc, tco) are unaffected.
Co-Authored-By: Claude Opus 5 <[email protected]>
---
flang/docs/FlangDriver.md | 34 +++++++
.../flang/Optimizer/Passes/Pipelines.h | 11 +++
flang/lib/Frontend/FrontendActions.cpp | 6 ++
flang/lib/Optimizer/Passes/Pipelines.cpp | 17 ++++
.../unittests/Optimizer/PassPipelineTest.cpp | 98 ++++++++++++++++++-
5 files changed, 165 insertions(+), 1 deletion(-)
diff --git a/flang/docs/FlangDriver.md b/flang/docs/FlangDriver.md
index a4385f68c6f0d..e5fa1300ad140 100644
--- a/flang/docs/FlangDriver.md
+++ b/flang/docs/FlangDriver.md
@@ -550,6 +550,40 @@ config.registerHLFIROptEarlyEPCallbacks(
});
```
+### Reaching the Extension Points from a `-load`'ed Plugin
+
+The `MLIRToLLVMPassPipelineConfig` the frontend driver builds is a local of
+`CodeGenAction`, so a shared object loaded with `flang -fc1 -load` cannot get
at
+it directly. `fir::registerPassPipelineConfigCallback`
+(`flang/include/flang/Optimizer/Passes/Pipelines.h`) provides a process-global
+registry of *config augmentors* for that purpose. Register one from a static
+initializer -- the same idiom `FrontendPluginRegistry` uses for `-load`'ed
+plugin actions -- and the frontend will invoke it once the config has been
+built, before the pipeline is constructed:
+
+```c++
+struct MyPluginRegistration {
+ MyPluginRegistration() {
+ fir::registerPassPipelineConfigCallback(
+ [](MLIRToLLVMPassPipelineConfig &config) {
+ config.registerHLFIROptEarlyEPCallbacks(
+ [](mlir::PassManager &pm, llvm::OptimizationLevel) {
+ pm.addPass(createMyHLFIRPass());
+ });
+ });
+ }
+};
+static MyPluginRegistration myPluginRegistration;
+```
+
+The augmentors are invoked for both `-emit-fir`
+(`CodeGenAction::lowerHLFIRToFIR`) and the `-emit-llvm`/`-emit-obj` path
+(`CodeGenAction::generateLLVMIR`), so a plugin only needs to register once. The
+registry is append-only and callbacks run in registration order; it is intended
+to be populated from static initializers, which run before any compilation
+begins. Tools that build the same pipelines without invoking the registry (for
+example `bbc` and `tco`) are unaffected.
+
## 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 8d867612d405c..88d26e26d390a 100644
--- a/flang/include/flang/Optimizer/Passes/Pipelines.h
+++ b/flang/include/flang/Optimizer/Passes/Pipelines.h
@@ -116,6 +116,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 pass pipeline is built, so that a -load'ed plugin can register passes
at
+/// the pipeline extension points. Callbacks run in registration order.
Register
+/// from a static initializer, which runs before any compilation begins.
+void registerPassPipelineConfigCallback(
+ std::function<void(MLIRToLLVMPassPipelineConfig &)> callback);
+
+/// Invoke every callback 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 408f061f8bc6e..ce75eb1877e4e 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;
+ // Let -load'ed plugins augment the pipeline config.
+ fir::invokePassPipelineConfigCallbacks(config);
// Create the pass pipeline
fir::createHLFIRToFIRPassPipeline(pm, enableOpenMP, config);
(void)mlir::applyPassManagerCLOptions(pm);
@@ -751,6 +753,10 @@ void CodeGenAction::generateLLVMIR() {
config.SkipConvertComplexPow = pipelineTriple.isAMDGCN();
fir::registerDefaultInlinerPass(config);
+ // Let -load'ed plugins augment the pipeline config. This path reaches
+ // createHLFIRToFIRPassPipeline through createMLIRToLLVMPassPipeline.
+ 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 9c0ed483e0a2f..24b82ded220ea 100644
--- a/flang/lib/Optimizer/Passes/Pipelines.cpp
+++ b/flang/lib/Optimizer/Passes/Pipelines.cpp
@@ -191,6 +191,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/PassPipelineTest.cpp
b/flang/unittests/Optimizer/PassPipelineTest.cpp
index 8a6690504c361..3adc1a1162c46 100644
--- a/flang/unittests/Optimizer/PassPipelineTest.cpp
+++ b/flang/unittests/Optimizer/PassPipelineTest.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 config augmentor registry that -load'ed 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.
@@ -220,4 +221,99 @@ TEST(HLFIRExtensionPoint, NoCallbacksIsNoOp) {
/*last=*/false));
}
+// The registry is process-global and append-only, with no way to unregister,
so
+// a callback capturing a local by reference would be re-invoked from a later
+// test with the referent destroyed. The tests record into this
+// process-lifetime recorder instead, and each asserts only on the markers it
+// wrote, so they do not depend on execution order.
+struct AugmentorRecorder {
+ 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;
+ }
+};
+
+AugmentorRecorder &recorder() {
+ static AugmentorRecorder 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);
+ // The callback receives the config the pipeline will be built from.
+ EXPECT_EQ(recorder().seenConfig, &config);
+}
+
+// The plugin shape: the augmentor 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);
+
+ EXPECT_TRUE(recorder().epRan);
+ EXPECT_NE(pipelineAsString(pm).find("ep-marker"), std::string::npos);
+}
+
+// A config never handed to invokePassPipelineConfigCallbacks is unaffected,
+// which is what keeps bbc and tco out of the registry.
+TEST(PassPipelineConfigCallback, NotInvokedMeansNoEffect) {
+ fir::registerPassPipelineConfigCallback(
+ [](MLIRToLLVMPassPipelineConfig &config) {
+ config.registerHLFIROptEarlyEPCallbacks(
+ [](mlir::PassManager &pm, llvm::OptimizationLevel) {
+ pm.addPass(std::make_unique<MarkerPass>());
+ });
+ });
+
+ recorder().reset();
+ mlir::MLIRContext context;
+ mlir::PassManager pm(&context, mlir::ModuleOp::getOperationName());
+ MLIRToLLVMPassPipelineConfig config(llvm::OptimizationLevel::O2);
+ fir::createHLFIRToFIRPassPipeline(pm, fir::EnableOpenMP::None, config);
+
+ EXPECT_EQ(pipelineAsString(pm).find("ep-marker"), std::string::npos);
+}
+
} // namespace
_______________________________________________
llvm-branch-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-branch-commits