[llvm-branch-commits] [flang] [flang] Let a directive keep the loop it owns when its body branches (PR #227707)
Kareem Ergawy via llvm-branch-commits
llvm-branch-commits at lists.llvm.org
Wed Sep 30 06:20:07 PDT 2026
https://github.com/ergawy created https://github.com/llvm/llvm-project/pull/227707
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.
>From d08b5994d06c652257364642af3e36ed4771945e 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] [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 2f665a6cbf693..baaeb72906cbf 100644
--- a/flang/lib/Lower/PFTBuilder.cpp
+++ b/flang/lib/Lower/PFTBuilder.cpp
@@ -2990,15 +2990,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
@@ -3015,11 +3036,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: }
More information about the llvm-branch-commits
mailing list