[Mlir-commits] [mlir] [mlir] [arith] Fold trunc(extremum(ext(lhs), ext(rhs))) (PR #214658)

Chuanqi Xu llvmlistbot at llvm.org
Tue Aug 11 02:26:12 PDT 2026


https://github.com/ChuanqiXu9 updated https://github.com/llvm/llvm-project/pull/214658

>From abc8469e3fee5325dfb5d9e81625881274b417b7 Mon Sep 17 00:00:00 2001
From: Chuanqi Xu <chuanqi.xcq at alibaba-inc.com>
Date: Fri, 7 Aug 2026 15:54:51 +0800
Subject: [PATCH] [mlir] [arith] Fold trunc(extremum(ext(lhs), ext(rhs)))

For example, now we can't fold the following case:

```
%lhs = arith.extf %arg0 : f16 to f32
%rhs = arith.extf %arg1 : f16 to f32
%maximum = arith.maximumf %lhs, %rhs fastmath<nnan> : f32
%maximumTrunc = arith.truncf %maximum : f32 to f16
```

to

```
%maximum = arith.maximumf %arg0, %arg1 fastmath<nnan> : f16
```

And this patch made this optimization. We can extend the op list to
arith.select, arith.cmpf and so on in the future.

I was told this may be intentional as some old hardware doesn't have
instructions to deal with maximum with fp16. But I think in the higher
level we should do such optimizations. And specific hardware can
make their own transformations according to their specific details.

AI assisted.
---
 mlir/lib/Dialect/Arith/IR/ArithOps.cpp    | 166 +++++++++++++++++-
 mlir/test/Dialect/Arith/canonicalize.mlir | 203 +++++++++++++++++++++-
 2 files changed, 354 insertions(+), 15 deletions(-)

diff --git a/mlir/lib/Dialect/Arith/IR/ArithOps.cpp b/mlir/lib/Dialect/Arith/IR/ArithOps.cpp
index ff6a5d4a0c29a..b3cc5b5706d3c 100644
--- a/mlir/lib/Dialect/Arith/IR/ArithOps.cpp
+++ b/mlir/lib/Dialect/Arith/IR/ArithOps.cpp
@@ -9,6 +9,7 @@
 #include <cassert>
 #include <cstdint>
 #include <functional>
+#include <type_traits>
 #include <utility>
 
 #include "mlir/Dialect/Arith/IR/Arith.h"
@@ -1364,6 +1365,149 @@ void arith::SubFOp::getCanonicalizationPatterns(RewritePatternSet &patterns,
   patterns.add<SubFOfNegZero>(context);
 }
 
+namespace {
+
+// A helper for the conversion:
+//
+//    trunc(extremum(ext(lhs), ext(rhs))) -> extremum(lhs, rhs)
+//
+// To make sure that type conversion won't lose information.
+//
+// For floating point conversion, we can't allow conversion between
+// -0 and +0. More importantly, we can't allow a conversion from
+// sNaN to qNaN. That changes semantics.
+static bool isLosslesslyConvertibleTo(const llvm::fltSemantics &from,
+                                      const llvm::fltSemantics &to) {
+  if (&from == &to)
+    return true;
+
+  if (!llvm::APFloatBase::isRepresentableBy(from, to))
+    return false;
+
+  if ((from.hasZero && !to.hasZero) ||
+      (from.hasSignedRepr && !to.hasSignedRepr))
+    return false;
+
+  // NegativeZero NaN encoding repurposes the negative-zero bit pattern, so a
+  // conversion to such a format cannot preserve a source negative zero.
+  bool fromHasSignedZero =
+      from.hasZero && from.hasSignedRepr &&
+      from.nanEncoding != llvm::fltNanEncoding::NegativeZero;
+  bool toHasSignedZero = to.hasZero && to.hasSignedRepr &&
+                         to.nanEncoding != llvm::fltNanEncoding::NegativeZero;
+  if (fromHasSignedZero && !toHasSignedZero)
+    return false;
+
+  // isRepresentableBy compares the normalized exponent ranges. Also ensure
+  // that the smallest source value, which may be denormal, is represented
+  // exactly by the destination semantics.
+  llvm::APFloat smallestFrom = llvm::APFloat::getSmallest(from);
+  bool losesInfo = false;
+  (void)smallestFrom.convert(to, llvm::APFloat::rmNearestTiesToEven,
+                             &losesInfo);
+  if (losesInfo)
+    return false;
+
+  if (from.nonFiniteBehavior == llvm::fltNonfiniteBehavior::FiniteOnly)
+    return true;
+
+  if (from.nonFiniteBehavior == llvm::fltNonfiniteBehavior::IEEE754) {
+    // Converting an IEEE signaling NaN to another semantics quiets it, so the
+    // original value cannot be recovered by converting it back.
+    return false;
+  }
+
+  // NanOnly formats have no signaling NaNs. IEEE semantics can represent
+  // their quiet NaNs; conversions between NanOnly formats are conservatively
+  // accepted only when they use the same NaN encoding.
+  if (to.nonFiniteBehavior == llvm::fltNonfiniteBehavior::IEEE754)
+    return true;
+  return to.nonFiniteBehavior == llvm::fltNonfiniteBehavior::NanOnly &&
+         from.nanEncoding == to.nanEncoding;
+}
+
+static bool
+isLosslesslyConvertibleToIgnoringNaNs(const llvm::fltSemantics &from,
+                                      const llvm::fltSemantics &to) {
+  llvm::fltSemantics finiteFrom = from;
+  llvm::fltSemantics finiteTo = to;
+  finiteFrom.nonFiniteBehavior = llvm::fltNonfiniteBehavior::FiniteOnly;
+  finiteTo.nonFiniteBehavior = llvm::fltNonfiniteBehavior::FiniteOnly;
+  return isLosslesslyConvertibleTo(finiteFrom, finiteTo);
+}
+
+/// Narrow an extremum whose operands were extended from the result type:
+///
+///   trunc(extremum(ext(lhs), ext(rhs))) -> extremum(lhs, rhs)
+///
+/// The concrete extension is part of the pattern so each extremum is only
+/// registered with extensions that preserve its ordering.
+/// For floating-point types, also require the extension to represent every
+/// source value exactly.
+template <typename TruncOp, typename ExtOp, typename ExtremumOp>
+struct NarrowExtremum final : OpRewritePattern<TruncOp> {
+  using OpRewritePattern<TruncOp>::OpRewritePattern;
+
+  LogicalResult matchAndRewrite(TruncOp truncOp,
+                                PatternRewriter &rewriter) const override {
+    auto extremumOp = truncOp.getIn().template getDefiningOp<ExtremumOp>();
+    if (!extremumOp || !extremumOp->hasOneUse())
+      return failure();
+
+    auto lhsExt = extremumOp.getLhs().template getDefiningOp<ExtOp>();
+    auto rhsExt = extremumOp.getRhs().template getDefiningOp<ExtOp>();
+    if (!lhsExt || !rhsExt)
+      return failure();
+
+    Value lhs = lhsExt.getIn();
+    Value rhs = rhsExt.getIn();
+    Type narrowType = truncOp.getType();
+    if (lhs.getType() != narrowType || rhs.getType() != narrowType)
+      return failure();
+
+    // A floating-point extension is not necessarily lossless between arbitrary
+    // floating-point semantics, even when the destination has a larger bit
+    // width. In particular, it may lose the sign of zero or quiet a signaling
+    // NaN, either of which can change an extremum's result. `nnan` lets us
+    // disregard the latter, but all other source values must round-trip.
+    //
+    // See the discussion in https://github.com/llvm/llvm-project/pull/214658.
+    if (auto narrowFloatType =
+            dyn_cast<FloatType>(getElementTypeOrSelf(narrowType))) {
+      auto wideFloatType =
+          dyn_cast<FloatType>(getElementTypeOrSelf(extremumOp.getType()));
+      if (!wideFloatType)
+        return failure();
+
+      const llvm::fltSemantics &narrowSemantics =
+          narrowFloatType.getFloatSemantics();
+      const llvm::fltSemantics &wideSemantics =
+          wideFloatType.getFloatSemantics();
+      if (!isLosslesslyConvertibleTo(narrowSemantics, wideSemantics)) {
+        if constexpr (std::is_same_v<TruncOp, TruncFOp>) {
+          // TODO: We can tolerant qNaN here. But we don't have a mode
+          // to exclude sNaN. If we're able to do that, we can be more
+          // tolerant here.
+          if (!bitEnumContainsAll(extremumOp.getFastmath(),
+                                  FastMathFlags::nnan) ||
+              !isLosslesslyConvertibleToIgnoringNaNs(narrowSemantics,
+                                                     wideSemantics))
+            return failure();
+        } else {
+          return failure();
+        }
+      }
+    }
+
+    rewriter.replaceOpWithNewOp<ExtremumOp>(truncOp, TypeRange{narrowType},
+                                            ValueRange{lhs, rhs},
+                                            extremumOp->getAttrs());
+    return success();
+  }
+};
+
+} // namespace
+
 //===----------------------------------------------------------------------===//
 // MaximumFOp
 //===----------------------------------------------------------------------===//
@@ -1876,9 +2020,11 @@ bool arith::TruncIOp::areCastCompatible(TypeRange inputs, TypeRange outputs) {
 
 void arith::TruncIOp::getCanonicalizationPatterns(RewritePatternSet &patterns,
                                                   MLIRContext *context) {
-  patterns
-      .add<TruncIExtSIToExtSI, TruncIExtUIToExtUI, TruncIShrSIToTrunciShrUI>(
-          context);
+  patterns.add<NarrowExtremum<TruncIOp, ExtSIOp, MaxSIOp>,
+               NarrowExtremum<TruncIOp, ExtSIOp, MinSIOp>,
+               NarrowExtremum<TruncIOp, ExtUIOp, MaxUIOp>,
+               NarrowExtremum<TruncIOp, ExtUIOp, MinUIOp>, TruncIExtSIToExtSI,
+               TruncIExtUIToExtUI, TruncIShrSIToTrunciShrUI>(context);
 }
 
 LogicalResult arith::TruncIOp::verify() {
@@ -1898,10 +2044,10 @@ OpFoldResult arith::TruncFOp::fold(FoldAdaptor adaptor) {
     auto srcType = cast<FloatType>(getElementTypeOrSelf(src.getType()));
     auto intermediateType =
         cast<FloatType>(getElementTypeOrSelf(extOp.getType()));
-    // Check if the srcType is representable in the intermediateType.
-    if (llvm::APFloatBase::isRepresentableBy(
-            srcType.getFloatSemantics(),
-            intermediateType.getFloatSemantics())) {
+    // Check whether every source value round-trips through the intermediate
+    // type, including signaling NaNs and signed zero.
+    if (isLosslesslyConvertibleTo(srcType.getFloatSemantics(),
+                                  intermediateType.getFloatSemantics())) {
       // truncf(extf(a)) -> truncf(a)
       if (srcType.getWidth() > resElemType.getWidth()) {
         setOperand(src);
@@ -1932,7 +2078,11 @@ OpFoldResult arith::TruncFOp::fold(FoldAdaptor adaptor) {
 
 void arith::TruncFOp::getCanonicalizationPatterns(RewritePatternSet &patterns,
                                                   MLIRContext *context) {
-  patterns.add<TruncFSIToFPToSIToFP, TruncFUIToFPToUIToFP>(context);
+  patterns.add<NarrowExtremum<TruncFOp, ExtFOp, MaximumFOp>,
+               NarrowExtremum<TruncFOp, ExtFOp, MaxNumFOp>,
+               NarrowExtremum<TruncFOp, ExtFOp, MinimumFOp>,
+               NarrowExtremum<TruncFOp, ExtFOp, MinNumFOp>,
+               TruncFSIToFPToSIToFP, TruncFUIToFPToUIToFP>(context);
 }
 
 bool arith::TruncFOp::areCastCompatible(TypeRange inputs, TypeRange outputs) {
diff --git a/mlir/test/Dialect/Arith/canonicalize.mlir b/mlir/test/Dialect/Arith/canonicalize.mlir
index 0c06aa6e861a4..75352dadf04de 100644
--- a/mlir/test/Dialect/Arith/canonicalize.mlir
+++ b/mlir/test/Dialect/Arith/canonicalize.mlir
@@ -1121,8 +1121,9 @@ func.func @extFPVectorConstant() -> vector<2xf128> {
 }
 
 // CHECK-LABEL: @truncExtf
-//       CHECK-NOT:  truncf
-//       CHECK:   return  %arg0
+//       CHECK:  %[[EXT:.*]] = arith.extf %arg0 : f32 to f64
+//       CHECK:  %[[TRUNC:.*]] = arith.truncf %[[EXT]] : f64 to f32
+//       CHECK:  return %[[TRUNC]]
 func.func @truncExtf(%arg0: f32) -> f32 {
   %extf = arith.extf %arg0 : f32 to f64
   %trunc = arith.truncf %extf : f64 to f32
@@ -1130,8 +1131,9 @@ func.func @truncExtf(%arg0: f32) -> f32 {
 }
 
 // CHECK-LABEL: @truncExtf1
-//       CHECK-NOT:  truncf
-//       CHECK:   return  %arg0
+//       CHECK:  %[[EXT:.*]] = arith.extf %arg0 : bf16 to f32
+//       CHECK:  %[[TRUNC:.*]] = arith.truncf %[[EXT]] : f32 to bf16
+//       CHECK:  return %[[TRUNC]]
 func.func @truncExtf1(%arg0: bf16) -> bf16 {
   %extf = arith.extf %arg0 : bf16 to f32
   %trunc = arith.truncf %extf : f32 to bf16
@@ -1150,15 +1152,202 @@ func.func @truncExtf2(%arg0: bf16) -> f16 {
 }
 
 // CHECK-LABEL: @truncExtf3
-//       CHECK:  %[[ARG0:.+]]: f32
-//       CHECK:  %[[CST:.*]] = arith.truncf %[[ARG0:.+]] : f32 to f16
-//       CHECK:   return  %[[CST:.*]]
+//       CHECK:  %[[EXT:.*]] = arith.extf %arg0 : f32 to f64
+//       CHECK:  %[[TRUNC:.*]] = arith.truncf %[[EXT]] : f64 to f16
+//       CHECK:  return %[[TRUNC]]
 func.func @truncExtf3(%arg0: f32) -> f16 {
   %extf = arith.extf %arg0 : f32 to f64
   %truncf = arith.truncf %extf : f64 to f16
   return %truncf : f16
 }
 
+// CHECK-LABEL: @narrowExtremaOfExtf
+// CHECK-NOT: arith.extf
+// CHECK-NOT: arith.truncf
+// CHECK: %[[MAXIMUM:.*]] = arith.maximumf %arg0, %arg1 fastmath<nnan> : f16
+// CHECK: %[[MAXNUM:.*]] = arith.maxnumf %arg0, %arg1 fastmath<nnan> : f16
+// CHECK: %[[MINIMUM:.*]] = arith.minimumf %arg0, %arg1 fastmath<nnan> : f16
+// CHECK: %[[MINNUM:.*]] = arith.minnumf %arg0, %arg1 fastmath<nnan> : f16
+// CHECK: return %[[MAXIMUM]], %[[MAXNUM]], %[[MINIMUM]], %[[MINNUM]]
+func.func @narrowExtremaOfExtf(%arg0: f16, %arg1: f16)
+    -> (f16, f16, f16, f16) {
+  %lhs = arith.extf %arg0 : f16 to f32
+  %rhs = arith.extf %arg1 : f16 to f32
+  %maximum = arith.maximumf %lhs, %rhs fastmath<nnan> : f32
+  %maxnum = arith.maxnumf %lhs, %rhs fastmath<nnan> : f32
+  %minimum = arith.minimumf %lhs, %rhs fastmath<nnan> : f32
+  %minnum = arith.minnumf %lhs, %rhs fastmath<nnan> : f32
+  %maximumTrunc = arith.truncf %maximum : f32 to f16
+  %maxnumTrunc = arith.truncf %maxnum : f32 to f16
+  %minimumTrunc = arith.truncf %minimum : f32 to f16
+  %minnumTrunc = arith.truncf %minnum : f32 to f16
+  return %maximumTrunc, %maxnumTrunc, %minimumTrunc, %minnumTrunc
+      : f16, f16, f16, f16
+}
+
+// CHECK-LABEL: @narrowIntegerExtrema
+// CHECK-NOT: arith.extsi
+// CHECK-NOT: arith.extui
+// CHECK-NOT: arith.trunci
+// CHECK: %[[SMAX:.*]] = arith.maxsi %arg0, %arg1 : i8
+// CHECK: %[[SMIN:.*]] = arith.minsi %arg0, %arg1 : i8
+// CHECK: %[[UMAX:.*]] = arith.maxui %arg0, %arg1 : i8
+// CHECK: %[[UMIN:.*]] = arith.minui %arg0, %arg1 : i8
+// CHECK: return %[[SMAX]], %[[SMIN]], %[[UMAX]], %[[UMIN]]
+func.func @narrowIntegerExtrema(%arg0: i8, %arg1: i8)
+    -> (i8, i8, i8, i8) {
+  %slhs = arith.extsi %arg0 : i8 to i32
+  %srhs = arith.extsi %arg1 : i8 to i32
+  %ulhs = arith.extui %arg0 : i8 to i32
+  %urhs = arith.extui %arg1 : i8 to i32
+  %smax = arith.maxsi %slhs, %srhs : i32
+  %smin = arith.minsi %slhs, %srhs : i32
+  %umax = arith.maxui %ulhs, %urhs : i32
+  %umin = arith.minui %ulhs, %urhs : i32
+  %smaxTrunc = arith.trunci %smax : i32 to i8
+  %sminTrunc = arith.trunci %smin : i32 to i8
+  %umaxTrunc = arith.trunci %umax : i32 to i8
+  %uminTrunc = arith.trunci %umin : i32 to i8
+  return %smaxTrunc, %sminTrunc, %umaxTrunc, %uminTrunc : i8, i8, i8, i8
+}
+
+// CHECK-LABEL: @doNotNarrowIntegerExtremumWithWideUse
+// CHECK: %[[LHS:.*]] = arith.extsi %arg0 : i8 to i32
+// CHECK: %[[RHS:.*]] = arith.extsi %arg1 : i8 to i32
+// CHECK: %[[MAX:.*]] = arith.maxsi %[[LHS]], %[[RHS]] : i32
+// CHECK: %[[TRUNC:.*]] = arith.trunci %[[MAX]] : i32 to i8
+// CHECK: return %[[TRUNC]], %[[MAX]]
+func.func @doNotNarrowIntegerExtremumWithWideUse(%arg0: i8, %arg1: i8)
+    -> (i8, i32) {
+  %lhs = arith.extsi %arg0 : i8 to i32
+  %rhs = arith.extsi %arg1 : i8 to i32
+  %max = arith.maxsi %lhs, %rhs : i32
+  %trunc = arith.trunci %max : i32 to i8
+  return %trunc, %max : i8, i32
+}
+
+// Zero extension does not preserve signed ordering across the sign boundary.
+// CHECK-LABEL: @doNotNarrowSignedExtremumOfZeroExtension
+// CHECK: %[[LHS:.*]] = arith.extui %arg0 : i8 to i32
+// CHECK: %[[RHS:.*]] = arith.extui %arg1 : i8 to i32
+// CHECK: %[[MAX:.*]] = arith.maxsi %[[LHS]], %[[RHS]] : i32
+// CHECK: %[[TRUNC:.*]] = arith.trunci %[[MAX]] : i32 to i8
+// CHECK: return %[[TRUNC]]
+func.func @doNotNarrowSignedExtremumOfZeroExtension(
+    %arg0: i8, %arg1: i8) -> i8 {
+  %lhs = arith.extui %arg0 : i8 to i32
+  %rhs = arith.extui %arg1 : i8 to i32
+  %max = arith.maxsi %lhs, %rhs : i32
+  %trunc = arith.trunci %max : i32 to i8
+  return %trunc : i8
+}
+
+// CHECK-LABEL: @doNotNarrowExtremumWithWideUse
+// CHECK: %[[LHS:.*]] = arith.extf %arg0 : f16 to f32
+// CHECK: %[[RHS:.*]] = arith.extf %arg1 : f16 to f32
+// CHECK: %[[MAXIMUM:.*]] = arith.maximumf %[[LHS]], %[[RHS]] : f32
+// CHECK: %[[TRUNC:.*]] = arith.truncf %[[MAXIMUM]] : f32 to f16
+// CHECK: return %[[TRUNC]], %[[MAXIMUM]]
+func.func @doNotNarrowExtremumWithWideUse(%arg0: f16, %arg1: f16)
+    -> (f16, f32) {
+  %lhs = arith.extf %arg0 : f16 to f32
+  %rhs = arith.extf %arg1 : f16 to f32
+  %maximum = arith.maximumf %lhs, %rhs : f32
+  %trunc = arith.truncf %maximum : f32 to f16
+  return %trunc, %maximum : f16, f32
+}
+
+// CHECK-LABEL: @doNotNarrowExtremumThroughInexactExtension
+// CHECK: %[[LHS:.*]] = arith.extf %arg0 : f8E8M0FNU to f16
+// CHECK: %[[RHS:.*]] = arith.extf %arg1 : f8E8M0FNU to f16
+// CHECK: %[[MAXIMUM:.*]] = arith.maximumf %[[LHS]], %[[RHS]] : f16
+// CHECK: %[[TRUNC:.*]] = arith.truncf %[[MAXIMUM]] : f16 to f8E8M0FNU
+// CHECK: return %[[TRUNC]]
+func.func @doNotNarrowExtremumThroughInexactExtension(
+    %arg0: f8E8M0FNU, %arg1: f8E8M0FNU) -> f8E8M0FNU {
+  %lhs = arith.extf %arg0 : f8E8M0FNU to f16
+  %rhs = arith.extf %arg1 : f8E8M0FNU to f16
+  %maximum = arith.maximumf %lhs, %rhs : f16
+  %trunc = arith.truncf %maximum : f16 to f8E8M0FNU
+  return %trunc : f8E8M0FNU
+}
+
+// f8E4M3FNUZ has no negative zero. For two f4E2M1FN negative-zero arguments
+// (bits 0x8), the wide computation returns +0 (bits 0x0) while the narrow
+// computation returns -0 (bits 0x8).
+// CHECK-LABEL: @doNotNarrowExtremumThroughFNUZ
+// CHECK: %[[LHS:.*]] = arith.extf %arg0 : f4E2M1FN to f8E4M3FNUZ
+// CHECK: %[[RHS:.*]] = arith.extf %arg1 : f4E2M1FN to f8E4M3FNUZ
+// CHECK: %[[MAXIMUM:.*]] = arith.maximumf %[[LHS]], %[[RHS]] : f8E4M3FNUZ
+// CHECK: %[[TRUNC:.*]] = arith.truncf %[[MAXIMUM]] : f8E4M3FNUZ to f4E2M1FN
+// CHECK: return %[[TRUNC]]
+func.func @doNotNarrowExtremumThroughFNUZ(
+    %arg0: f4E2M1FN, %arg1: f4E2M1FN) -> f4E2M1FN {
+  %lhs = arith.extf %arg0 : f4E2M1FN to f8E4M3FNUZ
+  %rhs = arith.extf %arg1 : f4E2M1FN to f8E4M3FNUZ
+  %maximum = arith.maximumf %lhs, %rhs : f8E4M3FNUZ
+  %trunc = arith.truncf %maximum : f8E4M3FNUZ to f4E2M1FN
+  return %trunc : f4E2M1FN
+}
+
+// Extending an f8E5M2 signaling NaN (bits 0x7d) to f16 quiets it. With 1.0
+// (bits 0x3c) as the other operand, the wide maxnum returns 1.0 while the
+// narrow operation returns the quieted NaN (bits 0x7f).
+// CHECK-LABEL: @doNotNarrowMaxNumThroughSNaNQuieting
+// CHECK: %[[LHS:.*]] = arith.extf %arg0 : f8E5M2 to f16
+// CHECK: %[[RHS:.*]] = arith.extf %arg1 : f8E5M2 to f16
+// CHECK: %[[MAXNUM:.*]] = arith.maxnumf %[[LHS]], %[[RHS]] : f16
+// CHECK: %[[MAXTRUNC:.*]] = arith.truncf %[[MAXNUM]] : f16 to f8E5M2
+// CHECK: return %[[MAXTRUNC]]
+func.func @doNotNarrowMaxNumThroughSNaNQuieting(
+    %arg0: f8E5M2, %arg1: f8E5M2) -> f8E5M2 {
+  %lhs = arith.extf %arg0 : f8E5M2 to f16
+  %rhs = arith.extf %arg1 : f8E5M2 to f16
+  %maxnum = arith.maxnumf %lhs, %rhs : f16
+  %trunc = arith.truncf %maxnum : f16 to f8E5M2
+  return %trunc : f8E5M2
+}
+
+// Extending an f8E5M2 signaling NaN (bits 0x7d) to f16 quiets it. With 1.0
+// (bits 0x3c) as the other operand, the wide minnum returns 1.0 while the
+// narrow operation returns the quieted NaN (bits 0x7f).
+// CHECK-LABEL: @doNotNarrowMinNumThroughSNaNQuieting
+// CHECK: %[[LHS:.*]] = arith.extf %arg0 : f8E5M2 to f16
+// CHECK: %[[RHS:.*]] = arith.extf %arg1 : f8E5M2 to f16
+// CHECK: %[[MINNUM:.*]] = arith.minnumf %[[LHS]], %[[RHS]] : f16
+// CHECK: %[[MINTRUNC:.*]] = arith.truncf %[[MINNUM]] : f16 to f8E5M2
+// CHECK: return %[[MINTRUNC]]
+func.func @doNotNarrowMinNumThroughSNaNQuieting(
+    %arg0: f8E5M2, %arg1: f8E5M2) -> f8E5M2 {
+  %lhs = arith.extf %arg0 : f8E5M2 to f16
+  %rhs = arith.extf %arg1 : f8E5M2 to f16
+  %minnum = arith.minnumf %lhs, %rhs : f16
+  %trunc = arith.truncf %minnum : f16 to f8E5M2
+  return %trunc : f8E5M2
+}
+
+// The same representational differences also prevent folding the bare
+// truncf(extf(x)) round trips.
+// CHECK-LABEL: @doNotFoldFNUZRoundTrip
+// CHECK: %[[EXT:.*]] = arith.extf %arg0 : f4E2M1FN to f8E4M3FNUZ
+// CHECK: %[[TRUNC:.*]] = arith.truncf %[[EXT]] : f8E4M3FNUZ to f4E2M1FN
+// CHECK: return %[[TRUNC]]
+func.func @doNotFoldFNUZRoundTrip(%arg0: f4E2M1FN) -> f4E2M1FN {
+  %ext = arith.extf %arg0 : f4E2M1FN to f8E4M3FNUZ
+  %trunc = arith.truncf %ext : f8E4M3FNUZ to f4E2M1FN
+  return %trunc : f4E2M1FN
+}
+
+// CHECK-LABEL: @doNotFoldSNaNRoundTrip
+// CHECK: %[[EXT:.*]] = arith.extf %arg0 : f8E5M2 to f16
+// CHECK: %[[TRUNC:.*]] = arith.truncf %[[EXT]] : f16 to f8E5M2
+// CHECK: return %[[TRUNC]]
+func.func @doNotFoldSNaNRoundTrip(%arg0: f8E5M2) -> f8E5M2 {
+  %ext = arith.extf %arg0 : f8E5M2 to f16
+  %trunc = arith.truncf %ext : f16 to f8E5M2
+  return %trunc : f8E5M2
+}
+
 // CHECK-LABEL: @truncSitofp
 //       CHECK:     %[[SITOFP:.*]] = arith.sitofp %[[ARG0:.*]] : i32 to f32
 //       CHECK-NOT: truncf



More information about the Mlir-commits mailing list