Relax dead condition of array-length.
by implementing instructionMayHaveSideEffects:
as long as the given array is known to be non-null, array-length is
free from side effects---NPE.
It is not removable yet, though, because non-null IR, which informs
the nullability of array, is wiped out before dead code removal.
Bug: 129530569, 120920488
Change-Id: I75226415200271149f01f0dfa97b038aa3f32739
diff --git a/src/main/java/com/android/tools/r8/ir/code/ArrayLength.java b/src/main/java/com/android/tools/r8/ir/code/ArrayLength.java
index 6e6a9f8..9de9b6e 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ArrayLength.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ArrayLength.java
@@ -11,6 +11,7 @@
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.AbstractError;
import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
import com.android.tools.r8.ir.conversion.CfBuilder;
import com.android.tools.r8.ir.conversion.DexBuilder;
@@ -70,6 +71,27 @@
}
@Override
+ public AbstractError instructionInstanceCanThrow(
+ AppView<? extends AppInfo> appView, DexType context) {
+ if (array().typeLattice.isNullable()) {
+ return AbstractError.specific(appView.dexItemFactory().npeType);
+ }
+
+ return AbstractError.bottom();
+ }
+
+ @Override
+ public boolean instructionMayHaveSideEffects(
+ AppView<? extends AppInfo> appView, DexType context) {
+ return instructionInstanceCanThrow(appView, context).isThrowing();
+ }
+
+ @Override
+ public boolean canBeDeadCode(AppView<? extends AppInfo> appView, IRCode code) {
+ return !instructionMayHaveSideEffects(appView, code.method.method.holder);
+ }
+
+ @Override
public boolean identicalAfterRegisterAllocation(Instruction other, RegisterAllocator allocator) {
if (super.identicalAfterRegisterAllocation(other, allocator)) {
// The array length instruction doesn't carry the element type. The art verifier doesn't
diff --git a/src/test/java/com/android/tools/r8/naming/arraytypes/ArrayTypesTest.java b/src/test/java/com/android/tools/r8/naming/arraytypes/ArrayTypesTest.java
index 5315868..adfbcec 100644
--- a/src/test/java/com/android/tools/r8/naming/arraytypes/ArrayTypesTest.java
+++ b/src/test/java/com/android/tools/r8/naming/arraytypes/ArrayTypesTest.java
@@ -4,8 +4,11 @@
package com.android.tools.r8.naming.arraytypes;
+import static org.junit.Assume.assumeTrue;
+
import com.android.tools.r8.TestBase;
-import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.FileUtils;
import com.android.tools.r8.utils.StringUtils;
@@ -24,7 +27,7 @@
@RunWith(Parameterized.class)
public class ArrayTypesTest extends TestBase {
- private final Backend backend;
+ private final TestParameters parameters;
private static String packageName;
private static String arrayBaseTypeDescriptor;
@@ -32,13 +35,13 @@
private static String generatedTestClassName;
private static String expectedOutput;
- public ArrayTypesTest(Backend backend) {
- this.backend = backend;
+ public ArrayTypesTest(TestParameters parameters) {
+ this.parameters = parameters;
}
- @Parameterized.Parameters(name = "Backend: {0}")
- public static Object[] data() {
- return ToolHelper.getBackends();
+ @Parameterized.Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimes().build();
}
@BeforeClass
@@ -66,13 +69,14 @@
}
private void runR8Test(boolean enableMinification) throws Exception {
- testForR8(backend)
+ testForR8(parameters.getBackend())
.minification(enableMinification)
.addProgramClasses(Main.class, A.class)
.addProgramClassFileData(generateTestClass())
.addKeepMainRule(Main.class)
.addKeepRules("-keep class " + generatedTestClassName + " { test(...); }")
- .run(Main.class)
+ .setMinApi(parameters.getRuntime())
+ .run(parameters.getRuntime(), Main.class)
.assertSuccessWithOutput(expectedOutput);
}
@@ -95,27 +99,28 @@
StringUtils.lines(
A.class.getTypeName() + " -> " + packageName + ".a:"));
- testForR8(backend)
+ testForR8(parameters.getBackend())
.addProgramClasses(Main.class, A.class)
.addProgramClassFileData(generateTestClass())
.addKeepMainRule(Main.class)
.addKeepRules("-applymapping " + mappingFile.toAbsolutePath())
.noMinification()
.noTreeShaking()
- .run(Main.class)
+ .setMinApi(parameters.getRuntime())
+ .run(parameters.getRuntime(), Main.class)
.assertSuccessWithOutput(expectedOutput);
}
@Test
public void testD8() throws Exception {
- if (backend == Backend.DEX) {
- testForD8()
- .addProgramClasses(Main.class, A.class)
- .addProgramClassFileData(generateTestClass())
- .run(Main.class)
- .writeProcessResult(System.out)
- .assertSuccessWithOutput(expectedOutput);
- }
+ assumeTrue(parameters.isDexRuntime());
+ testForD8()
+ .addProgramClasses(Main.class, A.class)
+ .addProgramClassFileData(generateTestClass())
+ .setMinApi(parameters.getRuntime())
+ .run(parameters.getRuntime(), Main.class)
+ .writeProcessResult(System.out)
+ .assertSuccessWithOutput(expectedOutput);
}
public static byte[] generateTestClass() {
diff --git a/src/test/java/com/android/tools/r8/shaking/array/DeadArrayLengthTest.java b/src/test/java/com/android/tools/r8/shaking/array/DeadArrayLengthTest.java
new file mode 100644
index 0000000..4b24afc
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/array/DeadArrayLengthTest.java
@@ -0,0 +1,131 @@
+// 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.shaking.array;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.NeverPropagateValue;
+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.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.Streams;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class DeadArrayLengthTest extends TestBase {
+ private static final Class<?> MAIN = TestClass.class;
+ private static final String EXPECTED_OUTPUT = StringUtils.lines("1", "Expected NPE", "3");
+
+ private final TestParameters parameters;
+
+ @Parameterized.Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimes().build();
+ }
+
+ public DeadArrayLengthTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ private void inspect(CodeInspector inspector) {
+ ClassSubject main = inspector.clazz(MAIN);
+ assertThat(main, isPresent());
+
+ MethodSubject nonNull = main.uniqueMethodWithName("clearlyNonNull");
+ assertThat(nonNull, isPresent());
+ assertEquals(
+ 0,
+ Streams.stream(nonNull.iterateInstructions(InstructionSubject::isArrayLength)).count());
+
+ MethodSubject nullable = main.uniqueMethodWithName("isNullable");
+ assertThat(nullable, isPresent());
+ assertEquals(
+ 1,
+ Streams.stream(nullable.iterateInstructions(InstructionSubject::isArrayLength)).count());
+
+ MethodSubject nullCheck = main.uniqueMethodWithName("afterNullCheck");
+ assertThat(nullCheck, isPresent());
+ // TODO(b/120920488): could be zero if we extend non-null to first dead code removal.
+ assertEquals(
+ 1,
+ Streams.stream(nullCheck.iterateInstructions(InstructionSubject::isArrayLength)).count());
+ }
+
+ @Test
+ public void testD8() throws Exception {
+ assumeTrue(parameters.isDexRuntime());
+ testForD8()
+ .release()
+ .addProgramClasses(MAIN)
+ .setMinApi(parameters.getRuntime())
+ .run(parameters.getRuntime(), MAIN)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT)
+ .inspect(this::inspect);
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ testForR8(parameters.getBackend())
+ .addProgramClasses(MAIN)
+ .addKeepClassAndMembersRules(MAIN)
+ .enableMemberValuePropagationAnnotations()
+ .noMinification()
+ .setMinApi(parameters.getRuntime())
+ .run(parameters.getRuntime(), MAIN)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT)
+ .inspect(this::inspect);
+ }
+
+ static class TestClass {
+ @NeverPropagateValue
+ static int clearlyNonNull(int size) {
+ int[] array = new int[size];
+ int l = array.length;
+ return System.currentTimeMillis() > 0 ? 1 : -1;
+ }
+
+ @NeverPropagateValue
+ static int isNullable(int[] args) {
+ // Not used, but can't be dead due to the potential NPE.
+ int l = args.length;
+ return System.currentTimeMillis() > 0 ? 2 : -2;
+ }
+
+ @NeverPropagateValue
+ static int afterNullCheck(int[] args) {
+ if (args != null) {
+ // Can be removed.
+ int l = args.length;
+ return System.currentTimeMillis() > 0 ? 8 : -8;
+ }
+ return System.currentTimeMillis() > 0 ? 3 : -3;
+ }
+
+ public static void main(String[] args) {
+ System.out.println(clearlyNonNull(args.length));
+ try {
+ System.out.println(isNullable(null));
+ fail("Expect to see NPE");
+ } catch (NullPointerException npe) {
+ System.out.println("Expected NPE");
+ }
+ try {
+ System.out.println(afterNullCheck(null));
+ } catch (NullPointerException npe) {
+ fail("Not expect to see NPE");
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java
index ded4f14..1223b61 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java
@@ -6,6 +6,7 @@
import com.android.tools.r8.cf.code.CfArithmeticBinop;
+import com.android.tools.r8.cf.code.CfArrayLength;
import com.android.tools.r8.cf.code.CfArrayStore;
import com.android.tools.r8.cf.code.CfCheckCast;
import com.android.tools.r8.cf.code.CfConstClass;
@@ -306,6 +307,11 @@
}
@Override
+ public boolean isArrayLength() {
+ return instruction instanceof CfArrayLength;
+ }
+
+ @Override
public boolean isArrayPut() {
return instruction instanceof CfArrayStore;
}
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java
index 3b04382..c195dc0 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java
@@ -11,6 +11,7 @@
import com.android.tools.r8.code.AputObject;
import com.android.tools.r8.code.AputShort;
import com.android.tools.r8.code.AputWide;
+import com.android.tools.r8.code.ArrayLength;
import com.android.tools.r8.code.CheckCast;
import com.android.tools.r8.code.Const;
import com.android.tools.r8.code.Const16;
@@ -395,6 +396,11 @@
}
@Override
+ public boolean isArrayLength() {
+ return instruction instanceof ArrayLength;
+ }
+
+ @Override
public boolean isArrayPut() {
return instruction instanceof Aput
|| instruction instanceof AputBoolean
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java
index d297a3b..43ef5d4 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java
@@ -90,6 +90,8 @@
boolean isNewArray();
+ boolean isArrayLength();
+
boolean isArrayPut();
boolean isMonitorEnter();