Reland "Hygienic lambda desugaring."

Bug: 158159959
Change-Id: Id9e58b1ab66d51494f66f59ae4241fced1dae420
Fixes: 176211449
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index 4def5d0..b762557 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -271,6 +271,9 @@
         if (result != null) {
           appView.setAppInfo(new AppInfo(result.commit, appView.appInfo().getMainDexClasses()));
           appView.pruneItems(result.prunedItems);
+          if (result.lens != null) {
+            appView.setGraphLens(result.lens);
+          }
         }
         new CfApplicationWriter(
                 appView,
@@ -315,6 +318,9 @@
         if (result != null) {
           appView.setAppInfo(new AppInfo(result.commit, appView.appInfo().getMainDexClasses()));
           appView.pruneItems(result.prunedItems);
+          if (result.lens != null) {
+            appView.setGraphLens(result.lens);
+          }
         }
 
         new ApplicationWriter(
diff --git a/src/main/java/com/android/tools/r8/L8.java b/src/main/java/com/android/tools/r8/L8.java
index f610d83..e611c5a 100644
--- a/src/main/java/com/android/tools/r8/L8.java
+++ b/src/main/java/com/android/tools/r8/L8.java
@@ -133,6 +133,9 @@
       if (result != null) {
         appView.setAppInfo(new AppInfo(result.commit, appView.appInfo().getMainDexClasses()));
         appView.pruneItems(result.prunedItems);
+        if (result.lens != null) {
+          appView.setGraphLens(result.lens);
+        }
       }
 
       NamingLens namingLens = PrefixRewritingNamingLens.createPrefixRewritingNamingLens(appView);
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 9e7a967..125b84d 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -838,12 +838,21 @@
           appView.getSyntheticItems().computeFinalSynthetics(appView);
       if (result != null) {
         if (appView.appInfo().hasLiveness()) {
-          appViewWithLiveness.setAppInfo(
-              appViewWithLiveness.appInfo().rebuildWithLiveness(result.commit));
+          if (result.lens == null) {
+            appViewWithLiveness.setAppInfo(
+                appViewWithLiveness.appInfo().rebuildWithLiveness(result.commit));
+          } else {
+            appViewWithLiveness.rewriteWithLensAndApplication(
+                result.lens, result.commit.getApplication().asDirect());
+          }
+          appViewWithLiveness.pruneItems(result.prunedItems);
         } else {
           appView.setAppInfo(appView.appInfo().rebuildWithClassHierarchy(result.commit));
+          appView.pruneItems(result.prunedItems);
+          if (result.lens != null) {
+            appView.setGraphLens(result.lens);
+          }
         }
-        appViewWithLiveness.pruneItems(result.prunedItems);
       }
 
       // Perform minification.
diff --git a/src/main/java/com/android/tools/r8/code/Instruction.java b/src/main/java/com/android/tools/r8/code/Instruction.java
index 977dafd..ccfd713 100644
--- a/src/main/java/com/android/tools/r8/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/code/Instruction.java
@@ -332,6 +332,9 @@
   @Override
   public final void acceptHashing(HashingVisitor visitor) {
     // Rather than traverse the full instruction, the compare ID will likely give a reasonable hash.
+    // TODO(b/158159959): This will likely lead to a lot of distinct synthetics hashing to the same
+    //  hash as many have the same instruction pattern such as an invoke of the impl method or a
+    //  field access.
     visitor.visitInt(getCompareToId());
   }
 
diff --git a/src/main/java/com/android/tools/r8/dex/FileWriter.java b/src/main/java/com/android/tools/r8/dex/FileWriter.java
index ef6b7cd..bbe4a90 100644
--- a/src/main/java/com/android/tools/r8/dex/FileWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/FileWriter.java
@@ -55,7 +55,7 @@
 import com.android.tools.r8.naming.MemberNaming.Signature;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.position.MethodPosition;
-import com.android.tools.r8.synthesis.SyntheticItems;
+import com.android.tools.r8.synthesis.SyntheticNaming;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.DexVersion;
 import com.android.tools.r8.utils.InternalOptions;
@@ -323,7 +323,7 @@
     for (DexType type : mapping.getTypes()) {
       if (type.isClassType()) {
         assert DexString.isValidSimpleName(apiLevel, type.getName());
-        assert SyntheticItems.verifyNotInternalSynthetic(type);
+        assert SyntheticNaming.verifyNotInternalSynthetic(type);
       }
     }
 
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 917f0de..7da0bc5 100644
--- a/src/main/java/com/android/tools/r8/dex/VirtualFile.java
+++ b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
@@ -28,7 +28,7 @@
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.shaking.MainDexClasses;
-import com.android.tools.r8.synthesis.SyntheticItems;
+import com.android.tools.r8.synthesis.SyntheticNaming;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions;
@@ -667,7 +667,7 @@
 
     @Override
     public boolean addType(DexType type) {
-      assert SyntheticItems.verifyNotInternalSynthetic(type);
+      assert SyntheticNaming.verifyNotInternalSynthetic(type);
       return types.add(type);
     }
 
@@ -790,7 +790,7 @@
 
     @Override
     public boolean addType(DexType type) {
-      assert SyntheticItems.verifyNotInternalSynthetic(type);
+      assert SyntheticNaming.verifyNotInternalSynthetic(type);
       return maybeInsert(type, types, base.types);
     }
 
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 cfacdbb..302dff4 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfo.java
@@ -123,11 +123,6 @@
     }
   }
 
-  public Collection<DexProgramClass> synthesizedClasses() {
-    assert checkIfObsolete();
-    return syntheticItems.getPendingSyntheticClasses();
-  }
-
   public Collection<DexProgramClass> classes() {
     assert checkIfObsolete();
     return app.classes();
diff --git a/src/main/java/com/android/tools/r8/graph/CfCode.java b/src/main/java/com/android/tools/r8/graph/CfCode.java
index eb83898..4d45d55 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCode.java
@@ -247,6 +247,9 @@
   public void acceptHashing(HashingVisitor visitor) {
     // Rather than hash the entire content, hash the sizes and each instruction "type" which
     // should provide a fast yet reasonably distinct key.
+    // TODO(b/158159959): This will likely lead to a lot of distinct synthetics hashing to the same
+    //  hash as many have the same instruction pattern such as an invoke of the impl method or a
+    //  field access.
     visitor.visitInt(instructions.size());
     visitor.visitInt(tryCatchRanges.size());
     visitor.visitInt(localVariables.size());
diff --git a/src/main/java/com/android/tools/r8/graph/DexAnnotation.java b/src/main/java/com/android/tools/r8/graph/DexAnnotation.java
index bcb7974..3483fbb 100644
--- a/src/main/java/com/android/tools/r8/graph/DexAnnotation.java
+++ b/src/main/java/com/android/tools/r8/graph/DexAnnotation.java
@@ -14,6 +14,8 @@
 import com.android.tools.r8.graph.DexValue.DexValueString;
 import com.android.tools.r8.graph.DexValue.DexValueType;
 import com.android.tools.r8.ir.desugar.CovariantReturnTypeAnnotationTransformer;
+import com.android.tools.r8.synthesis.SyntheticNaming;
+import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.Pair;
 import com.android.tools.r8.utils.structural.StructuralItem;
@@ -399,13 +401,18 @@
   }
 
   public static DexAnnotation createAnnotationSynthesizedClass(
-      DexType synthesizingContext, DexItemFactory dexItemFactory) {
-    DexValueType value = new DexValueType(synthesizingContext);
-    DexAnnotationElement element = new DexAnnotationElement(dexItemFactory.valueString, value);
+      SyntheticKind kind, DexType synthesizingContext, DexItemFactory dexItemFactory) {
+    DexAnnotationElement kindElement =
+        new DexAnnotationElement(
+            dexItemFactory.kindString,
+            new DexValueString(dexItemFactory.createString(kind.descriptor)));
+    DexAnnotationElement typeElement =
+        new DexAnnotationElement(dexItemFactory.valueString, new DexValueType(synthesizingContext));
     return new DexAnnotation(
         VISIBILITY_BUILD,
         new DexEncodedAnnotation(
-            dexItemFactory.annotationSynthesizedClass, new DexAnnotationElement[] {element}));
+            dexItemFactory.annotationSynthesizedClass,
+            new DexAnnotationElement[] {kindElement, typeElement}));
   }
 
   public static boolean hasSynthesizedClassAnnotation(
@@ -413,7 +420,7 @@
     return getSynthesizedClassAnnotationContextType(annotations, factory) != null;
   }
 
-  public static DexType getSynthesizedClassAnnotationContextType(
+  public static Pair<SyntheticKind, DexType> getSynthesizedClassAnnotationContextType(
       DexAnnotationSet annotations, DexItemFactory factory) {
     if (annotations.size() != 1) {
       return null;
@@ -422,17 +429,31 @@
     if (annotation.annotation.type != factory.annotationSynthesizedClass) {
       return null;
     }
-    if (annotation.annotation.elements.length != 1) {
+    if (annotation.annotation.elements.length != 2) {
       return null;
     }
-    DexAnnotationElement element = annotation.annotation.elements[0];
-    if (element.name != factory.valueString) {
+    assert factory.kindString.isLessThan(factory.valueString);
+    DexAnnotationElement kindElement = annotation.annotation.elements[0];
+    if (kindElement.name != factory.kindString) {
       return null;
     }
-    if (!element.value.isDexValueType()) {
+    if (!kindElement.value.isDexValueString()) {
       return null;
     }
-    return element.value.asDexValueType().getValue();
+    SyntheticKind kind =
+        SyntheticNaming.SyntheticKind.fromDescriptor(
+            kindElement.value.asDexValueString().getValue().toString());
+    if (kind == null) {
+      return null;
+    }
+    DexAnnotationElement valueElement = annotation.annotation.elements[1];
+    if (valueElement.name != factory.valueString) {
+      return null;
+    }
+    if (!valueElement.value.isDexValueType()) {
+      return null;
+    }
+    return new Pair<>(kind, valueElement.value.asDexValueType().getValue());
   }
 
   public static DexAnnotation createAnnotationSynthesizedClassMap(
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index 91203c8..2236b6c 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -76,14 +76,12 @@
 import com.android.tools.r8.utils.OptionalBool;
 import com.android.tools.r8.utils.Pair;
 import com.android.tools.r8.utils.structural.CompareToVisitor;
-import com.android.tools.r8.utils.structural.CompareToVisitorWithTypeEquivalence;
 import com.android.tools.r8.utils.structural.HashingVisitor;
-import com.android.tools.r8.utils.structural.HashingVisitorWithTypeEquivalence;
 import com.android.tools.r8.utils.structural.Ordered;
-import com.android.tools.r8.utils.structural.RepresentativeMap;
+import com.android.tools.r8.utils.structural.StructuralItem;
+import com.android.tools.r8.utils.structural.StructuralMapping;
 import com.android.tools.r8.utils.structural.StructuralSpecification;
 import com.google.common.collect.ImmutableList;
-import com.google.common.hash.Hasher;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceArrayMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
 import java.util.ArrayList;
@@ -95,7 +93,8 @@
 import java.util.function.IntPredicate;
 import org.objectweb.asm.Opcodes;
 
-public class DexEncodedMethod extends DexEncodedMember<DexEncodedMethod, DexMethod> {
+public class DexEncodedMethod extends DexEncodedMember<DexEncodedMethod, DexMethod>
+    implements StructuralItem<DexEncodedMethod> {
 
   public static final String CONFIGURATION_DEBUGGING_PREFIX = "Shaking error: Missing method in ";
 
@@ -338,6 +337,16 @@
     return deprecated;
   }
 
+  @Override
+  public DexEncodedMethod self() {
+    return this;
+  }
+
+  @Override
+  public StructuralMapping<DexEncodedMethod> getStructuralMapping() {
+    return DexEncodedMethod::syntheticSpecify;
+  }
+
   // Visitor specifying the structure of the method with respect to its "synthetic" content.
   // TODO(b/171867022): Generalize this so that it determines any method in full.
   private static void syntheticSpecify(StructuralSpecification<DexEncodedMethod, ?> spec) {
@@ -356,28 +365,12 @@
             DexEncodedMethod::hashCodeObject);
   }
 
-  public void hashSyntheticContent(Hasher hasher, RepresentativeMap map) {
-    HashingVisitorWithTypeEquivalence.run(this, hasher, map, DexEncodedMethod::syntheticSpecify);
-  }
-
-  public boolean isSyntheticContentEqual(DexEncodedMethod other) {
-    return syntheticCompareTo(other) == 0;
-  }
-
-  public int syntheticCompareTo(DexEncodedMethod other) {
-    // Consider the holder types to be equivalent, using the holder of this method as the
-    // representative.
-    RepresentativeMap map = t -> t == other.getHolderType() ? getHolderType() : t;
-    return CompareToVisitorWithTypeEquivalence.run(
-        this, other, map, DexEncodedMethod::syntheticSpecify);
-  }
-
   private static int compareCodeObject(Code code1, Code code2, CompareToVisitor visitor) {
     if (code1.isCfCode() && code2.isCfCode()) {
       return code1.asCfCode().acceptCompareTo(code2.asCfCode(), visitor);
     }
     if (code1.isDexCode() && code2.isDexCode()) {
-      return visitor.visit(code1.asDexCode(), code2.asDexCode(), DexCode::compareTo);
+      return code1.asDexCode().acceptCompareTo(code2.asDexCode(), visitor);
     }
     throw new Unreachable(
         "Unexpected attempt to compare incompatible synthetic objects: " + code1 + " and " + code2);
@@ -387,9 +380,7 @@
     if (code.isCfCode()) {
       code.asCfCode().acceptHashing(visitor);
     } else {
-      // TODO(b/158159959): Implement a more precise hashing on code objects.
-      assert code.isDexCode();
-      visitor.visitInt(code.hashCode());
+      code.asDexCode().acceptHashing(visitor);
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 385fb58..f148019 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -299,6 +299,7 @@
   public final DexString throwableArrayDescriptor = createString("[Ljava/lang/Throwable;");
 
   public final DexString valueString = createString("value");
+  public final DexString kindString = createString("kind");
 
   public final DexType booleanType = createStaticallyKnownType(booleanDescriptor);
   public final DexType byteType = createStaticallyKnownType(byteDescriptor);
diff --git a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
index 2a6ebf5..7d69899 100644
--- a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
@@ -21,6 +21,9 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.TraversalContinuation;
 import com.android.tools.r8.utils.structural.Ordered;
+import com.android.tools.r8.utils.structural.StructuralItem;
+import com.android.tools.r8.utils.structural.StructuralMapping;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import java.util.ArrayList;
@@ -37,7 +40,7 @@
 import java.util.function.Supplier;
 
 public class DexProgramClass extends DexClass
-    implements ProgramDefinition, Supplier<DexProgramClass> {
+    implements ProgramDefinition, Supplier<DexProgramClass>, StructuralItem<DexProgramClass> {
 
   @FunctionalInterface
   public interface ChecksumSupplier {
@@ -144,6 +147,36 @@
     synthesizedDirectlyFrom.forEach(this::addSynthesizedFrom);
   }
 
+  @Override
+  public DexProgramClass self() {
+    return this;
+  }
+
+  @Override
+  public StructuralMapping<DexProgramClass> getStructuralMapping() {
+    return DexProgramClass::specify;
+  }
+
+  private static void specify(StructuralSpecification<DexProgramClass, ?> spec) {
+    spec.withItem(c -> c.type)
+        .withItem(c -> c.superType)
+        .withItem(c -> c.interfaces)
+        .withItem(c -> c.accessFlags)
+        .withNullableItem(c -> c.sourceFile)
+        .withNullableItem(c -> c.initialClassFileVersion)
+        .withBool(c -> c.deprecated)
+        .withNullableItem(DexClass::getNestHostClassAttribute)
+        .withItemCollection(DexClass::getNestMembersClassAttributes)
+        .withItem(DexDefinition::annotations)
+        // TODO(b/158159959): Make signatures structural.
+        .withAssert(c -> c.classSignature == ClassSignature.noSignature())
+        .withItemArray(c -> c.staticFields)
+        .withItemArray(c -> c.instanceFields)
+        .withItemCollection(DexClass::allMethodsSorted)
+        // TODO(b/168584485): Synthesized-from is being removed (empty for new synthetics).
+        .withAssert(c -> c.synthesizedFrom.isEmpty());
+  }
+
   public void forEachProgramField(Consumer<? super ProgramField> consumer) {
     forEachField(field -> consumer.accept(new ProgramField(this, field)));
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexType.java b/src/main/java/com/android/tools/r8/graph/DexType.java
index 376abd6..cab5904 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -9,7 +9,6 @@
 import static com.android.tools.r8.ir.desugar.InterfaceMethodRewriter.COMPANION_CLASS_NAME_SUFFIX;
 import static com.android.tools.r8.ir.desugar.InterfaceMethodRewriter.DISPATCH_CLASS_NAME_SUFFIX;
 import static com.android.tools.r8.ir.desugar.InterfaceMethodRewriter.EMULATE_LIBRARY_CLASS_NAME_SUFFIX;
-import static com.android.tools.r8.ir.desugar.LambdaRewriter.LAMBDA_CLASS_NAME_PREFIX;
 import static com.android.tools.r8.ir.desugar.LambdaRewriter.LAMBDA_GROUP_CLASS_NAME_PREFIX;
 import static com.android.tools.r8.ir.optimize.enums.UnboxedEnumMemberRelocator.ENUM_UNBOXING_UTILITY_CLASS_SUFFIX;
 
@@ -21,7 +20,7 @@
 import com.android.tools.r8.ir.desugar.TwrCloseResourceRewriter;
 import com.android.tools.r8.ir.optimize.ServiceLoaderRewriter;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.synthesis.SyntheticItems;
+import com.android.tools.r8.synthesis.SyntheticNaming;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions.OutlineOptions;
 import com.android.tools.r8.utils.structural.CompareToVisitor;
@@ -42,7 +41,7 @@
   // Bundletool is merging classes that may originate from a build with an old version of R8.
   // Allow merging of classes that use names from older versions of R8.
   private static List<String> OLD_SYNTHESIZED_NAMES =
-      ImmutableList.of("$r8$backportedMethods$utility", "$r8$java8methods$utility");
+      ImmutableList.of("$r8$backportedMethods$utility", "$r8$java8methods$utility", "-$$Lambda$");
 
   public final DexString descriptor;
   private String toStringCache = null;
@@ -303,22 +302,15 @@
   }
 
   // TODO(b/158159959): Remove usage of name-based identification.
-  public boolean isD8R8SynthesizedLambdaClassType() {
-    String name = toSourceString();
-    return name.contains(LAMBDA_CLASS_NAME_PREFIX);
-  }
-
-  // TODO(b/158159959): Remove usage of name-based identification.
   public boolean isD8R8SynthesizedClassType() {
     String name = toSourceString();
     // The synthesized classes listed here must always be unique to a program context and thus
     // never duplicated for distinct inputs.
-    return
-    // Hygienic suffix.
-    name.contains(COMPANION_CLASS_NAME_SUFFIX)
+    return false
+        // Hygienic suffix.
+        || name.contains(COMPANION_CLASS_NAME_SUFFIX)
         // New and hygienic synthesis infrastructure.
-        || name.contains(SyntheticItems.INTERNAL_SYNTHETIC_CLASS_SEPARATOR)
-        || name.contains(SyntheticItems.EXTERNAL_SYNTHETIC_CLASS_SEPARATOR)
+        || SyntheticNaming.isSyntheticName(name)
         // Only generated in core lib.
         || name.contains(EMULATE_LIBRARY_CLASS_NAME_SUFFIX)
         || name.contains(TYPE_WRAPPER_SUFFIX)
@@ -338,7 +330,6 @@
     // newer releases can be used to merge previous builds.
     return name.contains(ENUM_UNBOXING_UTILITY_CLASS_SUFFIX) // Shared among enums.
         || name.contains(SyntheticArgumentClass.SYNTHETIC_CLASS_SUFFIX)
-        || name.contains(LAMBDA_CLASS_NAME_PREFIX) // Could collide.
         || name.contains(LAMBDA_GROUP_CLASS_NAME_PREFIX) // Could collide.
         || name.contains(DISPATCH_CLASS_NAME_SUFFIX) // Shared on reference.
         || name.contains(OutlineOptions.CLASS_NAME) // Global singleton.
diff --git a/src/main/java/com/android/tools/r8/graph/GraphLens.java b/src/main/java/com/android/tools/r8/graph/GraphLens.java
index 0e3d784..2328408 100644
--- a/src/main/java/com/android/tools/r8/graph/GraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/GraphLens.java
@@ -5,7 +5,6 @@
 
 import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 import static com.android.tools.r8.horizontalclassmerging.ClassMerger.CLASS_ID_FIELD_NAME;
-import static com.android.tools.r8.ir.desugar.LambdaRewriter.LAMBDA_CLASS_NAME_PREFIX;
 import static com.android.tools.r8.ir.desugar.LambdaRewriter.LAMBDA_INSTANCE_FIELD_NAME;
 
 import com.android.tools.r8.errors.Unreachable;
@@ -620,7 +619,7 @@
         // that they can be mapped back to the original program.
         DexField originalField = getOriginalFieldSignature(field.getReference());
         assert originalFields.contains(originalField)
-                || isD8R8SynthesizedField(originalField, dexItemFactory)
+                || isD8R8SynthesizedField(originalField, appView)
             : "Unable to map field `"
                 + field.getReference().toSourceString()
                 + "` back to original program";
@@ -638,16 +637,16 @@
     return true;
   }
 
-  private boolean isD8R8SynthesizedField(DexField field, DexItemFactory dexItemFactory) {
+  private boolean isD8R8SynthesizedField(DexField field, AppView<?> appView) {
     // TODO(b/167947782): Should be a general check to see if the field is D8/R8 synthesized
     //  instead of relying on field names.
-    if (field.match(dexItemFactory.objectMembers.clinitField)) {
+    if (field.match(appView.dexItemFactory().objectMembers.clinitField)) {
       return true;
     }
     if (field.getName().toSourceString().equals(CLASS_ID_FIELD_NAME)) {
       return true;
     }
-    if (field.getHolderType().toSourceString().contains(LAMBDA_CLASS_NAME_PREFIX)
+    if (appView.getSyntheticItems().isSyntheticClass(field.getHolderType())
         && field.getName().toSourceString().equals(LAMBDA_INSTANCE_FIELD_NAME)) {
       return true;
     }
diff --git a/src/main/java/com/android/tools/r8/graph/NestHostClassAttribute.java b/src/main/java/com/android/tools/r8/graph/NestHostClassAttribute.java
index e007f4e..c9969a6 100644
--- a/src/main/java/com/android/tools/r8/graph/NestHostClassAttribute.java
+++ b/src/main/java/com/android/tools/r8/graph/NestHostClassAttribute.java
@@ -5,12 +5,19 @@
 package com.android.tools.r8.graph;
 
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.StructuralItem;
+import com.android.tools.r8.utils.structural.StructuralMapping;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import org.objectweb.asm.ClassWriter;
 
-public class NestHostClassAttribute {
+public class NestHostClassAttribute implements StructuralItem<NestHostClassAttribute> {
 
   private final DexType nestHost;
 
+  private static void specify(StructuralSpecification<NestHostClassAttribute, ?> spec) {
+    spec.withItem(a -> a.nestHost);
+  }
+
   public NestHostClassAttribute(DexType nestHost) {
     this.nestHost = nestHost;
   }
@@ -27,4 +34,14 @@
     assert nestHost != null;
     writer.visitNestHost(lens.lookupInternalName(nestHost));
   }
+
+  @Override
+  public NestHostClassAttribute self() {
+    return this;
+  }
+
+  @Override
+  public StructuralMapping<NestHostClassAttribute> getStructuralMapping() {
+    return NestHostClassAttribute::specify;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/NestMemberClassAttribute.java b/src/main/java/com/android/tools/r8/graph/NestMemberClassAttribute.java
index f9d1a35..0d7c19d 100644
--- a/src/main/java/com/android/tools/r8/graph/NestMemberClassAttribute.java
+++ b/src/main/java/com/android/tools/r8/graph/NestMemberClassAttribute.java
@@ -5,14 +5,21 @@
 package com.android.tools.r8.graph;
 
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.StructuralItem;
+import com.android.tools.r8.utils.structural.StructuralMapping;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.util.Collections;
 import java.util.List;
 import org.objectweb.asm.ClassWriter;
 
-public class NestMemberClassAttribute {
+public class NestMemberClassAttribute implements StructuralItem<NestMemberClassAttribute> {
 
   private final DexType nestMember;
 
+  private static void specify(StructuralSpecification<NestMemberClassAttribute, ?> spec) {
+    spec.withItem(a -> a.nestMember);
+  }
+
   public NestMemberClassAttribute(DexType nestMember) {
     this.nestMember = nestMember;
   }
@@ -29,4 +36,14 @@
     assert nestMember != null;
     writer.visitNestMember(lens.lookupInternalName(nestMember));
   }
+
+  @Override
+  public NestMemberClassAttribute self() {
+    return this;
+  }
+
+  @Override
+  public StructuralMapping<NestMemberClassAttribute> getStructuralMapping() {
+    return NestMemberClassAttribute::specify;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/TreeFixerBase.java b/src/main/java/com/android/tools/r8/graph/TreeFixerBase.java
index a10db7147..c1d5981 100644
--- a/src/main/java/com/android/tools/r8/graph/TreeFixerBase.java
+++ b/src/main/java/com/android/tools/r8/graph/TreeFixerBase.java
@@ -15,7 +15,7 @@
   private final AppView<?> appView;
   private final DexItemFactory dexItemFactory;
 
-  private Map<DexType, DexProgramClass> newProgramClasses = null;
+  private final Map<DexType, DexProgramClass> programClassCache = new IdentityHashMap<>();
   private final Map<DexType, DexProgramClass> synthesizedFromClasses = new IdentityHashMap<>();
   private final Map<DexProto, DexProto> protoFixupCache = new IdentityHashMap<>();
 
@@ -53,13 +53,13 @@
   }
 
   /** Fixup a collection of classes. */
-  public Collection<DexProgramClass> fixupClasses(Collection<DexProgramClass> classes) {
-    assert newProgramClasses == null;
-    newProgramClasses = new IdentityHashMap<>();
+  public List<DexProgramClass> fixupClasses(Collection<DexProgramClass> classes) {
+    List<DexProgramClass> newProgramClasses = new ArrayList<>();
     for (DexProgramClass clazz : classes) {
-      newProgramClasses.computeIfAbsent(clazz.getType(), ignore -> fixupClass(clazz));
+      newProgramClasses.add(
+          programClassCache.computeIfAbsent(clazz.getType(), ignore -> fixupClass(clazz)));
     }
-    return newProgramClasses.values();
+    return newProgramClasses;
   }
 
   // Should remain private as the correctness of the fixup requires the lazy 'newProgramClasses'.
@@ -70,7 +70,7 @@
             clazz.getOriginKind(),
             clazz.getOrigin(),
             clazz.getAccessFlags(),
-            fixupType(clazz.superType),
+            clazz.superType == null ? null : fixupType(clazz.superType),
             fixupTypeList(clazz.interfaces),
             clazz.getSourceFile(),
             fixupNestHost(clazz.getNestHostClassAttribute()),
@@ -250,7 +250,6 @@
   // Should remain private as its correctness relies on the setup of 'newProgramClasses'.
   private Collection<DexProgramClass> fixupSynthesizedFrom(
       Collection<DexProgramClass> synthesizedFrom) {
-    assert newProgramClasses != null;
     if (synthesizedFrom.isEmpty()) {
       return synthesizedFrom;
     }
@@ -261,7 +260,7 @@
       //  is no longer in the application?
       Map<DexType, DexProgramClass> classes =
           appView.appInfo().definitionForWithoutExistenceAssert(clazz.getType()) != null
-              ? newProgramClasses
+              ? programClassCache
               : synthesizedFromClasses;
       DexProgramClass newClass =
           classes.computeIfAbsent(clazz.getType(), ignore -> fixupClass(clazz));
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index e2b0e47..bc20e17 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -409,11 +409,10 @@
     }
   }
 
-  private void synthesizeLambdaClasses(Builder<?> builder, ExecutorService executorService)
-      throws ExecutionException {
+  private void synthesizeLambdaClasses(ExecutorService executorService) throws ExecutionException {
     if (lambdaRewriter != null) {
       assert !appView.enableWholeProgramOptimizations();
-      lambdaRewriter.finalizeLambdaDesugaringForD8(builder, this, executorService);
+      lambdaRewriter.finalizeLambdaDesugaringForD8(this, executorService);
     }
   }
 
@@ -435,6 +434,7 @@
       Flavor includeAllResources,
       ExecutorService executorService)
       throws ExecutionException {
+    assert !appView.getSyntheticItems().hasPendingSyntheticClasses();
     if (interfaceMethodRewriter != null) {
       interfaceMethodRewriter.desugarInterfaceMethods(
           builder, includeAllResources, executorService);
@@ -522,7 +522,19 @@
     builder.setHighestSortingString(highestSortingString);
 
     desugarNestBasedAccess(builder, executor);
-    synthesizeLambdaClasses(builder, executor);
+
+    // Synthesize lambda classes and commit to the app in full.
+    synthesizeLambdaClasses(executor);
+    if (appView.getSyntheticItems().hasPendingSyntheticClasses()) {
+      appView.setAppInfo(
+          new AppInfo(
+              appView.appInfo().getSyntheticItems().commit(builder.build()),
+              appView.appInfo().getMainDexClasses()));
+      application = appView.appInfo().app();
+      builder = application.builder();
+      builder.setHighestSortingString(highestSortingString);
+    }
+
     desugarInterfaceMethods(builder, ExcludeDexResources, executor);
     synthesizeTwrCloseResourceUtilityClass(builder, executor);
     processSynthesizedJava8UtilityClasses(executor);
@@ -533,10 +545,10 @@
 
     timing.end();
 
-    DexApplication app = builder.build();
+    application = builder.build();
     appView.setAppInfo(
         new AppInfo(
-            appView.appInfo().getSyntheticItems().commit(app),
+            appView.appInfo().getSyntheticItems().commit(application),
             appView.appInfo().getMainDexClasses()));
   }
 
@@ -794,6 +806,14 @@
       appView.clearCodeRewritings();
     }
 
+    // Commit synthetics before creating a builder (otherwise the builder will not include the
+    // synthetics.)
+    if (appView.getSyntheticItems().hasPendingSyntheticClasses()) {
+      appView.setAppInfo(
+          appView
+              .appInfo()
+              .rebuildWithLiveness(appView.getSyntheticItems().commit(appView.appInfo().app())));
+    }
     // Build a new application with jumbo string info.
     Builder<?> builder = appView.appInfo().app().builder();
     builder.setHighestSortingString(highestSortingString);
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
index 40b18c9..14d10c5 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
@@ -33,6 +33,7 @@
 import com.android.tools.r8.ir.desugar.backports.NumericMethodRewrites;
 import com.android.tools.r8.ir.desugar.backports.ObjectsMethodRewrites;
 import com.android.tools.r8.ir.desugar.backports.OptionalMethodRewrites;
+import com.android.tools.r8.synthesis.SyntheticNaming;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.InternalOptions;
@@ -1406,6 +1407,7 @@
       return appInfo
           .getSyntheticItems()
           .createMethod(
+              SyntheticNaming.SyntheticKind.BACKPORT,
               context,
               appInfo.dexItemFactory(),
               builder ->
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 891beb3..5a00a4b 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
@@ -57,6 +57,7 @@
 import com.android.tools.r8.origin.SynthesizedOrigin;
 import com.android.tools.r8.position.MethodPosition;
 import com.android.tools.r8.shaking.MainDexClasses;
+import com.android.tools.r8.synthesis.SyntheticNaming;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.IterableUtils;
@@ -398,6 +399,7 @@
             appView
                 .getSyntheticItems()
                 .createMethod(
+                    SyntheticNaming.SyntheticKind.STATIC_INTERFACE_CALL,
                     context.getHolder(),
                     factory,
                     syntheticMethodBuilder ->
@@ -1139,6 +1141,7 @@
       Builder<?> builder, Flavor flavour, Consumer<ProgramMethod> newSynthesizedMethodConsumer) {
     ClassProcessor processor = new ClassProcessor(appView, this, newSynthesizedMethodConsumer);
     // First we compute all desugaring *without* introducing forwarding methods.
+    assert appView.getSyntheticItems().verifyNonLegacySyntheticsAreCommitted();
     for (DexProgramClass clazz : builder.getProgramClasses()) {
       if (shouldProcess(clazz, flavour, false)) {
         if (appView.isAlreadyLibraryDesugared(clazz)) {
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
index a50581e..966cfec 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
@@ -8,7 +8,6 @@
 import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.ClassAccessFlags;
 import com.android.tools.r8.graph.DexAnnotationSet;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
@@ -20,10 +19,8 @@
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.DexTypeList;
 import com.android.tools.r8.graph.DexValue.DexValueNull;
 import com.android.tools.r8.graph.FieldAccessFlags;
-import com.android.tools.r8.graph.GenericSignature.ClassSignature;
 import com.android.tools.r8.graph.GenericSignature.FieldTypeSignature;
 import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
 import com.android.tools.r8.graph.MethodAccessFlags;
@@ -37,18 +34,11 @@
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackSimple;
 import com.android.tools.r8.ir.synthetic.ForwardMethodSourceCode;
 import com.android.tools.r8.ir.synthetic.SynthesizedCode;
-import com.android.tools.r8.origin.SynthesizedOrigin;
+import com.android.tools.r8.synthesis.SyntheticClassBuilder;
 import com.android.tools.r8.utils.OptionalBool;
-import com.google.common.base.Suppliers;
-import com.google.common.primitives.Longs;
-import java.nio.ByteBuffer;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.Supplier;
-import java.util.zip.CRC32;
 
 /**
  * Represents lambda class generated for a lambda descriptor in context of lambda instantiation
@@ -77,31 +67,27 @@
   final DexMethod classConstructor;
   public final DexField lambdaField;
   public final Target target;
-  public final AtomicBoolean addToMainDexList = new AtomicBoolean(false);
-  private final Collection<DexProgramClass> synthesizedFrom = new ArrayList<>(1);
-  private final Supplier<DexProgramClass> lazyDexClass =
-      Suppliers.memoize(this::synthesizeLambdaClass); // NOTE: thread-safe.
+
+  // Considered final but is set after due to circularity in allocation.
+  private DexProgramClass clazz = null;
 
   LambdaClass(
+      SyntheticClassBuilder builder,
       AppView<?> appView,
       LambdaRewriter rewriter,
       ProgramMethod accessedFrom,
-      DexType lambdaClassType,
       LambdaDescriptor descriptor) {
     assert rewriter != null;
-    assert lambdaClassType != null;
     assert descriptor != null;
-
+    this.type = builder.getType();
     this.appView = appView;
     this.rewriter = rewriter;
-    this.type = lambdaClassType;
     this.descriptor = descriptor;
 
-    DexItemFactory factory = appView.dexItemFactory();
+    DexItemFactory factory = builder.getFactory();
     DexProto constructorProto = factory.createProto(
         factory.voidType, descriptor.captures.values);
-    this.constructor =
-        factory.createMethod(lambdaClassType, constructorProto, factory.constructorMethodName);
+    this.constructor = factory.createMethod(type, constructorProto, factory.constructorMethodName);
 
     this.target = createTarget(accessedFrom);
 
@@ -109,98 +95,32 @@
     this.classConstructor =
         !stateless
             ? null
-            : factory.createMethod(
-                lambdaClassType, constructorProto, factory.classConstructorMethodName);
+            : factory.createMethod(type, constructorProto, factory.classConstructorMethodName);
     this.lambdaField =
-        !stateless
-            ? null
-            : factory.createField(lambdaClassType, lambdaClassType, rewriter.instanceFieldName);
+        !stateless ? null : factory.createField(type, type, rewriter.instanceFieldName);
+
+    // Synthesize the program class one all fields are set.
+    synthesizeLambdaClass(builder);
   }
 
-  // Generate unique lambda class type for lambda descriptor and instantiation point context.
-  public static DexType createLambdaClassType(
-      AppView<?> appView, ProgramMethod accessedFrom, LambdaDescriptor match) {
-    StringBuilder lambdaClassDescriptor = new StringBuilder("L");
-
-    // We always create lambda class in the same package where it is referenced.
-    String packageDescriptor = accessedFrom.getHolderType().getPackageDescriptor();
-    if (!packageDescriptor.isEmpty()) {
-      lambdaClassDescriptor.append(packageDescriptor).append('/');
-    }
-
-    // Lambda class name prefix
-    lambdaClassDescriptor.append(LambdaRewriter.LAMBDA_CLASS_NAME_PREFIX);
-
-    // If the lambda class should match 1:1 the class it is accessed from, we
-    // just add the name of this type to make lambda class name unique.
-    // It also helps link the class lambda originated from in some cases.
-    if (match.delegatesToLambdaImplMethod() || match.needsAccessor(accessedFrom)) {
-      lambdaClassDescriptor.append(accessedFrom.getHolderType().getName()).append('$');
-    }
-
-    // Add unique lambda descriptor id
-    lambdaClassDescriptor.append(match.uniqueId).append(';');
-    return appView.dexItemFactory().createType(lambdaClassDescriptor.toString());
-  }
-
-  public final DexProgramClass getOrCreateLambdaClass() {
-    return lazyDexClass.get();
-  }
-
-  private DexProgramClass synthesizeLambdaClass() {
-    DexMethod mainMethod =
-        appView.dexItemFactory().createMethod(type, descriptor.erasedProto, descriptor.name);
-
-    DexProgramClass clazz =
-        new DexProgramClass(
-            type,
-            null,
-            new SynthesizedOrigin("lambda desugaring", getClass()),
-            // Make the synthesized class public, as it might end up being accessed from a different
-            // classloader (package private access is not allowed across classloaders, b/72538146).
-            ClassAccessFlags.fromDexAccessFlags(
-                Constants.ACC_FINAL | Constants.ACC_SYNTHETIC | Constants.ACC_PUBLIC),
-            appView.dexItemFactory().objectType,
-            buildInterfaces(),
-            appView.dexItemFactory().createString("lambda"),
-            null,
-            Collections.emptyList(),
-            null,
-            Collections.emptyList(),
-            ClassSignature.noSignature(),
-            DexAnnotationSet.empty(),
-            synthesizeStaticFields(),
-            synthesizeInstanceFields(),
-            synthesizeDirectMethods(),
-            synthesizeVirtualMethods(mainMethod),
-            appView.dexItemFactory().getSkipNameValidationForTesting(),
-            LambdaClass::computeChecksumForSynthesizedClass);
-    appView.appInfo().addSynthesizedClass(clazz, false);
-
-    // The method addSynthesizedFrom() may be called concurrently. To avoid a Concurrent-
-    // ModificationException we must use synchronization.
-    synchronized (synthesizedFrom) {
-      synthesizedFrom.forEach(clazz::addSynthesizedFrom);
-    }
+  public final DexProgramClass getLambdaProgramClass() {
+    assert clazz != null;
     return clazz;
   }
 
-  private static long computeChecksumForSynthesizedClass(DexProgramClass clazz) {
-    // Checksum of synthesized classes are compute based off the depending input. This might
-    // create false positives (ie: unchanged lambda class detected as changed even thought only
-    // an unrelated part from a synthesizedFrom class is changed).
+  void setClass(DexProgramClass clazz) {
+    assert this.clazz == null;
+    assert clazz != null;
+    assert type == clazz.type;
+    this.clazz = clazz;
+  }
 
-    // Ideally, we should use some hashcode of the dex program class that is deterministic across
-    // compiles.
-    Collection<DexProgramClass> synthesizedFrom = clazz.getSynthesizedFrom();
-    ByteBuffer buffer = ByteBuffer.allocate(synthesizedFrom.size() * Longs.BYTES);
-    for (DexProgramClass from : synthesizedFrom) {
-      buffer.putLong(from.getChecksum());
-    }
-    CRC32 crc = new CRC32();
-    byte[] array = buffer.array();
-    crc.update(array, 0, array.length);
-    return crc.getValue();
+  private void synthesizeLambdaClass(SyntheticClassBuilder builder) {
+    builder.setInterfaces(descriptor.interfaces);
+    synthesizeStaticFields(builder);
+    synthesizeInstanceFields(builder);
+    synthesizeDirectMethods(builder);
+    synthesizeVirtualMethods(builder);
   }
 
   final DexField getCaptureField(int index) {
@@ -216,24 +136,15 @@
     return descriptor.isStateless();
   }
 
-  void addSynthesizedFrom(DexProgramClass clazz) {
-    assert clazz != null;
-    synchronized (synthesizedFrom) {
-      if (synthesizedFrom.add(clazz)) {
-        // The lambda class may already have been synthesized, and we therefore need to update the
-        // synthesized lambda class as well.
-        getOrCreateLambdaClass().addSynthesizedFrom(clazz);
-      }
-    }
-  }
-
   // Synthesize virtual methods.
-  private DexEncodedMethod[] synthesizeVirtualMethods(DexMethod mainMethod) {
-    DexEncodedMethod[] methods = new DexEncodedMethod[1 + descriptor.bridges.size()];
-    int index = 0;
+  private void synthesizeVirtualMethods(SyntheticClassBuilder builder) {
+    DexMethod mainMethod =
+        appView.dexItemFactory().createMethod(type, descriptor.erasedProto, descriptor.name);
+
+    List<DexEncodedMethod> methods = new ArrayList<>(1 + descriptor.bridges.size());
 
     // Synthesize main method.
-    methods[index++] =
+    methods.add(
         new DexEncodedMethod(
             mainMethod,
             MethodAccessFlags.fromSharedAccessFlags(
@@ -242,13 +153,13 @@
             DexAnnotationSet.empty(),
             ParameterAnnotationsList.empty(),
             LambdaMainMethodSourceCode.build(this, mainMethod),
-            true);
+            true));
 
     // Synthesize bridge methods.
     for (DexProto bridgeProto : descriptor.bridges) {
       DexMethod bridgeMethod =
           appView.dexItemFactory().createMethod(type, bridgeProto, descriptor.name);
-      methods[index++] =
+      methods.add(
           new DexEncodedMethod(
               bridgeMethod,
               MethodAccessFlags.fromSharedAccessFlags(
@@ -261,18 +172,18 @@
               DexAnnotationSet.empty(),
               ParameterAnnotationsList.empty(),
               LambdaBridgeMethodSourceCode.build(this, bridgeMethod, mainMethod),
-              true);
+              true));
     }
-    return methods;
+    builder.setVirtualMethods(methods);
   }
 
   // Synthesize direct methods.
-  private DexEncodedMethod[] synthesizeDirectMethods() {
+  private void synthesizeDirectMethods(SyntheticClassBuilder builder) {
     boolean stateless = isStateless();
-    DexEncodedMethod[] methods = new DexEncodedMethod[stateless ? 2 : 1];
+    List<DexEncodedMethod> methods = new ArrayList<>(stateless ? 2 : 1);
 
     // Constructor.
-    methods[0] =
+    methods.add(
         new DexEncodedMethod(
             constructor,
             MethodAccessFlags.fromSharedAccessFlags(
@@ -283,11 +194,11 @@
             DexAnnotationSet.empty(),
             ParameterAnnotationsList.empty(),
             LambdaConstructorSourceCode.build(this),
-            true);
+            true));
 
     // Class constructor for stateless lambda classes.
     if (stateless) {
-      methods[1] =
+      methods.add(
           new DexEncodedMethod(
               classConstructor,
               MethodAccessFlags.fromSharedAccessFlags(
@@ -296,61 +207,50 @@
               DexAnnotationSet.empty(),
               ParameterAnnotationsList.empty(),
               LambdaClassConstructorSourceCode.build(this),
-              true);
-      feedback.classInitializerMayBePostponed(methods[1]);
+              true));
+      feedback.classInitializerMayBePostponed(methods.get(1));
     }
-    return methods;
+    builder.setDirectMethods(methods);
   }
 
   // Synthesize instance fields to represent captured values.
-  private DexEncodedField[] synthesizeInstanceFields() {
+  private void synthesizeInstanceFields(SyntheticClassBuilder builder) {
     DexType[] fieldTypes = descriptor.captures.values;
     int fieldCount = fieldTypes.length;
-    DexEncodedField[] fields = new DexEncodedField[fieldCount];
+    List<DexEncodedField> fields = new ArrayList<>(fieldCount);
     for (int i = 0; i < fieldCount; i++) {
       FieldAccessFlags accessFlags =
           FieldAccessFlags.fromSharedAccessFlags(
               Constants.ACC_FINAL | Constants.ACC_SYNTHETIC | Constants.ACC_PUBLIC);
-      fields[i] =
+      fields.add(
           new DexEncodedField(
               getCaptureField(i),
               accessFlags,
               FieldTypeSignature.noSignature(),
               DexAnnotationSet.empty(),
-              null);
+              null));
     }
-    return fields;
+    builder.setInstanceFields(fields);
   }
 
   // Synthesize static fields to represent singleton instance.
-  private DexEncodedField[] synthesizeStaticFields() {
-    if (!isStateless()) {
-      return DexEncodedField.EMPTY_ARRAY;
+  private void synthesizeStaticFields(SyntheticClassBuilder builder) {
+    if (isStateless()) {
+      // Create instance field for stateless lambda.
+      assert this.lambdaField != null;
+      builder.setStaticFields(
+          Collections.singletonList(
+              new DexEncodedField(
+                  this.lambdaField,
+                  FieldAccessFlags.fromSharedAccessFlags(
+                      Constants.ACC_PUBLIC
+                          | Constants.ACC_FINAL
+                          | Constants.ACC_SYNTHETIC
+                          | Constants.ACC_STATIC),
+                  FieldTypeSignature.noSignature(),
+                  DexAnnotationSet.empty(),
+                  DexValueNull.NULL)));
     }
-
-    // Create instance field for stateless lambda.
-    assert this.lambdaField != null;
-    DexEncodedField[] fields = new DexEncodedField[1];
-    fields[0] =
-        new DexEncodedField(
-            this.lambdaField,
-            FieldAccessFlags.fromSharedAccessFlags(
-                Constants.ACC_PUBLIC
-                    | Constants.ACC_FINAL
-                    | Constants.ACC_SYNTHETIC
-                    | Constants.ACC_STATIC),
-            FieldTypeSignature.noSignature(),
-            DexAnnotationSet.empty(),
-            DexValueNull.NULL);
-    return fields;
-  }
-
-  // Build a list of implemented interfaces.
-  private DexTypeList buildInterfaces() {
-    List<DexType> interfaces = descriptor.interfaces;
-    return interfaces.isEmpty()
-        ? DexTypeList.empty()
-        : new DexTypeList(interfaces.toArray(DexType.EMPTY_ARRAY));
   }
 
   // Creates a delegation target for this particular lambda class. Note that we
@@ -643,11 +543,15 @@
                         newMethod.getCode(), callTarget.getArity(), appView);
                     return newMethod;
                   });
-
-      assert replacement != null
-          : "Unexpected failure to find direct lambda target for: " + implMethod.qualifiedName();
-
-      return new ProgramMethod(implMethodHolder, replacement);
+      if (replacement != null) {
+        return new ProgramMethod(implMethodHolder, replacement);
+      }
+      // The method might already have been moved by another invoke-dynamic targeting it.
+      // If so, it must be defined on the holder.
+      ProgramMethod modified = implMethodHolder.lookupProgramMethod(callTarget);
+      assert modified != null;
+      assert modified.getDefinition().isNonPrivateVirtualMethod();
+      return modified;
     }
   }
 
@@ -713,11 +617,27 @@
                     rewriter.forcefullyMoveMethod(encodedMethod.method, callTarget);
                     return newMethod;
                   });
-      return new ProgramMethod(implMethodHolder, replacement);
+      if (replacement != null) {
+        return new ProgramMethod(implMethodHolder, replacement);
+      }
+      // The method might already have been moved by another invoke-dynamic targeting it.
+      // If so, it must be defined on the holder.
+      ProgramMethod modified = implMethodHolder.lookupProgramMethod(callTarget);
+      assert modified != null;
+      assert modified.getDefinition().isNonPrivateVirtualMethod();
+      return modified;
     }
 
     private ProgramMethod createSyntheticAccessor(
         DexMethod implMethod, DexProgramClass implMethodHolder) {
+      // The accessor might already have been created by another invoke-dynamic targeting it.
+      ProgramMethod existing = implMethodHolder.lookupProgramMethod(callTarget);
+      if (existing != null) {
+        assert existing.getAccessFlags().isSynthetic();
+        assert existing.getAccessFlags().isPublic();
+        assert existing.getDefinition().isVirtualMethod();
+        return existing;
+      }
       MethodAccessFlags accessorFlags =
           MethodAccessFlags.fromSharedAccessFlags(
               Constants.ACC_SYNTHETIC | Constants.ACC_PUBLIC, false);
@@ -762,6 +682,15 @@
       DexProgramClass accessorClass = appView.definitionForProgramType(callTarget.holder);
       assert accessorClass != null;
 
+      // The accessor might already have been created by another invoke-dynamic targeting it.
+      ProgramMethod existing = accessorClass.lookupProgramMethod(callTarget);
+      if (existing != null) {
+        assert existing.getAccessFlags().isSynthetic();
+        assert existing.getAccessFlags().isPublic();
+        assert existing.getAccessFlags().isStatic();
+        return existing;
+      }
+
       // Always make the method public to provide access when r8 minification is allowed to move
       // the lambda class accessing this method to another package (-allowaccessmodification).
       MethodAccessFlags accessorFlags =
@@ -779,11 +708,7 @@
               AccessorMethodSourceCode.build(LambdaClass.this, callTarget),
               true);
 
-      // We may arrive here concurrently so we need must update the methods of the class atomically.
-      synchronized (accessorClass) {
-        accessorClass.addDirectMethod(accessorEncodedMethod);
-      }
-
+      accessorClass.addDirectMethod(accessorEncodedMethod);
       return new ProgramMethod(accessorClass, accessorEncodedMethod);
     }
   }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaRewriter.java
index f464fdd..ee459b4 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaRewriter.java
@@ -16,7 +16,6 @@
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.CfCode;
-import com.android.tools.r8.graph.DexApplication.Builder;
 import com.android.tools.r8.graph.DexCallSite;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -28,19 +27,10 @@
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.GraphLens.NestedGraphLens;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.analysis.type.Nullability;
-import com.android.tools.r8.ir.analysis.type.TypeElement;
-import com.android.tools.r8.ir.code.BasicBlock;
-import com.android.tools.r8.ir.code.IRCode;
-import com.android.tools.r8.ir.code.InstructionListIterator;
-import com.android.tools.r8.ir.code.InvokeCustom;
-import com.android.tools.r8.ir.code.InvokeDirect;
-import com.android.tools.r8.ir.code.NewInstance;
-import com.android.tools.r8.ir.code.StaticGet;
-import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.ir.conversion.IRConverter;
-import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.synthesis.SyntheticNaming;
+import com.android.tools.r8.utils.Box;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap;
 import com.android.tools.r8.utils.collections.BidirectionalOneToOneMap;
 import com.android.tools.r8.utils.collections.SortedProgramMethodSet;
@@ -49,9 +39,8 @@
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.IdentityHashMap;
+import java.util.Collections;
 import java.util.List;
-import java.util.ListIterator;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
@@ -69,7 +58,6 @@
 public class LambdaRewriter {
 
   // Public for testing.
-  public static final String LAMBDA_CLASS_NAME_PREFIX = "-$$Lambda$";
   public static final String LAMBDA_GROUP_CLASS_NAME_PREFIX = "-$$LambdaGroup$";
   static final String EXPECTED_LAMBDA_METHOD_PREFIX = "lambda$";
   public static final String LAMBDA_INSTANCE_FIELD_NAME = "INSTANCE";
@@ -81,16 +69,10 @@
   private final LambdaRewriterLens.Builder lensBuilder = LambdaRewriterLens.builder();
   private final Set<DexMethod> forcefullyMovedMethods = Sets.newIdentityHashSet();
 
-  // Maps call sites seen so far to inferred lambda descriptor. It is intended
-  // to help avoid re-matching call sites we already seen. Note that same call
-  // site may match one or several lambda classes.
-  //
-  // NOTE: synchronize concurrent access on `knownCallSites`.
-  private final Map<DexCallSite, LambdaDescriptor> knownCallSites = new IdentityHashMap<>();
   // Maps lambda class type into lambda class representation. Since lambda class
   // type uniquely defines lambda class, effectively canonicalizes lambda classes.
   // NOTE: synchronize concurrent access on `knownLambdaClasses`.
-  private final Map<DexType, LambdaClass> knownLambdaClasses = new IdentityHashMap<>();
+  private final List<LambdaClass> knownLambdaClasses = new ArrayList<>();
 
   public LambdaRewriter(AppView<?> appView) {
     this.appView = appView;
@@ -138,7 +120,7 @@
           if (descriptor == null) {
             return null;
           }
-          return getOrCreateLambdaClass(descriptor, method);
+          return createLambdaClass(descriptor, method);
         });
   }
 
@@ -209,16 +191,10 @@
   }
 
   /** Generates lambda classes and adds them to the builder. */
-  public void finalizeLambdaDesugaringForD8(
-      Builder<?> builder, IRConverter converter, ExecutorService executorService)
+  public void finalizeLambdaDesugaringForD8(IRConverter converter, ExecutorService executorService)
       throws ExecutionException {
     synthesizeAccessibilityBridgesForLambdaClassesD8(
-        knownLambdaClasses.values(), converter, executorService);
-    for (LambdaClass lambdaClass : knownLambdaClasses.values()) {
-      DexProgramClass synthesizedClass = lambdaClass.getOrCreateLambdaClass();
-      appView.appInfo().addSynthesizedClass(synthesizedClass, lambdaClass.addToMainDexList.get());
-      builder.addSynthesizedClass(synthesizedClass);
-    }
+        knownLambdaClasses, converter, executorService);
     fixup();
     optimizeSynthesizedClasses(converter, executorService);
   }
@@ -226,183 +202,35 @@
   private void optimizeSynthesizedClasses(IRConverter converter, ExecutorService executorService)
       throws ExecutionException {
     converter.optimizeSynthesizedClasses(
-        knownLambdaClasses.values().stream()
-            .map(LambdaClass::getOrCreateLambdaClass)
+        knownLambdaClasses.stream()
+            .map(LambdaClass::getLambdaProgramClass)
             .collect(ImmutableSet.toImmutableSet()),
         executorService);
   }
 
-  // Matches invoke-custom instruction operands to infer lambda descriptor
-  // corresponding to this lambda invocation point.
-  //
-  // Returns the lambda descriptor or `MATCH_FAILED`.
-  private LambdaDescriptor inferLambdaDescriptor(DexCallSite callSite, ProgramMethod context) {
-    // We check the map before and after inferring lambda descriptor to minimize time
-    // spent in synchronized block. As a result we may throw away calculated descriptor
-    // in rare case when another thread has same call site processed concurrently,
-    // but this is a low price to pay comparing to making whole method synchronous.
-    LambdaDescriptor descriptor = getKnown(knownCallSites, callSite);
-    return descriptor != null
-        ? descriptor
-        : putIfAbsent(
-            knownCallSites,
-            callSite,
-            LambdaDescriptor.infer(callSite, appView.appInfoForDesugaring(), context));
-  }
-
-  // Returns a lambda class corresponding to the lambda descriptor and context,
-  // creates the class if it does not yet exist.
-  public LambdaClass getOrCreateLambdaClass(
-      LambdaDescriptor descriptor, ProgramMethod accessedFrom) {
-    DexType lambdaClassType = LambdaClass.createLambdaClassType(appView, accessedFrom, descriptor);
-    // We check the map twice to to minimize time spent in synchronized block.
-    LambdaClass lambdaClass = getKnown(knownLambdaClasses, lambdaClassType);
-    if (lambdaClass == null) {
-      lambdaClass =
-          putIfAbsent(
-              knownLambdaClasses,
-              lambdaClassType,
-              new LambdaClass(appView, this, accessedFrom, lambdaClassType, descriptor));
-      if (appView.options().isDesugaredLibraryCompilation()) {
-        DexType rewrittenType =
-            appView.rewritePrefix.rewrittenType(accessedFrom.getHolderType(), appView);
-        if (rewrittenType == null) {
-          rewrittenType =
-              appView
-                  .options()
-                  .desugaredLibraryConfiguration
-                  .getEmulateLibraryInterface()
-                  .get(accessedFrom.getHolderType());
-        }
-        if (rewrittenType != null) {
-          addRewritingPrefix(accessedFrom, rewrittenType, lambdaClassType);
-        }
-      }
-    }
-    lambdaClass.addSynthesizedFrom(accessedFrom.getHolder());
-    if (appView.appInfo().getMainDexClasses().contains(accessedFrom.getHolder())) {
-      lambdaClass.addToMainDexList.set(true);
+  // Creates a lambda class corresponding to the lambda descriptor and context.
+  public LambdaClass createLambdaClass(LambdaDescriptor descriptor, ProgramMethod accessedFrom) {
+    Box<LambdaClass> box = new Box<>();
+    DexProgramClass clazz =
+        appView
+            .getSyntheticItems()
+            .createClass(
+                SyntheticNaming.SyntheticKind.LAMBDA,
+                accessedFrom.getHolder(),
+                appView.dexItemFactory(),
+                builder ->
+                    box.set(new LambdaClass(builder, appView, this, accessedFrom, descriptor)));
+    // Immediately set the actual program class on the lambda.
+    LambdaClass lambdaClass = box.get();
+    lambdaClass.setClass(clazz);
+    synchronized (knownLambdaClasses) {
+      knownLambdaClasses.add(lambdaClass);
     }
     return lambdaClass;
   }
 
-  private LambdaClass getKnownLambdaClass(LambdaDescriptor descriptor, ProgramMethod accessedFrom) {
-    DexType lambdaClassType = LambdaClass.createLambdaClassType(appView, accessedFrom, descriptor);
-    return getKnown(knownLambdaClasses, lambdaClassType);
-  }
-
-  private void addRewritingPrefix(
-      ProgramMethod context, DexType rewritten, DexType lambdaClassType) {
-    String javaName = lambdaClassType.toString();
-    String typeString = context.getHolderType().toString();
-    String actualPrefix = typeString.substring(0, typeString.lastIndexOf('.'));
-    String rewrittenString = rewritten.toString();
-    String actualRewrittenPrefix = rewrittenString.substring(0, rewrittenString.lastIndexOf('.'));
-    assert javaName.startsWith(actualPrefix);
-    appView.rewritePrefix.rewriteType(
-        lambdaClassType,
-        appView
-            .dexItemFactory()
-            .createType(
-                DescriptorUtils.javaTypeToDescriptor(
-                    actualRewrittenPrefix + javaName.substring(actualPrefix.length()))));
-  }
-
-  private static <K, V> V getKnown(Map<K, V> map, K key) {
-    synchronized (map) {
-      return map.get(key);
-    }
-  }
-
-  private static <K, V> V putIfAbsent(Map<K, V> map, K key, V value) {
-    synchronized (map) {
-      V known = map.get(key);
-      if (known != null) {
-        return known;
-      }
-      map.put(key, value);
-      return value;
-    }
-  }
-
-  // Patches invoke-custom instruction to create or get an instance
-  // of the generated lambda class.
-  private void patchInstruction(
-      InvokeCustom invoke,
-      LambdaClass lambdaClass,
-      IRCode code,
-      ListIterator<BasicBlock> blocks,
-      InstructionListIterator instructions,
-      Set<Value> affectedValues) {
-    assert lambdaClass != null;
-    assert instructions != null;
-
-    // The value representing new lambda instance: we reuse the
-    // value from the original invoke-custom instruction, and thus
-    // all its usages.
-    Value lambdaInstanceValue = invoke.outValue();
-    if (lambdaInstanceValue == null) {
-      // The out value might be empty in case it was optimized out.
-      lambdaInstanceValue =
-          code.createValue(
-              TypeElement.fromDexType(lambdaClass.type, Nullability.maybeNull(), appView));
-    } else {
-      affectedValues.add(lambdaInstanceValue);
-    }
-
-    // For stateless lambdas we replace InvokeCustom instruction with StaticGet
-    // reading the value of INSTANCE field created for singleton lambda class.
-    if (lambdaClass.isStateless()) {
-      instructions.replaceCurrentInstruction(
-          new StaticGet(lambdaInstanceValue, lambdaClass.lambdaField));
-      // Note that since we replace one throwing operation with another we don't need
-      // to have any special handling for catch handlers.
-      return;
-    }
-
-    // For stateful lambdas we always create a new instance since we need to pass
-    // captured values to the constructor.
-    //
-    // We replace InvokeCustom instruction with a new NewInstance instruction
-    // instantiating lambda followed by InvokeDirect instruction calling a
-    // constructor on it.
-    //
-    //    original:
-    //      Invoke-Custom rResult <- { rArg0, rArg1, ... }; call site: ...
-    //
-    //    result:
-    //      NewInstance   rResult <-  LambdaClass
-    //      Invoke-Direct { rResult, rArg0, rArg1, ... }; method: void LambdaClass.<init>(...)
-    lambdaInstanceValue.setType(
-        lambdaInstanceValue.getType().asReferenceType().asDefinitelyNotNull());
-    NewInstance newInstance = new NewInstance(lambdaClass.type, lambdaInstanceValue);
-    instructions.replaceCurrentInstruction(newInstance);
-
-    List<Value> arguments = new ArrayList<>();
-    arguments.add(lambdaInstanceValue);
-    arguments.addAll(invoke.arguments()); // Optional captures.
-    InvokeDirect constructorCall =
-        new InvokeDirect(lambdaClass.constructor, null /* no return value */, arguments);
-    instructions.add(constructorCall);
-    constructorCall.setPosition(newInstance.getPosition());
-
-    // If we don't have catch handlers we are done.
-    if (!constructorCall.getBlock().hasCatchHandlers()) {
-      return;
-    }
-
-    // Move the iterator back to position it between the two instructions, split
-    // the block between the two instructions, and copy the catch handlers.
-    instructions.previous();
-    assert instructions.peekNext().isInvokeDirect();
-    BasicBlock currentBlock = newInstance.getBlock();
-    BasicBlock nextBlock = instructions.split(code, blocks);
-    assert !instructions.hasNext();
-    nextBlock.copyCatchHandlers(code, blocks, currentBlock, appView.options());
-  }
-
-  public Map<DexType, LambdaClass> getKnownLambdaClasses() {
-    return knownLambdaClasses;
+  public Collection<LambdaClass> getKnownLambdaClasses() {
+    return Collections.unmodifiableList(knownLambdaClasses);
   }
 
   public NestedGraphLens fixup() {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/UtilityMethodsForCodeOptimizations.java b/src/main/java/com/android/tools/r8/ir/optimize/UtilityMethodsForCodeOptimizations.java
index 1b55f13..a972b39 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/UtilityMethodsForCodeOptimizations.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/UtilityMethodsForCodeOptimizations.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.optimize.templates.CfUtilityMethodsForCodeOptimizations;
 import com.android.tools.r8.synthesis.SyntheticItems;
+import com.android.tools.r8.synthesis.SyntheticNaming;
 import com.android.tools.r8.utils.InternalOptions;
 
 public class UtilityMethodsForCodeOptimizations {
@@ -31,6 +32,7 @@
     SyntheticItems syntheticItems = appView.getSyntheticItems();
     ProgramMethod syntheticMethod =
         syntheticItems.createMethod(
+            SyntheticNaming.SyntheticKind.TO_STRING_IF_NOT_NULL,
             context,
             dexItemFactory,
             builder ->
@@ -60,6 +62,7 @@
     SyntheticItems syntheticItems = appView.getSyntheticItems();
     ProgramMethod syntheticMethod =
         syntheticItems.createMethod(
+            SyntheticNaming.SyntheticKind.THROW_CCE_IF_NOT_NULL,
             context,
             dexItemFactory,
             builder ->
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
index b5dfee6..9868a64 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
@@ -1090,10 +1090,18 @@
       } else if (classInitializerSideEffect.canBePostponed()) {
         feedback.classInitializerMayBePostponed(method);
       } else {
-        assert !context.getHolderType().isD8R8SynthesizedLambdaClassType()
-                || options.debug
-                || appView.appInfo().hasPinnedInstanceInitializer(context.getHolderType())
-                || appView.options().horizontalClassMergerOptions().isJavaLambdaMergingEnabled()
+        assert options.debug
+                || appView
+                    .getSyntheticItems()
+                    .verifySyntheticLambdaProperty(
+                        context.getHolder(),
+                        lambdaClass ->
+                            appView.appInfo().hasPinnedInstanceInitializer(lambdaClass.getType())
+                                || appView
+                                    .options()
+                                    .horizontalClassMergerOptions()
+                                    .isJavaLambdaMergingEnabled(),
+                        nonLambdaClass -> true)
             : "Unexpected observable side effects from lambda `" + context.toSourceString() + "`";
       }
       return;
diff --git a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
index ef724ec..cf6a117 100644
--- a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
@@ -40,7 +40,7 @@
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.naming.ProguardMapSupplier;
 import com.android.tools.r8.references.Reference;
-import com.android.tools.r8.synthesis.SyntheticItems;
+import com.android.tools.r8.synthesis.SyntheticNaming;
 import com.android.tools.r8.utils.AsmUtils;
 import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.InternalOptions;
@@ -124,7 +124,7 @@
         marker.isRelocator() ? Optional.empty() : Optional.of(marker.toString());
     LensCodeRewriterUtils rewriter = new LensCodeRewriterUtils(appView);
     for (DexProgramClass clazz : application.classes()) {
-      assert SyntheticItems.verifyNotInternalSynthetic(clazz.getType());
+      assert SyntheticNaming.verifyNotInternalSynthetic(clazz.getType());
       try {
         writeClass(clazz, consumer, rewriter, markerString);
       } catch (ClassTooLargeException e) {
@@ -194,6 +194,7 @@
     for (int i = 0; i < clazz.interfaces.values.length; i++) {
       interfaces[i] = namingLens.lookupInternalName(clazz.interfaces.values[i]);
     }
+    assert SyntheticNaming.verifyNotInternalSynthetic(name);
     writer.visit(version.raw(), access, name, signature, superName, interfaces);
     writeAnnotations(writer::visitAnnotation, clazz.annotations().annotations);
     ImmutableMap<DexString, DexValue> defaults = getAnnotationDefaults(clazz.annotations());
diff --git a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
index b374e58..73037aa 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -269,7 +269,7 @@
     this.switchMaps = switchMaps;
     this.lockCandidates = lockCandidates;
     this.initClassReferences = initClassReferences;
-    verify();
+    assert verify();
   }
 
   private AppInfoWithLiveness(AppInfoWithLiveness previous, CommittedItems committedItems) {
@@ -366,10 +366,11 @@
         previous.initClassReferences);
   }
 
-  private void verify() {
+  private boolean verify() {
     assert keepInfo.verifyPinnedTypesAreLive(liveTypes);
     assert objectAllocationInfoCollection.verifyAllocatedTypesAreLive(
         liveTypes, getMissingClasses(), this);
+    return true;
   }
 
   private static KeepInfoCollection extendPinnedItems(
@@ -454,7 +455,7 @@
     this.lockCandidates = previous.lockCandidates;
     this.initClassReferences = previous.initClassReferences;
     previous.markObsolete();
-    verify();
+    assert verify();
   }
 
   public static AppInfoWithLivenessModifier modifier() {
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index c15fdb8..90416fd 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -921,7 +921,7 @@
       assert contextMethod.getCode().isCfCode() : "Unexpected input type with lambdas";
       CfCode code = contextMethod.getCode().asCfCode();
       if (code != null) {
-        LambdaClass lambdaClass = lambdaRewriter.getOrCreateLambdaClass(descriptor, context);
+        LambdaClass lambdaClass = lambdaRewriter.createLambdaClass(descriptor, context);
         lambdaClasses.put(lambdaClass.type, new Pair<>(lambdaClass, context));
         lambdaCallSites
             .computeIfAbsent(context, k -> new IdentityHashMap<>())
@@ -3038,13 +3038,9 @@
       return empty;
     }
 
-    void addInstantiatedClass(
-        DexProgramClass clazz, ProgramMethod context, boolean isMainDexClass) {
+    void addInstantiatedClass(DexProgramClass clazz, ProgramMethod context) {
       assert !syntheticInstantiations.containsKey(clazz.type);
       syntheticInstantiations.put(clazz.type, new Pair<>(clazz, context));
-      if (isMainDexClass) {
-        mainDexTypes.add(clazz);
-      }
     }
 
     void addClasspathClass(DexClasspathClass clazz) {
@@ -3066,10 +3062,6 @@
 
     void amendApplication(Builder appBuilder) {
       assert !isEmpty();
-      for (Pair<DexProgramClass, ProgramMethod> clazzAndContext :
-          syntheticInstantiations.values()) {
-        appBuilder.addProgramClass(clazzAndContext.getFirst());
-      }
       appBuilder.addClasspathClasses(syntheticClasspathClasses.values());
     }
 
@@ -3193,8 +3185,8 @@
       // Add all desugared classes to the application, main-dex list, and mark them instantiated.
       LambdaClass lambdaClass = lambdaClassAndContext.getFirst();
       ProgramMethod context = lambdaClassAndContext.getSecond();
-      DexProgramClass programClass = lambdaClass.getOrCreateLambdaClass();
-      additions.addInstantiatedClass(programClass, context, lambdaClass.addToMainDexList.get());
+      DexProgramClass programClass = lambdaClass.getLambdaProgramClass();
+      additions.addInstantiatedClass(programClass, context);
       // Mark the instance constructor targeted and live.
       DexEncodedMethod constructor = programClass.lookupDirectMethod(lambdaClass.constructor);
       KeepReason reason = KeepReason.instantiatedIn(context);
@@ -3349,10 +3341,9 @@
     lambdaRewriter
         .getKnownLambdaClasses()
         .forEach(
-            (type, lambda) -> {
-              DexProgramClass synthesizedClass = lambda.getOrCreateLambdaClass();
+            lambda -> {
+              DexProgramClass synthesizedClass = lambda.getLambdaProgramClass();
               assert synthesizedClass != null;
-              assert synthesizedClass == appInfo().definitionForWithoutExistenceAssert(type);
               assert liveTypes.contains(synthesizedClass);
               if (synthesizedClass == null) {
                 return;
diff --git a/src/main/java/com/android/tools/r8/synthesis/CommittedItems.java b/src/main/java/com/android/tools/r8/synthesis/CommittedItems.java
index 424fd8d..e60dfa3 100644
--- a/src/main/java/com/android/tools/r8/synthesis/CommittedItems.java
+++ b/src/main/java/com/android/tools/r8/synthesis/CommittedItems.java
@@ -7,8 +7,6 @@
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexType;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
 import java.util.Collection;
 import java.util.function.Function;
 
@@ -28,23 +26,19 @@
   // Immutable package accessible fields to allow SyntheticItems creation.
   final DexApplication application;
   final int nextSyntheticId;
-  final ImmutableSet<DexType> legacySyntheticTypes;
-  final ImmutableMap<DexType, SyntheticReference> syntheticItems;
+  final CommittedSyntheticsCollection committed;
   final ImmutableList<DexType> committedTypes;
 
   CommittedItems(
       int nextSyntheticId,
       DexApplication application,
-      ImmutableSet<DexType> legacySyntheticTypes,
-      ImmutableMap<DexType, SyntheticReference> syntheticItems,
+      CommittedSyntheticsCollection committed,
       ImmutableList<DexType> committedTypes) {
-    assert verifyTypesAreInApp(application, legacySyntheticTypes);
-    assert verifyTypesAreInApp(application, syntheticItems.keySet());
     this.nextSyntheticId = nextSyntheticId;
     this.application = application;
-    this.legacySyntheticTypes = legacySyntheticTypes;
-    this.syntheticItems = syntheticItems;
+    this.committed = committed;
     this.committedTypes = committedTypes;
+    committed.verifyTypesAreInApp(application);
   }
 
   // Conversion to a mutable synthetic items collection. Should only be used in AppInfo creation.
@@ -62,7 +56,7 @@
 
   @Deprecated
   public Collection<DexType> getLegacySyntheticTypes() {
-    return legacySyntheticTypes;
+    return committed.getLegacyTypes();
   }
 
   @Override
@@ -70,11 +64,4 @@
     // All synthetic types are committed to the application so lookup is just the base lookup.
     return baseDefinitionFor.apply(type);
   }
-
-  private static boolean verifyTypesAreInApp(DexApplication app, Collection<DexType> types) {
-    for (DexType type : types) {
-      assert app.programDefinitionFor(type) != null : "Missing synthetic: " + type;
-    }
-    return true;
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/synthesis/CommittedSyntheticsCollection.java b/src/main/java/com/android/tools/r8/synthesis/CommittedSyntheticsCollection.java
new file mode 100644
index 0000000..582a39a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/synthesis/CommittedSyntheticsCollection.java
@@ -0,0 +1,243 @@
+// Copyright (c) 2020, 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 com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GraphLens.NonIdentityGraphLens;
+import com.android.tools.r8.graph.PrunedItems;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+
+/**
+ * Immutable collection of committed items.
+ *
+ * <p>This structure is to make it easier to pass the items from SyntheticItems to CommittedItems
+ * and back while also providing a builder for updating the committed synthetics.
+ */
+class CommittedSyntheticsCollection {
+
+  static class Builder {
+    private final CommittedSyntheticsCollection parent;
+    private ImmutableMap.Builder<DexType, SyntheticClassReference> newNonLegacyClasses = null;
+    private ImmutableMap.Builder<DexType, SyntheticMethodReference> newNonLegacyMethods = null;
+    private ImmutableSet.Builder<DexType> newLegacyClasses = null;
+
+    public Builder(CommittedSyntheticsCollection parent) {
+      this.parent = parent;
+    }
+
+    public Builder addItem(SyntheticDefinition<?, ?> definition) {
+      definition.toReference().apply(this::addNonLegacyMethod, this::addNonLegacyClass);
+      return this;
+    }
+
+    public Builder addNonLegacyClass(SyntheticClassDefinition definition) {
+      return addNonLegacyClass(definition.toReference());
+    }
+
+    public Builder addNonLegacyClass(SyntheticClassReference reference) {
+      if (newNonLegacyClasses == null) {
+        newNonLegacyClasses = ImmutableMap.builder();
+      }
+      newNonLegacyClasses.put(reference.getHolder(), reference);
+      return this;
+    }
+
+    public Builder addNonLegacyMethod(SyntheticMethodDefinition definition) {
+      return addNonLegacyMethod(definition.toReference());
+    }
+
+    public Builder addNonLegacyMethod(SyntheticMethodReference reference) {
+      if (newNonLegacyMethods == null) {
+        newNonLegacyMethods = ImmutableMap.builder();
+      }
+      newNonLegacyMethods.put(reference.getHolder(), reference);
+      return this;
+    }
+
+    public Builder addLegacyClasses(Collection<DexProgramClass> classes) {
+      if (newLegacyClasses == null) {
+        newLegacyClasses = ImmutableSet.builder();
+      }
+      classes.forEach(c -> newLegacyClasses.add(c.getType()));
+      return this;
+    }
+
+    public Builder addLegacyClass(DexType type) {
+      if (newLegacyClasses == null) {
+        newLegacyClasses = ImmutableSet.builder();
+      }
+      newLegacyClasses.add(type);
+      return this;
+    }
+
+    public CommittedSyntheticsCollection build() {
+      if (newNonLegacyClasses == null && newNonLegacyMethods == null && newLegacyClasses == null) {
+        return parent;
+      }
+      ImmutableMap<DexType, SyntheticClassReference> allNonLegacyClasses =
+          newNonLegacyClasses == null
+              ? parent.nonLegacyClasses
+              : newNonLegacyClasses.putAll(parent.nonLegacyClasses).build();
+      ImmutableMap<DexType, SyntheticMethodReference> allNonLegacyMethods =
+          newNonLegacyMethods == null
+              ? parent.nonLegacyMethods
+              : newNonLegacyMethods.putAll(parent.nonLegacyMethods).build();
+      ImmutableSet<DexType> allLegacyClasses =
+          newLegacyClasses == null
+              ? parent.legacyTypes
+              : newLegacyClasses.addAll(parent.legacyTypes).build();
+      return new CommittedSyntheticsCollection(
+          allLegacyClasses, allNonLegacyMethods, allNonLegacyClasses);
+    }
+  }
+
+  private static final CommittedSyntheticsCollection EMPTY =
+      new CommittedSyntheticsCollection(ImmutableSet.of(), ImmutableMap.of(), ImmutableMap.of());
+
+  /**
+   * Immutable set of synthetic types in the application (eg, committed).
+   *
+   * <p>TODO(b/158159959): Remove legacy support.
+   */
+  private final ImmutableSet<DexType> legacyTypes;
+
+  /** Mapping from synthetic type to its synthetic method item description. */
+  private final ImmutableMap<DexType, SyntheticMethodReference> nonLegacyMethods;
+
+  /** Mapping from synthetic type to its synthetic class item description. */
+  private final ImmutableMap<DexType, SyntheticClassReference> nonLegacyClasses;
+
+  public CommittedSyntheticsCollection(
+      ImmutableSet<DexType> legacyTypes,
+      ImmutableMap<DexType, SyntheticMethodReference> nonLegacyMethods,
+      ImmutableMap<DexType, SyntheticClassReference> nonLegacyClasses) {
+    this.legacyTypes = legacyTypes;
+    this.nonLegacyMethods = nonLegacyMethods;
+    this.nonLegacyClasses = nonLegacyClasses;
+    assert legacyTypes.size() + nonLegacyMethods.size() + nonLegacyClasses.size()
+        == Sets.union(Sets.union(nonLegacyMethods.keySet(), nonLegacyClasses.keySet()), legacyTypes)
+            .size();
+  }
+
+  public static CommittedSyntheticsCollection empty() {
+    return EMPTY;
+  }
+
+  Builder builder() {
+    return new Builder(this);
+  }
+
+  boolean isEmpty() {
+    return legacyTypes.isEmpty() && nonLegacyMethods.isEmpty() && nonLegacyClasses.isEmpty();
+  }
+
+  boolean containsType(DexType type) {
+    return containsLegacyType(type) || containsNonLegacyType(type);
+  }
+
+  public boolean containsLegacyType(DexType type) {
+    return legacyTypes.contains(type);
+  }
+
+  public boolean containsNonLegacyType(DexType type) {
+    return nonLegacyMethods.containsKey(type) || nonLegacyClasses.containsKey(type);
+  }
+
+  public ImmutableSet<DexType> getLegacyTypes() {
+    return legacyTypes;
+  }
+
+  public ImmutableMap<DexType, SyntheticMethodReference> getNonLegacyMethods() {
+    return nonLegacyMethods;
+  }
+
+  public ImmutableMap<DexType, SyntheticClassReference> getNonLegacyClasses() {
+    return nonLegacyClasses;
+  }
+
+  public SyntheticReference<?, ?> getNonLegacyItem(DexType type) {
+    SyntheticMethodReference reference = nonLegacyMethods.get(type);
+    if (reference != null) {
+      return reference;
+    }
+    return nonLegacyClasses.get(type);
+  }
+
+  public void forEachNonLegacyItem(Consumer<SyntheticReference<?, ?>> fn) {
+    nonLegacyMethods.forEach((t, r) -> fn.accept(r));
+    nonLegacyClasses.forEach((t, r) -> fn.accept(r));
+  }
+
+  CommittedSyntheticsCollection pruneItems(PrunedItems prunedItems) {
+    Set<DexType> removed = prunedItems.getNoLongerSyntheticItems();
+    if (removed.isEmpty()) {
+      return this;
+    }
+    Builder builder = CommittedSyntheticsCollection.empty().builder();
+    boolean changed = false;
+    for (DexType type : legacyTypes) {
+      if (removed.contains(type)) {
+        changed = true;
+      } else {
+        builder.addLegacyClass(type);
+      }
+    }
+    for (SyntheticMethodReference reference : nonLegacyMethods.values()) {
+      if (removed.contains(reference.getHolder())) {
+        changed = true;
+      } else {
+        builder.addNonLegacyMethod(reference);
+      }
+    }
+    for (SyntheticClassReference reference : nonLegacyClasses.values()) {
+      if (removed.contains(reference.getHolder())) {
+        changed = true;
+      } else {
+        builder.addNonLegacyClass(reference);
+      }
+    }
+    return changed ? builder.build() : this;
+  }
+
+  CommittedSyntheticsCollection rewriteWithLens(NonIdentityGraphLens lens) {
+    return new CommittedSyntheticsCollection(
+        lens.rewriteTypes(legacyTypes),
+        rewriteItems(nonLegacyMethods, lens),
+        rewriteItems(nonLegacyClasses, lens));
+  }
+
+  private static <R extends SyntheticReference<R, ?>> ImmutableMap<DexType, R> rewriteItems(
+      Map<DexType, R> items, NonIdentityGraphLens lens) {
+    ImmutableMap.Builder<DexType, R> rewrittenItems = ImmutableMap.builder();
+    for (R reference : items.values()) {
+      R rewritten = reference.rewrite(lens);
+      if (rewritten != null) {
+        rewrittenItems.put(rewritten.getHolder(), rewritten);
+      }
+    }
+    return rewrittenItems.build();
+  }
+
+  boolean verifyTypesAreInApp(DexApplication application) {
+    assert verifyTypesAreInApp(application, legacyTypes);
+    assert verifyTypesAreInApp(application, nonLegacyMethods.keySet());
+    assert verifyTypesAreInApp(application, nonLegacyClasses.keySet());
+    return true;
+  }
+
+  private static boolean verifyTypesAreInApp(DexApplication app, Collection<DexType> types) {
+    for (DexType type : types) {
+      assert app.programDefinitionFor(type) != null : "Missing synthetic: " + type;
+    }
+    return true;
+  }
+}
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 f5d4b66..2e371d0 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SynthesizingContext.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SynthesizingContext.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.graph.ProgramDefinition;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.MainDexClasses;
+import com.android.tools.r8.synthesis.SyntheticNaming.Phase;
 import java.util.Comparator;
 import java.util.Set;
 
@@ -53,6 +54,20 @@
     return new SynthesizingContext(synthesizingContextType, clazz.type, clazz.origin);
   }
 
+  static SynthesizingContext fromSyntheticContextChange(
+      DexType syntheticType, SynthesizingContext oldContext, DexItemFactory factory) {
+    String descriptor = syntheticType.toDescriptorString();
+    int i = descriptor.indexOf(SyntheticNaming.getPhaseSeparator(Phase.INTERNAL));
+    if (i <= 0) {
+      assert false : "Unexpected synthetic without internal separator: " + syntheticType;
+      return null;
+    }
+    DexType newContext = factory.createType(descriptor.substring(0, i) + ";");
+    return newContext == oldContext.getSynthesizingContextType()
+        ? oldContext
+        : new SynthesizingContext(newContext, newContext, oldContext.inputContextOrigin);
+  }
+
   private SynthesizingContext(
       DexType synthesizingContextType, DexType inputContextType, Origin inputContextOrigin) {
     this.synthesizingContextType = synthesizingContextType;
@@ -75,14 +90,6 @@
     return inputContextOrigin;
   }
 
-  DexType createHygienicType(String syntheticId, DexItemFactory factory) {
-    // If the context is a synthetic input, then use its annotated context as the hygienic context.
-    String contextDesc = synthesizingContextType.toDescriptorString();
-    String prefix = contextDesc.substring(0, contextDesc.length() - 1);
-    String suffix = SyntheticItems.INTERNAL_SYNTHETIC_CLASS_SEPARATOR + syntheticId + ";";
-    return factory.createType(prefix + suffix);
-  }
-
   SynthesizingContext rewrite(NonIdentityGraphLens lens) {
     DexType rewrittenInputeContextType = lens.lookupType(inputContextType);
     DexType rewrittenSynthesizingContextType = lens.lookupType(synthesizingContextType);
@@ -127,15 +134,17 @@
   void addIfDerivedFromMainDexClass(
       DexProgramClass externalSyntheticClass,
       MainDexClasses mainDexClasses,
-      Set<DexType> allMainDexTypes,
-      Set<DexType> derivedMainDexTypesToIgnore) {
+      Set<DexType> allMainDexTypes) {
     // The input context type (not the annotated context) determines if the derived class is to be
     // in main dex.
     // TODO(b/168584485): Once resolved allMainDexTypes == mainDexClasses.
     if (allMainDexTypes.contains(inputContextType)) {
       mainDexClasses.add(externalSyntheticClass);
-      // Mark the type as to be ignored when computing main-dex placement for legacy types.
-      derivedMainDexTypesToIgnore.add(inputContextType);
     }
   }
+
+  @Override
+  public String toString() {
+    return "SynthesizingContext{" + getSynthesizingContextType() + "}";
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticClassBuilder.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticClassBuilder.java
index 4f289c7..9d2460b 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticClassBuilder.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticClassBuilder.java
@@ -33,8 +33,10 @@
 
   private DexType superType;
   private DexTypeList interfaces = DexTypeList.empty();
-
-  private int nextMethodId = 0;
+  private List<DexEncodedField> staticFields = new ArrayList<>();
+  private List<DexEncodedField> instanceFields = new ArrayList<>();
+  private List<DexEncodedMethod> directMethods = new ArrayList<>();
+  private List<DexEncodedMethod> virtualMethods = new ArrayList<>();
   private List<SyntheticMethodBuilder> methods = new ArrayList<>();
 
   SyntheticClassBuilder(DexType type, SynthesizingContext context, DexItemFactory factory) {
@@ -52,12 +54,40 @@
     return type;
   }
 
-  private String getNextMethodName() {
-    return SyntheticItems.INTERNAL_SYNTHETIC_METHOD_PREFIX + nextMethodId++;
+  public SyntheticClassBuilder setInterfaces(List<DexType> interfaces) {
+    this.interfaces =
+        interfaces.isEmpty()
+            ? DexTypeList.empty()
+            : new DexTypeList(interfaces.toArray(DexType.EMPTY_ARRAY));
+    return this;
+  }
+
+  public SyntheticClassBuilder setStaticFields(List<DexEncodedField> fields) {
+    staticFields.clear();
+    staticFields.addAll(fields);
+    return this;
+  }
+
+  public SyntheticClassBuilder setInstanceFields(List<DexEncodedField> fields) {
+    instanceFields.clear();
+    instanceFields.addAll(fields);
+    return this;
+  }
+
+  public SyntheticClassBuilder setDirectMethods(Iterable<DexEncodedMethod> methods) {
+    directMethods.clear();
+    methods.forEach(directMethods::add);
+    return this;
+  }
+
+  public SyntheticClassBuilder setVirtualMethods(Iterable<DexEncodedMethod> methods) {
+    virtualMethods.clear();
+    methods.forEach(virtualMethods::add);
+    return this;
   }
 
   public SyntheticClassBuilder addMethod(Consumer<SyntheticMethodBuilder> fn) {
-    SyntheticMethodBuilder method = new SyntheticMethodBuilder(this, getNextMethodName());
+    SyntheticMethodBuilder method = new SyntheticMethodBuilder(this);
     fn.accept(method);
     methods.add(method);
     return this;
@@ -65,35 +95,27 @@
 
   DexProgramClass build() {
     ClassAccessFlags accessFlags =
-        ClassAccessFlags.fromSharedAccessFlags(Constants.ACC_PUBLIC | Constants.ACC_SYNTHETIC);
+        ClassAccessFlags.fromSharedAccessFlags(
+            Constants.ACC_FINAL | Constants.ACC_PUBLIC | Constants.ACC_SYNTHETIC);
     Kind originKind = null;
     DexString sourceFile = null;
     NestHostClassAttribute nestHost = null;
     List<NestMemberClassAttribute> nestMembers = Collections.emptyList();
     EnclosingMethodAttribute enclosingMembers = null;
     List<InnerClassAttribute> innerClasses = Collections.emptyList();
-    DexEncodedField[] staticFields = DexEncodedField.EMPTY_ARRAY;
-    DexEncodedField[] instanceFields = DexEncodedField.EMPTY_ARRAY;
-    DexEncodedMethod[] directMethods = DexEncodedMethod.EMPTY_ARRAY;
-    DexEncodedMethod[] virtualMethods = DexEncodedMethod.EMPTY_ARRAY;
-    assert !methods.isEmpty();
-    List<DexEncodedMethod> directs = new ArrayList<>(methods.size());
-    List<DexEncodedMethod> virtuals = new ArrayList<>(methods.size());
     for (SyntheticMethodBuilder builder : methods) {
       DexEncodedMethod method = builder.build();
       if (method.isNonPrivateVirtualMethod()) {
-        virtuals.add(method);
+        virtualMethods.add(method);
       } else {
-        directs.add(method);
+        directMethods.add(method);
       }
     }
-    if (!directs.isEmpty()) {
-      directMethods = directs.toArray(new DexEncodedMethod[directs.size()]);
-    }
-    if (!virtuals.isEmpty()) {
-      virtualMethods = virtuals.toArray(new DexEncodedMethod[virtuals.size()]);
-    }
-    long checksum = 7 * (long) directs.hashCode() + 11 * (long) virtuals.hashCode();
+    long checksum =
+        7 * (long) directMethods.hashCode()
+            + 11 * (long) virtualMethods.hashCode()
+            + 13 * (long) staticFields.hashCode()
+            + 17 * (long) instanceFields.hashCode();
     return new DexProgramClass(
         type,
         originKind,
@@ -108,10 +130,10 @@
         innerClasses,
         ClassSignature.noSignature(),
         DexAnnotationSet.empty(),
-        staticFields,
-        instanceFields,
-        directMethods,
-        virtualMethods,
+        staticFields.toArray(new DexEncodedField[staticFields.size()]),
+        instanceFields.toArray(new DexEncodedField[instanceFields.size()]),
+        directMethods.toArray(new DexEncodedMethod[directMethods.size()]),
+        virtualMethods.toArray(new DexEncodedMethod[virtualMethods.size()]),
         factory.getSkipNameValidationForTesting(),
         c -> checksum);
   }
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticClassDefinition.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticClassDefinition.java
new file mode 100644
index 0000000..4b0b2cf
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticClassDefinition.java
@@ -0,0 +1,65 @@
+// Copyright (c) 2020, 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 com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
+import com.android.tools.r8.utils.structural.RepresentativeMap;
+import com.google.common.hash.Hasher;
+
+/**
+ * Definition of a synthetic class item.
+ *
+ * <p>This class is internal to the synthetic items collection, thus package-protected.
+ */
+class SyntheticClassDefinition
+    extends SyntheticDefinition<SyntheticClassReference, SyntheticClassDefinition> {
+
+  private final DexProgramClass clazz;
+
+  SyntheticClassDefinition(SyntheticKind kind, SynthesizingContext context, DexProgramClass clazz) {
+    super(kind, context);
+    this.clazz = clazz;
+  }
+
+  public DexProgramClass getProgramClass() {
+    return clazz;
+  }
+
+  @Override
+  SyntheticClassReference toReference() {
+    return new SyntheticClassReference(getKind(), getContext(), clazz.getType());
+  }
+
+  @Override
+  DexProgramClass getHolder() {
+    return clazz;
+  }
+
+  @Override
+  public boolean isValid() {
+    return clazz.isPublic() && clazz.isFinal() && clazz.accessFlags.isSynthetic();
+  }
+
+  @Override
+  void internalComputeHash(Hasher hasher, RepresentativeMap map) {
+    clazz.hashWithTypeEquivalence(hasher, map);
+  }
+
+  @Override
+  int internalCompareTo(SyntheticClassDefinition o, RepresentativeMap map) {
+    return clazz.compareWithTypeEquivalenceTo(o.clazz, map);
+  }
+
+  @Override
+  public String toString() {
+    return "SyntheticClass{ clazz = "
+        + clazz.type.toSourceString()
+        + ", kind = "
+        + getKind()
+        + ", context = "
+        + getContext()
+        + " }";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticClassReference.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticClassReference.java
new file mode 100644
index 0000000..9c79ab9
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticClassReference.java
@@ -0,0 +1,73 @@
+// Copyright (c) 2020, 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 com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GraphLens.NonIdentityGraphLens;
+import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * Reference to a synthetic class item.
+ *
+ * <p>This class is internal to the synthetic items collection, thus package-protected.
+ */
+class SyntheticClassReference
+    extends SyntheticReference<SyntheticClassReference, SyntheticClassDefinition> {
+  final DexType type;
+
+  SyntheticClassReference(SyntheticKind kind, SynthesizingContext context, DexType type) {
+    super(kind, context);
+    this.type = type;
+  }
+
+  @Override
+  DexType getHolder() {
+    return type;
+  }
+
+  @Override
+  SyntheticClassDefinition lookupDefinition(Function<DexType, DexClass> definitions) {
+    DexClass clazz = definitions.apply(type);
+    if (clazz == null) {
+      return null;
+    }
+    assert clazz.isProgramClass();
+    return new SyntheticClassDefinition(getKind(), getContext(), clazz.asProgramClass());
+  }
+
+  @Override
+  SyntheticClassReference rewrite(NonIdentityGraphLens lens) {
+    DexType rewritten = lens.lookupType(type);
+    // If the reference has been non-trivially rewritten the compiler has changed it and it can no
+    // longer be considered a synthetic. The context may or may not have changed.
+    if (type != rewritten && !lens.isSimpleRenaming(type, rewritten)) {
+      // If the referenced item is rewritten, it should be moved to another holder as the
+      // synthetic holder is no longer part of the synthetic collection.
+      assert SyntheticNaming.verifyNotInternalSynthetic(rewritten);
+      return null;
+    }
+    SynthesizingContext context = getContext().rewrite(lens);
+    if (context == getContext() && rewritten == type) {
+      return this;
+    }
+    // Ensure that if a synthetic moves its context moves consistently.
+    if (type != rewritten) {
+      context =
+          SynthesizingContext.fromSyntheticContextChange(rewritten, context, lens.dexItemFactory());
+      if (context == null) {
+        return null;
+      }
+    }
+    return new SyntheticClassReference(getKind(), context, rewritten);
+  }
+
+  @Override
+  void apply(
+      Consumer<SyntheticMethodReference> onMethod, Consumer<SyntheticClassReference> onClass) {
+    onClass.accept(this);
+  }
+}
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 01f8825..526f4fc 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java
@@ -4,30 +4,71 @@
 package com.android.tools.r8.synthesis;
 
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
 import com.android.tools.r8.utils.structural.RepresentativeMap;
 import com.google.common.hash.HashCode;
+import com.google.common.hash.Hasher;
+import com.google.common.hash.Hashing;
 
 /**
  * Base type for the definition of a synthetic item.
  *
  * <p>This class is internal to the synthetic items collection, thus package-protected.
  */
-abstract class SyntheticDefinition {
+abstract class SyntheticDefinition<
+    R extends SyntheticReference<R, D>, D extends SyntheticDefinition<R, D>> {
+
+  private final SyntheticKind kind;
   private final SynthesizingContext context;
 
-  SyntheticDefinition(SynthesizingContext context) {
+  SyntheticDefinition(SyntheticKind kind, SynthesizingContext context) {
+    assert kind != null;
+    assert context != null;
+    this.kind = kind;
     this.context = context;
   }
 
-  abstract SyntheticReference toReference();
+  abstract R toReference();
 
-  SynthesizingContext getContext() {
+  final SyntheticKind getKind() {
+    return kind;
+  }
+
+  final SynthesizingContext getContext() {
     return context;
   }
 
   abstract DexProgramClass getHolder();
 
-  abstract HashCode computeHash(RepresentativeMap map, boolean intermediate);
+  final HashCode computeHash(RepresentativeMap map, boolean intermediate) {
+    Hasher hasher = Hashing.murmur3_128().newHasher();
+    if (intermediate) {
+      // If in intermediate mode, include the context type as sharing is restricted to within a
+      // single context.
+      getContext().getSynthesizingContextType().hashWithTypeEquivalence(hasher, map);
+    }
+    internalComputeHash(hasher, map);
+    return hasher.hash();
+  }
 
-  abstract boolean isEquivalentTo(SyntheticDefinition other, boolean intermediate);
+  abstract void internalComputeHash(Hasher hasher, RepresentativeMap map);
+
+  final boolean isEquivalentTo(D other, boolean includeContext) {
+    return compareTo(other, includeContext) == 0;
+  }
+
+  int compareTo(D other, boolean includeContext) {
+    if (includeContext) {
+      int order = getContext().compareTo(other.getContext());
+      if (order != 0) {
+        return order;
+      }
+    }
+    RepresentativeMap map = t -> t == other.getHolder().getType() ? getHolder().getType() : t;
+    return internalCompareTo(other, map);
+  }
+
+  abstract int internalCompareTo(D other, RepresentativeMap map);
+
+  public abstract boolean isValid();
 }
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 c18b29d..67391a2 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
@@ -6,22 +6,31 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexAnnotation;
 import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GraphLens;
-import com.android.tools.r8.graph.GraphLens.Builder;
 import com.android.tools.r8.graph.GraphLens.NestedGraphLens;
+import com.android.tools.r8.graph.GraphLens.NonIdentityGraphLens;
 import com.android.tools.r8.graph.PrunedItems;
+import com.android.tools.r8.graph.TreeFixerBase;
+import com.android.tools.r8.ir.code.NumberGenerator;
 import com.android.tools.r8.shaking.MainDexClasses;
-import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.collections.BidirectionalManyToManyRepresentativeMap;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeHashMap;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
 import com.android.tools.r8.utils.structural.RepresentativeMap;
 import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Sets;
 import com.google.common.hash.HashCode;
@@ -32,37 +41,146 @@
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Set;
 import java.util.TreeSet;
-import java.util.function.Predicate;
+import java.util.function.Function;
 
 public class SyntheticFinalization {
 
   public static class Result {
     public final CommittedItems commit;
+    public final NonIdentityGraphLens lens;
     public final PrunedItems prunedItems;
 
-    public Result(CommittedItems commit, PrunedItems prunedItems) {
+    public Result(
+        CommittedItems commit, SyntheticFinalizationGraphLens lens, PrunedItems prunedItems) {
       this.commit = commit;
+      this.lens = lens;
       this.prunedItems = prunedItems;
     }
   }
 
-  private static class EquivalenceGroup<T extends SyntheticDefinition & Comparable<T>>
-      implements Comparable<EquivalenceGroup<T>> {
-    private List<T> members;
+  public static class SyntheticFinalizationGraphLens extends NestedGraphLens {
 
-    EquivalenceGroup(T singleton) {
-      this(singleton, Collections.singletonList(singleton));
+    private final Map<DexType, DexType> syntheticTypeMap;
+    private final Map<DexMethod, DexMethod> syntheticMethodsMap;
+
+    private SyntheticFinalizationGraphLens(
+        GraphLens previous,
+        Map<DexType, DexType> syntheticClassesMap,
+        Map<DexMethod, DexMethod> syntheticMethodsMap,
+        Map<DexType, DexType> typeMap,
+        BidirectionalManyToOneRepresentativeMap<DexField, DexField> fieldMap,
+        Map<DexMethod, DexMethod> methodMap,
+        BidirectionalManyToManyRepresentativeMap<DexMethod, DexMethod> originalMethodSignatures,
+        DexItemFactory factory) {
+      super(typeMap, methodMap, fieldMap, originalMethodSignatures, previous, factory);
+      this.syntheticTypeMap = syntheticClassesMap;
+      this.syntheticMethodsMap = syntheticMethodsMap;
     }
 
-    EquivalenceGroup(T representative, List<T> members) {
+    // The mapping is many to one, so the inverse is only defined up to equivalence groups.
+    // Override the access to renamed signatures to first check for synthetic mappings before
+    // using the original item mappings of the
+
+    @Override
+    public DexField getRenamedFieldSignature(DexField originalField) {
+      if (syntheticTypeMap.containsKey(originalField.holder)) {
+        DexField renamed = fieldMap.get(originalField);
+        if (renamed != null) {
+          return renamed;
+        }
+      }
+      return super.getRenamedFieldSignature(originalField);
+    }
+
+    @Override
+    public DexMethod getRenamedMethodSignature(DexMethod originalMethod, GraphLens applied) {
+      if (syntheticTypeMap.containsKey(originalMethod.holder)) {
+        DexMethod renamed = methodMap.get(originalMethod);
+        if (renamed != null) {
+          return renamed;
+        }
+      }
+      DexMethod renamed = syntheticMethodsMap.get(originalMethod);
+      return renamed != null ? renamed : super.getRenamedMethodSignature(originalMethod, applied);
+    }
+  }
+
+  private static class Builder {
+
+    // Forward mapping of internal to external synthetics.
+    Map<DexType, DexType> syntheticClassesMap = new IdentityHashMap<>();
+    Map<DexMethod, DexMethod> syntheticMethodsMap = new IdentityHashMap<>();
+
+    Map<DexType, DexType> typeMap = new IdentityHashMap<>();
+    BidirectionalManyToOneRepresentativeHashMap<DexField, DexField> fieldMap =
+        new BidirectionalManyToOneRepresentativeHashMap<>();
+    Map<DexMethod, DexMethod> methodMap = new IdentityHashMap<>();
+
+    protected final BidirectionalOneToOneHashMap<DexMethod, DexMethod> originalMethodSignatures =
+        new BidirectionalOneToOneHashMap<>();
+
+    void moveSyntheticClass(DexType from, DexType to) {
+      assert !syntheticClassesMap.containsKey(from);
+      syntheticClassesMap.put(from, to);
+      typeMap.put(from, to);
+    }
+
+    void moveSyntheticMethod(DexMethod from, DexMethod to) {
+      assert !syntheticMethodsMap.containsKey(from);
+      syntheticMethodsMap.put(from, to);
+      methodMap.put(from, to);
+    }
+
+    void move(DexType from, DexType to) {
+      typeMap.put(from, to);
+    }
+
+    void move(DexField from, DexField to) {
+      fieldMap.put(from, to);
+    }
+
+    void move(DexMethod from, DexMethod to) {
+      methodMap.put(from, to);
+      originalMethodSignatures.put(to, from);
+    }
+
+    SyntheticFinalizationGraphLens build(GraphLens previous, DexItemFactory factory) {
+      assert verifySubMap(syntheticClassesMap, typeMap);
+      if (typeMap.isEmpty() && fieldMap.isEmpty() && methodMap.isEmpty()) {
+        return null;
+      }
+      return new SyntheticFinalizationGraphLens(
+          previous,
+          syntheticClassesMap,
+          syntheticMethodsMap,
+          typeMap,
+          fieldMap,
+          methodMap,
+          originalMethodSignatures,
+          factory);
+    }
+
+    private static <K, V> boolean verifySubMap(Map<K, V> sub, Map<K, V> sup) {
+      for (Entry<K, V> entry : sub.entrySet()) {
+        assert sup.get(entry.getKey()) == entry.getValue();
+      }
+      return true;
+    }
+  }
+
+  public static class EquivalenceGroup<T extends SyntheticDefinition<?, T>> {
+    private final List<T> members;
+
+    public EquivalenceGroup(T representative, List<T> members) {
       assert !members.isEmpty();
       assert members.get(0) == representative;
       this.members = members;
     }
 
-    T getRepresentative() {
+    public T getRepresentative() {
       return members.get(0);
     }
 
@@ -70,89 +188,103 @@
       return members;
     }
 
+    public int compareToIncludingContext(EquivalenceGroup<T> other) {
+      return getRepresentative().compareTo(other.getRepresentative(), true);
+    }
+
+    public int compareTo(EquivalenceGroup<T> other, boolean includeContext) {
+      return getRepresentative().compareTo(other.getRepresentative(), includeContext);
+    }
+
     @Override
-    public int compareTo(EquivalenceGroup<T> other) {
-      return getRepresentative().compareTo(other.getRepresentative());
+    public String toString() {
+      return "EquivalenceGroup{ members = "
+          + members.size()
+          + ", repr = "
+          + getRepresentative()
+          + " }";
     }
   }
 
   private final InternalOptions options;
-  private final ImmutableSet<DexType> legacySyntheticTypes;
-  private final ImmutableMap<DexType, SyntheticReference> syntheticItems;
+  private final CommittedSyntheticsCollection synthetics;
 
-  SyntheticFinalization(
-      InternalOptions options,
-      ImmutableSet<DexType> legacySyntheticTypes,
-      ImmutableMap<DexType, SyntheticReference> syntheticItems) {
+  SyntheticFinalization(InternalOptions options, CommittedSyntheticsCollection synthetics) {
     this.options = options;
-    this.legacySyntheticTypes = legacySyntheticTypes;
-    this.syntheticItems = syntheticItems;
+    this.synthetics = synthetics;
   }
 
   public Result computeFinalSynthetics(AppView<?> appView) {
     assert verifyNoNestedSynthetics();
-    DexApplication application = appView.appInfo().app();
+    DexApplication application;
     MainDexClasses mainDexClasses = appView.appInfo().getMainDexClasses();
-    GraphLens graphLens = appView.graphLens();
-
-    Map<DexType, SyntheticMethodDefinition> methodDefinitions =
-        lookupSyntheticMethodDefinitions(application);
-
-    Collection<List<SyntheticMethodDefinition>> potentialEquivalences =
-        computePotentialEquivalences(methodDefinitions, options.intermediate);
-
-    Map<DexType, EquivalenceGroup<SyntheticMethodDefinition>> equivalences =
-        computeActualEquivalences(potentialEquivalences, options.intermediate, options.itemFactory);
-
-    Builder lensBuilder = NestedGraphLens.builder();
-    List<DexProgramClass> newProgramClasses = new ArrayList<>();
     List<DexProgramClass> finalSyntheticClasses = new ArrayList<>();
-    Set<DexType> derivedMainDexTypesToIgnore = Sets.newIdentityHashSet();
-    buildLensAndProgram(
-        appView,
-        equivalences,
-        syntheticItems::containsKey,
-        mainDexClasses,
-        lensBuilder,
-        newProgramClasses,
-        finalSyntheticClasses,
-        derivedMainDexTypesToIgnore);
-
-    newProgramClasses.addAll(finalSyntheticClasses);
+    Builder lensBuilder = new Builder();
+    {
+      Map<DexType, NumberGenerator> generators = new IdentityHashMap<>();
+      application =
+          buildLensAndProgram(
+              appView,
+              computeEquivalences(appView, synthetics.getNonLegacyMethods().values(), generators),
+              computeEquivalences(appView, synthetics.getNonLegacyClasses().values(), generators),
+              mainDexClasses,
+              lensBuilder,
+              finalSyntheticClasses);
+    }
 
     handleSynthesizedClassMapping(
-        finalSyntheticClasses, application, options, mainDexClasses, derivedMainDexTypesToIgnore);
+        finalSyntheticClasses, application, options, mainDexClasses, lensBuilder.typeMap);
 
-    DexApplication app = application.builder().replaceProgramClasses(newProgramClasses).build();
-
-    appView.setGraphLens(lensBuilder.build(options.itemFactory, graphLens));
     assert appView.appInfo().getMainDexClasses() == mainDexClasses;
 
     Set<DexType> finalSyntheticTypes = Sets.newIdentityHashSet();
     finalSyntheticClasses.forEach(clazz -> finalSyntheticTypes.add(clazz.getType()));
 
     Set<DexType> prunedSynthetics = Sets.newIdentityHashSet();
-    for (DexType type : syntheticItems.keySet()) {
-      if (!finalSyntheticTypes.contains(type)) {
-        prunedSynthetics.add(type);
-      }
-    }
+    synthetics.forEachNonLegacyItem(
+        reference -> {
+          DexType type = reference.getHolder();
+          if (!finalSyntheticTypes.contains(type)) {
+            prunedSynthetics.add(type);
+          }
+        });
 
     return new Result(
         new CommittedItems(
             SyntheticItems.INVALID_ID_AFTER_SYNTHETIC_FINALIZATION,
-            app,
-            legacySyntheticTypes,
-            ImmutableMap.of(),
+            application,
+            new CommittedSyntheticsCollection(
+                synthetics.getLegacyTypes(), ImmutableMap.of(), ImmutableMap.of()),
             ImmutableList.of()),
-        PrunedItems.builder().setPrunedApp(app).addRemovedClasses(prunedSynthetics).build());
+        lensBuilder.build(appView.graphLens(), appView.dexItemFactory()),
+        PrunedItems.builder()
+            .setPrunedApp(application)
+            .addRemovedClasses(prunedSynthetics)
+            .build());
+  }
+
+  private <R extends SyntheticReference<R, D>, D extends SyntheticDefinition<R, D>>
+      Map<DexType, EquivalenceGroup<D>> computeEquivalences(
+          AppView<?> appView,
+          ImmutableCollection<R> references,
+          Map<DexType, NumberGenerator> generators) {
+    boolean intermediate = appView.options().intermediate;
+    Map<DexType, D> definitions = lookupDefinitions(appView, references);
+    Collection<List<D>> potentialEquivalences =
+        computePotentialEquivalences(definitions, intermediate, appView.dexItemFactory());
+    return computeActualEquivalences(potentialEquivalences, generators, appView, intermediate);
+  }
+
+  private boolean isNotSyntheticType(DexType type) {
+    return !synthetics.containsNonLegacyType(type);
   }
 
   private boolean verifyNoNestedSynthetics() {
     // Check that a context is never itself synthetic class.
-    for (SyntheticReference item : syntheticItems.values()) {
-      assert !syntheticItems.containsKey(item.getContext().getSynthesizingContextType());
-    }
+    synthetics.forEachNonLegacyItem(
+        item -> {
+          assert isNotSyntheticType(item.getContext().getSynthesizingContextType());
+        });
     return true;
   }
 
@@ -161,7 +293,7 @@
       DexApplication application,
       InternalOptions options,
       MainDexClasses mainDexClasses,
-      Set<DexType> derivedMainDexTypesToIgnore) {
+      Map<DexType, DexType> derivedMainDexTypesToIgnore) {
     boolean includeSynthesizedClassMappingInOutput = shouldAnnotateSynthetics(options);
     if (includeSynthesizedClassMappingInOutput) {
       updateSynthesizedClassMapping(application, finalSyntheticClasses);
@@ -177,7 +309,7 @@
       DexApplication application, List<DexProgramClass> finalSyntheticClasses) {
     ListMultimap<DexProgramClass, DexProgramClass> originalToSynthesized =
         ArrayListMultimap.create();
-    for (DexType type : legacySyntheticTypes) {
+    for (DexType type : synthetics.getLegacyTypes()) {
       DexProgramClass clazz = DexProgramClass.asProgramClassOrNull(application.definitionFor(type));
       if (clazz != null) {
         for (DexProgramClass origin : clazz.getSynthesizedFrom()) {
@@ -214,7 +346,7 @@
   private void updateMainDexListWithSynthesizedClassMap(
       DexApplication application,
       MainDexClasses mainDexClasses,
-      Set<DexType> derivedMainDexTypesToIgnore) {
+      Map<DexType, DexType> derivedMainDexTypesToIgnore) {
     if (mainDexClasses.isEmpty()) {
       return;
     }
@@ -228,12 +360,11 @@
                 DexAnnotation.readAnnotationSynthesizedClassMap(
                     programClass, application.dexItemFactory);
             for (DexType type : derived) {
-              if (!derivedMainDexTypesToIgnore.contains(type)) {
-                DexProgramClass syntheticClass =
-                    DexProgramClass.asProgramClassOrNull(application.definitionFor(type));
-                if (syntheticClass != null) {
-                  newMainDexClasses.add(syntheticClass);
-                }
+              DexType mappedType = derivedMainDexTypesToIgnore.getOrDefault(type, type);
+              DexProgramClass syntheticClass =
+                  DexProgramClass.asProgramClassOrNull(application.definitionFor(mappedType));
+              if (syntheticClass != null) {
+                newMainDexClasses.add(syntheticClass);
               }
             }
           }
@@ -248,24 +379,16 @@
     }
   }
 
-  private static void buildLensAndProgram(
+  private static DexApplication buildLensAndProgram(
       AppView<?> appView,
       Map<DexType, EquivalenceGroup<SyntheticMethodDefinition>> syntheticMethodGroups,
-      Predicate<DexType> isSyntheticType,
+      Map<DexType, EquivalenceGroup<SyntheticClassDefinition>> syntheticClassGroups,
       MainDexClasses mainDexClasses,
       Builder lensBuilder,
-      List<DexProgramClass> normalClasses,
-      List<DexProgramClass> newSyntheticClasses,
-      Set<DexType> derivedMainDexTypesToIgnore) {
+      List<DexProgramClass> newSyntheticClasses) {
+    DexApplication application = appView.appInfo().app();
     DexItemFactory factory = appView.dexItemFactory();
 
-    for (DexProgramClass clazz : appView.appInfo().classes()) {
-      if (!isSyntheticType.test(clazz.type)) {
-        assert SyntheticItems.verifyNotInternalSynthetic(clazz.type);
-        normalClasses.add(clazz);
-      }
-    }
-
     // TODO(b/168584485): Remove this once class-mapping support is removed.
     Set<DexType> derivedMainDexTypes = Sets.newIdentityHashSet();
     mainDexClasses.forEach(
@@ -280,60 +403,178 @@
           }
         });
 
+    Set<DexType> pruned = Sets.newIdentityHashSet();
     syntheticMethodGroups.forEach(
         (syntheticType, syntheticGroup) -> {
           SyntheticMethodDefinition representative = syntheticGroup.getRepresentative();
           SynthesizingContext context = representative.getContext();
           context.registerPrefixRewriting(syntheticType, appView);
-          SyntheticClassBuilder builder =
-              new SyntheticClassBuilder(syntheticType, context, factory);
-          // TODO(b/158159959): Support grouping multiple methods per synthetic class.
-          builder.addMethod(
-              methodBuilder -> {
-                DexEncodedMethod definition = representative.getMethod().getDefinition();
-                methodBuilder
-                    .setAccessFlags(definition.accessFlags)
-                    .setProto(definition.getProto())
-                    .setClassFileVersion(
-                        definition.hasClassFileVersion() ? definition.getClassFileVersion() : null)
-                    .setCode(m -> definition.getCode());
-              });
-          DexProgramClass externalSyntheticClass = builder.build();
-          if (shouldAnnotateSynthetics(appView.options())) {
-            externalSyntheticClass.setAnnotations(
-                externalSyntheticClass
-                    .annotations()
-                    .getWithAddedOrReplaced(
-                        DexAnnotation.createAnnotationSynthesizedClass(
-                            context.getSynthesizingContextType(), factory)));
-          }
+          DexProgramClass externalSyntheticClass =
+              createExternalMethodClass(syntheticType, representative, factory);
+          newSyntheticClasses.add(externalSyntheticClass);
+          addSyntheticMarker(representative.getKind(), externalSyntheticClass, context, appView);
           assert externalSyntheticClass.getMethodCollection().size() == 1;
           DexEncodedMethod externalSyntheticMethod =
               externalSyntheticClass.methods().iterator().next();
-          newSyntheticClasses.add(externalSyntheticClass);
           for (SyntheticMethodDefinition member : syntheticGroup.getMembers()) {
-            if (member.getMethod().getReference() != externalSyntheticMethod.method) {
-              lensBuilder.map(member.getMethod().getReference(), externalSyntheticMethod.method);
-            }
-            member
-                .getContext()
-                .addIfDerivedFromMainDexClass(
-                    externalSyntheticClass,
-                    mainDexClasses,
-                    derivedMainDexTypes,
-                    derivedMainDexTypesToIgnore);
-            // TODO(b/168584485): Remove this once class-mapping support is removed.
-            DexProgramClass from =
-                DexProgramClass.asProgramClassOrNull(
-                    appView
-                        .appInfo()
-                        .definitionForWithoutExistenceAssert(
-                            member.getContext().getSynthesizingContextType()));
-            if (from != null) {
-              externalSyntheticClass.addSynthesizedFrom(from);
+            DexMethod memberReference = member.getMethod().getReference();
+            pruned.add(member.getHolder().getType());
+            if (memberReference != externalSyntheticMethod.method) {
+              lensBuilder.moveSyntheticMethod(memberReference, externalSyntheticMethod.method);
             }
           }
         });
+
+    List<DexProgramClass> deduplicatedClasses = new ArrayList<>();
+    syntheticClassGroups.forEach(
+        (syntheticType, syntheticGroup) -> {
+          SyntheticClassDefinition representative = syntheticGroup.getRepresentative();
+          SynthesizingContext context = representative.getContext();
+          context.registerPrefixRewriting(syntheticType, appView);
+          DexProgramClass externalSyntheticClass = representative.getProgramClass();
+          newSyntheticClasses.add(externalSyntheticClass);
+          addSyntheticMarker(representative.getKind(), externalSyntheticClass, context, appView);
+          for (SyntheticClassDefinition member : syntheticGroup.getMembers()) {
+            DexProgramClass memberClass = member.getProgramClass();
+            DexType memberType = memberClass.getType();
+            pruned.add(memberType);
+            if (memberType != syntheticType) {
+              lensBuilder.moveSyntheticClass(memberType, syntheticType);
+            }
+            // The aliasing of the non-representative members needs to be recorded manually.
+            if (member != representative) {
+              deduplicatedClasses.add(memberClass);
+            }
+          }
+        });
+
+    List<DexProgramClass> newProgramClasses = new ArrayList<>(newSyntheticClasses);
+    for (DexProgramClass clazz : application.classes()) {
+      if (!pruned.contains(clazz.type)) {
+        newProgramClasses.add(clazz);
+      }
+    }
+    application = application.builder().replaceProgramClasses(newProgramClasses).build();
+
+    // We can only assert that the method container classes are in here as the classes need
+    // to be rewritten by the tree-fixer.
+    for (DexType key : syntheticMethodGroups.keySet()) {
+      assert application.definitionFor(key) != null;
+    }
+
+    newSyntheticClasses.clear();
+
+    DexApplication.Builder<?> builder = application.builder();
+    TreeFixerBase treeFixer =
+        new TreeFixerBase(appView) {
+          @Override
+          public DexType mapClassType(DexType type) {
+            return lensBuilder.syntheticClassesMap.getOrDefault(type, type);
+          }
+
+          @Override
+          public void recordFieldChange(DexField from, DexField to) {
+            lensBuilder.move(from, to);
+          }
+
+          @Override
+          public void recordMethodChange(DexMethod from, DexMethod to) {
+            lensBuilder.move(from, to);
+          }
+
+          @Override
+          public void recordClassChange(DexType from, DexType to) {
+            lensBuilder.move(from, to);
+          }
+        };
+    treeFixer.fixupClasses(deduplicatedClasses);
+    builder.replaceProgramClasses(treeFixer.fixupClasses(application.classes()));
+    application = builder.build();
+
+    // Add the synthesized from after repackaging which changed class definitions.
+    final DexApplication appForLookup = application;
+    syntheticClassGroups.forEach(
+        (syntheticType, syntheticGroup) -> {
+          DexProgramClass externalSyntheticClass = appForLookup.programDefinitionFor(syntheticType);
+          newSyntheticClasses.add(externalSyntheticClass);
+          for (SyntheticClassDefinition member : syntheticGroup.getMembers()) {
+            addMainDexAndSynthesizedFromForMember(
+                member,
+                externalSyntheticClass,
+                mainDexClasses,
+                derivedMainDexTypes,
+                appForLookup::programDefinitionFor);
+          }
+        });
+    syntheticMethodGroups.forEach(
+        (syntheticType, syntheticGroup) -> {
+          DexProgramClass externalSyntheticClass = appForLookup.programDefinitionFor(syntheticType);
+          newSyntheticClasses.add(externalSyntheticClass);
+          for (SyntheticMethodDefinition member : syntheticGroup.getMembers()) {
+            addMainDexAndSynthesizedFromForMember(
+                member,
+                externalSyntheticClass,
+                mainDexClasses,
+                derivedMainDexTypes,
+                appForLookup::programDefinitionFor);
+          }
+        });
+
+    for (DexType key : syntheticMethodGroups.keySet()) {
+      assert application.definitionFor(key) != null;
+    }
+
+    for (DexType key : syntheticClassGroups.keySet()) {
+      assert application.definitionFor(key) != null;
+    }
+
+    return application;
+  }
+
+  private static void addSyntheticMarker(
+      SyntheticKind kind,
+      DexProgramClass externalSyntheticClass,
+      SynthesizingContext context,
+      AppView<?> appView) {
+    if (shouldAnnotateSynthetics(appView.options())) {
+      SyntheticMarker.addMarkerToClass(
+          externalSyntheticClass, kind, context, appView.dexItemFactory());
+    }
+  }
+
+  private static DexProgramClass createExternalMethodClass(
+      DexType syntheticType, SyntheticMethodDefinition representative, DexItemFactory factory) {
+    SyntheticClassBuilder builder =
+        new SyntheticClassBuilder(syntheticType, representative.getContext(), factory);
+    // TODO(b/158159959): Support grouping multiple methods per synthetic class.
+    builder.addMethod(
+        methodBuilder -> {
+          DexEncodedMethod definition = representative.getMethod().getDefinition();
+          methodBuilder
+              .setName(SyntheticNaming.INTERNAL_SYNTHETIC_METHOD_PREFIX)
+              .setAccessFlags(definition.accessFlags)
+              .setProto(definition.getProto())
+              .setClassFileVersion(
+                  definition.hasClassFileVersion() ? definition.getClassFileVersion() : null)
+              .setCode(m -> definition.getCode());
+        });
+    return builder.build();
+  }
+
+  private static void addMainDexAndSynthesizedFromForMember(
+      SyntheticDefinition<?, ?> member,
+      DexProgramClass externalSyntheticClass,
+      MainDexClasses mainDexClasses,
+      Set<DexType> derivedMainDexTypes,
+      Function<DexType, DexProgramClass> definitions) {
+    member
+        .getContext()
+        .addIfDerivedFromMainDexClass(externalSyntheticClass, mainDexClasses, derivedMainDexTypes);
+    // TODO(b/168584485): Remove this once class-mapping support is removed.
+    DexProgramClass from = definitions.apply(member.getContext().getSynthesizingContextType());
+    if (from != null) {
+      externalSyntheticClass.addSynthesizedFrom(from);
+    }
   }
 
   private static boolean shouldAnnotateSynthetics(InternalOptions options) {
@@ -345,9 +586,12 @@
     return options.intermediate && !options.cfToCfDesugar;
   }
 
-  private static <T extends SyntheticDefinition & Comparable<T>>
+  private <T extends SyntheticDefinition<?, T>>
       Map<DexType, EquivalenceGroup<T>> computeActualEquivalences(
-          Collection<List<T>> potentialEquivalences, boolean intermediate, DexItemFactory factory) {
+          Collection<List<T>> potentialEquivalences,
+          Map<DexType, NumberGenerator> generators,
+          AppView<?> appView,
+          boolean intermediate) {
     Map<DexType, List<EquivalenceGroup<T>>> groupsPerContext = new IdentityHashMap<>();
     potentialEquivalences.forEach(
         members -> {
@@ -368,20 +612,24 @@
     Map<DexType, EquivalenceGroup<T>> equivalences = new IdentityHashMap<>();
     groupsPerContext.forEach(
         (context, groups) -> {
-          groups.sort(EquivalenceGroup::compareTo);
+          // Sort the equivalence groups that go into 'context' including the context type of the
+          // representative which is equal to 'context' here (see assert below).
+          groups.sort(EquivalenceGroup::compareToIncludingContext);
           for (int i = 0; i < groups.size(); i++) {
             EquivalenceGroup<T> group = groups.get(i);
+            assert group.getRepresentative().getContext().getSynthesizingContextType() == context;
             // Two equivalence groups in same context type must be distinct otherwise the assignment
             // of the synthetic name will be non-deterministic between the two.
             assert i == 0 || checkGroupsAreDistinct(groups.get(i - 1), group);
-            DexType representativeType = createExternalType(context, i, factory);
+            SyntheticKind kind = group.members.get(0).getKind();
+            DexType representativeType = createExternalType(kind, context, generators, appView);
             equivalences.put(representativeType, group);
           }
         });
     return equivalences;
   }
 
-  private static <T extends SyntheticDefinition & Comparable<T>> List<List<T>> groupEquivalent(
+  private static <T extends SyntheticDefinition<?, T>> List<List<T>> groupEquivalent(
       List<T> potentialEquivalence, boolean intermediate) {
     List<List<T>> groups = new ArrayList<>();
     // Each other member is in a shared group if it is actually equivalent to the first member.
@@ -403,42 +651,61 @@
     return groups;
   }
 
-  private static <T extends SyntheticDefinition & Comparable<T>> boolean checkGroupsAreDistinct(
+  private static <T extends SyntheticDefinition<?, T>> boolean checkGroupsAreDistinct(
       EquivalenceGroup<T> g1, EquivalenceGroup<T> g2) {
-    assert g1.compareTo(g2) != 0;
+    int order = g1.compareToIncludingContext(g2);
+    assert order != 0;
+    assert order != g2.compareToIncludingContext(g1);
     return true;
   }
 
-  private static <T extends SyntheticDefinition & Comparable<T>> T findDeterministicRepresentative(
+  private static <T extends SyntheticDefinition<?, T>> T findDeterministicRepresentative(
       List<T> members) {
     // Pick a deterministic member as representative.
     T smallest = members.get(0);
     for (int i = 1; i < members.size(); i++) {
       T next = members.get(i);
-      if (next.compareTo(smallest) < 0) {
+      if (next.compareTo(smallest, true) < 0) {
         smallest = next;
       }
     }
     return smallest;
   }
 
-  private static DexType createExternalType(
-      DexType representativeContext, int nextContextId, DexItemFactory factory) {
-    return factory.createType(
-        DescriptorUtils.getDescriptorFromClassBinaryName(
-            representativeContext.getInternalName()
-                + SyntheticItems.EXTERNAL_SYNTHETIC_CLASS_SEPARATOR
-                + nextContextId));
+  private DexType createExternalType(
+      SyntheticKind kind,
+      DexType representativeContext,
+      Map<DexType, NumberGenerator> generators,
+      AppView<?> appView) {
+    NumberGenerator generator =
+        generators.computeIfAbsent(representativeContext, k -> new NumberGenerator());
+    DexType externalType;
+    do {
+      externalType =
+          SyntheticNaming.createExternalType(
+              kind,
+              representativeContext,
+              Integer.toString(generator.next()),
+              appView.dexItemFactory());
+      DexClass clazz = appView.appInfo().definitionForWithoutExistenceAssert(externalType);
+      if (clazz != null && isNotSyntheticType(clazz.type)) {
+        assert options.testing.allowConflictingSyntheticTypes
+            : "Unexpected creation of an existing external synthetic type: " + clazz;
+        externalType = null;
+      }
+    } while (externalType == null);
+    return externalType;
   }
 
-  private static <T extends SyntheticDefinition> Collection<List<T>> computePotentialEquivalences(
-      Map<DexType, T> definitions, boolean intermediate) {
+  private static <T extends SyntheticDefinition<?, T>>
+      Collection<List<T>> computePotentialEquivalences(
+          Map<DexType, T> definitions, boolean intermediate, DexItemFactory factory) {
     if (definitions.isEmpty()) {
       return Collections.emptyList();
     }
-    Set<DexType> allTypes = definitions.keySet();
-    DexType representative = allTypes.iterator().next();
-    RepresentativeMap map = t -> allTypes.contains(t) ? representative : t;
+    // Map all synthetic types to the java 'void' type. This is not an actual valid type, so it
+    // cannot collide with any valid java type providing a good hashing key for the synthetics.
+    RepresentativeMap map = t -> definitions.containsKey(t) ? factory.voidType : t;
     Map<HashCode, List<T>> equivalences = new HashMap<>(definitions.size());
     for (T definition : definitions.values()) {
       HashCode hash = definition.computeHash(map, intermediate);
@@ -447,25 +714,24 @@
     return equivalences.values();
   }
 
-  private Map<DexType, SyntheticMethodDefinition> lookupSyntheticMethodDefinitions(
-      DexApplication finalApp) {
-    Map<DexType, SyntheticMethodDefinition> methods = new IdentityHashMap<>(syntheticItems.size());
-    for (SyntheticReference reference : syntheticItems.values()) {
-      SyntheticDefinition definition = reference.lookupDefinition(finalApp::definitionFor);
-      if (definition == null || !(definition instanceof SyntheticMethodDefinition)) {
+  private <R extends SyntheticReference<R, D>, D extends SyntheticDefinition<R, D>>
+      Map<DexType, D> lookupDefinitions(AppView<?> appView, Collection<R> references) {
+    Map<DexType, D> definitions = new IdentityHashMap<>(references.size());
+    for (R reference : references) {
+      D definition = reference.lookupDefinition(appView::definitionFor);
+      if (definition == null) {
         // We expect pruned definitions to have been removed.
         assert false;
         continue;
       }
-      SyntheticMethodDefinition method = (SyntheticMethodDefinition) definition;
-      if (SyntheticMethodBuilder.isValidSyntheticMethod(method.getMethod().getDefinition())) {
-        methods.put(method.getHolder().getType(), method);
+      if (definition.isValid()) {
+        definitions.put(reference.getHolder(), definition);
       } else {
         // Failing this check indicates that an optimization has modified the synthetic in a
         // disruptive way.
         assert false;
       }
     }
-    return methods;
+    return definitions;
   }
 }
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 bc7d42e..179f990 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
@@ -6,12 +6,8 @@
 import com.android.tools.r8.errors.InternalCompilerError;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.ClassAccessFlags;
-import com.android.tools.r8.graph.DexAnnotation;
-import com.android.tools.r8.graph.DexAnnotationSet;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
@@ -21,11 +17,8 @@
 import com.android.tools.r8.graph.PrunedItems;
 import com.android.tools.r8.ir.conversion.MethodProcessingId;
 import com.android.tools.r8.synthesis.SyntheticFinalization.Result;
+import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ImmutableSet.Builder;
-import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -35,148 +28,112 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Consumer;
 import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.function.Supplier;
+import java.util.stream.Collectors;
 
 public class SyntheticItems implements SyntheticDefinitionsProvider {
 
   static final int INVALID_ID_AFTER_SYNTHETIC_FINALIZATION = -1;
 
-  /**
-   * The internal synthetic class separator is only used for representing synthetic items during
-   * compilation. In particular, this separator must never be used to write synthetic classes to the
-   * final compilation result.
-   */
-  public static final String INTERNAL_SYNTHETIC_CLASS_SEPARATOR = "-$$InternalSynthetic";
-
-  /**
-   * The external synthetic class separator is used when writing classes. It may appear in types
-   * during compilation as the output of a compilation may be the input to another.
-   */
-  public static final String EXTERNAL_SYNTHETIC_CLASS_SEPARATOR = "-$$ExternalSynthetic";
-
-  /** Method prefix when generating synthetic methods in a class. */
-  public static final String INTERNAL_SYNTHETIC_METHOD_PREFIX = "m";
-
-  public static boolean verifyNotInternalSynthetic(DexType type) {
-    assert !type.toDescriptorString().contains(SyntheticItems.INTERNAL_SYNTHETIC_CLASS_SEPARATOR);
-    return true;
-  }
-
   /** Globally incremented id for the next internal synthetic class. */
   private int nextSyntheticId;
 
-  /**
-   * Thread safe collection of synthesized classes that are not yet committed to the application.
-   *
-   * <p>TODO(b/158159959): Remove legacy support.
-   */
-  private final Map<DexType, DexProgramClass> legacyPendingClasses = new ConcurrentHashMap<>();
+  /** Collection of pending items. */
+  private static class PendingSynthetics {
+    /**
+     * Thread safe collection of synthesized classes that are not yet committed to the application.
+     *
+     * <p>TODO(b/158159959): Remove legacy support.
+     */
+    private final Map<DexType, DexProgramClass> legacyClasses = new ConcurrentHashMap<>();
 
-  /**
-   * Immutable set of synthetic types in the application (eg, committed).
-   *
-   * <p>TODO(b/158159959): Remove legacy support.
-   */
-  private final ImmutableSet<DexType> legacySyntheticTypes;
+    /** Thread safe collection of synthetic items not yet committed to the application. */
+    private final ConcurrentHashMap<DexType, SyntheticDefinition<?, ?>> nonLegacyDefinitions =
+        new ConcurrentHashMap<>();
 
-  /** Thread safe collection of synthetic items not yet committed to the application. */
-  private final ConcurrentHashMap<DexType, SyntheticDefinition> pendingDefinitions =
-      new ConcurrentHashMap<>();
+    boolean isEmpty() {
+      return legacyClasses.isEmpty() && nonLegacyDefinitions.isEmpty();
+    }
 
-  /** Mapping from synthetic type to its synthetic description. */
-  private final ImmutableMap<DexType, SyntheticReference> nonLecacySyntheticItems;
+    boolean containsType(DexType type) {
+      return legacyClasses.containsKey(type) || nonLegacyDefinitions.containsKey(type);
+    }
+
+    boolean verifyNotRewritten(NonIdentityGraphLens lens) {
+      assert legacyClasses.keySet().equals(lens.rewriteTypes(legacyClasses.keySet()));
+      assert nonLegacyDefinitions.keySet().equals(lens.rewriteTypes(nonLegacyDefinitions.keySet()));
+      return true;
+    }
+
+    Collection<DexProgramClass> getAllClasses() {
+      List<DexProgramClass> allPending =
+          new ArrayList<>(nonLegacyDefinitions.size() + legacyClasses.size());
+      for (SyntheticDefinition<?, ?> item : nonLegacyDefinitions.values()) {
+        allPending.add(item.getHolder());
+      }
+      allPending.addAll(legacyClasses.values());
+      return Collections.unmodifiableList(allPending);
+    }
+  }
+
+  private final CommittedSyntheticsCollection committed;
+
+  private final PendingSynthetics pending = new PendingSynthetics();
 
   // Only for use from initial AppInfo/AppInfoWithClassHierarchy create functions. */
   public static CommittedItems createInitialSyntheticItems(DexApplication application) {
     return new CommittedItems(
-        0, application, ImmutableSet.of(), ImmutableMap.of(), ImmutableList.of());
+        0, application, CommittedSyntheticsCollection.empty(), ImmutableList.of());
   }
 
   // Only for conversion to a mutable synthetic items collection.
   SyntheticItems(CommittedItems commit) {
-    this(commit.nextSyntheticId, commit.legacySyntheticTypes, commit.syntheticItems);
+    this(commit.nextSyntheticId, commit.committed);
   }
 
-  private SyntheticItems(
-      int nextSyntheticId,
-      ImmutableSet<DexType> legacySyntheticTypes,
-      ImmutableMap<DexType, SyntheticReference> nonLecacySyntheticItems) {
+  private SyntheticItems(int nextSyntheticId, CommittedSyntheticsCollection committed) {
     this.nextSyntheticId = nextSyntheticId;
-    this.legacySyntheticTypes = legacySyntheticTypes;
-    this.nonLecacySyntheticItems = nonLecacySyntheticItems;
-    assert Sets.intersection(nonLecacySyntheticItems.keySet(), legacySyntheticTypes).isEmpty();
+    this.committed = committed;
   }
 
   public static void collectSyntheticInputs(AppView<AppInfo> appView) {
     // Collecting synthetic items must be the very first task after application build.
     SyntheticItems synthetics = appView.getSyntheticItems();
     assert synthetics.nextSyntheticId == 0;
-    assert synthetics.nonLecacySyntheticItems.isEmpty();
-    assert !synthetics.hasPendingSyntheticClasses();
+    assert synthetics.committed.isEmpty();
+    assert synthetics.pending.isEmpty();
     if (appView.options().intermediate) {
       // If the compilation is in intermediate mode the synthetics should just be passed through.
       return;
     }
-    ImmutableMap.Builder<DexType, SyntheticReference> pending = ImmutableMap.builder();
+    CommittedSyntheticsCollection.Builder builder = synthetics.committed.builder();
     // TODO(b/158159959): Consider identifying synthetics in the input reader to speed this up.
     for (DexProgramClass clazz : appView.appInfo().classes()) {
-      DexType annotatedContextType = isSynthesizedMethodsContainer(clazz, appView.dexItemFactory());
-      if (annotatedContextType == null) {
-        continue;
+      SyntheticMarker marker =
+          SyntheticMarker.stripMarkerFromClass(clazz, appView.dexItemFactory());
+      if (marker.isSyntheticMethods()) {
+        clazz.forEachProgramMethod(
+            // TODO(b/158159959): Support having multiple methods per class.
+            method -> {
+              builder.addNonLegacyMethod(
+                  new SyntheticMethodDefinition(marker.getKind(), marker.getContext(), method));
+            });
+      } else if (marker.isSyntheticClass()) {
+        builder.addNonLegacyClass(
+            new SyntheticClassDefinition(marker.getKind(), marker.getContext(), clazz));
       }
-      clazz.setAnnotations(DexAnnotationSet.empty());
-      SynthesizingContext context =
-          SynthesizingContext.fromSyntheticInputClass(clazz, annotatedContextType);
-      clazz.forEachProgramMethod(
-          // TODO(b/158159959): Support having multiple methods per class.
-          method -> {
-            method.getDefinition().setAnnotations(DexAnnotationSet.empty());
-            pending.put(clazz.type, new SyntheticMethodDefinition(context, method).toReference());
-          });
     }
-    pending.putAll(synthetics.nonLecacySyntheticItems);
-    ImmutableMap<DexType, SyntheticReference> nonLegacySyntheticItems = pending.build();
-    if (nonLegacySyntheticItems.isEmpty()) {
+    CommittedSyntheticsCollection committed = builder.build();
+    if (committed.isEmpty()) {
       return;
     }
     CommittedItems commit =
         new CommittedItems(
-            synthetics.nextSyntheticId,
-            appView.appInfo().app(),
-            synthetics.legacySyntheticTypes,
-            nonLegacySyntheticItems,
-            ImmutableList.of());
+            synthetics.nextSyntheticId, appView.appInfo().app(), committed, ImmutableList.of());
     appView.setAppInfo(new AppInfo(commit, appView.appInfo().getMainDexClasses()));
   }
 
-  private static DexType isSynthesizedMethodsContainer(
-      DexProgramClass clazz, DexItemFactory factory) {
-    ClassAccessFlags flags = clazz.accessFlags;
-    if (!flags.isSynthetic() || flags.isAbstract() || flags.isEnum()) {
-      return null;
-    }
-    DexType contextType =
-        DexAnnotation.getSynthesizedClassAnnotationContextType(clazz.annotations(), factory);
-    if (contextType == null) {
-      return null;
-    }
-    if (clazz.superType != factory.objectType) {
-      return null;
-    }
-    if (!clazz.interfaces.isEmpty()) {
-      return null;
-    }
-    if (clazz.annotations().size() != 1) {
-      return null;
-    }
-    for (DexEncodedMethod method : clazz.methods()) {
-      if (!SyntheticMethodBuilder.isValidSyntheticMethod(method)) {
-        return null;
-      }
-    }
-    return contextType;
-  }
-
   // Internal synthetic id creation helpers.
 
   private synchronized String getNextSyntheticId() {
@@ -191,49 +148,52 @@
 
   @Override
   public DexClass definitionFor(DexType type, Function<DexType, DexClass> baseDefinitionFor) {
-    DexProgramClass pending = legacyPendingClasses.get(type);
-    if (pending == null) {
-      SyntheticDefinition item = pendingDefinitions.get(type);
+    DexProgramClass clazz = pending.legacyClasses.get(type);
+    if (clazz == null) {
+      SyntheticDefinition<?, ?> item = pending.nonLegacyDefinitions.get(type);
       if (item != null) {
-        pending = item.getHolder();
+        clazz = item.getHolder();
       }
     }
-    if (pending != null) {
+    if (clazz != null) {
       assert baseDefinitionFor.apply(type) == null
           : "Pending synthetic definition also present in the active program: " + type;
-      return pending;
+      return clazz;
     }
     return baseDefinitionFor.apply(type);
   }
 
+  public boolean verifyNonLegacySyntheticsAreCommitted() {
+    assert pending.nonLegacyDefinitions.isEmpty()
+        : "Uncommitted synthetics: "
+            + pending.nonLegacyDefinitions.keySet().stream()
+                .map(DexType::getName)
+                .collect(Collectors.joining());
+    return true;
+  }
+
   public boolean hasPendingSyntheticClasses() {
-    return !legacyPendingClasses.isEmpty() || !pendingDefinitions.isEmpty();
+    return !pending.isEmpty();
   }
 
   public Collection<DexProgramClass> getPendingSyntheticClasses() {
-    List<DexProgramClass> pending =
-        new ArrayList<>(pendingDefinitions.size() + legacyPendingClasses.size());
-    for (SyntheticDefinition item : pendingDefinitions.values()) {
-      pending.add(item.getHolder());
-    }
-    pending.addAll(legacyPendingClasses.values());
-    return Collections.unmodifiableList(pending);
+    return pending.getAllClasses();
   }
 
   private boolean isCommittedSynthetic(DexType type) {
-    return nonLecacySyntheticItems.containsKey(type) || legacySyntheticTypes.contains(type);
+    return committed.containsType(type);
   }
 
   private boolean isLegacyCommittedSynthetic(DexType type) {
-    return legacySyntheticTypes.contains(type);
+    return committed.containsLegacyType(type);
   }
 
   public boolean isPendingSynthetic(DexType type) {
-    return pendingDefinitions.containsKey(type) || legacyPendingClasses.containsKey(type);
+    return pending.containsType(type);
   }
 
   public boolean isLegacyPendingSynthetic(DexType type) {
-    return legacyPendingClasses.containsKey(type);
+    return pending.legacyClasses.containsKey(type);
   }
 
   public boolean isSyntheticClass(DexType type) {
@@ -247,6 +207,27 @@
     return isSyntheticClass(clazz.type);
   }
 
+  // The compiler should not inspect the kind of a synthetic, so this provided only as a assertion
+  // utility.
+  public boolean verifySyntheticLambdaProperty(
+      DexProgramClass clazz,
+      Predicate<DexProgramClass> ifIsLambda,
+      Predicate<DexProgramClass> ifNotLambda) {
+    SyntheticReference<?, ?> reference = committed.getNonLegacyItem(clazz.getType());
+    if (reference == null) {
+      SyntheticDefinition<?, ?> definition = pending.nonLegacyDefinitions.get(clazz.getType());
+      if (definition != null) {
+        reference = definition.toReference();
+      }
+    }
+    if (reference != null && reference.getKind() == SyntheticKind.LAMBDA) {
+      assert ifIsLambda.test(clazz);
+    } else {
+      assert ifNotLambda.test(clazz);
+    }
+    return true;
+  }
+
   public boolean isLegacySyntheticClass(DexType type) {
     return isLegacyCommittedSynthetic(type) || isLegacyPendingSynthetic(type);
   }
@@ -256,18 +237,21 @@
   }
 
   public Collection<DexProgramClass> getLegacyPendingClasses() {
-    return Collections.unmodifiableCollection(legacyPendingClasses.values());
+    return Collections.unmodifiableCollection(pending.legacyClasses.values());
   }
 
   private SynthesizingContext getSynthesizingContext(ProgramDefinition context) {
-    SyntheticDefinition pendingItemContext = pendingDefinitions.get(context.getContextType());
-    if (pendingItemContext != null) {
-      return pendingItemContext.getContext();
+    DexType contextType = context.getContextType();
+    SyntheticDefinition<?, ?> existingDefinition = pending.nonLegacyDefinitions.get(contextType);
+    if (existingDefinition != null) {
+      return existingDefinition.getContext();
     }
-    SyntheticReference committedItemContext = nonLecacySyntheticItems.get(context.getContextType());
-    return committedItemContext != null
-        ? committedItemContext.getContext()
-        : SynthesizingContext.fromNonSyntheticInputContext(context);
+    SyntheticReference<?, ?> existingReference = committed.getNonLegacyItem(contextType);
+    if (existingReference != null) {
+      return existingReference.getContext();
+    }
+    // This context is not nested in an existing synthetic context so create a new "leaf" context.
+    return SynthesizingContext.fromNonSyntheticInputContext(context);
   }
 
   // Addition and creation of synthetic items.
@@ -276,25 +260,49 @@
   public void addLegacySyntheticClass(DexProgramClass clazz) {
     assert clazz.type.isD8R8SynthesizedClassType();
     assert !isCommittedSynthetic(clazz.type);
-    DexProgramClass previous = legacyPendingClasses.put(clazz.type, clazz);
+    assert !pending.nonLegacyDefinitions.containsKey(clazz.type);
+    DexProgramClass previous = pending.legacyClasses.put(clazz.type, clazz);
     assert previous == null || previous == clazz;
   }
 
+  public DexProgramClass createClass(
+      SyntheticKind kind,
+      DexProgramClass context,
+      DexItemFactory factory,
+      Consumer<SyntheticClassBuilder> fn) {
+    // Obtain the outer synthesizing context in the case the context itself is synthetic.
+    // This is to ensure a flat input-type -> synthetic-item mapping.
+    SynthesizingContext outerContext = getSynthesizingContext(context);
+    DexType type =
+        SyntheticNaming.createInternalType(kind, outerContext, getNextSyntheticId(), factory);
+    SyntheticClassBuilder classBuilder = new SyntheticClassBuilder(type, outerContext, factory);
+    fn.accept(classBuilder);
+    DexProgramClass clazz = classBuilder.build();
+    addPendingDefinition(new SyntheticClassDefinition(kind, outerContext, clazz));
+    return clazz;
+  }
+
   /** Create a single synthetic method item. */
   public ProgramMethod createMethod(
-      ProgramDefinition context, DexItemFactory factory, Consumer<SyntheticMethodBuilder> fn) {
-    return createMethod(context, factory, fn, this::getNextSyntheticId);
+      SyntheticKind kind,
+      ProgramDefinition context,
+      DexItemFactory factory,
+      Consumer<SyntheticMethodBuilder> fn) {
+    return createMethod(kind, context, factory, fn, this::getNextSyntheticId);
   }
 
   public ProgramMethod createMethod(
+      SyntheticKind kind,
       ProgramDefinition context,
       DexItemFactory factory,
       Consumer<SyntheticMethodBuilder> fn,
       MethodProcessingId methodProcessingId) {
-    return createMethod(context, factory, fn, methodProcessingId::getFullyQualifiedIdAndIncrement);
+    return createMethod(
+        kind, context, factory, fn, methodProcessingId::getFullyQualifiedIdAndIncrement);
   }
 
   private ProgramMethod createMethod(
+      SyntheticKind kind,
       ProgramDefinition context,
       DexItemFactory factory,
       Consumer<SyntheticMethodBuilder> fn,
@@ -303,16 +311,20 @@
     // Obtain the outer synthesizing context in the case the context itself is synthetic.
     // This is to ensure a flat input-type -> synthetic-item mapping.
     SynthesizingContext outerContext = getSynthesizingContext(context);
-    DexType type = outerContext.createHygienicType(syntheticIdSupplier.get(), factory);
+    DexType type =
+        SyntheticNaming.createInternalType(kind, outerContext, syntheticIdSupplier.get(), factory);
     SyntheticClassBuilder classBuilder = new SyntheticClassBuilder(type, outerContext, factory);
-    DexProgramClass clazz = classBuilder.addMethod(fn).build();
+    DexProgramClass clazz =
+        classBuilder
+            .addMethod(fn.andThen(m -> m.setName(SyntheticNaming.INTERNAL_SYNTHETIC_METHOD_PREFIX)))
+            .build();
     ProgramMethod method = new ProgramMethod(clazz, clazz.methods().iterator().next());
-    addPendingDefinition(new SyntheticMethodDefinition(outerContext, method));
+    addPendingDefinition(new SyntheticMethodDefinition(kind, outerContext, method));
     return method;
   }
 
-  private void addPendingDefinition(SyntheticDefinition definition) {
-    pendingDefinitions.put(definition.getHolder().getType(), definition);
+  private void addPendingDefinition(SyntheticDefinition<?, ?> definition) {
+    pending.nonLegacyDefinitions.put(definition.getHolder().getType(), definition);
   }
 
   // Commit of the synthetic items to a new fully populated application.
@@ -322,125 +334,48 @@
   }
 
   public CommittedItems commitPrunedItems(PrunedItems prunedItems) {
-    return commit(
-        prunedItems.getPrunedApp(),
-        prunedItems.getNoLongerSyntheticItems(),
-        legacyPendingClasses,
-        legacySyntheticTypes,
-        pendingDefinitions,
-        nonLecacySyntheticItems,
-        nextSyntheticId);
+    return commit(prunedItems, pending, committed, nextSyntheticId);
   }
 
   public CommittedItems commitRewrittenWithLens(
       DexApplication application, NonIdentityGraphLens lens) {
-    // Rewrite the previously committed synthetic types.
-    ImmutableSet<DexType> rewrittenLegacyTypes = lens.rewriteTypes(this.legacySyntheticTypes);
-    ImmutableMap.Builder<DexType, SyntheticReference> rewrittenItems = ImmutableMap.builder();
-    for (SyntheticReference reference : nonLecacySyntheticItems.values()) {
-      SyntheticReference rewritten = reference.rewrite(lens);
-      if (rewritten != null) {
-        rewrittenItems.put(rewritten.getHolder(), rewritten);
-      }
-    }
-    // No pending item should need rewriting.
-    assert legacyPendingClasses.keySet().equals(lens.rewriteTypes(legacyPendingClasses.keySet()));
-    assert pendingDefinitions.keySet().equals(lens.rewriteTypes(pendingDefinitions.keySet()));
+    assert pending.verifyNotRewritten(lens);
     return commit(
-        application,
-        Collections.emptySet(),
-        legacyPendingClasses,
-        rewrittenLegacyTypes,
-        pendingDefinitions,
-        rewrittenItems.build(),
-        nextSyntheticId);
+        PrunedItems.empty(application), pending, committed.rewriteWithLens(lens), nextSyntheticId);
   }
 
   private static CommittedItems commit(
-      DexApplication application,
-      Set<DexType> removedClasses,
-      Map<DexType, DexProgramClass> legacyPendingClasses,
-      ImmutableSet<DexType> legacySyntheticTypes,
-      ConcurrentHashMap<DexType, SyntheticDefinition> pendingDefinitions,
-      ImmutableMap<DexType, SyntheticReference> syntheticItems,
+      PrunedItems prunedItems,
+      PendingSynthetics pending,
+      CommittedSyntheticsCollection committed,
       int nextSyntheticId) {
-    // Legacy synthetics must already have been committed.
-    assert verifyClassesAreInApp(application, legacyPendingClasses.values());
-    // Add the set of legacy definitions to the synthetic types.
-    ImmutableSet<DexType> mergedLegacyTypes = legacySyntheticTypes;
-    if (!legacyPendingClasses.isEmpty() || !removedClasses.isEmpty()) {
-      ImmutableSet.Builder<DexType> legacyBuilder = ImmutableSet.builder();
-      filteredAdd(legacySyntheticTypes, removedClasses, legacyBuilder);
-      filteredAdd(legacyPendingClasses.keySet(), removedClasses, legacyBuilder);
-      mergedLegacyTypes = legacyBuilder.build();
-    }
-    // The set of synthetic items is the union of the previous types plus the pending additions.
-    ImmutableMap<DexType, SyntheticReference> mergedItems;
+    DexApplication application = prunedItems.getPrunedApp();
+    Set<DexType> removedClasses = prunedItems.getNoLongerSyntheticItems();
+    CommittedSyntheticsCollection.Builder builder = committed.builder();
+    // Legacy synthetics must already have been committed to the app.
+    assert verifyClassesAreInApp(application, pending.legacyClasses.values());
+    builder.addLegacyClasses(pending.legacyClasses.values());
+    // Compute the synthetic additions and add them to the application.
     ImmutableList<DexType> additions;
     DexApplication amendedApplication;
-    if (pendingDefinitions.isEmpty()) {
-      mergedItems = filteredCopy(syntheticItems, removedClasses);
+    if (pending.nonLegacyDefinitions.isEmpty()) {
       additions = ImmutableList.of();
       amendedApplication = application;
     } else {
       DexApplication.Builder<?> appBuilder = application.builder();
-      ImmutableMap.Builder<DexType, SyntheticReference> itemsBuilder = ImmutableMap.builder();
       ImmutableList.Builder<DexType> additionsBuilder = ImmutableList.builder();
-      for (SyntheticDefinition definition : pendingDefinitions.values()) {
-        if (removedClasses.contains(definition.getHolder().getType())) {
-          continue;
+      for (SyntheticDefinition<?, ?> definition : pending.nonLegacyDefinitions.values()) {
+        if (!removedClasses.contains(definition.getHolder().getType())) {
+          additionsBuilder.add(definition.getHolder().getType());
+          appBuilder.addProgramClass(definition.getHolder());
+          builder.addItem(definition);
         }
-        SyntheticReference reference = definition.toReference();
-        itemsBuilder.put(reference.getHolder(), reference);
-        additionsBuilder.add(definition.getHolder().getType());
-        appBuilder.addProgramClass(definition.getHolder());
       }
-      filteredAdd(syntheticItems, removedClasses, itemsBuilder);
-      mergedItems = itemsBuilder.build();
       additions = additionsBuilder.build();
       amendedApplication = appBuilder.build();
     }
     return new CommittedItems(
-        nextSyntheticId, amendedApplication, mergedLegacyTypes, mergedItems, additions);
-  }
-
-  private static void filteredAdd(
-      Set<DexType> input, Set<DexType> excludeSet, Builder<DexType> result) {
-    if (excludeSet.isEmpty()) {
-      result.addAll(input);
-    } else {
-      for (DexType type : input) {
-        if (!excludeSet.contains(type)) {
-          result.add(type);
-        }
-      }
-    }
-  }
-
-  private static ImmutableMap<DexType, SyntheticReference> filteredCopy(
-      ImmutableMap<DexType, SyntheticReference> syntheticItems, Set<DexType> removedClasses) {
-    if (removedClasses.isEmpty()) {
-      return syntheticItems;
-    }
-    ImmutableMap.Builder<DexType, SyntheticReference> builder = ImmutableMap.builder();
-    filteredAdd(syntheticItems, removedClasses, builder);
-    return builder.build();
-  }
-
-  private static void filteredAdd(
-      ImmutableMap<DexType, SyntheticReference> syntheticItems,
-      Set<DexType> removedClasses,
-      ImmutableMap.Builder<DexType, SyntheticReference> builder) {
-    if (removedClasses.isEmpty()) {
-      builder.putAll(syntheticItems);
-    } else {
-      syntheticItems.forEach(
-          (t, r) -> {
-            if (!removedClasses.contains(t)) {
-              builder.put(t, r);
-            }
-          });
-    }
+        nextSyntheticId, amendedApplication, builder.build().pruneItems(prunedItems), additions);
   }
 
   private static boolean verifyClassesAreInApp(
@@ -455,8 +390,6 @@
 
   public Result computeFinalSynthetics(AppView<?> appView) {
     assert !hasPendingSyntheticClasses();
-    return new SyntheticFinalization(
-            appView.options(), legacySyntheticTypes, nonLecacySyntheticItems)
-        .computeFinalSynthetics(appView);
+    return new SyntheticFinalization(appView.options(), committed).computeFinalSynthetics(appView);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticMarker.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticMarker.java
new file mode 100644
index 0000000..03b7e36
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticMarker.java
@@ -0,0 +1,96 @@
+// Copyright (c) 2020, 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 com.android.tools.r8.graph.ClassAccessFlags;
+import com.android.tools.r8.graph.DexAnnotation;
+import com.android.tools.r8.graph.DexAnnotationSet;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
+import com.android.tools.r8.utils.Pair;
+
+public class SyntheticMarker {
+
+  public static void addMarkerToClass(
+      DexProgramClass clazz,
+      SyntheticKind kind,
+      SynthesizingContext context,
+      DexItemFactory factory) {
+    clazz.setAnnotations(
+        clazz
+            .annotations()
+            .getWithAddedOrReplaced(
+                DexAnnotation.createAnnotationSynthesizedClass(
+                    kind, context.getSynthesizingContextType(), factory)));
+  }
+
+  public static SyntheticMarker stripMarkerFromClass(
+      DexProgramClass clazz, DexItemFactory factory) {
+    SyntheticMarker marker = internalStripMarkerFromClass(clazz, factory);
+    assert marker != NO_MARKER
+        || DexAnnotation.getSynthesizedClassAnnotationContextType(clazz.annotations(), factory)
+            == null;
+    return marker;
+  }
+
+  private static SyntheticMarker internalStripMarkerFromClass(
+      DexProgramClass clazz, DexItemFactory factory) {
+    ClassAccessFlags flags = clazz.accessFlags;
+    if (clazz.superType != factory.objectType) {
+      return NO_MARKER;
+    }
+    if (!flags.isSynthetic() || flags.isAbstract() || flags.isEnum()) {
+      return NO_MARKER;
+    }
+    Pair<SyntheticKind, DexType> info =
+        DexAnnotation.getSynthesizedClassAnnotationContextType(clazz.annotations(), factory);
+    if (info == null) {
+      return NO_MARKER;
+    }
+    assert clazz.annotations().size() == 1;
+    SyntheticKind kind = info.getFirst();
+    DexType context = info.getSecond();
+    if (kind.isSingleSyntheticMethod) {
+      if (!clazz.interfaces.isEmpty()) {
+        return NO_MARKER;
+      }
+      for (DexEncodedMethod method : clazz.methods()) {
+        if (!SyntheticMethodBuilder.isValidSyntheticMethod(method)) {
+          return NO_MARKER;
+        }
+      }
+    }
+    clazz.setAnnotations(DexAnnotationSet.empty());
+    return new SyntheticMarker(kind, SynthesizingContext.fromSyntheticInputClass(clazz, context));
+  }
+
+  private static final SyntheticMarker NO_MARKER = new SyntheticMarker(null, null);
+
+  private final SyntheticKind kind;
+  private final SynthesizingContext context;
+
+  public SyntheticMarker(SyntheticKind kind, SynthesizingContext context) {
+    this.kind = kind;
+    this.context = context;
+  }
+
+  public boolean isSyntheticMethods() {
+    return kind != null && kind.isSingleSyntheticMethod;
+  }
+
+  public boolean isSyntheticClass() {
+    return kind != null && !kind.isSingleSyntheticMethod;
+  }
+
+  public SyntheticKind getKind() {
+    return kind;
+  }
+
+  public SynthesizingContext getContext() {
+    return context;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodBuilder.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodBuilder.java
index 9fcc7d9..60e3079 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodBuilder.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodBuilder.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ParameterAnnotationsList;
@@ -20,15 +21,25 @@
   }
 
   private final SyntheticClassBuilder parent;
-  private final String name;
+  private DexString name = null;
   private DexProto proto = null;
   private CfVersion classFileVersion;
   private SyntheticCodeGenerator codeGenerator = null;
   private MethodAccessFlags accessFlags = null;
 
-  SyntheticMethodBuilder(SyntheticClassBuilder parent, String name) {
+  SyntheticMethodBuilder(SyntheticClassBuilder parent) {
     this.parent = parent;
+  }
+
+  public SyntheticMethodBuilder setName(String name) {
+    return setName(parent.getFactory().createString(name));
+  }
+
+  public SyntheticMethodBuilder setName(DexString name) {
+    assert name != null;
+    assert this.name == null;
     this.name = name;
+    return this;
   }
 
   public SyntheticMethodBuilder setProto(DexProto proto) {
@@ -52,6 +63,7 @@
   }
 
   DexEncodedMethod build() {
+    assert name != null;
     boolean isCompilerSynthesized = true;
     DexMethod methodSignature = getMethodSignature();
     DexEncodedMethod method =
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodDefinition.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodDefinition.java
index ecc1a81..0ba1127 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodDefinition.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodDefinition.java
@@ -3,27 +3,24 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.synthesis;
 
-import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
 import com.android.tools.r8.utils.structural.RepresentativeMap;
-import com.google.common.hash.HashCode;
 import com.google.common.hash.Hasher;
-import com.google.common.hash.Hashing;
-import java.util.Comparator;
 
 /**
  * Definition of a synthetic method item.
  *
  * <p>This class is internal to the synthetic items collection, thus package-protected.
  */
-class SyntheticMethodDefinition extends SyntheticDefinition
-    implements Comparable<SyntheticMethodDefinition> {
+class SyntheticMethodDefinition
+    extends SyntheticDefinition<SyntheticMethodReference, SyntheticMethodDefinition> {
 
   private final ProgramMethod method;
 
-  SyntheticMethodDefinition(SynthesizingContext context, ProgramMethod method) {
-    super(context);
+  SyntheticMethodDefinition(SyntheticKind kind, SynthesizingContext context, ProgramMethod method) {
+    super(kind, context);
     this.method = method;
   }
 
@@ -32,8 +29,8 @@
   }
 
   @Override
-  SyntheticReference toReference() {
-    return new SyntheticMethodReference(getContext(), method.getReference());
+  SyntheticMethodReference toReference() {
+    return new SyntheticMethodReference(getKind(), getContext(), method.getReference());
   }
 
   @Override
@@ -42,39 +39,18 @@
   }
 
   @Override
-  HashCode computeHash(RepresentativeMap map, boolean intermediate) {
-    Hasher hasher = Hashing.sha256().newHasher();
-    if (intermediate) {
-      // If in intermediate mode, include the context type as sharing is restricted to within a
-      // single context.
-      hasher.putInt(getContext().getSynthesizingContextType().hashCode());
-    }
-    method.getDefinition().hashSyntheticContent(hasher, map);
-    return hasher.hash();
+  void internalComputeHash(Hasher hasher, RepresentativeMap map) {
+    method.getDefinition().hashWithTypeEquivalence(hasher, map);
   }
 
   @Override
-  boolean isEquivalentTo(SyntheticDefinition other, boolean intermediate) {
-    if (!(other instanceof SyntheticMethodDefinition)) {
-      return false;
-    }
-    if (intermediate
-        && getContext().getSynthesizingContextType()
-            != other.getContext().getSynthesizingContextType()) {
-      // If in intermediate mode, only synthetics within the same context should be considered
-      // equal.
-      return false;
-    }
-    SyntheticMethodDefinition o = (SyntheticMethodDefinition) other;
-    return method.getDefinition().isSyntheticContentEqual(o.method.getDefinition());
+  int internalCompareTo(SyntheticMethodDefinition other, RepresentativeMap map) {
+    return method.getDefinition().compareWithTypeEquivalenceTo(other.method.getDefinition(), map);
   }
 
-  // Since methods are sharable they must define an order from which representatives can be found.
   @Override
-  public int compareTo(SyntheticMethodDefinition other) {
-    return Comparator.comparing(SyntheticMethodDefinition::getContext)
-        .thenComparing(m -> m.method.getDefinition(), DexEncodedMethod::syntheticCompareTo)
-        .compare(this, other);
+  public boolean isValid() {
+    return SyntheticMethodBuilder.isValidSyntheticMethod(method.getDefinition());
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodReference.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodReference.java
index 28913d0..cade86e 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodReference.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodReference.java
@@ -5,10 +5,11 @@
 
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GraphLens.NonIdentityGraphLens;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
+import java.util.function.Consumer;
 import java.util.function.Function;
 
 /**
@@ -16,37 +17,35 @@
  *
  * <p>This class is internal to the synthetic items collection, thus package-protected.
  */
-class SyntheticMethodReference extends SyntheticReference {
+class SyntheticMethodReference
+    extends SyntheticReference<SyntheticMethodReference, SyntheticMethodDefinition> {
   final DexMethod method;
 
-  SyntheticMethodReference(SynthesizingContext context, DexMethod method) {
-    super(context);
+  SyntheticMethodReference(SyntheticKind kind, SynthesizingContext context, DexMethod method) {
+    super(kind, context);
     this.method = method;
   }
 
   @Override
-  DexReference getReference() {
-    return method;
-  }
-
-  @Override
   DexType getHolder() {
     return method.holder;
   }
 
   @Override
-  SyntheticDefinition lookupDefinition(Function<DexType, DexClass> definitions) {
+  SyntheticMethodDefinition lookupDefinition(Function<DexType, DexClass> definitions) {
     DexClass clazz = definitions.apply(method.holder);
     if (clazz == null) {
       return null;
     }
     assert clazz.isProgramClass();
     ProgramMethod definition = clazz.asProgramClass().lookupProgramMethod(method);
-    return definition != null ? new SyntheticMethodDefinition(getContext(), definition) : null;
+    return definition != null
+        ? new SyntheticMethodDefinition(getKind(), getContext(), definition)
+        : null;
   }
 
   @Override
-  SyntheticReference rewrite(NonIdentityGraphLens lens) {
+  SyntheticMethodReference rewrite(NonIdentityGraphLens lens) {
     DexMethod rewritten = lens.lookupMethod(method);
     // If the reference has been non-trivially rewritten the compiler has changed it and it can no
     // longer be considered a synthetic. The context may or may not have changed.
@@ -54,12 +53,28 @@
       // If the referenced item is rewritten, it should be moved to another holder as the
       // synthetic holder is no longer part of the synthetic collection.
       assert method.holder != rewritten.holder;
-      assert SyntheticItems.verifyNotInternalSynthetic(rewritten.holder);
+      assert SyntheticNaming.verifyNotInternalSynthetic(rewritten.holder);
       return null;
     }
     SynthesizingContext context = getContext().rewrite(lens);
-    return context == getContext() && rewritten == method
-        ? this
-        : new SyntheticMethodReference(context, rewritten);
+    if (context == getContext() && rewritten == method) {
+      return this;
+    }
+    // Ensure that if a synthetic moves its context moves consistently.
+    if (method != rewritten) {
+      context =
+          SynthesizingContext.fromSyntheticContextChange(
+              rewritten.holder, context, lens.dexItemFactory());
+      if (context == null) {
+        return null;
+      }
+    }
+    return new SyntheticMethodReference(getKind(), context, rewritten);
+  }
+
+  @Override
+  void apply(
+      Consumer<SyntheticMethodReference> onMethod, Consumer<SyntheticClassReference> onClass) {
+    onMethod.accept(this);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
new file mode 100644
index 0000000..644a359
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
@@ -0,0 +1,161 @@
+// Copyright (c) 2020, 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 com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.DescriptorUtils;
+
+public class SyntheticNaming {
+
+  /**
+   * Enumeration of all kinds of synthetic items.
+   *
+   * <p>The synthetic kinds are used to provide hinting about what a synthetic item represents. The
+   * kinds must *not* be used be the compiler and are only meant for "debugging". The compiler and
+   * its test may use the kind information as part of asserting properties of the compiler. The kind
+   * will be put into any non-minified synthetic name and thus the kind "descriptor" must be a
+   * distinct for each kind.
+   */
+  public enum SyntheticKind {
+    // Class synthetics.
+    LAMBDA("Lambda", false),
+    // Method synthetics.
+    BACKPORT("Backport", true),
+    STATIC_INTERFACE_CALL("StaticInterfaceCall", true),
+    TO_STRING_IF_NOT_NULL("ToStringIfNotNull", true),
+    THROW_CCE_IF_NOT_NULL("ThrowCCEIfNotNull", true);
+
+    public final String descriptor;
+    public final boolean isSingleSyntheticMethod;
+
+    SyntheticKind(String descriptor, boolean isSingleSyntheticMethod) {
+      this.descriptor = descriptor;
+      this.isSingleSyntheticMethod = isSingleSyntheticMethod;
+    }
+
+    public static SyntheticKind fromDescriptor(String descriptor) {
+      for (SyntheticKind kind : values()) {
+        if (kind.descriptor.equals(descriptor)) {
+          return kind;
+        }
+      }
+      return null;
+    }
+  }
+
+  /**
+   * The internal synthetic class separator is only used for representing synthetic items during
+   * compilation. In particular, this separator must never be used to write synthetic classes to the
+   * final compilation result.
+   */
+  private static final String INTERNAL_SYNTHETIC_CLASS_SEPARATOR = "-$$InternalSynthetic";
+  /**
+   * The external synthetic class separator is used when writing classes. It may appear in types
+   * during compilation as the output of a compilation may be the input to another.
+   */
+  private static final String EXTERNAL_SYNTHETIC_CLASS_SEPARATOR = "-$$ExternalSynthetic";
+  /** Method prefix when generating synthetic methods in a class. */
+  static final String INTERNAL_SYNTHETIC_METHOD_PREFIX = "m";
+
+  // TODO(b/158159959): Remove usage of name-based identification.
+  public static boolean isSyntheticName(String typeName) {
+    return typeName.contains(INTERNAL_SYNTHETIC_CLASS_SEPARATOR)
+        || typeName.contains(EXTERNAL_SYNTHETIC_CLASS_SEPARATOR);
+  }
+
+  static DexType createInternalType(
+      SyntheticKind kind, SynthesizingContext context, String id, DexItemFactory factory) {
+    return createType(
+        INTERNAL_SYNTHETIC_CLASS_SEPARATOR,
+        kind,
+        context.getSynthesizingContextType(),
+        id,
+        factory);
+  }
+
+  static DexType createExternalType(
+      SyntheticKind kind, DexType context, String id, DexItemFactory factory) {
+    return createType(EXTERNAL_SYNTHETIC_CLASS_SEPARATOR, kind, context, id, factory);
+  }
+
+  private static DexType createType(
+      String separator, SyntheticKind kind, DexType context, String id, DexItemFactory factory) {
+    return factory.createType(createDescriptor(separator, kind, context.getInternalName(), id));
+  }
+
+  private static String createDescriptor(
+      String separator, SyntheticKind kind, String context, String id) {
+    return DescriptorUtils.getDescriptorFromClassBinaryName(
+        context + separator + kind.descriptor + id);
+  }
+
+  public static boolean verifyNotInternalSynthetic(DexType type) {
+    return verifyNotInternalSynthetic(type.toDescriptorString());
+  }
+
+  public static boolean verifyNotInternalSynthetic(ClassReference reference) {
+    return verifyNotInternalSynthetic(reference.getDescriptor());
+  }
+
+  public static boolean verifyNotInternalSynthetic(String typeBinaryNameOrDescriptor) {
+    assert !typeBinaryNameOrDescriptor.contains(INTERNAL_SYNTHETIC_CLASS_SEPARATOR);
+    return true;
+  }
+
+  // Visible via package protection in SyntheticItemsTestUtils.
+
+  enum Phase {
+    INTERNAL,
+    EXTERNAL
+  }
+
+  static String getPhaseSeparator(Phase phase) {
+    assert phase != null;
+    return phase == Phase.INTERNAL
+        ? INTERNAL_SYNTHETIC_CLASS_SEPARATOR
+        : EXTERNAL_SYNTHETIC_CLASS_SEPARATOR;
+  }
+
+  static ClassReference makeSyntheticReferenceForTest(
+      ClassReference context, SyntheticKind kind, String id) {
+    return Reference.classFromDescriptor(
+        createDescriptor(EXTERNAL_SYNTHETIC_CLASS_SEPARATOR, kind, context.getBinaryName(), id));
+  }
+
+  static boolean isSynthetic(ClassReference clazz, Phase phase, SyntheticKind kind) {
+    String typeName = clazz.getTypeName();
+    String separator = getPhaseSeparator(phase);
+    int i = typeName.indexOf(separator);
+    return i >= 0 && checkMatchFrom(kind, typeName, i, separator);
+  }
+
+  private static boolean checkMatchFrom(
+      SyntheticKind kind, String name, int i, String externalSyntheticClassSeparator) {
+    int end = i + externalSyntheticClassSeparator.length() + kind.descriptor.length();
+    if (end >= name.length()) {
+      return false;
+    }
+    String prefix = name.substring(i, end);
+    return prefix.equals(externalSyntheticClassSeparator + kind.descriptor)
+        && isInt(name.substring(end));
+  }
+
+  private static boolean isInt(String str) {
+    if (str.isEmpty()) {
+      return false;
+    }
+    if ('0' == str.charAt(0)) {
+      return str.length() == 1;
+    }
+    for (int i = 0; i < str.length(); i++) {
+      if (!Character.isDigit(str.charAt(i))) {
+        return false;
+      }
+    }
+    return true;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticReference.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticReference.java
index 6618378..ea33564 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticReference.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticReference.java
@@ -4,9 +4,10 @@
 package com.android.tools.r8.synthesis;
 
 import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GraphLens.NonIdentityGraphLens;
+import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
+import java.util.function.Consumer;
 import java.util.function.Function;
 
 /**
@@ -14,16 +15,24 @@
  *
  * <p>This class is internal to the synthetic items collection, thus package-protected.
  */
-abstract class SyntheticReference {
+abstract class SyntheticReference<
+    R extends SyntheticReference<R, D>, D extends SyntheticDefinition<R, D>> {
+
+  private final SyntheticKind kind;
   private final SynthesizingContext context;
 
-  SyntheticReference(SynthesizingContext context) {
+  SyntheticReference(SyntheticKind kind, SynthesizingContext context) {
+    assert kind != null;
+    assert context != null;
+    this.kind = kind;
     this.context = context;
   }
 
-  abstract SyntheticDefinition lookupDefinition(Function<DexType, DexClass> definitions);
+  abstract D lookupDefinition(Function<DexType, DexClass> definitions);
 
-  abstract DexReference getReference();
+  final SyntheticKind getKind() {
+    return kind;
+  }
 
   final SynthesizingContext getContext() {
     return context;
@@ -31,5 +40,8 @@
 
   abstract DexType getHolder();
 
-  abstract SyntheticReference rewrite(NonIdentityGraphLens lens);
+  abstract R rewrite(NonIdentityGraphLens lens);
+
+  abstract void apply(
+      Consumer<SyntheticMethodReference> onMethod, Consumer<SyntheticClassReference> onClass);
 }
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 db914a4..c375074 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -1280,6 +1280,7 @@
 
     public boolean enable = true;
     public boolean enableConstructorMerging = true;
+    // TODO(b/174809311): Update or remove the option and its tests after new lambdas synthetics.
     public boolean enableJavaLambdaMerging = false;
     public boolean enableKotlinLambdaMerging = true;
 
@@ -1473,6 +1474,8 @@
     public boolean allowInvalidCfAccessFlags =
         System.getProperty("com.android.tools.r8.allowInvalidCfAccessFlags") != null;
 
+    public boolean allowConflictingSyntheticTypes = false;
+
     // Flag to allow processing of resources in D8. A data resource consumer still needs to be
     // specified.
     public boolean enableD8ResourcesPassThrough = false;
diff --git a/src/main/java/com/android/tools/r8/utils/ListUtils.java b/src/main/java/com/android/tools/r8/utils/ListUtils.java
index 40ae9ce..fe6feab 100644
--- a/src/main/java/com/android/tools/r8/utils/ListUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ListUtils.java
@@ -40,6 +40,14 @@
     return -1;
   }
 
+  public static <S, T> List<T> map(Iterable<S> list, Function<S, T> fn) {
+    List<T> result = new ArrayList<>();
+    for (S element : list) {
+      result.add(fn.apply(element));
+    }
+    return result;
+  }
+
   public static <S, T> List<T> map(Collection<S> list, Function<S, T> fn) {
     List<T> result = new ArrayList<>(list.size());
     for (S element : list) {
diff --git a/src/main/java/com/android/tools/r8/utils/Pair.java b/src/main/java/com/android/tools/r8/utils/Pair.java
index ac58f56..c2f3489 100644
--- a/src/main/java/com/android/tools/r8/utils/Pair.java
+++ b/src/main/java/com/android/tools/r8/utils/Pair.java
@@ -50,4 +50,9 @@
   public boolean equals(Object obj) {
     throw new Unreachable("Pair does not want to support equality!");
   }
+
+  @Override
+  public String toString() {
+    return "Pair{" + first + ", " + second + '}';
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorBase.java b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorBase.java
index 234732c..c721599 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorBase.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorBase.java
@@ -18,29 +18,39 @@
 /** Base class to share most visiting methods */
 public abstract class CompareToVisitorBase extends CompareToVisitor {
 
+  private static boolean DEBUG = false;
+
+  // Helper to debug insert a breakpoint on order values.
+  public static int debug(int order) {
+    if (DEBUG && order != 0) {
+      return order;
+    }
+    return order;
+  }
+
   @Override
   public final int visitBool(boolean value1, boolean value2) {
-    return Boolean.compare(value1, value2);
+    return debug(Boolean.compare(value1, value2));
   }
 
   @Override
   public final int visitInt(int value1, int value2) {
-    return Integer.compare(value1, value2);
+    return debug(Integer.compare(value1, value2));
   }
 
   @Override
   public int visitLong(long value1, long value2) {
-    return Long.compare(value1, value2);
+    return debug(Long.compare(value1, value2));
   }
 
   @Override
   public int visitFloat(float value1, float value2) {
-    return Float.compare(value1, value2);
+    return debug(Float.compare(value1, value2));
   }
 
   @Override
   public int visitDouble(double value1, double value2) {
-    return Double.compare(value1, value2);
+    return debug(Double.compare(value1, value2));
   }
 
   @Override
@@ -53,12 +63,12 @@
     if (order == 0) {
       order = visitBool(it1.hasNext(), it2.hasNext());
     }
-    return order;
+    return debug(order);
   }
 
   @Override
   public int visitDexString(DexString string1, DexString string2) {
-    return string1.compareTo(string2);
+    return debug(string1.compareTo(string2));
   }
 
   @Override
@@ -74,19 +84,19 @@
         order = visitDexMethod(reference1.asDexMethod(), reference2.asDexMethod());
       }
     }
-    return order;
+    return debug(order);
   }
 
   @Override
   public final <S> int visit(S item1, S item2, Comparator<S> comparator) {
-    return comparator.compare(item1, item2);
+    return debug(comparator.compare(item1, item2));
   }
 
   @Override
   public final <S> int visit(S item1, S item2, StructuralMapping<S> accept) {
     ItemSpecification<S> itemVisitor = new ItemSpecification<>(item1, item2, this);
     accept.apply(itemVisitor);
-    return itemVisitor.order;
+    return debug(itemVisitor.order);
   }
 
   private static class ItemSpecification<T>
@@ -198,7 +208,7 @@
     }
 
     @Override
-    protected <S> ItemSpecification<T> withItemIterator(
+    protected <S> ItemSpecification<T> withCustomItemIterator(
         Function<T, Iterator<S>> getter, CompareToAccept<S> compare, HashingAccept<S> hasher) {
       if (order == 0) {
         order = parent.visitItemIterator(getter.apply(item1), getter.apply(item2), compare);
diff --git a/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithNamingLens.java b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithNamingLens.java
index f581065..7569929 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithNamingLens.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithNamingLens.java
@@ -29,34 +29,35 @@
 
   @Override
   public int visitDexType(DexType type1, DexType type2) {
-    return namingLens
-        .lookupDescriptor(type1)
-        .acceptCompareTo(namingLens.lookupDescriptor(type2), this);
+    return debug(
+        namingLens
+            .lookupDescriptor(type1)
+            .acceptCompareTo(namingLens.lookupDescriptor(type2), this));
   }
 
   @Override
   public int visitDexField(DexField field1, DexField field2) {
     int order = field1.holder.acceptCompareTo(field2.holder, this);
     if (order != 0) {
-      return order;
+      return debug(order);
     }
     order = namingLens.lookupName(field1).acceptCompareTo(namingLens.lookupName(field2), this);
     if (order != 0) {
-      return order;
+      return debug(order);
     }
-    return field1.type.acceptCompareTo(field2.type, this);
+    return debug(field1.type.acceptCompareTo(field2.type, this));
   }
 
   @Override
   public int visitDexMethod(DexMethod method1, DexMethod method2) {
     int order = method1.holder.acceptCompareTo(method2.holder, this);
     if (order != 0) {
-      return order;
+      return debug(order);
     }
     order = namingLens.lookupName(method1).acceptCompareTo(namingLens.lookupName(method2), this);
     if (order != 0) {
-      return order;
+      return debug(order);
     }
-    return method1.proto.acceptCompareTo(method2.proto, this);
+    return debug(method1.proto.acceptCompareTo(method2.proto, this));
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithTypeEquivalence.java b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithTypeEquivalence.java
index 89bd02b..ccb5c9e 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithTypeEquivalence.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithTypeEquivalence.java
@@ -28,6 +28,6 @@
   public int visitDexType(DexType type1, DexType type2) {
     DexType repr1 = representatives.getRepresentative(type1);
     DexType repr2 = representatives.getRepresentative(type2);
-    return repr1.getDescriptor().acceptCompareTo(repr2.getDescriptor(), this);
+    return debug(repr1.getDescriptor().acceptCompareTo(repr2.getDescriptor(), this));
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/structural/HashCodeVisitor.java b/src/main/java/com/android/tools/r8/utils/structural/HashCodeVisitor.java
index 892183d..244d404 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/HashCodeVisitor.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/HashCodeVisitor.java
@@ -105,7 +105,7 @@
   }
 
   @Override
-  protected <S> HashCodeVisitor<T> withItemIterator(
+  protected <S> HashCodeVisitor<T> withCustomItemIterator(
       Function<T, Iterator<S>> getter, CompareToAccept<S> compare, HashingAccept<S> hasher) {
     Iterator<S> it = getter.apply(item);
     while (it.hasNext()) {
diff --git a/src/main/java/com/android/tools/r8/utils/structural/HashingVisitorWithTypeEquivalence.java b/src/main/java/com/android/tools/r8/utils/structural/HashingVisitorWithTypeEquivalence.java
index e63ac6c..6c388fb 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/HashingVisitorWithTypeEquivalence.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/HashingVisitorWithTypeEquivalence.java
@@ -170,7 +170,7 @@
     }
 
     @Override
-    protected <S> ItemSpecification<T> withItemIterator(
+    protected <S> ItemSpecification<T> withCustomItemIterator(
         Function<T, Iterator<S>> getter, CompareToAccept<S> compare, HashingAccept<S> hasher) {
       parent.visitItemIterator(getter.apply(item), hasher);
       return this;
diff --git a/src/main/java/com/android/tools/r8/utils/structural/StructuralSpecification.java b/src/main/java/com/android/tools/r8/utils/structural/StructuralSpecification.java
index a81fc18..69d8796 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/StructuralSpecification.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/StructuralSpecification.java
@@ -51,12 +51,12 @@
       HashingAccept<S> hasher);
 
   /** Base implementation for visiting an enumeration of items. */
-  protected abstract <S> V withItemIterator(
+  protected abstract <S> V withCustomItemIterator(
       Function<T, Iterator<S>> getter, CompareToAccept<S> compare, HashingAccept<S> hasher);
 
   public final <S> V withCustomItemCollection(
       Function<T, Collection<S>> getter, StructuralAcceptor<S> acceptor) {
-    return withItemIterator(getter.andThen(Collection::iterator), acceptor, acceptor);
+    return withCustomItemIterator(getter.andThen(Collection::iterator), acceptor, acceptor);
   }
 
   /**
@@ -79,24 +79,23 @@
         predicate, getter, StructuralItem::acceptCompareTo, StructuralItem::acceptHashing);
   }
 
+  public final <S extends StructuralItem<S>> V withItemIterator(Function<T, Iterator<S>> getter) {
+    return withCustomItemIterator(
+        getter, StructuralItem::acceptCompareTo, StructuralItem::acceptHashing);
+  }
+
   public final <S extends StructuralItem<S>> V withItemCollection(
       Function<T, Collection<S>> getter) {
-    return withItemIterator(
-        getter.andThen(Collection::iterator),
-        StructuralItem::acceptCompareTo,
-        StructuralItem::acceptHashing);
+    return withItemIterator(getter.andThen(Collection::iterator));
   }
 
   public final <S extends StructuralItem<S>> V withItemArray(Function<T, S[]> getter) {
-    return withItemIterator(
-        getter.andThen(a -> Arrays.asList(a).iterator()),
-        StructuralItem::acceptCompareTo,
-        StructuralItem::acceptHashing);
+    return withItemIterator(getter.andThen(a -> Arrays.asList(a).iterator()));
   }
 
   public final <S extends StructuralItem<S>> V withItemArrayAllowingNullMembers(
       Function<T, S[]> getter) {
-    return withItemIterator(
+    return withCustomItemIterator(
         getter.andThen(a -> Arrays.asList(a).iterator()),
         (a, b, visitor) -> {
           if (a == null || b == null) {
diff --git a/src/test/examplesAndroidO/multidex004/ref-list-1.txt b/src/test/examplesAndroidO/multidex004/ref-list-1.txt
index ac6e888..c817c33 100644
--- a/src/test/examplesAndroidO/multidex004/ref-list-1.txt
+++ b/src/test/examplesAndroidO/multidex004/ref-list-1.txt
@@ -1,4 +1,4 @@
-Lmultidex004/-$$Lambda$MainActivity$g120D_43GXrTOaB3kfYt6wSIJh4;
+Lmultidex004/MainActivity-$$ExternalSyntheticLambda0;
 Lmultidex004/MainActivity;
 Lmultidex004/VersionInterface;
 Lmultidex004/VersionStatic;
diff --git a/src/test/java/com/android/tools/r8/D8IncrementalRunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/D8IncrementalRunExamplesAndroidOTest.java
index 726136f..f5348c3 100644
--- a/src/test/java/com/android/tools/r8/D8IncrementalRunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/D8IncrementalRunExamplesAndroidOTest.java
@@ -13,9 +13,10 @@
 import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.ir.desugar.InterfaceMethodRewriter;
-import com.android.tools.r8.ir.desugar.LambdaRewriter;
 import com.android.tools.r8.ir.desugar.TwrCloseResourceRewriter;
-import com.android.tools.r8.synthesis.SyntheticItems;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.DescriptorUtils;
@@ -93,12 +94,13 @@
         for (String descriptor : descriptors) {
           // classes are either lambda classes used by the main class, companion classes of the main
           // interface, the main class/interface, or for JDK9, desugaring of try-with-resources.
+          ClassReference reference = Reference.classFromDescriptor(descriptor);
           Assert.assertTrue(
-              descriptor.contains(LambdaRewriter.LAMBDA_CLASS_NAME_PREFIX)
-                  || descriptor.endsWith(InterfaceMethodRewriter.COMPANION_CLASS_NAME_SUFFIX + ";")
+              descriptor.endsWith(InterfaceMethodRewriter.COMPANION_CLASS_NAME_SUFFIX + ";")
                   || descriptor.endsWith(InterfaceMethodRewriter.DISPATCH_CLASS_NAME_SUFFIX + ";")
                   || descriptor.equals(TwrCloseResourceRewriter.UTILITY_CLASS_DESCRIPTOR)
-                  || descriptor.contains(SyntheticItems.EXTERNAL_SYNTHETIC_CLASS_SEPARATOR)
+                  || SyntheticItemsTestUtils.isExternalLambda(reference)
+                  || SyntheticItemsTestUtils.isExternalStaticInterfaceCall(reference)
                   || descriptor.equals(mainClassDescriptor));
         }
         String classDescriptor =
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
index 7157943..389b0fa 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
@@ -107,14 +107,14 @@
         .withOptionConsumer(opts -> opts.enableClassInlining = false)
         .withBuilderTransformation(
             b -> b.addProguardConfiguration(PROGUARD_OPTIONS, Origin.unknown()))
-        .withDexCheck(inspector -> checkLambdaCount(inspector, 101, "lambdadesugaring"))
+        .withDexCheck(inspector -> checkLambdaCount(inspector, 102, "lambdadesugaring"))
         .run();
 
     test("lambdadesugaring", "lambdadesugaring", "LambdaDesugaring")
         .withMinApiLevel(ToolHelper.getMinApiLevelForDexVmNoHigherThan(AndroidApiLevel.K))
         .withBuilderTransformation(
             b -> b.addProguardConfiguration(PROGUARD_OPTIONS, Origin.unknown()))
-        .withDexCheck(inspector -> checkLambdaCount(inspector, 6, "lambdadesugaring"))
+        .withDexCheck(inspector -> checkLambdaCount(inspector, 7, "lambdadesugaring"))
         .run();
   }
 
@@ -146,14 +146,14 @@
         .withOptionConsumer(opts -> opts.enableClassInlining = false)
         .withBuilderTransformation(
             b -> b.addProguardConfiguration(PROGUARD_OPTIONS, Origin.unknown()))
-        .withDexCheck(inspector -> checkLambdaCount(inspector, 101, "lambdadesugaring"))
+        .withDexCheck(inspector -> checkLambdaCount(inspector, 102, "lambdadesugaring"))
         .run();
 
     test("lambdadesugaring", "lambdadesugaring", "LambdaDesugaring")
         .withMinApiLevel(AndroidApiLevel.N)
         .withBuilderTransformation(
             b -> b.addProguardConfiguration(PROGUARD_OPTIONS, Origin.unknown()))
-        .withDexCheck(inspector -> checkLambdaCount(inspector, 6, "lambdadesugaring"))
+        .withDexCheck(inspector -> checkLambdaCount(inspector, 7, "lambdadesugaring"))
         .run();
   }
 
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/JavaLambdaMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/JavaLambdaMergingTest.java
index adabb3e..63c1f18 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/JavaLambdaMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/JavaLambdaMergingTest.java
@@ -4,16 +4,18 @@
 
 package com.android.tools.r8.classmerging.horizontal;
 
-import static com.android.tools.r8.ir.desugar.LambdaRewriter.LAMBDA_CLASS_NAME_PREFIX;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
 import com.android.tools.r8.utils.codeinspector.VerticallyMergedClassesInspector;
 import java.util.Set;
 import java.util.stream.Collectors;
+import org.junit.Ignore;
 import org.junit.Test;
 
 public class JavaLambdaMergingTest extends HorizontalClassMergingTestBase {
@@ -23,6 +25,7 @@
   }
 
   @Test
+  @Ignore("b/174809311): Test does not work with hygienic lambdas. Rewrite or remove")
   public void test() throws Exception {
     testForR8(parameters.getBackend())
         .addInnerClasses(getClass())
@@ -38,16 +41,12 @@
             inspector -> {
               Set<DexType> lambdaSources =
                   inspector.getSources().stream()
-                      .filter(x -> x.toSourceString().contains(LAMBDA_CLASS_NAME_PREFIX))
+                      .filter(JavaLambdaMergingTest::isLambda)
                       .collect(Collectors.toSet());
               assertEquals(3, lambdaSources.size());
               DexType firstTarget = inspector.getTarget(lambdaSources.iterator().next());
               for (DexType lambdaSource : lambdaSources) {
-                assertTrue(
-                    inspector
-                        .getTarget(lambdaSource)
-                        .toSourceString()
-                        .contains(LAMBDA_CLASS_NAME_PREFIX));
+                assertTrue(isLambda(inspector.getTarget(lambdaSource)));
                 assertEquals(firstTarget, inspector.getTarget(lambdaSource));
               }
             })
@@ -59,6 +58,11 @@
         .assertSuccessWithOutputLines("Hello world!");
   }
 
+  private static boolean isLambda(DexType type) {
+    return SyntheticItemsTestUtils.isExternalLambda(
+        Reference.classFromDescriptor(type.toDescriptorString()));
+  }
+
   public static class Main {
 
     public static void main(String[] args) {
diff --git a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
index 13d59dc..a5a1a8a 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.ClassAccessFlags;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.naming.ClassNamingForNameMapper;
 import com.android.tools.r8.naming.ClassNamingForNameMapper.MappedRange;
@@ -2161,8 +2162,28 @@
       }
 
       private static boolean isInLambdaClass(VmMirror mirror, Location location) {
-        String classSig = mirror.getClassSignature(location.classID);
-        return classSig.contains("$$Lambda$");
+        // TODO(b/174809573): These "lambda" specific methods are highly questionable.
+        //   Determine the exact filtering behavior of intellij (which is very likely not name
+        //   based) and update this filter accordingly.
+        CommandPacket cmd =
+            new CommandPacket(
+                ReferenceTypeCommandSet.CommandSetID, ReferenceTypeCommandSet.ModifiersCommand);
+        cmd.setNextValueAsReferenceTypeID(location.classID);
+        ReplyPacket reply = mirror.performCommand(cmd);
+        mirror.checkReply(reply);
+        int modifiers = reply.getNextValueAsInt();
+        ClassAccessFlags flags = ClassAccessFlags.fromCfAccessFlags(modifiers);
+        if (!flags.isSynthetic()) {
+          return false;
+        }
+        String signature = mirror.getClassSignature(location.classID);
+        if (signature.contains("-CC")) {
+          // TODO(b/174809573): The need to return false here indicates a questionable test
+          //  expectation. Either the test is incorrect or there is a bug in our generation of
+          //  -CC classes marked as synthetic as that would lead to unwanted debugger filtering.
+          return false;
+        }
+        return true;
       }
 
       private static boolean isLambdaMethod(VmMirror mirror, Location location) {
diff --git a/src/test/java/com/android/tools/r8/desugar/DesugarMissingTypeLambdaTest.java b/src/test/java/com/android/tools/r8/desugar/DesugarMissingTypeLambdaTest.java
index 8f304c6..875ead2 100644
--- a/src/test/java/com/android/tools/r8/desugar/DesugarMissingTypeLambdaTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/DesugarMissingTypeLambdaTest.java
@@ -3,8 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.desugar;
 
-import static org.hamcrest.CoreMatchers.containsString;
-import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
@@ -19,6 +17,7 @@
 import com.android.tools.r8.errors.InterfaceDesugarMissingTypeDiagnostic;
 import com.android.tools.r8.position.Position;
 import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.StringUtils;
 import org.junit.Test;
@@ -79,7 +78,7 @@
         assertEquals(
             Reference.classFromClass(MissingInterface.class), desugarWarning.getMissingType());
         // TODO(b/132671303): The context class should not be the synthesized lambda class.
-        assertThat(desugarWarning.getContextType().getDescriptor(), containsString("$$Lambda"));
+        assertTrue(SyntheticItemsTestUtils.isInternalLambda(desugarWarning.getContextType()));
         // TODO(b/132671303): The position info should be the method context.
         assertEquals(Position.UNKNOWN, desugarWarning.getPosition());
       }
diff --git a/src/test/java/com/android/tools/r8/desugar/DesugarToClassFile.java b/src/test/java/com/android/tools/r8/desugar/DesugarToClassFile.java
index c9377fe..dc6e894 100644
--- a/src/test/java/com/android/tools/r8/desugar/DesugarToClassFile.java
+++ b/src/test/java/com/android/tools/r8/desugar/DesugarToClassFile.java
@@ -48,7 +48,7 @@
   private void checkHasLambdaClass(CodeInspector inspector) {
     assertTrue(
         inspector.allClasses().stream()
-            .anyMatch(subject -> subject.getOriginalName().contains("-$$Lambda$")));
+            .anyMatch(subject -> subject.isSynthesizedJavaLambdaClass()));
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/desugar/backports/BackportDuplicationTest.java b/src/test/java/com/android/tools/r8/desugar/backports/BackportDuplicationTest.java
index d094c78..acfc021 100644
--- a/src/test/java/com/android/tools/r8/desugar/backports/BackportDuplicationTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/backports/BackportDuplicationTest.java
@@ -3,9 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.desugar.backports;
 
-import static org.hamcrest.CoreMatchers.containsString;
-import static org.hamcrest.CoreMatchers.not;
-import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
@@ -16,10 +13,10 @@
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.desugar.backports.AbstractBackportTest.MiniAssert;
 import com.android.tools.r8.references.MethodReference;
-import com.android.tools.r8.synthesis.SyntheticItems;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
+import com.android.tools.r8.synthesis.SyntheticNaming;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.StringUtils;
-import com.android.tools.r8.utils.SyntheticItemsTestUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
 import com.google.common.collect.ImmutableList;
@@ -200,9 +197,7 @@
   private void checkNoOriginalsAndNoInternalSynthetics(CodeInspector inspector) {
     inspector.forAllClasses(
         clazz -> {
-          assertThat(
-              clazz.getFinalName(),
-              not(containsString(SyntheticItems.INTERNAL_SYNTHETIC_CLASS_SEPARATOR)));
+          SyntheticNaming.verifyNotInternalSynthetic(clazz.getFinalReference());
           if (!clazz.getOriginalName().equals(MiniAssert.class.getTypeName())) {
             clazz.forAllMethods(
                 method ->
@@ -233,11 +228,11 @@
     // of intermediates.
     Set<MethodReference> expectedSynthetics =
         ImmutableSet.of(
-            SyntheticItemsTestUtils.syntheticMethod(
+            SyntheticItemsTestUtils.syntheticBackportMethod(
                 User1.class, 0, Character.class.getMethod("compare", char.class, char.class)),
-            SyntheticItemsTestUtils.syntheticMethod(
+            SyntheticItemsTestUtils.syntheticBackportMethod(
                 User1.class, 1, Boolean.class.getMethod("compare", boolean.class, boolean.class)),
-            SyntheticItemsTestUtils.syntheticMethod(
+            SyntheticItemsTestUtils.syntheticBackportMethod(
                 User2.class, 0, Integer.class.getMethod("compare", int.class, int.class)));
     assertEquals(expectedSynthetics, getSyntheticMethods(inspector));
   }
diff --git a/src/test/java/com/android/tools/r8/desugar/backports/BackportMainDexTest.java b/src/test/java/com/android/tools/r8/desugar/backports/BackportMainDexTest.java
index 45f8957..05f84af 100644
--- a/src/test/java/com/android/tools/r8/desugar/backports/BackportMainDexTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/backports/BackportMainDexTest.java
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.desugar.backports;
 
-import static com.android.tools.r8.synthesis.SyntheticItems.EXTERNAL_SYNTHETIC_CLASS_SEPARATOR;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
@@ -25,11 +24,11 @@
 import com.android.tools.r8.references.ClassReference;
 import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.StringUtils;
-import com.android.tools.r8.utils.SyntheticItemsTestUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
@@ -57,11 +56,6 @@
   static final List<Class<?>> MAIN_DEX_LIST_CLASSES =
       ImmutableList.of(MiniAssert.class, TestClass.class, User2.class);
 
-  static final String SyntheticUnderUser1 =
-      User1.class.getTypeName() + EXTERNAL_SYNTHETIC_CLASS_SEPARATOR;
-  static final String SyntheticUnderUser2 =
-      User2.class.getTypeName() + EXTERNAL_SYNTHETIC_CLASS_SEPARATOR;
-
   private final TestParameters parameters;
 
   @Parameterized.Parameters(name = "{0}")
@@ -341,16 +335,16 @@
   private ImmutableSet<MethodReference> getNonMainDexExpectedSynthetics()
       throws NoSuchMethodException {
     return ImmutableSet.of(
-        SyntheticItemsTestUtils.syntheticMethod(
+        SyntheticItemsTestUtils.syntheticBackportMethod(
             User1.class, 1, Boolean.class.getMethod("compare", boolean.class, boolean.class)));
   }
 
   private ImmutableSet<MethodReference> getMainDexExpectedSynthetics()
       throws NoSuchMethodException {
     return ImmutableSet.of(
-        SyntheticItemsTestUtils.syntheticMethod(
+        SyntheticItemsTestUtils.syntheticBackportMethod(
             User1.class, 0, Character.class.getMethod("compare", char.class, char.class)),
-        SyntheticItemsTestUtils.syntheticMethod(
+        SyntheticItemsTestUtils.syntheticBackportMethod(
             User2.class, 0, Integer.class.getMethod("compare", int.class, int.class)));
   }
 
diff --git a/src/test/java/com/android/tools/r8/desugar/bridge/LambdaReturnTypeBridgeTest.java b/src/test/java/com/android/tools/r8/desugar/bridge/LambdaReturnTypeBridgeTest.java
index 2dbcc30..fdb1044 100644
--- a/src/test/java/com/android/tools/r8/desugar/bridge/LambdaReturnTypeBridgeTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/bridge/LambdaReturnTypeBridgeTest.java
@@ -62,12 +62,7 @@
             codeInspector -> {
               boolean foundBridge = false;
               for (FoundClassSubject clazz : codeInspector.allClasses()) {
-                if (clazz
-                    .getOriginalName()
-                    .contains(
-                        "-$$Lambda$"
-                            + LambdaWithMultipleImplementingInterfaces.class.getSimpleName()
-                            + "$")) {
+                if (clazz.isSynthesizedJavaLambdaClass()) {
                   // Find bridge method and check whether or not it has a cast.
                   for (FoundMethodSubject bridge : clazz.allMethods(FoundMethodSubject::isBridge)) {
                     foundBridge = true;
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryDeterminismTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryDeterminismTest.java
index f7b5e5d..bfb1885 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryDeterminismTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryDeterminismTest.java
@@ -17,11 +17,16 @@
 import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
 import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Sets.SetView;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.Collections;
 import java.util.IdentityHashMap;
 import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
 import org.junit.Assume;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -53,12 +58,29 @@
   private void assertIdenticalInspectors(Path libDexFile1, Path libDexFile2) throws IOException {
     CodeInspector i1 = new CodeInspector(libDexFile1.resolve("classes.dex"));
     CodeInspector i2 = new CodeInspector(libDexFile2.resolve("classes.dex"));
+    assertIdenticalInspectors(i1, i2);
+  }
+
+  public static void assertIdenticalInspectors(CodeInspector i1, CodeInspector i2) {
     assertEquals(i1.allClasses().size(), i2.allClasses().size());
     Map<DexEncodedMethod, DexEncodedMethod> diffs = new IdentityHashMap<>();
     for (FoundClassSubject clazz1 : i1.allClasses()) {
       ClassSubject clazz = i2.clazz(clazz1.getOriginalName());
       assertTrue(clazz.isPresent());
       FoundClassSubject clazz2 = clazz.asFoundClassSubject();
+      Set<String> methods1 =
+          clazz1.allMethods().stream().map(m -> m.toString()).collect(Collectors.toSet());
+      Set<String> methods2 =
+          clazz2.allMethods().stream().map(m -> m.toString()).collect(Collectors.toSet());
+      SetView<String> union = Sets.union(methods1, methods2);
+      assertEquals(
+          "Inspector 1 contains more methods",
+          Collections.emptySet(),
+          Sets.difference(union, methods1));
+      assertEquals(
+          "Inspector 2 contains more methods",
+          Collections.emptySet(),
+          Sets.difference(union, methods2));
       assertEquals(clazz1.allMethods().size(), clazz2.allMethods().size());
       for (FoundMethodSubject method1 : clazz1.allMethods()) {
         MethodSubject method = clazz2.method(method1.asMethodReference());
@@ -79,7 +101,7 @@
     assertTrue(printDiffs(diffs), diffs.isEmpty());
   }
 
-  private String printDiffs(Map<DexEncodedMethod, DexEncodedMethod> diffs) {
+  private static String printDiffs(Map<DexEncodedMethod, DexEncodedMethod> diffs) {
     StringBuilder sb = new StringBuilder();
     sb.append("The following methods had differences from one dex file to the other (")
         .append(diffs.size())
diff --git a/src/test/java/com/android/tools/r8/desugar/lambdas/DeduplicateLambdasWithDefaultMethodsTest.java b/src/test/java/com/android/tools/r8/desugar/lambdas/DeduplicateLambdasWithDefaultMethodsTest.java
new file mode 100644
index 0000000..b252cf4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/lambdas/DeduplicateLambdasWithDefaultMethodsTest.java
@@ -0,0 +1,107 @@
+// Copyright (c) 2020, 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.lambdas;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
+import com.google.common.collect.ImmutableSet;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class DeduplicateLambdasWithDefaultMethodsTest extends TestBase {
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public DeduplicateLambdasWithDefaultMethodsTest(TestParameters parameters) {
+    parameters.assertNoneRuntime();
+  }
+
+  @Test
+  public void test() throws Exception {
+    assertEquals(
+        ImmutableSet.of(
+            Reference.classFromClass(I.class),
+            Reference.classFromClass(TestClass.class),
+            SyntheticItemsTestUtils.syntheticCompanionClass(I.class),
+            SyntheticItemsTestUtils.syntheticLambdaClass(TestClass.class, 0)),
+        testForD8(Backend.CF)
+            .addInnerClasses(getClass())
+            .setIntermediate(true)
+            .setMinApi(AndroidApiLevel.B)
+            .compile()
+            .inspector()
+            .allClasses()
+            .stream()
+            .map(FoundClassSubject::getFinalReference)
+            .collect(Collectors.toSet()));
+  }
+
+  interface I {
+    void foo();
+
+    // Lots of methods which may cause the ordering of methods on a class to change between builds.
+    default void a() {
+      System.out.print("a");
+    }
+
+    default void b() {
+      System.out.print("b");
+    }
+
+    default void c() {
+      System.out.print("c");
+    }
+
+    default void x() {
+      System.out.print("x");
+    }
+
+    default void y() {
+      System.out.print("y");
+    }
+
+    default void z() {
+      System.out.print("z");
+    }
+  }
+
+  public static class TestClass {
+    private static void foo() {
+      System.out.println("foo");
+    }
+
+    private static void pI(I i) {
+      i.a();
+      i.b();
+      i.c();
+      i.x();
+      i.y();
+      i.z();
+      i.foo();
+    }
+
+    public static void main(String[] args) {
+      // Duplication of the same lambda, each of which should become a shared instance.
+      pI(TestClass::foo);
+      pI(TestClass::foo);
+      pI(TestClass::foo);
+      pI(TestClass::foo);
+      pI(TestClass::foo);
+      pI(TestClass::foo);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaEqualityTest.java b/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaEqualityTest.java
new file mode 100644
index 0000000..90c8009
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaEqualityTest.java
@@ -0,0 +1,140 @@
+// Copyright (c) 2020, 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.lambdas;
+
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.StringUtils;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+/**
+ * These tests document the behavior of lambdas w.r.t identity and equality.
+ *
+ * <p>The D8 and R8 compilers take the stance that a program should not rely on either identity or
+ * equality of any lambda metafactory allocated lambda. Thus the status of these tests differ
+ * between JVM, D8/CF, D8/DEX and R8 runs as the compilers may or may not share classes and
+ * allocations as seen fit.
+ */
+@RunWith(Parameterized.class)
+public class LambdaEqualityTest extends TestBase {
+
+  static final String EXPECTED_JAVAC =
+      StringUtils.lines(
+          "Same method refs",
+          "true",
+          "true",
+          "true",
+          "Different method refs",
+          "false",
+          "false",
+          "false",
+          "Empty lambda",
+          "false",
+          "false",
+          "false");
+
+  static final String EXPECTED_D8 =
+      StringUtils.lines(
+          "Same method refs",
+          "true",
+          "true",
+          "true",
+          "Different method refs",
+          "true", // D8 will share the class for the method references.
+          "false",
+          "false",
+          "Empty lambda",
+          "false",
+          "false",
+          "false");
+
+  static final String EXPECTED_R8 =
+      StringUtils.lines(
+          "Same method refs",
+          "true",
+          "true",
+          "true",
+          "Different method refs",
+          "true", // R8 will share the class for the method references.
+          "false",
+          "false",
+          "Empty lambda",
+          "true", // R8 will eliminate the call to the impl method thus making lambdas equal.
+          "true",
+          "true");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
+  }
+
+  public LambdaEqualityTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForRuntime(parameters)
+        .addInnerClasses(LambdaEqualityTest.class)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED_JAVAC);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    testForD8(parameters.getBackend())
+        .addInnerClasses(LambdaEqualityTest.class)
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED_D8);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(LambdaEqualityTest.class)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(TestClass.class)
+        .addKeepMethodRules(
+            Reference.methodFromMethod(
+                TestClass.class.getDeclaredMethod(
+                    "compare", String.class, MyInterface.class, MyInterface.class)))
+        .run(parameters.getRuntime(), TestClass.class)
+        // The use of invoke dynamics prohibits the optimization and sharing of lambdas in R8.
+        .assertSuccessWithOutput(parameters.isCfRuntime() ? EXPECTED_JAVAC : EXPECTED_R8);
+  }
+
+  interface MyInterface {
+    void foo();
+  }
+
+  static class TestClass {
+
+    public static void compare(String msg, MyInterface i1, MyInterface i2) {
+      System.out.println(msg);
+      System.out.println(i1.getClass() == i2.getClass());
+      System.out.println(i1 == i2);
+      System.out.println(i1.equals(i2));
+    }
+
+    public static void main(String[] args) {
+      MyInterface println = System.out::println;
+      // These lambdas are physically the same and should remain so in all cases.
+      compare("Same method refs", println, println);
+      // These lambdas can be shared as they reference the same actual function.
+      compare("Different method refs", println, System.out::println);
+      // These lambdas cannot be shared (by D8) as javac will generate a lambda$main$X for each.
+      compare("Empty lambda", () -> {}, () -> {});
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaNamingConflictTest.java b/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaNamingConflictTest.java
new file mode 100644
index 0000000..1a6e364
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaNamingConflictTest.java
@@ -0,0 +1,113 @@
+// Copyright (c) 2020, 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.lambdas;
+
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class LambdaNamingConflictTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("boo!");
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters()
+        .withAllRuntimes()
+        .withApiLevel(AndroidApiLevel.B)
+        .enableApiLevelsForCf()
+        .build();
+  }
+
+  // The expected synthetic name is the context of the lambda, TestClass, and the first id.
+  private static final ClassReference CONFLICTING_NAME =
+      SyntheticItemsTestUtils.syntheticLambdaClass(TestClass.class, 0);
+
+  private final TestParameters parameters;
+
+  public LambdaNamingConflictTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .addProgramClasses(I.class)
+        .addProgramClassFileData(getConflictingNameClass())
+        .addProgramClassFileData(getTransformedMainClass())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    testForD8(parameters.getBackend())
+        .addProgramClasses(I.class)
+        .addProgramClassFileData(getConflictingNameClass())
+        .addProgramClassFileData(getTransformedMainClass())
+        .setMinApi(parameters.getApiLevel())
+        .addOptionsModification(o -> o.testing.allowConflictingSyntheticTypes = true)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(I.class)
+        .addProgramClassFileData(getConflictingNameClass())
+        .addProgramClassFileData(getTransformedMainClass())
+        .setMinApi(parameters.getApiLevel())
+        .addOptionsModification(o -> o.testing.allowConflictingSyntheticTypes = true)
+        .addKeepMainRule(TestClass.class)
+        // Ensure that R8 cannot remove or rename the conflicting name.
+        .addKeepClassAndMembersRules(CONFLICTING_NAME.getTypeName())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  private byte[] getTransformedMainClass() throws Exception {
+    return transformer(TestClass.class)
+        .transformMethodInsnInMethod(
+            "main",
+            (opcode, owner, name, descriptor, isInterface, visitor) ->
+                visitor.visitMethodInsn(
+                    opcode, CONFLICTING_NAME.getBinaryName(), name, descriptor, isInterface))
+        .transform();
+  }
+
+  private byte[] getConflictingNameClass() throws Exception {
+    return transformer(WillBeConflictingName.class)
+        .setClassDescriptor(CONFLICTING_NAME.getDescriptor())
+        .transform();
+  }
+
+  interface I {
+    void bar();
+  }
+
+  static class WillBeConflictingName {
+    public static void foo(I i) {
+      i.bar();
+    }
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      WillBeConflictingName.foo(() -> System.out.println("boo!"));
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaStaticInstanceFieldDuplicationTest.java b/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaStaticInstanceFieldDuplicationTest.java
new file mode 100644
index 0000000..ac5b076
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaStaticInstanceFieldDuplicationTest.java
@@ -0,0 +1,281 @@
+// Copyright (c) 2020, 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.lambdas;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Sets.SetView;
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.List;
+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 LambdaStaticInstanceFieldDuplicationTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("User1.1", "User1.2", "User2");
+
+  static final List<Class<?>> CLASSES =
+      ImmutableList.of(TestClass.class, MyConsumer.class, Accept.class, User1.class, User2.class);
+
+  static final List<String> CLASS_TYPE_NAMES =
+      CLASSES.stream().map(Class::getTypeName).collect(Collectors.toList());
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters()
+        .withAllRuntimes()
+        .withApiLevel(AndroidApiLevel.J)
+        .enableApiLevelsForCf()
+        .build();
+  }
+
+  public LambdaStaticInstanceFieldDuplicationTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    // R8 does not support desugaring with class file output so this test is only valid for DEX.
+    assumeTrue(parameters.isDexRuntime());
+    runR8(false);
+    runR8(true);
+  }
+
+  private void runR8(boolean minify) throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(CLASSES)
+        .addKeepMainRule(TestClass.class)
+        // Prevent R8 from eliminating the lambdas by keeping the application of them.
+        .addKeepClassAndMembersRules(Accept.class)
+        .setMinApi(parameters.getApiLevel())
+        .minification(minify)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkNoOriginalsAndNoInternalSynthetics);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    testForD8(parameters.getBackend())
+        .addProgramClasses(CLASSES)
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkNoOriginalsAndNoInternalSynthetics)
+        .inspect(this::checkExpectedSynthetics);
+    ;
+  }
+
+  @Test
+  public void testD8Merging() throws Exception {
+    assumeTrue(
+        "b/147485959: Merging does not happen for CF due to lack of synthetic annotations",
+        parameters.isDexRuntime());
+    boolean intermediate = true;
+    runD8Merging(intermediate);
+  }
+
+  @Test
+  public void testD8MergingNonIntermediate() throws Exception {
+    boolean intermediate = false;
+    runD8Merging(intermediate);
+  }
+
+  private void runD8Merging(boolean intermediate) throws Exception {
+    // Compile part 1 of the input (maybe intermediate)
+    Path out1 =
+        testForD8(parameters.getBackend())
+            .addProgramClasses(User1.class)
+            .addClasspathClasses(CLASSES)
+            .setMinApi(parameters.getApiLevel())
+            .setIntermediate(intermediate)
+            .compile()
+            .writeToZip();
+
+    // Compile part 2 of the input (maybe intermediate)
+    Path out2 =
+        testForD8(parameters.getBackend())
+            .addProgramClasses(User2.class)
+            .addClasspathClasses(CLASSES)
+            .setMinApi(parameters.getApiLevel())
+            .setIntermediate(intermediate)
+            .compile()
+            .writeToZip();
+
+    SetView<MethodReference> syntheticsInParts =
+        Sets.union(
+            getSyntheticMethods(new CodeInspector(out1)),
+            getSyntheticMethods(new CodeInspector(out2)));
+
+    // Merge parts as an intermediate artifact.
+    // This will not merge synthetics regardless of the setting of intermediate.
+    Path out3 = temp.newFolder().toPath().resolve("out3.zip");
+    testForD8(parameters.getBackend())
+        .addProgramClasses(TestClass.class, MyConsumer.class, Accept.class)
+        .addProgramFiles(out1, out2)
+        .setMinApi(parameters.getApiLevel())
+        .setIntermediate(true)
+        .compile()
+        .writeToZip(out3)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkNoOriginalsAndNoInternalSynthetics)
+        .inspect(inspector -> assertEquals(syntheticsInParts, getSyntheticMethods(inspector)));
+
+    // Finally do a non-intermediate merge.
+    testForD8(parameters.getBackend())
+        .addProgramFiles(out3)
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkNoOriginalsAndNoInternalSynthetics)
+        .inspect(
+            inspector -> {
+              if (intermediate) {
+                // If all previous builds where intermediate then synthetics are merged.
+                checkExpectedSynthetics(inspector);
+              } else {
+                // Otherwise merging non-intermediate artifacts, synthetics will not be identified.
+                // Check that they are exactly as in the part inputs.
+                assertEquals(syntheticsInParts, getSyntheticMethods(inspector));
+              }
+            });
+  }
+
+  @Test
+  public void testD8FilePerClassFile() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    runD8FilePerMode(OutputMode.DexFilePerClassFile);
+  }
+
+  @Test
+  public void testD8FilePerClass() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    runD8FilePerMode(OutputMode.DexFilePerClass);
+  }
+
+  public void runD8FilePerMode(OutputMode outputMode) throws Exception {
+    Path perClassOutput =
+        testForD8(parameters.getBackend())
+            .setOutputMode(outputMode)
+            .addProgramClasses(CLASSES)
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .writeToZip();
+    testForD8()
+        .addProgramFiles(perClassOutput)
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkNoOriginalsAndNoInternalSynthetics)
+        .inspect(this::checkExpectedSynthetics);
+  }
+
+  private void checkNoOriginalsAndNoInternalSynthetics(CodeInspector inspector) {
+    inspector.forAllClasses(
+        clazz -> {
+          assertFalse(SyntheticItemsTestUtils.isInternalLambda(clazz.getFinalReference()));
+          clazz.forAllMethods(
+              method ->
+                  assertTrue(
+                      "Unexpected invoke dynamic:\n" + method.getMethod().codeToString(),
+                      method.isAbstract()
+                          || method
+                              .streamInstructions()
+                              .noneMatch(InstructionSubject::isInvokeDynamic)));
+        });
+  }
+
+  private Set<MethodReference> getSyntheticMethods(CodeInspector inspector) {
+    Set<MethodReference> methods = new HashSet<>();
+    inspector.allClasses().stream()
+        .filter(c -> !CLASS_TYPE_NAMES.contains(c.getFinalName()))
+        .forEach(
+            c ->
+                c.allMethods(m -> !m.isInstanceInitializer() && !m.isClassInitializer())
+                    .forEach(m -> methods.add(m.asMethodReference())));
+    return methods;
+  }
+
+  private void checkExpectedSynthetics(CodeInspector inspector) throws Exception {
+    // Hardcoded set of expected synthetics in a "final" build. This set could change if the
+    // compiler makes any changes to the naming, sorting or grouping of synthetics. It is hard-coded
+    // here to check that the compiler generates this deterministically for any single run or merge
+    // of intermediates.
+    Set<MethodReference> expectedSynthetics =
+        ImmutableSet.of(
+            // User1 has two lambdas.
+            SyntheticItemsTestUtils.syntheticLambdaMethod(
+                User1.class, 0, MyConsumer.class.getMethod("accept", Object.class)),
+            SyntheticItemsTestUtils.syntheticLambdaMethod(
+                User1.class, 1, MyConsumer.class.getMethod("accept", Object.class)),
+            // User2 has one lambda.
+            SyntheticItemsTestUtils.syntheticLambdaMethod(
+                User2.class, 0, MyConsumer.class.getMethod("accept", Object.class)));
+    assertEquals(expectedSynthetics, getSyntheticMethods(inspector));
+  }
+
+  interface MyConsumer {
+    void accept(Object o);
+  }
+
+  static class Accept {
+    public static void accept(Object o, MyConsumer consumer) {
+      consumer.accept(o);
+    }
+  }
+
+  static class User1 {
+
+    private static void testSystemPrintln1() {
+      // The lambda will reference a lambda$x method on User1 which is created by javac for each
+      // lambda on the class. Thus there can be no sharing unless R8 inlines the lambda method into
+      // the desugared lambda classes.
+      Accept.accept("1.1", o -> System.out.println("User" + o));
+    }
+
+    private static void testSystemPrintln2() {
+      Accept.accept("1.2", o -> System.out.println("User" + o));
+    }
+  }
+
+  static class User2 {
+
+    private static void testSystemPrintln() {
+      Accept.accept("2", o -> System.out.println("User" + o));
+    }
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      User1.testSystemPrintln1();
+      User1.testSystemPrintln2();
+      User2.testSystemPrintln();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaToSysOutPrintlnDuplicationTest.java b/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaToSysOutPrintlnDuplicationTest.java
new file mode 100644
index 0000000..5b73125
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaToSysOutPrintlnDuplicationTest.java
@@ -0,0 +1,265 @@
+// Copyright (c) 2020, 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.lambdas;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Sets.SetView;
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.List;
+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 LambdaToSysOutPrintlnDuplicationTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("User1", "User2");
+
+  static final List<Class<?>> CLASSES =
+      ImmutableList.of(TestClass.class, MyConsumer.class, Accept.class, User1.class, User2.class);
+
+  static final List<String> CLASS_TYPE_NAMES =
+      CLASSES.stream().map(Class::getTypeName).collect(Collectors.toList());
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters()
+        .withAllRuntimes()
+        .withApiLevel(AndroidApiLevel.J)
+        .enableApiLevelsForCf()
+        .build();
+  }
+
+  public LambdaToSysOutPrintlnDuplicationTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    // R8 does not support desugaring with class file output so this test is only valid for DEX.
+    assumeTrue(parameters.isDexRuntime());
+    runR8(false);
+    runR8(true);
+  }
+
+  private void runR8(boolean minify) throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(CLASSES)
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters.getApiLevel())
+        .minification(minify)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkNoOriginalsAndNoInternalSynthetics);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    testForD8(parameters.getBackend())
+        .addProgramClasses(CLASSES)
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkNoOriginalsAndNoInternalSynthetics)
+        .inspect(this::checkExpectedSynthetics);
+    ;
+  }
+
+  @Test
+  public void testD8Merging() throws Exception {
+    assumeTrue(
+        "b/147485959: Merging does not happen for CF due to lack of synthetic annotations",
+        parameters.isDexRuntime());
+    boolean intermediate = true;
+    runD8Merging(intermediate);
+  }
+
+  @Test
+  public void testD8MergingNonIntermediate() throws Exception {
+    boolean intermediate = false;
+    runD8Merging(intermediate);
+  }
+
+  private void runD8Merging(boolean intermediate) throws Exception {
+    // Compile part 1 of the input (maybe intermediate)
+    Path out1 =
+        testForD8(parameters.getBackend())
+            .addProgramClasses(User1.class)
+            .addClasspathClasses(CLASSES)
+            .setMinApi(parameters.getApiLevel())
+            .setIntermediate(intermediate)
+            .compile()
+            .writeToZip();
+
+    // Compile part 2 of the input (maybe intermediate)
+    Path out2 =
+        testForD8(parameters.getBackend())
+            .addProgramClasses(User2.class)
+            .addClasspathClasses(CLASSES)
+            .setMinApi(parameters.getApiLevel())
+            .setIntermediate(intermediate)
+            .compile()
+            .writeToZip();
+
+    SetView<MethodReference> syntheticsInParts =
+        Sets.union(
+            getSyntheticMethods(new CodeInspector(out1)),
+            getSyntheticMethods(new CodeInspector(out2)));
+
+    // Merge parts as an intermediate artifact.
+    // This will not merge synthetics regardless of the setting of intermediate.
+    Path out3 = temp.newFolder().toPath().resolve("out3.zip");
+    testForD8(parameters.getBackend())
+        .addProgramClasses(TestClass.class, MyConsumer.class, Accept.class)
+        .addProgramFiles(out1, out2)
+        .setMinApi(parameters.getApiLevel())
+        .setIntermediate(true)
+        .compile()
+        .writeToZip(out3)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkNoOriginalsAndNoInternalSynthetics)
+        .inspect(inspector -> assertEquals(syntheticsInParts, getSyntheticMethods(inspector)));
+
+    // Finally do a non-intermediate merge.
+    testForD8(parameters.getBackend())
+        .addProgramFiles(out3)
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkNoOriginalsAndNoInternalSynthetics)
+        .inspect(
+            inspector -> {
+              if (intermediate) {
+                // If all previous builds where intermediate then synthetics are merged.
+                checkExpectedSynthetics(inspector);
+              } else {
+                // Otherwise merging non-intermediate artifacts, synthetics will not be identified.
+                // Check that they are exactly as in the part inputs.
+                assertEquals(syntheticsInParts, getSyntheticMethods(inspector));
+              }
+            });
+  }
+
+  @Test
+  public void testD8FilePerClassFile() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    runD8FilePerMode(OutputMode.DexFilePerClassFile);
+  }
+
+  @Test
+  public void testD8FilePerClass() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    runD8FilePerMode(OutputMode.DexFilePerClass);
+  }
+
+  public void runD8FilePerMode(OutputMode outputMode) throws Exception {
+    Path perClassOutput =
+        testForD8(parameters.getBackend())
+            .setOutputMode(outputMode)
+            .addProgramClasses(CLASSES)
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .writeToZip();
+    testForD8()
+        .addProgramFiles(perClassOutput)
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkNoOriginalsAndNoInternalSynthetics)
+        .inspect(this::checkExpectedSynthetics);
+  }
+
+  private void checkNoOriginalsAndNoInternalSynthetics(CodeInspector inspector) {
+    inspector.forAllClasses(
+        clazz -> {
+          assertFalse(SyntheticItemsTestUtils.isInternalLambda(clazz.getFinalReference()));
+          clazz.forAllMethods(
+              method ->
+                  assertTrue(
+                      "Unexpected invoke dynamic:\n" + method.getMethod().codeToString(),
+                      method.isAbstract()
+                          || method
+                              .streamInstructions()
+                              .noneMatch(InstructionSubject::isInvokeDynamic)));
+        });
+  }
+
+  private Set<MethodReference> getSyntheticMethods(CodeInspector inspector) {
+    Set<MethodReference> methods = new HashSet<>();
+    inspector.allClasses().stream()
+        .filter(c -> !CLASS_TYPE_NAMES.contains(c.getFinalName()))
+        .forEach(
+            c ->
+                c.allMethods(m -> !m.isInstanceInitializer())
+                    .forEach(m -> methods.add(m.asMethodReference())));
+    return methods;
+  }
+
+  private void checkExpectedSynthetics(CodeInspector inspector) throws Exception {
+    // Hardcoded set of expected synthetics in a "final" build. This set could change if the
+    // compiler makes any changes to the naming, sorting or grouping of synthetics. It is hard-coded
+    // here to check that the compiler generates this deterministically for any single run or merge
+    // of intermediates.
+    Set<MethodReference> expectedSynthetics =
+        ImmutableSet.of(
+            SyntheticItemsTestUtils.syntheticLambdaMethod(
+                User1.class, 0, MyConsumer.class.getMethod("accept", Object.class)));
+    assertEquals(expectedSynthetics, getSyntheticMethods(inspector));
+  }
+
+  interface MyConsumer {
+    void accept(Object o);
+  }
+
+  static class Accept {
+    public static void accept(Object o, MyConsumer consumer) {
+      consumer.accept(o);
+    }
+  }
+
+  static class User1 {
+
+    private static void testSystemPrintln() {
+      Accept.accept("User1", System.out::println);
+    }
+  }
+
+  static class User2 {
+
+    private static void testSystemPrintln() {
+      Accept.accept("User2", System.out::println);
+    }
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      User1.testSystemPrintln();
+      User2.testSystemPrintln();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/lambdas/LegacyLambdaMergeTest.java b/src/test/java/com/android/tools/r8/desugar/lambdas/LegacyLambdaMergeTest.java
new file mode 100644
index 0000000..dd00acf
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/lambdas/LegacyLambdaMergeTest.java
@@ -0,0 +1,123 @@
+// Copyright (c) 2020, 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.lambdas;
+
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.D8TestCompileResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.graph.AccessFlags;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.transformers.ClassFileTransformer.MethodInsnTransform;
+import com.android.tools.r8.transformers.ClassFileTransformer.TypeInsnTransform;
+import com.android.tools.r8.utils.StringUtils;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.objectweb.asm.MethodVisitor;
+
+@RunWith(Parameterized.class)
+public class LegacyLambdaMergeTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("Hello, world");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevels().build();
+  }
+
+  public LegacyLambdaMergeTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .addProgramClassFileData(getTransformedMain())
+        // Add the lambda twice (JVM just picks the first).
+        .addProgramClassFileData(getTransformedLambda())
+        .addProgramClassFileData(getTransformedLambda())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    // Merging legacy lambdas is only valid for DEX inputs, thus also not R8 applicable.
+    assumeTrue(parameters.isDexRuntime());
+    D8TestCompileResult lambda =
+        testForD8()
+            .setMinApi(parameters.getApiLevel())
+            .addProgramClassFileData(getTransformedLambda())
+            .compile();
+    testForD8()
+        .setMinApi(parameters.getApiLevel())
+        .addProgramClassFileData(getTransformedMain())
+        // Add the lambda twice.
+        .addProgramFiles(lambda.writeToZip())
+        .addProgramFiles(lambda.writeToZip())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  private ClassReference LAMBDA =
+      Reference.classFromDescriptor(
+          Reference.classFromClass(WillBeLambda.class)
+              .getDescriptor()
+              .replace("WillBeLambda", "-$$Lambda$XYZ"));
+
+  private byte[] getTransformedLambda() throws Exception {
+    return transformer(WillBeLambda.class)
+        .setClassDescriptor(LAMBDA.getDescriptor())
+        .setAccessFlags(AccessFlags::setSynthetic)
+        .transform();
+  }
+
+  private byte[] getTransformedMain() throws Exception {
+    return transformer(TestClass.class)
+        .transformMethodInsnInMethod(
+            "main",
+            new MethodInsnTransform() {
+              @Override
+              public void visitMethodInsn(
+                  int opcode,
+                  String owner,
+                  String name,
+                  String descriptor,
+                  boolean isInterface,
+                  MethodVisitor visitor) {
+                visitor.visitMethodInsn(
+                    opcode, LAMBDA.getBinaryName(), name, descriptor, isInterface);
+              }
+            })
+        .transformTypeInsnInMethod(
+            "main",
+            new TypeInsnTransform() {
+              @Override
+              public void visitTypeInsn(int opcode, String type, MethodVisitor visitor) {
+                visitor.visitTypeInsn(opcode, LAMBDA.getBinaryName());
+              }
+            })
+        .transform();
+  }
+
+  static class WillBeLambda {
+    public void foo() {
+      System.out.println("Hello, world");
+    }
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      new WillBeLambda().foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/lambdas/mergedcontext/MergedContextTest.java b/src/test/java/com/android/tools/r8/desugar/lambdas/mergedcontext/MergedContextTest.java
new file mode 100644
index 0000000..35e31d5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/lambdas/mergedcontext/MergedContextTest.java
@@ -0,0 +1,106 @@
+// Copyright (c) 2021, 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.lambdas.mergedcontext;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.StringUtils;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class MergedContextTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("B::foo", "C::bar");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevels().build();
+  }
+
+  public MergedContextTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(TestClass.class, A.class, B.class, C.class)
+        .addKeepClassAndMembersRules(TestClass.class)
+        .addKeepRules("-repackageclasses \"repackaged\"")
+        .enableInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .addHorizontallyMergedClassesInspector(
+            inspector -> {
+              inspector.assertClassNotMerged(C.class);
+              inspector.assertMergedInto(B.class, A.class);
+            })
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  /* This class will be merged with class B (with A being the result). This class has a package
+   * protected access to ensure that it cannot be repackaged. */
+  @NeverClassInline
+  public static class A {
+
+    @NeverInline
+    public void ensureNotRepackaged() {
+      TestClass.packageProtectedMethodToDisableRepackage();
+    }
+  }
+
+  /* This class is merged into A. */
+  @NeverClassInline
+  public static class B {
+
+    @NeverInline
+    public Runnable foo() {
+      C c = new C();
+      // This synthetic lambda class uses package protected access to C. Its context will initially
+      // be B, thus the synthetic will internally be B-$$Synthetic. The lambda can be repackaged
+      // together with the accessed class C. However, once A and B are merged as A, the context
+      // implicitly changes. If repackaging does not either see or adjust the context, the result
+      // will be that the external synthetic lambda will become A-$$Synthetic,
+      // with the consequence that the call to repackaged.C.protectedMethod() will throw IAE.
+      return () -> {
+        System.out.println("B::foo");
+        c.packageProtectedMethod();
+      };
+    }
+  }
+
+  @NeverClassInline
+  @NoHorizontalClassMerging
+  public static class C {
+
+    @NeverInline
+    void packageProtectedMethod() {
+      System.out.println("C::bar");
+    }
+  }
+
+  static class TestClass {
+
+    static void packageProtectedMethodToDisableRepackage() {
+      if (System.nanoTime() < 0) {
+        throw new RuntimeException();
+      }
+    }
+
+    public static void main(String[] args) {
+      new A().ensureNotRepackaged();
+      new B().foo().run();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugaring/lambdanames/PackageDependentLambdaNamesTest.java b/src/test/java/com/android/tools/r8/desugaring/lambdanames/PackageDependentLambdaNamesTest.java
index 934a15b..5730599 100644
--- a/src/test/java/com/android/tools/r8/desugaring/lambdanames/PackageDependentLambdaNamesTest.java
+++ b/src/test/java/com/android/tools/r8/desugaring/lambdanames/PackageDependentLambdaNamesTest.java
@@ -47,9 +47,9 @@
     if (parameters.isDexRuntime()) {
       result.inspect(
           inspector -> {
-            // When in the same package we expect the two System.out::print lambdas to be shared.
+            // With the hygienic synthetics the reference to System.out::print can always be shared.
             assertEquals(
-                samePackage ? 2 : 3,
+                2,
                 inspector.allClasses().stream()
                     .filter(c -> c.isSynthesizedJavaLambdaClass())
                     .count());
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
index e0266bd..38bb02e 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
@@ -4,7 +4,6 @@
 
 package com.android.tools.r8.ir.optimize.classinliner;
 
-import static com.android.tools.r8.ir.desugar.LambdaRewriter.LAMBDA_CLASS_NAME_PREFIX;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.CoreMatchers.containsString;
@@ -322,8 +321,8 @@
     Set<String> expectedTypes = Sets.newHashSet("java.lang.StringBuilder");
     expectedTypes.addAll(
         inspector.allClasses().stream()
+            .filter(FoundClassSubject::isSynthesizedJavaLambdaClass)
             .map(FoundClassSubject::getFinalName)
-            .filter(name -> name.contains(LAMBDA_CLASS_NAME_PREFIX))
             .collect(Collectors.toList()));
     assertEquals(expectedTypes, collectTypes(clazz.uniqueMethodWithName("testStatefulLambda")));
     assertTrue(
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java
index 7565cf7..1bbcb9e 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java
@@ -6,6 +6,7 @@
 
 import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
 import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
@@ -16,6 +17,7 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.FileUtils;
 import com.google.common.collect.ImmutableList;
@@ -59,8 +61,8 @@
     @Override
     public void finished(DiagnosticsHandler handler) {
       String string = builder.toString();
-      assertTrue(string.contains(testClassMainDexName));
-      assertTrue(string.contains("Lambda"));
+      assertThat(string, containsString(testClassMainDexName));
+      assertThat(string, SyntheticItemsTestUtils.containsExternalSyntheticReference());
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java
index 52b60fe..cf2aeeb 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java
@@ -24,9 +24,9 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ThrowableConsumer;
 import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.ir.desugar.LambdaRewriter;
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.shaking.WhyAreYouKeepingConsumer;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.Box;
 import com.android.tools.r8.utils.DescriptorUtils;
@@ -433,13 +433,13 @@
     for (int i = 0; i < refList.length; i++) {
       String reference = refList[i].trim();
       // The main dex list generator does not do any lambda desugaring.
-      if (!isLambda(reference)) {
+      if (!isExternalSyntheticLambda(reference)) {
         if (mainDexGeneratorMainDexList.size() <= i - nonLambdaOffset) {
           fail("Main dex list generator is missing '" + reference + "'");
         }
         String fromList = mainDexGeneratorMainDexList.get(i - nonLambdaOffset);
         String fromConsumer = mainDexGeneratorMainDexListFromConsumer.get(i - nonLambdaOffset);
-        if (isLambda(fromList)) {
+        if (isExternalSyntheticLambda(fromList)) {
           assertEquals(Backend.DEX, backend);
           assertEquals(fromList, fromConsumer);
           nonLambdaOffset--;
@@ -481,8 +481,8 @@
     assertArrayEquals(entriesUnsorted, entriesSorted);
   }
 
-  private boolean isLambda(String mainDexEntry) {
-    return mainDexEntry.contains(LambdaRewriter.LAMBDA_CLASS_NAME_PREFIX);
+  private boolean isExternalSyntheticLambda(String mainDexEntry) {
+    return SyntheticItemsTestUtils.isExternalLambda(Reference.classFromDescriptor(mainDexEntry));
   }
 
   private String mainDexStringToDescriptor(String mainDexString) {
@@ -492,11 +492,8 @@
   }
 
   private void checkSameMainDexEntry(String reference, String computed) {
-    if (isLambda(reference)) {
-      // For lambda classes we check that there is a lambda class for the right containing
-      // class. However, we do not check the hash for the generated lambda class. The hash
-      // changes for different compiler versions because different compiler versions generate
-      // different lambda implementation method names.
+    if (isExternalSyntheticLambda(reference)) {
+      // For synthetic classes we check that the context classes match.
       reference = reference.substring(0, reference.lastIndexOf('$'));
       computed = computed.substring(0, computed.lastIndexOf('$'));
     }
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexWithSynthesizedClassesTest.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexWithSynthesizedClassesTest.java
index d102004..9fa7fe9 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexWithSynthesizedClassesTest.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexWithSynthesizedClassesTest.java
@@ -11,7 +11,7 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.ir.desugar.LambdaRewriter;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -100,10 +100,11 @@
         inspector.allClasses().stream()
             .anyMatch(
                 clazz ->
-                    clazz.getOriginalName().contains(LambdaRewriter.LAMBDA_CLASS_NAME_PREFIX)
+                    clazz.isSynthesizedJavaLambdaClass()
                         && clazz
-                            .getOriginalName()
-                            .contains("$" + lambdaHolder.getSimpleName() + "$")));
+                            .getOriginalReference()
+                            .equals(
+                                SyntheticItemsTestUtils.syntheticLambdaClass(lambdaHolder, 0))));
   }
 
   static class TestClass {
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/DesugarLambdaRetraceTest.java b/src/test/java/com/android/tools/r8/naming/retrace/DesugarLambdaRetraceTest.java
index 3959d84..6ae3a6f 100644
--- a/src/test/java/com/android/tools/r8/naming/retrace/DesugarLambdaRetraceTest.java
+++ b/src/test/java/com/android/tools/r8/naming/retrace/DesugarLambdaRetraceTest.java
@@ -14,6 +14,8 @@
 import com.android.tools.r8.CompilationMode;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.naming.retrace.StackTrace.StackTraceLine;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.google.common.collect.ImmutableList;
 import java.util.Collection;
@@ -57,7 +59,8 @@
   }
 
   private boolean isSynthesizedLambdaFrame(StackTraceLine line) {
-    return line.className.contains("-$$Lambda$");
+    // TODO(141287349): The mapping should not map the external name to the internal name!
+    return SyntheticItemsTestUtils.isInternalLambda(Reference.classFromTypeName(line.className));
   }
 
   private void checkLambdaFrame(StackTrace retracedStackTrace) {
diff --git a/src/test/java/com/android/tools/r8/shaking/PreserveDesugaredLambdaTest.java b/src/test/java/com/android/tools/r8/shaking/PreserveDesugaredLambdaTest.java
index cd153f2..c2f8041 100644
--- a/src/test/java/com/android/tools/r8/shaking/PreserveDesugaredLambdaTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/PreserveDesugaredLambdaTest.java
@@ -79,8 +79,7 @@
                     codeInspector.allClasses().stream()
                         .anyMatch(
                             c -> {
-                              if (c.getOriginalName()
-                                  .contains("-$$Lambda$PreserveDesugaredLambdaTest$Main")) {
+                              if (c.isSynthesizedJavaLambdaClass()) {
                                 assertThat(c.uniqueMethodWithName("computeTheFoo"), isPresent());
                                 return true;
                               }
diff --git a/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptViaClassInitializerTestRunner.java b/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptViaClassInitializerTestRunner.java
index 4d385a0..436c030 100644
--- a/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptViaClassInitializerTestRunner.java
+++ b/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptViaClassInitializerTestRunner.java
@@ -21,6 +21,7 @@
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.shaking.WhyAreYouKeepingConsumer;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.graphinspector.GraphInspector;
@@ -28,6 +29,7 @@
 import java.io.ByteArrayOutputStream;
 import java.io.PrintStream;
 import java.util.function.Supplier;
+import org.hamcrest.Matcher;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -129,12 +131,13 @@
     consumer.printWhyAreYouKeeping(classFromClass(A.class), new PrintStream(baos));
     assertThat(baos.toString(), containsString(KEPT_REASON_SUFFIX));
 
-    // TODO(b/124499108): Currently synthetic lambda classes are referenced,
+    // TODO(b/124499108): Currently (internal) synthetic lambda classes are referenced,
     //  should be their originating context.
+    Matcher<String> hasLambda = SyntheticItemsTestUtils.containsInternalSyntheticReference();
     if (parameters.isDexRuntime()) {
-      assertThat(baos.toString(), containsString("-$$Lambda$"));
+      assertThat(baos.toString(), hasLambda);
     } else {
-      assertThat(baos.toString(), not(containsString("-$$Lambda$")));
+      assertThat(baos.toString(), not(hasLambda));
     }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java b/src/test/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java
new file mode 100644
index 0000000..dba85d6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java
@@ -0,0 +1,73 @@
+// Copyright (c) 2020, 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 org.hamcrest.CoreMatchers.containsString;
+
+import com.android.tools.r8.ir.desugar.InterfaceMethodRewriter;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.synthesis.SyntheticNaming.Phase;
+import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
+import java.lang.reflect.Method;
+import org.hamcrest.Matcher;
+
+public class SyntheticItemsTestUtils {
+
+  public static ClassReference syntheticCompanionClass(Class<?> clazz) {
+    return Reference.classFromDescriptor(
+        InterfaceMethodRewriter.getCompanionClassDescriptor(
+            Reference.classFromClass(clazz).getDescriptor()));
+  }
+
+  private static ClassReference syntheticClass(Class<?> clazz, SyntheticKind kind, int id) {
+    return SyntheticNaming.makeSyntheticReferenceForTest(
+        Reference.classFromClass(clazz), kind, "" + id);
+  }
+
+  public static MethodReference syntheticBackportMethod(Class<?> clazz, int id, Method method) {
+    ClassReference syntheticHolder =
+        syntheticClass(clazz, SyntheticNaming.SyntheticKind.BACKPORT, id);
+    MethodReference originalMethod = Reference.methodFromMethod(method);
+    return Reference.methodFromDescriptor(
+        syntheticHolder.getDescriptor(),
+        SyntheticNaming.INTERNAL_SYNTHETIC_METHOD_PREFIX,
+        originalMethod.getMethodDescriptor());
+  }
+
+  public static ClassReference syntheticLambdaClass(Class<?> clazz, int id) {
+    return syntheticClass(clazz, SyntheticNaming.SyntheticKind.LAMBDA, id);
+  }
+
+  public static MethodReference syntheticLambdaMethod(Class<?> clazz, int id, Method method) {
+    ClassReference syntheticHolder = syntheticLambdaClass(clazz, id);
+    MethodReference originalMethod = Reference.methodFromMethod(method);
+    return Reference.methodFromDescriptor(
+        syntheticHolder.getDescriptor(),
+        originalMethod.getMethodName(),
+        originalMethod.getMethodDescriptor());
+  }
+
+  public static boolean isInternalLambda(ClassReference reference) {
+    return SyntheticNaming.isSynthetic(reference, Phase.INTERNAL, SyntheticKind.LAMBDA);
+  }
+
+  public static boolean isExternalLambda(ClassReference reference) {
+    return SyntheticNaming.isSynthetic(reference, Phase.EXTERNAL, SyntheticKind.LAMBDA);
+  }
+
+  public static boolean isExternalStaticInterfaceCall(ClassReference reference) {
+    return SyntheticNaming.isSynthetic(
+        reference, Phase.EXTERNAL, SyntheticKind.STATIC_INTERFACE_CALL);
+  }
+
+  public static Matcher<String> containsInternalSyntheticReference() {
+    return containsString(SyntheticNaming.getPhaseSeparator(Phase.INTERNAL));
+  }
+
+  public static Matcher<String> containsExternalSyntheticReference() {
+    return containsString(SyntheticNaming.getPhaseSeparator(Phase.EXTERNAL));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/utils/SyntheticItemsTestUtils.java b/src/test/java/com/android/tools/r8/utils/SyntheticItemsTestUtils.java
deleted file mode 100644
index 9e78bac..0000000
--- a/src/test/java/com/android/tools/r8/utils/SyntheticItemsTestUtils.java
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (c) 2020, 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.utils;
-
-import com.android.tools.r8.references.ClassReference;
-import com.android.tools.r8.references.MethodReference;
-import com.android.tools.r8.references.Reference;
-import com.android.tools.r8.synthesis.SyntheticItems;
-import java.lang.reflect.Method;
-
-public class SyntheticItemsTestUtils {
-
-  public static ClassReference syntheticClass(Class<?> clazz, int id) {
-    return Reference.classFromTypeName(
-        clazz.getTypeName() + SyntheticItems.EXTERNAL_SYNTHETIC_CLASS_SEPARATOR + id);
-  }
-
-  public static MethodReference syntheticMethod(Class<?> clazz, int id, Method method) {
-    ClassReference syntheticHolder = syntheticClass(clazz, id);
-    MethodReference originalMethod = Reference.methodFromMethod(method);
-    return Reference.methodFromDescriptor(
-        syntheticHolder.getDescriptor(),
-        SyntheticItems.INTERNAL_SYNTHETIC_METHOD_PREFIX + 0,
-        originalMethod.getMethodDescriptor());
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentClassSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentClassSubject.java
index ffe8151..a42fa4b 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentClassSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentClassSubject.java
@@ -136,6 +136,11 @@
   }
 
   @Override
+  public ClassReference getOriginalReference() {
+    return null;
+  }
+
+  @Override
   public ClassReference getFinalReference() {
     return null;
   }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java
index 3a9976f..f927e62 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java
@@ -285,6 +285,7 @@
         && ((CfInvoke) instruction).getOpcode() == Opcodes.INVOKESPECIAL;
   }
 
+  @Override
   public boolean isInvokeDynamic() {
     return instruction instanceof CfInvokeDynamic;
   }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/ClassSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/ClassSubject.java
index a2d32eb..20d7466 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/ClassSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/ClassSubject.java
@@ -191,6 +191,8 @@
 
   public abstract String getOriginalBinaryName();
 
+  public abstract ClassReference getOriginalReference();
+
   public abstract ClassReference getFinalReference();
 
   public abstract String getFinalName();
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java
index 4b9f733..2ae5f05 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java
@@ -209,6 +209,11 @@
     return false;
   }
 
+  @Override
+  public boolean isInvokeDynamic() {
+    return isInvokeCustom();
+  }
+
   public boolean isInvokeCustom() {
     return instruction instanceof InvokeCustom || instruction instanceof InvokeCustomRange;
   }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundClassSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundClassSubject.java
index a676666..4220b50 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundClassSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundClassSubject.java
@@ -34,6 +34,7 @@
 import com.android.tools.r8.retrace.RetraceTypeResult;
 import com.android.tools.r8.retrace.RetracedField;
 import com.android.tools.r8.retrace.Retracer;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.ZipUtils;
@@ -389,6 +390,11 @@
   }
 
   @Override
+  public ClassReference getOriginalReference() {
+    return Reference.classFromDescriptor(getOriginalDescriptor());
+  }
+
+  @Override
   public ClassReference getFinalReference() {
     return Reference.classFromDescriptor(getFinalDescriptor());
   }
@@ -430,7 +436,9 @@
 
   @Override
   public boolean isSynthesizedJavaLambdaClass() {
-    return dexClass.type.getName().contains("$Lambda$");
+    // TODO(141287349): Make this precise based on the map input.
+    return SyntheticItemsTestUtils.isExternalLambda(getOriginalReference())
+        || SyntheticItemsTestUtils.isExternalLambda(getFinalReference());
   }
 
   @Override
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java
index 56436e9..8b34260 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java
@@ -43,6 +43,8 @@
 
   boolean isInvokeSpecial();
 
+  boolean isInvokeDynamic();
+
   DexMethod getMethod();
 
   boolean isNop();
diff --git a/tools/toolhelper.py b/tools/toolhelper.py
index 45d84e2..1cfd0f0 100644
--- a/tools/toolhelper.py
+++ b/tools/toolhelper.py
@@ -27,6 +27,10 @@
   cmd.append(jdk.GetJavaExecutable())
   if extra_args:
     cmd.extend(extra_args)
+  agent, args = extract_debug_agent_from_args(args)
+  if agent:
+    cmd.append(
+        '-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005')
   if debug:
     cmd.append('-ea')
   if profile:
@@ -103,3 +107,13 @@
     else:
       args.append(arg)
   return lib, args
+
+def extract_debug_agent_from_args(input_args):
+  agent = False
+  args = []
+  for arg in input_args:
+    if arg in ('--debug-agent', '--debug_agent'):
+      agent = True
+    else:
+      args.append(arg)
+  return agent, args