[Mlir-commits] [llvm] [mlir] [APFloat] Report the sign and the zero a conversion cannot represent (PR #216056)
llvmlistbot at llvm.org
llvmlistbot at llvm.org
Thu Aug 13 07:10:18 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-mlir
Author: Hung Kuan Tseng (Tim096)
<details>
<summary>Changes</summary>
Fixes #<!-- -->215445, in the direction @<!-- -->tgymnich confirmed there.
`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.
---
Full diff: https://github.com/llvm/llvm-project/pull/216056.diff
6 Files Affected:
- (modified) llvm/lib/Support/APFloat.cpp (+38-26)
- (modified) llvm/unittests/ADT/APFloatTest.cpp (+56-2)
- (modified) mlir/lib/AsmParser/AttributeParser.cpp (+9)
- (modified) mlir/lib/AsmParser/Parser.cpp (+8)
- (modified) mlir/test/Dialect/Arith/canonicalize.mlir (+24)
- (modified) mlir/test/IR/invalid-builtin-attributes.mlir (+18)
``````````diff
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..03a35d3191cc4 100644
--- a/llvm/unittests/ADT/APFloatTest.cpp
+++ b/llvm/unittests/ADT/APFloatTest.cpp
@@ -2334,6 +2334,59 @@ TEST(APFloatTest, Float8E8M0FNUNaNConvert) {
}
}
+// Test that converting into a format that cannot represent the sign, or that
+// has no encoding for zero, reports the loss. Callers decide whether to keep
+// the result from losesInfo, and the value that comes out is not the value
+// that went in: the sign bit has nowhere to go, and zero is replaced by the
+// smallest normalized value.
+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) {
+ for (double Value : {-2.0, -0.0}) {
+ APFloat test(Value);
+ bool losesInfo = false;
+ APFloat::opStatus status =
+ test.convert(*Sem, APFloat::rmNearestTiesToEven, &losesInfo);
+ EXPECT_TRUE(losesInfo);
+ EXPECT_EQ(status, APFloat::opInexact);
+ }
+
+ // The magnitude alone still converts exactly.
+ APFloat test(2.0);
+ bool losesInfo = true;
+ APFloat::opStatus status =
+ test.convert(*Sem, APFloat::rmNearestTiesToEven, &losesInfo);
+ EXPECT_FALSE(losesInfo);
+ EXPECT_EQ(status, APFloat::opOK);
+ EXPECT_FALSE(test.isNegative());
+ }
+
+ // Float8E8M0FNU has no zero either, and substitutes 2^-127 for one.
+ {
+ APFloat test(0.0);
+ bool losesInfo = false;
+ APFloat::opStatus status = test.convert(
+ APFloat::Float8E8M0FNU(), APFloat::rmNearestTiesToEven, &losesInfo);
+ EXPECT_TRUE(losesInfo);
+ EXPECT_EQ(status, APFloat::opInexact);
+ EXPECT_EQ(APInt(8, 0), test.bitcastToAPInt());
+ }
+
+ // Float8E5M3FNU does have one, so a positive zero converts exactly.
+ {
+ 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());
+ }
+}
+
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 +9934,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/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 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
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
+}
``````````
</details>
https://github.com/llvm/llvm-project/pull/216056
More information about the Mlir-commits
mailing list