Merge "Trivial class inliner"
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index aa99407..2d52c64 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -155,6 +155,7 @@
       // Disable global optimizations.
       options.enableMinification = false;
       options.enableInlining = false;
+      options.enableClassInlining = false;
       options.outline.enabled = false;
 
       DexApplication app = new ApplicationReader(inputApp, options, timing).read(executor);
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index bb7c37f..4c65134 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -333,6 +333,8 @@
     // Disable some of R8 optimizations.
     assert internal.enableInlining;
     internal.enableInlining = false;
+    assert internal.enableClassInlining;
+    internal.enableClassInlining = false;
     assert internal.enableSwitchMapRemoval;
     internal.enableSwitchMapRemoval = false;
     assert internal.outline.enabled;
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 9d6e40d..dafa35a 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -701,6 +701,7 @@
     if (internal.debug) {
       // TODO(zerny): Should we support inlining in debug mode? b/62937285
       internal.enableInlining = false;
+      internal.enableClassInlining = false;
       // TODO(zerny): Should we support outlining in debug mode? b/62937285
       internal.outline.enabled = false;
     }
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfo.java b/src/main/java/com/android/tools/r8/graph/AppInfo.java
index 55bc2db..7daef42 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfo.java
@@ -3,15 +3,21 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableMap.Builder;
+import com.google.common.collect.Sets;
+import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Consumer;
 
@@ -475,6 +481,40 @@
     return result;
   }
 
+  public boolean canTriggerStaticInitializer(DexType clazz) {
+    Set<DexType> knownInterfaces = Sets.newIdentityHashSet();
+
+    // Process superclass chain.
+    while (clazz != null && clazz != dexItemFactory.objectType) {
+      DexClass definition = definitionFor(clazz);
+      if (definition == null || definition.hasClassInitializer()) {
+        return true; // Assume it *may* trigger if we didn't find the definition.
+      }
+      knownInterfaces.addAll(Arrays.asList(definition.interfaces.values));
+      clazz = definition.superType;
+    }
+
+    // Process interfaces.
+    Queue<DexType> queue = new ArrayDeque<>(knownInterfaces);
+    while (!queue.isEmpty()) {
+      DexType iface = queue.remove();
+      DexClass definition = definitionFor(iface);
+      if (definition == null || definition.hasClassInitializer()) {
+        return true; // Assume it *may* trigger if we didn't find the definition.
+      }
+      if (!definition.accessFlags.isInterface()) {
+        throw new Unreachable(iface.toSourceString() + " is expected to be an interface");
+      }
+
+      for (DexType superIface : definition.interfaces.values) {
+        if (knownInterfaces.add(superIface)) {
+          queue.add(superIface);
+        }
+      }
+    }
+    return false;
+  }
+
   public interface ResolutionResult {
 
     DexEncodedMethod asResultOfResolve();
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 0cabaf1..eae59a0 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -44,9 +44,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.ImmutableMap;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.function.Consumer;
 
 public class DexEncodedMethod extends KeyedDexItem<DexMethod> implements ResolutionResult {
@@ -564,6 +567,8 @@
     private boolean useIdentifierNameString = false;
     private boolean checksNullReceiverBeforeAnySideEffect = false;
     private boolean triggersClassInitBeforeAnySideEffect = false;
+    private Set<DexField> receiverOnlyUsedForReadingFields = null;
+    private Map<DexField, Integer> onlyInitializesFieldsWithNoOtherSideEffects = null;
 
     private OptimizationInfo() {
       // Intentionally left empty.
@@ -600,6 +605,15 @@
       return returnsConstant;
     }
 
+    public boolean isReceiverOnlyUsedForReadingFields(Set<DexField> fields) {
+      return receiverOnlyUsedForReadingFields != null &&
+          fields.containsAll(receiverOnlyUsedForReadingFields);
+    }
+
+    public Map<DexField, Integer> onlyInitializesFieldsWithNoOtherSideEffects() {
+      return onlyInitializesFieldsWithNoOtherSideEffects;
+    }
+
     public long getReturnedConstant() {
       assert returnsConstant();
       return returnedConstant;
@@ -641,6 +655,19 @@
       returnedConstant = value;
     }
 
+    private void markReceiverOnlyUsedForReadingFields(Set<DexField> fields) {
+      receiverOnlyUsedForReadingFields = fields;
+    }
+
+    private void markOnlyInitializesFieldsWithNoOtherSideEffects(Map<DexField, Integer> mapping) {
+      if (mapping == null) {
+        onlyInitializesFieldsWithNoOtherSideEffects = null;
+      } else {
+        onlyInitializesFieldsWithNoOtherSideEffects = mapping.isEmpty()
+            ? Collections.emptyMap() : ImmutableMap.copyOf(mapping);
+      }
+    }
+
     private void markForceInline() {
       forceInline = true;
     }
@@ -698,6 +725,15 @@
     ensureMutableOI().markReturnsConstant(value);
   }
 
+  synchronized public void markReceiverOnlyUsedForReadingFields(Set<DexField> fields) {
+    ensureMutableOI().markReceiverOnlyUsedForReadingFields(fields);
+  }
+
+  synchronized public void markOnlyInitializesFieldsWithNoOtherSideEffects(
+      Map<DexField, Integer> mapping) {
+    ensureMutableOI().markOnlyInitializesFieldsWithNoOtherSideEffects(mapping);
+  }
+
   synchronized public void markForceInline() {
     ensureMutableOI().markForceInline();
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index b971ab4..8ea7b67 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -115,6 +115,7 @@
   public final DexString valueOfMethodName = createString("valueOf");
 
   public final DexString getClassMethodName = createString("getClass");
+  public final DexString finalizeMethodName = createString("finalize");
   public final DexString ordinalMethodName = createString("ordinal");
   public final DexString desiredAssertionStatusMethodName = createString("desiredAssertionStatus");
   public final DexString forNameMethodName = createString("forName");
@@ -325,12 +326,15 @@
 
     public final DexMethod getClass;
     public final DexMethod constructor;
+    public final DexMethod finalize;
 
     private ObjectMethods() {
       getClass = createMethod(objectDescriptor, getClassMethodName, classDescriptor,
           DexString.EMPTY_ARRAY);
       constructor = createMethod(objectDescriptor,
           constructorMethodName, voidType.descriptor, DexString.EMPTY_ARRAY);
+      finalize = createMethod(objectDescriptor,
+          finalizeMethodName, voidType.descriptor, DexString.EMPTY_ARRAY);
     }
   }
 
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 bb13495..1ec23d0 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
@@ -358,6 +358,15 @@
     return true;
   }
 
+  public boolean hasCatchHandlers() {
+    for (BasicBlock block : blocks) {
+      if (block.hasCatchHandlers()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   private boolean consistentDefUseChains() {
     Set<Value> values = new HashSet<>();
 
@@ -578,16 +587,23 @@
   }
 
   public List<Value> collectArguments() {
+    return collectArguments(false);
+  }
+
+  public List<Value> collectArguments(boolean ignoreReceiver) {
     final List<Value> arguments = new ArrayList<>();
     Iterator<Instruction> iterator = blocks.get(0).iterator();
     while (iterator.hasNext()) {
       Instruction instruction = iterator.next();
       if (instruction.isArgument()) {
-        arguments.add(instruction.asArgument().outValue());
+        Value out = instruction.asArgument().outValue();
+        if (!ignoreReceiver || !out.isThis()) {
+          arguments.add(out);
+        }
       }
     }
     assert arguments.size()
-        == method.method.getArity() + (method.accessFlags.isStatic() ? 0 : 1);
+        == method.method.getArity() + ((method.accessFlags.isStatic() || ignoreReceiver) ? 0 : 1);
     return arguments;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java b/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java
index d280dfe..81d4cdc 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java
@@ -131,6 +131,6 @@
 
   @Override
   public InlineAction computeInlining(InliningOracle decider, DexType invocationContext) {
-    return decider.computeForInvokePolymorpic(this, invocationContext);
+    return decider.computeForInvokePolymorphic(this, invocationContext);
   }
 }
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 20cbd10..fe7cd10 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
@@ -46,6 +46,7 @@
 import com.android.tools.r8.ir.optimize.NonNullTracker;
 import com.android.tools.r8.ir.optimize.Outliner;
 import com.android.tools.r8.ir.optimize.PeepholeOptimizer;
+import com.android.tools.r8.ir.optimize.classinliner.ClassInliner;
 import com.android.tools.r8.ir.optimize.lambda.LambdaMerger;
 import com.android.tools.r8.ir.regalloc.LinearScanRegisterAllocator;
 import com.android.tools.r8.ir.regalloc.RegisterAllocator;
@@ -87,6 +88,7 @@
   private final LambdaRewriter lambdaRewriter;
   private final InterfaceMethodRewriter interfaceMethodRewriter;
   private final LambdaMerger lambdaMerger;
+  private final ClassInliner classInliner;
   private final InternalOptions options;
   private final CfgPrinter printer;
   private final GraphLense graphLense;
@@ -160,6 +162,9 @@
       this.identifierNameStringMarker = null;
       this.devirtualizer = null;
     }
+    this.classInliner =
+        (options.enableClassInlining && options.enableInlining && inliner != null)
+            ? new ClassInliner(appInfo.dexItemFactory) : null;
   }
 
   /**
@@ -686,6 +691,15 @@
       assert code.isConsistentSSA();
     }
 
+    if (classInliner != null) {
+      assert options.enableInlining && inliner != null;
+      classInliner.processMethodCode(
+          appInfo.withSubtyping(), method, code, isProcessedConcurrently,
+          methodsToInline -> inliner.performForcedInlining(method, code, methodsToInline)
+      );
+      assert code.isConsistentSSA();
+    }
+
     if (options.outline.enabled) {
       outlineHandler.accept(code, method);
       assert code.isConsistentSSA();
@@ -705,6 +719,12 @@
       assert code.isConsistentSSA();
     }
 
+    // Analysis must be done after method is rewritten by logArgumentTypes()
+    codeRewriter.identifyReceiverOnlyUsedForReadingFields(method, code, feedback);
+    if (method.isInstanceInitializer()) {
+      codeRewriter.identifyOnlyInitializesFieldsWithNoOtherSideEffects(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 b2c5cfd..26ca6b2 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
@@ -5,7 +5,10 @@
 package com.android.tools.r8.ir.conversion;
 
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+import java.util.Map;
+import java.util.Set;
 
 public interface OptimizationFeedback {
   void methodReturnsArgument(DexEncodedMethod method, int argument);
@@ -15,4 +18,7 @@
   void markProcessed(DexEncodedMethod method, Constraint state);
   void markCheckNullReceiverBeforeAnySideEffect(DexEncodedMethod method, boolean mark);
   void markTriggerClassInitBeforeAnySideEffect(DexEncodedMethod method, boolean mark);
+  void markReceiverOnlyUsedForReadingFields(DexEncodedMethod method, Set<DexField> fields);
+  void markOnlyInitializesFieldsWithNoOtherSideEffects(
+      DexEncodedMethod method, Map<DexField, Integer> mapping);
 }
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 ac1972b..645df64 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
@@ -5,7 +5,10 @@
 package com.android.tools.r8.ir.conversion;
 
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+import java.util.Map;
+import java.util.Set;
 
 public class OptimizationFeedbackDirect implements OptimizationFeedback {
 
@@ -43,4 +46,15 @@
   public void markTriggerClassInitBeforeAnySideEffect(DexEncodedMethod method, boolean mark) {
     method.markTriggerClassInitBeforeAnySideEffect(mark);
   }
+
+  @Override
+  public void markReceiverOnlyUsedForReadingFields(DexEncodedMethod method, Set<DexField> fields) {
+    method.markReceiverOnlyUsedForReadingFields(fields);
+  }
+
+  @Override
+  public void markOnlyInitializesFieldsWithNoOtherSideEffects(DexEncodedMethod method,
+      Map<DexField, Integer> mapping) {
+    method.markOnlyInitializesFieldsWithNoOtherSideEffects(mapping);
+  }
 }
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 80cb04f..97fe052 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
@@ -5,7 +5,10 @@
 package com.android.tools.r8.ir.conversion;
 
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+import java.util.Map;
+import java.util.Set;
 
 public class OptimizationFeedbackIgnore implements OptimizationFeedback {
 
@@ -29,4 +32,13 @@
 
   @Override
   public void markTriggerClassInitBeforeAnySideEffect(DexEncodedMethod method, boolean mark) {}
+
+  @Override
+  public void markReceiverOnlyUsedForReadingFields(DexEncodedMethod method, Set<DexField> fields) {
+  }
+
+  @Override
+  public void markOnlyInitializesFieldsWithNoOtherSideEffects(DexEncodedMethod method,
+      Map<DexField, Integer> mapping) {
+  }
 }
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 85a3ac6..e5c8962 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
@@ -47,10 +47,13 @@
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.If;
 import com.android.tools.r8.ir.code.If.Type;
+import com.android.tools.r8.ir.code.InstanceGet;
+import com.android.tools.r8.ir.code.InstancePut;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionIterator;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.Invoke;
+import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.InvokeNewArray;
 import com.android.tools.r8.ir.code.InvokeVirtual;
@@ -733,6 +736,122 @@
     }
   }
 
+  public void identifyReceiverOnlyUsedForReadingFields(
+      DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
+    if (!method.isNonAbstractVirtualMethod()) {
+      return;
+    }
+
+    feedback.markReceiverOnlyUsedForReadingFields(method, null);
+
+    Value instance = code.getThis();
+    if (instance.numberOfPhiUsers() > 0) {
+      return;
+    }
+
+    Set<DexField> fields = Sets.newIdentityHashSet();
+    for (Instruction insn : instance.uniqueUsers()) {
+      if (!insn.isInstanceGet()) {
+        return;
+      }
+      InstanceGet get = insn.asInstanceGet();
+      if (get.dest() == instance || get.object() != instance) {
+        return;
+      }
+      fields.add(get.getField());
+    }
+    feedback.markReceiverOnlyUsedForReadingFields(method, fields);
+  }
+
+  public void identifyOnlyInitializesFieldsWithNoOtherSideEffects(
+      DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
+    assert method.isInstanceInitializer();
+
+    feedback.markOnlyInitializesFieldsWithNoOtherSideEffects(method, null);
+
+    if (code.hasCatchHandlers()) {
+      return;
+    }
+
+    List<Value> args = code.collectArguments(true /* not interested in receiver */);
+    Map<DexField, Integer> mapping = new IdentityHashMap<>();
+    Value receiver = code.getThis();
+
+    InstructionIterator it = code.instructionIterator();
+    while (it.hasNext()) {
+      Instruction instruction = it.next();
+
+      // Mark an argument.
+      if (instruction.isArgument()) {
+        continue;
+      }
+
+      // Allow super call to java.lang.Object.<init>() on 'this'.
+      if (instruction.isInvokeDirect()) {
+        InvokeDirect invokedDirect = instruction.asInvokeDirect();
+        if (invokedDirect.getInvokedMethod() != dexItemFactory.objectMethods.constructor ||
+            invokedDirect.getReceiver() != receiver) {
+          return;
+        }
+        continue;
+      }
+
+      // Allow final return.
+      if (instruction.isReturn()) {
+        continue;
+      }
+
+      // Allow assignment to this class' fields. If the assigned value is an argument
+      // reference update the mep. Otherwise just allow assigning any value, since all
+      // invalid values should be filtered out at the definitions.
+      if (instruction.isInstancePut()) {
+        InstancePut instancePut = instruction.asInstancePut();
+        DexField field = instancePut.getField();
+        if (instancePut.object() != receiver) {
+          return;
+        }
+
+        Value value = instancePut.value();
+        if (value.isArgument() && !value.isThis()) {
+          assert (args.contains(value));
+          mapping.put(field, args.indexOf(value));
+        } else {
+          mapping.remove(field);
+        }
+        continue;
+      }
+
+      // Allow non-throwing constants.
+      if (instruction.isConstInstruction()) {
+        if (instruction.instructionTypeCanThrow()) {
+          return;
+        }
+        continue;
+      }
+
+      // Allow goto instructions jumping to the next block.
+      if (instruction.isGoto()) {
+        if (instruction.getBlock().getNumber() + 1 !=
+            instruction.asGoto().getTarget().getNumber()) {
+          return;
+        }
+        continue;
+      }
+
+      // Allow binary and unary instructions if they don't throw.
+      if (instruction.isBinop() || instruction.isUnop()) {
+        if (instruction.instructionTypeCanThrow()) {
+          return;
+        }
+        continue;
+      }
+
+      return;
+    }
+
+    feedback.markOnlyInitializesFieldsWithNoOtherSideEffects(method, mapping);
+  }
+
   /**
    * An enum used to classify instructions according to a particular effect that they produce.
    *
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
new file mode 100644
index 0000000..90081d1
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
@@ -0,0 +1,351 @@
+// 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;
+
+import com.android.tools.r8.ApiLevelException;
+import com.android.tools.r8.graph.Code;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.TypeEnvironment;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
+import com.android.tools.r8.ir.code.InvokePolymorphic;
+import com.android.tools.r8.ir.code.InvokeStatic;
+import com.android.tools.r8.ir.conversion.CallSiteInformation;
+import com.android.tools.r8.ir.optimize.Inliner.InlineAction;
+import com.android.tools.r8.ir.optimize.Inliner.Reason;
+import com.android.tools.r8.logging.Log;
+import java.util.ListIterator;
+import java.util.function.Predicate;
+
+final class DefaultInliningOracle implements InliningOracle, InliningStrategy {
+
+  private final Inliner inliner;
+  private final DexEncodedMethod method;
+  private final IRCode code;
+  private final TypeEnvironment typeEnvironment;
+  private final CallSiteInformation callSiteInformation;
+  private final Predicate<DexEncodedMethod> isProcessedConcurrently;
+  private final InliningInfo info;
+  private final int inliningInstructionLimit;
+  private int instructionAllowance;
+
+  DefaultInliningOracle(
+      Inliner inliner,
+      DexEncodedMethod method,
+      IRCode code,
+      TypeEnvironment typeEnvironment,
+      CallSiteInformation callSiteInformation,
+      Predicate<DexEncodedMethod> isProcessedConcurrently,
+      int inliningInstructionLimit,
+      int inliningInstructionAllowance) {
+    this.inliner = inliner;
+    this.method = method;
+    this.code = code;
+    this.typeEnvironment = typeEnvironment;
+    this.callSiteInformation = callSiteInformation;
+    this.isProcessedConcurrently = isProcessedConcurrently;
+    info = Log.ENABLED ? new InliningInfo(method) : null;
+    this.inliningInstructionLimit = inliningInstructionLimit;
+    this.instructionAllowance = inliningInstructionAllowance;
+  }
+
+  @Override
+  public void finish() {
+    if (Log.ENABLED) {
+      Log.debug(getClass(), info.toString());
+    }
+  }
+
+  private DexEncodedMethod validateCandidate(InvokeMethod invoke, DexType invocationContext) {
+    DexEncodedMethod candidate =
+        invoke.computeSingleTarget(inliner.appInfo, typeEnvironment, invocationContext);
+    if ((candidate == null)
+        || (candidate.getCode() == null)
+        || inliner.appInfo.definitionFor(candidate.method.getHolder()).isLibraryClass()) {
+      if (info != null) {
+        info.exclude(invoke, "No inlinee");
+      }
+      return null;
+    }
+    // Ignore the implicit receiver argument.
+    int numberOfArguments =
+        invoke.arguments().size() - (invoke.isInvokeMethodWithReceiver() ? 1 : 0);
+    if (numberOfArguments != candidate.method.getArity()) {
+      if (info != null) {
+        info.exclude(invoke, "Argument number mismatch");
+      }
+      return null;
+    }
+    return candidate;
+  }
+
+  private Reason computeInliningReason(DexEncodedMethod target) {
+    if (target.getOptimizationInfo().forceInline()) {
+      return Reason.FORCE;
+    }
+    if (inliner.appInfo.hasLiveness()
+        && inliner.appInfo.withLiveness().alwaysInline.contains(target)) {
+      return Reason.ALWAYS;
+    }
+    if (callSiteInformation.hasSingleCallSite(target)) {
+      return Reason.SINGLE_CALLER;
+    }
+    if (isDoubleInliningTarget(target)) {
+      return Reason.DUAL_CALLER;
+    }
+    return Reason.SIMPLE;
+  }
+
+  private boolean canInlineStaticInvoke(DexEncodedMethod method, DexEncodedMethod target) {
+    // Only proceed with inlining a static invoke if:
+    // - the holder for the target equals the holder for the method, or
+    // - the target method always triggers class initialization of its holder before any other side
+    //   effect (hence preserving class initialization semantics).
+    // - there is no non-trivial class initializer.
+    DexType targetHolder = target.method.getHolder();
+    if (method.method.getHolder() == targetHolder) {
+      return true;
+    }
+    DexClass clazz = inliner.appInfo.definitionFor(targetHolder);
+    assert clazz != null;
+    if (target.getOptimizationInfo().triggersClassInitBeforeAnySideEffect()) {
+      return true;
+    }
+    return classInitializationHasNoSideffects(targetHolder);
+  }
+
+  /**
+   * Check for class initializer side effects when loading this class, as inlining might remove the
+   * load operation.
+   * <p>
+   * See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-5.html#jvms-5.5.
+   * <p>
+   * For simplicity, we are conservative and consider all interfaces, not only the ones with default
+   * methods.
+   */
+  private boolean classInitializationHasNoSideffects(DexType classToCheck) {
+    DexClass clazz = inliner.appInfo.definitionFor(classToCheck);
+    if ((clazz == null)
+        || clazz.hasNonTrivialClassInitializer()
+        || clazz.defaultValuesForStaticFieldsMayTriggerAllocation()) {
+      return false;
+    }
+    for (DexType iface : clazz.interfaces.values) {
+      if (!classInitializationHasNoSideffects(iface)) {
+        return false;
+      }
+    }
+    return clazz.superType == null || classInitializationHasNoSideffects(clazz.superType);
+  }
+
+  private synchronized boolean isDoubleInliningTarget(DexEncodedMethod candidate) {
+    // 10 is found from measuring.
+    return inliner.isDoubleInliningTarget(callSiteInformation, candidate)
+        && candidate.getCode().estimatedSizeForInliningAtMost(10);
+  }
+
+  private boolean passesInliningConstraints(InvokeMethod invoke, DexEncodedMethod candidate,
+      Reason reason) {
+    if (method == candidate) {
+      // Cannot handle recursive inlining at this point.
+      // Force inlined method should never be recursive.
+      assert !candidate.getOptimizationInfo().forceInline();
+      if (info != null) {
+        info.exclude(invoke, "direct recursion");
+      }
+      return false;
+    }
+
+    if (reason != Reason.FORCE && isProcessedConcurrently.test(candidate)) {
+      if (info != null) {
+        info.exclude(invoke, "is processed in parallel");
+      }
+      return false;
+    }
+
+    // Abort inlining attempt if method -> target access is not right.
+    if (!inliner.hasInliningAccess(method, candidate)) {
+      if (info != null) {
+        info.exclude(invoke, "target does not have right access");
+      }
+      return false;
+    }
+
+    DexClass holder = inliner.appInfo.definitionFor(candidate.method.getHolder());
+    if (holder.isInterface()) {
+      // Art978_virtual_interfaceTest correctly expects an IncompatibleClassChangeError exception at
+      // runtime.
+      if (info != null) {
+        info.exclude(invoke, "Do not inline target if method holder is an interface class");
+      }
+      return false;
+    }
+
+    if (holder.isLibraryClass()) {
+      // Library functions should not be inlined.
+      return false;
+    }
+
+    // Don't inline if target is synchronized.
+    if (candidate.accessFlags.isSynchronized()) {
+      if (info != null) {
+        info.exclude(invoke, "target is synchronized");
+      }
+      return false;
+    }
+
+    // Attempt to inline a candidate that is only called twice.
+    if ((reason == Reason.DUAL_CALLER) && (inliner.doubleInlining(method, candidate) == null)) {
+      if (info != null) {
+        info.exclude(invoke, "target is not ready for double inlining");
+      }
+      return false;
+    }
+
+    if (reason == Reason.SIMPLE) {
+      // If we are looking for a simple method, only inline if actually simple.
+      Code code = candidate.getCode();
+      if (!code.estimatedSizeForInliningAtMost(inliningInstructionLimit)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override
+  public InlineAction computeForInvokeWithReceiver(
+      InvokeMethodWithReceiver invoke, DexType invocationContext) {
+    DexEncodedMethod candidate = validateCandidate(invoke, invocationContext);
+    if (candidate == null || inliner.isBlackListed(candidate.method)) {
+      return null;
+    }
+
+    // We can only inline an instance method call if we preserve the null check semantic (which
+    // would throw NullPointerException if the receiver is null). Therefore we can inline only if
+    // one of the following conditions is true:
+    // * the candidate inlinee checks null receiver before any side effect
+    // * the receiver is known to be non-null
+    boolean receiverIsNeverNull =
+        !typeEnvironment.getLatticeElement(invoke.getReceiver()).isNullable();
+    if (!receiverIsNeverNull
+        && !candidate.getOptimizationInfo().checksNullReceiverBeforeAnySideEffect()) {
+      if (info != null) {
+        info.exclude(invoke, "receiver for candidate can be null");
+      }
+      return null;
+    }
+
+    Reason reason = computeInliningReason(candidate);
+    if (!candidate.isInliningCandidate(method, reason, inliner.appInfo)) {
+      // Abort inlining attempt if the single target is not an inlining candidate.
+      if (info != null) {
+        info.exclude(invoke, "target is not identified for inlining");
+      }
+      return null;
+    }
+
+    if (!passesInliningConstraints(invoke, candidate, reason)) {
+      return null;
+    }
+
+    if (info != null) {
+      info.include(invoke.getType(), candidate);
+    }
+    return new InlineAction(candidate, invoke, reason);
+  }
+
+  @Override
+  public InlineAction computeForInvokeStatic(InvokeStatic invoke, DexType invocationContext) {
+    DexEncodedMethod candidate = validateCandidate(invoke, invocationContext);
+    if (candidate == null || inliner.isBlackListed(candidate.method)) {
+      return null;
+    }
+
+    Reason reason = computeInliningReason(candidate);
+    // Determine if this should be inlined no matter how big it is.
+    if (!candidate.isInliningCandidate(method, reason, inliner.appInfo)) {
+      // Abort inlining attempt if the single target is not an inlining candidate.
+      if (info != null) {
+        info.exclude(invoke, "target is not identified for inlining");
+      }
+      return null;
+    }
+
+    // Abort inlining attempt if we can not guarantee class for static target has been initialized.
+    if (!canInlineStaticInvoke(method, candidate)) {
+      if (info != null) {
+        info.exclude(invoke, "target is static but we cannot guarantee class has been initialized");
+      }
+      return null;
+    }
+
+    if (!passesInliningConstraints(invoke, candidate, reason)) {
+      return null;
+    }
+
+    if (info != null) {
+      info.include(invoke.getType(), candidate);
+    }
+    return new InlineAction(candidate, invoke, reason);
+  }
+
+  @Override
+  public InlineAction computeForInvokePolymorphic(
+      InvokePolymorphic invoke, DexType invocationContext) {
+    // TODO: No inlining of invoke polymorphic for now.
+    if (info != null) {
+      info.exclude(invoke, "inlining through invoke signature polymorpic is not supported");
+    }
+    return null;
+  }
+
+  @Override
+  public void ensureMethodProcessed(
+      DexEncodedMethod target, IRCode inlinee) throws ApiLevelException {
+    if (!target.isProcessed()) {
+      if (Log.ENABLED) {
+        Log.verbose(getClass(), "Forcing extra inline on " + target.toSourceString());
+      }
+      inliner.performInlining(
+          target, inlinee, typeEnvironment, isProcessedConcurrently, callSiteInformation);
+    }
+  }
+
+  @Override
+  public boolean exceededAllowance() {
+    return instructionAllowance < 0;
+  }
+
+  @Override
+  public void markInlined(IRCode inlinee) {
+    instructionAllowance -= inliner.numberOfInstructions(inlinee);
+  }
+
+  @Override
+  public ListIterator<BasicBlock> updateTypeInformationIfNeeded(IRCode inlinee,
+      ListIterator<BasicBlock> blockIterator, BasicBlock block, BasicBlock invokeSuccessor) {
+    if (inliner.options.enableNonNullTracking) {
+      // Move the cursor back to where the inlinee blocks are added.
+      blockIterator = code.blocks.listIterator(code.blocks.indexOf(block));
+      // Kick off the tracker to add non-null IRs only to the inlinee blocks.
+      new NonNullTracker()
+          .addNonNullInPart(code, blockIterator, inlinee.blocks::contains);
+      // Move the cursor forward to where the inlinee blocks end.
+      blockIterator = code.blocks.listIterator(code.blocks.indexOf(invokeSuccessor));
+    }
+    // Update type env for inlined blocks.
+    typeEnvironment.analyzeBlocks(inlinee.topologicallySortedBlocks());
+    // TODO(b/69964136): need a test where refined env in inlinee affects the caller.
+    return blockIterator;
+  }
+
+  @Override
+  public DexType getReceiverTypeIfKnown(InvokeMethod invoke) {
+    return null; // Maybe improve later.
+  }
+}
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
new file mode 100644
index 0000000..86d40d9
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java
@@ -0,0 +1,85 @@
+// 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;
+
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
+import com.android.tools.r8.ir.code.InvokePolymorphic;
+import com.android.tools.r8.ir.code.InvokeStatic;
+import com.android.tools.r8.ir.optimize.Inliner.InlineAction;
+import com.android.tools.r8.ir.optimize.Inliner.Reason;
+import java.util.ListIterator;
+import java.util.Map;
+
+final class ForcedInliningOracle implements InliningOracle, InliningStrategy {
+  private final DexEncodedMethod method;
+  private final Map<InvokeMethodWithReceiver, Inliner.InliningInfo> invokesToInline;
+
+  ForcedInliningOracle(DexEncodedMethod method,
+      Map<InvokeMethodWithReceiver, Inliner.InliningInfo> invokesToInline) {
+    this.method = method;
+    this.invokesToInline = invokesToInline;
+  }
+
+  @Override
+  public void finish() {
+  }
+
+  @Override
+  public InlineAction computeForInvokeWithReceiver(
+      InvokeMethodWithReceiver invoke, DexType invocationContext) {
+    Inliner.InliningInfo info = invokesToInline.get(invoke);
+    if (info == null) {
+      return null;
+    }
+
+    assert method != info.target;
+    return new InlineAction(info.target, invoke, Reason.FORCE);
+  }
+
+  @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.
+  }
+
+  @Override
+  public void ensureMethodProcessed(DexEncodedMethod target, IRCode inlinee) {
+    assert target.isProcessed();
+  }
+
+  @Override
+  public ListIterator<BasicBlock> updateTypeInformationIfNeeded(IRCode inlinee,
+      ListIterator<BasicBlock> blockIterator, BasicBlock block, BasicBlock invokeSuccessor) {
+    return blockIterator;
+  }
+
+  @Override
+  public boolean exceededAllowance() {
+    return false; // Never exceeds allowance.
+  }
+
+  @Override
+  public void markInlined(IRCode inlinee) {
+  }
+
+  @Override
+  public DexType getReceiverTypeIfKnown(InvokeMethod invoke) {
+    assert invoke.isInvokeMethodWithReceiver();
+    Inliner.InliningInfo info = invokesToInline.get(invoke.asInvokeMethodWithReceiver());
+    assert info != null;
+    return info.receiverType;
+  }
+}
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 d6efdbb..a86130f 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
@@ -21,6 +21,7 @@
 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;
@@ -28,7 +29,6 @@
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.conversion.LensCodeRewriter;
 import com.android.tools.r8.ir.conversion.OptimizationFeedback;
-import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
@@ -43,10 +43,11 @@
 import java.util.stream.Collectors;
 
 public class Inliner {
+  private static final int INITIAL_INLINING_INSTRUCTION_ALLOWANCE = 1500;
 
   protected final AppInfoWithLiveness appInfo;
   private final GraphLense graphLense;
-  private final InternalOptions options;
+  final InternalOptions options;
 
   // State for inlining methods which are known to be called twice.
   private boolean applyDoubleInlining = false;
@@ -278,7 +279,7 @@
     }
   }
 
-  private int numberOfInstructions(IRCode code) {
+  final int numberOfInstructions(IRCode code) {
     int numOfInstructions = 0;
     for (BasicBlock block : code.blocks) {
       numOfInstructions += block.getInstructions().size();
@@ -364,6 +365,23 @@
     return true;
   }
 
+  public static class InliningInfo {
+    public final DexEncodedMethod target;
+    public final DexType receiverType; // null, if unknown
+
+    public InliningInfo(DexEncodedMethod target, DexType receiverType) {
+      this.target = target;
+      this.receiverType = receiverType;
+    }
+  }
+
+  public void performForcedInlining(DexEncodedMethod method, IRCode code,
+      Map<InvokeMethodWithReceiver, InliningInfo> invokesToInline) throws ApiLevelException {
+
+    ForcedInliningOracle oracle = new ForcedInliningOracle(method, invokesToInline);
+    performInliningImpl(oracle, oracle, method, code);
+  }
+
   public void performInlining(
       DexEncodedMethod method,
       IRCode code,
@@ -371,29 +389,40 @@
       Predicate<DexEncodedMethod> isProcessedConcurrently,
       CallSiteInformation callSiteInformation)
       throws ApiLevelException {
-    int instruction_allowance = 1500;
-    instruction_allowance -= numberOfInstructions(code);
-    if (instruction_allowance < 0) {
-      return;
-    }
-    InliningOracle oracle =
-        new InliningOracle(
+
+    DefaultInliningOracle oracle =
+        new DefaultInliningOracle(
             this,
             method,
+            code,
             typeEnvironment,
             callSiteInformation,
             isProcessedConcurrently,
-            options.inliningInstructionLimit);
+            options.inliningInstructionLimit,
+            INITIAL_INLINING_INSTRUCTION_ALLOWANCE - numberOfInstructions(code));
+
+    performInliningImpl(oracle, oracle, method, code);
+  }
+
+  private void performInliningImpl(
+      InliningStrategy strategy,
+      InliningOracle oracle,
+      DexEncodedMethod method,
+      IRCode code)
+      throws ApiLevelException {
+    if (strategy.exceededAllowance()) {
+      return;
+    }
 
     List<BasicBlock> blocksToRemove = new ArrayList<>();
     ListIterator<BasicBlock> blockIterator = code.listIterator();
-    while (blockIterator.hasNext() && (instruction_allowance >= 0)) {
+    while (blockIterator.hasNext() && !strategy.exceededAllowance()) {
       BasicBlock block = blockIterator.next();
       if (blocksToRemove.contains(block)) {
         continue;
       }
       InstructionListIterator iterator = block.listIterator();
-      while (iterator.hasNext() && (instruction_allowance >= 0)) {
+      while (iterator.hasNext() && !strategy.exceededAllowance()) {
         Instruction current = iterator.next();
         if (current.isInvokeMethod()) {
           InvokeMethod invoke = current.asInvokeMethod();
@@ -416,49 +445,27 @@
               if (block.hasCatchHandlers() && inlinee.computeNormalExitBlocks().isEmpty()) {
                 continue;
               }
+
               // If this code did not go through the full pipeline, apply inlining to make sure
               // that force inline targets get processed.
-              if (!target.isProcessed()) {
-                assert result.reason == Reason.FORCE;
-                if (Log.ENABLED) {
-                  Log.verbose(getClass(), "Forcing extra inline on " + target.toSourceString());
-                }
-                performInlining(
-                    target, inlinee, typeEnvironment, isProcessedConcurrently, callSiteInformation);
-              }
+              strategy.ensureMethodProcessed(target, inlinee);
+
               // Make sure constructor inlining is legal.
               assert !target.isClassInitializer();
               if (target.isInstanceInitializer()
                   && !legalConstructorInline(method, invoke, inlinee)) {
                 continue;
               }
-              DexType downcast = null;
-              if (invoke.isInvokeMethodWithReceiver()) {
-                // If the invoke has a receiver but the declared method holder is different
-                // from the computed target holder, inlining requires a downcast of the receiver.
-                if (target.method.getHolder() != invoke.getInvokedMethod().getHolder()) {
-                  downcast = result.target.method.getHolder();
-                }
-              }
+              DexType downcast = createDowncastIfNeeded(strategy, invoke, target);
               // Inline the inlinee code in place of the invoke instruction
               // Back up before the invoke instruction.
               iterator.previous();
-              instruction_allowance -= numberOfInstructions(inlinee);
-              if (instruction_allowance >= 0 || result.ignoreInstructionBudget()) {
+              strategy.markInlined(inlinee);
+              if (!strategy.exceededAllowance() || result.ignoreInstructionBudget()) {
                 BasicBlock invokeSuccessor =
                     iterator.inlineInvoke(code, inlinee, blockIterator, blocksToRemove, downcast);
-                if (options.enableNonNullTracking) {
-                  // Move the cursor back to where the inlinee blocks are added.
-                  blockIterator = code.blocks.listIterator(code.blocks.indexOf(block));
-                  // Kick off the tracker to add non-null IRs only to the inlinee blocks.
-                  new NonNullTracker()
-                      .addNonNullInPart(code, blockIterator, inlinee.blocks::contains);
-                  // Move the cursor forward to where the inlinee blocks end.
-                  blockIterator = code.blocks.listIterator(code.blocks.indexOf(invokeSuccessor));
-                }
-                // Update type env for inlined blocks.
-                typeEnvironment.analyzeBlocks(inlinee.topologicallySortedBlocks());
-                // TODO(b/69964136): need a test where refined env in inlinee affects the caller.
+                blockIterator = strategy.
+                    updateTypeInformationIfNeeded(inlinee, blockIterator, block, invokeSuccessor);
 
                 // If we inlined the invoke from a bridge method, it is no longer a bridge method.
                 if (method.accessFlags.isBridge()) {
@@ -481,4 +488,22 @@
     code.removeAllTrivialPhis();
     assert code.isConsistentSSA();
   }
+
+  private DexType createDowncastIfNeeded(
+      InliningStrategy strategy, InvokeMethod invoke, DexEncodedMethod target) {
+    if (invoke.isInvokeMethodWithReceiver()) {
+      // If the invoke has a receiver but the actual type of the receiver is different
+      // from the computed target holder, inlining requires a downcast of the receiver.
+      DexType assumedReceiverType = strategy.getReceiverTypeIfKnown(invoke);
+      if (assumedReceiverType == null) {
+        // In case we don't know exact type of the receiver we use declared
+        // method holder as a fallback.
+        assumedReceiverType = invoke.getInvokedMethod().getHolder();
+      }
+      if (assumedReceiverType != target.method.getHolder()) {
+        return target.method.getHolder();
+      }
+    }
+    return null;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java
index beb4caa..08f8045 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java
@@ -1,296 +1,28 @@
 // Copyright (c) 2017, 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;
 
-import com.android.tools.r8.graph.Code;
-import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.ir.analysis.type.TypeEnvironment;
-import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
 import com.android.tools.r8.ir.code.InvokePolymorphic;
 import com.android.tools.r8.ir.code.InvokeStatic;
-import com.android.tools.r8.ir.conversion.CallSiteInformation;
 import com.android.tools.r8.ir.optimize.Inliner.InlineAction;
-import com.android.tools.r8.ir.optimize.Inliner.Reason;
-import com.android.tools.r8.logging.Log;
-import java.util.function.Predicate;
 
 /**
- * The InliningOracle contains information needed for when inlining
- * other methods into @method.
+ * The InliningOracle contains information needed for when inlining other methods into @method.
  */
-public class InliningOracle {
+public interface InliningOracle {
 
-  private final Inliner inliner;
-  private final DexEncodedMethod method;
-  private final TypeEnvironment typeEnvironment;
-  private final CallSiteInformation callSiteInformation;
-  private final Predicate<DexEncodedMethod> isProcessedConcurrently;
-  private final InliningInfo info;
-  private final int inliningInstructionLimit;
+  void finish();
 
-  InliningOracle(
-      Inliner inliner,
-      DexEncodedMethod method,
-      TypeEnvironment typeEnvironment,
-      CallSiteInformation callSiteInformation,
-      Predicate<DexEncodedMethod> isProcessedConcurrently,
-      int inliningInstructionLimit) {
-    this.inliner = inliner;
-    this.method = method;
-    this.typeEnvironment = typeEnvironment;
-    this.callSiteInformation = callSiteInformation;
-    this.isProcessedConcurrently = isProcessedConcurrently;
-    info = Log.ENABLED ? new InliningInfo(method) : null;
-    this.inliningInstructionLimit = inliningInstructionLimit;
-  }
+  InlineAction computeForInvokeWithReceiver(
+      InvokeMethodWithReceiver invoke, DexType invocationContext);
 
-  void finish() {
-    if (Log.ENABLED) {
-      Log.debug(getClass(), info.toString());
-    }
-  }
+  InlineAction computeForInvokeStatic(
+      InvokeStatic invoke, DexType invocationContext);
 
-  private DexEncodedMethod validateCandidate(InvokeMethod invoke, DexType invocationContext) {
-    DexEncodedMethod candidate =
-        invoke.computeSingleTarget(inliner.appInfo, typeEnvironment, invocationContext);
-    if ((candidate == null)
-        || (candidate.getCode() == null)
-        || inliner.appInfo.definitionFor(candidate.method.getHolder()).isLibraryClass()) {
-      if (info != null) {
-        info.exclude(invoke, "No inlinee");
-      }
-      return null;
-    }
-    // Ignore the implicit receiver argument.
-    int numberOfArguments =
-        invoke.arguments().size() - (invoke.isInvokeMethodWithReceiver() ? 1 : 0);
-    if (numberOfArguments != candidate.method.getArity()) {
-      if (info != null) {
-        info.exclude(invoke, "Argument number mismatch");
-      }
-      return null;
-    }
-    return candidate;
-  }
-
-  private Reason computeInliningReason(DexEncodedMethod target) {
-    if (target.getOptimizationInfo().forceInline()) {
-      return Reason.FORCE;
-    }
-    if (inliner.appInfo.hasLiveness()
-        && inliner.appInfo.withLiveness().alwaysInline.contains(target)) {
-      return Reason.ALWAYS;
-    }
-    if (callSiteInformation.hasSingleCallSite(target)) {
-      return Reason.SINGLE_CALLER;
-    }
-    if (isDoubleInliningTarget(target)) {
-      return Reason.DUAL_CALLER;
-    }
-    return Reason.SIMPLE;
-  }
-
-  private boolean canInlineStaticInvoke(DexEncodedMethod method, DexEncodedMethod target) {
-    // Only proceed with inlining a static invoke if:
-    // - the holder for the target equals the holder for the method, or
-    // - the target method always triggers class initialization of its holder before any other side
-    //   effect (hence preserving class initialization semantics).
-    // - there is no non-trivial class initializer.
-    DexType targetHolder = target.method.getHolder();
-    if (method.method.getHolder() == targetHolder) {
-      return true;
-    }
-    DexClass clazz = inliner.appInfo.definitionFor(targetHolder);
-    assert clazz != null;
-    if (target.getOptimizationInfo().triggersClassInitBeforeAnySideEffect()) {
-      return true;
-    }
-    return classInitializationHasNoSideffects(targetHolder);
-  }
-
-  /**
-   * Check for class initializer side effects when loading this class, as inlining might remove the
-   * load operation.
-   * <p>
-   * See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-5.html#jvms-5.5.
-   * <p>
-   * For simplicity, we are conservative and consider all interfaces, not only the ones with default
-   * methods.
-   */
-  private boolean classInitializationHasNoSideffects(DexType classToCheck) {
-    DexClass clazz = inliner.appInfo.definitionFor(classToCheck);
-    if ((clazz == null)
-        || clazz.hasNonTrivialClassInitializer()
-        || clazz.defaultValuesForStaticFieldsMayTriggerAllocation()) {
-      return false;
-    }
-    for (DexType iface : clazz.interfaces.values) {
-      if (!classInitializationHasNoSideffects(iface)) {
-        return false;
-      }
-    }
-    return clazz.superType == null || classInitializationHasNoSideffects(clazz.superType);
-  }
-
-  private synchronized boolean isDoubleInliningTarget(DexEncodedMethod candidate) {
-    // 10 is found from measuring.
-    return inliner.isDoubleInliningTarget(callSiteInformation, candidate)
-        && candidate.getCode().estimatedSizeForInliningAtMost(10);
-  }
-
-  private boolean passesInliningConstraints(InvokeMethod invoke, DexEncodedMethod candidate,
-      Reason reason) {
-    if (method == candidate) {
-      // Cannot handle recursive inlining at this point.
-      // Force inlined method should never be recursive.
-      assert !candidate.getOptimizationInfo().forceInline();
-      if (info != null) {
-        info.exclude(invoke, "direct recursion");
-      }
-      return false;
-    }
-
-    if (reason != Reason.FORCE && isProcessedConcurrently.test(candidate)) {
-      if (info != null) {
-        info.exclude(invoke, "is processed in parallel");
-      }
-      return false;
-    }
-
-    // Abort inlining attempt if method -> target access is not right.
-    if (!inliner.hasInliningAccess(method, candidate)) {
-      if (info != null) {
-        info.exclude(invoke, "target does not have right access");
-      }
-      return false;
-    }
-
-    DexClass holder = inliner.appInfo.definitionFor(candidate.method.getHolder());
-    if (holder.isInterface()) {
-      // Art978_virtual_interfaceTest correctly expects an IncompatibleClassChangeError exception at
-      // runtime.
-      if (info != null) {
-        info.exclude(invoke, "Do not inline target if method holder is an interface class");
-      }
-      return false;
-    }
-
-    if (holder.isLibraryClass()) {
-      // Library functions should not be inlined.
-      return false;
-    }
-
-    // Don't inline if target is synchronized.
-    if (candidate.accessFlags.isSynchronized()) {
-      if (info != null) {
-        info.exclude(invoke, "target is synchronized");
-      }
-      return false;
-    }
-
-    // Attempt to inline a candidate that is only called twice.
-    if ((reason == Reason.DUAL_CALLER) && (inliner.doubleInlining(method, candidate) == null)) {
-      if (info != null) {
-        info.exclude(invoke, "target is not ready for double inlining");
-      }
-      return false;
-    }
-
-    if (reason == Reason.SIMPLE) {
-      // If we are looking for a simple method, only inline if actually simple.
-      Code code = candidate.getCode();
-      if (!code.estimatedSizeForInliningAtMost(inliningInstructionLimit)) {
-        return false;
-      }
-    }
-    return true;
-  }
-
-  public InlineAction computeForInvokeWithReceiver(
-      InvokeMethodWithReceiver invoke, DexType invocationContext) {
-    DexEncodedMethod candidate = validateCandidate(invoke, invocationContext);
-    if (candidate == null || inliner.isBlackListed(candidate.method)) {
-      return null;
-    }
-
-    // We can only inline an instance method call if we preserve the null check semantic (which
-    // would throw NullPointerException if the receiver is null). Therefore we can inline only if
-    // one of the following conditions is true:
-    // * the candidate inlinee checks null receiver before any side effect
-    // * the receiver is known to be non-null
-    boolean receiverIsNeverNull =
-        !typeEnvironment.getLatticeElement(invoke.getReceiver()).isNullable();
-    if (!receiverIsNeverNull
-        && !candidate.getOptimizationInfo().checksNullReceiverBeforeAnySideEffect()) {
-      if (info != null) {
-        info.exclude(invoke, "receiver for candidate can be null");
-      }
-      return null;
-    }
-
-    Reason reason = computeInliningReason(candidate);
-    if (!candidate.isInliningCandidate(method, reason, inliner.appInfo)) {
-      // Abort inlining attempt if the single target is not an inlining candidate.
-      if (info != null) {
-        info.exclude(invoke, "target is not identified for inlining");
-      }
-      return null;
-    }
-
-    if (!passesInliningConstraints(invoke, candidate, reason)) {
-      return null;
-    }
-
-    if (info != null) {
-      info.include(invoke.getType(), candidate);
-    }
-    return new InlineAction(candidate, invoke, reason);
-  }
-
-  public InlineAction computeForInvokeStatic(InvokeStatic invoke, DexType invocationContext) {
-    DexEncodedMethod candidate = validateCandidate(invoke, invocationContext);
-    if (candidate == null || inliner.isBlackListed(candidate.method)) {
-      return null;
-    }
-
-    Reason reason = computeInliningReason(candidate);
-    // Determine if this should be inlined no matter how big it is.
-    if (!candidate.isInliningCandidate(method, reason, inliner.appInfo)) {
-      // Abort inlining attempt if the single target is not an inlining candidate.
-      if (info != null) {
-        info.exclude(invoke, "target is not identified for inlining");
-      }
-      return null;
-    }
-
-    // Abort inlining attempt if we can not guarantee class for static target has been initialized.
-    if (!canInlineStaticInvoke(method, candidate)) {
-      if (info != null) {
-        info.exclude(invoke, "target is static but we cannot guarantee class has been initialized");
-      }
-      return null;
-    }
-
-    if (!passesInliningConstraints(invoke, candidate, reason)) {
-      return null;
-    }
-
-    if (info != null) {
-      info.include(invoke.getType(), candidate);
-    }
-    return new InlineAction(candidate, invoke, reason);
-  }
-
-  public InlineAction computeForInvokePolymorpic(
-      InvokePolymorphic invoke, DexType invocationContext) {
-    // TODO: No inlining of invoke polymorphic for now.
-    if (info != null) {
-      info.exclude(invoke, "inlining through invoke signature polymorpic is not supported");
-    }
-    return null;
-  }
+  InlineAction computeForInvokePolymorphic(
+      InvokePolymorphic invoke, DexType invocationContext);
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java b/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java
new file mode 100644
index 0000000..2b0c7e5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java
@@ -0,0 +1,27 @@
+// 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;
+
+import com.android.tools.r8.ApiLevelException;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import java.util.ListIterator;
+
+interface InliningStrategy {
+  boolean exceededAllowance();
+
+  void markInlined(IRCode inlinee);
+
+  void ensureMethodProcessed(
+      DexEncodedMethod target, IRCode inlinee) throws ApiLevelException;
+
+  ListIterator<BasicBlock> updateTypeInformationIfNeeded(IRCode inlinee,
+      ListIterator<BasicBlock> blockIterator, BasicBlock block, BasicBlock invokeSuccessor);
+
+  DexType getReceiverTypeIfKnown(InvokeMethod invoke);
+}
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
new file mode 100644
index 0000000..7d13b1a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
@@ -0,0 +1,385 @@
+// 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.ApiLevelException;
+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.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.IRCode;
+import com.android.tools.r8.ir.code.InstanceGet;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InvokeDirect;
+import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
+import com.android.tools.r8.ir.code.NewInstance;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.optimize.Inliner.InliningInfo;
+import com.android.tools.r8.ir.optimize.Inliner.Reason;
+import com.google.common.collect.Streams;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+public final class ClassInliner {
+  private final DexItemFactory factory;
+  private final ConcurrentHashMap<DexType, Boolean> knownClasses = new ConcurrentHashMap<>();
+
+  private static final Map<DexField, Integer> NO_MAPPING = new IdentityHashMap<>();
+
+  public interface InlinerAction {
+    void inline(Map<InvokeMethodWithReceiver, InliningInfo> methods) throws ApiLevelException;
+  }
+
+  public ClassInliner(DexItemFactory factory) {
+    this.factory = factory;
+  }
+
+  // Process method code and inline eligible class instantiations, in short:
+  //
+  // - collect all 'new-instance' instructions in the original code. Note that class
+  // inlining, if happens, mutates code and can add 'new-instance' instructions.
+  // Processing them as well is possible, but does not seem to have much value.
+  //
+  // - for each 'new-instance' we check if it is eligible for inlining, i.e:
+  //     -> the class of the new instance is 'eligible' (see computeClassEligible(...))
+  //     -> the instance is initialized with 'eligible' constructor (see
+  //        onlyInitializesFieldsWithNoOtherSideEffects flag in method's optimization
+  //        info); eligible constructor also defines a set of instance fields directly
+  //        initialized with parameter values, called field initialization mapping below
+  //     -> has only 'eligible' uses, i.e:
+  //          * it is a receiver of a field read if the field is present in the
+  //            field initialization mapping
+  //          * it is a receiver of virtual or interface call with single target being
+  //            a method only reading fields in the current field initialization mapping
+  //
+  // - inline eligible 'new-instance' instructions, i.e:
+  //     -> force inline methods called on the instance (which may introduce additional
+  //        instance field reads, but only for fields present in the current field
+  //        initialization mapping)
+  //     -> replace instance field reads with appropriate values passed to the constructor
+  //        according to field initialization mapping
+  //     -> remove constructor call
+  //     -> remove 'new-instance' instructions
+  //
+  // For example:
+  //
+  // Original code:
+  //   class C {
+  //     static class L {
+  //       final int x;
+  //       L(int x) {
+  //         this.x = x;
+  //       }
+  //       int getX() {
+  //         return x;
+  //       }
+  //     }
+  //     static int method1() {
+  //       return new L(1).x;
+  //     }
+  //     static int method2() {
+  //       return new L(1).getX();
+  //     }
+  //   }
+  //
+  // Code after class C is 'inlined':
+  //   class C {
+  //     static int method1() {
+  //       return 1;
+  //     }
+  //     static int method2() {
+  //       return 1;
+  //     }
+  //   }
+  //
+  public final void processMethodCode(
+      AppInfoWithSubtyping appInfo,
+      DexEncodedMethod method,
+      IRCode code,
+      Predicate<DexEncodedMethod> isProcessedConcurrently,
+      InlinerAction inliner) throws ApiLevelException {
+
+    // Collect all the new-instance instructions in the code before inlining.
+    List<NewInstance> newInstances = Streams.stream(code.instructionIterator())
+        .filter(Instruction::isNewInstance)
+        .map(Instruction::asNewInstance)
+        .collect(Collectors.toList());
+
+    nextNewInstance:
+    for (NewInstance newInstance : newInstances) {
+      Value eligibleInstance = newInstance.outValue();
+      if (eligibleInstance == null) {
+        continue;
+      }
+
+      DexType eligibleClass = newInstance.clazz;
+      if (!isClassEligible(appInfo, eligibleClass)) {
+        continue;
+      }
+
+      // No Phi users.
+      if (eligibleInstance.numberOfPhiUsers() > 0) {
+        continue;
+      }
+
+      Set<Instruction> uniqueUsers = eligibleInstance.uniqueUsers();
+
+      // Find an initializer invocation.
+      InvokeDirect eligibleInitCall = null;
+      Map<DexField, Integer> mappings = null;
+      for (Instruction user : uniqueUsers) {
+        if (!user.isInvokeDirect()) {
+          continue;
+        }
+
+        InvokeDirect candidate = user.asInvokeDirect();
+        DexMethod candidateInit = candidate.getInvokedMethod();
+        if (factory.isConstructor(candidateInit) &&
+            candidate.inValues().lastIndexOf(eligibleInstance) == 0) {
+
+          if (candidateInit.holder != eligibleClass) {
+            // Inlined constructor call? We won't get field initialization mapping in this
+            // case, but since we only support eligible classes extending java.lang.Object,
+            // it's safe to assume an empty mapping.
+            if (candidateInit.holder == factory.objectType) {
+              mappings = Collections.emptyMap();
+            }
+
+          } else {
+            // Is it a call to an *eligible* constructor?
+            mappings = getConstructorFieldMappings(appInfo, candidateInit, isProcessedConcurrently);
+          }
+
+          eligibleInitCall = candidate;
+          break;
+        }
+      }
+
+      if (mappings == null) {
+        continue;
+      }
+
+      // Check all regular users.
+      Map<InvokeMethodWithReceiver, InliningInfo> methodCalls = new IdentityHashMap<>();
+
+      for (Instruction user : uniqueUsers) {
+        if (user == eligibleInitCall) {
+          continue /* next user */;
+        }
+
+        if (user.isInstanceGet()) {
+          InstanceGet instanceGet = user.asInstanceGet();
+          if (mappings.containsKey(instanceGet.getField())) {
+            continue /* next user */;
+          }
+
+          // Not replaceable field read.
+          continue nextNewInstance;
+        }
+
+        if (user.isInvokeVirtual() || user.isInvokeInterface()) {
+          InvokeMethodWithReceiver invoke = user.asInvokeMethodWithReceiver();
+          if (invoke.inValues().lastIndexOf(eligibleInstance) > 0) {
+            continue nextNewInstance; // Instance must only be passes as a receiver.
+          }
+
+          DexEncodedMethod singleTarget =
+              findSingleTarget(appInfo, invoke, eligibleClass);
+          if (singleTarget == null) {
+            continue nextNewInstance;
+          }
+          if (isProcessedConcurrently.test(singleTarget)) {
+            continue nextNewInstance;
+          }
+          if (method == singleTarget) {
+            continue nextNewInstance; // Don't inline itself.
+          }
+
+          if (!singleTarget.getOptimizationInfo()
+              .isReceiverOnlyUsedForReadingFields(mappings.keySet())) {
+            continue nextNewInstance; // Target must be trivial.
+          }
+
+          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.
+            continue nextNewInstance;
+          }
+
+          methodCalls.put(invoke, new InliningInfo(singleTarget, eligibleClass));
+          continue /* next user */;
+        }
+
+        continue nextNewInstance; // Unsupported user.
+      }
+
+      // Force-inline of method invocation if any.
+      inlineAllCalls(inliner, methodCalls);
+      assert assertOnlyConstructorAndFieldReads(eligibleInstance, eligibleInitCall, mappings);
+
+      // Replace all field reads with arguments passed to the constructor.
+      patchFieldReads(eligibleInstance, eligibleInitCall, mappings);
+      assert assertOnlyConstructor(eligibleInstance, eligibleInitCall);
+
+      // Remove constructor call and new-instance instructions.
+      removeInstruction(eligibleInitCall);
+      removeInstruction(newInstance);
+      code.removeAllTrivialPhis();
+    }
+  }
+
+  private void inlineAllCalls(InlinerAction inliner,
+      Map<InvokeMethodWithReceiver, InliningInfo> methodCalls) throws ApiLevelException {
+    if (!methodCalls.isEmpty()) {
+      inliner.inline(methodCalls);
+    }
+  }
+
+  private void patchFieldReads(
+      Value instance, InvokeDirect invokeMethod, Map<DexField, Integer> mappings) {
+    for (Instruction user : instance.uniqueUsers()) {
+      if (!user.isInstanceGet()) {
+        continue;
+      }
+      InstanceGet fieldRead = user.asInstanceGet();
+
+      // Replace the field read with
+      assert mappings.containsKey(fieldRead.getField());
+      Value arg = invokeMethod.inValues().get(1 + mappings.get(fieldRead.getField()));
+      assert arg != null;
+      Value value = fieldRead.outValue();
+      if (value != null) {
+        value.replaceUsers(arg);
+        assert value.numberOfAllUsers() == 0;
+      }
+
+      // Remove instruction.
+      removeInstruction(fieldRead);
+    }
+  }
+
+  private void removeInstruction(Instruction instruction) {
+    instruction.inValues().forEach(v -> v.removeUser(instruction));
+    instruction.getBlock().removeInstruction(instruction);
+  }
+
+  private boolean assertOnlyConstructorAndFieldReads(
+      Value instance, InvokeDirect invokeMethod, Map<DexField, Integer> mappings) {
+    for (Instruction user : instance.uniqueUsers()) {
+      if (user != invokeMethod &&
+          !(user.isInstanceGet() && mappings.containsKey(user.asFieldInstruction().getField()))) {
+        throw new Unreachable("Not all calls are inlined!");
+      }
+    }
+    return true;
+  }
+
+  private boolean assertOnlyConstructor(Value instance, InvokeDirect invokeMethod) {
+    for (Instruction user : instance.uniqueUsers()) {
+      if (user != invokeMethod) {
+        throw new Unreachable("Not all field reads are substituted!");
+      }
+    }
+    return true;
+  }
+
+  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 Map<DexField, Integer> getConstructorFieldMappings(
+      AppInfo appInfo, DexMethod init, Predicate<DexEncodedMethod> isProcessedConcurrently) {
+    assert isClassEligible(appInfo, init.holder);
+
+    DexEncodedMethod definition = appInfo.definitionFor(init);
+    if (definition == null) {
+      return NO_MAPPING;
+    }
+
+    if (isProcessedConcurrently.test(definition)) {
+      return NO_MAPPING;
+    }
+
+    if (definition.accessFlags.isAbstract() || definition.accessFlags.isNative()) {
+      return NO_MAPPING;
+    }
+
+    return definition.getOptimizationInfo().onlyInitializesFieldsWithNoOtherSideEffects();
+  }
+
+  private boolean isClassEligible(AppInfo appInfo, DexType clazz) {
+    Boolean eligible = knownClasses.get(clazz);
+    if (eligible == null) {
+      Boolean computed = computeClassEligible(appInfo, clazz);
+      Boolean existing = knownClasses.putIfAbsent(clazz, computed);
+      assert existing == null || existing == computed;
+      eligible = existing == null ? computed : existing;
+    }
+    return eligible;
+  }
+
+  // Class is eligible for this optimization. Eligibility implementation:
+  //   - not an abstract or interface
+  //   - directly extends java.lang.Object
+  //   - does not declare finalizer
+  //   - does not trigger any static initializers
+  private boolean computeClassEligible(AppInfo appInfo, DexType clazz) {
+    DexClass definition = appInfo.definitionFor(clazz);
+    if (definition == null || definition.isLibraryClass() ||
+        definition.accessFlags.isAbstract() || definition.accessFlags.isInterface()) {
+      return false;
+    }
+
+    // Must directly extend Object.
+    if (definition.superType != factory.objectType) {
+      return false;
+    }
+
+    // Class must not define finalizer.
+    for (DexEncodedMethod method : definition.virtualMethods()) {
+      if (method.method.name == factory.finalizeMethodName &&
+          method.method.proto == factory.objectMethods.finalize.proto) {
+        return false;
+      }
+    }
+
+    // Check for static initializers in this class or any of interfaces it implements.
+    return !appInfo.canTriggerStaticInitializer(clazz);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 4c40d61..92c68aa 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -81,6 +81,7 @@
       enableDevirtualization = false;
       enableNonNullTracking = false;
       enableInlining = false;
+      enableClassInlining = false;
       enableSwitchMapRemoval = false;
       outline.enabled = false;
       enableValuePropagation = false;
@@ -97,6 +98,7 @@
   public boolean enableDevirtualization = true;
   public boolean enableNonNullTracking = true;
   public boolean enableInlining = true;
+  public boolean enableClassInlining = true;
   public int inliningInstructionLimit = 5;
   public boolean enableSwitchMapRemoval = true;
   public final OutlineOptions outline = new OutlineOptions();
diff --git a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
index ac0955e..b27569b 100644
--- a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
@@ -995,6 +995,14 @@
       "625-checker-licm-regressions"
   );
 
+  private static List<String> requireClassInliningToBeDisabled = ImmutableList.of(
+      // Test depends on exception produced for missing method or similar cases, but
+      // after class inlining removes class instantiations and references the exception
+      // is not produced.
+      "042-new-instance",
+      "075-verification-error"
+  );
+
   private static List<String> hasMissingClasses = ImmutableList.of(
       "091-override-package-private-method",
       "003-omnibus-opcodes",
@@ -1085,6 +1093,8 @@
     private final boolean outputMayDiffer;
     // Whether to disable inlining
     private final boolean disableInlining;
+    // Whether to disable class inlining
+    private final boolean disableClassInlining;
     // Has missing classes.
     private final boolean hasMissingClasses;
 
@@ -1102,6 +1112,7 @@
         boolean expectedToFailWithX8,
         boolean outputMayDiffer,
         boolean disableInlining,
+        boolean disableClassInlining,
         boolean hasMissingClasses,
         DexVm dexVm) {
       this.name = name;
@@ -1117,6 +1128,7 @@
       this.expectedToFailWithX8 = expectedToFailWithX8;
       this.outputMayDiffer = outputMayDiffer;
       this.disableInlining = disableInlining;
+      this.disableClassInlining = disableClassInlining;
       this.hasMissingClasses = hasMissingClasses;
     }
 
@@ -1142,6 +1154,7 @@
           false,
           false,
           disableInlining,
+          true, // Disable class inlining for JCTF tests.
           false,
           dexVm);
     }
@@ -1306,6 +1319,7 @@
                 expectedToFailWithCompilerSet.contains(name),
                 outputMayDiffer.contains(name),
                 requireInliningToBeDisabled.contains(name),
+                requireClassInliningToBeDisabled.contains(name),
                 hasMissingClasses.contains(name),
                 dexVm));
       }
@@ -1372,11 +1386,12 @@
       String resultPath,
       CompilationMode compilationMode,
       boolean disableInlining,
+      boolean disableClassInlining,
       boolean hasMissingClasses)
       throws IOException, ProguardRuleParserException, ExecutionException, CompilationException,
       CompilationFailedException {
     executeCompilerUnderTest(compilerUnderTest, fileNames, resultPath, compilationMode, null,
-        disableInlining, hasMissingClasses);
+        disableInlining, disableClassInlining, hasMissingClasses);
   }
 
   private void executeCompilerUnderTest(
@@ -1385,7 +1400,9 @@
       String resultPath,
       CompilationMode mode,
       String keepRulesFile,
-      boolean disableInlining, boolean hasMissingClasses)
+      boolean disableInlining,
+      boolean disableClassInlining,
+      boolean hasMissingClasses)
       throws IOException, ProguardRuleParserException, ExecutionException, CompilationException,
         CompilationFailedException {
     assert mode != null;
@@ -1523,6 +1540,9 @@
                 if (disableInlining) {
                   options.enableInlining = false;
                 }
+                if (disableClassInlining) {
+                  options.enableClassInlining = false;
+                }
                 options.lineNumberOptimization = LineNumberOptimization.OFF;
                 // Some tests actually rely on missing classes for what they test.
                 options.ignoreMissingClasses = hasMissingClasses;
@@ -1734,7 +1754,8 @@
       throws IOException, ProguardRuleParserException, ExecutionException, CompilationException,
       CompilationFailedException {
     executeCompilerUnderTest(compilerUnderTest, fileNames, resultDir.getAbsolutePath(), mode,
-        specification.disableInlining, specification.hasMissingClasses);
+        specification.disableInlining, specification.disableClassInlining,
+        specification.hasMissingClasses);
 
     if (!ToolHelper.artSupported()) {
       return;
@@ -1907,7 +1928,8 @@
       try {
         executeCompilerUnderTest(
             compilerUnderTest, fileNames, resultDir.getCanonicalPath(), compilationMode,
-            specification.disableInlining, specification.hasMissingClasses);
+            specification.disableInlining, specification.disableClassInlining,
+            specification.hasMissingClasses);
       } catch (CompilationException | CompilationFailedException e) {
         throw new CompilationError(e.getMessage(), e);
       } catch (ExecutionException e) {
@@ -1919,13 +1941,15 @@
       expectException(Throwable.class);
       executeCompilerUnderTest(
           compilerUnderTest, fileNames, resultDir.getCanonicalPath(), compilationMode,
-          specification.disableInlining, specification.hasMissingClasses);
+          specification.disableInlining, specification.disableClassInlining,
+          specification.hasMissingClasses);
       System.err.println("Should have failed R8/D8 compilation with an exception.");
       return;
     } else {
       executeCompilerUnderTest(
           compilerUnderTest, fileNames, resultDir.getCanonicalPath(), compilationMode,
-          specification.disableInlining, specification.hasMissingClasses);
+          specification.disableInlining, specification.disableClassInlining,
+          specification.hasMissingClasses);
     }
 
     if (!specification.skipArt && ToolHelper.artSupported()) {
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java b/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java
index b973818..62dd4af 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java
@@ -155,24 +155,27 @@
                 .build());
         break;
       }
-      case R8:
-        {
-          R8Command command =
-              addInputFile(R8Command.builder())
-                  .setOutput(getOutputFile(), outputMode)
-                  .setMode(mode)
-                  .build();
-          ExceptionUtils.withR8CompilationHandler(
-              command.getReporter(),
-              () ->
-                  ToolHelper.runR8(
-                      command,
-                      options -> {
-                        options.lineNumberOptimization = LineNumberOptimization.OFF;
-                        options.enableCfFrontend = frontend == Frontend.CF;
-                      }));
-          break;
-        }
+      case R8: {
+        R8Command command =
+            addInputFile(R8Command.builder())
+                .setOutput(getOutputFile(), outputMode)
+                .setMode(mode)
+                .build();
+        ExceptionUtils.withR8CompilationHandler(
+            command.getReporter(),
+            () ->
+                ToolHelper.runR8(
+                    command,
+                    options -> {
+                      options.lineNumberOptimization = LineNumberOptimization.OFF;
+                      options.enableCfFrontend = frontend == Frontend.CF;
+                      if (output == Output.CF) {
+                        // Class inliner is not supported with CF backend yet.
+                        options.enableClassInlining = false;
+                      }
+                    }));
+        break;
+      }
       default:
         throw new Unreachable();
     }
diff --git a/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
index e256ae4..c6a2bd3 100644
--- a/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
@@ -39,6 +39,7 @@
 
   private void configure(InternalOptions options) {
     options.enableClassMerging = true;
+    options.enableClassInlining = false;
   }
 
   private void runR8(Path proguardConfig, Consumer<InternalOptions> optionsConsumer)
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/CheckCastRemovalTest.java b/src/test/java/com/android/tools/r8/ir/optimize/CheckCastRemovalTest.java
index 1f7c990..be7ee92 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/CheckCastRemovalTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/CheckCastRemovalTest.java
@@ -50,7 +50,7 @@
     List<String> pgConfigs = ImmutableList.of(
         "-keep class " + CLASS_NAME + " { *; }",
         "-dontshrink");
-    AndroidApp app = compileWithR8(builder, pgConfigs, null);
+    AndroidApp app = compileWithR8(builder, pgConfigs, o -> o.enableClassInlining = false);
 
     DexEncodedMethod method = getMethod(app, CLASS_NAME, main);
     assertNotNull(method);
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/MemberValuePropagationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/MemberValuePropagationTest.java
index e8f2a2a..47f0db3 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/MemberValuePropagationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/MemberValuePropagationTest.java
@@ -110,7 +110,7 @@
             .addProguardConfigurationFiles(proguardConfig)
             .setDisableMinification(true)
             .build(),
-        null);
+        o -> o.enableClassInlining = false);
     return dexOutputDir.resolve("classes.dex");
   }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
new file mode 100644
index 0000000..e9319d2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
@@ -0,0 +1,161 @@
+// 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.VmTestRunner;
+import com.android.tools.r8.code.NewInstance;
+import com.android.tools.r8.graph.DexCode;
+import com.android.tools.r8.ir.optimize.classinliner.trivial.ClassWithFinal;
+import com.android.tools.r8.ir.optimize.classinliner.trivial.CycleReferenceAB;
+import com.android.tools.r8.ir.optimize.classinliner.trivial.CycleReferenceBA;
+import com.android.tools.r8.ir.optimize.classinliner.trivial.EmptyClass;
+import com.android.tools.r8.ir.optimize.classinliner.trivial.EmptyClassWithInitializer;
+import com.android.tools.r8.ir.optimize.classinliner.trivial.Iface1;
+import com.android.tools.r8.ir.optimize.classinliner.trivial.Iface1Impl;
+import com.android.tools.r8.ir.optimize.classinliner.trivial.Iface2;
+import com.android.tools.r8.ir.optimize.classinliner.trivial.Iface2Impl;
+import com.android.tools.r8.ir.optimize.classinliner.trivial.ReferencedFields;
+import com.android.tools.r8.ir.optimize.classinliner.trivial.TrivialTestClass;
+import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.google.common.collect.Sets;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(VmTestRunner.class)
+public class ClassInlinerTest extends TestBase {
+  @Test
+  public void testTrivial() throws Exception {
+    byte[][] classes = {
+        ToolHelper.getClassAsBytes(TrivialTestClass.class),
+        ToolHelper.getClassAsBytes(TrivialTestClass.Inner.class),
+        ToolHelper.getClassAsBytes(ReferencedFields.class),
+        ToolHelper.getClassAsBytes(EmptyClass.class),
+        ToolHelper.getClassAsBytes(EmptyClassWithInitializer.class),
+        ToolHelper.getClassAsBytes(Iface1.class),
+        ToolHelper.getClassAsBytes(Iface1Impl.class),
+        ToolHelper.getClassAsBytes(Iface2.class),
+        ToolHelper.getClassAsBytes(Iface2Impl.class),
+        ToolHelper.getClassAsBytes(CycleReferenceAB.class),
+        ToolHelper.getClassAsBytes(CycleReferenceBA.class),
+        ToolHelper.getClassAsBytes(ClassWithFinal.class)
+    };
+    String main = TrivialTestClass.class.getCanonicalName();
+    ProcessResult javaOutput = runOnJava(main, classes);
+    assertEquals(0, javaOutput.exitCode);
+
+    AndroidApp app = runR8(buildAndroidApp(classes), TrivialTestClass.class);
+
+    DexInspector inspector = new DexInspector(app);
+    ClassSubject clazz = inspector.clazz(TrivialTestClass.class);
+
+    assertEquals(
+        Collections.singleton("java.lang.StringBuilder"),
+        collectNewInstanceTypes(clazz, "testInner"));
+
+    assertEquals(
+        Collections.emptySet(),
+        collectNewInstanceTypes(clazz, "testConstructorMapping1"));
+
+    assertEquals(
+        Collections.singleton(
+            "com.android.tools.r8.ir.optimize.classinliner.trivial.ReferencedFields"),
+        collectNewInstanceTypes(clazz, "testConstructorMapping2"));
+
+    assertEquals(
+        Collections.singleton("java.lang.StringBuilder"),
+        collectNewInstanceTypes(clazz, "testConstructorMapping3"));
+
+    assertEquals(
+        Collections.emptySet(),
+        collectNewInstanceTypes(clazz, "testEmptyClass"));
+
+    assertEquals(
+        Collections.singleton(
+            "com.android.tools.r8.ir.optimize.classinliner.trivial.EmptyClassWithInitializer"),
+        collectNewInstanceTypes(clazz, "testEmptyClassWithInitializer"));
+
+    assertEquals(
+        Collections.singleton(
+            "com.android.tools.r8.ir.optimize.classinliner.trivial.ClassWithFinal"),
+        collectNewInstanceTypes(clazz, "testClassWithFinalizer"));
+
+    assertEquals(
+        Collections.emptySet(),
+        collectNewInstanceTypes(clazz, "testCallOnIface1"));
+
+    assertEquals(
+        Collections.singleton(
+            "com.android.tools.r8.ir.optimize.classinliner.trivial.Iface2Impl"),
+        collectNewInstanceTypes(clazz, "testCallOnIface2"));
+
+    assertEquals(
+        Sets.newHashSet(
+            "com.android.tools.r8.ir.optimize.classinliner.trivial.CycleReferenceAB",
+            "java.lang.StringBuilder"),
+        collectNewInstanceTypes(clazz, "testCycles"));
+
+    assertEquals(
+        Sets.newHashSet("java.lang.StringBuilder",
+            "com.android.tools.r8.ir.optimize.classinliner.trivial.CycleReferenceAB"),
+        collectNewInstanceTypes(inspector.clazz(CycleReferenceAB.class), "foo", "int"));
+
+    assertFalse(inspector.clazz(CycleReferenceBA.class).isPresent());
+  }
+
+  private Set<String> collectNewInstanceTypes(
+      ClassSubject clazz, String methodName, String... params) {
+    assertNotNull(clazz);
+    MethodSignature signature = new MethodSignature(methodName, "void", params);
+    DexCode code = clazz.method(signature).getMethod().getCode().asDexCode();
+    return filterInstructionKind(code, NewInstance.class)
+        .map(insn -> ((NewInstance) insn).getType().toSourceString())
+        .collect(Collectors.toSet());
+  }
+
+  private AndroidApp runR8(AndroidApp app, Class mainClass) throws Exception {
+    String config = keepMainProguardConfiguration(mainClass) + "\n"
+        + "-dontobfuscate\n"
+        + "-allowaccessmodification";
+
+    AndroidApp compiled = compileWithR8(app, config, o -> o.enableClassInlining = true);
+
+    // Materialize file for execution.
+    Path generatedDexFile = temp.getRoot().toPath().resolve("classes.jar");
+    compiled.writeToZip(generatedDexFile, OutputMode.DexIndexed);
+
+    // Run with ART.
+    String artOutput = ToolHelper.runArtNoVerificationErrors(
+        generatedDexFile.toString(), mainClass.getCanonicalName());
+
+    // Compare with Java.
+    ToolHelper.ProcessResult javaResult = ToolHelper.runJava(
+        ToolHelper.getClassPathForTests(), mainClass.getCanonicalName());
+
+    if (javaResult.exitCode != 0) {
+      System.out.println(javaResult.stdout);
+      System.err.println(javaResult.stderr);
+      fail("JVM failed for: " + mainClass);
+    }
+    assertEquals("JVM and ART output differ", javaResult.stdout, artOutput);
+
+    return compiled;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/ClassWithFinal.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/ClassWithFinal.java
new file mode 100644
index 0000000..7e0239c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/ClassWithFinal.java
@@ -0,0 +1,16 @@
+// 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.trivial;
+
+public class ClassWithFinal {
+  public String doNothing() {
+    return "nothing at all";
+  }
+
+  @Override
+  protected void finalize() throws Throwable {
+    doNothing();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/CycleReferenceAB.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/CycleReferenceAB.java
new file mode 100644
index 0000000..d26641a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/CycleReferenceAB.java
@@ -0,0 +1,26 @@
+// 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.trivial;
+
+public class CycleReferenceAB {
+  private String a;
+
+  public CycleReferenceAB(String a) {
+    this.a = a;
+  }
+
+  public void foo(int depth) {
+    CycleReferenceBA ba = new CycleReferenceBA("depth=" + depth);
+    System.out.println("CycleReferenceAB::foo(" + depth + ")");
+    if (depth > 0) {
+      ba.foo(depth - 1);
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "CycleReferenceAB(" + a + ")";
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/CycleReferenceBA.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/CycleReferenceBA.java
new file mode 100644
index 0000000..1c5d147
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/CycleReferenceBA.java
@@ -0,0 +1,26 @@
+// 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.trivial;
+
+public class CycleReferenceBA {
+  private String a;
+
+  public CycleReferenceBA(String a) {
+    this.a = a;
+  }
+
+  public void foo(int depth) {
+    CycleReferenceAB ab = new CycleReferenceAB("depth=" + depth);
+    System.out.println("CycleReferenceBA::foo(" + depth + ")");
+    if (depth > 0) {
+      ab.foo(depth - 1);
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "CycleReferenceBA(" + a + ")";
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/EmptyClass.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/EmptyClass.java
new file mode 100644
index 0000000..cedfd77
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/EmptyClass.java
@@ -0,0 +1,11 @@
+// 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.trivial;
+
+public class EmptyClass {
+  public String doNothing() {
+    return "nothing at all";
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/EmptyClassWithInitializer.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/EmptyClassWithInitializer.java
new file mode 100644
index 0000000..9671ea5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/EmptyClassWithInitializer.java
@@ -0,0 +1,15 @@
+// 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.trivial;
+
+public class EmptyClassWithInitializer {
+  public String doNothing() {
+    return "nothing at all";
+  }
+
+  static {
+    System.out.println("EmptyClassWithInitializer.<clinit>()");
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/Iface1.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/Iface1.java
new file mode 100644
index 0000000..8320ec9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/Iface1.java
@@ -0,0 +1,9 @@
+// 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.trivial;
+
+public interface Iface1 {
+  void foo();
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/Iface1Impl.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/Iface1Impl.java
new file mode 100644
index 0000000..e42a04b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/Iface1Impl.java
@@ -0,0 +1,18 @@
+// 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.trivial;
+
+public class Iface1Impl implements Iface1 {
+  final String value;
+
+  public Iface1Impl(String value) {
+    this.value = value;
+  }
+
+  @Override
+  public void foo() {
+    System.out.println(value);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/Iface2.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/Iface2.java
new file mode 100644
index 0000000..93a3d5d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/Iface2.java
@@ -0,0 +1,9 @@
+// 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.trivial;
+
+public interface Iface2 extends Iface1 {
+  Integer CONSTANT = 123;
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/Iface2Impl.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/Iface2Impl.java
new file mode 100644
index 0000000..17dd4c1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/Iface2Impl.java
@@ -0,0 +1,18 @@
+// 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.trivial;
+
+public class Iface2Impl implements Iface2 {
+  final String value;
+
+  public Iface2Impl(String value) {
+    this.value = value;
+  }
+
+  @Override
+  public void foo() {
+    System.out.println(value);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/ReferencedFields.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/ReferencedFields.java
new file mode 100644
index 0000000..90263d6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/ReferencedFields.java
@@ -0,0 +1,31 @@
+// 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.trivial;
+
+public class ReferencedFields {
+  private String a;
+  private String b;
+
+  public ReferencedFields(String a, String b) {
+    this.a = a;
+    this.b = b;
+  }
+
+  public ReferencedFields(String a) {
+    this.a = a;
+  }
+
+  public String getConcat() {
+    return a + " " + b;
+  }
+
+  public String getA() {
+    return a;
+  }
+
+  public String getB() {
+    return b;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/TrivialTestClass.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/TrivialTestClass.java
new file mode 100644
index 0000000..7e3cba2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/TrivialTestClass.java
@@ -0,0 +1,122 @@
+// 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.trivial;
+
+public class TrivialTestClass {
+  private static int ID = 0;
+
+  private static String next() {
+    return Integer.toString(ID++);
+  }
+
+  public static void main(String[] args) {
+    TrivialTestClass test = new TrivialTestClass();
+    test.testInner();
+    test.testConstructorMapping1();
+    test.testConstructorMapping2();
+    test.testConstructorMapping3();
+    test.testEmptyClass();
+    test.testEmptyClassWithInitializer();
+    test.testClassWithFinalizer();
+    test.testCallOnIface1();
+    test.testCallOnIface2();
+    test.testCycles();
+  }
+
+  private synchronized void testInner() {
+    Inner inner = new Inner("inner{", 123, next() + "}");
+    System.out.println(inner.toString() + " " + inner.getPrefix() + " = " + inner.prefix);
+  }
+
+  private synchronized void testConstructorMapping1() {
+    ReferencedFields o = new ReferencedFields(next());
+    System.out.println(o.getA());
+  }
+
+  private synchronized void testConstructorMapping2() {
+    ReferencedFields o = new ReferencedFields(next());
+    System.out.println(o.getB());
+  }
+
+  private synchronized void testConstructorMapping3() {
+    ReferencedFields o = new ReferencedFields(next(), next());
+    System.out.println(o.getA() + o.getB() + o.getConcat());
+  }
+
+  private synchronized void testEmptyClass() {
+    new EmptyClass();
+  }
+
+  private synchronized void testEmptyClassWithInitializer() {
+    new EmptyClassWithInitializer();
+  }
+
+  private synchronized void testClassWithFinalizer() {
+    new ClassWithFinal();
+  }
+
+  private void callOnIface1(Iface1 iface) {
+    iface.foo();
+  }
+
+  private synchronized void testCallOnIface1() {
+    callOnIface1(new Iface1Impl(next()));
+  }
+
+  private void callOnIface2(Iface2 iface) {
+    iface.foo();
+  }
+
+  private synchronized void testCallOnIface2() {
+    callOnIface2(new Iface2Impl(next()));
+    System.out.println(Iface2Impl.CONSTANT); // Keep constant referenced
+  }
+
+  private synchronized void testCycles() {
+    new CycleReferenceAB("first").foo(3);
+    new CycleReferenceBA("second").foo(4);
+  }
+
+  public class Inner {
+    private String prefix;
+    private String suffix;
+    private double id;
+
+    public Inner(String prefix, double id, String suffix) {
+      this.prefix = prefix;
+      this.suffix = suffix;
+      this.id = id;
+    }
+
+    @Override
+    public String toString() {
+      return prefix + id + suffix;
+    }
+
+    public String getPrefix() {
+      return prefix;
+    }
+
+    public String getSuffix() {
+      return suffix;
+    }
+
+    public double getId() {
+      return id;
+    }
+
+    public void setPrefix(String prefix) {
+      this.prefix = prefix;
+    }
+
+    public void setSuffix(String suffix) {
+      this.suffix = suffix;
+    }
+
+    public void setId(double id) {
+      this.id = id;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java b/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
index 93f4249..bb83093 100644
--- a/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
+++ b/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
@@ -26,6 +26,7 @@
 import com.android.tools.r8.utils.DexInspector.FieldSubject;
 import com.android.tools.r8.utils.DexInspector.MethodSubject;
 import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.collect.ImmutableList;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -34,6 +35,7 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Consumer;
 import org.junit.Assume;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -183,13 +185,23 @@
         + "}";
   }
 
-  protected void runTest(String folder, String mainClass, AndroidAppInspector inspector)
-      throws Exception {
-    runTest(folder, mainClass, null, inspector);
+  protected void runTest(String folder, String mainClass,
+      AndroidAppInspector inspector) throws Exception {
+    runTest(folder, mainClass, null, null, inspector);
+  }
+
+  protected void runTest(String folder, String mainClass,
+      Consumer<InternalOptions> optionsConsumer, AndroidAppInspector inspector) throws Exception {
+    runTest(folder, mainClass, null, optionsConsumer, inspector);
+  }
+
+  protected void runTest(String folder, String mainClass,
+      String extraProguardRules, AndroidAppInspector inspector) throws Exception {
+    runTest(folder, mainClass, extraProguardRules, null, inspector);
   }
 
   protected void runTest(String folder, String mainClass, String extraProguardRules,
-      AndroidAppInspector inspector) throws Exception {
+      Consumer<InternalOptions> optionsConsumer, AndroidAppInspector inspector) throws Exception {
     Assume.assumeTrue(ToolHelper.artSupported());
 
     String proguardRules = buildProguardRules(mainClass);
@@ -206,7 +218,7 @@
     // Build with R8
     AndroidApp.Builder builder = AndroidApp.builder();
     builder.addProgramFiles(classpath);
-    AndroidApp app = compileWithR8(builder.build(), proguardRules);
+    AndroidApp app = compileWithR8(builder.build(), proguardRules, optionsConsumer);
 
     // Materialize file for execution.
     Path generatedDexFile = temp.getRoot().toPath().resolve("classes.jar");
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinLambdaMergingTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinLambdaMergingTest.java
index 315e980..ee9065f 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinLambdaMergingTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinLambdaMergingTest.java
@@ -252,7 +252,7 @@
   @Test
   public void testTrivialKs() throws Exception {
     final String mainClassName = "lambdas_kstyle_trivial.MainKt";
-    runTest("lambdas_kstyle_trivial", mainClassName, null, (app) -> {
+    runTest("lambdas_kstyle_trivial", mainClassName, (app) -> {
       Verifier verifier = new Verifier(app);
       String pkg = "lambdas_kstyle_trivial";
 
@@ -293,7 +293,7 @@
   @Test
   public void testCapturesKs() throws Exception {
     final String mainClassName = "lambdas_kstyle_captures.MainKt";
-    runTest("lambdas_kstyle_captures", mainClassName, null, (app) -> {
+    runTest("lambdas_kstyle_captures", mainClassName, (app) -> {
       Verifier verifier = new Verifier(app);
       String pkg = "lambdas_kstyle_captures";
       String grpPkg = allowAccessModification ? "" : pkg;
@@ -318,7 +318,7 @@
   @Test
   public void testGenericsNoSignatureKs() throws Exception {
     final String mainClassName = "lambdas_kstyle_generics.MainKt";
-    runTest("lambdas_kstyle_generics", mainClassName, null, (app) -> {
+    runTest("lambdas_kstyle_generics", mainClassName, (app) -> {
       Verifier verifier = new Verifier(app);
       String pkg = "lambdas_kstyle_generics";
       String grpPkg = allowAccessModification ? "" : pkg;
@@ -387,7 +387,7 @@
   @Test
   public void testTrivialJs() throws Exception {
     final String mainClassName = "lambdas_jstyle_trivial.MainKt";
-    runTest("lambdas_jstyle_trivial", mainClassName, null, (app) -> {
+    runTest("lambdas_jstyle_trivial", mainClassName, (app) -> {
       Verifier verifier = new Verifier(app);
       String pkg = "lambdas_jstyle_trivial";
       String grp = allowAccessModification ? "" : pkg;
@@ -435,7 +435,7 @@
   @Test
   public void testSingleton() throws Exception {
     final String mainClassName = "lambdas_singleton.MainKt";
-    runTest("lambdas_singleton", mainClassName, null, (app) -> {
+    runTest("lambdas_singleton", mainClassName, (app) -> {
       Verifier verifier = new Verifier(app);
       String pkg = "lambdas_singleton";
       String grp = allowAccessModification ? "" : pkg;
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
index 73d70e1..f884376 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
@@ -12,6 +12,8 @@
 import com.android.tools.r8.utils.DexInspector;
 import com.android.tools.r8.utils.DexInspector.ClassSubject;
 import com.android.tools.r8.utils.DexInspector.FieldSubject;
+import com.android.tools.r8.utils.InternalOptions;
+import java.util.function.Consumer;
 import org.junit.Test;
 
 public class R8KotlinPropertiesTest extends AbstractR8KotlinTestBase {
@@ -79,11 +81,13 @@
           .addProperty("internalLateInitProp", JAVA_LANG_STRING, Visibility.INTERNAL)
           .addProperty("publicLateInitProp", JAVA_LANG_STRING, Visibility.PUBLIC);
 
+  private Consumer<InternalOptions> disableClassInliner = o -> o.enableClassInlining = false;
+
   @Test
   public void testMutableProperty_getterAndSetterAreRemoveIfNotUsed() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_noUseOfProperties");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassExists(dexInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -100,7 +104,7 @@
   public void testMutableProperty_privateIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_usePrivateProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassExists(dexInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -122,7 +126,7 @@
   public void testMutableProperty_protectedIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_useProtectedProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassExists(dexInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -145,7 +149,7 @@
   public void testMutableProperty_internalIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_useInternalProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassExists(dexInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -168,7 +172,7 @@
   public void testMutableProperty_publicIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_usePublicProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassExists(dexInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -191,7 +195,7 @@
   public void testMutableProperty_primitivePropertyIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_usePrimitiveProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassExists(dexInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -216,7 +220,7 @@
   public void testLateInitProperty_getterAndSetterAreRemoveIfNotUsed() throws Exception {
     String mainClass = addMainToClasspath("properties/LateInitPropertyKt",
         "lateInitProperty_noUseOfProperties");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassExists(dexInspector,
           LATE_INIT_PROPERTY_CLASS.getClassName());
@@ -233,7 +237,7 @@
   public void testLateInitProperty_privateIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties/LateInitPropertyKt", "lateInitProperty_usePrivateLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassExists(dexInspector,
           LATE_INIT_PROPERTY_CLASS.getClassName());
@@ -256,7 +260,7 @@
   public void testLateInitProperty_protectedIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/LateInitPropertyKt",
         "lateInitProperty_useProtectedLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassExists(dexInspector,
           LATE_INIT_PROPERTY_CLASS.getClassName());
@@ -277,7 +281,7 @@
   public void testLateInitProperty_internalIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties/LateInitPropertyKt", "lateInitProperty_useInternalLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassExists(dexInspector,
           LATE_INIT_PROPERTY_CLASS.getClassName());
@@ -296,7 +300,7 @@
   public void testLateInitProperty_publicIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties/LateInitPropertyKt", "lateInitProperty_usePublicLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassExists(dexInspector,
           LATE_INIT_PROPERTY_CLASS.getClassName());
@@ -315,7 +319,7 @@
   public void testUserDefinedProperty_getterAndSetterAreRemoveIfNotUsed() throws Exception {
     String mainClass = addMainToClasspath(
         "properties/UserDefinedPropertyKt", "userDefinedProperty_noUseOfProperties");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassExists(dexInspector,
           USER_DEFINED_PROPERTY_CLASS.getClassName());
@@ -332,7 +336,7 @@
   public void testUserDefinedProperty_publicIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties/UserDefinedPropertyKt", "userDefinedProperty_useProperties");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassExists(dexInspector,
           USER_DEFINED_PROPERTY_CLASS.getClassName());
@@ -358,7 +362,7 @@
   public void testCompanionProperty_primitivePropertyCannotBeInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties.CompanionPropertiesKt", "companionProperties_usePrimitiveProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject outerClass = checkClassExists(dexInspector,
           "properties.CompanionProperties");
@@ -389,7 +393,7 @@
   public void testCompanionProperty_privatePropertyIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties.CompanionPropertiesKt", "companionProperties_usePrivateProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject outerClass = checkClassExists(dexInspector,
           "properties.CompanionProperties");
@@ -423,7 +427,7 @@
   public void testCompanionProperty_internalPropertyCannotBeInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties.CompanionPropertiesKt", "companionProperties_useInternalProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject outerClass = checkClassExists(dexInspector,
           "properties.CompanionProperties");
@@ -454,7 +458,7 @@
   public void testCompanionProperty_publicPropertyCannotBeInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties.CompanionPropertiesKt", "companionProperties_usePublicProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject outerClass = checkClassExists(dexInspector,
           "properties.CompanionProperties");
@@ -486,7 +490,7 @@
     final TestKotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
         "companionLateInitProperties_usePrivateLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject outerClass = checkClassExists(dexInspector, testedClass.getOuterClassName());
       ClassSubject companionClass = checkClassExists(dexInspector, testedClass.getClassName());
@@ -517,7 +521,7 @@
     final TestKotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
         "companionLateInitProperties_useInternalLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject outerClass = checkClassExists(dexInspector, testedClass.getOuterClassName());
       ClassSubject companionClass = checkClassExists(dexInspector, testedClass.getClassName());
@@ -541,7 +545,7 @@
     final TestKotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
         "companionLateInitProperties_usePublicLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject outerClass = checkClassExists(dexInspector, testedClass.getOuterClassName());
       ClassSubject companionClass = checkClassExists(dexInspector, testedClass.getClassName());
diff --git a/src/test/java/com/android/tools/r8/regress/b69825683/Regress69825683Test.java b/src/test/java/com/android/tools/r8/regress/b69825683/Regress69825683Test.java
index 9026740..a185f4c 100644
--- a/src/test/java/com/android/tools/r8/regress/b69825683/Regress69825683Test.java
+++ b/src/test/java/com/android/tools/r8/regress/b69825683/Regress69825683Test.java
@@ -33,7 +33,7 @@
         "-dontobfuscate"),
         Origin.unknown());
     builder.setProgramConsumer(DexIndexedConsumer.emptyConsumer());
-    AndroidApp app = ToolHelper.runR8(builder.build());
+    AndroidApp app = ToolHelper.runR8(builder.build(), o -> o.enableClassInlining = false);
     DexInspector inspector = new DexInspector(app);
     List<FoundClassSubject> classes = inspector.allClasses();
 
@@ -66,7 +66,7 @@
         "-dontobfuscate"),
         Origin.unknown());
     builder.setProgramConsumer(DexIndexedConsumer.emptyConsumer());
-    AndroidApp app = ToolHelper.runR8(builder.build());
+    AndroidApp app = ToolHelper.runR8(builder.build(), o -> o.enableClassInlining = false);
     DexInspector inspector = new DexInspector(app);
     List<FoundClassSubject> classes = inspector.allClasses();
 
diff --git a/src/test/java/com/android/tools/r8/shaking/defaultmethods/DefaultMethodsTest.java b/src/test/java/com/android/tools/r8/shaking/defaultmethods/DefaultMethodsTest.java
index 3de225d..5845dd3 100644
--- a/src/test/java/com/android/tools/r8/shaking/defaultmethods/DefaultMethodsTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/defaultmethods/DefaultMethodsTest.java
@@ -39,7 +39,7 @@
         "-dontobfuscate"),
         Origin.unknown());
     builder.addProguardConfiguration(additionalKeepRules, Origin.unknown());
-    AndroidApp app = ToolHelper.runR8(builder.build());
+    AndroidApp app = ToolHelper.runR8(builder.build(), o -> o.enableClassInlining = false);
     inspection.accept(new DexInspector(app));
   }
 
diff --git a/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ForceProguardCompatibilityTest.java b/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ForceProguardCompatibilityTest.java
index 1151648..91fc9d4 100644
--- a/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ForceProguardCompatibilityTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ForceProguardCompatibilityTest.java
@@ -599,7 +599,7 @@
     AndroidApp app;
     builder.setProgramConsumer(DexIndexedConsumer.emptyConsumer());
     try {
-      app = ToolHelper.runR8(builder.build());
+      app = ToolHelper.runR8(builder.build(), o -> o.enableClassInlining = false);
     } catch (CompilationError e) {
       assertTrue(!forceProguardCompatibility && (!innerClasses || !enclosingMethod));
       return;
@@ -656,7 +656,7 @@
     builder.setMinApiLevel(AndroidApiLevel.O.getLevel());
     Path proguardCompatibilityRules = temp.newFile().toPath();
     builder.setProguardCompatibilityRulesOutput(proguardCompatibilityRules);
-    AndroidApp app = ToolHelper.runR8(builder.build());
+    AndroidApp app = ToolHelper.runR8(builder.build(), o -> o.enableClassInlining = false);
     inspection.accept(new DexInspector(app));
     // Check the Proguard compatibility configuration generated.
     ProguardConfigurationParser parser =