[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> ®ions) {
// 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