[flang-commits] [flang] [flang] Rewrite boxproc ops inside module-level regions with -fsafe-trampoline (PR #232167)

Eugene Epshteyn via flang-commits flang-commits at lists.llvm.org
Wed Oct 14 19:49:53 PDT 2026


https://github.com/eugeneepshteyn created https://github.com/llvm/llvm-project/pull/232167

With `-fsafe-trampoline`, `BoxedProcedurePass` processes functions one by one (so that `TrampolineFree` can be inserted before each return) and then visits the remaining module-level operations — but only those top-level operations themselves, never the operations nested in their regions. A module or `SAVE` procedure pointer with an initializer is a `fir.global` whose initializer region contains a `fir.emboxproc`; that op survived the pass and codegen aborted with `not yet implemented: fir.emboxproc codegen` for any such procedure pointer, whether or not an internal procedure or a trampoline is involved. The default path's recursive module walk does not have this gap.

Walk the regions of the non-function module-level operations the way the default path does, skipping functions and their contents since they have already been processed (including functions nested in a `gpu.module`).

Tests: a `fir-opt` regression over `fir.global` initializers with null and non-null procedure targets, checked in both modes, and an integration test compiling module procedure pointers with `-fsafe-trampoline` to LLVM IR.

Fixes #223705.


>From 2d4123bcd9db132cad77fbe7ac5c062667295751 Mon Sep 17 00:00:00 2001
From: Eugene Epshteyn <eepshteyn at nvidia.com>
Date: Wed, 14 Oct 2026 19:45:41 -0700
Subject: [PATCH] [flang] Rewrite boxproc ops inside module-level regions with
 -fsafe-trampoline

With -fsafe-trampoline, BoxedProcedurePass processes functions one by one
(so that TrampolineFree can be inserted before each return) and then
visits the remaining module-level operations. It visited only those
top-level operations themselves, never the operations nested in their
regions. A module or SAVE procedure pointer with an initializer is a
fir.global whose initializer region contains a fir.emboxproc, so that op
survived the pass and codegen aborted with "not yet implemented:
fir.emboxproc codegen" for any such procedure pointer, whether or not an
internal procedure or a trampoline is involved. The default path's
recursive module walk does not have this gap.

Walk the regions of the non-function module-level operations the way the
default path does, skipping functions and their contents since they have
already been processed (including functions nested in a gpu.module).

This fixes issue 223705.
---
 .../lib/Optimizer/CodeGen/BoxedProcedure.cpp  | 14 +++++-
 .../Fir/boxproc-safe-trampoline-global.fir    | 48 +++++++++++++++++++
 .../safe-trampoline-procptr-global.f90        | 32 +++++++++++++
 3 files changed, 92 insertions(+), 2 deletions(-)
 create mode 100644 flang/test/Fir/boxproc-safe-trampoline-global.fir
 create mode 100644 flang/test/Integration/safe-trampoline-procptr-global.f90

diff --git a/flang/lib/Optimizer/CodeGen/BoxedProcedure.cpp b/flang/lib/Optimizer/CodeGen/BoxedProcedure.cpp
index 1ec365664b2958..535a1f629d62de 100644
--- a/flang/lib/Optimizer/CodeGen/BoxedProcedure.cpp
+++ b/flang/lib/Optimizer/CodeGen/BoxedProcedure.cpp
@@ -258,12 +258,22 @@ class BoxedProcedurePass
         [&](mlir::Operation *op) { processOp(op, rewriter, typeConverter); });
   }
 
-  /// Process non-function ops at module level (globals, etc.)
+  /// Process non-function ops at module level (globals, etc.), including the
+  /// ops nested in their regions: a procedure pointer with an initializer is a
+  /// fir.global whose initializer region contains a fir.emboxproc, and that
+  /// op has no codegen, so it must be rewritten here like the default path's
+  /// recursive walk does. Functions were already processed one by one above
+  /// (also those nested in a gpu.module), so their contents are skipped.
   void processModuleLevelOps(mlir::IRRewriter &rewriter,
                              BoxprocTypeRewriter &typeConverter) {
     for (auto &op : getModule().getBody()->getOperations())
       if (!mlir::isa<mlir::func::FuncOp>(op))
-        processOp(&op, rewriter, typeConverter);
+        op.walk([&](mlir::Operation *nested) {
+          if (mlir::isa<mlir::func::FuncOp>(nested) ||
+              nested->getParentOfType<mlir::func::FuncOp>())
+            return;
+          processOp(nested, rewriter, typeConverter);
+        });
   }
 
   /// Insert _FortranATrampolineFree calls before every return in the function.
diff --git a/flang/test/Fir/boxproc-safe-trampoline-global.fir b/flang/test/Fir/boxproc-safe-trampoline-global.fir
new file mode 100644
index 00000000000000..ba0161c4ed1e14
--- /dev/null
+++ b/flang/test/Fir/boxproc-safe-trampoline-global.fir
@@ -0,0 +1,48 @@
+// RUN: fir-opt --boxed-procedure="use-safe-trampoline=true" %s | FileCheck %s
+// RUN: fir-opt --boxed-procedure %s | FileCheck %s
+
+// A module or SAVE procedure pointer with an initializer is lowered to a
+// fir.global whose initializer region emboxes a procedure. The boxed-procedure
+// pass must rewrite the ops inside that region in both modes: fir.emboxproc has
+// no codegen, and the safe-trampoline path used to visit only the fir.global
+// op itself, leaving the initializer untouched and the global's type
+// inconsistent with its fir.has_value.
+
+// CHECK-LABEL: fir.global @_QMmEnull_ptr : () -> () {
+// CHECK:         %[[ZERO:.*]] = fir.zero_bits () -> ()
+// CHECK:         %[[NULL:.*]] = fir.convert %[[ZERO]] : (() -> ()) -> (() -> ())
+// CHECK:         fir.has_value %[[NULL]] : () -> ()
+// CHECK-NOT:     fir.emboxproc
+fir.global @_QMmEnull_ptr : !fir.boxproc<() -> ()> {
+  %0 = fir.zero_bits () -> ()
+  %1 = fir.emboxproc %0 : (() -> ()) -> !fir.boxproc<() -> ()>
+  fir.has_value %1 : !fir.boxproc<() -> ()>
+}
+
+// CHECK-LABEL: fir.global @_QMmEtarget_ptr : () -> () {
+// CHECK:         %[[ADDR:.*]] = fir.address_of(@_QMmPcallee) : () -> ()
+// CHECK:         %[[FUNC:.*]] = fir.convert %[[ADDR]] : (() -> ()) -> (() -> ())
+// CHECK:         fir.has_value %[[FUNC]] : () -> ()
+// CHECK-NOT:     fir.emboxproc
+fir.global @_QMmEtarget_ptr : !fir.boxproc<() -> ()> {
+  %0 = fir.address_of(@_QMmPcallee) : () -> ()
+  %1 = fir.emboxproc %0 : (() -> ()) -> !fir.boxproc<() -> ()>
+  fir.has_value %1 : !fir.boxproc<() -> ()>
+}
+
+// CHECK-LABEL: func.func @_QMmPcallee()
+func.func @_QMmPcallee() {
+  return
+}
+
+// CHECK-LABEL: func.func @_QMmPcall_through_ptr()
+// CHECK:         %[[REF:.*]] = fir.address_of(@_QMmEtarget_ptr) : !fir.ref<() -> ()>
+// CHECK:         %[[LOAD:.*]] = fir.load %[[REF]] : !fir.ref<() -> ()>
+// CHECK:         fir.call %[[LOAD]]() : () -> ()
+func.func @_QMmPcall_through_ptr() {
+  %0 = fir.address_of(@_QMmEtarget_ptr) : !fir.ref<!fir.boxproc<() -> ()>>
+  %1 = fir.load %0 : !fir.ref<!fir.boxproc<() -> ()>>
+  %2 = fir.box_addr %1 : (!fir.boxproc<() -> ()>) -> (() -> ())
+  fir.call %2() : () -> ()
+  return
+}
diff --git a/flang/test/Integration/safe-trampoline-procptr-global.f90 b/flang/test/Integration/safe-trampoline-procptr-global.f90
new file mode 100644
index 00000000000000..9afff1ba7483fb
--- /dev/null
+++ b/flang/test/Integration/safe-trampoline-procptr-global.f90
@@ -0,0 +1,32 @@
+!===----------------------------------------------------------------------===!
+! This directory can be used to add Integration tests involving multiple
+! stages of the compiler (for eg. from Fortran to LLVM IR). It should not
+! contain executable tests. We should only add tests here sparingly and only
+! if there is no other way to test. Repeat this message in each test that is
+! added to this directory and sub-directories.
+!===----------------------------------------------------------------------===!
+
+! RUN: %flang_fc1 -fsafe-trampoline -emit-llvm -o - %s | FileCheck %s
+!
+! A module procedure pointer with an initializer is a global whose initializer
+! emboxes a procedure. With -fsafe-trampoline the boxed-procedure pass used to
+! leave that fir.emboxproc in place, and codegen aborted with
+! "not yet implemented: fir.emboxproc codegen". No internal procedure or
+! trampoline is involved: the globals must simply lower to plain pointers.
+
+! CHECK: @_QMmEnull_ptr = {{.*}}global ptr null
+! CHECK: @_QMmEtarget_ptr = {{.*}}global ptr @_QMmPcallee
+! CHECK-NOT: TrampolineInit
+
+module m
+  implicit none
+  abstract interface
+    subroutine callback()
+    end subroutine
+  end interface
+  procedure(callback), pointer :: null_ptr => null()
+  procedure(callback), pointer :: target_ptr => callee
+contains
+  subroutine callee()
+  end subroutine
+end module



More information about the flang-commits mailing list