Skip to content

Commit

Permalink
Document JavaScript engine-specific handling of NaN values
Browse files Browse the repository at this point in the history
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: #895
Related: 8277671
  • Loading branch information
tryone144 committed Dec 20, 2024
1 parent c313e51 commit dcb4b78
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 9 deletions.
25 changes: 25 additions & 0 deletions classlib/src/main/java/org/teavm/classlib/java/lang/TDouble.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
26 changes: 26 additions & 0 deletions classlib/src/main/java/org/teavm/classlib/java/lang/TFloat.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
15 changes: 12 additions & 3 deletions tests/src/test/java/org/teavm/classlib/java/lang/DoubleTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
15 changes: 12 additions & 3 deletions tests/src/test/java/org/teavm/classlib/java/lang/FloatTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit dcb4b78

Please sign in to comment.