Implement a "dependent optimizations" framework.

- This framework is used for speeding up optimizations by running
  optimizers on just modified fields and methods.

- Use the framework to speedup MethodInliner (both inner loop and
  outer loop).

- Add unit tests for MethodInliner.

Change-Id: Ieb38b77294a2004ee03047f6f6aac41164395329
diff --git a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
index 66559a9..6b9497a 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
@@ -80,6 +80,7 @@
 import com.google.gwt.dev.jjs.impl.MethodCallSpecializer;
 import com.google.gwt.dev.jjs.impl.MethodCallTightener;
 import com.google.gwt.dev.jjs.impl.MethodInliner;
+import com.google.gwt.dev.jjs.impl.OptimizerContext;
 import com.google.gwt.dev.jjs.impl.OptimizerStats;
 import com.google.gwt.dev.jjs.impl.Pruner;
 import com.google.gwt.dev.jjs.impl.RecordRebinds;
@@ -357,7 +358,6 @@
         addSyntheticArtifacts(unifiedAst, permutation, startTimeMs, permutationId, jjsmap,
             dependenciesAndRecorder, internedLiteralByVariableName, isSourceMapsEnabled, jsFragments,
             sizeBreakdowns, sourceInfoMaps, permutationResult);
-
         return permutationResult;
       } catch (Throwable e) {
         throw CompilationProblemReporter.logAndTranslateException(logger, e);
@@ -1132,7 +1132,8 @@
            * compiles, so let's avoid doing potentially superlinear optimizations on the unified
            * AST.
            */
-          optimizeJavaOneTime("Early Optimization", jprogram.getNodeCount());
+          optimizeJavaOneTime("Early Optimization", jprogram.getNodeCount(),
+              new OptimizerContext(jprogram));
         }
       }
     }
@@ -1450,6 +1451,7 @@
     boolean atMaxLevel = options.getOptimizationLevel() == OptionOptimize.OPTIMIZE_LEVEL_MAX;
     int passLimit = atMaxLevel ? MAX_PASSES : options.getOptimizationLevel();
     float minChangeRate = atMaxLevel ? FIXED_POINT_CHANGE_RATE : EFFICIENT_CHANGE_RATE;
+    OptimizerContext optimizerCtx = new OptimizerContext(jprogram);
     while (true) {
       passCount++;
       if (passCount > passLimit) {
@@ -1460,7 +1462,7 @@
         throw new InterruptedException();
       }
       AstDumper.maybeDumpAST(jprogram);
-      OptimizerStats stats = optimizeJavaOneTime("Pass " + passCount, nodeCount);
+      OptimizerStats stats = optimizeJavaOneTime("Pass " + passCount, nodeCount, optimizerCtx);
       allOptimizerStats.add(stats);
       lastNodeCount = nodeCount;
       nodeCount = jprogram.getNodeCount();
@@ -1493,23 +1495,24 @@
     }
   }
 
-  private OptimizerStats optimizeJavaOneTime(String passName, int numNodes) {
+  private OptimizerStats optimizeJavaOneTime(String passName, int numNodes,
+      OptimizerContext optimizerCtx) {
     Event optimizeEvent = SpeedTracerLogger.start(CompilerEventType.OPTIMIZE, "phase", "loop");
     // Clinits might have become empty become empty.
     jprogram.typeOracle.recomputeAfterOptimizations(jprogram.getDeclaredTypes());
     OptimizerStats stats = new OptimizerStats(passName);
-    stats.add(Pruner.exec(jprogram, true).recordVisits(numNodes));
-    stats.add(Finalizer.exec(jprogram).recordVisits(numNodes));
-    stats.add(MakeCallsStatic.exec(jprogram, options.shouldAddRuntimeChecks())
+    stats.add(Pruner.exec(jprogram, true, optimizerCtx).recordVisits(numNodes));
+    stats.add(Finalizer.exec(jprogram, optimizerCtx).recordVisits(numNodes));
+    stats.add(MakeCallsStatic.exec(jprogram, options.shouldAddRuntimeChecks(), optimizerCtx)
         .recordVisits(numNodes));
-    stats.add(TypeTightener.exec(jprogram).recordVisits(numNodes));
-    stats.add(MethodCallTightener.exec(jprogram).recordVisits(numNodes));
+    stats.add(TypeTightener.exec(jprogram, optimizerCtx).recordVisits(numNodes));
+    stats.add(MethodCallTightener.exec(jprogram, optimizerCtx).recordVisits(numNodes));
     // Note: Specialization should be done before inlining.
-    stats.add(MethodCallSpecializer.exec(jprogram).recordVisits(numNodes));
-    stats.add(DeadCodeElimination.exec(jprogram).recordVisits(numNodes));
-    stats.add(MethodInliner.exec(jprogram).recordVisits(numNodes));
+    stats.add(MethodCallSpecializer.exec(jprogram, optimizerCtx).recordVisits(numNodes));
+    stats.add(DeadCodeElimination.exec(jprogram, optimizerCtx).recordVisits(numNodes));
+    stats.add(MethodInliner.exec(jprogram, optimizerCtx).recordVisits(numNodes));
     if (options.shouldInlineLiteralParameters()) {
-      stats.add(SameParameterValueOptimizer.exec(jprogram).recordVisits(numNodes));
+      stats.add(SameParameterValueOptimizer.exec(jprogram, optimizerCtx).recordVisits(numNodes));
     }
     if (options.shouldOrdinalizeEnums()) {
       stats.add(EnumOrdinalizer.exec(jprogram).recordVisits(numNodes));
diff --git a/dev/core/src/com/google/gwt/dev/jjs/ast/JModVisitor.java b/dev/core/src/com/google/gwt/dev/jjs/ast/JModVisitor.java
index 7339446..a4c48a4 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/ast/JModVisitor.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/ast/JModVisitor.java
@@ -53,14 +53,14 @@
     public void insertAfter(JNode node) {
       checkRemoved();
       list.add(index + 1, (T) node);
-      ++numVisitorChanges;
+      madeChanges();
     }
 
     @Override
     public void insertBefore(JNode node) {
       checkRemoved();
       list.add(index++, (T) node);
-      ++numVisitorChanges;
+      madeChanges();
     }
 
     @Override
@@ -73,7 +73,7 @@
       checkState();
       list.remove(index--);
       removed = true;
-      ++numVisitorChanges;
+      madeChanges();
     }
 
     @Override
@@ -82,7 +82,7 @@
       checkReplacement(list.get(index), node);
       list.set(index, (T) node);
       replaced = true;
-      ++numVisitorChanges;
+      madeChanges();
     }
 
     /**
@@ -141,14 +141,14 @@
     public void insertAfter(JNode node) {
       checkRemoved();
       list = Lists.add(list, index + 1, (T) node);
-      ++numVisitorChanges;
+      madeChanges();
     }
 
     @Override
     public void insertBefore(JNode node) {
       checkRemoved();
       list = Lists.add(list, index++, (T) node);
-      ++numVisitorChanges;
+      madeChanges();
     }
 
     @Override
@@ -161,7 +161,7 @@
       checkState();
       list = Lists.remove(list, index--);
       removed = true;
-      ++numVisitorChanges;
+      madeChanges();
     }
 
     @Override
@@ -170,7 +170,7 @@
       checkReplacement(list.get(index), node);
       list = Lists.set(list, index, (T) node);
       replaced = true;
-      ++numVisitorChanges;
+      madeChanges();
     }
 
     /**
@@ -252,9 +252,8 @@
       if (!canRemove) {
         throw new UnsupportedOperationException("Can't remove " + node);
       }
-
       this.node = null;
-      ++numVisitorChanges;
+      madeChanges();
     }
 
     @Override
@@ -265,7 +264,7 @@
       checkReplacement(this.node, node);
       this.node = node;
       replaced = true;
-      ++numVisitorChanges;
+      madeChanges();
     }
   }
 
@@ -380,4 +379,5 @@
   protected void traverse(JNode node, Context context) {
     node.traverse(this, context);
   }
+
 }
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/CallGraph.java b/dev/core/src/com/google/gwt/dev/jjs/impl/CallGraph.java
new file mode 100644
index 0000000..07ae11f
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/CallGraph.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.dev.jjs.impl;
+
+import com.google.gwt.dev.jjs.ast.Context;
+import com.google.gwt.dev.jjs.ast.JMethod;
+import com.google.gwt.dev.jjs.ast.JMethodCall;
+import com.google.gwt.dev.jjs.ast.JProgram;
+import com.google.gwt.dev.jjs.ast.JVisitor;
+import com.google.gwt.thirdparty.guava.common.collect.HashMultimap;
+import com.google.gwt.thirdparty.guava.common.collect.Multimap;
+import com.google.gwt.thirdparty.guava.common.collect.Sets;
+
+import java.util.Set;
+
+/**
+ * Call graph, which records {callee->callers} and {caller->callees} pairs.
+ */
+public class CallGraph {
+
+  /**
+   * Visitor used to build call graph.
+   */
+  private class BuildCallGraphVisitor extends JVisitor {
+
+    // TODO(leafwang): This call graph does not take into account overloads nor calls that happen
+    // in JSNI methods.
+
+    private JMethod currentMethod;
+
+    @Override
+    public void endVisit(JMethod x, Context ctx) {
+      assert (currentMethod == x);
+      currentMethod = null;
+    }
+
+    @Override
+    public void endVisit(JMethodCall x, Context ctx) {
+      JMethod calleeMethod = x.getTarget();
+      assert (currentMethod != null);
+      calleeCallersPairs.put(calleeMethod, currentMethod);
+      callerCalleesPairs.put(currentMethod, calleeMethod);
+    }
+
+    @Override
+    public boolean visit(JMethod x, Context ctx) {
+      assert (currentMethod == null);
+      currentMethod = x;
+      return true;
+    }
+  }
+
+  private Multimap<JMethod, JMethod> calleeCallersPairs = HashMultimap.create();
+  private Multimap<JMethod, JMethod> callerCalleesPairs = HashMultimap.create();
+
+  /**
+   * Build the call graph of a JProgram.
+   */
+  public void buildCallGraph(JProgram program) {
+    resetCallGraph();
+    BuildCallGraphVisitor buildCallGraphVisitor = new BuildCallGraphVisitor();
+    buildCallGraphVisitor.accept(program);
+  }
+
+  public void resetCallGraph() {
+    calleeCallersPairs.clear();
+    callerCalleesPairs.clear();
+  }
+
+  /**
+   * Update call graph of a JMethod.
+   */
+  public void updateCallGraphOfMethod(JMethod method) {
+    removeMethod(method);
+    BuildCallGraphVisitor callSiteVisitor = new BuildCallGraphVisitor();
+    callSiteVisitor.currentMethod = method;
+    callSiteVisitor.accept(method.getBody());
+  }
+
+  /**
+   * For removing a method, remove the {caller->callees} and {callee->callers} pairs that are
+   * related to the method.
+   */
+  public void removeMethod(JMethod method) {
+    for (JMethod calleeMethod : callerCalleesPairs.get(method)) {
+      calleeCallersPairs.remove(calleeMethod, method);
+    }
+    callerCalleesPairs.removeAll(method);
+  }
+
+  /**
+   * Return all the callers of a set of callee methods.
+   */
+  public Set<JMethod> getCallers(Set<JMethod> calleeMethods) {
+    assert (calleeMethods != null);
+    Set<JMethod> callerMethods = Sets.newLinkedHashSet();
+    for (JMethod calleeMethod : calleeMethods) {
+      callerMethods.addAll(calleeCallersPairs.get(calleeMethod));
+    }
+    return callerMethods;
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/DeadCodeElimination.java b/dev/core/src/com/google/gwt/dev/jjs/impl/DeadCodeElimination.java
index 588a75b..cf68a5d 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/DeadCodeElimination.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/DeadCodeElimination.java
@@ -47,7 +47,6 @@
 import com.google.gwt.dev.jjs.ast.JLongLiteral;
 import com.google.gwt.dev.jjs.ast.JMethod;
 import com.google.gwt.dev.jjs.ast.JMethodCall;
-import com.google.gwt.dev.jjs.ast.JModVisitor;
 import com.google.gwt.dev.jjs.ast.JNewInstance;
 import com.google.gwt.dev.jjs.ast.JNode;
 import com.google.gwt.dev.jjs.ast.JParameterRef;
@@ -110,8 +109,11 @@
    * {@link #cast(JExpression, SourceInfo, JType, JExpression) simplifyCast}, so
    * that more simplifications can be made on a single pass through a tree.
    */
-  public class DeadCodeVisitor extends JModVisitor {
-    private JMethod currentMethod = null;
+  public class DeadCodeVisitor extends JChangeTrackingVisitor {
+
+    public DeadCodeVisitor(OptimizerContext optimizerCtx) {
+      super(optimizerCtx);
+    }
 
     /**
      * Expressions whose result does not matter. A parent node should add any
@@ -421,7 +423,7 @@
      */
     @Override
     public void endVisit(JIfStatement x, Context ctx) {
-      maybeReplaceMe(x, Simplifier.ifStatement(x, currentMethod), ctx);
+      maybeReplaceMe(x, Simplifier.ifStatement(x, getCurrentMethod()), ctx);
     }
 
     /**
@@ -444,11 +446,6 @@
       }
     }
 
-    @Override
-    public void endVisit(JMethod x, Context ctx) {
-      currentMethod = null;
-    }
-
     /**
      * Resolve method calls that can be computed statically.
      */
@@ -756,12 +753,6 @@
     }
 
     @Override
-    public boolean visit(JMethod x, Context ctx) {
-      currentMethod = x;
-      return true;
-    }
-
-    @Override
     public boolean visit(JMethodCall x, Context ctx) {
       JMethod target = x.getTarget();
       if (target.isStatic() && x.getInstance() != null) {
@@ -1980,12 +1971,34 @@
 
   public static final String NAME = DeadCodeElimination.class.getSimpleName();
 
+  // TODO(leafwang): remove this entry point when it is no longer needed.
   public static OptimizerStats exec(JProgram program) {
-    return new DeadCodeElimination(program).execImpl(program);
+    return new DeadCodeElimination(program).execImpl(program, new OptimizerContext(program));
   }
 
+  // TODO(leafwang): remove this entry point when it is no longer needed.
   public static OptimizerStats exec(JProgram program, JNode node) {
-    return new DeadCodeElimination(program).execImpl(node);
+    return new DeadCodeElimination(program).execImpl(node, new OptimizerContext(program));
+  }
+
+  public static OptimizerStats exec(JProgram program, OptimizerContext optimizerCtx) {
+    OptimizerStats stats = new DeadCodeElimination(program).execImpl(program, optimizerCtx);
+    optimizerCtx.incOptimizationStep();
+    return stats;
+  }
+
+  /**
+   * Apply DeadCodeElimination on a set of methods, mark the modifications in a single step.
+   */
+  public static OptimizerStats exec(JProgram program, Set<JMethod> methods,
+      OptimizerContext optimizerCtx) {
+    OptimizerStats stats = new OptimizerStats(NAME);
+    for (JMethod method : methods) {
+      OptimizerStats innerStats = new DeadCodeElimination(program).execImpl(method, optimizerCtx);
+      stats.recordModified(innerStats.getNumMods());
+    }
+    optimizerCtx.incOptimizationStep();
+    return stats;
   }
 
   private final JProgram program;
@@ -2006,11 +2019,11 @@
     typeClassMap.put(program.getTypePrimitiveShort(), short.class);
   }
 
-  private OptimizerStats execImpl(JNode node) {
+  private OptimizerStats execImpl(JNode node, OptimizerContext optimizerCtx) {
     OptimizerStats stats = new OptimizerStats(NAME);
     Event optimizeEvent = SpeedTracerLogger.start(CompilerEventType.OPTIMIZE, "optimizer", NAME);
 
-    DeadCodeVisitor deadCodeVisitor = new DeadCodeVisitor();
+    DeadCodeVisitor deadCodeVisitor = new DeadCodeVisitor(optimizerCtx);
     deadCodeVisitor.accept(node);
     stats.recordModified(deadCodeVisitor.getNumMods());
     optimizeEvent.end("didChange", "" + stats.didChange());
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/Finalizer.java b/dev/core/src/com/google/gwt/dev/jjs/impl/Finalizer.java
index 1494364..c09c475 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/Finalizer.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/Finalizer.java
@@ -27,7 +27,6 @@
 import com.google.gwt.dev.jjs.ast.JLocal;
 import com.google.gwt.dev.jjs.ast.JMethod;
 import com.google.gwt.dev.jjs.ast.JMethodBody;
-import com.google.gwt.dev.jjs.ast.JModVisitor;
 import com.google.gwt.dev.jjs.ast.JParameter;
 import com.google.gwt.dev.jjs.ast.JPostfixOperation;
 import com.google.gwt.dev.jjs.ast.JPrefixOperation;
@@ -59,7 +58,11 @@
    * program. But if it wasn't implemented, then the enclosing class should have
    * come up as not instantiated and been culled. So I think it's not possible.
    */
-  private class FinalizeVisitor extends JModVisitor {
+  private class FinalizeVisitor extends JChangeTrackingVisitor {
+
+    public FinalizeVisitor(OptimizerContext optimizerCtx) {
+      super(optimizerCtx);
+    }
 
     @Override
     public void endVisit(JClassType x, Context ctx) {
@@ -74,7 +77,7 @@
     }
 
     @Override
-    public void endVisit(JField x, Context ctx) {
+    public void exitField(JField x, Context ctx) {
       if (!x.isVolatile()) {
         maybeFinalize(x);
       }
@@ -86,7 +89,7 @@
     }
 
     @Override
-    public void endVisit(JMethod x, Context ctx) {
+    public void exitMethod(JMethod x, Context ctx) {
       if (!x.isFinal() && !isOverridden.contains(x)) {
         setFinal(x);
       }
@@ -208,9 +211,18 @@
 
   private static final String NAME = Finalizer.class.getSimpleName();
 
+  // TODO(leafwang): remove this entry point when it is no longer needed.
   public static OptimizerStats exec(JProgram program) {
     Event optimizeEvent = SpeedTracerLogger.start(CompilerEventType.OPTIMIZE, "optimizer", NAME);
-    OptimizerStats stats = new Finalizer().execImpl(program);
+    OptimizerStats stats = new Finalizer().execImpl(program, new OptimizerContext(program));
+    optimizeEvent.end("didChange", "" + stats.didChange());
+    return stats;
+  }
+
+  public static OptimizerStats exec(JProgram program, OptimizerContext optimizerCtx) {
+    Event optimizeEvent = SpeedTracerLogger.start(CompilerEventType.OPTIMIZE, "optimizer", NAME);
+    OptimizerStats stats = new Finalizer().execImpl(program, optimizerCtx);
+    optimizerCtx.incOptimizationStep();
     optimizeEvent.end("didChange", "" + stats.didChange());
     return stats;
   }
@@ -221,14 +233,11 @@
 
   private final Set<JClassType> isSubclassed = new HashSet<JClassType>();
 
-  private Finalizer() {
-  }
-
-  private OptimizerStats execImpl(JProgram program) {
+  private OptimizerStats execImpl(JProgram program, OptimizerContext optimizerCtx) {
     MarkVisitor marker = new MarkVisitor();
     marker.accept(program);
 
-    FinalizeVisitor finalizer = new FinalizeVisitor();
+    FinalizeVisitor finalizer = new FinalizeVisitor(optimizerCtx);
     finalizer.accept(program);
 
     return new OptimizerStats(NAME).recordModified(finalizer.getNumMods());
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/JChangeTrackingVisitor.java b/dev/core/src/com/google/gwt/dev/jjs/impl/JChangeTrackingVisitor.java
new file mode 100644
index 0000000..91caad0
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/JChangeTrackingVisitor.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.dev.jjs.impl;
+
+import com.google.gwt.dev.jjs.ast.Context;
+import com.google.gwt.dev.jjs.ast.JField;
+import com.google.gwt.dev.jjs.ast.JMethod;
+import com.google.gwt.dev.jjs.ast.JModVisitor;
+
+/**
+ * A visitor for optimizing an AST.
+ */
+public abstract class JChangeTrackingVisitor extends JModVisitor {
+
+  private JField currentField;
+  private JMethod currentMethod;
+  private final OptimizerContext optimizerCtx;
+  private boolean fieldModified = false;
+  private boolean methodModified = false;
+
+  public JChangeTrackingVisitor(OptimizerContext optimizerCtx) {
+    this.optimizerCtx = optimizerCtx;
+  }
+
+  public boolean enterField(JField f, Context ctx) {
+    return true;
+  }
+
+  public boolean enterMethod(JMethod x, Context ctx) {
+    return true;
+  }
+
+  public JField getCurrentField() {
+    return currentField;
+  }
+
+  public JMethod getCurrentMethod() {
+    return currentMethod;
+  }
+
+  public void exitField(JField f, Context ctx) {
+    return;
+  }
+
+  public void exitMethod(JMethod x, Context ctx) {
+    return;
+  }
+
+  @Override
+  public final void endVisit(JField x, Context ctx) {
+    exitField(x, ctx);
+    if (fieldModified) {
+      optimizerCtx.markModifiedField(x);
+    }
+    currentField = null;
+  }
+
+  @Override
+  public final void endVisit(JMethod x, Context ctx) {
+    exitMethod(x, ctx);
+    if (methodModified) {
+      optimizerCtx.markModifiedMethod(x);
+    }
+    currentMethod = null;
+  }
+
+  @Override
+  public final boolean visit(JField x, Context ctx) {
+    currentField = x;
+    fieldModified = false;
+    return enterField(x, ctx);
+  }
+
+  @Override
+  public final boolean visit(JMethod x, Context ctx) {
+    currentMethod = x;
+    methodModified = false;
+    return enterMethod(x, ctx);
+  }
+
+  @Override
+  protected void madeChanges() {
+    super.madeChanges();
+    if (currentMethod != null) {
+      methodModified = true;
+    }
+    if (currentField != null) {
+      fieldModified = true;
+    }
+  }
+
+  public void fieldWasRemoved(JField field) {
+    optimizerCtx.removeField(field);
+  }
+
+  public void methodWasRemoved(JMethod method) {
+    optimizerCtx.removeMethod(method);
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/MakeCallsStatic.java b/dev/core/src/com/google/gwt/dev/jjs/impl/MakeCallsStatic.java
index 5945ddf..b9d7372 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/MakeCallsStatic.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/MakeCallsStatic.java
@@ -27,7 +27,6 @@
 import com.google.gwt.dev.jjs.ast.JMethod.Specialization;
 import com.google.gwt.dev.jjs.ast.JMethodBody;
 import com.google.gwt.dev.jjs.ast.JMethodCall;
-import com.google.gwt.dev.jjs.ast.JModVisitor;
 import com.google.gwt.dev.jjs.ast.JParameter;
 import com.google.gwt.dev.jjs.ast.JParameterRef;
 import com.google.gwt.dev.jjs.ast.JProgram;
@@ -105,12 +104,14 @@
      * thisRefs must be replaced with paramRefs to the synthetic this param.
      * ParameterRefs also need to be targeted to the params in the new method.
      */
-    private class RewriteMethodBody extends JModVisitor {
+    private class RewriteMethodBody extends JChangeTrackingVisitor {
 
       private final JParameter thisParam;
       private final Map<JParameter, JParameter> varMap;
 
-      public RewriteMethodBody(JParameter thisParam, Map<JParameter, JParameter> varMap) {
+      public RewriteMethodBody(JParameter thisParam, Map<JParameter, JParameter> varMap,
+          OptimizerContext optimizerCtx) {
+        super(optimizerCtx);
         this.thisParam = thisParam;
         this.varMap = varMap;
       }
@@ -130,9 +131,16 @@
     }
 
     private final JProgram program;
+    private final OptimizerContext optimizerCtx;
+
+    private CreateStaticImplsVisitor(JProgram program, OptimizerContext optimizerCtx) {
+      this.program = program;
+      this.optimizerCtx = optimizerCtx;
+    }
 
     CreateStaticImplsVisitor(JProgram program) {
       this.program = program;
+      this.optimizerCtx = null;
     }
 
     @Override
@@ -217,13 +225,18 @@
         // Accept the body to avoid the recursion blocker.
         rewriter.accept(jsFunc.getBody());
       } else {
-        RewriteMethodBody rewriter = new RewriteMethodBody(thisParam, varMap);
+        RewriteMethodBody rewriter = new RewriteMethodBody(thisParam, varMap, optimizerCtx);
         rewriter.accept(movedBody);
       }
 
       // Add the new method as a static impl of the old method
       program.putStaticImpl(x, newMethod);
       enclosingType.getMethods().add(myIndexInClass + 1, newMethod);
+
+      if (optimizerCtx != null) {
+        optimizerCtx.markModifiedMethod(x);
+        optimizerCtx.markModifiedMethod(newMethod);
+      }
       return false;
     }
   }
@@ -299,10 +312,15 @@
    * CreateStaticMethodVisitor, go and rewrite the call sites to call the static
    * method instead.
    */
-  private class RewriteCallSites extends JModVisitor {
+  private class RewriteCallSites extends JChangeTrackingVisitor {
+
     private boolean currentMethodIsInitiallyLive;
     private ControlFlowAnalyzer initiallyLive;
 
+    public RewriteCallSites(OptimizerContext optimizerCtx) {
+      super(optimizerCtx);
+    }
+
     /**
      * In cases where callers are directly referencing (effectively) final
      * instance methods, rewrite the call site to reference the newly-generated
@@ -332,7 +350,7 @@
     }
 
     @Override
-    public boolean visit(JMethod x, Context ctx) {
+    public boolean enterMethod(JMethod x, Context ctx) {
       currentMethodIsInitiallyLive = initiallyLive.getLiveFieldsAndMethods().contains(x);
       return true;
     }
@@ -420,14 +438,23 @@
 
   private static final String NAME = MakeCallsStatic.class.getSimpleName();
 
-  public static OptimizerStats exec(JProgram program, boolean addRuntimeChecks) {
+  public static OptimizerStats exec(JProgram program, boolean addRuntimeChecks,
+      OptimizerContext optimizerCtx) {
     Event optimizeEvent = SpeedTracerLogger.start(CompilerEventType.OPTIMIZE, "optimizer", NAME);
-    OptimizerStats stats = new MakeCallsStatic(program, addRuntimeChecks).execImpl();
+    OptimizerStats stats = new MakeCallsStatic(program, addRuntimeChecks).execImpl(optimizerCtx);
+    optimizerCtx.incOptimizationStep();
     optimizeEvent.end("didChange", "" + stats.didChange());
     return stats;
   }
 
-
+  // TODO(leafwang): remove this entry point when it is no longer needed.
+  public static OptimizerStats exec(JProgram program, boolean addRuntimeChecks) {
+    Event optimizeEvent = SpeedTracerLogger.start(CompilerEventType.OPTIMIZE, "optimizer", NAME);
+    OptimizerStats stats =
+        new MakeCallsStatic(program, addRuntimeChecks).execImpl(new OptimizerContext(program));
+    optimizeEvent.end("didChange", "" + stats.didChange());
+    return stats;
+  }
 
   protected Set<JMethod> toBeMadeStatic = new HashSet<JMethod>();
 
@@ -439,12 +466,12 @@
     this.converter = new StaticCallConverter(program, addRuntimeChecks);
   }
 
-  private OptimizerStats execImpl() {
+  private OptimizerStats execImpl(OptimizerContext optimizerCtx) {
     OptimizerStats stats = new OptimizerStats(NAME);
     FindStaticDispatchSitesVisitor finder = new FindStaticDispatchSitesVisitor();
     finder.accept(program);
 
-    CreateStaticImplsVisitor creator = new CreateStaticImplsVisitor(program);
+    CreateStaticImplsVisitor creator = new CreateStaticImplsVisitor(program, optimizerCtx);
     for (JMethod method : toBeMadeStatic) {
       creator.accept(method);
     }
@@ -468,7 +495,7 @@
      * optimizations can unlock devirtualizations even if no more static impls
      * are created.
      */
-    RewriteCallSites rewriter = new RewriteCallSites();
+    RewriteCallSites rewriter = new RewriteCallSites(optimizerCtx);
     rewriter.accept(program);
     stats.recordModified(rewriter.getNumMods());
     assert (rewriter.didChange() || toBeMadeStatic.isEmpty());
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/MethodCallSpecializer.java b/dev/core/src/com/google/gwt/dev/jjs/impl/MethodCallSpecializer.java
index 661e42b..1e5b005 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/MethodCallSpecializer.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/MethodCallSpecializer.java
@@ -18,7 +18,6 @@
 import com.google.gwt.dev.jjs.ast.Context;
 import com.google.gwt.dev.jjs.ast.JMethod;
 import com.google.gwt.dev.jjs.ast.JMethodCall;
-import com.google.gwt.dev.jjs.ast.JModVisitor;
 import com.google.gwt.dev.jjs.ast.JNullType;
 import com.google.gwt.dev.jjs.ast.JProgram;
 import com.google.gwt.dev.jjs.ast.JType;
@@ -36,7 +35,11 @@
  */
 public class MethodCallSpecializer {
 
-  private class MethodCallSpecializingVisitor extends JModVisitor {
+  private class MethodCallSpecializingVisitor extends JChangeTrackingVisitor {
+
+    public MethodCallSpecializingVisitor(OptimizerContext optimizerCtx) {
+      super(optimizerCtx);
+    }
 
     @Override
     public void endVisit(JMethodCall x, Context ctx) {
@@ -83,9 +86,18 @@
 
   public static final String NAME = MethodCallSpecializer.class.getSimpleName();
 
+  // TODO(leafwang): remove this entry point when it is no longer needed.
   public static OptimizerStats exec(JProgram program) {
     Event optimizeEvent = SpeedTracerLogger.start(CompilerEventType.OPTIMIZE, "optimizer", NAME);
-    OptimizerStats stats = new MethodCallSpecializer(program).execImpl();
+    OptimizerStats stats = new MethodCallSpecializer(program).execImpl(new OptimizerContext(program));
+    optimizeEvent.end("didChange", "" + stats.didChange());
+    return stats;
+  }
+
+  public static OptimizerStats exec(JProgram program, OptimizerContext optimizerCtx) {
+    Event optimizeEvent = SpeedTracerLogger.start(CompilerEventType.OPTIMIZE, "optimizer", NAME);
+    OptimizerStats stats = new MethodCallSpecializer(program).execImpl(optimizerCtx);
+    optimizerCtx.incOptimizationStep();
     optimizeEvent.end("didChange", "" + stats.didChange());
     return stats;
   }
@@ -96,10 +108,9 @@
     this.program = program;
   }
 
-  private OptimizerStats execImpl() {
-    MethodCallSpecializingVisitor specializer =
-        new MethodCallSpecializingVisitor();
+  private OptimizerStats execImpl(OptimizerContext optimizerCtx) {
+    MethodCallSpecializingVisitor specializer = new MethodCallSpecializingVisitor(optimizerCtx);
     specializer.accept(program);
     return new OptimizerStats(NAME).recordModified(specializer.getNumMods());
   }
-}
\ No newline at end of file
+}
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/MethodCallTightener.java b/dev/core/src/com/google/gwt/dev/jjs/impl/MethodCallTightener.java
index fce0df8..d72d0da 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/MethodCallTightener.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/MethodCallTightener.java
@@ -19,7 +19,6 @@
 import com.google.gwt.dev.jjs.ast.JClassType;
 import com.google.gwt.dev.jjs.ast.JMethod;
 import com.google.gwt.dev.jjs.ast.JMethodCall;
-import com.google.gwt.dev.jjs.ast.JModVisitor;
 import com.google.gwt.dev.jjs.ast.JNewInstance;
 import com.google.gwt.dev.jjs.ast.JProgram;
 import com.google.gwt.dev.jjs.ast.JRunAsync;
@@ -52,7 +51,11 @@
    * Updates polymorphic method calls to tighter bindings based on the type of
    * the qualifier.
    */
-  public class MethodCallTighteningVisitor extends JModVisitor {
+  public class MethodCallTighteningVisitor extends JChangeTrackingVisitor {
+
+    public MethodCallTighteningVisitor(OptimizerContext optimizerCtx) {
+      super(optimizerCtx);
+    }
 
     @Override
     public void endVisit(JMethodCall x, Context ctx) {
@@ -114,9 +117,18 @@
 
   public static final String NAME = MethodCallTightener.class.getSimpleName();
 
+  // TODO(leafwang): remove this entry point when it is no longer needed.
   public static OptimizerStats exec(JProgram program) {
     Event optimizeEvent = SpeedTracerLogger.start(CompilerEventType.OPTIMIZE, "optimizer", NAME);
-    OptimizerStats stats = new MethodCallTightener(program).execImpl();
+    OptimizerStats stats = new MethodCallTightener(program).execImpl(new OptimizerContext(program));
+    optimizeEvent.end("didChange", "" + stats.didChange());
+    return stats;
+  }
+
+  public static OptimizerStats exec(JProgram program, OptimizerContext optimizerCtx) {
+    Event optimizeEvent = SpeedTracerLogger.start(CompilerEventType.OPTIMIZE, "optimizer", NAME);
+    OptimizerStats stats = new MethodCallTightener(program).execImpl(optimizerCtx);
+    optimizerCtx.incOptimizationStep();
     optimizeEvent.end("didChange", "" + stats.didChange());
     return stats;
   }
@@ -127,9 +139,9 @@
     this.program = program;
   }
 
-  private OptimizerStats execImpl() {
-    MethodCallTighteningVisitor tightener = new MethodCallTighteningVisitor();
+  private OptimizerStats execImpl(OptimizerContext optimizerCtx) {
+    MethodCallTighteningVisitor tightener = new MethodCallTighteningVisitor(optimizerCtx);
     tightener.accept(program);
     return new OptimizerStats(NAME).recordModified(tightener.getNumMods());
   }
-}
\ No newline at end of file
+}
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/MethodInliner.java b/dev/core/src/com/google/gwt/dev/jjs/impl/MethodInliner.java
index de4aca5..84d6d33 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/MethodInliner.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/MethodInliner.java
@@ -44,7 +44,6 @@
 import com.google.gwt.thirdparty.guava.common.collect.Lists;
 import com.google.gwt.thirdparty.guava.common.collect.Sets;
 
-import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
 
@@ -78,8 +77,11 @@
   /**
    * Method inlining visitor.
    */
-  private class InliningVisitor extends JModVisitor {
-    protected final Set<JMethod> modifiedMethods = new LinkedHashSet<JMethod>();
+  private class InliningVisitor extends JChangeTrackingVisitor {
+
+    public InliningVisitor(OptimizerContext optimizerCtx) {
+      super(optimizerCtx);
+    }
 
     /**
      * Resets with each new visitor, which is good since things that couldn't be
@@ -94,15 +96,10 @@
     }
 
     @Override
-    public void endVisit(JMethod x, Context ctx) {
-      currentMethod = null;
-    }
-
-    @Override
     public void endVisit(JMethodCall x, Context ctx) {
       JMethod method = x.getTarget();
 
-      if (currentMethod == method) {
+      if (getCurrentMethod() == method) {
         // Never try to inline a recursive call!
         return;
       }
@@ -180,8 +177,7 @@
     }
 
     @Override
-    public boolean visit(JMethod x, Context ctx) {
-      currentMethod = x;
+    public boolean enterMethod(JMethod x, Context ctx) {
       if (program.getStaticImpl(x) != null) {
         /*
          * Never inline a static impl into the calling instance method. We used
@@ -209,7 +205,7 @@
 
     private JMethodCall createClinitCall(JMethodCall x) {
       JDeclaredType targetType = x.getTarget().getEnclosingType().getClinitTarget();
-      if (!currentMethod.getEnclosingType().checkClinitTo(targetType)) {
+      if (!getCurrentMethod().getEnclosingType().checkClinitTo(targetType)) {
         // Access from this class to the target class won't trigger a clinit
         return null;
       }
@@ -295,15 +291,6 @@
     }
 
     /**
-     * Replace the current expression with a given replacement and mark the
-     * method as modified.
-     */
-    private void replaceMeAndRecordModification(Context ctx, JExpression replacement) {
-      ctx.replaceMe(replacement);
-      modifiedMethods.add(currentMethod);
-    }
-
-    /**
      * Inline a call to an expression. Returns {@code InlineResult.BLACKLIST} if the method is
      * deemed not inlineable regardless of call site; {@code InlineResult.DO_NOT_BLACKLIST}
      * otherwise.
@@ -368,8 +355,7 @@
       if (orderVisitor.checkResults() == SideEffectCheck.NO_REFERENCES) {
         List<JExpression> expressions = expressionsIncludingArgs(x);
         expressions.addAll(bodyAsExpressionList);
-        replaceMeAndRecordModification(ctx,
-            JjsUtils.createOptimizedMultiExpression(ignoringReturn, expressions));
+        ctx.replaceMe(JjsUtils.createOptimizedMultiExpression(ignoringReturn, expressions));
         return InlineResult.DO_NOT_BLACKLIST;
       }
 
@@ -403,7 +389,7 @@
       replacer.accept(bodyAsExpressionList);
       bodyAsExpressionList.add(0, x.getInstance());
       bodyAsExpressionList.add(1, createClinitCall(x));
-      replaceMeAndRecordModification(ctx, JjsUtils.createOptimizedMultiExpression(ignoringReturn,
+      ctx.replaceMe(JjsUtils.createOptimizedMultiExpression(ignoringReturn,
           bodyAsExpressionList));
       return InlineResult.DO_NOT_BLACKLIST;
     }
@@ -543,14 +529,20 @@
 
   public static String NAME = MethodInliner.class.getSimpleName();
 
-  public static OptimizerStats exec(JProgram program) {
+  public static OptimizerStats exec(JProgram program, OptimizerContext optimizerCtx) {
     Event optimizeEvent = SpeedTracerLogger.start(CompilerEventType.OPTIMIZE, "optimizer", NAME);
-    OptimizerStats stats = new MethodInliner(program).execImpl();
+    OptimizerStats stats = new MethodInliner(program).execImpl(optimizerCtx);
     optimizeEvent.end("didChange", "" + stats.didChange());
     return stats;
   }
 
-  private JMethod currentMethod;
+  // TODO(leafwang): remove this entry point when it is no longer needed.
+  public static OptimizerStats exec(JProgram program) {
+    Event optimizeEvent = SpeedTracerLogger.start(CompilerEventType.OPTIMIZE, "optimizer", NAME);
+    OptimizerStats stats = new MethodInliner(program).execImpl(new OptimizerContext(program));
+    optimizeEvent.end("didChange", "" + stats.didChange());
+    return stats;
+  }
 
   private final JProgram program;
 
@@ -558,26 +550,52 @@
     this.program = program;
   }
 
-  private OptimizerStats execImpl() {
+  private OptimizerStats execImpl(OptimizerContext optimizerCtx) {
     OptimizerStats stats = new OptimizerStats(NAME);
+    int lastStep = optimizerCtx.getLastStepFor(NAME);
     while (true) {
-      InliningVisitor inliner = new InliningVisitor();
-      inliner.accept(program);
+      InliningVisitor inliner = new InliningVisitor(optimizerCtx);
+
+      // TODO(leafwang): generalize this part to avoid explicitly implementing this loop in each
+      // Visitor.
+      Set<JMethod> modifiedMethods = optimizerCtx.getModifiedMethodsSince(lastStep);
+      Set<JMethod> affectedMethods = affectedMethods(modifiedMethods, optimizerCtx);
+      for (JMethod method : affectedMethods) {
+        inliner.accept(method);
+      }
+
       stats.recordModified(inliner.getNumMods());
+      lastStep = optimizerCtx.getOptimizationStep();
+      optimizerCtx.incOptimizationStep();
+
       if (!inliner.didChange()) {
         break;
       }
 
       // Run a cleanup on the methods we just modified
-      for (JMethod method : inliner.modifiedMethods) {
-        OptimizerStats innerStats = DeadCodeElimination.exec(program, method);
-        stats.recordModified(innerStats.getNumMods());
-      }
+      OptimizerStats dceStats = DeadCodeElimination.exec(program,
+          optimizerCtx.getModifiedMethodsAt(lastStep), optimizerCtx);
+      stats.recordModified(dceStats.getNumMods());
     }
+    optimizerCtx.setLastStepFor(NAME, optimizerCtx.getOptimizationStep());
     return stats;
   }
 
   /**
+   * Return the set of methods affected (because they are or callers of) by the modifications to the
+   * given set functions.
+   */
+  private Set<JMethod> affectedMethods(Set<JMethod> modifiedMethods, OptimizerContext optimizerCtx) {
+    Set<JMethod> affectedMethods = Sets.newLinkedHashSet();
+    if (modifiedMethods == null || modifiedMethods.size() == 0) {
+      return affectedMethods;
+    }
+    affectedMethods.addAll(modifiedMethods);
+    affectedMethods.addAll(optimizerCtx.getCallers(modifiedMethods));
+    return affectedMethods;
+  }
+
+  /**
    * Insert an implicit cast if the types differ; it might get optimized out
    * later, but in some cases it will force correct math evaluation.
    */
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/OptimizerContext.java b/dev/core/src/com/google/gwt/dev/jjs/impl/OptimizerContext.java
new file mode 100644
index 0000000..5920c6d
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/OptimizerContext.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.dev.jjs.impl;
+
+import com.google.gwt.dev.jjs.ast.JDeclaredType;
+import com.google.gwt.dev.jjs.ast.JField;
+import com.google.gwt.dev.jjs.ast.JMethod;
+import com.google.gwt.dev.jjs.ast.JProgram;
+import com.google.gwt.thirdparty.guava.common.collect.HashMultiset;
+import com.google.gwt.thirdparty.guava.common.collect.Lists;
+import com.google.gwt.thirdparty.guava.common.collect.Multiset;
+import com.google.gwt.thirdparty.guava.common.collect.Sets;
+
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Maintains dependence and modification information for AST optimizers.
+ * <p>
+ * Is updated incrementally.
+ */
+public class OptimizerContext {
+  private int optimizationStep = -1;
+
+  private CallGraph callGraph = new CallGraph();
+
+  // TODO(leafwang): add other dependencies here
+
+  /**
+   * A mapping from methods to the numbers of the most recent step in which they were modified.
+   */
+  private Multiset<JMethod> modificationStepByMethod = HashMultiset.create();
+
+  /**
+   * A list of modified methods in each step.
+   */
+  private List<Set<JMethod>> methodsByModificationStep = Lists.newArrayList();
+
+  /**
+   * A mapping from methods to the numbers of the most recent step in which they were modified.
+   */
+  private Multiset<JField> modificationStepByField = HashMultiset.create();
+
+  /**
+   * A list of modified fields in each step.
+   */
+  private List<Set<JField>> fieldsByModificationStep = Lists.newArrayList();
+
+  /**
+   * A mapping from optimizers to their last modification step.
+   */
+  private Multiset<String> lastStepForOptimizer = HashMultiset.create();
+
+  public OptimizerContext(JProgram program) {
+    incOptimizationStep();
+    initializeModifications(program);
+    buildCallGraph(program);
+    incOptimizationStep();
+  }
+
+  /**
+   * Add modified field to the modification information.
+   */
+  public void markModifiedField(JField modifiedField) {
+    fieldsByModificationStep.get(modificationStepByField.count(modifiedField)).remove(
+        modifiedField);
+    fieldsByModificationStep.get(optimizationStep).add(modifiedField);
+    modificationStepByField.setCount(modifiedField, optimizationStep);
+
+    // TODO(leafwang): update related dependence information here.
+  }
+
+  /**
+   * Add modified method to both the modification and dependence information.
+   */
+  public void markModifiedMethod(JMethod modifiedMethod) {
+    methodsByModificationStep.get(modificationStepByMethod.count(modifiedMethod)).remove(
+        modifiedMethod);
+    methodsByModificationStep.get(optimizationStep).add(modifiedMethod);
+    modificationStepByMethod.setCount(modifiedMethod, optimizationStep);
+    callGraph.updateCallGraphOfMethod(modifiedMethod);
+  }
+
+  public Set<JMethod> getCallers(Set<JMethod> calleeMethods) {
+    return callGraph.getCallers(calleeMethods);
+  }
+
+  /**
+   * Return the last modification step for a given optimizer.
+   */
+  public int getLastStepFor(String optimizerName) {
+    return lastStepForOptimizer.count(optimizerName);
+  }
+
+  /**
+   * Return all the effective modified fields since a given step.
+   */
+  public Set<JField> getModifiedFieldsSince(int stepSince) {
+    Set<JField> result = Sets.newLinkedHashSet();
+    for (int i = stepSince; i <= optimizationStep; i++) {
+      result.addAll(fieldsByModificationStep.get(i));
+    }
+    return result;
+  }
+
+  /**
+   * Return all the modified methods at a given step.
+   */
+  public Set<JMethod> getModifiedMethodsAt(int step) {
+    assert (step >= 0 && step <= optimizationStep);
+    return new LinkedHashSet<JMethod>(methodsByModificationStep.get(step));
+  }
+
+  /**
+   * Return all the effective modified methods since a given step.
+   */
+  public Set<JMethod> getModifiedMethodsSince(int stepSince) {
+    Set<JMethod> result = Sets.newLinkedHashSet();
+    for (int i = stepSince; i <= optimizationStep; i++) {
+      result.addAll(methodsByModificationStep.get(i));
+    }
+    return result;
+  }
+
+  /**
+   * Return the current optimization step number.
+   */
+  public int getOptimizationStep() {
+    return optimizationStep;
+  }
+
+  /**
+   * Increase the optimization step by 1, create a new set to record modifications in this step.
+   */
+  public void incOptimizationStep() {
+    methodsByModificationStep.add(new LinkedHashSet<JMethod>());
+    fieldsByModificationStep.add(new LinkedHashSet<JField>());
+    optimizationStep++;
+  }
+
+  /**
+   * Remove field from the modification information.
+   */
+  public void removeField(JField field) {
+    fieldsByModificationStep.get(modificationStepByField.count(field)).remove(field);
+    modificationStepByField.remove(field);
+  }
+
+  /**
+   * Remove method from both the dependence and modification information.
+   */
+  public void removeMethod(JMethod method) {
+    methodsByModificationStep.get(modificationStepByMethod.count(method)).remove(method);
+    modificationStepByMethod.remove(method);
+    callGraph.removeMethod(method);
+  }
+
+  /**
+   * Set the last modification step of a given optimizer.
+   */
+  public void setLastStepFor(String optimizerName, int step) {
+    lastStepForOptimizer.setCount(optimizerName, step);
+  }
+
+  private void buildCallGraph(JProgram program) {
+    callGraph.buildCallGraph(program);
+  }
+
+  private void initializeModifications(JProgram program) {
+    for (JDeclaredType type : program.getModuleDeclaredTypes()) {
+      fieldsByModificationStep.get(0).addAll(type.getFields());
+      methodsByModificationStep.get(0).addAll(type.getMethods());
+    }
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/Pruner.java b/dev/core/src/com/google/gwt/dev/jjs/impl/Pruner.java
index 0880fe5..8f41e26 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/Pruner.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/Pruner.java
@@ -35,7 +35,6 @@
 import com.google.gwt.dev.jjs.ast.JMethod;
 import com.google.gwt.dev.jjs.ast.JMethodBody;
 import com.google.gwt.dev.jjs.ast.JMethodCall;
-import com.google.gwt.dev.jjs.ast.JModVisitor;
 import com.google.gwt.dev.jjs.ast.JNameOf;
 import com.google.gwt.dev.jjs.ast.JNewInstance;
 import com.google.gwt.dev.jjs.ast.JNode;
@@ -89,7 +88,7 @@
    * references to pruned variables and methods by references to the null field
    * and null method, and drop assignments to pruned variables.
    */
-  private class CleanupRefsVisitor extends JModVisitor {
+  private class CleanupRefsVisitor extends JChangeTrackingVisitor {
     private final Stack<JExpression> lValues = new Stack<JExpression>();
     private final Map<JMethod, ArrayList<JParameter>> methodToOriginalParamsMap;
     private final Set<? extends JNode> referencedNonTypes;
@@ -99,7 +98,8 @@
     }
 
     public CleanupRefsVisitor(Set<? extends JNode> referencedNodes,
-        Map<JMethod, ArrayList<JParameter>> methodToOriginalParamsMap) {
+        Map<JMethod, ArrayList<JParameter>> methodToOriginalParamsMap, OptimizerContext optimizerCtx) {
+      super(optimizerCtx);
       this.referencedNonTypes = referencedNodes;
       this.methodToOriginalParamsMap = methodToOriginalParamsMap;
     }
@@ -151,7 +151,7 @@
     }
 
     @Override
-    public void endVisit(JMethod x, Context ctx) {
+    public void exitMethod(JMethod x, Context ctx) {
       JType type = x.getType();
       if (type instanceof JReferenceType &&
           !program.typeOracle.isInstantiatedType((JReferenceType) type)) {
@@ -336,14 +336,15 @@
    * Remove any unreferenced classes and interfaces from JProgram. Remove any
    * unreferenced methods and fields from their containing classes.
    */
-  private class PruneVisitor extends JModVisitor {
+  private class PruneVisitor extends JChangeTrackingVisitor {
     private final Map<JMethod, ArrayList<JParameter>> methodToOriginalParamsMap =
         new HashMap<JMethod, ArrayList<JParameter>>();
     private final Set<? extends JNode> referencedNonTypes;
     private final Set<? extends JReferenceType> referencedTypes;
 
     public PruneVisitor(Set<? extends JReferenceType> referencedTypes,
-        Set<? extends JNode> referencedNodes) {
+        Set<? extends JNode> referencedNodes, OptimizerContext optimizerCtx) {
+      super(optimizerCtx);
       this.referencedTypes = referencedTypes;
       this.referencedNonTypes = referencedNodes;
     }
@@ -358,6 +359,7 @@
       for (int i = 0; i < type.getFields().size(); ++i) {
         JField field = type.getFields().get(i);
         if (!referencedNonTypes.contains(field)) {
+          fieldWasRemoved(field);
           type.removeField(i);
           madeChanges();
           --i;
@@ -369,7 +371,9 @@
         if (!referencedNonTypes.contains(method)) {
           // Never prune clinit directly out of the class.
           if (i > 0) {
+            methodWasRemoved(method);
             type.removeMethod(i);
+            methodWasRemoved(program.getStaticImpl(method));
             program.removeStaticImplMapping(method);
             madeChanges();
             --i;
@@ -391,6 +395,7 @@
         JField field = type.getFields().get(i);
         // all interface fields are static and final
         if (!isReferenced || !referencedNonTypes.contains(field)) {
+          fieldWasRemoved(field);
           type.removeField(i);
           madeChanges();
           --i;
@@ -402,6 +407,7 @@
         JMethod method = type.getMethods().get(i);
         // all other interface methods are instance and abstract
         if (!isInstantiated || !referencedNonTypes.contains(method)) {
+          methodWasRemoved(method);
           type.removeMethod(i);
           assert program.instanceMethodForStaticImpl(method) == null;
           madeChanges();
@@ -413,7 +419,7 @@
     }
 
     @Override
-    public boolean visit(JMethod x, Context ctx) {
+    public boolean enterMethod(JMethod x, Context ctx) {
       if (!x.canBePolymorphic()) {
         /*
          * Don't prune parameters on unreferenced methods. The methods might not
@@ -498,9 +504,19 @@
 
   private static final String NAME = Pruner.class.getSimpleName();
 
+  public static OptimizerStats exec(JProgram program, boolean noSpecialTypes,
+      OptimizerContext optimizerCtx) {
+    Event optimizeEvent = SpeedTracerLogger.start(CompilerEventType.OPTIMIZE, "optimizer", NAME);
+    OptimizerStats stats = new Pruner(program, noSpecialTypes).execImpl(optimizerCtx);
+    optimizerCtx.incOptimizationStep();
+    optimizeEvent.end("didChange", "" + stats.didChange());
+    return stats;
+  }
+
+  // TODO(leafwang): remove this entry point when it is no longer needed.
   public static OptimizerStats exec(JProgram program, boolean noSpecialTypes) {
     Event optimizeEvent = SpeedTracerLogger.start(CompilerEventType.OPTIMIZE, "optimizer", NAME);
-    OptimizerStats stats = new Pruner(program, noSpecialTypes).execImpl();
+    OptimizerStats stats = new Pruner(program, noSpecialTypes).execImpl(new OptimizerContext(program));
     optimizeEvent.end("didChange", "" + stats.didChange());
     return stats;
   }
@@ -612,7 +628,7 @@
     this.saveCodeGenTypes = saveCodeGenTypes;
   }
 
-  private OptimizerStats execImpl() {
+  private OptimizerStats execImpl(OptimizerContext optimizerCtx) {
     OptimizerStats stats = new OptimizerStats(NAME);
 
     ControlFlowAnalyzer livenessAnalyzer = new ControlFlowAnalyzer(program);
@@ -640,15 +656,16 @@
 
     PruneVisitor pruner =
         new PruneVisitor(livenessAnalyzer.getReferencedTypes(), livenessAnalyzer
-            .getLiveFieldsAndMethods());
+            .getLiveFieldsAndMethods(), optimizerCtx);
     pruner.accept(program);
     stats.recordModified(pruner.getNumMods());
+
     if (!pruner.didChange()) {
       return stats;
     }
     CleanupRefsVisitor cleaner =
         new CleanupRefsVisitor(livenessAnalyzer.getLiveFieldsAndMethods(), pruner
-            .getMethodToOriginalParamsMap());
+            .getMethodToOriginalParamsMap(), optimizerCtx);
     cleaner.accept(program.getDeclaredTypes());
     return stats;
   }
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/SameParameterValueOptimizer.java b/dev/core/src/com/google/gwt/dev/jjs/impl/SameParameterValueOptimizer.java
index 97e8394..fcd27be 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/SameParameterValueOptimizer.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/SameParameterValueOptimizer.java
@@ -21,7 +21,6 @@
 import com.google.gwt.dev.jjs.ast.JExpression;
 import com.google.gwt.dev.jjs.ast.JMethod;
 import com.google.gwt.dev.jjs.ast.JMethodCall;
-import com.google.gwt.dev.jjs.ast.JModVisitor;
 import com.google.gwt.dev.jjs.ast.JNode;
 import com.google.gwt.dev.jjs.ast.JParameter;
 import com.google.gwt.dev.jjs.ast.JParameterRef;
@@ -163,12 +162,14 @@
   /**
    * Substitute all parameter references with expression.
    */
-  private class SubstituteParameterVisitor extends JModVisitor {
+  private class SubstituteParameterVisitor extends JChangeTrackingVisitor {
     private final CloneExpressionVisitor cloner;
     private final JExpression expression;
     private final JParameter parameter;
 
-    public SubstituteParameterVisitor(JParameter parameter, JExpression expression) {
+    public SubstituteParameterVisitor(JParameter parameter, JExpression expression,
+        OptimizerContext optimizerCtx) {
+      super(optimizerCtx);
       this.parameter = parameter;
       this.expression = expression;
       cloner = new CloneExpressionVisitor();
@@ -186,7 +187,16 @@
 
   public static OptimizerStats exec(JProgram program) {
     Event optimizeEvent = SpeedTracerLogger.start(CompilerEventType.OPTIMIZE, "optimizer", NAME);
-    OptimizerStats stats = new SameParameterValueOptimizer(program).execImpl(program);
+    OptimizerStats stats =
+        new SameParameterValueOptimizer(program).execImpl(program, new OptimizerContext(program));
+    optimizeEvent.end("didChange", "" + stats.didChange());
+    return stats;
+  }
+
+  public static OptimizerStats exec(JProgram program, OptimizerContext optimizerCtx) {
+    Event optimizeEvent = SpeedTracerLogger.start(CompilerEventType.OPTIMIZE, "optimizer", NAME);
+    OptimizerStats stats = new SameParameterValueOptimizer(program).execImpl(program, optimizerCtx);
+    optimizerCtx.incOptimizationStep();
     optimizeEvent.end("didChange", "" + stats.didChange());
     return stats;
   }
@@ -215,7 +225,7 @@
     this.program = program;
   }
 
-  private OptimizerStats execImpl(JNode node) {
+  private OptimizerStats execImpl(JNode node, OptimizerContext optimizerCtx) {
     OptimizerStats stats = new OptimizerStats(NAME);
     AnalysisVisitor analysisVisitor = new AnalysisVisitor();
     analysisVisitor.accept(node);
@@ -228,7 +238,7 @@
       if (valueLiteral != null) {
         SubstituteParameterVisitor substituteParameterVisitor =
             new SubstituteParameterVisitor(parameter, Simplifier.cast(parameter.getType(),
-                valueLiteral));
+                valueLiteral), optimizerCtx);
         substituteParameterVisitor.accept(parameter.getEnclosingMethod());
         stats.recordModified(substituteParameterVisitor.getNumMods());
       }
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/TypeTightener.java b/dev/core/src/com/google/gwt/dev/jjs/impl/TypeTightener.java
index 7ba0e69..a531e5b 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/TypeTightener.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/TypeTightener.java
@@ -35,7 +35,6 @@
 import com.google.gwt.dev.jjs.ast.JLocal;
 import com.google.gwt.dev.jjs.ast.JMethod;
 import com.google.gwt.dev.jjs.ast.JMethodCall;
-import com.google.gwt.dev.jjs.ast.JModVisitor;
 import com.google.gwt.dev.jjs.ast.JNewInstance;
 import com.google.gwt.dev.jjs.ast.JNullType;
 import com.google.gwt.dev.jjs.ast.JParameter;
@@ -125,7 +124,11 @@
   /**
    * Replaces dangling null references with dummy calls.
    */
-  public class FixDanglingRefsVisitor extends JModVisitor {
+  public class FixDanglingRefsVisitor extends JChangeTrackingVisitor {
+
+    public FixDanglingRefsVisitor(OptimizerContext optimizerCtx) {
+      super(optimizerCtx);
+    }
 
     @Override
     public void endVisit(JFieldRef x, Context ctx) {
@@ -370,7 +373,11 @@
    *
    * Also optimize dynamic casts and instanceof operations where possible.
    */
-  public class TightenTypesVisitor extends JModVisitor {
+  public class TightenTypesVisitor extends JChangeTrackingVisitor {
+
+    public TightenTypesVisitor(OptimizerContext optimizerCtx) {
+      super(optimizerCtx);
+    }
 
     /**
      * Tries to determine a specific concrete type for the cast, then either
@@ -443,7 +450,7 @@
     }
 
     @Override
-    public void endVisit(JField x, Context ctx) {
+    public void exitField(JField x, Context ctx) {
       if (!x.isVolatile()) {
         tighten(x);
       }
@@ -523,7 +530,7 @@
      * Tighten based on return types and overrides.
      */
     @Override
-    public void endVisit(JMethod x, Context ctx) {
+    public void exitMethod(JMethod x, Context ctx) {
       if (!(x.getType() instanceof JReferenceType)) {
         return;
       }
@@ -650,7 +657,7 @@
     }
 
     @Override
-    public boolean visit(JMethod x, Context ctx) {
+    public boolean enterMethod(JMethod x, Context ctx) {
       /*
        * Explicitly NOT visiting native methods since we can't infer further
        * type information.
@@ -781,9 +788,18 @@
 
   private static final String NAME = TypeTightener.class.getSimpleName();
 
+  public static OptimizerStats exec(JProgram program, OptimizerContext optimizerCtx) {
+    Event optimizeEvent = SpeedTracerLogger.start(CompilerEventType.OPTIMIZE, "optimizer", NAME);
+    OptimizerStats stats = new TypeTightener(program).execImpl(optimizerCtx);
+    optimizerCtx.incOptimizationStep();
+    optimizeEvent.end("didChange", "" + stats.didChange());
+    return stats;
+  }
+
+  // TODO(leafwang): remove this entry point when it is no longer needed.
   public static OptimizerStats exec(JProgram program) {
     Event optimizeEvent = SpeedTracerLogger.start(CompilerEventType.OPTIMIZE, "optimizer", NAME);
-    OptimizerStats stats = new TypeTightener(program).execImpl();
+    OptimizerStats stats = new TypeTightener(program).execImpl(new OptimizerContext(program));
     optimizeEvent.end("didChange", "" + stats.didChange());
     return stats;
   }
@@ -864,7 +880,7 @@
     typeNull = program.getTypeNull();
   }
 
-  private OptimizerStats execImpl() {
+  private OptimizerStats execImpl(OptimizerContext optimizerCtx) {
     OptimizerStats stats = new OptimizerStats(NAME);
     RecordVisitor recorder = new RecordVisitor();
     recorder.record(program);
@@ -879,7 +895,7 @@
      * output.
      */
     while (true) {
-      TightenTypesVisitor tightener = new TightenTypesVisitor();
+      TightenTypesVisitor tightener = new TightenTypesVisitor(optimizerCtx);
       tightener.accept(program);
       stats.recordModified(tightener.getNumMods());
       if (!tightener.didChange()) {
@@ -888,7 +904,7 @@
     }
 
     if (stats.didChange()) {
-      FixDanglingRefsVisitor fixer = new FixDanglingRefsVisitor();
+      FixDanglingRefsVisitor fixer = new FixDanglingRefsVisitor(optimizerCtx);
       fixer.accept(program);
     }
 
diff --git a/dev/core/test/com/google/gwt/dev/jjs/impl/MethodInlinerTest.java b/dev/core/test/com/google/gwt/dev/jjs/impl/MethodInlinerTest.java
new file mode 100644
index 0000000..be21d82
--- /dev/null
+++ b/dev/core/test/com/google/gwt/dev/jjs/impl/MethodInlinerTest.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.dev.jjs.impl;
+
+import com.google.gwt.dev.jjs.ast.JMethod;
+import com.google.gwt.dev.jjs.ast.JProgram;
+
+/**
+ * Test for {@link MethodInliner}.
+ */
+public class MethodInlinerTest extends OptimizerTestBase {
+  @Override
+  protected void setUp() throws Exception {
+    super.setUp();
+  }
+
+  public void testNoMethodCall() throws Exception {
+    addSnippetClassDecl("static int fun1(int a) { return a; }");
+    addSnippetClassDecl("static int fun2(int a, int b) { return a + b; }");
+    Result result = optimize("void", "");
+    assertEquals("static int fun1(int a){\n" + "  return a;\n}",
+        result.findMethod("fun1").toSource());
+    assertEquals("static int fun2(int a, int b){\n" + "  return a + b;\n}",
+        result.findMethod("fun2").toSource());
+  }
+
+  public void testSimple() throws Exception {
+    addSnippetClassDecl("static int fun1(int a) { return a; }");
+    addSnippetClassDecl("static int fun2(int a, int b) { return a + b; }");
+    addSnippetClassDecl("static int fun3(int a) { return 1 + fun1(a); }");
+    addSnippetClassDecl("static int fun4() {", "  int a = 1;", "  int b = 2;",
+        "  return fun1(a) + fun2(a, b); }");
+    Result result = optimize("int", "return fun3(1) + fun4();");
+
+    // one method call in caller
+    assertEquals("static int fun3(int a){\n" + "  return 1 + a;\n" + "}",
+        result.findMethod("fun3").toSource());
+
+    // two method calls in caller
+    assertEquals(
+        "static int fun4(){\n" + "  int a = 1;\n" + "  int b = 2;\n" + "  return a + a + b;\n}",
+        result.findMethod("fun4").toSource());
+  }
+
+  public void testLargerMethodBody() throws Exception {
+    addSnippetClassDecl("static void fun0(int a) { int b = a; b++; assert(b>0); }");
+    addSnippetClassDecl("static int fun1(int a) { fun0(a); return 2 + a; }");
+    addSnippetClassDecl("static int fun2(int a) { return 1 + fun1(a); }");
+    addSnippetClassDecl("static int fun3() {", "  int a = 1;", "  int b = 2;", "  a = a + fun1(a);",
+        "  return a + fun1(b); }");
+    Result result = optimize("int", "return fun2(1);");
+    assertEquals(
+        "static int fun1(int a){\n" + "  EntryPoint.fun0(a);\n" + "  return 2 + a;\n" + "}",
+        result.findMethod("fun1").toSource());
+    // one method call in caller
+    assertEquals("static int fun2(int a){\n" + "  return (EntryPoint.fun0(a), 1 + 2 + a);\n}",
+        result.findMethod("fun2").toSource());
+
+    // two method calls in caller
+    assertEquals("static int fun3(){\n" + "  int a = 1;\n" + "  int b = 2;\n"
+        + "  a = a + ((EntryPoint.fun0(a), 2 + a));\n"
+        + "  return a + ((EntryPoint.fun0(b), 2 + b));\n}", result.findMethod("fun3").toSource());
+  }
+
+  public void testMoreCallSequences() throws Exception {
+    addSnippetClassDecl("static int fun1(int a) { return a; }");
+    addSnippetClassDecl("static int fun2(int a) { return fun1(a)+1; }");
+    addSnippetClassDecl("static int fun3(int a) { return fun2(a) + 2; }");
+    addSnippetClassDecl("static int fun4(int a) { return fun3(a) + 3; }");
+    Result result = optimize("int", "return fun4(1);");
+    assertEquals("static int fun2(int a){\n" + "  return a + 1;\n}",
+        result.findMethod("fun2").toSource());
+    assertEquals("static int fun3(int a){\n" + "  return a + 1 + 2;\n}",
+        result.findMethod("fun3").toSource());
+    assertEquals("static int fun4(int a){\n" + "  return a + 1 + 2 + 3;\n}",
+        result.findMethod("fun4").toSource());
+  }
+
+  public void testDeadCodeElimination_notInlinable() throws Exception {
+    // fun1 cannot be inlined
+    addSnippetClassDecl("static boolean test1(int a)" + "{ return a>1; }");
+    addSnippetClassDecl("static int fun1(int a)" + "{ if (test1(a)) { return a; }"
+        + "else {switch(a) { case 1: a++; break; default: a=a+2; break; }; return a; }" + "}");
+    addSnippetClassDecl("static int fun2(int a)" + "{return fun1(a);}");
+    Result result = optimize("int", "return fun2(0);");
+    assertEquals("static int fun1(int a){\n" + "  if (a > 1) {\n" + "    return a;\n"
+        + "  } else {\n" + "    switch (a)  {\n" + "      case 1: \n" + "      ++a;\n"
+        + "      break;\n" + "      default: \n" + "      a = a + 2;\n" + "    }\n"
+        + "    return a;\n" + "  }\n" + "}", result.findMethod("fun1").toSource());
+    assertEquals("static int fun2(int a){\n" + "  return EntryPoint.fun1(a);\n" + "}",
+        result.findMethod("fun2").toSource());
+  }
+
+  public void testDeadCodeElimination_delayedInline() throws Exception {
+    // same fun1() and fun2() as the previous test, but different test1();
+    // fun1 can be inlined after inling test1 and DeadCodeElimination
+    addSnippetClassDecl("static boolean test1(int a)" + "{ return true; }");
+    addSnippetClassDecl("static int fun1(int a)" + "{ if (test1(a)) { return a; }"
+        + "else {switch(a) { case 1: a++; break; default: a=a+2; break; }; return a; }" + "}");
+    addSnippetClassDecl("static int fun2(int a)" + "{return fun1(a);}");
+    Result result = optimize("int", "return fun2(0);");
+    assertEquals("static int fun1(int a){\n" + "  return a;\n}",
+        result.findMethod("fun1").toSource());
+    assertEquals("static int fun2(int a){\n" + "  return a;\n}",
+        result.findMethod("fun2").toSource());
+  }
+
+  public void testRecursion1() throws Exception {
+    // one recursive function, and one call to the function
+    addSnippetClassDecl("static int fun1(int a) { return a<=0 ? a : fun1(a-1)+a; }");
+    addSnippetClassDecl("static int fun2(int b) { return b + fun1(b); }");
+    Result result = optimize("int", "return fun2(5);");
+    assertEquals(
+        "static int fun1(int a){\n" + "  return a <= 0 ? a : EntryPoint.fun1(a - 1) + a;\n" + "}",
+        result.findMethod("fun1").toSource());
+    // never inline a recursive function
+    assertEquals("static int fun2(int b){\n" + "  return b + EntryPoint.fun1(b);\n" + "}",
+        result.findMethod("fun2").toSource());
+  }
+
+  public void testRecursion2() throws Exception {
+    // one call inside a recursive function
+    addSnippetClassDecl("static int fun1(int a) { return a + 1; }");
+    addSnippetClassDecl("static int fun2(int b) { return b<=0 ? fun1(b) : fun2(b-1)+b; }");
+    Result result = optimize("int", "return fun2(5);");
+    // recursive function can inline other functions
+    assertEquals("static int fun2(int b){\n"
+        + "  return b <= 0 ? b + 1 : EntryPoint.fun2(b - 1) + b;\n" + "}",
+        result.findMethod("fun2").toSource());
+  }
+
+  public void testRecursion3() throws Exception {
+    // nested recursive call
+    addSnippetClassDecl("static int fun1(int a) { return a<=0 ? a : fun2(a-1)+a; }");
+    addSnippetClassDecl("static int fun2(int a) { return a<=0 ? a : fun1(a-1)+a; }");
+    Result result = optimize("int", "return fun1(0);");
+    assertEquals("static int fun1(int a){\n"
+        + "  return a <= 0 ? a : (a - 1 <= 0 ? a - 1 : EntryPoint.fun1(a - 1 - 1) + a - 1) + a;\n"
+        + "}", result.findMethod("fun1").toSource());
+    assertEquals(
+        "static int fun2(int a){\n" + "  return a <= 0 ? a : EntryPoint.fun1(a - 1) + a;\n" + "}",
+        result.findMethod("fun2").toSource());
+  }
+
+  @Override
+  protected boolean optimizeMethod(JProgram program, JMethod method) {
+    program.addEntryMethod(findMainMethod(program));
+    boolean didChange = false;
+    while (MethodInliner.exec(program).didChange()) {
+      didChange = true;
+    }
+    return didChange;
+  }
+}