[flang-commits] [flang] [llvm] [flang] Guard absent optional operands in elemental character MIN/MAX (PR #191244)
Sairudra More via flang-commits
flang-commits at lists.llvm.org
Thu Apr 9 09:53:28 PDT 2026
https://github.com/Saieiei created https://github.com/llvm/llvm-project/pull/191244
Follow-up to #189464. That change fixed result-length computation for elemental character MIN/MAX, but the elemental kernel body could still access absent optional operands unconditionally and crash at runtime. Guard optional element access in HLFIR lowering and add execute coverage for omitted optional arguments.
>From 203bfdd9c95970780fee0e8067dd1c4e36d0d254 Mon Sep 17 00:00:00 2001
From: Sairudra More <moresair at pe31.hpc.amslabs.hpecorp.net>
Date: Mon, 30 Mar 2026 14:52:45 -0500
Subject: [PATCH] [flang] Handle elemental character MIN/MAX result length in
HLFIR
Fix lowering of elemental character MIN/MAX in HLFIR.
Previously, these cases could hit a lowering-time TODO and abort. This
change computes the result length as the maximum length of the present
actual arguments.
For optional arguments, the length computation is guarded so it is only
evaluated when the argument is present. Add regression coverage for
differing-length, three-argument, and optional argument cases.
---
.../test/Driver/elemental-char-min-max.f90 | 47 +++++++
flang/lib/Lower/ConvertCall.cpp | 51 ++++++-
flang/lib/Lower/HlfirIntrinsics.cpp | 50 ++++++-
.../HLFIR/elemental-character-min-max.f90 | 129 ++++++++++++++++++
4 files changed, 272 insertions(+), 5 deletions(-)
create mode 100644 flang-rt/test/Driver/elemental-char-min-max.f90
create mode 100644 flang/test/Lower/HLFIR/elemental-character-min-max.f90
diff --git a/flang-rt/test/Driver/elemental-char-min-max.f90 b/flang-rt/test/Driver/elemental-char-min-max.f90
new file mode 100644
index 0000000000000..f1865f08b5d8a
--- /dev/null
+++ b/flang-rt/test/Driver/elemental-char-min-max.f90
@@ -0,0 +1,47 @@
+! Test elemental character MIN/MAX with optional absent argument executes
+! correctly — previously crashed at runtime when the optional was absent.
+! UNSUPPORTED: offload-cuda
+
+! RUN: %flang %isysroot -L"%libdir" %s -o %t
+! RUN: env LD_LIBRARY_PATH="$LD_LIBRARY_PATH:%libdir" %t | FileCheck %s
+
+program test_elemental_char_minmax_optional
+ implicit none
+ character(3) :: x(3), y(3), r(3)
+
+ x = ['all', 'zed', 'ht!']
+ y = ['bac', 'rig', 'rte']
+
+ ! Call min without the optional argument — previously caused a runtime crash
+ ! because the elemental kernel body accessed the absent optional unconditionally.
+ r = min_opt(x, y)
+ print *, r
+ ! CHECK: allright!
+
+ ! Call max without the optional argument.
+ r = max_opt(x, y)
+ print *, r
+ ! CHECK: baczedrte
+
+ ! Call min with the optional argument present.
+ r = min_opt(x, y, ['aaa', 'aaa', 'aaa'])
+ print *, r
+ ! CHECK: aaaaaaaaa
+
+contains
+
+ function min_opt(a, b, c) result(res)
+ character(*), intent(in) :: a(:), b(:)
+ character(*), intent(in), optional :: c(:)
+ character(len(a)) :: res(size(a))
+ res = min(a, b, c)
+ end function
+
+ function max_opt(a, b, c) result(res)
+ character(*), intent(in) :: a(:), b(:)
+ character(*), intent(in), optional :: c(:)
+ character(len(a)) :: res(size(a))
+ res = max(a, b, c)
+ end function
+
+end program
diff --git a/flang/lib/Lower/ConvertCall.cpp b/flang/lib/Lower/ConvertCall.cpp
index ae9d1733d053d..ea65a49355330 100644
--- a/flang/lib/Lower/ConvertCall.cpp
+++ b/flang/lib/Lower/ConvertCall.cpp
@@ -2758,10 +2758,53 @@ class ElementalIntrinsicCallBuilder
intrinsic->name == "merge")
return loweredActuals[0].value().genCharLength(
callContext.loc, callContext.getBuilder());
- // Character MIN/MAX is the min/max of the arguments length that are
- // present.
- TODO(callContext.loc,
- "compute elemental character min/max function result length in HLFIR");
+ // Character MIN/MAX result length is the length of the longest
+ // argument that is present.
+ assert(intrinsic &&
+ (intrinsic->name == "min" || intrinsic->name == "max") &&
+ "unexpected elemental intrinsic with character result");
+ fir::FirOpBuilder &builder = callContext.getBuilder();
+ mlir::Location loc = callContext.loc;
+ mlir::Type idxTy = builder.getIndexType();
+ mlir::Value resultLength;
+ for (auto &preparedActual : loweredActuals) {
+ if (!preparedActual)
+ continue;
+ mlir::Value argLen;
+ if (preparedActual->handleDynamicOptional()) {
+ // genCharLength must not be called on an absent optional: the
+ // descriptor may be unreadable (e.g. assumed-length character).
+ // Guard it with a fir.if so the read only happens when present.
+ mlir::Value zero = builder.createIntegerConstant(loc, idxTy, 0);
+ mlir::Value isPresent = preparedActual->getIsPresent();
+ auto &capture = *preparedActual;
+ argLen =
+ builder
+ .genIfOp(loc, {idxTy}, isPresent,
+ /*withElseRegion=*/true)
+ .genThen([&]() {
+ mlir::Value len = capture.genCharLength(loc, builder);
+ len = builder.createConvert(loc, idxTy, len);
+ fir::ResultOp::create(builder, loc, len);
+ })
+ .genElse([&]() { fir::ResultOp::create(builder, loc, zero); })
+ .getResults()[0];
+ } else {
+ argLen = preparedActual->genCharLength(loc, builder);
+ argLen = builder.createConvert(loc, idxTy, argLen);
+ }
+ if (!resultLength) {
+ resultLength = argLen;
+ } else {
+ mlir::Value cmp = mlir::arith::CmpIOp::create(
+ builder, loc, mlir::arith::CmpIPredicate::sgt, argLen,
+ resultLength);
+ resultLength = mlir::arith::SelectOp::create(builder, loc, cmp, argLen,
+ resultLength);
+ }
+ }
+ assert(resultLength && "MIN/MAX must have at least two arguments");
+ return resultLength;
}
mlir::Value getPolymorphicResultMold(
diff --git a/flang/lib/Lower/HlfirIntrinsics.cpp b/flang/lib/Lower/HlfirIntrinsics.cpp
index 9ee30e52af697..335d96d298a04 100644
--- a/flang/lib/Lower/HlfirIntrinsics.cpp
+++ b/flang/lib/Lower/HlfirIntrinsics.cpp
@@ -465,7 +465,55 @@ mlir::Value HlfirCharExtremumLowering::lowerImpl(
const Fortran::lower::PreparedActualArguments &loweredActuals,
const fir::IntrinsicArgumentLoweringRules *argLowering,
mlir::Type stmtResultType) {
- auto operands = getOperandVector(loweredActuals, argLowering);
+ // Check whether any argument is dynamically optional. When an optional
+ // character argument is absent, calling getActual() to read the character
+ // length from the descriptor is unsafe. Guard each optional argument's
+ // element access inside a fir.if.
+ bool hasDynamicOptional = llvm::any_of(loweredActuals, [](const auto &arg) {
+ return arg.has_value() && arg->handleDynamicOptional();
+ });
+ if (!hasDynamicOptional) {
+ auto operands = getOperandVector(loweredActuals, argLowering);
+ assert(operands.size() >= 2);
+ return createOp<hlfir::CharExtremumOp>(pred, mlir::ValueRange{operands});
+ }
+
+ // For absent optional arguments, use the first required argument's element
+ // as a neutral dummy so that min/max semantics are preserved:
+ // min(a, b, a) == min(a, b) and max(a, b, a) == max(a, b).
+ // Scan ahead to find the first non-optional argument so it can be used as
+ // the neutral element for any optional arguments that appear earlier.
+ mlir::Value firstRequired;
+ for (const auto &opt_arg : loweredActuals) {
+ if (opt_arg && !opt_arg->handleDynamicOptional()) {
+ firstRequired = loadTrivialScalar(*opt_arg);
+ break;
+ }
+ }
+ assert(firstRequired &&
+ "MIN/MAX must have at least one non-optional argument");
+ llvm::SmallVector<mlir::Value> operands;
+ for (const auto &opt_arg : loweredActuals) {
+ if (!opt_arg)
+ continue;
+ if (!opt_arg->handleDynamicOptional()) {
+ mlir::Value elem = loadTrivialScalar(*opt_arg);
+ operands.push_back(elem);
+ } else {
+ mlir::Value isPresent = opt_arg->getIsPresent();
+ mlir::Type elemType = firstRequired.getType();
+ const Fortran::lower::PreparedActualArgument &argRef = *opt_arg;
+ mlir::Value elem =
+ builder.genIfOp(loc, {elemType}, isPresent, /*withElseRegion=*/true)
+ .genThen([&]() {
+ fir::ResultOp::create(builder, loc, loadTrivialScalar(argRef));
+ })
+ .genElse(
+ [&]() { fir::ResultOp::create(builder, loc, firstRequired); })
+ .getResults()[0];
+ operands.push_back(elem);
+ }
+ }
assert(operands.size() >= 2);
return createOp<hlfir::CharExtremumOp>(pred, mlir::ValueRange{operands});
}
diff --git a/flang/test/Lower/HLFIR/elemental-character-min-max.f90 b/flang/test/Lower/HLFIR/elemental-character-min-max.f90
new file mode 100644
index 0000000000000..fc149f0759188
--- /dev/null
+++ b/flang/test/Lower/HLFIR/elemental-character-min-max.f90
@@ -0,0 +1,129 @@
+! Test lowering of elemental character MIN/MAX to HLFIR
+! RUN: bbc -emit-hlfir -o - %s | FileCheck %s
+
+! Test elemental character MIN with two array arguments of the same length.
+subroutine test_elemental_char_min(a, b, res)
+ character(5) :: a(10), b(10), res(10)
+ res = min(a, b)
+end subroutine
+! CHECK-LABEL: func.func @_QPtest_elemental_char_min(
+! CHECK: %[[C5_A:.*]] = arith.constant 5 : index
+! CHECK: %[[A:.*]]:2 = hlfir.declare {{.*}}Ea"
+! CHECK: %[[C5_B:.*]] = arith.constant 5 : index
+! CHECK: %[[B:.*]]:2 = hlfir.declare {{.*}}Eb"
+! CHECK: %[[RES:.*]]:2 = hlfir.declare {{.*}}Eres"
+! CHECK: %[[CMP:.*]] = arith.cmpi sgt, %[[C5_B]], %[[C5_A]] : index
+! CHECK: %[[RESULT_LEN:.*]] = arith.select %[[CMP]], %[[C5_B]], %[[C5_A]] : index
+! CHECK: %[[ELEMENTAL:.*]] = hlfir.elemental %{{.*}} typeparams %[[RESULT_LEN]] unordered : (!fir.shape<1>, index) -> !hlfir.expr<10x!fir.char<1,?>> {
+! CHECK: ^bb0(%[[IDX:.*]]: index):
+! CHECK: %{{.*}} = hlfir.char_extremum min, %{{.*}}, %{{.*}} :
+! CHECK: hlfir.yield_element
+! CHECK: }
+
+! Test elemental character MAX with two array arguments.
+subroutine test_elemental_char_max(a, b, res)
+ character(5) :: a(10), b(10), res(10)
+ res = max(a, b)
+end subroutine
+! CHECK-LABEL: func.func @_QPtest_elemental_char_max(
+! CHECK: %[[ELEMENTAL:.*]] = hlfir.elemental %{{.*}} typeparams %{{.*}} unordered : (!fir.shape<1>, index) -> !hlfir.expr<10x!fir.char<1,?>> {
+! CHECK: ^bb0(%[[IDX:.*]]: index):
+! CHECK: %{{.*}} = hlfir.char_extremum max, %{{.*}}, %{{.*}} :
+! CHECK: hlfir.yield_element
+! CHECK: }
+
+! Test elemental character MIN with different argument lengths.
+! The result length must be the maximum of the argument lengths.
+subroutine test_elemental_char_min_diff_len(a, b, res)
+ character(5) :: a(10)
+ character(7) :: b(10), res(10)
+ res = min(a, b)
+end subroutine
+! CHECK-LABEL: func.func @_QPtest_elemental_char_min_diff_len(
+! CHECK: %[[C5:.*]] = arith.constant 5 : index
+! CHECK: %[[A:.*]]:2 = hlfir.declare {{.*}}Ea"
+! CHECK: %[[C7:.*]] = arith.constant 7 : index
+! CHECK: %[[B:.*]]:2 = hlfir.declare {{.*}}Eb"
+! CHECK: %[[CMP:.*]] = arith.cmpi sgt, %[[C7]], %[[C5]] : index
+! CHECK: %[[RESULT_LEN:.*]] = arith.select %[[CMP]], %[[C7]], %[[C5]] : index
+! CHECK: %[[ELEMENTAL:.*]] = hlfir.elemental %{{.*}} typeparams %[[RESULT_LEN]] unordered : (!fir.shape<1>, index) -> !hlfir.expr<10x!fir.char<1,?>> {
+
+! Test elemental character MIN with three array arguments.
+! The result length must be the maximum across all argument lengths.
+subroutine test_elemental_char_min_three(a, b, c, res)
+ character(5) :: a(10), b(10), c(10), res(10)
+ res = min(a, b, c)
+end subroutine
+! CHECK-LABEL: func.func @_QPtest_elemental_char_min_three(
+! CHECK: %[[C5_A:.*]] = arith.constant 5 : index
+! CHECK: %[[A:.*]]:2 = hlfir.declare {{.*}}Ea"
+! CHECK: %[[C5_B:.*]] = arith.constant 5 : index
+! CHECK: %[[B:.*]]:2 = hlfir.declare {{.*}}Eb"
+! CHECK: %[[C5_C:.*]] = arith.constant 5 : index
+! CHECK: %[[C:.*]]:2 = hlfir.declare {{.*}}Ec"
+! CHECK: %[[CMP1:.*]] = arith.cmpi sgt, %[[C5_B]], %[[C5_A]] : index
+! CHECK: %[[MAX1:.*]] = arith.select %[[CMP1]], %[[C5_B]], %[[C5_A]] : index
+! CHECK: %[[CMP2:.*]] = arith.cmpi sgt, %[[C5_C]], %[[MAX1]] : index
+! CHECK: %[[RESULT_LEN:.*]] = arith.select %[[CMP2]], %[[C5_C]], %[[MAX1]] : index
+! CHECK: %[[ELEMENTAL:.*]] = hlfir.elemental %{{.*}} typeparams %[[RESULT_LEN]] unordered : (!fir.shape<1>, index) -> !hlfir.expr<10x!fir.char<1,?>> {
+! CHECK: ^bb0(%[[IDX:.*]]: index):
+! CHECK: %{{.*}} = hlfir.char_extremum min, %{{.*}}, %{{.*}}, %{{.*}} :
+! CHECK: hlfir.yield_element
+! CHECK: }
+
+! Test elemental character MIN with an optional dummy argument of fixed length.
+! The result length must guard the optional argument's length with fir.if so
+! genCharLength is not called on an absent optional (unsafe for assumed length).
+subroutine test_elemental_char_min_optional(a, b, c, res)
+ character(5) :: a(10), b(10), res(10)
+ character(5), optional :: c(10)
+ res = min(a, b, c)
+end subroutine
+! CHECK-LABEL: func.func @_QPtest_elemental_char_min_optional(
+! CHECK: %[[C5_A:.*]] = arith.constant 5 : index
+! CHECK: %[[A:.*]]:2 = hlfir.declare {{.*}}Ea"
+! CHECK: %[[C5_B:.*]] = arith.constant 5 : index
+! CHECK: %[[B:.*]]:2 = hlfir.declare {{.*}}Eb"
+! CHECK: %[[C5_C:.*]] = arith.constant 5 : index
+! CHECK: %[[C:.*]]:2 = hlfir.declare {{.*}}Ec"
+! CHECK: %[[IS_PRESENT:.*]] = fir.is_present %[[C]]#0
+! CHECK: %[[CMP1:.*]] = arith.cmpi sgt, %[[C5_B]], %[[C5_A]] : index
+! CHECK: %[[MAX1:.*]] = arith.select %[[CMP1]], %[[C5_B]], %[[C5_A]] : index
+! CHECK: %[[C0:.*]] = arith.constant 0 : index
+! CHECK: %[[OPT_LEN:.*]] = fir.if %[[IS_PRESENT]] -> (index) {
+! CHECK: fir.result %[[C5_C]] : index
+! CHECK: } else {
+! CHECK: fir.result %[[C0]] : index
+! CHECK: }
+! CHECK: %[[CMP2:.*]] = arith.cmpi sgt, %[[OPT_LEN]], %[[MAX1]] : index
+! CHECK: %[[RESULT_LEN:.*]] = arith.select %[[CMP2]], %[[OPT_LEN]], %[[MAX1]] : index
+! CHECK: %[[ELEMENTAL:.*]] = hlfir.elemental %{{.*}} typeparams %[[RESULT_LEN]]
+
+! Test elemental character MIN with an assumed-length optional dummy argument.
+! genCharLength reads from the descriptor, so it must be guarded with fir.if
+! to avoid reading an invalid descriptor when the argument is absent.
+subroutine test_elemental_char_min_assumed_optional(a, b, c, res)
+ character(*), intent(in) :: a(:), b(:)
+ character(*), intent(in), optional :: c(:)
+ character(*), intent(out) :: res(:)
+ res = min(a, b, c)
+end subroutine
+! CHECK-LABEL: func.func @_QPtest_elemental_char_min_assumed_optional(
+! CHECK: %[[A:.*]]:2 = hlfir.declare {{.*}}Ea"
+! CHECK: %[[B:.*]]:2 = hlfir.declare {{.*}}Eb"
+! CHECK: %[[C:.*]]:2 = hlfir.declare {{.*}}Ec"
+! CHECK: %[[IS_PRESENT:.*]] = fir.is_present %[[C]]#0
+! CHECK: %[[LEN_A:.*]] = fir.box_elesize %[[A]]#1
+! CHECK: %[[LEN_B:.*]] = fir.box_elesize %[[B]]#1
+! CHECK: %[[CMP1:.*]] = arith.cmpi sgt, %[[LEN_B]], %[[LEN_A]] : index
+! CHECK: %[[MAX1:.*]] = arith.select %[[CMP1]], %[[LEN_B]], %[[LEN_A]] : index
+! CHECK: %[[C0:.*]] = arith.constant 0 : index
+! CHECK: %[[OPT_LEN:.*]] = fir.if %[[IS_PRESENT]] -> (index) {
+! CHECK: %[[LEN_C:.*]] = fir.box_elesize %[[C]]#1
+! CHECK: fir.result %[[LEN_C]] : index
+! CHECK: } else {
+! CHECK: fir.result %[[C0]] : index
+! CHECK: }
+! CHECK: %[[CMP2:.*]] = arith.cmpi sgt, %[[OPT_LEN]], %[[MAX1]] : index
+! CHECK: %[[RESULT_LEN:.*]] = arith.select %[[CMP2]], %[[OPT_LEN]], %[[MAX1]] : index
+! CHECK: %[[ELEMENTAL:.*]] = hlfir.elemental %{{.*}} typeparams %[[RESULT_LEN]] unordered
More information about the flang-commits
mailing list