Allow repeated intermediate compilations with synthetics.

This introduces partial support for compiling intermediate artifacts
multiple times and introducing additional synthetics derived from
previous synthetics in a hygienic way. Prior to this the compiler
would possibly fail on such inputs.

Note that the repeated compilations are not yet safe as the
introduction of synthetics from the same non-synthetic class at
multiple steps is not hygienic and may create distinct duplicate
classes.

See RepeatedCompilationSyntheticsTest.

Bug: b/241351268
Bug: b/271235788
Change-Id: Ife3af6ad96624fb322b655e642b6dc72568e59b2
diff --git a/src/main/java/com/android/tools/r8/DexFilePerClassFileConsumerData.java b/src/main/java/com/android/tools/r8/DexFilePerClassFileConsumerData.java
index e93aa0c..fba6b8d 100644
--- a/src/main/java/com/android/tools/r8/DexFilePerClassFileConsumerData.java
+++ b/src/main/java/com/android/tools/r8/DexFilePerClassFileConsumerData.java
@@ -23,4 +23,13 @@
 
   /** Diagnostics handler for reporting. */
   DiagnosticsHandler getDiagnosticsHandler();
+
+  /**
+   * Class descriptor of the primary-class's synthetic context.
+   *
+   * <p>If primary class is a compiler-synthesized class (i.e. it is an input that was synthesized
+   * by a prior D8 intermediate compilation) then the value is the descriptor of the class that
+   * caused the primary class to be synthesized. The value is null in all other cases.
+   */
+  String getSynthesizingContextForPrimaryClass();
 }
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
index e9aab77..d0daf58 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -626,6 +626,7 @@
           .acceptDexFile(
               new DexFilePerClassFileConsumerDataImpl(
                   primaryClassDescriptor,
+                  virtualFile.getPrimaryClassSynthesizingContextDescriptor(),
                   data,
                   virtualFile.getClassDescriptors(),
                   options.reporter));
diff --git a/src/main/java/com/android/tools/r8/dex/VirtualFile.java b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
index 6d1af5d..692192d 100644
--- a/src/main/java/com/android/tools/r8/dex/VirtualFile.java
+++ b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
@@ -49,7 +49,6 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.IdentityHashMap;
 import java.util.Iterator;
@@ -73,6 +72,7 @@
   private final StartupOrder startupOrder;
 
   private final DexString primaryClassDescriptor;
+  private final DexString primaryClassSynthesizingContextDescriptor;
   private DebugRepresentation debugRepresentation;
 
   VirtualFile(int id, AppView<?> appView) {
@@ -102,12 +102,23 @@
     this.id = id;
     this.indexedItems = new VirtualFileIndexedItemCollection(appView);
     this.transaction = new IndexedItemTransaction(indexedItems, appView);
-    this.primaryClassDescriptor =
-        primaryClass == null
-            ? null
-            : appView.getNamingLens().lookupClassDescriptor(primaryClass.type);
     this.featureSplit = featureSplit;
     this.startupOrder = startupOrder;
+    if (primaryClass == null) {
+      primaryClassDescriptor = null;
+      primaryClassSynthesizingContextDescriptor = null;
+    } else {
+      DexType type = primaryClass.getType();
+      primaryClassDescriptor = appView.getNamingLens().lookupClassDescriptor(type);
+      Collection<DexType> contexts = appView.getSyntheticItems().getSynthesizingContextTypes(type);
+      if (contexts.size() == 1) {
+        primaryClassSynthesizingContextDescriptor =
+            appView.getNamingLens().lookupClassDescriptor(contexts.iterator().next());
+      } else {
+        assert contexts.isEmpty();
+        primaryClassSynthesizingContextDescriptor = null;
+      }
+    }
   }
 
   public int getId() {
@@ -135,6 +146,12 @@
     return primaryClassDescriptor == null ? null : primaryClassDescriptor.toString();
   }
 
+  public String getPrimaryClassSynthesizingContextDescriptor() {
+    return primaryClassSynthesizingContextDescriptor == null
+        ? null
+        : primaryClassSynthesizingContextDescriptor.toString();
+  }
+
   public void setDebugRepresentation(DebugRepresentation debugRepresentation) {
     assert debugRepresentation != null;
     assert this.debugRepresentation == null;
@@ -319,35 +336,36 @@
 
     @Override
     public List<VirtualFile> run() {
-      HashMap<DexProgramClass, VirtualFile> files = new HashMap<>();
-      Collection<DexProgramClass> synthetics = new ArrayList<>();
+      Map<DexType, VirtualFile> files = new IdentityHashMap<>();
+      Map<DexType, List<DexProgramClass>> derivedSynthetics = new LinkedHashMap<>();
       // Assign dedicated virtual files for all program classes.
       for (DexProgramClass clazz : classes) {
-        // TODO(b/181636450): Simplify this making use of the assumption that synthetics are never
-        //  duplicated.
-        if (!combineSyntheticClassesWithPrimaryClass
-            || !appView.getSyntheticItems().isSyntheticClass(clazz)) {
-          VirtualFile file = new VirtualFile(virtualFiles.size(), appView, clazz);
-          virtualFiles.add(file);
-          file.addClass(clazz);
-          files.put(clazz, file);
-          // Commit this early, so that we do not keep the transaction state around longer than
-          // needed and clear the underlying sets.
-          file.commitTransaction();
-        } else {
-          synthetics.add(clazz);
+        if (combineSyntheticClassesWithPrimaryClass) {
+          DexType inputContextType =
+              appView
+                  .getSyntheticItems()
+                  .getSynthesizingInputContext(clazz.getType(), appView.options());
+          if (inputContextType != null && inputContextType != clazz.getType()) {
+            derivedSynthetics.computeIfAbsent(inputContextType, k -> new ArrayList<>()).add(clazz);
+            continue;
+          }
         }
-      }
-      for (DexProgramClass synthetic : synthetics) {
-        Collection<DexType> synthesizingContexts =
-            appView.getSyntheticItems().getSynthesizingContextTypes(synthetic.getType());
-        assert synthesizingContexts.size() == 1;
-        DexProgramClass inputType =
-            appView.definitionForProgramType(synthesizingContexts.iterator().next());
-        VirtualFile file = files.get(inputType);
-        file.addClass(synthetic);
+        VirtualFile file = new VirtualFile(virtualFiles.size(), appView, clazz);
+        virtualFiles.add(file);
+        file.addClass(clazz);
+        files.put(clazz.getType(), file);
+        // Commit this early, so that we do not keep the transaction state around longer than
+        // needed and clear the underlying sets.
         file.commitTransaction();
       }
+      derivedSynthetics.forEach(
+          (inputContextType, synthetics) -> {
+            VirtualFile file = files.get(inputContextType);
+            for (DexProgramClass synthetic : synthetics) {
+              file.addClass(synthetic);
+              file.commitTransaction();
+            }
+          });
       return virtualFiles;
     }
   }
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfo.java b/src/main/java/com/android/tools/r8/graph/AppInfo.java
index e3cb622..d480e3f 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfo.java
@@ -88,7 +88,7 @@
     return new AppInfo(app, syntheticItems, mainDexInfo, new BooleanBox());
   }
 
-  protected InternalOptions options() {
+  public InternalOptions options() {
     return app.options;
   }
 
@@ -188,20 +188,19 @@
       return;
     }
     Origin dependencyOrigin = dependency.getOrigin();
-    java.util.Collection<DexType> dependents =
-        getSyntheticItems().getSynthesizingContextTypes(dependent.getType());
-    if (dependents.isEmpty()) {
-      reportDependencyEdge(consumer, dependencyOrigin, dependent);
+    Collection<Origin> dependentOrigins =
+        getSyntheticItems().getSynthesizingOrigin(dependent.getType());
+    if (dependentOrigins.isEmpty()) {
+      reportDependencyEdge(consumer, dependencyOrigin, dependent.getOrigin());
     } else {
-      for (DexType type : dependents) {
-        reportDependencyEdge(consumer, dependencyOrigin, definitionFor(type));
+      for (Origin dependentOrigin : dependentOrigins) {
+        reportDependencyEdge(consumer, dependencyOrigin, dependentOrigin);
       }
     }
   }
 
   private void reportDependencyEdge(
-      DesugarGraphConsumer consumer, Origin dependencyOrigin, DexClass clazz) {
-    Origin dependentOrigin = clazz.getOrigin();
+      DesugarGraphConsumer consumer, Origin dependencyOrigin, Origin dependentOrigin) {
     if (dependencyOrigin == GlobalSyntheticOrigin.instance()
         || dependentOrigin == GlobalSyntheticOrigin.instance()) {
       // D8/R8 does not report edges to synthetic classes that D8/R8 generates.
diff --git a/src/main/java/com/android/tools/r8/synthesis/SynthesizingContext.java b/src/main/java/com/android/tools/r8/synthesis/SynthesizingContext.java
index 91d352f..e92442b 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SynthesizingContext.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SynthesizingContext.java
@@ -97,6 +97,10 @@
     this.featureSplit = featureSplit;
   }
 
+  public boolean isSyntheticInputClass() {
+    return synthesizingContextType != inputContextType;
+  }
+
   @Override
   public int compareTo(SynthesizingContext other) {
     return Comparator
@@ -104,6 +108,7 @@
         // choose the context prefix for items.
         .comparing(SynthesizingContext::getSynthesizingContextType)
         // To ensure that equals coincides with compareTo == 0, we then compare 'type'.
+        // Also, the input type context is used as the hygienic prefix in intermediate modes.
         .thenComparing(c -> c.inputContextType)
         .compare(this, other);
   }
@@ -112,6 +117,10 @@
     return synthesizingContextType;
   }
 
+  DexType getSynthesizingInputContext(boolean intermediate) {
+    return intermediate ? inputContextType : getSynthesizingContextType();
+  }
+
   Origin getInputContextOrigin() {
     return inputContextOrigin;
   }
@@ -121,14 +130,14 @@
   }
 
   SynthesizingContext rewrite(NonIdentityGraphLens lens) {
-    DexType rewrittenInputeContextType = lens.lookupType(inputContextType);
+    DexType rewrittenInputContextType = lens.lookupType(inputContextType);
     DexType rewrittenSynthesizingContextType = lens.lookupType(synthesizingContextType);
-    return rewrittenInputeContextType == inputContextType
+    return rewrittenInputContextType == inputContextType
             && rewrittenSynthesizingContextType == synthesizingContextType
         ? this
         : new SynthesizingContext(
             rewrittenSynthesizingContextType,
-            rewrittenInputeContextType,
+            rewrittenInputContextType,
             inputContextOrigin,
             featureSplit);
   }
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 097df5d..1a75811 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.synthesis;
 
 import com.android.tools.r8.features.ClassToFeatureSplitMap;
+import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GraphLens;
@@ -58,7 +59,13 @@
     return context;
   }
 
-  final String getPrefixForExternalSyntheticType() {
+  final String getPrefixForExternalSyntheticType(AppView<?> appView) {
+    if (!appView.options().intermediate && context.isSyntheticInputClass() && !kind.isGlobal()) {
+      // If the input class was a synthetic and the build is non-intermediate, unwind the synthetic
+      // name back to the original context (if present in the textual type).
+      return SyntheticNaming.getOuterContextFromExternalSyntheticType(
+          getKind(), getHolder().getType());
+    }
     return SyntheticNaming.getPrefixForExternalSyntheticType(getKind(), getHolder().getType());
   }
 
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 8cf1e77..44f53b1 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
@@ -218,7 +218,7 @@
   }
 
   Result computeFinalSynthetics(AppView<?> appView, Timing timing) {
-    assert verifyNoNestedSynthetics(appView.dexItemFactory());
+    assert verifyNoNestedSynthetics(appView);
     assert verifyOneSyntheticPerSyntheticClass();
     DexApplication application;
     Builder lensBuilder = new Builder();
@@ -334,7 +334,7 @@
     return !committed.containsType(type);
   }
 
-  private boolean verifyNoNestedSynthetics(DexItemFactory dexItemFactory) {
+  private boolean verifyNoNestedSynthetics(AppView<?> appView) {
     // Check that the prefix of each synthetic is never itself synthetic.
     committed.forEachItem(
         item -> {
@@ -345,8 +345,12 @@
               SyntheticNaming.getPrefixForExternalSyntheticType(item.getKind(), item.getHolder());
           assert !prefix.contains(SyntheticNaming.getPhaseSeparator(Phase.INTERNAL));
           DexType context =
-              dexItemFactory.createType(DescriptorUtils.getDescriptorFromClassBinaryName(prefix));
-          assert isNotSyntheticType(context) || synthetics.isGlobalSyntheticClass(context);
+              appView
+                  .dexItemFactory()
+                  .createType(DescriptorUtils.getDescriptorFromClassBinaryName(prefix));
+          assert isNotSyntheticType(context)
+              || item.getContext().isSyntheticInputClass()
+              || synthetics.isGlobalSyntheticClass(context);
         });
     return true;
   }
@@ -599,7 +603,7 @@
             } else {
               groupsPerPrefix
                   .computeIfAbsent(
-                      group.getRepresentative().getPrefixForExternalSyntheticType(),
+                      group.getRepresentative().getPrefixForExternalSyntheticType(appView),
                       k -> new ArrayList<>())
                   .add(group);
             }
@@ -615,7 +619,7 @@
             EquivalenceGroup<T> group = groups.get(i);
             assert group
                 .getRepresentative()
-                .getPrefixForExternalSyntheticType()
+                .getPrefixForExternalSyntheticType(appView)
                 .equals(externalSyntheticTypePrefix);
             // Two equivalence groups in same context type must be distinct otherwise the assignment
             // of the synthetic name will be non-deterministic between the two.
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 631231c..49746f4 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
@@ -37,8 +37,10 @@
 import com.android.tools.r8.graph.PrunedItems;
 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.synthesis.SyntheticFinalization.Result;
 import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
+import com.android.tools.r8.utils.Box;
 import com.android.tools.r8.utils.ConsumerUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.IterableUtils;
@@ -549,6 +551,33 @@
     return syntheticContextsToSyntheticClasses;
   }
 
+  public Collection<Origin> getSynthesizingOrigin(DexType type) {
+    if (!isSynthetic(type)) {
+      return Collections.emptyList();
+    }
+    ImmutableList.Builder<Origin> builder = ImmutableList.builder();
+    forEachSynthesizingContext(
+        type,
+        context -> {
+          builder.add(context.getInputContextOrigin());
+        });
+    return builder.build();
+  }
+
+  public DexType getSynthesizingInputContext(DexType syntheticType, InternalOptions options) {
+    if (!isSynthetic(syntheticType)) {
+      return null;
+    }
+    Box<DexType> uniqueInputContext = new Box<>(null);
+    forEachSynthesizingContext(
+        syntheticType,
+        context -> {
+          assert uniqueInputContext.get() == null;
+          uniqueInputContext.set(context.getSynthesizingInputContext(options.intermediate));
+        });
+    return uniqueInputContext.get();
+  }
+
   public interface SynthesizingContextOracle {
 
     Set<DexReference> getSynthesizingContexts(DexProgramClass clazz);
@@ -729,9 +758,7 @@
     // This is to ensure a flat input-type -> synthetic-item mapping.
     SynthesizingContext outerContext = getSynthesizingContext(context.getClassContext(), appView);
     Function<SynthesizingContext, DexType> contextToType =
-        c ->
-            SyntheticNaming.createInternalType(
-                kind, c, context.getSyntheticSuffix(), appView.dexItemFactory());
+        c -> SyntheticNaming.createInternalType(kind, c, context.getSyntheticSuffix(), appView);
     return internalCreateProgramClass(
         kind, fn, outerContext, contextToType.apply(outerContext), contextToType, appView);
   }
@@ -1028,8 +1055,7 @@
     SynthesizingContext outerContext = getSynthesizingContext(context, appView);
     SyntheticKind kind = kindSelector.select(naming);
     DexType type =
-        SyntheticNaming.createInternalType(
-            kind, outerContext, syntheticIdSupplier.get(), appView.dexItemFactory());
+        SyntheticNaming.createInternalType(kind, outerContext, syntheticIdSupplier.get(), appView);
     SyntheticProgramClassBuilder classBuilder =
         new SyntheticProgramClassBuilder(type, kind, outerContext, appView.dexItemFactory());
     DexProgramClass clazz =
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticMarker.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticMarker.java
index aebc9ae..cf548f9 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticMarker.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticMarker.java
@@ -193,7 +193,7 @@
     if (kind.isGlobal()) {
       return type;
     }
-    String prefix = SyntheticNaming.getPrefixForExternalSyntheticType(kind, type);
+    String prefix = SyntheticNaming.getOuterContextFromExternalSyntheticType(kind, type);
     return factory.createType(DescriptorUtils.getDescriptorFromClassBinaryName(prefix));
   }
 
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
index 5c81e4d..95105f8 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.Version;
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.desugar.itf.InterfaceDesugaringForTesting;
@@ -419,6 +420,19 @@
     return binaryName.substring(0, index);
   }
 
+  static String getOuterContextFromExternalSyntheticType(SyntheticKind kind, DexType type) {
+    assert !kind.isGlobal();
+    String binaryName = type.toBinaryName();
+    int index =
+        binaryName.indexOf(
+            kind.isFixedSuffixSynthetic() ? kind.descriptor : EXTERNAL_SYNTHETIC_CLASS_SEPARATOR);
+    if (index < 0) {
+      throw new Unreachable(
+          "Unexpected failure to determine the context of synthetic class: " + binaryName);
+    }
+    return binaryName.substring(0, index);
+  }
+
   static DexType createFixedType(
       SyntheticKind kind, SynthesizingContext context, DexItemFactory factory) {
     assert kind.isFixedSuffixSynthetic();
@@ -426,14 +440,14 @@
   }
 
   static DexType createInternalType(
-      SyntheticKind kind, SynthesizingContext context, String id, DexItemFactory factory) {
+      SyntheticKind kind, SynthesizingContext context, String id, AppView<?> appView) {
     assert !kind.isFixedSuffixSynthetic();
     return createType(
         INTERNAL_SYNTHETIC_CLASS_SEPARATOR,
         kind,
-        context.getSynthesizingContextType(),
+        context.getSynthesizingInputContext(appView.options().intermediate),
         id,
-        factory);
+        appView.dexItemFactory());
   }
 
   static DexType createExternalType(
diff --git a/src/main/java/com/android/tools/r8/utils/DexFilePerClassFileConsumerDataImpl.java b/src/main/java/com/android/tools/r8/utils/DexFilePerClassFileConsumerDataImpl.java
index 4e028d5..9dc9e8b 100644
--- a/src/main/java/com/android/tools/r8/utils/DexFilePerClassFileConsumerDataImpl.java
+++ b/src/main/java/com/android/tools/r8/utils/DexFilePerClassFileConsumerDataImpl.java
@@ -11,16 +11,19 @@
 public class DexFilePerClassFileConsumerDataImpl implements DexFilePerClassFileConsumerData {
 
   private final String primaryClassDescriptor;
+  private final String synthesizingContextDescriptor;
   private final ByteDataView data;
   private final Set<String> classDescriptors;
   private final DiagnosticsHandler handler;
 
   public DexFilePerClassFileConsumerDataImpl(
       String primaryClassDescriptor,
+      String synthesizingContextDescriptor,
       ByteDataView data,
       Set<String> classDescriptors,
       DiagnosticsHandler handler) {
     this.primaryClassDescriptor = primaryClassDescriptor;
+    this.synthesizingContextDescriptor = synthesizingContextDescriptor;
     this.data = data;
     this.classDescriptors = classDescriptors;
     this.handler = handler;
@@ -32,6 +35,11 @@
   }
 
   @Override
+  public String getSynthesizingContextForPrimaryClass() {
+    return synthesizingContextDescriptor;
+  }
+
+  @Override
   public ByteDataView getByteDataView() {
     return data;
   }
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..f4ef14b 100644
--- a/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java
+++ b/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java
@@ -13,6 +13,7 @@
 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;
@@ -58,7 +59,8 @@
           ArtProfilesForRewritingApiTest.ApiTest.class,
           StartupProfileApiTest.ApiTest.class,
           ClassConflictResolverTest.ApiTest.class,
-          ProguardKeepRuleDiagnosticsApiTest.ApiTest.class);
+          ProguardKeepRuleDiagnosticsApiTest.ApiTest.class,
+          PerClassSyntheticContextsTest.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/dexconsumers/PerClassSyntheticContextsTest.java
new file mode 100644
index 0000000..69f7b9b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/compilerapi/dexconsumers/PerClassSyntheticContextsTest.java
@@ -0,0 +1,111 @@
+// 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;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.ClassFileConsumer;
+import com.android.tools.r8.ClassFileConsumerData;
+import com.android.tools.r8.D8;
+import com.android.tools.r8.D8Command;
+import com.android.tools.r8.DexFilePerClassFileConsumer;
+import com.android.tools.r8.DexFilePerClassFileConsumerData;
+import com.android.tools.r8.DiagnosticsHandler;
+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.function.Consumer;
+import org.junit.Test;
+
+public class PerClassSyntheticContextsTest extends CompilerApiTestRunner {
+
+  public PerClassSyntheticContextsTest(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 acceptClassFile(ClassFileConsumerData data) {
+                outputs.put(data.getClassDescriptor(), data.getByteDataCopy());
+              }
+
+              @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 {
+      D8.run(
+          D8Command.builder()
+              .addClassProgramData(input, Origin.unknown())
+              .addLibraryFiles(getJava8RuntimeJar())
+              .setMinApiLevel(1)
+              .setProgramConsumer(
+                  new DexFilePerClassFileConsumer() {
+                    @Override
+                    public void acceptDexFile(DexFilePerClassFileConsumerData data) {
+                      syntheticContext.accept(data.getSynthesizingContextForPrimaryClass());
+                    }
+
+                    @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/src/test/java/com/android/tools/r8/synthesis/RepeatedCompilationNestedSyntheticsTest.java b/src/test/java/com/android/tools/r8/synthesis/RepeatedCompilationNestedSyntheticsTest.java
new file mode 100644
index 0000000..e273552
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/synthesis/RepeatedCompilationNestedSyntheticsTest.java
@@ -0,0 +1,208 @@
+// 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.synthesis;
+
+import static com.android.tools.r8.synthesis.SyntheticItemsTestUtils.syntheticBackportClass;
+import static com.android.tools.r8.synthesis.SyntheticItemsTestUtils.syntheticLambdaClass;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.ClassFileConsumer;
+import com.android.tools.r8.ClassFileConsumerData;
+import com.android.tools.r8.DesugarGraphConsumer;
+import com.android.tools.r8.DexFilePerClassFileConsumer;
+import com.android.tools.r8.DexFilePerClassFileConsumerData;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.BooleanBox;
+import com.google.common.collect.ImmutableSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class RepeatedCompilationNestedSyntheticsTest extends TestBase {
+
+  private final TestParameters parameters;
+  private final Backend intermediateBackend;
+
+  @Parameterized.Parameters(name = "{0}, intermediate: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withDefaultDexRuntime().withMinimumApiLevel().build(),
+        Backend.values());
+  }
+
+  public RepeatedCompilationNestedSyntheticsTest(
+      TestParameters parameters, Backend intermediateBackend) {
+    this.parameters = parameters;
+    this.intermediateBackend = intermediateBackend;
+  }
+
+  @Test
+  public void test() throws Exception {
+    assertEquals(Backend.DEX, parameters.getBackend());
+
+    ClassReference syntheticLambdaClass = syntheticLambdaClass(UsesBackport.class, 0);
+    ImmutableSet<String> expectedClassOutputs =
+        ImmutableSet.of(descriptor(UsesBackport.class), syntheticLambdaClass.getDescriptor());
+
+    Map<String, byte[]> firstCompilation = new HashMap<>();
+    testForD8(Backend.CF)
+        // High API level such that only the lambda is desugared.
+        .setMinApi(AndroidApiLevel.S)
+        .setIntermediate(true)
+        .addClasspathClasses(I.class)
+        .addProgramClasses(UsesBackport.class)
+        .setProgramConsumer(
+            new ClassFileConsumer() {
+              @Override
+              public void acceptClassFile(ClassFileConsumerData data) {
+                firstCompilation.put(data.getClassDescriptor(), data.getByteDataCopy());
+              }
+
+              @Override
+              public void finished(DiagnosticsHandler handler) {}
+            })
+        .compile();
+    assertEquals(expectedClassOutputs, firstCompilation.keySet());
+
+    Map<String, byte[]> secondCompilation = new HashMap<>();
+    ImmutableSet.Builder<String> allDescriptors = ImmutableSet.builder();
+    BooleanBox matched = new BooleanBox(false);
+    for (Entry<String, byte[]> entry : firstCompilation.entrySet()) {
+      byte[] bytes = entry.getValue();
+      Origin origin =
+          new Origin(Origin.root()) {
+            @Override
+            public String part() {
+              return entry.getKey();
+            }
+          };
+      testForD8(intermediateBackend)
+          .setMinApi(parameters)
+          .setIntermediate(true)
+          .addClasspathClasses(I.class)
+          .apply(b -> b.getBuilder().addClassProgramData(bytes, origin))
+          .apply(
+              b ->
+                  b.getBuilder()
+                      .setDesugarGraphConsumer(
+                          new DesugarGraphConsumer() {
+
+                            @Override
+                            public void accept(Origin dependent, Origin dependency) {
+                              assertThat(
+                                  dependency.toString(), containsString(binaryName(I.class)));
+                              assertThat(
+                                  dependent.toString(),
+                                  containsString(syntheticLambdaClass.getBinaryName()));
+                              matched.set(true);
+                            }
+
+                            @Override
+                            public void finished() {}
+                          }))
+          .applyIf(
+              intermediateBackend == Backend.CF,
+              b ->
+                  b.setProgramConsumer(
+                      new ClassFileConsumer() {
+                        @Override
+                        public void acceptClassFile(ClassFileConsumerData data) {
+                          secondCompilation.put(data.getClassDescriptor(), data.getByteDataCopy());
+                          allDescriptors.add(data.getClassDescriptor());
+                        }
+
+                        @Override
+                        public void finished(DiagnosticsHandler handler) {}
+                      }),
+              b ->
+                  b.setProgramConsumer(
+                      new DexFilePerClassFileConsumer() {
+                        @Override
+                        public synchronized void acceptDexFile(
+                            DexFilePerClassFileConsumerData data) {
+                          secondCompilation.put(
+                              data.getPrimaryClassDescriptor(), data.getByteDataCopy());
+                          allDescriptors.addAll(data.getClassDescriptors());
+                        }
+
+                        @Override
+                        public void finished(DiagnosticsHandler handler) {}
+                      }))
+          .compile();
+    }
+    assertTrue(matched.get());
+    // The dex file per class file output should maintain the exact same set of primary descriptors.
+    if (intermediateBackend == Backend.DEX) {
+      assertEquals(expectedClassOutputs, secondCompilation.keySet());
+    }
+    // The total set of classes should also include the backport. The backport should be
+    // hygienically placed under the synthetic lambda (not the context of the lambda!).
+    assertEquals(
+        ImmutableSet.<String>builder()
+            .addAll(expectedClassOutputs)
+            .add(syntheticBackportClass(syntheticLambdaClass, 0).getDescriptor())
+            .build(),
+        allDescriptors.build());
+
+    testForD8(Backend.DEX)
+        .setMinApi(parameters)
+        .addProgramClasses(I.class, TestClass.class)
+        .applyIf(
+            intermediateBackend == Backend.CF,
+            b -> b.addProgramClassFileData(secondCompilation.values()),
+            b -> b.addProgramDexFileData(secondCompilation.values()))
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("1")
+        .inspect(
+            inspector -> {
+              Set<String> descriptors =
+                  inspector.allClasses().stream()
+                      .map(c -> c.getFinalReference().getDescriptor())
+                      .collect(Collectors.toSet());
+              assertEquals(
+                  ImmutableSet.of(
+                      descriptor(I.class),
+                      descriptor(TestClass.class),
+                      descriptor(UsesBackport.class),
+                      // The merge step will reestablish the original contexts, thus both the lambda
+                      // and the backport are placed under the non-synthetic input class
+                      // UsesBackport.
+                      syntheticBackportClass(UsesBackport.class, 0).getDescriptor(),
+                      syntheticLambdaClass(UsesBackport.class, 1).getDescriptor()),
+                  descriptors);
+            });
+  }
+
+  interface I {
+    int compare(boolean b1, boolean b2);
+  }
+
+  static class UsesBackport {
+    public static I foo() {
+      return Boolean::compare;
+    }
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      System.out.println(UsesBackport.foo().compare(true, false));
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/synthesis/RepeatedCompilationSyntheticsTest.java b/src/test/java/com/android/tools/r8/synthesis/RepeatedCompilationSyntheticsTest.java
new file mode 100644
index 0000000..842c56e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/synthesis/RepeatedCompilationSyntheticsTest.java
@@ -0,0 +1,166 @@
+// 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.synthesis;
+
+import static com.android.tools.r8.synthesis.SyntheticItemsTestUtils.syntheticBackportClass;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import com.android.tools.r8.ByteDataView;
+import com.android.tools.r8.ClassFileConsumer;
+import com.android.tools.r8.DexFilePerClassFileConsumer;
+import com.android.tools.r8.DexFilePerClassFileConsumerData;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class RepeatedCompilationSyntheticsTest extends TestBase {
+
+  private final String EXPECTED = StringUtils.lines("-2", "254");
+
+  private final TestParameters parameters;
+  private final Backend intermediateBackend;
+
+  private final AndroidApiLevel API_WITH_BYTE_COMPARE = AndroidApiLevel.K;
+
+  @Parameterized.Parameters(name = "{0}, intermediate: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withDefaultDexRuntime().withMinimumApiLevel().build(),
+        Backend.values());
+  }
+
+  public RepeatedCompilationSyntheticsTest(TestParameters parameters, Backend intermediateBackend) {
+    this.parameters = parameters;
+    this.intermediateBackend = intermediateBackend;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForD8(parameters.getBackend())
+        .addProgramClasses(TestClass.class)
+        .addProgramClassFileData(getTransformedUsesBackport())
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void test() throws Exception {
+    assertEquals(Backend.DEX, parameters.getBackend());
+
+    Map<String, byte[]> firstCompilation = new HashMap<>();
+    testForD8(Backend.CF)
+        // High API level such that only the compareUnsigned is desugared.
+        .setMinApi(API_WITH_BYTE_COMPARE)
+        .setIntermediate(true)
+        .addProgramClassFileData(getTransformedUsesBackport())
+        .setProgramConsumer(
+            new ClassFileConsumer() {
+              @Override
+              public synchronized void accept(
+                  ByteDataView data, String descriptor, DiagnosticsHandler handler) {
+                byte[] old = firstCompilation.put(descriptor, data.copyByteData());
+                assertNull("Duplicate " + descriptor, old);
+              }
+
+              @Override
+              public void finished(DiagnosticsHandler handler) {}
+            })
+        .compile();
+    assertEquals(
+        ImmutableSet.of(
+            descriptor(UsesBackport.class),
+            syntheticBackportClass(UsesBackport.class, 0).getDescriptor()),
+        firstCompilation.keySet());
+
+    List<String> secondCompilation = new ArrayList<>();
+    for (Entry<String, byte[]> entry : firstCompilation.entrySet()) {
+      byte[] bytes = entry.getValue();
+      testForD8(intermediateBackend)
+          .setMinApi(parameters)
+          .setIntermediate(true)
+          .addProgramClassFileData(bytes)
+          .applyIf(
+              intermediateBackend == Backend.CF,
+              b ->
+                  b.setProgramConsumer(
+                      new ClassFileConsumer() {
+                        @Override
+                        public synchronized void accept(
+                            ByteDataView data, String descriptor, DiagnosticsHandler handler) {
+                          secondCompilation.add(descriptor);
+                        }
+
+                        @Override
+                        public void finished(DiagnosticsHandler handler) {}
+                      }),
+              b ->
+                  b.setProgramConsumer(
+                      new DexFilePerClassFileConsumer() {
+                        @Override
+                        public synchronized void acceptDexFile(
+                            DexFilePerClassFileConsumerData data) {
+                          secondCompilation.addAll(data.getClassDescriptors());
+                        }
+
+                        @Override
+                        public void finished(DiagnosticsHandler handler) {}
+                      }))
+          .compile();
+    }
+
+    // TODO(b/271235788): The repeated compilation is unsound and we have duplicate definitions of
+    //  the backports both using the same type name.
+    secondCompilation.sort(String::compareTo);
+    assertEquals(
+        ImmutableList.of(
+            syntheticBackportClass(UsesBackport.class, 0).getDescriptor(),
+            syntheticBackportClass(UsesBackport.class, 0).getDescriptor(),
+            descriptor(UsesBackport.class)),
+        secondCompilation);
+  }
+
+  private byte[] getTransformedUsesBackport() throws Exception {
+    return transformer(UsesBackport.class)
+        .transformMethodInsnInMethod(
+            "bar",
+            (opcode, owner, name, descriptor, isInterface, visitor) -> {
+              assertEquals("compare", name);
+              visitor.visitMethodInsn(opcode, owner, "compareUnsigned", descriptor, isInterface);
+            })
+        .transform();
+  }
+
+  static class UsesBackport {
+    public static int foo(byte[] bs) {
+      return Byte.compare(bs[0], bs[1]);
+    }
+
+    public static int bar(byte[] bs) {
+      return Byte.compare /*Unsigned*/(bs[0], bs[1]);
+    }
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      System.out.println(UsesBackport.foo(new byte[] {-1, 1}));
+      System.out.println(UsesBackport.bar(new byte[] {-1, 1}));
+    }
+  }
+}