Optimizing class inliner to favor kotlin j-/k-style lambdas.

We add an analysis of how method parameters are being used. Currently
we only identify parameters that are not used, and parameters that
are only used for calling a method on it exactly one time.

(Note that the latter pattern is typical for lambdas passed into a
method which calls them in some context.)

With this information we extend class inliner to allow the eligible
instance to be passed as an argument to a method (extra method below)
and we know that the corresponding parameter is used in one of the
two supported special ways. If it is, we:

 ** if the eligible instance is passed to an unused parameter of the
    extra method and the eligible instance can be inlined without this
    use, we just replace this use with null and proceed with eligible
    instance inlining

 ** if the eligible instance is passed to a parameter of the extra
    method with one call on it, we look if this call will become an
    eligible use after the extra method is inlined, and if it is we
    try to force-inline extra method and proceed with eligible
    instance inlining after that. (Note that extra method size
    contributes to combined inlined instructions size).

NOTE: unfortunately I had to restructure ClassInliner since it was
getting more complicated, this will make review more difficult. Those
are mostly class and method moves/renames and trivial changes.

We have just few instances where new patterns were applied on gmscore,
totalling to about 1K size reduce.

Bug: 80135467
Change-Id: I8fc747b80bd4846a1227c6bdbb064b563dd89419
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/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);
     }
   }