Use Enum#valueOf(String) in type switch
- This creates an edge from enumEq methods to
the clinit of the corresponding enum.
Change-Id: I271afccdd11129c1e255e452ef5975e534b96c1a
diff --git a/src/main/java/com/android/tools/r8/graph/FieldAccessFlags.java b/src/main/java/com/android/tools/r8/graph/FieldAccessFlags.java
index cc2e10c..a3581c9 100644
--- a/src/main/java/com/android/tools/r8/graph/FieldAccessFlags.java
+++ b/src/main/java/com/android/tools/r8/graph/FieldAccessFlags.java
@@ -137,6 +137,10 @@
set(Constants.ACC_ENUM);
}
+ public void unsetEnum() {
+ unset(Constants.ACC_ENUM);
+ }
+
public static class Builder extends BuilderBase<Builder, FieldAccessFlags> {
public Builder() {
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/typeswitch/SwitchHelperGenerator.java b/src/main/java/com/android/tools/r8/ir/desugar/typeswitch/SwitchHelperGenerator.java
index 72098d9..5010220 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/typeswitch/SwitchHelperGenerator.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/typeswitch/SwitchHelperGenerator.java
@@ -6,9 +6,9 @@
import static com.android.tools.r8.ir.synthetic.TypeSwitchSyntheticCfCodeProvider.allowsInlinedIntegerEquality;
-import com.android.tools.r8.cf.code.CfConstClass;
import com.android.tools.r8.cf.code.CfConstNumber;
import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfInvoke;
import com.android.tools.r8.cf.code.CfNewArray;
import com.android.tools.r8.cf.code.CfReturnVoid;
import com.android.tools.r8.cf.code.CfStaticFieldWrite;
@@ -146,7 +146,21 @@
CfCode cfCode = TypeSwitchMethods.TypeSwitchMethods_switchEnumEq(factory, methodSig);
List<CfInstruction> newInstructions =
ListUtils.map(
- cfCode.getInstructions(), i -> i.isConstClass() ? new CfConstClass(enumType) : i);
+ cfCode.getInstructions(),
+ i -> {
+ if (i.isInvokeStatic()) {
+ CfInvoke invoke = i.asInvoke();
+ if (invoke.getMethod().getName().isIdenticalTo(factory.valueOfMethodName)) {
+ DexMethod newMethod =
+ factory.createMethod(
+ enumType,
+ factory.createProto(enumType, factory.stringType),
+ factory.valueOfMethodName);
+ return new CfInvoke(invoke.getOpcode(), newMethod, invoke.isInterface());
+ }
+ }
+ return i;
+ });
cfCode.setInstructions(newInstructions);
return cfCode;
},
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/typeswitch/TypeSwitchIRRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/typeswitch/TypeSwitchIRRewriter.java
index ea11bfe..3727b03 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/typeswitch/TypeSwitchIRRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/typeswitch/TypeSwitchIRRewriter.java
@@ -4,7 +4,6 @@
package com.android.tools.r8.ir.desugar.typeswitch;
-import com.android.tools.r8.dex.code.CfOrDexInstruction;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DefaultUseRegistryWithResult;
import com.android.tools.r8.graph.DexField;
@@ -35,7 +34,6 @@
import com.android.tools.r8.utils.collections.BidirectionalManyToOneMap;
import com.google.common.collect.ImmutableList;
import java.util.IdentityHashMap;
-import java.util.ListIterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -77,12 +75,13 @@
UseRegistryWithResult<DexType, ProgramMethod> registry =
new DefaultUseRegistryWithResult<>(appView, uniqueStaticMethod) {
@Override
- public void registerConstClass(
- DexType type,
- ListIterator<? extends CfOrDexInstruction> iterator,
- boolean ignoreCompatRules) {
+ public void registerInvokeStatic(DexMethod method) {
assert getResult() == null;
- setResult(type);
+ if (method.getName().isIdenticalTo(dexItemFactory().valueOfMethodName)
+ && method.getArity() == 1
+ && method.getParameter(0).isIdenticalTo(dexItemFactory().stringType)) {
+ setResult(method.getHolderType());
+ }
}
};
DexType enumType = uniqueStaticMethod.registerCodeReferencesWithResult(registry);
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/typeswitch/TypeSwitchMethods.java b/src/main/java/com/android/tools/r8/ir/desugar/typeswitch/TypeSwitchMethods.java
index 4bb8026..08611d3 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/typeswitch/TypeSwitchMethods.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/typeswitch/TypeSwitchMethods.java
@@ -11,7 +11,6 @@
import com.android.tools.r8.cf.code.CfArrayLoad;
import com.android.tools.r8.cf.code.CfArrayStore;
import com.android.tools.r8.cf.code.CfCheckCast;
-import com.android.tools.r8.cf.code.CfConstClass;
import com.android.tools.r8.cf.code.CfConstNull;
import com.android.tools.r8.cf.code.CfConstNumber;
import com.android.tools.r8.cf.code.CfFrame;
@@ -42,7 +41,8 @@
public final class TypeSwitchMethods {
public static void registerSynthesizedCodeReferences(DexItemFactory factory) {
- factory.createSynthesizedType("Ljava/lang/Enum;");
+ factory.createSynthesizedType(
+ "Lcom/android/tools/r8/cfmethodgeneration/TypeSwitchMethods$Enum;");
factory.createSynthesizedType("Ljava/lang/Number;");
factory.createSynthesizedType("[Ljava/lang/Object;");
}
@@ -60,66 +60,36 @@
CfLabel label9 = new CfLabel();
CfLabel label10 = new CfLabel();
CfLabel label11 = new CfLabel();
- CfLabel label12 = new CfLabel();
- CfLabel label13 = new CfLabel();
- CfLabel label14 = new CfLabel();
return new CfCode(
method.holder,
4,
- 7,
+ 6,
ImmutableList.of(
label0,
new CfLoad(ValueType.OBJECT, 1),
new CfLoad(ValueType.INT, 2),
new CfArrayLoad(MemberType.OBJECT),
- new CfIf(IfType.NE, ValueType.OBJECT, label11),
+ new CfIf(IfType.NE, ValueType.OBJECT, label8),
label1,
new CfConstNull(),
new CfStore(ValueType.OBJECT, 4),
label2,
- new CfConstClass(factory.createType("Ljava/lang/Enum;")),
- new CfStore(ValueType.OBJECT, 5),
- label3,
- new CfLoad(ValueType.OBJECT, 5),
- new CfInvoke(
- 182,
- factory.createMethod(
- factory.classType,
- factory.createProto(factory.booleanType),
- factory.createString("isEnum")),
- false),
- new CfIf(IfType.EQ, ValueType.INT, label6),
- label4,
- new CfLoad(ValueType.OBJECT, 5),
- new CfStore(ValueType.OBJECT, 6),
- label5,
- new CfLoad(ValueType.OBJECT, 6),
new CfLoad(ValueType.OBJECT, 3),
new CfInvoke(
184,
factory.createMethod(
- factory.createType("Ljava/lang/Enum;"),
+ factory.createType(
+ "Lcom/android/tools/r8/cfmethodgeneration/TypeSwitchMethods$Enum;"),
factory.createProto(
- factory.createType("Ljava/lang/Enum;"),
- factory.classType,
+ factory.createType(
+ "Lcom/android/tools/r8/cfmethodgeneration/TypeSwitchMethods$Enum;"),
factory.stringType),
factory.createString("valueOf")),
false),
new CfStore(ValueType.OBJECT, 4),
- label6,
- new CfFrame(
- new Int2ObjectAVLTreeMap<>(
- new int[] {0, 1, 2, 3, 4},
- new FrameType[] {
- FrameType.initializedNonNullReference(factory.objectType),
- FrameType.initializedNonNullReference(
- factory.createType("[Ljava/lang/Object;")),
- FrameType.intType(),
- FrameType.initializedNonNullReference(factory.stringType),
- FrameType.initializedNonNullReference(factory.objectType)
- })),
- new CfGoto(label8),
- label7,
+ label3,
+ new CfGoto(label5),
+ label4,
new CfFrame(
new Int2ObjectAVLTreeMap<>(
new int[] {0, 1, 2, 3, 4},
@@ -134,7 +104,7 @@
new ArrayDeque<>(
Arrays.asList(FrameType.initializedNonNullReference(factory.throwableType)))),
new CfStore(ValueType.OBJECT, 5),
- label8,
+ label5,
new CfFrame(
new Int2ObjectAVLTreeMap<>(
new int[] {0, 1, 2, 3, 4},
@@ -149,7 +119,7 @@
new CfLoad(ValueType.OBJECT, 1),
new CfLoad(ValueType.INT, 2),
new CfLoad(ValueType.OBJECT, 4),
- new CfIf(IfType.NE, ValueType.OBJECT, label9),
+ new CfIf(IfType.NE, ValueType.OBJECT, label6),
new CfNew(factory.objectType),
new CfStackInstruction(CfStackInstruction.Opcode.Dup),
new CfInvoke(
@@ -159,8 +129,8 @@
factory.createProto(factory.voidType),
factory.createString("<init>")),
false),
- new CfGoto(label10),
- label9,
+ new CfGoto(label7),
+ label6,
new CfFrame(
new Int2ObjectAVLTreeMap<>(
new int[] {0, 1, 2, 3, 4},
@@ -178,7 +148,7 @@
factory.createType("[Ljava/lang/Object;")),
FrameType.intType()))),
new CfLoad(ValueType.OBJECT, 4),
- label10,
+ label7,
new CfFrame(
new Int2ObjectAVLTreeMap<>(
new int[] {0, 1, 2, 3, 4},
@@ -197,7 +167,7 @@
FrameType.intType(),
FrameType.initializedNonNullReference(factory.objectType)))),
new CfArrayStore(MemberType.OBJECT),
- label11,
+ label8,
new CfFrame(
new Int2ObjectAVLTreeMap<>(
new int[] {0, 1, 2, 3},
@@ -212,10 +182,10 @@
new CfLoad(ValueType.OBJECT, 1),
new CfLoad(ValueType.INT, 2),
new CfArrayLoad(MemberType.OBJECT),
- new CfIfCmp(IfType.NE, ValueType.OBJECT, label12),
+ new CfIfCmp(IfType.NE, ValueType.OBJECT, label9),
new CfConstNumber(1, ValueType.INT),
- new CfGoto(label13),
- label12,
+ new CfGoto(label10),
+ label9,
new CfFrame(
new Int2ObjectAVLTreeMap<>(
new int[] {0, 1, 2, 3},
@@ -227,7 +197,7 @@
FrameType.initializedNonNullReference(factory.stringType)
})),
new CfConstNumber(0, ValueType.INT),
- label13,
+ label10,
new CfFrame(
new Int2ObjectAVLTreeMap<>(
new int[] {0, 1, 2, 3},
@@ -240,10 +210,10 @@
}),
new ArrayDeque<>(Arrays.asList(FrameType.intType()))),
new CfReturn(ValueType.INT),
- label14),
+ label11),
ImmutableList.of(
new CfTryCatch(
- label2, label6, ImmutableList.of(factory.throwableType), ImmutableList.of(label7))),
+ label2, label3, ImmutableList.of(factory.throwableType), ImmutableList.of(label4))),
ImmutableList.of());
}
diff --git a/src/test/java/com/android/tools/r8/cfmethodgeneration/TypeSwitchMethods.java b/src/test/java/com/android/tools/r8/cfmethodgeneration/TypeSwitchMethods.java
index a9e65d0..16b1a6c 100644
--- a/src/test/java/com/android/tools/r8/cfmethodgeneration/TypeSwitchMethods.java
+++ b/src/test/java/com/android/tools/r8/cfmethodgeneration/TypeSwitchMethods.java
@@ -6,17 +6,17 @@
public class TypeSwitchMethods {
+ private enum Enum {
+ A;
+ }
+
// By design this is lock-free so the JVM may compute several times the same value.
public static boolean switchEnumEq(Object value, Object[] cache, int index, String name) {
if (cache[index] == null) {
Object resolved = null;
try {
// Enum.class should be replaced when used by the correct enum class.
- Class<?> clazz = Enum.class;
- if (clazz.isEnum()) {
- Class<? extends Enum> enumClazz = (Class<? extends Enum>) clazz;
- resolved = Enum.valueOf(enumClazz, name);
- }
+ resolved = Enum.valueOf(name);
} catch (Throwable t) {
}
// R8 sets a sentinel if resolution has failed.
diff --git a/src/test/java24/com/android/tools/r8/jdk24/switchpatternmatching/EnumSwitchOldSyntaxTest.java b/src/test/java24/com/android/tools/r8/jdk24/switchpatternmatching/EnumSwitchOldSyntaxTest.java
index 52a69d3..7f4b2de 100644
--- a/src/test/java24/com/android/tools/r8/jdk24/switchpatternmatching/EnumSwitchOldSyntaxTest.java
+++ b/src/test/java24/com/android/tools/r8/jdk24/switchpatternmatching/EnumSwitchOldSyntaxTest.java
@@ -3,12 +3,16 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.jdk24.switchpatternmatching;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
import com.android.tools.r8.JdkClassFileProvider;
import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestParametersCollection;
import com.android.tools.r8.TestRuntime.CfVm;
import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@@ -60,9 +64,19 @@
.addKeepMainRule(Main.class)
.addKeepEnumsRule()
.run(parameters.getRuntime(), Main.class)
+ .inspect(this::assert2Classes)
.assertSuccessWithOutput(EXPECTED_OUTPUT);
}
+ private void assert2Classes(CodeInspector i) {
+ if (parameters.getPartialCompilationTestParameters().isSome()) {
+ return;
+ }
+ assertTrue(i.clazz(E.class).isPresent());
+ assertTrue(i.clazz(Main.class).isPresent());
+ assertEquals(2, i.allClasses().size());
+ }
+
public enum E {
E1,
E2,
diff --git a/src/test/java24/com/android/tools/r8/jdk24/switchpatternmatching/TypeSwitchEnumAsClassTest.java b/src/test/java24/com/android/tools/r8/jdk24/switchpatternmatching/TypeSwitchEnumAsClassTest.java
new file mode 100644
index 0000000..1e99ca6
--- /dev/null
+++ b/src/test/java24/com/android/tools/r8/jdk24/switchpatternmatching/TypeSwitchEnumAsClassTest.java
@@ -0,0 +1,130 @@
+// Copyright (c) 2025, 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.jdk24.switchpatternmatching;
+
+import static com.android.tools.r8.desugar.switchpatternmatching.SwitchTestHelper.hasJdk21TypeSwitch;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.JdkClassFileProvider;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestBuilder;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.utils.StringUtils;
+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.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class TypeSwitchEnumAsClassTest extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters()
+ .withCfRuntimesStartingFromIncluding(CfVm.JDK24)
+ .withDexRuntimes()
+ .withAllApiLevelsAlsoForCf()
+ .withPartialCompilation()
+ .build();
+ }
+
+ public static String EXPECTED_OUTPUT =
+ StringUtils.lines("null", "String", "Array of int, length = 0", "Other");
+
+ @Test
+ public void testJvm() throws Exception {
+ assumeTrue(parameters.isCfRuntime());
+ CodeInspector inspector = new CodeInspector(ToolHelper.getClassFileForTestClass(Main.class));
+ assertTrue(
+ hasJdk21TypeSwitch(inspector.clazz(Main.class).uniqueMethodWithOriginalName("typeSwitch")));
+
+ parameters.assumeJvmTestParameters();
+ testForJvm(parameters)
+ .apply(this::addModifiedProgramClasses)
+ .run(parameters.getRuntime(), Main.class)
+ .apply(this::assertResult);
+ }
+
+ private void assertResult(TestRunResult<?> r) {
+ r.assertSuccessWithOutput(EXPECTED_OUTPUT);
+ }
+
+ private <T extends TestBuilder<?, T>> void addModifiedProgramClasses(
+ TestBuilder<?, T> testBuilder) throws Exception {
+ testBuilder
+ .addProgramClassFileData(transformer(Main.class).clearNest().transform())
+ .addProgramClassFileData(transformer(C.class).clearNest().transform())
+ .addProgramClassFileData(transformer(Color.class).clearEnum().clearNest().transform());
+ }
+
+ @Test
+ public void testD8() throws Exception {
+ testForD8(parameters)
+ .apply(this::addModifiedProgramClasses)
+ .run(parameters.getRuntime(), Main.class)
+ .apply(this::assertResult);
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ parameters.assumeR8TestParameters();
+ testForR8(parameters)
+ .apply(this::addModifiedProgramClasses)
+ .applyIf(
+ parameters.isCfRuntime(),
+ b -> b.addLibraryProvider(JdkClassFileProvider.fromSystemJdk()))
+ .addKeepMainRule(Main.class)
+ .run(parameters.getRuntime(), Main.class)
+ .apply(this::assertResult);
+ }
+
+ // Enum will be a class at runtime.
+ enum Color {
+ RED,
+ GREEN,
+ BLUE;
+ }
+
+ // Class will be missing at runtime.
+ static class C {
+
+ @Override
+ public String toString() {
+ return "CCC";
+ }
+ }
+
+ static class Main {
+
+ static void typeSwitch(Object obj) {
+ switch (obj) {
+ case null -> System.out.println("null");
+ case Color.RED -> System.out.println("RED!!!");
+ case Color.BLUE -> System.out.println("BLUE!!!");
+ case Color.GREEN -> System.out.println("GREEN!!!");
+ case String string -> System.out.println("String");
+ case C c -> System.out.println(c.toString() + "!!!");
+ case int[] intArray -> System.out.println("Array of int, length = " + intArray.length);
+ default -> System.out.println("Other");
+ }
+ }
+
+ public static void main(String[] args) {
+ typeSwitch(null);
+ typeSwitch("s");
+ typeSwitch(new int[] {});
+ typeSwitch(new Object());
+ }
+ }
+}
diff --git a/src/test/testbase/java/com/android/tools/r8/transformers/ClassFileTransformer.java b/src/test/testbase/java/com/android/tools/r8/transformers/ClassFileTransformer.java
index 8b3e63c..1998f5e 100644
--- a/src/test/testbase/java/com/android/tools/r8/transformers/ClassFileTransformer.java
+++ b/src/test/testbase/java/com/android/tools/r8/transformers/ClassFileTransformer.java
@@ -415,6 +415,53 @@
});
}
+ // Note that this clears <init> and <clinit> methods.
+ public ClassFileTransformer clearEnum() {
+ return addClassTransformer(
+ new ClassTransformer() {
+ @Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ String superName,
+ String[] interfaces) {
+ ClassAccessFlags classAccessFlags = ClassAccessFlags.fromCfAccessFlags(access);
+ assert classAccessFlags.isEnum();
+ classAccessFlags.unsetEnum();
+ String newSuper = superName.equals("java/lang/Enum") ? "java/lang/Object" : superName;
+ super.visit(
+ version,
+ classAccessFlags.getAsCfAccessFlags(),
+ name,
+ signature,
+ newSuper,
+ interfaces);
+ }
+
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String descriptor, String signature, String[] exceptions) {
+ if (name.equals("<clinit>") || name.equals("<init>")) {
+ return null;
+ }
+ return super.visitMethod(access, name, descriptor, signature, exceptions);
+ }
+
+ @Override
+ public FieldVisitor visitField(
+ int access, String name, String descriptor, String signature, Object value) {
+ FieldAccessFlags fieldAccessFlags = FieldAccessFlags.fromCfAccessFlags(access);
+ if (fieldAccessFlags.isEnum()) {
+ fieldAccessFlags.unsetEnum();
+ }
+ return super.visitField(
+ fieldAccessFlags.getAsCfAccessFlags(), name, descriptor, signature, value);
+ }
+ });
+ }
+
public ClassFileTransformer clearNest() {
return setMinVersion(CfVm.JDK11)
.addClassTransformer(