From 3c05afcb1fc1356e6db76592a5fdd82467cf5bf1 Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sun, 30 Aug 2026 22:52:32 +0530 Subject: [PATCH] fix(javascript): underflow tiny float16 magnitudes to signed zero toFloat16Bits shifted the significand by (-1 - exponent) for subnormals; for magnitudes below 2^-32 the shift count reaches 32 and JS masks it with & 31, leaving garbage bits (1e-10 encoded as -15352, 1e-11 as NaN). Underflow values below the smallest float16 subnormal to signed zero. --- javascript/packages/core/lib/types/float16.ts | 7 +++++++ javascript/test/number.test.ts | 14 ++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/javascript/packages/core/lib/types/float16.ts b/javascript/packages/core/lib/types/float16.ts index de2fc9100d..20ffcd332c 100644 --- a/javascript/packages/core/lib/types/float16.ts +++ b/javascript/packages/core/lib/types/float16.ts @@ -35,6 +35,13 @@ export function toFloat16Bits(value: number) { return sign | 0x7c00; } + if (exponent < -24) { + // Too small for a float16 subnormal. Larger shifts below would wrap + // (JS masks shift counts with & 31) and leave garbage bits, so + // underflow to signed zero. + return sign; + } + if (exponent < -14) { return sign | ((significand | 0x800000) >> (13 - 14 - exponent)); } diff --git a/javascript/test/number.test.ts b/javascript/test/number.test.ts index c1e84754f8..7da913422c 100644 --- a/javascript/test/number.test.ts +++ b/javascript/test/number.test.ts @@ -154,6 +154,20 @@ describe("number", () => { expect(result.a).toBe(NaN); }); + test("should float16 underflow tiny magnitudes to signed zero", () => { + // Magnitudes below the smallest float16 subnormal must encode as zero; + // shift counts of 32 or more wrapped (JS masks them with & 31) and left + // garbage bits in the half. + const fory = new Fory({ compatible: false, ref: true }); + const { serialize, deserialize } = fory.register( + Type.struct({ typeName: "example.f16zero" }, { a: Type.float16() }), + ); + expect(deserialize(serialize({ a: 1e-10 })).a).toBe(0); + expect(deserialize(serialize({ a: 1e-11 })).a).toBe(0); + expect(deserialize(serialize({ a: 1e-40 })).a).toBe(0); + expect(deserialize(serialize({ a: -1e-10 })).a).toBe(-0); + }); + test("should float16 Infinity work", () => { const fory = new Fory({ compatible: false, ref: true }); const serializer = fory.register(