diff --git a/dev/core/src/com/google/gwt/dev/javac/testing/impl/JavaResourceBase.java b/dev/core/src/com/google/gwt/dev/javac/testing/impl/JavaResourceBase.java index e4fe4fea2cf..563dc2b25f2 100644 --- a/dev/core/src/com/google/gwt/dev/javac/testing/impl/JavaResourceBase.java +++ b/dev/core/src/com/google/gwt/dev/javac/testing/impl/JavaResourceBase.java @@ -330,30 +330,38 @@ public class JavaResourceBase { createMockJavaResource("java.lang.String", "package java.lang;", "import java.io.Serializable;", + "import javaemul.internal.annotations.ConstantFoldCandidate;", "import javaemul.internal.annotations.SpecializeMethod;", "public final class String implements Comparable, CharSequence, Serializable {", " public String() { }", " public String(char c) { }", " public String(String s) { }", - " public static String _String() { return \"\"; }", - " public static String _String(char c) { return \"\" + c; }", - " public static String _String(String s) { return s; }", +// " public static String _String() { return \"\"; }", +// " public static String _String(char c) { return \"\" + c; }", +// " public static String _String(String s) { return s; }", + " @ConstantFoldCandidate", " public char charAt(int index) { return 'a'; }", " public int compareTo(String other) { return -1; }", + " @ConstantFoldCandidate", " @SpecializeMethod(params = String.class, target = \"equals\")", " public boolean equals(Object other) {", " return (other instanceof String) && equals((String) other);", " }", + " @ConstantFoldCandidate", " private native boolean equals(String obj) /*-{ return false; }-*/;", + " @ConstantFoldCandidate", " public boolean equalsIgnoreCase(String str) { return false; }", + " @ConstantFoldCandidate", " public int length() { return 0; }", " public static String valueOf(int i) { return \"\" + i; }", " public static String valueOf(char c) { return \"\" + c; }", + " @ConstantFoldCandidate", " public int hashCode() { return 0; }", " public String replace(char c1, char c2) { return null; }", " public boolean startsWith(String str) { return false; }", " public native String substring(int start, int len) /*-{ return \"\"; }-*/;", " public String toLowerCase() { return null; }", + " @ConstantFoldCandidate", " public String toString() { return this; }", " public static String valueOf(boolean b) { return null; }", "}"); diff --git a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java index efd7bf15a33..5b0ba60629b 100644 --- a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java +++ b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java @@ -538,6 +538,8 @@ private void optimizeJava() throws InterruptedException { if (shouldOptimize()) { optimizeJavaToFixedPoint(); RemoveEmptySuperCalls.exec(jprogram); + RemoveSpecializations.exec(jprogram); + MethodInliner.exec(jprogram); } } @@ -551,7 +553,6 @@ private void optimizeJs(Set inlinableJsFunctions) throws InterruptedExce private void postNormalizationOptimizeJava() { try (SimpleEvent ignored = new SimpleEvent("Post-Normalization Java Optimizers")) { if (shouldOptimize()) { - RemoveSpecializations.exec(jprogram); Pruner.exec(jprogram, false, OptimizerContext.NULL_OPTIMIZATION_CONTEXT); // Last Java optimization step, update type oracle accordingly. jprogram.typeOracle.recomputeAfterOptimizations(jprogram.getDeclaredTypes()); diff --git a/dev/core/src/com/google/gwt/dev/jjs/ast/JMethod.java b/dev/core/src/com/google/gwt/dev/jjs/ast/JMethod.java index cecef6ac680..67a64d89195 100644 --- a/dev/core/src/com/google/gwt/dev/jjs/ast/JMethod.java +++ b/dev/core/src/com/google/gwt/dev/jjs/ast/JMethod.java @@ -59,6 +59,7 @@ public int compare(JMethod m1, JMethod m2) { private boolean preventDevirtualization = false; private boolean hasSideEffects = true; private boolean defaultMethod = false; + private boolean allowConstantFolding = false; private boolean syntheticAccidentalOverride = false; private Set suppressedWarnings; @@ -322,6 +323,15 @@ public boolean isJsMethodVarargs() { JParameter lastParameter = Iterables.getLast(getParams()); return lastParameter.isVarargs(); } + + public boolean isConstantFoldingAllowed() { + return allowConstantFolding; + } + + public void setAllowConstantFolding(boolean allowConstantFolding) { + this.allowConstantFolding = allowConstantFolding; + } + /** * AST representation of @SpecializeMethod. */ diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/DeadCodeElimination.java b/dev/core/src/com/google/gwt/dev/jjs/impl/DeadCodeElimination.java index 17caa5ecafa..6291e85ce3e 100644 --- a/dev/core/src/com/google/gwt/dev/jjs/impl/DeadCodeElimination.java +++ b/dev/core/src/com/google/gwt/dev/jjs/impl/DeadCodeElimination.java @@ -487,10 +487,8 @@ public void endVisit(JMethodCall x, Context ctx) { // Normal optimizations. JDeclaredType targetType = target.getEnclosingType(); - if (targetType == program.getTypeJavaLangString() || - (instance != null && - instance.getType().getUnderlyingType() == program.getTypeJavaLangString())) { - tryOptimizeStringCall(x, ctx, target); + if (target.isConstantFoldingAllowed()) { + tryFoldMethodCall(x, ctx, target); } else if (JProgram.isClinit(target)) { // Eliminate the call if the target is now empty. if (!targetType.hasClinit()) { @@ -1832,9 +1830,10 @@ private JLiteral tryGetConstant(JVariableRef x) { /** * Replace String methods having literal args with the static result. */ - private void tryOptimizeStringCall(JMethodCall x, Context ctx, JMethod method) { + private void tryFoldMethodCall(JMethodCall x, Context ctx, JMethod method) { if (method.getType() == program.getTypeVoid()) { + // TODO should warn, this shouldn't even be decorated return; } @@ -1851,23 +1850,40 @@ private void tryOptimizeStringCall(JMethodCall x, Context ctx, JMethod method) { ? x.getArgs().subList(1, x.getArgs().size()) : x.getArgs(); - // Handle toString specially to make sure it is a noop on non null string objects. - if (method.getName().endsWith("toString")) { - // replaces s.toString() with s - if (!instance.getType().canBeNull()) { - // Only replace when it known to be non null, otherwise it should follow the normal path - // and throw an NPE if null at runtime. - ctx.replaceMe(instance); - } + Class methodEnclosingType; + try { + methodEnclosingType = Class.forName(method.getEnclosingType().getName()); + } catch (ClassNotFoundException e) { + // Fall back to built-in types only + methodEnclosingType = classObjectForType(method.getEnclosingType()); + } + if (methodEnclosingType == null) { + // Can't find a type to invoke the method on, give up return; } - Object instanceLiteral = tryTranslateLiteral(instance, String.class); - method = isStaticImplMethod ? program.instanceMethodForStaticImpl(method) : method; + final Object instanceLiteral; + if (instance != null) { + instanceLiteral = tryTranslateLiteral(instance, methodEnclosingType); + method = isStaticImplMethod ? program.instanceMethodForStaticImpl(method) : method; + + // Handle toString specially to make sure it is a noop on non-null string objects. + if (method.getName().equals("toString") && method.getOriginalParamTypes().isEmpty()) { + // replaces s.toString() with s + if (!instance.getType().canBeNull() && instance.getType() == program.getTypeJavaLangString().strengthenToNonNull()) { + // Only replace when it is known to be non-null, otherwise it should follow the normal path + // and throw an NPE if null at runtime. + ctx.replaceMe(instance); + } + return; + } - if (instanceLiteral == null && !method.isStatic()) { - // Instance method on an instance that was not a literal. - return; + if (instanceLiteral == null && !method.isStatic()) { + // Instance method on an instance that was not a literal. + return; + } + } else { + instanceLiteral = null; } // Extract constant values from arguments or bail out if not possible, and also extract the @@ -1885,20 +1901,20 @@ private void tryOptimizeStringCall(JMethodCall x, Context ctx, JMethod method) { } // Finally invoke the method statically. - JLiteral resultValue = staticallyInvokeStringMethod( - method.getName(), instanceLiteral, parametersClasses, argumentConstantValues); + JLiteral resultValue = staticallyInvokeMethod( + method.getName(), methodEnclosingType, instanceLiteral, parametersClasses, argumentConstantValues); if (resultValue != null) { ctx.replaceMe(resultValue); } } - private JLiteral staticallyInvokeStringMethod( - String methodName, Object instance, Class[] parameterClasses, Object[] argumentValues) { - Method actualMethod = getMethod(methodName, String.class, parameterClasses); + private JLiteral staticallyInvokeMethod( + String methodName, Class methodEnclosingType, Object instance, Class[] parameterClasses, Object[] argumentValues) { + Method actualMethod = getMethod(methodName, methodEnclosingType, parameterClasses); if (actualMethod == null) { // Convert all parameters types to Object to find a more generic applicable method. Arrays.fill(parameterClasses, Object.class); - actualMethod = getMethod(methodName, String.class, parameterClasses); + actualMethod = getMethod(methodName, methodEnclosingType, parameterClasses); } if (actualMethod == null) { return null; @@ -1979,31 +1995,28 @@ private Object tryTranslateLiteral(JExpression maybeLiteral, Class type) { } // TODO: make this way better by a mile if (type == boolean.class && maybeLiteral instanceof JBooleanLiteral) { - return Boolean.valueOf(((JBooleanLiteral) maybeLiteral).getValue()); + return ((JBooleanLiteral) maybeLiteral).getValue(); } if (type == char.class && maybeLiteral instanceof JCharLiteral) { - return Character.valueOf(((JCharLiteral) maybeLiteral).getValue()); + return ((JCharLiteral) maybeLiteral).getValue(); } if (type == double.class && maybeLiteral instanceof JFloatLiteral) { - return new Double(((JFloatLiteral) maybeLiteral).getValue()); + return ((JFloatLiteral) maybeLiteral).getValue(); } if (type == double.class && maybeLiteral instanceof JDoubleLiteral) { - return new Double(((JDoubleLiteral) maybeLiteral).getValue()); + return ((JDoubleLiteral) maybeLiteral).getValue(); } if (type == int.class && maybeLiteral instanceof JIntLiteral) { - return Integer.valueOf(((JIntLiteral) maybeLiteral).getValue()); + return ((JIntLiteral) maybeLiteral).getValue(); } if (type == long.class && maybeLiteral instanceof JLongLiteral) { - return Long.valueOf(((JLongLiteral) maybeLiteral).getValue()); + return ((JLongLiteral) maybeLiteral).getValue(); } if (type == String.class && maybeLiteral instanceof JStringLiteral) { return ((JStringLiteral) maybeLiteral).getValue(); } - if (type == Object.class) { - // We already know it is a JValueLiteral instance - return ((JValueLiteral) maybeLiteral).getValueObj(); - } - return null; + // We already know it is a JValueLiteral instance + return ((JValueLiteral) maybeLiteral).getValueObj(); } } @@ -2072,17 +2085,27 @@ private static Set affectedMethods(OptimizerContext optimizerCtx) { public DeadCodeElimination(JProgram program) { this.program = program; + // TODO try to start with the root types were concerned with and work upwards? classObjectsByType = new ImmutableMap.Builder() .put(program.getTypeJavaLangObject(), Object.class) .put(program.getTypeJavaLangString(), String.class) + .put(program.getIndexedType("CharSequence"), CharSequence.class) .put(program.getTypePrimitiveBoolean(), boolean.class) + .put(program.getTypePrimitiveBoolean().getWrapperTypeName(), Boolean.class) .put(program.getTypePrimitiveByte(), byte.class) + .put(program.getTypePrimitiveByte().getWrapperTypeName(), Byte.class) .put(program.getTypePrimitiveChar(), char.class) + .put(program.getTypePrimitiveChar().getWrapperTypeName(), Character.class) .put(program.getTypePrimitiveDouble(), double.class) + .put(program.getTypePrimitiveDouble().getWrapperTypeName(), Double.class) .put(program.getTypePrimitiveFloat(), float.class) + .put(program.getTypePrimitiveFloat().getWrapperTypeName(), Float.class) .put(program.getTypePrimitiveInt(), int.class) + .put(program.getTypePrimitiveInt().getWrapperTypeName(), Integer.class) .put(program.getTypePrimitiveLong(), long.class) + .put(program.getTypePrimitiveLong().getWrapperTypeName(), Long.class) .put(program.getTypePrimitiveShort(), short.class) + .put(program.getTypePrimitiveShort().getWrapperTypeName(), Short.class) .build(); } diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/GwtAstBuilder.java b/dev/core/src/com/google/gwt/dev/jjs/impl/GwtAstBuilder.java index eb8b806cc27..905a2d979cc 100644 --- a/dev/core/src/com/google/gwt/dev/jjs/impl/GwtAstBuilder.java +++ b/dev/core/src/com/google/gwt/dev/jjs/impl/GwtAstBuilder.java @@ -4479,6 +4479,7 @@ private void createMethod(AbstractMethodDeclaration x) { private void processAnnotations(Annotation[] annotations, JMethod method) { maybeAddMethodSpecialization(annotations, method); + maybeMarkConstantFoldCandidate(annotations, method); maybeSetInliningMode(annotations, method); maybeSetHasNoSideEffects(annotations, method); JsInteropUtil.maybeSetJsInteropProperties(method, shouldExport(method), annotations); @@ -4541,6 +4542,13 @@ private void maybeAddMethodSpecialization(Annotation[] annotations, JMethod meth method.setSpecialization(paramTypes, returnsType, targetMethod); } + private void maybeMarkConstantFoldCandidate(Annotation[] annotations, JMethod method) { + if (JdtUtil.getAnnotationByName( + annotations, "javaemul.internal.annotations.ConstantFoldCandidate") != null) { + method.setAllowConstantFolding(true); + } + } + private void createParameter(SourceInfo info, LocalVariableBinding binding, boolean isVarargs, JMethod method, Annotation... annotations) { createParameter(info, binding, intern(binding.name), method, isVarargs, annotations); diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/MakeCallsStatic.java b/dev/core/src/com/google/gwt/dev/jjs/impl/MakeCallsStatic.java index 33e4920dd73..0c60d129586 100644 --- a/dev/core/src/com/google/gwt/dev/jjs/impl/MakeCallsStatic.java +++ b/dev/core/src/com/google/gwt/dev/jjs/impl/MakeCallsStatic.java @@ -165,6 +165,7 @@ public boolean visit(JMethod x, Context ctx) { .getAccess()); newMethod.setInliningMode(x.getInliningMode()); newMethod.setHasSideEffects(x.hasSideEffects()); + newMethod.setAllowConstantFolding(x.isConstantFoldingAllowed()); newMethod.setSynthetic(); newMethod.addThrownExceptions(x.getThrownExceptions()); if (x.isJsOverlay()) { diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/MethodInliner.java b/dev/core/src/com/google/gwt/dev/jjs/impl/MethodInliner.java index d6cec047540..7a33475923e 100644 --- a/dev/core/src/com/google/gwt/dev/jjs/impl/MethodInliner.java +++ b/dev/core/src/com/google/gwt/dev/jjs/impl/MethodInliner.java @@ -131,6 +131,11 @@ public void endVisit(JMethodCall x, Context ctx) { return; } + if (method.isConstantFoldingAllowed()) { + // Can't inline this method while it might still be constant-folded. + return; + } + JMethod.Specialization specialization = getCurrentMethod().getSpecialization(); // If we have a specialization, don't inline that away - specializations must be called // so they aren't pruned or type tightened into uselessness. diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/RemoveSpecializations.java b/dev/core/src/com/google/gwt/dev/jjs/impl/RemoveSpecializations.java index 25595175bf5..41c6177e262 100644 --- a/dev/core/src/com/google/gwt/dev/jjs/impl/RemoveSpecializations.java +++ b/dev/core/src/com/google/gwt/dev/jjs/impl/RemoveSpecializations.java @@ -34,8 +34,16 @@ public RemoveSpecializations(JProgram program) { @Override public boolean visit(JMethod x, Context ctx) { + boolean modified = false; if (x.getSpecialization() != null) { x.removeSpecialization(); + modified = true; + } + if (x.isConstantFoldingAllowed()) { + x.setAllowConstantFolding(false); + modified = true; + } + if (modified) { numMods++; } diff --git a/user/build.xml b/user/build.xml index eeb048fe690..1d352ccc0f8 100755 --- a/user/build.xml +++ b/user/build.xml @@ -15,7 +15,7 @@ - + @@ -432,7 +432,7 @@ value="${test.args.web.selenium} -draftCompile"/> + excludes="${gwt.junit.testcase.web.excludes},**/OptimizedOnly*"/> + excludes="${gwt.junit.testcase.web.excludes},**/OptimizedOnly*"/> long as an object. */ @@ -43,6 +44,7 @@ private static Long get(long l) { public static final int BYTES = SIZE / Byte.SIZE; public static final Class TYPE = long.class; + @ConstantFoldCandidate public static int bitCount(long l) { int high = LongUtils.getHighBits(l); int low = (int) l; @@ -68,6 +70,7 @@ public static int hashCode(long l) { return LongUtils.getHighBits(l) ^ (int) l; } + @ConstantFoldCandidate public static long highestOneBit(long l) { int high = LongUtils.getHighBits(l); if (high != 0) { @@ -89,6 +92,7 @@ public static long min(long a, long b) { return Math.min(a, b); } + @ConstantFoldCandidate public static int numberOfLeadingZeros(long l) { int high = LongUtils.getHighBits(l); if (high != 0) { @@ -98,6 +102,7 @@ public static int numberOfLeadingZeros(long l) { } } + @ConstantFoldCandidate public static int numberOfTrailingZeros(long l) { int low = (int) l; if (low != 0) { @@ -111,28 +116,33 @@ public static long parseLong(String s) throws NumberFormatException { return parseLong(s, 10); } + @ConstantFoldCandidate public static long parseLong(String s, int radix) throws NumberFormatException { return __parseAndValidateLong(s, radix); } + @ConstantFoldCandidate public static long reverse(long l) { int high = LongUtils.getHighBits(l); int low = (int) l; return LongUtils.fromBits(Integer.reverse(high), Integer.reverse(low)); } + @ConstantFoldCandidate public static long reverseBytes(long l) { int high = LongUtils.getHighBits(l); int low = (int) l; return LongUtils.fromBits(Integer.reverseBytes(high), Integer.reverseBytes(low)); } + @ConstantFoldCandidate public static long rotateLeft(long i, int distance) { long lowerBits = i >>> (SIZE - distance); long upperBits = i << distance; return upperBits | lowerBits; } + @ConstantFoldCandidate public static long rotateRight(long i, int distance) { long upperBits = i << (SIZE - distance); long lowerBits = i >>> distance; @@ -153,14 +163,17 @@ public static long sum(long a, long b) { return a + b; } + @ConstantFoldCandidate public static String toBinaryString(long value) { return toPowerOfTwoUnsignedString(value, 1); } + @ConstantFoldCandidate public static String toHexString(long value) { return toPowerOfTwoUnsignedString(value, 4); } + @ConstantFoldCandidate public static String toOctalString(long value) { return toPowerOfTwoUnsignedString(value, 3); } diff --git a/user/super/com/google/gwt/emul/java/lang/Math.java b/user/super/com/google/gwt/emul/java/lang/Math.java index 393d888c224..6016ea7d9b8 100644 --- a/user/super/com/google/gwt/emul/java/lang/Math.java +++ b/user/super/com/google/gwt/emul/java/lang/Math.java @@ -18,6 +18,7 @@ import static javaemul.internal.InternalPreconditions.checkCriticalArithmetic; import javaemul.internal.JsUtils; +import javaemul.internal.annotations.ConstantFoldCandidate; import jsinterop.annotations.JsMethod; import jsinterop.annotations.JsPackage; import jsinterop.annotations.JsType; @@ -33,12 +34,15 @@ public final class Math { private static final double PI_OVER_180 = PI / 180.0; private static final double PI_UNDER_180 = 180.0 / PI; + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.abs") public static native double abs(double x); + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.abs") public static native float abs(float x); + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.abs") public static native int abs(int x); @@ -46,28 +50,34 @@ public static long abs(long x) { return x < 0 ? -x : x; } + @ConstantFoldCandidate public static int absExact(int v) { checkCriticalArithmetic(v != Integer.MIN_VALUE); return abs(v); } + @ConstantFoldCandidate public static long absExact(long v) { checkCriticalArithmetic(v != Long.MIN_VALUE); return abs(v); } + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.acos") public static native double acos(double x); + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.asin") public static native double asin(double x); + @ConstantFoldCandidate public static int addExact(int x, int y) { double r = (double) x + (double) y; checkCriticalArithmetic(isSafeIntegerRange(r)); return (int) r; } + @ConstantFoldCandidate public static long addExact(long x, long y) { long r = x + y; // "Hacker's Delight" 2-12 Overflow if both arguments have the opposite sign of the result @@ -75,18 +85,23 @@ public static long addExact(long x, long y) { return r; } + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.atan") public static native double atan(double x); + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.atan2") public static native double atan2(double y, double x); + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.cbrt") public static native double cbrt(double x); + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.ceil") public static native double ceil(double x); + @ConstantFoldCandidate public static double copySign(double magnitude, double sign) { return isNegative(sign) ? -abs(magnitude) : abs(magnitude); } @@ -105,46 +120,56 @@ public static float copySign(float magnitude, float sign) { @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.cosh") public static native double cosh(double x); + @ConstantFoldCandidate public static int decrementExact(int x) { checkCriticalArithmetic(x != Integer.MIN_VALUE); return x - 1; } + @ConstantFoldCandidate public static long decrementExact(long x) { checkCriticalArithmetic(x != Long.MIN_VALUE); return x - 1; } + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.exp") public static native double exp(double x); + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.expm1") public static native double expm1(double d); + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.floor") public static native double floor(double x); + @ConstantFoldCandidate public static int floorDiv(int dividend, int divisor) { checkCriticalArithmetic(divisor != 0); // round down division if the signs are different and modulo not zero return ((dividend ^ divisor) >= 0 ? dividend / divisor : ((dividend + 1) / divisor) - 1); } + @ConstantFoldCandidate public static long floorDiv(long dividend, long divisor) { checkCriticalArithmetic(divisor != 0); // round down division if the signs are different and modulo not zero return ((dividend ^ divisor) >= 0 ? dividend / divisor : ((dividend + 1) / divisor) - 1); } + @ConstantFoldCandidate public static long floorDiv(long dividend, int divisor) { return floorDiv(dividend, (long) divisor); } + @ConstantFoldCandidate public static int floorMod(int dividend, int divisor) { checkCriticalArithmetic(divisor != 0); return ((dividend % divisor) + divisor) % divisor; } + @ConstantFoldCandidate public static long floorMod(long dividend, long divisor) { checkCriticalArithmetic(divisor != 0); return ((dividend % divisor) + divisor) % divisor; @@ -154,6 +179,7 @@ public static int floorMod(long dividend, int divisor) { return (int) floorMod(dividend, (long) divisor); } + @ConstantFoldCandidate @SuppressWarnings("CheckStyle.MethodName") public static double IEEEremainder(double v, double m) { double ratio = v / m; @@ -166,11 +192,13 @@ public static double IEEEremainder(double v, double m) { return closest == 0 ? v : v - m * closest; } + @ConstantFoldCandidate public static int getExponent(double v) { int[] intBits = JsUtils.doubleToRawIntBits(v); return ((intBits[1] >> 20) & 2047) - Double.MAX_EXPONENT; } + @ConstantFoldCandidate public static int getExponent(float v) { return ((JsUtils.floatToRawIntBits(v) >> 23) & 255) - Float.MAX_EXPONENT; } @@ -194,34 +222,43 @@ public static float ulp(float v) { return (float) Math.pow(2, exponent - 23); } + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.hypot") public static native double hypot(double x, double y); + @ConstantFoldCandidate public static int incrementExact(int x) { checkCriticalArithmetic(x != Integer.MAX_VALUE); return x + 1; } + @ConstantFoldCandidate public static long incrementExact(long x) { checkCriticalArithmetic(x != Long.MAX_VALUE); return x + 1; } + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.log") public static native double log(double x); + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.log10") public static native double log10(double x); + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.log1p") public static native double log1p(double x); + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.max") public static native double max(double x, double y); + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.max") public static native float max(float x, float y); + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.max") public static native int max(int x, int y); @@ -229,12 +266,15 @@ public static long max(long x, long y) { return x > y ? x : y; } + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.min") public static native double min(double x, double y); + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.min") public static native float min(float x, float y); + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.min") public static native int min(int x, int y); @@ -286,9 +326,11 @@ public static long negateExact(long x) { return -x; } + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.pow") public static native double pow(double x, double exp); + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.random") public static native double random(); @@ -345,6 +387,7 @@ public static float scalb(float f, int scaleFactor) { return (float) scalb((double) f, scaleFactor); } + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.sign") public static native double signum(double d); @@ -352,18 +395,23 @@ public static float signum(float f) { return (float) signum((double) f); } + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.sin") public static native double sin(double x); + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.sinh") public static native double sinh(double x); + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.sqrt") public static native double sqrt(double x); + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.tan") public static native double tan(double x); + @ConstantFoldCandidate @JsMethod(namespace = JsPackage.GLOBAL, name = "Math.tanh") public static native double tanh(double x); @@ -371,6 +419,7 @@ public static double toDegrees(double x) { return x * PI_UNDER_180; } + @ConstantFoldCandidate public static int toIntExact(long x) { int ix = (int) x; checkCriticalArithmetic(ix == x); @@ -381,6 +430,7 @@ public static double toRadians(double x) { return x * PI_OVER_180; } + @ConstantFoldCandidate public static double nextAfter(double start, double direction) { // Simple case described by Javadoc: if (start == direction) { diff --git a/user/super/com/google/gwt/emul/java/lang/String.java b/user/super/com/google/gwt/emul/java/lang/String.java index 67b1cff1107..6af19ac1b04 100644 --- a/user/super/com/google/gwt/emul/java/lang/String.java +++ b/user/super/com/google/gwt/emul/java/lang/String.java @@ -42,6 +42,7 @@ import javaemul.internal.EmulatedCharset; import javaemul.internal.JsUtils; import javaemul.internal.NativeRegExp; +import javaemul.internal.annotations.ConstantFoldCandidate; import javaemul.internal.annotations.DoNotInline; import jsinterop.annotations.JsMethod; import jsinterop.annotations.JsNonNull; @@ -364,24 +365,29 @@ private NativeString asNativeString() { } @Override + @ConstantFoldCandidate public char charAt(int index) { checkStringElementIndex(index, length()); return asNativeString().charCodeAt(index); } + @ConstantFoldCandidate public int codePointAt(int index) { return Character.codePointAt(this, index, length()); } + @ConstantFoldCandidate public int codePointBefore(int index) { return Character.codePointBefore(this, index, 0); } + @ConstantFoldCandidate public int codePointCount(int beginIndex, int endIndex) { return Character.codePointCount(this, beginIndex, endIndex); } @Override + @ConstantFoldCandidate public int compareTo(String other) { // Trick compiler into thinking that these are double so what we could do arithmetic comparison // which is supported on underlying JavaScript strings. @@ -390,31 +396,38 @@ public int compareTo(String other) { return a == b ? 0 : (a < b ? -1 : 1); } + @ConstantFoldCandidate public int compareToIgnoreCase(String other) { return toLowerCase().compareTo(other.toLowerCase()); } + @ConstantFoldCandidate public String concat(String str) { return checkNotNull(this) + checkNotNull(str); } + @ConstantFoldCandidate public boolean contains(CharSequence s) { return asNativeString().includes(s.toString()); } + @ConstantFoldCandidate public boolean contentEquals(CharSequence cs) { return equals(cs.toString()); } + @ConstantFoldCandidate public boolean contentEquals(StringBuffer sb) { return equals(sb.toString()); } + @ConstantFoldCandidate public boolean endsWith(String suffix) { return asNativeString().endsWith(suffix); } // Marked with @DoNotInline because we don't have static eval for "==" yet. + @ConstantFoldCandidate @DoNotInline @Override public boolean equals(Object other) { @@ -423,6 +436,7 @@ public boolean equals(Object other) { return checkNotNull(this) == other; } + @ConstantFoldCandidate public boolean equalsIgnoreCase(String other) { checkNotNull(this); if (other == null) { @@ -460,6 +474,7 @@ private void getChars0(int srcBegin, int srcEnd, char[] dst, int dstBegin) { } @Override + @ConstantFoldCandidate public int hashCode() { int h = 0; for (int i = 0; i < length(); i++) { @@ -469,42 +484,52 @@ public int hashCode() { return h; } + @ConstantFoldCandidate public int indexOf(int codePoint) { return indexOf(fromCodePoint(codePoint)); } + @ConstantFoldCandidate public int indexOf(int codePoint, int startIndex) { return indexOf(fromCodePoint(codePoint), startIndex); } + @ConstantFoldCandidate public int indexOf(String str) { return asNativeString().indexOf(str); } + @ConstantFoldCandidate public int indexOf(String str, int startIndex) { return asNativeString().indexOf(str, startIndex); } + @ConstantFoldCandidate public String intern() { return checkNotNull(this); } + @ConstantFoldCandidate public int lastIndexOf(int codePoint) { return lastIndexOf(fromCodePoint(codePoint)); } + @ConstantFoldCandidate public int lastIndexOf(int codePoint, int startIndex) { return lastIndexOf(fromCodePoint(codePoint), startIndex); } + @ConstantFoldCandidate public int lastIndexOf(String str) { return asNativeString().lastIndexOf(str); } + @ConstantFoldCandidate public int lastIndexOf(String str, int start) { return start < 0 ? -1 : asNativeString().lastIndexOf(str, start); } + @ConstantFoldCandidate @Override public int length() { return asNativeString().length; @@ -528,6 +553,7 @@ public int offsetByCodePoints(int index, int codePointOffset) { return Character.offsetByCodePoints(this, index, codePointOffset); } + @ConstantFoldCandidate public boolean regionMatches(boolean ignoreCase, int toffset, String other, int ooffset, int len) { checkNotNull(other); @@ -550,6 +576,7 @@ public boolean regionMatches(int toffset, String other, int ooffset, int len) { return regionMatches(false, toffset, other, ooffset, len); } + @ConstantFoldCandidate public String replace(char from, char to) { // Translate 'from' into unicode escape sequence (\\u and a four-digit hexadecimal number). // Escape sequence replacement is used instead of a string literal replacement @@ -560,6 +587,7 @@ public String replace(char from, char to) { return nativeReplaceAll(regex, replace); } + @ConstantFoldCandidate public String replace(CharSequence from, CharSequence to) { // Implementation note: This uses a regex replacement instead of // a string literal replacement because Safari does not @@ -585,6 +613,7 @@ public String replace(CharSequence from, CharSequence to) { * * TODO(jat): properly handle Java regex syntax */ + @ConstantFoldCandidate public String replaceAll(String regex, String replace) { replace = translateReplaceString(replace); return nativeReplaceAll(regex, replace); @@ -602,6 +631,7 @@ String nativeReplaceAll(String regex, String replace) { * * TODO(jat): properly handle Java regex syntax */ + @ConstantFoldCandidate public String replaceFirst(String regex, String replace) { replace = translateReplaceString(replace); NativeRegExp jsRegEx = new NativeRegExp(regex); @@ -689,24 +719,29 @@ public String[] split(String regex, int maxMatch) { return out; } + @ConstantFoldCandidate public boolean startsWith(String prefix) { return asNativeString().startsWith(prefix); } + @ConstantFoldCandidate public boolean startsWith(String prefix, int toffset) { return asNativeString().startsWith(prefix, toffset); } + @ConstantFoldCandidate @Override public CharSequence subSequence(int beginIndex, int endIndex) { return substring(beginIndex, endIndex); } + @ConstantFoldCandidate public String substring(int beginIndex) { checkStringElementIndex(beginIndex, length() + 1); return asNativeString().substr(beginIndex); } + @ConstantFoldCandidate public String substring(int beginIndex, int endIndex) { checkStringBounds(beginIndex, endIndex, length()); return asNativeString().substr(beginIndex, endIndex - beginIndex); @@ -727,6 +762,7 @@ public char[] toCharArray() { * a transformation based on native locale of the browser, you can do * {@code toLowerCase(Locale.getDefault())} instead. */ + @ConstantFoldCandidate public String toLowerCase() { return asNativeString().toLowerCase(); } @@ -737,27 +773,32 @@ public String toLowerCase() { * to {@code toLowerCase} which performs the right thing for the limited set of Locale's * predefined in GWT Locale emulation. */ + @ConstantFoldCandidate public String toLowerCase(Locale locale) { return locale == Locale.getDefault() ? asNativeString().toLocaleLowerCase() : asNativeString().toLowerCase(); } // See the notes in lowerCase pair. + @ConstantFoldCandidate public String toUpperCase() { return asNativeString().toUpperCase(); } // See the notes in lowerCase pair. + @ConstantFoldCandidate public String toUpperCase(Locale locale) { return locale == Locale.getDefault() ? asNativeString().toLocaleUpperCase() : asNativeString().toUpperCase(); } @Override + @ConstantFoldCandidate public String toString() { return checkNotNull(this); } + @ConstantFoldCandidate public String trim() { int length = length(); int start = 0; @@ -771,6 +812,7 @@ public String trim() { return start > 0 || end < length ? substring(start, end) : this; } + @ConstantFoldCandidate public String strip() { int length = length(); int start = getLeadingWhitespaceLength(); @@ -780,14 +822,17 @@ public String strip() { return substring(start, length - getTrailingWhitespaceLength()); } + @ConstantFoldCandidate public String stripLeading() { return substring(getLeadingWhitespaceLength()); } + @ConstantFoldCandidate public String stripTrailing() { return substring(0, length() - getTrailingWhitespaceLength()); } + @ConstantFoldCandidate public boolean isBlank() { return length() == getLeadingWhitespaceLength(); } @@ -796,6 +841,7 @@ public Stream lines() { return StreamSupport.stream(new LinesSpliterator(), false); } + @ConstantFoldCandidate public String repeat(int count) { checkArgument(count >= 0, "count is negative: " + count); return asNativeString().repeat(count); @@ -825,6 +871,7 @@ private int getTrailingWhitespaceLength() { return length; } + @ConstantFoldCandidate public String indent(int spaces) { if (isEmpty()) { return ""; @@ -840,6 +887,7 @@ public String indent(int spaces) { return indentedLines.collect(Collectors.joining("\n", "", "\n")); } + @ConstantFoldCandidate public String stripIndent() { if (isEmpty()) { return ""; diff --git a/user/super/com/google/gwt/emul/javaemul/internal/annotations/ConstantFoldCandidate.java b/user/super/com/google/gwt/emul/javaemul/internal/annotations/ConstantFoldCandidate.java new file mode 100644 index 00000000000..0a3b783614a --- /dev/null +++ b/user/super/com/google/gwt/emul/javaemul/internal/annotations/ConstantFoldCandidate.java @@ -0,0 +1,43 @@ +/* + * Copyright 2026 GWT Project Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package javaemul.internal.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * An annotation to mark a method as a candidate for constant folding by the compiler. This method + * should not have its contents inlined until after the optimization loop has halted, then it can be + * inlined. During the loop, the compiler may replaces calls to this method if all arguments are + * literal constants, and if the instance is a constant as well, and if the method has an analogous + * implementation in the JDK. The method is assumed to be free from side effects. + *

+ * Only Strings and boxed primitives are valid instances for the purposes of this optimization + * though static methods in other classes may be considered as well if they only take literal + * parameters at a callsite. + *

+ * It is generally unnecessary to annotate methods that just perform standard math operations, + * since the compiler can inline and execute those operations on constants already. + *

+ * TODO what about methods that throw exceptions? + * TODO what about enums? + *

+ * Internal SDK use only, might change or disappear at any time. + */ +@CompilerHint +@Target(ElementType.METHOD) +public @interface ConstantFoldCandidate { +} \ No newline at end of file diff --git a/user/super/com/google/gwt/emul/javaemul/internal/annotations/ForceInline.java b/user/super/com/google/gwt/emul/javaemul/internal/annotations/ForceInline.java index c0561c2b127..ebcc9dd7099 100644 --- a/user/super/com/google/gwt/emul/javaemul/internal/annotations/ForceInline.java +++ b/user/super/com/google/gwt/emul/javaemul/internal/annotations/ForceInline.java @@ -19,7 +19,8 @@ import java.lang.annotation.Target; /** - * An annotation to mark a given method as not inlineable. + * An annotation to mark a given method as not inlineable. If the method + * cannot be inlined for any reason, the compiler will fail. *

* Internal SDK use only, might change or disappear at any time. */ diff --git a/user/test/com/google/gwt/dev/jjs/OptimizedOnlyCompilerSuite.java b/user/test/com/google/gwt/dev/jjs/OptimizedOnlyCompilerSuite.java index bb5c85a8bb5..3cfa70b23c0 100644 --- a/user/test/com/google/gwt/dev/jjs/OptimizedOnlyCompilerSuite.java +++ b/user/test/com/google/gwt/dev/jjs/OptimizedOnlyCompilerSuite.java @@ -41,7 +41,7 @@ public static Test suite() { suite.addTestSuite(ArrayStoreOptimizationTest.class); suite.addTestSuite(StringOptimizationTest.class); suite.addTestSuite(CastOptimizationTest.class); - suite.addTestSuite(JsOverlayMethodOptimizationTest.class); + suite.addTestSuite(JsOverlayMethodOptimizationTest.class); suite.addTestSuite(SpecializationTest.class); suite.addTestSuite(HasNoSideEffectsTest.class); suite.addTestSuite(UncheckedCastOptimizationTest.class);