Restrict intermediate sharing to input contexts
This does not hit a issue in D8 as the actual equivalence checks include
the input context in the definition of SynthesizingContext::compareTo.
This change includes a regression to catch if that property breaks and
aligns the hashing with compareTo by including the input context.
Change-Id: I1efdfe8be744e19204e63ee39b12ce8b0ff13479
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java
index f0080f3..dc0a4ac 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java
@@ -84,9 +84,11 @@
return hasher.hash();
}
if (intermediate) {
- // If in intermediate mode, include the context type as sharing is restricted to within a
- // single context.
- getContext().getSynthesizingContextType().hashWithTypeEquivalence(hasher, map);
+ // If in intermediate mode, include the *input* context type to restrict sharing.
+ // This restricts sharing to only allow sharing the synthetics with the same *input* context.
+ // If the synthetic is itself an input from a previous compilation it is restricted to share
+ // within its own context only. The input context should not be mapped to an equivalence type.
+ getContext().getSynthesizingInputContext(intermediate).hash(hasher);
}
hasher.putInt(context.getFeatureSplit().hashCode());
internalComputeHash(hasher, map);
@@ -103,7 +105,6 @@
return compareTo(other, includeContext, graphLens, classToFeatureSplitMap) == 0;
}
- @SuppressWarnings("ReferenceEquality")
int compareTo(
D other,
boolean includeContext,
@@ -138,10 +139,12 @@
if (graphLens.isNonIdentityLens()) {
DexType thisOrigType = graphLens.getOriginalType(thisType);
DexType otherOrigType = graphLens.getOriginalType(otherType);
- if (thisType != thisOrigType || otherType != otherOrigType) {
+ if (thisType.isNotIdenticalTo(thisOrigType) || otherType.isNotIdenticalTo(otherOrigType)) {
map =
t -> {
- if (t == otherType || t == thisOrigType || t == otherOrigType) {
+ if (DexType.identical(t, otherType)
+ || DexType.identical(t, thisOrigType)
+ || DexType.identical(t, otherOrigType)) {
return thisType;
}
return t;
@@ -149,7 +152,7 @@
}
}
if (map == null) {
- map = t -> t == otherType ? thisType : t;
+ map = t -> otherType.isIdenticalTo(t) ? thisType : t;
}
return internalCompareTo(other, map);
}
diff --git a/src/test/java/com/android/tools/r8/desugar/backports/BackportDuplicationTest.java b/src/test/java/com/android/tools/r8/desugar/backports/BackportDuplicationTest.java
index 128178a..ba3e8e6 100644
--- a/src/test/java/com/android/tools/r8/desugar/backports/BackportDuplicationTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/backports/BackportDuplicationTest.java
@@ -16,6 +16,8 @@
import com.android.tools.r8.DexFilePerClassFileConsumer;
import com.android.tools.r8.DiagnosticsHandler;
import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.SyntheticInfoConsumer;
+import com.android.tools.r8.SyntheticInfoConsumerData;
import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestParametersCollection;
@@ -23,7 +25,9 @@
import com.android.tools.r8.ToolHelper.ArtCommandBuilder;
import com.android.tools.r8.ToolHelper.ProcessResult;
import com.android.tools.r8.desugar.backports.AbstractBackportTest.MiniAssert;
+import com.android.tools.r8.references.ClassReference;
import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.Reference;
import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
import com.android.tools.r8.synthesis.SyntheticNaming;
import com.android.tools.r8.utils.AndroidApiLevel;
@@ -37,8 +41,10 @@
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.junit.Test;
@@ -342,6 +348,115 @@
assertEquals(expectedSynthetics, getSyntheticMethods(inspector));
}
+ private static class ContextCollector implements SyntheticInfoConsumer {
+
+ Map<ClassReference, Set<ClassReference>> contextToSynthetics = new HashMap<>();
+
+ @Override
+ public synchronized void acceptSyntheticInfo(SyntheticInfoConsumerData data) {
+ contextToSynthetics
+ .computeIfAbsent(data.getSynthesizingContextClass(), k -> new HashSet<>())
+ .add(data.getSyntheticClass());
+ }
+
+ @Override
+ public void finished() {}
+ }
+
+ private static class PerFileCollector extends DexFilePerClassFileConsumer.ForwardingConsumer {
+
+ Map<ClassReference, byte[]> data = new HashMap<>();
+ ContextCollector contexts = new ContextCollector();
+
+ public PerFileCollector() {
+ super(null);
+ }
+
+ @Override
+ public boolean combineSyntheticClassesWithPrimaryClass() {
+ return false;
+ }
+
+ @Override
+ public synchronized void accept(
+ String primaryClassDescriptor,
+ ByteDataView data,
+ Set<String> descriptors,
+ DiagnosticsHandler handler) {
+ super.accept(primaryClassDescriptor, data, descriptors, handler);
+ this.data.put(Reference.classFromDescriptor(primaryClassDescriptor), data.copyByteData());
+ }
+ }
+
+ @Test
+ public void testDoubleCompileSyntheticInputsD8() throws Exception {
+ assumeTrue(parameters.isDexRuntime());
+ // This is a regression test for the pathological case of recompiling intermediates in
+ // intermediate mode, but where the second round of compilation can share more than what was
+ // originally shared. Such a case should never be hit by any reasonable compilation pipeline,
+ // but it could by chance happen if a bytecode transformation was between the two intermediate
+ // steps that ended up making two previously distinct synthetics equivalent. To simulate such
+ // a case the sharing of synthetics is internally disabled so that we know the second round
+ // would see equivalent synthetics.
+
+ // Compile part 1 of the input with sharing completely disabled.
+ PerFileCollector out1 = new PerFileCollector();
+ testForD8(parameters.getBackend())
+ .addOptionsModification(o -> o.testing.enableSyntheticSharing = false)
+ .addProgramClasses(User1.class)
+ .addClasspathClasses(CLASSES)
+ .setMinApi(parameters)
+ .setIntermediate(true)
+ .setProgramConsumer(out1)
+ .apply(b -> b.getBuilder().setSyntheticInfoConsumer(out1.contexts))
+ .compile();
+ // The total number of outputs is 8 of which 7 are synthetics in User1.
+ ClassReference user1 = Reference.classFromClass(User1.class);
+ assertEquals(8, out1.data.size());
+ assertEquals(1, out1.contexts.contextToSynthetics.size());
+ assertEquals(7, out1.contexts.contextToSynthetics.get(user1).size());
+
+ // Recompile a "shard" containing all the synthetics, but not the context.
+ PerFileCollector out2 = new PerFileCollector();
+ testForD8(parameters.getBackend())
+ .apply(
+ b ->
+ out1.contexts
+ .contextToSynthetics
+ .get(user1)
+ .forEach(synthetic -> b.addProgramDexFileData(out1.data.get(synthetic))))
+ .addClasspathClasses(CLASSES)
+ .setMinApi(parameters)
+ .setIntermediate(true)
+ .setProgramConsumer(out2)
+ .apply(b -> b.getBuilder().setSyntheticInfoConsumer(out2.contexts))
+ .compile();
+ // Again the total number of synthetics should remain 7 with no sharing taking place.
+ assertEquals(7, out2.data.size());
+
+ // Compile the remaining program inputs not compiled in the above.
+ // Note: the order of final synthetics depends on compiling to intermediates before merge.
+ Path out3 =
+ testForD8(parameters.getBackend())
+ .addProgramClasses(MiniAssert.class, TestClass.class, User2.class)
+ .setMinApi(parameters)
+ .setIntermediate(true)
+ .compile()
+ .writeToZip();
+
+ // Merge all into a final build.
+ testForD8(parameters.getBackend())
+ .addProgramDexFileData(out1.data.get(user1))
+ .addProgramDexFileData(out2.data.values())
+ .addProgramFiles(out3)
+ .setMinApi(parameters)
+ .setIntermediate(false)
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutput(EXPECTED)
+ .inspect(this::checkNoOriginalsAndNoInternalSynthetics)
+ .inspect(this::checkExpectedSynthetics);
+ }
+
static class User1 {
private static void testBooleanCompare() {