Skip to content

Commit ec23cfa

Browse files
authored
Math emulation improvements (#10176)
Fixes #9711 by using native emulation of `log1p`, `expm1` and `hypot`. Fixes #9663 by adding the implementation of `IEEEremainder` Adds `absExact`, `multiplyFull`, `ulp`, and `getExponent` + missing variations of `multiplyExact`, `floorDiv`, `floorMod` for completeness. Only missing methods for JVM 17 compatibility are `multiplyHigh` and `fma` which might be better handled in #9909
1 parent 7b221c1 commit ec23cfa

File tree

8 files changed

+493
-79
lines changed

8 files changed

+493
-79
lines changed

user/super/com/google/gwt/emul/java/lang/Math.java

Lines changed: 92 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import static javaemul.internal.InternalPreconditions.checkCriticalArithmetic;
1919

20+
import javaemul.internal.JsUtils;
2021
import jsinterop.annotations.JsMethod;
2122
import jsinterop.annotations.JsPackage;
2223
import jsinterop.annotations.JsType;
@@ -25,13 +26,6 @@
2526
* Math utility methods and constants.
2627
*/
2728
public final class Math {
28-
// The following methods are not implemented because JS doesn't provide the
29-
// necessary pieces:
30-
// public static double ulp (double x)
31-
// public static float ulp (float x)
32-
// public static int getExponent (double d)
33-
// public static int getExponent (float f)
34-
// public static double IEEEremainder(double f1, double f2)
3529

3630
public static final double E = 2.7182818284590452354;
3731
public static final double PI = 3.14159265358979323846;
@@ -52,6 +46,16 @@ public static long abs(long x) {
5246
return x < 0 ? -x : x;
5347
}
5448

49+
public static int absExact(int v) {
50+
checkCriticalArithmetic(v != Integer.MIN_VALUE);
51+
return abs(v);
52+
}
53+
54+
public static long absExact(long v) {
55+
checkCriticalArithmetic(v != Long.MIN_VALUE);
56+
return abs(v);
57+
}
58+
5559
@JsMethod(namespace = JsPackage.GLOBAL, name = "Math.acos")
5660
public static native double acos(double x);
5761

@@ -77,9 +81,8 @@ public static long addExact(long x, long y) {
7781
@JsMethod(namespace = JsPackage.GLOBAL, name = "Math.atan2")
7882
public static native double atan2(double y, double x);
7983

80-
public static double cbrt(double x) {
81-
return x == 0 || !Double.isFinite(x) ? x : pow(x, 1.0 / 3.0);
82-
}
84+
@JsMethod(namespace = JsPackage.GLOBAL, name = "Math.cbrt")
85+
public static native double cbrt(double x);
8386

8487
@JsMethod(namespace = JsPackage.GLOBAL, name = "Math.ceil")
8588
public static native double ceil(double x);
@@ -99,9 +102,8 @@ public static float copySign(float magnitude, float sign) {
99102
@JsMethod(namespace = JsPackage.GLOBAL, name = "Math.cos")
100103
public static native double cos(double x);
101104

102-
public static double cosh(double x) {
103-
return (exp(x) + exp(-x)) / 2;
104-
}
105+
@JsMethod(namespace = JsPackage.GLOBAL, name = "Math.cosh")
106+
public static native double cosh(double x);
105107

106108
public static int decrementExact(int x) {
107109
checkCriticalArithmetic(x != Integer.MIN_VALUE);
@@ -116,9 +118,8 @@ public static long decrementExact(long x) {
116118
@JsMethod(namespace = JsPackage.GLOBAL, name = "Math.exp")
117119
public static native double exp(double x);
118120

119-
public static double expm1(double d) {
120-
return d == 0 ? d : exp(d) - 1;
121-
}
121+
@JsMethod(namespace = JsPackage.GLOBAL, name = "Math.expm1")
122+
public static native double expm1(double d);
122123

123124
@JsMethod(namespace = JsPackage.GLOBAL, name = "Math.floor")
124125
public static native double floor(double x);
@@ -135,6 +136,10 @@ public static long floorDiv(long dividend, long divisor) {
135136
return ((dividend ^ divisor) >= 0 ? dividend / divisor : ((dividend + 1) / divisor) - 1);
136137
}
137138

139+
public static long floorDiv(long dividend, int divisor) {
140+
return floorDiv(dividend, (long) divisor);
141+
}
142+
138143
public static int floorMod(int dividend, int divisor) {
139144
checkCriticalArithmetic(divisor != 0);
140145
return ((dividend % divisor) + divisor) % divisor;
@@ -145,11 +150,53 @@ public static long floorMod(long dividend, long divisor) {
145150
return ((dividend % divisor) + divisor) % divisor;
146151
}
147152

148-
public static double hypot(double x, double y) {
149-
return Double.isInfinite(x) || Double.isInfinite(y) ?
150-
Double.POSITIVE_INFINITY : sqrt(x * x + y * y);
153+
public static long floorMod(long dividend, int divisor) {
154+
return floorMod(dividend, (long) divisor);
155+
}
156+
157+
@SuppressWarnings("CheckStyle.MethodName")
158+
public static double IEEEremainder(double v, double m) {
159+
double ratio = v / m;
160+
double closest = Math.ceil(ratio);
161+
double frac = Math.abs(closest - ratio);
162+
if (frac > 0.5 || frac == 0.5 && (closest % 2 != 0)) {
163+
closest = Math.floor(ratio);
164+
}
165+
// if closest == 0 and m == inf, avoid multiplication
166+
return closest == 0 ? v : v - m * closest;
167+
}
168+
169+
public static int getExponent(double v) {
170+
int[] intBits = JsUtils.doubleToRawIntBits(v);
171+
return ((intBits[1] >> 20) & 2047) - Double.MAX_EXPONENT;
172+
}
173+
174+
public static int getExponent(float v) {
175+
return ((JsUtils.floatToRawIntBits(v) >> 23) & 255) - Float.MAX_EXPONENT;
176+
}
177+
178+
public static double ulp(double v) {
179+
if (!Double.isFinite(v)) {
180+
return Math.abs(v);
181+
}
182+
int exponent = Math.getExponent(v);
183+
if (exponent == -1023) {
184+
return Double.MIN_VALUE;
185+
}
186+
return Math.pow(2, exponent - 52);
187+
}
188+
189+
public static float ulp(float v) {
190+
int exponent = Math.getExponent(v);
191+
if (exponent == -Float.MAX_EXPONENT) {
192+
return Float.MIN_VALUE;
193+
}
194+
return (float) Math.pow(2, exponent - 23);
151195
}
152196

197+
@JsMethod(namespace = JsPackage.GLOBAL, name = "Math.hypot")
198+
public static native double hypot(double x, double y);
199+
153200
public static int incrementExact(int x) {
154201
checkCriticalArithmetic(x != Integer.MAX_VALUE);
155202
return x + 1;
@@ -163,13 +210,11 @@ public static long incrementExact(long x) {
163210
@JsMethod(namespace = JsPackage.GLOBAL, name = "Math.log")
164211
public static native double log(double x);
165212

166-
public static double log10(double x) {
167-
return log(x) * NativeMath.LOG10E;
168-
}
213+
@JsMethod(namespace = JsPackage.GLOBAL, name = "Math.log10")
214+
public static native double log10(double x);
169215

170-
public static double log1p(double x) {
171-
return x == 0 ? x : log(x + 1);
172-
}
216+
@JsMethod(namespace = JsPackage.GLOBAL, name = "Math.log1p")
217+
public static native double log1p(double x);
173218

174219
@JsMethod(namespace = JsPackage.GLOBAL, name = "Math.max")
175220
public static native double max(double x, double y);
@@ -197,12 +242,28 @@ public static long min(long x, long y) {
197242
return x < y ? x : y;
198243
}
199244

245+
public static long multiplyFull(int x, int y) {
246+
return (long) x * (long) y;
247+
}
248+
200249
public static int multiplyExact(int x, int y) {
201250
double r = (double) x * (double) y;
202251
checkCriticalArithmetic(isSafeIntegerRange(r));
203252
return (int) r;
204253
}
205254

255+
public static long multiplyExact(long x, int y) {
256+
if (y == -1) {
257+
return negateExact(x);
258+
}
259+
if (y == 0) {
260+
return 0;
261+
}
262+
long r = x * y;
263+
checkCriticalArithmetic(r / y == x);
264+
return r;
265+
}
266+
206267
public static long multiplyExact(long x, long y) {
207268
if (y == -1) {
208269
return negateExact(x);
@@ -284,13 +345,8 @@ public static float scalb(float f, int scaleFactor) {
284345
return (float) scalb((double) f, scaleFactor);
285346
}
286347

287-
public static double signum(double d) {
288-
if (d == 0 || Double.isNaN(d)) {
289-
return d;
290-
} else {
291-
return d < 0 ? -1 : 1;
292-
}
293-
}
348+
@JsMethod(namespace = JsPackage.GLOBAL, name = "Math.sign")
349+
public static native double signum(double d);
294350

295351
public static float signum(float f) {
296352
return (float) signum((double) f);
@@ -299,28 +355,17 @@ public static float signum(float f) {
299355
@JsMethod(namespace = JsPackage.GLOBAL, name = "Math.sin")
300356
public static native double sin(double x);
301357

302-
public static double sinh(double x) {
303-
return x == 0.0 ? x : (exp(x) - exp(-x)) / 2;
304-
}
358+
@JsMethod(namespace = JsPackage.GLOBAL, name = "Math.sinh")
359+
public static native double sinh(double x);
305360

306361
@JsMethod(namespace = JsPackage.GLOBAL, name = "Math.sqrt")
307362
public static native double sqrt(double x);
308363

309364
@JsMethod(namespace = JsPackage.GLOBAL, name = "Math.tan")
310365
public static native double tan(double x);
311366

312-
public static double tanh(double x) {
313-
if (x == 0.0) {
314-
// -0.0 should return -0.0.
315-
return x;
316-
}
317-
318-
double e2x = exp(2 * x);
319-
if (Double.isInfinite(e2x)) {
320-
return 1;
321-
}
322-
return (e2x - 1) / (e2x + 1);
323-
}
367+
@JsMethod(namespace = JsPackage.GLOBAL, name = "Math.tanh")
368+
public static native double tanh(double x);
324369

325370
public static double toDegrees(double x) {
326371
return x * PI_UNDER_180;
@@ -411,7 +456,6 @@ private static boolean isSafeIntegerRange(double value) {
411456

412457
@JsType(isNative = true, name = "Math", namespace = JsPackage.GLOBAL)
413458
private static class NativeMath {
414-
public static double LOG10E;
415459
public static native double round(double x);
416460
}
417461
}

user/super/com/google/gwt/emul/javaemul/internal/JsUtils.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,14 @@ public static float intBitsToFloat(int value) {
123123
return JsUtils.<float[]>uncheckedCast(new Float32Array(buf))[0];
124124
}
125125

126-
public static long doubleToRawLongBits(double value) {
126+
public static int[] doubleToRawIntBits(double value) {
127127
ArrayBuffer buf = new ArrayBuffer(8);
128128
JsUtils.<double[]>uncheckedCast(new Float64Array(buf))[0] = value;
129-
int[] intBits = JsUtils.<int[]>uncheckedCast(new Uint32Array(buf));
129+
return JsUtils.<int[]>uncheckedCast(new Uint32Array(buf));
130+
}
131+
132+
public static long doubleToRawLongBits(double value) {
133+
int[] intBits = doubleToRawIntBits(value);
130134
return LongUtils.fromBits(intBits[0] | 0, intBits[1] | 0);
131135
}
132136

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2025 GWT Project Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.google.gwt.emultest.java17.lang;
17+
18+
import com.google.gwt.emultest.java.util.EmulTestBase;
19+
20+
/**
21+
* Tests for java.lang.String Java 12 API emulation.
22+
*/
23+
public class MathTest extends EmulTestBase {
24+
25+
public void testAbsExact() {
26+
assertEquals(Integer.MAX_VALUE, Math.absExact(hideFromCompiler(Integer.MAX_VALUE)));
27+
assertEquals(0, Math.absExact(hideFromCompiler(0)));
28+
assertEquals(1, Math.absExact(hideFromCompiler(-1)));
29+
assertThrowsArithmetic(() -> Math.absExact(hideFromCompiler(Integer.MIN_VALUE)));
30+
assertEquals(Long.MAX_VALUE, Math.absExact(hideFromCompiler(Long.MAX_VALUE)));
31+
assertEquals(0, Math.absExact(hideFromCompiler(0l)));
32+
assertEquals(1, Math.absExact(hideFromCompiler(-1l)));
33+
assertThrowsArithmetic(() -> Math.absExact(hideFromCompiler(Long.MIN_VALUE)));
34+
}
35+
36+
public void testAbsExactWithFolding() {
37+
assertEquals(Integer.MAX_VALUE, Math.absExact(Integer.MAX_VALUE));
38+
assertEquals(0, Math.absExact(0));
39+
assertEquals(1, Math.absExact(-1));
40+
assertThrowsArithmetic(() -> Math.absExact(Integer.MIN_VALUE));
41+
assertEquals(Long.MAX_VALUE, Math.absExact(Long.MAX_VALUE));
42+
assertEquals(0, Math.absExact(0L));
43+
assertEquals(1, Math.absExact(-1L));
44+
assertThrowsArithmetic(() -> Math.absExact(Long.MIN_VALUE));
45+
}
46+
47+
private void assertThrowsArithmetic(Runnable check) {
48+
try {
49+
check.run();
50+
fail("Should have failed");
51+
} catch (ArithmeticException ex) {
52+
// good
53+
}
54+
}
55+
}

user/test/com/google/gwt/emultest/EmulJava17Suite.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package com.google.gwt.emultest;
1717

1818
import com.google.gwt.emultest.java17.lang.CharSequenceTest;
19+
import com.google.gwt.emultest.java17.lang.MathTest;
1920
import com.google.gwt.emultest.java17.lang.StringTest;
2021
import com.google.gwt.emultest.java17.util.MapEntryTest;
2122
import com.google.gwt.emultest.java17.util.stream.CollectorsTest;
@@ -32,6 +33,7 @@
3233
@Suite.SuiteClasses({
3334
CharSequenceTest.class,
3435
StringTest.class,
36+
MathTest.class,
3537
CollectorsTest.class,
3638
StreamTest.class,
3739
DoubleStreamTest.class,

user/test/com/google/gwt/emultest/java/lang/DoubleTest.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,8 @@ public void testDoubleConstants() {
161161
assertTrue(Double.MIN_VALUE < Double.MAX_VALUE);
162162
assertFalse(Double.NaN == Double.NaN);
163163
assertEquals(64, Double.SIZE);
164-
// jdk1.6 assertEquals(Math.getExponent(Double.MAX_VALUE),
165-
// Double.MAX_EXPONENT);
166-
// jdk1.6 assertEquals(Math.getExponent(Double.MIN_NORMAL),
167-
// Double.MIN_EXPONENT);
164+
assertEquals(Math.getExponent(Double.MAX_VALUE), Double.MAX_EXPONENT);
165+
assertEquals(Math.getExponent(Double.MIN_NORMAL), Double.MIN_EXPONENT);
168166
// issue 8073 - used to fail in prod mode
169167
assertFalse(Double.isInfinite(Double.NaN));
170168
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2025 GWT Project Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.google.gwt.emultest.java17.lang;
17+
18+
import com.google.gwt.dev.util.arg.SourceLevel;
19+
import com.google.gwt.emultest.java.util.EmulTestBase;
20+
import com.google.gwt.junit.JUnitShell;
21+
22+
public class MathTest extends EmulTestBase {
23+
24+
public void testAbsExact() {
25+
assertFalse(isGwtSourceLevel17());
26+
}
27+
28+
public void testAbsExactWithFolding() {
29+
assertFalse(isGwtSourceLevel17());
30+
}
31+
32+
private boolean isGwtSourceLevel17() {
33+
return JUnitShell.getCompilerOptions().getSourceLevel().compareTo(SourceLevel.JAVA17) >= 0;
34+
}
35+
}

0 commit comments

Comments
 (0)