Add an API to provide information about synthetics.

The synthetic information API can be used to track synthetic
intermediates during builds to ensure that subsequent any
non-intermediate merge step can provide the full set of synthetic
intermediates in the compilation unit.

Bug: b/241351268
Change-Id: Ibcb8aec7ce59c5729ea1308809175e0336c79575
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index f578416..7b2351b 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -330,6 +330,8 @@
 
       timing.end(); // post-converter
 
+      reportSyntheticInformation(appView);
+
       if (options.isGeneratingClassFiles()) {
         new CfApplicationWriter(appView, marker).write(options.getClassFileConsumer(), inputApp);
       } else {
@@ -348,6 +350,15 @@
     }
   }
 
+  private static void reportSyntheticInformation(AppView<?> appView) {
+    SyntheticInfoConsumer consumer = appView.options().getSyntheticInfoConsumer();
+    if (consumer == null || !appView.options().intermediate) {
+      return;
+    }
+    appView.getSyntheticItems().reportSyntheticsInformation(consumer);
+    consumer.finished();
+  }
+
   private static void initializeAssumeInfoCollection(AppView<AppInfo> appView) {
     AssumeInfoCollection.Builder assumeInfoCollectionBuilder = AssumeInfoCollection.builder();
     AbstractValueFactory abstractValueFactory = appView.abstractValueFactory();
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index ab7a46f..276c60c 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -89,6 +89,7 @@
     private List<GlobalSyntheticsResourceProvider> globalSyntheticsResourceProviders =
         new ArrayList<>();
     private DesugarGraphConsumer desugarGraphConsumer = null;
+    private SyntheticInfoConsumer syntheticInfoConsumer = null;
     private StringConsumer desugaredLibraryKeepRuleConsumer = null;
     private String synthesizedClassPrefix = "";
     private boolean enableMainDexListCheck = true;
@@ -267,6 +268,21 @@
       return self();
     }
 
+    /** Get the consumer that will receive information about compiler synthesized classes. */
+    public SyntheticInfoConsumer getSyntheticInfoConsumer() {
+      return syntheticInfoConsumer;
+    }
+
+    /**
+     * Set the consumer that will receive information about compiler synthesized classes.
+     *
+     * <p>Setting the consumer will clear any previously set consumer.
+     */
+    public Builder setSyntheticInfoConsumer(SyntheticInfoConsumer syntheticInfoConsumer) {
+      this.syntheticInfoConsumer = syntheticInfoConsumer;
+      return self();
+    }
+
     /**
      * Add a collection of startup profile providers that should be used for distributing the
      * program classes in dex.
@@ -450,6 +466,7 @@
           getIncludeClassesChecksum(),
           getDexClassChecksumFilter(),
           getDesugarGraphConsumer(),
+          getSyntheticInfoConsumer(),
           desugaredLibraryKeepRuleConsumer,
           desugaredLibrarySpecification,
           getAssertionsConfiguration(),
@@ -473,6 +490,7 @@
 
   private final boolean intermediate;
   private final GlobalSyntheticsConsumer globalSyntheticsConsumer;
+  private final SyntheticInfoConsumer syntheticInfoConsumer;
   private final DesugarGraphConsumer desugarGraphConsumer;
   private final StringConsumer desugaredLibraryKeepRuleConsumer;
   private final DesugaredLibrarySpecification desugaredLibrarySpecification;
@@ -543,6 +561,7 @@
       boolean encodeChecksum,
       BiPredicate<String, Long> dexClassChecksumFilter,
       DesugarGraphConsumer desugarGraphConsumer,
+      SyntheticInfoConsumer syntheticInfoConsumer,
       StringConsumer desugaredLibraryKeepRuleConsumer,
       DesugaredLibrarySpecification desugaredLibrarySpecification,
       List<AssertionsConfiguration> assertionsConfiguration,
@@ -584,6 +603,7 @@
         classConflictResolver);
     this.intermediate = intermediate;
     this.globalSyntheticsConsumer = globalSyntheticsConsumer;
+    this.syntheticInfoConsumer = syntheticInfoConsumer;
     this.desugarGraphConsumer = desugarGraphConsumer;
     this.desugaredLibraryKeepRuleConsumer = desugaredLibraryKeepRuleConsumer;
     this.desugaredLibrarySpecification = desugaredLibrarySpecification;
@@ -600,6 +620,7 @@
     super(printHelp, printVersion);
     intermediate = false;
     globalSyntheticsConsumer = null;
+    syntheticInfoConsumer = null;
     desugarGraphConsumer = null;
     desugaredLibraryKeepRuleConsumer = null;
     desugaredLibrarySpecification = null;
@@ -635,6 +656,7 @@
     internal.intermediate = intermediate;
     internal.retainCompileTimeAnnotations = intermediate;
     internal.setGlobalSyntheticsConsumer(globalSyntheticsConsumer);
+    internal.setSyntheticInfoConsumer(syntheticInfoConsumer);
     internal.desugarGraphConsumer = desugarGraphConsumer;
     internal.mainDexKeepRules = mainDexKeepRules;
     internal.proguardMapConsumer = proguardMapConsumer;
diff --git a/src/main/java/com/android/tools/r8/SyntheticInfoConsumer.java b/src/main/java/com/android/tools/r8/SyntheticInfoConsumer.java
new file mode 100644
index 0000000..2bfc5b2
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/SyntheticInfoConsumer.java
@@ -0,0 +1,34 @@
+// 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;
+
+@Keep
+public interface SyntheticInfoConsumer {
+
+  /**
+   * Callback with information about a compiler synthesized class.
+   *
+   * <p>This callback is only used in intermediate mode builds where the compiler may synthesize
+   * intermediate classes that can be de-duplicated and merged in a later non-incremental step.
+   *
+   * <p>This callback will always be called before the synthetic class is included in the data to
+   * any compiler outputs. Thus, it is safe to assume the information provided here is present when
+   * any compiler output consumers are called for the synthetic class. E.g., in the callbacks of
+   * {@link ClassFileConsumer} or {@link DexFilePerClassFileConsumer}.
+   *
+   * <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 data Information about the synthetic class.
+   */
+  void acceptSyntheticInfo(SyntheticInfoConsumerData data);
+
+  /**
+   * Callback indicating no more synthetics will be generated for the active compilation unit.
+   *
+   * <p>Note: this callback places no other guarantees on number of calls or on which threads.
+   */
+  void finished();
+}
diff --git a/src/main/java/com/android/tools/r8/SyntheticInfoConsumerData.java b/src/main/java/com/android/tools/r8/SyntheticInfoConsumerData.java
new file mode 100644
index 0000000..be28596
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/SyntheticInfoConsumerData.java
@@ -0,0 +1,17 @@
+// 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;
+
+import com.android.tools.r8.references.ClassReference;
+
+/** Information about a compiler synthesized class. */
+@Keep
+public interface SyntheticInfoConsumerData {
+
+  /** Get the reference for the compiler synthesized class. */
+  ClassReference getSyntheticClass();
+
+  /** Get the reference for the context that gave rise to the synthesized class. */
+  ClassReference getSynthesizingContextClass();
+}
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
index 045e2df..596b8aa 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
@@ -66,9 +66,6 @@
 
 public class SyntheticFinalization {
 
-  // TODO(b/237413146): Implement a non-quadratic grouping algorithm.
-  private static final int GROUP_COUNT_THRESHOLD = 10;
-
   public static class Result {
     public final CommittedItems commit;
     public final NonIdentityGraphLens lens;
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
index 619ffc9..2a6b03a 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
@@ -6,6 +6,8 @@
 import static com.android.tools.r8.utils.ConsumerUtils.emptyConsumer;
 
 import com.android.tools.r8.FeatureSplit;
+import com.android.tools.r8.SyntheticInfoConsumer;
+import com.android.tools.r8.SyntheticInfoConsumerData;
 import com.android.tools.r8.contexts.CompilationContext.UniqueContext;
 import com.android.tools.r8.errors.MissingGlobalSyntheticsConsumerDiagnostic;
 import com.android.tools.r8.errors.Unreachable;
@@ -38,6 +40,8 @@
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.synthesis.SyntheticFinalization.Result;
 import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
 import com.android.tools.r8.utils.Box;
@@ -1138,4 +1142,40 @@
     return new SyntheticFinalization(appView.options(), this, committed)
         .computeFinalSynthetics(appView, timing);
   }
+
+  public void reportSyntheticsInformation(SyntheticInfoConsumer consumer) {
+    assert isFinalized();
+    Map<DexType, DexType> seen = new IdentityHashMap<>();
+    committed.forEachItem(
+        ref -> {
+          DexType holder = ref.getHolder();
+          DexType context = ref.getContext().getSynthesizingContextType();
+          DexType old = seen.put(holder, context);
+          assert old == null || old == context;
+          if (old == null) {
+            consumer.acceptSyntheticInfo(new SyntheticInfoConsumerDataImpl(holder, context));
+          }
+        });
+  }
+
+  private static class SyntheticInfoConsumerDataImpl implements SyntheticInfoConsumerData {
+
+    private final DexType holder;
+    private final DexType context;
+
+    public SyntheticInfoConsumerDataImpl(DexType holder, DexType context) {
+      this.holder = holder;
+      this.context = context;
+    }
+
+    @Override
+    public ClassReference getSyntheticClass() {
+      return Reference.classFromDescriptor(holder.toDescriptorString());
+    }
+
+    @Override
+    public ClassReference getSynthesizingContextClass() {
+      return Reference.classFromDescriptor(context.toDescriptorString());
+    }
+  }
 }
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 e6c92f3..e1be5bc 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.ProgramConsumer;
 import com.android.tools.r8.SourceFileProvider;
 import com.android.tools.r8.StringConsumer;
+import com.android.tools.r8.SyntheticInfoConsumer;
 import com.android.tools.r8.Version;
 import com.android.tools.r8.androidapi.ComputedApiLevel;
 import com.android.tools.r8.cf.CfVersion;
@@ -180,6 +181,7 @@
   public ProgramClassConflictResolver programClassConflictResolver = null;
 
   private GlobalSyntheticsConsumer globalSyntheticsConsumer = null;
+  private SyntheticInfoConsumer syntheticInfoConsumer = null;
 
   public DataResourceConsumer dataResourceConsumer;
   public FeatureSplitConfiguration featureSplitConfiguration;
@@ -518,6 +520,14 @@
     this.globalSyntheticsConsumer = globalSyntheticsConsumer;
   }
 
+  public void setSyntheticInfoConsumer(SyntheticInfoConsumer syntheticInfoConsumer) {
+    this.syntheticInfoConsumer = syntheticInfoConsumer;
+  }
+
+  public SyntheticInfoConsumer getSyntheticInfoConsumer() {
+    return syntheticInfoConsumer;
+  }
+
   public boolean isDesugaredLibraryCompilation() {
     return machineDesugaredLibrarySpecification.isLibraryCompilation();
   }
diff --git a/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTest.java b/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTest.java
index 7a10578..60ca0c6 100644
--- a/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTest.java
+++ b/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTest.java
@@ -80,8 +80,17 @@
     return PostStartupMockClass.class;
   }
 
+  public static Path getProjectRoot() {
+    String userDirProperty = System.getProperty("user.dir");
+    if (userDirProperty.endsWith("d8_r8/test")) {
+      return Paths.get(userDirProperty).getParent().getParent();
+    }
+    return Paths.get("");
+  }
+
   public Path getJava8RuntimeJar() {
-    return Paths.get("third_party", "openjdk", "openjdk-rt-1.8", "rt.jar");
+    return getProjectRoot()
+        .resolve(Paths.get("third_party", "openjdk", "openjdk-rt-1.8", "rt.jar"));
   }
 
   public List<String> getKeepMainRules(Class<?> clazz) {
diff --git a/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java b/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java
index fd3e16d..c2d4a30 100644
--- a/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java
+++ b/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java
@@ -23,6 +23,7 @@
 import com.android.tools.r8.compilerapi.mockdata.PostStartupMockClass;
 import com.android.tools.r8.compilerapi.sourcefile.CustomSourceFileTest;
 import com.android.tools.r8.compilerapi.startupprofile.StartupProfileApiTest;
+import com.android.tools.r8.compilerapi.syntheticscontexts.SyntheticContextsConsumerTest;
 import com.android.tools.r8.compilerapi.testsetup.ApiTestingSetUpTest;
 import com.android.tools.r8.compilerapi.wrappers.CommandLineParserTest;
 import com.android.tools.r8.compilerapi.wrappers.EnableMissingLibraryApiModelingTest;
@@ -58,7 +59,8 @@
           ArtProfilesForRewritingApiTest.ApiTest.class,
           StartupProfileApiTest.ApiTest.class,
           ClassConflictResolverTest.ApiTest.class,
-          ProguardKeepRuleDiagnosticsApiTest.ApiTest.class);
+          ProguardKeepRuleDiagnosticsApiTest.ApiTest.class,
+          SyntheticContextsConsumerTest.ApiTest.class);
 
   private final TemporaryFolder temp;
 
diff --git a/src/test/java/com/android/tools/r8/compilerapi/syntheticscontexts/SyntheticContextsConsumerTest.java b/src/test/java/com/android/tools/r8/compilerapi/syntheticscontexts/SyntheticContextsConsumerTest.java
new file mode 100644
index 0000000..f596594
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/compilerapi/syntheticscontexts/SyntheticContextsConsumerTest.java
@@ -0,0 +1,134 @@
+// 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.compilerapi.syntheticscontexts;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.ByteDataView;
+import com.android.tools.r8.ClassFileConsumer;
+import com.android.tools.r8.D8;
+import com.android.tools.r8.D8Command;
+import com.android.tools.r8.DexFilePerClassFileConsumer;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.SyntheticInfoConsumer;
+import com.android.tools.r8.SyntheticInfoConsumerData;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.compilerapi.CompilerApiTest;
+import com.android.tools.r8.compilerapi.CompilerApiTestRunner;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
+import org.junit.Test;
+
+public class SyntheticContextsConsumerTest extends CompilerApiTestRunner {
+
+  public SyntheticContextsConsumerTest(TestParameters parameters) {
+    super(parameters);
+  }
+
+  @Override
+  public Class<? extends CompilerApiTest> binaryTestClass() {
+    return ApiTest.class;
+  }
+
+  @Test
+  public void test() throws Exception {
+    // First compile to CF such that we have an input class that has a synthetic context.
+    ClassReference backport = SyntheticItemsTestUtils.syntheticBackportClass(UsesBackport.class, 0);
+    Map<String, byte[]> outputs = new HashMap<>();
+    testForD8(Backend.CF)
+        .addProgramClasses(UsesBackport.class)
+        .setIntermediate(true)
+        .setMinApi(1)
+        .setProgramConsumer(
+            new ClassFileConsumer() {
+
+              @Override
+              public void accept(ByteDataView data, String descriptor, DiagnosticsHandler handler) {
+                outputs.put(descriptor, data.copyByteData());
+              }
+
+              @Override
+              public void finished(DiagnosticsHandler handler) {}
+            })
+        .compile()
+        .writeToZip();
+    // Run using the API test to obtain the backport context.
+    new ApiTest(ApiTest.PARAMETERS)
+        .run(
+            outputs.get(backport.getDescriptor()),
+            context -> assertEquals(descriptor(UsesBackport.class), context));
+  }
+
+  public static class UsesBackport {
+    public static void foo() {
+      Boolean.compare(true, false);
+    }
+  }
+
+  public static class ApiTest extends CompilerApiTest {
+
+    public ApiTest(Object parameters) {
+      super(parameters);
+    }
+
+    public void run(byte[] input, Consumer<String> syntheticContext) throws Exception {
+      Map<String, String> synthetic2context = new ConcurrentHashMap<>();
+      D8.run(
+          D8Command.builder()
+              .addClassProgramData(input, Origin.unknown())
+              .addLibraryFiles(getJava8RuntimeJar())
+              .setMinApiLevel(1)
+              .setSyntheticInfoConsumer(
+                  new SyntheticInfoConsumer() {
+                    @Override
+                    public void acceptSyntheticInfo(SyntheticInfoConsumerData data) {
+                      synthetic2context.put(
+                          data.getSyntheticClass().getDescriptor(),
+                          data.getSynthesizingContextClass().getDescriptor());
+                    }
+
+                    @Override
+                    public void finished() {
+                      // nothing to finish up.
+                    }
+                  })
+              .setProgramConsumer(
+                  new DexFilePerClassFileConsumer() {
+
+                    @Override
+                    public void accept(
+                        String primaryClassDescriptor,
+                        ByteDataView data,
+                        Set<String> descriptors,
+                        DiagnosticsHandler handler) {
+                      syntheticContext.accept(synthetic2context.get(primaryClassDescriptor));
+                    }
+
+                    @Override
+                    public void finished(DiagnosticsHandler handler) {
+                      // nothing to finish up.
+                    }
+                  })
+              .build());
+    }
+
+    @Test
+    public void test() throws Exception {
+      byte[] input = getBytesForClass(getMockClass());
+      run(
+          input,
+          context -> {
+            if (context != null) {
+              throw new RuntimeException("unexpected");
+            }
+          });
+    }
+  }
+}
diff --git a/third_party/binary_compatibility_tests/compiler_api_tests.tar.gz.sha1 b/third_party/binary_compatibility_tests/compiler_api_tests.tar.gz.sha1
index dc12e2c..045eba0 100644
--- a/third_party/binary_compatibility_tests/compiler_api_tests.tar.gz.sha1
+++ b/third_party/binary_compatibility_tests/compiler_api_tests.tar.gz.sha1
@@ -1 +1 @@
-35ed1547b79e22eb4b933b6deb0f929d7ff8aebc
\ No newline at end of file
+c0f7efc9b7e87e1ab045bb95e661507fe2eb0371
\ No newline at end of file