[flang-commits] [flang] [flang] Short-circuit scalar .AND./.OR. guarded by PRESENT() (PR #201524)

via flang-commits flang-commits at lists.llvm.org
Thu Jun 4 01:17:05 PDT 2026


https://github.com/khaki3 updated https://github.com/llvm/llvm-project/pull/201524

>From efad2bec586fa306ab14c78805ac592eeebf4222 Mon Sep 17 00:00:00 2001
From: Kazuaki Matsumura <kmatsumura at nvidia.com>
Date: Thu, 4 Jun 2026 01:16:54 -0700
Subject: [PATCH 1/2] [flang] Short-circuit scalar .AND./.OR. guarded by
 PRESENT()

The idiom

  IF (PRESENT(x) .AND. SIZE(x, d) == n) ...

is common in Fortran code that passes an absent OPTIONAL argument and
relies on the inquiry on the right not being reached when x is absent.
Fortran logical operators are not required to short-circuit, so lowering
evaluated SIZE(x, d) unconditionally and dereferenced the (null)
descriptor of the absent argument, crashing at runtime when the guarding
PRESENT test was meant to prevent it.

Lower a scalar .AND./.OR. whose left operand references PRESENT with
short-circuit semantics: the right operand is generated inside an fir.if
so it is only evaluated when the left operand makes its value relevant
(true for .AND., false for .OR.). The standard permits not evaluating an
operand whose value is not needed (F2023 10.1.7). Because .AND. is
left-associative, chained guards such as

  PRESENT(a) .AND. PRESENT(b) .AND. SIZE(a, d) == SIZE(b, d)

evaluate the descriptor reads only after all presence tests pass.
---
 flang/lib/Lower/ConvertExprToHLFIR.cpp | 81 ++++++++++++++++++++++++++
 1 file changed, 81 insertions(+)

diff --git a/flang/lib/Lower/ConvertExprToHLFIR.cpp b/flang/lib/Lower/ConvertExprToHLFIR.cpp
index ad680269dea5c..0fb98b89a28eb 100644
--- a/flang/lib/Lower/ConvertExprToHLFIR.cpp
+++ b/flang/lib/Lower/ConvertExprToHLFIR.cpp
@@ -13,6 +13,7 @@
 #include "flang/Lower/ConvertExprToHLFIR.h"
 #include "flang/Evaluate/shape.h"
 #include "flang/Evaluate/tools.h"
+#include "flang/Evaluate/traverse.h"
 #include "flang/Lower/AbstractConverter.h"
 #include "flang/Lower/Allocatable.h"
 #include "flang/Lower/CallInterface.h"
@@ -51,6 +52,24 @@ static bool isParenthesized(const Fortran::evaluate::Expr<T> &expr) {
   }
 }
 
+/// Return true if \p expr references the intrinsic function PRESENT. Used to
+/// recognize the `PRESENT(x) .AND. <expr using x>` (and `.OR.`) guard idiom so
+/// the guarded operand can be lowered with short-circuit semantics, avoiding an
+/// unconditional descriptor read on an argument that is only known to be
+/// present once the presence test is true.
+template <typename A>
+static bool exprReferencesPresent(const A &expr) {
+  struct PresentFinder : public Fortran::evaluate::AnyTraverse<PresentFinder> {
+    PresentFinder() : Fortran::evaluate::AnyTraverse<PresentFinder>(*this) {}
+    using Fortran::evaluate::AnyTraverse<PresentFinder>::operator();
+    bool
+    operator()(const Fortran::evaluate::SpecificIntrinsic &intrinsic) const {
+      return intrinsic.name == "present";
+    }
+  } finder;
+  return finder(expr);
+}
+
 /// Lower Designators to HLFIR.
 class HlfirDesignatorBuilder {
 private:
@@ -1697,12 +1716,74 @@ class HlfirBuilder {
     return hlfir::EntityWithAttributes{elemental};
   }
 
+  /// Lower a scalar `.AND.`/`.OR.` with short-circuit semantics: the right
+  /// operand is evaluated inside an `fir.if` so that, for `.AND.`, it only runs
+  /// when the left operand is true (and for `.OR.`, only when it is false).
+  /// This is used for the `PRESENT(x) .AND. <expr using x>` guard idiom, so the
+  /// descriptor read in the right operand is not emitted on a path where the
+  /// optional argument may be absent. Fortran does not mandate short-circuit
+  /// evaluation, but the standard permits not evaluating an operand whose value
+  /// is not needed (F2023 10.1.7).
+  template <typename D, typename R, typename LO, typename RO>
+  hlfir::EntityWithAttributes
+  genShortCircuitLogicalOp(const Fortran::evaluate::Operation<D, R, LO, RO> &op,
+                           Fortran::evaluate::LogicalOperator logicalOp) {
+    fir::FirOpBuilder &builder = getBuilder();
+    mlir::Location loc = getLoc();
+    mlir::Type resultType = Fortran::lower::getFIRType(
+        builder.getContext(), R::category, R::kind, /*params=*/{});
+
+    hlfir::Entity left = hlfir::loadTrivialScalar(loc, builder, gen(op.left()));
+    mlir::Value cond = builder.createConvert(loc, builder.getI1Type(), left);
+
+    auto genRhs = [&]() {
+      hlfir::Entity right =
+          hlfir::loadTrivialScalar(loc, builder, gen(op.right()));
+      fir::ResultOp::create(builder, loc,
+                            builder.createConvert(loc, resultType, right));
+    };
+    auto genConst = [&](bool value) {
+      fir::ResultOp::create(
+          builder, loc,
+          builder.createConvert(loc, resultType,
+                                builder.createBool(loc, value)));
+    };
+
+    // .AND.: left true  -> result is rhs, left false -> result is .false.
+    // .OR. : left true  -> result is .true., left false -> result is rhs.
+    const bool isAnd = logicalOp == Fortran::evaluate::LogicalOperator::And;
+    mlir::Value result = isAnd ? builder
+                                     .genIfOp(loc, {resultType}, cond,
+                                              /*withElseRegion=*/true)
+                                     .genThen([&]() { genRhs(); })
+                                     .genElse([&]() { genConst(false); })
+                                     .getResults()[0]
+                               : builder
+                                     .genIfOp(loc, {resultType}, cond,
+                                              /*withElseRegion=*/true)
+                                     .genThen([&]() { genConst(true); })
+                                     .genElse([&]() { genRhs(); })
+                                     .getResults()[0];
+    return hlfir::EntityWithAttributes{result};
+  }
+
   template <typename D, typename R, typename LO, typename RO>
   hlfir::EntityWithAttributes
   gen(const Fortran::evaluate::Operation<D, R, LO, RO> &op) {
     auto &builder = getBuilder();
     mlir::Location loc = getLoc();
     const int rank = op.Rank();
+    if constexpr (R::category == Fortran::common::TypeCategory::Logical &&
+                  std::is_same_v<
+                      D, Fortran::evaluate::LogicalOperation<R::kind>>) {
+      const Fortran::evaluate::LogicalOperator logicalOp =
+          op.derived().logicalOperator;
+      if (rank == 0 &&
+          (logicalOp == Fortran::evaluate::LogicalOperator::And ||
+           logicalOp == Fortran::evaluate::LogicalOperator::Or) &&
+          exprReferencesPresent(op.left()))
+        return genShortCircuitLogicalOp(op, logicalOp);
+    }
     BinaryOp<D> binaryOp;
     auto left = hlfir::loadTrivialScalar(loc, builder, gen(op.left()));
     auto right = hlfir::loadTrivialScalar(loc, builder, gen(op.right()));

>From 8f80cd5d9671026e1348a4a72a3434221455c6e3 Mon Sep 17 00:00:00 2001
From: Kazuaki Matsumura <kmatsumura at nvidia.com>
Date: Thu, 4 Jun 2026 01:07:31 -0700
Subject: [PATCH 2/2] [flang][test] Add short-circuit PRESENT-guard lowering
 test

Cover scalar .AND./.OR. whose left operand references PRESENT: the
descriptor read in the right operand must be lowered inside an fir.if and
not be combined via arith.andi, including chained presence tests.
---
 .../Lower/optional-present-short-circuit.f90  | 71 +++++++++++++++++++
 1 file changed, 71 insertions(+)
 create mode 100644 flang/test/Lower/optional-present-short-circuit.f90

diff --git a/flang/test/Lower/optional-present-short-circuit.f90 b/flang/test/Lower/optional-present-short-circuit.f90
new file mode 100644
index 0000000000000..7f3c9462e7ad8
--- /dev/null
+++ b/flang/test/Lower/optional-present-short-circuit.f90
@@ -0,0 +1,71 @@
+! Test short-circuit evaluation of a scalar .AND./.OR. whose left operand tests
+! the presence of an OPTIONAL argument. The right operand -- which reads the
+! descriptor of the optional argument -- must be lowered inside an fir.if so it
+! is not evaluated (and the descriptor not dereferenced) when the argument is
+! absent. Fortran does not require short-circuit evaluation, but the standard
+! permits not evaluating an operand whose value is not needed (F2023 10.1.7).
+
+! RUN: bbc -emit-hlfir %s -o - | FileCheck %s
+
+! CHECK-LABEL: func.func @_QPand_guard(
+! CHECK-SAME:      %[[FIELD:.*]]: !fir.box<!fir.array<?x?x?xf32>> {{.*}}"field"
+! CHECK-SAME:      %[[MASK:.*]]: !fir.box<!fir.array<?x?x?xf32>> {{.*}}"mask"
+subroutine and_guard(field, mask)
+  real, intent(in) :: field(:,:,:)
+  real, intent(in), optional :: mask(:,:,:)
+  ! CHECK: %[[MASKD:.*]]:2 = hlfir.declare %[[MASK]]
+  ! CHECK: %[[PRES:.*]] = fir.is_present %[[MASKD]]#1
+  ! CHECK: %{{.*}} = fir.if %[[PRES]] -> (!fir.logical<4>) {
+  ! The descriptor read of the optional argument is emitted only inside the
+  ! presence-guarded region, with no second presence test around it.
+  ! CHECK-NOT: fir.is_present
+  ! CHECK: fir.box_dims %[[MASKD]]#0
+  ! CHECK: } else {
+  ! CHECK: arith.constant false
+  ! CHECK: }
+  ! CHECK-NOT: arith.andi
+  if (present(mask) .and. (size(mask,3) /= size(field,3))) then
+    error stop 'inconsistent dimensions'
+  end if
+end subroutine
+
+! Chained presence tests: the descriptor reads of both optional arguments are
+! evaluated only after all the presence tests pass.
+! CHECK-LABEL: func.func @_QPchained_and_guard(
+subroutine chained_and_guard(a, b)
+  real, intent(in), optional :: a(:,:,:)
+  real, intent(in), optional :: b(:,:,:)
+  ! CHECK: %[[AD:.*]]:2 = hlfir.declare {{.*}}Ea"
+  ! CHECK: %[[BD:.*]]:2 = hlfir.declare {{.*}}Eb"
+  ! present(a) .and. present(b) is itself short-circuited.
+  ! CHECK: %[[PA:.*]] = fir.is_present %[[AD]]#1
+  ! CHECK: fir.if %[[PA]] -> (!fir.logical<4>) {
+  ! CHECK: fir.is_present %[[BD]]#1
+  ! CHECK: }
+  ! The size comparison (descriptor reads of a and b) is nested under the
+  ! combined presence test.
+  ! CHECK: fir.if %{{.*}} -> (!fir.logical<4>) {
+  ! CHECK: fir.box_dims %[[AD]]#0
+  ! CHECK: fir.box_dims %[[BD]]#0
+  if (present(a) .and. present(b) .and. (size(a,3) /= size(b,3))) then
+    error stop 'mismatch'
+  end if
+end subroutine
+
+! .OR. short-circuits when the left operand is true, so the right operand (the
+! descriptor read) is lowered in the else region and only runs when the
+! argument is present.
+! CHECK-LABEL: func.func @_QPor_guard(
+subroutine or_guard(mask)
+  real, intent(in), optional :: mask(:,:,:)
+  ! CHECK: %[[MASKD:.*]]:2 = hlfir.declare {{.*}}Emask"
+  ! CHECK: %[[PRES:.*]] = fir.is_present %[[MASKD]]#1
+  ! CHECK: %[[NOTPRES:.*]] = arith.xori %[[PRES]], %{{.*}} : i1
+  ! CHECK: fir.if %[[NOTPRES]] -> (!fir.logical<4>) {
+  ! CHECK: } else {
+  ! CHECK: fir.box_dims %[[MASKD]]#0
+  ! CHECK: }
+  if (.not. present(mask) .or. (size(mask,3) == 1)) then
+    error stop 'absent or trivial'
+  end if
+end subroutine



More information about the flang-commits mailing list