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 c57bc22..52735a5 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -324,6 +324,8 @@
 
       timing.end(); // post-converter
 
+      reportSyntheticInformation(appView);
+
       if (options.isGeneratingClassFiles()) {
         new CfApplicationWriter(appView, marker).write(options.getClassFileConsumer(), inputApp);
       } else {
@@ -342,6 +344,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 13c12a2..d1495c2 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -90,6 +90,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;
@@ -268,6 +269,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.
@@ -456,6 +472,7 @@
           getIncludeClassesChecksum(),
           getDexClassChecksumFilter(),
           getDesugarGraphConsumer(),
+          getSyntheticInfoConsumer(),
           desugaredLibraryKeepRuleConsumer,
           desugaredLibrarySpecification,
           getAssertionsConfiguration(),
@@ -479,6 +496,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;
@@ -549,6 +567,7 @@
       boolean encodeChecksum,
       BiPredicate<String, Long> dexClassChecksumFilter,
       DesugarGraphConsumer desugarGraphConsumer,
+      SyntheticInfoConsumer syntheticInfoConsumer,
       StringConsumer desugaredLibraryKeepRuleConsumer,
       DesugaredLibrarySpecification desugaredLibrarySpecification,
       List<AssertionsConfiguration> assertionsConfiguration,
@@ -590,6 +609,7 @@
         classConflictResolver);
     this.intermediate = intermediate;
     this.globalSyntheticsConsumer = globalSyntheticsConsumer;
+    this.syntheticInfoConsumer = syntheticInfoConsumer;
     this.desugarGraphConsumer = desugarGraphConsumer;
     this.desugaredLibraryKeepRuleConsumer = desugaredLibraryKeepRuleConsumer;
     this.desugaredLibrarySpecification = desugaredLibrarySpecification;
@@ -606,6 +626,7 @@
     super(printHelp, printVersion);
     intermediate = false;
     globalSyntheticsConsumer = null;
+    syntheticInfoConsumer = null;
     desugarGraphConsumer = null;
     desugaredLibraryKeepRuleConsumer = null;
     desugaredLibrarySpecification = null;
@@ -641,6 +662,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 44f53b1..efe5d62 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 49746f4..3bbb9ae 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;
@@ -1169,4 +1173,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 d3f4b4c..0d7a71c 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;
@@ -181,6 +182,7 @@
   public ProgramClassConflictResolver programClassConflictResolver = null;
 
   private GlobalSyntheticsConsumer globalSyntheticsConsumer = null;
+  private SyntheticInfoConsumer syntheticInfoConsumer = null;
 
   public DataResourceConsumer dataResourceConsumer;
   public FeatureSplitConfiguration featureSplitConfiguration;
@@ -517,6 +519,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 f4ef14b..c2d4a30 100644
--- a/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java
+++ b/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java
@@ -13,7 +13,6 @@
 import com.android.tools.r8.compilerapi.assertionconfiguration.AssertionConfigurationTest;
 import com.android.tools.r8.compilerapi.classconflictresolver.ClassConflictResolverTest;
 import com.android.tools.r8.compilerapi.desugardependencies.DesugarDependenciesTest;
-import com.android.tools.r8.compilerapi.dexconsumers.PerClassSyntheticContextsTest;
 import com.android.tools.r8.compilerapi.diagnostics.ProguardKeepRuleDiagnosticsApiTest;
 import com.android.tools.r8.compilerapi.diagnostics.UnsupportedFeaturesDiagnosticApiTest;
 import com.android.tools.r8.compilerapi.globalsynthetics.GlobalSyntheticsTest;
@@ -24,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;
@@ -60,7 +60,7 @@
           StartupProfileApiTest.ApiTest.class,
           ClassConflictResolverTest.ApiTest.class,
           ProguardKeepRuleDiagnosticsApiTest.ApiTest.class,
-          PerClassSyntheticContextsTest.ApiTest.class);
+          SyntheticContextsConsumerTest.ApiTest.class);
 
   private final TemporaryFolder temp;
 
diff --git a/src/test/java/com/android/tools/r8/compilerapi/dexconsumers/PerClassSyntheticContextsTest.java b/src/test/java/com/android/tools/r8/compilerapi/syntheticscontexts/SyntheticContextsConsumerTest.java
similarity index 73%
rename from src/test/java/com/android/tools/r8/compilerapi/dexconsumers/PerClassSyntheticContextsTest.java
rename to src/test/java/com/android/tools/r8/compilerapi/syntheticscontexts/SyntheticContextsConsumerTest.java
index 7172b17..f596594 100644
--- a/src/test/java/com/android/tools/r8/compilerapi/dexconsumers/PerClassSyntheticContextsTest.java
+++ b/src/test/java/com/android/tools/r8/compilerapi/syntheticscontexts/SyntheticContextsConsumerTest.java
@@ -1,9 +1,9 @@
 // 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.dexconsumers;
+package com.android.tools.r8.compilerapi.syntheticscontexts;
 
-import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertEquals;
 
 import com.android.tools.r8.ByteDataView;
 import com.android.tools.r8.ClassFileConsumer;
@@ -11,6 +11,8 @@
 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;
@@ -20,12 +22,13 @@
 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 PerClassSyntheticContextsTest extends CompilerApiTestRunner {
+public class SyntheticContextsConsumerTest extends CompilerApiTestRunner {
 
-  public PerClassSyntheticContextsTest(TestParameters parameters) {
+  public SyntheticContextsConsumerTest(TestParameters parameters) {
     super(parameters);
   }
 
@@ -60,10 +63,7 @@
     new ApiTest(ApiTest.PARAMETERS)
         .run(
             outputs.get(backport.getDescriptor()),
-            context -> {
-              // TODO(b/241351268): This should be the UsesBackport class as context.
-              assertNull(context);
-            });
+            context -> assertEquals(descriptor(UsesBackport.class), context));
   }
 
   public static class UsesBackport {
@@ -79,11 +79,26 @@
     }
 
     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() {
 
@@ -93,8 +108,7 @@
                         ByteDataView data,
                         Set<String> descriptors,
                         DiagnosticsHandler handler) {
-                      // TODO(b/241351268): Inform the caller of the context once possible.
-                      syntheticContext.accept(null);
+                      syntheticContext.accept(synthetic2context.get(primaryClassDescriptor));
                     }
 
                     @Override