Add tests for string-switch conversion
Bug: 135559645
Change-Id: I579c3a9f6d7aa9e1508099d25a90c73e2d1062bd
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 43a2815..ab5a2dd 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -780,6 +780,7 @@
public final DexMethod compareTo;
public final DexMethod compareToIgnoreCase;
+ public final DexMethod hashCode;
public final DexMethod valueOf;
public final DexMethod toString;
public final DexMethod intern;
@@ -822,6 +823,7 @@
createMethod(stringDescriptor, compareToIgnoreCaseMethodName, intDescriptor,
needsOneString);
+ hashCode = createMethod(stringType, createProto(intType), "hashCode");
valueOf = createMethod(
stringDescriptor, valueOfMethodName, stringDescriptor, needsOneObject);
toString = createMethod(
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index 6d2464f..b9c7a67 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -779,6 +779,10 @@
return null;
}
+ public boolean isStringSwitch() {
+ return false;
+ }
+
public boolean isInstanceGet() {
return false;
}
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 7e41a90..0e1f2c1 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -204,6 +204,7 @@
// the actual catch handler allowed when inlining. Threshold found empirically by testing on
// GMS Core.
public int inliningControlFlowResolutionBlocksThreshold = 15;
+ public boolean enableStringSwitchConversion = false;
public boolean enableSwitchMapRemoval = true;
public final OutlineOptions outline = new OutlineOptions();
public boolean enableValuePropagation = true;
diff --git a/src/test/java/com/android/tools/r8/ir/conversion/StringSwitchConversionFromIfTest.java b/src/test/java/com/android/tools/r8/ir/conversion/StringSwitchConversionFromIfTest.java
new file mode 100644
index 0000000..dae544a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/conversion/StringSwitchConversionFromIfTest.java
@@ -0,0 +1,253 @@
+// 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.conversion;
+
+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.assertNotEquals;
+import static org.junit.Assume.assumeFalse;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.Reporter;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Streams;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class StringSwitchConversionFromIfTest extends TestBase {
+
+ private final boolean enableStringSwitchConversion;
+ private final TestParameters parameters;
+
+ @Parameterized.Parameters(name = "{1}, enable string switch conversion: {0}")
+ public static List<Object[]> data() {
+ return buildParameters(BooleanUtils.values(), getTestParameters().withAllRuntimes().build());
+ }
+
+ public StringSwitchConversionFromIfTest(
+ boolean enableStringSwitchConversion, TestParameters parameters) {
+ this.enableStringSwitchConversion = enableStringSwitchConversion;
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void test() throws Exception {
+ // TODO(b/135559645): Add support for identifying string-switch instructions in the generated
+ // bytecode.
+ assumeFalse(enableStringSwitchConversion);
+
+ testForR8(parameters.getBackend())
+ .addInnerClasses(StringSwitchConversionFromIfTest.class)
+ .addKeepMainRule(TestClass.class)
+ .addOptionsModification(
+ options -> {
+ assert !options.enableStringSwitchConversion;
+ options.enableStringSwitchConversion = enableStringSwitchConversion;
+ })
+ .enableInliningAnnotations()
+ // TODO(b/135560746): Add support for treating the keys of a string-switch instruction as an
+ // identifier name string.
+ .noMinification()
+ .setMinApi(parameters.getRuntime())
+ .compile()
+ .inspect(
+ inspector -> {
+ ClassSubject classSubject = inspector.clazz(TestClass.class);
+ assertThat(classSubject, isPresent());
+
+ List<String> methodNames =
+ ImmutableList.of(
+ "test",
+ "testWithMultipleHashCodeInvocations",
+ "testWithSwitch",
+ "testNullCheckIsPreserved");
+ for (String methodName : methodNames) {
+ MethodSubject methodSubject = classSubject.uniqueMethodWithName(methodName);
+ assertThat(methodSubject, isPresent());
+
+ DexItemFactory dexItemFactory = new DexItemFactory();
+ InternalOptions options = new InternalOptions(dexItemFactory, new Reporter());
+ options.enableStringSwitchConversion = enableStringSwitchConversion;
+
+ IRCode code = methodSubject.buildIR(options);
+ List<Value> hashCodeValues =
+ Streams.stream(code.instructions())
+ .filter(instruction -> isInvokeStringHashCode(instruction, dexItemFactory))
+ .map(Instruction::asInvokeVirtual)
+ .map(Instruction::outValue)
+ .collect(Collectors.toList());
+
+ // There should be at least one call to `int String.hashCode()`, since the
+ // instruction may throw. This holds even if the string-switch instruction is
+ // compiled to a sequence of `if (x.equals("..."))` instructions that do not even
+ // use the hash code.
+ assertNotEquals(0, hashCodeValues.size());
+
+ // We indirectly verify that the string-switch instructions have been identified
+ // by checking that none of the hash codes are used for anything. Note that this
+ // only holds with a backend that compiles string-switch instruction to
+ // `if (x.equals("..."))` instructions that do not use the hash code for anything.
+ assertEquals(
+ methodName,
+ enableStringSwitchConversion,
+ hashCodeValues.stream().filter(Objects::nonNull).noneMatch(Value::isUsed));
+
+ // Check that the IR has exactly one StringSwitch instruction iff string-switch
+ // conversion is enabled.
+ long numberOfStringSwitchInstructions =
+ Streams.stream(code.instructions()).filter(Instruction::isStringSwitch).count();
+ assertEquals(enableStringSwitchConversion, numberOfStringSwitchInstructions == 1);
+ }
+ })
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutputLines("Hello world!", "Caught NPE");
+ }
+
+ @Test
+ public void testCorrectnessOfConstants() {
+ assertEquals(A.class.getTypeName(), TestClass.NAME_1);
+ assertEquals(A.class.getTypeName().hashCode(), TestClass.HASH_1);
+ assertEquals(B.class.getTypeName(), TestClass.NAME_2);
+ assertEquals(B.class.getTypeName().hashCode(), TestClass.HASH_2);
+ }
+
+ private static boolean isInvokeStringHashCode(
+ Instruction instruction, DexItemFactory dexItemFactory) {
+ // Intentionally using toSourceString() because `instruction.getInvokedMethod()` belongs to
+ // another factory than the given `dexItemFactory`.
+ String signature = dexItemFactory.stringMethods.hashCode.toSourceString();
+ return instruction.isInvokeVirtual()
+ && instruction.asInvokeVirtual().getInvokedMethod().toSourceString().equals(signature);
+ }
+
+ static class TestClass {
+
+ private static final String NAME_1 =
+ "com.android.tools.r8.ir.conversion.StringSwitchConversionFromIfTest$A";
+ private static final int HASH_1 = -1340982899;
+
+ private static final String NAME_2 =
+ "com.android.tools.r8.ir.conversion.StringSwitchConversionFromIfTest$B";
+ private static final int HASH_2 = -1340982898;
+
+ public static void main(String[] args) {
+ test(A.class);
+ test(B.class);
+ testWithMultipleHashCodeInvocations(A.class);
+ testWithMultipleHashCodeInvocations(B.class);
+ testWithSwitch(A.class);
+ testWithSwitch(B.class);
+
+ try {
+ testNullCheckIsPreserved(System.currentTimeMillis() >= 0 ? null : A.class);
+ } catch (NullPointerException e) {
+ System.out.println("Caught NPE");
+ }
+ }
+
+ @NeverInline
+ private static void test(Class<?> clazz) {
+ String className = clazz.getName();
+ int hashCode = className.hashCode();
+ int result = -1;
+ if (hashCode == HASH_1) {
+ if (className.equals(NAME_1)) {
+ result = 0;
+ }
+ } else if (hashCode == HASH_2) {
+ if (className.equals(NAME_2)) {
+ result = 1;
+ }
+ }
+ if (result == 0) {
+ System.out.print("He");
+ } else if (result == 1) {
+ System.out.print("l");
+ }
+ }
+
+ @NeverInline
+ private static void testWithMultipleHashCodeInvocations(Class<?> clazz) {
+ String className = clazz.getName();
+ int result = -1;
+ if (className.hashCode() == HASH_1) {
+ if (className.equals(NAME_1)) {
+ result = 0;
+ }
+ } else if (className.hashCode() == HASH_2) {
+ if (className.equals(NAME_2)) {
+ result = 1;
+ }
+ }
+ if (result == 0) {
+ System.out.print("l");
+ } else if (result == 1) {
+ System.out.print("o");
+ }
+ }
+
+ @NeverInline
+ private static void testWithSwitch(Class<?> clazz) {
+ String className = clazz.getName();
+ int result = -1;
+ if (className.hashCode() == HASH_1) {
+ if (className.equals(NAME_1)) {
+ result = 0;
+ }
+ } else if (className.hashCode() == HASH_2) {
+ if (className.equals(NAME_2)) {
+ result = 1;
+ }
+ }
+ switch (result) {
+ case 0:
+ System.out.print(" world");
+ break;
+ case 1:
+ System.out.println("!");
+ break;
+ default:
+ // Ignore.
+ }
+ }
+
+ @NeverInline
+ private static void testNullCheckIsPreserved(Class<?> clazz) {
+ switch (clazz.getName()) {
+ case "A":
+ System.out.println("Unexpected (A)");
+ break;
+ case "B":
+ System.out.println("Unexpected (B)");
+ break;
+ case "C":
+ System.out.println("Unexpected (C)");
+ break;
+ default:
+ System.out.println("Unexpected (default)");
+ }
+ }
+ }
+
+ static class A {}
+
+ static class B {}
+}
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentMethodSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentMethodSubject.java
index 860c36f..b3e5eec 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentMethodSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentMethodSubject.java
@@ -6,15 +6,15 @@
import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.naming.MemberNaming.MethodSignature;
import com.android.tools.r8.naming.MemberNaming.Signature;
+import com.android.tools.r8.utils.InternalOptions;
public class AbsentMethodSubject extends MethodSubject {
@Override
- public IRCode buildIR(DexItemFactory dexItemFactory) {
+ public IRCode buildIR(InternalOptions options) {
throw new Unreachable("Cannot build IR for an absent method");
}
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundMethodSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundMethodSubject.java
index 6b47139..190ec4f 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundMethodSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundMethodSubject.java
@@ -21,7 +21,6 @@
import com.android.tools.r8.graph.DexDebugInfo;
import com.android.tools.r8.graph.DexDebugPositionState;
import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexString;
import com.android.tools.r8.graph.JarCode;
import com.android.tools.r8.ir.code.IRCode;
@@ -30,7 +29,6 @@
import com.android.tools.r8.naming.signature.GenericSignatureParser;
import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.Reporter;
import com.android.tools.r8.utils.codeinspector.LocalVariableTable.LocalVariableTableEntry;
import com.google.common.collect.ImmutableList;
import it.unimi.dsi.fastutil.objects.Reference2IntMap;
@@ -53,10 +51,8 @@
}
@Override
- public IRCode buildIR(DexItemFactory dexItemFactory) {
- InternalOptions options = new InternalOptions(dexItemFactory, new Reporter());
+ public IRCode buildIR(InternalOptions options) {
options.programConsumer = DexIndexedConsumer.emptyConsumer();
-
DexEncodedMethod method = getMethod();
return method
.getCode()
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/MethodSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/MethodSubject.java
index 0dc81f1..4fe5fb0 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/MethodSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/MethodSubject.java
@@ -8,6 +8,8 @@
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.Reporter;
import com.google.common.collect.Streams;
import java.util.Iterator;
import java.util.function.Predicate;
@@ -19,7 +21,11 @@
return buildIR(new DexItemFactory());
}
- public abstract IRCode buildIR(DexItemFactory dexItemFactory);
+ public IRCode buildIR(DexItemFactory dexItemFactory) {
+ return buildIR(new InternalOptions(dexItemFactory, new Reporter()));
+ }
+
+ public abstract IRCode buildIR(InternalOptions options);
public final boolean isAbsent() {
return !isPresent();