Synthesize InitClass instruction in inliner

Change-Id: I8781141485e1b3d942610377c4649ea88618f770
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
index f1c5dc9..efdf473 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
@@ -128,62 +128,6 @@
     return false;
   }
 
-  private boolean canInlineStaticInvoke(
-      InvokeStatic invoke,
-      DexEncodedMethod method,
-      DexEncodedMethod target,
-      ClassInitializationAnalysis classInitializationAnalysis,
-      WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
-    // Only proceed with inlining a static invoke if:
-    // - the holder for the target is a subtype of the holder for the method,
-    // - the target method always triggers class initialization of its holder before any other side
-    //   effect (hence preserving class initialization semantics),
-    // - the current method has already triggered the holder for the target method to be
-    //   initialized, or
-    // - there is no non-trivial class initializer.
-    DexType targetHolder = target.method.holder;
-    if (appView.appInfo().isSubtype(method.method.holder, targetHolder)) {
-      return true;
-    }
-    DexClass clazz = appView.definitionFor(targetHolder);
-    assert clazz != null;
-    if (target.getOptimizationInfo().triggersClassInitBeforeAnySideEffect()) {
-      return true;
-    }
-    if (!method.isStatic()) {
-      boolean targetIsGuaranteedToBeInitialized =
-          appView.withInitializedClassesInInstanceMethods(
-              analysis ->
-                  analysis.isClassDefinitelyLoadedInInstanceMethodsOn(
-                      target.method.holder, method.method.holder),
-              false);
-      if (targetIsGuaranteedToBeInitialized) {
-        return true;
-      }
-    }
-    if (classInitializationAnalysis.isClassDefinitelyLoadedBeforeInstruction(
-        target.method.holder, invoke)) {
-      return true;
-    }
-    // Check for class initializer side effects when loading this class, as inlining might remove
-    // the load operation.
-    //
-    // See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-5.html#jvms-5.5.
-    //
-    // For simplicity, we are conservative and consider all interfaces, not only the ones with
-    // default methods.
-    if (!clazz.classInitializationMayHaveSideEffects(appView)) {
-      return true;
-    }
-
-    if (appView.rootSet().bypassClinitForInlining.contains(target.method)) {
-      return true;
-    }
-
-    whyAreYouNotInliningReporter.reportMustTriggerClassInitialization();
-    return false;
-  }
-
   @Override
   public boolean passesInliningConstraints(
       InvokeMethod invoke,
@@ -391,12 +335,70 @@
       Reason reason,
       ClassInitializationAnalysis classInitializationAnalysis,
       WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
-    // Abort inlining attempt if we can not guarantee class for static target has been initialized.
-    if (!canInlineStaticInvoke(
-        invoke, method, singleTarget, classInitializationAnalysis, whyAreYouNotInliningReporter)) {
-      return null;
+    InlineAction action = new InlineAction(singleTarget, invoke, reason);
+    if (isTargetClassInitialized(invoke, method, singleTarget, classInitializationAnalysis)) {
+      return action;
     }
-    return new InlineAction(singleTarget, invoke, reason);
+    if (appView.canUseInitClass()) {
+      action.setShouldSynthesizeInitClass();
+      return action;
+    }
+    whyAreYouNotInliningReporter.reportMustTriggerClassInitialization();
+    return null;
+  }
+
+  private boolean isTargetClassInitialized(
+      InvokeStatic invoke,
+      DexEncodedMethod method,
+      DexEncodedMethod target,
+      ClassInitializationAnalysis classInitializationAnalysis) {
+    // Only proceed with inlining a static invoke if:
+    // - the holder for the target is a subtype of the holder for the method,
+    // - the target method always triggers class initialization of its holder before any other side
+    //   effect (hence preserving class initialization semantics),
+    // - the current method has already triggered the holder for the target method to be
+    //   initialized, or
+    // - there is no non-trivial class initializer.
+    DexType targetHolder = target.method.holder;
+    if (appView.appInfo().isSubtype(method.method.holder, targetHolder)) {
+      return true;
+    }
+    DexClass clazz = appView.definitionFor(targetHolder);
+    assert clazz != null;
+    if (target.getOptimizationInfo().triggersClassInitBeforeAnySideEffect()) {
+      return true;
+    }
+    if (!method.isStatic()) {
+      boolean targetIsGuaranteedToBeInitialized =
+          appView.withInitializedClassesInInstanceMethods(
+              analysis ->
+                  analysis.isClassDefinitelyLoadedInInstanceMethodsOn(
+                      target.method.holder, method.method.holder),
+              false);
+      if (targetIsGuaranteedToBeInitialized) {
+        return true;
+      }
+    }
+    if (classInitializationAnalysis.isClassDefinitelyLoadedBeforeInstruction(
+        target.method.holder, invoke)) {
+      return true;
+    }
+    // Check for class initializer side effects when loading this class, as inlining might remove
+    // the load operation.
+    //
+    // See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-5.html#jvms-5.5.
+    //
+    // For simplicity, we are conservative and consider all interfaces, not only the ones with
+    // default methods.
+    if (!clazz.classInitializationMayHaveSideEffects(appView)) {
+      return true;
+    }
+
+    if (appView.rootSet().bypassClinitForInlining.contains(target.method)) {
+      return true;
+    }
+
+    return false;
   }
 
   @Override
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 c9bf058..041d5a8 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
@@ -26,6 +26,7 @@
 import com.android.tools.r8.ir.code.CatchHandlers.CatchHandler;
 import com.android.tools.r8.ir.code.ConstClass;
 import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InitClass;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionIterator;
 import com.android.tools.r8.ir.code.InstructionListIterator;
@@ -573,6 +574,7 @@
     public final Invoke invoke;
     final Reason reason;
 
+    private boolean shouldSynthesizeInitClass;
     private boolean shouldSynthesizeNullCheckForReceiver;
 
     InlineAction(DexEncodedMethod target, Invoke invoke, Reason reason) {
@@ -581,7 +583,13 @@
       this.reason = reason;
     }
 
+    void setShouldSynthesizeInitClass() {
+      assert !shouldSynthesizeNullCheckForReceiver;
+      shouldSynthesizeInitClass = true;
+    }
+
     void setShouldSynthesizeNullCheckForReceiver() {
+      assert !shouldSynthesizeInitClass;
       shouldSynthesizeNullCheckForReceiver = true;
     }
 
@@ -599,6 +607,12 @@
       // Build the IR for a yet not processed method, and perform minimal IR processing.
       IRCode code = inliningIRProvider.getInliningIR(invoke, target);
 
+      // Insert a init class instruction if this is needed to preserve class initialization
+      // semantics.
+      if (shouldSynthesizeInitClass) {
+        synthesizeInitClass(code);
+      }
+
       // Insert a null check if this is needed to preserve the implicit null check for the receiver.
       // This is only needed if we do not also insert a monitor-enter instruction, since that will
       // throw a NPE if the receiver is null.
@@ -737,6 +751,21 @@
       return new InlineeWithReason(code, reason);
     }
 
+    private void synthesizeInitClass(IRCode code) {
+      List<Value> arguments = code.collectArguments();
+      BasicBlock entryBlock = code.entryBlock();
+
+      // Insert a new block between the last argument instruction and the first actual instruction
+      // of the method.
+      BasicBlock initClassBlock =
+          entryBlock.listIterator(code, arguments.size()).split(code, 0, null);
+      assert !initClassBlock.hasCatchHandlers();
+
+      InstructionListIterator iterator = initClassBlock.listIterator(code);
+      iterator.setInsertionPosition(entryBlock.exit().getPosition());
+      iterator.add(new InitClass(code.createValue(TypeLatticeElement.getInt()), target.holder()));
+    }
+
     private void synthesizeNullCheckForReceiver(AppView<?> appView, IRCode code) {
       List<Value> arguments = code.collectArguments();
       if (!arguments.isEmpty()) {
diff --git a/src/test/java/com/android/tools/r8/classmerging/InliningAfterStaticClassMergerTest.java b/src/test/java/com/android/tools/r8/classmerging/InliningAfterStaticClassMergerTest.java
index 23c2d55..5fdd52b 100644
--- a/src/test/java/com/android/tools/r8/classmerging/InliningAfterStaticClassMergerTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/InliningAfterStaticClassMergerTest.java
@@ -102,9 +102,18 @@
     FoundClassSubject clazz = classes.get(0);
     assertEquals(StaticMergeCandidateA.class.getTypeName(), clazz.getOriginalName());
 
-    // Check that StaticMergeCandidateB.m() has not been moved into StaticMergeCandidateA, because
-    // that would disable inlining of it.
-    assertEquals(1, clazz.allMethods().size());
+    if (parameters.isCfRuntime()) {
+      // Check that StaticMergeCandidateB.m() has not been moved into StaticMergeCandidateA, because
+      // that would disable inlining of it.
+      assertEquals(1, clazz.allMethods().size());
+    } else {
+      // Check that StaticMergeCandidateB.m() has been inlined.
+      assertEquals(0, clazz.allMethods().size());
+
+      // Check that a static field has been synthesized in order to trigger class initialization.
+      assertEquals(1, clazz.allStaticFields().size());
+      assertEquals("$r8$clinit", clazz.allStaticFields().get(0).getOriginalName());
+    }
 
     // Check that the test class only has a main method.
     ClassSubject classSubject = inspector.clazz(TestClass.class);
diff --git a/src/test/java/com/android/tools/r8/graph/initializedclasses/InitializedClassesInInstanceMethodsTest.java b/src/test/java/com/android/tools/r8/graph/initializedclasses/InitializedClassesInInstanceMethodsTest.java
index 43015bf..ee86dc8 100644
--- a/src/test/java/com/android/tools/r8/graph/initializedclasses/InitializedClassesInInstanceMethodsTest.java
+++ b/src/test/java/com/android/tools/r8/graph/initializedclasses/InitializedClassesInInstanceMethodsTest.java
@@ -30,7 +30,8 @@
 
   @Parameters(name = "{1}, enabled: {0}")
   public static List<Object[]> data() {
-    return buildParameters(BooleanUtils.values(), getTestParameters().withAllRuntimes().build());
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
   }
 
   public InitializedClassesInInstanceMethodsTest(
@@ -53,7 +54,7 @@
         .allowAccessModification()
         .enableNeverClassInliningAnnotations()
         .enableInliningAnnotations()
-        .setMinApi(parameters.getRuntime())
+        .setMinApi(parameters.getApiLevel())
         .compile()
         .inspect(this::verifyOutput)
         .run(parameters.getRuntime(), TestClass.class)
@@ -71,12 +72,15 @@
     assertThat(outerClassSubject.uniqueMethodWithName("exclamationMark"), not(isPresent()));
 
     int numberOfExpectedAccessibilityBridges =
-        enableInitializedClassesInInstanceMethodsAnalysis ? 0 : 3;
+        enableInitializedClassesInInstanceMethodsAnalysis || parameters.isDexRuntime() ? 0 : 3;
     assertEquals(
         numberOfExpectedAccessibilityBridges,
         outerClassSubject
             .allMethods(method -> method.getOriginalName().contains("access$"))
             .size());
+    assertEquals(
+        parameters.isDexRuntime() && !enableInitializedClassesInInstanceMethodsAnalysis,
+        outerClassSubject.uniqueFieldWithName("$r8$clinit").isPresent());
 
     ClassSubject aClassSubject = inspector.clazz(Outer.A.class);
     assertThat(aClassSubject, isPresent());
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/SingletonClassInitializerPatternCannotBePostponedTest.java b/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/SingletonClassInitializerPatternCannotBePostponedTest.java
index d29753b..4213323 100644
--- a/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/SingletonClassInitializerPatternCannotBePostponedTest.java
+++ b/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/SingletonClassInitializerPatternCannotBePostponedTest.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.ir.analysis.sideeffect;
 
 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.TestBase;
@@ -47,9 +48,16 @@
     ClassSubject classSubject = inspector.clazz(A.class);
     assertThat(classSubject, isPresent());
 
-    // A.inlineable() cannot be inlined because it should trigger the class initialization of A,
-    // which should trigger the class initialization of B, which will print "Hello".
-    assertThat(classSubject.uniqueMethodWithName("inlineable"), isPresent());
+    if (parameters.isCfRuntime()) {
+      // A.inlineable() cannot be inlined because it should trigger the class initialization of A,
+      // which should trigger the class initialization of B, which will print "Hello".
+      assertThat(classSubject.uniqueFieldWithName("$r8$clinit"), not(isPresent()));
+      assertThat(classSubject.uniqueMethodWithName("inlineable"), isPresent());
+    } else {
+      // A static field A.$r8$clinit has been synthesized to allow inlining of A.inlineable().
+      assertThat(classSubject.uniqueFieldWithName("$r8$clinit"), isPresent());
+      assertThat(classSubject.uniqueMethodWithName("inlineable"), not(isPresent()));
+    }
   }
 
   static class TestClass {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/InliningFromCurrentClassTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/InliningFromCurrentClassTest.java
index 239b944..27f3735 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/InliningFromCurrentClassTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/InliningFromCurrentClassTest.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.ir.optimize.inliner;
 
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.accessesField;
 import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethod;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
@@ -12,14 +13,30 @@
 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.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 
+@RunWith(Parameterized.class)
 public class InliningFromCurrentClassTest extends TestBase {
 
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public InliningFromCurrentClassTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
   @Test
   public void test() throws Exception {
     String expectedOutput =
@@ -29,17 +46,18 @@
             "In A.inlineable1()",
             "In B.inlineable2()",
             "In C.<clinit>()",
-            "In C.notInlineable()");
+            "In C.inlineableWithInitClass()");
 
     testForJvm().addTestClasspath().run(TestClass.class).assertSuccessWithOutput(expectedOutput);
 
     CodeInspector inspector =
-        testForR8(Backend.DEX)
+        testForR8(parameters.getBackend())
             .addInnerClasses(InliningFromCurrentClassTest.class)
             .addKeepMainRule(TestClass.class)
             .enableInliningAnnotations()
             .enableMergeAnnotations()
-            .run(TestClass.class)
+            .setMinApi(parameters.getApiLevel())
+            .run(parameters.getRuntime(), TestClass.class)
             .assertSuccessWithOutput(expectedOutput)
             .inspector();
 
@@ -52,18 +70,24 @@
     ClassSubject classC = inspector.clazz(C.class);
     assertThat(classC, isPresent());
 
+    MethodSubject testMethod = classB.uniqueMethodWithName("test");
+    assertThat(testMethod, isPresent());
+
     MethodSubject inlineable1Method = classA.uniqueMethodWithName("inlineable1");
     assertThat(inlineable1Method, not(isPresent()));
 
     MethodSubject inlineable2Method = classB.uniqueMethodWithName("inlineable2");
     assertThat(inlineable2Method, not(isPresent()));
 
-    MethodSubject notInlineableMethod = classC.uniqueMethodWithName("notInlineable");
-    assertThat(notInlineableMethod, isPresent());
-
-    MethodSubject testMethod = classB.uniqueMethodWithName("test");
-    assertThat(testMethod, isPresent());
-    assertThat(testMethod, invokesMethod(notInlineableMethod));
+    MethodSubject inlineableWithInitClassMethod =
+        classC.uniqueMethodWithName("inlineableWithInitClass");
+    if (parameters.isCfRuntime()) {
+      assertThat(inlineableWithInitClassMethod, isPresent());
+      assertThat(testMethod, invokesMethod(inlineableWithInitClassMethod));
+    } else {
+      assertThat(inlineableWithInitClassMethod, not(isPresent()));
+      assertThat(testMethod, accessesField(classC.uniqueFieldWithName("$r8$clinit")));
+    }
   }
 
   static class TestClass {
@@ -96,7 +120,7 @@
     static void test() {
       A.inlineable1();
       B.inlineable2();
-      C.notInlineable();
+      C.inlineableWithInitClass();
     }
 
     static void inlineable2() {
@@ -110,8 +134,8 @@
       System.out.println("In C.<clinit>()");
     }
 
-    static void notInlineable() {
-      System.out.println("In C.notInlineable()");
+    static void inlineableWithInitClass() {
+      System.out.println("In C.inlineableWithInitClass()");
     }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java
index 5dacc5a..86a21c1 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.utils.codeinspector;
 
+import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
 import java.util.function.Predicate;
 import org.hamcrest.Description;
@@ -12,6 +13,35 @@
 
 public class CodeMatchers {
 
+  public static Matcher<MethodSubject> accessesField(FieldSubject targetSubject) {
+    if (!targetSubject.isPresent()) {
+      throw new IllegalArgumentException();
+    }
+    DexField target = targetSubject.getField().field;
+    return new TypeSafeMatcher<MethodSubject>() {
+      @Override
+      protected boolean matchesSafely(MethodSubject subject) {
+        if (!subject.isPresent()) {
+          return false;
+        }
+        if (!subject.getMethod().hasCode()) {
+          return false;
+        }
+        return subject.streamInstructions().anyMatch(isFieldAccessWithTarget(target));
+      }
+
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("accesses field `" + target.toSourceString() + "`");
+      }
+
+      @Override
+      public void describeMismatchSafely(final MethodSubject subject, Description description) {
+        description.appendText("method did not");
+      }
+    };
+  }
+
   public static Matcher<MethodSubject> invokesMethod(MethodSubject targetSubject) {
     if (!targetSubject.isPresent()) {
       throw new IllegalArgumentException();
@@ -44,4 +74,8 @@
   public static Predicate<InstructionSubject> isInvokeWithTarget(DexMethod target) {
     return instruction -> instruction.isInvoke() && instruction.getMethod() == target;
   }
+
+  public static Predicate<InstructionSubject> isFieldAccessWithTarget(DexField target) {
+    return instruction -> instruction.isFieldAccess() && instruction.getField() == target;
+  }
 }