diff --git a/java/fory-core/src/main/java/org/apache/fory/type/Float16.java b/java/fory-core/src/main/java/org/apache/fory/type/Float16.java index c76eaa362c..a3fa1a0b5d 100644 --- a/java/fory-core/src/main/java/org/apache/fory/type/Float16.java +++ b/java/fory-core/src/main/java/org/apache/fory/type/Float16.java @@ -123,7 +123,10 @@ private static short floatToFloat16Bits(float f32) { int shift = 1 - newExp; int netShift = 13 + shift; - if (netShift >= 24) { + // Shifting past the hidden bit is what actually flushes a value to zero. At netShift + // == 24 the round and sticky bits still decide: every float strictly between the + // 2^-25 mid-point and 2^-24 rounds up to the smallest subnormal. + if (netShift > 24) { outExp = 0; outMant = 0; } else { diff --git a/java/fory-core/src/test/java/org/apache/fory/type/Float16Test.java b/java/fory-core/src/test/java/org/apache/fory/type/Float16Test.java index 23f573e3dc..5853874daa 100644 --- a/java/fory-core/src/test/java/org/apache/fory/type/Float16Test.java +++ b/java/fory-core/src/test/java/org/apache/fory/type/Float16Test.java @@ -313,6 +313,44 @@ public void testConstants() { assertEquals((short) 0x0001, Float16.MIN_VALUE.toBits()); } + private static int encoded(float value) { + return Float16.toBits(value) & 0xFFFF; + } + + @Test + public void testRoundsUpToMinValueJustAboveTheSubnormalMidpoint() { + // The midpoint between zero and the smallest subnormal is 2^-25. The tie goes to the even + // encoding, which is zero, so 2^-25 must encode as a zero of the matching sign. + assertEquals(encoded(Float.intBitsToFloat(0x33000000)), 0x0000); + assertEquals(encoded(Float.intBitsToFloat(0xb3000000)), 0x8000); + // The largest float strictly below the midpoint is nearer to zero than to 2^-24. + assertEquals(encoded(Float.intBitsToFloat(0x32ffffff)), 0x0000); + + assertEquals(encoded(Float.intBitsToFloat(0x33000001)), 0x0001); + assertEquals(encoded(3.0e-8f), 0x0001); + assertEquals(encoded(4.4702e-8f), 0x0001); + assertEquals(encoded(Float.intBitsToFloat(0x337fffff)), 0x0001); + assertEquals(encoded(-3.0e-8f), 0x8001); + assertEquals(encoded((float) Math.pow(2, -24)), 0x0001); + assertEquals(Float16.parse("3e-8").toBits(), (short) 0x0001); + } + + @Test + public void testNothingBetweenTheSubnormalMidpointAndMinValueIsFlushedToZero() { + // Every one of the 2 * (2^23 - 1) floats strictly between 2^-25 and 2^-24 is closer to 2^-24 + // than to zero, so none of them may encode as a zero of either sign. + int flushed = 0; + for (int bits = 0x33000001; bits < 0x33800000; bits++) { + if (encoded(Float.intBitsToFloat(bits)) != 0x0001) { + flushed++; + } + if (encoded(Float.intBitsToFloat(bits | 0x80000000)) != 0x8001) { + flushed++; + } + } + assertEquals(flushed, 0, "floats in (2^-25, 2^-24) must round to MIN_VALUE"); + } + @Test public void testSizeConstants() { assertEquals(16, Float16.SIZE_BITS);