[flang-commits] [flang] [flang] Add HLFIR-to-FIR pass pipeline extension points (PR #212194)

Valentin Churavy via flang-commits flang-commits at lists.llvm.org
Mon Aug 3 07:00:35 PDT 2026


https://github.com/vchuravy updated https://github.com/llvm/llvm-project/pull/212194

>From caa4ccb5f768d3c970a51cad1fae543aeee210f7 Mon Sep 17 00:00:00 2001
From: Valentin Churavy <v.churavy at gmail.com>
Date: Wed, 22 Jul 2026 17:04:57 +0200
Subject: [PATCH] [flang] Add HLFIR-to-FIR pass pipeline extension points

The FIR optimizer extension points (FIROptEarly, FIRInliner, FIROptLast) all
run after HLFIR has been lowered to FIR, so the HLFIR intrinsic operations
(hlfir.sum, hlfir.matmul, ...) are gone by the time they run. Transformations
that need to see those operations have nowhere to attach.

Add two extension points to createHLFIRToFIRPassPipeline:

  * HLFIROptEarly, at the start of the pipeline, before any HLFIR
    simplification or inlining.
  * HLFIROptLast, just before createLowerHLFIRIntrinsics.

Drivers register passes through registerHLFIROptEarlyEPCallbacks and
registerHLFIROptLastEPCallbacks on MLIRToLLVMPassPipelineConfig. The invoke
methods are const so they can be called on the const config the HLFIR pipeline
receives. With no callbacks registered the pipeline is unchanged.

Co-Authored-By: Claude Opus 5 <noreply at anthropic.com>
---
 flang/docs/FlangDriver.md                     |  24 +++
 flang/include/flang/Tools/CrossToolHelpers.h  |  33 ++++
 flang/lib/Optimizer/Passes/Pipelines.cpp      |   8 +
 flang/unittests/Optimizer/CMakeLists.txt      |   2 +
 .../Optimizer/HLFIRExtensionPointsTest.cpp    | 141 ++++++++++++++++++
 5 files changed, 208 insertions(+)
 create mode 100644 flang/unittests/Optimizer/HLFIRExtensionPointsTest.cpp

diff --git a/flang/docs/FlangDriver.md b/flang/docs/FlangDriver.md
index 4edc99944ad44..07feba4f4674b 100644
--- a/flang/docs/FlangDriver.md
+++ b/flang/docs/FlangDriver.md
@@ -525,6 +525,30 @@ passes at different points of the default pass pipeline. An example use of these
 extension point callbacks is shown in `registerDefaultInlinerPass` to invoke the
 default inliner pass in `flang`.
 
+The FIR optimizer extension points all run after HLFIR has been lowered to FIR,
+so the high-level HLFIR intrinsic operations (`hlfir.sum`, `hlfir.matmul`, ...)
+are no longer available at those points. For transformations that need to see
+those operations before they are lowered to FIR/runtime calls, the HLFIR-to-FIR pass pipeline
+`createHLFIRToFIRPassPipeline` provides two additional extension points:
+
+* `invokeHLFIROptEarlyEPCallbacks` runs at the very beginning of the pipeline,
+  before any HLFIR simplification or inlining, while the HLFIR intrinsic
+  operations are still in their original form.
+* `invokeHLFIROptLastEPCallbacks` runs just before `createLowerHLFIRIntrinsics`,
+  the final opportunity to process HLFIR intrinsic operations before they are
+  lowered.
+
+Drivers register passes into these using `registerHLFIROptEarlyEPCallbacks` and
+`registerHLFIROptLastEPCallbacks` on the `MLIRToLLVMPassPipelineConfig` (defined
+in `flang/include/flang/Tools/CrossToolHelpers.h`), for example:
+
+```c++
+config.registerHLFIROptEarlyEPCallbacks(
+    [](mlir::PassManager &pm, llvm::OptimizationLevel) {
+      pm.addPass(createMyHLFIRPass());
+    });
+```
+
 ## LLVM Pass Plugins
 
 Pass plugins are dynamic shared objects that consist of one or more LLVM IR
diff --git a/flang/include/flang/Tools/CrossToolHelpers.h b/flang/include/flang/Tools/CrossToolHelpers.h
index 7bae6e9824795..2151911a170b5 100644
--- a/flang/include/flang/Tools/CrossToolHelpers.h
+++ b/flang/include/flang/Tools/CrossToolHelpers.h
@@ -46,6 +46,18 @@ class FlangEPCallBacks {
     FIROptLastEPCallbacks.push_back(C);
   }
 
+  void registerHLFIROptEarlyEPCallbacks(
+      const std::function<void(mlir::PassManager &, llvm::OptimizationLevel)>
+          &C) {
+    HLFIROptEarlyEPCallbacks.push_back(C);
+  }
+
+  void registerHLFIROptLastEPCallbacks(
+      const std::function<void(mlir::PassManager &, llvm::OptimizationLevel)>
+          &C) {
+    HLFIROptLastEPCallbacks.push_back(C);
+  }
+
   void invokeFIROptEarlyEPCallbacks(
       mlir::PassManager &pm, llvm::OptimizationLevel optLevel) {
     for (auto &C : FIROptEarlyEPCallbacks)
@@ -64,6 +76,19 @@ class FlangEPCallBacks {
       C(pm, optLevel);
   };
 
+  // Const so they can be invoked on the const config the HLFIR pipeline takes.
+  void invokeHLFIROptEarlyEPCallbacks(
+      mlir::PassManager &pm, llvm::OptimizationLevel optLevel) const {
+    for (auto &C : HLFIROptEarlyEPCallbacks)
+      C(pm, optLevel);
+  };
+
+  void invokeHLFIROptLastEPCallbacks(
+      mlir::PassManager &pm, llvm::OptimizationLevel optLevel) const {
+    for (auto &C : HLFIROptLastEPCallbacks)
+      C(pm, optLevel);
+  };
+
 private:
   llvm::SmallVector<
       std::function<void(mlir::PassManager &, llvm::OptimizationLevel)>, 1>
@@ -76,6 +101,14 @@ class FlangEPCallBacks {
   llvm::SmallVector<
       std::function<void(mlir::PassManager &, llvm::OptimizationLevel)>, 1>
       FIROptLastEPCallbacks;
+
+  llvm::SmallVector<
+      std::function<void(mlir::PassManager &, llvm::OptimizationLevel)>, 1>
+      HLFIROptEarlyEPCallbacks;
+
+  llvm::SmallVector<
+      std::function<void(mlir::PassManager &, llvm::OptimizationLevel)>, 1>
+      HLFIROptLastEPCallbacks;
 };
 
 /// Configuriation for the MLIR to LLVM pass pipeline.
diff --git a/flang/lib/Optimizer/Passes/Pipelines.cpp b/flang/lib/Optimizer/Passes/Pipelines.cpp
index ba346bfb62111..48ae9f3bea58c 100644
--- a/flang/lib/Optimizer/Passes/Pipelines.cpp
+++ b/flang/lib/Optimizer/Passes/Pipelines.cpp
@@ -263,6 +263,10 @@ void createHLFIRToFIRPassPipeline(mlir::PassManager &pm,
                                   EnableOpenMP enableOpenMP,
                                   const MLIRToLLVMPassPipelineConfig &config) {
   llvm::OptimizationLevel optLevel = config.OptLevel;
+
+  // Early HLFIR Optimizer EP Callback
+  config.invokeHLFIROptEarlyEPCallbacks(pm, optLevel);
+
   if (optLevel != llvm::OptimizationLevel::O0) {
     addNestedPassToAllTopLevelOperations<PassConstructor>(
         pm, hlfir::createExpressionSimplification);
@@ -310,6 +314,10 @@ void createHLFIRToFIRPassPipeline(mlir::PassManager &pm,
   }
   pm.addPass(hlfir::createLowerHLFIROrderedAssignments(
       {/*tryFusingAssignments=*/optLevel != llvm::OptimizationLevel::O0}));
+
+  // Last HLFIR Optimizer EP Callback
+  config.invokeHLFIROptLastEPCallbacks(pm, optLevel);
+
   pm.addPass(hlfir::createLowerHLFIRIntrinsics());
 
   hlfir::BufferizeHLFIROptions bufferizeOptions;
diff --git a/flang/unittests/Optimizer/CMakeLists.txt b/flang/unittests/Optimizer/CMakeLists.txt
index 6f83b2ac268da..0e0c12b21bb56 100644
--- a/flang/unittests/Optimizer/CMakeLists.txt
+++ b/flang/unittests/Optimizer/CMakeLists.txt
@@ -19,6 +19,7 @@ set(LIBS
   FIRTransforms
   HLFIRDialect
   MIFDialect
+  flangPasses
 )
 
 add_flang_unittest(FlangOptimizerTests
@@ -44,6 +45,7 @@ add_flang_unittest(FlangOptimizerTests
   FIRContextTest.cpp
   FIRTypesTest.cpp
   FortranVariableTest.cpp
+  HLFIRExtensionPointsTest.cpp
   InternalNamesTest.cpp
   KindMappingTest.cpp
   RTBuilder.cpp
diff --git a/flang/unittests/Optimizer/HLFIRExtensionPointsTest.cpp b/flang/unittests/Optimizer/HLFIRExtensionPointsTest.cpp
new file mode 100644
index 0000000000000..2dc17b458ea9a
--- /dev/null
+++ b/flang/unittests/Optimizer/HLFIRExtensionPointsTest.cpp
@@ -0,0 +1,141 @@
+//===- HLFIRExtensionPointsTest.cpp ---------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Tests for the HLFIR extension points of the HLFIR-to-FIR pass pipeline.
+//
+// The callbacks run when the pipeline is built, so no IR is needed: building
+// the pipeline is enough to observe them.
+//
+//===----------------------------------------------------------------------===//
+
+#include "gtest/gtest.h"
+#include "mlir/IR/BuiltinOps.h"
+#include "mlir/IR/MLIRContext.h"
+#include "mlir/Pass/Pass.h"
+#include "mlir/Pass/PassManager.h"
+#include "flang/Optimizer/Passes/Pipelines.h"
+#include "flang/Tools/CrossToolHelpers.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/raw_ostream.h"
+#include <string>
+#include <vector>
+
+namespace {
+
+/// A no-op pass, identifiable by name in a textual pipeline, used to locate an
+/// extension point.
+struct MarkerPass : public mlir::PassWrapper<MarkerPass,
+                        mlir::OperationPass<mlir::ModuleOp>> {
+  MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(MarkerPass)
+
+  llvm::StringRef getArgument() const override { return "ep-marker"; }
+  llvm::StringRef getDescription() const override {
+    return "No-op pass used to locate an extension point in a pipeline";
+  }
+  void runOnOperation() override {}
+};
+
+// Both callbacks are invoked, Early before Last.
+TEST(HLFIRExtensionPoint, CallbacksAreInvokedInOrder) {
+  mlir::MLIRContext context;
+  mlir::PassManager pm(&context, mlir::ModuleOp::getOperationName());
+  MLIRToLLVMPassPipelineConfig config(llvm::OptimizationLevel::O2);
+
+  std::vector<std::string> order;
+  size_t earlySizeAtCall = ~size_t{0}; // sentinel
+
+  config.registerHLFIROptEarlyEPCallbacks(
+      [&](mlir::PassManager &nestedPm, llvm::OptimizationLevel) {
+        order.push_back("early");
+        earlySizeAtCall = nestedPm.size();
+      });
+  config.registerHLFIROptLastEPCallbacks(
+      [&](mlir::PassManager &, llvm::OptimizationLevel) {
+        order.push_back("last");
+      });
+
+  fir::createHLFIRToFIRPassPipeline(pm, fir::EnableOpenMP::None, config);
+
+  ASSERT_EQ(order.size(), 2u);
+  EXPECT_EQ(order[0], "early");
+  EXPECT_EQ(order[1], "last");
+  // Early runs before anything has been added to the pipeline.
+  EXPECT_EQ(earlySizeAtCall, 0u);
+  EXPECT_GT(pm.size(), 0u);
+}
+
+// The callbacks fire at every level, including O0 where the simplification
+// passes are skipped.
+TEST(HLFIRExtensionPoint, CallbacksAreInvokedAtEveryOptLevel) {
+  for (llvm::OptimizationLevel level :
+      {llvm::OptimizationLevel::O0, llvm::OptimizationLevel::O1,
+          llvm::OptimizationLevel::O2, llvm::OptimizationLevel::O3}) {
+    mlir::MLIRContext context;
+    mlir::PassManager pm(&context, mlir::ModuleOp::getOperationName());
+    MLIRToLLVMPassPipelineConfig config(level);
+
+    int earlyCount = 0;
+    int lastCount = 0;
+    llvm::OptimizationLevel seenLevel = llvm::OptimizationLevel::O0;
+    config.registerHLFIROptEarlyEPCallbacks(
+        [&](mlir::PassManager &, llvm::OptimizationLevel cbLevel) {
+          ++earlyCount;
+          seenLevel = cbLevel;
+        });
+    config.registerHLFIROptLastEPCallbacks(
+        [&](mlir::PassManager &, llvm::OptimizationLevel) { ++lastCount; });
+
+    fir::createHLFIRToFIRPassPipeline(pm, fir::EnableOpenMP::None, config);
+
+    EXPECT_EQ(earlyCount, 1);
+    EXPECT_EQ(lastCount, 1);
+    // The callback is handed the level the pipeline was configured with.
+    EXPECT_EQ(seenLevel, level);
+  }
+}
+
+// Early must precede simplify-hlfir-intrinsics and Last must sit between it and
+// lower-hlfir-intrinsics. Asserting on the textual pipeline means a reordering
+// of createHLFIRToFIRPassPipeline breaks this test.
+TEST(HLFIRExtensionPoint, MarkersAreAtTheDocumentedPositions) {
+  mlir::MLIRContext context;
+  mlir::PassManager pm(&context, mlir::ModuleOp::getOperationName());
+  MLIRToLLVMPassPipelineConfig config(llvm::OptimizationLevel::O2);
+
+  // A MarkerPass at each extension point locates it in the textual pipeline.
+  config.registerHLFIROptEarlyEPCallbacks(
+      [](mlir::PassManager &nestedPm, llvm::OptimizationLevel) {
+        nestedPm.addPass(std::make_unique<MarkerPass>());
+      });
+  config.registerHLFIROptLastEPCallbacks(
+      [](mlir::PassManager &nestedPm, llvm::OptimizationLevel) {
+        nestedPm.addPass(std::make_unique<MarkerPass>());
+      });
+
+  fir::createHLFIRToFIRPassPipeline(pm, fir::EnableOpenMP::None, config);
+
+  std::string pipeline;
+  llvm::raw_string_ostream os(pipeline);
+  pm.printAsTextualPipeline(os);
+
+  size_t earlyMarker = pipeline.find("ep-marker");
+  ASSERT_NE(earlyMarker, std::string::npos) << pipeline;
+  size_t lastMarker = pipeline.find("ep-marker", earlyMarker + 1);
+  ASSERT_NE(lastMarker, std::string::npos) << pipeline;
+
+  size_t simplify = pipeline.find("simplify-hlfir-intrinsics");
+  ASSERT_NE(simplify, std::string::npos) << pipeline;
+  size_t lowerIntrinsics = pipeline.find("lower-hlfir-intrinsics");
+  ASSERT_NE(lowerIntrinsics, std::string::npos) << pipeline;
+
+  EXPECT_LT(earlyMarker, simplify) << pipeline;
+  EXPECT_GT(lastMarker, simplify) << pipeline;
+  EXPECT_LT(lastMarker, lowerIntrinsics) << pipeline;
+}
+
+} // namespace



More information about the flang-commits mailing list