From dcb4b78cf369d6cf344b9e705332fc250f0d504a Mon Sep 17 00:00:00 2001 From: Bernd Busse Date: Fri, 20 Dec 2024 20:27:50 +0100 Subject: [PATCH] Document JavaScript engine-specific handling of NaN values Most JavaScript engines perform NaN-canonicalization. This results in information loss when converting a long to a double and back, iff the bits encode a non-canonical NaN value. V8 in chrome seems to be an exception that keeps the bits intact. See: https://github.com/konsoletyper/teavm/issues/895 Related: 8277671376b7745f9f33a1dc1206cbb8d66a0cb2 --- .../org/teavm/classlib/java/lang/TDouble.java | 25 ++++++++++++++++++ .../org/teavm/classlib/java/lang/TFloat.java | 26 +++++++++++++++++++ .../teavm/classlib/java/lang/DoubleTest.java | 15 ++++++++--- .../teavm/classlib/java/lang/FloatTest.java | 15 ++++++++--- .../classlib/java/nio/ByteBufferTest.java | 3 --- 5 files changed, 75 insertions(+), 9 deletions(-) diff --git a/classlib/src/main/java/org/teavm/classlib/java/lang/TDouble.java b/classlib/src/main/java/org/teavm/classlib/java/lang/TDouble.java index 526fcefaf..32af040b7 100644 --- a/classlib/src/main/java/org/teavm/classlib/java/lang/TDouble.java +++ b/classlib/src/main/java/org/teavm/classlib/java/lang/TDouble.java @@ -268,6 +268,17 @@ public boolean isInfinite() { @Import(name = "teavm_reinterpretDoubleToLong") @NoSideEffects @Unmanaged + /** + * Platform dependent behaviour in the JS backend. + * + * The JavaScript engines in Firefox and Safari have special handling of NaN values. + * + * Except for V8 in chrome, most engines seem to canonicalize NaN values for internal + * bookkeeping (and using these NaN values as object pointers internally). This is not + * expected to change in the future. + * + * @see #longBitsToDouble(double) + */ public static native long doubleToRawLongBits(double value); public static long doubleToLongBits(double value) { @@ -281,6 +292,20 @@ public static long doubleToLongBits(double value) { @Import(name = "teavm_reinterpretLongToDouble") @NoSideEffects @Unmanaged + /** + * Platform dependent behaviour in the JS backend. + * + * The JavaScript engines in Firefox and Safari have special handling of NaN values. + * + * Except for V8 in chrome, most engines seem to canonicalize NaN values for internal + * bookkeeping (and using these NaN values as object pointers internally). This is not + * expected to change in the future. + * + * As a result, application code intending to run with the JS backend should not depend + * on `long`s retaining their bits through the conversion of a `double`. + * + * See https://github.com/konsoletyper/teavm/issues/895 + */ public static native double longBitsToDouble(long bits); public static String toHexString(double d) { diff --git a/classlib/src/main/java/org/teavm/classlib/java/lang/TFloat.java b/classlib/src/main/java/org/teavm/classlib/java/lang/TFloat.java index 6553cc9f2..adaa07811 100644 --- a/classlib/src/main/java/org/teavm/classlib/java/lang/TFloat.java +++ b/classlib/src/main/java/org/teavm/classlib/java/lang/TFloat.java @@ -264,6 +264,17 @@ public int compareTo(TFloat other) { @Import(name = "teavm_reinterpretFloatToInt") @NoSideEffects @Unmanaged + /** + * Platform dependent behaviour in the JS backend. + * + * The JavaScript engines in Firefox and Safari have special handling of NaN values. + * + * Except for V8 in chrome, most engines seem to canonicalize NaN values for internal + * bookkeeping (and using these NaN values as object pointers internally). This is not + * expected to change in the future. + * + * @see #intBitsToFloat(float) + */ public static native int floatToRawIntBits(float value); public static int floatToIntBits(float value) { @@ -277,6 +288,21 @@ public static int floatToIntBits(float value) { @Import(name = "teavm_reinterpretIntToFloat") @NoSideEffects @Unmanaged + /** + * Platform dependent behaviour in the JS backend. + * + * The JavaScript engines in Firefox and Safari have special handling of NaN values. + * + * Except for V8 in chrome, most engines seem to canonicalize NaN values for internal + * bookkeeping (and using these NaN values as object pointers internally). This is not + * expected to change in the future. + * + * As a result, application code intending to run with the JS backend should not depend + * on `long`s retaining their bits through the conversion of a `double` — or `float` + * for that matter. + * + * See https://github.com/konsoletyper/teavm/issues/895 + */ public static native float intBitsToFloat(int bits); public static String toHexString(float f) { diff --git a/tests/src/test/java/org/teavm/classlib/java/lang/DoubleTest.java b/tests/src/test/java/org/teavm/classlib/java/lang/DoubleTest.java index d867188f8..c4f3da9e1 100644 --- a/tests/src/test/java/org/teavm/classlib/java/lang/DoubleTest.java +++ b/tests/src/test/java/org/teavm/classlib/java/lang/DoubleTest.java @@ -150,15 +150,24 @@ public void compares() { assertEquals(-1, Double.compare(-0.0, 0.0)); } + /** + * This test will fail on the JavaScript engines in Firefox and Safari. + * + * This is due to how JS numbers (i.e. `double`s) are handled internally. Except for V8 in chrome, + * most engines seem to canonicalize NaN values for internal bookkeeping (and using these NaN values + * as object pointers internally). This is not expected to change in the future. + * + * See https://github.com/konsoletyper/teavm/issues/895 + */ @Test public void testNaN() { assertTrue(Double.isNaN(OTHER_NAN)); - assertTrue(OTHER_NAN != OTHER_NAN); - assertTrue(OTHER_NAN != Double.NaN); + assertTrue(OTHER_NAN != OTHER_NAN); // fail + assertTrue(OTHER_NAN != Double.NaN); // fail assertEquals(Double.valueOf(Double.NaN), Double.valueOf(Double.NaN)); assertEquals(Double.valueOf(OTHER_NAN), Double.valueOf(Double.NaN)); assertEquals(Double.valueOf(OTHER_NAN), Double.valueOf(OTHER_NAN)); - assertNotEquals(Double.doubleToRawLongBits(OTHER_NAN), Double.doubleToRawLongBits(Double.NaN)); + assertNotEquals(Double.doubleToRawLongBits(OTHER_NAN), Double.doubleToRawLongBits(Double.NaN)); // fail assertEquals(Double.doubleToLongBits(OTHER_NAN), Double.doubleToLongBits(Double.NaN)); } } diff --git a/tests/src/test/java/org/teavm/classlib/java/lang/FloatTest.java b/tests/src/test/java/org/teavm/classlib/java/lang/FloatTest.java index 9b67fca41..010a6cbb7 100644 --- a/tests/src/test/java/org/teavm/classlib/java/lang/FloatTest.java +++ b/tests/src/test/java/org/teavm/classlib/java/lang/FloatTest.java @@ -132,15 +132,24 @@ public void compares() { assertEquals(-1, Float.compare(-0.0f, 0.0f)); } + /** + * This test will fail on the JavaScript engines in Firefox and Safari. + * + * This is due to how JS numbers (i.e. `double`s) are handled internally. Except for V8 in chrome, + * most engines seem to canonicalize NaN values for internal bookkeeping (and using these NaN values + * as object pointers internally). This is not expected to change in the future. + * + * See https://github.com/konsoletyper/teavm/issues/895 + */ @Test public void testNaN() { assertTrue(Float.isNaN(OTHER_NAN)); - assertTrue(OTHER_NAN != OTHER_NAN); - assertTrue(OTHER_NAN != Double.NaN); + assertTrue(OTHER_NAN != OTHER_NAN); // fail + assertTrue(OTHER_NAN != Double.NaN); // fail assertEquals(Float.valueOf(Float.NaN), Float.valueOf(Float.NaN)); assertEquals(Float.valueOf(OTHER_NAN), Float.valueOf(Float.NaN)); assertEquals(Float.valueOf(OTHER_NAN), Float.valueOf(OTHER_NAN)); - assertNotEquals(Float.floatToRawIntBits(OTHER_NAN), Float.floatToRawIntBits(Float.NaN)); + assertNotEquals(Float.floatToRawIntBits(OTHER_NAN), Float.floatToRawIntBits(Float.NaN)); // fail assertEquals(Float.floatToIntBits(OTHER_NAN), Float.floatToIntBits(Float.NaN)); } } diff --git a/tests/src/test/java/org/teavm/classlib/java/nio/ByteBufferTest.java b/tests/src/test/java/org/teavm/classlib/java/nio/ByteBufferTest.java index 842c66b8f..cd26d8b3c 100644 --- a/tests/src/test/java/org/teavm/classlib/java/nio/ByteBufferTest.java +++ b/tests/src/test/java/org/teavm/classlib/java/nio/ByteBufferTest.java @@ -29,7 +29,6 @@ import java.nio.ReadOnlyBufferException; import org.junit.Test; import org.junit.runner.RunWith; -import org.teavm.classlib.java.lang.DoubleTest; import org.teavm.junit.TeaVMTestRunner; @RunWith(TeaVMTestRunner.class) @@ -631,8 +630,6 @@ public void putsDouble() { buffer.putDouble(1, 2.0); assertArrayEquals(new byte[] { 63, 64, 0, 0, 0, 0, 0, 0, 0, 55, 0, 0, 0, 0, 0, 0 }, array); - buffer.putDouble(0, DoubleTest.OTHER_NAN); - assertArrayEquals(new byte[] { 127, -8, 0, 0, 0, 0, 0, 1, 0, 55, 0, 0, 0, 0, 0, 0 }, array); } @Test