Merge "Update tests that assume no vertical class merging"
diff --git a/build.gradle b/build.gradle
index 7f41caa..7761e37 100644
--- a/build.gradle
+++ b/build.gradle
@@ -501,6 +501,9 @@
     task.relocate('org.apache.commons', 'com.android.tools.r8.org.apache.commons')
     task.relocate('org.objectweb.asm', 'com.android.tools.r8.org.objectweb.asm')
     task.relocate('it.unimi.dsi.fastutil', 'com.android.tools.r8.it.unimi.dsi.fastutil')
+    task.relocate('kotlin', 'com.android.tools.r8.jetbrains.kotlin')
+    task.relocate('kotlinx', 'com.android.tools.r8.jetbrains.kotlinx')
+    task.relocate('org.jetbrains', 'com.android.tools.r8.org.jetbrains')
 }
 
 task repackageDeps(type: ShadowJar) {
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 6cbad63..69a744f 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfo.java
@@ -481,15 +481,15 @@
     return result;
   }
 
-  public boolean canTriggerStaticInitializer(DexType type) {
+  public boolean canTriggerStaticInitializer(DexType type, boolean ignoreTypeItself) {
     Set<DexType> knownInterfaces = Sets.newIdentityHashSet();
 
     // Process superclass chain.
     DexType clazz = type;
     while (clazz != null && clazz != dexItemFactory.objectType) {
       DexClass definition = definitionFor(clazz);
-      if (canTriggerStaticInitializer(definition)) {
-        return true; // Assume it *may* trigger if we didn't find the definition.
+      if (canTriggerStaticInitializer(definition) && (!ignoreTypeItself || clazz != type)) {
+        return true;
       }
       knownInterfaces.addAll(Arrays.asList(definition.interfaces.values));
       clazz = definition.superType;
@@ -501,7 +501,7 @@
       DexType iface = queue.remove();
       DexClass definition = definitionFor(iface);
       if (canTriggerStaticInitializer(definition)) {
-        return true; // Assume it *may* trigger if we didn't find the definition.
+        return true;
       }
       if (!definition.isInterface()) {
         throw new Unreachable(iface.toSourceString() + " is expected to be an interface");
@@ -517,6 +517,7 @@
   }
 
   private static boolean canTriggerStaticInitializer(DexClass clazz) {
+    // Assume it *may* trigger if we didn't find the definition.
     return clazz == null || clazz.hasClassInitializer();
   }
 
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 d234702..4281e80 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -590,6 +590,24 @@
     }
   }
 
+  public static class TrivialInitializer {
+    private TrivialInitializer() {
+    }
+
+    public static final class InstanceClassTrivialInitializer extends TrivialInitializer {
+      public static final InstanceClassTrivialInitializer INSTANCE =
+          new InstanceClassTrivialInitializer();
+    }
+
+    public static final class ClassTrivialInitializer extends TrivialInitializer {
+      public final DexField field;
+
+      public ClassTrivialInitializer(DexField field) {
+        this.field = field;
+      }
+    }
+  }
+
   public static class OptimizationInfo {
 
     private int returnedArgument = -1;
@@ -604,6 +622,7 @@
     // Stores information about instance methods and constructors for
     // class inliner, null value indicates that the method is not eligible.
     private ClassInlinerEligibility classInlinerEligibility = null;
+    private TrivialInitializer trivialInitializerInfo = null;
 
     private OptimizationInfo() {
       // Intentionally left empty.
@@ -648,6 +667,14 @@
       return this.classInlinerEligibility;
     }
 
+    private void setTrivialInitializer(TrivialInitializer info) {
+      this.trivialInitializerInfo = info;
+    }
+
+    public TrivialInitializer getTrivialInitializerInfo() {
+      return this.trivialInitializerInfo;
+    }
+
     public long getReturnedConstant() {
       assert returnsConstant();
       return returnedConstant;
@@ -750,6 +777,10 @@
     ensureMutableOI().setClassInlinerEligibility(eligibility);
   }
 
+  synchronized public void setTrivialInitializer(TrivialInitializer info) {
+    ensureMutableOI().setTrivialInitializer(info);
+  }
+
   synchronized public void markForceInline() {
     ensureMutableOI().markForceInline();
   }
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 7d7e5f3..735fffa 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
@@ -725,17 +725,20 @@
       interfaceMethodRewriter.rewriteMethodReferences(method, code);
       assert code.isConsistentSSA();
     }
-    if (lambdaMerger != null) {
-      lambdaMerger.processMethodCode(method, code);
+
+    if (classInliner != null) {
+      // Class inliner should work before lambda merger, so if it inlines the
+      // lambda, it is not get collected by merger.
+      assert options.enableInlining && inliner != null;
+      classInliner.processMethodCode(
+          appInfo.withLiveness(), method, code, isProcessedConcurrently,
+          methodsToInline -> inliner.performForcedInlining(method, code, methodsToInline)
+      );
       assert code.isConsistentSSA();
     }
 
-    if (classInliner != null) {
-      assert options.enableInlining && inliner != null;
-      classInliner.processMethodCode(
-          appInfo.withSubtyping(), method, code, isProcessedConcurrently,
-          methodsToInline -> inliner.performForcedInlining(method, code, methodsToInline)
-      );
+    if (lambdaMerger != null) {
+      lambdaMerger.processMethodCode(method, code);
       assert code.isConsistentSSA();
     }
 
@@ -760,6 +763,9 @@
 
     // Analysis must be done after method is rewritten by logArgumentTypes()
     codeRewriter.identifyClassInlinerEligibility(method, code, feedback);
+    if (method.isInstanceInitializer() || method.isClassInitializer()) {
+      codeRewriter.identifyTrivialInitializer(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 16a77c8..2f17a4c 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexEncodedMethod.ClassInlinerEligibility;
+import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 
 public interface OptimizationFeedback {
@@ -17,4 +18,5 @@
   void markCheckNullReceiverBeforeAnySideEffect(DexEncodedMethod method, boolean mark);
   void markTriggerClassInitBeforeAnySideEffect(DexEncodedMethod method, boolean mark);
   void setClassInlinerEligibility(DexEncodedMethod method, ClassInlinerEligibility eligibility);
+  void setTrivialInitializer(DexEncodedMethod method, TrivialInitializer info);
 }
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 7303b6b..1026e9b 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexEncodedMethod.ClassInlinerEligibility;
+import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 
 public class OptimizationFeedbackDirect implements OptimizationFeedback {
@@ -50,4 +51,9 @@
       DexEncodedMethod method, ClassInlinerEligibility eligibility) {
     method.setClassInlinerEligibility(eligibility);
   }
+
+  @Override
+  public void setTrivialInitializer(DexEncodedMethod method, TrivialInitializer info) {
+    method.setTrivialInitializer(info);
+  }
 }
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 6915a2f..8fac02f 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexEncodedMethod.ClassInlinerEligibility;
+import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 
 public class OptimizationFeedbackIgnore implements OptimizationFeedback {
@@ -35,4 +36,8 @@
   public void setClassInlinerEligibility(
       DexEncodedMethod method, ClassInlinerEligibility eligibility) {
   }
+
+  @Override
+  public void setTrivialInitializer(DexEncodedMethod method, TrivialInitializer info) {
+  }
 }
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 e610d20..c3f02da 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
@@ -13,6 +13,9 @@
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexEncodedMethod.ClassInlinerEligibility;
+import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer;
+import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer.ClassTrivialInitializer;
+import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer.InstanceClassTrivialInitializer;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
@@ -48,6 +51,7 @@
 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.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;
@@ -812,6 +816,187 @@
         method, new ClassInlinerEligibility(receiverUsedAsReturnValue));
   }
 
+  public void identifyTrivialInitializer(
+      DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
+    boolean isInstanceInitializer = method.isInstanceInitializer();
+    boolean isClassInitializer = method.isClassInitializer();
+    assert isInstanceInitializer || isClassInitializer;
+    if (method.accessFlags.isNative()) {
+      return;
+    }
+
+    DexClass clazz = appInfo.definitionFor(method.method.holder);
+    if (clazz == null) {
+      return;
+    }
+
+    feedback.setTrivialInitializer(method,
+        isInstanceInitializer
+            ? computeInstanceInitializerInfo(code, clazz, appInfo::definitionFor)
+            : computeClassInitializerInfo(code, clazz));
+  }
+
+  // This method defines trivial instance initializer as follows:
+  //
+  // ** The initializer may only call the initializer of the base class, which
+  //    itself must be trivial.
+  //
+  // ** java.lang.Object.<init>() is considered trivial.
+  //
+  // ** all arguments passed to a super-class initializer must be non-throwing
+  //    constants or arguments.
+  //
+  // ** Assigns arguments or non-throwing constants to fields of this class.
+  //
+  // (Note that this initializer does not have to have zero arguments.)
+  private TrivialInitializer computeInstanceInitializerInfo(
+      IRCode code, DexClass clazz, Function<DexType, DexClass> typeToClass) {
+    Value receiver = code.getThis();
+
+    InstructionIterator it = code.instructionIterator();
+    while (it.hasNext()) {
+      Instruction insn = it.next();
+
+      if (insn.isReturn()) {
+        continue;
+      }
+
+      if (insn.isArgument()) {
+        continue;
+      }
+
+      if (insn.isConstInstruction()) {
+        if (insn.instructionInstanceCanThrow()) {
+          return null;
+        } else {
+          continue;
+        }
+      }
+
+      if (insn.isInvokeDirect()) {
+        InvokeDirect invokedDirect = insn.asInvokeDirect();
+        DexMethod invokedMethod = invokedDirect.getInvokedMethod();
+
+        if (invokedMethod.holder != clazz.superType) {
+          return null;
+        }
+
+        // java.lang.Object.<init>() is considered trivial.
+        if (invokedMethod == dexItemFactory.objectMethods.constructor) {
+          continue;
+        }
+
+        DexClass holder = typeToClass.apply(invokedMethod.holder);
+        if (holder == null) {
+          return null;
+        }
+
+        DexEncodedMethod callTarget = holder.lookupDirectMethod(invokedMethod);
+        if (callTarget == null ||
+            !callTarget.isInstanceInitializer() ||
+            callTarget.getOptimizationInfo().getTrivialInitializerInfo() == null ||
+            invokedDirect.getReceiver() != receiver) {
+          return null;
+        }
+
+        for (Value value : invokedDirect.inValues()) {
+          if (value != receiver && !(value.isConstant() || value.isArgument())) {
+            return null;
+          }
+        }
+        continue;
+      }
+
+      if (insn.isInstancePut()) {
+        InstancePut instancePut = insn.asInstancePut();
+        DexEncodedField field = clazz.lookupInstanceField(instancePut.getField());
+        if (field == null ||
+            instancePut.object() != receiver ||
+            (instancePut.value() != receiver && !instancePut.value().isArgument())) {
+          return null;
+        }
+        continue;
+      }
+
+      // Other instructions make the instance initializer not eligible.
+      return null;
+    }
+
+    return InstanceClassTrivialInitializer.INSTANCE;
+  }
+
+  // This method defines trivial class initializer as follows:
+  //
+  // ** The initializer may only instantiate an instance of the same class,
+  //    initialize it with a call to a trivial constructor *without* arguments,
+  //    and assign this instance to a static final field of the same class.
+  //
+  private synchronized TrivialInitializer computeClassInitializerInfo(IRCode code, DexClass clazz) {
+    InstructionIterator it = code.instructionIterator();
+
+    Value createdSingletonInstance = null;
+    DexField singletonField = null;
+
+    while (it.hasNext()) {
+      Instruction insn = it.next();
+
+      if (insn.isReturn()) {
+        continue;
+      }
+
+      if (insn.isNewInstance()) {
+        NewInstance newInstance = insn.asNewInstance();
+        if (createdSingletonInstance != null ||
+            newInstance.clazz != clazz.type ||
+            insn.outValue() == null) {
+          return null;
+        }
+        createdSingletonInstance = insn.outValue();
+        continue;
+      }
+
+      if (insn.isInvokeDirect()) {
+        InvokeDirect invokedDirect = insn.asInvokeDirect();
+        if (createdSingletonInstance == null ||
+            invokedDirect.getReceiver() != createdSingletonInstance) {
+          return null;
+        }
+
+        DexEncodedMethod callTarget = clazz.lookupDirectMethod(invokedDirect.getInvokedMethod());
+        if (callTarget == null ||
+            !callTarget.isInstanceInitializer() ||
+            !callTarget.method.proto.parameters.isEmpty() ||
+            callTarget.getOptimizationInfo().getTrivialInitializerInfo() == null) {
+          return null;
+        }
+        continue;
+      }
+
+      if (insn.isStaticPut()) {
+        StaticPut staticPut = insn.asStaticPut();
+        if (singletonField != null ||
+            createdSingletonInstance == null ||
+            staticPut.inValue() != createdSingletonInstance) {
+          return null;
+        }
+
+        DexEncodedField field = clazz.lookupStaticField(staticPut.getField());
+        if (field == null ||
+            !field.accessFlags.isStatic() ||
+            !field.accessFlags.isFinal()) {
+          return null;
+        }
+        singletonField = field.field;
+        continue;
+      }
+
+      // Other instructions make the class initializer not eligible.
+      return null;
+    }
+
+    return singletonField == null ? null : new ClassTrivialInitializer(singletonField);
+  }
+
   /**
    * 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/classinliner/ClassInliner.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
index d7a57df..e6b61f8 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
@@ -10,6 +10,8 @@
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexEncodedMethod.ClassInlinerEligibility;
+import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer;
+import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer.ClassTrivialInitializer;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
@@ -22,12 +24,13 @@
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
-import com.android.tools.r8.ir.code.NewInstance;
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.ir.optimize.Inliner.InliningInfo;
 import com.android.tools.r8.ir.optimize.Inliner.Reason;
+import com.android.tools.r8.kotlin.KotlinInfo;
+import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.google.common.collect.Streams;
 import java.util.ArrayList;
 import java.util.IdentityHashMap;
@@ -55,11 +58,12 @@
 
   // 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 may add new 'new-instance' instructions.
-  // Processing them as well is possible, but does not seem to bring much value.
+  // - collect all 'new-instance' and 'static-get' instructions (called roots below) in
+  // the original code. Note that class inlining, if happens, mutates code and may add
+  // new root instructions. Processing them as well is possible, but does not seem to
+  // bring much value.
   //
-  // - for each 'new-instance' we check if it is eligible for inlining, i.e:
+  // - for each 'new-instance' root 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 comments in
   //        CodeRewriter::identifyClassInlinerEligibility(...))
@@ -71,14 +75,20 @@
   //            NOTE: if method receiver is used as a return value, the method call
   //            should ignore return value
   //
-  // - inline eligible 'new-instance' instructions, i.e:
+  // - for each 'static-get' root we check if it is eligible for inlining, i.e:
+  //     -> the class of the new instance is 'eligible' (see computeClassEligible(...))
+  //        *and* has a trivial class constructor (see CoreRewriter::computeClassInitializerInfo
+  //        and description in isClassAndUsageEligible(...)) initializing the field root reads
+  //     -> has only 'eligible' uses, (see above)
+  //
+  // - inline eligible root instructions, i.e:
   //     -> force inline methods called on the instance (including the initializer);
   //        (this may introduce additional instance field reads/writes on the receiver)
   //     -> replace instance field reads with appropriate values calculated based on
   //        fields writes
-  //     -> remove the call to superclass initializer
+  //     -> remove the call to superclass initializer (if root is 'new-instance')
   //     -> remove all field writes
-  //     -> remove 'new-instance' instructions
+  //     -> remove root instructions
   //
   // For example:
   //
@@ -93,12 +103,21 @@
   //         return x;
   //       }
   //     }
+  //     static class F {
+  //       final static F I = new F();
+  //       int getX() {
+  //         return 123;
+  //       }
+  //     }
   //     static int method1() {
   //       return new L(1).x;
   //     }
   //     static int method2() {
   //       return new L(1).getX();
   //     }
+  //     static int method3() {
+  //       return F.I.getX();
+  //     }
   //   }
   //
   // Code after class C is 'inlined':
@@ -109,47 +128,59 @@
   //     static int method2() {
   //       return 1;
   //     }
+  //     static int method3() {
+  //       return "F::getX";
+  //     }
   //   }
   //
   public final void processMethodCode(
-      AppInfoWithSubtyping appInfo,
+      AppInfoWithLiveness appInfo,
       DexEncodedMethod method,
       IRCode code,
       Predicate<DexEncodedMethod> isProcessedConcurrently,
       InlinerAction inliner) {
 
-    // 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 all the new-instance and static-get instructions in the code before inlining.
+    List<Instruction> roots = Streams.stream(code.instructionIterator())
+        .filter(insn -> insn.isNewInstance() || insn.isStaticGet())
         .collect(Collectors.toList());
 
-    for (NewInstance newInstance : newInstances) {
-      Value eligibleInstance = newInstance.outValue();
+    for (Instruction root : roots) {
+      Value eligibleInstance = root.outValue();
       if (eligibleInstance == null) {
         continue;
       }
 
-      DexType eligibleClass = newInstance.clazz;
-      if (!isClassEligible(appInfo, eligibleClass)) {
+      DexType eligibleClass = root.isNewInstance()
+          ? root.asNewInstance().clazz : root.asStaticGet().getField().type;
+      DexClass eligibleClassDefinition = appInfo.definitionFor(eligibleClass);
+      if (eligibleClassDefinition == null) {
         continue;
       }
 
-      Map<InvokeMethodWithReceiver, InliningInfo> methodCalls = checkInstanceUsersEligibility(
-          appInfo, method, isProcessedConcurrently, newInstance, eligibleInstance, eligibleClass);
+      // Check the static initializer of the type.
+      if (!isClassAndUsageEligible(root, eligibleClass,
+          eligibleClassDefinition, isProcessedConcurrently, appInfo)) {
+        continue;
+      }
+
+      Map<InvokeMethodWithReceiver, InliningInfo> methodCalls =
+          checkInstanceUsersEligibility(appInfo, method, isProcessedConcurrently,
+              root, eligibleInstance, eligibleClass, eligibleClassDefinition);
       if (methodCalls == null) {
         continue;
       }
 
-      if (getTotalEstimatedMethodSize(methodCalls) >= totalMethodInstructionLimit) {
+      if (!instructionLimitExempt(eligibleClassDefinition, appInfo) &&
+          getTotalEstimatedMethodSize(methodCalls) >= totalMethodInstructionLimit) {
         continue;
       }
 
       // Inline the class instance.
       forceInlineAllMethodInvocations(inliner, methodCalls);
-      removeSuperClassInitializerAndFieldReads(code, newInstance, eligibleInstance);
+      removeSuperClassInitializerAndFieldReads(code, root, eligibleInstance);
       removeFieldWrites(eligibleInstance, eligibleClass);
-      removeInstruction(newInstance);
+      removeInstruction(root);
 
       // Restore normality.
       code.removeAllTrivialPhis();
@@ -157,10 +188,96 @@
     }
   }
 
+  private boolean isClassAndUsageEligible(Instruction root,
+      DexType eligibleClass, DexClass eligibleClassDefinition,
+      Predicate<DexEncodedMethod> isProcessedConcurrently, AppInfoWithLiveness appInfo) {
+    if (!isClassEligible(appInfo, eligibleClass)) {
+      return false;
+    }
+
+    if (root.isNewInstance()) {
+      // There must be no static initializer on the class itself.
+      return !eligibleClassDefinition.hasClassInitializer();
+    }
+
+    assert root.isStaticGet();
+
+    // Checking if we can safely inline class implemented following singleton-like
+    // pattern, by which we assume a static final field holding on to the reference
+    // initialized in class constructor.
+    //
+    // In general we are targeting cases when the class is defined as:
+    //
+    //   class X {
+    //     static final X F;
+    //     static {
+    //       F = new X();
+    //     }
+    //   }
+    //
+    // and being used as follows:
+    //
+    //   void foo() {
+    //     f = X.F;
+    //     f.bar();
+    //   }
+    //
+    // The main difference from the similar case of class inliner with 'new-instance'
+    // instruction is that in this case the instance we inline is not just leaked, but
+    // is actually published via X.F field. There are several risks we need to address
+    // in this case:
+    //
+    //    Risk: instance stored in field X.F has changed after it was initialized in
+    //      class initializer
+    //    Solution: we assume that final field X.F is not modified outside the class
+    //      initializer. In rare cases when it is (e.g. via reflections) it should
+    //      be marked with keep rules
+    //
+    //    Risk: instance stored in field X.F is not initialized yet
+    //    Solution: not initialized instance can only be visible if X.<clinit>
+    //      triggers other class initialization which references X.F. This
+    //      situation should never happen if we:
+    //        -- don't allow any superclasses to have static initializer,
+    //        -- don't allow any subclasses,
+    //        -- guarantee the class has trivial class initializer
+    //           (see CodeRewriter::computeClassInitializerInfo), and
+    //        -- guarantee the instance is initialized with trivial instance
+    //           initializer (see CodeRewriter::computeInstanceInitializerInfo)
+    //
+    //    Risk: instance stored in field X.F was mutated
+    //    Solution: we require that class X does not have any instance fields, and
+    //      if any of its superclasses has instance fields, accessing them will make
+    //      this instance not eligible for inlining. I.e. even though the instance is
+    //      publicized and its state has been mutated, it will not effect the logic
+    //      of class inlining
+    //
+
+    if (eligibleClassDefinition.instanceFields().length > 0 ||
+        !eligibleClassDefinition.accessFlags.isFinal()) {
+      return false;
+    }
+
+    // Singleton instance must be initialized in class constructor.
+    DexEncodedMethod classInitializer = eligibleClassDefinition.getClassInitializer();
+    if (classInitializer == null || isProcessedConcurrently.test(classInitializer)) {
+      return false;
+    }
+
+    TrivialInitializer info =
+        classInitializer.getOptimizationInfo().getTrivialInitializerInfo();
+    assert info == null || info instanceof ClassTrivialInitializer;
+    DexField instanceField = root.asStaticGet().getField();
+    // Singleton instance field must NOT be pinned.
+    return info != null &&
+        ((ClassTrivialInitializer) info).field == instanceField &&
+        !appInfo.isPinned(eligibleClassDefinition.lookupStaticField(instanceField).field);
+  }
+
   private Map<InvokeMethodWithReceiver, InliningInfo> checkInstanceUsersEligibility(
       AppInfoWithSubtyping appInfo, DexEncodedMethod method,
       Predicate<DexEncodedMethod> isProcessedConcurrently,
-      NewInstance newInstanceInsn, Value receiver, DexType clazz) {
+      Instruction root, Value receiver,
+      DexType eligibleClass, DexClass eligibleClassDefinition) {
 
     // No Phi users.
     if (receiver.numberOfPhiUsers() > 0) {
@@ -169,14 +286,13 @@
 
     Map<InvokeMethodWithReceiver, InliningInfo> methodCalls = new IdentityHashMap<>();
 
-    DexClass definition = appInfo.definitionFor(clazz);
-
     for (Instruction user : receiver.uniqueUsers()) {
       // Field read/write.
       if (user.isInstanceGet() ||
           (user.isInstancePut() && user.asInstancePut().value() != receiver)) {
         DexField field = user.asFieldInstruction().getField();
-        if (field.clazz == newInstanceInsn.clazz && definition.lookupInstanceField(field) != null) {
+        if (field.clazz == eligibleClass &&
+            eligibleClassDefinition.lookupInstanceField(field) != null) {
           // Since class inliner currently only supports classes directly extending
           // java.lang.Object, we don't need to worry about fields defined in superclasses.
           continue;
@@ -184,10 +300,10 @@
         return null; // Not eligible.
       }
 
-      // Eligible constructor call.
-      if (user.isInvokeDirect()) {
+      // Eligible constructor call (for new instance roots only).
+      if (user.isInvokeDirect() && root.isNewInstance()) {
         InliningInfo inliningInfo = isEligibleConstructorCall(appInfo, method,
-            user.asInvokeDirect(), receiver, clazz, isProcessedConcurrently);
+            user.asInvokeDirect(), receiver, eligibleClass, isProcessedConcurrently);
         if (inliningInfo != null) {
           methodCalls.put(user.asInvokeDirect(), inliningInfo);
           continue;
@@ -199,7 +315,7 @@
       if (user.isInvokeVirtual() || user.isInvokeInterface()) {
         InliningInfo inliningInfo = isEligibleMethodCall(
             appInfo, method, user.asInvokeMethodWithReceiver(),
-            receiver, clazz, isProcessedConcurrently);
+            receiver, eligibleClass, isProcessedConcurrently);
         if (inliningInfo != null) {
           methodCalls.put(user.asInvokeMethodWithReceiver(), inliningInfo);
           continue;
@@ -215,11 +331,12 @@
   // Remove call to superclass initializer, replace field reads with appropriate
   // values, insert phis when needed.
   private void removeSuperClassInitializerAndFieldReads(
-      IRCode code, NewInstance newInstance, Value eligibleInstance) {
+      IRCode code, Instruction root, Value eligibleInstance) {
     Map<DexField, FieldValueHelper> fieldHelpers = new IdentityHashMap<>();
     for (Instruction user : eligibleInstance.uniqueUsers()) {
       // Remove the call to superclass constructor.
-      if (user.isInvokeDirect() &&
+      if (root.isNewInstance() &&
+          user.isInvokeDirect() &&
           user.asInvokeDirect().getInvokedMethod() == factory.objectMethods.constructor) {
         removeInstruction(user);
         continue;
@@ -227,7 +344,7 @@
 
       if (user.isInstanceGet()) {
         // Replace a field read with appropriate value.
-        replaceFieldRead(code, newInstance, user.asInstanceGet(), fieldHelpers);
+        replaceFieldRead(code, root, user.asInstanceGet(), fieldHelpers);
         continue;
       }
 
@@ -261,13 +378,24 @@
     return totalSize;
   }
 
-  private void replaceFieldRead(IRCode code, NewInstance newInstance,
+  private boolean instructionLimitExempt(
+      DexClass eligibleClassDefinition, AppInfoWithLiveness appInfo) {
+    if (appInfo.isPinned(eligibleClassDefinition.type)) {
+      return false;
+    }
+    KotlinInfo kotlinInfo = eligibleClassDefinition.getKotlinInfo();
+    return kotlinInfo != null &&
+        kotlinInfo.isSyntheticClass() &&
+        kotlinInfo.asSyntheticClass().isLambda();
+  }
+
+  private void replaceFieldRead(IRCode code, Instruction root,
       InstanceGet fieldRead, Map<DexField, FieldValueHelper> fieldHelpers) {
 
     Value value = fieldRead.outValue();
     if (value != null) {
       FieldValueHelper helper = fieldHelpers.computeIfAbsent(
-          fieldRead.getField(), field -> new FieldValueHelper(field, code, newInstance));
+          fieldRead.getField(), field -> new FieldValueHelper(field, code, root));
       Value newValue = helper.getValueForFieldRead(fieldRead.getBlock(), fieldRead);
       value.replaceUsers(newValue);
       for (FieldValueHelper fieldValueHelper : fieldHelpers.values()) {
@@ -282,16 +410,16 @@
   private static final class FieldValueHelper {
     final DexField field;
     final IRCode code;
-    final NewInstance newInstance;
+    final Instruction root;
 
     private Value defaultValue = null;
     private final Map<BasicBlock, Value> ins = new IdentityHashMap<>();
     private final Map<BasicBlock, Value> outs = new IdentityHashMap<>();
 
-    private FieldValueHelper(DexField field, IRCode code, NewInstance newInstance) {
+    private FieldValueHelper(DexField field, IRCode code, Instruction root) {
       this.field = field;
       this.code = code;
-      this.newInstance = newInstance;
+      this.root = root;
     }
 
     void replaceValue(Value oldValue, Value newValue) {
@@ -370,10 +498,10 @@
         Instruction instruction = iterator.previous();
         assert instruction != null;
 
-        if (instruction == newInstance ||
+        if (instruction == root ||
             (instruction.isInstancePut() &&
                 instruction.asInstancePut().getField() == field &&
-                instruction.asInstancePut().object() == newInstance.outValue())) {
+                instruction.asInstancePut().object() == root.outValue())) {
           valueProducingInsn = instruction;
           break;
         }
@@ -386,14 +514,14 @@
         return valueProducingInsn.asInstancePut().value();
       }
 
-      assert newInstance == valueProducingInsn;
+      assert root == valueProducingInsn;
       if (defaultValue == null) {
         // If we met newInstance it means that default value is supposed to be used.
         defaultValue = code.createValue(ValueType.fromDexType(field.type));
         ConstNumber defaultValueInsn = new ConstNumber(defaultValue, 0);
-        defaultValueInsn.setPosition(newInstance.getPosition());
+        defaultValueInsn.setPosition(root.getPosition());
         LinkedList<Instruction> instructions = block.getInstructions();
-        instructions.add(instructions.indexOf(newInstance) + 1, defaultValueInsn);
+        instructions.add(instructions.indexOf(root) + 1, defaultValueInsn);
         defaultValueInsn.setBlock(block);
       }
       return defaultValue;
@@ -539,7 +667,7 @@
   //   - is not an abstract class or interface
   //   - directly extends java.lang.Object
   //   - does not declare finalizer
-  //   - does not trigger any static initializers
+  //   - does not trigger any static initializers except for its own
   private boolean computeClassEligible(AppInfo appInfo, DexType clazz) {
     DexClass definition = appInfo.definitionFor(clazz);
     if (definition == null || definition.isLibraryClass() ||
@@ -561,6 +689,6 @@
     }
 
     // Check for static initializers in this class or any of interfaces it implements.
-    return !appInfo.canTriggerStaticInitializer(clazz);
+    return !appInfo.canTriggerStaticInitializer(clazz, true);
   }
 }
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
index 38768cd..51a5262 100644
--- 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
@@ -16,13 +16,17 @@
 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.Instruction;
 import com.android.tools.r8.code.NewInstance;
+import com.android.tools.r8.code.Sget;
 import com.android.tools.r8.graph.DexCode;
 import com.android.tools.r8.ir.optimize.classinliner.builders.BuildersTestClass;
 import com.android.tools.r8.ir.optimize.classinliner.builders.ControlFlow;
 import com.android.tools.r8.ir.optimize.classinliner.builders.Pair;
 import com.android.tools.r8.ir.optimize.classinliner.builders.PairBuilder;
 import com.android.tools.r8.ir.optimize.classinliner.builders.Tuple;
+import com.android.tools.r8.ir.optimize.classinliner.code.C;
+import com.android.tools.r8.ir.optimize.classinliner.code.CodeTestClass;
 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;
@@ -46,6 +50,7 @@
 import java.util.Collections;
 import java.util.Set;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -67,64 +72,64 @@
         ToolHelper.getClassAsBytes(CycleReferenceBA.class),
         ToolHelper.getClassAsBytes(ClassWithFinal.class)
     };
-    String main = TrivialTestClass.class.getCanonicalName();
-    ProcessResult javaOutput = runOnJavaRaw(main, classes);
-    assertEquals(0, javaOutput.exitCode);
-
     AndroidApp app = runR8(buildAndroidApp(classes), TrivialTestClass.class);
 
+    String javaOutput = runOnJava(TrivialTestClass.class);
+    String artOutput = runOnArt(app, TrivialTestClass.class);
+    assertEquals(javaOutput, artOutput);
+
     DexInspector inspector = new DexInspector(app);
     ClassSubject clazz = inspector.clazz(TrivialTestClass.class);
 
     assertEquals(
         Collections.singleton("java.lang.StringBuilder"),
-        collectNewInstanceTypes(clazz, "testInner"));
+        collectTypes(clazz, "testInner", "void"));
 
     assertEquals(
         Collections.emptySet(),
-        collectNewInstanceTypes(clazz, "testConstructorMapping1"));
+        collectTypes(clazz, "testConstructorMapping1", "void"));
 
     assertEquals(
         Collections.emptySet(),
-        collectNewInstanceTypes(clazz, "testConstructorMapping2"));
+        collectTypes(clazz, "testConstructorMapping2", "void"));
 
     assertEquals(
         Collections.singleton("java.lang.StringBuilder"),
-        collectNewInstanceTypes(clazz, "testConstructorMapping3"));
+        collectTypes(clazz, "testConstructorMapping3", "void"));
 
     assertEquals(
         Collections.emptySet(),
-        collectNewInstanceTypes(clazz, "testEmptyClass"));
+        collectTypes(clazz, "testEmptyClass", "void"));
 
     assertEquals(
         Collections.singleton(
             "com.android.tools.r8.ir.optimize.classinliner.trivial.EmptyClassWithInitializer"),
-        collectNewInstanceTypes(clazz, "testEmptyClassWithInitializer"));
+        collectTypes(clazz, "testEmptyClassWithInitializer", "void"));
 
     assertEquals(
         Collections.singleton(
             "com.android.tools.r8.ir.optimize.classinliner.trivial.ClassWithFinal"),
-        collectNewInstanceTypes(clazz, "testClassWithFinalizer"));
+        collectTypes(clazz, "testClassWithFinalizer", "void"));
 
     assertEquals(
         Collections.emptySet(),
-        collectNewInstanceTypes(clazz, "testCallOnIface1"));
+        collectTypes(clazz, "testCallOnIface1", "void"));
 
     assertEquals(
         Collections.singleton(
             "com.android.tools.r8.ir.optimize.classinliner.trivial.Iface2Impl"),
-        collectNewInstanceTypes(clazz, "testCallOnIface2"));
+        collectTypes(clazz, "testCallOnIface2", "void"));
 
     assertEquals(
         Sets.newHashSet(
             "com.android.tools.r8.ir.optimize.classinliner.trivial.CycleReferenceAB",
             "java.lang.StringBuilder"),
-        collectNewInstanceTypes(clazz, "testCycles"));
+        collectTypes(clazz, "testCycles", "void"));
 
     assertEquals(
         Sets.newHashSet("java.lang.StringBuilder",
             "com.android.tools.r8.ir.optimize.classinliner.trivial.CycleReferenceAB"),
-        collectNewInstanceTypes(inspector.clazz(CycleReferenceAB.class), "foo", "int"));
+        collectTypes(inspector.clazz(CycleReferenceAB.class), "foo", "void", "int"));
 
     assertFalse(inspector.clazz(CycleReferenceBA.class).isPresent());
   }
@@ -139,12 +144,12 @@
         ToolHelper.getClassAsBytes(PairBuilder.class),
         ToolHelper.getClassAsBytes(ControlFlow.class),
     };
-    String main = BuildersTestClass.class.getCanonicalName();
-    ProcessResult javaOutput = runOnJavaRaw(main, classes);
-    assertEquals(0, javaOutput.exitCode);
-
     AndroidApp app = runR8(buildAndroidApp(classes), BuildersTestClass.class);
 
+    String javaOutput = runOnJava(BuildersTestClass.class);
+    String artOutput = runOnArt(app, BuildersTestClass.class);
+    assertEquals(javaOutput, artOutput);
+
     DexInspector inspector = new DexInspector(app);
     ClassSubject clazz = inspector.clazz(BuildersTestClass.class);
 
@@ -152,7 +157,7 @@
         Sets.newHashSet(
             "com.android.tools.r8.ir.optimize.classinliner.builders.Pair",
             "java.lang.StringBuilder"),
-        collectNewInstanceTypes(clazz, "testSimpleBuilder"));
+        collectTypes(clazz, "testSimpleBuilder", "void"));
 
     // Note that Pair created instances were also inlined in the following method since
     // we use 'System.out.println(pX.toString())', if we used 'System.out.println(pX)'
@@ -160,25 +165,25 @@
     // would make it not eligible for inlining.
     assertEquals(
         Collections.singleton("java.lang.StringBuilder"),
-        collectNewInstanceTypes(clazz, "testSimpleBuilderWithMultipleBuilds"));
+        collectTypes(clazz, "testSimpleBuilderWithMultipleBuilds", "void"));
 
     assertFalse(inspector.clazz(PairBuilder.class).isPresent());
 
     assertEquals(
         Collections.singleton("java.lang.StringBuilder"),
-        collectNewInstanceTypes(clazz, "testBuilderConstructors"));
+        collectTypes(clazz, "testBuilderConstructors", "void"));
 
     assertFalse(inspector.clazz(Tuple.class).isPresent());
 
     assertEquals(
         Collections.singleton("java.lang.StringBuilder"),
-        collectNewInstanceTypes(clazz, "testWithControlFlow"));
+        collectTypes(clazz, "testWithControlFlow", "void"));
 
     assertFalse(inspector.clazz(ControlFlow.class).isPresent());
 
     assertEquals(
         Collections.emptySet(),
-        collectNewInstanceTypes(clazz, "testWithMoreControlFlow"));
+        collectTypes(clazz, "testWithMoreControlFlow", "void"));
 
     assertFalse(inspector.clazz(BuildersTestClass.Pos.class).isPresent());
   }
@@ -216,14 +221,65 @@
     assertThat(artResult.stderr, containsString("IncompatibleClassChangeError"));
   }
 
-  private Set<String> collectNewInstanceTypes(
-      ClassSubject clazz, String methodName, String... params) {
+  @Test
+  public void testCodeSample() throws Exception {
+    byte[][] classes = {
+        ToolHelper.getClassAsBytes(C.class),
+        ToolHelper.getClassAsBytes(C.L.class),
+        ToolHelper.getClassAsBytes(C.F.class),
+        ToolHelper.getClassAsBytes(CodeTestClass.class)
+    };
+    AndroidApp app = runR8(buildAndroidApp(classes), CodeTestClass.class);
+
+    String javaOutput = runOnJava(CodeTestClass.class);
+    String artOutput = runOnArt(app, CodeTestClass.class);
+    assertEquals(javaOutput, artOutput);
+
+    DexInspector inspector = new DexInspector(app);
+    ClassSubject clazz = inspector.clazz(C.class);
+
+    assertEquals(
+        Collections.emptySet(),
+        collectTypes(clazz, "method1", "int"));
+
+    assertEquals(
+        Collections.emptySet(),
+        collectTypes(clazz, "method2", "int"));
+
+    assertEquals(
+        Collections.emptySet(),
+        collectTypes(clazz, "method3", "int"));
+
+    assertFalse(inspector.clazz(C.L.class).isPresent());
+    assertFalse(inspector.clazz(C.F.class).isPresent());
+  }
+
+  private Set<String> collectTypes(
+      ClassSubject clazz, String methodName, String retValue, String... params) {
+    return Stream.concat(
+        collectNewInstanceTypesWithRetValue(clazz, methodName, retValue, params),
+        collectStaticGetTypesWithRetValue(clazz, methodName, retValue, params)
+    ).collect(Collectors.toSet());
+  }
+
+  private Stream<String> collectNewInstanceTypesWithRetValue(
+      ClassSubject clazz, String methodName, String retValue, String... params) {
     assertNotNull(clazz);
-    MethodSignature signature = new MethodSignature(methodName, "void", params);
+    MethodSignature signature = new MethodSignature(methodName, retValue, params);
     DexCode code = clazz.method(signature).getMethod().getCode().asDexCode();
     return filterInstructionKind(code, NewInstance.class)
-        .map(insn -> ((NewInstance) insn).getType().toSourceString())
-        .collect(Collectors.toSet());
+        .map(insn -> ((NewInstance) insn).getType().toSourceString());
+  }
+
+  private Stream<String> collectStaticGetTypesWithRetValue(
+      ClassSubject clazz, String methodName, String retValue, String... params) {
+    assertNotNull(clazz);
+    MethodSignature signature = new MethodSignature(methodName, retValue, params);
+    DexCode code = clazz.method(signature).getMethod().getCode().asDexCode();
+    return filterInstructionKind(code, Sget.class)
+        .map(Instruction::getField)
+        .filter(field -> field.clazz == field.type)
+        .map(field -> field.clazz.toSourceString());
   }
 
   private AndroidApp runR8(AndroidApp app, Class mainClass) throws Exception {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/code/C.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/code/C.java
new file mode 100644
index 0000000..38e75c4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/code/C.java
@@ -0,0 +1,39 @@
+// 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.code;
+
+public class C {
+  public static class L {
+    public final int x;
+
+    public L(int x) {
+      this.x = x;
+    }
+
+    public int getX() {
+      return x;
+    }
+  }
+
+  public final static class F {
+    public final static F I = new F();
+
+    public int getX() {
+      return 123;
+    }
+  }
+
+  public synchronized static int method1() {
+    return new L(1).x;
+  }
+
+  public synchronized static int method2() {
+    return new L(1).getX();
+  }
+
+  public synchronized static int method3() {
+    return F.I.getX();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/code/CodeTestClass.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/code/CodeTestClass.java
new file mode 100644
index 0000000..eb5dc10
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/code/CodeTestClass.java
@@ -0,0 +1,14 @@
+// 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.code;
+
+public class CodeTestClass {
+  public static void main(String[] args) {
+    System.out.println(C.method1());
+    System.out.println(C.method2());
+    System.out.println(C.method3());
+  }
+}
+
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
new file mode 100644
index 0000000..eaae740
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
@@ -0,0 +1,108 @@
+// 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.kotlin;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+
+import com.android.tools.r8.code.NewInstance;
+import com.android.tools.r8.code.SgetObject;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexCode;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.google.common.collect.Sets;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.Test;
+
+public class KotlinClassInlinerTest extends AbstractR8KotlinTestBase {
+  private static boolean isLambda(DexClass clazz) {
+    return !clazz.type.getPackageDescriptor().startsWith("kotlin") &&
+        (isKStyleLambdaOrGroup(clazz) || isJStyleLambdaOrGroup(clazz));
+  }
+
+  private static boolean isKStyleLambdaOrGroup(DexClass clazz) {
+    return clazz.superType.descriptor.toString().equals("Lkotlin/jvm/internal/Lambda;");
+  }
+
+  private static boolean isJStyleLambdaOrGroup(DexClass clazz) {
+    return clazz.superType.descriptor.toString().equals("Ljava/lang/Object;") &&
+        clazz.interfaces.size() == 1;
+  }
+
+  private static Predicate<DexType> createLambdaCheck(DexInspector inspector) {
+    Set<DexType> lambdaClasses = inspector.allClasses().stream()
+        .filter(clazz -> isLambda(clazz.getDexClass()))
+        .map(clazz -> clazz.getDexClass().type)
+        .collect(Collectors.toSet());
+    return lambdaClasses::contains;
+  }
+
+  @Test
+  public void testJStyleLambdas() throws Exception {
+    final String mainClassName = "class_inliner_lambda_j_style.MainKt";
+    runTest("class_inliner_lambda_j_style", mainClassName, (app) -> {
+      DexInspector inspector = new DexInspector(app);
+      Predicate<DexType> lambdaCheck = createLambdaCheck(inspector);
+      ClassSubject clazz = inspector.clazz(mainClassName);
+
+      assertEquals(
+          Sets.newHashSet(),
+          collectAccessedLambdaTypes(lambdaCheck, clazz, "testStateless"));
+
+      assertEquals(
+          Sets.newHashSet(
+              "class_inliner_lambda_j_style.MainKt$testStateful$3"),
+          collectAccessedLambdaTypes(lambdaCheck, clazz, "testStateful"));
+
+      assertFalse(
+          inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateful$1").isPresent());
+
+      assertEquals(
+          Sets.newHashSet(
+              "class_inliner_lambda_j_style.MainKt$testStateful2$1"),
+          collectAccessedLambdaTypes(lambdaCheck, clazz, "testStateful2"));
+
+      assertEquals(
+          Sets.newHashSet(
+              "class_inliner_lambda_j_style.MainKt$testStateful3$1"),
+          collectAccessedLambdaTypes(lambdaCheck, clazz, "testStateful3"));
+    });
+  }
+
+  private Set<String> collectAccessedLambdaTypes(
+      Predicate<DexType> isLambdaType, ClassSubject clazz, String methodName, String... params) {
+    assertNotNull(clazz);
+    MethodSignature signature = new MethodSignature(methodName, "void", params);
+    DexCode code = clazz.method(signature).getMethod().getCode().asDexCode();
+    return Stream.concat(
+        filterInstructionKind(code, NewInstance.class)
+            .map(insn -> ((NewInstance) insn).getType()),
+        filterInstructionKind(code, SgetObject.class)
+            .map(insn -> insn.getField().getHolder())
+    )
+        .filter(isLambdaType)
+        .map(DexType::toSourceString)
+        .collect(Collectors.toSet());
+  }
+
+  @Override
+  protected void runTest(String folder, String mainClass,
+      AndroidAppInspector inspector) throws Exception {
+    runTest(
+        folder, mainClass, null,
+        options -> {
+          options.enableInlining = true;
+          options.enableClassInlining = true;
+          options.enableLambdaMerging = false;
+        }, inspector);
+  }
+}
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 6ebb08b..dc9b642 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
@@ -587,7 +587,7 @@
     final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.ObjectPropertiesKt", "objectProperties_usePrimitiveProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject objectClass = checkClassIsKept(dexInspector, testedClass.getClassName());
       String propertyName = "primitiveProp";
@@ -614,7 +614,7 @@
     final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.ObjectPropertiesKt", "objectProperties_usePrivateProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject objectClass = checkClassIsKept(dexInspector, testedClass.getClassName());
       String propertyName = "privateProp";
@@ -641,7 +641,7 @@
     final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.ObjectPropertiesKt", "objectProperties_useInternalProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject objectClass = checkClassIsKept(dexInspector, testedClass.getClassName());
       String propertyName = "internalProp";
@@ -668,7 +668,7 @@
     final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.ObjectPropertiesKt", "objectProperties_usePublicProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject objectClass = checkClassIsKept(dexInspector, testedClass.getClassName());
       String propertyName = "publicProp";
@@ -695,7 +695,7 @@
     final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.ObjectPropertiesKt", "objectProperties_useLateInitPrivateProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject objectClass = checkClassIsKept(dexInspector, testedClass.getClassName());
       String propertyName = "privateLateInitProp";
@@ -722,7 +722,7 @@
     final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.ObjectPropertiesKt", "objectProperties_useLateInitInternalProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject objectClass = checkClassIsKept(dexInspector, testedClass.getClassName());
       String propertyName = "internalLateInitProp";
@@ -745,7 +745,7 @@
     final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.ObjectPropertiesKt", "objectProperties_useLateInitPublicProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject objectClass = checkClassIsKept(dexInspector, testedClass.getClassName());
       String propertyName = "publicLateInitProp";
@@ -768,7 +768,7 @@
     final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.FilePropertiesKt", "fileProperties_usePrimitiveProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject objectClass = checkClassIsKept(dexInspector, testedClass.getClassName());
       String propertyName = "primitiveProp";
@@ -795,7 +795,7 @@
     final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.FilePropertiesKt", "fileProperties_usePrivateProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject objectClass = checkClassIsKept(dexInspector, testedClass.getClassName());
       String propertyName = "privateProp";
@@ -821,7 +821,7 @@
     final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.FilePropertiesKt", "fileProperties_useInternalProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject objectClass = checkClassIsKept(dexInspector, testedClass.getClassName());
       String propertyName = "internalProp";
@@ -847,7 +847,7 @@
     final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.FilePropertiesKt", "fileProperties_usePublicProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject objectClass = checkClassIsKept(dexInspector, testedClass.getClassName());
       String propertyName = "publicProp";
@@ -874,7 +874,7 @@
     final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.FilePropertiesKt", "fileProperties_useLateInitPrivateProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject fileClass = checkClassIsKept(dexInspector, testedClass.getClassName());
       String propertyName = "privateLateInitProp";
@@ -900,7 +900,7 @@
     final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.FilePropertiesKt", "fileProperties_useLateInitInternalProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject objectClass = checkClassIsKept(dexInspector, testedClass.getClassName());
       String propertyName = "internalLateInitProp";
@@ -924,7 +924,7 @@
     final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.FilePropertiesKt", "fileProperties_useLateInitPublicProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject objectClass = checkClassIsKept(dexInspector, testedClass.getClassName());
       String propertyName = "publicLateInitProp";
diff --git a/src/test/kotlinR8TestResources/class_inliner_lambda_j_style/SamIface.java b/src/test/kotlinR8TestResources/class_inliner_lambda_j_style/SamIface.java
new file mode 100644
index 0000000..bccef3e
--- /dev/null
+++ b/src/test/kotlinR8TestResources/class_inliner_lambda_j_style/SamIface.java
@@ -0,0 +1,21 @@
+// 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 class_inliner_lambda_j_style;
+
+public interface SamIface {
+  String foo();
+
+  class Consumer {
+    public static void consume(SamIface iface) {
+      System.out.println(iface.foo());
+    }
+
+    public static void consumeBig(SamIface iface) {
+      System.out.println("Bigger than inline limit, class name: " + iface.getClass().getName());
+      System.out.println("Bigger than inline limit, result: '" + iface.foo() + "'");
+      consume(iface);
+    }
+  }
+}
diff --git a/src/test/kotlinR8TestResources/class_inliner_lambda_j_style/main.kt b/src/test/kotlinR8TestResources/class_inliner_lambda_j_style/main.kt
new file mode 100644
index 0000000..ebf26cd
--- /dev/null
+++ b/src/test/kotlinR8TestResources/class_inliner_lambda_j_style/main.kt
@@ -0,0 +1,99 @@
+// 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 class_inliner_lambda_j_style
+
+private var COUNT = 0
+
+fun nextInt() = COUNT++
+fun next() = "${nextInt()}".padStart(3, '0')
+
+fun main(args: Array<String>) {
+    testStateless()
+    testStateful()
+    testStateful2()
+    testStateful3()
+}
+
+@Synchronized
+fun testStateless() {
+    SamIface.Consumer.consume { "123" }
+    SamIface.Consumer.consume { "ABC" }
+    SamIface.Consumer.consume {
+        var x = 0
+        println("A: ${x++}")
+        println("B: ${x++}")
+        println("C: ${x++}")
+        println("D: ${x++}")
+        println("E: ${x++}")
+        "outer + $x"
+    }
+}
+
+@Synchronized
+fun testStateful() {
+    var someVariable = 0
+
+    SamIface.Consumer.consume {
+        println("A: someVariable = $someVariable")
+        someVariable += 1
+        "B: someVariable = $someVariable"
+    }
+    SamIface.Consumer.consume {
+        SamIface.Consumer.consume {
+            println("E: someVariable = $someVariable")
+            someVariable += 1
+            "F: someVariable = $someVariable"
+        }
+        for (i in 1..20) {
+            someVariable += 1
+            if (i % 4 == 0) {
+                println("G: someVariable = $someVariable")
+            }
+        }
+        someVariable += 1
+        "H: someVariable = $someVariable"
+    }
+    SamIface.Consumer.consumeBig {
+        println("I: someVariable = $someVariable")
+        someVariable += 1
+        "J: someVariable = $someVariable"
+    }
+}
+
+@Synchronized
+fun testStateful2() {
+    var someVariable = 0
+    SamIface.Consumer.consumeBig {
+        println("[Z] A: someVariable = $someVariable")
+        someVariable += 1
+        println("[Z] B: someVariable = $someVariable")
+        someVariable += 1
+        println("[Z] C: someVariable = $someVariable")
+        someVariable += 1
+        println("[Z] D: someVariable = $someVariable")
+        someVariable += 1
+        println("[Z] E: someVariable = $someVariable")
+        someVariable += 1
+        "[Z] F: someVariable = $someVariable"
+    }
+}
+
+@Synchronized
+fun testStateful3() {
+    var someVariable = 0
+    SamIface.Consumer.consumeBig {
+        println("[W] A: someVariable = $someVariable")
+        someVariable += 1
+        println("[W] B: someVariable = $someVariable")
+        someVariable += 1
+        println("[W] C: someVariable = $someVariable")
+        someVariable += 1
+        println("[W] D: someVariable = $someVariable")
+        someVariable += 1
+        println("[W] E: someVariable = $someVariable")
+        someVariable += 1
+        "[W] F: someVariable = $someVariable"
+    }
+}