Initial API for obtaining the dependency graph for desugaring.
Bug: 138988172
Change-Id: I26d31a1d39ef1d62513242651971d73014a8aa79
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index 717a26f..4925c20 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -64,6 +64,7 @@
public static class Builder extends BaseCompilerCommand.Builder<D8Command, Builder> {
private boolean intermediate = false;
+ private DesugarGraphConsumer desugarGraphConsumer = null;
private Builder() {
this(new DefaultD8DiagnosticsHandler());
@@ -112,6 +113,21 @@
return self();
}
+ /** Get the consumer that will receive dependency information for desugaring. */
+ public DesugarGraphConsumer getDesugarGraphConsumer() {
+ return desugarGraphConsumer;
+ }
+
+ /**
+ * Set the consumer that will receive dependency information for desugaring.
+ *
+ * <p>Setting the consumer will clear any previously set consumer.
+ */
+ public Builder setDesugarGraphConsumer(DesugarGraphConsumer desugarGraphConsumer) {
+ this.desugarGraphConsumer = desugarGraphConsumer;
+ return self();
+ }
+
@Override
Builder self() {
return this;
@@ -173,13 +189,15 @@
isOptimizeMultidexForLinearAlloc(),
getSpecialLibraryConfiguration(),
getIncludeClassesChecksum(),
- getDexClassChecksumFilter());
+ getDexClassChecksumFilter(),
+ getDesugarGraphConsumer());
}
}
static final String USAGE_MESSAGE = D8CommandParser.USAGE_MESSAGE;
- private boolean intermediate = false;
+ private final boolean intermediate;
+ private final DesugarGraphConsumer desugarGraphConsumer;
public static Builder builder() {
return new Builder();
@@ -233,7 +251,8 @@
boolean optimizeMultidexForLinearAlloc,
String specialLibraryConfiguration,
boolean encodeChecksum,
- BiPredicate<String, Long> dexClassChecksumFilter) {
+ BiPredicate<String, Long> dexClassChecksumFilter,
+ DesugarGraphConsumer desugarGraphConsumer) {
super(
inputApp,
mode,
@@ -247,10 +266,13 @@
encodeChecksum,
dexClassChecksumFilter);
this.intermediate = intermediate;
+ this.desugarGraphConsumer = desugarGraphConsumer;
}
private D8Command(boolean printHelp, boolean printVersion) {
super(printHelp, printVersion);
+ intermediate = false;
+ desugarGraphConsumer = null;
}
private void configureLibraryDesugaring(InternalOptions options) {
@@ -268,6 +290,7 @@
internal.minApiLevel = getMinApiLevel();
internal.intermediate = intermediate;
internal.readCompileTimeAnnotations = intermediate;
+ internal.desugarGraphConsumer = desugarGraphConsumer;
// Assert and fixup defaults.
assert !internal.isShrinking();
diff --git a/src/main/java/com/android/tools/r8/DesugarGraphConsumer.java b/src/main/java/com/android/tools/r8/DesugarGraphConsumer.java
new file mode 100644
index 0000000..f771dd5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/DesugarGraphConsumer.java
@@ -0,0 +1,26 @@
+// 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;
+
+import com.android.tools.r8.origin.Origin;
+
+/** Consumer for receiving dependency edges for desugaring. */
+@KeepForSubclassing
+public interface DesugarGraphConsumer {
+
+ /**
+ * Callback indicating that code originating from {@code src} was used to correctly desugar some
+ * code originating from {@code dst}.
+ *
+ * <p>In other words, {@code src} is a dependency for the desugaring of {@code dst}.
+ *
+ * <p>Note: this callback may be called on multiple threads.
+ *
+ * <p>Note: this callback places no guarantees on order of calls or on duplicate calls.
+ *
+ * @param src Origin of some code input that is needed to desugar {@code dst}.
+ * @param dst Origin of some code that was dependent on code in {@code src}.
+ */
+ void accept(Origin src, Origin dst);
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java b/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java
index 044b50b..6833067 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java
@@ -186,6 +186,8 @@
} else {
DexClass superClass = appView.definitionFor(current.superType);
if (superClass != null) {
+ // TODO(b/138988172): Can we avoid traversing the full hierarchy for each type?
+ InterfaceMethodRewriter.reportDependencyEdge(superClass, current, appView);
current = superClass;
} else {
String message = "Default method desugaring of `" + clazz.toSourceString() + "` failed";
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
index a241bf9..5f49c84 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
@@ -4,6 +4,7 @@
package com.android.tools.r8.ir.desugar;
+import com.android.tools.r8.DesugarGraphConsumer;
import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.errors.Unimplemented;
import com.android.tools.r8.graph.AppInfo;
@@ -1125,7 +1126,6 @@
warnMissingInterface(classToDesugar, implementing, iface);
return helper.wrapInCollection();
}
-
if (!definedInterface.isInterface()) {
throw new CompilationError(
"Type " + iface.toSourceString() + " is referenced as an interface from `"
@@ -1140,6 +1140,12 @@
return helper.wrapInCollection();
}
+ // At this point we likely have a non-library type that may depend on default method information
+ // from its interfaces and the dependency should be reported.
+ if (!definedInterface.isLibraryClass()) {
+ reportDependencyEdge(definedInterface, implementing, appView);
+ }
+
// Merge information from all superinterfaces.
for (DexType superinterface : definedInterface.interfaces.values) {
helper.merge(getOrCreateInterfaceInfo(classToDesugar, definedInterface, superinterface));
@@ -1159,4 +1165,16 @@
return helper.wrapInCollection();
}
+
+ public static void reportDependencyEdge(
+ DexClass dependency, DexClass dependent, AppView<?> appView) {
+ DesugarGraphConsumer consumer = appView.options().desugarGraphConsumer;
+ if (consumer != null) {
+ Origin dependencyOrigin = dependency.getOrigin();
+ Origin dependentOrigin = dependent.getOrigin();
+ if (dependencyOrigin != dependentOrigin) {
+ consumer.accept(dependencyOrigin, dependentOrigin);
+ }
+ }
+ }
}
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 e4b84f6..b9dd6c6 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -9,6 +9,7 @@
import com.android.tools.r8.CompilationMode;
import com.android.tools.r8.DataResourceConsumer;
import com.android.tools.r8.DataResourceProvider;
+import com.android.tools.r8.DesugarGraphConsumer;
import com.android.tools.r8.DexFilePerClassFileConsumer;
import com.android.tools.r8.DexIndexedConsumer;
import com.android.tools.r8.ProgramConsumer;
@@ -638,6 +639,10 @@
// of the resulting program must be reported to the consumer.
public GraphConsumer mainDexKeptGraphConsumer = null;
+ // If null, no desugaring dependencies need to be provided. If non-null, each dependency between
+ // code objects needed for correct desugaring needs to be provided to the consumer.
+ public DesugarGraphConsumer desugarGraphConsumer = null;
+
public Path proguardCompatibilityRulesOutput = null;
public Consumer<List<ProguardConfigurationRule>> syntheticProguardRulesConsumer = null;
diff --git a/src/test/java/com/android/tools/r8/desugar/graph/InterfaceToImplementingClassDependencyTest.java b/src/test/java/com/android/tools/r8/desugar/graph/InterfaceToImplementingClassDependencyTest.java
new file mode 100644
index 0000000..703a237
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/graph/InterfaceToImplementingClassDependencyTest.java
@@ -0,0 +1,118 @@
+// 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.desugar.graph;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.DesugarGraphConsumer;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+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 InterfaceToImplementingClassDependencyTest extends TestBase {
+
+ // The simplest program that gives rise to a dependency edge in the graph is an interface with
+ // an implementing class. In this case, changes to the interface could require re-dexing the
+ // implementing class despite no derived changes happing to the classfile for the implementing
+ // class.
+ //
+ // For example, adding default method I.foo will trigger javac compilation of I and A to I.class
+ // and A.class, here I.class will have changed, but A.class is unchanged, thus only I.class will
+ // be compiled to dex if nothing informs about the desugaring dependency. That is incorrect,
+ // since A.dex needs to contain the desugared code for calling the default method.
+ //
+ // Note: the dependency of I for the compilation of A exists even when no default methods do.
+
+ public interface I {
+ // Emtpy.
+ }
+
+ public class A implements I {
+ // Empty.
+ }
+
+ public static class TestClass {
+
+ public static void main(String[] args) {
+ System.out.println("Hello World!");
+ }
+ }
+
+ // Test runner follows.
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimes().withAllApiLevels().build();
+ }
+
+ private final TestParameters parameters;
+
+ public InterfaceToImplementingClassDependencyTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void test() throws Exception {
+ if (parameters.isCfRuntime()) {
+ testForJvm()
+ .addProgramClasses(I.class, A.class, TestClass.class)
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutputLines("Hello World!");
+ } else {
+ MyDesugarGraphConsumer consumer = new MyDesugarGraphConsumer();
+ Origin originI = makeOrigin("I");
+ Origin originA = makeOrigin("A");
+ testForD8()
+ .setMinApi(parameters.getApiLevel())
+ .addProgramClasses(TestClass.class)
+ .apply(
+ b ->
+ b.getBuilder()
+ .setDesugarGraphConsumer(consumer)
+ .addClassProgramData(ToolHelper.getClassAsBytes(I.class), originI)
+ .addClassProgramData(ToolHelper.getClassAsBytes(A.class), originA))
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutputLines("Hello World!");
+ // If API level indicates desugaring is needed check the edges are reported.
+ if (parameters.getApiLevel().getLevel() < AndroidApiLevel.N.getLevel()) {
+ assertEquals(1, consumer.edges.size());
+ assertEquals(originI, consumer.edges.keySet().iterator().next());
+ assertEquals(originA, consumer.edges.values().iterator().next().iterator().next());
+ } else {
+ assertEquals(0, consumer.edges.size());
+ }
+ }
+ }
+
+ private Origin makeOrigin(String name) {
+ return new Origin(Origin.root()) {
+ @Override
+ public String part() {
+ return name;
+ }
+ };
+ }
+
+ public static class MyDesugarGraphConsumer implements DesugarGraphConsumer {
+
+ Map<Origin, Set<Origin>> edges = new HashMap<>();
+
+ @Override
+ public synchronized void accept(Origin src, Origin dst) {
+ edges.computeIfAbsent(src, s -> new HashSet<>()).add(dst);
+ }
+ }
+}