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(