Ensure horizontal class merger only merges fields with the same flags
Fixes: 175855245
Fixes: 175849452
Change-Id: I48b9297355247c7d613b1eb857d3f47f25287743
diff --git a/src/main/java/com/android/tools/r8/graph/AccessFlags.java b/src/main/java/com/android/tools/r8/graph/AccessFlags.java
index e47b428..98806f6 100644
--- a/src/main/java/com/android/tools/r8/graph/AccessFlags.java
+++ b/src/main/java/com/android/tools/r8/graph/AccessFlags.java
@@ -204,8 +204,9 @@
set(Constants.ACC_FINAL);
}
- public void unsetFinal() {
+ public T unsetFinal() {
unset(Constants.ACC_FINAL);
+ return self();
}
public boolean isSynthetic() {
@@ -216,8 +217,9 @@
set(Constants.ACC_SYNTHETIC);
}
- public void unsetSynthetic() {
+ public T unsetSynthetic() {
unset(Constants.ACC_SYNTHETIC);
+ return self();
}
public void demoteFromSynthetic() {
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedField.java b/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
index e90a784..fc0eeb0 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
@@ -220,6 +220,10 @@
return isStatic();
}
+ public boolean isSynthetic() {
+ return accessFlags.isSynthetic();
+ }
+
public boolean isVolatile() {
return accessFlags.isVolatile();
}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInstanceFieldsMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInstanceFieldsMerger.java
index 488e33e..23cb57c 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInstanceFieldsMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInstanceFieldsMerger.java
@@ -9,9 +9,9 @@
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.horizontalclassmerging.HorizontalClassMergerGraphLens.Builder;
+import com.android.tools.r8.horizontalclassmerging.policies.SameInstanceFields.InstanceFieldInfo;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.android.tools.r8.utils.IterableUtils;
-import com.android.tools.r8.utils.ListUtils;
import com.google.common.collect.Iterables;
import java.util.ArrayList;
import java.util.Collection;
@@ -19,7 +19,6 @@
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
-import java.util.Map.Entry;
public class ClassInstanceFieldsMerger {
@@ -54,62 +53,70 @@
* type.
*/
public void addFields(DexProgramClass clazz) {
- Map<DexType, LinkedList<DexEncodedField>> availableFieldsPerFieldType =
- computeAvailableFieldsPerFieldType();
+ Map<InstanceFieldInfo, LinkedList<DexEncodedField>> availableFieldsByExactInfo =
+ getAvailableFieldsByExactInfo();
List<DexEncodedField> needsMerge = new ArrayList<>();
// Pass 1: Match fields that have the exact same type.
for (DexEncodedField oldField : clazz.instanceFields()) {
- DexEncodedField newField =
- removeFirstCompatibleField(oldField, availableFieldsPerFieldType.get(oldField.getType()));
- if (newField != null) {
- fieldMappings.get(newField).add(oldField);
- } else {
+ InstanceFieldInfo info = InstanceFieldInfo.createExact(oldField);
+ LinkedList<DexEncodedField> availableFieldsWithExactSameInfo =
+ availableFieldsByExactInfo.get(info);
+ if (availableFieldsWithExactSameInfo == null || availableFieldsWithExactSameInfo.isEmpty()) {
needsMerge.add(oldField);
+ } else {
+ DexEncodedField newField = availableFieldsWithExactSameInfo.removeFirst();
+ fieldMappings.get(newField).add(oldField);
+ if (availableFieldsWithExactSameInfo.isEmpty()) {
+ availableFieldsByExactInfo.remove(info);
+ }
}
}
// Pass 2: Match fields that do not have the same reference type.
+ Map<InstanceFieldInfo, LinkedList<DexEncodedField>> availableFieldsByRelaxedInfo =
+ getAvailableFieldsByRelaxedInfo(availableFieldsByExactInfo);
for (DexEncodedField oldField : needsMerge) {
assert oldField.getType().isReferenceType();
- DexEncodedField newField = null;
- for (Entry<DexType, LinkedList<DexEncodedField>> availableFieldsForType :
- availableFieldsPerFieldType.entrySet()) {
- assert availableFieldsForType.getKey().isReferenceType()
- || availableFieldsForType.getValue().isEmpty();
- newField = removeFirstCompatibleField(oldField, availableFieldsForType.getValue());
- if (newField != null) {
- break;
- }
- }
+ DexEncodedField newField =
+ availableFieldsByRelaxedInfo
+ .get(InstanceFieldInfo.createRelaxed(oldField, appView.dexItemFactory()))
+ .removeFirst();
assert newField != null;
assert newField.getType().isReferenceType();
fieldMappings.get(newField).add(oldField);
}
}
- private Map<DexType, LinkedList<DexEncodedField>> computeAvailableFieldsPerFieldType() {
- Map<DexType, LinkedList<DexEncodedField>> availableFieldsPerFieldType = new LinkedHashMap<>();
+ private Map<InstanceFieldInfo, LinkedList<DexEncodedField>> getAvailableFieldsByExactInfo() {
+ Map<InstanceFieldInfo, LinkedList<DexEncodedField>> availableFieldsByInfo =
+ new LinkedHashMap<>();
for (DexEncodedField field : fieldMappings.keySet()) {
- availableFieldsPerFieldType
- .computeIfAbsent(field.type(), ignore -> new LinkedList<>())
+ availableFieldsByInfo
+ .computeIfAbsent(InstanceFieldInfo.createExact(field), ignore -> new LinkedList<>())
.add(field);
}
- return availableFieldsPerFieldType;
+ return availableFieldsByInfo;
}
- public DexEncodedField removeFirstCompatibleField(
- DexEncodedField oldField, LinkedList<DexEncodedField> availableFields) {
- if (availableFields == null) {
- return null;
- }
- return ListUtils.removeFirstMatch(
- availableFields,
- field -> field.getAccessFlags().isSameVisibility(oldField.getAccessFlags()))
- .orElse(null);
+ private Map<InstanceFieldInfo, LinkedList<DexEncodedField>> getAvailableFieldsByRelaxedInfo(
+ Map<InstanceFieldInfo, LinkedList<DexEncodedField>> availableFieldsByExactInfo) {
+ Map<InstanceFieldInfo, LinkedList<DexEncodedField>> availableFieldsByRelaxedInfo =
+ new LinkedHashMap<>();
+ availableFieldsByExactInfo.forEach(
+ (info, fields) ->
+ availableFieldsByRelaxedInfo
+ .computeIfAbsent(
+ info.toInfoWithRelaxedType(appView.dexItemFactory()),
+ ignore -> new LinkedList<>())
+ .addAll(fields));
+ return availableFieldsByRelaxedInfo;
}
private void fixAccessFlags(DexEncodedField newField, Collection<DexEncodedField> oldFields) {
+ if (newField.isSynthetic() && Iterables.any(oldFields, oldField -> !oldField.isSynthetic())) {
+ newField.getAccessFlags().demoteFromSynthetic();
+ }
if (newField.isFinal() && Iterables.any(oldFields, oldField -> !oldField.isFinal())) {
newField.getAccessFlags().demoteFromFinal();
}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/FieldMultiset.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/FieldMultiset.java
deleted file mode 100644
index 65127b5..0000000
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/FieldMultiset.java
+++ /dev/null
@@ -1,85 +0,0 @@
-// Copyright (c) 2020, 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.horizontalclassmerging;
-
-import com.android.tools.r8.graph.AccessFlags;
-import com.android.tools.r8.graph.DexEncodedField;
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexType;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
-
-public class FieldMultiset {
-
- private static class VisibilitySignature {
- private int publicVisible = 0;
- private int protectedVisible = 0;
- private int privateVisible = 0;
- private int packagePrivateVisible = 0;
-
- public void addAccessModifier(AccessFlags accessFlags) {
- if (accessFlags.isPublic()) {
- publicVisible++;
- } else if (accessFlags.isPrivate()) {
- privateVisible++;
- } else if (accessFlags.isPackagePrivate()) {
- packagePrivateVisible++;
- } else if (accessFlags.isProtected()) {
- protectedVisible++;
- }
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- VisibilitySignature that = (VisibilitySignature) o;
- return publicVisible == that.publicVisible
- && protectedVisible == that.protectedVisible
- && privateVisible == that.privateVisible
- && packagePrivateVisible == that.packagePrivateVisible;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(publicVisible, protectedVisible, privateVisible, packagePrivateVisible);
- }
- }
-
- // This *must* not be an IdentityHashMap, because hash equality does not work for the values.
- private final Map<DexType, VisibilitySignature> fields = new HashMap<>();
-
- public FieldMultiset(DexProgramClass clazz) {
- for (DexEncodedField field : clazz.instanceFields()) {
- fields
- .computeIfAbsent(field.type(), ignore -> new VisibilitySignature())
- .addAccessModifier(field.getAccessFlags());
- }
- }
-
- public FieldMultiset(Iterable<DexEncodedField> instanceFields) {
- for (DexEncodedField field : instanceFields) {
- fields
- .computeIfAbsent(field.type(), ignore -> new VisibilitySignature())
- .addAccessModifier(field.getAccessFlags());
- }
- }
-
- @Override
- public int hashCode() {
- return fields.hashCode();
- }
-
- @Override
- public boolean equals(Object object) {
- if (object instanceof FieldMultiset) {
- FieldMultiset other = (FieldMultiset) object;
- return fields.equals(other.fields);
- } else {
- return false;
- }
- }
-}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
index ed6ddce..6115e7e 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
@@ -13,6 +13,7 @@
import com.android.tools.r8.horizontalclassmerging.policies.DontMergeSynchronizedClasses;
import com.android.tools.r8.horizontalclassmerging.policies.IgnoreSynthetics;
import com.android.tools.r8.horizontalclassmerging.policies.LimitGroups;
+import com.android.tools.r8.horizontalclassmerging.policies.MinimizeFieldCasts;
import com.android.tools.r8.horizontalclassmerging.policies.NoAnnotations;
import com.android.tools.r8.horizontalclassmerging.policies.NoClassInitializerWithObservableSideEffects;
import com.android.tools.r8.horizontalclassmerging.policies.NoClassesOrMembersWithAnnotations;
@@ -33,7 +34,7 @@
import com.android.tools.r8.horizontalclassmerging.policies.PreventMethodImplementation;
import com.android.tools.r8.horizontalclassmerging.policies.RespectPackageBoundaries;
import com.android.tools.r8.horizontalclassmerging.policies.SameFeatureSplit;
-import com.android.tools.r8.horizontalclassmerging.policies.SameFields;
+import com.android.tools.r8.horizontalclassmerging.policies.SameInstanceFields;
import com.android.tools.r8.horizontalclassmerging.policies.SameNestHost;
import com.android.tools.r8.horizontalclassmerging.policies.SameParentClass;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
@@ -105,7 +106,7 @@
RuntimeTypeCheckInfo runtimeTypeCheckInfo) {
return ImmutableList.of(
new NotMatchedByNoHorizontalClassMerging(appView),
- new SameFields(appView),
+ new SameInstanceFields(appView),
new NoInterfaces(),
new NoAnnotations(),
new NoEnums(appView),
@@ -132,6 +133,7 @@
new SameFeatureSplit(appView),
new RespectPackageBoundaries(appView),
new DontMergeSynchronizedClasses(appView),
+ new MinimizeFieldCasts(),
new LimitGroups(appView));
}
@@ -175,15 +177,8 @@
HorizontalClassMergerGraphLens.Builder lensBuilder,
FieldAccessInfoCollectionModifier.Builder fieldAccessChangesBuilder,
SyntheticArgumentClass syntheticArgumentClass) {
-
- HorizontalClassMergerGraphLens lens =
- new TreeFixer(
- appView,
- mergedClasses,
- lensBuilder,
- fieldAccessChangesBuilder,
- syntheticArgumentClass)
- .fixupTypeReferences();
- return lens;
+ return new TreeFixer(
+ appView, mergedClasses, lensBuilder, fieldAccessChangesBuilder, syntheticArgumentClass)
+ .fixupTypeReferences();
}
}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java
index 02e4684..9e4ebf5 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java
@@ -45,6 +45,10 @@
classes.add(clazz);
}
+ public void add(MergeGroup group) {
+ classes.addAll(group.getClasses());
+ }
+
public void addAll(Collection<DexProgramClass> classes) {
this.classes.addAll(classes);
}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/MinimizeFieldCasts.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/MinimizeFieldCasts.java
new file mode 100644
index 0000000..73e0786
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/MinimizeFieldCasts.java
@@ -0,0 +1,73 @@
+// Copyright (c) 2020, 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.horizontalclassmerging.policies;
+
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
+import com.google.common.collect.HashMultiset;
+import com.google.common.collect.Multiset;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class MinimizeFieldCasts extends MultiClassPolicy {
+
+ @Override
+ public final Collection<MergeGroup> apply(MergeGroup group) {
+ // First find all classes that can be merged without changing field types.
+ Map<Multiset<DexType>, MergeGroup> newGroups = new LinkedHashMap<>();
+ group.forEach(clazz -> addExact(clazz, newGroups));
+
+ // Create a single group from all trivial groups.
+ MergeGroup pendingGroup = new MergeGroup();
+ newGroups
+ .values()
+ .removeIf(
+ newGroup -> {
+ if (newGroup.isTrivial()) {
+ pendingGroup.add(newGroup);
+ return true;
+ }
+ return false;
+ });
+
+ if (pendingGroup.isEmpty()) {
+ return newGroups.values();
+ }
+
+ if (!pendingGroup.isTrivial()) {
+ List<MergeGroup> newGroupsIncludingRelaxedGroup = new ArrayList<>(newGroups.values());
+ newGroupsIncludingRelaxedGroup.add(pendingGroup);
+ return newGroupsIncludingRelaxedGroup;
+ }
+
+ MergeGroup smallestNewGroup = null;
+ for (MergeGroup newGroup : newGroups.values()) {
+ if (smallestNewGroup == null || newGroup.size() < smallestNewGroup.size()) {
+ smallestNewGroup = newGroup;
+ }
+ }
+ assert smallestNewGroup != null;
+ smallestNewGroup.add(pendingGroup);
+ return newGroups.values();
+ }
+
+ private void addExact(DexProgramClass clazz, Map<Multiset<DexType>, MergeGroup> groups) {
+ groups.computeIfAbsent(getExactMergeKey(clazz), ignore -> new MergeGroup()).add(clazz);
+ }
+
+ private Multiset<DexType> getExactMergeKey(DexProgramClass clazz) {
+ Multiset<DexType> fieldTypes = HashMultiset.create();
+ for (DexEncodedField field : clazz.instanceFields()) {
+ fieldTypes.add(field.getType());
+ }
+ return fieldTypes;
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameFields.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameFields.java
deleted file mode 100644
index adeb3ce..0000000
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameFields.java
+++ /dev/null
@@ -1,78 +0,0 @@
-// Copyright (c) 2020, 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.horizontalclassmerging.policies;
-
-import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexEncodedField;
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.horizontalclassmerging.FieldMultiset;
-import com.android.tools.r8.horizontalclassmerging.MergeGroup;
-import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
-import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.google.common.collect.Iterables;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-public class SameFields extends MultiClassPolicy {
-
- private final AppView<AppInfoWithLiveness> appView;
-
- public SameFields(AppView<AppInfoWithLiveness> appView) {
- this.appView = appView;
- }
-
- public void addTo(FieldMultiset key, DexProgramClass clazz, Map<FieldMultiset, MergeGroup> map) {
- map.computeIfAbsent(key, ignore -> new MergeGroup()).add(clazz);
- }
-
- @Override
- public final Collection<MergeGroup> apply(MergeGroup group) {
- // First find all classes that can be merged without changing field types.
- Map<FieldMultiset, MergeGroup> groups = new LinkedHashMap<>();
- group.getClasses().forEach(clazz -> addTo(getMergeKey(clazz), clazz, groups));
-
- // For each trivial group, try to generalise it and then group it.
- Map<FieldMultiset, MergeGroup> generalizedGroups = new LinkedHashMap<>();
- Iterator<MergeGroup> iterator = groups.values().iterator();
- while (iterator.hasNext()) {
- MergeGroup newGroup = iterator.next();
- if (newGroup.isTrivial()) {
- newGroup
- .getClasses()
- .forEach(clazz -> addTo(getGeneralizedMergeKey(clazz), clazz, generalizedGroups));
- iterator.remove();
- }
- }
-
- removeTrivialGroups(generalizedGroups.values());
-
- List<MergeGroup> newGroups = new ArrayList<>();
- newGroups.addAll(groups.values());
- newGroups.addAll(generalizedGroups.values());
- return newGroups;
- }
-
- public FieldMultiset getMergeKey(DexProgramClass clazz) {
- return new FieldMultiset(clazz);
- }
-
- public DexEncodedField generalizeField(DexEncodedField field) {
- if (!field.type().isReferenceType()) {
- return field;
- }
- return field.toTypeSubstitutedField(
- field
- .getReference()
- .withType(appView.dexItemFactory().objectType, appView.dexItemFactory()));
- }
-
- public FieldMultiset getGeneralizedMergeKey(DexProgramClass clazz) {
- return new FieldMultiset(Iterables.transform(clazz.instanceFields(), this::generalizeField));
- }
-}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameInstanceFields.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameInstanceFields.java
new file mode 100644
index 0000000..13e0388
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameInstanceFields.java
@@ -0,0 +1,84 @@
+// Copyright (c) 2020, 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.horizontalclassmerging.policies;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.FieldAccessFlags;
+import com.android.tools.r8.horizontalclassmerging.MultiClassSameReferencePolicy;
+import com.android.tools.r8.horizontalclassmerging.policies.SameInstanceFields.InstanceFieldInfo;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.google.common.collect.HashMultiset;
+import com.google.common.collect.Multiset;
+import java.util.Objects;
+
+public class SameInstanceFields extends MultiClassSameReferencePolicy<Multiset<InstanceFieldInfo>> {
+
+ private final DexItemFactory dexItemFactory;
+
+ public SameInstanceFields(AppView<AppInfoWithLiveness> appView) {
+ this.dexItemFactory = appView.dexItemFactory();
+ }
+
+ @Override
+ public Multiset<InstanceFieldInfo> getMergeKey(DexProgramClass clazz) {
+ Multiset<InstanceFieldInfo> fields = HashMultiset.create();
+ for (DexEncodedField field : clazz.instanceFields()) {
+ fields.add(InstanceFieldInfo.createRelaxed(field, dexItemFactory));
+ }
+ return fields;
+ }
+
+ public static class InstanceFieldInfo {
+
+ private final FieldAccessFlags accessFlags;
+ private final DexType type;
+
+ private InstanceFieldInfo(FieldAccessFlags accessFlags, DexType type) {
+ this.accessFlags =
+ FieldAccessFlags.fromSharedAccessFlags(accessFlags.materialize())
+ .unsetFinal()
+ .unsetSynthetic();
+ this.type = type;
+ }
+
+ public static InstanceFieldInfo createExact(DexEncodedField field) {
+ return new InstanceFieldInfo(field.getAccessFlags(), field.getType());
+ }
+
+ public static InstanceFieldInfo createRelaxed(
+ DexEncodedField field, DexItemFactory dexItemFactory) {
+ return new InstanceFieldInfo(
+ field.getAccessFlags(),
+ field.getType().isReferenceType() ? dexItemFactory.objectType : field.getType());
+ }
+
+ public FieldAccessFlags getAccessFlags() {
+ return accessFlags;
+ }
+
+ public InstanceFieldInfo toInfoWithRelaxedType(DexItemFactory dexItemFactory) {
+ return new InstanceFieldInfo(
+ accessFlags, type.isReferenceType() ? dexItemFactory.objectType : type);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ InstanceFieldInfo info = (InstanceFieldInfo) obj;
+ return accessFlags.materialize() == info.accessFlags.materialize() && type == info.type;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(accessFlags, type);
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/FieldsWithDifferentAccessFlagsTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/FieldsWithDifferentAccessFlagsTest.java
new file mode 100644
index 0000000..58acdbf
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/FieldsWithDifferentAccessFlagsTest.java
@@ -0,0 +1,89 @@
+/*
+ * // Copyright (c) 2020, 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.classmerging.horizontal;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
+import org.junit.Test;
+
+public class FieldsWithDifferentAccessFlagsTest extends HorizontalClassMergingTestBase {
+
+ public FieldsWithDifferentAccessFlagsTest(
+ TestParameters parameters, boolean enableHorizontalClassMerging) {
+ super(parameters, enableHorizontalClassMerging);
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ testForR8(parameters.getBackend())
+ .addInnerClasses(getClass())
+ .addKeepMainRule(Main.class)
+ .addOptionsModification(
+ options ->
+ options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
+ .addHorizontallyMergedClassesInspectorIf(
+ enableHorizontalClassMerging, HorizontallyMergedClassesInspector::assertNoClassesMerged)
+ .enableInliningAnnotations()
+ .enableNeverClassInliningAnnotations()
+ .setMinApi(parameters.getApiLevel())
+ .compile()
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutputLines("A", "B", "C");
+ }
+
+ @NeverClassInline
+ public static class A {
+ public volatile String msg;
+
+ public A(String msg) {
+ this.msg = msg;
+ }
+
+ @NeverInline
+ public void foo() {
+ System.out.println(msg);
+ }
+ }
+
+ @NeverClassInline
+ public static class B {
+ public transient String msg;
+
+ public B(String msg) {
+ this.msg = msg;
+ }
+
+ @NeverInline
+ public void foo() {
+ System.out.println(msg);
+ }
+ }
+
+ @NeverClassInline
+ public static class C {
+ public String msg;
+
+ public C(String msg) {
+ this.msg = msg;
+ }
+
+ @NeverInline
+ public void foo() {
+ System.out.println(msg);
+ }
+ }
+
+ public static class Main {
+ public static void main(String[] args) {
+ new A(System.currentTimeMillis() > 0 ? "A" : null).foo();
+ new B(System.currentTimeMillis() > 0 ? "B" : null).foo();
+ new C(System.currentTimeMillis() > 0 ? "C" : null).foo();
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/MinimizeFieldCastsTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/MinimizeFieldCastsTest.java
new file mode 100644
index 0000000..90ae326
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/MinimizeFieldCastsTest.java
@@ -0,0 +1,121 @@
+// Copyright (c) 2020, 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.classmerging.horizontal;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.TestParameters;
+import org.junit.Test;
+
+public class MinimizeFieldCastsTest extends HorizontalClassMergingTestBase {
+
+ public MinimizeFieldCastsTest(TestParameters parameters, boolean enableHorizontalClassMerging) {
+ super(parameters, enableHorizontalClassMerging);
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ testForR8(parameters.getBackend())
+ .addInnerClasses(getClass())
+ .addKeepMainRule(Main.class)
+ .addOptionsModification(
+ options ->
+ options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
+ .enableInliningAnnotations()
+ .enableNeverClassInliningAnnotations()
+ .enableNoHorizontalClassMergingAnnotations()
+ .setMinApi(parameters.getApiLevel())
+ .addHorizontallyMergedClassesInspectorIf(
+ enableHorizontalClassMerging,
+ inspector ->
+ // Two merge groups are expected since we attempt to merge classes in a way that
+ // avoids merging fields with different types unless strictly required for merging.
+ inspector.assertMergedInto(B.class, A.class).assertMergedInto(D.class, C.class))
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutputLines("Foo", "Foo", "Bar", "Bar");
+ }
+
+ @NeverClassInline
+ public static class A {
+ FooGreeter greeter;
+
+ A(FooGreeter greeter) {
+ this.greeter = greeter;
+ }
+
+ void greet() {
+ greeter.greet();
+ }
+ }
+
+ @NeverClassInline
+ public static class B {
+ FooGreeter greeter;
+
+ B(FooGreeter greeter) {
+ this.greeter = greeter;
+ }
+
+ void greet() {
+ greeter.greet();
+ }
+ }
+
+ @NeverClassInline
+ public static class C {
+ BarGreeter greeter;
+
+ C(BarGreeter greeter) {
+ this.greeter = greeter;
+ }
+
+ void greet() {
+ greeter.greet();
+ }
+ }
+
+ @NeverClassInline
+ public static class D {
+ BarGreeter greeter;
+
+ D(BarGreeter greeter) {
+ this.greeter = greeter;
+ }
+
+ void greet() {
+ greeter.greet();
+ }
+ }
+
+ @NeverClassInline
+ @NoHorizontalClassMerging
+ static class FooGreeter {
+
+ @NeverInline
+ void greet() {
+ System.out.println("Foo");
+ }
+ }
+
+ @NeverClassInline
+ @NoHorizontalClassMerging
+ static class BarGreeter {
+
+ @NeverInline
+ void greet() {
+ System.out.println("Bar");
+ }
+ }
+
+ static class Main {
+ public static void main(String[] args) {
+ new A(new FooGreeter()).greet();
+ new B(new FooGreeter()).greet();
+ new C(new BarGreeter()).greet();
+ new D(new BarGreeter()).greet();
+ }
+ }
+}