Fix Non determinism in enum emulated dispatch
Bug: b/271385332
Change-Id: I1a36eb74521bee20dd7120ae70d1d20beada1503
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
index 6e75300..c5ce773 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
@@ -68,10 +68,11 @@
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.Sets;
-import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap;
-import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectLinkedOpenHashMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectSortedMap;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
@@ -643,8 +644,8 @@
LocalEnumUnboxingUtilityClass localUtilityClass,
Map<DexMethod, DexEncodedMethod> localUtilityMethods,
DexClassAndMethod superMethod,
- ProgramMethodSet subimplementations) {
- assert !subimplementations.isEmpty();
+ ProgramMethodSet unorderedSubimplementations) {
+ assert !unorderedSubimplementations.isEmpty();
DexMethod superUtilityMethod;
if (superMethod.isProgramMethod()) {
superUtilityMethod =
@@ -657,7 +658,9 @@
superUtilityMethod = localUtilityClass.computeToStringUtilityMethod(factory);
}
Map<DexMethod, DexMethod> overrideToUtilityMethods = new IdentityHashMap<>();
- for (ProgramMethod subMethod : subimplementations) {
+ List<ProgramMethod> sortedSubimplementations = new ArrayList<>(unorderedSubimplementations);
+ sortedSubimplementations.sort(Comparator.comparing(ProgramMethod::getHolderType));
+ for (ProgramMethod subMethod : sortedSubimplementations) {
DexMethod subEnumLocalUtilityMethod =
installLocalUtilityMethod(localUtilityClass, localUtilityMethods, subMethod);
assert subEnumLocalUtilityMethod != null;
@@ -667,7 +670,7 @@
installDispatchMethod(
localUtilityClass,
localUtilityMethods,
- subimplementations.iterator().next(),
+ sortedSubimplementations.iterator().next(),
superUtilityMethod,
overrideToUtilityMethods)
.getReference();
@@ -719,7 +722,7 @@
fixupProto(factory.prependHolderToProto(representative.getReference())),
localUtilityClass.getType(),
newMethodSignature -> !localUtilityMethods.containsKey(newMethodSignature));
- Int2ObjectMap<DexMethod> methodMap = new Int2ObjectArrayMap<>();
+ Int2ObjectSortedMap<DexMethod> methodMap = new Int2ObjectLinkedOpenHashMap<>();
IdentityHashMap<DexType, DexMethod> typeToMethod = new IdentityHashMap<>();
map.forEach(
(methodReference, newMethodReference) ->
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/EnumUnboxingCfCodeProvider.java b/src/main/java/com/android/tools/r8/ir/synthetic/EnumUnboxingCfCodeProvider.java
index 545e609..755f207 100644
--- a/src/main/java/com/android/tools/r8/ir/synthetic/EnumUnboxingCfCodeProvider.java
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/EnumUnboxingCfCodeProvider.java
@@ -36,8 +36,7 @@
import com.android.tools.r8.ir.code.ValueType;
import com.android.tools.r8.ir.optimize.enums.EnumDataMap.EnumData;
import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldMappingData;
-import com.android.tools.r8.utils.IntBox;
-import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectSortedMap;
import java.util.ArrayList;
import java.util.List;
import org.objectweb.asm.Opcodes;
@@ -73,13 +72,13 @@
private final GraphLens codeLens;
private final DexMethod superEnumMethod;
- private final Int2ObjectMap<DexMethod> methodMap;
+ private final Int2ObjectSortedMap<DexMethod> methodMap;
public EnumUnboxingMethodDispatchCfCodeProvider(
AppView<?> appView,
DexType holder,
DexMethod superEnumMethod,
- Int2ObjectMap<DexMethod> methodMap) {
+ Int2ObjectSortedMap<DexMethod> methodMap) {
super(appView, holder);
this.codeLens = appView.codeLens();
this.superEnumMethod = superEnumMethod;
@@ -107,10 +106,9 @@
for (DexType parameter : representative.getParameters()) {
frameBuilder.appendLocal(FrameType.initialized(parameter));
}
- IntBox index = new IntBox();
methodMap.forEach(
(unboxedEnumValue, method) -> {
- boolean lastCase = index.incrementAndGet() == methodMap.size() && !hasDefaultCase;
+ boolean lastCase = methodMap.lastIntKey() == unboxedEnumValue && !hasDefaultCase;
if (!lastCase) {
CfLabel dest = new CfLabel();
instructions.add(new CfLoad(ValueType.fromDexType(factory.intType), 0));
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/enummerging/DeterministicEnumMergingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/enummerging/DeterministicEnumMergingTest.java
new file mode 100644
index 0000000..00bb1cf
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/enummerging/DeterministicEnumMergingTest.java
@@ -0,0 +1,125 @@
+// Copyright (c) 2023, 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.enumunboxing.enummerging;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.enumunboxing.EnumUnboxingTestBase;
+import java.util.List;
+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 DeterministicEnumMergingTest extends EnumUnboxingTestBase {
+
+ private final TestParameters parameters;
+ private final boolean enumValueOptimization;
+ private final EnumKeepRules enumKeepRules;
+
+ @Parameters(name = "{0} valueOpt: {1} keep: {2}")
+ public static List<Object[]> data() {
+ return enumUnboxingTestParameters();
+ }
+
+ public DeterministicEnumMergingTest(
+ TestParameters parameters, boolean enumValueOptimization, EnumKeepRules enumKeepRules) {
+ this.parameters = parameters;
+ this.enumValueOptimization = enumValueOptimization;
+ this.enumKeepRules = enumKeepRules;
+ }
+
+ @Test
+ public void testEnumUnboxingDeterminism() throws Exception {
+ R8TestCompileResult compile1 =
+ testForR8(parameters.getBackend())
+ .addInnerClasses(getClass())
+ .addKeepMainRule(Main.class)
+ .addKeepRules(enumKeepRules.getKeepRules())
+ .addOptionsModification(opt -> opt.testing.enableEnumWithSubtypesUnboxing = true)
+ .addEnumUnboxingInspector(inspector -> inspector.assertUnboxed(MyEnum.class))
+ .enableInliningAnnotations()
+ .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+ .setMinApi(parameters)
+ .compile();
+ R8TestCompileResult compile2 =
+ testForR8(parameters.getBackend())
+ .addInnerClasses(getClass())
+ .addKeepMainRule(Main.class)
+ .addKeepRules(enumKeepRules.getKeepRules())
+ .addOptionsModification(opt -> opt.testing.enableEnumWithSubtypesUnboxing = true)
+ .addEnumUnboxingInspector(inspector -> inspector.assertUnboxed(MyEnum.class))
+ .enableInliningAnnotations()
+ .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+ .setMinApi(parameters)
+ .compile();
+ assertIdenticalInspectors(compile1.inspector(), compile2.inspector());
+ compile1
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutputLines("336", "74", "22", "10794", "96", "44", "52", "3084");
+ }
+
+ enum MyEnum {
+ // The more cases, the highest the chance to trigger non determinism.
+ // The operate methods have to be moved in the same order, and the dispatch method has to
+ // be generated in the same order too.
+ A(8) {
+ @NeverInline
+ @Override
+ public long operate(long another) {
+ return num * another;
+ }
+ },
+ B(32) {
+ @NeverInline
+ @Override
+ public long operate(long another) {
+ return num + another;
+ }
+ },
+ C(64) {
+ @NeverInline
+ @Override
+ public long operate(long another) {
+ return num - another;
+ }
+ },
+ D(256) {
+
+ @NeverInline
+ @Override
+ public long operate(long another) {
+ return num * another + another;
+ }
+ };
+ final long num;
+
+ MyEnum(long num) {
+ this.num = num;
+ }
+
+ public abstract long operate(long another);
+ }
+
+ static class Main {
+
+ public static void main(String[] args) {
+ System.out.println(MyEnum.A.operate(42));
+ System.out.println(MyEnum.B.operate(42));
+ System.out.println(MyEnum.C.operate(42));
+ System.out.println(MyEnum.D.operate(42));
+ System.out.println(indirect(MyEnum.A));
+ System.out.println(indirect(MyEnum.B));
+ System.out.println(indirect(MyEnum.C));
+ System.out.println(indirect(MyEnum.D));
+ }
+
+ @NeverInline
+ public static long indirect(MyEnum e) {
+ return e.operate(12);
+ }
+ }
+}