[Mlir-commits] [mlir] [mlir][arith] Only fold truncf(sitofp(x)) to sitofp(x) when the first conversion is exact (PR #233536)

Stanislav Bardyuk llvmlistbot at llvm.org
Mon Oct 19 11:51:11 PDT 2026


https://github.com/kodlan created https://github.com/llvm/llvm-project/pull/233536

The canonicalization patterns that turn truncf(sitofp(x)) into sitofp(x) (and the uitofp variant) assume the intermediate conversion is exact. When the integer is wider than the intermediate float's mantissa, the original program rounds twice and the folded one rounds once, so the results can differ. For i64 -> f64 -> f32 with x = 2^60 + 2^36 + 1 the program prints 0x5D800000 before canonicalization and 0x5D800001 after.

Add a constraint that every value of the integer type converts to the intermediate type exactly: the magnitude bits (width minus one for sitofp, the full width for uitofp) must fit the mantissa, which is the rule InstCombine uses for fptrunc(sitofp x), and the value must also fit the exponent range so the tiny float types cannot overflow. Narrow cases like i32 -> f64 -> f32 keep folding; wide ones keep both conversions.

Fixes #233369

>From 3d2d38a57af10dfdaa10584b0bb384a6846616f6 Mon Sep 17 00:00:00 2001
From: Stanislav Bardyuk <sbardyuk at google.com>
Date: Mon, 19 Oct 2026 18:48:15 +0000
Subject: [PATCH] [mlir][arith] Only fold truncf(sitofp(x)) to sitofp(x) when
 the first conversion is exact

The patterns that turn truncf(sitofp(x)) and truncf(uitofp(x)) into a
single conversion assume the intermediate conversion is exact. When the
integer is wider than the intermediate float's mantissa the original
program rounds twice and the folded one once, so the results can differ
(i64 -> f64 -> f32 gives 0x5D800000 before and 0x5D800001 after
canonicalization for x = 2^60 + 2^36 + 1).

Require that every value of the integer type converts exactly: the
magnitude bits must fit the mantissa, as InstCombine checks for
fptrunc(sitofp x), and the exponent range so tiny float types cannot
overflow.

Fixes #233369
---
 .../Dialect/Arith/IR/ArithCanonicalization.td | 18 ++++-
 mlir/lib/Dialect/Arith/IR/ArithOps.cpp        | 13 +++
 mlir/test/Dialect/Arith/canonicalize.mlir     | 80 +++++++++++++++++++
 3 files changed, 107 insertions(+), 4 deletions(-)

diff --git a/mlir/lib/Dialect/Arith/IR/ArithCanonicalization.td b/mlir/lib/Dialect/Arith/IR/ArithCanonicalization.td
index ac6ccebc56b0a..7f400c5c9352b 100644
--- a/mlir/lib/Dialect/Arith/IR/ArithCanonicalization.td
+++ b/mlir/lib/Dialect/Arith/IR/ArithCanonicalization.td
@@ -521,17 +521,27 @@ def TruncIShrSIToTrunciShrUI :
 // TruncFOp
 //===----------------------------------------------------------------------===//
 
-// truncf(sitofp(x)) -> sitofp(x) if default rounding mode.
+// An inexact intermediate conversion rounds twice and may change the result.
+def IsExactSIToFP :
+    Constraint<CPred<"isExactIntToFPCast($0, $1, /*isSigned=*/true)">,
+               "exact sitofp">;
+def IsExactUIToFP :
+    Constraint<CPred<"isExactIntToFPCast($0, $1, /*isSigned=*/false)">,
+               "exact uitofp">;
+
+// truncf(sitofp(x)) -> sitofp(x) if default rounding mode and sitofp is exact.
 def TruncFSIToFPToSIToFP :
     Pat<(Arith_TruncFOp:$tr (Arith_SIToFPOp:$fp $x), $rmf, $fmf),
         (Arith_SIToFPOp $x),
-        [(Constraint<CPred<"$0 == nullptr">, "default rounding mode"> $rmf)]>;
+        [(Constraint<CPred<"$0 == nullptr">, "default rounding mode"> $rmf),
+         (IsExactSIToFP $x, $fp)]>;
 
-// truncf(uitofp(x)) -> uitofp(x) if default rounding mode.
+// truncf(uitofp(x)) -> uitofp(x) if default rounding mode and uitofp is exact.
 def TruncFUIToFPToUIToFP :
     Pat<(Arith_TruncFOp:$tr (Arith_UIToFPOp:$fp $x, $nneg), $rmf, $fmf),
         (Arith_UIToFPOp $x, $nneg),
-        [(Constraint<CPred<"$0 == nullptr">, "default rounding mode"> $rmf)]>;
+        [(Constraint<CPred<"$0 == nullptr">, "default rounding mode"> $rmf),
+         (IsExactUIToFP $x, $fp)]>;
 
 //===----------------------------------------------------------------------===//
 // SIToFPOp
diff --git a/mlir/lib/Dialect/Arith/IR/ArithOps.cpp b/mlir/lib/Dialect/Arith/IR/ArithOps.cpp
index 7ed50c6ab787c..e3b85eee622cf 100644
--- a/mlir/lib/Dialect/Arith/IR/ArithOps.cpp
+++ b/mlir/lib/Dialect/Arith/IR/ArithOps.cpp
@@ -196,6 +196,19 @@ static int64_t getScalarOrElementWidth(Value value) {
   return getScalarOrElementWidth(value.getType());
 }
 
+/// Returns true if every value of `intValue`'s integer type converts exactly.
+static bool isExactIntToFPCast(Value intValue, Value fpValue, bool isSigned) {
+  auto fpTy = dyn_cast<FloatType>(getElementTypeOrSelf(fpValue.getType()));
+  int64_t intWidth = getScalarOrElementWidth(intValue);
+  if (!fpTy || intWidth <= 0)
+    return false;
+  // The magnitude bits must fit the mantissa and the exponent range.
+  int64_t magnitudeBits = intWidth - (isSigned ? 1 : 0);
+  return magnitudeBits <= fpTy.getFPMantissaWidth() &&
+         intWidth - 1 <=
+             APFloat::semanticsMaxExponent(fpTy.getFloatSemantics());
+}
+
 static FailureOr<APInt> getIntOrSplatIntValue(Attribute attr) {
   APInt value;
   if (matchPattern(attr, m_ConstantInt(&value)))
diff --git a/mlir/test/Dialect/Arith/canonicalize.mlir b/mlir/test/Dialect/Arith/canonicalize.mlir
index 8ea3bc61d979a..328b43cb4636f 100644
--- a/mlir/test/Dialect/Arith/canonicalize.mlir
+++ b/mlir/test/Dialect/Arith/canonicalize.mlir
@@ -1505,6 +1505,86 @@ func.func @truncUitofp_nneg(%arg0: i32) -> f32 {
   return %trunc : f32
 }
 
+// i64 does not fit the f64 mantissa, so the two roundings must be kept.
+// CHECK-LABEL: @truncSitofpInexact
+//       CHECK:     %[[SITOFP:.*]] = arith.sitofp %[[ARG0:.*]] : i64 to f64
+//       CHECK:     %[[TRUNCF:.*]] = arith.truncf %[[SITOFP]] : f64 to f32
+//       CHECK:     return %[[TRUNCF]]
+func.func @truncSitofpInexact(%arg0: i64) -> f32 {
+  %sitofp = arith.sitofp %arg0 : i64 to f64
+  %trunc = arith.truncf %sitofp : f64 to f32
+  return %trunc : f32
+}
+
+// CHECK-LABEL: @truncUitofpInexact
+//       CHECK:     %[[UITOFP:.*]] = arith.uitofp %[[ARG0:.*]] : i64 to f64
+//       CHECK:     %[[TRUNCF:.*]] = arith.truncf %[[UITOFP]] : f64 to f32
+//       CHECK:     return %[[TRUNCF]]
+func.func @truncUitofpInexact(%arg0: i64) -> f32 {
+  %uitofp = arith.uitofp %arg0 : i64 to f64
+  %trunc = arith.truncf %uitofp : f64 to f32
+  return %trunc : f32
+}
+
+// A signed i54 has 53 magnitude bits, which exactly fit the f64 mantissa.
+// CHECK-LABEL: @truncSitofpExactBoundary
+//       CHECK:     %[[SITOFP:.*]] = arith.sitofp %[[ARG0:.*]] : i54 to f32
+//       CHECK-NOT: truncf
+//       CHECK:     return %[[SITOFP]]
+func.func @truncSitofpExactBoundary(%arg0: i54) -> f32 {
+  %sitofp = arith.sitofp %arg0 : i54 to f64
+  %trunc = arith.truncf %sitofp : f64 to f32
+  return %trunc : f32
+}
+
+// CHECK-LABEL: @truncSitofpInexactBoundary
+//       CHECK:     arith.sitofp %{{.*}} : i55 to f64
+//       CHECK:     arith.truncf
+func.func @truncSitofpInexactBoundary(%arg0: i55) -> f32 {
+  %sitofp = arith.sitofp %arg0 : i55 to f64
+  %trunc = arith.truncf %sitofp : f64 to f32
+  return %trunc : f32
+}
+
+// An unsigned i53 fits the f64 mantissa, an unsigned i54 does not.
+// CHECK-LABEL: @truncUitofpExactBoundary
+//       CHECK:     %[[UITOFP:.*]] = arith.uitofp %[[ARG0:.*]] : i53 to f32
+//       CHECK-NOT: truncf
+//       CHECK:     return %[[UITOFP]]
+func.func @truncUitofpExactBoundary(%arg0: i53) -> f32 {
+  %uitofp = arith.uitofp %arg0 : i53 to f64
+  %trunc = arith.truncf %uitofp : f64 to f32
+  return %trunc : f32
+}
+
+// CHECK-LABEL: @truncUitofpInexactBoundary
+//       CHECK:     arith.uitofp %{{.*}} : i54 to f64
+//       CHECK:     arith.truncf
+func.func @truncUitofpInexactBoundary(%arg0: i54) -> f32 {
+  %uitofp = arith.uitofp %arg0 : i54 to f64
+  %trunc = arith.truncf %uitofp : f64 to f32
+  return %trunc : f32
+}
+
+// CHECK-LABEL: @truncSitofpExactVector
+//       CHECK:     %[[SITOFP:.*]] = arith.sitofp %[[ARG0:.*]] : vector<2xi32> to vector<2xf32>
+//       CHECK-NOT: truncf
+//       CHECK:     return %[[SITOFP]]
+func.func @truncSitofpExactVector(%arg0: vector<2xi32>) -> vector<2xf32> {
+  %sitofp = arith.sitofp %arg0 : vector<2xi32> to vector<2xf64>
+  %trunc = arith.truncf %sitofp : vector<2xf64> to vector<2xf32>
+  return %trunc : vector<2xf32>
+}
+
+// CHECK-LABEL: @truncSitofpInexactVector
+//       CHECK:     arith.sitofp %{{.*}} : vector<2xi64> to vector<2xf64>
+//       CHECK:     arith.truncf
+func.func @truncSitofpInexactVector(%arg0: vector<2xi64>) -> vector<2xf32> {
+  %sitofp = arith.sitofp %arg0 : vector<2xi64> to vector<2xf64>
+  %trunc = arith.truncf %sitofp : vector<2xf64> to vector<2xf32>
+  return %trunc : vector<2xf32>
+}
+
 // CHECK-LABEL: @sitofpExtsi
 //       CHECK:     %[[SITOFP:.*]] = arith.sitofp %[[ARG0:.*]] : i8 to bf16
 //       CHECK:     return %[[SITOFP]]



More information about the Mlir-commits mailing list