[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