[Mlir-commits] [mlir] [flang][acc] Prune the acc.loop zero-trip edge when the body is proven to run (PR #219287)

Susan Tan ス-ザン タン llvmlistbot at llvm.org
Fri Aug 28 08:44:48 PDT 2026


https://github.com/SusanTan updated https://github.com/llvm/llvm-project/pull/219287

>From d92ae99428a1947b959c01ffe78ba425ae8d305f Mon Sep 17 00:00:00 2001
From: Susan Tan <zujunt at nvidia.com>
Date: Thu, 27 Aug 2026 12:14:36 -0700
Subject: [PATCH 1/6] impl

---
 .../mlir/Dialect/OpenACC/OpenACCOps.td        |   3 +-
 mlir/lib/Dialect/OpenACC/IR/CMakeLists.txt    |   1 +
 mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp       | 102 +++++++++++++++++-
 .../OpenACC/region-branchop-interface.mlir    |  53 ++++++++-
 4 files changed, 153 insertions(+), 6 deletions(-)

diff --git a/mlir/include/mlir/Dialect/OpenACC/OpenACCOps.td b/mlir/include/mlir/Dialect/OpenACC/OpenACCOps.td
index 375d2517da6c6..2baa501738eda 100644
--- a/mlir/include/mlir/Dialect/OpenACC/OpenACCOps.td
+++ b/mlir/include/mlir/Dialect/OpenACC/OpenACCOps.td
@@ -2635,7 +2635,8 @@ def OpenACC_LoopOp
           "loop", [AttrSizedOperandSegments, AutomaticAllocationScope,
                    RecursiveMemoryEffects,
                    DeclareOpInterfaceMethods<ComputeRegionOpInterface>,
-                   DeclareOpInterfaceMethods<LoopLikeOpInterface>,
+                   DeclareOpInterfaceMethods<LoopLikeOpInterface,
+                                             ["getStaticTripCount"]>,
                    DeclareOpInterfaceMethods<RegionBranchOpInterface,
                                              ["getSuccessorInputs"]>,
                    MemoryEffects<[MemWrite<OpenACC_ConstructResource>]>]> {
diff --git a/mlir/lib/Dialect/OpenACC/IR/CMakeLists.txt b/mlir/lib/Dialect/OpenACC/IR/CMakeLists.txt
index b04a30b442de0..409155ec18fe6 100644
--- a/mlir/lib/Dialect/OpenACC/IR/CMakeLists.txt
+++ b/mlir/lib/Dialect/OpenACC/IR/CMakeLists.txt
@@ -15,6 +15,7 @@ add_mlir_dialect_library(MLIROpenACCDialect
 
   LINK_LIBS PUBLIC
   MLIRIR
+  MLIRDialectUtils
   MLIRGPUDialect
   MLIRLLVMDialect
   MLIRMemRefDialect
diff --git a/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp b/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp
index 360eac356cceb..01ca536e624d0 100644
--- a/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp
+++ b/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp
@@ -12,6 +12,7 @@
 #include "mlir/Dialect/LLVMIR/LLVMDialect.h"
 #include "mlir/Dialect/LLVMIR/LLVMTypes.h"
 #include "mlir/Dialect/MemRef/IR/MemRef.h"
+#include "mlir/Dialect/Utils/StaticValueUtils.h"
 #include "mlir/IR/Builders.h"
 #include "mlir/IR/BuiltinAttributes.h"
 #include "mlir/IR/BuiltinOps.h"
@@ -23,6 +24,7 @@
 #include "mlir/IR/SymbolTable.h"
 #include "mlir/Support/LLVM.h"
 #include "mlir/Transforms/DialectConversion.h"
+#include "llvm/ADT/APSInt.h"
 #include "llvm/ADT/SmallSet.h"
 #include "llvm/ADT/TypeSwitch.h"
 #include "llvm/Support/LogicalResult.h"
@@ -607,7 +609,26 @@ void LoopOp::getSuccessorRegions(RegionBranchPoint point,
     return;
   }
 
-  // Structured loops: model a loop-shaped region graph similar to scf.for.
+  // Structured loops: model a loop-shaped region graph similar to scf.for,
+  // including the trip-count refinements that drop edges the loop can never
+  // take.
+  if (std::optional<APInt> tripCount = getStaticTripCount()) {
+    if (point.isParent()) {
+      // A known-empty loop branches straight back to the parent. Otherwise
+      // the body is guaranteed to run, so nothing can branch past it.
+      if (tripCount->isZero())
+        regions.push_back(RegionSuccessor(getOperation()));
+      else
+        regions.push_back(RegionSuccessor(&getRegion()));
+      return;
+    }
+    if (tripCount->isOne()) {
+      // A single iteration has no backedge.
+      regions.push_back(RegionSuccessor(getOperation()));
+      return;
+    }
+  }
+
   regions.push_back(RegionSuccessor(&getRegion()));
   regions.push_back(RegionSuccessor(getOperation()));
 }
@@ -3904,6 +3925,85 @@ llvm::SmallVector<mlir::Region *> acc::LoopOp::getLoopRegions() {
   return {&getRegion()};
 }
 
+/// Trip count of a single `acc.loop` control dimension.
+///
+/// `constantTripCount` models the exclusive `iv < ub` form used by `scf.for`.
+/// An `acc.loop` dimension is exclusive as well, unless `inclusiveUpperbound`
+/// is set for it, in which case the equivalent exclusive bound is `ub + 1`.
+static std::optional<APInt> getStaticDimTripCount(Value lb, Value ub,
+                                                  Value step, bool inclusive) {
+  // `acc.loop` bounds are plain values, so there is no static `ub - lb` to
+  // recover once the bounds themselves are not constants.
+  auto noUbMinusLb = [](Value, Value, bool) -> std::optional<llvm::APSInt> {
+    return std::nullopt;
+  };
+
+  if (!inclusive)
+    return constantTripCount(lb, ub, step, /*isSigned=*/true, noUbMinusLb);
+
+  // `iv <= ub` with matching bounds is a single iteration. This has to be
+  // answered before the conversion below, which would otherwise report the
+  // empty range that `constantTripCount` derives from matching bounds.
+  if (lb == ub) {
+    std::optional<std::pair<APInt, bool>> stepCst = getConstantAPIntValue(step);
+    if (!stepCst || stepCst->first.isZero())
+      return std::nullopt;
+    return APInt(stepCst->first.getBitWidth(), 1);
+  }
+
+  // `ub + 1` is only representable for a known upper bound that does not
+  // overflow.
+  std::optional<std::pair<APInt, bool>> ubCst = getConstantAPIntValue(ub);
+  if (!ubCst || ubCst->first.isMaxSignedValue())
+    return std::nullopt;
+  OpFoldResult exclusiveUb = IntegerAttr::get(ub.getType(), ubCst->first + 1);
+  return constantTripCount(lb, exclusiveUb, step, /*isSigned=*/true,
+                           noUbMinusLb);
+}
+
+std::optional<APInt> LoopOp::getStaticTripCount() {
+  // An unstructured or container-like loop has no counted control of its own:
+  // the iteration space is described inside the region.
+  if (getUnstructured() || isContainerLike())
+    return std::nullopt;
+
+  ValueRange lbs = getLowerbound();
+  ValueRange ubs = getUpperbound();
+  ValueRange steps = getStep();
+  if (lbs.size() != ubs.size() || lbs.size() != steps.size())
+    return std::nullopt;
+
+  std::optional<ArrayRef<bool>> inclusive = getInclusiveUpperbound();
+
+  // Collapsed dimensions are iterated as a product, so a single empty
+  // dimension makes the whole loop empty even when the others are unknown.
+  APInt tripCount(64, 1);
+  bool anyUnknown = false;
+  for (unsigned i = 0, e = lbs.size(); i < e; ++i) {
+    bool dimInclusive = inclusive && i < inclusive->size() && (*inclusive)[i];
+    std::optional<APInt> dim =
+        getStaticDimTripCount(lbs[i], ubs[i], steps[i], dimInclusive);
+    if (!dim) {
+      anyUnknown = true;
+      continue;
+    }
+    if (dim->isZero())
+      return APInt(64, 0);
+    if (dim->getActiveBits() > 64) {
+      anyUnknown = true;
+      continue;
+    }
+    bool overflow = false;
+    tripCount = tripCount.umul_ov(dim->zextOrTrunc(64), overflow);
+    if (overflow)
+      anyUnknown = true;
+  }
+
+  if (anyUnknown)
+    return std::nullopt;
+  return tripCount;
+}
+
 /// loop-control ::= `control` `(` ssa-id-and-type-list `)` `=`
 /// `(` ssa-id-and-type-list `)` `to` `(` ssa-id-and-type-list `)` `step`
 /// `(` ssa-id-and-type-list `)`
diff --git a/mlir/test/Dialect/OpenACC/region-branchop-interface.mlir b/mlir/test/Dialect/OpenACC/region-branchop-interface.mlir
index 708b48cbdfe5c..a003f7984fbee 100644
--- a/mlir/test/Dialect/OpenACC/region-branchop-interface.mlir
+++ b/mlir/test/Dialect/OpenACC/region-branchop-interface.mlir
@@ -119,10 +119,9 @@ func.func @last_mod_openacc_host_data(%arg0: memref<f32>, %mapped: memref<f32>)
 // CHECK-NEXT:   - loop_region
 // CHECK-LABEL: test_tag: acc_loop_after:
 // CHECK:  operand #0
-// CHECK-DAG:   - pre
-// CHECK-DAG:   - loop_region
-// the last writer is either the pre-loop store or
-// the store in the loop depending on the iteration count
+// CHECK-NEXT:   - loop_region
+// these bounds run the body at least once, so the store in the loop is the
+// only possible last writer
 // CHECK-LABEL: test_tag: acc_loop_post:
 // CHECK:  operand #0
 // CHECK-NEXT:   - post_loop
@@ -150,6 +149,52 @@ func.func @last_mod_openacc_loop(%arg0: memref<f32>) -> memref<f32> {
 
 // -----
 
+// structured acc.loop with an unknown upper bound: the body may or may not
+// run, so the edge that branches past it is kept.
+//
+// CHECK-LABEL: test_tag: acc_loop_dynamic_after:
+// CHECK:  operand #0
+// CHECK-DAG:   - pre
+// CHECK-DAG:   - loop_region
+func.func @last_mod_openacc_loop_dynamic(%arg0: memref<f32>, %n: i32) -> memref<f32> {
+  %zero = arith.constant 0.0 : f32
+  %one = arith.constant 1.0 : f32
+  memref.store %zero, %arg0[] {tag_name = "pre"} : memref<f32>
+  %c1_i32 = arith.constant 1 : i32
+  acc.loop control(%iv : i32) = (%c1_i32 : i32) to (%n : i32)
+      step (%c1_i32 : i32) {
+    memref.store %one, %arg0[] {tag_name = "loop_region"} : memref<f32>
+    acc.yield
+  } auto_
+  memref.load %arg0[] {tag = "acc_loop_dynamic_after"} : memref<f32>
+  return %arg0 : memref<f32>
+}
+
+// -----
+
+// structured acc.loop over a provably empty iteration space: the body is
+// never entered, so the store before the loop is the last writer.
+//
+// CHECK-LABEL: test_tag: acc_loop_empty_after:
+// CHECK:  operand #0
+// CHECK-NEXT:   - pre
+func.func @last_mod_openacc_loop_empty(%arg0: memref<f32>) -> memref<f32> {
+  %zero = arith.constant 0.0 : f32
+  %one = arith.constant 1.0 : f32
+  memref.store %zero, %arg0[] {tag_name = "pre"} : memref<f32>
+  %c1_i32 = arith.constant 1 : i32
+  %c10_i32 = arith.constant 10 : i32
+  acc.loop control(%iv : i32) = (%c10_i32 : i32) to (%c10_i32 : i32)
+      step (%c1_i32 : i32) {
+    memref.store %one, %arg0[] {tag_name = "loop_region"} : memref<f32>
+    acc.yield
+  } auto_
+  memref.load %arg0[] {tag = "acc_loop_empty_after"} : memref<f32>
+  return %arg0 : memref<f32>
+}
+
+// -----
+
 // Unstructured acc.loop: the RegionBranch is modeled with explicit CFG and early
 // exits, and the RegionBranch graph only exposes a single entry and single
 // exit edge (no region backedge).

>From 32d5123bbd3edc8f10cfa1fb65766a19aa67b295 Mon Sep 17 00:00:00 2001
From: Susan Tan <zujunt at nvidia.com>
Date: Thu, 27 Aug 2026 13:29:12 -0700
Subject: [PATCH 2/6] tweak

---
 .../mlir/Dialect/OpenACC/OpenACCOps.td        |   3 +-
 mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp       | 154 +++++++-----------
 .../OpenACC/region-branchop-interface.mlir    |  58 ++++++-
 3 files changed, 118 insertions(+), 97 deletions(-)

diff --git a/mlir/include/mlir/Dialect/OpenACC/OpenACCOps.td b/mlir/include/mlir/Dialect/OpenACC/OpenACCOps.td
index 2baa501738eda..375d2517da6c6 100644
--- a/mlir/include/mlir/Dialect/OpenACC/OpenACCOps.td
+++ b/mlir/include/mlir/Dialect/OpenACC/OpenACCOps.td
@@ -2635,8 +2635,7 @@ def OpenACC_LoopOp
           "loop", [AttrSizedOperandSegments, AutomaticAllocationScope,
                    RecursiveMemoryEffects,
                    DeclareOpInterfaceMethods<ComputeRegionOpInterface>,
-                   DeclareOpInterfaceMethods<LoopLikeOpInterface,
-                                             ["getStaticTripCount"]>,
+                   DeclareOpInterfaceMethods<LoopLikeOpInterface>,
                    DeclareOpInterfaceMethods<RegionBranchOpInterface,
                                              ["getSuccessorInputs"]>,
                    MemoryEffects<[MemWrite<OpenACC_ConstructResource>]>]> {
diff --git a/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp b/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp
index 01ca536e624d0..f03bddca19a73 100644
--- a/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp
+++ b/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp
@@ -24,7 +24,6 @@
 #include "mlir/IR/SymbolTable.h"
 #include "mlir/Support/LLVM.h"
 #include "mlir/Transforms/DialectConversion.h"
-#include "llvm/ADT/APSInt.h"
 #include "llvm/ADT/SmallSet.h"
 #include "llvm/ADT/TypeSwitch.h"
 #include "llvm/Support/LogicalResult.h"
@@ -595,6 +594,59 @@ ValueRange HostDataOp::getSuccessorInputs(RegionSuccessor successor) {
   return getSingleRegionSuccessorInputs(getOperation(), successor);
 }
 
+/// Whether the body of a structured `acc.loop` is proven to run.
+enum class BodyExecution {
+  /// The body runs at least once, so control cannot branch past it.
+  Always,
+  /// The body never runs, so control cannot enter it.
+  Never,
+  /// Neither could be proven, so both edges are possible.
+  Maybe
+};
+
+/// Prove whether the body of `loopOp` runs. A counted dimension whose entry
+/// test already fails at its lower bound runs exactly zero times, so a single
+/// comparison decides both `Always` and `Never`. Bounds that are not constant
+/// prove nothing.
+static BodyExecution getBodyExecution(LoopOp loopOp) {
+  // A container-like loop describes its iteration space inside the region.
+  if (loopOp.isContainerLike())
+    return BodyExecution::Maybe;
+
+  ValueRange lbs = loopOp.getLowerbound();
+  ValueRange ubs = loopOp.getUpperbound();
+  ValueRange steps = loopOp.getStep();
+  if (lbs.size() != ubs.size() || lbs.size() != steps.size())
+    return BodyExecution::Maybe;
+
+  std::optional<ArrayRef<bool>> inclusive = loopOp.getInclusiveUpperbound();
+
+  BodyExecution result = BodyExecution::Always;
+  for (unsigned i = 0, e = lbs.size(); i < e; ++i) {
+    std::optional<int64_t> lb = getConstantIntValue(lbs[i]);
+    std::optional<int64_t> ub = getConstantIntValue(ubs[i]);
+    std::optional<int64_t> step = getConstantIntValue(steps[i]);
+    // A zero step either never advances or never runs; the two are
+    // indistinguishable here.
+    if (!lb || !ub || !step || *step == 0) {
+      result = BodyExecution::Maybe;
+      continue;
+    }
+
+    // A descending dimension compares against its bound the other way round.
+    bool closed = inclusive && i < inclusive->size() && (*inclusive)[i];
+    bool runsOnce = *step > 0 ? (closed ? *lb <= *ub : *lb < *ub)
+                              : (closed ? *lb >= *ub : *lb > *ub);
+    // The dimensions are iterated as a nest, so one empty dimension empties
+    // the whole nest whatever the others do, while the body runs only if every
+    // dimension runs.
+    if (!runsOnce)
+      return BodyExecution::Never;
+  }
+
+  return result;
+}
+
 void LoopOp::getSuccessorRegions(RegionBranchPoint point,
                                  SmallVectorImpl<RegionSuccessor> &regions) {
   // Unstructured loops: the body may contain arbitrary CFG and early exits.
@@ -610,22 +662,17 @@ void LoopOp::getSuccessorRegions(RegionBranchPoint point,
   }
 
   // Structured loops: model a loop-shaped region graph similar to scf.for,
-  // including the trip-count refinements that drop edges the loop can never
-  // take.
-  if (std::optional<APInt> tripCount = getStaticTripCount()) {
-    if (point.isParent()) {
-      // A known-empty loop branches straight back to the parent. Otherwise
-      // the body is guaranteed to run, so nothing can branch past it.
-      if (tripCount->isZero())
-        regions.push_back(RegionSuccessor(getOperation()));
-      else
-        regions.push_back(RegionSuccessor(&getRegion()));
+  // minus the entry edge the loop is proven not to take.
+  if (point.isParent()) {
+    switch (getBodyExecution(*this)) {
+    case BodyExecution::Always:
+      regions.push_back(RegionSuccessor(&getRegion()));
       return;
-    }
-    if (tripCount->isOne()) {
-      // A single iteration has no backedge.
+    case BodyExecution::Never:
       regions.push_back(RegionSuccessor(getOperation()));
       return;
+    case BodyExecution::Maybe:
+      break;
     }
   }
 
@@ -3925,85 +3972,6 @@ llvm::SmallVector<mlir::Region *> acc::LoopOp::getLoopRegions() {
   return {&getRegion()};
 }
 
-/// Trip count of a single `acc.loop` control dimension.
-///
-/// `constantTripCount` models the exclusive `iv < ub` form used by `scf.for`.
-/// An `acc.loop` dimension is exclusive as well, unless `inclusiveUpperbound`
-/// is set for it, in which case the equivalent exclusive bound is `ub + 1`.
-static std::optional<APInt> getStaticDimTripCount(Value lb, Value ub,
-                                                  Value step, bool inclusive) {
-  // `acc.loop` bounds are plain values, so there is no static `ub - lb` to
-  // recover once the bounds themselves are not constants.
-  auto noUbMinusLb = [](Value, Value, bool) -> std::optional<llvm::APSInt> {
-    return std::nullopt;
-  };
-
-  if (!inclusive)
-    return constantTripCount(lb, ub, step, /*isSigned=*/true, noUbMinusLb);
-
-  // `iv <= ub` with matching bounds is a single iteration. This has to be
-  // answered before the conversion below, which would otherwise report the
-  // empty range that `constantTripCount` derives from matching bounds.
-  if (lb == ub) {
-    std::optional<std::pair<APInt, bool>> stepCst = getConstantAPIntValue(step);
-    if (!stepCst || stepCst->first.isZero())
-      return std::nullopt;
-    return APInt(stepCst->first.getBitWidth(), 1);
-  }
-
-  // `ub + 1` is only representable for a known upper bound that does not
-  // overflow.
-  std::optional<std::pair<APInt, bool>> ubCst = getConstantAPIntValue(ub);
-  if (!ubCst || ubCst->first.isMaxSignedValue())
-    return std::nullopt;
-  OpFoldResult exclusiveUb = IntegerAttr::get(ub.getType(), ubCst->first + 1);
-  return constantTripCount(lb, exclusiveUb, step, /*isSigned=*/true,
-                           noUbMinusLb);
-}
-
-std::optional<APInt> LoopOp::getStaticTripCount() {
-  // An unstructured or container-like loop has no counted control of its own:
-  // the iteration space is described inside the region.
-  if (getUnstructured() || isContainerLike())
-    return std::nullopt;
-
-  ValueRange lbs = getLowerbound();
-  ValueRange ubs = getUpperbound();
-  ValueRange steps = getStep();
-  if (lbs.size() != ubs.size() || lbs.size() != steps.size())
-    return std::nullopt;
-
-  std::optional<ArrayRef<bool>> inclusive = getInclusiveUpperbound();
-
-  // Collapsed dimensions are iterated as a product, so a single empty
-  // dimension makes the whole loop empty even when the others are unknown.
-  APInt tripCount(64, 1);
-  bool anyUnknown = false;
-  for (unsigned i = 0, e = lbs.size(); i < e; ++i) {
-    bool dimInclusive = inclusive && i < inclusive->size() && (*inclusive)[i];
-    std::optional<APInt> dim =
-        getStaticDimTripCount(lbs[i], ubs[i], steps[i], dimInclusive);
-    if (!dim) {
-      anyUnknown = true;
-      continue;
-    }
-    if (dim->isZero())
-      return APInt(64, 0);
-    if (dim->getActiveBits() > 64) {
-      anyUnknown = true;
-      continue;
-    }
-    bool overflow = false;
-    tripCount = tripCount.umul_ov(dim->zextOrTrunc(64), overflow);
-    if (overflow)
-      anyUnknown = true;
-  }
-
-  if (anyUnknown)
-    return std::nullopt;
-  return tripCount;
-}
-
 /// loop-control ::= `control` `(` ssa-id-and-type-list `)` `=`
 /// `(` ssa-id-and-type-list `)` `to` `(` ssa-id-and-type-list `)` `step`
 /// `(` ssa-id-and-type-list `)`
diff --git a/mlir/test/Dialect/OpenACC/region-branchop-interface.mlir b/mlir/test/Dialect/OpenACC/region-branchop-interface.mlir
index a003f7984fbee..da10a76f3af46 100644
--- a/mlir/test/Dialect/OpenACC/region-branchop-interface.mlir
+++ b/mlir/test/Dialect/OpenACC/region-branchop-interface.mlir
@@ -172,8 +172,9 @@ func.func @last_mod_openacc_loop_dynamic(%arg0: memref<f32>, %n: i32) -> memref<
 
 // -----
 
-// structured acc.loop over a provably empty iteration space: the body is
-// never entered, so the store before the loop is the last writer.
+// structured acc.loop with matching bounds and an exclusive upper bound: the
+// entry test already fails at the lower bound, so the body never runs and the
+// store before the loop is the last writer.
 //
 // CHECK-LABEL: test_tag: acc_loop_empty_after:
 // CHECK:  operand #0
@@ -195,6 +196,59 @@ func.func @last_mod_openacc_loop_empty(%arg0: memref<f32>) -> memref<f32> {
 
 // -----
 
+// structured acc.loop counting down with an inclusive upper bound: `lb` is
+// above `ub`, which an ascending-only comparison would misread as an empty
+// iteration space. This loop runs 10 times, so the body is guaranteed to run
+// and the store inside it is the only possible last writer.
+//
+// CHECK-LABEL: test_tag: acc_loop_descending_after:
+// CHECK:  operand #0
+// CHECK-NEXT:   - loop_region
+func.func @last_mod_openacc_loop_descending(%arg0: memref<f32>) -> memref<f32> {
+  %zero = arith.constant 0.0 : f32
+  %one = arith.constant 1.0 : f32
+  memref.store %zero, %arg0[] {tag_name = "pre"} : memref<f32>
+  %c1_i32 = arith.constant 1 : i32
+  %cm1_i32 = arith.constant -1 : i32
+  %c10_i32 = arith.constant 10 : i32
+  acc.loop control(%iv : i32) = (%c10_i32 : i32) to (%c1_i32 : i32)
+      step (%cm1_i32 : i32) {
+    memref.store %one, %arg0[] {tag_name = "loop_region"} : memref<f32>
+    acc.yield
+  } auto_ inclusiveUpperbound(array<i1: true>)
+  memref.load %arg0[] {tag = "acc_loop_descending_after"} : memref<f32>
+  return %arg0 : memref<f32>
+}
+
+// -----
+
+// structured acc.loop stepping down away from an inclusive upper bound above
+// it: the body never runs, so the store before the loop is the last writer.
+// Comparing the bounds as if the step were ascending would instead prove the
+// body always runs, which is the opposite conclusion.
+//
+// CHECK-LABEL: test_tag: acc_loop_descending_empty_after:
+// CHECK:  operand #0
+// CHECK-NEXT:   - pre
+func.func @last_mod_openacc_loop_descending_empty(%arg0: memref<f32>)
+    -> memref<f32> {
+  %zero = arith.constant 0.0 : f32
+  %one = arith.constant 1.0 : f32
+  memref.store %zero, %arg0[] {tag_name = "pre"} : memref<f32>
+  %c1_i32 = arith.constant 1 : i32
+  %cm1_i32 = arith.constant -1 : i32
+  %c10_i32 = arith.constant 10 : i32
+  acc.loop control(%iv : i32) = (%c1_i32 : i32) to (%c10_i32 : i32)
+      step (%cm1_i32 : i32) {
+    memref.store %one, %arg0[] {tag_name = "loop_region"} : memref<f32>
+    acc.yield
+  } auto_ inclusiveUpperbound(array<i1: true>)
+  memref.load %arg0[] {tag = "acc_loop_descending_empty_after"} : memref<f32>
+  return %arg0 : memref<f32>
+}
+
+// -----
+
 // Unstructured acc.loop: the RegionBranch is modeled with explicit CFG and early
 // exits, and the RegionBranch graph only exposes a single entry and single
 // exit edge (no region backedge).

>From 0f011e6cdb0fedbb585b07900048b13db283dc68 Mon Sep 17 00:00:00 2001
From: Susan Tan <zujunt at nvidia.com>
Date: Thu, 27 Aug 2026 14:05:05 -0700
Subject: [PATCH 3/6] tweak

---
 mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp | 36 +++++++++++++++----------
 1 file changed, 22 insertions(+), 14 deletions(-)

diff --git a/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp b/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp
index f03bddca19a73..d1d2ea8d3da66 100644
--- a/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp
+++ b/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp
@@ -594,13 +594,15 @@ ValueRange HostDataOp::getSuccessorInputs(RegionSuccessor successor) {
   return getSingleRegionSuccessorInputs(getOperation(), successor);
 }
 
-/// Whether the body of a structured `acc.loop` is proven to run.
+/// Whether the body of a structured `acc.loop` is proven to run. This decides
+/// which edges out of the parent are feasible; the edges out of the region are
+/// unaffected.
 enum class BodyExecution {
-  /// The body runs at least once, so control cannot branch past it.
+  /// The body runs at least once, so the parent cannot bypass the region.
   Always,
-  /// The body never runs, so control cannot enter it.
+  /// The body never runs, so the parent cannot enter the region.
   Never,
-  /// Neither could be proven, so both edges are possible.
+  /// Neither could be proven, so the parent may do either.
   Maybe
 };
 
@@ -613,30 +615,34 @@ static BodyExecution getBodyExecution(LoopOp loopOp) {
   if (loopOp.isContainerLike())
     return BodyExecution::Maybe;
 
+  // The verifier guarantees one lower bound, upper bound and step per
+  // dimension.
   ValueRange lbs = loopOp.getLowerbound();
   ValueRange ubs = loopOp.getUpperbound();
   ValueRange steps = loopOp.getStep();
-  if (lbs.size() != ubs.size() || lbs.size() != steps.size())
-    return BodyExecution::Maybe;
-
-  std::optional<ArrayRef<bool>> inclusive = loopOp.getInclusiveUpperbound();
 
   BodyExecution result = BodyExecution::Always;
   for (unsigned i = 0, e = lbs.size(); i < e; ++i) {
     std::optional<int64_t> lb = getConstantIntValue(lbs[i]);
     std::optional<int64_t> ub = getConstantIntValue(ubs[i]);
     std::optional<int64_t> step = getConstantIntValue(steps[i]);
-    // A zero step either never advances or never runs; the two are
-    // indistinguishable here.
+    // An unknown bound cannot be tested, and a zero step either spins forever
+    // or never starts. Neither proves `Always`, but a later dimension may
+    // still prove the nest empty: `(0 to %n)` collapsed with `(0 to 0)`.
     if (!lb || !ub || !step || *step == 0) {
       result = BodyExecution::Maybe;
       continue;
     }
 
-    // A descending dimension compares against its bound the other way round.
-    bool closed = inclusive && i < inclusive->size() && (*inclusive)[i];
-    bool runsOnce = *step > 0 ? (closed ? *lb <= *ub : *lb < *ub)
-                              : (closed ? *lb >= *ub : *lb > *ub);
+    // The entry test at the lower bound. A descending dimension compares
+    // against its bound the other way round. The attribute is absent when
+    // every dimension is exclusive as in `scf.for`, and the verifier otherwise
+    // guarantees one entry per dimension.
+    std::optional<ArrayRef<bool>> inclusiveUbs =
+        loopOp.getInclusiveUpperbound();
+    bool inclusiveUb = inclusiveUbs && (*inclusiveUbs)[i];
+    bool runsOnce = *step > 0 ? (inclusiveUb ? *lb <= *ub : *lb < *ub)
+                              : (inclusiveUb ? *lb >= *ub : *lb > *ub);
     // The dimensions are iterated as a nest, so one empty dimension empties
     // the whole nest whatever the others do, while the body runs only if every
     // dimension runs.
@@ -644,6 +650,8 @@ static BodyExecution getBodyExecution(LoopOp loopOp) {
       return BodyExecution::Never;
   }
 
+  // No dimension was empty, so the body runs unless some dimension was
+  // unknown.
   return result;
 }
 

>From fc151a43910f1ebf9004a16bee9ab073186e8c86 Mon Sep 17 00:00:00 2001
From: Susan Tan <zujunt at nvidia.com>
Date: Thu, 27 Aug 2026 14:14:01 -0700
Subject: [PATCH 4/6] add tests

---
 .../OpenACC/region-branchop-interface.mlir    | 280 ++++++++++++++++++
 1 file changed, 280 insertions(+)

diff --git a/mlir/test/Dialect/OpenACC/region-branchop-interface.mlir b/mlir/test/Dialect/OpenACC/region-branchop-interface.mlir
index da10a76f3af46..fc001def066e2 100644
--- a/mlir/test/Dialect/OpenACC/region-branchop-interface.mlir
+++ b/mlir/test/Dialect/OpenACC/region-branchop-interface.mlir
@@ -249,6 +249,286 @@ func.func @last_mod_openacc_loop_descending_empty(%arg0: memref<f32>)
 
 // -----
 
+// structured acc.loop whose bounds match and whose upper bound is inclusive:
+// this runs exactly once. Same bounds as @last_mod_openacc_loop_empty, which
+// runs zero times because its upper bound is exclusive, so ignoring
+// `inclusiveUpperbound` here would reach the opposite conclusion.
+//
+// CHECK-LABEL: test_tag: acc_loop_inclusive_single_after:
+// CHECK:  operand #0
+// CHECK-NEXT:   - loop_region
+func.func @last_mod_openacc_loop_inclusive_single(%arg0: memref<f32>)
+    -> memref<f32> {
+  %zero = arith.constant 0.0 : f32
+  %one = arith.constant 1.0 : f32
+  memref.store %zero, %arg0[] {tag_name = "pre"} : memref<f32>
+  %c1_i32 = arith.constant 1 : i32
+  %c10_i32 = arith.constant 10 : i32
+  acc.loop control(%iv : i32) = (%c10_i32 : i32) to (%c10_i32 : i32)
+      step (%c1_i32 : i32) {
+    memref.store %one, %arg0[] {tag_name = "loop_region"} : memref<f32>
+    acc.yield
+  } auto_ inclusiveUpperbound(array<i1: true>)
+  memref.load %arg0[] {tag = "acc_loop_inclusive_single_after"} : memref<f32>
+  return %arg0 : memref<f32>
+}
+
+// -----
+
+// structured acc.loop ascending with an inclusive upper bound below its lower
+// bound: the body never runs.
+//
+// CHECK-LABEL: test_tag: acc_loop_inclusive_empty_after:
+// CHECK:  operand #0
+// CHECK-NEXT:   - pre
+func.func @last_mod_openacc_loop_inclusive_empty(%arg0: memref<f32>)
+    -> memref<f32> {
+  %zero = arith.constant 0.0 : f32
+  %one = arith.constant 1.0 : f32
+  memref.store %zero, %arg0[] {tag_name = "pre"} : memref<f32>
+  %c1_i32 = arith.constant 1 : i32
+  %c10_i32 = arith.constant 10 : i32
+  %c11_i32 = arith.constant 11 : i32
+  acc.loop control(%iv : i32) = (%c11_i32 : i32) to (%c10_i32 : i32)
+      step (%c1_i32 : i32) {
+    memref.store %one, %arg0[] {tag_name = "loop_region"} : memref<f32>
+    acc.yield
+  } auto_ inclusiveUpperbound(array<i1: true>)
+  memref.load %arg0[] {tag = "acc_loop_inclusive_empty_after"} : memref<f32>
+  return %arg0 : memref<f32>
+}
+
+// -----
+
+// structured acc.loop counting down with an exclusive upper bound: the body
+// runs 9 times. Together with @last_mod_openacc_loop_descending this covers
+// both upper-bound kinds for a descending step.
+//
+// CHECK-LABEL: test_tag: acc_loop_descending_exclusive_after:
+// CHECK:  operand #0
+// CHECK-NEXT:   - loop_region
+func.func @last_mod_openacc_loop_descending_exclusive(%arg0: memref<f32>)
+    -> memref<f32> {
+  %zero = arith.constant 0.0 : f32
+  %one = arith.constant 1.0 : f32
+  memref.store %zero, %arg0[] {tag_name = "pre"} : memref<f32>
+  %c1_i32 = arith.constant 1 : i32
+  %cm1_i32 = arith.constant -1 : i32
+  %c10_i32 = arith.constant 10 : i32
+  acc.loop control(%iv : i32) = (%c10_i32 : i32) to (%c1_i32 : i32)
+      step (%cm1_i32 : i32) {
+    memref.store %one, %arg0[] {tag_name = "loop_region"} : memref<f32>
+    acc.yield
+  } auto_
+  memref.load %arg0[] {tag = "acc_loop_descending_exclusive_after"}
+      : memref<f32>
+  return %arg0 : memref<f32>
+}
+
+// -----
+
+// structured acc.loop counting down with matching bounds and an exclusive
+// upper bound: the body never runs. The inclusive variant of these same bounds
+// would run once, so this is the descending mirror of
+// @last_mod_openacc_loop_inclusive_single.
+//
+// CHECK-LABEL: test_tag: acc_loop_descending_exclusive_empty_after:
+// CHECK:  operand #0
+// CHECK-NEXT:   - pre
+func.func @last_mod_openacc_loop_descending_exclusive_empty(%arg0: memref<f32>)
+    -> memref<f32> {
+  %zero = arith.constant 0.0 : f32
+  %one = arith.constant 1.0 : f32
+  memref.store %zero, %arg0[] {tag_name = "pre"} : memref<f32>
+  %cm1_i32 = arith.constant -1 : i32
+  %c10_i32 = arith.constant 10 : i32
+  acc.loop control(%iv : i32) = (%c10_i32 : i32) to (%c10_i32 : i32)
+      step (%cm1_i32 : i32) {
+    memref.store %one, %arg0[] {tag_name = "loop_region"} : memref<f32>
+    acc.yield
+  } auto_
+  memref.load %arg0[] {tag = "acc_loop_descending_exclusive_empty_after"}
+      : memref<f32>
+  return %arg0 : memref<f32>
+}
+
+// -----
+
+// structured acc.loop whose step overshoots the upper bound: the entry test
+// still holds at the lower bound, so the body runs once and no trip count is
+// needed to see it.
+//
+// CHECK-LABEL: test_tag: acc_loop_big_step_after:
+// CHECK:  operand #0
+// CHECK-NEXT:   - loop_region
+func.func @last_mod_openacc_loop_big_step(%arg0: memref<f32>) -> memref<f32> {
+  %zero = arith.constant 0.0 : f32
+  %one = arith.constant 1.0 : f32
+  memref.store %zero, %arg0[] {tag_name = "pre"} : memref<f32>
+  %c0_i32 = arith.constant 0 : i32
+  %c5_i32 = arith.constant 5 : i32
+  %c100_i32 = arith.constant 100 : i32
+  acc.loop control(%iv : i32) = (%c0_i32 : i32) to (%c5_i32 : i32)
+      step (%c100_i32 : i32) {
+    memref.store %one, %arg0[] {tag_name = "loop_region"} : memref<f32>
+    acc.yield
+  } auto_
+  memref.load %arg0[] {tag = "acc_loop_big_step_after"} : memref<f32>
+  return %arg0 : memref<f32>
+}
+
+// -----
+
+// structured acc.loop with a zero step: it either never advances or never
+// starts, so neither edge can be ruled out.
+//
+// CHECK-LABEL: test_tag: acc_loop_zero_step_after:
+// CHECK:  operand #0
+// CHECK-DAG:   - pre
+// CHECK-DAG:   - loop_region
+func.func @last_mod_openacc_loop_zero_step(%arg0: memref<f32>) -> memref<f32> {
+  %zero = arith.constant 0.0 : f32
+  %one = arith.constant 1.0 : f32
+  memref.store %zero, %arg0[] {tag_name = "pre"} : memref<f32>
+  %c0_i32 = arith.constant 0 : i32
+  %c1_i32 = arith.constant 1 : i32
+  %c10_i32 = arith.constant 10 : i32
+  acc.loop control(%iv : i32) = (%c1_i32 : i32) to (%c10_i32 : i32)
+      step (%c0_i32 : i32) {
+    memref.store %one, %arg0[] {tag_name = "loop_region"} : memref<f32>
+    acc.yield
+  } auto_
+  memref.load %arg0[] {tag = "acc_loop_zero_step_after"} : memref<f32>
+  return %arg0 : memref<f32>
+}
+
+// -----
+
+// container-like acc.loop: it carries no bounds of its own, so its iteration
+// space cannot be inspected here even though the contained scf.for is proven
+// to run. Both edges out of the acc.loop are kept.
+//
+// CHECK-LABEL: test_tag: acc_loop_container_after:
+// CHECK:  operand #0
+// CHECK-DAG:   - pre
+// CHECK-DAG:   - loop_region
+func.func @last_mod_openacc_loop_container(%arg0: memref<f32>) -> memref<f32> {
+  %zero = arith.constant 0.0 : f32
+  %one = arith.constant 1.0 : f32
+  memref.store %zero, %arg0[] {tag_name = "pre"} : memref<f32>
+  %c0 = arith.constant 0 : index
+  %c1 = arith.constant 1 : index
+  %c10 = arith.constant 10 : index
+  acc.loop {
+    scf.for %i = %c0 to %c10 step %c1 {
+      memref.store %one, %arg0[] {tag_name = "loop_region"} : memref<f32>
+    }
+    acc.yield
+  } auto_
+  memref.load %arg0[] {tag = "acc_loop_container_after"} : memref<f32>
+  return %arg0 : memref<f32>
+}
+
+// -----
+
+// collapsed acc.loop whose every dimension runs: the body is entered.
+//
+// CHECK-LABEL: test_tag: acc_loop_collapsed_after:
+// CHECK:  operand #0
+// CHECK-NEXT:   - loop_region
+func.func @last_mod_openacc_loop_collapsed(%arg0: memref<f32>) -> memref<f32> {
+  %zero = arith.constant 0.0 : f32
+  %one = arith.constant 1.0 : f32
+  memref.store %zero, %arg0[] {tag_name = "pre"} : memref<f32>
+  %c1_i32 = arith.constant 1 : i32
+  %c10_i32 = arith.constant 10 : i32
+  acc.loop control(%i : i32, %j : i32) = (%c1_i32, %c1_i32 : i32, i32)
+      to (%c10_i32, %c10_i32 : i32, i32) step (%c1_i32, %c1_i32 : i32, i32) {
+    memref.store %one, %arg0[] {tag_name = "loop_region"} : memref<f32>
+    acc.yield
+  } auto_
+  memref.load %arg0[] {tag = "acc_loop_collapsed_after"} : memref<f32>
+  return %arg0 : memref<f32>
+}
+
+// -----
+
+// collapsed acc.loop with one empty dimension: the dimensions are iterated as
+// a nest, so the whole iteration space is empty even though the first
+// dimension would run on its own.
+//
+// CHECK-LABEL: test_tag: acc_loop_collapsed_empty_after:
+// CHECK:  operand #0
+// CHECK-NEXT:   - pre
+func.func @last_mod_openacc_loop_collapsed_empty(%arg0: memref<f32>)
+    -> memref<f32> {
+  %zero = arith.constant 0.0 : f32
+  %one = arith.constant 1.0 : f32
+  memref.store %zero, %arg0[] {tag_name = "pre"} : memref<f32>
+  %c1_i32 = arith.constant 1 : i32
+  %c10_i32 = arith.constant 10 : i32
+  acc.loop control(%i : i32, %j : i32) = (%c1_i32, %c10_i32 : i32, i32)
+      to (%c10_i32, %c10_i32 : i32, i32) step (%c1_i32, %c1_i32 : i32, i32) {
+    memref.store %one, %arg0[] {tag_name = "loop_region"} : memref<f32>
+    acc.yield
+  } auto_
+  memref.load %arg0[] {tag = "acc_loop_collapsed_empty_after"} : memref<f32>
+  return %arg0 : memref<f32>
+}
+
+// -----
+
+// collapsed acc.loop pairing an unknown dimension with an empty one: an empty
+// dimension zeroes the nest whatever the others do, so the unknown dimension
+// does not stop the body from being proven unreachable.
+//
+// CHECK-LABEL: test_tag: acc_loop_collapsed_unknown_empty_after:
+// CHECK:  operand #0
+// CHECK-NEXT:   - pre
+func.func @last_mod_openacc_loop_collapsed_unknown_empty(%arg0: memref<f32>,
+    %n: i32) -> memref<f32> {
+  %zero = arith.constant 0.0 : f32
+  %one = arith.constant 1.0 : f32
+  memref.store %zero, %arg0[] {tag_name = "pre"} : memref<f32>
+  %c1_i32 = arith.constant 1 : i32
+  %c10_i32 = arith.constant 10 : i32
+  acc.loop control(%i : i32, %j : i32) = (%c1_i32, %c10_i32 : i32, i32)
+      to (%n, %c10_i32 : i32, i32) step (%c1_i32, %c1_i32 : i32, i32) {
+    memref.store %one, %arg0[] {tag_name = "loop_region"} : memref<f32>
+    acc.yield
+  } auto_
+  memref.load %arg0[] {tag = "acc_loop_collapsed_unknown_empty_after"}
+      : memref<f32>
+  return %arg0 : memref<f32>
+}
+
+// -----
+
+// collapsed acc.loop pairing an unknown dimension with one that runs: the body
+// runs only if every dimension runs, so the unknown dimension keeps both edges.
+//
+// CHECK-LABEL: test_tag: acc_loop_collapsed_unknown_after:
+// CHECK:  operand #0
+// CHECK-DAG:   - pre
+// CHECK-DAG:   - loop_region
+func.func @last_mod_openacc_loop_collapsed_unknown(%arg0: memref<f32>,
+    %n: i32) -> memref<f32> {
+  %zero = arith.constant 0.0 : f32
+  %one = arith.constant 1.0 : f32
+  memref.store %zero, %arg0[] {tag_name = "pre"} : memref<f32>
+  %c1_i32 = arith.constant 1 : i32
+  %c10_i32 = arith.constant 10 : i32
+  acc.loop control(%i : i32, %j : i32) = (%c1_i32, %c1_i32 : i32, i32)
+      to (%n, %c10_i32 : i32, i32) step (%c1_i32, %c1_i32 : i32, i32) {
+    memref.store %one, %arg0[] {tag_name = "loop_region"} : memref<f32>
+    acc.yield
+  } auto_
+  memref.load %arg0[] {tag = "acc_loop_collapsed_unknown_after"} : memref<f32>
+  return %arg0 : memref<f32>
+}
+
+// -----
+
 // Unstructured acc.loop: the RegionBranch is modeled with explicit CFG and early
 // exits, and the RegionBranch graph only exposes a single entry and single
 // exit edge (no region backedge).

>From 92085297535053edee61403f9ed0b5351fbfc4b2 Mon Sep 17 00:00:00 2001
From: Susan Tan <zujunt at nvidia.com>
Date: Thu, 27 Aug 2026 14:16:03 -0700
Subject: [PATCH 5/6] tweak

---
 mlir/test/Dialect/OpenACC/region-branchop-interface.mlir | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/mlir/test/Dialect/OpenACC/region-branchop-interface.mlir b/mlir/test/Dialect/OpenACC/region-branchop-interface.mlir
index fc001def066e2..8e6f2b7c7dedb 100644
--- a/mlir/test/Dialect/OpenACC/region-branchop-interface.mlir
+++ b/mlir/test/Dialect/OpenACC/region-branchop-interface.mlir
@@ -179,6 +179,7 @@ func.func @last_mod_openacc_loop_dynamic(%arg0: memref<f32>, %n: i32) -> memref<
 // CHECK-LABEL: test_tag: acc_loop_empty_after:
 // CHECK:  operand #0
 // CHECK-NEXT:   - pre
+// CHECK-NOT:   - loop_region
 func.func @last_mod_openacc_loop_empty(%arg0: memref<f32>) -> memref<f32> {
   %zero = arith.constant 0.0 : f32
   %one = arith.constant 1.0 : f32
@@ -230,6 +231,7 @@ func.func @last_mod_openacc_loop_descending(%arg0: memref<f32>) -> memref<f32> {
 // CHECK-LABEL: test_tag: acc_loop_descending_empty_after:
 // CHECK:  operand #0
 // CHECK-NEXT:   - pre
+// CHECK-NOT:   - loop_region
 func.func @last_mod_openacc_loop_descending_empty(%arg0: memref<f32>)
     -> memref<f32> {
   %zero = arith.constant 0.0 : f32
@@ -281,6 +283,7 @@ func.func @last_mod_openacc_loop_inclusive_single(%arg0: memref<f32>)
 // CHECK-LABEL: test_tag: acc_loop_inclusive_empty_after:
 // CHECK:  operand #0
 // CHECK-NEXT:   - pre
+// CHECK-NOT:   - loop_region
 func.func @last_mod_openacc_loop_inclusive_empty(%arg0: memref<f32>)
     -> memref<f32> {
   %zero = arith.constant 0.0 : f32
@@ -335,6 +338,7 @@ func.func @last_mod_openacc_loop_descending_exclusive(%arg0: memref<f32>)
 // CHECK-LABEL: test_tag: acc_loop_descending_exclusive_empty_after:
 // CHECK:  operand #0
 // CHECK-NEXT:   - pre
+// CHECK-NOT:   - loop_region
 func.func @last_mod_openacc_loop_descending_exclusive_empty(%arg0: memref<f32>)
     -> memref<f32> {
   %zero = arith.constant 0.0 : f32
@@ -460,6 +464,7 @@ func.func @last_mod_openacc_loop_collapsed(%arg0: memref<f32>) -> memref<f32> {
 // CHECK-LABEL: test_tag: acc_loop_collapsed_empty_after:
 // CHECK:  operand #0
 // CHECK-NEXT:   - pre
+// CHECK-NOT:   - loop_region
 func.func @last_mod_openacc_loop_collapsed_empty(%arg0: memref<f32>)
     -> memref<f32> {
   %zero = arith.constant 0.0 : f32
@@ -485,6 +490,7 @@ func.func @last_mod_openacc_loop_collapsed_empty(%arg0: memref<f32>)
 // CHECK-LABEL: test_tag: acc_loop_collapsed_unknown_empty_after:
 // CHECK:  operand #0
 // CHECK-NEXT:   - pre
+// CHECK-NOT:   - loop_region
 func.func @last_mod_openacc_loop_collapsed_unknown_empty(%arg0: memref<f32>,
     %n: i32) -> memref<f32> {
   %zero = arith.constant 0.0 : f32

>From f7713d12ddbc41155ec8438b2c3b24ff9057693f Mon Sep 17 00:00:00 2001
From: Susan Tan <zujunt at nvidia.com>
Date: Fri, 28 Aug 2026 08:44:30 -0700
Subject: [PATCH 6/6] tweak

---
 mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp | 1 +
 1 file changed, 1 insertion(+)

diff --git a/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp b/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp
index d1d2ea8d3da66..327fd5df49259 100644
--- a/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp
+++ b/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp
@@ -641,6 +641,7 @@ static BodyExecution getBodyExecution(LoopOp loopOp) {
     std::optional<ArrayRef<bool>> inclusiveUbs =
         loopOp.getInclusiveUpperbound();
     bool inclusiveUb = inclusiveUbs && (*inclusiveUbs)[i];
+    assert(*step != 0 && "zero step should have been filtered out");
     bool runsOnce = *step > 0 ? (inclusiveUb ? *lb <= *ub : *lb < *ub)
                               : (inclusiveUb ? *lb >= *ub : *lb > *ub);
     // The dimensions are iterated as a nest, so one empty dimension empties



More information about the Mlir-commits mailing list