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

llvmlistbot at llvm.org llvmlistbot at llvm.org
Mon Aug 17 02:21:40 PDT 2026


Author: Hung Kuan Tseng
Date: 2026-08-17T11:21:34+02:00
New Revision: 898b0188d9016c310629e53383c56365f6ee861a

URL: https://github.com/llvm/llvm-project/commit/898b0188d9016c310629e53383c56365f6ee861a
DIFF: https://github.com/llvm/llvm-project/commit/898b0188d9016c310629e53383c56365f6ee861a.diff

LOG: [APFloat] Report the sign and the zero a conversion cannot represent (#216056)

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

| property | formats today | what happens |
|---|---|---|
| `hasSignedRepr == false` | `f8E8M0FNU`, `f8E5M3FNU` | the sign bit is
carried into a format with no room for it |
| `hasZero == false` | `f8E8M0FNU` | zero is replaced by the smallest
normalized value, 2^-127 |

Both were reported as `opOK` with `losesInfo == false`. Callers gate on
`losesInfo` -- that is how `arith.truncf`'s folder decides whether a
constant
fold is legal -- so they kept a value the format cannot hold.

### What went wrong in MLIR

```mlir
%c = arith.constant -2.000000e+00 : f32
%t = arith.truncf %c : f32 to f8E8M0FNU    // --canonicalize
```

folded, and printing the folded constant reached `convertFromString`,
whose
leading-minus path is an `llvm_unreachable`:

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

The zero case did not crash, it disagreed: the folder produced 2^-127,
while
`--arith-expand=include-f8e8m0` reads the same `0x00` encoding back as
`0.0`.

### The two commits

1. **`APFloat`**: 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 instead of returning early, so the
checks
see every conversion. `arith.truncf` then leaves both constants alone.

2. **MLIR parser**: `arith.constant -2.000000e+00 : f8E8M0FNU` needs no
pass at
all -- parsing and printing the file was enough to assert, because the
attribute is built with `FloatAttr::get` and never consults `losesInfo`.
Both
   literal paths now emit a diagnostic:

   ```
error: negative floating point literal for a type with no signed
representation
   ```

### One test changed on purpose

`APFloatTest.ConvertDoubleToE8M0FNU` pinned the old status for the zero
case:

```cpp
  // For E8M0, zero encoding is represented as the smallest normalized value.
  EXPECT_FALSE(losesInfo);
  EXPECT_EQ(status, APFloat::opOK);
```

The substituted value is kept -- only the two lines above change, since
2^-127
is not the value that went in. Please say if that substitution is meant
to be
reported as exact; everything else here follows from it.

### Not covered

Writing `0.0 : f8E8M0FNU` as a literal still yields 2^-127 silently.
That is a
value question rather than a crash, and rejecting the literal is a
bigger
decision than this patch should take; the folder no longer produces it.

### Verification

| check | result |
|---|---|
| `ADTTests` | 2188 passed, 0 failed |
| `check-mlir` | 3848 passed, 0 failed (611 unsupported, 1 expectedly
failed) |
| `git clang-format` | clean |

Both reproducers from the issue were rerun: the fold is gone (`-2.0` and
`0.0`
into `f8E8M0FNU`, and `-2.0` into `f8E5M3FNU` all stay as
`arith.truncf`), and
the literal now produces a diagnostic instead of an abort.

Fixes #215445

Added: 
    

Modified: 
    llvm/lib/Support/APFloat.cpp
    llvm/unittests/ADT/APFloatTest.cpp
    mlir/lib/AsmParser/AttributeParser.cpp
    mlir/lib/AsmParser/Parser.cpp
    mlir/test/Dialect/Arith/canonicalize.mlir
    mlir/test/IR/invalid-builtin-attributes.mlir

Removed: 
    


################################################################################
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 0c5103c4b49e4..b4529815b3dd8 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 
diff erent 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/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/Dialect/Arith/canonicalize.mlir b/mlir/test/Dialect/Arith/canonicalize.mlir
index e8e72926cb563..efee81bf09959 100644
--- a/mlir/test/Dialect/Arith/canonicalize.mlir
+++ b/mlir/test/Dialect/Arith/canonicalize.mlir
@@ -1668,6 +1668,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

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