[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:02:51 PDT 2026


================
@@ -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;
----------------
vchuravy wrote:

This style is used in a couple of places, one example is https://github.com/llvm/llvm-project/blob/d2fcb0c9e11f8d8bedc45db0aeba9e0a53141fe5/flang/unittests/Optimizer/InternalNamesTest.cpp#L44-L45

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


More information about the flang-commits mailing list