Skip to content

Commit 59ec486

Browse files
Implement DataView: Float16
* Implement Float16 Tests cover: - Zero (positive and negative) - Simple values (1, -1, 2, 0.5, etc.) - Infinity (positive and negative) - NaN - Overflow cases (values > 65504) - Underflow cases (values < 2^-24) - Denormalized numbers - Normal numbers - Precision and rounding - Endianness (little-endian vs big-endian) - Bit pattern validation - Round-trip consistency - Powers of two - Common float values Also fixed floating-point literal precision warning. * Add IEEE 754 Float16 reference tests with Node.js compatibility Tests validate our Float16 implementation against IEEE 754 binary16 specification. All test cases use known bit patterns and expected values from the specification. Test coverage: - Exact bit pattern validation for special values (0, ±1, ±Inf, NaN) - Powers of two from 2^-14 to 2^15 - Denormalized number handling - Rounding behavior (rounds to nearest representable value) - Overflow and underflow edge cases - Endianness (both little and big-endian) --------- Co-authored-by: vladimir.zhuravlev <vladimir.zhuravlev@servicenow.com>
1 parent 1268c62 commit 59ec486

6 files changed

Lines changed: 1929 additions & 32 deletions

File tree

rhino/src/main/java/org/mozilla/javascript/typedarrays/ByteIo.java

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
package org.mozilla.javascript.typedarrays;
88

99
public class ByteIo {
10+
11+
// Float16 constants
12+
private static final double FLOAT16_MIN_NORMAL = 6.103515625e-5; // 2^-14
13+
private static final double FLOAT16_MIN_SUBNORMAL = 5.960464477539063E-8; // 2^-24
14+
1015
public static Byte readInt8(byte[] buf, int offset) {
1116
return Byte.valueOf(buf[offset]);
1217
}
@@ -162,6 +167,177 @@ public static void writeUint64(byte[] buf, int offset, long val, boolean littleE
162167
}
163168
}
164169

170+
public static Float readFloat16(byte[] buf, int offset, boolean littleEndian) {
171+
int bits = doReadInt16(buf, offset, littleEndian) & 0xffff;
172+
173+
// Extract sign, exponent, and mantissa
174+
int sign = (bits >>> 15) & 0x1;
175+
int exponent = (bits >>> 10) & 0x1f;
176+
int mantissa = bits & 0x3ff;
177+
178+
// Handle special cases
179+
if (exponent == 0) {
180+
if (mantissa == 0) {
181+
// Zero
182+
return sign == 0 ? 0.0f : -0.0f;
183+
}
184+
185+
// Denormalized number
186+
float value = (float) ((double) mantissa / (1 << 10) * FLOAT16_MIN_NORMAL);
187+
return sign == 0 ? value : -value;
188+
189+
} else if (exponent == 31) {
190+
if (mantissa == 0) {
191+
// Infinity
192+
return sign == 0 ? Float.POSITIVE_INFINITY : Float.NEGATIVE_INFINITY;
193+
}
194+
195+
// NaN
196+
return Float.NaN;
197+
198+
} else {
199+
// Normalized number
200+
float value =
201+
(float) ((1.0 + (double) mantissa / (1 << 10)) * Math.pow(2, exponent - 15));
202+
return sign == 0 ? value : -value;
203+
}
204+
}
205+
206+
public static void writeFloat16(byte[] buf, int offset, double val, boolean littleEndian) {
207+
float fval = (float) val;
208+
209+
// Handle special cases
210+
if (Float.isNaN(fval)) {
211+
doWriteInt16(buf, offset, 0x7e00, littleEndian);
212+
return;
213+
}
214+
215+
int sign = (Float.floatToIntBits(fval) >>> 31) & 0x1;
216+
float absVal = Math.abs(fval);
217+
218+
if (Float.isInfinite(fval)) {
219+
// Infinity
220+
doWriteInt16(buf, offset, (sign << 15) | 0x7c00, littleEndian);
221+
return;
222+
}
223+
224+
if (absVal == 0.0f) {
225+
// Zero
226+
doWriteInt16(buf, offset, sign << 15, littleEndian);
227+
return;
228+
}
229+
230+
// Convert to float16
231+
int exponent;
232+
int mantissa;
233+
234+
// Check overflow using original double precision
235+
// Max representable float16 is 65504 (exp=30, mantissa=0x3ff)
236+
// Values >= 65520 definitely overflow to infinity
237+
// Values in [65504, 65520) might round to 65504 or infinity
238+
double absValDouble = Math.abs(val);
239+
if (absValDouble >= 65520.0) {
240+
// Definite overflow to infinity
241+
doWriteInt16(buf, offset, (sign << 15) | 0x7c00, littleEndian);
242+
return;
243+
} else if (absValDouble > 65504.0) {
244+
// Near overflow: value is between max finite and definite overflow
245+
// These values might round to 65504 or infinity depending on exact value
246+
// 65504 = 0x7BFF: exp=30, mantissa=0x3ff (all 1s)
247+
// Halfway point to next value would cause overflow
248+
// Values < 65520 should round to 65504
249+
doWriteInt16(buf, offset, (sign << 15) | 0x7BFF, littleEndian);
250+
return;
251+
}
252+
if (absVal < (float) FLOAT16_MIN_NORMAL) {
253+
// Denormalized number - IEEE 754 round-to-nearest-even
254+
// Use original double precision for accuracy in denormalized range
255+
exponent = 0;
256+
257+
double ratio = Math.abs(val) / FLOAT16_MIN_SUBNORMAL;
258+
int mantissaBase = (int) ratio;
259+
double fractional = ratio - mantissaBase;
260+
261+
// IEEE 754 round-to-nearest-even
262+
if (fractional > 0.5) {
263+
// More than halfway: round up
264+
mantissa = mantissaBase + 1;
265+
} else if (fractional == 0.5) {
266+
// Tie: round to even
267+
if ((mantissaBase & 1) != 0) {
268+
mantissa = mantissaBase + 1; // Round up if odd
269+
} else {
270+
mantissa = mantissaBase; // Keep if even
271+
}
272+
} else {
273+
// Less than halfway: round down
274+
mantissa = mantissaBase;
275+
}
276+
} else {
277+
// Normalized
278+
int exp32 = ((Float.floatToIntBits(fval) >>> 23) & 0xff) - 127;
279+
exponent = exp32 + 15;
280+
281+
// DEFENSIVE CHECK (UNCOVERED): Exponent overflow to infinity
282+
// This check is unreachable through normal API usage because:
283+
// - Guard at line 239 catches all values >= 65520 (which have exp32 >= 16)
284+
// - For exponent >= 31: need exp32 >= 16, meaning Float32 >= 2^16 = 65536
285+
// - But 65536 >= 65520, so caught by guard before reaching here
286+
// - Mathematical impossibility: Can't pass guard (< 65520) AND overflow (>= 65536)
287+
// This defensive check is kept for defense-in-depth in case guards are modified.
288+
// See UNCOVERED_CODE_EXPLANATION.md for full mathematical proof.
289+
if (exponent >= 31) {
290+
// Overflow to infinity
291+
doWriteInt16(buf, offset, (sign << 15) | 0x7c00, littleEndian);
292+
return;
293+
}
294+
295+
// Normalized mantissa processing
296+
// Note: exponent > 0 is guaranteed here because:
297+
// - We're in the normalized path (absVal >= FLOAT16_MIN_NORMAL = 2^-14)
298+
// - Therefore exp32 >= -14, so exponent = exp32 + 15 >= 1
299+
int mant32 = Float.floatToIntBits(fval) & 0x7fffff;
300+
301+
// Implement IEEE 754 round-to-nearest-even
302+
int bitsToShift = mant32 & 0x1fff; // Bits [12:0] that will be lost
303+
mantissa = mant32 >>> 13; // Bits [22:13] become the new mantissa
304+
305+
if (bitsToShift > 0x1000) {
306+
// More than halfway: round up
307+
mantissa++;
308+
} else if (bitsToShift == 0x1000) {
309+
// Exactly halfway: round to even (check LSB)
310+
if ((mantissa & 1) != 0) {
311+
mantissa++; // Round up if odd
312+
}
313+
}
314+
// else: Less than halfway, round down (mantissa already truncated)
315+
316+
// Handle rounding overflow
317+
if (mantissa >= 0x400) {
318+
exponent++;
319+
mantissa = 0;
320+
// DEFENSIVE CHECK (UNCOVERED): Mantissa rounding causes exponent overflow
321+
// This check is unreachable through normal API usage because:
322+
// - For this to execute: need exponent=30 initially, then mantissa rounds to 0x400
323+
// - This requires Float32 values near 2^16 (approximately 65504-65536 range)
324+
// - Guard at line 243 catches all values > 65504
325+
// - Guard at line 239 catches all values >= 65520
326+
// - Complete coverage: All overflow cases caught by guards before reaching here
327+
// This defensive check is kept for defense-in-depth in case guards are modified.
328+
// See UNCOVERED_CODE_EXPLANATION.md for full mathematical proof.
329+
if (exponent >= 31) {
330+
// Overflow to infinity
331+
doWriteInt16(buf, offset, (sign << 15) | 0x7c00, littleEndian);
332+
return;
333+
}
334+
}
335+
}
336+
337+
int bits = (sign << 15) | (exponent << 10) | mantissa;
338+
doWriteInt16(buf, offset, bits, littleEndian);
339+
}
340+
165341
public static Float readFloat32(byte[] buf, int offset, boolean littleEndian) {
166342
long base = readUint32Primitive(buf, offset, littleEndian);
167343
return Float.valueOf(Float.intBitsToFloat((int) base));

rhino/src/main/java/org/mozilla/javascript/typedarrays/NativeDataView.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ public static Object init(Context cx, Scriptable scope, boolean sealed) {
7474

7575
constructor.definePrototypeProperty(
7676
SymbolKey.TO_STRING_TAG, CLASS_NAME, DONTENUM | READONLY);
77+
constructor.definePrototypeMethod(scope, "getFloat16", 1, NativeDataView::js_getFloat16);
7778
constructor.definePrototypeMethod(scope, "getFloat32", 1, NativeDataView::js_getFloat32);
7879
constructor.definePrototypeMethod(scope, "getFloat64", 1, NativeDataView::js_getFloat64);
7980
constructor.definePrototypeMethod(scope, "getInt8", 1, NativeDataView::js_getInt8);
@@ -85,6 +86,7 @@ public static Object init(Context cx, Scriptable scope, boolean sealed) {
8586
constructor.definePrototypeMethod(scope, "getBigInt64", 1, NativeDataView::js_getBigInt64);
8687
constructor.definePrototypeMethod(
8788
scope, "getBigUint64", 1, NativeDataView::js_getBigUint64);
89+
constructor.definePrototypeMethod(scope, "setFloat16", 2, NativeDataView::js_setFloat16);
8890
constructor.definePrototypeMethod(scope, "setFloat32", 2, NativeDataView::js_setFloat32);
8991
constructor.definePrototypeMethod(scope, "setFloat64", 2, NativeDataView::js_setFloat64);
9092
constructor.definePrototypeMethod(scope, "setInt8", 2, NativeDataView::js_setInt8);
@@ -239,6 +241,12 @@ private static Object js_getFloat64(
239241
return realThis.js_getFloat(cx, scope, 8, args);
240242
}
241243

244+
private static Object js_getFloat16(
245+
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
246+
NativeDataView realThis = realThis(thisObj);
247+
return realThis.js_getFloat(cx, scope, 2, args);
248+
}
249+
242250
private Object js_getFloat(Context cx, Scriptable scope, int bytes, Object[] args) {
243251
int pos = ScriptRuntime.toIndex(isArg(args, 0) ? args[0] : Undefined.instance);
244252

@@ -254,6 +262,8 @@ private Object js_getFloat(Context cx, Scriptable scope, int bytes, Object[] arg
254262
}
255263

256264
switch (bytes) {
265+
case 2:
266+
return ByteIo.readFloat16(arrayBuffer.buffer, offset + pos, littleEndian);
257267
case 4:
258268
return ByteIo.readFloat32(arrayBuffer.buffer, offset + pos, littleEndian);
259269
case 8:
@@ -386,6 +396,13 @@ private static Object js_setFloat64(
386396
return Undefined.instance;
387397
}
388398

399+
private static Object js_setFloat16(
400+
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
401+
NativeDataView realThis = realThis(thisObj);
402+
realThis.js_setFloat(cx, scope, 2, args);
403+
return Undefined.instance;
404+
}
405+
389406
private void js_setFloat(Context cx, Scriptable scope, int bytes, Object[] args) {
390407
int pos = ScriptRuntime.toIndex(isArg(args, 0) ? args[0] : Undefined.instance);
391408

@@ -403,6 +420,9 @@ private void js_setFloat(Context cx, Scriptable scope, int bytes, Object[] args)
403420
}
404421

405422
switch (bytes) {
423+
case 2:
424+
ByteIo.writeFloat16(arrayBuffer.buffer, offset + pos, val, littleEndian);
425+
break;
406426
case 4:
407427
ByteIo.writeFloat32(arrayBuffer.buffer, offset + pos, val, littleEndian);
408428
break;

0 commit comments

Comments
 (0)