[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 06:59:07 PDT 2026


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

>From fd2a46ba9d36615b00c72c14dbd1429d43759e1f 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 1/4] [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 -- for example automatic differentiation via
Enzyme-MLIR -- 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.

The tests assert on printAsTextualPipeline output rather than just on the
callbacks firing, so that a reordering of the pipeline breaks them, and cover
every optimization level.

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

diff --git a/flang/docs/FlangDriver.md b/flang/docs/FlangDriver.md
index 4edc99944ad44..a4385f68c6f0d 100644
--- a/flang/docs/FlangDriver.md
+++ b/flang/docs/FlangDriver.md
@@ -525,6 +525,31 @@ 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 (for example
+automatic differentiation), 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..678033d131b5a 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
@@ -46,6 +47,7 @@ add_flang_unittest(FlangOptimizerTests
   FortranVariableTest.cpp
   InternalNamesTest.cpp
   KindMappingTest.cpp
+  PassPipelineTest.cpp
   RTBuilder.cpp
 DEPENDS
   CUFDialect
diff --git a/flang/unittests/Optimizer/PassPipelineTest.cpp b/flang/unittests/Optimizer/PassPipelineTest.cpp
new file mode 100644
index 0000000000000..8a6690504c361
--- /dev/null
+++ b/flang/unittests/Optimizer/PassPipelineTest.cpp
@@ -0,0 +1,223 @@
+//===- PassPipelineTest.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 "mlir/Transforms/Passes.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 {}
+};
+
+/// Render \p pm as a textual pipeline.
+std::string pipelineAsString(mlir::PassManager &pm) {
+  std::string out;
+  llvm::raw_string_ostream os(out);
+  pm.printAsTextualPipeline(os);
+  return out;
+}
+
+/// Build a HLFIR-to-FIR pipeline at \p level with a MarkerPass injected at each
+/// requested extension point, and return it as a textual pipeline.
+std::string pipelineWithMarkers(
+    llvm::OptimizationLevel level, bool early, bool last) {
+  mlir::MLIRContext context;
+  mlir::PassManager pm(&context, mlir::ModuleOp::getOperationName());
+  MLIRToLLVMPassPipelineConfig config(level);
+  if (early) {
+    config.registerHLFIROptEarlyEPCallbacks(
+        [](mlir::PassManager &nestedPm, llvm::OptimizationLevel) {
+          nestedPm.addPass(std::make_unique<MarkerPass>());
+        });
+  }
+  if (last) {
+    config.registerHLFIROptLastEPCallbacks(
+        [](mlir::PassManager &nestedPm, llvm::OptimizationLevel) {
+          nestedPm.addPass(std::make_unique<MarkerPass>());
+        });
+  }
+  fir::createHLFIRToFIRPassPipeline(pm, fir::EnableOpenMP::None, config);
+  return pipelineAsString(pm);
+}
+
+// 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) {
+  std::string pipeline = pipelineWithMarkers(llvm::OptimizationLevel::O2,
+      /*early=*/true, /*last=*/true);
+
+  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;
+}
+
+// At O0 the simplification passes are absent, but Last must still precede
+// lower-hlfir-intrinsics.
+TEST(HLFIRExtensionPoint, LastMarkerPrecedesLoweringAtO0) {
+  std::string pipeline = pipelineWithMarkers(llvm::OptimizationLevel::O0,
+      /*early=*/false, /*last=*/true);
+
+  size_t marker = pipeline.find("ep-marker");
+  ASSERT_NE(marker, std::string::npos) << pipeline;
+  size_t lowerIntrinsics = pipeline.find("lower-hlfir-intrinsics");
+  ASSERT_NE(lowerIntrinsics, std::string::npos) << pipeline;
+  EXPECT_LT(marker, lowerIntrinsics) << pipeline;
+}
+
+// A callback may add passes at the extension point.
+TEST(HLFIRExtensionPoint, CallbackCanAddPasses) {
+  mlir::MLIRContext context;
+  mlir::PassManager pm(&context, mlir::ModuleOp::getOperationName());
+  MLIRToLLVMPassPipelineConfig config(llvm::OptimizationLevel::O0);
+
+  size_t sizeBefore = ~size_t{0};
+  size_t sizeAfter = ~size_t{0};
+  config.registerHLFIROptEarlyEPCallbacks(
+      [&](mlir::PassManager &nestedPm, llvm::OptimizationLevel) {
+        sizeBefore = nestedPm.size();
+        nestedPm.addPass(mlir::createCanonicalizerPass());
+        sizeAfter = nestedPm.size();
+      });
+
+  fir::createHLFIRToFIRPassPipeline(pm, fir::EnableOpenMP::None, config);
+
+  EXPECT_EQ(sizeBefore, 0u);
+  EXPECT_EQ(sizeAfter, 1u);
+}
+
+// Callbacks at the same extension point run in registration order.
+TEST(HLFIRExtensionPoint, MultipleCallbacksRunInRegistrationOrder) {
+  mlir::MLIRContext context;
+  mlir::PassManager pm(&context, mlir::ModuleOp::getOperationName());
+  MLIRToLLVMPassPipelineConfig config(llvm::OptimizationLevel::O2);
+
+  std::vector<int> order;
+  for (int i = 0; i < 3; ++i) {
+    config.registerHLFIROptEarlyEPCallbacks(
+        [&order, i](mlir::PassManager &, llvm::OptimizationLevel) {
+          order.push_back(i);
+        });
+  }
+
+  fir::createHLFIRToFIRPassPipeline(pm, fir::EnableOpenMP::None, config);
+
+  EXPECT_EQ(order, (std::vector<int>{0, 1, 2}));
+}
+
+// With no callbacks registered the pipeline is unchanged.
+TEST(HLFIRExtensionPoint, NoCallbacksIsNoOp) {
+  mlir::MLIRContext context;
+  mlir::PassManager pm(&context, mlir::ModuleOp::getOperationName());
+  MLIRToLLVMPassPipelineConfig config(llvm::OptimizationLevel::O2);
+
+  fir::createHLFIRToFIRPassPipeline(pm, fir::EnableOpenMP::None, config);
+
+  EXPECT_GT(pm.size(), 0u);
+  EXPECT_EQ(pipelineAsString(pm),
+      pipelineWithMarkers(llvm::OptimizationLevel::O2, /*early=*/false,
+          /*last=*/false));
+}
+
+} // namespace

>From a5f0cbfddf9e88a4e5fe9c6dca63259fc1efc550 Mon Sep 17 00:00:00 2001
From: Valentin Churavy <v.churavy at gmail.com>
Date: Mon, 3 Aug 2026 15:47:10 +0200
Subject: [PATCH 2/4] Rename to HLFIRExtensionPointsTest

---
 flang/unittests/Optimizer/CMakeLists.txt      |   2 +-
 ...eTest.cpp => HLFIRExtensionPointsTest.cpp} | 112 +++---------------
 2 files changed, 18 insertions(+), 96 deletions(-)
 rename flang/unittests/Optimizer/{PassPipelineTest.cpp => HLFIRExtensionPointsTest.cpp} (61%)

diff --git a/flang/unittests/Optimizer/CMakeLists.txt b/flang/unittests/Optimizer/CMakeLists.txt
index 678033d131b5a..0e0c12b21bb56 100644
--- a/flang/unittests/Optimizer/CMakeLists.txt
+++ b/flang/unittests/Optimizer/CMakeLists.txt
@@ -45,9 +45,9 @@ add_flang_unittest(FlangOptimizerTests
   FIRContextTest.cpp
   FIRTypesTest.cpp
   FortranVariableTest.cpp
+  HLFIRExtensionPointsTest.cpp
   InternalNamesTest.cpp
   KindMappingTest.cpp
-  PassPipelineTest.cpp
   RTBuilder.cpp
 DEPENDS
   CUFDialect
diff --git a/flang/unittests/Optimizer/PassPipelineTest.cpp b/flang/unittests/Optimizer/HLFIRExtensionPointsTest.cpp
similarity index 61%
rename from flang/unittests/Optimizer/PassPipelineTest.cpp
rename to flang/unittests/Optimizer/HLFIRExtensionPointsTest.cpp
index 8a6690504c361..9c0660d31eab4 100644
--- a/flang/unittests/Optimizer/PassPipelineTest.cpp
+++ b/flang/unittests/Optimizer/HLFIRExtensionPointsTest.cpp
@@ -1,4 +1,4 @@
-//===- PassPipelineTest.cpp -----------------------------------------------===//
+//===- 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.
@@ -18,7 +18,6 @@
 #include "mlir/IR/MLIRContext.h"
 #include "mlir/Pass/Pass.h"
 #include "mlir/Pass/PassManager.h"
-#include "mlir/Transforms/Passes.h"
 #include "flang/Optimizer/Passes/Pipelines.h"
 #include "flang/Tools/CrossToolHelpers.h"
 #include "llvm/ADT/StringRef.h"
@@ -41,35 +40,26 @@ struct MarkerPass : public mlir::PassWrapper<MarkerPass,
   void runOnOperation() override {}
 };
 
-/// Render \p pm as a textual pipeline.
-std::string pipelineAsString(mlir::PassManager &pm) {
-  std::string out;
-  llvm::raw_string_ostream os(out);
-  pm.printAsTextualPipeline(os);
-  return out;
-}
-
 /// Build a HLFIR-to-FIR pipeline at \p level with a MarkerPass injected at each
-/// requested extension point, and return it as a textual pipeline.
-std::string pipelineWithMarkers(
-    llvm::OptimizationLevel level, bool early, bool last) {
+/// extension point, and return it as a textual pipeline.
+std::string pipelineWithMarkers(llvm::OptimizationLevel level) {
   mlir::MLIRContext context;
   mlir::PassManager pm(&context, mlir::ModuleOp::getOperationName());
   MLIRToLLVMPassPipelineConfig config(level);
-  if (early) {
-    config.registerHLFIROptEarlyEPCallbacks(
-        [](mlir::PassManager &nestedPm, llvm::OptimizationLevel) {
-          nestedPm.addPass(std::make_unique<MarkerPass>());
-        });
-  }
-  if (last) {
-    config.registerHLFIROptLastEPCallbacks(
-        [](mlir::PassManager &nestedPm, llvm::OptimizationLevel) {
-          nestedPm.addPass(std::make_unique<MarkerPass>());
-        });
-  }
+  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);
-  return pipelineAsString(pm);
+
+  std::string out;
+  llvm::raw_string_ostream os(out);
+  pm.printAsTextualPipeline(os);
+  return out;
 }
 
 // Both callbacks are invoked, Early before Last.
@@ -135,8 +125,7 @@ TEST(HLFIRExtensionPoint, CallbacksAreInvokedAtEveryOptLevel) {
 // lower-hlfir-intrinsics. Asserting on the textual pipeline means a reordering
 // of createHLFIRToFIRPassPipeline breaks this test.
 TEST(HLFIRExtensionPoint, MarkersAreAtTheDocumentedPositions) {
-  std::string pipeline = pipelineWithMarkers(llvm::OptimizationLevel::O2,
-      /*early=*/true, /*last=*/true);
+  std::string pipeline = pipelineWithMarkers(llvm::OptimizationLevel::O2);
 
   size_t earlyMarker = pipeline.find("ep-marker");
   ASSERT_NE(earlyMarker, std::string::npos) << pipeline;
@@ -153,71 +142,4 @@ TEST(HLFIRExtensionPoint, MarkersAreAtTheDocumentedPositions) {
   EXPECT_LT(lastMarker, lowerIntrinsics) << pipeline;
 }
 
-// At O0 the simplification passes are absent, but Last must still precede
-// lower-hlfir-intrinsics.
-TEST(HLFIRExtensionPoint, LastMarkerPrecedesLoweringAtO0) {
-  std::string pipeline = pipelineWithMarkers(llvm::OptimizationLevel::O0,
-      /*early=*/false, /*last=*/true);
-
-  size_t marker = pipeline.find("ep-marker");
-  ASSERT_NE(marker, std::string::npos) << pipeline;
-  size_t lowerIntrinsics = pipeline.find("lower-hlfir-intrinsics");
-  ASSERT_NE(lowerIntrinsics, std::string::npos) << pipeline;
-  EXPECT_LT(marker, lowerIntrinsics) << pipeline;
-}
-
-// A callback may add passes at the extension point.
-TEST(HLFIRExtensionPoint, CallbackCanAddPasses) {
-  mlir::MLIRContext context;
-  mlir::PassManager pm(&context, mlir::ModuleOp::getOperationName());
-  MLIRToLLVMPassPipelineConfig config(llvm::OptimizationLevel::O0);
-
-  size_t sizeBefore = ~size_t{0};
-  size_t sizeAfter = ~size_t{0};
-  config.registerHLFIROptEarlyEPCallbacks(
-      [&](mlir::PassManager &nestedPm, llvm::OptimizationLevel) {
-        sizeBefore = nestedPm.size();
-        nestedPm.addPass(mlir::createCanonicalizerPass());
-        sizeAfter = nestedPm.size();
-      });
-
-  fir::createHLFIRToFIRPassPipeline(pm, fir::EnableOpenMP::None, config);
-
-  EXPECT_EQ(sizeBefore, 0u);
-  EXPECT_EQ(sizeAfter, 1u);
-}
-
-// Callbacks at the same extension point run in registration order.
-TEST(HLFIRExtensionPoint, MultipleCallbacksRunInRegistrationOrder) {
-  mlir::MLIRContext context;
-  mlir::PassManager pm(&context, mlir::ModuleOp::getOperationName());
-  MLIRToLLVMPassPipelineConfig config(llvm::OptimizationLevel::O2);
-
-  std::vector<int> order;
-  for (int i = 0; i < 3; ++i) {
-    config.registerHLFIROptEarlyEPCallbacks(
-        [&order, i](mlir::PassManager &, llvm::OptimizationLevel) {
-          order.push_back(i);
-        });
-  }
-
-  fir::createHLFIRToFIRPassPipeline(pm, fir::EnableOpenMP::None, config);
-
-  EXPECT_EQ(order, (std::vector<int>{0, 1, 2}));
-}
-
-// With no callbacks registered the pipeline is unchanged.
-TEST(HLFIRExtensionPoint, NoCallbacksIsNoOp) {
-  mlir::MLIRContext context;
-  mlir::PassManager pm(&context, mlir::ModuleOp::getOperationName());
-  MLIRToLLVMPassPipelineConfig config(llvm::OptimizationLevel::O2);
-
-  fir::createHLFIRToFIRPassPipeline(pm, fir::EnableOpenMP::None, config);
-
-  EXPECT_GT(pm.size(), 0u);
-  EXPECT_EQ(pipelineAsString(pm),
-      pipelineWithMarkers(llvm::OptimizationLevel::O2, /*early=*/false,
-          /*last=*/false));
-}
-
 } // namespace

>From b5a28bd193f7547c1a515241d4351073288febf3 Mon Sep 17 00:00:00 2001
From: Valentin Churavy <v.churavy at gmail.com>
Date: Mon, 3 Aug 2026 15:51:26 +0200
Subject: [PATCH 3/4] fixup! Rename to HLFIRExtensionPointsTest

---
 flang/docs/FlangDriver.md | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/flang/docs/FlangDriver.md b/flang/docs/FlangDriver.md
index a4385f68c6f0d..07feba4f4674b 100644
--- a/flang/docs/FlangDriver.md
+++ b/flang/docs/FlangDriver.md
@@ -528,8 +528,7 @@ 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 (for example
-automatic differentiation), the HLFIR-to-FIR pass pipeline
+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,

>From 33cadbe887bf04314f15d38922fbfa9953321008 Mon Sep 17 00:00:00 2001
From: Valentin Churavy <v.churavy at gmail.com>
Date: Mon, 3 Aug 2026 15:58:46 +0200
Subject: [PATCH 4/4] fixup! Rename to HLFIRExtensionPointsTest

---
 .../Optimizer/HLFIRExtensionPointsTest.cpp    | 42 +++++++++----------
 1 file changed, 19 insertions(+), 23 deletions(-)

diff --git a/flang/unittests/Optimizer/HLFIRExtensionPointsTest.cpp b/flang/unittests/Optimizer/HLFIRExtensionPointsTest.cpp
index 9c0660d31eab4..2dc17b458ea9a 100644
--- a/flang/unittests/Optimizer/HLFIRExtensionPointsTest.cpp
+++ b/flang/unittests/Optimizer/HLFIRExtensionPointsTest.cpp
@@ -40,28 +40,6 @@ struct MarkerPass : public mlir::PassWrapper<MarkerPass,
   void runOnOperation() override {}
 };
 
-/// Build a HLFIR-to-FIR pipeline at \p level with a MarkerPass injected at each
-/// extension point, and return it as a textual pipeline.
-std::string pipelineWithMarkers(llvm::OptimizationLevel level) {
-  mlir::MLIRContext context;
-  mlir::PassManager pm(&context, mlir::ModuleOp::getOperationName());
-  MLIRToLLVMPassPipelineConfig config(level);
-  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 out;
-  llvm::raw_string_ostream os(out);
-  pm.printAsTextualPipeline(os);
-  return out;
-}
-
 // Both callbacks are invoked, Early before Last.
 TEST(HLFIRExtensionPoint, CallbacksAreInvokedInOrder) {
   mlir::MLIRContext context;
@@ -125,7 +103,25 @@ TEST(HLFIRExtensionPoint, CallbacksAreInvokedAtEveryOptLevel) {
 // lower-hlfir-intrinsics. Asserting on the textual pipeline means a reordering
 // of createHLFIRToFIRPassPipeline breaks this test.
 TEST(HLFIRExtensionPoint, MarkersAreAtTheDocumentedPositions) {
-  std::string pipeline = pipelineWithMarkers(llvm::OptimizationLevel::O2);
+  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;



More information about the flang-commits mailing list