Improve JsStaticEval to be able to perform optimizations on trees of JsBinaryOperations.

Patch by: bobv
Review by: cromwellian

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@6736 8db76d5a-ed1c-0410-87a9-c151d255dfc7
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 6cb7c4d..6845e21 100644
--- a/dev/core/src/com/google/gwt/dev/js/JsStaticEval.java
+++ b/dev/core/src/com/google/gwt/dev/js/JsStaticEval.java
@@ -21,6 +21,7 @@
 import com.google.gwt.dev.js.ast.JsBinaryOperation;
 import com.google.gwt.dev.js.ast.JsBinaryOperator;
 import com.google.gwt.dev.js.ast.JsBlock;
+import com.google.gwt.dev.js.ast.JsBooleanLiteral;
 import com.google.gwt.dev.js.ast.JsBreak;
 import com.google.gwt.dev.js.ast.JsConditional;
 import com.google.gwt.dev.js.ast.JsContext;
@@ -42,14 +43,17 @@
 import com.google.gwt.dev.js.ast.JsUnaryOperator;
 import com.google.gwt.dev.js.ast.JsValueLiteral;
 import com.google.gwt.dev.js.ast.JsVars;
+import com.google.gwt.dev.js.ast.JsVisitable;
 import com.google.gwt.dev.js.ast.JsVisitor;
 import com.google.gwt.dev.js.ast.JsWhile;
-import com.google.gwt.dev.js.ast.JsBooleanLiteral;
 import com.google.gwt.dev.js.ast.JsVars.JsVar;
 
 import java.util.ArrayList;
+import java.util.EnumSet;
 import java.util.HashSet;
+import java.util.IdentityHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -151,12 +155,21 @@
 
     private Set<JsExpression> evalBooleanContext = new HashSet<JsExpression>();
 
+    /**
+     * This is used by {@link #additionCoercesToString}.
+     */
+    private Map<JsExpression, Boolean> coercesToStringMap = new IdentityHashMap<JsExpression, Boolean>();
+
     @Override
     public void endVisit(JsBinaryOperation x, JsContext<JsExpression> ctx) {
       JsBinaryOperator op = x.getOperator();
       JsExpression arg1 = x.getArg1();
       JsExpression arg2 = x.getArg2();
-      if (op == JsBinaryOperator.AND) {
+
+      if (MATH_ASSOCIATIVE.contains(op)
+          && trySimplifyAssociativeExpression(x, ctx)) {
+        // Nothing else to do
+      } else if (op == JsBinaryOperator.AND) {
         shortCircuitAnd(arg1, arg2, ctx);
       } else if (op == JsBinaryOperator.OR) {
         shortCircuitOr(arg1, arg2, ctx);
@@ -454,6 +467,52 @@
       return true;
     }
 
+    /**
+     * Given an expression, determine if the addition operator would cause a
+     * string coercion to happen.
+     */
+    private boolean additionCoercesToString(JsExpression expr) {
+      if (expr instanceof JsStringLiteral) {
+        return true;
+      }
+
+      /*
+       * Because the nodes passed into this method are visited on exit, it is
+       * worthwile to memoize the result for this function.
+       */
+      Boolean toReturn = coercesToStringMap.get(expr);
+      if (toReturn != null) {
+        return toReturn;
+      }
+      toReturn = false;
+
+      if (expr instanceof JsBinaryOperation) {
+        JsBinaryOperation op = (JsBinaryOperation) expr;
+        switch (op.getOperator()) {
+          case ADD:
+            toReturn = additionCoercesToString(op.getArg1())
+                || additionCoercesToString(op.getArg2());
+            break;
+          case COMMA:
+            toReturn = additionCoercesToString(op.getArg2());
+            break;
+        }
+
+        if (op.getOperator().isAssignment()) {
+          toReturn = additionCoercesToString(op.getArg2());
+        }
+      }
+
+      /*
+       * TODO: Consider adding heuristics to detect String(foo), typeof(foo),
+       * and foo.toString(). The latter is debatable, since an implementation
+       * might not actually return a string.
+       */
+
+      coercesToStringMap.put(expr, toReturn);
+      return toReturn;
+    }
+
     private boolean appendLiteral(StringBuilder result, JsValueLiteral val) {
       if (val instanceof JsNumberLiteral) {
         double number = ((JsNumberLiteral) val).getValue();
@@ -469,7 +528,7 @@
       }
       return true;
     }
-        
+
     /**
      * This method MUST be called whenever any statements are removed from a
      * function. This is because some statements, such as JsVars or JsFunction
@@ -502,7 +561,7 @@
     }
 
     /*
-     * String.valueOf(Double) produces trailing .0 on integers which is 
+     * String.valueOf(Double) produces trailing .0 on integers which is
      * incorrect for Javascript which produces conversions to string without
      * trailing zeroes. Without this, int + String will turn out wrong.
      */
@@ -514,7 +573,7 @@
       }
       return num;
     }
-        
+
     private SourceInfo makeSourceInfo(HasSourceInfo x, String m) {
       return x.getSourceInfo().makeChild(StaticEvalVisitor.class, m);
     }
@@ -592,23 +651,121 @@
         JsExpression arg2, JsContext<JsExpression> ctx) {
       if (arg1 instanceof JsValueLiteral && arg2 instanceof JsValueLiteral) {
         // case: number + number
-        if (arg1 instanceof JsNumberLiteral && 
-            arg2 instanceof JsNumberLiteral) {
-          ctx.replaceMe(program.getNumberLiteral(
-              ((JsNumberLiteral) arg1).getValue() + 
-              ((JsNumberLiteral) arg2).getValue()));
+        if (arg1 instanceof JsNumberLiteral && arg2 instanceof JsNumberLiteral) {
+          double value = ((JsNumberLiteral) arg1).getValue()
+              + ((JsNumberLiteral) arg2).getValue();
+          ctx.replaceMe(program.getNumberLiteral(value));
         } else {
           // cases: number + string or string + number
           StringBuilder result = new StringBuilder();
-          if (appendLiteral(result, (JsValueLiteral) arg1) && 
-              appendLiteral(result, (JsValueLiteral) arg2)) {
+          if (appendLiteral(result, (JsValueLiteral) arg1)
+              && appendLiteral(result, (JsValueLiteral) arg2)) {
             ctx.replaceMe(program.getStringLiteral(original.getSourceInfo(),
                 result.toString()));
           }
         }
       }
     }
-  
+
+    /**
+     * Attempts to simplify adjoining binary expressions with mathematically
+     * associative operators. This pass also tries to make these binary
+     * expressions as left-normal as possible.
+     */
+    private boolean trySimplifyAssociativeExpression(JsBinaryOperation x,
+        JsContext<JsExpression> ctx) {
+      boolean toReturn = false;
+      JsBinaryOperator op = x.getOperator();
+      JsExpression arg1 = x.getArg1();
+      JsExpression arg2 = x.getArg2();
+
+      /*
+       * First, we'll try to normalize the nesting of any binary expressions
+       * that we encounter. If we do this correctly,it will help to cut down on
+       * the number of unnecessary parens in the emitted JS.
+       */
+      // (X) O (c O d) ==> ((X) O c) O d
+      {
+        JsBinaryOperation rightOp = null;
+        if (arg2 instanceof JsBinaryOperation) {
+          rightOp = (JsBinaryOperation) arg2;
+        }
+        if (rightOp != null && !rightOp.getOperator().isAssignment()
+            && op == rightOp.getOperator()) {
+
+          if (op == JsBinaryOperator.ADD) {
+            /*
+             * JS type coercion is a problem if we don't know for certain that
+             * the right-hand expression will definitely be evaluated in a
+             * string context.
+             */
+            boolean mustBeString = additionCoercesToString(rightOp.getArg1())
+                || (additionCoercesToString(arg1) && additionCoercesToString(rightOp.getArg2()));
+            if (!mustBeString) {
+              return toReturn;
+            }
+          }
+
+          // (X) O c --> Try to reduce this
+          JsExpression newLeft = new JsBinaryOperation(x.getSourceInfo(), op,
+              arg1, rightOp.getArg1());
+
+          // Reset local vars with new state
+          op = rightOp.getOperator();
+          arg1 = accept(newLeft);
+          arg2 = rightOp.getArg2();
+          x = new JsBinaryOperation(x.getSourceInfo(), op, arg1, arg2);
+
+          ctx.replaceMe(x);
+          toReturn = didChange = true;
+        }
+      }
+
+      /*
+       * Now that we know that our AST is as left-normal as we can make it
+       * (because this method is called from endVisit), we now try to simplify
+       * the left-right node and the right node.
+       */
+      // (a O b) O c ==> a O s
+      {
+        JsBinaryOperation leftOp = null;
+        JsExpression leftLeft = null;
+        JsExpression leftRight = null;
+
+        if (arg1 instanceof JsBinaryOperation) {
+          leftOp = (JsBinaryOperation) arg1;
+          if (op.getPrecedence() == leftOp.getOperator().getPrecedence()) {
+            leftLeft = leftOp.getArg1();
+            leftRight = leftOp.getArg2();
+          }
+        }
+
+        if (leftRight != null) {
+          if (op == JsBinaryOperator.ADD) {
+            // Behavior as described above
+            boolean mustBeString = additionCoercesToString(leftRight)
+                || (additionCoercesToString(leftLeft) && additionCoercesToString(arg2));
+            if (!mustBeString) {
+              return toReturn;
+            }
+          }
+
+          // (b O c)
+          JsBinaryOperation middle = new JsBinaryOperation(x.getSourceInfo(),
+              op, leftRight, arg2);
+          StaticEvalVisitor v = new StaticEvalVisitor();
+          JsExpression maybeSimplified = v.accept(middle);
+
+          if (v.didChange()) {
+            x.setArg1(leftLeft);
+            x.setArg2(maybeSimplified);
+            toReturn = didChange = true;
+          }
+        }
+      }
+      return toReturn;
+    }
+
     private void trySimplifyEq(JsExpression original, JsExpression arg1,
         JsExpression arg2, JsContext<JsExpression> ctx) {
       JsExpression updated = simplifyEq(original, arg1, arg2);
@@ -661,10 +818,22 @@
     }
   }
 
+  /**
+   * A set of the JS operators that are mathematically associative in nature.
+   */
+  private static final Set<JsBinaryOperator> MATH_ASSOCIATIVE = EnumSet.of(
+      JsBinaryOperator.ADD, JsBinaryOperator.AND, JsBinaryOperator.BIT_AND,
+      JsBinaryOperator.BIT_OR, JsBinaryOperator.BIT_XOR,
+      JsBinaryOperator.COMMA, JsBinaryOperator.MUL, JsBinaryOperator.OR);
+
   public static boolean exec(JsProgram program) {
     return (new JsStaticEval(program)).execImpl();
   }
 
+  public static <T extends JsVisitable> T exec(JsProgram program, T node) {
+    return (new JsStaticEval(program)).execImpl(node);
+  }
+
   /**
    * Attempts to extract a single expression from a given statement and returns
    * it. If no such expression exists, returns <code>null</code>.
@@ -769,4 +938,8 @@
 
     return sev.didChange();
   }
+
+  public <T extends JsVisitable> T execImpl(T node) {
+    return new StaticEvalVisitor().accept(node);
+  }
 }
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 ea54e44..b68fd15 100644
--- a/dev/core/test/com/google/gwt/dev/js/JsStaticEvalTest.java
+++ b/dev/core/test/com/google/gwt/dev/js/JsStaticEvalTest.java
@@ -30,6 +30,65 @@
     assertEquals("alert('Hello 42.2');", optimize("alert('Hello ' + 42.2);"));
   }
 
+  public void testAssociativity() throws Exception {
+    // Simple test
+    assertEquals("alert(a||b||c||d);", optimize("alert((a||b)||(c||d));"));
+    assertEquals("alert(a&&b&&c&&d);", optimize("alert((a&&b)&&(c&&d));"));
+
+    // Preserve precedence
+    assertEquals("alert((a||b)&&(c||d));",
+        optimize("alert((a || b) && (c || d));"));
+    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()"));
+
+    // Don't damage math expressions
+    assertEquals("alert(seconds/(60*60));",
+        optimize("alert(seconds / (60 * 60))"));
+    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(1+(a='2')+3+4);",
+        optimize("alert(1 + (a = '2') + 3 + 4);"));
+    assertEquals("alert(1+(a='2')+7);",
+        optimize("alert(1 + (a = '2') + (3 + 4));"));
+
+    // Break comma expressions up
+    assertEquals("alert((a(),b(),c(),d));",
+        optimize("alert(((a(),b()),(c(),d)));"));
+    // and remove expressions without side effects
+    assertEquals("alert(d);", optimize("alert(((a,b),(c,d)));"));
+
+    // Pattern of coercing a numeric add operation to a string
+    assertEquals("alert(''+(a+b));", optimize("alert('' + (a + b))"));
+    assertEquals("alert('foo'+(a+b));",
+        optimize("alert('foo' + ('' + (a + b)))"));
+
+    // Tests involving numeric and string literals and identifiers
+    assertEquals("alert(21+(1+$foo));",
+        optimize("alert((20 + 1) + (1 + $foo));"));
+    assertEquals("alert('211'+$foo);",
+        optimize("alert((20 + 1) + ('1' + $foo));"));
+    assertEquals("alert('2011'+$foo);",
+        optimize("alert((20 + '1') + ('1' + $foo));"));
+    assertEquals("alert('2011'+$foo);",
+        optimize("alert(('20' + 1) + ('1' + $foo));"));
+    assertEquals("alert('2011'+$foo);",
+        optimize("alert(('20' + '1') + ('1' + $foo));"));
+
+    // These are also tricky, because $foo could be non-numeric
+    assertEquals("alert($foo+1+21);", optimize("alert(($foo + 1) + (20 + 1));"));
+    assertEquals("alert($bar+13+7+(2+$foo));",
+        optimize("alert((($bar + (10 + 3)) + (2 + 5)) + (2 + $foo));"));
+
+    // Without type info, there's nothing that can be done for this expr
+    assertEquals("alert($foo+($bar+($baz+$quux)));",
+        optimize("alert($foo + ($bar + ($baz + $quux)));"));
+  }
+
   public void testIfWithEmptyThen() throws Exception {
     assertEquals("a();", optimize("if (a()) { }"));
   }