https://github.com/vchuravy updated https://github.com/llvm/llvm-project/pull/212195
>From f4f02f7f9bb415a7f44e93f36bf0f6f620172367 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 +++ .../Optimizer/HLFIRExtensionPointsTest.cpp | 106 +++++++++++++++++- 5 files changed, 173 insertions(+), 1 deletion(-) diff --git a/flang/docs/FlangDriver.md b/flang/docs/FlangDriver.md index 07feba4f4674b..55b4534cbffbe 100644 --- a/flang/docs/FlangDriver.md +++ b/flang/docs/FlangDriver.md @@ -549,6 +549,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 c50d41844941e..1ac73ba02a7cd 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 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 8955a8f61e513..44cffdbfb49ab 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 48ae9f3bea58c..c2d317c02ed37 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 2dc17b458ea9a..275b282d9b91a 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 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. @@ -138,4 +139,107 @@ TEST(HLFIRExtensionPoint, MarkersAreAtTheDocumentedPositions) { EXPECT_LT(lastMarker, lowerIntrinsics) << pipeline; } +// 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); + + 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; +} + +// 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); + + std::string pipeline; + llvm::raw_string_ostream os(pipeline); + pm.printAsTextualPipeline(os); + + EXPECT_EQ(pipeline.find("ep-marker"), std::string::npos) << pipeline; +} + } // namespace _______________________________________________ llvm-branch-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-branch-commits
