-
Notifications
You must be signed in to change notification settings - Fork 384
Simplify boolean expressions in JavaScript AST #10261
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<JsStatement> mustExec = new ArrayList<JsStatement>(); | ||
|
|
||
| public MustExecVisitor() { | ||
| MustExecVisitor() { | ||
| } | ||
|
|
||
| @Override | ||
|
|
@@ -144,21 +145,53 @@ 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. | ||
| * | ||
| * TODO: borrow more concepts from | ||
| * {@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<JsExpression> evalBooleanContext = new HashSet<JsExpression>(); | ||
| /** | ||
| * Stores how are expression evaluations used. | ||
| * Missing entry = no coercion, difference between null and false matters. | ||
| */ | ||
| private final Map<JsExpression, EvalMode> evalContext = new HashMap<>(); | ||
|
|
||
| /** | ||
| * This is used by {@link #additionCoercesToString}. | ||
| */ | ||
| private Map<JsExpression, Boolean> coercesToStringMap = new IdentityHashMap<JsExpression, Boolean>(); | ||
| private Map<JsExpression, Boolean> 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); | ||
|
zbynek marked this conversation as resolved.
|
||
| 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. | ||
| * | ||
| * <pre> | ||
| * 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() | ||
| * </pre> | ||
| * In boolean context also | ||
| * <pre> | ||
| * if (isWhatever() && true()) -> if (isWhatever()) unless true() has side effects | ||
| * if (isWhatever() && false()) -> if (false()) unless isWhatever() side effects | ||
| * </pre> | ||
| */ | ||
| protected static JsExpression shortCircuitAnd(JsBinaryOperation expr) { | ||
| protected static JsExpression shortCircuitAnd(JsBinaryOperation expr, | ||
| Map<JsExpression, EvalMode> 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)) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same concern, just a containsKey, so it could be explicitly VOID. |
||
| 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. | ||
| * | ||
| * <pre> | ||
| * 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() | ||
| * </pre> | ||
| * In boolean context also | ||
| * <pre> | ||
| * if (isWhatever() || false()) -> if (isWhatever()) unless false() has side effects | ||
| * if (isWhatever() || true()) -> if (true()) unless isWhatever() side effects | ||
| * </pre> | ||
| */ | ||
| protected static JsExpression shortCircuitOr(JsBinaryOperation expr) { | ||
| protected static JsExpression shortCircuitOr(JsBinaryOperation expr, | ||
| Map<JsExpression, EvalMode> 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)) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should this containsKey(expr) be a get(expr) == BOOL? It might have been missed in the last update, but seems implied by the javadoc. If it is VOID, I would assume we actually throw away arg2... but maybe a later patch (see my comment on the TODO for this nested type). |
||
| 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; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're quite close to throwing away ignored expression results here. Probably not worth adding a few more VOID checks at this stage