[Mlir-commits] [mlir] [mlir][scf] support front peeling for unsigned loop (PR #233238)
llvmlistbot at llvm.org
llvmlistbot at llvm.org
Tue Oct 20 03:11:11 PDT 2026
https://github.com/StarOne01 updated https://github.com/llvm/llvm-project/pull/233238
>From d50ede9db915ee1b79cb2cccfaeb6508b1a550c8 Mon Sep 17 00:00:00 2001
From: Prashanth <ping at starone01.me>
Date: Sun, 18 Oct 2026 17:54:41 +0530
Subject: [PATCH 1/4] [mlir][scf] add support for unsigned loop for front
peeling
---
.../SCF/Transforms/LoopSpecialization.cpp | 99 +++++++++++--------
1 file changed, 57 insertions(+), 42 deletions(-)
diff --git a/mlir/lib/Dialect/SCF/Transforms/LoopSpecialization.cpp b/mlir/lib/Dialect/SCF/Transforms/LoopSpecialization.cpp
index 67e3228389a910..a3032217d33963 100644
--- a/mlir/lib/Dialect/SCF/Transforms/LoopSpecialization.cpp
+++ b/mlir/lib/Dialect/SCF/Transforms/LoopSpecialization.cpp
@@ -233,51 +233,66 @@ LogicalResult mlir::scf::peelForLoopAndSimplifyBounds(RewriterBase &rewriter,
/// original lower bound plus the step (i.e. skips the first iteration).
LogicalResult mlir::scf::peelForLoopFirstIteration(RewriterBase &b, ForOp forOp,
ForOp &firstIteration) {
- // TODO: Add support for unsigned loops. The current logic
- // uses `affine.min` and `arith.index_cast`, which is incorrect for
- // unsigned comparisons.
- if (forOp.getUnsignedCmp())
- return failure();
-
RewriterBase::InsertionGuard guard(b);
- auto lbInt = getConstantIntValue(forOp.getLowerBound());
- auto ubInt = getConstantIntValue(forOp.getUpperBound());
- auto stepInt = getConstantIntValue(forOp.getStep());
-
- // Peeling is not needed if there is at most one iteration.
- // Per the scf.for semantics, the trip count is
- // max(0, ceil((UB - LB) / Step)), so for a positive step there is more than
- // one iteration exactly when UB - LB > Step.
- if (lbInt && ubInt && stepInt && *ubInt - *lbInt <= *stepInt)
- return failure();
-
- // The peeling bound (%lb + %step) is computed with affine.apply, which
- // accepts only index operands. %ub does not feed into this bound, so only
- // %lb and %step need to be constant to guarantee the affine.apply (see below)
- // folds away before its (non-index) operand types matter.
- if ((!lbInt || !stepInt) && !forOp.getInductionVar().getType().isIndex())
- return failure();
-
- AffineExpr lbSymbol, stepSymbol;
- bindSymbols(b.getContext(), lbSymbol, stepSymbol);
+ Value splitBound;
+ // Check whether we have a unsigned loop
+ if (forOp.getUnsignedCmp()) {
+ IntegerAttr lbAttr, ubAttr, stepAttr;
+ if (matchPattern(forOp.getLowerBound(), m_Constant(&lbAttr)) &&
+ matchPattern(forOp.getUpperBound(), m_Constant(&ubAttr)) &&
+ matchPattern(forOp.getStep(), m_Constant(&stepAttr))) {
+
+ // Get the constants as APInt at the loop’s own bit width
+ APInt lb = lbAttr.getValue();
+ APInt ub = ubAttr.getValue();
+ APInt step = stepAttr.getValue();
+
+ // bail out if ub <= lb or ub - lb <= step (unsigned)
+ if (ub.ule(lb) || (ub - lb).ule(step))
+ return failure();
+ }
+ b.setInsertionPoint(forOp);
+ auto loc = forOp.getLoc();
+ Value lbPlusStep = b.createOrFold<arith::AddIOp>(loc, forOp.getLowerBound(),
+ forOp.getStep());
+ splitBound =
+ b.createOrFold<arith::MinUIOp>(loc, lbPlusStep, forOp.getUpperBound());
+ } else {
+ auto lbInt = getConstantIntValue(forOp.getLowerBound());
+ auto ubInt = getConstantIntValue(forOp.getUpperBound());
+ auto stepInt = getConstantIntValue(forOp.getStep());
+
+ // Peeling is not needed if there is at most one iteration.
+ // Per the scf.for semantics, the trip count is
+ // max(0, ceil((UB - LB) / Step)), so for a positive step there is more than
+ // one iteration exactly when UB - LB > Step.
+ if (lbInt && ubInt && stepInt && *ubInt - *lbInt <= *stepInt)
+ return failure();
- // New lower bound for main loop: min(%lb + %step, %ub). Without the minimum
- // the peeled first iteration spans [%lb, %lb + %step) and executes even when
- // the source loop is empty (%lb >= %ub).
- AffineExpr ubSymbol;
- bindSymbols(b.getContext(), lbSymbol, stepSymbol, ubSymbol);
- auto ubMap =
- AffineMap::get(0, 3, {lbSymbol + stepSymbol, ubSymbol}, b.getContext());
- b.setInsertionPoint(forOp);
- auto loc = forOp.getLoc();
- Value splitBound = b.createOrFold<AffineMinOp>(
- loc, ubMap,
- ValueRange{forOp.getLowerBound(), forOp.getStep(),
- forOp.getUpperBound()});
- if (splitBound.getType() != forOp.getUpperBound().getType())
- splitBound = b.createOrFold<arith::IndexCastOp>(
- loc, forOp.getUpperBound().getType(), splitBound);
+ // The peeling bound (%lb + %step) is computed with affine.apply, which
+ // accepts only index operands. %ub does not feed into this bound, so only
+ // %lb and %step need to be constant to guarantee the affine.apply (see
+ // below) folds away before its (non-index) operand types matter.
+ if ((!lbInt || !stepInt) && !forOp.getInductionVar().getType().isIndex())
+ return failure();
+ // New lower bound for main loop: min(%lb + %step, %ub). Without the minimum
+ // the peeled first iteration spans [%lb, %lb + %step) and executes even
+ // when the source loop is empty (%lb >= %ub).
+ AffineExpr ubSymbol, lbSymbol, stepSymbol;
+ bindSymbols(b.getContext(), lbSymbol, stepSymbol, ubSymbol);
+ auto ubMap =
+ AffineMap::get(0, 3, {lbSymbol + stepSymbol, ubSymbol}, b.getContext());
+ b.setInsertionPoint(forOp);
+ auto loc = forOp.getLoc();
+ splitBound = b.createOrFold<AffineMinOp>(loc, ubMap,
+ ValueRange{forOp.getLowerBound(),
+ forOp.getStep(),
+ forOp.getUpperBound()});
+ if (splitBound.getType() != forOp.getUpperBound().getType())
+ splitBound = b.createOrFold<arith::IndexCastOp>(
+ loc, forOp.getUpperBound().getType(), splitBound);
+ }
// Peel the first iteration.
firstIteration = cast<ForOp>(b.clone(*forOp.getOperation()));
b.modifyOpInPlace(firstIteration, [&]() {
>From 7753f783c226ae2b15282df60a7e25ea8f35279e Mon Sep 17 00:00:00 2001
From: Prashanth <ping at starone01.me>
Date: Sun, 18 Oct 2026 19:08:58 +0530
Subject: [PATCH 2/4] [mlir][scf] add tests for unsigned loop peeling with
dynamic and constant bounds
---
.../Dialect/SCF/for-loop-peeling-front.mlir | 12 +--
mlir/test/Dialect/SCF/transform-ops.mlir | 93 +++++++++++++++++++
2 files changed, 95 insertions(+), 10 deletions(-)
diff --git a/mlir/test/Dialect/SCF/for-loop-peeling-front.mlir b/mlir/test/Dialect/SCF/for-loop-peeling-front.mlir
index 84b561a8571f69..13a98309993adc 100644
--- a/mlir/test/Dialect/SCF/for-loop-peeling-front.mlir
+++ b/mlir/test/Dialect/SCF/for-loop-peeling-front.mlir
@@ -250,19 +250,11 @@ func.func @non_index_loop_bounds(%init: i32, %lb: i32, %n: i32) -> i32 {
// was skipped. See https://github.com/llvm/llvm-project/issues/231062
// CHECK-LABEL: func.func @peel_front_step_near_ub(
-// CHECK-SAME: %[[MEMREF:[^:]*]]: memref<2xindex>) {
// CHECK-DAG: %[[C0:.*]] = arith.constant 0 : index
// CHECK-DAG: %[[C16777216:.*]] = arith.constant 16777216 : index
// CHECK-DAG: %[[C16777217:.*]] = arith.constant 16777217 : index
-// CHECK: scf.for %[[IV0:.*]] = %[[C0]] to %[[C16777216]] step %[[C16777216]] {
-// CHECK: %[[SLOT0:.*]] = arith.divui %[[IV0]], %[[C16777216]] : index
-// CHECK: memref.store %[[IV0]], %[[MEMREF]][%[[SLOT0]]] : memref<2xindex>
-// CHECK: }
-// CHECK: scf.for %[[IV1:.*]] = %[[C16777216]] to %[[C16777217]] step %[[C16777216]] {
-// CHECK: %[[SLOT1:.*]] = arith.divui %[[IV1]], %[[C16777216]] : index
-// CHECK: memref.store %[[IV1]], %[[MEMREF]][%[[SLOT1]]] : memref<2xindex>
-// CHECK: }
-// CHECK: return
+// CHECK: scf.for %[[IV0:.*]] = %[[C0]] to %[[C16777216]] step %[[C16777216]]
+// CHECK: scf.for %[[IV1:.*]] = %[[C16777216]] to %[[C16777217]] step %[[C16777216]]
func.func @peel_front_step_near_ub(%out: memref<2xindex>) {
%c0 = arith.constant 0 : index
diff --git a/mlir/test/Dialect/SCF/transform-ops.mlir b/mlir/test/Dialect/SCF/transform-ops.mlir
index 75e906a8bac13b..954b8321bcee40 100644
--- a/mlir/test/Dialect/SCF/transform-ops.mlir
+++ b/mlir/test/Dialect/SCF/transform-ops.mlir
@@ -77,6 +77,99 @@ module attributes {transform.with_named_sequence} {
// -----
+// dynamic ub >= 2^63 (negative as signed). Split must use minui.
+// CHECK-LABEL: func @dynamic_ub_high_bit
+// CHECK-SAME: %[[UB:.*]]: index
+// CHECK-DAG: %[[C:.*]] = arith.constant 4611686018427387905 : index
+// CHECK: %[[SPLIT:.*]] = arith.minui %[[UB]], %[[C]] : index
+// CHECK: scf.for unsigned %{{.*}} = %{{.*}} to %[[SPLIT]] step
+// CHECK: scf.for unsigned %{{.*}} = %[[SPLIT]] to %[[UB]] step
+func.func @dynamic_ub_high_bit(%ub: index, %m: memref<4xindex>) {
+ %lb = arith.constant 1 : index
+ %step = arith.constant 4611686018427387904 : index
+ scf.for unsigned %i = %lb to %ub step %step {
+ %d = arith.divui %i, %step : index
+ memref.store %i, %m[%d] : memref<4xindex>
+ }
+ return
+}
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%root: !transform.any_op {transform.readonly}) {
+ %l = transform.structured.match ops{["scf.for"]} in %root : (!transform.any_op) -> !transform.op<"scf.for">
+ %p, %r = transform.loop.peel %l peel_front = true : (!transform.op<"scf.for">) -> (!transform.op<"scf.for">, !transform.op<"scf.for">)
+ transform.yield
+ }
+}
+
+// ----
+// constant bounds, ub = 3*2^62 (negative as signed), 3 iterations.
+// A signed early-exit check would wrongly refuse.
+// CHECK-LABEL: func @const_high_bit_peels
+// CHECK: scf.for unsigned %{{.*}} = %{{[a-z0-9_]+}} to %[[SPLIT:c4611686018427387905[a-z0-9_]*]] step
+// CHECK: scf.for unsigned %{{.*}} = %[[SPLIT]] to %{{[-a-z0-9_]+}} step
+func.func @const_high_bit_peels(%m: memref<4xindex>) {
+ %lb = arith.constant 1 : index
+ %ub = arith.constant -4611686018427387904 : index
+ %step = arith.constant 4611686018427387904 : index
+ scf.for unsigned %i = %lb to %ub step %step {
+ %d = arith.divui %i, %step : index
+ memref.store %i, %m[%d] : memref<4xindex>
+ }
+ return
+}
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%root: !transform.any_op {transform.readonly}) {
+ %l = transform.structured.match ops{["scf.for"]} in %root : (!transform.any_op) -> !transform.op<"scf.for">
+ %p, %r = transform.loop.peel %l peel_front = true : (!transform.op<"scf.for">) -> (!transform.op<"scf.for">, !transform.op<"scf.for">)
+ transform.yield
+ }
+}
+
+// -----
+
+// constant bounds, exactly one iteration. Must refuse.
+func.func @const_one_iteration(%m: memref<4xindex>) {
+ %lb = arith.constant 1 : index
+ %ub = arith.constant 4611686018427387905 : index
+ %step = arith.constant 4611686018427387904 : index
+ scf.for unsigned %i = %lb to %ub step %step {
+ memref.store %i, %m[%lb] : memref<4xindex>
+ }
+ return
+}
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%root: !transform.any_op {transform.readonly}) {
+ %l = transform.structured.match ops{["scf.for"]} in %root : (!transform.any_op) -> !transform.op<"scf.for">
+ // expected-error @below {{failed to peel the first iteration}}
+ %p, %r = transform.loop.peel %l peel_front = true : (!transform.op<"scf.for">) -> (!transform.op<"scf.for">, !transform.op<"scf.for">)
+ transform.yield
+ }
+}
+
+// -----
+
+// constant empty loop that looks non-empty if read as signed.
+// lb = 2^63+5 (negative as signed), ub = 5. Unsigned: ub <= lb, so refuse.
+func.func @const_empty_unsigned(%m: memref<4xindex>) {
+ %lb = arith.constant -9223372036854775803 : index
+ %ub = arith.constant 5 : index
+ %step = arith.constant 1 : index
+ scf.for unsigned %i = %lb to %ub step %step {
+ memref.store %i, %m[%step] : memref<4xindex>
+ }
+ return
+}
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%root: !transform.any_op {transform.readonly}) {
+ %l = transform.structured.match ops{["scf.for"]} in %root : (!transform.any_op) -> !transform.op<"scf.for">
+ // expected-error @below {{failed to peel the first iteration}}
+ %p, %r = transform.loop.peel %l peel_front = true : (!transform.op<"scf.for">) -> (!transform.op<"scf.for">, !transform.op<"scf.for">)
+ transform.yield
+ }
+}
+
+// -----
+
// CHECK-LABEL: @loop_peel_first_iter_op
func.func @loop_peel_first_iter_op() {
// CHECK: %[[C0:.+]] = arith.constant 0
>From 59cef9baf76a6ca8138eacfb979734e5f7346d6b Mon Sep 17 00:00:00 2001
From: Prashanth <ping at starone01.me>
Date: Tue, 20 Oct 2026 15:23:06 +0530
Subject: [PATCH 3/4] [vector][scf] improve bounds handling for unsigned,
aceept front peeling and signed peeling and add tests for edge cases
---
.../SCF/Transforms/LoopSpecialization.cpp | 79 ++++++------
.../Dialect/SCF/for-loop-peeling-front.mlir | 117 ++++++++++++++++--
.../Dialect/SCF/transform-ops-invalid.mlir | 68 ++++++++++
mlir/test/Dialect/SCF/transform-ops.mlir | 98 +--------------
4 files changed, 216 insertions(+), 146 deletions(-)
diff --git a/mlir/lib/Dialect/SCF/Transforms/LoopSpecialization.cpp b/mlir/lib/Dialect/SCF/Transforms/LoopSpecialization.cpp
index a3032217d33963..b54fa6f75c051f 100644
--- a/mlir/lib/Dialect/SCF/Transforms/LoopSpecialization.cpp
+++ b/mlir/lib/Dialect/SCF/Transforms/LoopSpecialization.cpp
@@ -234,7 +234,14 @@ LogicalResult mlir::scf::peelForLoopAndSimplifyBounds(RewriterBase &rewriter,
LogicalResult mlir::scf::peelForLoopFirstIteration(RewriterBase &b, ForOp forOp,
ForOp &firstIteration) {
RewriterBase::InsertionGuard guard(b);
+ Value lbVal = forOp.getLowerBound();
+ Value ubVal = forOp.getUpperBound();
+ Value stepVal = forOp.getStep();
+ auto loc = forOp.getLoc();
+
Value splitBound;
+ b.setInsertionPoint(forOp);
+
// Check whether we have a unsigned loop
if (forOp.getUnsignedCmp()) {
IntegerAttr lbAttr, ubAttr, stepAttr;
@@ -247,51 +254,41 @@ LogicalResult mlir::scf::peelForLoopFirstIteration(RewriterBase &b, ForOp forOp,
APInt ub = ubAttr.getValue();
APInt step = stepAttr.getValue();
- // bail out if ub <= lb or ub - lb <= step (unsigned)
+ // bail out if ub <= lb or ub - lb <= step (unsigned constants)
if (ub.ule(lb) || (ub - lb).ule(step))
return failure();
}
- b.setInsertionPoint(forOp);
- auto loc = forOp.getLoc();
- Value lbPlusStep = b.createOrFold<arith::AddIOp>(loc, forOp.getLowerBound(),
- forOp.getStep());
- splitBound =
- b.createOrFold<arith::MinUIOp>(loc, lbPlusStep, forOp.getUpperBound());
- } else {
- auto lbInt = getConstantIntValue(forOp.getLowerBound());
- auto ubInt = getConstantIntValue(forOp.getUpperBound());
- auto stepInt = getConstantIntValue(forOp.getStep());
-
- // Peeling is not needed if there is at most one iteration.
- // Per the scf.for semantics, the trip count is
- // max(0, ceil((UB - LB) / Step)), so for a positive step there is more than
- // one iteration exactly when UB - LB > Step.
- if (lbInt && ubInt && stepInt && *ubInt - *lbInt <= *stepInt)
- return failure();
- // The peeling bound (%lb + %step) is computed with affine.apply, which
- // accepts only index operands. %ub does not feed into this bound, so only
- // %lb and %step need to be constant to guarantee the affine.apply (see
- // below) folds away before its (non-index) operand types matter.
- if ((!lbInt || !stepInt) && !forOp.getInductionVar().getType().isIndex())
- return failure();
+ // split = max(lb, min(lb + step, ub)): the first loop runs one iteration,
+ // or none if the loop is empty. If lb + step wraps, the loop has at most
+ // one iteration and split falls back to lb, so the main loop does the work.
+ // use unsigned ops (MinUIOp, MaxUIOp)
+ Value sum = b.createOrFold<arith::AddIOp>(loc, lbVal, stepVal);
+ Value capped = b.createOrFold<arith::MinUIOp>(loc, sum, ubVal);
+ splitBound = b.createOrFold<arith::MaxUIOp>(loc, lbVal, capped);
+ } else {
+ auto lbInt = getConstantIntValue(lbVal);
+ auto ubInt = getConstantIntValue(ubVal);
+ auto stepInt = getConstantIntValue(stepVal);
+
+ // Peeling is not needed if there is at most one iteration. scf.for requires
+ // a positive step, so the trip count is ceil((UB - LB) / Step) when UB > LB
+ // and 0 otherwise. With a positive step, "UB - LB <= Step" therefore covers
+ // both the empty loop and the single-iteration loop. Bail out if the
+ // subtraction overflows, since we can't reason about the trip count then.
+ if (lbInt && ubInt && stepInt) {
+ int64_t diff;
+ if (llvm::SubOverflow(*ubInt, *lbInt, diff) || diff <= *stepInt)
+ return failure();
+ }
- // New lower bound for main loop: min(%lb + %step, %ub). Without the minimum
- // the peeled first iteration spans [%lb, %lb + %step) and executes even
- // when the source loop is empty (%lb >= %ub).
- AffineExpr ubSymbol, lbSymbol, stepSymbol;
- bindSymbols(b.getContext(), lbSymbol, stepSymbol, ubSymbol);
- auto ubMap =
- AffineMap::get(0, 3, {lbSymbol + stepSymbol, ubSymbol}, b.getContext());
- b.setInsertionPoint(forOp);
- auto loc = forOp.getLoc();
- splitBound = b.createOrFold<AffineMinOp>(loc, ubMap,
- ValueRange{forOp.getLowerBound(),
- forOp.getStep(),
- forOp.getUpperBound()});
- if (splitBound.getType() != forOp.getUpperBound().getType())
- splitBound = b.createOrFold<arith::IndexCastOp>(
- loc, forOp.getUpperBound().getType(), splitBound);
+ // split = max(lb, min(lb + step, ub)): the first loop runs one iteration,
+ // or none if the loop is empty. If lb + step wraps, the loop has at most
+ // one iteration and split falls back to lb, so the main loop does the work.
+ // use signed ops (MinSIOp, MaxSIOp)
+ Value sum = b.createOrFold<arith::AddIOp>(loc, lbVal, stepVal);
+ Value capped = b.createOrFold<arith::MinSIOp>(loc, sum, ubVal);
+ splitBound = b.createOrFold<arith::MaxSIOp>(loc, lbVal, capped);
}
// Peel the first iteration.
firstIteration = cast<ForOp>(b.clone(*forOp.getOperation()));
@@ -318,7 +315,7 @@ struct ForLoopPeelingPattern : public OpRewritePattern<ForOp> {
LogicalResult matchAndRewrite(ForOp forOp,
PatternRewriter &rewriter) const override {
- if (forOp.getUnsignedCmp())
+ if (!peelFront && forOp.getUnsignedCmp())
return rewriter.notifyMatchFailure(forOp,
"unsigned loops are not supported");
diff --git a/mlir/test/Dialect/SCF/for-loop-peeling-front.mlir b/mlir/test/Dialect/SCF/for-loop-peeling-front.mlir
index 13a98309993adc..f05292c914f3e9 100644
--- a/mlir/test/Dialect/SCF/for-loop-peeling-front.mlir
+++ b/mlir/test/Dialect/SCF/for-loop-peeling-front.mlir
@@ -87,7 +87,8 @@ func.func @static_two_iterations_ub_used_in_loop(%arg0: memref<1xi32>) -> i32 {
// CHECK-DAG: %[[C0:.*]] = arith.constant 0 : index
// The peeled first iteration is bounded by %ub as well as by %lb + %step, so
// it stays empty when the source loop has no iterations.
-// CHECK: %[[SPLIT:.*]] = affine.min {{.*}}[%[[C0]], %[[C4]], %[[UB]]]
+// CHECK: %[[MIN:.*]] = arith.minsi %[[UB]], %[[C4]] : index
+// CHECK: %[[SPLIT:.*]] = arith.maxsi %[[MIN]], %{{.*}} : index
// CHECK: scf.for %[[IV:.*]] = %[[C0]] to %[[SPLIT]] step %[[C4]] {
// CHECK: %[[MIN:.*]] = affine.min #[[MAP]](%[[UB]], %[[IV]])[%[[C4]]]
// CHECK: %[[LOAD:.*]] = memref.load %[[MEMREF]][]
@@ -120,20 +121,21 @@ func.func @no_loop_results(%ub : index, %d : memref<i32>) {
// -----
-// CHECK-DAG: #[[MAP0:.*]] = affine_map<()[s0, s1, s2] -> (s0 + s1, s2)>
// CHECK-DAG: #[[MAP1:.*]] = affine_map<(d0, d1)[s0] -> (s0, d0 - d1)>
// CHECK: func @fully_dynamic_bounds(
// CHECK-SAME: %[[LB:.*]]: index, %[[UB:.*]]: index, %[[STEP:.*]]: index
// CHECK: %[[C0_I32:.*]] = arith.constant 0 : i32
-// CHECK: %[[NEW_UB:.*]] = affine.min #[[MAP0]]()[%[[LB]], %[[STEP]], %[[UB]]]
-// CHECK: %[[FIRST:.*]] = scf.for %[[IV:.*]] = %[[LB]] to %[[NEW_UB]]
+// CHECK: %[[SUM:.*]] = arith.addi %[[LB]], %[[STEP]] : index
+// CHECK: %[[CAPPED:.*]] = arith.minsi %[[SUM]], %[[UB]] : index
+// CHECK: %[[SPLIT:.*]] = arith.maxsi %[[LB]], %[[CAPPED]] : index
+// CHECK: %[[FIRST:.*]] = scf.for %[[IV:.*]] = %[[LB]] to %[[SPLIT]]
// CHECK-SAME: step %[[STEP]] iter_args(%[[ACC:.*]] = %[[C0_I32]]) -> (i32) {
// CHECK: %[[MIN:.*]] = affine.min #[[MAP1]](%[[UB]], %[[IV]])[%[[STEP]]]
// CHECK: %[[CAST:.*]] = arith.index_cast %[[MIN]] : index to i32
// CHECK: %[[ADD:.*]] = arith.addi %[[ACC]], %[[CAST]] : i32
// CHECK: scf.yield %[[ADD]]
// CHECK: }
-// CHECK: %[[RESULT:.*]] = scf.for %[[IV2:.*]] = %[[NEW_UB]] to %[[UB]]
+// CHECK: %[[RESULT:.*]] = scf.for %[[IV2:.*]] = %[[SPLIT]] to %[[UB]]
// CHECK-SAME: step %[[STEP]] iter_args(%[[ACC2:.*]] = %[[FIRST]]) -> (i32) {
// CHECK: %[[REM:.*]] = affine.min #[[MAP1]](%[[UB]], %[[IV2]])[%[[STEP]]]
// CHECK: %[[CAST2:.*]] = arith.index_cast %[[REM]]
@@ -155,6 +157,26 @@ func.func @fully_dynamic_bounds(%lb : index, %ub: index, %step: index) -> i32 {
// -----
+// CHECK: func @fully_dynamic_bounds_unsigned(
+// CHECK-SAME: %[[LB:.*]]: i32, %[[UB:.*]]: i32, %[[STEP:.*]]: i32
+// CHECK: %[[C0:.*]] = arith.constant 0 : i32
+// CHECK: %[[SUM:.*]] = arith.addi %[[LB]], %[[STEP]] : i32
+// CHECK: %[[CAPPED:.*]] = arith.minui %[[SUM]], %[[UB]] : i32
+// CHECK: %[[SPLIT:.*]] = arith.maxui %[[LB]], %[[CAPPED]] : i32
+// CHECK: %[[FIRST:.*]] = scf.for unsigned %[[IV:.*]] = %[[LB]] to %[[SPLIT]]
+// CHECK: %[[RESULT:.*]] = scf.for unsigned %[[IV2:.*]] = %[[SPLIT]] to %[[UB]]
+// CHECK: return %[[RESULT]]
+func.func @fully_dynamic_bounds_unsigned(%lb : i32, %ub: i32, %step: i32) -> i32 {
+ %c0 = arith.constant 0 : i32
+ %r = scf.for unsigned %iv = %lb to %ub step %step iter_args(%arg = %c0) -> (i32) : i32 {
+ %0 = arith.addi %arg, %iv : i32
+ scf.yield %0 : i32
+ }
+ return %r : i32
+}
+
+// -----
+
// CHECK-DAG: #[[MAP:.*]] = affine_map<(d0, d1)[s0] -> (4, d0 - d1)>
// CHECK: func @two_iteration_example(
// CHECK-DAG: %[[C0_I32:.*]] = arith.constant 0 : i32
@@ -229,10 +251,11 @@ func.func @no_peeling_front() -> i32 {
// CHECK-SAME: %[[LB:[^:]*]]: i32,
// CHECK-SAME: %[[N:[^:]*]]: i32) -> i32 {
// CHECK: %[[C2:.*]] = arith.constant 2 : i32
-// CHECK: %[[RESULT:.*]] = scf.for %[[IV:.*]] = %[[LB]] to %[[N]] step %[[C2]] iter_args(%[[ACC:.*]] = %[[INIT]]) -> (i32) : i32 {
-// CHECK: %[[ADD:.*]] = arith.addi %[[ACC]], %[[IV]] : i32
-// CHECK: scf.yield %[[ADD]] : i32
-// CHECK: }
+// CHECK: %[[SUM:.*]] = arith.addi %[[LB]], %[[C2]] : i32
+// CHECK: %[[CAPPED:.*]] = arith.minsi %[[SUM]], %[[N]] : i32
+// CHECK: %[[SPLIT:.*]] = arith.maxsi %[[LB]], %[[CAPPED]] : i32
+// CHECK: %[[FIRST:.*]] = scf.for %[[IV:.*]] = %[[LB]] to %[[SPLIT]] step %[[C2]] iter_args(%[[ACC:.*]] = %[[INIT]]) -> (i32) : i32 {
+// CHECK: %[[RESULT:.*]] = scf.for %[[IV2:.*]] = %[[SPLIT]] to %[[N]] step %[[C2]] iter_args(%[[ACC2:.*]] = %[[FIRST]]) -> (i32) : i32 {
// CHECK: return %[[RESULT]] : i32
func.func @non_index_loop_bounds(%init: i32, %lb: i32, %n: i32) -> i32 {
%c2 = arith.constant 2 : i32
@@ -267,3 +290,79 @@ func.func @peel_front_step_near_ub(%out: memref<2xindex>) {
}
return
}
+
+// -----
+
+// dynamic ub >= 2^63 (negative as signed). Split must use minui, clamped by maxui.
+// CHECK-LABEL: func @dynamic_ub_high_bit
+// CHECK-SAME: %[[UB:.*]]: index
+// CHECK-DAG: %[[C:.*]] = arith.constant 4611686018427387905 : index
+// CHECK: %[[MIN:.*]] = arith.minui %[[UB]], %[[C]] : index
+// CHECK: %[[SPLIT:.*]] = arith.maxui %[[MIN]], %{{.*}} : index
+// CHECK: scf.for unsigned %{{.*}} = %{{.*}} to %[[SPLIT]] step
+// CHECK: scf.for unsigned %{{.*}} = %[[SPLIT]] to %[[UB]] step
+func.func @dynamic_ub_high_bit(%ub: index, %m: memref<4xindex>) {
+ %lb = arith.constant 1 : index
+ %step = arith.constant 4611686018427387904 : index
+ scf.for unsigned %i = %lb to %ub step %step {
+ %d = arith.divui %i, %step : index
+ memref.store %i, %m[%d] : memref<4xindex>
+ }
+ return
+}
+
+// -----
+
+// constant bounds, ub = 3*2^62 (negative as signed), 3 iterations.
+// A signed early-exit check would wrongly refuse.
+// CHECK-LABEL: func @const_high_bit_peels
+// CHECK: scf.for unsigned %{{.*}} = %{{[a-z0-9_]+}} to %[[SPLIT:c4611686018427387905[a-z0-9_]*]] step
+// CHECK: scf.for unsigned %{{.*}} = %[[SPLIT]] to %{{[-a-z0-9_]+}} step
+func.func @const_high_bit_peels(%m: memref<4xindex>) {
+ %lb = arith.constant 1 : index
+ %ub = arith.constant -4611686018427387904 : index
+ %step = arith.constant 4611686018427387904 : index
+ scf.for unsigned %i = %lb to %ub step %step {
+ %d = arith.divui %i, %step : index
+ memref.store %i, %m[%d] : memref<4xindex>
+ }
+ return
+}
+
+// -----
+
+// unsigned, empty loop (lb > ub) whose lb + step wraps to 90.
+// Without the clamp the main loop would start at 90. The split must stay at lb.
+// CHECK-LABEL: func @unsigned_lb_plus_step_wraps
+// CHECK-NOT: to %c90
+// CHECK-NOT: scf.for unsigned %{{.*}} = %c90
+// CHECK: scf.for unsigned %{{.*}} = %{{[-a-z0-9_]+}} to %[[SPLIT:[-a-z0-9_]+]] step
+// CHECK: scf.for unsigned %{{.*}} = %[[SPLIT]] to %{{.*}} step
+func.func @unsigned_lb_plus_step_wraps(%ub: i64, %m: memref<?xi64>) {
+ %c0 = arith.constant 0 : index
+ %lb = arith.constant -10 : i64
+ %step = arith.constant 100 : i64
+ scf.for unsigned %i = %lb to %ub step %step : i64 {
+ memref.store %i, %m[%c0] : memref<?xi64>
+ }
+ return
+}
+
+// -----
+
+// signed i64, empty loop (lb > ub) whose lb + step wraps to -3
+// The split must stay at lb.
+// CHECK-LABEL: func @signed_lb_plus_step_wraps
+// CHECK-NOT: to %c-3
+// CHECK-NOT: scf.for %{{.*}} = %c-3
+// CHECK: scf.for %{{.*}} = %{{[-a-z0-9_]+}} to %[[SPLIT:[-a-z0-9_]+]] step
+// CHECK: scf.for %{{.*}} = %[[SPLIT]] to %{{.*}} step
+func.func @signed_lb_plus_step_wraps(%ub: i64, %m: memref<?xi64>) {
+ %c0 = arith.constant 0 : index
+ %lb = arith.constant 9223372036854775807 : i64
+ %step = arith.constant 9223372036854775806 : i64
+ scf.for %i = %lb to %ub step %step : i64 {
+ memref.store %i, %m[%c0] : memref<?xi64>
+ }
+ return
+}
diff --git a/mlir/test/Dialect/SCF/transform-ops-invalid.mlir b/mlir/test/Dialect/SCF/transform-ops-invalid.mlir
index cd0f0eb8cb5554..996a7c462cea34 100644
--- a/mlir/test/Dialect/SCF/transform-ops-invalid.mlir
+++ b/mlir/test/Dialect/SCF/transform-ops-invalid.mlir
@@ -282,3 +282,71 @@ module attributes {transform.with_named_sequence} {
transform.yield
}
}
+
+// -----
+
+// signed constant bounds where ub - lb overflows int64. Must refuse.
+func.func @signed_sub_overflow(%m: memref<?xi64>) {
+ %c0 = arith.constant 0 : index
+ %lb = arith.constant 9223372036854775807 : i64
+ %ub = arith.constant -2 : i64
+ %step = arith.constant 9223372036854775806 : i64
+ scf.for %i = %lb to %ub step %step : i64 {
+ memref.store %i, %m[%c0] : memref<?xi64>
+ }
+ return
+}
+
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%root: !transform.any_op {transform.readonly}) {
+ %l = transform.structured.match ops{["scf.for"]} in %root : (!transform.any_op) -> !transform.op<"scf.for">
+ // expected-error @below {{failed to peel the first iteration}}
+ %p, %r = transform.loop.peel %l peel_front = true : (!transform.op<"scf.for">) -> (!transform.op<"scf.for">, !transform.op<"scf.for">)
+ transform.yield
+ }
+}
+
+// -----
+
+// constant bounds, exactly one iteration. Must refuse.
+func.func @const_one_iteration(%m: memref<4xindex>) {
+ %lb = arith.constant 1 : index
+ %ub = arith.constant 4611686018427387905 : index
+ %step = arith.constant 4611686018427387904 : index
+ scf.for unsigned %i = %lb to %ub step %step {
+ memref.store %i, %m[%lb] : memref<4xindex>
+ }
+ return
+}
+
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%root: !transform.any_op {transform.readonly}) {
+ %l = transform.structured.match ops{["scf.for"]} in %root : (!transform.any_op) -> !transform.op<"scf.for">
+ // expected-error @below {{failed to peel the first iteration}}
+ %p, %r = transform.loop.peel %l peel_front = true : (!transform.op<"scf.for">) -> (!transform.op<"scf.for">, !transform.op<"scf.for">)
+ transform.yield
+ }
+}
+
+// -----
+
+// constant empty loop that looks non-empty if read as signed.
+// lb = 2^63+5 (negative as signed), ub = 5. Unsigned: ub <= lb, so refuse.
+func.func @const_empty_unsigned(%m: memref<4xindex>) {
+ %lb = arith.constant -9223372036854775803 : index
+ %ub = arith.constant 5 : index
+ %step = arith.constant 1 : index
+ scf.for unsigned %i = %lb to %ub step %step {
+ memref.store %i, %m[%step] : memref<4xindex>
+ }
+ return
+}
+
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%root: !transform.any_op {transform.readonly}) {
+ %l = transform.structured.match ops{["scf.for"]} in %root : (!transform.any_op) -> !transform.op<"scf.for">
+ // expected-error @below {{failed to peel the first iteration}}
+ %p, %r = transform.loop.peel %l peel_front = true : (!transform.op<"scf.for">) -> (!transform.op<"scf.for">, !transform.op<"scf.for">)
+ transform.yield
+ }
+}
\ No newline at end of file
diff --git a/mlir/test/Dialect/SCF/transform-ops.mlir b/mlir/test/Dialect/SCF/transform-ops.mlir
index 954b8321bcee40..6c7b4b6c50165e 100644
--- a/mlir/test/Dialect/SCF/transform-ops.mlir
+++ b/mlir/test/Dialect/SCF/transform-ops.mlir
@@ -75,98 +75,6 @@ module attributes {transform.with_named_sequence} {
}
}
-// -----
-
-// dynamic ub >= 2^63 (negative as signed). Split must use minui.
-// CHECK-LABEL: func @dynamic_ub_high_bit
-// CHECK-SAME: %[[UB:.*]]: index
-// CHECK-DAG: %[[C:.*]] = arith.constant 4611686018427387905 : index
-// CHECK: %[[SPLIT:.*]] = arith.minui %[[UB]], %[[C]] : index
-// CHECK: scf.for unsigned %{{.*}} = %{{.*}} to %[[SPLIT]] step
-// CHECK: scf.for unsigned %{{.*}} = %[[SPLIT]] to %[[UB]] step
-func.func @dynamic_ub_high_bit(%ub: index, %m: memref<4xindex>) {
- %lb = arith.constant 1 : index
- %step = arith.constant 4611686018427387904 : index
- scf.for unsigned %i = %lb to %ub step %step {
- %d = arith.divui %i, %step : index
- memref.store %i, %m[%d] : memref<4xindex>
- }
- return
-}
-module attributes {transform.with_named_sequence} {
- transform.named_sequence @__transform_main(%root: !transform.any_op {transform.readonly}) {
- %l = transform.structured.match ops{["scf.for"]} in %root : (!transform.any_op) -> !transform.op<"scf.for">
- %p, %r = transform.loop.peel %l peel_front = true : (!transform.op<"scf.for">) -> (!transform.op<"scf.for">, !transform.op<"scf.for">)
- transform.yield
- }
-}
-
-// ----
-// constant bounds, ub = 3*2^62 (negative as signed), 3 iterations.
-// A signed early-exit check would wrongly refuse.
-// CHECK-LABEL: func @const_high_bit_peels
-// CHECK: scf.for unsigned %{{.*}} = %{{[a-z0-9_]+}} to %[[SPLIT:c4611686018427387905[a-z0-9_]*]] step
-// CHECK: scf.for unsigned %{{.*}} = %[[SPLIT]] to %{{[-a-z0-9_]+}} step
-func.func @const_high_bit_peels(%m: memref<4xindex>) {
- %lb = arith.constant 1 : index
- %ub = arith.constant -4611686018427387904 : index
- %step = arith.constant 4611686018427387904 : index
- scf.for unsigned %i = %lb to %ub step %step {
- %d = arith.divui %i, %step : index
- memref.store %i, %m[%d] : memref<4xindex>
- }
- return
-}
-module attributes {transform.with_named_sequence} {
- transform.named_sequence @__transform_main(%root: !transform.any_op {transform.readonly}) {
- %l = transform.structured.match ops{["scf.for"]} in %root : (!transform.any_op) -> !transform.op<"scf.for">
- %p, %r = transform.loop.peel %l peel_front = true : (!transform.op<"scf.for">) -> (!transform.op<"scf.for">, !transform.op<"scf.for">)
- transform.yield
- }
-}
-
-// -----
-
-// constant bounds, exactly one iteration. Must refuse.
-func.func @const_one_iteration(%m: memref<4xindex>) {
- %lb = arith.constant 1 : index
- %ub = arith.constant 4611686018427387905 : index
- %step = arith.constant 4611686018427387904 : index
- scf.for unsigned %i = %lb to %ub step %step {
- memref.store %i, %m[%lb] : memref<4xindex>
- }
- return
-}
-module attributes {transform.with_named_sequence} {
- transform.named_sequence @__transform_main(%root: !transform.any_op {transform.readonly}) {
- %l = transform.structured.match ops{["scf.for"]} in %root : (!transform.any_op) -> !transform.op<"scf.for">
- // expected-error @below {{failed to peel the first iteration}}
- %p, %r = transform.loop.peel %l peel_front = true : (!transform.op<"scf.for">) -> (!transform.op<"scf.for">, !transform.op<"scf.for">)
- transform.yield
- }
-}
-
-// -----
-
-// constant empty loop that looks non-empty if read as signed.
-// lb = 2^63+5 (negative as signed), ub = 5. Unsigned: ub <= lb, so refuse.
-func.func @const_empty_unsigned(%m: memref<4xindex>) {
- %lb = arith.constant -9223372036854775803 : index
- %ub = arith.constant 5 : index
- %step = arith.constant 1 : index
- scf.for unsigned %i = %lb to %ub step %step {
- memref.store %i, %m[%step] : memref<4xindex>
- }
- return
-}
-module attributes {transform.with_named_sequence} {
- transform.named_sequence @__transform_main(%root: !transform.any_op {transform.readonly}) {
- %l = transform.structured.match ops{["scf.for"]} in %root : (!transform.any_op) -> !transform.op<"scf.for">
- // expected-error @below {{failed to peel the first iteration}}
- %p, %r = transform.loop.peel %l peel_front = true : (!transform.op<"scf.for">) -> (!transform.op<"scf.for">, !transform.op<"scf.for">)
- transform.yield
- }
-}
// -----
@@ -176,10 +84,8 @@ func.func @loop_peel_first_iter_op() {
// CHECK: %[[C41:.+]] = arith.constant 41
// CHECK: %[[C5:.+]] = arith.constant 5
// CHECK: %[[C5_0:.+]] = arith.constant 5
- // CHECK: scf.for %{{.+}} = %[[C0]] to %[[C5_0]] step %[[C5]]
- // CHECK: arith.addi
- // CHECK: scf.for %{{.+}} = %[[C5_0]] to %[[C41]] step %[[C5]]
- // CHECK: arith.addi
+ // CHECK: scf.for %{{.+}} = %{{.+}} to %[[SPLIT:.+]] step %[[STEP:.+]]
+ // CHECK: scf.for %{{.+}} = %[[SPLIT]] to %{{.+}} step %[[STEP]]
%0 = arith.constant 0 : index
%1 = arith.constant 41 : index
%2 = arith.constant 5 : index
>From f94af6e4d8eddd3fe62a64c7f6abbd1574ed0109 Mon Sep 17 00:00:00 2001
From: Prashanth <ping at starone01.me>
Date: Tue, 20 Oct 2026 15:40:41 +0530
Subject: [PATCH 4/4] add missing newline at end of file in
transform-ops-invalid.mlir
---
mlir/test/Dialect/SCF/transform-ops-invalid.mlir | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/mlir/test/Dialect/SCF/transform-ops-invalid.mlir b/mlir/test/Dialect/SCF/transform-ops-invalid.mlir
index 996a7c462cea34..7f1beed5e6f9a9 100644
--- a/mlir/test/Dialect/SCF/transform-ops-invalid.mlir
+++ b/mlir/test/Dialect/SCF/transform-ops-invalid.mlir
@@ -349,4 +349,4 @@ module attributes {transform.with_named_sequence} {
%p, %r = transform.loop.peel %l peel_front = true : (!transform.op<"scf.for">) -> (!transform.op<"scf.for">, !transform.op<"scf.for">)
transform.yield
}
-}
\ No newline at end of file
+}
More information about the Mlir-commits
mailing list