Fix class inliner bail-out after force inlining

Bug: 145090972
Change-Id: Ifde30391c6839374649957b4b6f350e7fb1924ed
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 6eb6a94..3b51579 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfo.java
@@ -535,9 +535,12 @@
   public DexEncodedField resolveFieldOn(DexType type, DexField desc) {
     assert checkIfObsolete();
     DexClass holder = definitionFor(type);
-    if (holder == null) {
-      return null;
-    }
+    return holder != null ? resolveFieldOn(holder, desc) : null;
+  }
+
+  public DexEncodedField resolveFieldOn(DexClass holder, DexField desc) {
+    assert checkIfObsolete();
+    assert holder != null;
     // Step 1: Class declares the field.
     DexEncodedField result = holder.lookupField(desc);
     if (result != null) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
index 2f98fa7..36112c9 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
@@ -4,9 +4,11 @@
 
 package com.android.tools.r8.ir.optimize.classinliner;
 
+
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
@@ -20,6 +22,7 @@
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.If;
 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.InstructionOrPhi;
 import com.android.tools.r8.ir.code.Invoke.Type;
@@ -305,14 +308,12 @@
         if (user.isInstanceGet()
             || (user.isInstancePut()
                 && receivers.addIllegalReceiverAlias(user.asInstancePut().value()))) {
-          DexField field = user.asFieldInstruction().getField();
-          if (field.holder == 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;
+          DexEncodedField field =
+              appView.appInfo().resolveField(user.asFieldInstruction().getField());
+          if (field == null || field.isStatic()) {
+            return user; // Not eligible.
           }
-          return user; // Not eligible.
+          continue;
         }
 
         if (user.isInvokeMethod()) {
@@ -467,10 +468,44 @@
     if (methodCallsOnInstance.isEmpty()) {
       return false;
     }
+
     assert methodCallsOnInstance.keySet().stream()
         .map(InvokeMethodWithReceiver::getReceiver)
         .allMatch(receivers::isReceiverAlias);
+
     inliner.performForcedInlining(method, code, methodCallsOnInstance, inliningIRProvider);
+
+    // In case we are class inlining an object allocation that does not inherit directly from
+    // java.lang.Object, we need keep force inlining the constructor until we reach
+    // java.lang.Object.<init>().
+    if (root.isNewInstance()) {
+      do {
+        methodCallsOnInstance.clear();
+        for (Instruction instruction : eligibleInstance.uniqueUsers()) {
+          if (instruction.isInvokeDirect()) {
+            InvokeDirect invoke = instruction.asInvokeDirect();
+            Value receiver = invoke.getReceiver();
+            if (receiver == eligibleInstance) {
+              DexMethod invokedMethod = invoke.getInvokedMethod();
+              if (appView.dexItemFactory().isConstructor(invokedMethod)
+                  && invokedMethod != appView.dexItemFactory().objectMethods.constructor) {
+                methodCallsOnInstance.put(
+                    invoke,
+                    new InliningInfo(
+                        appView.definitionFor(invokedMethod), root.asNewInstance().clazz));
+                break;
+              }
+            } else {
+              assert receiver.getAliasedValue() != eligibleInstance;
+            }
+          }
+        }
+        if (!methodCallsOnInstance.isEmpty()) {
+          inliner.performForcedInlining(method, code, methodCallsOnInstance, inliningIRProvider);
+        }
+      } while (!methodCallsOnInstance.isEmpty());
+    }
+
     return true;
   }
 
@@ -495,13 +530,14 @@
   private void removeMiscUsages(IRCode code) {
     boolean needToRemoveUnreachableBlocks = false;
     for (Instruction user : eligibleInstance.uniqueUsers()) {
-      // Remove the call to superclass constructor.
-      if (root.isNewInstance()
-          && user.isInvokeDirect()
-          && appView.dexItemFactory().isConstructor(user.asInvokeDirect().getInvokedMethod())
-          && user.asInvokeDirect().getInvokedMethod().holder == eligibleClassDefinition.superType) {
-        removeInstruction(user);
-        continue;
+      // Remove the call to java.lang.Object.<init>().
+      if (user.isInvokeDirect()) {
+        InvokeDirect invoke = user.asInvokeDirect();
+        if (root.isNewInstance()
+            && invoke.getInvokedMethod() == appView.dexItemFactory().objectMethods.constructor) {
+          removeInstruction(invoke);
+          continue;
+        }
       }
 
       if (user.isIf()) {
@@ -615,7 +651,10 @@
                 + "` after field reads removed: "
                 + user);
       }
-      if (user.asInstancePut().getField().holder != eligibleClass) {
+      InstancePut instancePut = user.asInstancePut();
+      DexEncodedField field =
+          appView.appInfo().resolveFieldOn(eligibleClassDefinition, instancePut.getField());
+      if (field == null) {
         throw new Unreachable(
             "Unexpected field write left in method `"
                 + method.method.toSourceString()
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
index 3d5f386..ee49c97 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
@@ -444,7 +444,7 @@
 
       if (insn.isInstancePut()) {
         InstancePut instancePut = insn.asInstancePut();
-        DexEncodedField field = clazz.lookupInstanceField(instancePut.getField());
+        DexEncodedField field = appView.appInfo().resolveFieldOn(clazz, instancePut.getField());
         if (field == null
             || instancePut.object() != receiver
             || (instancePut.value() != receiver && !instancePut.value().isArgument())) {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/BuilderWithInheritanceTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/BuilderWithInheritanceTest.java
index 02eb602..a1042ce 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/BuilderWithInheritanceTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/BuilderWithInheritanceTest.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.ir.optimize.classinliner;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.NeverInline;
@@ -46,7 +47,7 @@
             .run(parameters.getRuntime(), TestClass.class)
             .assertSuccessWithOutput("42")
             .inspector();
-    assertThat(inspector.clazz(Builder.class), isPresent());
+    assertThat(inspector.clazz(Builder.class), not(isPresent()));
   }
 
   static class TestClass {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/EscapeFromParentConstructorTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/EscapeFromParentConstructorTest.java
new file mode 100644
index 0000000..2062667
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/EscapeFromParentConstructorTest.java
@@ -0,0 +1,84 @@
+// Copyright (c) 2019, 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 com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NeverMerge;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class EscapeFromParentConstructorTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public EscapeFromParentConstructorTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(EscapeFromParentConstructorTest.class)
+        .addKeepMainRule(TestClass.class)
+        .enableInliningAnnotations()
+        .enableMergeAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("Hello world!");
+  }
+
+  private void inspect(CodeInspector inspector) {
+    // The receiver escapes from BuilderBase.<init>(), and therefore, Builder is not considered
+    // eligible for class inlining.
+    assertThat(inspector.clazz(BuilderBase.class), isPresent());
+    assertThat(inspector.clazz(Builder.class), isPresent());
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      System.out.println(new Builder().build());
+    }
+  }
+
+  @NeverMerge
+  static class BuilderBase {
+
+    String greeting;
+
+    BuilderBase() {
+      initialize();
+    }
+
+    @NeverInline
+    void initialize() {
+      this.greeting = "Hello world!";
+    }
+  }
+
+  static class Builder extends BuilderBase {
+
+    String build() {
+      return greeting;
+    }
+  }
+}