This is an automated email from the ASF dual-hosted git repository.

chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git


The following commit(s) were added to refs/heads/main by this push:
     new 67e7036f4 fix(javascript): round float16 values to nearest even (#4007)
67e7036f4 is described below

commit 67e7036f417f798c440d46623a10f99fe9406c29
Author: Shawn Yang <[email protected]>
AuthorDate: Mon Aug 31 11:03:23 2026 +0800

    fix(javascript): round float16 values to nearest even (#4007)
    
    ## Why?
    
    
    
    ## What does this PR do?
    
    
    
    ## Related issues
    
    
    
    ## AI Contribution Checklist
    
    
    
    - [ ] Substantial AI assistance was used in this PR: `yes` / `no`
    - [ ] If `yes`, I included a completed [AI Contribution
    
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
    in this PR description and the required `AI Usage Disclosure`.
    - [ ] If `yes`, my PR description includes the required `ai_review`
    summary and screenshot evidence or equivalent persisted links of the
    final clean AI review results from both fresh reviewers described in
    `AI_POLICY.md`, the Fory-guided reviewer and the independent general
    reviewer, on the current PR diff or current HEAD after the latest code
    changes.
    
    
    
    ## Does this PR introduce any user-facing change?
    
    
    
    - [ ] Does this PR introduce any public API change?
    - [ ] Does this PR introduce any binary protocol compatibility change?
    
    ## Benchmark
---
 .../javascript/supported-types.md                  |  2 +
 javascript/packages/core/lib/types/float16.ts      | 49 ++++++++++++++--------
 javascript/test/array.test.ts                      | 15 +++++++
 javascript/test/number.test.ts                     | 44 ++++++++++++++++++-
 4 files changed, 92 insertions(+), 18 deletions(-)

diff --git a/docs/object-serialization/javascript/supported-types.md 
b/docs/object-serialization/javascript/supported-types.md
index 57ca8eb01..7949aeb1a 100644
--- a/docs/object-serialization/javascript/supported-types.md
+++ b/docs/object-serialization/javascript/supported-types.md
@@ -70,6 +70,8 @@ Type.bfloat16();
 
 `float16` and `bfloat16` are useful when interoperating with languages or 
payloads that use reduced-precision numeric formats.
 
+`Type.float16()` rounds numbers to the nearest half-precision value, choosing 
the value with an even least-significant bit when the input is exactly halfway 
between two values. Float16 array conversion uses the same rounding rule. 
Signed zero, infinities, and NaN are preserved; values can underflow to signed 
zero or overflow to signed infinity.
+
 ## Arrays and Typed Arrays
 
 ### Lists
diff --git a/javascript/packages/core/lib/types/float16.ts 
b/javascript/packages/core/lib/types/float16.ts
index 20ffcd332..ad6681a80 100644
--- a/javascript/packages/core/lib/types/float16.ts
+++ b/javascript/packages/core/lib/types/float16.ts
@@ -17,36 +17,51 @@
  * under the License.
  */
 
-const float32View = new Float32Array(1);
-const int32View = new Int32Array(float32View.buffer);
+const float64View = new DataView(new ArrayBuffer(8));
 
 export function toFloat16Bits(value: number) {
-  float32View[0] = value;
-  const floatValue = int32View[0];
-  const sign = (floatValue >>> 16) & 0x8000;
-  const exponent = ((floatValue >>> 23) & 0xff) - 127;
-  const significand = floatValue & 0x7fffff;
+  // Round directly from binary64: narrowing to binary32 first can move a value
+  // onto a binary16 midpoint and change the ties-to-even result.
+  float64View.setFloat64(0, value);
+  const high = float64View.getUint32(0);
+  const sign = (high >>> 16) & 0x8000;
+  const exponent = ((high >>> 20) & 0x7ff) - 1023;
+  let significand = high & 0xfffff;
 
-  if (exponent === 128) {
-    return sign | 0x7c00 | (significand !== 0 ? 0x0200 : 0);
+  if (exponent === 1024) {
+    return sign | 0x7c00 | (significand !== 0 || float64View.getUint32(4) !== 
0 ? 0x0200 : 0);
   }
 
   if (exponent > 15) {
     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.
+  if (exponent < -25) {
+    // Below half the smallest subnormal, round to signed zero before the
+    // shift count can reach 32 and wrap in JavaScript.
     return sign;
   }
 
+  let shift = 10;
+  let bits = (exponent + 15) << 10;
   if (exponent < -14) {
-    return sign | ((significand | 0x800000) >> (13 - 14 - exponent));
-  }
-
-  return sign | ((exponent + 15) << 10) | (significand >> 13);
+    shift = -exponent - 4;
+    significand |= 0x100000;
+    bits = 0;
+  }
+  bits |= significand >>> shift;
+  const remainder = significand & ((1 << shift) - 1);
+  const halfway = 1 << (shift - 1);
+  // The low binary64 word distinguishes an exact tie from a value above it.
+  // Increment the complete encoding so rounding can carry into the exponent.
+  if (
+    remainder > halfway ||
+    (remainder === halfway && (float64View.getUint32(4) !== 0 || (bits & 1) 
!== 0))
+  ) {
+    bits++;
+  }
+
+  return sign | bits;
 }
 
 export function fromFloat16Bits(bits: number): number {
diff --git a/javascript/test/array.test.ts b/javascript/test/array.test.ts
index d6b66a4b0..989bbf7f8 100644
--- a/javascript/test/array.test.ts
+++ b/javascript/test/array.test.ts
@@ -215,6 +215,21 @@ describe("array", () => {
     expect(Array.from(result.a6 as Iterable<number>)[2]).toBeCloseTo(-4.5, 1);
   });
 
+  test("rounds float16 arrays to nearest even", () => {
+    const values = [4e-8, 1 + 2 ** -11, 1 + 3 * 2 ** -11, 1 + 2 ** -11 + 2 ** 
-52, -65520];
+    const expected = [2 ** -24, 1, 1 + 2 ** -9, 1 + 2 ** -10, -Infinity];
+    const array = new ForyFloat16Array(values);
+    expect(Array.from(array)).toEqual(expected);
+
+    const fory = new Fory({ compatible: false, ref: true });
+    const { serialize, deserialize } = fory.register(
+      Type.struct({ typeName: "example.f16round" }, { values: 
Type.float16Array() }),
+    );
+    for (const input of [values, array]) {
+      expect(Array.from(deserialize(serialize({ values: input 
})).values)).toEqual(expected);
+    }
+  });
+
   test("should bfloat16Array work", () => {
     const typeinfo = Type.struct(
       {
diff --git a/javascript/test/number.test.ts b/javascript/test/number.test.ts
index 7da913422..006b633e7 100644
--- a/javascript/test/number.test.ts
+++ b/javascript/test/number.test.ts
@@ -19,6 +19,7 @@
 
 import Fory, { Type } from "../packages/core/index";
 import { describe, expect, test } from "@jest/globals";
+import { toFloat16Bits } from "../packages/core/lib/types/float16";
 
 describe("number", () => {
   test("should i8 work", () => {
@@ -155,7 +156,7 @@ describe("number", () => {
   });
 
   test("should float16 underflow tiny magnitudes to signed zero", () => {
-    // Magnitudes below the smallest float16 subnormal must encode as zero;
+    // Magnitudes below half the smallest float16 subnormal 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 });
@@ -168,6 +169,47 @@ describe("number", () => {
     expect(deserialize(serialize({ a: -1e-10 })).a).toBe(-0);
   });
 
+  test.each([
+    [0, 0x0000],
+    [Number.MIN_VALUE, 0x0000],
+    [2 ** -25 - 2 ** -78, 0x0000],
+    [2 ** -25, 0x0000],
+    [2 ** -25 + 2 ** -77, 0x0001],
+    [4e-8, 0x0001],
+    [2 ** -24, 0x0001],
+    [3 * 2 ** -25 - 2 ** -76, 0x0001],
+    [3 * 2 ** -25, 0x0002],
+    [2 ** -14 - 2 ** -25 - 2 ** -67, 0x03ff],
+    [2 ** -14 - 2 ** -25, 0x0400],
+    [2 ** -14, 0x0400],
+    [1 + 2 ** -11, 0x3c00],
+    [1 + 2 ** -11 + 2 ** -52, 0x3c01],
+    [1.0008, 0x3c01],
+    [1 + 3 * 2 ** -11 - 2 ** -52, 0x3c01],
+    [1 + 3 * 2 ** -11, 0x3c02],
+    [2 - 2 ** -11 - 2 ** -52, 0x3fff],
+    [2 - 2 ** -11, 0x4000],
+    [65504, 0x7bff],
+    [65520 - 2 ** -37, 0x7bff],
+    [65520, 0x7c00],
+    [Number.MAX_VALUE, 0x7c00],
+    [Infinity, 0x7c00],
+  ])("rounds float16 %s to bits %s", (value, bits) => {
+    expect(toFloat16Bits(value)).toBe(bits);
+    expect(toFloat16Bits(-value)).toBe(bits | 0x8000);
+  });
+
+  test("rounds float16 fields to nearest even", () => {
+    const fory = new Fory({ compatible: false, ref: true });
+    const { serialize, deserialize } = fory.register(
+      Type.struct({ typeName: "example.f16round" }, { a: Type.float16() }),
+    );
+    expect(deserialize(serialize({ a: 4e-8 })).a).toBe(2 ** -24);
+    expect(deserialize(serialize({ a: 1 + 3 * 2 ** -11 })).a).toBe(1 + 2 ** 
-9);
+    expect(deserialize(serialize({ a: 1 + 2 ** -11 + 2 ** -52 })).a).toBe(1 + 
2 ** -10);
+    expect(deserialize(serialize({ a: -65520 })).a).toBe(-Infinity);
+  });
+
   test("should float16 Infinity work", () => {
     const fory = new Fory({ compatible: false, ref: true });
     const serializer = fory.register(


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to