[Mlir-commits] [mlir] [mlir] Compute ceildivs consistently for INT_MIN operands (PR #215696)
Hung Kuan Tseng
llvmlistbot at llvm.org
Fri Aug 14 06:05:39 PDT 2026
https://github.com/Tim096 updated https://github.com/llvm/llvm-project/pull/215696
>From e53103d1871a4044af0e1d483313a402d28a957b Mon Sep 17 00:00:00 2001
From: Hung-Kuan Tseng <tseng.tim096 at gmail.com>
Date: Wed, 12 Aug 2026 06:26:36 +0800
Subject: [PATCH 1/4] [mlir][index] Fold and lower ceildivs without negating an
operand
`index.ceildivs` computes the ceiling as `-(-n / m)` when the operands
have different signs, in `calculateCeilDivS` and in both lowerings,
`ConvertIndexCeilDivS` for LLVM and `ConvertIndexCeilDivSPattern` for
SPIR-V. Negating `INT_MIN` wraps, so all three compute a positive result
for `ceildivs(INT_MIN, [positive number])`.
The folder never returns that value, because `foldBinaryOpChecked`
requires the 32-bit and the 64-bit result to agree and they do not: for
`index.ceildivs(-2147483648, 7)` the 32-bit computation gives
`306783378` and the 64-bit one `-306783378`, so it does not fold at all.
The lowerings have no such check;
`-convert-index-to-llvm=index-bitwidth=32` and
`-convert-index-to-spirv=use-64bit-index=false` both emit the wrapped
computation, which evaluates to `306783378`.
Compute the ceiling without negating either operand instead. sdiv
truncates towards zero, which already rounds up whenever the exact
quotient is negative, so the only correction needed is +1 when the
operands share a sign and the division is inexact. The folder gets that
from `APIntOps::RoundingSDiv` with `Rounding::UP`; the lowerings emit
the same shape, which is what ExpandOps.cpp already uses to expand
`arith.ceildivsi`.
`ceildivs(INT_MIN, -1)` no longer folds. Its exact result, 2^31, is not
representable on 32-bit; the old computation passed the consistency
check there only because the correction wrapped back to `INT_MIN`, and
the lowering emits `sdiv INT_MIN, -1`, which is poison.
`arith.ceildivsi` does not fold this case either.
That case is rejected with the bit test `sdiv_ov` itself performs rather
than by calling `sdiv_ov` and discarding its quotient, because
`RoundingSDiv` already divides internally and the fold has no reason to
divide twice.
---
.../Conversion/IndexToLLVM/IndexToLLVM.cpp | 48 ++++++++-----------
.../Conversion/IndexToSPIRV/IndexToSPIRV.cpp | 43 +++++++----------
mlir/lib/Dialect/Index/IR/IndexOps.cpp | 23 +++------
.../Conversion/IndexToLLVM/index-to-llvm.mlir | 44 ++++++++++-------
.../IndexToSPIRV/index-to-spirv.mlir | 44 ++++++++++-------
.../Dialect/Index/index-canonicalize.mlir | 39 ++++++++++++---
6 files changed, 132 insertions(+), 109 deletions(-)
diff --git a/mlir/lib/Conversion/IndexToLLVM/IndexToLLVM.cpp b/mlir/lib/Conversion/IndexToLLVM/IndexToLLVM.cpp
index ba7d2b6fb83aa..00c9644a6fe4f 100644
--- a/mlir/lib/Conversion/IndexToLLVM/IndexToLLVM.cpp
+++ b/mlir/lib/Conversion/IndexToLLVM/IndexToLLVM.cpp
@@ -25,8 +25,8 @@ namespace {
// ConvertIndexCeilDivS
//===----------------------------------------------------------------------===//
-/// Convert `ceildivs(n, m)` into `x = m > 0 ? -1 : 1` and then
-/// `n*m > 0 ? (n+x)/m + 1 : -(-n/m)`.
+/// Convert `ceildivs(n, m)` into `z = n / m` and then
+/// `z*m != n && (n < 0) == (m < 0) ? z + 1 : z`.
struct ConvertIndexCeilDivS : mlir::ConvertOpToLLVMPattern<CeilDivSOp> {
using ConvertOpToLLVMPattern::ConvertOpToLLVMPattern;
@@ -38,33 +38,27 @@ struct ConvertIndexCeilDivS : mlir::ConvertOpToLLVMPattern<CeilDivSOp> {
Value m = adaptor.getRhs();
Value zero = LLVM::ConstantOp::create(rewriter, loc, n.getType(), 0);
Value posOne = LLVM::ConstantOp::create(rewriter, loc, n.getType(), 1);
- Value negOne = LLVM::ConstantOp::create(rewriter, loc, n.getType(), -1);
-
- // Compute `x`.
- Value mPos =
- LLVM::ICmpOp::create(rewriter, loc, LLVM::ICmpPredicate::sgt, m, zero);
- Value x = LLVM::SelectOp::create(rewriter, loc, mPos, negOne, posOne);
- // Compute the positive result.
- Value nPlusX = LLVM::AddOp::create(rewriter, loc, n, x);
- Value nPlusXDivM = LLVM::SDivOp::create(rewriter, loc, nPlusX, m);
- Value posRes = LLVM::AddOp::create(rewriter, loc, nPlusXDivM, posOne);
-
- // Compute the negative result.
- Value negN = LLVM::SubOp::create(rewriter, loc, zero, n);
- Value negNDivM = LLVM::SDivOp::create(rewriter, loc, negN, m);
- Value negRes = LLVM::SubOp::create(rewriter, loc, zero, negNDivM);
-
- // Pick the positive result if `n` and `m` have the same sign and `n` is
- // non-zero, i.e. `(n > 0) == (m > 0) && n != 0`.
- Value nPos =
- LLVM::ICmpOp::create(rewriter, loc, LLVM::ICmpPredicate::sgt, n, zero);
+ // Compute the truncated quotient. sdiv rounds towards zero, so it already
+ // rounds up whenever the exact quotient is negative.
+ Value quotient = LLVM::SDivOp::create(rewriter, loc, n, m);
+ Value quotientPlusOne =
+ LLVM::AddOp::create(rewriter, loc, quotient, posOne);
+
+ // Correct the quotient by one if the division is inexact and the exact
+ // quotient is positive, i.e. if `n` and `m` have the same sign.
+ Value product = LLVM::MulOp::create(rewriter, loc, quotient, m);
+ Value inexact = LLVM::ICmpOp::create(rewriter, loc, LLVM::ICmpPredicate::ne,
+ n, product);
+ Value nNeg =
+ LLVM::ICmpOp::create(rewriter, loc, LLVM::ICmpPredicate::slt, n, zero);
+ Value mNeg =
+ LLVM::ICmpOp::create(rewriter, loc, LLVM::ICmpPredicate::slt, m, zero);
Value sameSign = LLVM::ICmpOp::create(rewriter, loc,
- LLVM::ICmpPredicate::eq, nPos, mPos);
- Value nNonZero =
- LLVM::ICmpOp::create(rewriter, loc, LLVM::ICmpPredicate::ne, n, zero);
- Value cmp = LLVM::AndOp::create(rewriter, loc, sameSign, nNonZero);
- rewriter.replaceOpWithNewOp<LLVM::SelectOp>(op, cmp, posRes, negRes);
+ LLVM::ICmpPredicate::eq, nNeg, mNeg);
+ Value cmp = LLVM::AndOp::create(rewriter, loc, inexact, sameSign);
+ rewriter.replaceOpWithNewOp<LLVM::SelectOp>(op, cmp, quotientPlusOne,
+ quotient);
return success();
}
};
diff --git a/mlir/lib/Conversion/IndexToSPIRV/IndexToSPIRV.cpp b/mlir/lib/Conversion/IndexToSPIRV/IndexToSPIRV.cpp
index 92526da965157..99368d3cc184d 100644
--- a/mlir/lib/Conversion/IndexToSPIRV/IndexToSPIRV.cpp
+++ b/mlir/lib/Conversion/IndexToSPIRV/IndexToSPIRV.cpp
@@ -100,9 +100,9 @@ struct ConvertIndexConstantOpPattern final : OpConversionPattern<ConstantOp> {
// ConvertIndexCeilDivS
//===----------------------------------------------------------------------===//
-/// Convert `ceildivs(n, m)` into `x = m > 0 ? -1 : 1` and then
-/// `n*m > 0 ? (n+x)/m + 1 : -(-n/m)`. Formula taken from the equivalent
-/// conversion in IndexToLLVM.
+/// Convert `ceildivs(n, m)` into `z = n / m` and then
+/// `z*m != n && (n < 0) == (m < 0) ? z + 1 : z`. Formula taken from the
+/// equivalent conversion in IndexToLLVM.
struct ConvertIndexCeilDivSPattern final : OpConversionPattern<CeilDivSOp> {
using Base::Base;
@@ -119,30 +119,23 @@ struct ConvertIndexCeilDivSPattern final : OpConversionPattern<CeilDivSOp> {
IntegerAttr::get(nType, 0));
Value posOne = spirv::ConstantOp::create(rewriter, loc, nType,
IntegerAttr::get(nType, 1));
- Value negOne = spirv::ConstantOp::create(rewriter, loc, nType,
- IntegerAttr::get(nType, -1));
- // Compute `x`.
- Value mPos = spirv::SGreaterThanOp::create(rewriter, loc, m, zero);
- Value x = spirv::SelectOp::create(rewriter, loc, mPos, negOne, posOne);
+ // Compute the truncated quotient. Signed division rounds towards zero, so
+ // it already rounds up whenever the exact quotient is negative.
+ Value quotient = spirv::SDivOp::create(rewriter, loc, n, m);
+ Value quotientPlusOne =
+ spirv::IAddOp::create(rewriter, loc, quotient, posOne);
- // Compute the positive result.
- Value nPlusX = spirv::IAddOp::create(rewriter, loc, n, x);
- Value nPlusXDivM = spirv::SDivOp::create(rewriter, loc, nPlusX, m);
- Value posRes = spirv::IAddOp::create(rewriter, loc, nPlusXDivM, posOne);
-
- // Compute the negative result.
- Value negN = spirv::ISubOp::create(rewriter, loc, zero, n);
- Value negNDivM = spirv::SDivOp::create(rewriter, loc, negN, m);
- Value negRes = spirv::ISubOp::create(rewriter, loc, zero, negNDivM);
-
- // Pick the positive result if `n` and `m` have the same sign and `n` is
- // non-zero, i.e. `(n > 0) == (m > 0) && n != 0`.
- Value nPos = spirv::SGreaterThanOp::create(rewriter, loc, n, zero);
- Value sameSign = spirv::LogicalEqualOp::create(rewriter, loc, nPos, mPos);
- Value nNonZero = spirv::INotEqualOp::create(rewriter, loc, n, zero);
- Value cmp = spirv::LogicalAndOp::create(rewriter, loc, sameSign, nNonZero);
- rewriter.replaceOpWithNewOp<spirv::SelectOp>(op, cmp, posRes, negRes);
+ // Correct the quotient by one if the division is inexact and the exact
+ // quotient is positive, i.e. if `n` and `m` have the same sign.
+ Value product = spirv::IMulOp::create(rewriter, loc, quotient, m);
+ Value inexact = spirv::INotEqualOp::create(rewriter, loc, n, product);
+ Value nNeg = spirv::SLessThanOp::create(rewriter, loc, n, zero);
+ Value mNeg = spirv::SLessThanOp::create(rewriter, loc, m, zero);
+ Value sameSign = spirv::LogicalEqualOp::create(rewriter, loc, nNeg, mNeg);
+ Value cmp = spirv::LogicalAndOp::create(rewriter, loc, inexact, sameSign);
+ rewriter.replaceOpWithNewOp<spirv::SelectOp>(op, cmp, quotientPlusOne,
+ quotient);
return success();
}
};
diff --git a/mlir/lib/Dialect/Index/IR/IndexOps.cpp b/mlir/lib/Dialect/Index/IR/IndexOps.cpp
index 2b1baa8b44643..d522c970db62b 100644
--- a/mlir/lib/Dialect/Index/IR/IndexOps.cpp
+++ b/mlir/lib/Dialect/Index/IR/IndexOps.cpp
@@ -245,27 +245,18 @@ OpFoldResult DivUOp::fold(FoldAdaptor adaptor) {
// CeilDivSOp
//===----------------------------------------------------------------------===//
-/// Compute `ceildivs(n, m)` as `x = m > 0 ? -1 : 1` and then
-/// `n*m > 0 ? (n+x)/m + 1 : -(-n/m)`.
+/// Compute `ceildivs(n, m)` by rounding the quotient of `n / m` towards
+/// positive infinity.
static std::optional<APInt> calculateCeilDivS(const APInt &n, const APInt &m) {
// Don't fold division by zero.
if (m.isZero())
return std::nullopt;
- // Short-circuit the zero case.
- if (n.isZero())
- return n;
+ // Don't fold `INT_MIN / -1`, the one quotient that is not representable.
+ // Neither operand is negated, so every other `INT_MIN` dividend is fine.
+ if (n.isMinSignedValue() && m.isAllOnes())
+ return std::nullopt;
- bool mGtZ = m.sgt(0);
- if (n.sgt(0) != mGtZ) {
- // If the operands have different signs, compute the negative result. Signed
- // division overflow is not possible, since if `m == -1`, `n` can be at most
- // `INT_MAX`, and `-INT_MAX != INT_MIN` in two's complement.
- return -(-n).sdiv(m);
- }
- // Otherwise, compute the positive result. Signed division overflow is not
- // possible since if `m == -1`, `x` will be `1`.
- int64_t x = mGtZ ? -1 : 1;
- return (n + x).sdiv(m) + 1;
+ return llvm::APIntOps::RoundingSDiv(n, m, APInt::Rounding::UP);
}
OpFoldResult CeilDivSOp::fold(FoldAdaptor adaptor) {
diff --git a/mlir/test/Conversion/IndexToLLVM/index-to-llvm.mlir b/mlir/test/Conversion/IndexToLLVM/index-to-llvm.mlir
index 007929ed677fa..15797bfaa411d 100644
--- a/mlir/test/Conversion/IndexToLLVM/index-to-llvm.mlir
+++ b/mlir/test/Conversion/IndexToLLVM/index-to-llvm.mlir
@@ -54,25 +54,18 @@ func.func @ceildivs(%n: index, %m: index) -> index {
// CHECK-DAG: %[[N:.*]] = builtin.unrealized_conversion_cast %[[NI]]
// CHECK-DAG: %[[M:.*]] = builtin.unrealized_conversion_cast %[[MI]]
// CHECK: %[[ZERO:.*]] = llvm.mlir.constant(0 :
- // CHECK: %[[POS_ONE:.*]] = llvm.mlir.constant(1 :
- // CHECK: %[[NEG_ONE:.*]] = llvm.mlir.constant(-1 :
-
- // CHECK: %[[M_POS:.*]] = llvm.icmp "sgt" %[[M]], %[[ZERO]]
- // CHECK: %[[X:.*]] = llvm.select %[[M_POS]], %[[NEG_ONE]], %[[POS_ONE]]
-
- // CHECK: %[[N_PLUS_X:.*]] = llvm.add %[[N]], %[[X]]
- // CHECK: %[[N_PLUS_X_DIV_M:.*]] = llvm.sdiv %[[N_PLUS_X]], %[[M]]
- // CHECK: %[[POS_RES:.*]] = llvm.add %[[N_PLUS_X_DIV_M]], %[[POS_ONE]]
+ // CHECK: %[[ONE:.*]] = llvm.mlir.constant(1 :
- // CHECK: %[[NEG_N:.*]] = llvm.sub %[[ZERO]], %[[N]]
- // CHECK: %[[NEG_N_DIV_M:.*]] = llvm.sdiv %[[NEG_N]], %[[M]]
- // CHECK: %[[NEG_RES:.*]] = llvm.sub %[[ZERO]], %[[NEG_N_DIV_M]]
+ // CHECK: %[[QUOTIENT:.*]] = llvm.sdiv %[[N]], %[[M]]
+ // CHECK: %[[QUOTIENT_PLUS_ONE:.*]] = llvm.add %[[QUOTIENT]], %[[ONE]]
- // CHECK: %[[N_POS:.*]] = llvm.icmp "sgt" %[[N]], %[[ZERO]]
- // CHECK: %[[SAME_SIGN:.*]] = llvm.icmp "eq" %[[N_POS]], %[[M_POS]]
- // CHECK: %[[N_NON_ZERO:.*]] = llvm.icmp "ne" %[[N]], %[[ZERO]]
- // CHECK: %[[CMP:.*]] = llvm.and %[[SAME_SIGN]], %[[N_NON_ZERO]]
- // CHECK: %[[RESULT:.*]] = llvm.select %[[CMP]], %[[POS_RES]], %[[NEG_RES]]
+ // CHECK: %[[PRODUCT:.*]] = llvm.mul %[[QUOTIENT]], %[[M]]
+ // CHECK: %[[INEXACT:.*]] = llvm.icmp "ne" %[[N]], %[[PRODUCT]]
+ // CHECK: %[[N_NEG:.*]] = llvm.icmp "slt" %[[N]], %[[ZERO]]
+ // CHECK: %[[M_NEG:.*]] = llvm.icmp "slt" %[[M]], %[[ZERO]]
+ // CHECK: %[[SAME_SIGN:.*]] = llvm.icmp "eq" %[[N_NEG]], %[[M_NEG]]
+ // CHECK: %[[CMP:.*]] = llvm.and %[[INEXACT]], %[[SAME_SIGN]]
+ // CHECK: %[[RESULT:.*]] = llvm.select %[[CMP]], %[[QUOTIENT_PLUS_ONE]], %[[QUOTIENT]]
%result = index.ceildivs %n, %m
// CHECK: %[[RESULTI:.*]] = builtin.unrealized_conversion_cast %[[RESULT]]
@@ -80,6 +73,23 @@ func.func @ceildivs(%n: index, %m: index) -> index {
return %result : index
}
+// INDEX32-LABEL: @ceildivs_intmin_dividend
+// INDEX64-LABEL: @ceildivs_intmin_dividend
+func.func @ceildivs_intmin_dividend() -> index {
+ %n = index.constant -2147483648
+ %m = index.constant 7
+
+ // The conversion folds an op before it looks for a pattern, and the fold
+ // needs the 32-bit and the 64-bit result to agree, which they now do. They
+ // did not while the dividend was negated, so the sequence above ran instead
+ // and computed 306783378 on 32-bit.
+ // INDEX32: llvm.mlir.constant(-306783378 : i32) : i32
+ // INDEX64: llvm.mlir.constant(-306783378 : i64) : i64
+ %result = index.ceildivs %n, %m
+
+ return %result : index
+}
+
// CHECK-LABEL: @ceildivu
// CHECK-SAME: %[[NI:.*]]: index, %[[MI:.*]]: index
func.func @ceildivu(%n: index, %m: index) -> index {
diff --git a/mlir/test/Conversion/IndexToSPIRV/index-to-spirv.mlir b/mlir/test/Conversion/IndexToSPIRV/index-to-spirv.mlir
index 2d00adff2e8ba..d9b6508a6233c 100644
--- a/mlir/test/Conversion/IndexToSPIRV/index-to-spirv.mlir
+++ b/mlir/test/Conversion/IndexToSPIRV/index-to-spirv.mlir
@@ -71,25 +71,18 @@ func.func @ceildivs(%n: index, %m: index) -> index {
// CHECK-DAG: %[[N:.*]] = builtin.unrealized_conversion_cast %[[NI]]
// CHECK-DAG: %[[M:.*]] = builtin.unrealized_conversion_cast %[[MI]]
// CHECK: %[[ZERO:.*]] = spirv.Constant 0
- // CHECK: %[[POS_ONE:.*]] = spirv.Constant 1
- // CHECK: %[[NEG_ONE:.*]] = spirv.Constant -1
-
- // CHECK: %[[M_POS:.*]] = spirv.SGreaterThan %[[M]], %[[ZERO]]
- // CHECK: %[[X:.*]] = spirv.Select %[[M_POS]], %[[NEG_ONE]], %[[POS_ONE]]
-
- // CHECK: %[[N_PLUS_X:.*]] = spirv.IAdd %[[N]], %[[X]]
- // CHECK: %[[N_PLUS_X_DIV_M:.*]] = spirv.SDiv %[[N_PLUS_X]], %[[M]]
- // CHECK: %[[POS_RES:.*]] = spirv.IAdd %[[N_PLUS_X_DIV_M]], %[[POS_ONE]]
+ // CHECK: %[[ONE:.*]] = spirv.Constant 1
- // CHECK: %[[NEG_N:.*]] = spirv.ISub %[[ZERO]], %[[N]]
- // CHECK: %[[NEG_N_DIV_M:.*]] = spirv.SDiv %[[NEG_N]], %[[M]]
- // CHECK: %[[NEG_RES:.*]] = spirv.ISub %[[ZERO]], %[[NEG_N_DIV_M]]
+ // CHECK: %[[QUOTIENT:.*]] = spirv.SDiv %[[N]], %[[M]]
+ // CHECK: %[[QUOTIENT_PLUS_ONE:.*]] = spirv.IAdd %[[QUOTIENT]], %[[ONE]]
- // CHECK: %[[N_POS:.*]] = spirv.SGreaterThan %[[N]], %[[ZERO]]
- // CHECK: %[[SAME_SIGN:.*]] = spirv.LogicalEqual %[[N_POS]], %[[M_POS]]
- // CHECK: %[[N_NON_ZERO:.*]] = spirv.INotEqual %[[N]], %[[ZERO]]
- // CHECK: %[[CMP:.*]] = spirv.LogicalAnd %[[SAME_SIGN]], %[[N_NON_ZERO]]
- // CHECK: %[[RESULT:.*]] = spirv.Select %[[CMP]], %[[POS_RES]], %[[NEG_RES]]
+ // CHECK: %[[PRODUCT:.*]] = spirv.IMul %[[QUOTIENT]], %[[M]]
+ // CHECK: %[[INEXACT:.*]] = spirv.INotEqual %[[N]], %[[PRODUCT]]
+ // CHECK: %[[N_NEG:.*]] = spirv.SLessThan %[[N]], %[[ZERO]]
+ // CHECK: %[[M_NEG:.*]] = spirv.SLessThan %[[M]], %[[ZERO]]
+ // CHECK: %[[SAME_SIGN:.*]] = spirv.LogicalEqual %[[N_NEG]], %[[M_NEG]]
+ // CHECK: %[[CMP:.*]] = spirv.LogicalAnd %[[INEXACT]], %[[SAME_SIGN]]
+ // CHECK: %[[RESULT:.*]] = spirv.Select %[[CMP]], %[[QUOTIENT_PLUS_ONE]], %[[QUOTIENT]]
%result = index.ceildivs %n, %m
// %[[RESULTI:.*] = builtin.unrealized_conversion_cast %[[RESULT]]
@@ -97,6 +90,23 @@ func.func @ceildivs(%n: index, %m: index) -> index {
return %result : index
}
+// INDEX32-LABEL: @ceildivs_intmin_dividend
+// INDEX64-LABEL: @ceildivs_intmin_dividend
+func.func @ceildivs_intmin_dividend() -> index {
+ %n = index.constant -2147483648
+ %m = index.constant 7
+
+ // The conversion folds an op before it looks for a pattern, and the fold
+ // needs the 32-bit and the 64-bit result to agree, which they now do. They
+ // did not while the dividend was negated, so the sequence above ran instead
+ // and computed 306783378 on 32-bit.
+ // INDEX32: spirv.Constant -306783378 : i32
+ // INDEX64: spirv.Constant -306783378 : i64
+ %result = index.ceildivs %n, %m
+
+ return %result : index
+}
+
// CHECK-LABEL: @ceildivu
// CHECK-SAME: %[[NI:.*]]: index, %[[MI:.*]]: index
func.func @ceildivu(%n: index, %m: index) -> index {
diff --git a/mlir/test/Dialect/Index/index-canonicalize.mlir b/mlir/test/Dialect/Index/index-canonicalize.mlir
index 45da6ea57d796..25b74350ca70a 100644
--- a/mlir/test/Dialect/Index/index-canonicalize.mlir
+++ b/mlir/test/Dialect/Index/index-canonicalize.mlir
@@ -150,32 +150,57 @@ func.func @ceildivs() -> (index, index, index) {
}
// CHECK-LABEL: @ceildivs_neg
-func.func @ceildivs_neg() -> index {
+func.func @ceildivs_neg() -> (index, index) {
%c5 = index.constant -5
%c2 = index.constant 2
- // CHECK: %[[A:.*]] = index.constant -2
+ %cn2 = index.constant -2
+
+ // CHECK-DAG: %[[A:.*]] = index.constant -2
%0 = index.ceildivs %c5, %c2
- // CHECK: return %[[A]]
- return %0 : index
+
+ // Both operands are negative, so the exact quotient is positive and the
+ // truncated one has to be corrected.
+ // CHECK-DAG: %[[B:.*]] = index.constant 3
+ %1 = index.ceildivs %c5, %cn2
+
+ // CHECK: return %[[A]], %[[B]]
+ return %0, %1 : index, index
}
// CHECK-LABEL: @ceildivs_edge
func.func @ceildivs_edge() -> (index, index) {
+ // CHECK-DAG: %[[B:.*]] = index.constant -2147483647
+ // CHECK-DAG: %[[NEG_ONE:.*]] = index.constant -1
+ // CHECK-DAG: %[[INT_MIN:.*]] = index.constant -2147483648
%cn1 = index.constant -1
%cIntMin = index.constant -2147483648
%cIntMax = index.constant 2147483647
- // The result is 0 on 32-bit.
- // CHECK-DAG: %[[A:.*]] = index.constant 2147483648
+ // The result, 2147483648, is not representable on 32-bit, so this does not
+ // fold on either bitwidth.
+ // CHECK: %[[A:.*]] = index.ceildivs %[[INT_MIN]], %[[NEG_ONE]]
%0 = index.ceildivs %cIntMin, %cn1
- // CHECK-DAG: %[[B:.*]] = index.constant -2147483647
%1 = index.ceildivs %cIntMax, %cn1
// CHECK: return %[[A]], %[[B]]
return %0, %1 : index, index
}
+// CHECK-LABEL: @ceildivs_intmin_dividend
+func.func @ceildivs_intmin_dividend() -> index {
+ %c7 = index.constant 7
+ %cIntMin = index.constant -2147483648
+
+ // Negating the dividend wrapped on 32-bit and gave a positive result there,
+ // which disagreed with the 64-bit one, so this used not to fold at all.
+ // CHECK: %[[A:.*]] = index.constant -306783378
+ %0 = index.ceildivs %cIntMin, %c7
+
+ // CHECK: return %[[A]]
+ return %0 : index
+}
+
// CHECK-LABEL: @ceildivu
func.func @ceildivu() -> index {
%0 = index.constant 0x200000001
>From 34131c49288a7f4f55cccb0b177aa1559294f315 Mon Sep 17 00:00:00 2001
From: Hung-Kuan Tseng <tseng.tim096 at gmail.com>
Date: Thu, 13 Aug 2026 19:11:39 +0800
Subject: [PATCH 2/4] [mlir][affine] Expand ceildiv without negating the
dividend
`expandAffineExpr` lowers affine `ceildiv` as
`a <= 0 ? -(-a / b) : (a - 1) / b + 1`. Negating `INT_MIN` wraps, so for
`INT64_MIN ceildiv 7` the lowered arithmetic evaluates to
`1317624576693539401`, while both the constant folder
(`divideCeilSigned`) and range inference (`intrange::inferCeilDivS`)
give the exact quotient, `-1317624576693539401`. Canonicalizing or
running range optimizations before the lowering therefore changes the
result.
Affine `ceildiv` requires a strictly positive divisor -- the expansion
already relied on that, and rejects a non-positive constant one -- so
the ceiling needs no negation and no sign test:
a ceildiv b = let q = a / b in a > q * b ? q + 1 : q
`q * b` is `a` minus the remainder, so for a positive `b` the comparison
holds exactly when the division is inexact and the exact quotient is
positive, which is when truncation has to be corrected. This is the same
correction `index.ceildivs` uses, with the divisor's sign known.
`@lowered_affine_ceildiv` in Transforms/canonicalize.mlir is a copy of
what this lowering emits, so it changes with it.
---
mlir/lib/Dialect/Affine/Utils/Utils.cpp | 31 ++++++------
.../AffineToStandard/lower-affine.mlir | 49 ++++++++++++-------
mlir/test/Transforms/canonicalize.mlir | 32 +++++-------
3 files changed, 58 insertions(+), 54 deletions(-)
diff --git a/mlir/lib/Dialect/Affine/Utils/Utils.cpp b/mlir/lib/Dialect/Affine/Utils/Utils.cpp
index 7043083298615..60de6744b4e67 100644
--- a/mlir/lib/Dialect/Affine/Utils/Utils.cpp
+++ b/mlir/lib/Dialect/Affine/Utils/Utils.cpp
@@ -152,10 +152,15 @@ class AffineApplyExpander
/// single division operation as
///
/// a ceildiv b =
- /// let negative = a <= 0 in
- /// let absolute = negative ? -a : a - 1 in
- /// let quotient = absolute / b in
- /// negative ? -quotient : quotient + 1
+ /// let quotient = a / b in
+ /// a > quotient * b ? quotient + 1 : quotient
+ ///
+ /// Signed division rounds towards zero, so it already rounds up whenever the
+ /// exact quotient is negative. `quotient * b` is `a` minus the remainder, so
+ /// a positive divisor, which affine ceildiv requires, makes the comparison
+ /// hold exactly when the division is inexact and the exact quotient is
+ /// positive. Unlike the arith.ceildivsi expansion, no sign comparison is
+ /// needed.
///
/// Note: not using arith.ceildivsi for the same reason as explained in the
/// visitFloorDivExpr comment.
@@ -170,21 +175,15 @@ class AffineApplyExpander
auto rhs = visit(expr.getRHS());
assert(lhs && rhs && "unexpected affine expr lowering failure");
- Value zeroCst = arith::ConstantIndexOp::create(builder, loc, 0);
Value oneCst = arith::ConstantIndexOp::create(builder, loc, 1);
- Value nonPositive = arith::CmpIOp::create(
- builder, loc, arith::CmpIPredicate::sle, lhs, zeroCst);
- Value negated = arith::SubIOp::create(builder, loc, zeroCst, lhs);
- Value decremented = arith::SubIOp::create(builder, loc, lhs, oneCst);
- Value dividend = arith::SelectOp::create(builder, loc, nonPositive, negated,
- decremented);
- Value quotient = arith::DivSIOp::create(builder, loc, dividend, rhs);
- Value negatedQuotient =
- arith::SubIOp::create(builder, loc, zeroCst, quotient);
+ Value quotient = arith::DivSIOp::create(builder, loc, lhs, rhs);
+ Value product = arith::MulIOp::create(builder, loc, quotient, rhs);
+ Value roundUp = arith::CmpIOp::create(
+ builder, loc, arith::CmpIPredicate::sgt, lhs, product);
Value incrementedQuotient =
arith::AddIOp::create(builder, loc, quotient, oneCst);
- Value result = arith::SelectOp::create(
- builder, loc, nonPositive, negatedQuotient, incrementedQuotient);
+ Value result = arith::SelectOp::create(builder, loc, roundUp,
+ incrementedQuotient, quotient);
return result;
}
diff --git a/mlir/test/Conversion/AffineToStandard/lower-affine.mlir b/mlir/test/Conversion/AffineToStandard/lower-affine.mlir
index 943b8675d183c..e2b13368f09a9 100644
--- a/mlir/test/Conversion/AffineToStandard/lower-affine.mlir
+++ b/mlir/test/Conversion/AffineToStandard/lower-affine.mlir
@@ -1,4 +1,5 @@
// RUN: mlir-opt -lower-affine %s | FileCheck %s
+// RUN: mlir-opt -lower-affine -canonicalize %s | FileCheck %s --check-prefix=FOLDED
// CHECK-LABEL: func @empty() {
func.func @empty() {
@@ -559,36 +560,48 @@ func.func @affine_apply_floordiv_dynamic_divisor(%arg0 : index, %arg1 : index) -
// CHECK-LABEL: func @affine_apply_ceildiv
func.func @affine_apply_ceildiv(%arg0 : index) -> (index) {
// CHECK-NEXT: %[[c42:.*]] = arith.constant 42 : index
-// CHECK-NEXT: %[[c0:.*]] = arith.constant 0 : index
// CHECK-NEXT: %[[c1:.*]] = arith.constant 1 : index
-// CHECK-NEXT: %[[v0:.*]] = arith.cmpi sle, %{{.*}}, %[[c0]] : index
-// CHECK-NEXT: %[[v1:.*]] = arith.subi %[[c0]], %{{.*}} : index
-// CHECK-NEXT: %[[v2:.*]] = arith.subi %{{.*}}, %[[c1]] : index
-// CHECK-NEXT: %[[v3:.*]] = arith.select %[[v0]], %[[v1]], %[[v2]] : index
-// CHECK-NEXT: %[[v4:.*]] = arith.divsi %[[v3]], %[[c42]] : index
-// CHECK-NEXT: %[[v5:.*]] = arith.subi %[[c0]], %[[v4]] : index
-// CHECK-NEXT: %[[v6:.*]] = arith.addi %[[v4]], %[[c1]] : index
-// CHECK-NEXT: %[[v7:.*]] = arith.select %[[v0]], %[[v5]], %[[v6]] : index
+// CHECK-NEXT: %[[v0:.*]] = arith.divsi %{{.*}}, %[[c42]] : index
+// CHECK-NEXT: %[[v1:.*]] = arith.muli %[[v0]], %[[c42]] : index
+// CHECK-NEXT: %[[v2:.*]] = arith.cmpi sgt, %{{.*}}, %[[v1]] : index
+// CHECK-NEXT: %[[v3:.*]] = arith.addi %[[v0]], %[[c1]] : index
+// CHECK-NEXT: %[[v4:.*]] = arith.select %[[v2]], %[[v3]], %[[v0]] : index
%0 = affine.apply #map_ceildiv (%arg0)
return %0 : index
}
#map_ceildiv_dynamic_divisor = affine_map<(i)[s] -> (i ceildiv s)>
// CHECK-LABEL: func @affine_apply_ceildiv_dynamic_divisor
func.func @affine_apply_ceildiv_dynamic_divisor(%arg0 : index, %arg1 : index) -> (index) {
-// CHECK-NEXT: %[[c0:.*]] = arith.constant 0 : index
// CHECK-NEXT: %[[c1:.*]] = arith.constant 1 : index
-// CHECK-NEXT: %[[v0:.*]] = arith.cmpi sle, %{{.*}}, %[[c0]] : index
-// CHECK-NEXT: %[[v1:.*]] = arith.subi %[[c0]], %{{.*}} : index
-// CHECK-NEXT: %[[v2:.*]] = arith.subi %{{.*}}, %[[c1]] : index
-// CHECK-NEXT: %[[v3:.*]] = arith.select %[[v0]], %[[v1]], %[[v2]] : index
-// CHECK-NEXT: %[[v4:.*]] = arith.divsi %[[v3]], %arg1 : index
-// CHECK-NEXT: %[[v5:.*]] = arith.subi %[[c0]], %[[v4]] : index
-// CHECK-NEXT: %[[v6:.*]] = arith.addi %[[v4]], %[[c1]] : index
-// CHECK-NEXT: %[[v7:.*]] = arith.select %[[v0]], %[[v5]], %[[v6]] : index
+// CHECK-NEXT: %[[v0:.*]] = arith.divsi %{{.*}}, %arg1 : index
+// CHECK-NEXT: %[[v1:.*]] = arith.muli %[[v0]], %arg1 : index
+// CHECK-NEXT: %[[v2:.*]] = arith.cmpi sgt, %{{.*}}, %[[v1]] : index
+// CHECK-NEXT: %[[v3:.*]] = arith.addi %[[v0]], %[[c1]] : index
+// CHECK-NEXT: %[[v4:.*]] = arith.select %[[v2]], %[[v3]], %[[v0]] : index
%0 = affine.apply #map_ceildiv_dynamic_divisor (%arg0)[%arg1]
return %0 : index
}
+#map_ceildiv_seven = affine_map<(i) -> (i ceildiv 7)>
+// The lowered arithmetic has to agree with the constant folder
+// (`divideCeilSigned`) and with range inference (`intrange::inferCeilDivS`),
+// both of which give the exact, negative quotient for an INT_MIN dividend.
+// Negating the dividend, which the lowering used to do, made this positive
+// (`1317624576693539401`).
+//
+// The dividend is hidden behind an `arith.addi` so that the map is lowered
+// rather than folded; the affine folder never reaches the expansion.
+// FOLDED-LABEL: func @affine_apply_ceildiv_intmin_dividend
+func.func @affine_apply_ceildiv_intmin_dividend() -> (index) {
+ %c0 = arith.constant 0 : index
+ %cmin = arith.constant -9223372036854775808 : index
+ %dividend = arith.addi %cmin, %c0 : index
+// FOLDED: %[[c:.*]] = arith.constant -1317624576693539401 : index
+// FOLDED-NEXT: return %[[c]]
+ %0 = affine.apply #map_ceildiv_seven (%dividend)
+ return %0 : index
+}
+
// CHECK-LABEL: func @affine_load
func.func @affine_load(%arg0 : index) {
%0 = memref.alloc() : memref<10xf32>
diff --git a/mlir/test/Transforms/canonicalize.mlir b/mlir/test/Transforms/canonicalize.mlir
index 8e02c06a0a293..7e0352c87289c 100644
--- a/mlir/test/Transforms/canonicalize.mlir
+++ b/mlir/test/Transforms/canonicalize.mlir
@@ -655,32 +655,24 @@ func.func @lowered_affine_ceildiv() -> (index, index) {
// CHECK-DAG: %c-1 = arith.constant -1 : index
%c-43 = arith.constant -43 : index
%c42 = arith.constant 42 : index
- %c0 = arith.constant 0 : index
%c1 = arith.constant 1 : index
- %0 = arith.cmpi sle, %c-43, %c0 : index
- %1 = arith.subi %c0, %c-43 : index
- %2 = arith.subi %c-43, %c1 : index
- %3 = arith.select %0, %1, %2 : index
- %4 = arith.divsi %3, %c42 : index
- %5 = arith.subi %c0, %4 : index
- %6 = arith.addi %4, %c1 : index
- %7 = arith.select %0, %5, %6 : index
+ %0 = arith.divsi %c-43, %c42 : index
+ %1 = arith.muli %0, %c42 : index
+ %2 = arith.cmpi sgt, %c-43, %1 : index
+ %3 = arith.addi %0, %c1 : index
+ %4 = arith.select %2, %3, %0 : index
// CHECK-DAG: %c2 = arith.constant 2 : index
%c43 = arith.constant 43 : index
%c42_0 = arith.constant 42 : index
- %c0_1 = arith.constant 0 : index
- %c1_2 = arith.constant 1 : index
- %8 = arith.cmpi sle, %c43, %c0_1 : index
- %9 = arith.subi %c0_1, %c43 : index
- %10 = arith.subi %c43, %c1_2 : index
- %11 = arith.select %8, %9, %10 : index
- %12 = arith.divsi %11, %c42_0 : index
- %13 = arith.subi %c0_1, %12 : index
- %14 = arith.addi %12, %c1_2 : index
- %15 = arith.select %8, %13, %14 : index
+ %c1_0 = arith.constant 1 : index
+ %5 = arith.divsi %c43, %c42_0 : index
+ %6 = arith.muli %5, %c42_0 : index
+ %7 = arith.cmpi sgt, %c43, %6 : index
+ %8 = arith.addi %5, %c1_0 : index
+ %9 = arith.select %7, %8, %5 : index
// CHECK-NEXT: return %c-1, %c2
- return %7, %15 : index, index
+ return %4, %9 : index, index
}
// Checks that NOP casts are removed.
>From dbf7a5961bcae7b4cd89acac3e421ed03b8591d0 Mon Sep 17 00:00:00 2001
From: Hung-Kuan Tseng <tseng.tim096 at gmail.com>
Date: Wed, 12 Aug 2026 00:51:34 +0800
Subject: [PATCH 3/4] [mlir] Drop the ceildivsi INT_MIN workarounds in range
inference
`inferCeilDivS` negates the quotient of `MININT / [positive number]`,
which is not signed ceiling division. It was added so that the analysis
agreed with what the op actually computed: the expansion at the time was
`-(-a / b)`, and negating `MININT` is a noop, so that expression returned
a positive value (#116284, fixing #115293). A second workaround unions in
the range of `[MININT + 1, smax]` whenever the dividend range starts at
`MININT`, to cover the discontinuity the first one introduces (#121062).
#133774 replaced that expansion with the mathematical ceiling in April
2025, and the workarounds were left behind. With the previous commit,
nothing else in tree reports a positive value there; on main, for
`ceildivsi(INT64_MIN, 1189465982)`:
-arith-expand -canonicalize folds it to -7754212542
-int-range-optimizations proves it equal to 7754212542
Drop both workarounds. What is left has the same shape as
`inferFloorDivS`: `inferDivSRange` only evaluates at the range endpoints,
which bounds the result because ceildivsi is monotonic in each operand
once the divisor cannot change sign -- and `inferDivSRange` already gives
up unless it cannot. With the negation gone there is no discontinuity for
the second workaround to patch up, which the existing
`@ceil_divsi_full_range` test pins: it still does not fold.
---
.../Interfaces/Utils/InferIntRangeCommon.cpp | 19 +-------------
.../Dialect/Arith/int-range-interface.mlir | 25 ++++++++++++++++---
2 files changed, 23 insertions(+), 21 deletions(-)
diff --git a/mlir/lib/Interfaces/Utils/InferIntRangeCommon.cpp b/mlir/lib/Interfaces/Utils/InferIntRangeCommon.cpp
index c9f49fda726e7..f3a9db75bb406 100644
--- a/mlir/lib/Interfaces/Utils/InferIntRangeCommon.cpp
+++ b/mlir/lib/Interfaces/Utils/InferIntRangeCommon.cpp
@@ -376,26 +376,9 @@ mlir::intrange::inferCeilDivS(ArrayRef<ConstantIntRanges> argRanges) {
result.sadd_ov(APInt(result.getBitWidth(), 1), overflowed);
return overflowed ? std::optional<APInt>() : corrected;
}
- // Special case where the usual implementation of ceilDiv causes
- // INT_MIN / [positive number] to be positive. This doesn't match the
- // definition of signed ceiling division mathematically, but it prevents
- // inconsistent constant-folding results. This arises because (-int_min) is
- // still negative, so -(-int_min / b) is -(int_min / b), which is
- // positive See #115293.
- if (lhs.isMinSignedValue() && rhs.sgt(1)) {
- return -result;
- }
return result;
};
- ConstantIntRanges result = inferDivSRange(lhs, rhs, ceilDivSIFix);
- if (lhs.smin().isMinSignedValue() && lhs.smax().sgt(lhs.smin())) {
- // If lhs range includes INT_MIN and lhs is not a single value, we can
- // suddenly wrap to positive val, skipping entire negative range, add
- // [INT_MIN + 1, smax()] range to the result to handle this.
- auto newLhs = ConstantIntRanges::fromSigned(lhs.smin() + 1, lhs.smax());
- result = result.rangeUnion(inferDivSRange(newLhs, rhs, ceilDivSIFix));
- }
- return result;
+ return inferDivSRange(lhs, rhs, ceilDivSIFix);
}
ConstantIntRanges
diff --git a/mlir/test/Dialect/Arith/int-range-interface.mlir b/mlir/test/Dialect/Arith/int-range-interface.mlir
index dd8240299ef7e..cff3f5d3a632c 100644
--- a/mlir/test/Dialect/Arith/int-range-interface.mlir
+++ b/mlir/test/Dialect/Arith/int-range-interface.mlir
@@ -272,19 +272,38 @@ func.func @ceil_divsi_full_range(%6: index) -> index {
return %55 : index
}
-// CHECK-LABEL: func @ceil_divsi_intmin_bug_115293
+// ceildivsi(INT_MIN, x > 1) is negative, as the mathematical definition of
+// signed ceiling division requires. See #115293, where it was inferred to be
+// positive to match a folder that gave up on INT_MIN operands.
+// CHECK-LABEL: func @ceil_divsi_intmin
// CHECK: %[[ret:.*]] = arith.constant true
// CHECK: return %[[ret]]
-func.func @ceil_divsi_intmin_bug_115293() -> i1 {
+func.func @ceil_divsi_intmin() -> i1 {
%intMin_i64 = test.with_bounds { smin = -9223372036854775808 : si64, smax = -9223372036854775808 : si64, umin = 9223372036854775808 : ui64, umax = 9223372036854775808 : ui64 } : i64
%denom_i64 = test.with_bounds { smin = 1189465982 : si64, smax = 1189465982 : si64, umin = 1189465982 : ui64, umax = 1189465982 : ui64 } : i64
- %res_i64 = test.with_bounds { smin = 7754212542 : si64, smax = 7754212542 : si64, umin = 7754212542 : ui64, umax = 7754212542 : ui64 } : i64
+ %res_i64 = test.with_bounds { smin = -7754212542 : si64, smax = -7754212542 : si64, umin = 18446744065955339074 : ui64, umax = 18446744065955339074 : ui64 } : i64
%0 = arith.ceildivsi %intMin_i64, %denom_i64 : i64
%1 = arith.cmpi eq, %0, %res_i64 : i64
func.return %1 : i1
}
+// A dividend range that starts at INT_MIN needs no special case either:
+// ceildivsi is monotonic in its dividend, so the endpoints bound it. Here every
+// quotient is negative, which #115293 could not conclude.
+// CHECK-LABEL: func @ceil_divsi_intmin_dividend_range
+// CHECK: %[[ret:.*]] = arith.constant true
+// CHECK: return %[[ret]]
+func.func @ceil_divsi_intmin_dividend_range() -> i1 {
+ %c0_i64 = arith.constant 0 : i64
+ %c64_i64 = arith.constant 64 : i64
+ %num_i64 = test.with_bounds { smin = -9223372036854775808 : si64, smax = -9223372036854775553 : si64, umin = 9223372036854775808 : ui64, umax = 9223372036854776063 : ui64 } : i64
+
+ %0 = arith.ceildivsi %num_i64, %c64_i64 : i64
+ %1 = arith.cmpi slt, %0, %c0_i64 : i64
+ func.return %1 : i1
+}
+
// CHECK-LABEL: func @floor_divsi
// CHECK: %[[true:.*]] = arith.constant true
// CHECK: return %[[true]]
>From 358a393ac99cef258653e5e3d46fb57b29189c5e Mon Sep 17 00:00:00 2001
From: Hung-Kuan Tseng <tseng.tim096 at gmail.com>
Date: Wed, 12 Aug 2026 06:29:41 +0800
Subject: [PATCH 4/4] [mlir][index] Add a 32-bit regression for ceildivs range
inference
`index.ceildivs` infers its result range twice, once on the 64-bit
operands and once on their 32-bit truncations, and unions the two. A
dividend range that only starts at `INT_MIN` after that truncation
therefore reaches the workarounds that the previous commit removed, and
no `arith` test covers it.
---
.../Dialect/Index/int-range-inference.mlir | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/mlir/test/Dialect/Index/int-range-inference.mlir b/mlir/test/Dialect/Index/int-range-inference.mlir
index 951624d573a64..74c4b93a6e217 100644
--- a/mlir/test/Dialect/Index/int-range-inference.mlir
+++ b/mlir/test/Dialect/Index/int-range-inference.mlir
@@ -64,3 +64,21 @@ func.func @add_big(%arg0 : index) -> i1 {
%3 = index.cmp uge(%1, %cmin)
func.return %3 : i1
}
+
+// The dividend range starts at INT_MIN once truncated to 32 bits. The 32-bit
+// inference used to negate the quotient there, which put a positive value in
+// the result range and left the comparison unresolved.
+// CHECK-LABEL: func @ceildivs_intmin_dividend
+// CHECK: %[[true:.*]] = index.bool.constant true
+// CHECK: return %[[true]]
+func.func @ceildivs_intmin_dividend(%arg0 : index) -> i1 {
+ %c0 = index.constant 0
+ %c7 = index.constant 7
+ %cmin = index.constant -2147483648
+ %cmax = index.constant -2147483393
+ %0 = index.maxs %arg0, %cmin
+ %1 = index.mins %0, %cmax
+ %2 = index.ceildivs %1, %c7
+ %3 = index.cmp slt(%2, %c0)
+ func.return %3 : i1
+}
More information about the Mlir-commits
mailing list