[llvm-branch-commits] [flang] [flang] Detect loops whose branching is confined to their body (PR #225757)

Kareem Ergawy via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Thu Sep 24 03:06:56 PDT 2026


https://github.com/ergawy updated https://github.com/llvm/llvm-project/pull/225757

>From c462f43ddaea605649ca93f823d1e23183b33902 Mon Sep 17 00:00:00 2001
From: ergawy <kareem.ergawy at gmail.com>
Date: Mon, 21 Sep 2026 06:16:09 -0700
Subject: [PATCH 1/2] [flang] Detect loops whose branching is confined to their
 body

A DO loop is classified as either structured or unstructured, and a single
raw branch anywhere in its body forces the loop -- and every construct
enclosing it -- onto the unstructured path.

That is stronger than necessary. A loop keeps its structured control flow
as long as its branching neither leaves its body nor enters it from
outside. Classify such a loop separately from a fully unstructured one.

This only classifies: lowering is unchanged. PFT dumps mark the new
classification with '~', which is what the tests key on.
---
 flang/include/flang/Lower/PFTBuilder.h        |  57 ++++-
 flang/lib/Lower/PFTBuilder.cpp                | 214 ++++++++++++++++--
 .../pre-fir-tree-unstructured-internals.f90   | 137 +++++++++++
 flang/test/Lower/trailing-cycle.f90           |  15 +-
 4 files changed, 388 insertions(+), 35 deletions(-)
 create mode 100644 flang/test/Lower/pre-fir-tree-unstructured-internals.f90

diff --git a/flang/include/flang/Lower/PFTBuilder.h b/flang/include/flang/Lower/PFTBuilder.h
index 7e748ae554a20..930214244c74c 100644
--- a/flang/include/flang/Lower/PFTBuilder.h
+++ b/flang/include/flang/Lower/PFTBuilder.h
@@ -28,6 +28,7 @@
 #include "flang/Semantics/symbol.h"
 #include "llvm/Support/ErrorHandling.h"
 #include "llvm/Support/raw_ostream.h"
+#include <algorithm>
 
 namespace Fortran::lower::pft {
 
@@ -322,6 +323,50 @@ struct Evaluation : EvaluationVariant {
   /// Return the FunctionLikeUnit containing this evaluation (or nullptr).
   FunctionLikeUnit *getOwningProcedure() const;
 
+  /// How this evaluation's control flow is lowered. Ordered by how much it
+  /// constrains lowering so that classification can only strengthen; see
+  /// markControlFlow.
+  enum class ControlFlow {
+    /// Lowered structurally
+    Structured,
+    /// Lowered structurally, but the body holds unstructured control flow
+    /// confined to it. Lowering folds that body -- not the construct, and not
+    /// the loop control -- into a parent region, so the structured op's
+    /// single-block region stays well formed.
+    ///
+    /// For now, this only applies to DO constructs.
+    StructuredWithUnstructuredInternals,
+    /// Lowered as raw CFG blocks.
+    Unstructured,
+  };
+
+  /// Strengthen the classification to \p kind; it never weakens. This is what
+  /// makes the analysis order-independent: a construct marked Unstructured by
+  /// any one child stays Unstructured whatever its siblings contribute.
+  void markControlFlow(ControlFlow kind) {
+    controlFlow = std::max(controlFlow, kind);
+  }
+
+  void markUnstructured() { markControlFlow(ControlFlow::Unstructured); }
+
+  /// Lower the classification to \p kind. Only for an analysis that has proven
+  /// a stronger classification unnecessary; every other caller wants
+  /// markControlFlow, which never weakens.
+  void weakenControlFlow(ControlFlow kind) {
+    controlFlow = std::min(controlFlow, kind);
+  }
+
+  /// True when control flow is not fully structured, category (c) included.
+  /// Existing consumers ask this to decide whether raw blocks are needed, and
+  /// a category (c) construct still needs them until its body is wrapped, so
+  /// it must answer true here. Use hasUnstructuredInternals() to single out
+  /// category (c) itself.
+  bool isUnstructured() const { return controlFlow != ControlFlow::Structured; }
+
+  bool hasUnstructuredInternals() const {
+    return controlFlow == ControlFlow::StructuredWithUnstructuredInternals;
+  }
+
   bool lowerAsStructured() const;
   bool lowerAsUnstructured() const;
   bool forceAsUnstructured() const;
@@ -339,11 +384,11 @@ struct Evaluation : EvaluationVariant {
   // from anywhere within the construct.
   //
   // An unstructured construct is one that contains some form of goto. This
-  // is indicated by the isUnstructured member flag, which may be set on a
-  // statement and propagated to enclosing constructs. This distinction allows
-  // a structured IF or DO statement to be materialized with custom structured
-  // FIR operations. An unstructured statement is materialized as mlir
-  // operation sequences that include explicit branches.
+  // is indicated by the controlFlow member, which may be set on a statement and
+  // propagated to enclosing constructs. This distinction allows a structured IF
+  // or DO statement to be materialized with custom structured FIR operations.
+  // An unstructured statement is materialized as mlir operation sequences that
+  // include explicit branches.
   //
   // The block member is set for statements that begin a new block. This
   // block is the target of any branch to the statement. Statements may have
@@ -374,7 +419,7 @@ struct Evaluation : EvaluationVariant {
   llvm::SmallVector<Evaluation *, 0> extraControlSuccessors;
   Evaluation *constructExit{nullptr};    // set for constructs
   bool isNewBlock{false};                // evaluation begins a new basic block
-  bool isUnstructured{false};  // evaluation has unstructured control flow
+  ControlFlow controlFlow{ControlFlow::Structured};
   bool negateCondition{false}; // If[Then]Stmt condition must be negated
   bool activeConstruct{false}; // temporarily set for some constructs
   // The enclosing evaluation-list traversal should skip this evaluation once
diff --git a/flang/lib/Lower/PFTBuilder.cpp b/flang/lib/Lower/PFTBuilder.cpp
index e6a4d11904679..948589a11b7d7 100644
--- a/flang/lib/Lower/PFTBuilder.cpp
+++ b/flang/lib/Lower/PFTBuilder.cpp
@@ -34,6 +34,9 @@ llvm::cl::opt<bool> wrapUnstructuredConstructsInExecuteRegion(
 
 using namespace Fortran;
 
+static void detectStructuredWithUnstructuredInternals(
+    Fortran::lower::pft::FunctionLikeUnit &unit);
+
 namespace {
 static llvm::cl::opt<bool> lowerDoWhileToSCFWhile(
     "lower-do-while-to-scf-while", llvm::cl::init(false),
@@ -525,11 +528,16 @@ class PFTBuilder {
   }
 
   void exitFunction() {
+    lower::pft::FunctionLikeUnit *exitingUnit = currentFunctionUnit;
     currentFunctionUnit = nullptr; // Clear when exiting function
     rewriteIfGotos();
     endFunctionBody();
     analyzeBranches(nullptr, *evaluationListStack.back()); // add branch links
 
+    // Branch analysis is complete, so the incoming-branch map is too.
+    if (exitingUnit)
+      detectStructuredWithUnstructuredInternals(*exitingUnit);
+
     processEntryPoints();
     containsStmtStack.pop_back();
     popEvaluationList();
@@ -862,7 +870,7 @@ class PFTBuilder {
       if (const auto *expr = std::get_if<parser::Expr>(&format.u)) {
         if (semantics::ExprHasTypeCategory(*semantics::GetExpr(*expr),
                                            common::TypeCategory::Integer))
-          eval.isUnstructured = true;
+          eval.markUnstructured();
       }
     };
     auto analyzeSpecs{[&](const auto &specList) {
@@ -916,7 +924,7 @@ class PFTBuilder {
   /// Mark the target of a branch as a new block.
   void markBranchTarget(lower::pft::Evaluation &sourceEvaluation,
                         lower::pft::Evaluation &targetEvaluation) {
-    sourceEvaluation.isUnstructured = true;
+    sourceEvaluation.markUnstructured();
     if (!sourceEvaluation.controlSuccessor)
       sourceEvaluation.controlSuccessor = &targetEvaluation;
     else if (sourceEvaluation.controlSuccessor != &targetEvaluation &&
@@ -942,11 +950,11 @@ class PFTBuilder {
       if (sourceConstruct != targetConstruct) // branch into a construct body
         for (lower::pft::Evaluation *eval = &targetEvaluation; eval;
              eval = eval->parentConstruct) {
-          eval->isUnstructured = true;
+          eval->markUnstructured();
           // If the branch is a backward branch into an already analyzed
           // DO or IF construct, mark the construct exit as a new block.
-          // For a forward branch, the isUnstructured flag will cause this
-          // to be done when the construct is analyzed.
+          // For a forward branch, the Unstructured classification will cause
+          // this to be done when the construct is analyzed.
           if (eval->constructExit && (eval->isA<parser::DoConstruct>() ||
                                       eval->isA<parser::IfConstruct>()))
             eval->constructExit->isNewBlock = true;
@@ -1051,7 +1059,7 @@ class PFTBuilder {
             markBranchTarget(eval, *construct->constructExit);
           },
           [&](const parser::FailImageStmt &) {
-            eval.isUnstructured = true;
+            eval.markUnstructured();
             if (eval.lexicalSuccessor->lexicalSuccessor)
               markSuccessorAsNewBlock(eval);
           },
@@ -1061,12 +1069,12 @@ class PFTBuilder {
             lastConstructStmtEvaluation = &eval;
           },
           [&](const parser::ReturnStmt &) {
-            eval.isUnstructured = true;
+            eval.markUnstructured();
             if (eval.lexicalSuccessor->lexicalSuccessor)
               markSuccessorAsNewBlock(eval);
           },
           [&](const parser::StopStmt &) {
-            eval.isUnstructured = true;
+            eval.markUnstructured();
             if (eval.lexicalSuccessor->lexicalSuccessor)
               markSuccessorAsNewBlock(eval);
           },
@@ -1094,7 +1102,7 @@ class PFTBuilder {
               target->isNewBlock = true;
               for (lower::pft::Evaluation *parent = target->parentConstruct;
                    parent; parent = parent->parentConstruct) {
-                parent->isUnstructured = true;
+                parent->markUnstructured();
                 // The exit of an enclosing DO or IF construct is a new block.
                 if (parent->constructExit &&
                     (parent->isA<parser::DoConstruct>() ||
@@ -1140,7 +1148,7 @@ class PFTBuilder {
                 for (auto label : iter->second)
                   markIfBranchTarget(label);
             }
-            eval.isUnstructured = true;
+            eval.markUnstructured();
             markSuccessorAsNewBlock(eval);
           },
 
@@ -1181,7 +1189,7 @@ class PFTBuilder {
             const auto &loopControl =
                 std::get<std::optional<parser::LoopControl>>(s.t);
             if (!loopControl.has_value()) {
-              eval.isUnstructured = true; // infinite loop
+              eval.markUnstructured(); // infinite loop
               return;
             }
             eval.nonNopSuccessor().isNewBlock = true;
@@ -1190,13 +1198,13 @@ class PFTBuilder {
                     std::get_if<parser::LoopControl::Bounds>(&loopControl->u)) {
               if (bounds->Name().thing.symbol->GetType()->IsNumeric(
                       common::TypeCategory::Real))
-                eval.isUnstructured = true; // real-valued loop control
+                eval.markUnstructured(); // real-valued loop control
             } else if (std::get_if<parser::ScalarLogicalExpr>(
                            &loopControl->u)) {
               // Leave DO WHILE structured when -lower-do-while-to-scf-while is
               // enabled; branch analysis will mark unstructured cases.
               if (!lowerDoWhileToSCFWhile)
-                eval.isUnstructured = true; // while loop
+                eval.markUnstructured(); // while loop
             }
           },
           [&](const parser::EndDoStmt &) {
@@ -1277,7 +1285,7 @@ class PFTBuilder {
           },
           [&](const parser::CaseConstruct &) {
             eval.constructExit = &eval.evaluationList->back();
-            eval.isUnstructured = true;
+            eval.markUnstructured();
           },
           [&](const parser::ChangeTeamConstruct &) {
             eval.constructExit = &eval.evaluationList->back();
@@ -1290,11 +1298,11 @@ class PFTBuilder {
           [&](const parser::IfConstruct &) { setConstructExit(eval); },
           [&](const parser::SelectRankConstruct &) {
             eval.constructExit = &eval.evaluationList->back();
-            eval.isUnstructured = true;
+            eval.markUnstructured();
           },
           [&](const parser::SelectTypeConstruct &) {
             eval.constructExit = &eval.evaluationList->back();
-            eval.isUnstructured = true;
+            eval.markUnstructured();
           },
           [&](const parser::WhereConstruct &) { setConstructExit(eval); },
 
@@ -1315,12 +1323,12 @@ class PFTBuilder {
       if (eval.evaluationList)
         analyzeBranches(&eval, *eval.evaluationList);
 
-      // Propagate isUnstructured flag to enclosing construct -- unless the
-      // wrap pass will fold this construct into a self-contained
+      // Propagate the Unstructured classification to the enclosing construct --
+      // unless the wrap pass will fold this construct into a self-contained
       // scf.execute_region, in which case the parent sees only a single op.
-      if (parentConstruct && eval.isUnstructured &&
+      if (parentConstruct && eval.isUnstructured() &&
           !lower::pft::isWrappableConstruct(eval, semanticsContext))
-        parentConstruct->isUnstructured = true;
+        parentConstruct->markUnstructured();
 
       // The successor of a branch starts a new block.
       if (eval.controlSuccessor && eval.isActionStmt() &&
@@ -1472,7 +1480,11 @@ class PFTDumper {
                       const std::string &indentString, int indent = 1) {
     llvm::StringRef name = evaluationName(eval);
     llvm::StringRef newBlock = eval.isNewBlock ? "^" : "";
-    llvm::StringRef bang = eval.isUnstructured ? "!" : "";
+    // "!" marks an unstructured evaluation. "~" marks one that is structured
+    // on the outside but has unstructured internals.
+    llvm::StringRef bang = eval.hasUnstructuredInternals()
+                               ? "~"
+                               : (eval.isUnstructured() ? "!" : "");
     outputStream << indentString;
     if (eval.printIndex)
       outputStream << eval.printIndex << ' ';
@@ -1720,7 +1732,7 @@ bool Fortran::lower::pft::Evaluation::lowerAsStructured() const {
 }
 
 bool Fortran::lower::pft::Evaluation::lowerAsUnstructured() const {
-  return isUnstructured || clDisableStructuredFir;
+  return isUnstructured() || clDisableStructuredFir;
 }
 
 bool Fortran::lower::pft::Evaluation::forceAsUnstructured() const {
@@ -2750,13 +2762,169 @@ static bool isOmpLoopBody(const Fortran::lower::pft::Evaluation &eval,
   return isAssociatedLoop(chain, loop->GetNestedLoop(), n);
 }
 
+/// The evaluations forming a loop's body: everything between the loop control
+/// statements, which bracket it and are lowered outside any body wrap.
+static llvm::iterator_range<Fortran::lower::pft::EvaluationList::const_iterator>
+loopBodyRange(const Fortran::lower::pft::Evaluation &loop) {
+  const auto &list = *loop.evaluationList;
+  return llvm::make_range(std::next(list.begin()), std::prev(list.end()));
+}
+
+/// True when \p eval lies in \p loop's body rather than in its loop control.
+static bool isInLoopBody(const Fortran::lower::pft::Evaluation *eval,
+                         const Fortran::lower::pft::Evaluation &loop) {
+  if (!eval || !loop.evaluationList || loop.evaluationList->empty())
+    return false;
+  const Fortran::lower::pft::Evaluation *first = &loop.evaluationList->front();
+  const Fortran::lower::pft::Evaluation *last = &loop.evaluationList->back();
+  for (const Fortran::lower::pft::Evaluation *p = eval; p;
+       p = p->parentConstruct)
+    if (p->parentConstruct == &loop)
+      return p != first && p != last;
+  return false;
+}
+
+/// A loop that can be lowered structurally even though its body holds
+/// unstructured control flow, because that control flow is confined to the body
+/// and can be folded into an SESE region.
+///
+/// The loop qualifies when:
+///   1. every branch leaving its body lands back inside that body, and
+///   2. every branch into its body comes from inside that body,
+/// and the body holds nothing a region cannot accommodate: an infinite DO
+/// never reaches the region's yield so RegionDCE would drop it, a ReturnStmt
+/// builds the function's final block in the current region, and a listless
+/// assigned GO TO has targets that cannot be enumerated -- so condition 1
+/// cannot be decided at all rather than merely failing.
+///
+/// A CYCLE is not an escape: its target is the EndDoStmt, which is where the
+/// wrap's yield sits, so it lands on the boundary. An EXIT targets the
+/// construct exit, beyond the loop entirely, and does escape.
+static bool isStructurableWithUnstructuredInternals(
+    const Fortran::lower::pft::Evaluation &loop,
+    const Fortran::lower::pft::FunctionLikeUnit &unit) {
+
+  if (!loop.isUnstructured() || !loop.evaluationList ||
+      loop.evaluationList->size() < 3)
+    return false;
+
+  // Only an increment loop keeps all of its control outside the body. A DO
+  // WHILE or an infinite DO lowers through a header block and a back edge, and
+  // a do concurrent has no plain bounds triple either, so in each case the
+  // loop's own control flow runs through the body a wrap would cover.
+  const auto *doConstruct = loop.getIf<parser::DoConstruct>();
+  if (!doConstruct)
+    return false;
+
+  const auto &loopControl = doConstruct->GetLoopControl();
+  if (!loopControl)
+    return false;
+
+  const auto *bounds =
+      std::get_if<parser::LoopControl::Bounds>(&loopControl->u);
+  if (!bounds)
+    return false;
+
+  // A REAL control variable does not lower to fir.do_loop, whose induction
+  // variable must be a signless integer or index, so such a loop is lowered as
+  // raw CFG whatever its body looks like.
+  const semantics::Symbol *ctrlVar = bounds->Name().thing.symbol;
+  if (!ctrlVar)
+    return false;
+
+  const semantics::DeclTypeSpec *ctrlType = ctrlVar->GetType();
+  if (!ctrlType || ctrlType->category() != semantics::DeclTypeSpec::Numeric ||
+      ctrlType->numericTypeSpec().category() != common::TypeCategory::Integer)
+    return false;
+
+  const Fortran::lower::pft::Evaluation *endDoStmt =
+      &loop.evaluationList->back();
+
+  auto isInfiniteDo = [](const parser::DoConstruct *d) {
+    return d && !d->GetLoopControl().has_value();
+  };
+
+  auto targetEscapes = [&](const Fortran::lower::pft::Evaluation *target) {
+    return target != endDoStmt && !isInLoopBody(target, loop);
+  };
+
+  std::function<bool(const Fortran::lower::pft::Evaluation &)> check =
+      [&](const Fortran::lower::pft::Evaluation &e) -> bool {
+    if (e.isA<parser::ReturnStmt>() ||
+        isInfiniteDo(e.getIf<parser::DoConstruct>()))
+      return false;
+
+    if (const auto *g = e.getIf<parser::AssignedGotoStmt>())
+      if (std::get<std::list<parser::Label>>(g->t).empty())
+        return false;
+
+    // Condition 1: nothing leaves the body, CYCLE excepted.
+    if (e.controlSuccessor && targetEscapes(e.controlSuccessor))
+      return false;
+
+    for (const Fortran::lower::pft::Evaluation *extra :
+         e.extraControlSuccessors)
+      if (targetEscapes(extra))
+        return false;
+
+    // Condition 2: nothing enters the body from outside it. This is the
+    // lookup the incoming-branch map exists for.
+    auto it = unit.incomingBranches.find(&e);
+    if (it != unit.incomingBranches.end())
+      for (const Fortran::lower::pft::Evaluation *src : it->second)
+        if (!isInLoopBody(src, loop))
+          return false;
+
+    if (e.evaluationList)
+      for (const Fortran::lower::pft::Evaluation &nested : *e.evaluationList)
+        if (!check(nested))
+          return false;
+
+    return true;
+  };
+
+  for (const Fortran::lower::pft::Evaluation &e : loopBodyRange(loop))
+    if (!check(e))
+      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 a parent being lowered
+/// as raw CFG, so leaving the parent Unstructured is conservative but correct.
+static void detectStructuredWithUnstructuredInternals(
+    Fortran::lower::pft::FunctionLikeUnit &unit) {
+  std::function<void(Fortran::lower::pft::EvaluationList &)> visit =
+      [&](Fortran::lower::pft::EvaluationList &list) {
+        for (Fortran::lower::pft::Evaluation &e : list) {
+          if (e.evaluationList)
+            visit(*e.evaluationList);
+
+          if (e.isA<parser::DoConstruct>() &&
+              isStructurableWithUnstructuredInternals(e, unit))
+            // The one place the classification weakens: detection has proven
+            // Unstructured unnecessary.
+            e.weakenControlFlow(Fortran::lower::pft::Evaluation::ControlFlow::
+                                    StructuredWithUnstructuredInternals);
+        }
+      };
+  visit(unit.evaluationList);
+}
+
 bool Fortran::lower::pft::isWrappableConstruct(
     const Fortran::lower::pft::Evaluation &eval,
     const Fortran::semantics::SemanticsContext &semaCtx) {
   if (!wrapUnstructuredConstructsInExecuteRegion)
     return false;
 
-  if (!eval.isUnstructured)
+  if (!eval.isUnstructured())
     return false;
 
   if (!(eval.isA<Fortran::parser::DoConstruct>() ||
diff --git a/flang/test/Lower/pre-fir-tree-unstructured-internals.f90 b/flang/test/Lower/pre-fir-tree-unstructured-internals.f90
new file mode 100644
index 0000000000000..30236e51bb40b
--- /dev/null
+++ b/flang/test/Lower/pre-fir-tree-unstructured-internals.f90
@@ -0,0 +1,137 @@
+! RUN: %flang_fc1 -fdebug-dump-pft %s 2>&1 | FileCheck %s
+
+! Detection of loops whose control flow is structured on the outside but has
+! raw branching confined to the body. Such a loop is marked '~' in the dump, as
+! opposed to '!' for a fully unstructured one and no marker for a structured
+! one.
+
+! Two forward GOTOs that both land inside the body. Nothing enters the body
+! from outside and nothing leaves it, so the loop qualifies even though its
+! internals are branch-based.
+subroutine internal_gotos(a, n)
+  real :: a(n)
+  ! CHECK:   <<DoConstruct~>> -> 10
+  ! CHECK:     6 GotoStmt! -> 8: goto 20
+  ! CHECK:     8 ^AssignmentStmt <- 6: 20 a(i) = a(i) + 1.0
+  ! CHECK:   <<End DoConstruct~>>
+  do i = 1, n
+    if (a(i) > 0.0) goto 10
+    a(i) = 1.0
+    goto 20
+10  a(i) = 2.0
+20  a(i) = a(i) + 1.0
+  end do
+end subroutine
+
+! A CYCLE targets the EndDoStmt, which is the boundary between the body and the
+! loop control, so it does not count as escaping the body.
+subroutine cycle_in_if_block(a, n)
+  real :: a(n)
+  ! CHECK:   <<DoConstruct~>> -> 8
+  ! CHECK:     [[CYC:[0-9]+]] CycleStmt! -> [[END:[0-9]+]]: cycle
+  ! CHECK:     [[END]] ^EndDoStmt -> 1 <- [[CYC]]: end do
+  ! CHECK:   <<End DoConstruct~>>
+  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
+
+! The GOTO leaves the loop entirely, so the branch graph is not contained.
+subroutine escaping_goto(a, n)
+  real :: a(n)
+  ! CHECK:   <<DoConstruct!>> -> 7
+  ! CHECK:   <<End DoConstruct!>>
+  do i = 1, n
+    if (a(i) > 0.0) goto 20
+    a(i) = 2.0
+  end do
+20 continue
+end subroutine
+
+! A RETURN escapes the loop and the procedure both.
+subroutine body_has_return(a, n)
+  real :: a(n)
+  ! CHECK:   <<DoConstruct!>> -> 7
+  ! CHECK:   <<End DoConstruct!>>
+  do i = 1, n
+    if (a(i) > 0.0) return
+    a(i) = 3.0
+  end do
+end subroutine
+
+! The branch originates outside the loop and lands inside its body, so the body
+! has an external entry point and cannot be wrapped.
+subroutine incoming_from_outside(a, n)
+  real :: a(n)
+  ! CHECK:   <<DoConstruct!>> -> 8
+  ! CHECK:   <<End DoConstruct!>>
+  if (n < 0) goto 30
+  do i = 1, n
+    a(i) = 4.0
+30  continue
+  end do
+end subroutine
+
+! An inner infinite DO has no structured loop control to preserve.
+subroutine infinite_inner(a, n)
+  real :: a(n)
+  ! CHECK:   <<DoConstruct!>> -> 9
+  ! CHECK:     <<DoConstruct!>> -> 8
+  ! CHECK:     <<End DoConstruct!>>
+  ! CHECK:   <<End DoConstruct!>>
+  do i = 1, n
+    do
+      a(i) = 1.0
+      if (a(i) > 0.0) exit
+    end do
+  end do
+end subroutine
+
+! The shape that motivates this work: a forward GOTO raised inside a nested
+! IF, jumping over a whole inner DO construct and landing on the last statement
+! of the outer loop body. Both endpoints are inside the body, so the outer loop
+! is category (c) even though the branch crosses construct boundaries. Note the
+! two inner DO constructs stay structured -- only the outer loop carries the
+! branching.
+subroutine goto_over_inner_loop(qfx, hfx, a, its, ite, jts, jte, force, flux)
+  real :: qfx(ite,jte), hfx(ite,jte), a(ite,jte)
+  logical :: force
+  integer :: flux
+  ! CHECK:   <<DoConstruct~>> -> 18
+  ! CHECK:     <<DoConstruct>> -> 6
+  ! CHECK:     <<End DoConstruct>>
+  ! CHECK:       [[GOTO:[0-9]+]] ^GotoStmt! -> [[TGT:[0-9]+]]: goto 350
+  ! CHECK:     <<DoConstruct>> -> [[TGT]]
+  ! CHECK:     <<End DoConstruct>>
+  ! CHECK:     [[TGT]] ^ContinueStmt <- [[GOTO]]: 350 continue
+  ! CHECK:     17 EndDoStmt -> 1: enddo
+  ! CHECK:   <<End DoConstruct~>>
+  do j = jts, jte
+    do 330 i = its, ite
+      a(i,j) = a(i,j) + 1.0
+330 continue
+335 continue
+    if (force) then
+      if (flux .eq. 1) goto 350
+    endif
+    do i = its, ite
+      qfx(i,j) = 0.
+      hfx(i,j) = 0.
+    enddo
+350 continue
+  enddo
+end subroutine
+
+! No branching at all; detection must leave it unmarked.
+subroutine fully_structured(a, n)
+  real :: a(n)
+  ! CHECK:   <<DoConstruct>> -> 4
+  ! CHECK:   <<End DoConstruct>>
+  do i = 1, n
+    a(i) = 5.0
+  end do
+end subroutine
diff --git a/flang/test/Lower/trailing-cycle.f90 b/flang/test/Lower/trailing-cycle.f90
index 47f904cb46c99..4a69a3b4fc0ba 100644
--- a/flang/test/Lower/trailing-cycle.f90
+++ b/flang/test/Lower/trailing-cycle.f90
@@ -58,8 +58,10 @@ subroutine trailing_cycle(a, n)
     end do
   end do outer
 
-  ! A labeled CYCLE may be a branch target and is kept.
-  ! CHECK:   <<DoConstruct!>> -> 25
+  ! A labeled CYCLE may be a branch target and is kept. Its branches stay
+  ! inside the loop body, so the loop is category (c): structured control with
+  ! unstructured internals, marked '~' rather than '!'.
+  ! CHECK:   <<DoConstruct~>> -> 25
   ! CHECK:     18 NonLabelDoStmt -> 24: do i = 1, n
   ! CHECK:     <<IfConstruct>> -> 23
   ! CHECK:       19 ^IfStmt [negate] -> 23: if(a(i) > 0.0) goto 10
@@ -68,20 +70,21 @@ subroutine trailing_cycle(a, n)
   ! CHECK:     <<End IfConstruct>>
   ! CHECK:     23 CycleStmt! -> 24: 10 cycle
   ! CHECK:     24 ^EndDoStmt -> 18 <- 23: end do
-  ! CHECK:   <<End DoConstruct!>>
+  ! CHECK:   <<End DoConstruct~>>
   do i = 1, n
     if (a(i) > 0.0) goto 10
     a(i) = 4.0
 10  cycle
   end do
 
-  ! A CYCLE that is not last is a real branch and is kept.
-  ! CHECK:   <<DoConstruct!>> -> 29
+  ! A CYCLE that is not last is a real branch and is kept. It targets the
+  ! EndDoStmt, which is the loop-body boundary, so this is category (c) too.
+  ! CHECK:   <<DoConstruct~>> -> 29
   ! CHECK:     25 ^NonLabelDoStmt -> 28: do i = 1, n
   ! CHECK:     26 ^CycleStmt! -> 28: cycle
   ! CHECK:     27 ^AssignmentStmt: a(i) = 5.0
   ! CHECK:     28 ^EndDoStmt -> 25 <- 26: end do
-  ! CHECK:   <<End DoConstruct!>>
+  ! CHECK:   <<End DoConstruct~>>
   do i = 1, n
     cycle
     a(i) = 5.0

>From e92cba6d48b68889085ca48c51e236bd882c3afa Mon Sep 17 00:00:00 2001
From: ergawy <kareem.ergawy at gmail.com>
Date: Thu, 24 Sep 2026 02:27:27 -0700
Subject: [PATCH 2/2] [flang] Resolve an assigned GO TO's targets from the
 completed assign map

An assigned GO TO reaches any label ASSIGNed to its variable, and a label
list does not bound that: lowering allows a branch to any ASSIGNed label
whether or not the list names it. Branch analysis only sees the ASSIGNs
preceding the GO TO in program order, so the successors it records, and the
incoming branches derived from them, can be incomplete.

The symbol-to-labels map is complete once branch analysis has finished,
which is when the classification runs. Ask it for the full target set
instead of trusting the recorded successors, so a loop whose assigned GO TO
stays within its body is still recognised.
---
 flang/lib/Lower/PFTBuilder.cpp                | 24 +++++++++-
 .../test/Lower/pre-fir-tree-assigned-goto.f90 | 45 +++++++++++++++++++
 2 files changed, 67 insertions(+), 2 deletions(-)

diff --git a/flang/lib/Lower/PFTBuilder.cpp b/flang/lib/Lower/PFTBuilder.cpp
index 948589a11b7d7..8b1a8e9765dc7 100644
--- a/flang/lib/Lower/PFTBuilder.cpp
+++ b/flang/lib/Lower/PFTBuilder.cpp
@@ -2854,9 +2854,29 @@ static bool isStructurableWithUnstructuredInternals(
         isInfiniteDo(e.getIf<parser::DoConstruct>()))
       return false;
 
-    if (const auto *g = e.getIf<parser::AssignedGotoStmt>())
-      if (std::get<std::list<parser::Label>>(g->t).empty())
+    // An assigned GO TO reaches any label ASSIGNed to its variable, and a label
+    // list does not bound that: lowering deliberately allows a branch to any
+    // ASSIGNed label whether or not the list names it. The successors
+    // analyzeBranches recorded are therefore incomplete, since it only sees the
+    // ASSIGNs that precede the GO TO in program order.
+    //
+    // The symbol-to-labels map is complete once branch analysis has finished,
+    // which is when this runs, so ask it for the full target set instead of
+    // trusting the recorded successors.
+    if (const auto *g = e.getIf<parser::AssignedGotoStmt>()) {
+      const semantics::Symbol *sym = std::get<parser::Name>(g->t).symbol;
+      if (!sym)
         return false;
+      auto assigned = unit.assignSymbolLabelMap.find(*sym);
+      if (assigned == unit.assignSymbolLabelMap.end())
+        return false;
+      for (parser::Label label : assigned->second) {
+        auto target = unit.labelEvaluationMap.find(label);
+        if (target == unit.labelEvaluationMap.end() ||
+            targetEscapes(target->second))
+          return false;
+      }
+    }
 
     // Condition 1: nothing leaves the body, CYCLE excepted.
     if (e.controlSuccessor && targetEscapes(e.controlSuccessor))
diff --git a/flang/test/Lower/pre-fir-tree-assigned-goto.f90 b/flang/test/Lower/pre-fir-tree-assigned-goto.f90
index 757a838d592b8..bb922f67679ec 100644
--- a/flang/test/Lower/pre-fir-tree-assigned-goto.f90
+++ b/flang/test/Lower/pre-fir-tree-assigned-goto.f90
@@ -45,3 +45,48 @@ subroutine assigned_goto_repeated_label(j)
 10 print *, "ten"
 20 print *, "twenty"
 end subroutine
+
+! The classification of a loop asks for the full target set rather than the
+! recorded successors: branch analysis only sees the ASSIGNs that precede the
+! GO TO in program order, while the symbol-to-labels map is complete once it
+! has finished.
+
+! Both labels ASSIGN'd to m lie in the loop body, so the branching is
+! self-contained and the loop keeps its structured form. The ASSIGN of label 10
+! follows the GO TO, so the recorded successors name only label 20 -- index 4
+! carries no "<-" edge.
+! CHECK-LABEL: Subroutine targets_inside
+! CHECK: <<DoConstruct~>>
+! CHECK: [[GOTO:[0-9]+]] ^AssignedGotoStmt! -> [[L20:[0-9]+]]: go to m
+! CHECK: 10 a(i) = 1.0
+! CHECK: [[L20]] ^AssignmentStmt <- [[GOTO]]: 20 a(i) = a(i) + 1.0
+! CHECK: <<End DoConstruct~>>
+subroutine targets_inside(a, n)
+  real :: a(n)
+  integer :: m
+  assign 20 to m
+  do i = 1, n
+    go to m
+10  a(i) = 1.0
+20  a(i) = a(i) + 1.0
+    assign 10 to m
+  end do
+end subroutine
+
+! Label 30 is ASSIGN'd to m as well and lies outside the loop, so a branch can
+! leave the body and the loop stays unstructured.
+! CHECK-LABEL: Subroutine target_outside
+! CHECK: <<DoConstruct!>>
+! CHECK: <<End DoConstruct!>>
+subroutine target_outside(a, n)
+  real :: a(n)
+  integer :: m
+  assign 20 to m
+  do i = 1, n
+    go to m
+10  a(i) = 1.0
+    assign 30 to m
+  end do
+20 continue
+30 continue
+end subroutine



More information about the llvm-branch-commits mailing list