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());
});
}