[flang-commits] [flang] [flang][cuda] Allow host procedure references in host, device subprograms (PR #206736)

Eugene Epshteyn via flang-commits flang-commits at lists.llvm.org
Tue Jun 30 06:49:26 PDT 2026


https://github.com/eugeneepshteyn updated https://github.com/llvm/llvm-project/pull/206736

>From 7d06ca0019e533ea53d5ff1986b4264b94474191 Mon Sep 17 00:00:00 2001
From: Eugene Epshteyn <eepshteyn at nvidia.com>
Date: Mon, 29 Jun 2026 20:09:48 -0400
Subject: [PATCH] [flang][cuda] Allow host procedure references in host,device
 subprograms

An ATTRIBUTES(HOST,DEVICE) subprogram is compiled for the host as well as
the device, so referencing a host procedure (typically guarded at run time
by a test such as ON_DEVICE()) is legitimate in its host compilation.
Two semantic checks were wrongly rejecting such code:

* CheckSubprogram flagged an interface body declared inside a device
  subprogram as "may not be an internal procedure of CUDA device
  subprogram". An interface body (like a dummy procedure) is not an
  internal procedure, so exclude both from this check. A real internal
  procedure is still rejected.

* DeviceExprChecker rejected calls to host procedures with "may not be
  called in device code" even from host,device subprograms. Thread the
  existing isHostDevice flag of DeviceContextChecker down to
  DeviceExprChecker and suppress only that diagnostic when the enclosing
  subprogram is host,device. Pure device and global subprograms continue
  to reject host calls, and all other device-code diagnostics are
  preserved.
---
 flang/lib/Semantics/check-cuda.cpp            | 148 ++++++++++--------
 flang/lib/Semantics/check-declarations.cpp    |   2 +-
 .../CUDA/cuf-hostdevice-host-call.cuf         |  71 +++++++++
 3 files changed, 155 insertions(+), 66 deletions(-)
 create mode 100644 flang/test/Semantics/CUDA/cuf-hostdevice-host-call.cuf

diff --git a/flang/lib/Semantics/check-cuda.cpp b/flang/lib/Semantics/check-cuda.cpp
index 32ba35e83a120..a257a47d92ca9 100644
--- a/flang/lib/Semantics/check-cuda.cpp
+++ b/flang/lib/Semantics/check-cuda.cpp
@@ -74,7 +74,8 @@ struct DeviceExprChecker
     : public evaluate::AnyTraverse<DeviceExprChecker, MaybeMsg> {
   using Result = MaybeMsg;
   using Base = evaluate::AnyTraverse<DeviceExprChecker, Result>;
-  explicit DeviceExprChecker(SemanticsContext &c) : Base(*this), context_{c} {}
+  explicit DeviceExprChecker(SemanticsContext &c, bool allowHostCallees = false)
+      : Base(*this), context_{c}, allowHostCallees_{allowHostCallees} {}
   using Base::operator();
   Result operator()(const evaluate::ProcedureDesignator &x) const {
     if (const Symbol * sym{x.GetInterfaceSymbol()}) {
@@ -112,11 +113,18 @@ struct DeviceExprChecker
       return {};
     }
 
+    // A host,device subprogram is compiled for the host as well as the device,
+    // so a call to a host procedure (typically guarded at run time by a test
+    // such as ON_DEVICE()) is legitimate in its host compilation.
+    if (allowHostCallees_) {
+      return {};
+    }
     return parser::MessageFormattedText(
         "'%s' may not be called in device code"_err_en_US, x.GetName());
   }
 
   SemanticsContext &context_;
+  bool allowHostCallees_{false};
 };
 
 static bool IsHostArray(const Symbol &symbol) {
@@ -194,18 +202,20 @@ struct FindHostArray
 };
 
 template <typename A>
-static MaybeMsg CheckUnwrappedExpr(SemanticsContext &context, const A &x) {
+static MaybeMsg CheckUnwrappedExpr(
+    SemanticsContext &context, const A &x, bool allowHostCallees = false) {
   if (const auto *expr{parser::Unwrap<parser::Expr>(x)}) {
-    return DeviceExprChecker{context}(expr->typedExpr);
+    return DeviceExprChecker{context, allowHostCallees}(expr->typedExpr);
   }
   return {};
 }
 
 template <typename A>
-static void CheckUnwrappedExpr(
-    SemanticsContext &context, SourceName at, const A &x) {
+static void CheckUnwrappedExpr(SemanticsContext &context, SourceName at,
+    const A &x, bool allowHostCallees = false) {
   if (const auto *expr{parser::Unwrap<parser::Expr>(x)}) {
-    if (auto msg{DeviceExprChecker{context}(expr->typedExpr)}) {
+    if (auto msg{
+            DeviceExprChecker{context, allowHostCallees}(expr->typedExpr)}) {
       context.Say(at, std::move(*msg));
     }
   }
@@ -213,120 +223,127 @@ static void CheckUnwrappedExpr(
 
 template <bool CUF_KERNEL> struct ActionStmtChecker {
   template <typename A>
-  static MaybeMsg WhyNotOk(SemanticsContext &context, const A &x) {
+  static MaybeMsg WhyNotOk(
+      SemanticsContext &context, const A &x, bool allowHostCallees = false) {
     if constexpr (ConstraintTrait<A>) {
-      return WhyNotOk(context, x.thing);
+      return WhyNotOk(context, x.thing, allowHostCallees);
     } else if constexpr (WrapperTrait<A>) {
-      return WhyNotOk(context, x.v);
+      return WhyNotOk(context, x.v, allowHostCallees);
     } else if constexpr (UnionTrait<A>) {
-      return WhyNotOk(context, x.u);
+      return WhyNotOk(context, x.u, allowHostCallees);
     } else if constexpr (TupleTrait<A>) {
-      return WhyNotOk(context, x.t);
+      return WhyNotOk(context, x.t, allowHostCallees);
     } else {
       return parser::MessageFormattedText{
           "Statement may not appear in device code"_err_en_US};
     }
   }
   template <typename A>
-  static MaybeMsg WhyNotOk(
-      SemanticsContext &context, const common::Indirection<A> &x) {
-    return WhyNotOk(context, x.value());
+  static MaybeMsg WhyNotOk(SemanticsContext &context,
+      const common::Indirection<A> &x, bool allowHostCallees = false) {
+    return WhyNotOk(context, x.value(), allowHostCallees);
   }
   template <typename... As>
-  static MaybeMsg WhyNotOk(
-      SemanticsContext &context, const std::variant<As...> &x) {
+  static MaybeMsg WhyNotOk(SemanticsContext &context,
+      const std::variant<As...> &x, bool allowHostCallees = false) {
     return common::visit(
-        [&context](const auto &x) { return WhyNotOk(context, x); }, x);
+        [&context, allowHostCallees](
+            const auto &x) { return WhyNotOk(context, x, allowHostCallees); },
+        x);
   }
   template <std::size_t J = 0, typename... As>
-  static MaybeMsg WhyNotOk(
-      SemanticsContext &context, const std::tuple<As...> &x) {
+  static MaybeMsg WhyNotOk(SemanticsContext &context,
+      const std::tuple<As...> &x, bool allowHostCallees = false) {
     if constexpr (J == sizeof...(As)) {
       return {};
-    } else if (auto msg{WhyNotOk(context, std::get<J>(x))}) {
+    } else if (auto msg{WhyNotOk(context, std::get<J>(x), allowHostCallees)}) {
       return msg;
     } else {
-      return WhyNotOk<(J + 1)>(context, x);
+      return WhyNotOk<(J + 1)>(context, x, allowHostCallees);
     }
   }
   template <typename A>
-  static MaybeMsg WhyNotOk(SemanticsContext &context, const std::list<A> &x) {
+  static MaybeMsg WhyNotOk(SemanticsContext &context, const std::list<A> &x,
+      bool allowHostCallees = false) {
     for (const auto &y : x) {
-      if (MaybeMsg result{WhyNotOk(context, y)}) {
+      if (MaybeMsg result{WhyNotOk(context, y, allowHostCallees)}) {
         return result;
       }
     }
     return {};
   }
   template <typename A>
-  static MaybeMsg WhyNotOk(
-      SemanticsContext &context, const std::optional<A> &x) {
+  static MaybeMsg WhyNotOk(SemanticsContext &context, const std::optional<A> &x,
+      bool allowHostCallees = false) {
     if (x) {
-      return WhyNotOk(context, *x);
+      return WhyNotOk(context, *x, allowHostCallees);
     } else {
       return {};
     }
   }
   template <typename A>
-  static MaybeMsg WhyNotOk(
-      SemanticsContext &context, const parser::UnlabeledStatement<A> &x) {
-    return WhyNotOk(context, x.statement);
+  static MaybeMsg WhyNotOk(SemanticsContext &context,
+      const parser::UnlabeledStatement<A> &x, bool allowHostCallees = false) {
+    return WhyNotOk(context, x.statement, allowHostCallees);
   }
   template <typename A>
-  static MaybeMsg WhyNotOk(
-      SemanticsContext &context, const parser::Statement<A> &x) {
-    return WhyNotOk(context, x.statement);
+  static MaybeMsg WhyNotOk(SemanticsContext &context,
+      const parser::Statement<A> &x, bool allowHostCallees = false) {
+    return WhyNotOk(context, x.statement, allowHostCallees);
   }
-  static MaybeMsg WhyNotOk(
-      SemanticsContext &context, const parser::AllocateStmt &) {
+  static MaybeMsg WhyNotOk(SemanticsContext &context,
+      const parser::AllocateStmt &, bool allowHostCallees = false) {
     return {}; // AllocateObjects are checked elsewhere
   }
-  static MaybeMsg WhyNotOk(
-      SemanticsContext &context, const parser::AllocateCoarraySpec &) {
+  static MaybeMsg WhyNotOk(SemanticsContext &context,
+      const parser::AllocateCoarraySpec &, bool allowHostCallees = false) {
     return parser::MessageFormattedText(
         "A coarray may not be allocated on the device"_err_en_US);
   }
-  static MaybeMsg WhyNotOk(
-      SemanticsContext &context, const parser::DeallocateStmt &) {
+  static MaybeMsg WhyNotOk(SemanticsContext &context,
+      const parser::DeallocateStmt &, bool allowHostCallees = false) {
     return {}; // AllocateObjects are checked elsewhere
   }
-  static MaybeMsg WhyNotOk(
-      SemanticsContext &context, const parser::AssignmentStmt &x) {
-    return DeviceExprChecker{context}(x.typedAssignment);
+  static MaybeMsg WhyNotOk(SemanticsContext &context,
+      const parser::AssignmentStmt &x, bool allowHostCallees = false) {
+    return DeviceExprChecker{context, allowHostCallees}(x.typedAssignment);
   }
-  static MaybeMsg WhyNotOk(
-      SemanticsContext &context, const parser::CallStmt &x) {
-    return DeviceExprChecker{context}(x.typedCall);
+  static MaybeMsg WhyNotOk(SemanticsContext &context, const parser::CallStmt &x,
+      bool allowHostCallees = false) {
+    return DeviceExprChecker{context, allowHostCallees}(x.typedCall);
   }
-  static MaybeMsg WhyNotOk(
-      SemanticsContext &context, const parser::ContinueStmt &) {
+  static MaybeMsg WhyNotOk(SemanticsContext &context,
+      const parser::ContinueStmt &, bool allowHostCallees = false) {
     return {};
   }
-  static MaybeMsg WhyNotOk(SemanticsContext &, const parser::PauseStmt &) {
+  static MaybeMsg WhyNotOk(SemanticsContext &, const parser::PauseStmt &,
+      bool allowHostCallees = false) {
     return parser::MessageFormattedText{
         "device subprograms may not contain PAUSE statements"_err_en_US};
   }
-  static MaybeMsg WhyNotOk(SemanticsContext &context, const parser::IfStmt &x) {
-    if (auto result{CheckUnwrappedExpr(
-            context, std::get<parser::ScalarLogicalExpr>(x.t))}) {
+  static MaybeMsg WhyNotOk(SemanticsContext &context, const parser::IfStmt &x,
+      bool allowHostCallees = false) {
+    if (auto result{CheckUnwrappedExpr(context,
+            std::get<parser::ScalarLogicalExpr>(x.t), allowHostCallees)}) {
       return result;
     }
     return WhyNotOk(context,
-        std::get<parser::UnlabeledStatement<parser::ActionStmt>>(x.t)
-            .statement);
+        std::get<parser::UnlabeledStatement<parser::ActionStmt>>(x.t).statement,
+        allowHostCallees);
   }
-  static MaybeMsg WhyNotOk(
-      SemanticsContext &context, const parser::NullifyStmt &x) {
+  static MaybeMsg WhyNotOk(SemanticsContext &context,
+      const parser::NullifyStmt &x, bool allowHostCallees = false) {
     for (const auto &y : x.v) {
-      if (MaybeMsg result{DeviceExprChecker{context}(y.typedExpr)}) {
+      if (MaybeMsg result{
+              DeviceExprChecker{context, allowHostCallees}(y.typedExpr)}) {
         return result;
       }
     }
     return {};
   }
-  static MaybeMsg WhyNotOk(
-      SemanticsContext &context, const parser::PointerAssignmentStmt &x) {
-    return DeviceExprChecker{context}(x.typedAssignment);
+  static MaybeMsg WhyNotOk(SemanticsContext &context,
+      const parser::PointerAssignmentStmt &x, bool allowHostCallees = false) {
+    return DeviceExprChecker{context, allowHostCallees}(x.typedAssignment);
   }
 };
 
@@ -524,13 +541,13 @@ template <bool IsCUFKernelDo> class DeviceContextChecker {
                 ErrorIfHostSymbol(assign->rhs, source);
               }
               if (auto msg{ActionStmtChecker<IsCUFKernelDo>::WhyNotOk(
-                      context_, x)}) {
+                      context_, x, isHostDevice)}) {
                 context_.Say(source, std::move(*msg));
               }
             },
             [&](const auto &x) {
               if (auto msg{ActionStmtChecker<IsCUFKernelDo>::WhyNotOk(
-                      context_, x)}) {
+                      context_, x, isHostDevice)}) {
                 context_.Say(source, std::move(*msg));
               }
             },
@@ -540,13 +557,13 @@ template <bool IsCUFKernelDo> class DeviceContextChecker {
   void Check(const parser::IfConstruct &ic) {
     const auto &ifS{std::get<parser::Statement<parser::IfThenStmt>>(ic.t)};
     CheckUnwrappedExpr(context_, ifS.source,
-        std::get<parser::ScalarLogicalExpr>(ifS.statement.t));
+        std::get<parser::ScalarLogicalExpr>(ifS.statement.t), isHostDevice);
     Check(std::get<parser::Block>(ic.t));
     for (const auto &eib :
         std::get<std::list<parser::IfConstruct::ElseIfBlock>>(ic.t)) {
       const auto &eIfS{std::get<parser::Statement<parser::ElseIfStmt>>(eib.t)};
       CheckUnwrappedExpr(context_, eIfS.source,
-          std::get<parser::ScalarLogicalExpr>(eIfS.statement.t));
+          std::get<parser::ScalarLogicalExpr>(eIfS.statement.t), isHostDevice);
       Check(std::get<parser::Block>(eib.t));
     }
     if (const auto &eb{
@@ -557,8 +574,8 @@ template <bool IsCUFKernelDo> class DeviceContextChecker {
   void Check(const parser::IfStmt &is) {
     const auto &uS{
         std::get<parser::UnlabeledStatement<parser::ActionStmt>>(is.t)};
-    CheckUnwrappedExpr(
-        context_, uS.source, std::get<parser::ScalarLogicalExpr>(is.t));
+    CheckUnwrappedExpr(context_, uS.source,
+        std::get<parser::ScalarLogicalExpr>(is.t), isHostDevice);
     Check(uS.statement, uS.source);
   }
   void Check(const parser::LoopControl::Bounds &bounds) {
@@ -594,7 +611,8 @@ template <bool IsCUFKernelDo> class DeviceContextChecker {
     Check(DEREF(parser::Unwrap<parser::Expr>(x)));
   }
   void Check(const parser::Expr &expr) {
-    if (MaybeMsg msg{DeviceExprChecker{context_}(expr.typedExpr)}) {
+    if (MaybeMsg msg{
+            DeviceExprChecker{context_, isHostDevice}(expr.typedExpr)}) {
       context_.Say(expr.source, std::move(*msg));
     }
   }
diff --git a/flang/lib/Semantics/check-declarations.cpp b/flang/lib/Semantics/check-declarations.cpp
index 6a2fd40aeec79..944e77c9454ef 100644
--- a/flang/lib/Semantics/check-declarations.cpp
+++ b/flang/lib/Semantics/check-declarations.cpp
@@ -1754,7 +1754,7 @@ void CheckHelper::CheckSubprogram(
     messages_.Say(symbol.name(),
         "A subroutine may not have LAUNCH_BOUNDS() or CLUSTER_DIMS() unless it has ATTRIBUTES(GLOBAL) or ATTRIBUTES(GRID_GLOBAL)"_err_en_US);
   }
-  if (!IsStmtFunction(symbol)) {
+  if (!IsStmtFunction(symbol) && !details.isInterface() && !details.isDummy()) {
     if (const Scope * outerDevice{FindCUDADeviceContext(&symbol.owner())};
         outerDevice && outerDevice->symbol()) {
       if (auto *msg{messages_.Say(symbol.name(),
diff --git a/flang/test/Semantics/CUDA/cuf-hostdevice-host-call.cuf b/flang/test/Semantics/CUDA/cuf-hostdevice-host-call.cuf
new file mode 100644
index 0000000000000..a86bcb5c7ac36
--- /dev/null
+++ b/flang/test/Semantics/CUDA/cuf-hostdevice-host-call.cuf
@@ -0,0 +1,71 @@
+! RUN: %python %S/../test_errors.py %s %flang_fc1
+! Calls to host procedures, and interface bodies for them, are allowed in a
+! host,device subprogram because it is also compiled for the host.
+
+module m
+contains
+  ! A host,device subprogram may declare an interface body for a host procedure
+  ! and call it; such a call is typically guarded at run time.
+  attributes(host,device) subroutine reduce_max(x, y)
+    interface
+      double precision function hostmaxxy(x, y)
+        double precision, intent(in) :: x, y
+      end function
+    end interface
+    double precision, intent(in) :: y
+    double precision, intent(inout) :: x
+    if (on_device()) then
+      x = atomicmax(x, y)
+    else
+      x = hostmaxxy(x, y)
+    end if
+  end subroutine
+
+  ! Interface bodies for both a host and a device procedure are accepted.
+  attributes(host,device) subroutine reduce_max2(x, y)
+    interface
+      double precision function hostmaxxy(x, y)
+        double precision, intent(in) :: x, y
+      end function
+    end interface
+    interface
+      attributes(device) double precision function devicemaxxy(x, y)
+        double precision, intent(in) :: x, y
+      end function
+    end interface
+    double precision, intent(in) :: y
+    double precision, intent(inout) :: x
+    if (on_device()) then
+      x = devicemaxxy(x, y)
+    else
+      x = hostmaxxy(x, y)
+    end if
+  end subroutine
+
+  ! A pure device subprogram may not call a host procedure.
+  attributes(device) subroutine devsub(x)
+    interface
+      double precision function hostfunc()
+      end function
+    end interface
+    double precision, intent(inout) :: x
+    !ERROR: 'hostfunc' may not be called in device code
+    x = hostfunc()
+  end subroutine
+
+  ! A real internal procedure of a device subprogram is still rejected.
+  attributes(device) subroutine devsub2
+   contains
+    !ERROR: 'inner' may not be an internal procedure of CUDA device subprogram 'devsub2'
+    subroutine inner
+    end
+  end subroutine
+
+  ! A real internal procedure of a host,device subprogram is still rejected.
+  attributes(host,device) subroutine hdsub
+   contains
+    !ERROR: 'inner2' may not be an internal procedure of CUDA device subprogram 'hdsub'
+    subroutine inner2
+    end
+  end subroutine
+end module



More information about the flang-commits mailing list