https://github.com/vchuravy updated https://github.com/llvm/llvm-project/pull/212196
>From da46b327a32317665294700c6f0ec238eed3279d Mon Sep 17 00:00:00 2001 From: Valentin Churavy <[email protected]> Date: Sun, 26 Jul 2026 23:53:30 +0200 Subject: [PATCH] [flang] Add an example plugin exercising the HLFIR pipeline extension points The HLFIR extension points, the config augmentor registry and fir-opt's symbol export exist to let an out-of-tree MLIR pass run while the HLFIR intrinsic operations (hlfir.sum, hlfir.matmul, ...) are still present. None of that was covered end to end, and there was no worked example of how to use it. Add flang/examples/HLFIRPipelinePlugin, modelled on PrintFlangFunctionNames. It contributes a pass that prints the HLFIR operations still present in the module, tagged with the pipeline position it was inserted at, and exposes it through both plugin entry points: a static initializer calling fir::registerPassPipelineConfigCallback for the `flang -fc1 -load` path, and mlirGetPassPluginInfo so fir-opt can load it with --load-pass-plugin. Nothing is linked into the shared object: MLIR, FIR and flang symbols resolve against the host tool, which is what export_executable_symbols_for_plugins on flang and fir-opt provides. Removing the fir-opt export drops it from ~105k exported dynamic symbols to one and makes --load-pass-plugin fail to load. Two tests use it. Examples/hlfir-pipeline-plugin.f90 covers -emit-fir and -emit-llvm, plus a no-plugin negative check; at -O0 the intrinsics reach both extension points, while at -O2 SimplifyHLFIRIntrinsics has expanded them before the Last one, which is why both exist. Examples/fir-opt-pass-plugin.fir covers --load-pass-plugin and the pass showing up in fir-opt's registry. Co-Authored-By: Claude Opus 5 <[email protected]> --- flang/docs/FlangDriver.md | 17 +++ flang/examples/CMakeLists.txt | 1 + .../HLFIRPipelinePlugin/CMakeLists.txt | 15 +++ .../HLFIRPipelinePlugin.cpp | 115 ++++++++++++++++++ flang/test/CMakeLists.txt | 1 + flang/test/Examples/fir-opt-pass-plugin.fir | 36 ++++++ flang/test/Examples/hlfir-pipeline-plugin.f90 | 53 ++++++++ 7 files changed, 238 insertions(+) create mode 100644 flang/examples/HLFIRPipelinePlugin/CMakeLists.txt create mode 100644 flang/examples/HLFIRPipelinePlugin/HLFIRPipelinePlugin.cpp create mode 100644 flang/test/Examples/fir-opt-pass-plugin.fir create mode 100644 flang/test/Examples/hlfir-pipeline-plugin.f90 diff --git a/flang/docs/FlangDriver.md b/flang/docs/FlangDriver.md index 55b4534cbffbe..f2d6bf06c42ab 100644 --- a/flang/docs/FlangDriver.md +++ b/flang/docs/FlangDriver.md @@ -583,6 +583,23 @@ 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. +`flang/examples/HLFIRPipelinePlugin` is a complete working example, exercised by +`flang/test/Examples/hlfir-pipeline-plugin.f90`. + +### Loading MLIR Pass Plugins into `fir-opt` + +`fir-opt` is built on `MlirOptMain` and therefore accepts MLIR's +`--load-pass-plugin` and `--load-dialect-plugin` options. As with `mlir-opt`, +this requires the tool to export its symbols so that the plugin can resolve the +MLIR, FIR and HLFIR symbols it uses against the host +(`export_executable_symbols_for_plugins` in +`flang/tools/fir-opt/CMakeLists.txt`): + +```bash +fir-opt --load-pass-plugin=./MyPasses.so \ + --pass-pipeline='builtin.module(my-hlfir-pass)' input.fir +``` + ## LLVM Pass Plugins Pass plugins are dynamic shared objects that consist of one or more LLVM IR diff --git a/flang/examples/CMakeLists.txt b/flang/examples/CMakeLists.txt index 746a6f23ae015..c91cfd82aff9e 100644 --- a/flang/examples/CMakeLists.txt +++ b/flang/examples/CMakeLists.txt @@ -1,3 +1,4 @@ +add_subdirectory(HLFIRPipelinePlugin) add_subdirectory(PrintFlangFunctionNames) add_subdirectory(FlangOmpReport) add_subdirectory(FeatureList) diff --git a/flang/examples/HLFIRPipelinePlugin/CMakeLists.txt b/flang/examples/HLFIRPipelinePlugin/CMakeLists.txt new file mode 100644 index 0000000000000..3cf00a9641e34 --- /dev/null +++ b/flang/examples/HLFIRPipelinePlugin/CMakeLists.txt @@ -0,0 +1,15 @@ +# TODO: Note that this is currently only available on Linux. +# On Windows, we would also have to specify e.g. `PLUGIN_TOOL`. +# +# Nothing is linked in here on purpose: the symbols the plugin uses are resolved +# against the host tool (`flang -fc1` or `fir-opt`) when the shared object is +# loaded, which is why both export their symbols for plugins. +add_llvm_example_library(flangHLFIRPipelinePlugin + MODULE + HLFIRPipelinePlugin.cpp + + DEPENDS + acc_gen + flangPasses + omp_gen +) diff --git a/flang/examples/HLFIRPipelinePlugin/HLFIRPipelinePlugin.cpp b/flang/examples/HLFIRPipelinePlugin/HLFIRPipelinePlugin.cpp new file mode 100644 index 0000000000000..3bdf10ce0fc72 --- /dev/null +++ b/flang/examples/HLFIRPipelinePlugin/HLFIRPipelinePlugin.cpp @@ -0,0 +1,115 @@ +//===-- HLFIRPipelinePlugin.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 +// +//===----------------------------------------------------------------------===// +// +// Example plugin showing how an out-of-tree MLIR pass can be added to flang's +// HLFIR-to-FIR pass pipeline, while the HLFIR intrinsic operations (hlfir.sum, +// hlfir.matmul, ...) are still present. It contributes a pass that prints those +// operations, tagged with the pipeline position it was inserted at. +// +// The pass is exposed through both plugin entry points. For `flang -fc1 -load` +// a static initializer calls fir::registerPassPipelineConfigCallback to reach +// the config the frontend builds, and registers the pass at the HLFIROptEarly +// and HLFIROptLast extension points. For fir-opt, mlirGetPassPluginInfo makes +// it available to --load-pass-plugin: +// +// fir-opt --load-pass-plugin=./flangHLFIRPipelinePlugin.so \ +// --pass-pipeline='builtin.module(print-hlfir-intrinsics)' +// +// Nothing is linked into the shared object: MLIR, FIR and flang symbols resolve +// against the host tool. +// +//===----------------------------------------------------------------------===// + +#include "flang/Optimizer/Passes/Pipelines.h" +#include "flang/Tools/CrossToolHelpers.h" + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" +#include "mlir/Pass/PassManager.h" +#include "mlir/Pass/PassRegistry.h" +#include "mlir/Tools/Plugins/PassPlugin.h" + +#include "llvm/Config/llvm-config.h" +#include "llvm/Support/Compiler.h" +#include "llvm/Support/raw_ostream.h" + +namespace { + +/// Print every HLFIR operation still present in the module, tagged with a +/// caller-supplied label identifying the pipeline position. Reports on the +/// whole `hlfir` dialect rather than a hard-coded op list, so that the example +/// does not need to link the HLFIR dialect library. +struct PrintHLFIRIntrinsicsPass + : public mlir::PassWrapper<PrintHLFIRIntrinsicsPass, + mlir::OperationPass<mlir::ModuleOp>> { + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(PrintHLFIRIntrinsicsPass) + + PrintHLFIRIntrinsicsPass() = default; + explicit PrintHLFIRIntrinsicsPass(llvm::StringRef labelValue) { + label = labelValue.str(); + } + PrintHLFIRIntrinsicsPass(const PrintHLFIRIntrinsicsPass &other) + : mlir::PassWrapper<PrintHLFIRIntrinsicsPass, + mlir::OperationPass<mlir::ModuleOp>>(other) { + label = other.label; + } + + llvm::StringRef getArgument() const override { + return "print-hlfir-intrinsics"; + } + llvm::StringRef getDescription() const override { + return "Print the HLFIR operations that are still present in the module"; + } + + Option<std::string> label{*this, "label", + llvm::cl::desc("Tag printed with every line, identifying the pipeline " + "position this pass was inserted at"), + llvm::cl::init("hlfir")}; + + void runOnOperation() override { + llvm::outs() << "[" << label << "] begin\n"; + getOperation().walk([&](mlir::Operation *op) { + if (op->getName().getDialectNamespace() == "hlfir") { + llvm::outs() << "[" << label << "] " << op->getName().getStringRef() + << "\n"; + } + }); + llvm::outs() << "[" << label << "] end\n"; + } +}; + +/// Registers the pass at both HLFIR extension points. +struct FlangPipelineRegistration { + FlangPipelineRegistration() { + fir::registerPassPipelineConfigCallback( + [](MLIRToLLVMPassPipelineConfig &config) { + config.registerHLFIROptEarlyEPCallbacks( + [](mlir::PassManager &pm, llvm::OptimizationLevel) { + pm.addPass( + std::make_unique<PrintHLFIRIntrinsicsPass>("hlfir-early")); + }); + config.registerHLFIROptLastEPCallbacks( + [](mlir::PassManager &pm, llvm::OptimizationLevel) { + pm.addPass( + std::make_unique<PrintHLFIRIntrinsicsPass>("hlfir-last")); + }); + }); + } +}; + +// Runs when the shared object is dlopen'd, before any compilation starts. +static FlangPipelineRegistration flangPipelineRegistration; + +} // namespace + +/// Entry point used by fir-opt's --load-pass-plugin. +extern "C" LLVM_ATTRIBUTE_WEAK mlir::PassPluginLibraryInfo +mlirGetPassPluginInfo() { + return {MLIR_PLUGIN_API_VERSION, "HLFIRPipelinePlugin", LLVM_VERSION_STRING, + []() { mlir::PassRegistration<PrintHLFIRIntrinsicsPass>(); }}; +} diff --git a/flang/test/CMakeLists.txt b/flang/test/CMakeLists.txt index 50980a521b241..76e5b0809b31b 100644 --- a/flang/test/CMakeLists.txt +++ b/flang/test/CMakeLists.txt @@ -138,6 +138,7 @@ if (LLVM_INCLUDE_EXAMPLES) flangPrintFunctionNames flangOmpReport flangFeatureList + flangHLFIRPipelinePlugin ) endif () diff --git a/flang/test/Examples/fir-opt-pass-plugin.fir b/flang/test/Examples/fir-opt-pass-plugin.fir new file mode 100644 index 0000000000000..47e5016d1a203 --- /dev/null +++ b/flang/test/Examples/fir-opt-pass-plugin.fir @@ -0,0 +1,36 @@ +// Check that fir-opt can load an out-of-tree MLIR pass plugin. This needs +// fir-opt to export its symbols (export_executable_symbols_for_plugins in +// flang/tools/fir-opt/CMakeLists.txt); without it the shared object cannot +// resolve the MLIR/FIR symbols it uses and dlopen fails. +// This requires that the examples are built (LLVM_INCLUDE_EXAMPLES=ON) to +// access flangHLFIRPipelinePlugin.so + +// REQUIRES: plugins, examples +// XFAIL: system-aix + +// RUN: fir-opt --load-pass-plugin=%llvmshlibdir/flangHLFIRPipelinePlugin%pluginext \ +// RUN: --pass-pipeline='builtin.module(print-hlfir-intrinsics{label=fir-opt})' \ +// RUN: -o /dev/null %s | FileCheck %s + +// The plugin's pass must also show up in fir-opt's pass registry. +// RUN: fir-opt --load-pass-plugin=%llvmshlibdir/flangHLFIRPipelinePlugin%pluginext \ +// RUN: --help 2>&1 | FileCheck %s --check-prefix=HELP + +// CHECK: [fir-opt] begin +// CHECK: [fir-opt] hlfir.matmul +// CHECK: [fir-opt] hlfir.sum +// CHECK: [fir-opt] end + +// HELP: --print-hlfir-intrinsics + +func.func @test_hlfir_intrinsics(%a: !fir.ref<!fir.array<3x3xf32>>, + %b: !fir.ref<!fir.array<3x3xf32>>, + %c: !fir.ref<!fir.array<3x3xf32>>, + %s: !fir.ref<f32>) { + %0 = hlfir.matmul %a %b : (!fir.ref<!fir.array<3x3xf32>>, !fir.ref<!fir.array<3x3xf32>>) -> !hlfir.expr<3x3xf32> + hlfir.assign %0 to %c : !hlfir.expr<3x3xf32>, !fir.ref<!fir.array<3x3xf32>> + hlfir.destroy %0 : !hlfir.expr<3x3xf32> + %1 = hlfir.sum %a : (!fir.ref<!fir.array<3x3xf32>>) -> f32 + hlfir.assign %1 to %s : f32, !fir.ref<f32> + return +} diff --git a/flang/test/Examples/hlfir-pipeline-plugin.f90 b/flang/test/Examples/hlfir-pipeline-plugin.f90 new file mode 100644 index 0000000000000..0bbeefb6c9880 --- /dev/null +++ b/flang/test/Examples/hlfir-pipeline-plugin.f90 @@ -0,0 +1,53 @@ +! Check that a `flang -fc1 -load`'ed plugin can insert MLIR passes into the +! HLFIR-to-FIR pass pipeline through the HLFIR extension points. +! This requires that the examples are built (LLVM_INCLUDE_EXAMPLES=ON) to access +! flangHLFIRPipelinePlugin.so + +! REQUIRES: plugins, examples +! XFAIL: system-aix + +! At -O0 no HLFIR simplification runs, so the intrinsics reach both extension +! points. -emit-fir exercises CodeGenAction::lowerHLFIRToFIR. +! RUN: %flang_fc1 -load %llvmshlibdir/flangHLFIRPipelinePlugin%pluginext \ +! RUN: -emit-fir -o /dev/null %s 2>&1 | FileCheck %s + +! The same callbacks must fire on the -emit-llvm path, which builds its own +! config in CodeGenAction::generateLLVMIR. +! RUN: %flang_fc1 -load %llvmshlibdir/flangHLFIRPipelinePlugin%pluginext \ +! RUN: -emit-llvm -o /dev/null %s 2>&1 | FileCheck %s + +! At -O2 SimplifyHLFIRIntrinsics has expanded the intrinsics before the Last +! extension point, which is why both extension points exist. +! RUN: %flang_fc1 -load %llvmshlibdir/flangHLFIRPipelinePlugin%pluginext \ +! RUN: -O2 -emit-fir -o /dev/null %s 2>&1 | FileCheck %s --check-prefix=O2 + +! Without the plugin nothing is printed. +! RUN: %flang_fc1 -emit-fir -o /dev/null %s 2>&1 \ +! RUN: | FileCheck %s --check-prefix=NOPLUGIN --allow-empty + +! CHECK: [hlfir-early] begin +! CHECK: [hlfir-early] hlfir.matmul +! CHECK: [hlfir-early] hlfir.sum +! CHECK: [hlfir-early] end +! CHECK: [hlfir-last] begin +! CHECK: [hlfir-last] hlfir.matmul +! CHECK: [hlfir-last] hlfir.sum +! CHECK: [hlfir-last] end + +! O2: [hlfir-early] begin +! O2: [hlfir-early] hlfir.matmul +! O2: [hlfir-early] hlfir.sum +! O2: [hlfir-early] end +! O2: [hlfir-last] begin +! O2-NOT: hlfir.matmul +! O2-NOT: hlfir.sum +! O2: [hlfir-last] end + +! NOPLUGIN-NOT: hlfir-early +! NOPLUGIN-NOT: hlfir-last + +subroutine test_hlfir_intrinsics(a, b, c, s) + real :: a(3,3), b(3,3), c(3,3), s + c = matmul(a, b) + s = sum(a) +end subroutine _______________________________________________ llvm-branch-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-branch-commits
