[Mlir-commits] [mlir] [mlir][arith] Fold ceildivsi with MININT operands (PR #214637)

Hung Kuan Tseng llvmlistbot at llvm.org
Tue Aug 11 21:47:09 PDT 2026


https://github.com/Tim096 updated https://github.com/llvm/llvm-project/pull/214637

>From 60759dbd7cde5c2345cda150121dbd97d49498ef Mon Sep 17 00:00:00 2001
From: Hung-Kuan Tseng <tseng.tim096 at gmail.com>
Date: Fri, 7 Aug 2026 12:24:41 +0800
Subject: [PATCH 1/3] [mlir][arith] Add ceildivsi tests for a MININT divisor

CeilDivSIOp::fold negates a negative divisor, so it also gives up when the
divisor, rather than the dividend, is MININT. Only the dividend case is
covered today, and only the dividend case is mentioned in the TODO on the
folder.

Pre-commit the missing cases, plus MININT / -1, which must keep folding
back to the original op because its result is not representable.
---
 mlir/test/Transforms/constant-fold.mlir | 57 +++++++++++++++++++++++++
 1 file changed, 57 insertions(+)

diff --git a/mlir/test/Transforms/constant-fold.mlir b/mlir/test/Transforms/constant-fold.mlir
index 0b393bf0556b9..79382c06b32b7 100644
--- a/mlir/test/Transforms/constant-fold.mlir
+++ b/mlir/test/Transforms/constant-fold.mlir
@@ -516,6 +516,63 @@ func.func @simple_arith.ceildivsi_overflow() -> (i8, i16, i32) {
 
 // -----
 
+// The divisor, rather than the dividend, is MININT here. The folder negates a
+// negative divisor, which overflows for MININT, so neither of the first two
+// operations folds even though both results are representable.
+
+// TODO: The folder should be able to fold the following by avoiding
+// intermediate operations that overflow.
+
+// CHECK-LABEL: func @simple_arith.ceildivsi_minint_divisor
+//   CHECK-DAG: %[[MIN_I8:.*]] = arith.constant -128 : i8
+//   CHECK-DAG: %[[C_7:.*]] = arith.constant 7 : i8
+//   CHECK-DAG: %[[C_M9:.*]] = arith.constant -9 : i8
+//   CHECK-DAG: %[[C_1:.*]] = arith.constant 1 : i8
+//       CHECK: %[[DIV_1:.*]] = arith.ceildivsi %[[C_7]], %[[MIN_I8]] : i8
+//  CHECK-NEXT: %[[DIV_2:.*]] = arith.ceildivsi %[[C_M9]], %[[MIN_I8]] : i8
+//  CHECK-NEXT: return %[[DIV_1]], %[[DIV_2]], %[[C_1]]
+func.func @simple_arith.ceildivsi_minint_divisor() -> (i8, i8, i8) {
+  %min_int_i8 = arith.constant -128 : i8
+  %0 = arith.constant 7 : i8
+  %1 = arith.constant -9 : i8
+
+  // ceil(7 / -128) = 0
+  %2 = arith.ceildivsi %0, %min_int_i8 : i8
+  // ceil(-9 / -128) = 1
+  %3 = arith.ceildivsi %1, %min_int_i8 : i8
+  // ceil(-128 / -128) = 1, already folded by the ceildivsi(x, x) -> 1 pattern.
+  %4 = arith.ceildivsi %min_int_i8, %min_int_i8 : i8
+
+  return %2, %3, %4 : i8, i8, i8
+}
+
+// -----
+
+// ceil(MININT / -1) is -MININT, which is not representable. Unlike the cases
+// above, these must never fold.
+
+// CHECK-LABEL: func @simple_arith.ceildivsi_minint_div_minus_one
+//       CHECK: arith.ceildivsi
+//  CHECK-NEXT: arith.ceildivsi
+//  CHECK-NEXT: arith.ceildivsi
+func.func @simple_arith.ceildivsi_minint_div_minus_one() -> (i8, i16, i32) {
+  %min_int_i8 = arith.constant -128 : i8
+  %0 = arith.constant -1 : i8
+  %1 = arith.ceildivsi %min_int_i8, %0 : i8
+
+  %min_int_i16 = arith.constant -32768 : i16
+  %2 = arith.constant -1 : i16
+  %3 = arith.ceildivsi %min_int_i16, %2 : i16
+
+  %min_int_i32 = arith.constant -2147483648 : i32
+  %4 = arith.constant -1 : i32
+  %5 = arith.ceildivsi %min_int_i32, %4 : i32
+
+  return %1, %3, %5 : i8, i16, i32
+}
+
+// -----
+
 // CHECK-LABEL: func @simple_arith.ceildivui
 func.func @simple_arith.ceildivui() -> (i32, i32, i32, i32, i32) {
   // CHECK-DAG: [[C0:%.+]] = arith.constant 0

>From 3d1eca067c4a1168d8cd04014bd886f9c1176cdd Mon Sep 17 00:00:00 2001
From: Hung-Kuan Tseng <tseng.tim096 at gmail.com>
Date: Fri, 7 Aug 2026 12:53:46 +0800
Subject: [PATCH 2/3] [mlir][arith] Fold ceildivsi with MININT operands

CeilDivSIOp::fold computed the ceiling by negating the operands so that
the division ran on two non-negative values. Negating MININT overflows,
so it gave up on any operand that was MININT, even when the result was
perfectly representable: ceildivsi(-128, 7) : i8 is -18, but did not
fold. The folder's TODO mentions only a MININT dividend; a MININT
divisor was affected too, and accounted for more than half of the
missed cases.

Compute the ceiling without negating anything 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. This is the shape ExpandOps.cpp
already uses to expand the op.

The only case that now bails is MININT / -1, whose result is not
representable, plus division by zero as before. Exhaustive comparison
against an oracle over every i4 and i8 operand pair agrees on all of
them: for i8, 507 of the 65280 pairs did not fold before and 1 does not
fold now, with no pair folding to a different value in either version.

signedCeilNonnegInputs has no callers left and is removed.
---
 mlir/lib/Dialect/Arith/IR/ArithOps.cpp  | 74 +++++++------------------
 mlir/test/Transforms/constant-fold.mlir | 48 +++++-----------
 2 files changed, 36 insertions(+), 86 deletions(-)

diff --git a/mlir/lib/Dialect/Arith/IR/ArithOps.cpp b/mlir/lib/Dialect/Arith/IR/ArithOps.cpp
index fc50284fc8a94..5e4b465b511b1 100644
--- a/mlir/lib/Dialect/Arith/IR/ArithOps.cpp
+++ b/mlir/lib/Dialect/Arith/IR/ArithOps.cpp
@@ -912,18 +912,6 @@ Speculation::Speculatability arith::DivSIOp::getSpeculatability() {
   return getDivSISpeculatability(getRhs());
 }
 
-//===----------------------------------------------------------------------===//
-// Ceil and floor division folding helpers
-//===----------------------------------------------------------------------===//
-
-static APInt signedCeilNonnegInputs(const APInt &a, const APInt &b,
-                                    bool &overflow) {
-  // Returns (a-1)/b + 1
-  APInt one(a.getBitWidth(), 1, true); // Signed value 1.
-  APInt val = a.ssub_ov(one, overflow).sdiv_ov(b, overflow);
-  return val.sadd_ov(one, overflow);
-}
-
 //===----------------------------------------------------------------------===//
 // CeilDivUIOp
 //===----------------------------------------------------------------------===//
@@ -988,56 +976,36 @@ OpFoldResult arith::CeilDivSIOp::fold(FoldAdaptor adaptor) {
     return getIntegerAttrOfType(getType(), 1);
 
   // Don't fold if it would overflow or if it requires a division by zero.
-  // TODO: This hook won't fold operations where a = MININT, because
-  // negating MININT overflows. This can be improved.
   bool overflowOrDiv0 = false;
   auto result = constFoldBinaryOp<IntegerAttr>(
-      adaptor.getOperands(), [&](APInt a, const APInt &b) {
+      adaptor.getOperands(), [&](const APInt &a, const APInt &b) {
         if (overflowOrDiv0 || !b) {
           overflowOrDiv0 = true;
           return a;
         }
-        if (!a)
-          return a;
-        // After this point we know that neither a or b are zero.
-        unsigned bits = a.getBitWidth();
-        APInt zero = APInt::getZero(bits);
-        bool aGtZero = a.sgt(zero);
-        bool bGtZero = b.sgt(zero);
-        if (aGtZero && bGtZero) {
-          // Both positive, return ceil(a, b).
-          return signedCeilNonnegInputs(a, b, overflowOrDiv0);
-        }
-
-        // No folding happens if any of the intermediate arithmetic operations
-        // overflows.
-        bool overflowNegA = false;
-        bool overflowNegB = false;
+        // Compute the ceiling without negating either operand, so that MININT
+        // operands still fold whenever the result is representable.
+        //
+        // sdiv truncates towards zero, so it already rounds up whenever the
+        // exact quotient is negative. When the exact quotient is positive, i.e.
+        // when the operands have the same sign, an inexact division has to be
+        // corrected by one. This mirrors the expansion in ExpandOps.cpp.
         bool overflowDiv = false;
-        bool overflowNegRes = false;
-        if (!aGtZero && !bGtZero) {
-          // Both negative, return ceil(-a, -b).
-          APInt posA = zero.ssub_ov(a, overflowNegA);
-          APInt posB = zero.ssub_ov(b, overflowNegB);
-          APInt res = signedCeilNonnegInputs(posA, posB, overflowDiv);
-          overflowOrDiv0 = (overflowNegA || overflowNegB || overflowDiv);
-          return res;
-        }
-        if (!aGtZero && bGtZero) {
-          // A is negative, b is positive, return - ( -a / b).
-          APInt posA = zero.ssub_ov(a, overflowNegA);
-          APInt div = posA.sdiv_ov(b, overflowDiv);
-          APInt res = zero.ssub_ov(div, overflowNegRes);
-          overflowOrDiv0 = (overflowNegA || overflowDiv || overflowNegRes);
-          return res;
+        APInt quotient = a.sdiv_ov(b, overflowDiv);
+        if (overflowDiv) {
+          // MININT / -1. The exact result is -MININT, which is not
+          // representable.
+          overflowOrDiv0 = true;
+          return a;
         }
-        // A is positive, b is negative, return - (a / -b).
-        APInt posB = zero.ssub_ov(b, overflowNegB);
-        APInt div = a.sdiv_ov(posB, overflowDiv);
-        APInt res = zero.ssub_ov(div, overflowNegRes);
+        if (a.isNegative() != b.isNegative() || quotient * b == a)
+          return quotient;
 
-        overflowOrDiv0 = (overflowNegB || overflowDiv || overflowNegRes);
-        return res;
+        // The correction cannot overflow: it only applies when the exact
+        // quotient is positive and the division is inexact, which bounds the
+        // quotient well below the maximum. Check anyway, at no cost.
+        APInt one(a.getBitWidth(), 1, /*isSigned=*/true);
+        return quotient.sadd_ov(one, overflowOrDiv0);
       });
 
   return overflowOrDiv0 ? Attribute() : result;
diff --git a/mlir/test/Transforms/constant-fold.mlir b/mlir/test/Transforms/constant-fold.mlir
index 79382c06b32b7..f3ca18c56baab 100644
--- a/mlir/test/Transforms/constant-fold.mlir
+++ b/mlir/test/Transforms/constant-fold.mlir
@@ -478,35 +478,26 @@ func.func @simple_arith.ceildivsi() -> (i32, i32, i32, i32, i32) {
 
 // -----
 
-// CHECK-LABEL: func @simple_arith.ceildivsi_overflow
-func.func @simple_arith.ceildivsi_overflow() -> (i8, i16, i32) {
-  // The negative values below are MININTs for the corresponding bit-width. The
-  // folder will try to negate them (so that the division operates on two
-  // positive numbers), but that would cause overflow (negating MININT
-  // overflows). Hence folding should not happen and the original ceildivsi is
-  // preserved.
-
-  // TODO: The folder should be able to fold the following by avoiding
-  // intermediate operations that overflow.
-
-  // CHECK-DAG: %[[C_1:.*]] = arith.constant 7 : i8
-  // CHECK-DAG: %[[MIN_I8:.*]] = arith.constant -128 : i8
-  // CHECK-DAG: %[[C_2:.*]] = arith.constant 7 : i16
-  // CHECK-DAG: %[[MIN_I16:.*]] = arith.constant -32768 : i16
-  // CHECK-DAG: %[[C_3:.*]] = arith.constant 7 : i32
-  // CHECK-DAG: %[[MIN_I32:.*]] = arith.constant -2147483648 : i32
-
-  // CHECK-NEXT: %[[CEILDIV_1:.*]] = arith.ceildivsi %[[MIN_I8]], %[[C_1]]  : i8
+// The dividends below are MININTs for the corresponding bit-width. Every
+// result is representable, so all of them fold.
+
+// CHECK-LABEL: func @simple_arith.ceildivsi_minint_dividend
+//   CHECK-DAG: %[[CEILDIV_1:.*]] = arith.constant -18 : i8
+//   CHECK-DAG: %[[CEILDIV_2:.*]] = arith.constant -4681 : i16
+//   CHECK-DAG: %[[CEILDIV_3:.*]] = arith.constant -306783378 : i32
+//       CHECK: return %[[CEILDIV_1]], %[[CEILDIV_2]], %[[CEILDIV_3]]
+func.func @simple_arith.ceildivsi_minint_dividend() -> (i8, i16, i32) {
+  // ceil(-128 / 7) = -18
   %0 = arith.constant 7 : i8
   %min_int_i8 = arith.constant -128 : i8
   %2 = arith.ceildivsi %min_int_i8, %0 : i8
 
-  // CHECK-NEXT: %[[CEILDIV_2:.*]] = arith.ceildivsi %[[MIN_I16]], %[[C_2]]  : i16
+  // ceil(-32768 / 7) = -4681
   %3 = arith.constant 7 : i16
   %min_int_i16 = arith.constant -32768 : i16
   %5 = arith.ceildivsi %min_int_i16, %3 : i16
 
-  // CHECK-NEXT: %[[CEILDIV_2:.*]] = arith.ceildivsi %[[MIN_I32]], %[[C_3]]  : i32
+  // ceil(-2147483648 / 7) = -306783378
   %6 = arith.constant 7 : i32
   %min_int_i32 = arith.constant -2147483648 : i32
   %8 = arith.ceildivsi %min_int_i32, %6 : i32
@@ -516,21 +507,12 @@ func.func @simple_arith.ceildivsi_overflow() -> (i8, i16, i32) {
 
 // -----
 
-// The divisor, rather than the dividend, is MININT here. The folder negates a
-// negative divisor, which overflows for MININT, so neither of the first two
-// operations folds even though both results are representable.
-
-// TODO: The folder should be able to fold the following by avoiding
-// intermediate operations that overflow.
+// The divisor, rather than the dividend, is MININT here.
 
 // CHECK-LABEL: func @simple_arith.ceildivsi_minint_divisor
-//   CHECK-DAG: %[[MIN_I8:.*]] = arith.constant -128 : i8
-//   CHECK-DAG: %[[C_7:.*]] = arith.constant 7 : i8
-//   CHECK-DAG: %[[C_M9:.*]] = arith.constant -9 : i8
+//   CHECK-DAG: %[[C_0:.*]] = arith.constant 0 : i8
 //   CHECK-DAG: %[[C_1:.*]] = arith.constant 1 : i8
-//       CHECK: %[[DIV_1:.*]] = arith.ceildivsi %[[C_7]], %[[MIN_I8]] : i8
-//  CHECK-NEXT: %[[DIV_2:.*]] = arith.ceildivsi %[[C_M9]], %[[MIN_I8]] : i8
-//  CHECK-NEXT: return %[[DIV_1]], %[[DIV_2]], %[[C_1]]
+//       CHECK: return %[[C_0]], %[[C_1]], %[[C_1]]
 func.func @simple_arith.ceildivsi_minint_divisor() -> (i8, i8, i8) {
   %min_int_i8 = arith.constant -128 : i8
   %0 = arith.constant 7 : i8

>From b55df1b5551d0b57d8e7fea7d5cdd85782b006ee Mon Sep 17 00:00:00 2001
From: Hung-Kuan Tseng <tseng.tim096 at gmail.com>
Date: Wed, 12 Aug 2026 00:51:35 +0800
Subject: [PATCH 3/3] [mlir][arith] Add a ceildivsi vector fold test for the
 shared overflow flag

CeilDivSIOp::fold uses one overflow flag for the whole fold, so a single
element whose result is not representable has to discard the result for
every element. Nothing pinned that: the MININT / -1 tests are all
scalar. Add a vector that folds next to two that must not, differing
only in where the unrepresentable element sits, since the flag is
checked once after all the elements have been visited.
---
 mlir/test/Transforms/constant-fold.mlir | 30 +++++++++++++++++++++++++
 1 file changed, 30 insertions(+)

diff --git a/mlir/test/Transforms/constant-fold.mlir b/mlir/test/Transforms/constant-fold.mlir
index f3ca18c56baab..589a7cedb8a8f 100644
--- a/mlir/test/Transforms/constant-fold.mlir
+++ b/mlir/test/Transforms/constant-fold.mlir
@@ -555,6 +555,36 @@ func.func @simple_arith.ceildivsi_minint_div_minus_one() -> (i8, i16, i32) {
 
 // -----
 
+// One overflow flag is shared by every element of a vector fold, so a single
+// element whose result is not representable discards the whole fold, wherever
+// in the vector it sits.
+
+// CHECK-LABEL: func @simple_arith.ceildivsi_vector
+//   CHECK-DAG: %[[FOLDED:.*]] = arith.constant dense<[-18, 1, -1]> : vector<3xi8>
+//       CHECK: %[[LAST:.*]] = arith.ceildivsi
+//  CHECK-NEXT: %[[FIRST:.*]] = arith.ceildivsi
+//  CHECK-NEXT: return %[[FOLDED]], %[[LAST]], %[[FIRST]]
+func.func @simple_arith.ceildivsi_vector() -> (vector<3xi8>, vector<3xi8>, vector<3xi8>) {
+  // ceil(-128 / 7) = -18, ceil(-9 / -128) = 1, ceil(5 / -3) = -1
+  %0 = arith.constant dense<[-128, -9, 5]> : vector<3xi8>
+  %1 = arith.constant dense<[7, -128, -3]> : vector<3xi8>
+
+  // MININT / -1 as the last element, then as the first, so that the flag is
+  // set both after and before the representable elements are visited.
+  %2 = arith.constant dense<[-128, -9, -128]> : vector<3xi8>
+  %3 = arith.constant dense<[7, -128, -1]> : vector<3xi8>
+  %4 = arith.constant dense<[-128, -128, -9]> : vector<3xi8>
+  %5 = arith.constant dense<[-1, 7, -128]> : vector<3xi8>
+
+  %6 = arith.ceildivsi %0, %1 : vector<3xi8>
+  %7 = arith.ceildivsi %2, %3 : vector<3xi8>
+  %8 = arith.ceildivsi %4, %5 : vector<3xi8>
+
+  return %6, %7, %8 : vector<3xi8>, vector<3xi8>, vector<3xi8>
+}
+
+// -----
+
 // CHECK-LABEL: func @simple_arith.ceildivui
 func.func @simple_arith.ceildivui() -> (i32, i32, i32, i32, i32) {
   // CHECK-DAG: [[C0:%.+]] = arith.constant 0



More information about the Mlir-commits mailing list