Fix JsInliner for mutually-recursive functions.

Patch by: bobv
Review by: spoon

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@5597 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 1379f8a..0a8b1a2 100644
--- a/dev/core/src/com/google/gwt/dev/js/JsInliner.java
+++ b/dev/core/src/com/google/gwt/dev/js/JsInliner.java
@@ -722,6 +722,14 @@
    */
   private static class InliningVisitor extends JsModVisitor {
     private final Set<JsFunction> blacklist = new HashSet<JsFunction>();
+    /**
+     * This reflects the functions that are currently being inlined to prevent
+     * infinite expansion.
+     */
+    private final Stack<JsFunction> inlining = new Stack<JsFunction>();
+    /**
+     * This reflects which function the visitor is currently visiting.
+     */
     private final Stack<JsFunction> functionStack = new Stack<JsFunction>();
     private final InvocationCountingVisitor invocationCountingVisitor = new InvocationCountingVisitor();
     private final Stack<List<JsName>> newLocalVariableStack = new Stack<List<JsName>>();
@@ -859,13 +867,13 @@
        * when trying operate on references to external functions, or functions
        * as arguments to another function.
        */
-      JsFunction f = isFunction(x.getQualifier());
-      if (f == null) {
+      JsFunction invokedFunction = isFunction(x.getQualifier());
+      if (invokedFunction == null) {
         return;
       }
 
       // Don't inline blacklisted functions
-      if (blacklist.contains(f)) {
+      if (blacklist.contains(invokedFunction)) {
         return;
       }
 
@@ -873,165 +881,38 @@
        * The current function has been mutated so as to be self-recursive. Ban
        * it from any future inlining to prevent infinite expansion.
        */
-      if (f == callerFunction) {
-        blacklist.add(f);
+      if (invokedFunction == callerFunction) {
+        blacklist.add(invokedFunction);
         return;
       }
 
-      List<JsStatement> statements;
-      if (f.getBody() != null) {
-        statements = new ArrayList<JsStatement>(f.getBody().getStatements());
-      } else {
+      /*
+       * We are already in the middle of attempting to inline a call to this
+       * function. This check prevents infinite expansion across
+       * mutually-recursive, inlinable functions. Any invocation skipped by this
+       * logic will be re-visited in the <code>op = accept(op)</code> call in
+       * the outermost JsInvocation.
+       */
+      if (inlining.contains(invokedFunction)) {
+        return;
+      }
+
+      inlining.push(invokedFunction);
+      JsExpression op = process(x, callerFunction, invokedFunction);
+
+      if (x != op) {
         /*
-         * Will see this with certain classes whose clinits are folded into the
-         * main JsProgram body.
+         * 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 total number of passes required to finalize the AST.
          */
-        statements = Collections.emptyList();
+        op = accept(op);
+        ctx.replaceMe(op);
       }
 
-      List<JsExpression> hoisted = new ArrayList<JsExpression>(
-          statements.size());
-      List<JsName> localVariableNames = new ArrayList<JsName>();
-      boolean sawReturnStatement = false;
-
-      for (JsStatement statement : statements) {
-        if (sawReturnStatement) {
-          /*
-           * We've already seen a return statement, but there are still more
-           * statements. The target is unsafe to inline, so bail. Note: in most
-           * cases JsStaticEval will have removed any statements following a
-           * return statement.
-           * 
-           * The reason we have to bail is that the return statement's
-           * expression MUST be the last thing evaluated.
-           * 
-           * TODO(bobv): maybe it could still be inlined with smart
-           * transformation?
-           */
-          return;
-        }
-
-        /*
-         * Create replacement expressions to use in place of the original
-         * statements. It is important that the replacement is newly-minted and
-         * therefore not referenced by any other AST nodes. Consider the case of
-         * a common, delegating function. If the hoisted expressions were not
-         * distinct objects, it would not be possible to substitute different
-         * JsNameRefs at different call sites.
-         */
-        JsExpression h = hoistedExpression(program, statement,
-            localVariableNames);
-        if (h == null) {
-          return;
-        }
-
-        if (isReturnStatement(statement)) {
-          sawReturnStatement = true;
-          hoisted.add(h);
-        } else if (hasSideEffects(Collections.singletonList(h))) {
-          hoisted.add(h);
-        }
+      if (inlining.pop() != invokedFunction) {
+        throw new RuntimeException("Unexpected function popped");
       }
-
-      /*
-       * If the inlined method has no return statement, synthesize an undefined
-       * reference. It will be reclaimed if the method call is from a
-       * JsExprStmt.
-       */
-      if (!sawReturnStatement) {
-        hoisted.add(program.getUndefinedLiteral());
-      }
-
-      assert (hoisted.size() > 0);
-
-      /*
-       * Build up the new comma expression from right-to-left; building the
-       * rightmost comma expressions first. Bootstrapping with i.previous()
-       * ensures that this logic will function correctly in the case of a single
-       * expression.
-       */
-      SourceInfo sourceInfo = x.getSourceInfo().makeChild(
-          InliningVisitor.class, "Inlined invocation");
-      ListIterator<JsExpression> i = hoisted.listIterator(hoisted.size());
-      JsExpression op = i.previous();
-      while (i.hasPrevious()) {
-        JsBinaryOperation outerOp = new JsBinaryOperation(sourceInfo,
-            JsBinaryOperator.COMMA);
-        outerOp.setArg1(i.previous());
-        outerOp.setArg2(op);
-        op = outerOp;
-      }
-
-      // Confirm that the expression conforms to the desired heuristics
-      if (!isInlinable(program, callerFunction, f, x.getArguments(), op)) {
-        return;
-      }
-
-      // 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;
-        String base = f.getName() + "_" + name.getIdent();
-        JsScope scope = callerFunction.getScope();
-        HashMap<String, Integer> startIdent = startIdentForScope.get(scope);
-        if (startIdent == null) {
-          startIdent = new HashMap<String, Integer>();
-          startIdentForScope.put(scope, startIdent);
-        }
-
-        Integer s = startIdent.get(base);
-        int suffix = (s == null) ? 0 : s.intValue();
-        do {
-          ident = base + "_" + suffix++;
-        } while (scope.findExistingName(ident) != null);
-        startIdent.put(base, suffix);
-
-        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.
-      op = (new CommaNormalizer(localVariableNames)).accept(op);
-
-      /*
-       * Compare the relative complexity of the original invocation versus the
-       * inlined form.
-       */
-      int originalComplexity = complexity(x);
-      int inlinedComplexity = complexity(op);
-      double ratio = ((double) inlinedComplexity) / originalComplexity;
-      if (ratio > MAX_COMPLEXITY_INCREASE && isInvokedMoreThanOnce(f)) {
-        return;
-      }
-
-      if (callerFunction == programFunction && localVariableNames.size() > 0) {
-        // Don't add additional variables to the top-level program.
-        return;
-      }
-
-      // We've committed to the inlining, ensure the vars are created
-      newLocalVariableStack.peek().addAll(localVariableNames);
-
-      // update invocation counts according to this inlining
-      invocationCountingVisitor.removeCountsFor(x);
-      invocationCountingVisitor.accept(op);
-
-      /*
-       * 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
-       * total number of passes required to finalize the AST.
-       */
-      op = accept(op);
-      ctx.replaceMe(op);
     }
 
     @Override
@@ -1111,6 +992,166 @@
       Integer count = invocationCountingVisitor.invocationCount(f);
       return count == null || count > 1;
     }
+
+    /**
+     * Determine if <code>invokedFunction</code> can be inlined into
+     * <code>callerFunction</code> at callsite <code>x</code>.
+     * 
+     * @return An expression equivalent to <code>x</code>
+     */
+    private JsExpression process(JsInvocation x, JsFunction callerFunction,
+        JsFunction invokedFunction) {
+      List<JsStatement> statements;
+      if (invokedFunction.getBody() != null) {
+        statements = new ArrayList<JsStatement>(
+            invokedFunction.getBody().getStatements());
+      } else {
+        /*
+         * Will see this with certain classes whose clinits are folded into the
+         * main JsProgram body.
+         */
+        statements = Collections.emptyList();
+      }
+
+      List<JsExpression> hoisted = new ArrayList<JsExpression>(
+          statements.size());
+      List<JsName> localVariableNames = new ArrayList<JsName>();
+      boolean sawReturnStatement = false;
+
+      for (JsStatement statement : statements) {
+        if (sawReturnStatement) {
+          /*
+           * We've already seen a return statement, but there are still more
+           * statements. The target is unsafe to inline, so bail. Note: in most
+           * cases JsStaticEval will have removed any statements following a
+           * return statement.
+           * 
+           * The reason we have to bail is that the return statement's
+           * expression MUST be the last thing evaluated.
+           * 
+           * TODO(bobv): maybe it could still be inlined with smart
+           * transformation?
+           */
+          return x;
+        }
+
+        /*
+         * Create replacement expressions to use in place of the original
+         * statements. It is important that the replacement is newly-minted and
+         * therefore not referenced by any other AST nodes. Consider the case of
+         * a common, delegating function. If the hoisted expressions were not
+         * distinct objects, it would not be possible to substitute different
+         * JsNameRefs at different call sites.
+         */
+        JsExpression h = hoistedExpression(program, statement,
+            localVariableNames);
+        if (h == null) {
+          return x;
+        }
+
+        if (isReturnStatement(statement)) {
+          sawReturnStatement = true;
+          hoisted.add(h);
+        } else if (hasSideEffects(Collections.singletonList(h))) {
+          hoisted.add(h);
+        }
+      }
+
+      /*
+       * If the inlined method has no return statement, synthesize an undefined
+       * reference. It will be reclaimed if the method call is from a
+       * JsExprStmt.
+       */
+      if (!sawReturnStatement) {
+        hoisted.add(program.getUndefinedLiteral());
+      }
+
+      assert (hoisted.size() > 0);
+
+      /*
+       * Build up the new comma expression from right-to-left; building the
+       * rightmost comma expressions first. Bootstrapping with i.previous()
+       * ensures that this logic will function correctly in the case of a single
+       * expression.
+       */
+      SourceInfo sourceInfo = x.getSourceInfo().makeChild(
+          InliningVisitor.class, "Inlined invocation");
+      ListIterator<JsExpression> i = hoisted.listIterator(hoisted.size());
+      JsExpression op = i.previous();
+      while (i.hasPrevious()) {
+        JsBinaryOperation outerOp = new JsBinaryOperation(sourceInfo,
+            JsBinaryOperator.COMMA);
+        outerOp.setArg1(i.previous());
+        outerOp.setArg2(op);
+        op = outerOp;
+      }
+
+      // Confirm that the expression conforms to the desired heuristics
+      if (!isInlinable(program, callerFunction, invokedFunction,
+          x.getArguments(), op)) {
+        return x;
+      }
+
+      // Perform the name replacement
+      NameRefReplacerVisitor v = new NameRefReplacerVisitor(x, invokedFunction);
+      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;
+        String base = invokedFunction.getName() + "_" + name.getIdent();
+        JsScope scope = callerFunction.getScope();
+        HashMap<String, Integer> startIdent = startIdentForScope.get(scope);
+        if (startIdent == null) {
+          startIdent = new HashMap<String, Integer>();
+          startIdentForScope.put(scope, startIdent);
+        }
+
+        Integer s = startIdent.get(base);
+        int suffix = (s == null) ? 0 : s.intValue();
+        do {
+          ident = base + "_" + suffix++;
+        } while (scope.findExistingName(ident) != null);
+        startIdent.put(base, suffix);
+
+        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.
+      op = (new CommaNormalizer(localVariableNames)).accept(op);
+
+      /*
+       * Compare the relative complexity of the original invocation versus the
+       * inlined form.
+       */
+      int originalComplexity = complexity(x);
+      int inlinedComplexity = complexity(op);
+      double ratio = ((double) inlinedComplexity) / originalComplexity;
+      if (ratio > MAX_COMPLEXITY_INCREASE
+          && isInvokedMoreThanOnce(invokedFunction)) {
+        return x;
+      }
+
+      if (callerFunction == programFunction && localVariableNames.size() > 0) {
+        // Don't add additional variables to the top-level program.
+        return x;
+      }
+
+      // We've committed to the inlining, ensure the vars are created
+      newLocalVariableStack.peek().addAll(localVariableNames);
+
+      // update invocation counts according to this inlining
+      invocationCountingVisitor.removeCountsFor(x);
+      invocationCountingVisitor.accept(op);
+      return op;
+    }
   }
 
   /**
diff --git a/dev/core/test/com/google/gwt/dev/js/JsInlinerTest.java b/dev/core/test/com/google/gwt/dev/js/JsInlinerTest.java
index bcbe315..b652df4 100644
--- a/dev/core/test/com/google/gwt/dev/js/JsInlinerTest.java
+++ b/dev/core/test/com/google/gwt/dev/js/JsInlinerTest.java
@@ -27,27 +27,50 @@
  */
 public class JsInlinerTest extends OptimizerTestBase {
 
-  public void testSelfRecursion() throws Exception {
-    String input = "function a1() { return blah && b1() }"
-        + "function b1() { return bar && a1()}" + "function c() { a1() } c()";
-    input = optimize(input, JsSymbolResolver.class, FixStaticRefsVisitor.class,
-        JsInliner.class, JsUnusedFunctionRemover.class);
-
-    String expected = "function a1() { return blah && bar && a1() }"
-        + "function c() { a1() } c()";
-    expected = optimize(expected);
-    assertEquals(expected, input);
-  }
-
   private static class FixStaticRefsVisitor extends JsModVisitor {
+    public static void exec(JsProgram program) {
+      (new FixStaticRefsVisitor()).accept(program);
+    }
+
     @Override
     public void endVisit(JsFunction x, JsContext<JsExpression> ctx) {
       JsName name = x.getName();
       name.setStaticRef(x);
     }
+  }
 
-    public static void exec(JsProgram program) {
-      (new FixStaticRefsVisitor()).accept(program);
-    }
+  /**
+   * A test for mutually-recursive functions. Setup:
+   * 
+   * <pre>
+   * a -> b, c
+   * b -> a, c
+   * c -> a, c
+   * </pre>
+   */
+  public void testMutualRecursion() throws Exception {
+    String input = "function a1() { return ex ? b1() : c1() }"
+        + "function b1() { return ex2 ? a1(): c1(); }"
+        + "function c1() { return ex2? a1():c1(); } c1()";
+    String expected = "function a1() { return ex ? (ex2 ? a1() : c1()) : c1() }"
+        + "function c1() { return ex2 ? a1() :c1(); } c1()";
+    compare(expected, input);
+  }
+
+  public void testSelfRecursion() throws Exception {
+    String input = "function a1() { return blah && b1() }"
+        + "function b1() { return bar && a1()}" + "function c() { a1() } c()";
+
+    String expected = "function a1() { return blah && bar && a1() }"
+        + "function c() { a1() } c()";
+
+    compare(expected, input);
+  }
+
+  private void compare(String expected, String input) throws Exception {
+    input = optimize(input, JsSymbolResolver.class, FixStaticRefsVisitor.class,
+        JsInliner.class, JsUnusedFunctionRemover.class);
+    expected = optimize(expected);
+    assertEquals(expected, input);
   }
 }