Allow clinits that create a singleton instance to be postponed

Bug: 142762129
Change-Id: Iaa472dcf1b52dad304488ed3855fbceb16f20f9d
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index bed2abeb..f60e877 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -730,10 +730,13 @@
 
   public class EnumMethods {
 
-    public DexMethod valueOf;
-    public DexMethod ordinal;
-    public DexMethod name;
-    public DexMethod toString;
+    public final DexMethod valueOf;
+    public final DexMethod ordinal;
+    public final DexMethod name;
+    public final DexMethod toString;
+
+    public final DexMethod finalize =
+        createMethod(enumType, createProto(voidType), finalizeMethodName);
 
     private EnumMethods() {
       valueOf =
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/ValueMayDependOnEnvironmentAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/ValueMayDependOnEnvironmentAnalysis.java
index 6fde03b..d654c87 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/ValueMayDependOnEnvironmentAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/ValueMayDependOnEnvironmentAnalysis.java
@@ -5,16 +5,21 @@
 package com.android.tools.r8.ir.analysis;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.code.ArrayPut;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InvokeDirect;
+import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.InvokeNewArray;
 import com.android.tools.r8.ir.code.NewArrayEmpty;
 import com.android.tools.r8.ir.code.NewArrayFilledData;
 import com.android.tools.r8.ir.code.StaticPut;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.utils.LongInterval;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import java.util.Set;
 
@@ -38,6 +43,9 @@
     if (isConstantArrayThroughoutMethod(root)) {
       return false;
     }
+    if (isNewInstanceWithoutEnvironmentDependentFields(root)) {
+      return false;
+    }
     return true;
   }
 
@@ -144,6 +152,66 @@
     return !valueMayBeMutatedBeforeMethodExit(root, consumedInstructions);
   }
 
+  private boolean isNewInstanceWithoutEnvironmentDependentFields(Value value) {
+    assert !value.hasAliasedValue();
+
+    if (value.isPhi() || !value.definition.isNewInstance()) {
+      return false;
+    }
+
+    // Find the single constructor invocation.
+    InvokeMethod constructorInvoke = null;
+    for (Instruction instruction : value.uniqueUsers()) {
+      if (!instruction.isInvokeDirect()) {
+        continue;
+      }
+
+      InvokeDirect invoke = instruction.asInvokeDirect();
+      if (!appView.dexItemFactory().isConstructor(invoke.getInvokedMethod())) {
+        continue;
+      }
+
+      if (invoke.getReceiver().getAliasedValue() != value) {
+        continue;
+      }
+
+      if (constructorInvoke == null) {
+        constructorInvoke = invoke;
+      } else {
+        // Not a single constructor invocation, give up.
+        return false;
+      }
+    }
+
+    if (constructorInvoke == null) {
+      // Didn't find a constructor invocation, give up.
+      return false;
+    }
+
+    // Check that it is a trivial initializer (otherwise, the constructor could do anything).
+    DexEncodedMethod constructor = appView.definitionFor(constructorInvoke.getInvokedMethod());
+    if (constructor == null) {
+      return false;
+    }
+
+    TrivialInitializer initializerInfo =
+        constructor.getOptimizationInfo().getTrivialInitializerInfo();
+    if (initializerInfo == null || !initializerInfo.isTrivialInstanceInitializer()) {
+      return false;
+    }
+
+    // Check that none of the arguments to the constructor depend on the environment.
+    for (int i = 1; i < constructorInvoke.arguments().size(); i++) {
+      Value argument = constructorInvoke.arguments().get(i);
+      if (valueMayDependOnEnvironment(argument)) {
+        return false;
+      }
+    }
+
+    // Finally, check that the object does not escape.
+    return !valueMayBeMutatedBeforeMethodExit(value, ImmutableSet.of(constructorInvoke));
+  }
+
   private boolean valueMayBeMutatedBeforeMethodExit(Value value, Set<Instruction> whitelist) {
     assert !value.hasAliasedValue();
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java b/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
index 48baef4..ab51239 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
@@ -25,6 +25,7 @@
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
+import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -218,14 +219,21 @@
         assert false : "Expected to be able to find the enclosing class of a method definition";
         return true;
       }
-      boolean targetMayHaveSideEffects;
+
       if (appViewWithLiveness.appInfo().noSideEffects.containsKey(target.method)) {
-        targetMayHaveSideEffects = false;
-      } else {
-        targetMayHaveSideEffects = target.getOptimizationInfo().mayHaveSideEffects();
+        return false;
       }
 
-      return targetMayHaveSideEffects;
+      MethodOptimizationInfo optimizationInfo = target.getOptimizationInfo();
+      if (target.isInstanceInitializer()) {
+        TrivialInitializer trivialInitializerInfo = optimizationInfo.getTrivialInitializerInfo();
+        if (trivialInitializerInfo != null
+            && trivialInitializerInfo.isTrivialInstanceInitializer()) {
+          return false;
+        }
+      }
+
+      return target.getOptimizationInfo().mayHaveSideEffects();
     }
 
     return true;
diff --git a/src/main/java/com/android/tools/r8/ir/code/NewInstance.java b/src/main/java/com/android/tools/r8/ir/code/NewInstance.java
index 7e6fffe..7a38882 100644
--- a/src/main/java/com/android/tools/r8/ir/code/NewInstance.java
+++ b/src/main/java/com/android/tools/r8/ir/code/NewInstance.java
@@ -11,7 +11,10 @@
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ResolutionResult;
 import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis;
 import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis.AnalysisAssumption;
 import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis.Query;
@@ -168,6 +171,18 @@
       return true;
     }
 
+    // Verify that the object does not have a finalizer.
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    ResolutionResult finalizeResolutionResult =
+        appView.appInfo().resolveMethod(clazz, dexItemFactory.objectMethods.finalize);
+    if (finalizeResolutionResult.hasSingleTarget()) {
+      DexMethod finalizeMethod = finalizeResolutionResult.asSingleTarget().method;
+      if (finalizeMethod != dexItemFactory.enumMethods.finalize
+          && finalizeMethod != dexItemFactory.objectMethods.finalize) {
+        return true;
+      }
+    }
+
     return false;
   }
 
diff --git a/src/test/examples/classmerging/ConflictingInterfaceSignaturesTest.java b/src/test/examples/classmerging/ConflictingInterfaceSignaturesTest.java
index 223ff37..7de7a1f 100644
--- a/src/test/examples/classmerging/ConflictingInterfaceSignaturesTest.java
+++ b/src/test/examples/classmerging/ConflictingInterfaceSignaturesTest.java
@@ -12,6 +12,17 @@
 
     B b = new InterfaceImpl();
     b.foo();
+
+    // Ensure that the instantiations are not dead code eliminated.
+    escape(a);
+    escape(b);
+  }
+
+  @NeverInline
+  static void escape(Object o) {
+    if (System.currentTimeMillis() < 0) {
+      System.out.println(o);
+    }
   }
 
   public interface A {
diff --git a/src/test/examples/classmerging/MethodCollisionTest.java b/src/test/examples/classmerging/MethodCollisionTest.java
index a9010a4..4456498 100644
--- a/src/test/examples/classmerging/MethodCollisionTest.java
+++ b/src/test/examples/classmerging/MethodCollisionTest.java
@@ -7,8 +7,22 @@
 public class MethodCollisionTest {
 
   public static void main(String[] args) {
-    new B().m();
-    new D().m();
+    B b = new B();
+    b.m();
+
+    D d = new D();
+    d.m();
+
+    // Ensure that the instantiations are not dead code eliminated.
+    escape(b);
+    escape(d);
+  }
+
+  @NeverInline
+  static void escape(Object o) {
+    if (System.currentTimeMillis() < 0) {
+      System.out.println(o);
+    }
   }
 
   public static class A {
diff --git a/src/test/examples/classmerging/NeverInline.java b/src/test/examples/classmerging/NeverInline.java
new file mode 100644
index 0000000..9438902
--- /dev/null
+++ b/src/test/examples/classmerging/NeverInline.java
@@ -0,0 +1,10 @@
+// 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 classmerging;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+@Target({ElementType.METHOD})
+public @interface NeverInline {}
diff --git a/src/test/examples/classmerging/SimpleInterfaceAccessTest.java b/src/test/examples/classmerging/SimpleInterfaceAccessTest.java
index ce7bf14..6fde90e 100644
--- a/src/test/examples/classmerging/SimpleInterfaceAccessTest.java
+++ b/src/test/examples/classmerging/SimpleInterfaceAccessTest.java
@@ -18,6 +18,17 @@
     // package references OtherSimpleInterface.
     OtherSimpleInterface y = new OtherSimpleInterfaceImpl();
     y.bar();
+
+    // Ensure that the instantiations are not dead code eliminated.
+    escape(x);
+    escape(y);
+  }
+
+  @NeverInline
+  static void escape(Object o) {
+    if (System.currentTimeMillis() < 0) {
+      System.out.println(o);
+    }
   }
 
   // Should only be merged into OtherSimpleInterfaceImpl if access modifications are allowed.
diff --git a/src/test/examples/classmerging/SuperCallRewritingTest.java b/src/test/examples/classmerging/SuperCallRewritingTest.java
index 7a3d45c..6f59419 100644
--- a/src/test/examples/classmerging/SuperCallRewritingTest.java
+++ b/src/test/examples/classmerging/SuperCallRewritingTest.java
@@ -7,6 +7,17 @@
 public class SuperCallRewritingTest {
   public static void main(String[] args) {
     System.out.println("Calling referencedMethod on SubClassThatReferencesSuperMethod");
-    System.out.println(new SubClassThatReferencesSuperMethod().referencedMethod());
+    SubClassThatReferencesSuperMethod obj = new SubClassThatReferencesSuperMethod();
+    System.out.println(obj.referencedMethod());
+
+    // Ensure that the instantiations are not dead code eliminated.
+    escape(obj);
+  }
+
+  @NeverInline
+  static void escape(Object o) {
+    if (System.currentTimeMillis() < 0) {
+      System.out.println(o);
+    }
   }
 }
diff --git a/src/test/examples/classmerging/SyntheticBridgeSignaturesTest.java b/src/test/examples/classmerging/SyntheticBridgeSignaturesTest.java
index a17d30a..c263e98 100644
--- a/src/test/examples/classmerging/SyntheticBridgeSignaturesTest.java
+++ b/src/test/examples/classmerging/SyntheticBridgeSignaturesTest.java
@@ -15,6 +15,17 @@
     BSub b = new BSub();
     a.m(b);
     b.m(a);
+
+    // Ensure that the instantiations are not dead code eliminated.
+    escape(a);
+    escape(b);
+  }
+
+  @NeverInline
+  static void escape(Object o) {
+    if (System.currentTimeMillis() < 0) {
+      System.out.println(o);
+    }
   }
 
   private static class A {
diff --git a/src/test/examples/classmerging/TemplateMethodTest.java b/src/test/examples/classmerging/TemplateMethodTest.java
index ac9de15..fe7de76 100644
--- a/src/test/examples/classmerging/TemplateMethodTest.java
+++ b/src/test/examples/classmerging/TemplateMethodTest.java
@@ -9,6 +9,16 @@
   public static void main(String[] args) {
     AbstractClass obj = new AbstractClassImpl();
     obj.foo();
+
+    // Ensure that the instantiations are not dead code eliminated.
+    escape(obj);
+  }
+
+  @NeverInline
+  static void escape(Object o) {
+    if (System.currentTimeMillis() < 0) {
+      System.out.println(o);
+    }
   }
 
   private abstract static class AbstractClass {
diff --git a/src/test/examples/classmerging/Test.java b/src/test/examples/classmerging/Test.java
index 6d5c51f..d3b2762 100644
--- a/src/test/examples/classmerging/Test.java
+++ b/src/test/examples/classmerging/Test.java
@@ -13,8 +13,17 @@
     ConflictingInterfaceImpl impl = new ConflictingInterfaceImpl();
     callMethodOnIface(impl);
     System.out.println(new SubClassThatReferencesSuperMethod().referencedMethod());
-    System.out.println(new Outer().getInstance().method());
+    Outer outer = new Outer();
+    Outer.SubClass inner = outer.getInstance();
+    System.out.println(outer.getInstance().method());
     System.out.println(new SubClass(42));
+
+    // Ensure that the instantiations are not dead code eliminated.
+    escape(clazz);
+    escape(iface);
+    escape(impl);
+    escape(inner);
+    escape(outer);
   }
 
   private static void callMethodOnIface(GenericInterface iface) {
@@ -31,4 +40,11 @@
     System.out.println(ClassWithConflictingMethod.conflict(null));
     System.out.println(OtherClassWithConflictingMethod.conflict(null));
   }
+
+  @NeverInline
+  static void escape(Object o) {
+    if (System.currentTimeMillis() < 0) {
+      System.out.println(o);
+    }
+  }
 }
diff --git a/src/test/examples/classmerging/keep-rules.txt b/src/test/examples/classmerging/keep-rules.txt
index c75058d..11a66c6 100644
--- a/src/test/examples/classmerging/keep-rules.txt
+++ b/src/test/examples/classmerging/keep-rules.txt
@@ -68,4 +68,8 @@
   public static void main(...);
 }
 
+-neverinline class * {
+  @classmerging.NeverInline <methods>;
+}
+
 -printmapping
diff --git a/src/test/examplesAndroidO/classmerging/MergeDefaultMethodIntoClassTest.java b/src/test/examplesAndroidO/classmerging/MergeDefaultMethodIntoClassTest.java
index 10f995d..5158228 100644
--- a/src/test/examplesAndroidO/classmerging/MergeDefaultMethodIntoClassTest.java
+++ b/src/test/examplesAndroidO/classmerging/MergeDefaultMethodIntoClassTest.java
@@ -11,6 +11,16 @@
     // invoke-interface instruction and not invoke-virtual instruction.
     A obj = new B();
     obj.f();
+
+    // Ensure that the instantiations are not dead code eliminated.
+    escape(obj);
+  }
+
+  @NeverInline
+  static void escape(Object o) {
+    if (System.currentTimeMillis() < 0) {
+      System.out.println(o);
+    }
   }
 
   public interface A {
diff --git a/src/test/examplesAndroidO/classmerging/NestedDefaultInterfaceMethodsTest.java b/src/test/examplesAndroidO/classmerging/NestedDefaultInterfaceMethodsTest.java
index 7a0e325..f237c2e 100644
--- a/src/test/examplesAndroidO/classmerging/NestedDefaultInterfaceMethodsTest.java
+++ b/src/test/examplesAndroidO/classmerging/NestedDefaultInterfaceMethodsTest.java
@@ -7,7 +7,18 @@
 public class NestedDefaultInterfaceMethodsTest {
 
   public static void main(String[] args) {
-    new C().m();
+    C obj = new C();
+    obj.m();
+
+    // Ensure that the instantiations are not dead code eliminated.
+    escape(obj);
+  }
+
+  @NeverInline
+  static void escape(Object o) {
+    if (System.currentTimeMillis() < 0) {
+      System.out.println(o);
+    }
   }
 
   public interface A {
diff --git a/src/test/examplesAndroidO/classmerging/NeverInline.java b/src/test/examplesAndroidO/classmerging/NeverInline.java
new file mode 100644
index 0000000..9438902
--- /dev/null
+++ b/src/test/examplesAndroidO/classmerging/NeverInline.java
@@ -0,0 +1,10 @@
+// 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 classmerging;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+@Target({ElementType.METHOD})
+public @interface NeverInline {}
diff --git a/src/test/examplesAndroidO/classmerging/keep-rules.txt b/src/test/examplesAndroidO/classmerging/keep-rules.txt
index 4df182e..18a133d 100644
--- a/src/test/examplesAndroidO/classmerging/keep-rules.txt
+++ b/src/test/examplesAndroidO/classmerging/keep-rules.txt
@@ -14,5 +14,9 @@
   public static void main(...);
 }
 
+-neverinline class * {
+  @classmerging.NeverInline <methods>;
+}
+
 # TODO(herhut): Consider supporting merging of inner-class attributes.
 # -keepattributes *
\ No newline at end of file
diff --git a/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerTest.java b/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerTest.java
index 6b4f17d..82385a2 100644
--- a/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerTest.java
@@ -10,8 +10,6 @@
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.OutputMode;
@@ -78,7 +76,6 @@
       .resolve("classmerging").resolve("keep-rules-dontoptimize.txt");
 
   private void configure(InternalOptions options) {
-    options.enableClassInlining = false;
     options.enableSideEffectAnalysis = false;
     options.enableUnusedInterfaceRemoval = false;
     options.testing.nondeterministicCycleElimination = true;
@@ -90,6 +87,7 @@
         testForR8(Backend.DEX)
             .addProgramFiles(EXAMPLE_JAR)
             .addKeepRuleFiles(proguardConfig)
+            .enableProguardTestOptions()
             .noMinification()
             .addOptionsModification(optionsConsumer)
             .compile()
@@ -110,18 +108,18 @@
     runR8(EXAMPLE_KEEP, this::configure);
     // GenericInterface should be merged into GenericInterfaceImpl.
     for (String candidate : CAN_BE_MERGED) {
-      assertFalse(inspector.clazz(candidate).isPresent());
+      assertThat(inspector.clazz(candidate), not(isPresent()));
     }
-    assertTrue(inspector.clazz("classmerging.GenericInterfaceImpl").isPresent());
-    assertTrue(inspector.clazz("classmerging.Outer$SubClass").isPresent());
-    assertTrue(inspector.clazz("classmerging.SubClass").isPresent());
+    assertThat(inspector.clazz("classmerging.GenericInterfaceImpl"), isPresent());
+    assertThat(inspector.clazz("classmerging.Outer$SubClass"), isPresent());
+    assertThat(inspector.clazz("classmerging.SubClass"), isPresent());
   }
 
   @Test
   public void testClassesHaveNotBeenMerged() throws Throwable {
     runR8(DONT_OPTIMIZE, null);
     for (String candidate : CAN_BE_MERGED) {
-      assertTrue(inspector.clazz(candidate).isPresent());
+      assertThat(inspector.clazz(candidate), isPresent());
     }
   }
 
@@ -304,8 +302,8 @@
   @Test
   public void testConflictWasDetected() throws Throwable {
     runR8(EXAMPLE_KEEP, this::configure);
-    assertTrue(inspector.clazz("classmerging.ConflictingInterface").isPresent());
-    assertTrue(inspector.clazz("classmerging.ConflictingInterfaceImpl").isPresent());
+    assertThat(inspector.clazz("classmerging.ConflictingInterface"), isPresent());
+    assertThat(inspector.clazz("classmerging.ConflictingInterfaceImpl"), isPresent());
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/SingletonClassInitializerPatternCanBePostponedTest.java b/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/SingletonClassInitializerPatternCanBePostponedTest.java
new file mode 100644
index 0000000..7813a2d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/SingletonClassInitializerPatternCanBePostponedTest.java
@@ -0,0 +1,86 @@
+// 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.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;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+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 SingletonClassInitializerPatternCanBePostponedTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public SingletonClassInitializerPatternCanBePostponedTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(SingletonClassInitializerPatternCanBePostponedTest.class)
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("Hello world!");
+  }
+
+  private void inspect(CodeInspector inspector) {
+    ClassSubject classSubject = inspector.clazz(A.class);
+    assertThat(classSubject, isPresent());
+
+    // A.inlineable() should be inlined because we should be able to determine that A.<clinit>() can
+    // safely be postponed.
+    assertThat(classSubject.uniqueMethodWithName("inlineable"), not(isPresent()));
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      A.inlineable();
+      System.out.println(A.getInstance().getMessage());
+    }
+  }
+
+  static class A {
+
+    static A INSTANCE = new A(" world!");
+
+    final String message;
+
+    A(String message) {
+      this.message = message;
+    }
+
+    static void inlineable() {
+      System.out.print("Hello");
+    }
+
+    static A getInstance() {
+      return INSTANCE;
+    }
+
+    String getMessage() {
+      return message;
+    }
+  }
+}
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
new file mode 100644
index 0000000..d29753b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/SingletonClassInitializerPatternCannotBePostponedTest.java
@@ -0,0 +1,92 @@
+// 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.analysis.sideeffect;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+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.ClassSubject;
+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 SingletonClassInitializerPatternCannotBePostponedTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public SingletonClassInitializerPatternCannotBePostponedTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(SingletonClassInitializerPatternCannotBePostponedTest.class)
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("Hello world!");
+  }
+
+  private void inspect(CodeInspector inspector) {
+    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());
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      A.inlineable();
+      System.out.println(A.getInstance().getMessage());
+    }
+  }
+
+  static class A {
+
+    static B INSTANCE = new B("world!");
+
+    static void inlineable() {
+      System.out.print(" ");
+    }
+
+    static B getInstance() {
+      return INSTANCE;
+    }
+  }
+
+  static class B {
+
+    static {
+      System.out.print("Hello");
+    }
+
+    final String message;
+
+    B(String message) {
+      this.message = message;
+    }
+
+    String getMessage() {
+      return message;
+    }
+  }
+}
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 fbcc6a1..afdfabb 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
@@ -64,7 +64,8 @@
 
 @RunWith(Parameterized.class)
 public class ClassInlinerTest extends TestBase {
-  private Backend backend;
+
+  private final Backend backend;
 
   @Parameterized.Parameters(name = "Backend: {0}")
   public static Backend[] data() {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/EscapingBuilderTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/EscapingBuilderTest.java
index 4dd3551..bf5e92a 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/EscapingBuilderTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/EscapingBuilderTest.java
@@ -74,7 +74,11 @@
     }
 
     @NeverInline
-    public static void escape(Object o) {}
+    static void escape(Object o) {
+      if (System.currentTimeMillis() < 0) {
+        System.out.println(o);
+      }
+    }
   }
 
   // Simple builder that should be class inlined.
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
index b0828fe..f0c6a78 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
@@ -167,6 +167,7 @@
         testForR8(parameters.getBackend())
             .addProgramClasses(classes)
             .enableInliningAnnotations()
+            .enableSideEffectAnnotations()
             .addKeepMainRule(main)
             .allowAccessModification()
             .noMinification()
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/MoveToHostFieldOnlyTestClass.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/MoveToHostFieldOnlyTestClass.java
index f725d21..eb8e6a4 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/MoveToHostFieldOnlyTestClass.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/MoveToHostFieldOnlyTestClass.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.optimize.staticizer.movetohost;
 
+import com.android.tools.r8.AssumeMayHaveSideEffects;
 import com.android.tools.r8.NeverInline;
 
 public class MoveToHostFieldOnlyTestClass {
@@ -12,6 +13,7 @@
     test.testOk_fieldOnly();
   }
 
+  @AssumeMayHaveSideEffects
   @NeverInline
   private void testOk_fieldOnly() {
     // Any instance method call whose target holder is not the candidate will invalidate candidacy,
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java
index f0a8a84..cfeb611 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java
@@ -291,30 +291,18 @@
   @Test
   public void testAccessor() throws Exception {
     TestKotlinCompanionClass testedClass = ACCESSOR_COMPANION_PROPERTY_CLASS;
-    String mainClass = addMainToClasspath("accessors.AccessorKt",
-        "accessor_accessPropertyFromCompanionClass");
+    String mainClass =
+        addMainToClasspath("accessors.AccessorKt", "accessor_accessPropertyFromCompanionClass");
     runTest(
         "accessors",
         mainClass,
         disableClassStaticizer,
-        (app) -> {
+        app -> {
+          // The classes are removed entirely as a result of member value propagation, inlining, and
+          // the fact that the classes do not have observable side effects.
           CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject outerClass =
-              checkClassIsKept(codeInspector, testedClass.getOuterClassName());
-          ClassSubject companionClass = checkClassIsKept(codeInspector, testedClass.getClassName());
-
-          // Property field has been removed due to member value propagation.
-          String propertyName = "property";
-          checkFieldIsRemoved(outerClass, JAVA_LANG_STRING, propertyName);
-
-          // The getter is always inlined since it just calls into the accessor.
-          MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
-          checkMethodIsAbsent(companionClass, getter);
-
-          // The accessor is also inlined.
-          MemberNaming.MethodSignature getterAccessor =
-              testedClass.getGetterAccessorForProperty(propertyName, AccessorKind.FROM_COMPANION);
-          checkMethodIsRemoved(outerClass, getterAccessor);
+          checkClassIsRemoved(codeInspector, testedClass.getOuterClassName());
+          checkClassIsRemoved(codeInspector, testedClass.getClassName());
         });
   }