[flang-commits] [flang] [flang] Let a directive keep the loop it owns when its body branches (PR #227707)
Kareem Ergawy via flang-commits
flang-commits at lists.llvm.org
Thu Oct 1 00:01:20 PDT 2026
https://github.com/ergawy updated https://github.com/llvm/llvm-project/pull/227707
>From f281b14f617bbdf0c70e44748c3a69240de58138 Mon Sep 17 00:00:00 2001
From: ergawy <kareem.ergawy at gmail.com>
Date: Wed, 30 Sep 2026 05:39:23 -0700
Subject: [PATCH 1/3] [flang] Let a directive keep the loop it owns when its
body branches
A loop whose branching is confined to its body keeps its structured
form, but the construct holding it stayed Unstructured. A directive does
not merely contain such a loop, it owns it, and its lowering reads the
construct's own classification to decide whether the loop op carries its
bounds. The directive was left with a bounds-free loop that nothing
could partition, and the loop it owns became a second one nested inside.
Reclassify a directive construct once the loops it holds no longer need
it to stay Unstructured. Children are visited first, so those loops have
already been reclassified by the time the construct is reached. A
construct whose branching leaves it is untouched, as is one holding a
branch of its own.
Taking a loop over also means genFIR(DoConstruct) -- where a plain loop
folds a body whose branching stays inside it into a region -- never runs
for that loop, so fold its body through the same helper. A construct
that takes over no loop, acc data or acc parallel without a loop
directive, keeps having its own evaluations lowered.
The cases this enables move out of the Todo tests, which asserted the
lowering was not yet implemented. The one whose EXIT leaves the loop
stays there.
---
flang/lib/Lower/Bridge.cpp | 14 ++
flang/lib/Lower/PFTBuilder.cpp | 37 +++-
.../acc-unstructured-combined-construct.f90 | 29 ----
.../Todo/acc-unstructured-loop-construct.f90 | 132 +--------------
.../OpenACC/acc-directive-loop-bounds.f90 | 66 ++++++++
.../acc-unstructured-combined-construct.f90 | 45 +++++
.../OpenACC/acc-unstructured-internals.f90 | 5 +-
.../acc-unstructured-loop-construct.f90 | 160 ++++++++++++++++++
flang/test/Lower/OpenACC/acc-unstructured.f90 | 39 +++--
.../OpenMP/wsloop-directive-loop-bounds.f90 | 34 ++++
10 files changed, 387 insertions(+), 174 deletions(-)
delete mode 100644 flang/test/Lower/OpenACC/Todo/acc-unstructured-combined-construct.f90
create mode 100644 flang/test/Lower/OpenACC/acc-directive-loop-bounds.f90
create mode 100644 flang/test/Lower/OpenACC/acc-unstructured-combined-construct.f90
create mode 100644 flang/test/Lower/OpenACC/acc-unstructured-loop-construct.f90
create mode 100644 flang/test/Lower/OpenMP/wsloop-directive-loop-bounds.f90
diff --git a/flang/lib/Lower/Bridge.cpp b/flang/lib/Lower/Bridge.cpp
index d4c0e547efe88..9a35b82b8f20c 100644
--- a/flang/lib/Lower/Bridge.cpp
+++ b/flang/lib/Lower/Bridge.cpp
@@ -3903,6 +3903,10 @@ class FirConverter : public Fortran::lower::AbstractConverter {
std::get_if<Fortran::parser::OpenACCCombinedConstruct>(&acc.u);
Fortran::lower::pft::Evaluation *curEval = &getEval();
+ // The loop the directive takes over, once the descent below has found it.
+ // A construct that owns no loop -- acc data, or acc parallel without a
+ // loop directive -- leaves this null and has its own evaluations lowered.
+ Fortran::lower::pft::Evaluation *absorbedLoop = nullptr;
bool collapseForce = false;
uint64_t collapseDepth = 1;
uint64_t loopCount = 1;
@@ -3947,6 +3951,11 @@ class FirConverter : public Fortran::lower::AbstractConverter {
break;
curEval = nextDo;
}
+ // The descent lands on the loop the directive takes over, and every
+ // level it steps through is one. A construct whose first evaluation is
+ // not a loop takes over none.
+ if (outerDo)
+ absorbedLoop = curEval;
}
// collapse(force: ...) allows statements between the loop levels the
@@ -4006,6 +4015,11 @@ class FirConverter : public Fortran::lower::AbstractConverter {
if (collapseForce && collapseDepth > 1) {
genCollapseForceBody();
+ } else if (absorbedLoop && absorbedLoop->lowerBodyAsWrappedRegion()) {
+ // Taking the loop over means genFIR(DoConstruct) -- where a plain loop
+ // folds a body that branches into a region -- never runs for it. Such a
+ // body still needs that region, so fold it through the same helper.
+ genLoopBodyEvaluations(*absorbedLoop, /*unstructuredContext=*/true);
} else if (curEval->hasNestedEvaluations()) {
for (Fortran::lower::pft::Evaluation &e : curEval->getNestedEvaluations())
genFIR(e);
diff --git a/flang/lib/Lower/PFTBuilder.cpp b/flang/lib/Lower/PFTBuilder.cpp
index d1fbf4e07e313..7331229c06d4a 100644
--- a/flang/lib/Lower/PFTBuilder.cpp
+++ b/flang/lib/Lower/PFTBuilder.cpp
@@ -3002,15 +3002,36 @@ static bool isStructurableWithUnstructuredInternals(
return true;
}
+/// Return true if \p construct owes its classification entirely to the
+/// evaluations it holds, none of which is Unstructured any more.
+static bool
+holdsNothingUnstructured(const Fortran::lower::pft::Evaluation &construct) {
+ if (!construct.evaluationList)
+ return false;
+ for (const Fortran::lower::pft::Evaluation &nested :
+ *construct.evaluationList)
+ if (nested.isUnstructured())
+ return false;
+ return true;
+}
+
/// Reclassify every qualifying loop in \p unit.
///
/// Runs after branch analysis, when the incoming-branch map is complete;
/// during analysis a branch later in the function would not yet be recorded
/// and condition 2 would read a partial map.
///
-/// Ancestors are deliberately left alone. A loop reclassified here lowers to a
-/// structured op, and a structured op is legal inside an unstructured parent,
-/// so leaving the parent Unstructured is conservative but correct.
+/// Ordinary ancestors are deliberately left alone. A loop reclassified here
+/// lowers to a structured op, and a structured op is legal inside an
+/// unstructured parent, so leaving the parent Unstructured is conservative but
+/// correct.
+///
+/// A directive construct is the exception, because it does not merely contain
+/// the loop: the directive owns it, and its lowering reads the construct's own
+/// classification to decide whether the loop op carries its bounds. Left
+/// Unstructured, the construct yields a bounds-free loop that nothing can
+/// partition, and the reclassified loop inside it becomes a second, nested
+/// one. Weaken such a construct once the loops it holds no longer need it.
static void detectStructuredWithUnstructuredInternals(
Fortran::lower::pft::FunctionLikeUnit &unit) {
// Such a loop is lowered with its body in an scf.execute_region: its
@@ -3027,11 +3048,19 @@ static void detectStructuredWithUnstructuredInternals(
visit(*e.evaluationList);
if (e.isA<parser::DoConstruct>() &&
- isStructurableWithUnstructuredInternals(e, unit))
+ isStructurableWithUnstructuredInternals(e, unit)) {
// The one place the classification weakens: detection has proven
// Unstructured unnecessary.
e.weakenControlFlow(Fortran::lower::pft::Evaluation::ControlFlow::
StructuredWithUnstructuredInternals);
+ continue;
+ }
+
+ // Children are visited first, so the loops this construct owns have
+ // already been reclassified by the time it is reached.
+ if (e.isExecutableDirective() && holdsNothingUnstructured(e))
+ e.weakenControlFlow(Fortran::lower::pft::Evaluation::ControlFlow::
+ StructuredWithUnstructuredInternals);
}
};
visit(unit.evaluationList);
diff --git a/flang/test/Lower/OpenACC/Todo/acc-unstructured-combined-construct.f90 b/flang/test/Lower/OpenACC/Todo/acc-unstructured-combined-construct.f90
deleted file mode 100644
index 110940d9e5069..0000000000000
--- a/flang/test/Lower/OpenACC/Todo/acc-unstructured-combined-construct.f90
+++ /dev/null
@@ -1,29 +0,0 @@
-! Each sub-file exercises a different unstructured-CFG pattern inside a
-! combined `acc parallel loop` construct (default parallelism is
-! `independent`).
-
-! RUN: bbc -fopenacc -emit-hlfir %s -o - | FileCheck %s --check-prefix=CYCLE2-OK
-! RUN: %not_todo_cmd bbc -fopenacc -emit-hlfir --emit-independent-loops-as-unstructured=false %s -o - 2>&1 | FileCheck %s --check-prefix=CYCLE2
-
-subroutine test_unstructured_collapse_cycle(a)
- integer :: i, j, jdiag
- real(8) :: a(:,:)
- jdiag = 4
- !$acc parallel loop collapse(2) copy(a)
- do j = 1, 8
- do i = 1, 8
- if (i == jdiag) then
- a(i, j) = 0.0d0
- cycle
- end if
- a(i, j) = real(i + j, 8)
- end do
- end do
- !$acc end parallel loop
-end subroutine
-
-! CYCLE2: not yet implemented: unstructured do loop in combined acc construct
-
-! CYCLE2-OK-LABEL: func.func @_QPtest_unstructured_collapse_cycle
-! CYCLE2-OK: acc.parallel combined(loop)
-! CYCLE2-OK: acc.loop combined(parallel)
diff --git a/flang/test/Lower/OpenACC/Todo/acc-unstructured-loop-construct.f90 b/flang/test/Lower/OpenACC/Todo/acc-unstructured-loop-construct.f90
index f26e95d235d04..a35a930a58de9 100644
--- a/flang/test/Lower/OpenACC/Todo/acc-unstructured-loop-construct.f90
+++ b/flang/test/Lower/OpenACC/Todo/acc-unstructured-loop-construct.f90
@@ -1,99 +1,12 @@
-! Each sub-file exercises a different unstructured-CFG pattern inside an
-! `acc loop` whose default parallelism resolves to `independent`.
+! An `acc loop` whose default parallelism resolves to `independent` and whose
+! body branching is not confined to it.
+!
+! The EXIT leaves the loop, so the loop stays unstructured and cannot be
+! lowered with its bounds on the op. The patterns that do keep their branching
+! inside the body live in ../acc-unstructured-loop-construct.f90.
-! RUN: split-file %s %t
-
-! By default (--emit-independent-loops-as-unstructured=true), the loops are
-! lowered to `acc.loop` operations.
-! RUN: bbc -fopenacc -emit-hlfir %t/goto_one_level.f90 -o - | FileCheck %s --check-prefix=GOTO1-OK
-! RUN: bbc -fopenacc -emit-hlfir %t/goto_with_intermediate.f90 -o - | FileCheck %s --check-prefix=GOTO2-OK
-! RUN: bbc -fopenacc -emit-hlfir %t/collapse_cycle.f90 -o - | FileCheck %s --check-prefix=CCYCLE-OK
-! RUN: bbc -fopenacc -emit-hlfir %t/cache_exit.f90 -o - | FileCheck %s --check-prefix=CEXIT-OK
-! RUN: bbc -fopenacc -emit-hlfir %t/cache_select_case.f90 -o - | FileCheck %s --check-prefix=CCASE-OK
-
-! With --emit-independent-loops-as-unstructured=false, the TODO is emitted.
-! RUN: %not_todo_cmd bbc -fopenacc -emit-hlfir --emit-independent-loops-as-unstructured=false %t/goto_one_level.f90 -o - 2>&1 | FileCheck %s --check-prefix=GOTO1
-! RUN: %not_todo_cmd bbc -fopenacc -emit-hlfir --emit-independent-loops-as-unstructured=false %t/goto_with_intermediate.f90 -o - 2>&1 | FileCheck %s --check-prefix=GOTO2
-! RUN: %not_todo_cmd bbc -fopenacc -emit-hlfir --emit-independent-loops-as-unstructured=false %t/collapse_cycle.f90 -o - 2>&1 | FileCheck %s --check-prefix=CCYCLE
-! RUN: %not_todo_cmd bbc -fopenacc -emit-hlfir --emit-independent-loops-as-unstructured=false %t/cache_exit.f90 -o - 2>&1 | FileCheck %s --check-prefix=CEXIT
-! RUN: %not_todo_cmd bbc -fopenacc -emit-hlfir --emit-independent-loops-as-unstructured=false %t/cache_select_case.f90 -o - 2>&1 | FileCheck %s --check-prefix=CCASE
-
-!--- goto_one_level.f90
-
-! GOTO exits the inner `acc loop seq` (one level), landing in the body of
-! the outer `acc loop gang vector`. Outer loop defaults to `independent`.
-subroutine test_unstructured6(N, A, B)
- implicit real*8 (a-h, o-z)
- !$acc routine gang
- dimension A(*), B(*)
- !$acc loop gang vector
- do 100 i = 1, N
- !$acc loop seq
- do 10 j = 1, 1000
- if (A(i) .gt. B(i)) goto 20
-10 continue
-20 B(i) = A(i)
-100 continue
-end subroutine
-
-! GOTO1: not yet implemented: unstructured do loop in independent OpenACC loop construct
-
-! GOTO1-OK-LABEL: func.func @_QPtest_unstructured6
-! GOTO1-OK: acc.loop {{.*}}gang vector
-! GOTO1-OK: acc.loop
-
-!--- goto_with_intermediate.f90
-
-! Same as above but with intermediate code between the inner loop end and
-! the GOTO target, exercising the jump-table dispatch path.
-subroutine test_unstructured7(A, B, C, N)
- implicit real*8 (a-h, o-z)
- !$acc routine gang
- dimension A(*), B(*), C(*)
- !$acc loop gang vector
- do 100 i = 1, N
- !$acc loop seq
- do 10 j = 1, 1000
- if (A(i) .gt. B(i)) goto 20
-10 continue
- C(i) = 999.0
-20 B(i) = A(i)
-100 continue
-end subroutine
-
-! GOTO2: not yet implemented: unstructured do loop in independent OpenACC loop construct
-
-! GOTO2-OK-LABEL: func.func @_QPtest_unstructured7
-! GOTO2-OK: acc.loop {{.*}}gang vector
-! GOTO2-OK: acc.loop
-
-!--- collapse_cycle.f90
-
-! Orphan `acc loop collapse(2)` with an early-exit (CYCLE) - defaults to
-! `independent` inside the (non-seq) acc routine.
-subroutine test_unstructured_collapse_loop_only(a)
- !$acc routine gang
- integer :: i, j, jdiag
- real(8) :: a(:,:)
- jdiag = 4
- !$acc loop collapse(2)
- do j = 1, 8
- do i = 1, 8
- if (i == jdiag) then
- a(i, j) = 0.0d0
- cycle
- end if
- a(i, j) = real(i + j, 8)
- end do
- end do
-end subroutine
-
-! CCYCLE: not yet implemented: unstructured do loop in independent OpenACC loop construct
-
-! CCYCLE-OK-LABEL: func.func @_QPtest_unstructured_collapse_loop_only
-! CCYCLE-OK: acc.loop
-
-!--- cache_exit.f90
+! RUN: bbc -fopenacc -emit-hlfir %s -o - | FileCheck %s --check-prefix=CEXIT-OK
+! RUN: %not_todo_cmd bbc -fopenacc -emit-hlfir --emit-independent-loops-as-unstructured=false %s -o - 2>&1 | FileCheck %s --check-prefix=CEXIT
! `acc loop` with `cache` directive and EXIT inside the body - the EXIT
! makes the loop unstructured. Orphan loop inside a (non-seq) acc routine
@@ -116,32 +29,3 @@ subroutine test_cache_single_element()
! CEXIT-OK-LABEL: func.func @_QPtest_cache_single_element
! CEXIT-OK: acc.loop
-
-!--- cache_select_case.f90
-
-! `acc loop` with `cache` directive and SELECT CASE inside the body - the
-! SELECT CASE makes the loop's body have unstructured CFG. Orphan loop
-! inside a (non-seq) acc routine defaults to `independent`.
-subroutine test_cache_nonunit_lb()
- !$acc routine gang
- integer :: arr(10:20)
- integer :: i
-
- !$acc loop
- do i = 10, 20
- !$acc cache(arr(15))
- select case (mod(i, 3))
- case (0)
- arr(i) = i * 2
- case (1)
- arr(i) = i * 3
- case default
- arr(i) = i
- end select
- end do
-end subroutine
-
-! CCASE: not yet implemented: unstructured do loop in independent OpenACC loop construct
-
-! CCASE-OK-LABEL: func.func @_QPtest_cache_nonunit_lb
-! CCASE-OK: acc.loop
diff --git a/flang/test/Lower/OpenACC/acc-directive-loop-bounds.f90 b/flang/test/Lower/OpenACC/acc-directive-loop-bounds.f90
new file mode 100644
index 0000000000000..edc1b2c0e54e3
--- /dev/null
+++ b/flang/test/Lower/OpenACC/acc-directive-loop-bounds.f90
@@ -0,0 +1,66 @@
+! A directive construct is the loop it owns: its lowering reads the construct's
+! own classification to decide whether the loop op carries its bounds. A loop
+! whose branching is confined to its body keeps its structured form, so the
+! construct holding it must be reclassified too -- otherwise the directive gets
+! a bounds-free acc.loop that nothing can partition, with the real loop nested
+! inside it.
+
+! RUN: %flang_fc1 -fopenacc -fdebug-dump-pft -o /dev/null %s 2>&1 | FileCheck %s
+! RUN: %flang_fc1 -fopenacc -emit-hlfir -o - %s | FileCheck %s --check-prefix=FIR
+
+! The CYCLE keeps its branching inside the loop body, so both the loop and the
+! construct that owns it are reclassified.
+subroutine parallel_loop_cycle(a, n)
+ real :: a(n)
+ integer :: n, i
+ !$acc parallel loop
+ do i = 1, n
+ if (a(i) > 0.0) then
+ a(i) = 1.0
+ cycle
+ end if
+ a(i) = 2.0
+ end do
+end subroutine
+
+! CHECK: Subroutine parallel_loop_cycle
+! CHECK: <<OpenACCConstruct~>>
+! CHECK: <<DoConstruct~>>
+
+! One acc.loop, and it is the directive's own: it carries the bounds, with the
+! body's branching folded into a region inside it. A second, nested acc.loop
+! here would mean the bounds landed on a loop the directive does not own.
+! FIR-LABEL: func.func @_QPparallel_loop_cycle
+! FIR: acc.parallel combined(loop) {
+! FIR-NOT: acc.loop
+! FIR: acc.loop combined(parallel) private({{.*}}) control(%{{.*}} : i32) = (%{{.*}} : i32) to (%{{.*}} : i32) step (%{{.*}} : i32) {
+! FIR: scf.execute_region no_inline {
+! FIR: cf.cond_br
+! FIR: scf.yield
+! FIR: }
+! FIR-NOT: acc.loop
+! FIR: acc.yield
+! FIR: }
+
+! Negative: the construct holds a GOTO of its own, so its branching is not
+! confined to the loop and it stays unstructured. The rewrite of a single-
+! statement IF body does not reach this one, so the GOTO survives.
+subroutine parallel_region_goto(a, n)
+ real :: a(n)
+ integer :: n, i
+ !$acc parallel
+ if (n > 0) then
+ a(1) = 0.0
+ goto 90
+ end if
+ !$acc loop
+ do i = 1, n
+ a(i) = 2.0
+ end do
+90 continue
+ !$acc end parallel
+end subroutine
+
+! CHECK: Subroutine parallel_region_goto
+! CHECK: <<OpenACCConstruct!>>
+! CHECK: GotoStmt!
diff --git a/flang/test/Lower/OpenACC/acc-unstructured-combined-construct.f90 b/flang/test/Lower/OpenACC/acc-unstructured-combined-construct.f90
new file mode 100644
index 0000000000000..e238aa2b06922
--- /dev/null
+++ b/flang/test/Lower/OpenACC/acc-unstructured-combined-construct.f90
@@ -0,0 +1,45 @@
+! An unstructured-CFG pattern inside a combined `acc parallel loop` construct
+! (default parallelism is `independent`).
+!
+! The IF-guarded CYCLE branches only within the loop body, so the loop keeps
+! its structured form and carries the construct that owns it along. The bounds
+! of both collapsed levels land on the directive's own acc.loop, whichever way
+! --emit-independent-loops-as-unstructured is set.
+
+! RUN: bbc -fopenacc -emit-hlfir %s -o - | FileCheck %s
+! RUN: bbc -fopenacc -emit-hlfir --emit-independent-loops-as-unstructured=false %s -o - | FileCheck %s
+
+subroutine test_unstructured_collapse_cycle(a)
+ integer :: i, j, jdiag
+ real(8) :: a(:,:)
+ jdiag = 4
+ !$acc parallel loop collapse(2) copy(a)
+ do j = 1, 8
+ do i = 1, 8
+ if (i == jdiag) then
+ a(i, j) = 0.0d0
+ cycle
+ end if
+ a(i, j) = real(i + j, 8)
+ end do
+ end do
+ !$acc end parallel loop
+end subroutine
+
+! CHECK-LABEL: func.func @_QPtest_unstructured_collapse_cycle
+! CHECK: acc.parallel combined(loop)
+! A second acc.loop nested here would mean the bounds landed on a loop the
+! directive does not own.
+! CHECK-NOT: acc.loop
+! CHECK: acc.loop combined(parallel) private({{.*}}) control(%{{.*}} : i32, %{{.*}} : i32) = (%{{.*}}, %{{.*}} : i32, i32) to (%{{.*}}, %{{.*}} : i32, i32) step (%{{.*}}, %{{.*}} : i32, i32) {
+! CHECK: scf.execute_region no_inline {
+! CHECK: cf.cond_br
+! CHECK: scf.yield
+! CHECK: }
+! CHECK: acc.yield
+! CHECK: } inclusiveUpperbound({{.*}}) collapse([2]) collapseDeviceType({{.*}}) independent
+!
+! Nothing else is nested in the compute region, which closes structured.
+! CHECK-NOT: acc.loop
+! CHECK: acc.yield
+! CHECK-NEXT: }
diff --git a/flang/test/Lower/OpenACC/acc-unstructured-internals.f90 b/flang/test/Lower/OpenACC/acc-unstructured-internals.f90
index f0be621384a6a..600ad68047591 100644
--- a/flang/test/Lower/OpenACC/acc-unstructured-internals.f90
+++ b/flang/test/Lower/OpenACC/acc-unstructured-internals.f90
@@ -59,9 +59,12 @@ subroutine parallel_loop_cycle(a, n)
end do
end subroutine
+! The directive owns this loop, so the bounds belong on its own acc.loop --
+! the one carrying combined(parallel) -- and not on a second one nested inside.
! CHECK-LABEL: func.func @_QPparallel_loop_cycle
! CHECK: acc.parallel combined(loop) {
-! CHECK: acc.loop private({{.*}}) control(%{{.*}} : i32) = (%{{.*}} : i32) to (%{{.*}} : i32) step (%{{.*}} : i32) {
+! CHECK-NOT: acc.loop
+! CHECK: acc.loop combined(parallel) private({{.*}}) control(%{{.*}} : i32) = (%{{.*}} : i32) to (%{{.*}} : i32) step (%{{.*}} : i32) {
! CHECK: scf.execute_region no_inline {
! CHECK: cf.cond_br %{{[0-9]+}}, ^bb[[CYCLE:[0-9]+]], ^bb[[BODY:[0-9]+]]
! CHECK: ^bb[[CYCLE]]:
diff --git a/flang/test/Lower/OpenACC/acc-unstructured-loop-construct.f90 b/flang/test/Lower/OpenACC/acc-unstructured-loop-construct.f90
new file mode 100644
index 0000000000000..b5ac87ae2bb23
--- /dev/null
+++ b/flang/test/Lower/OpenACC/acc-unstructured-loop-construct.f90
@@ -0,0 +1,160 @@
+! Each sub-file exercises a different unstructured-CFG pattern inside an
+! `acc loop` whose default parallelism resolves to `independent`.
+!
+! In each, the branching is confined to the loop body, so the loop keeps its
+! structured form and carries the construct that owns it along: the directive's
+! own acc.loop holds the bounds. That holds either way
+! --emit-independent-loops-as-unstructured is set.
+
+! RUN: split-file %s %t
+
+! RUN: bbc -fopenacc -emit-hlfir %t/goto_one_level.f90 -o - | FileCheck %s --check-prefix=GOTO1
+! RUN: bbc -fopenacc -emit-hlfir %t/goto_with_intermediate.f90 -o - | FileCheck %s --check-prefix=GOTO2
+! RUN: bbc -fopenacc -emit-hlfir %t/collapse_cycle.f90 -o - | FileCheck %s --check-prefix=CCYCLE
+! RUN: bbc -fopenacc -emit-hlfir %t/cache_select_case.f90 -o - | FileCheck %s --check-prefix=CCASE
+
+! RUN: bbc -fopenacc -emit-hlfir --emit-independent-loops-as-unstructured=false %t/goto_one_level.f90 -o - | FileCheck %s --check-prefix=GOTO1
+! RUN: bbc -fopenacc -emit-hlfir --emit-independent-loops-as-unstructured=false %t/goto_with_intermediate.f90 -o - | FileCheck %s --check-prefix=GOTO2
+! RUN: bbc -fopenacc -emit-hlfir --emit-independent-loops-as-unstructured=false %t/collapse_cycle.f90 -o - | FileCheck %s --check-prefix=CCYCLE
+! RUN: bbc -fopenacc -emit-hlfir --emit-independent-loops-as-unstructured=false %t/cache_select_case.f90 -o - | FileCheck %s --check-prefix=CCASE
+
+!--- goto_one_level.f90
+
+! GOTO exits the inner `acc loop seq` (one level), landing in the body of
+! the outer `acc loop gang vector`. Outer loop defaults to `independent`.
+subroutine test_unstructured6(N, A, B)
+ implicit real*8 (a-h, o-z)
+ !$acc routine gang
+ dimension A(*), B(*)
+ !$acc loop gang vector
+ do 100 i = 1, N
+ !$acc loop seq
+ do 10 j = 1, 1000
+ if (A(i) .gt. B(i)) goto 20
+10 continue
+20 B(i) = A(i)
+100 continue
+end subroutine
+
+! The outer loop the directive owns: bounds on the op, and its body, which the
+! GOTO branches within, folded into a region.
+! GOTO1-LABEL: func.func @_QPtest_unstructured6
+! GOTO1: acc.loop gang vector private({{.*}}) control(%{{.*}} : i32) = (%{{.*}} : i32) to (%{{.*}} : i32) step (%{{.*}} : i32) {
+! GOTO1: scf.execute_region no_inline {
+!
+! The inner loop, which the GOTO leaves: no bounds on the op, raw branching,
+! and marked unstructured.
+! GOTO1: acc.loop private({{.*}}) {
+! GOTO1: cf.cond_br
+! GOTO1: acc.yield
+! GOTO1: } seq unstructured
+!
+! Nothing further is nested in the outer loop, and it ends structured.
+! GOTO1-NOT: acc.loop
+! GOTO1: acc.yield
+! GOTO1-NEXT: } inclusiveUpperbound({{.*}}) independent
+
+!--- goto_with_intermediate.f90
+
+! Same as above but with intermediate code between the inner loop end and
+! the GOTO target, exercising the jump-table dispatch path.
+subroutine test_unstructured7(A, B, C, N)
+ implicit real*8 (a-h, o-z)
+ !$acc routine gang
+ dimension A(*), B(*), C(*)
+ !$acc loop gang vector
+ do 100 i = 1, N
+ !$acc loop seq
+ do 10 j = 1, 1000
+ if (A(i) .gt. B(i)) goto 20
+10 continue
+ C(i) = 999.0
+20 B(i) = A(i)
+100 continue
+end subroutine
+
+! Same nest as goto_one_level: the outer loop keeps its bounds and wraps its
+! body, the inner one the GOTO leaves keeps neither.
+! GOTO2-LABEL: func.func @_QPtest_unstructured7
+! GOTO2: acc.loop gang vector private({{.*}}) control(%{{.*}} : i32) = (%{{.*}} : i32) to (%{{.*}} : i32) step (%{{.*}} : i32) {
+! GOTO2: scf.execute_region no_inline {
+! GOTO2: acc.loop private({{.*}}) {
+! GOTO2: acc.yield
+! GOTO2: } seq unstructured
+!
+! The jump table: the selector the inner loop stored decides whether the
+! intermediate code between the loop end and the GOTO target runs.
+! GOTO2: %[[SEL:.*]] = fir.load
+! GOTO2: arith.cmpi eq, %[[SEL]], %{{.*}} : i32
+! GOTO2-NEXT: cf.cond_br
+!
+! GOTO2-NOT: acc.loop
+! GOTO2: acc.yield
+! GOTO2-NEXT: } inclusiveUpperbound({{.*}}) independent
+
+!--- collapse_cycle.f90
+
+! Orphan `acc loop collapse(2)` with an early-exit (CYCLE) - defaults to
+! `independent` inside the (non-seq) acc routine.
+subroutine test_unstructured_collapse_loop_only(a)
+ !$acc routine gang
+ integer :: i, j, jdiag
+ real(8) :: a(:,:)
+ jdiag = 4
+ !$acc loop collapse(2)
+ do j = 1, 8
+ do i = 1, 8
+ if (i == jdiag) then
+ a(i, j) = 0.0d0
+ cycle
+ end if
+ a(i, j) = real(i + j, 8)
+ end do
+ end do
+end subroutine
+
+! One loop for both collapsed levels: two induction variables on the op, and
+! the body the CYCLE branches within folded into a region. A second acc.loop
+! anywhere would mean a level landed on a loop of its own.
+! CCYCLE-LABEL: func.func @_QPtest_unstructured_collapse_loop_only
+! CCYCLE: acc.loop private({{.*}}) control(%{{.*}} : i32, %{{.*}} : i32) = (%{{.*}}, %{{.*}} : i32, i32) to (%{{.*}}, %{{.*}} : i32, i32) step (%{{.*}}, %{{.*}} : i32, i32) {
+! CCYCLE: scf.execute_region no_inline {
+! CCYCLE: cf.cond_br
+! CCYCLE-NOT: acc.loop
+! CCYCLE: acc.yield
+! CCYCLE-NEXT: } inclusiveUpperbound({{.*}}) collapse([2]) collapseDeviceType({{.*}}) independent
+
+!--- cache_select_case.f90
+
+! `acc loop` with `cache` directive and SELECT CASE inside the body - the
+! SELECT CASE makes the loop's body have unstructured CFG. Orphan loop
+! inside a (non-seq) acc routine defaults to `independent`.
+subroutine test_cache_nonunit_lb()
+ !$acc routine gang
+ integer :: arr(10:20)
+ integer :: i
+
+ !$acc loop
+ do i = 10, 20
+ !$acc cache(arr(15))
+ select case (mod(i, 3))
+ case (0)
+ arr(i) = i * 2
+ case (1)
+ arr(i) = i * 3
+ case default
+ arr(i) = i
+ end select
+ end do
+end subroutine
+
+! One loop, bounds on the op, and the SELECT CASE that makes the body branch
+! folded into a region along with the cache directive it holds.
+! CCASE-LABEL: func.func @_QPtest_cache_nonunit_lb
+! CCASE: acc.loop private({{.*}}) control(%{{.*}} : i32) = (%{{.*}} : i32) to (%{{.*}} : i32) step (%{{.*}} : i32) {
+! CCASE: scf.execute_region no_inline {
+! CCASE: acc.cache var({{.*}}) name("arr(15)")
+! CCASE: fir.select_case %{{.*}} : i32 [#fir.point, %{{.*}}, ^{{.*}}, #fir.point, %{{.*}}, ^{{.*}}, unit, ^{{.*}}]
+! CCASE-NOT: acc.loop
+! CCASE: acc.yield
+! CCASE-NEXT: } inclusiveUpperbound({{.*}}) independent
diff --git a/flang/test/Lower/OpenACC/acc-unstructured.f90 b/flang/test/Lower/OpenACC/acc-unstructured.f90
index 6a95f664f78df..b2f5de9cb9b93 100644
--- a/flang/test/Lower/OpenACC/acc-unstructured.f90
+++ b/flang/test/Lower/OpenACC/acc-unstructured.f90
@@ -180,8 +180,10 @@ subroutine test_unstructured6(N, A, B)
end subroutine
! CHECK-LABEL: func.func @_QPtest_unstructured6
-! CHECK: acc.loop gang vector
-! CHECK: acc.loop
+! The outer loop keeps its bounds on the op; the inner one, which the GOTO
+! leaves, has none.
+! CHECK: acc.loop gang vector private({{.*}}) control(%{{.*}} : i32) = (%{{.*}} : i32) to (%{{.*}} : i32) step (%{{.*}} : i32) {
+! CHECK: acc.loop private({{.*}}) {
! CHECK: arith.cmpf ogt
! CHECK: fir.store %{{.*}} to %{{.*}} : !fir.ref<i32>
! CHECK: acc.yield
@@ -205,9 +207,11 @@ subroutine test_unstructured7(A, B, C, N)
end subroutine
! CHECK-LABEL: func.func @_QPtest_unstructured7
-! CHECK: acc.loop gang vector
+! The outer loop keeps its bounds on the op; the inner one, which the GOTO
+! leaves, has none.
+! CHECK: acc.loop gang vector private({{.*}}) control(%{{.*}} : i32) = (%{{.*}} : i32) to (%{{.*}} : i32) step (%{{.*}} : i32) {
! Inner loop stores exit selector and yields:
-! CHECK: acc.loop
+! CHECK: acc.loop private({{.*}}) {
! CHECK: fir.store %{{.*}} to %{{.*}} : !fir.ref<i32>
! CHECK: acc.yield
! CHECK: } seq unstructured
@@ -275,18 +279,18 @@ subroutine test_unstructured_collapse_cycle(a)
! Both induction variables (j and i) are privatized:
! CHECK: %[[PRIVJ:.*]] = acc.private varPtr(%{{.*}} : !fir.ref<i32>) recipe(@privatization_ref_i32) implicit(true) name("j") -> !fir.ref<i32>
! CHECK: %[[PRIVI:.*]] = acc.private varPtr(%{{.*}} : !fir.ref<i32>) recipe(@privatization_ref_i32) implicit(true) name("i") -> !fir.ref<i32>
-! CHECK: acc.loop combined(serial) private(%[[PRIVJ]], %[[PRIVI]] : !fir.ref<i32>, !fir.ref<i32>) {
-! The IF-guarded CYCLE branches only within the body, so both loops keep their
-! bounds on the op -- control(...) rather than a cf trip-count test -- and the
-! raw blocks are confined to a wrap inside each body.
-! CHECK: acc.loop private({{.*}}) control(%{{.*}} : i32) = (%{{.*}} : i32) to (%{{.*}} : i32) step (%{{.*}} : i32) {
-! CHECK: scf.execute_region no_inline {
-! CHECK: acc.loop private({{.*}}) control(%{{.*}} : i32) = (%{{.*}} : i32) to (%{{.*}} : i32) step (%{{.*}} : i32) {
+! The IF-guarded CYCLE branches only within the body, so the directive's own
+! acc.loop keeps the bounds of both collapsed levels -- control(...) rather
+! than a cf trip-count test -- and the raw blocks are confined to a wrap inside
+! the body. No further acc.loop is nested inside it.
+! CHECK: acc.loop combined(serial) private(%[[PRIVJ]], %[[PRIVI]] : !fir.ref<i32>, !fir.ref<i32>) control(%{{.*}} : i32, %{{.*}} : i32) = (%{{.*}}, %{{.*}} : i32, i32) to (%{{.*}}, %{{.*}} : i32, i32) step (%{{.*}}, %{{.*}} : i32, i32) {
+! CHECK-NOT: acc.loop
! CHECK: scf.execute_region no_inline {
! CHECK: arith.cmpi eq
! CHECK: cf.cond_br
! CHECK: scf.yield
! CHECK: acc.yield
+! CHECK: } inclusiveUpperbound({{.*}}) collapse([2])
! `acc serial loop collapse(N)` with STOP in body: wrap-in-execute-region hides
! the unstructured if/stop and the three collapsed iterators lower as a single
@@ -333,9 +337,12 @@ subroutine test_unstructured_collapse_loop_only(a)
end subroutine
! CHECK-LABEL: func.func @_QPtest_unstructured_collapse_loop_only
-! Standalone acc.loop (no `combined(...)`):
-! CHECK: acc.loop private(%{{.*}}, %{{.*}} : !fir.ref<i32>, !fir.ref<i32>) {
-! CHECK: } collapse([2]) collapseDeviceType([#acc.device_type<none>]) independent unstructured
+! Standalone acc.loop (no `combined(...)`). The directive owns the loop, so it
+! carries the bounds of both collapsed levels and the body's branching is
+! confined to a wrap -- the op is no longer `unstructured`.
+! CHECK: acc.loop private(%{{.*}}, %{{.*}} : !fir.ref<i32>, !fir.ref<i32>) control(%{{.*}} : i32, %{{.*}} : i32) = (%{{.*}}, %{{.*}} : i32, i32) to (%{{.*}}, %{{.*}} : i32, i32) step (%{{.*}}, %{{.*}} : i32, i32) {
+! CHECK: scf.execute_region no_inline {
+! CHECK: } inclusiveUpperbound({{.*}}) collapse([2]) collapseDeviceType([#acc.device_type<none>]) independent
! Standalone `acc loop seq` with STOP: wrap-in-execute-region hides the
! if/stop and the DO lowers as structured acc.loop control(...) (no
@@ -515,7 +522,7 @@ subroutine test_unstructured_parallel_loop_stop(a, b, c)
! CHECK-LABEL: func.func @_QPtest_unstructured_parallel_loop_stop
! CHECK: acc.parallel combined(loop)
-! CHECK: acc.loop combined(parallel)
+! CHECK: acc.loop combined(parallel) private({{.*}}) control(%{{.*}} : i32) = (%{{.*}} : i32) to (%{{.*}} : i32) step (%{{.*}} : i32) {
! `!$acc parallel loop collapse(3)` with STOP in the innermost body. Same
! wrap behavior as above with an added collapse clause.
@@ -534,7 +541,7 @@ subroutine test_unstructured_parallel_loop_collapse3_stop(a)
! CHECK-LABEL: func.func @_QPtest_unstructured_parallel_loop_collapse3_stop
! CHECK: acc.parallel combined(loop)
-! CHECK: acc.loop combined(parallel)
+! CHECK: acc.loop combined(parallel) private({{.*}}) control(%{{.*}} : i32, %{{.*}} : i32, %{{.*}} : i32) = (%{{.*}}, %{{.*}}, %{{.*}} : i32, i32, i32) to (%{{.*}}, %{{.*}}, %{{.*}} : i32, i32, i32) step (%{{.*}}, %{{.*}}, %{{.*}} : i32, i32, i32) {
! Nested DO loops inside `!$acc kernels` where the inner loop branches to its
! own exit. Only the inner loop is unstructured, so the outer one still lowers
diff --git a/flang/test/Lower/OpenMP/wsloop-directive-loop-bounds.f90 b/flang/test/Lower/OpenMP/wsloop-directive-loop-bounds.f90
new file mode 100644
index 0000000000000..9b7be7bc6c549
--- /dev/null
+++ b/flang/test/Lower/OpenMP/wsloop-directive-loop-bounds.f90
@@ -0,0 +1,34 @@
+! A directive construct is the loop it owns, so a loop whose branching is
+! confined to its body carries the construct holding it along when it is
+! reclassified. The loop keeps its bounds and the body's branching is folded
+! into a region inside it.
+
+! RUN: %flang_fc1 -fopenmp -fdebug-dump-pft -o /dev/null %s 2>&1 | FileCheck %s
+! RUN: %flang_fc1 -fopenmp -emit-hlfir -o - %s | FileCheck %s --check-prefix=FIR
+
+subroutine wsloop_cycle(a, n)
+ real :: a(n)
+ integer :: n, i
+ !$omp parallel do
+ do i = 1, n
+ if (a(i) > 0.0) then
+ a(i) = 1.0
+ cycle
+ end if
+ a(i) = 2.0
+ end do
+ !$omp end parallel do
+end subroutine
+
+! CHECK: Subroutine wsloop_cycle
+! CHECK: <<OpenMPConstruct~>>
+! CHECK: <<DoConstruct~>>
+
+! FIR-LABEL: func.func @_QPwsloop_cycle
+! FIR: omp.parallel {
+! FIR: omp.wsloop private({{.*}}) {
+! FIR: omp.loop_nest (%{{.*}}) : i32 = (%{{.*}}) to (%{{.*}}) inclusive step (%{{.*}}) {
+! FIR: scf.execute_region no_inline {
+! FIR: cf.cond_br
+! FIR: scf.yield
+! FIR: }
>From 3774cff8c58b1cddcb7532518a77b21c89d08ad8 Mon Sep 17 00:00:00 2001
From: ergawy <kareem.ergawy at gmail.com>
Date: Wed, 30 Sep 2026 09:11:16 -0700
Subject: [PATCH 2/3] [flang][NFC] Correct a stale comment on loop
reclassification
Two places weaken the classification now, so calling this one "the one
place" is out of date.
---
flang/lib/Lower/PFTBuilder.cpp | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/flang/lib/Lower/PFTBuilder.cpp b/flang/lib/Lower/PFTBuilder.cpp
index 7331229c06d4a..2bbf6eebc2c73 100644
--- a/flang/lib/Lower/PFTBuilder.cpp
+++ b/flang/lib/Lower/PFTBuilder.cpp
@@ -3049,8 +3049,7 @@ static void detectStructuredWithUnstructuredInternals(
if (e.isA<parser::DoConstruct>() &&
isStructurableWithUnstructuredInternals(e, unit)) {
- // The one place the classification weakens: detection has proven
- // Unstructured unnecessary.
+ // Detection has proven Unstructured unnecessary for this loop.
e.weakenControlFlow(Fortran::lower::pft::Evaluation::ControlFlow::
StructuredWithUnstructuredInternals);
continue;
>From cd1cc7ceb7b822eb12f0c53c86d9c93f5b8b344f Mon Sep 17 00:00:00 2001
From: ergawy <kareem.ergawy at gmail.com>
Date: Wed, 30 Sep 2026 22:28:45 -0700
Subject: [PATCH 3/3] [flang][Test] Cover an unstructured combined construct
and collapse(force:)
Three cases the lowering handles but nothing exercised:
A combined construct whose body branching keeps it unstructured. The
GOTO jumps backwards, so the loop is not proven to terminate and the
directive cannot take it over; it keeps the TODO the deleted test
covered. A GOTO leaving the loop would not serve, nor EXIT or RETURN:
all three are rejected for a combined construct before lowering runs.
collapse(force:2) over a nest whose inner body branches within itself,
once with a CYCLE and once with GOTOs. Both levels' bounds belong on the
directive's own loop and the body is folded into a region. Two GOTOs to
two targets are needed: one IF-guarded GOTO over a single statement is
rewritten into a fir.if and never reaches the raw form.
---
.../acc-unstructured-combined-construct.f90 | 40 ++++++++++++
.../acc-collapse-force-unstructured-body.f90 | 65 +++++++++++++++++++
2 files changed, 105 insertions(+)
create mode 100644 flang/test/Lower/OpenACC/Todo/acc-unstructured-combined-construct.f90
create mode 100644 flang/test/Lower/OpenACC/acc-collapse-force-unstructured-body.f90
diff --git a/flang/test/Lower/OpenACC/Todo/acc-unstructured-combined-construct.f90 b/flang/test/Lower/OpenACC/Todo/acc-unstructured-combined-construct.f90
new file mode 100644
index 0000000000000..8747d0fb68eae
--- /dev/null
+++ b/flang/test/Lower/OpenACC/Todo/acc-unstructured-combined-construct.f90
@@ -0,0 +1,40 @@
+! A combined `acc parallel loop` whose default parallelism resolves to
+! `independent` and whose body branching keeps it unstructured.
+!
+! The GOTO jumps backwards, so the body reaches the GOTO again from its own
+! target and the loop is not proven to terminate. It stays unstructured and the
+! directive cannot take it over. The patterns that do keep their branching
+! confined live in ../acc-unstructured-combined-construct.f90.
+!
+! A GOTO leaving the loop would not serve here: it is rejected on both paths,
+! as is EXIT or RETURN in a combined construct.
+
+! RUN: bbc -fopenacc -emit-hlfir %s -o - | FileCheck %s --check-prefix=CBACK-OK
+! RUN: %not_todo_cmd bbc -fopenacc -emit-hlfir --emit-independent-loops-as-unstructured=false %s -o - 2>&1 | FileCheck %s --check-prefix=CBACK
+
+subroutine test_combined_backward_goto(a, n)
+ integer :: n, i
+ real :: a(n)
+
+ !$acc parallel loop
+ do i = 1, n
+20 continue
+ a(i) = a(i) * 2.0
+ if (a(i) < 100.0) goto 20
+ end do
+end subroutine
+
+! CBACK: not yet implemented: unstructured do loop in combined acc construct
+
+! By default the loop still lowers, but without its bounds on the op: the
+! directive did not take it over. The branching stays raw in the loop's own
+! region rather than being folded into one, which is what a loop that is
+! unstructured throughout gets.
+! CBACK-OK-LABEL: func.func @_QPtest_combined_backward_goto
+! CBACK-OK: acc.parallel combined(loop)
+! CBACK-OK: acc.loop combined(parallel) private({{.*}}) {
+! CBACK-OK-NOT: control(
+! CBACK-OK-NOT: scf.execute_region
+! CBACK-OK: cf.cond_br
+! CBACK-OK-NOT: scf.execute_region
+! CBACK-OK: } independent unstructured
diff --git a/flang/test/Lower/OpenACC/acc-collapse-force-unstructured-body.f90 b/flang/test/Lower/OpenACC/acc-collapse-force-unstructured-body.f90
new file mode 100644
index 0000000000000..e13accfff063c
--- /dev/null
+++ b/flang/test/Lower/OpenACC/acc-collapse-force-unstructured-body.f90
@@ -0,0 +1,65 @@
+! collapse(force:2) over a nest whose inner body branches within itself.
+!
+! The force lowering absorbs both levels into the directive's own acc.loop and
+! sinks whatever sits between them, so the bounds of both levels belong on that
+! op. The branching stays inside the body, which therefore still needs folding
+! into a region.
+
+! RUN: bbc -fopenacc -emit-hlfir %s -o - | FileCheck %s
+! RUN: bbc -fopenacc -emit-hlfir --emit-independent-loops-as-unstructured=false %s -o - | FileCheck %s
+
+! A CYCLE in the inner body: it branches to that body's own end.
+subroutine collapse_force_cycle(a, n, m)
+ integer :: n, m, i, j
+ real :: a(n,m)
+
+ !$acc parallel loop collapse(force:2)
+ do i = 1, n
+ do j = 1, m
+ if (a(i,j) > 0.0) then
+ a(i,j) = 1.0
+ cycle
+ end if
+ a(i,j) = 2.0
+ end do
+ end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPcollapse_force_cycle
+! CHECK: acc.parallel combined(loop)
+! CHECK: acc.loop combined(parallel) private({{.*}}) control(%{{.*}} : i32, %{{.*}} : i32) = (%{{.*}}, %{{.*}} : i32, i32) to (%{{.*}}, %{{.*}} : i32, i32) step (%{{.*}}, %{{.*}} : i32, i32) {
+! CHECK: scf.execute_region no_inline {
+! CHECK-NOT: acc.loop
+! CHECK: acc.yield
+! CHECK-NEXT: } inclusiveUpperbound({{.*}}) collapse([2])
+
+! The same nest with GOTOs to labels later in the inner body. Two of them, to
+! two targets: a single IF-guarded GOTO over one statement is rewritten into a
+! fir.if and never reaches the raw form this exercises.
+subroutine collapse_force_goto(a, n, m)
+ integer :: n, m, i, j
+ real :: a(n,m)
+
+ !$acc parallel loop collapse(force:2)
+ do i = 1, n
+ do j = 1, m
+ if (a(i,j) > 0.0) goto 20
+ if (a(i,j) < -1.0) goto 30
+ a(i,j) = 1.0
+ goto 40
+20 a(i,j) = 2.0
+ goto 40
+30 a(i,j) = 3.0
+40 continue
+ end do
+ end do
+end subroutine
+
+! CHECK-LABEL: func.func @_QPcollapse_force_goto
+! CHECK: acc.parallel combined(loop)
+! CHECK: acc.loop combined(parallel) private({{.*}}) control(%{{.*}} : i32, %{{.*}} : i32) = (%{{.*}}, %{{.*}} : i32, i32) to (%{{.*}}, %{{.*}} : i32, i32) step (%{{.*}}, %{{.*}} : i32, i32) {
+! CHECK: scf.execute_region no_inline {
+! CHECK: cf.cond_br
+! CHECK-NOT: acc.loop
+! CHECK: acc.yield
+! CHECK-NEXT: } inclusiveUpperbound({{.*}}) collapse([2])
More information about the flang-commits
mailing list