[Mlir-commits] [llvm] [mlir] [APFloat] Report the sign and the zero a conversion cannot represent (PR #216056)

Hung Kuan Tseng llvmlistbot at llvm.org
Sun Aug 16 09:34:36 PDT 2026


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

>From 8df0bf39c4b5dfe8740df38ed3e3a1b39b42dbfc Mon Sep 17 00:00:00 2001
From: Hung-Kuan Tseng <tseng.tim096 at gmail.com>
Date: Thu, 13 Aug 2026 22:08:48 +0800
Subject: [PATCH 1/2] [APFloat] Report the sign and the zero a conversion
 cannot represent

`convert` reports through `losesInfo` what rounding lost, but not what the
target format has no encoding for at all. Two properties are not rounding:

- `hasSignedRepr == false` (`f8E8M0FNU`, `f8E5M3FNU`): the sign bit is
  carried into a format that has no room for it. Printing the result
  reaches `convertFromString`, which is `llvm_unreachable` for a leading
  minus.
- `hasZero == false` (`f8E8M0FNU`): zero is replaced by the smallest
  normalized value, 2^-127, at the end of `convert`.

Both were reported as `opOK` with `losesInfo == false`, so a caller that
gates on `losesInfo` -- which is how MLIR's `arith.truncf` folder decides
whether a constant fold is legal -- kept a value the format cannot hold.
Folding `arith.truncf %cst : f32 to f8E8M0FNU` for a negative constant
therefore crashed on print, and for `0.0` it produced 2^-127 while
`--arith-expand=include-f8e8m0` reads that same encoding back as `0.0`.

Report both as `opInexact` with `losesInfo` set. The values that come out
are unchanged; only the status is. The NaN path is restructured to fall
through to the common tail rather than return early, so that these checks
see every conversion.

`ConvertDoubleToE8M0FNU` pinned the previous status for the zero case; it
now expects the loss to be reported, with the substituted value unchanged.
---
 llvm/lib/Support/APFloat.cpp              | 64 ++++++++++++--------
 llvm/unittests/ADT/APFloatTest.cpp        | 73 ++++++++++++++++++++++-
 mlir/test/Dialect/Arith/canonicalize.mlir | 24 ++++++++
 3 files changed, 133 insertions(+), 28 deletions(-)

diff --git a/llvm/lib/Support/APFloat.cpp b/llvm/lib/Support/APFloat.cpp
index 63007d88ed4d4..c7f776d257936 100644
--- a/llvm/lib/Support/APFloat.cpp
+++ b/llvm/lib/Support/APFloat.cpp
@@ -2548,35 +2548,36 @@ APFloat::opStatus IEEEFloat::convert(const fltSemantics &toSemantics,
       *losesInfo =
           fromSemantics.nonFiniteBehavior != fltNonfiniteBehavior::NanOnly;
       makeNaN(false, sign);
-      return is_signaling ? opInvalidOp : opOK;
-    }
-
-    // If NaN is negative zero, we need to create a new NaN to avoid converting
-    // NaN to -Inf.
-    if (fromSemantics.nanEncoding == fltNanEncoding::NegativeZero &&
-        semantics->nanEncoding != fltNanEncoding::NegativeZero)
-      makeNaN(false, false);
-
-    // If the source has no significand, there are no payload bits to carry
-    // over, and an all-zero significand would encode an Inf. Create a new NaN.
-    if (!APFloat::hasSignificand(fromSemantics))
-      makeNaN(false, sign);
+      fs = is_signaling ? opInvalidOp : opOK;
+    } else {
+      // If NaN is negative zero, we need to create a new NaN to avoid
+      // converting NaN to -Inf.
+      if (fromSemantics.nanEncoding == fltNanEncoding::NegativeZero &&
+          semantics->nanEncoding != fltNanEncoding::NegativeZero)
+        makeNaN(false, false);
+
+      // If the source has no significand, there are no payload bits to carry
+      // over, and an all-zero significand would encode an Inf. Create a new
+      // NaN.
+      if (!APFloat::hasSignificand(fromSemantics))
+        makeNaN(false, sign);
 
-    *losesInfo = lostFraction != lfExactlyZero || X86SpecialNan;
+      *losesInfo = lostFraction != lfExactlyZero || X86SpecialNan;
 
-    // For x87 extended precision, we want to make a NaN, not a special NaN if
-    // the input wasn't special either.
-    if (!X86SpecialNan && semantics == &APFloatBase::semX87DoubleExtended)
-      APInt::tcSetBit(significandParts(), semantics->precision - 1);
+      // For x87 extended precision, we want to make a NaN, not a special NaN
+      // if the input wasn't special either.
+      if (!X86SpecialNan && semantics == &APFloatBase::semX87DoubleExtended)
+        APInt::tcSetBit(significandParts(), semantics->precision - 1);
 
-    // Convert of sNaN creates qNaN and raises an exception (invalid op).
-    // This also guarantees that a sNaN does not become Inf on a truncation
-    // that loses all payload bits.
-    if (is_signaling) {
-      makeQuiet();
-      fs = opInvalidOp;
-    } else {
-      fs = opOK;
+      // Convert of sNaN creates qNaN and raises an exception (invalid op).
+      // This also guarantees that a sNaN does not become Inf on a truncation
+      // that loses all payload bits.
+      if (is_signaling) {
+        makeQuiet();
+        fs = opInvalidOp;
+      } else {
+        fs = opOK;
+      }
     }
   } else if (category == fcInfinity &&
              semantics->nonFiniteBehavior == fltNonfiniteBehavior::NanOnly) {
@@ -2596,6 +2597,17 @@ APFloat::opStatus IEEEFloat::convert(const fltSemantics &toSemantics,
     fs = opOK;
   }
 
+  // The target may have no encoding for a negative value, or none for zero.
+  // The paths above only report what rounding lost, so report these here too:
+  // a caller that checks losesInfo would otherwise accept a result the target
+  // cannot represent, and printing that result asserts.
+  if ((sign && !semantics->hasSignedRepr) ||
+      (category == fcZero && !semantics->hasZero)) {
+    *losesInfo = true;
+    if (fs == opOK)
+      fs = opInexact;
+  }
+
   if (category == fcZero && !semantics->hasZero)
     makeSmallestNormalized(false);
   return fs;
diff --git a/llvm/unittests/ADT/APFloatTest.cpp b/llvm/unittests/ADT/APFloatTest.cpp
index 4ef2814e1a7b3..6f441d10343af 100644
--- a/llvm/unittests/ADT/APFloatTest.cpp
+++ b/llvm/unittests/ADT/APFloatTest.cpp
@@ -2334,6 +2334,74 @@ TEST(APFloatTest, Float8E8M0FNUNaNConvert) {
   }
 }
 
+// Test that converting into a format that cannot represent the sign, or that
+// has no encoding for zero, reports the loss. The value that comes out is
+// still the one convert computed: the sign bit is carried into a format that
+// cannot spell it, and zero is replaced by the smallest normalized value. The
+// status is what tells a caller not to keep it.
+TEST(APFloatTest, ConvertLosesUnrepresentableSignAndZero) {
+  // Neither format has a sign; only Float8E8M0FNU also lacks a zero.
+  const fltSemantics *NoSignSemantics[] = {&APFloat::Float8E8M0FNU(),
+                                           &APFloat::Float8E5M3FNU()};
+
+  for (const fltSemantics *Sem : NoSignSemantics) {
+    // The magnitude converts exactly, so the sign is the whole of the loss.
+    APFloat test(-2.0);
+    bool losesInfo = false;
+    APFloat::opStatus status =
+        test.convert(*Sem, APFloat::rmNearestTiesToEven, &losesInfo);
+    EXPECT_TRUE(losesInfo);
+    EXPECT_EQ(status, APFloat::opInexact);
+    EXPECT_TRUE(test.isNegative());
+    EXPECT_EQ(-2.0, test.convertToDouble());
+
+    // The same magnitude without the sign has nothing to report.
+    test = APFloat(2.0);
+    losesInfo = true;
+    status = test.convert(*Sem, APFloat::rmNearestTiesToEven, &losesInfo);
+    EXPECT_FALSE(losesInfo);
+    EXPECT_EQ(status, APFloat::opOK);
+    EXPECT_FALSE(test.isNegative());
+    EXPECT_EQ(2.0, test.convertToDouble());
+  }
+
+  // Float8E8M0FNU has no zero either, and substitutes 2^-127 for one. That
+  // substitution is unsigned, so both zeros come out as the same value.
+  for (double Zero : {0.0, -0.0}) {
+    APFloat test(Zero);
+    bool losesInfo = false;
+    APFloat::opStatus status = test.convert(
+        APFloat::Float8E8M0FNU(), APFloat::rmNearestTiesToEven, &losesInfo);
+    EXPECT_TRUE(losesInfo);
+    EXPECT_EQ(status, APFloat::opInexact);
+    EXPECT_TRUE(test.isSmallestNormalized());
+    EXPECT_EQ(0x1.0p-127, test.convertToDouble());
+    EXPECT_EQ(APInt(8, 0), test.bitcastToAPInt());
+  }
+
+  // Float8E5M3FNU does have one, so a positive zero converts exactly, and a
+  // negative one keeps its value and loses only the sign.
+  {
+    APFloat test(0.0);
+    bool losesInfo = true;
+    APFloat::opStatus status = test.convert(
+        APFloat::Float8E5M3FNU(), APFloat::rmNearestTiesToEven, &losesInfo);
+    EXPECT_FALSE(losesInfo);
+    EXPECT_EQ(status, APFloat::opOK);
+    EXPECT_TRUE(test.isZero());
+    EXPECT_FALSE(test.isNegative());
+
+    test = APFloat(-0.0);
+    losesInfo = false;
+    status = test.convert(APFloat::Float8E5M3FNU(),
+                          APFloat::rmNearestTiesToEven, &losesInfo);
+    EXPECT_TRUE(losesInfo);
+    EXPECT_EQ(status, APFloat::opInexact);
+    EXPECT_TRUE(test.isZero());
+    EXPECT_TRUE(test.isNegative());
+  }
+}
+
 TEST(APFloatTest, getLargest) {
   EXPECT_EQ(3.402823466e+38f, APFloat::getLargest(APFloat::IEEEsingle()).convertToFloat());
   EXPECT_EQ(1.7976931348623158e+308, APFloat::getLargest(APFloat::IEEEdouble()).convertToDouble());
@@ -9881,13 +9949,14 @@ TEST(APFloatTest, ConvertDoubleToE8M0FNU) {
   EXPECT_EQ(status, APFloat::opOK);
 
   // For E8M0, zero encoding is represented as the smallest normalized value.
+  // That is a different value, so the conversion reports the loss.
   test = APFloat(APFloat::IEEEdouble(), "0.0");
   status = test.convert(APFloat::Float8E8M0FNU(), APFloat::rmNearestTiesToEven,
                         &losesInfo);
   EXPECT_TRUE(test.isSmallestNormalized());
   EXPECT_EQ(0x1.0p-127, test.convertToDouble());
-  EXPECT_FALSE(losesInfo);
-  EXPECT_EQ(status, APFloat::opOK);
+  EXPECT_TRUE(losesInfo);
+  EXPECT_EQ(status, APFloat::opInexact);
 
   // Test that the conversion of a power-of-two value is precise.
   test = APFloat(APFloat::IEEEdouble(), "8.0");
diff --git a/mlir/test/Dialect/Arith/canonicalize.mlir b/mlir/test/Dialect/Arith/canonicalize.mlir
index 89632305fa99d..1916784fb0602 100644
--- a/mlir/test/Dialect/Arith/canonicalize.mlir
+++ b/mlir/test/Dialect/Arith/canonicalize.mlir
@@ -1452,6 +1452,30 @@ func.func @truncFPConstantRounding() -> bf16 {
   return %0 : bf16
 }
 
+// f8E8M0FNU has no encoding for a negative value, so this conversion is not
+// lossless and is NOT folded. It used to fold, and printing the result of the
+// fold asserted in APFloat.
+// CHECK-LABEL: @truncFPConstantE8M0Negative
+//       CHECK:   arith.constant -2.000000e+00 : f32
+//       CHECK:   truncf
+func.func @truncFPConstantE8M0Negative() -> f8E8M0FNU {
+  %cst = arith.constant -2.000000e+00 : f32
+  %0 = arith.truncf %cst : f32 to f8E8M0FNU
+  return %0 : f8E8M0FNU
+}
+
+// It has no encoding for zero either: the 0x00 pattern is 2^-127. Folding used
+// to produce that value silently, while the expansion in ExpandOps.cpp reads
+// the same pattern back as 0.0.
+// CHECK-LABEL: @truncFPConstantE8M0Zero
+//       CHECK:   arith.constant 0.000000e+00 : f32
+//       CHECK:   truncf
+func.func @truncFPConstantE8M0Zero() -> f8E8M0FNU {
+  %cst = arith.constant 0.000000e+00 : f32
+  %0 = arith.truncf %cst : f32 to f8E8M0FNU
+  return %0 : f8E8M0FNU
+}
+
 // CHECK-LABEL: @tripleAddAdd
 //       CHECK:   %[[cres:.+]] = arith.constant 59 : index
 //       CHECK:   %[[add:.+]] = arith.addi %arg0, %[[cres]] : index

>From aa2025befe325ab43eb08005a6a69e9ec0c2aed6 Mon Sep 17 00:00:00 2001
From: Hung-Kuan Tseng <tseng.tim096 at gmail.com>
Date: Thu, 13 Aug 2026 22:08:49 +0800
Subject: [PATCH 2/2] [mlir][asm] Reject a negative float literal for a type
 with no sign

`f8E8M0FNU` and `f8E5M3FNU` have no encoding for a negative value. The
parser converted the literal anyway, and the attribute that came out kept
the sign bit, so printing it asserted inside APFloat:

  %c = arith.constant -2.000000e+00 : f8E8M0FNU

  This floating point format does not support signed values
  UNREACHABLE executed at llvm/lib/Support/APFloat.cpp:3190!

No pass has to run for this; parsing and printing the file is enough.
Emit a diagnostic instead, in both the scalar (`parseFloatAttr`) and the
dense-elements (`parseFloatFromLiteral`) paths.
---
 mlir/lib/AsmParser/AttributeParser.cpp       |  9 +++++++++
 mlir/lib/AsmParser/Parser.cpp                |  8 ++++++++
 mlir/test/IR/invalid-builtin-attributes.mlir | 18 ++++++++++++++++++
 3 files changed, 35 insertions(+)

diff --git a/mlir/lib/AsmParser/AttributeParser.cpp b/mlir/lib/AsmParser/AttributeParser.cpp
index ca8e4ae2cecbc..ba314fac86a37 100644
--- a/mlir/lib/AsmParser/AttributeParser.cpp
+++ b/mlir/lib/AsmParser/AttributeParser.cpp
@@ -339,6 +339,7 @@ ParseResult Parser::parseAttributeDict(NamedAttrList &attributes) {
 
 /// Parse a float attribute.
 Attribute Parser::parseFloatAttr(Type type, bool isNegative) {
+  SMLoc loc = getToken().getLoc();
   auto val = getToken().getFloatingPointValue();
   if (!val)
     return (emitError("floating point value too large for attribute"), nullptr);
@@ -353,6 +354,14 @@ Attribute Parser::parseFloatAttr(Type type, bool isNegative) {
   if (!isa<FloatType>(type))
     return (emitError("floating point value not valid for specified type"),
             nullptr);
+  // A type with no signed representation, such as f8E8M0FNU, has no encoding
+  // for a negative value. The conversion inside FloatAttr::get keeps the sign
+  // bit, and printing the attribute that comes out of it asserts.
+  if (isNegative && !APFloat::semanticsHasSignedRepr(
+                        cast<FloatType>(type).getFloatSemantics()))
+    return (emitError(loc, "negative floating point literal for a type with no "
+                           "signed representation"),
+            nullptr);
   return FloatAttr::get(type, isNegative ? -*val : *val);
 }
 
diff --git a/mlir/lib/AsmParser/Parser.cpp b/mlir/lib/AsmParser/Parser.cpp
index 952d7e460c6e2..5cfd2f793313b 100644
--- a/mlir/lib/AsmParser/Parser.cpp
+++ b/mlir/lib/AsmParser/Parser.cpp
@@ -405,6 +405,14 @@ ParseResult Parser::parseFloatFromLiteral(std::optional<APFloat> &result,
     if (!val)
       return emitError(tok.getLoc()) << "floating point value too large";
 
+    // A type with no signed representation, such as f8E8M0FNU, has no encoding
+    // for this value at all; the conversion below would keep the sign bit and
+    // produce a value that asserts when it is printed.
+    if (isNegative && !APFloat::semanticsHasSignedRepr(semantics))
+      return emitError(tok.getLoc())
+             << "negative floating point literal for a type with no signed "
+                "representation";
+
     result.emplace(isNegative ? -*val : *val);
     bool unused;
     result->convert(semantics, APFloat::rmNearestTiesToEven, &unused);
diff --git a/mlir/test/IR/invalid-builtin-attributes.mlir b/mlir/test/IR/invalid-builtin-attributes.mlir
index 0d00b3141fbc6..1989ce663cbb2 100644
--- a/mlir/test/IR/invalid-builtin-attributes.mlir
+++ b/mlir/test/IR/invalid-builtin-attributes.mlir
@@ -675,3 +675,21 @@ func.func @expect_to_parse_literal() {
   %0 = arith.constant dense<[23]> : tensor<1x!unknown<>>
   return
 }
+
+// -----
+
+// f8E8M0FNU has no encoding for a negative value. Converting the literal used
+// to keep the sign bit, and printing the result asserted in APFloat.
+func.func @negative_literal_without_signed_repr() {
+  // expected-error at below {{negative floating point literal for a type with no signed representation}}
+  %0 = arith.constant -2.000000e+00 : f8E8M0FNU
+  return
+}
+
+// -----
+
+func.func @negative_elements_literal_without_signed_repr() {
+  // expected-error at below {{negative floating point literal for a type with no signed representation}}
+  %0 = arith.constant dense<[1.000000e+00, -2.000000e+00]> : tensor<2xf8E8M0FNU>
+  return
+}



More information about the Mlir-commits mailing list