diff --git a/dev/core/src/com/google/gwt/dev/js/JsStaticEval.java b/dev/core/src/com/google/gwt/dev/js/JsStaticEval.java index d5edef08cc..cf3420a517 100644 --- a/dev/core/src/com/google/gwt/dev/js/JsStaticEval.java +++ b/dev/core/src/com/google/gwt/dev/js/JsStaticEval.java @@ -50,10 +50,11 @@ import com.google.gwt.dev.js.ast.JsWhile; import com.google.gwt.dev.js.rhino.ScriptRuntime; import com.google.gwt.dev.util.Ieee754_64_Arithmetic; +import com.google.gwt.thirdparty.guava.common.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.EnumSet; -import java.util.HashSet; +import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; @@ -104,7 +105,7 @@ private static class MustExecVisitor extends JsVisitor { private final List mustExec = new ArrayList(); - public MustExecVisitor() { + MustExecVisitor() { } @Override @@ -144,6 +145,14 @@ public boolean visit(JsFunction x, JsContext ctx) { } } + /** + * Describes how is the result of evaluation used. + */ + private enum EvalMode { + BOOL, // result will be coerced to a boolean + VOID // result will be discarded + } + /** * Does static evals. * @@ -151,14 +160,38 @@ public boolean visit(JsFunction x, JsContext ctx) { * {@link com.google.gwt.dev.jjs.impl.DeadCodeElimination}, such as ignored * expression results. */ - private class StaticEvalVisitor extends JsModVisitor { + @VisibleForTesting + static class StaticEvalVisitor extends JsModVisitor { - private Set evalBooleanContext = new HashSet(); + /** + * Stores how are expression evaluations used. + * Missing entry = no coercion, difference between null and false matters. + */ + private final Map evalContext = new HashMap<>(); /** * This is used by {@link #additionCoercesToString}. */ - private Map coercesToStringMap = new IdentityHashMap(); + private Map coercesToStringMap = new IdentityHashMap<>(); + + public boolean visit(JsExprStmt x, JsContext ctx) { + evalContext.put(x.getExpression(), EvalMode.VOID); + return true; + } + + public boolean visit(JsBinaryOperation x, JsContext ctx) { + if (evalContext.containsKey(x) + && (x.getOperator() == JsBinaryOperator.AND || x.getOperator() == JsBinaryOperator.OR)) { + evalContext.put(x.getArg1(), EvalMode.BOOL); + evalContext.put(x.getArg2(), evalContext.get(x)); + } else if (x.getOperator() == JsBinaryOperator.COMMA) { + evalContext.put(x.getArg1(), EvalMode.VOID); + if (evalContext.containsKey(x)) { + evalContext.put(x.getArg2(), evalContext.get(x)); + } + } + return true; + } @Override public void endVisit(JsBinaryOperation x, JsContext ctx) { @@ -167,11 +200,13 @@ public void endVisit(JsBinaryOperation x, JsContext ctx) { JsExpression arg2 = x.getArg2(); JsExpression result = x; - - if (op == JsBinaryOperator.AND) { - result = shortCircuitAnd(x); + if (evalContext.get(x) == EvalMode.VOID + && !arg2.hasSideEffects() && !op.isAssignment()) { + result = arg1; + } else if (op == JsBinaryOperator.AND) { + result = shortCircuitAnd(x, evalContext); } else if (op == JsBinaryOperator.OR) { - result = shortCircuitOr(x); + result = shortCircuitOr(x, evalContext); } else if (op == JsBinaryOperator.COMMA) { result = trySimplifyComma(x); } else if (op == JsBinaryOperator.EQ || op == JsBinaryOperator.REF_EQ) { @@ -195,7 +230,8 @@ public void endVisit(JsBinaryOperation x, JsContext ctx) { break; } } - + evalContext.remove(arg1); + evalContext.remove(arg2); result = maybeReorderOperations(result); if (result != x) { @@ -256,21 +292,29 @@ public void endVisit(JsBlock x, JsContext ctx) { @Override public void endVisit(JsConditional x, JsContext ctx) { - evalBooleanContext.remove(x.getTestExpression()); + evalContext.remove(x.getTestExpression()); + evalContext.remove(x.getThenExpression()); + evalContext.remove(x.getElseExpression()); JsExpression condExpr = x.getTestExpression(); JsExpression thenExpr = x.getThenExpression(); JsExpression elseExpr = x.getElseExpression(); - if (condExpr instanceof CanBooleanEval) { + if (condExpr instanceof JsBinaryOperation + && ((JsBinaryOperation) condExpr).getOperator() == JsBinaryOperator.COMMA) { + JsBinaryOperation condition = (JsBinaryOperation) condExpr; + JsExpression newConditional = accept(new JsConditional(x.getSourceInfo(), + condition.getArg2(), thenExpr, elseExpr)); + ctx.replaceMe(withSideEffect(newConditional, condition.getArg1(), x.getSourceInfo())); + } else if (condExpr instanceof CanBooleanEval) { CanBooleanEval condEval = (CanBooleanEval) condExpr; if (condEval.isBooleanTrue()) { JsBinaryOperation binOp = new JsBinaryOperation(x.getSourceInfo(), - JsBinaryOperator.AND, condExpr, thenExpr); + JsBinaryOperator.COMMA, condExpr, thenExpr); ctx.replaceMe(accept(binOp)); } else if (condEval.isBooleanFalse()) { - // e.g. (false() ? then : else) -> false() || else + // e.g. (false() ? then : else) -> (false() , else) JsBinaryOperation binOp = new JsBinaryOperation(x.getSourceInfo(), - JsBinaryOperator.OR, condExpr, elseExpr); + JsBinaryOperator.COMMA, condExpr, elseExpr); ctx.replaceMe(accept(binOp)); } } @@ -281,7 +325,7 @@ public void endVisit(JsConditional x, JsContext ctx) { */ @Override public void endVisit(JsDoWhile x, JsContext ctx) { - evalBooleanContext.remove(x.getCondition()); + evalContext.remove(x.getCondition()); JsExpression expr = x.getCondition(); if (expr instanceof CanBooleanEval) { @@ -304,6 +348,7 @@ public void endVisit(JsDoWhile x, JsContext ctx) { @Override public void endVisit(JsExprStmt x, JsContext ctx) { + evalContext.remove(x.getExpression()); if (!x.getExpression().hasSideEffects()) { if (ctx.canRemove()) { ctx.removeMe(); @@ -318,7 +363,7 @@ public void endVisit(JsExprStmt x, JsContext ctx) { */ @Override public void endVisit(JsFor x, JsContext ctx) { - evalBooleanContext.remove(x.getCondition()); + evalContext.remove(x.getCondition()); JsExpression expr = x.getCondition(); if (expr instanceof CanBooleanEval) { @@ -348,7 +393,7 @@ public void endVisit(JsFor x, JsContext ctx) { */ @Override public void endVisit(JsIf x, JsContext ctx) { - evalBooleanContext.remove(x.getIfExpr()); + evalContext.remove(x.getIfExpr()); JsExpression condExpr = x.getIfExpr(); if (condExpr instanceof CanBooleanEval) { @@ -402,10 +447,10 @@ public void endVisit(JsIf x, JsContext ctx) { @Override public void endVisit(JsPrefixOperation x, JsContext ctx) { if (x.getOperator() == JsUnaryOperator.NOT) { - evalBooleanContext.remove(x.getArg()); + evalContext.remove(x.getArg()); } - if (evalBooleanContext.contains(x)) { + if (evalContext.containsKey(x)) { if ((x.getOperator() == JsUnaryOperator.NOT) && (x.getArg() instanceof JsPrefixOperation)) { JsPrefixOperation arg = (JsPrefixOperation) x.getArg(); @@ -422,7 +467,7 @@ public void endVisit(JsPrefixOperation x, JsContext ctx) { */ @Override public void endVisit(JsWhile x, JsContext ctx) { - evalBooleanContext.remove(x.getCondition()); + evalContext.remove(x.getCondition()); JsExpression expr = x.getCondition(); if (expr instanceof CanBooleanEval) { @@ -443,39 +488,44 @@ public void endVisit(JsWhile x, JsContext ctx) { @Override public boolean visit(JsConditional x, JsContext ctx) { - evalBooleanContext.add(x.getTestExpression()); + evalContext.put(x.getTestExpression(), EvalMode.BOOL); + EvalMode evalMode = evalContext.get(x); + if (evalMode != null) { + evalContext.put(x.getThenExpression(), evalMode); + evalContext.put(x.getElseExpression(), evalMode); + } return true; } @Override public boolean visit(JsDoWhile x, JsContext ctx) { - evalBooleanContext.add(x.getCondition()); + evalContext.put(x.getCondition(), EvalMode.BOOL); return true; } @Override public boolean visit(JsFor x, JsContext ctx) { - evalBooleanContext.add(x.getCondition()); + evalContext.put(x.getCondition(), EvalMode.BOOL); return true; } @Override public boolean visit(JsIf x, JsContext ctx) { - evalBooleanContext.add(x.getIfExpr()); + evalContext.put(x.getIfExpr(), EvalMode.BOOL); return true; } @Override public boolean visit(JsPrefixOperation x, JsContext ctx) { if (x.getOperator() == JsUnaryOperator.NOT) { - evalBooleanContext.add(x.getArg()); + evalContext.put(x.getArg(), EvalMode.BOOL); } return true; } @Override public boolean visit(JsWhile x, JsContext ctx) { - evalBooleanContext.add(x.getCondition()); + evalContext.put(x.getCondition(), EvalMode.BOOL); return true; } @@ -607,41 +657,75 @@ public static int exec(JsProgram program) { * Simplify short circuit AND expressions. * *
-   * if (true && isWhatever()) -> if (isWhatever()), unless side effects
-   * if (false() && isWhatever()) -> if (false())
+   * return true() && isWhatever() -> return isWhatever(), unless true() has side effects
+   * return false() && isWhatever() -> return false()
+   * 
+ * In boolean context also + *
+   * if (isWhatever() && true()) -> if (isWhatever()) unless true() has side effects
+   * if (isWhatever() && false()) -> if (false()) unless isWhatever() side effects
    * 
*/ - protected static JsExpression shortCircuitAnd(JsBinaryOperation expr) { + protected static JsExpression shortCircuitAnd(JsBinaryOperation expr, + Map evalContext) { JsExpression arg1 = expr.getArg1(); JsExpression arg2 = expr.getArg2(); if (arg1 instanceof CanBooleanEval) { CanBooleanEval eval1 = (CanBooleanEval) arg1; - if (eval1.isBooleanTrue() && !arg1.hasSideEffects()) { - return arg2; + if (eval1.isBooleanTrue()) { + return withSideEffect(arg2, arg1, expr.getSourceInfo()); } else if (eval1.isBooleanFalse()) { return arg1; } } + if (arg2 instanceof CanBooleanEval && evalContext.containsKey(expr)) { + CanBooleanEval eval2 = (CanBooleanEval) arg2; + if (eval2.isBooleanTrue() && !arg2.hasSideEffects()) { + return arg1; + } else if (eval2.isBooleanFalse()) { + return withSideEffect(arg2, arg1, expr.getSourceInfo()); + } + } return expr; } + private static JsExpression withSideEffect(JsExpression main, JsExpression sideEffect, + SourceInfo sourceInfo) { + return !sideEffect.hasSideEffects() ? main + : new JsBinaryOperation(sourceInfo, JsBinaryOperator.COMMA, sideEffect, main); + } + /** * Simplify short circuit OR expressions. * *
-   * if (true() || isWhatever()) -> if (true())
-   * if (false || isWhatever()) -> if (isWhatever()), unless side effects
+   * return false() || isWhatever() -> return isWhatever(), unless false() has side effects
+   * return true() && isWhatever() -> return true()
+   * 
+ * In boolean context also + *
+   * if (isWhatever() || false()) -> if (isWhatever()) unless false() has side effects
+   * if (isWhatever() || true()) -> if (true()) unless isWhatever() side effects
    * 
*/ - protected static JsExpression shortCircuitOr(JsBinaryOperation expr) { + protected static JsExpression shortCircuitOr(JsBinaryOperation expr, + Map evalContext) { JsExpression arg1 = expr.getArg1(); JsExpression arg2 = expr.getArg2(); if (arg1 instanceof CanBooleanEval) { CanBooleanEval eval1 = (CanBooleanEval) arg1; - if (eval1.isBooleanTrue()) { + if (eval1.isBooleanFalse()) { + return withSideEffect(arg2, arg1, expr.getSourceInfo()); + } else if (eval1.isBooleanTrue()) { + return arg1; + } + } + if (arg2 instanceof CanBooleanEval && evalContext.containsKey(expr)) { + CanBooleanEval eval2 = (CanBooleanEval) arg2; + if (eval2.isBooleanFalse() && !arg2.hasSideEffects()) { return arg1; - } else if (eval1.isBooleanFalse() && !arg1.hasSideEffects()) { - return arg2; + } else if (eval2.isBooleanTrue()) { + return withSideEffect(arg2, arg1, expr.getSourceInfo()); } } return expr; diff --git a/dev/core/test/com/google/gwt/dev/js/JsStaticEvalTest.java b/dev/core/test/com/google/gwt/dev/js/JsStaticEvalTest.java index 3177b09319..d2e7ca5c5f 100644 --- a/dev/core/test/com/google/gwt/dev/js/JsStaticEvalTest.java +++ b/dev/core/test/com/google/gwt/dev/js/JsStaticEvalTest.java @@ -43,7 +43,8 @@ public void testAssociativity() throws Exception { assertEquals("alert(a&&b||c&&d);", optimize("alert((a && b) || ( c && d));")); assertEquals("a(),b&&c();", optimize("a(), b && c()")); - assertEquals("a()&&b,c();", optimize("a() && b, c()")); + assertEquals("a()&&b(),c();", optimize("a() && b(), c()")); + assertEquals("a(),c();", optimize("a() && b, c()")); // Don't damage math expressions assertEquals("alert(seconds/3600);", @@ -53,8 +54,8 @@ public void testAssociativity() throws Exception { assertEquals("alert(1-(1-foo));", optimize("alert(1 - (1 - foo))")); // Don't damage assignments - assertEquals("alert((a=0,b=foo));", - optimize("alert((a = 0, b = (bar, foo)))")); + assertEquals("alert((a=7,b=foo));", + optimize("alert((a = 7, b = (bar, foo)))")); assertEquals("alert(1+(a='2')+3+4);", optimize("alert(1 + (a = '2') + 3 + 4);")); assertEquals("alert(1+(a='2')+7);", @@ -87,7 +88,8 @@ public void testAssociativity() throws Exception { /** * Test for issue 7088. JsStatic eval infinite loop in - * {@link JsStaticEval.StaticEvalVisitor#endVisit(JsBlock, JsContext)} + * {@link JsStaticEval.StaticEvalVisitor#endVisit(com.google.gwt.dev.js.ast.JsBlock, + * com.google.gwt.dev.js.ast.JsContext)} */ public void testDeclareAfterReturn() throws Exception { // TODO(rluble): Note that the source output has the wrong precedence for function definition @@ -154,6 +156,68 @@ public void testLiteralCompares() throws Exception { assertEquals("alert(true);", optimize("alert(\"a\" !== null)")); } + public void testShortCircuitAnd() throws Exception { + assertEquals("alert(a);", optimize("alert(true && a)")); + assertEquals("alert(false);", optimize("alert(false && a)")); + + // these can't be simplified to maintain type + assertEquals("alert(a&&true);", optimize("alert(a && true)")); + assertEquals("alert(a&&false);", optimize("alert(a && false)")); + assertEquals("alert(!!a&&!!b);", optimize("alert(!!a && !!b)")); + assertEquals("alert(c|(bits&&1));", optimize("alert(c | (a, bits && 1))")); + + // in boolean context we can simplify more + assertEquals("alert(a&&b?c:d);", optimize("alert(!!a && !!b ? c :d)")); + assertEquals("alert(d);", optimize("alert(false && !!b ? c :d)")); + assertEquals("alert(b?c:d);", optimize("alert(true && !!b ? c :d)")); + assertEquals("alert(b?c:d1);", optimize("alert(b && true ? c :d1)")); + assertEquals("alert(d1);", optimize("alert(b && false ? c :d1)")); + assertEquals("alert(d2);", optimize("alert(a && false && b ? c :d2)")); + } + + public void testShortCircuitAndWithSideEffects() throws Exception { + assertEquals("a&&b();", optimize("!!a && !!b()")); + assertEquals("a();", optimize("a() && false && c();")); + assertEquals("a(),e();", optimize("a() && false && c() ? d() : e();")); + assertEquals("a()&&c()?d():e();", optimize("a() && true && c() ? d() : e();")); + } + + public void testSimplifyCommaInVoidContext() throws Exception { + assertEquals("a()&&b();", optimize("a()&&(b(),undefined)")); + assertEquals("a()?b():c();", optimize("a()?(b(),undefined):(c(),undefined)")); + } + + public void testSimplifyComma() throws Exception { + assertEquals("alert(!!a());", optimize("alert((true, !!a()))")); + assertEquals("alert((a(),true));", optimize("alert((!!a(), true))")); + } + + public void testShortCircuitOr() throws Exception { + assertEquals("alert(true);", optimize("alert(true || a)")); + assertEquals("alert(a);", optimize("alert(false || a)")); + + // these can't be simplified to maintain type + assertEquals("alert(a||true);", optimize("alert(a || true)")); + assertEquals("alert(a||false);", optimize("alert(a || false)")); + assertEquals("alert(!!a||!!b);", optimize("alert(!!a || !!b)")); + assertEquals("alert(c|(bits||0));", optimize("alert(c | (a, bits || 0))")); + + // in boolean context we can simplify more + assertEquals("alert(a||b?c:d);", optimize("alert(!!a || !!b ? c :d)")); + assertEquals("alert(c);", optimize("alert(true || !!b ? c :d)")); + assertEquals("alert(b?c:d);", optimize("alert(false || !!b ? c :d)")); + assertEquals("alert(b?c:d1);", optimize("alert(b || false ? c :d1)")); + assertEquals("alert(c1);", optimize("alert(b || true ? c1 :d)")); + assertEquals("alert(c2);", optimize("alert(a || true || b ? c2 :d)")); + } + + public void testShortCircuitOrWithSideEffects() throws Exception { + assertEquals("a||b();", optimize("!!a || !!b()")); + assertEquals("a();", optimize("a() || true || c();")); + assertEquals("a(),d();", optimize("a() || true || c() ? d() : e();")); + assertEquals("a()||c()?d():e();", optimize("a() || false || c() ? d() : e();")); + } + public void testLiteralEqNull() throws Exception { assertEquals("alert(false);", optimize("alert('test' == null)")); }