[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