Enable inlining of functions that use local variables.

Patch by: bobv
Review by: scott, spoon


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@2070 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/js/JsInliner.java b/dev/core/src/com/google/gwt/dev/js/JsInliner.java
index a58cad4..b098846 100644
--- a/dev/core/src/com/google/gwt/dev/js/JsInliner.java
+++ b/dev/core/src/com/google/gwt/dev/js/JsInliner.java
@@ -55,14 +55,15 @@
 import com.google.gwt.dev.js.ast.JsThisRef;
 import com.google.gwt.dev.js.ast.JsThrow;
 import com.google.gwt.dev.js.ast.JsUnaryOperation;
+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.JsVars.JsVar;
 
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.IdentityHashMap;
 import java.util.List;
@@ -158,6 +159,12 @@
         return;
       }
 
+      // If (X, a) and X has no side effects, replace with a
+      if (!hasSideEffects(x.getArg1())) {
+        ctx.replaceMe(x.getArg2());
+        return;
+      }
+
       JsBinaryOperation toUpdate = isComma(x.getArg2());
       if (toUpdate == null) {
         /*
@@ -675,6 +682,7 @@
   private static class InliningVisitor extends JsModVisitor {
     private final Set<JsFunction> blacklist = new HashSet<JsFunction>();
     private final Stack<JsFunction> functionStack = new Stack<JsFunction>();
+    private final Stack<List<JsName>> newLocalVariableStack = new Stack<List<JsName>>();
     private final JsProgram program;
 
     public InliningVisitor(JsProgram program) {
@@ -775,10 +783,40 @@
       if (!functionStack.pop().equals(x)) {
         throw new InternalCompilerException("Unexpected function popped");
       }
+
+      List<JsName> newLocalVariables = newLocalVariableStack.pop();
+
+      // Nothing to do
+      if (newLocalVariables.isEmpty()) {
+        return;
+      }
+
+      List<JsStatement> statements = x.getBody().getStatements();
+
+      // The body can't be empty if we have local variables to create
+      assert !statements.isEmpty();
+
+      // Find or create the JsVars as the first statement
+      JsVars vars;
+      if (statements.get(0) instanceof JsVars) {
+        vars = (JsVars) statements.get(0);
+      } else {
+        vars = new JsVars();
+        statements.add(0, vars);
+      }
+
+      // Add all variables
+      for (JsName name : newLocalVariables) {
+        vars.add(new JsVar(name));
+      }
     }
 
     @Override
     public void endVisit(JsInvocation x, JsContext<JsExpression> ctx) {
+      if (functionStack.isEmpty()) {
+        return;
+      }
+
       /*
        * We only want to look at invocations of things that we statically know
        * to be functions. Otherwise, we can't know what statements the
@@ -796,7 +834,9 @@
         return;
       }
 
-      List<JsStatement> statements = f.getBody().getStatements();
+      List<JsName> localVariableNames = new ArrayList<JsName>();
+      List<JsStatement> statements = new ArrayList<JsStatement>(
+          f.getBody().getStatements());
       List<JsExpression> hoisted = new ArrayList<JsExpression>(
           statements.size());
 
@@ -810,7 +850,8 @@
          * distinct objects, it would not be possible to substitute different
          * JsNameRefs at different call sites.
          */
-        JsExpression h = hoistedExpression(statement);
+        JsExpression h = hoistedExpression(program, statement,
+            localVariableNames);
         if (h == null) {
           return;
         }
@@ -857,6 +898,25 @@
 
       // Perform the name replacement
       NameRefReplacerVisitor v = new NameRefReplacerVisitor(x, f);
+      for (ListIterator<JsName> nameIterator = localVariableNames.listIterator(); nameIterator.hasNext();) {
+        JsName name = nameIterator.next();
+
+        /*
+         * Find an unused identifier in the caller's scope. It's possible that
+         * the same function has been inlined in multiple places within the
+         * function so we'll use a counter for disambiguation.
+         */
+        String ident;
+        int count = 0;
+        JsScope scope = functionStack.peek().getScope();
+        do {
+          ident = f.getName() + "_" + name.getIdent() + "_" + count++;
+        } while (scope.findExistingName(ident) != null);
+
+        JsName newName = scope.declareName(ident, name.getShortIdent());
+        v.setReplacementName(name, newName);
+        nameIterator.set(newName);
+      }
       op = v.accept(op);
 
       // Normalize any nested comma expressions that we may have generated.
@@ -872,6 +932,9 @@
         return;
       }
 
+      // We've committed to the inlining, ensure the vars are created
+      newLocalVariableStack.peek().addAll(localVariableNames);
+
       /*
        * See if any further inlining can be performed in the current context. By
        * attempting to maximize the level of inlining now, we can reduce the
@@ -885,6 +948,7 @@
     @Override
     public boolean visit(JsFunction x, JsContext<JsExpression> ctx) {
       functionStack.push(x);
+      newLocalVariableStack.push(new ArrayList<JsName>());
       return true;
     }
   }
@@ -897,7 +961,12 @@
      * Set up a map of parameter names back to the expressions that will be
      * passed in from the outer call site.
      */
-    final Map<JsName, JsExpression> paramsToArgsMap = new HashMap<JsName, JsExpression>();
+    final Map<JsName, JsExpression> paramsToArgsMap = new IdentityHashMap<JsName, JsExpression>();
+
+    /**
+     * Set up a map to record name replacements to perform.
+     */
+    final Map<JsName, JsName> nameReplacements = new IdentityHashMap<JsName, JsName>();
 
     /**
      * Constructor.
@@ -933,14 +1002,44 @@
         return;
       }
 
-      /*
-       * TODO if we ever allow mutable JsExpression types to be considered
-       * always flexible, then it would be necessary to clone the expression.
-       */
-      JsExpression original = paramsToArgsMap.get(x.getName());
+      JsExpression replacement = tryGetReplacementExpression(x.getName());
 
-      if (original != null) {
-        ctx.replaceMe(original);
+      if (replacement != null) {
+        ctx.replaceMe(replacement);
+      }
+    }
+
+    /**
+     * Set a replacement JsName for all references to a JsName.
+     * 
+     * @param name the name to replace
+     * @param newName the new name that should be used in place of references to
+     *          <code>name</code>
+     * @return the previous JsName the name would have been replaced with or
+     *         <code>null</code> if one was not previously set
+     */
+    public JsName setReplacementName(JsName name, JsName newName) {
+      return nameReplacements.put(name, newName);
+    }
+
+    /**
+     * Determine the replacement expression to use in place of a reference to a
+     * given name. Returns <code>null</code> if no replacement has been set
+     * for the name.
+     */
+    private JsExpression tryGetReplacementExpression(JsName name) {
+      if (paramsToArgsMap.containsKey(name)) {
+        /*
+         * TODO if we ever allow mutable JsExpression types to be considered
+         * always flexible, then it would be necessary to clone the expression.
+         */
+        return paramsToArgsMap.get(name);
+
+      } else if (nameReplacements.containsKey(name)) {
+        return nameReplacements.get(name).makeRef();
+
+      } else {
+        return null;
       }
     }
   }
@@ -1271,6 +1370,9 @@
       } else if (callerName != null && callerName.equals(calleeName)) {
         // The names are known to us and are the same
 
+      } else if (calleeName.getEnclosing().equals(calleeScope)) {
+        // It's a local variable in the callee
+
       } else {
         stable = false;
       }
@@ -1389,13 +1491,6 @@
   }
 
   /**
-   * Determine whether or not an AST node has side effects.
-   */
-  private static <T extends JsVisitable<T>> boolean hasSideEffects(T e) {
-    return hasSideEffects(Collections.singletonList(e));
-  }
-
-  /**
    * Determine whether or not a list of AST nodes have side effects.
    */
   private static <T extends JsVisitable<T>> boolean hasSideEffects(List<T> list) {
@@ -1405,23 +1500,70 @@
   }
 
   /**
+   * Determine whether or not an AST node has side effects.
+   */
+  private static <T extends JsVisitable<T>> boolean hasSideEffects(T e) {
+    return hasSideEffects(Collections.singletonList(e));
+  }
+
+  /**
    * Given a delegated JsStatement, construct an expression to hoist into the
    * outer caller. This does not perform any name replacement, but simply
    * constructs a mutable copy of the expression that can be manipulated
    * at-will.
+   * 
+   * @param program the enclosing JsProgram
+   * @param statement the statement from which to extract the expressions
+   * @param localVariableNames accumulates any local varables declared by
+   *          <code>statement</code>
+   * @return a JsExpression representing all expressions that would have been
+   *         evaluated by the statement
    */
-  private static JsExpression hoistedExpression(JsStatement statement) {
+  private static JsExpression hoistedExpression(JsProgram program,
+      JsStatement statement, List<JsName> localVariableNames) {
     JsExpression expression;
     if (statement instanceof JsExprStmt) {
+      // Extract the expression
       JsExprStmt exprStmt = (JsExprStmt) statement;
       expression = exprStmt.getExpression();
+
     } else if (statement instanceof JsReturn) {
+      // Extract the return value
       JsReturn ret = (JsReturn) statement;
       expression = ret.getExpr();
+
+    } else if (statement instanceof JsVars) {
+      // Create a comma expression for variable initializers
+      JsVars vars = (JsVars) statement;
+
+      // Rely on comma expression cleanup to remove this later.
+      expression = program.getNullLiteral();
+
+      for (JsVar var : vars) {
+        // Record the locally-defined variable
+        localVariableNames.add(var.getName());
+
+        // Extract the initialization expression
+        JsExpression init = var.getInitExpr();
+        if (init != null) {
+          JsBinaryOperation assignment = new JsBinaryOperation(
+              JsBinaryOperator.ASG);
+          assignment.setArg1(var.getName().makeRef());
+          assignment.setArg2(init);
+
+          // Multiple initializers go into a comma expression
+          JsBinaryOperation comma = new JsBinaryOperation(
+              JsBinaryOperator.COMMA);
+          comma.setArg1(expression);
+          comma.setArg2(assignment);
+          expression = comma;
+        }
+      }
     } else {
       return null;
     }
 
+    assert expression != null;
     return JsHoister.hoist(expression);
   }