Merge "Fixing inliningConstraintForVirtualInvoke for cases when no targets were found"
diff --git a/src/main/java/com/android/tools/r8/Version.java b/src/main/java/com/android/tools/r8/Version.java
index d2552d2..6549848 100644
--- a/src/main/java/com/android/tools/r8/Version.java
+++ b/src/main/java/com/android/tools/r8/Version.java
@@ -11,7 +11,7 @@
 
   // This field is accessed from release scripts using simple pattern matching.
   // Therefore, changing this field could break our release scripts.
-  public static final String LABEL = "1.3.3-dev";
+  public static final String LABEL = "1.3.4-dev";
 
   private Version() {
   }
diff --git a/src/main/java/com/android/tools/r8/cf/LoadStoreHelper.java b/src/main/java/com/android/tools/r8/cf/LoadStoreHelper.java
index f789d5f..12c1321 100644
--- a/src/main/java/com/android/tools/r8/cf/LoadStoreHelper.java
+++ b/src/main/java/com/android/tools/r8/cf/LoadStoreHelper.java
@@ -93,7 +93,6 @@
       Value value = instruction.inValues().get(i);
       StackValue stackValue = createStackValue(value, topOfStack++);
       add(load(stackValue, value), instruction, it);
-      value.removeUser(instruction);
       instruction.replaceValue(i, stackValue);
     }
     it.next();
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index 4281e80..362fc2c 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -25,8 +25,10 @@
 import com.android.tools.r8.dex.JumboStringRewriter;
 import com.android.tools.r8.dex.MixedSectionCollection;
 import com.android.tools.r8.graph.AppInfo.ResolutionResult;
+import com.android.tools.r8.graph.DexEncodedMethod.ParameterUsagesInfo.ParameterUsage;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Invoke;
+import com.android.tools.r8.ir.code.Invoke.Type;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.ValueNumberGenerator;
 import com.android.tools.r8.ir.code.ValueType;
@@ -43,10 +45,12 @@
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.InternalOptions;
+import com.google.common.collect.ImmutableList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.function.Consumer;
+import java.util.stream.Collectors;
 
 public class DexEncodedMethod extends KeyedDexItem<DexMethod> implements ResolutionResult {
 
@@ -594,20 +598,69 @@
     private TrivialInitializer() {
     }
 
-    public static final class InstanceClassTrivialInitializer extends TrivialInitializer {
-      public static final InstanceClassTrivialInitializer INSTANCE =
-          new InstanceClassTrivialInitializer();
+    // Defines instance trivial initialized, see details in comments
+    // to CodeRewriter::computeInstanceInitializerInfo(...)
+    public static final class TrivialInstanceInitializer extends TrivialInitializer {
+      public static final TrivialInstanceInitializer INSTANCE =
+          new TrivialInstanceInitializer();
     }
 
-    public static final class ClassTrivialInitializer extends TrivialInitializer {
+    // Defines class trivial initialized, see details in comments
+    // to CodeRewriter::computeClassInitializerInfo(...)
+    public static final class TrivialClassInitializer extends TrivialInitializer {
       public final DexField field;
 
-      public ClassTrivialInitializer(DexField field) {
+      public TrivialClassInitializer(DexField field) {
         this.field = field;
       }
     }
   }
 
+  public static final class ParameterUsagesInfo {
+    private ImmutableList<ParameterUsage> parametersUsages;
+
+    public ParameterUsagesInfo(List<ParameterUsage> usages) {
+      assert !usages.isEmpty();
+      parametersUsages = ImmutableList.copyOf(usages);
+      assert parametersUsages.size() ==
+          parametersUsages.stream().map(usage -> usage.index).collect(Collectors.toSet()).size();
+    }
+
+    public static abstract class ParameterUsage {
+      public final int index;
+
+      ParameterUsage(int index) {
+        this.index = index;
+      }
+    }
+
+    public static class SingleCallOfArgumentMethod extends ParameterUsage {
+      public final Invoke.Type type;
+      public final DexMethod method;
+
+      public SingleCallOfArgumentMethod(int index, Type type, DexMethod method) {
+        super(index);
+        this.type = type;
+        this.method = method;
+      }
+    }
+
+    public static class NotUsed extends ParameterUsage {
+      public NotUsed(int index) {
+        super(index);
+      }
+    }
+
+    ParameterUsage getParameterUsage(int parameter) {
+      for (ParameterUsage usage : parametersUsages) {
+        if (usage.index == parameter) {
+          return usage;
+        }
+      }
+      return null;
+    }
+  }
+
   public static class OptimizationInfo {
 
     private int returnedArgument = -1;
@@ -623,6 +676,7 @@
     // class inliner, null value indicates that the method is not eligible.
     private ClassInlinerEligibility classInlinerEligibility = null;
     private TrivialInitializer trivialInitializerInfo = null;
+    private ParameterUsagesInfo parametersUsages = null;
 
     private OptimizationInfo() {
       // Intentionally left empty.
@@ -638,6 +692,14 @@
       checksNullReceiverBeforeAnySideEffect = template.checksNullReceiverBeforeAnySideEffect;
     }
 
+    public void setParameterUsages(ParameterUsagesInfo parametersUsages) {
+      this.parametersUsages = parametersUsages;
+    }
+
+    public ParameterUsage getParameterUsages(int parameter) {
+      return parametersUsages == null ? null : parametersUsages.getParameterUsage(parameter);
+    }
+
     public boolean returnsArgument() {
       return returnedArgument != -1;
     }
@@ -777,6 +839,10 @@
     ensureMutableOI().setClassInlinerEligibility(eligibility);
   }
 
+  synchronized public void setParameterUsages(ParameterUsagesInfo parametersUsages) {
+    ensureMutableOI().setParameterUsages(parametersUsages);
+  }
+
   synchronized public void setTrivialInitializer(TrivialInitializer info) {
     ensureMutableOI().setTrivialInitializer(info);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCode.java b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
index c832991..cb5aad0 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCode.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
@@ -625,6 +625,10 @@
     return new ConstNumber(createValue(from.outType()), 0);
   }
 
+  public ConstNumber createConstNull() {
+    return new ConstNumber(createValue(ValueType.OBJECT), 0);
+  }
+
   public boolean doAllThrowingInstructionsHavePositions() {
     return allThrowingInstructionsHavePositions;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index 999699c..5d37ea6 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -163,8 +163,10 @@
   }
 
   public void replaceValue(int index, Value newValue) {
+    Value oldValue = inValues.get(index);
     inValues.set(index, newValue);
     newValue.addUser(this);
+    oldValue.removeUser(this);
   }
 
   public void replaceDebugValue(Value oldValue, Value newValue) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/Value.java b/src/main/java/com/android/tools/r8/ir/code/Value.java
index 908e0d9..05dd801 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Value.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Value.java
@@ -249,6 +249,11 @@
     return uniqueUsers = ImmutableSet.copyOf(users);
   }
 
+  public Instruction singleUniqueUser() {
+    assert ImmutableSet.copyOf(users).size() == 1;
+    return users.getFirst();
+  }
+
   public Set<Phi> uniquePhiUsers() {
     if (uniquePhiUsers != null) {
       return uniquePhiUsers;
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
index de86cb4..d9fa610 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
@@ -127,8 +127,8 @@
     types = new TypeVerificationHelper(code, factory, appInfo).computeVerificationTypes();
     splitExceptionalBlocks();
     LoadStoreHelper loadStoreHelper = new LoadStoreHelper(code, types);
-    loadStoreHelper.insertLoadsAndStores();
     DeadCodeRemover.removeDeadCode(code, rewriter, graphLense, options);
+    loadStoreHelper.insertLoadsAndStores();
     removeUnneededLoadsAndStores();
     registerAllocator = new CfRegisterAllocator(code, options);
     registerAllocator.allocateRegisters();
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index f43106b..479af89 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -61,6 +61,7 @@
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
+import com.google.common.base.Suppliers;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ListMultimap;
 import java.util.ArrayList;
@@ -730,9 +731,15 @@
       // Class inliner should work before lambda merger, so if it inlines the
       // lambda, it is not get collected by merger.
       assert options.enableInlining && inliner != null;
+      TypeEnvironment effectivelyFinalTypeEnvironment = typeEnvironment;
       classInliner.processMethodCode(
           appInfo.withLiveness(), method, code, isProcessedConcurrently,
-          methodsToInline -> inliner.performForcedInlining(method, code, methodsToInline)
+          methodsToInline -> inliner.performForcedInlining(method, code, methodsToInline),
+          Suppliers.memoize(() -> inliner.createDefaultOracle(
+              method, code, effectivelyFinalTypeEnvironment,
+              isProcessedConcurrently, callSiteInformation,
+              Integer.MAX_VALUE, Integer.MAX_VALUE)
+          )
       );
       assert code.isConsistentSSA();
     }
@@ -766,6 +773,7 @@
     if (method.isInstanceInitializer() || method.isClassInitializer()) {
       codeRewriter.identifyTrivialInitializer(method, code, feedback);
     }
+    codeRewriter.identifyParameterUsages(method, code, feedback);
 
     printMethod(code, "Optimized IR (SSA)");
     finalizeIR(method, code, feedback);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
index 2f17a4c..7ed34ba 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexEncodedMethod.ClassInlinerEligibility;
+import com.android.tools.r8.graph.DexEncodedMethod.ParameterUsagesInfo;
 import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 
@@ -19,4 +20,5 @@
   void markTriggerClassInitBeforeAnySideEffect(DexEncodedMethod method, boolean mark);
   void setClassInlinerEligibility(DexEncodedMethod method, ClassInlinerEligibility eligibility);
   void setTrivialInitializer(DexEncodedMethod method, TrivialInitializer info);
+  void setParameterUsages(DexEncodedMethod method, ParameterUsagesInfo parameterUsagesInfo);
 }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java
index 1026e9b..44588ab 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexEncodedMethod.ClassInlinerEligibility;
+import com.android.tools.r8.graph.DexEncodedMethod.ParameterUsagesInfo;
 import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 
@@ -56,4 +57,9 @@
   public void setTrivialInitializer(DexEncodedMethod method, TrivialInitializer info) {
     method.setTrivialInitializer(info);
   }
+
+  @Override
+  public void setParameterUsages(DexEncodedMethod method, ParameterUsagesInfo parameterUsagesInfo) {
+    method.setParameterUsages(parameterUsagesInfo);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
index 8fac02f..c2d6ede 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexEncodedMethod.ClassInlinerEligibility;
+import com.android.tools.r8.graph.DexEncodedMethod.ParameterUsagesInfo;
 import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 
@@ -40,4 +41,8 @@
   @Override
   public void setTrivialInitializer(DexEncodedMethod method, TrivialInitializer info) {
   }
+
+  @Override
+  public void setParameterUsages(DexEncodedMethod method, ParameterUsagesInfo parameterUsagesInfo) {
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
index c3f02da..7ea7f57 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
@@ -13,9 +13,13 @@
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexEncodedMethod.ClassInlinerEligibility;
+import com.android.tools.r8.graph.DexEncodedMethod.ParameterUsagesInfo;
+import com.android.tools.r8.graph.DexEncodedMethod.ParameterUsagesInfo.NotUsed;
+import com.android.tools.r8.graph.DexEncodedMethod.ParameterUsagesInfo.ParameterUsage;
+import com.android.tools.r8.graph.DexEncodedMethod.ParameterUsagesInfo.SingleCallOfArgumentMethod;
 import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer;
-import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer.ClassTrivialInitializer;
-import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer.InstanceClassTrivialInitializer;
+import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer.TrivialClassInitializer;
+import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer.TrivialInstanceInitializer;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
@@ -836,6 +840,34 @@
             : computeClassInitializerInfo(code, clazz));
   }
 
+  public void identifyParameterUsages(
+      DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
+    List<ParameterUsage> usages = new ArrayList<>();
+    List<Value> values = code.collectArguments(true);
+    for (int i = 0; i < values.size(); i++) {
+      Value value = values.get(i);
+
+      int numberOfAllUsages = value.numberOfAllUsers();
+      if (numberOfAllUsages == 0) {
+        usages.add(new NotUsed(i));
+        continue;
+      }
+
+      if (numberOfAllUsages == 1 && value.numberOfUsers() == 1) {
+        Instruction instruction = value.singleUniqueUser();
+        if (instruction.isInvokeMethodWithReceiver() &&
+            instruction.inValues().lastIndexOf(value) == 0) {
+          usages.add(new SingleCallOfArgumentMethod(i,
+              instruction.asInvokeMethodWithReceiver().getType(),
+              instruction.asInvokeMethodWithReceiver().getInvokedMethod()));
+        }
+        continue;
+      }
+    }
+
+    feedback.setParameterUsages(method, usages.isEmpty() ? null : new ParameterUsagesInfo(usages));
+  }
+
   // This method defines trivial instance initializer as follows:
   //
   // ** The initializer may only call the initializer of the base class, which
@@ -922,7 +954,7 @@
       return null;
     }
 
-    return InstanceClassTrivialInitializer.INSTANCE;
+    return TrivialInstanceInitializer.INSTANCE;
   }
 
   // This method defines trivial class initializer as follows:
@@ -994,7 +1026,7 @@
       return null;
     }
 
-    return singletonField == null ? null : new ClassTrivialInitializer(singletonField);
+    return singletonField == null ? null : new TrivialClassInitializer(singletonField);
   }
 
   /**
@@ -2059,7 +2091,7 @@
     for (BasicBlock block : code.blocks) {
       for (Phi phi : block.getPhis()) {
         if (!phi.hasLocalInfo() && phi.numberOfUsers() == 1 && phi.numberOfAllUsers() == 1) {
-          Instruction instruction = phi.uniqueUsers().iterator().next();
+          Instruction instruction = phi.singleUniqueUser();
           if (instruction.isDebugLocalWrite()) {
             removeDebugWriteOfPhi(phi, instruction.asDebugLocalWrite());
           }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java
index b56dc21..d3d1e2f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java
@@ -19,10 +19,10 @@
 
 final class ForcedInliningOracle implements InliningOracle, InliningStrategy {
   private final DexEncodedMethod method;
-  private final Map<InvokeMethodWithReceiver, Inliner.InliningInfo> invokesToInline;
+  private final Map<InvokeMethod, Inliner.InliningInfo> invokesToInline;
 
   ForcedInliningOracle(DexEncodedMethod method,
-      Map<InvokeMethodWithReceiver, Inliner.InliningInfo> invokesToInline) {
+      Map<InvokeMethod, Inliner.InliningInfo> invokesToInline) {
     this.method = method;
     this.invokesToInline = invokesToInline;
   }
@@ -34,6 +34,15 @@
   @Override
   public InlineAction computeForInvokeWithReceiver(
       InvokeMethodWithReceiver invoke, DexType invocationContext) {
+    return computeForInvoke(invoke);
+  }
+
+  @Override
+  public InlineAction computeForInvokeStatic(InvokeStatic invoke, DexType invocationContext) {
+    return computeForInvoke(invoke);
+  }
+
+  private InlineAction computeForInvoke(InvokeMethod invoke) {
     Inliner.InliningInfo info = invokesToInline.get(invoke);
     if (info == null) {
       return null;
@@ -44,12 +53,6 @@
   }
 
   @Override
-  public InlineAction computeForInvokeStatic(
-      InvokeStatic invoke, DexType invocationContext) {
-    return null; // Not yet supported.
-  }
-
-  @Override
   public InlineAction computeForInvokePolymorphic(
       InvokePolymorphic invoke, DexType invocationContext) {
     return null; // Not yet supported.
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
index 25cd969..22b14a4 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
@@ -20,7 +20,6 @@
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.Invoke;
 import com.android.tools.r8.ir.code.InvokeMethod;
-import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.code.ValueNumberGenerator;
@@ -376,7 +375,7 @@
   public void performForcedInlining(
       DexEncodedMethod method,
       IRCode code,
-      Map<InvokeMethodWithReceiver, InliningInfo> invokesToInline) {
+      Map<InvokeMethod, InliningInfo> invokesToInline) {
 
     ForcedInliningOracle oracle = new ForcedInliningOracle(method, invokesToInline);
     performInliningImpl(oracle, oracle, method, code);
@@ -389,20 +388,35 @@
       Predicate<DexEncodedMethod> isProcessedConcurrently,
       CallSiteInformation callSiteInformation) {
 
-    DefaultInliningOracle oracle =
-        new DefaultInliningOracle(
-            this,
-            method,
-            code,
-            typeEnvironment,
-            callSiteInformation,
-            isProcessedConcurrently,
-            options.inliningInstructionLimit,
-            INITIAL_INLINING_INSTRUCTION_ALLOWANCE - numberOfInstructions(code));
+    DefaultInliningOracle oracle = createDefaultOracle(
+        method, code, typeEnvironment,
+        isProcessedConcurrently, callSiteInformation,
+        options.inliningInstructionLimit,
+        INITIAL_INLINING_INSTRUCTION_ALLOWANCE - numberOfInstructions(code));
 
     performInliningImpl(oracle, oracle, method, code);
   }
 
+  public DefaultInliningOracle createDefaultOracle(
+      DexEncodedMethod method,
+      IRCode code,
+      TypeEnvironment typeEnvironment,
+      Predicate<DexEncodedMethod> isProcessedConcurrently,
+      CallSiteInformation callSiteInformation,
+      int inliningInstructionLimit,
+      int inliningInstructionAllowance) {
+
+    return new DefaultInliningOracle(
+        this,
+        method,
+        code,
+        typeEnvironment,
+        callSiteInformation,
+        isProcessedConcurrently,
+        inliningInstructionLimit,
+        inliningInstructionAllowance);
+  }
+
   private void performInliningImpl(
       InliningStrategy strategy, InliningOracle oracle, DexEncodedMethod method, IRCode code) {
     if (strategy.exceededAllowance()) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
index e6b61f8..b28158a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
@@ -4,42 +4,23 @@
 
 package com.android.tools.r8.ir.optimize.classinliner;
 
-import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfo;
-import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexEncodedMethod.ClassInlinerEligibility;
-import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer;
-import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer.ClassTrivialInitializer;
-import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
-import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.ir.code.BasicBlock;
-import com.android.tools.r8.ir.code.ConstNumber;
 import com.android.tools.r8.ir.code.IRCode;
-import com.android.tools.r8.ir.code.InstanceGet;
 import com.android.tools.r8.ir.code.Instruction;
-import com.android.tools.r8.ir.code.InstructionListIterator;
-import com.android.tools.r8.ir.code.InvokeDirect;
-import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
-import com.android.tools.r8.ir.code.Phi;
-import com.android.tools.r8.ir.code.Value;
-import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.optimize.Inliner.InliningInfo;
-import com.android.tools.r8.ir.optimize.Inliner.Reason;
-import com.android.tools.r8.kotlin.KotlinInfo;
+import com.android.tools.r8.ir.optimize.InliningOracle;
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.google.common.collect.Streams;
-import java.util.ArrayList;
-import java.util.IdentityHashMap;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Predicate;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
 public final class ClassInliner {
@@ -48,7 +29,7 @@
   private final ConcurrentHashMap<DexType, Boolean> knownClasses = new ConcurrentHashMap<>();
 
   public interface InlinerAction {
-    void inline(Map<InvokeMethodWithReceiver, InliningInfo> methods);
+    void inline(Map<InvokeMethod, InliningInfo> methods);
   }
 
   public ClassInliner(DexItemFactory factory, int totalMethodInstructionLimit) {
@@ -138,7 +119,8 @@
       DexEncodedMethod method,
       IRCode code,
       Predicate<DexEncodedMethod> isProcessedConcurrently,
-      InlinerAction inliner) {
+      InlinerAction inliner,
+      Supplier<InliningOracle> defaultOracle) {
 
     // Collect all the new-instance and static-get instructions in the code before inlining.
     List<Instruction> roots = Streams.stream(code.instructionIterator())
@@ -146,41 +128,25 @@
         .collect(Collectors.toList());
 
     for (Instruction root : roots) {
-      Value eligibleInstance = root.outValue();
-      if (eligibleInstance == null) {
+      InlineCandidateProcessor processor =
+          new InlineCandidateProcessor(factory, appInfo,
+              type -> isClassEligible(appInfo, type),
+              isProcessedConcurrently, method, root);
+
+      // Assess eligibility and compute inlining of direct calls and extra methods needed.
+      if (!processor.isInstanceEligible() ||
+          !processor.isClassAndUsageEligible() ||
+          !processor.areInstanceUsersEligible(defaultOracle)) {
         continue;
       }
 
-      DexType eligibleClass = root.isNewInstance()
-          ? root.asNewInstance().clazz : root.asStaticGet().getField().type;
-      DexClass eligibleClassDefinition = appInfo.definitionFor(eligibleClass);
-      if (eligibleClassDefinition == null) {
-        continue;
-      }
-
-      // Check the static initializer of the type.
-      if (!isClassAndUsageEligible(root, eligibleClass,
-          eligibleClassDefinition, isProcessedConcurrently, appInfo)) {
-        continue;
-      }
-
-      Map<InvokeMethodWithReceiver, InliningInfo> methodCalls =
-          checkInstanceUsersEligibility(appInfo, method, isProcessedConcurrently,
-              root, eligibleInstance, eligibleClass, eligibleClassDefinition);
-      if (methodCalls == null) {
-        continue;
-      }
-
-      if (!instructionLimitExempt(eligibleClassDefinition, appInfo) &&
-          getTotalEstimatedMethodSize(methodCalls) >= totalMethodInstructionLimit) {
+      // Is inlining allowed.
+      if (processor.getEstimatedCombinedSizeForInlining() >= totalMethodInstructionLimit) {
         continue;
       }
 
       // Inline the class instance.
-      forceInlineAllMethodInvocations(inliner, methodCalls);
-      removeSuperClassInitializerAndFieldReads(code, root, eligibleInstance);
-      removeFieldWrites(eligibleInstance, eligibleClass);
-      removeInstruction(root);
+      processor.processInlining(code, inliner);
 
       // Restore normality.
       code.removeAllTrivialPhis();
@@ -188,470 +154,6 @@
     }
   }
 
-  private boolean isClassAndUsageEligible(Instruction root,
-      DexType eligibleClass, DexClass eligibleClassDefinition,
-      Predicate<DexEncodedMethod> isProcessedConcurrently, AppInfoWithLiveness appInfo) {
-    if (!isClassEligible(appInfo, eligibleClass)) {
-      return false;
-    }
-
-    if (root.isNewInstance()) {
-      // There must be no static initializer on the class itself.
-      return !eligibleClassDefinition.hasClassInitializer();
-    }
-
-    assert root.isStaticGet();
-
-    // Checking if we can safely inline class implemented following singleton-like
-    // pattern, by which we assume a static final field holding on to the reference
-    // initialized in class constructor.
-    //
-    // In general we are targeting cases when the class is defined as:
-    //
-    //   class X {
-    //     static final X F;
-    //     static {
-    //       F = new X();
-    //     }
-    //   }
-    //
-    // and being used as follows:
-    //
-    //   void foo() {
-    //     f = X.F;
-    //     f.bar();
-    //   }
-    //
-    // The main difference from the similar case of class inliner with 'new-instance'
-    // instruction is that in this case the instance we inline is not just leaked, but
-    // is actually published via X.F field. There are several risks we need to address
-    // in this case:
-    //
-    //    Risk: instance stored in field X.F has changed after it was initialized in
-    //      class initializer
-    //    Solution: we assume that final field X.F is not modified outside the class
-    //      initializer. In rare cases when it is (e.g. via reflections) it should
-    //      be marked with keep rules
-    //
-    //    Risk: instance stored in field X.F is not initialized yet
-    //    Solution: not initialized instance can only be visible if X.<clinit>
-    //      triggers other class initialization which references X.F. This
-    //      situation should never happen if we:
-    //        -- don't allow any superclasses to have static initializer,
-    //        -- don't allow any subclasses,
-    //        -- guarantee the class has trivial class initializer
-    //           (see CodeRewriter::computeClassInitializerInfo), and
-    //        -- guarantee the instance is initialized with trivial instance
-    //           initializer (see CodeRewriter::computeInstanceInitializerInfo)
-    //
-    //    Risk: instance stored in field X.F was mutated
-    //    Solution: we require that class X does not have any instance fields, and
-    //      if any of its superclasses has instance fields, accessing them will make
-    //      this instance not eligible for inlining. I.e. even though the instance is
-    //      publicized and its state has been mutated, it will not effect the logic
-    //      of class inlining
-    //
-
-    if (eligibleClassDefinition.instanceFields().length > 0 ||
-        !eligibleClassDefinition.accessFlags.isFinal()) {
-      return false;
-    }
-
-    // Singleton instance must be initialized in class constructor.
-    DexEncodedMethod classInitializer = eligibleClassDefinition.getClassInitializer();
-    if (classInitializer == null || isProcessedConcurrently.test(classInitializer)) {
-      return false;
-    }
-
-    TrivialInitializer info =
-        classInitializer.getOptimizationInfo().getTrivialInitializerInfo();
-    assert info == null || info instanceof ClassTrivialInitializer;
-    DexField instanceField = root.asStaticGet().getField();
-    // Singleton instance field must NOT be pinned.
-    return info != null &&
-        ((ClassTrivialInitializer) info).field == instanceField &&
-        !appInfo.isPinned(eligibleClassDefinition.lookupStaticField(instanceField).field);
-  }
-
-  private Map<InvokeMethodWithReceiver, InliningInfo> checkInstanceUsersEligibility(
-      AppInfoWithSubtyping appInfo, DexEncodedMethod method,
-      Predicate<DexEncodedMethod> isProcessedConcurrently,
-      Instruction root, Value receiver,
-      DexType eligibleClass, DexClass eligibleClassDefinition) {
-
-    // No Phi users.
-    if (receiver.numberOfPhiUsers() > 0) {
-      return null; // Not eligible.
-    }
-
-    Map<InvokeMethodWithReceiver, InliningInfo> methodCalls = new IdentityHashMap<>();
-
-    for (Instruction user : receiver.uniqueUsers()) {
-      // Field read/write.
-      if (user.isInstanceGet() ||
-          (user.isInstancePut() && user.asInstancePut().value() != receiver)) {
-        DexField field = user.asFieldInstruction().getField();
-        if (field.clazz == eligibleClass &&
-            eligibleClassDefinition.lookupInstanceField(field) != null) {
-          // Since class inliner currently only supports classes directly extending
-          // java.lang.Object, we don't need to worry about fields defined in superclasses.
-          continue;
-        }
-        return null; // Not eligible.
-      }
-
-      // Eligible constructor call (for new instance roots only).
-      if (user.isInvokeDirect() && root.isNewInstance()) {
-        InliningInfo inliningInfo = isEligibleConstructorCall(appInfo, method,
-            user.asInvokeDirect(), receiver, eligibleClass, isProcessedConcurrently);
-        if (inliningInfo != null) {
-          methodCalls.put(user.asInvokeDirect(), inliningInfo);
-          continue;
-        }
-        return null; // Not eligible.
-      }
-
-      // Eligible virtual method call.
-      if (user.isInvokeVirtual() || user.isInvokeInterface()) {
-        InliningInfo inliningInfo = isEligibleMethodCall(
-            appInfo, method, user.asInvokeMethodWithReceiver(),
-            receiver, eligibleClass, isProcessedConcurrently);
-        if (inliningInfo != null) {
-          methodCalls.put(user.asInvokeMethodWithReceiver(), inliningInfo);
-          continue;
-        }
-        return null;  // Not eligible.
-      }
-
-      return null;  // Not eligible.
-    }
-    return methodCalls;
-  }
-
-  // Remove call to superclass initializer, replace field reads with appropriate
-  // values, insert phis when needed.
-  private void removeSuperClassInitializerAndFieldReads(
-      IRCode code, Instruction root, Value eligibleInstance) {
-    Map<DexField, FieldValueHelper> fieldHelpers = new IdentityHashMap<>();
-    for (Instruction user : eligibleInstance.uniqueUsers()) {
-      // Remove the call to superclass constructor.
-      if (root.isNewInstance() &&
-          user.isInvokeDirect() &&
-          user.asInvokeDirect().getInvokedMethod() == factory.objectMethods.constructor) {
-        removeInstruction(user);
-        continue;
-      }
-
-      if (user.isInstanceGet()) {
-        // Replace a field read with appropriate value.
-        replaceFieldRead(code, root, user.asInstanceGet(), fieldHelpers);
-        continue;
-      }
-
-      if (user.isInstancePut()) {
-        // Skip in this iteration since these instructions are needed to
-        // properly calculate what value should field reads be replaced with.
-        continue;
-      }
-
-      throw new Unreachable("Unexpected usage left after method inlining: " + user);
-    }
-  }
-
-  private void removeFieldWrites(Value receiver, DexType clazz) {
-    for (Instruction user : receiver.uniqueUsers()) {
-      if (!user.isInstancePut()) {
-        throw new Unreachable("Unexpected usage left after field reads removed: " + user);
-      }
-      if (user.asInstancePut().getField().clazz != clazz) {
-        throw new Unreachable("Unexpected field write left after field reads removed: " + user);
-      }
-      removeInstruction(user);
-    }
-  }
-
-  private int getTotalEstimatedMethodSize(Map<InvokeMethodWithReceiver, InliningInfo> methodCalls) {
-    int totalSize = 0;
-    for (InliningInfo info : methodCalls.values()) {
-      totalSize += info.target.getCode().estimatedSizeForInlining();
-    }
-    return totalSize;
-  }
-
-  private boolean instructionLimitExempt(
-      DexClass eligibleClassDefinition, AppInfoWithLiveness appInfo) {
-    if (appInfo.isPinned(eligibleClassDefinition.type)) {
-      return false;
-    }
-    KotlinInfo kotlinInfo = eligibleClassDefinition.getKotlinInfo();
-    return kotlinInfo != null &&
-        kotlinInfo.isSyntheticClass() &&
-        kotlinInfo.asSyntheticClass().isLambda();
-  }
-
-  private void replaceFieldRead(IRCode code, Instruction root,
-      InstanceGet fieldRead, Map<DexField, FieldValueHelper> fieldHelpers) {
-
-    Value value = fieldRead.outValue();
-    if (value != null) {
-      FieldValueHelper helper = fieldHelpers.computeIfAbsent(
-          fieldRead.getField(), field -> new FieldValueHelper(field, code, root));
-      Value newValue = helper.getValueForFieldRead(fieldRead.getBlock(), fieldRead);
-      value.replaceUsers(newValue);
-      for (FieldValueHelper fieldValueHelper : fieldHelpers.values()) {
-        fieldValueHelper.replaceValue(value, newValue);
-      }
-      assert value.numberOfAllUsers() == 0;
-    }
-    removeInstruction(fieldRead);
-  }
-
-  // Describes and caches what values are supposed to be used instead of field reads.
-  private static final class FieldValueHelper {
-    final DexField field;
-    final IRCode code;
-    final Instruction root;
-
-    private Value defaultValue = null;
-    private final Map<BasicBlock, Value> ins = new IdentityHashMap<>();
-    private final Map<BasicBlock, Value> outs = new IdentityHashMap<>();
-
-    private FieldValueHelper(DexField field, IRCode code, Instruction root) {
-      this.field = field;
-      this.code = code;
-      this.root = root;
-    }
-
-    void replaceValue(Value oldValue, Value newValue) {
-      for (Entry<BasicBlock, Value> entry : ins.entrySet()) {
-        if (entry.getValue() == oldValue) {
-          entry.setValue(newValue);
-        }
-      }
-      for (Entry<BasicBlock, Value> entry : outs.entrySet()) {
-        if (entry.getValue() == oldValue) {
-          entry.setValue(newValue);
-        }
-      }
-    }
-
-    Value getValueForFieldRead(BasicBlock block, Instruction valueUser) {
-      assert valueUser != null;
-      Value value = getValueDefinedInTheBlock(block, valueUser);
-      return value != null ? value : getOrCreateInValue(block);
-    }
-
-    private Value getOrCreateOutValue(BasicBlock block) {
-      Value value = outs.get(block);
-      if (value != null) {
-        return value;
-      }
-
-      value = getValueDefinedInTheBlock(block, null);
-      if (value == null) {
-        // No value defined in the block.
-        value = getOrCreateInValue(block);
-      }
-
-      assert value != null;
-      outs.put(block, value);
-      return value;
-    }
-
-    private Value getOrCreateInValue(BasicBlock block) {
-      Value value = ins.get(block);
-      if (value != null) {
-        return value;
-      }
-
-      List<BasicBlock> predecessors = block.getPredecessors();
-      if (predecessors.size() == 1) {
-        value = getOrCreateOutValue(predecessors.get(0));
-        ins.put(block, value);
-      } else {
-        // Create phi, add it to the block, cache in ins map for future use.
-        Phi phi = new Phi(code.valueNumberGenerator.next(),
-            block, ValueType.fromDexType(field.type), null);
-        ins.put(block, phi);
-
-        List<Value> operands = new ArrayList<>();
-        for (BasicBlock predecessor : block.getPredecessors()) {
-          operands.add(getOrCreateOutValue(predecessor));
-        }
-        // Add phi, but don't remove trivial phis; since we cache the phi
-        // we just created for future use we should delay removing trivial
-        // phis until we are done with replacing fields reads.
-        phi.addOperands(operands, false);
-        value = phi;
-      }
-
-      assert value != null;
-      return value;
-    }
-
-    private Value getValueDefinedInTheBlock(BasicBlock block, Instruction stopAt) {
-      InstructionListIterator iterator = stopAt == null ?
-          block.listIterator(block.getInstructions().size()) : block.listIterator(stopAt);
-
-      Instruction valueProducingInsn = null;
-      while (iterator.hasPrevious()) {
-        Instruction instruction = iterator.previous();
-        assert instruction != null;
-
-        if (instruction == root ||
-            (instruction.isInstancePut() &&
-                instruction.asInstancePut().getField() == field &&
-                instruction.asInstancePut().object() == root.outValue())) {
-          valueProducingInsn = instruction;
-          break;
-        }
-      }
-
-      if (valueProducingInsn == null) {
-        return null;
-      }
-      if (valueProducingInsn.isInstancePut()) {
-        return valueProducingInsn.asInstancePut().value();
-      }
-
-      assert root == valueProducingInsn;
-      if (defaultValue == null) {
-        // If we met newInstance it means that default value is supposed to be used.
-        defaultValue = code.createValue(ValueType.fromDexType(field.type));
-        ConstNumber defaultValueInsn = new ConstNumber(defaultValue, 0);
-        defaultValueInsn.setPosition(root.getPosition());
-        LinkedList<Instruction> instructions = block.getInstructions();
-        instructions.add(instructions.indexOf(root) + 1, defaultValueInsn);
-        defaultValueInsn.setBlock(block);
-      }
-      return defaultValue;
-    }
-  }
-
-  private void forceInlineAllMethodInvocations(
-      InlinerAction inliner, Map<InvokeMethodWithReceiver, InliningInfo> methodCalls) {
-    if (!methodCalls.isEmpty()) {
-      inliner.inline(methodCalls);
-    }
-  }
-
-  private void removeInstruction(Instruction instruction) {
-    instruction.inValues().forEach(v -> v.removeUser(instruction));
-    instruction.getBlock().removeInstruction(instruction);
-  }
-
-  private DexEncodedMethod findSingleTarget(
-      AppInfo appInfo, InvokeMethodWithReceiver invoke, DexType instanceType) {
-
-    // We don't use computeSingleTarget(...) on invoke since it sometimes fails to
-    // find the single target, while this code may be more successful since we exactly
-    // know what is the actual type of the receiver.
-
-    // Note that we also intentionally limit ourselves to methods directly defined in
-    // the instance's class. This may be improved later.
-
-    DexClass clazz = appInfo.definitionFor(instanceType);
-    if (clazz != null) {
-      DexMethod callee = invoke.getInvokedMethod();
-      for (DexEncodedMethod candidate : clazz.virtualMethods()) {
-        if (candidate.method.name == callee.name && candidate.method.proto == callee.proto) {
-          return candidate;
-        }
-      }
-    }
-    return null;
-  }
-
-  private InliningInfo isEligibleConstructorCall(
-      AppInfoWithSubtyping appInfo,
-      DexEncodedMethod method,
-      InvokeDirect initInvoke,
-      Value receiver,
-      DexType inlinedClass,
-      Predicate<DexEncodedMethod> isProcessedConcurrently) {
-
-    // Must be a constructor of the exact same class.
-    DexMethod init = initInvoke.getInvokedMethod();
-    if (!factory.isConstructor(init)) {
-      return null;
-    }
-    // Must be a constructor called on the receiver.
-    if (initInvoke.inValues().lastIndexOf(receiver) != 0) {
-      return null;
-    }
-
-    assert init.holder == inlinedClass
-        : "Inlined constructor? [invoke: " + initInvoke +
-        ", expected class: " + inlinedClass + "]";
-
-    DexEncodedMethod definition = appInfo.definitionFor(init);
-    if (definition == null || isProcessedConcurrently.test(definition)) {
-      return null;
-    }
-
-    if (!definition.isInliningCandidate(method, Reason.SIMPLE, appInfo)) {
-      // We won't be able to inline it here.
-
-      // Note that there may be some false negatives here since the method may
-      // reference private fields of its class which are supposed to be replaced
-      // with arguments after inlining. We should try and improve it later.
-
-      // Using -allowaccessmodification mitigates this.
-      return null;
-    }
-
-    return definition.getOptimizationInfo().getClassInlinerEligibility() != null
-        ? new InliningInfo(definition, inlinedClass) : null;
-  }
-
-  private InliningInfo isEligibleMethodCall(
-      AppInfoWithSubtyping appInfo,
-      DexEncodedMethod method,
-      InvokeMethodWithReceiver invoke,
-      Value receiver,
-      DexType inlinedClass,
-      Predicate<DexEncodedMethod> isProcessedConcurrently) {
-
-    if (invoke.inValues().lastIndexOf(receiver) > 0) {
-      return null; // Instance passed as an argument.
-    }
-
-    DexEncodedMethod singleTarget =
-        findSingleTarget(appInfo, invoke, inlinedClass);
-    if (singleTarget == null || isProcessedConcurrently.test(singleTarget)) {
-      return null;
-    }
-    if (method == singleTarget) {
-      return null; // Don't inline itself.
-    }
-
-    ClassInlinerEligibility eligibility =
-        singleTarget.getOptimizationInfo().getClassInlinerEligibility();
-    if (eligibility == null) {
-      return null;
-    }
-
-    // If the method returns receiver and the return value is actually
-    // used in the code the method is not eligible.
-    if (eligibility.returnsReceiver &&
-        invoke.outValue() != null && invoke.outValue().numberOfAllUsers() > 0) {
-      return null;
-    }
-
-    if (!singleTarget.isInliningCandidate(method, Reason.SIMPLE, appInfo)) {
-      // We won't be able to inline it here.
-
-      // Note that there may be some false negatives here since the method may
-      // reference private fields of its class which are supposed to be replaced
-      // with arguments after inlining. We should try and improve it later.
-
-      // Using -allowaccessmodification mitigates this.
-      return null;
-    }
-
-    return new InliningInfo(singleTarget, inlinedClass);
-  }
-
   private boolean isClassEligible(AppInfo appInfo, DexType clazz) {
     Boolean eligible = knownClasses.get(clazz);
     if (eligible == null) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/FieldValueHelper.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/FieldValueHelper.java
new file mode 100644
index 0000000..880ab89
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/FieldValueHelper.java
@@ -0,0 +1,143 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.classinliner;
+
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.ConstNumber;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.Phi;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.code.ValueType;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+// Describes and caches what values are supposed to be used instead of field reads.
+final class FieldValueHelper {
+  private final DexField field;
+  private final IRCode code;
+  private final Instruction root;
+
+  private Value defaultValue = null;
+  private final Map<BasicBlock, Value> ins = new IdentityHashMap<>();
+  private final Map<BasicBlock, Value> outs = new IdentityHashMap<>();
+
+  FieldValueHelper(DexField field, IRCode code, Instruction root) {
+    this.field = field;
+    this.code = code;
+    this.root = root;
+  }
+
+  void replaceValue(Value oldValue, Value newValue) {
+    for (Entry<BasicBlock, Value> entry : ins.entrySet()) {
+      if (entry.getValue() == oldValue) {
+        entry.setValue(newValue);
+      }
+    }
+    for (Entry<BasicBlock, Value> entry : outs.entrySet()) {
+      if (entry.getValue() == oldValue) {
+        entry.setValue(newValue);
+      }
+    }
+  }
+
+  Value getValueForFieldRead(BasicBlock block, Instruction valueUser) {
+    assert valueUser != null;
+    Value value = getValueDefinedInTheBlock(block, valueUser);
+    return value != null ? value : getOrCreateInValue(block);
+  }
+
+  private Value getOrCreateOutValue(BasicBlock block) {
+    Value value = outs.get(block);
+    if (value != null) {
+      return value;
+    }
+
+    value = getValueDefinedInTheBlock(block, null);
+    if (value == null) {
+      // No value defined in the block.
+      value = getOrCreateInValue(block);
+    }
+
+    assert value != null;
+    outs.put(block, value);
+    return value;
+  }
+
+  private Value getOrCreateInValue(BasicBlock block) {
+    Value value = ins.get(block);
+    if (value != null) {
+      return value;
+    }
+
+    List<BasicBlock> predecessors = block.getPredecessors();
+    if (predecessors.size() == 1) {
+      value = getOrCreateOutValue(predecessors.get(0));
+      ins.put(block, value);
+    } else {
+      // Create phi, add it to the block, cache in ins map for future use.
+      Phi phi = new Phi(code.valueNumberGenerator.next(),
+          block, ValueType.fromDexType(field.type), null);
+      ins.put(block, phi);
+
+      List<Value> operands = new ArrayList<>();
+      for (BasicBlock predecessor : block.getPredecessors()) {
+        operands.add(getOrCreateOutValue(predecessor));
+      }
+      // Add phi, but don't remove trivial phis; since we cache the phi
+      // we just created for future use we should delay removing trivial
+      // phis until we are done with replacing fields reads.
+      phi.addOperands(operands, false);
+      value = phi;
+    }
+
+    assert value != null;
+    return value;
+  }
+
+  private Value getValueDefinedInTheBlock(BasicBlock block, Instruction stopAt) {
+    InstructionListIterator iterator = stopAt == null ?
+        block.listIterator(block.getInstructions().size()) : block.listIterator(stopAt);
+
+    Instruction valueProducingInsn = null;
+    while (iterator.hasPrevious()) {
+      Instruction instruction = iterator.previous();
+      assert instruction != null;
+
+      if (instruction == root ||
+          (instruction.isInstancePut() &&
+              instruction.asInstancePut().getField() == field &&
+              instruction.asInstancePut().object() == root.outValue())) {
+        valueProducingInsn = instruction;
+        break;
+      }
+    }
+
+    if (valueProducingInsn == null) {
+      return null;
+    }
+    if (valueProducingInsn.isInstancePut()) {
+      return valueProducingInsn.asInstancePut().value();
+    }
+
+    assert root == valueProducingInsn;
+    if (defaultValue == null) {
+      // If we met newInstance it means that default value is supposed to be used.
+      defaultValue = code.createValue(ValueType.fromDexType(field.type));
+      ConstNumber defaultValueInsn = new ConstNumber(defaultValue, 0);
+      defaultValueInsn.setPosition(root.getPosition());
+      LinkedList<Instruction> instructions = block.getInstructions();
+      instructions.add(instructions.indexOf(root) + 1, defaultValueInsn);
+      defaultValueInsn.setBlock(block);
+    }
+    return defaultValue;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
new file mode 100644
index 0000000..9c553bd
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
@@ -0,0 +1,577 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.classinliner;
+
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexEncodedMethod.ClassInlinerEligibility;
+import com.android.tools.r8.graph.DexEncodedMethod.OptimizationInfo;
+import com.android.tools.r8.graph.DexEncodedMethod.ParameterUsagesInfo.NotUsed;
+import com.android.tools.r8.graph.DexEncodedMethod.ParameterUsagesInfo.ParameterUsage;
+import com.android.tools.r8.graph.DexEncodedMethod.ParameterUsagesInfo.SingleCallOfArgumentMethod;
+import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer;
+import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer.TrivialClassInitializer;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.ConstNumber;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InstanceGet;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.Invoke.Type;
+import com.android.tools.r8.ir.code.InvokeDirect;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.optimize.Inliner.InlineAction;
+import com.android.tools.r8.ir.optimize.Inliner.InliningInfo;
+import com.android.tools.r8.ir.optimize.Inliner.Reason;
+import com.android.tools.r8.ir.optimize.InliningOracle;
+import com.android.tools.r8.ir.optimize.classinliner.ClassInliner.InlinerAction;
+import com.android.tools.r8.kotlin.KotlinInfo;
+import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
+import com.android.tools.r8.utils.Pair;
+import com.google.common.collect.Lists;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+final class InlineCandidateProcessor {
+  private final DexItemFactory factory;
+  private final AppInfoWithLiveness appInfo;
+  private final Predicate<DexType> isClassEligible;
+  private final Predicate<DexEncodedMethod> isProcessedConcurrently;
+  private final DexEncodedMethod method;
+  private final Instruction root;
+
+  private Value eligibleInstance;
+  private DexType eligibleClass;
+  private DexClass eligibleClassDefinition;
+
+  private final Map<InvokeMethod, InliningInfo> methodCallsOnInstance
+      = new IdentityHashMap<>();
+  private final Map<InvokeMethod, InliningInfo> extraMethodCalls
+      = new IdentityHashMap<>();
+  private final List<Pair<InvokeMethod, Integer>> unusedArguments
+      = new ArrayList<>();
+
+  private int estimatedCombinedSizeForInlining = 0;
+
+  InlineCandidateProcessor(
+      DexItemFactory factory, AppInfoWithLiveness appInfo,
+      Predicate<DexType> isClassEligible,
+      Predicate<DexEncodedMethod> isProcessedConcurrently,
+      DexEncodedMethod method, Instruction root) {
+    this.factory = factory;
+    this.isClassEligible = isClassEligible;
+    this.method = method;
+    this.root = root;
+    this.appInfo = appInfo;
+    this.isProcessedConcurrently = isProcessedConcurrently;
+  }
+
+  int getEstimatedCombinedSizeForInlining() {
+    return estimatedCombinedSizeForInlining;
+  }
+
+  // Checks if the root instruction defines eligible value, i.e. the value
+  // exists and we have a definition of its class.
+  boolean isInstanceEligible() {
+    eligibleInstance = root.outValue();
+    if (eligibleInstance == null) {
+      return false;
+    }
+
+    eligibleClass = isNewInstance() ?
+        root.asNewInstance().clazz : root.asStaticGet().getField().type;
+    eligibleClassDefinition = appInfo.definitionFor(eligibleClass);
+    return eligibleClassDefinition != null;
+  }
+
+  // Checks if the class is eligible and is properly used. Regarding general class
+  // eligibility rules see comment on computeClassEligible(...).
+  //
+  // In addition to class being eligible this method also checks:
+  //   -- for 'new-instance' root:
+  //      * class itself does not have static initializer
+  //   -- for 'static-get' root:
+  //      * class does not have instance fields
+  //      * class is final
+  //      * class has class initializer marked as TrivialClassInitializer, and
+  //        class initializer initializes the field we are reading here.
+  boolean isClassAndUsageEligible() {
+    if (!isClassEligible.test(eligibleClass)) {
+      return false;
+    }
+
+    if (isNewInstance()) {
+      // There must be no static initializer on the class itself.
+      return !eligibleClassDefinition.hasClassInitializer();
+    }
+
+    assert root.isStaticGet();
+
+    // Checking if we can safely inline class implemented following singleton-like
+    // pattern, by which we assume a static final field holding on to the reference
+    // initialized in class constructor.
+    //
+    // In general we are targeting cases when the class is defined as:
+    //
+    //   class X {
+    //     static final X F;
+    //     static {
+    //       F = new X();
+    //     }
+    //   }
+    //
+    // and being used as follows:
+    //
+    //   void foo() {
+    //     f = X.F;
+    //     f.bar();
+    //   }
+    //
+    // The main difference from the similar case of class inliner with 'new-instance'
+    // instruction is that in this case the instance we inline is not just leaked, but
+    // is actually published via X.F field. There are several risks we need to address
+    // in this case:
+    //
+    //    Risk: instance stored in field X.F has changed after it was initialized in
+    //      class initializer
+    //    Solution: we assume that final field X.F is not modified outside the class
+    //      initializer. In rare cases when it is (e.g. via reflections) it should
+    //      be marked with keep rules
+    //
+    //    Risk: instance stored in field X.F is not initialized yet
+    //    Solution: not initialized instance can only be visible if X.<clinit>
+    //      triggers other class initialization which references X.F. This
+    //      situation should never happen if we:
+    //        -- don't allow any superclasses to have static initializer,
+    //        -- don't allow any subclasses,
+    //        -- guarantee the class has trivial class initializer
+    //           (see CodeRewriter::computeClassInitializerInfo), and
+    //        -- guarantee the instance is initialized with trivial instance
+    //           initializer (see CodeRewriter::computeInstanceInitializerInfo)
+    //
+    //    Risk: instance stored in field X.F was mutated
+    //    Solution: we require that class X does not have any instance fields, and
+    //      if any of its superclasses has instance fields, accessing them will make
+    //      this instance not eligible for inlining. I.e. even though the instance is
+    //      publicized and its state has been mutated, it will not effect the logic
+    //      of class inlining
+    //
+
+    if (eligibleClassDefinition.instanceFields().length > 0 ||
+        !eligibleClassDefinition.accessFlags.isFinal()) {
+      return false;
+    }
+
+    // Singleton instance must be initialized in class constructor.
+    DexEncodedMethod classInitializer = eligibleClassDefinition.getClassInitializer();
+    if (classInitializer == null || isProcessedConcurrently.test(classInitializer)) {
+      return false;
+    }
+
+    TrivialInitializer info =
+        classInitializer.getOptimizationInfo().getTrivialInitializerInfo();
+    assert info == null || info instanceof TrivialClassInitializer;
+    DexField instanceField = root.asStaticGet().getField();
+    // Singleton instance field must NOT be pinned.
+    return info != null &&
+        ((TrivialClassInitializer) info).field == instanceField &&
+        !appInfo.isPinned(eligibleClassDefinition.lookupStaticField(instanceField).field);
+  }
+
+  // Checks if the inlining candidate instance users are eligible,
+  // see comment on processMethodCode(...).
+  boolean areInstanceUsersEligible(Supplier<InliningOracle> defaultOracle) {
+    // No Phi users.
+    if (eligibleInstance.numberOfPhiUsers() > 0) {
+      return false; // Not eligible.
+    }
+
+    for (Instruction user : eligibleInstance.uniqueUsers()) {
+      // Field read/write.
+      if (user.isInstanceGet() ||
+          (user.isInstancePut() && user.asInstancePut().value() != eligibleInstance)) {
+        DexField field = user.asFieldInstruction().getField();
+        if (field.clazz == eligibleClass &&
+            eligibleClassDefinition.lookupInstanceField(field) != null) {
+          // Since class inliner currently only supports classes directly extending
+          // java.lang.Object, we don't need to worry about fields defined in superclasses.
+          continue;
+        }
+        return false; // Not eligible.
+      }
+
+      // Eligible constructor call (for new instance roots only).
+      if (user.isInvokeDirect() && root.isNewInstance()) {
+        InliningInfo inliningInfo = isEligibleConstructorCall(user.asInvokeDirect());
+        if (inliningInfo != null) {
+          methodCallsOnInstance.put(user.asInvokeDirect(), inliningInfo);
+          continue;
+        }
+      }
+
+      // Eligible virtual method call on the instance as a receiver.
+      if (user.isInvokeVirtual() || user.isInvokeInterface()) {
+        InliningInfo inliningInfo = isEligibleDirectMethodCall(user.asInvokeMethodWithReceiver());
+        if (inliningInfo != null) {
+          methodCallsOnInstance.put(user.asInvokeMethodWithReceiver(), inliningInfo);
+          continue;
+        }
+      }
+
+      // Eligible usage as an invocation argument.
+      if (user.isInvokeMethod()) {
+        if (isExtraMethodCallEligible(defaultOracle, user.asInvokeMethod())) {
+          continue;
+        }
+      }
+
+      return false;  // Not eligible.
+    }
+    return true;
+  }
+
+  // Process inlining, includes the following steps:
+  //
+  //  * replace unused instance usages as arguments which are never used
+  //  * inline extra methods if any, collect new direct method calls
+  //  * inline direct methods if any
+  //  * remove superclass initializer call and field reads
+  //  * remove field writes
+  //  * remove root instruction
+  //
+  void processInlining(IRCode code, InlinerAction inliner) {
+    replaceUsagesAsUnusedArgument(code);
+    forceInlineExtraMethodInvocations(inliner);
+    forceInlineDirectMethodInvocations(inliner);
+    removeSuperClassInitializerAndFieldReads(code);
+    removeFieldWrites();
+    removeInstruction(root);
+  }
+
+  private void replaceUsagesAsUnusedArgument(IRCode code) {
+    for (Pair<InvokeMethod, Integer> unusedArgument : unusedArguments) {
+      InvokeMethod invoke = unusedArgument.getFirst();
+      BasicBlock block = invoke.getBlock();
+
+      ConstNumber nullValue = code.createConstNull();
+      nullValue.setPosition(invoke.getPosition());
+      LinkedList<Instruction> instructions = block.getInstructions();
+      instructions.add(instructions.indexOf(invoke), nullValue);
+      nullValue.setBlock(block);
+
+      int argIndex = unusedArgument.getSecond() + (invoke.isInvokeMethodWithReceiver() ? 1 : 0);
+      invoke.replaceValue(argIndex, nullValue.outValue());
+    }
+    unusedArguments.clear();
+  }
+
+  private void forceInlineExtraMethodInvocations(InlinerAction inliner) {
+    if (extraMethodCalls.isEmpty()) {
+      return;
+    }
+
+    // Inline extra methods.
+    inliner.inline(extraMethodCalls);
+
+    // Reset the collections.
+    methodCallsOnInstance.clear();
+    extraMethodCalls.clear();
+    unusedArguments.clear();
+    estimatedCombinedSizeForInlining = 0;
+
+    // Repeat user analysis
+    if (!areInstanceUsersEligible(() -> {
+      throw new Unreachable("Inlining oracle is expected to be needed");
+    })) {
+      throw new Unreachable("Analysis must succeed after inlining of extra methods");
+    }
+    assert extraMethodCalls.isEmpty();
+    assert unusedArguments.isEmpty();
+  }
+
+  private void forceInlineDirectMethodInvocations(InlinerAction inliner) {
+    if (!methodCallsOnInstance.isEmpty()) {
+      inliner.inline(methodCallsOnInstance);
+    }
+  }
+
+  // Remove call to superclass initializer, replace field reads with appropriate
+  // values, insert phis when needed.
+  private void removeSuperClassInitializerAndFieldReads(IRCode code) {
+    Map<DexField, FieldValueHelper> fieldHelpers = new IdentityHashMap<>();
+    for (Instruction user : eligibleInstance.uniqueUsers()) {
+      // Remove the call to superclass constructor.
+      if (root.isNewInstance() &&
+          user.isInvokeDirect() &&
+          user.asInvokeDirect().getInvokedMethod() == factory.objectMethods.constructor) {
+        removeInstruction(user);
+        continue;
+      }
+
+      if (user.isInstanceGet()) {
+        // Replace a field read with appropriate value.
+        replaceFieldRead(code, user.asInstanceGet(), fieldHelpers);
+        continue;
+      }
+
+      if (user.isInstancePut()) {
+        // Skip in this iteration since these instructions are needed to
+        // properly calculate what value should field reads be replaced with.
+        continue;
+      }
+
+      throw new Unreachable("Unexpected usage left after method inlining: " + user);
+    }
+  }
+
+  private void replaceFieldRead(IRCode code,
+      InstanceGet fieldRead, Map<DexField, FieldValueHelper> fieldHelpers) {
+    Value value = fieldRead.outValue();
+    if (value != null) {
+      FieldValueHelper helper = fieldHelpers.computeIfAbsent(
+          fieldRead.getField(), field -> new FieldValueHelper(field, code, root));
+      Value newValue = helper.getValueForFieldRead(fieldRead.getBlock(), fieldRead);
+      value.replaceUsers(newValue);
+      for (FieldValueHelper fieldValueHelper : fieldHelpers.values()) {
+        fieldValueHelper.replaceValue(value, newValue);
+      }
+      assert value.numberOfAllUsers() == 0;
+    }
+    removeInstruction(fieldRead);
+  }
+
+  private void removeFieldWrites() {
+    for (Instruction user : eligibleInstance.uniqueUsers()) {
+      if (!user.isInstancePut()) {
+        throw new Unreachable("Unexpected usage left after field reads removed: " + user);
+      }
+      if (user.asInstancePut().getField().clazz != eligibleClass) {
+        throw new Unreachable("Unexpected field write left after field reads removed: " + user);
+      }
+      removeInstruction(user);
+    }
+  }
+
+  private InliningInfo isEligibleConstructorCall(InvokeDirect initInvoke) {
+    // Must be a constructor of the exact same class.
+    DexMethod init = initInvoke.getInvokedMethod();
+    if (!factory.isConstructor(init)) {
+      return null;
+    }
+    // Must be a constructor called on the receiver.
+    if (initInvoke.inValues().lastIndexOf(eligibleInstance) != 0) {
+      return null;
+    }
+
+    assert init.holder == eligibleClass
+        : "Inlined constructor? [invoke: " + initInvoke +
+        ", expected class: " + eligibleClass + "]";
+
+    DexEncodedMethod definition = appInfo.definitionFor(init);
+    if (definition == null || isProcessedConcurrently.test(definition)) {
+      return null;
+    }
+
+    if (!definition.isInliningCandidate(method, Reason.SIMPLE, appInfo)) {
+      // We won't be able to inline it here.
+
+      // Note that there may be some false negatives here since the method may
+      // reference private fields of its class which are supposed to be replaced
+      // with arguments after inlining. We should try and improve it later.
+
+      // Using -allowaccessmodification mitigates this.
+      return null;
+    }
+
+    return definition.getOptimizationInfo().getClassInlinerEligibility() != null
+        ? new InliningInfo(definition, eligibleClass) : null;
+  }
+
+  private InliningInfo isEligibleDirectMethodCall(InvokeMethodWithReceiver invoke) {
+    if (invoke.inValues().lastIndexOf(eligibleInstance) > 0) {
+      return null; // Instance passed as an argument.
+    }
+    return isEligibleMethodCall(invoke.getInvokedMethod(),
+        eligibility -> !eligibility.returnsReceiver ||
+            invoke.outValue() == null || invoke.outValue().numberOfAllUsers() == 0);
+  }
+
+  private InliningInfo isEligibleIndirectMethodCall(DexMethod callee) {
+    return isEligibleMethodCall(callee, eligibility -> !eligibility.returnsReceiver);
+  }
+
+  private InliningInfo isEligibleMethodCall(
+      DexMethod callee, Predicate<ClassInlinerEligibility> eligibilityAcceptanceCheck) {
+
+    DexEncodedMethod singleTarget = findSingleTarget(callee);
+    if (singleTarget == null || isProcessedConcurrently.test(singleTarget)) {
+      return null;
+    }
+    if (method == singleTarget) {
+      return null; // Don't inline itself.
+    }
+
+    ClassInlinerEligibility eligibility =
+        singleTarget.getOptimizationInfo().getClassInlinerEligibility();
+    if (eligibility == null) {
+      return null;
+    }
+
+    // If the method returns receiver and the return value is actually
+    // used in the code the method is not eligible.
+    if (!eligibilityAcceptanceCheck.test(eligibility)) {
+      return null;
+    }
+
+    if (!singleTarget.isInliningCandidate(method, Reason.SIMPLE, appInfo)) {
+      // We won't be able to inline it here.
+
+      // Note that there may be some false negatives here since the method may
+      // reference private fields of its class which are supposed to be replaced
+      // with arguments after inlining. We should try and improve it later.
+
+      // Using -allowaccessmodification mitigates this.
+      return null;
+    }
+
+    markSizeForInlining(singleTarget);
+    return new InliningInfo(singleTarget, eligibleClass);
+  }
+
+  // Analyzes if a method invoke the eligible instance is passed to is eligible. In short,
+  // it can be eligible if:
+  //
+  //   -- eligible instance is passed as argument #N which is not used in the method,
+  //      such cases are collected in 'unusedArguments' parameter and later replaced
+  //      with 'null' value
+  //
+  //   -- eligible instance is passed as argument #N which is only used in the method to
+  //      call a method on this object (we call it indirect method call), and method is
+  //      eligible according to the same rules defined for direct method call eligibility
+  //      (except we require the method receiver to not be used in return instruction)
+  //
+  //   -- method itself can be inlined
+  //
+  private synchronized boolean isExtraMethodCallEligible(
+      Supplier<InliningOracle> defaultOracle, InvokeMethod invokeMethod) {
+
+    List<Value> arguments = Lists.newArrayList(invokeMethod.inValues());
+
+    // Remove receiver from arguments.
+    if (invokeMethod.isInvokeMethodWithReceiver()) {
+      if (arguments.get(0) == eligibleInstance) {
+        // If we got here with invocation on receiver the user is ineligible.
+        return false;
+      }
+      arguments.remove(0);
+    }
+
+    // Need single target.
+    DexEncodedMethod singleTarget = invokeMethod.computeSingleTarget(appInfo);
+    if (singleTarget == null || isProcessedConcurrently.test(singleTarget)) {
+      return false;  // Not eligible.
+    }
+
+    OptimizationInfo optimizationInfo = singleTarget.getOptimizationInfo();
+
+    // Go through all arguments, see if all usages of eligibleInstance are good.
+    for (int argIndex = 0; argIndex < arguments.size(); argIndex++) {
+      Value argument = arguments.get(argIndex);
+      if (argument != eligibleInstance) {
+        continue; // Nothing to worry about.
+      }
+
+      // Have parameter usage info?
+      ParameterUsage parameterUsage = optimizationInfo.getParameterUsages(argIndex);
+      if (parameterUsage == null) {
+        return false;  // Don't know anything.
+      }
+
+      if (parameterUsage instanceof NotUsed) {
+        // Reference can be removed since it's not used.
+        unusedArguments.add(new Pair<>(invokeMethod, argIndex));
+        continue;
+      }
+
+      if (parameterUsage instanceof SingleCallOfArgumentMethod) {
+        // Method exactly one time calls a method on passed eligibleInstance.
+        SingleCallOfArgumentMethod info = (SingleCallOfArgumentMethod) parameterUsage;
+        if (info.type != Type.VIRTUAL && info.type != Type.INTERFACE) {
+          return false; // Don't support direct and super calls yet.
+        }
+
+        // Is the method called indirectly still eligible?
+        InliningInfo potentialInliningInfo = isEligibleIndirectMethodCall(info.method);
+        if (potentialInliningInfo != null) {
+          // Check if the method is inline-able by standard inliner.
+          InlineAction inlineAction =
+              invokeMethod.computeInlining(defaultOracle.get(), method.method.holder);
+          if (inlineAction != null) {
+            extraMethodCalls.put(invokeMethod, new InliningInfo(singleTarget, null));
+            continue;
+          }
+        }
+
+        return false;
+      }
+
+      return false; // All other argument usages are not eligible.
+    }
+
+    // Looks good.
+    markSizeForInlining(singleTarget);
+    return true;
+  }
+
+  private boolean exemptFromInstructionLimit(DexEncodedMethod inlinee) {
+    DexType inlineeHolder = inlinee.method.holder;
+    if (appInfo.isPinned(inlineeHolder)) {
+      return false;
+    }
+    DexClass inlineeClass = appInfo.definitionFor(inlineeHolder);
+    assert inlineeClass != null;
+
+    KotlinInfo kotlinInfo = inlineeClass.getKotlinInfo();
+    return kotlinInfo != null &&
+        kotlinInfo.isSyntheticClass() &&
+        kotlinInfo.asSyntheticClass().isLambda();
+  }
+
+  private void markSizeForInlining(DexEncodedMethod inlinee) {
+    if (!exemptFromInstructionLimit(inlinee)) {
+      estimatedCombinedSizeForInlining += inlinee.getCode().estimatedSizeForInlining();
+    }
+  }
+
+  private boolean isNewInstance() {
+    return root.isNewInstance();
+  }
+
+  private DexEncodedMethod findSingleTarget(DexMethod callee) {
+    // We don't use computeSingleTarget(...) on invoke since it sometimes fails to
+    // find the single target, while this code may be more successful since we exactly
+    // know what is the actual type of the receiver.
+
+    // Note that we also intentionally limit ourselves to methods directly defined in
+    // the instance's class. This may be improved later.
+    return eligibleClassDefinition.lookupVirtualMethod(callee);
+  }
+
+  private void removeInstruction(Instruction instruction) {
+    instruction.inValues().forEach(v -> v.removeUser(instruction));
+    instruction.getBlock().removeInstruction(instruction);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
index eaae740..46af30d 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
@@ -59,22 +59,25 @@
           collectAccessedLambdaTypes(lambdaCheck, clazz, "testStateless"));
 
       assertEquals(
-          Sets.newHashSet(
-              "class_inliner_lambda_j_style.MainKt$testStateful$3"),
+          Sets.newHashSet(),
           collectAccessedLambdaTypes(lambdaCheck, clazz, "testStateful"));
 
       assertFalse(
           inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateful$1").isPresent());
 
       assertEquals(
-          Sets.newHashSet(
-              "class_inliner_lambda_j_style.MainKt$testStateful2$1"),
+          Sets.newHashSet(),
           collectAccessedLambdaTypes(lambdaCheck, clazz, "testStateful2"));
 
+      assertFalse(
+          inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateful2$1").isPresent());
+
       assertEquals(
-          Sets.newHashSet(
-              "class_inliner_lambda_j_style.MainKt$testStateful3$1"),
+          Sets.newHashSet(),
           collectAccessedLambdaTypes(lambdaCheck, clazz, "testStateful3"));
+
+      assertFalse(
+          inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateful3$1").isPresent());
     });
   }
 
diff --git a/src/test/kotlinR8TestResources/class_inliner_lambda_j_style/SamIface.java b/src/test/kotlinR8TestResources/class_inliner_lambda_j_style/SamIface.java
index bccef3e..db0e940 100644
--- a/src/test/kotlinR8TestResources/class_inliner_lambda_j_style/SamIface.java
+++ b/src/test/kotlinR8TestResources/class_inliner_lambda_j_style/SamIface.java
@@ -13,8 +13,8 @@
     }
 
     public static void consumeBig(SamIface iface) {
-      System.out.println("Bigger than inline limit, class name: " + iface.getClass().getName());
-      System.out.println("Bigger than inline limit, result: '" + iface.foo() + "'");
+      System.out.println("Bigger than inline limit, class name: " + Consumer.class.getName());
+      System.out.println("Bigger than inline limit, result: '" + SamIface.class.getName() + "'");
       consume(iface);
     }
   }